[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\ngithub: [thalissonvs]\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Report a bug in pydoll\ntitle: \"[Bug]: \"\nlabels: [\"bug\", \"needs-triage\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # pydoll Bug Report\n        \n        Thank you for taking the time to report a bug. This form will guide you through providing the information needed to address the issue effectively.\n  \n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist before reporting\n      description: Please make sure you've completed the following steps before submitting a bug report.\n      options:\n        - label: I have searched for [similar issues](https://github.com/thalissonvs/pydoll/issues) and didn't find a duplicate.\n          required: true\n        - label: I have updated to the latest version of pydoll to verify the issue still exists.\n          required: true\n  \n  - type: input\n    id: version\n    attributes:\n      label: pydoll Version\n      description: What version of pydoll are you using when encountering this bug?\n      placeholder: e.g., 1.3.2\n    validations:\n      required: true\n  \n  - type: input\n    id: python_version\n    attributes:\n      label: Python Version\n      description: What version of Python are you using?\n      placeholder: e.g., 3.10.4\n    validations:\n      required: true\n  \n  - type: dropdown\n    id: os\n    attributes:\n      label: Operating System\n      description: What operating system are you using?\n      options:\n        - Windows\n        - macOS\n        - Linux\n        - Other (specify in environment details)\n    validations:\n      required: true\n  \n  - type: textarea\n    id: description\n    attributes:\n      label: Bug Description\n      description: A clear and concise description of what the bug is.\n      placeholder: When I try to use X feature, the library fails with error message Y...\n    validations:\n      required: true\n  \n  - type: textarea\n    id: reproduction_steps\n    attributes:\n      label: Steps to Reproduce\n      description: Step by step instructions to reproduce the bug.\n      placeholder: |\n        1. Import the library using `import pydoll`\n        2. Set up the client with `...`\n        3. Call method X with parameters Y\n        4. See error\n    validations:\n      required: true\n  \n  - type: textarea\n    id: code_example\n    attributes:\n      label: Code Example\n      description: |\n        A minimal, self-contained code example that demonstrates the issue.\n        This will be automatically formatted into code, so no need for backticks.\n      render: python\n      placeholder: |\n        from pydoll import Client\n        \n        client = Client(...)\n        \n        # Code that triggers the bug\n        result = client.some_method(...)\n        print(result)\n    validations:\n      required: true\n  \n  - type: textarea\n    id: expected_behavior\n    attributes:\n      label: Expected Behavior\n      description: A clear and concise description of what you expected to happen.\n      placeholder: The method should return X or perform Y...\n    validations:\n      required: false\n  \n  - type: textarea\n    id: actual_behavior\n    attributes:\n      label: Actual Behavior\n      description: What actually happened instead? Include full error messages and stack traces if applicable.\n      placeholder: The method raised an exception...\n    validations:\n      required: false\n  \n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant Log Output\n      description: |\n        If applicable, include any logs or error messages. \n        This will be automatically formatted, so no need for backticks.\n      render: shell\n      placeholder: |\n        Traceback (most recent call last):\n          File \"example.py\", line 10, in <module>\n            ...\n          File \".../pydoll/...\", line N, in some_method\n            ...\n        SomeError: Error message\n  \n  - type: textarea\n    id: additional_context\n    attributes:\n      label: Additional Context\n      description: Add any other context about the problem here (environment details, potential causes, solutions you've tried, etc.)\n      placeholder: I've tried reinstalling the package and using a different Python version, but the issue persists... \n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Questions & Discussions\n    url: https://github.com/thalissonvs/pydoll/discussions\n    about: Please ask and answer questions here. \n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation.yml",
    "content": "name: Documentation Issue\ndescription: Report missing, incorrect, or unclear documentation\ntitle: \"[Docs]: \"\nlabels: [\"documentation\", \"needs-triage\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # pydoll Documentation Issue\n        \n        Thank you for helping us improve the documentation. This form will guide you through providing the information needed to address documentation issues effectively.\n  \n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist before reporting\n      description: Please make sure you've completed the following steps before submitting a documentation issue.\n      options:\n        - label: I have searched for [similar documentation issues](https://github.com/thalissonvs/pydoll/issues) and didn't find a duplicate.\n          required: true\n        - label: I have checked the latest documentation to verify this issue still exists.\n          required: true\n  \n  - type: dropdown\n    id: type\n    attributes:\n      label: Type of Documentation Issue\n      description: What type of documentation issue are you reporting?\n      options:\n        - Missing documentation (information does not exist)\n        - Incorrect documentation (information is wrong)\n        - Unclear documentation (information is confusing or ambiguous)\n        - Outdated documentation (information is no longer valid)\n        - Other (please specify in description)\n    validations:\n      required: true\n  \n  - type: input\n    id: location\n    attributes:\n      label: Documentation Location\n      description: Where is the documentation with issues located? Provide URLs, file paths, or section names.\n      placeholder: e.g., https://docs.example.com/pydoll/api.html#section, README.md, API Reference for Client class\n    validations:\n      required: true\n  \n  - type: textarea\n    id: description\n    attributes:\n      label: Issue Description\n      description: Describe the issue with the documentation in detail.\n      placeholder: |\n        The documentation for the `Client.connect()` method doesn't mention the timeout parameter, \n        which I discovered by looking at the source code.\n    validations:\n      required: true\n  \n  - type: textarea\n    id: suggested_fix\n    attributes:\n      label: Suggested Fix\n      description: If you have a suggestion for how to fix the documentation, please provide it here.\n      placeholder: |\n        Add the following to the `Client.connect()` documentation:\n        \n        ```\n        Parameters:\n          timeout (float, optional): Connection timeout in seconds. Defaults to 30.\n        ```\n  \n  - type: textarea\n    id: additional_info\n    attributes:\n      label: Additional Information\n      description: Any additional context or information that might help address this documentation issue.\n      placeholder: |\n        I found this issue when trying to implement a connection with a shorter timeout for my specific use case.\n  \n  - type: dropdown\n    id: contribution\n    attributes:\n      label: Contribution\n      description: Would you be willing to contribute a fix for this documentation?\n      options:\n        - Yes, I'd be willing to submit a PR with the fix\n        - No, I don't have the capacity to fix this\n    validations:\n      required: true "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Suggest a new feature or enhancement for pydoll\ntitle: \"[Feature Request]: \"\nlabels: [\"enhancement\", \"needs-triage\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # pydoll Feature Request\n        \n        Thank you for taking the time to suggest a new feature. This form will guide you through providing the information needed to consider your suggestion effectively.\n  \n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist before requesting\n      description: Please make sure you've completed the following steps before submitting a feature request.\n      options:\n        - label: I have searched for [similar feature requests](https://github.com/thalissonvs/pydoll/issues) and didn't find a duplicate.\n          required: true\n        - label: I have checked the documentation to confirm this feature doesn't already exist.\n          required: true\n  \n  - type: textarea\n    id: problem\n    attributes:\n      label: Problem Statement\n      description: Is your feature request related to a problem? Please describe what you're trying to accomplish.\n      placeholder: I'm trying to accomplish X, but I'm unable to because Y...\n    validations:\n      required: true\n  \n  - type: textarea\n    id: solution\n    attributes:\n      label: Proposed Solution\n      description: Describe the solution you'd like to see implemented. Be as specific as possible.\n      placeholder: |\n        I would like to see a new method/class that can...\n        \n        Example usage might look like:\n        ```python\n        client.new_feature(param1, param2)\n        ```\n    validations:\n      required: true\n  \n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Alternatives Considered\n      description: Describe any alternative solutions or features you've considered.\n      placeholder: I've tried accomplishing this using X and Y approaches, but they don't work well because...\n  \n  - type: textarea\n    id: context\n    attributes:\n      label: Additional Context\n      description: Add any other context, code examples, or references that might help explain your feature request.\n      placeholder: |\n        Other libraries like X and Y have similar features that work like...\n        \n        This would help users who need to...\n  \n  - type: dropdown\n    id: importance\n    attributes:\n      label: Importance\n      description: How important is this feature to your use case?\n      options:\n        - Nice to have\n        - Important\n        - Critical (blocking my usage)\n    validations:\n      required: true\n  \n  - type: dropdown\n    id: contribution\n    attributes:\n      label: Contribution\n      description: Would you be willing to contribute this feature yourself?\n      options:\n        - Yes, I'd be willing to implement this feature\n        - I could help with parts of the implementation\n        - No, I don't have the capacity to implement this "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/refactoring.yml",
    "content": "name: Refactoring Request\ndescription: Suggest code refactoring to improve pydoll's quality, performance, or maintainability\ntitle: \"[Refactor]: \"\nlabels: [\"refactor\", \"needs-triage\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # pydoll Refactoring Request\n        \n        Thank you for suggesting improvements to our codebase. This form will guide you through providing the information needed to consider your refactoring suggestion effectively.\n  \n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist before suggesting refactoring\n      description: Please make sure you've completed the following steps before submitting a refactoring request.\n      options:\n        - label: I have searched for [similar refactoring requests](https://github.com/thalissonvs/pydoll/issues) and didn't find a duplicate.\n          required: true\n        - label: I have reviewed the current implementation to ensure my understanding is accurate.\n          required: true\n  \n  - type: textarea\n    id: current_implementation\n    attributes:\n      label: Current Implementation\n      description: Describe the current implementation and its limitations. Include file paths if known.\n      placeholder: |\n        The current implementation in `pydoll/module/file.py` has the following issues:\n        1. It uses an inefficient algorithm for...\n        2. The code structure makes it difficult to maintain because...\n    validations:\n      required: true\n  \n  - type: textarea\n    id: proposed_changes\n    attributes:\n      label: Proposed Changes\n      description: Describe the changes you're suggesting. Be as specific as possible.\n      placeholder: |\n        I suggest refactoring this code to:\n        1. Replace the current algorithm with X, which would improve performance by...\n        2. Restructure the class hierarchy to better separate concerns by...\n        \n        Example code sketch (if applicable):\n        ```python\n        def improved_method():\n            # better implementation\n        ```\n    validations:\n      required: true\n  \n  - type: textarea\n    id: benefits\n    attributes:\n      label: Benefits\n      description: Explain the benefits of this refactoring.\n      placeholder: |\n        This refactoring would:\n        - Improve performance by X%\n        - Make the code more maintainable by...\n        - Reduce code complexity by...\n        - Fix potential bugs such as...\n    validations:\n      required: true\n  \n  - type: dropdown\n    id: impact\n    attributes:\n      label: API Impact\n      description: Would this refactoring change the public API?\n      options:\n        - No API changes (internal refactoring only)\n        - Minor API changes (backward compatible)\n        - Breaking API changes\n    validations:\n      required: true\n  \n  - type: textarea\n    id: testing_approach\n    attributes:\n      label: Testing Approach\n      description: How can we verify that the refactoring doesn't break existing functionality?\n      placeholder: |\n        The refactoring can be tested by:\n        - Running the existing test suite\n        - Adding new tests for edge cases such as...\n        - Benchmarking performance before and after\n  \n  - type: dropdown\n    id: contribution\n    attributes:\n      label: Contribution\n      description: Would you be willing to contribute this refactoring yourself?\n      options:\n        - Yes, I'd be willing to implement this refactoring\n        - I could help with parts of the implementation\n        - No, I don't have the capacity to implement this "
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/bug_fix.md",
    "content": "# Bug Fix Pull Request\n\n## Related Issue(s)\n<!-- Link the bug report that's being fixed by this PR. Use the format: \"Fixes #123\" or \"Resolves #123\" -->\n\n## Bug Description\n<!-- Briefly describe the bug that's being fixed -->\n\n## Root Cause\n<!-- Explain the root cause of the bug -->\n\n## Solution\n<!-- Describe your solution to fix the bug -->\n\n## Verification Steps\n<!-- List the steps to verify this fix works -->\n1. \n2. \n3. \n\n## Code Example\n<!-- If applicable, provide a code example demonstrating the fix -->\n```python\n# Example code showing the fix\n```\n\n## Before / After\n<!-- If applicable, provide before/after screenshots or code snippets -->\n\n## Testing\n<!-- Describe the tests you added or modified to verify your fix -->\n\n## Testing Checklist\n- [ ] Added regression test that would have caught this bug\n- [ ] Modified existing tests to account for this fix\n- [ ] All tests pass\n- [ ] Edge cases have been tested\n\n## Impact\n<!-- Describe any potential impact this fix might have on existing functionality -->\n- [ ] Low (isolated fix with no side effects)\n- [ ] Medium (might affect closely related functionality)\n- [ ] High (affects multiple areas or changes core behavior)\n\n## Backwards Compatibility\n- [ ] This change is fully backward compatible\n- [ ] This change introduces backward incompatibilities (explain below)\n\n## Checklist before requesting a review\n- [ ] My code follows the style guidelines of this project\n- [ ] I have performed a self-review of my code\n- [ ] I have added test cases that prove my fix is effective\n- [ ] I have run `poetry run task lint` and fixed any issues\n- [ ] I have run `poetry run task test` and all tests pass\n- [ ] My commits follow the [conventional commits](https://www.conventionalcommits.org/) style with message explaining the fix "
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/refactoring.md",
    "content": "# Refactoring Pull Request\n\n## Refactoring Scope\n<!-- Describe which part of the codebase is being refactored -->\n\n## Related Issue(s)\n<!-- Link the refactoring issue that's being addressed by this PR. Use the format: \"Fixes #123\" or \"Resolves #123\" -->\n\n## Description\n<!-- Provide a clear and detailed description of the refactoring changes -->\n\n## Motivation\n<!-- Explain why this refactoring is necessary -->\n\n## Before / After\n<!-- If applicable, provide code examples showing the before and after of the refactoring -->\n\n### Before\n```python\n# Original code\n```\n\n### After\n```python\n# Refactored code\n```\n\n## Performance Impact\n<!-- If applicable, describe any performance improvements or potential impacts -->\n- [ ] Performance improved\n- [ ] Performance potentially decreased\n- [ ] No significant performance change\n- [ ] Performance impact unknown\n\n## Technical Debt\n<!-- Describe how this refactoring addresses technical debt -->\n\n## API Changes\n- [ ] No changes to public API\n- [ ] Public API changed, but backward compatible\n- [ ] Breaking changes to public API\n\n## Testing Strategy\n<!-- Describe how you've tested the refactoring -->\n\n## Testing Checklist\n- [ ] Existing tests updated\n- [ ] New tests added for previously uncovered cases\n- [ ] All tests pass\n- [ ] Code coverage maintained or improved\n\n## Risks and Mitigations\n<!-- Describe any potential risks introduced by this refactoring and how they were mitigated -->\n\n## Checklist before requesting a review\n- [ ] My code follows the style guidelines of this project\n- [ ] I have performed a thorough self-review of the refactored code\n- [ ] I have commented my code, particularly in complex areas\n- [ ] I have updated documentation if needed\n- [ ] I have run `poetry run task lint` and fixed any issues\n- [ ] I have run `poetry run task test` and all tests pass\n- [ ] My commits follow the [conventional commits](https://www.conventionalcommits.org/) style "
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/release.md",
    "content": "# Release Pull Request\n\n## Version\n<!-- Specify the new version number (e.g., 1.4.0) -->\n\n## Release Date\n<!-- Proposed date for this release -->\n\n## Release Type\n- [ ] Major (breaking changes)\n- [ ] Minor (new features, non-breaking)\n- [ ] Patch (bug fixes, non-breaking)\n\n## Change Summary\n<!-- Provide a high-level summary of the changes in this release -->\n\n## Key Changes\n<!-- List the major changes/features included in this release -->\n\n## Breaking Changes\n<!-- If applicable, list all breaking changes and migration instructions -->\n\n## Dependencies\n<!-- List any new or updated dependencies -->\n\n## Deprecations \n- While `get_element_text()` is still supported, it is **recommended** to use the new async property `element.text`.\n\n\n## Documentation\n<!-- Link to updated documentation -->\n\n## Release Checklist\n- [ ] Version number updated in pyproject.toml\n- [ ] Version number updated in cz.yaml\n- [ ] CHANGELOG.md updated with all changes\n- [ ] All tests passing\n- [ ] Documentation updated\n- [ ] API reference updated\n- [ ] Breaking changes documented\n- [ ] Migration guides prepared (if applicable)\n\n## Additional Release Notes\n<!-- Any additional information that should be included in release notes --> "
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!-- \nPlease choose the appropriate PR template for your change:\n\nFor bug fixes: .github/PULL_REQUEST_TEMPLATE/bug_fix.md\nFor refactoring: .github/PULL_REQUEST_TEMPLATE/refactoring.md\nFor releases: .github/PULL_REQUEST_TEMPLATE/release.md\n\nOr use this general template for other types of changes.\n-->\n\n# Pull Request\n\n## Description\n<!-- Provide a clear and concise description of what this PR accomplishes -->\n\n## Related Issue(s)\n<!-- Link the issues that are being addressed by this PR. Use the format: \"Fixes #123\" or \"Resolves #123\" -->\n\n## Type of Change\n<!-- Check the appropriate options that apply to this PR -->\n- [ ] Bug fix (non-breaking change which fixes an issue)\n- [ ] New feature (non-breaking change which adds functionality)\n- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)\n- [ ] Documentation update\n- [ ] Refactoring (no functional changes, no API changes)\n- [ ] Performance improvement\n- [ ] Tests (adding missing tests or correcting existing tests)\n- [ ] Build or CI/CD related changes\n\n## How Has This Been Tested?\n<!-- Describe the tests you ran to verify your changes. Provide instructions so reviewers can reproduce. -->\n\n```python\n# Include code examples if relevant\n```\n\n## Testing Checklist\n<!-- Check the testing aspects that apply to your change -->\n- [ ] Unit tests added/updated\n- [ ] Integration tests added/updated\n- [ ] All existing tests pass\n\n## Screenshots\n<!-- If applicable, add screenshots to help explain your changes -->\n\n## Implementation Details\n<!-- Provide any important details or context about the implementation -->\n\n## API Changes\n<!-- If applicable, describe any API changes -->\n\n## Additional Info\n<!-- Any additional information that might be useful for reviewers -->\n\n## Checklist before requesting a review\n- [ ] My code follows the style guidelines of this project\n- [ ] I have performed a self-review of my code\n- [ ] I have commented my code, particularly in hard-to-understand areas\n- [ ] I have made corresponding changes to the documentation\n- [ ] My changes generate no new warnings\n- [ ] I have added tests that prove my fix is effective or that my feature works\n- [ ] New and existing unit tests pass locally with my changes\n- [ ] I have run `poetry run task lint` and fixed any issues\n- [ ] I have run `poetry run task test` and all tests pass\n- [ ] My commits follow the [conventional commits](https://www.conventionalcommits.org/) style "
  },
  {
    "path": ".github/workflows/deploy-docs.yml",
    "content": "name: Deploy site + docs\n\non:\n  push:\n    branches: [main]\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Code Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.x'\n\n      - name: Install Dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install mkdocs mkdocs-material pymdown-extensions mkdocstrings[python] mkdocs-static-i18n\n\n      # Build MkDocs em pasta temporária\n      - name: Build MkDocs into temp folder\n        run: mkdocs build --site-dir temp_docs\n\n      # Criar estrutura final do site\n      - name: Prepare final site\n        run: |\n          mkdir -p site/docs\n          mkdir -p site/images\n          cp -r temp_docs/* site/docs/\n          cp -r public/* site/\n\n      - name: Deploy to GitHub Pages\n        uses: peaceiris/actions-gh-pages@v3\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: ./site\n          cname: pydoll.tech\n"
  },
  {
    "path": ".github/workflows/mypy.yml",
    "content": "name: MyPy CI\n\non:\n  push:\n    branches:\n      - '*'         # matches every branch that doesn't contain a '/'\n      - '*/*'       # matches every branch containing a single '/'\n      - '**'        # matches every branch\n  pull_request:\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      max-parallel: 4\n      matrix:\n        python-version: [\"3.11\"]\n\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v2\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install Dependencies\n        run: |\n          python -m pip install --upgrade pip\n          python -m pip install mypy\n          python -m pip install -e .\n          python -m mypy --install-types --non-interactive pydoll \n\n      - name: mypy\n        run: python -m mypy .\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish to PyPI (Poetry)\n\non: workflow_dispatch\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: \"3.10\"\n\n      - name: Install Poetry\n        run: |\n          python -m pip install --upgrade pip\n          pip install poetry\n\n      - name: Configure Poetry\n        run: poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }}\n\n      - name: Install dependencies\n        run: poetry install\n\n      - name: Build package\n        run: poetry build\n\n      - name: Publish to PyPI\n        run: poetry publish\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\non: workflow_dispatch\n\njobs:\n  version-cz:\n    runs-on: ubuntu-latest\n    name: \"Version CZ\"\n    outputs:\n      version: ${{ steps.cz.outputs.version }}\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      - id: cz\n        name: Create bump and changelog\n        uses: commitizen-tools/commitizen-action@master\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Print Version\n        run: echo \"Bumped to version ${{ steps.cz.outputs.version }}\"\n  \n  version-pyproject:\n    runs-on: ubuntu-latest\n    name: \"Version Pyproject\"\n    needs: version-cz\n    outputs:\n      version: ${{ needs.version-cz.outputs.version }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Install Poetry\n        run: |\n          curl -sSL https://install.python-poetry.org | python3 -\n          export PATH=\"$HOME/.local/bin:$PATH\"\n\n      - name: Update Poetry version in pyproject.toml\n        run: |\n          git config --global user.name \"github-actions[bot]\"\n          git config --global user.email \"github-actions[bot]@users.noreply.github.com\"\n          poetry version \"${{ needs.version-cz.outputs.version }}\"\n          git add pyproject.toml\n          git commit -m \"Update pyproject.toml to version ${{ needs.version-cz.outputs.version }}\"\n          git pull --rebase\n          git push\n\n      - name: Update poetry.lock\n        continue-on-error: true\n        run: |\n          git config --global user.name \"github-actions[bot]\"\n          git config --global user.email \"github-actions[bot]@users.noreply.github.com\"\n          poetry lock\n          git add poetry.lock\n          git commit -m \"Update poetry.lock\"\n          git pull --rebase\n          git push\n\n\n  release:\n    name: Release\n    needs: version-pyproject\n    runs-on: ubuntu-latest\n    steps:\n      - name: Create Release\n        uses: softprops/action-gh-release@v1\n        with:\n          draft: false\n          prerelease: false\n          generate_release_notes: true\n          tag_name: ${{ needs.version-pyproject.outputs.version }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/ruff-ci.yml",
    "content": "name: Ruff CI\n\non:\n  push:\n    branches:\n      - '*'         # matches every branch that doesn't contain a '/'\n      - '*/*'       # matches every branch containing a single '/'\n      - '**'        # matches every branch\n  pull_request:\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      max-parallel: 4\n      matrix:\n        python-version: [\"3.11\"]\n\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v2\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install Dependencies\n        run: |\n          python -m pip install --upgrade pip\n          python -m pip install ruff==0.7.1\n\n      - name: ruff\n        run: python -m ruff check .\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: PyDoll Tests\n\non:\n  push:\n  pull_request:\n\njobs:\n  tests:\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, windows-latest]\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v4\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install dependencies\n        run: |\n          python -m pip install poetry\n          poetry install\n      - name: Install Chrome\n        uses: browser-actions/setup-chrome@v1\n        with:\n          chrome-version: 132\n      - name: Run tests with coverage\n        run: |\n          poetry run pytest -s -x --cov=pydoll -vv --cov-report=xml\n\n      - name: Upload coverage to Codecov\n        uses: codecov/codecov-action@v5\n        with:\n          file: ./coverage.xml\n          flags: tests\n          name: PyDoll Tests\n          fail_ci_if_error: true\n          token: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\n\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control\n.pdm.toml\n.pdm-python\n.pdm-build/\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\n.czrc\n.ruff_cache/\n\n# Dev test file\ndev_test_file.py\n"
  },
  {
    "path": ".python-version",
    "content": "3.12.5\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## 2.21.3 (2026-03-14)\n\n### Fix\n\n- **test**: improve OOPIF integration test reliability\n- **iframe**: resolve nested OOPIF iframes inside shadow roots\n\n## 2.21.2 (2026-03-12)\n\n### Fix\n\n- release commit\n\n## 2.21.1 (2026-03-03)\n\n### Fix\n\n- **keyboard**: send correct key, code and keycode in type_text\n- **elements**: fix humanized interactions inside iframes\n- humanized scroll overshoot correction causes infinite loop\n\n## 2.21.0 (2026-03-01)\n\n### Feat\n\n- **interactions**: change humanize default from True to False\n\n### Fix\n\n- **elements**: forward humanize flag to click in type_text\n\n## 2.20.2 (2026-02-18)\n\n### Fix\n\n- **command**: increase default command timeout from 10s to 60s across multiple components\n- **tab**: remove temporary flag to avoid duplicate callback removal\n\n## 2.20.1 (2026-02-16)\n\n### Fix\n\n- **tab**: replace readyState polling with CDP events in navigation\n\n## 2.20.0 (2026-02-13)\n\n### Feat\n\n- **mouse**: add timing property for runtime configuration\n- **requests**: add record() and replay() to Request class\n- **requests**: add HAR network recorder\n- **protocol**: add HAR 1.2 type definitions\n\n### Fix\n\n- **requests**: use surgical callback removal instead of nuclear clear_callbacks\n\n### Refactor\n\n- **tab**: extract bundle static methods to utils module\n\n## 2.19.0 (2026-02-12)\n\n### Feat\n\n- **interactions**: default humanize=True for keyboard type_text\n- **elements**: integrate Mouse API into WebElement.click()\n- **interactions**: add Mouse API with humanized simulation\n- **browser**: add webrtc_leak_protection property to ChromiumOptions\n- **browser**: add automatic User-Agent consistency override\n\n### Fix\n\n- **utils**: harden SOCKS5 proxy forwarder security and robustness\n\n## 2.18.0 (2026-02-11)\n\n### Feat\n\n- **utils**: add SOCKS5 proxy forwarder and convert utils to package\n- **elements**: add cross-iframe selector support for XPath and CSS\n\n## 2.17.0 (2026-02-08)\n\n### Feat\n\n- **tab**: refactor cloudflare bypass to use shadow root traversal\n- **elements**: add shadow root timeout, CSS restriction and context propagation\n- **tab**: add find_shadow_roots with OOPIF traversal and timeout\n- **elements**: add shadow DOM support\n\n### Fix\n\n- **docs**: replace shadow.find() with query() in all documentation\n- **tests**: replace shadow.find() with query() in integration tests\n- **elements**: use float timeout and add contextual WaitElementTimeout messages\n\n## 2.16.0 (2026-02-06)\n\n### Feat\n\n- add clear method for input and enhance page load state handling\n\n### Fix\n\n- **browser**: support secure websocket connections\n\n## 2.15.1 (2026-01-04)\n\n### Fix\n\n- filter Symbol properties from element query results\n\n## 2.15.0 (2025-12-24)\n\n### Feat\n\n- Implement incognito mode cookie retrieval for `tab.get_cookies()` and update related documentation\n\n### Fix\n\n- inconsistence in type checking\n- Dispatch `KEY_DOWN` and `KEY_UP` events for character typing\n\n## 2.14.0 (2025-12-10)\n\n### Feat\n\n- get_tab_by_target method added\n- get_tab_by_target method added\n\n### Fix\n\n- adding type: ignore in JavascriptDialogOpeningEvent object\n- adding type: ignore in JavascriptDialogOpeningEvent object\n\n## 2.13.1 (2025-12-07)\n\n### Fix\n\n- add stuck scroll detection and minimum flick distance to humanized scroll, and correct scroll distance calculation.\n\n## 2.13.0 (2025-12-07)\n\n### Feat\n\n- Implement humanized keyboard typing and physics-based scroll, and add iframe interaction support.\n\n## 2.12.4 (2025-11-29)\n\n### Fix\n\n- optimize iframe resolution logic by adjusting backend node ID checks and enhancing child frame handling\n- refine OOPIF resolution and frame attachment logic for improved handling of backend node IDs\n- enhance OOPIF target attachment logic for improved session handling\n\n## 2.12.3 (2025-11-27)\n\n### Fix\n\n- improve frame retrieval logic for better session handling\n\n## 2.12.2 (2025-11-19)\n\n### Fix\n\n- adjust find_elements_mixin.py to refine return types and defaults\n\n## 2.12.1 (2025-11-14)\n\n### Fix\n\n- continue cleanup process if temporary directory still exists\n- adjust sleep duration for Windows and enhance temp dir cleanup\n- enhance error handling for locked files on Windows systems\n- remove unnecessary retry_times parameter in file processing\n- ensure temp directory cleanup handles Chromium locked files\n- enhance element selection and text extraction for better stability\n- handle oopif targets\n- change way to interact with iframes\n\n### Refactor\n\n- refactor iframe context handling in FindElementsMixin class\n\n### Perf\n\n- update Chrome options for better memory management and stability\n\n## 2.12.0 (2025-11-04)\n\n### Feat\n\n- **execute_script**: validate element argument usage\n- **tab,element,chrome**: revert arguments and add Chromium paths\n- add a retry decorator for handling function execution failures\n\n### Fix\n\n- import TopLevelTargetRequired in test_browser_tab.py\n- allow one additional retry attempt in the retry decorator\n\n### Refactor\n\n- **tab,element**: simplify execute_script parameters\n- **element**: move and enhance execute_script from tab\n- **tab**: separate execute_script concerns and enhance with comprehensive options\n\n## 2.11.0 (2025-11-02)\n\n### Feat\n\n- add input handling functions and key constants for editing\n- add KeyboardAPI for simulating keyboard input actions\n- add KeyboardAPI integration for enhanced keyboard control\n\n### Fix\n\n- enhance text insertion and deprecate legacy key methods\n\n## 2.10.0 (2025-11-01)\n\n### Feat\n\n- add ScrollAPI for enhanced page scrolling capabilities\n\n## 2.9.3 (2025-10-30)\n\n### Refactor\n\n- keep take_screenshot consistent\n- refactor type hints for better clarity and future compatibility\n\n## 2.9.2 (2025-10-19)\n\n### Fix\n\n- update process creation to capture output and clean proxy format\n- preserve query and fragment in WebSocket URL for tabs\n\n### Refactor\n\n- remove debug logging for request status and network events\n- refactor logger messages to use consistent single quotes\n- fix merge conflicts\n- add logging for browser lifecycle and context management events\n- refactor proxy parsing logic for improved clarity and efficiency\n\n## 2.9.1 (2025-10-15)\n\n### Fix\n\n- change download event handling to use PageEvent instead of BrowserEvent\n\n### Refactor\n\n- use early return in setup proxy method\n\n## 2.9.0 (2025-10-05)\n\n### Feat\n\n- add configurable page load state\n\n## 2.8.2 (2025-10-03)\n\n### Fix\n\n- implement proxy authentication handling for browser tabs\n- map exception when try to take screenshot of an iframe\n\n## 2.8.1 (2025-09-27)\n\n### Fix\n\n- store the opened tab in the _tabs_opened dictionary\n- **elements**: correctly detect parenthesized XPath expressions\n\n### Refactor\n\n- simplify FindElementsMixin._get_expression_type startswith checks into single tuple\n\n## 2.8.0 (2025-08-28)\n\n### Feat\n\n- adding get_siblings_elements method\n- adding get_children_elements method\n- refactor Tab class to support optional WebSocket address handling\n- add WebSocket connection support for existing browser instances\n- add optional WebSocket address support in connection handler\n\n### Fix\n\n- add get siblings and get childen methods a raise_exc option\n- improving children and parent retrive docstring and creating a private generic method for then\n- using new execute_script public method\n- solving conflicts\n- rename pages fixtures files and adding a error test\n\n### Refactor\n\n- refactor Tab class to improve initialization and error handling\n- refactor Browser class to manage opened tabs and WebSocket setup\n- add new exception classes for connection and WebSocket errors\n\n## 2.7.0 (2025-08-22)\n\n### Feat\n\n- refactor WebElement methods to use a unified naming convention\n- add Response type and new bring_to_front method to Tab class\n- improve element interactability scripts\n\n### Fix\n\n- **browser**: add google-chrome-stable path for Arch Linux AUR package\n- run actions to fix badges\n- enforce combined condition logic in wait_until\n- **web_element**: raise WaitElementTimeout on wait_until timeout\n\n### Refactor\n\n- update command responses to use Response for empty responses\n- **webelement**: simplify wait_until condition mapping\n\n## 2.6.0 (2025-08-10)\n\n### Feat\n\n- add DownloadTimeout exception for file download timeouts\n- add context manager for handling file downloads in Tab class\n\n### Refactor\n\n- add type checking for connection handler in mixin class\n- add type overloads for event callback in Browser class\n\n## 2.5.0 (2025-08-07)\n\n### Feat\n\n- add HTTP client functionality using the browser's fetch API\n- add HTTP response object for browser-based fetch requests\n- implement Request class for HTTP requests using fetch API\n- add Request handling and improve network log retrieval methods\n\n### Fix\n\n- reject cookies with empty names during parsing in Request class\n- refactor imports to include NotRequired and TypedDict from typing_extensions\n- update imports to use typing_extensions for compatibility reasons\n- check for None in events_enabled before updating params\n- remove unused event type aliases and clean up imports\n\n### Refactor\n\n- depreciating headless argument in start method and adding it in to browser options properties\n- add asynchronous function for makeRequest in JavaScript\n- refactor imports for cleaner organization and improved clarity\n- refactor type hints in FindElementsMixin for clarity and type safety\n- refactor type hints and improve command method signatures\n- refactor event handling to use specific event types for clarity\n- refactor connection handler to use CDPEvent and typed commands\n- refactor storage command methods to return specific command types\n- refactor target command methods to use specific command types\n- refactor command return types to specific command classes\n- refactor page commands to use specific command types directly\n- refactor network commands to use specific command types\n- refactor input command methods to return specific command types\n- refactor fetch_commands to use updated type definitions\n- refactor enums to inherit from str for better compatibility\n- refactor DOM command types for improved code clarity and structure\n- refactor command and event parameter types for better typing\n- refactor command responses to use EmptyResponse where applicable\n- improve protocol types for target domain\n- improve protocol types for storage domain\n- refactor command response types for improved readability and consistency\n- improve protocol types for page domain\n- add IncludeWhitespace and RelationType enums to DOM types\n- improve protocol types for input domain\n- refactor AuthChallengeResponse and remove legacy definitions\n- remove legacy WindowBoundsDict for cleaner type definitions\n- add new TypedDicts and enums for runtime event parameters\n- refactor DOM event types and methods for better clarity and structure\n- refactor fetch command return types for better clarity and structure\n- enhance browser command functionality with new methods and types\n- add TypedDict and Enum definitions for emulation and debugging\n- improve protocol types for network domain\n\n## 2.4.0 (2025-08-01)\n\n### Feat\n\n- changing bool prefs to properties and adding support to user-data-dir preferences\n- adding prefs options customization\n- add overloads for find and query methods in FindElementsMixin\n- add method to retrieve parent element and its attributes\n- implements start_timeout option\n\n### Fix\n\n- adding typehint and fixing some codes\n- removing options preferences private attributes\n- set default URL to 'about:blank' in create_target method\n- change navigation when creating a new tab\n- add type hinting support and update project description\n\n### Refactor\n\n- remove redundant asterisk from find method overloads and reorganize query method overloads\n- refine type hint for response parameter and improve key check\n\n## 2.3.1 (2025-07-12)\n\n### Fix\n\n- refactor click_option_tag to use direct script reference\n- update script to use closest for more reliable DOM selection\n- improve selection script for higher accuracy\n- use correct class name and id selector in query()\n- add fetch command methods to handle request processing\n\n### Refactor\n\n- change body type from dict to string in fetch command parameters\n- refactor continue_request and fulfill_request to use options\n- enhance continue_request and fulfill_request with new options\n\n## 2.3.0 (2025-06-25)\n\n### Feat\n\n- **connection**: Upgrade adapt websockets version to 14.0\n\n### Fix\n\n- refine selector condition to include attributes check\n\n## 2.2.3 (2025-06-20)\n\n### Fix\n\n- fix contextmanager for file upload\n\n## 2.2.2 (2025-06-18)\n\n### Fix\n\n- fix call_function_on parameters order\n\n### Refactor\n\n- replace BeautifulSoup with custom HTML text extractor\n\n## 2.2.1 (2025-06-16)\n\n### Fix\n\n- fix call parameters order in call_function_on method\n\n## 2.2.0 (2025-06-15)\n\n### Feat\n\n- add method to retrieve non-extension opened tabs as Tab instances\n\n### Refactor\n\n- refactor attribute assignments to include type annotations\n- implement singleton pattern for Tab instances by target_id\n\n## 2.1.0 (2025-06-14)\n\n### Feat\n\n- add new script-related exception classes for better handling\n- add functions to clean scripts and check return statements\n- add methods to retrieve network response body and logs\n\n### Fix\n\n- click in the input before typing and fix documentation\n\n### Refactor\n\n- add overloads for execute_script to improve type safety\n\n## 2.0.1 (2025-06-08)\n\n### Fix\n\n- fix private proxy configuration\n\n## 2.0.0 (2025-06-08)\n\n### BREAKING CHANGE\n\n- pydoll v2 finished\n\n### Feat\n\n- intuitive way to interact with iframes\n- refactor Keys class to Key and add utility methods for enums\n- add Event TypedDict for standardized event structure\n- add TargetEvent enum for Chrome DevTools Protocol events\n- add StorageEvent enumeration for Chrome DevTools Protocol events\n- add RuntimeEvent enumeration for Chrome DevTools Protocol events\n- add PageEvent enumeration for Chrome DevTools Protocol events\n- add NetworkEvent enumeration for Chrome DevTools Protocol events\n- add InputEvent enum for Chrome DevTools input events\n- add FetchEvent enumeration for Chrome DevTools Protocol events\n- add DomEvent enumeration for Chrome DevTools Protocol events\n- add BrowserEvent enum for Chrome DevTools protocol events\n- add methods to enable and disable the runtime domain commands\n- add new enums for whitespace, axes, pseudo types, and modes\n- add DOM response types and corresponding response classes\n- add DOM command types and parameter definitions for pydoll\n- add enums for key, mouse, touch, and drag event types\n- add input command types for touch, mouse, and keyboard events\n- enhance TargetCommands class with new methods for targets management\n- add TypedDicts for target response types and browser contexts\n- add TypedDict definitions for target command parameters\n- add storage-related enumerations for bucket durability and types\n- enhance StorageCommands with new methods for data management\n- add storage response types and related classes for handling data\n- add storage command types using TypedDict for structured params\n- add new enumeration classes for serialization and object types\n- add runtime response types for handling various object previews\n- add initial runtime command types for protocol handling\n- add constants for various encoding, formats, and policies\n- add TypedDict definitions for page response types and results\n- add typed dictionaries for various page command parameters\n- add new command parameter classes for network resource handling\n- add TypedDict definitions for network response types\n- organize command types into structured imports and exports\n- add network command types and parameters for cookie management\n- add enums for cookie priorities, connection types, and encodings\n- add response classes for browser window target retrieval\n- setup mkdocs and install related packages\n- add async text property for retrieving element text\n\n### Fix\n\n- remove target directory from .gitignore file\n- fix typo in USB_UNRESTRICTED constant for consistency\n- add new network command parameters and methods for cookies\n- change postData type from dict to string in ContinueRequestParams\n\n### Refactor\n\n- refactor screenshot path handling and enhance error checking\n- refactor type hints from List to built-in list for consistency\n- refine XPath condition handling and ensure integer coordinates\n- refactor condition checks to ensure against None values\n- refactor exception handling and add browser path validation function\n- rename BrowserOptionsManager to ChromiumOptionsManager\n- refactor Edge class to use ChromiumOptionsManager and simplify path validation\n- refactor Chrome class to use Chromium-specific options manager\n- refactor Browser class to use options manager and improve methods\n- refactor Options class to ChromiumOptions and use type hints\n- refactor to create ChromiumOptionsManager for better clarity\n- add abstract base classes for browser options management\n- use `message.get('id')` for safer ID checks in response\n- refactor message handling to support multiple message types\n- refactor element finding methods for enhanced flexibility and clarity\n- rename method for better clarity in captcha element handling\n- refactor type hints for event callback parameters and options\n- simplify ping call by inlining WebSocketClientProtocol cast\n- refactor EventsManager to use typed Event objects consistently\n- add runtime events management to the Tab class functionality\n- update event callback signatures for better type handling\n- remove unused import of Response in runtime_commands.py\n- add Response import to page_commands for improved functionality\n- refactor response classes to use TypedDict for better typing\n- refactor WebElement class to organize exception imports clearly\n- refactor exception handling in FindElementsMixin class\n- refactor exception handling to use custom timeout and connection errors\n- remove unused import statements in events_manager.py\n- refactor error handling to use specific exceptions for clarity\n- refactor error handling to use custom exception for arguments\n- fix PermissionError raising in TempDirectoryManager class\n- refactor error handling to use specific exceptions for clarity\n- handle unsupported OS with a custom exception in Edge class\n- raise UnsupportedOS exception for unsupported operating systems\n- refactor browser error handling and improve method return types\n- refactor exception classes to improve organization and clarity\n- refactor element finding methods to use updated command structure\n- refactor WebElement class for improved structure and clarity\n- refactor import statements and clean up code formatting\n- refactor command imports and enhance download behavior method\n- refactor Tab import and update FetchCommands method calls\n- refactor ConnectionHandler docstrings for clarity and conciseness\n- refactor command and event managers for improved type safety\n- refactor ConnectionHandler to improve WebSocket management and clarity\n- add Tab class for managing browser tabs via CDP integration\n- enhance TempDirectoryManager with detailed docstrings and type hints\n- refactor ProxyManager to enhance proxy credential handling\n- refactor Browser class to enhance automation capabilities and structure\n- move commands to a different module\n- define base structures for commands and responses in protocol\n- import Rect from dom_commands_types for response handling\n- refactor cookie-related types for improved clarity and consistency\n- remove unnecessary whitespace in docstring of InputCommands class\n- refactor DOM commands to improve structure and add functionality\n- refactor InputCommands to enhance user input simulation methods\n- add CookieParam TypedDict to define cookie attributes\n- add new runtime command methods for JavaScript bindings and promises\n- remove unused method to clear accepted encodings in network commands\n- update ResetPermissionsParams to use NotRequired for context ID\n- refactor PageCommands to improve structure and add type hints\n- simplify import statements by using wildcard imports for responses\n- add new response types and update existing response classes\n- consolidate command imports using wildcard imports for clarity\n- correct post_data type from dict to str in FetchCommands class\n- refactor NetworkCommands to use structured command parameters\n- refactor fetch command methods to use static methods directly\n- refactor BrowserCommands to use static methods and improve clarity\n- refactor response imports and update __all__ definitions\n- refactor import statements for better readability and structure\n- refactor import statements for consistency in response types\n- refactor import and rename EnableParams to FetchEnableParams\n- refactor import statement for CommandParams module path\n- refactor fetch command templates to use Command class\n- add enums for window states, download behaviors, and permissions\n- remove unused enum imports and rename base_types module\n- refactor command structures for better organization and clarity\n- rename command and response modules for better clarity\n- refactor imports for better organization and readability\n- add browser command methods for version, permissions, and downloads\n- add command and response types for protocol implementation\n- refactor execute_command to use type annotations for clarity\n- refactor command methods to specify response types in BrowserCommands\n- refactor command structures and introduce base CommandParams class\n- refactor browser command constants to use Command class type\n- refactor connection imports and rename manager files for clarity\n- refactor BrowserType import to a common constants module\n- refactor browser modules to use the new chromium structure\n- refactor element imports and remove deprecated element file\n- refactor import paths to use the protocol submodule structure\n- move command files to the protocol directory for better structure\n- rename insert_text to paste_text and remove unused files\n- refactor the `InputCommands` class to enhance clarity and simplicity in its operations\n- add deprecation warning to get_element_text()\n\n## 1.7.0 (2025-04-06)\n\n### Feat\n\n- refactor captcha handling with adjustable wait times and parameters\n\n## 1.6.0 (2025-04-06)\n\n### Feat\n\n- add connect method to handle existing port scenarios\n- create enable_auto_solve_cloudflare_captcha method\n- add context manager to bypass Cloudflare Turnstile captcha\n\n## 1.5.1 (2025-03-31)\n\n### Fix\n\n- handle headers input as list or dictionary in fetch command\n\n## 1.5.0 (2025-03-26)\n\n### Feat\n\n- add flag to run browser on headless mode on start function\n\n### Fix\n\n- Wait for the file `CrashpadMetrics-active.pma` to be deoccupied and cleaned up\n- Catch websockets.ConnectionClosed errors on duplicate close()\n- move connection closed log inside if statement\n\n## 1.4.0 (2025-03-23)\n\n### Feat\n\n- Update initialize_options method to allow optional browser_type parameter\n- Refactor Edge browser options handling to use EdgeOptions class\n- Supports initialization options based on browser type\n- Edge browser constructors to support optional connection port parameters\n- Add Microsoft Edge browser support\n- 为 Edge 浏览器添加默认用户数据目录支持\n- Add Microsoft Edge browser support\n\n### Refactor\n\n- Clean up imports and improve code formatting across browser modules\n- Simplify user data directory setup and enhance Edge browser path handling\n\n## 1.3.3 (2025-03-18)\n\n### Fix\n\n- solve browser invalid domain events issue\n- improve process termination\n- improve process management and deactivate websockets connection size limit\n\n### Refactor\n\n- import commands and evebts from __init__.py\n\n## 1.3.2 (2025-03-13)\n\n### Fix\n\n- fixed the tests and used lint for the OS multi path support\n- support multiple default Chrome paths on each OS\n\n## 1.3.1 (2025-03-12)\n\n### Fix\n\n- remove unnecessary encoding from screenshot response data\n\n## 1.3.0 (2025-03-12)\n\n### Feat\n\n- add method to retrieve screenshot as base64 encoded string\n\n## 1.2.4 (2025-03-11)\n\n### Fix\n\n- refactor Chrome constructor to use Optional for parameters\n\n## 1.2.3 (2025-03-11)\n\n### Fix\n\n- refactor proxy configuration retrieval for cleaner code flow\n\n## 1.2.2 (2025-03-10)\n\n### Fix\n\n- Get file extension from file path and changes use of reserved word 'format' to 'fmt'\n\n## 1.2.1 (2025-03-09)\n\n### Fix\n\n- resolve issue #29 where browser path was not found on macOS\n- Quickstart code given in README is wrong\n\n## 1.2.0 (2025-02-11)\n\n### Feat\n\n- add close method and command to Page class functionality\n\n## 1.1.0 (2025-02-11)\n\n### Feat\n\n- add method to retrieve Page instance by its ID in Browser class\n\n## 1.0.1 (2025-02-10)\n\n### Fix\n\n- add dialog property to ConnectionHandler and manage dialog state\n\n## 1.0.0 (2025-02-05)\n\n### BREAKING CHANGE\n\n- now you'll have to use By.CSS_SELECTOR instead of By.CSS\n\n### Feat\n\n- refactor import and export statements for better readability\n- update changelog for version 0.7.0 and fix dependency versions\n- add ping method to ConnectionHandler for browser connectivity check\n- add tests for BrowserCommands in test_browser_commands.py\n\n### Fix\n\n- add initial module files for commands, connection, events, and mixins\n- add connection port parameter to Chrome browser initialization\n- use deepcopy for templates to prevent mutation issues\n\n### Refactor\n\n- rename constant CSS to CSS_SELECTOR\n- add command imports and remove obsolete connection handler code\n- refactor methods to be static in ConnectionHandler class\n- refactor proxy configuration and cleanup logic in Browser class\n- refactor ConnectionHandler to improve WebSocket management logic\n- refactor Browser class initialization for better clarity and structure\n- refactor Browser initialization to enhance flexibility and defaults\n- refactor import statement for ConnectionHandler module\n- refactor import paths for ConnectionHandler in browser modules\n- implement ConnectionHandler for WebSocket browser automation\n- implement command and event management for asynchronous processing\n- remove unnecessary logging for WebSocket address fetching\n- refactor Chrome class to use BrowserOptionsManager for path validation\n- implement proxy and browser management in the new managers module\n- refactor Browser class to use manager classes for better structure\n- refactor DOM command scripts for clarity and efficiency\n\n## 0.7.0 (2024-12-09)\n\n### Feat\n\n- autoremove dialog from connection_handler when closed\n- add handle_dialog method to PageCommands class\n- add dialog handling methods to Page class\n- add support for handling JavaScript dialog opening events\n- refactor network response handling for base64 encoding support\n- add clipping option for screenshots and implement element capture\n\n### Fix\n\n- index error on method get_dialog_message\n- update screenshot format from 'jpg' to 'jpeg' for consistency\n- handle potential IndexError when retrieving valid page targetId\n- filter valid pages using URL condition instead of title check\n\n### Refactor\n\n- run ruff formatter to ensure code consistency\n- run ruff formatter to ensure code consistency\n- change screenshot format from PNG to JPG in commands and element\n\n## 0.6.0 (2024-11-18)\n\n### Feat\n\n- add callback ID handling for page load events in Page class\n- update event registration to return callback IDs and add removal\n- refactor DOM commands to use object_id instead of node_id\n\n### Fix\n\n- refactor page navigation and loading logic for efficiency\n- add page reload after navigating to a new URL in Page class\n- refactor URL navigation to use evaluate_script for efficiency\n- implement page refresh on URL unchanged and add navigation event\n- update object ID reference in Page class for clarity\n- refactor element search logic to simplify error handling\n- DomCommands using `object_id` instead of `node_id` to prevent bugs\n- handle OSError when cleaning up temporary directories in Browser\n\n### Refactor\n\n- change error log to warning for missing callback ID\n- refactor DOM command scripts for improved readability and reuse\n- rename methods for clarity and consistency in WebElement class\n- refactor parameter names for consistency in target methods\n- normalize variable naming for consistency in fetch commands\n\n## 0.5.1 (2024-11-12)\n\n### Fix\n\n- simplify outer HTML retrieval for consistent object handling\n- refactor click method to check option tag earlier in flow\n- refactor bounding box retrieval to access nested response value\n- handle KeyError instead of IndexError for element bounds retrieval\n- enhance DOM command methods and rename for clarity and consistency\n- add JavaScript bounding box retrieval for web elements\n- remove redundant top-checks for element clicks in WebElement\n\n## 0.5.0 (2024-11-11)\n\n### Feat\n\n- add method to generate command for calling a function on an object\n- implement script execution and visibility checks in click method\n- add JavaScript functions for element visibility and interaction\n\n### Refactor\n\n- enhance exception classes with descriptive error messages\n- simplify command creation by using RuntimeCommands.evaluate_script\n- refactor JavaScript execution and introduce runtime commands\n\n## 0.4.4 (2024-11-11)\n\n### Fix\n\n- remove redundant DOM content loaded event handling logic\n\n## 0.4.3 (2024-11-11)\n\n### Fix\n\n- rename event variables for clarity and improve timeout handling\n\n### Refactor\n\n- remove debug print statement from connection event handling\n\n## 0.4.2 (2024-11-11)\n\n### Fix\n\n- update event handling to use DOM_CONTENT_LOADED for page load\n- convert Browser context management to async methods\n\n### Refactor\n\n- fix string formatting in logger info message for clarity\n\n## 0.4.1 (2024-11-08)\n\n### Fix\n\n- fixes workflow removing unnecessary hifen\n- reduce sleep duration in key press handling for improved speed\n\n## 0.4.0 (2024-11-08)\n\n### Feat\n\n- add type_keys method for realistic key input simulation\n\n## 0.3.1 (2024-11-08)\n\n### Fix\n\n- addning new package version\n- removing encode utf8 in get_pdf_base64\n\n## 0.3.0 (2024-11-08)\n\n### Feat\n\n- set_download_path added in browser class methods\n\n## 0.2.0 (2024-11-08)\n\n### Feat\n\n- dynamic lib version using pyproject\n\n## 0.1.1 (2024-11-07)\n\n### Fix\n\n- ensure browser process terminates after executing close command\n\n## 0.1.0 (2024-11-07)\n\n### Feat\n\n- add method to delete all cookies from the browser session\n- add is_enabled property to check element's enabled status\n- add option to raise exception in wait_element method\n- add method to set browser download path via command\n- refactor text extraction using BeautifulSoup for accuracy\n- add method to get properties and improve XPath handling\n- refactor text retrieval methods and improve code readability\n- add timeout parameter to page navigation and loading methods\n- add cookie management and scroll into view functionality\n- add method to retrieve page PDF data as base64 string\n- add async property to retrieve inner HTML of the element\n- add async page_source property to retrieve page source code\n- add async property to retrieve the current page URL\n- add method to find multiple DOM elements using selectors\n- refactor WebElement to use FindElementsMixin for clarity\n- add FindElementsMixin for asynchronous DOM element handling\n- add methods to retrieve network response bodies from logs\n- add method to retrieve matching network logs from the page\n- add cookie management methods to the Browser class\n- add ElementNotFound exception to handle missing elements\n- add value property and handle option tag clicks in WebElement\n- rename FIND_ELEMENT_XPATH_TEMPLATE to EVALUATE_TEMPLATE\n- add exception handling for element not found in find_element method\n- downgrade Python version requirement to 3.10 in pyproject.toml\n- add async function to fetch browser WebSocket address\n- simplify text input handling by using insert_text command\n- add TargetCommands class for managing target operations\n- add method to generate command for disabling the Page domain\n- add method to generate text insertion commands for inputs\n- add Page class to manage browser page interactions and events\n- add page management methods to the Browser class\n- add detailed logging for command responses and event handling\n- add event classes for browser, DOM, fetch, and network actions\n- add NetworkCommands class for managing network operations\n- implement fetch command methods for handling requests and responses\n- add method to enable DOM domain events in DomCommands class\n- add proxy configuration and fetch event handling to Browser\n- refactor connection errors to use custom exceptions for clarity\n- add methods to clear callbacks and close WebSocket connection\n- remove unnecessary newline at the end of PageEvents class file\n- add context managers and async file handling for efficiency\n- implement singleton pattern and prevent multiple initializations\n- add dynamic connection port handling for browser instance\n- add temporary directory management for browser session storage\n- add logging for connection events and command executions\n- add PageEvents class with PAGE_LOADED event constant\n- add temporary callback option to event registration method\n- add page event handling and improve loading timeout management\n- add utility function to decode base64 images to bytes\n- add WebElement class for handling browser elements asynchronously\n- add enumeration for selector types in constants module\n- add PageCommands class for browser page control functions\n- add InputCommands class for handling mouse and keyboard events\n- implement DOM commands for interacting with web elements\n- refactor BrowserCommands to include new window management methods\n- implement some basic methods to navigate and control the browser instance\n- enhance ConnectionHandler with detailed docstrings for methods\n- add .gitignore, .python-version, and poetry.lock files\n\n### Fix\n\n- browser context now uses the storage commands to get cookies, while the page context us cookies, while page context uses network\n- update cookie retrieval to use NetworkCommands for consistency\n- remove download path method from Browser and add to Page class\n- add options to disable first-run and browser check flags\n- handle KeyError when retrieving network response bodies\n- use get() to safely retrieve attributes in WebElement class\n- rename class attribute retrieval for clarity and consistency\n- enhance get_properties and simplify text retrieval method\n- enhance create_web_element call with additional value parameter\n- fix incorrect key access in JavaScript evaluation result\n- update cookie management to clear browser cookies correctly\n- filter pages by title instead of URL in Browser class\n- filter out non-page entries when fetching valid page IDs\n- xpath element solved\n- refactor event callback storage to use unique callback IDs\n- add JavaScript execution method and enhance click offsets\n- simplify response handling and improve event callback structure\n- reorder page event enabling to ensure proper browser startup\n- add JSON handling and improve WebSocket command execution\n\n### Refactor\n\n- improve WebElement representation and handle None for nodeValue\n- add newline at end of file for ElementNotFound exception class\n- remove unused aiohttp import and clean up whitespace\n- remove unnecessary blank lines in storage.py for clarity\n- fix missing newline at the end of the file in page.py\n- remove unnecessary whitespace in InputCommands class methods\n- refactor DOM command methods for improved clarity and usability\n- refactor Page class to inherit from FindElementsMixin\n- refactor code to remove duplicate import of StorageCommands\n- clarify error messages for command and callback validation\n- refactor ConnectionHandler to simplify initialization and connect logic\n- remove unnecessary whitespace in element.py for cleaner code\n- refactor WebElement to enhance attribute retrieval methods\n- refactor connection handling and improve error messaging\n- refactor Browser class to use abstract base class and commands\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing Guide\n\nThank you for your interest in contributing to the project! This document provides guidelines and instructions to help you contribute effectively.\n\n## Table of Contents\n\n- [Environment Setup](#environment-setup)\n- [Development Workflow](#development-workflow)\n- [Code Standards](#code-standards)\n- [Testing](#testing)\n- [Commit Messages](#commit-messages)\n- [Pull Request Process](#pull-request-process)\n\n## Environment Setup\n\n### Prerequisites\n\n- Python 3.10 or higher\n- [Poetry](https://python-poetry.org/docs/#installation) for dependency management\n\n### Installation\n\n1. Clone the repository:\n   ```bash\n   git clone [REPOSITORY_URL]\n   cd pydoll\n   ```\n\n2. Install dependencies using Poetry:\n   ```bash\n   poetry install\n   ```\n\n3. Activate the virtual environment:\n   ```bash\n   poetry shell\n   ```\n\n## Development Workflow\n\n1. Create a new branch for your contribution:\n   ```bash\n   git checkout -b feature/your-feature-name\n   ```\n   or\n   ```bash\n   git checkout -b fix/your-fix-name\n   ```\n\n2. Make your changes following the code and testing guidelines.\n\n3. Check your code using the linter:\n   ```bash\n   poetry run task lint\n   ```\n\n4. Format your code:\n   ```bash\n   poetry run task format\n   ```\n\n5. Run the tests to ensure everything is working:\n   ```bash\n   poetry run task test\n   ```\n\n6. Commit your changes following the commit conventions (see below).\n\n7. Push your changes and open a Pull Request.\n\n## Code Standards\n\nThis project uses [Ruff](https://github.com/charliermarsh/ruff) for linting and code formatting. The code standards are defined in the `pyproject.toml` file.\n\n### Linting and Formatting\n\nTo check if your code follows the standards:\n\n```bash\npoetry run task lint\n```\n\nTo automatically fix some issues and format your code:\n\n```bash\npoetry run task format\n```\n\n**Important:** Make sure to resolve all linting issues before submitting your changes. Code that doesn't pass the linting checks will not be accepted.\n\n## Testing\n\n### Writing Tests\n\nFor each new feature or modification, it is **mandatory** to write corresponding tests. We use `pytest` for testing.\n\n- Tests should be placed in the `tests/` directory\n- Test file names should start with `test_`\n- Test function names should start with `test_`\n\n### Running Tests\n\nTo run all tests:\n\n```bash\npoetry run task test\n```\n\nThis will also generate a code coverage report (HTML) that can be viewed in the `htmlcov/` folder.\n\n## Commit Messages\n\nThis project follows the [Conventional Commits](https://www.conventionalcommits.org/) standard for commit messages. We use the `commitizen` tool to facilitate the creation of standardized commits.\n\n### Commit Message Structure\n\n```\n<type>[optional scope]: <description>\n\n[optional body]\n\n[optional footer(s)]\n```\n\n### Commit Types\n\n- **feat**: A new feature\n- **fix**: A bug fix\n- **docs**: Documentation-only changes\n- **style**: Changes that do not affect the meaning of the code (whitespace, formatting, etc.)\n- **refactor**: A code change that neither fixes a bug nor adds a feature\n- **perf**: A code change that improves performance\n- **test**: Adding or correcting tests\n- **build**: Changes that affect the build system or external dependencies\n- **ci**: Changes to CI configuration files\n- **chore**: Other changes that don't modify src or test files\n\n### Examples of Good Commit Messages\n\n```\nfeat(parser): add ability to parse arrays\n```\n\n```\nfix(networking): resolve connection timeout issue\n\nA problem was identified in the networking library that\ncaused unexpected timeouts. This change increases the\ndefault timeout from 10s to 30s.\n```\n\n## Pull Request Process\n\n1. Verify that your code passes all tests and linting checks.\n2. Push your branch to the repository.\n3. Open a Pull Request to the main branch.\n4. In the PR description, clearly explain what was changed and why.\n5. Link any related issues to your PR.\n6. Wait for the code review. Read the comments and make necessary changes.\n\n## Questions?\n\nIf you have questions or need help, open an issue in the repository or contact the project maintainers.\n\n---\n\nWe appreciate your contributions to make this project better! "
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright © 2025 AutoscrapeLabs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <img src=\"https://github.com/user-attachments/assets/2c380638-b04a-4b04-b1c8-2958e4237a94\" alt=\"Pydoll Logo\" /> <br>\n</p>\n<p align=\"center\">Async-native, fully typed, built for evasion and performance.</p>\n\n<p align=\"center\">\n    <a href=\"https://github.com/autoscrape-labs/pydoll/stargazers\"><img src=\"https://img.shields.io/github/stars/autoscrape-labs/pydoll?style=social\"></a>\n    <a href=\"https://codecov.io/gh/autoscrape-labs/pydoll\" >\n        <img src=\"https://codecov.io/gh/autoscrape-labs/pydoll/graph/badge.svg?token=40I938OGM9\"/>\n    </a>\n    <img src=\"https://github.com/autoscrape-labs/pydoll/actions/workflows/tests.yml/badge.svg\" alt=\"Tests\">\n    <img src=\"https://github.com/autoscrape-labs/pydoll/actions/workflows/ruff-ci.yml/badge.svg\" alt=\"Ruff CI\">\n    <img src=\"https://github.com/autoscrape-labs/pydoll/actions/workflows/mypy.yml/badge.svg\" alt=\"MyPy CI\">\n    <img src=\"https://img.shields.io/badge/python-%3E%3D3.10-blue\" alt=\"Python >= 3.10\">\n    <a href=\"https://deepwiki.com/autoscrape-labs/pydoll\"><img src=\"https://deepwiki.com/badge.svg\" alt=\"Ask DeepWiki\"></a>\n</p>\n\n<p align=\"center\">\n    <a href=\"https://pydoll.tech/\">Documentation</a> &middot;\n    <a href=\"#getting-started\">Getting Started</a> &middot;\n    <a href=\"#features\">Features</a> &middot;\n    <a href=\"#support\">Support</a>\n</p>\n\nPydoll automates Chromium-based browsers (Chrome, Edge) by connecting directly to the Chrome DevTools Protocol over WebSocket. No WebDriver binary, no `navigator.webdriver` flag, no compatibility issues.\n\nIt combines a high-level API for common tasks with low-level CDP access for fine-grained control over network, fingerprinting, and browser behavior. The entire codebase is async-native and fully type-checked with mypy.\n\n### Top Sponsors\n\n<a href=\"https://substack.thewebscraping.club/p/pydoll-webdriver-scraping?utm_source=github&utm_medium=repo&utm_campaign=pydoll\">\n    <img src=\"public/images/banner-the-webscraping-club.png\" alt=\"The Web Scraping Club\" />\n</a>\n\n<sub>Read a full review of Pydoll on <b><a href=\"https://substack.thewebscraping.club/p/pydoll-webdriver-scraping?utm_source=github&utm_medium=repo&utm_campaign=pydoll\">The Web Scraping Club</a></b>, the #1 newsletter dedicated to web scraping.</sub>\n\n### Sponsors\n\n<table>\n  <tr>\n    <td><a href=\"https://www.thordata.com/?ls=github&lk=pydoll\"><img src=\"public/images/Thordata-logo.png\" height=\"30\" alt=\"Thordata\" /></a></td>\n    <td><a href=\"https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc\"><img src=\"public/images/capsolver-logo.png\" height=\"40\" alt=\"CapSolver\" /></a></td>\n    <td><a href=\"https://www.testmuai.com/?utm_medium=sponsor&utm_source=pydoll\"><img src=\"public/images/logo-lamda-test.svg\" height=\"30\" width=\"130\" alt=\"LambdaTest\" /></a></td>\n  </tr>\n</table>\n\n<sub>[Learn more about our sponsors](SPONSORS.md) &middot; [Become a sponsor](https://github.com/sponsors/thalissonvs)</sub>\n\n### Why Pydoll\n\n- **Stealth-first**: Human-like mouse movement, realistic typing, and granular [browser preference](https://pydoll.tech/docs/features/configuration/browser-preferences/) control for fingerprint management.\n- **Async and typed**: Built on `asyncio` from the ground up, 100% type-checked with `mypy`. Full IDE autocompletion and static error checking.\n- **Network control**: [Intercept](https://pydoll.tech/docs/features/network/interception/) requests to block ads/trackers, [monitor](https://pydoll.tech/docs/features/network/monitoring/) traffic for API discovery, and make [authenticated HTTP requests](https://pydoll.tech/docs/features/network/http-requests/) that inherit the browser session.\n- **Shadow DOM and iframes**: Full support for [shadow roots](https://pydoll.tech/docs/deep-dive/architecture/shadow-dom/) (including closed) and cross-origin iframes. Discover, query, and interact with elements inside them using the same API.\n- **Ergonomic API**: `tab.find()` for most cases, `tab.query()` for complex [CSS/XPath selectors](https://pydoll.tech/docs/deep-dive/guides/selectors-guide/).\n\n## Installation\n\n```bash\npip install pydoll-python\n```\n\nNo WebDriver binaries or external dependencies required.\n\n## What's New\n\n<details>\n<summary><b>HAR Network Recording</b></summary>\n<br>\n\nRecord network activity during a browser session and export as HAR 1.2. Replay recorded requests to reproduce exact API sequences.\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n\n    async with tab.request.record() as capture:\n        await tab.go_to('https://example.com')\n\n    capture.save('flow.har')\n    print(f'Captured {len(capture.entries)} requests')\n\n    responses = await tab.request.replay('flow.har')\n```\n\nFilter by resource type:\n\n```python\nfrom pydoll.protocol.network.types import ResourceType\n\nasync with tab.request.record(\n    resource_types=[ResourceType.FETCH, ResourceType.XHR]\n) as capture:\n    await tab.go_to('https://example.com')\n```\n\n[HAR Recording Docs](https://pydoll.tech/docs/features/network/network-recording/)\n</details>\n\n<details>\n<summary><b>Page Bundles</b></summary>\n<br>\n\nSave the current page and all its assets (CSS, JS, images, fonts) as a `.zip` bundle for offline viewing. Optionally inline everything into a single HTML file.\n\n```python\nawait tab.save_bundle('page.zip')\nawait tab.save_bundle('page-inline.zip', inline_assets=True)\n```\n\n[Screenshots, PDFs & Bundles Docs](https://pydoll.tech/docs/features/automation/screenshots-and-pdfs/)\n</details>\n\n<details>\n<summary><b>Shadow DOM Support</b></summary>\n<br>\n\nFull Shadow DOM support, including closed shadow roots. Because Pydoll operates at the CDP level (below JavaScript), the `closed` mode restriction doesn't apply.\n\n```python\nshadow = await element.get_shadow_root()\nbutton = await shadow.query('.internal-btn')\nawait button.click()\n\n# Discover all shadow roots on the page\nshadow_roots = await tab.find_shadow_roots()\nfor sr in shadow_roots:\n    checkbox = await sr.query('input[type=\"checkbox\"]', raise_exc=False)\n    if checkbox:\n        await checkbox.click()\n```\n\nHighlights:\n- Closed shadow roots work without workarounds\n- `find_shadow_roots()` discovers every shadow root on the page\n- `timeout` parameter for polling until shadow roots appear\n- `deep=True` traverses cross-origin iframes (OOPIFs)\n- Standard `find()`, `query()`, `click()` API inside shadow roots\n\n```python\n# Cloudflare Turnstile inside a cross-origin iframe\nshadow_roots = await tab.find_shadow_roots(deep=True, timeout=10)\nfor sr in shadow_roots:\n    checkbox = await sr.query('input[type=\"checkbox\"]', raise_exc=False)\n    if checkbox:\n        await checkbox.click()\n```\n\n[Shadow DOM Docs](https://pydoll.tech/docs/deep-dive/architecture/shadow-dom/)\n</details>\n\n<details>\n<summary><b>Humanized Mouse Movement</b></summary>\n<br>\n\nMouse operations produce human-like cursor movement by default:\n\n- **Bezier curve paths** with asymmetric control points\n- **Fitts's Law timing**: duration scales with distance\n- **Minimum-jerk velocity**: bell-shaped speed profile\n- **Physiological tremor**: Gaussian noise scaled with velocity\n- **Overshoot correction**: ~70% chance on fast movements, then corrects back\n\n```python\nawait tab.mouse.move(500, 300)\nawait tab.mouse.click(500, 300)\nawait tab.mouse.drag(100, 200, 500, 400)\n\nbutton = await tab.find(id='submit')\nawait button.click()\n\n# Opt out when speed matters\nawait tab.mouse.click(500, 300, humanize=False)\n```\n\n[Mouse Control Docs](https://pydoll.tech/docs/features/automation/mouse-control/)\n</details>\n\n## Getting Started\n\n```python\nimport asyncio\nfrom pydoll.browser import Chrome\nfrom pydoll.constants import Key\n\nasync def google_search(query: str):\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://www.google.com')\n\n        search_box = await tab.find(tag_name='textarea', name='q')\n        await search_box.insert_text(query)\n        await tab.keyboard.press(Key.ENTER)\n\n        first_result = await tab.find(\n            tag_name='h3',\n            text='autoscrape-labs/pydoll',\n            timeout=10,\n        )\n        await first_result.click()\n\n        await tab.find(id='repository-container-header', timeout=10)\n        print(f\"Page loaded: {await tab.title}\")\n\nasyncio.run(google_search('pydoll site:github.com'))\n```\n\n## Features\n\n<details>\n<summary><b>Hybrid Automation (UI + API)</b></summary>\n<br>\n\nUse UI automation to pass login flows (CAPTCHAs, JS challenges), then switch to `tab.request` for fast API calls that inherit the full browser session: cookies, headers, and all.\n\n```python\n# Log in via UI\nawait tab.go_to('https://my-site.com/login')\nawait (await tab.find(id='username')).type_text('user')\nawait (await tab.find(id='password')).type_text('pass123')\nawait (await tab.find(id='login-btn')).click()\n\n# Make authenticated API calls using the browser session\nresponse = await tab.request.get('https://my-site.com/api/user/profile')\nuser_data = response.json()\n```\n[Hybrid Automation Docs](https://pydoll.tech/docs/features/network/http-requests/)\n</details>\n\n<details>\n<summary><b>Network Interception and Monitoring</b></summary>\n<br>\n\nMonitor traffic for API discovery or intercept requests to block ads, trackers, and unnecessary resources.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.network.types import ErrorReason\n\nasync def block_images():\n    async with Chrome() as browser:\n        tab = await browser.start()\n\n        async def block_resource(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            resource_type = event['params']['resourceType']\n\n            if resource_type in ['Image', 'Stylesheet']:\n                await tab.fail_request(request_id, ErrorReason.BLOCKED_BY_CLIENT)\n            else:\n                await tab.continue_request(request_id)\n\n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, block_resource)\n\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        await tab.disable_fetch_events()\n\nasyncio.run(block_images())\n```\n[Network Monitoring](https://pydoll.tech/docs/features/network/monitoring/) | [Request Interception](https://pydoll.tech/docs/features/network/interception/)\n</details>\n\n<details>\n<summary><b>Browser Fingerprint Control</b></summary>\n<br>\n\nGranular control over [browser preferences](https://pydoll.tech/docs/features/configuration/browser-preferences/): hundreds of internal Chrome settings for building consistent fingerprints.\n\n```python\noptions = ChromiumOptions()\n\noptions.browser_preferences = {\n    'profile': {\n        'default_content_setting_values': {\n            'notifications': 2,\n            'geolocation': 2,\n        },\n        'password_manager_enabled': False\n    },\n    'intl': {\n        'accept_languages': 'en-US,en',\n    },\n    'browser': {\n        'check_default_browser': False,\n    }\n}\n```\n[Browser Preferences Guide](https://pydoll.tech/docs/features/configuration/browser-preferences/)\n</details>\n\n<details>\n<summary><b>Concurrency, Contexts and Remote Connections</b></summary>\n<br>\n\nManage [multiple tabs](https://pydoll.tech/docs/features/browser-management/tabs/) and [browser contexts](https://pydoll.tech/docs/features/browser-management/contexts/) (isolated sessions) concurrently. Connect to browsers running in Docker or remote servers.\n\n```python\nasync def scrape_page(url, tab):\n    await tab.go_to(url)\n    return await tab.title\n\nasync def concurrent_scraping():\n    async with Chrome() as browser:\n        tab_google = await browser.start()\n        tab_ddg = await browser.new_tab()\n\n        results = await asyncio.gather(\n            scrape_page('https://google.com/', tab_google),\n            scrape_page('https://duckduckgo.com/', tab_ddg)\n        )\n        print(results)\n```\n[Multi-Tab Management](https://pydoll.tech/docs/features/browser-management/tabs/) | [Remote Connections](https://pydoll.tech/docs/features/advanced/remote-connections/)\n</details>\n\n<details>\n<summary><b>Retry Decorator</b></summary>\n<br>\n\nThe `@retry` decorator supports custom recovery logic between attempts (e.g., refreshing the page, rotating proxies) and exponential backoff.\n\n```python\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound, NetworkError\n\n@retry(\n    max_retries=3,\n    exceptions=[ElementNotFound, NetworkError],\n    on_retry=my_recovery_function,\n    exponential_backoff=True\n)\nasync def scrape_product(self, url: str):\n    # scraping logic\n    ...\n```\n[Retry Decorator Docs](https://pydoll.tech/docs/features/advanced/decorators/)\n</details>\n\n---\n\n## Contributing\n\nContributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.\n\n## Support\n\nIf you find Pydoll useful, consider [sponsoring the project on GitHub](https://github.com/sponsors/thalissonvs).\n\n## License\n\n[MIT License](LICENSE)\n"
  },
  {
    "path": "README_zh.md",
    "content": "<p align=\"center\">\n    <img src=\"https://github.com/user-attachments/assets/219f2dbc-37ed-4aea-a289-ba39cdbb335d\" alt=\"Pydoll Logo\" /> <br>\n</p>\n<h1 align=\"center\">Pydoll: Automate the Web, Naturally</h1>\n\n<p align=\"center\">\n    <a href=\"https://github.com/autoscrape-labs/pydoll/stargazers\"><img src=\"https://img.shields.io/github/stars/autoscrape-labs/pydoll?style=social\"></a>\n    <a href=\"https://codecov.io/gh/autoscrape-labs/pydoll\" >\n        <img src=\"https://codecov.io/gh/autoscrape-labs/pydoll/graph/badge.svg?token=40I938OGM9\"/>\n    </a>\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/tests.yml/badge.svg\" alt=\"Tests\">\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/ruff-ci.yml/badge.svg\" alt=\"Ruff CI\">\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/mypy.yml/badge.svg\" alt=\"MyPy CI\">\n    <img src=\"https://img.shields.io/badge/python-%3E%3D3.10-blue\" alt=\"Python >= 3.10\">\n    <a href=\"https://deepwiki.com/autoscrape-labs/pydoll\"><img src=\"https://deepwiki.com/badge.svg\" alt=\"Ask DeepWiki\"></a>\n</p>\n\n\n<p align=\"center\">\n  📖 <a href=\"https://autoscrape-labs.github.io/pydoll/\">文档</a> •\n  🚀 <a href=\"#-getting-started\">快速上手</a> •\n  ⚡ <a href=\"#-advanced-features\">高级特性</a> •\n  🤝 <a href=\"#-contributing\">贡献</a> •\n  💖 <a href=\"#-support-my-work\">赞助我</a>\n</p>\n\n- [English](README.md)\n\n设想以下场景：你需要实现浏览器任务的自动化操作——无论是测试Web应用程序、从网站采集数据，还是批量处理重复性流程。传统方法往往需要配置外部驱动程序、进行复杂的系统设置，还可能面临诸多兼容性问题。\n\n**Pydoll的诞生就是解决这些问题!!!**\n\nPydoll 采用全新设计理念，从零构建，直接对接 Chrome DevTools Protocol（CDP），无需依赖外部驱动。 这种精简的实现方式，结合高度拟真的点击、导航及元素交互机制，使其行为与真实用户几乎毫无区别。\n\n我们坚信，真正强大的自动化工具，不应让用户困于繁琐的配置学习，也不该让用户疲于应对反爬系统的风控。使用Pydoll，你只需专注核心业务逻辑——让自动化回归本质，而非纠缠于底层技术细节或防护机制。\n\n<div>\n  <h4>做一个好人，给我们一个星星 ⭐</h4> \n    没有星星，就没有Bug修复。开玩笑的（也许）\n</div>\n\n## 🌟 Pydoll 的核心优势\n\n- **零 WebDriver 依赖**：彻底告别驱动兼容性烦恼\n- **类人交互引擎**：能够通过行为验证码如 reCAPTCHA v3 或 Turnstile，取决于 IP 声誉和交互模式\n- **异步高性能**：支持高速自动化与多任务并行处理\n- **拟真交互体验**：完美复刻真实用户行为模式\n- **极简部署**：安装即用，开箱即自动化\n\n## 最新功能\n\n### 类人页面滚动 —— 像真实用户一样滚动！\n\n现在你可以控制页面滚动，支持平滑动画并自动等待完成：\n\n```python\nfrom pydoll.constants import ScrollPosition\n\n# 带平滑动画向下滚动（等待完成）\nawait tab.scroll.by(ScrollPosition.DOWN, 500, smooth=True)\n\n# 导航至特定位置\nawait tab.scroll.to_bottom(smooth=True)\nawait tab.scroll.to_top(smooth=True)\n\n# 需要速度时的即时滚动\nawait tab.scroll.by(ScrollPosition.UP, 300, smooth=False)\n```\n\n不同于立即返回的 `execute_script(\"window.scrollBy(...)\")`，滚动API使用CDP的`awaitPromise`等待浏览器的`scrollend`事件，确保后续操作仅在滚动完全完成后执行。非常适合截取屏幕截图、加载延迟内容或创建真实的阅读模式。\n\n### 键盘 API —— 完全控制键盘输入\n\n全新的 `KeyboardAPI` 为页面级别的所有键盘交互提供了简洁、集中的接口：\n\n```python\nfrom pydoll.constants import Key\n\n# 按单个键\nawait tab.keyboard.press(Key.ENTER)\nawait tab.keyboard.press(Key.TAB)\n\n# 使用快捷键/组合键（最多3个键）\nawait tab.keyboard.hotkey(Key.CONTROL, Key.A)  # 全选（有效！）\nawait tab.keyboard.hotkey(Key.CONTROL, Key.C)  # 复制（有效！）\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.ARROWRIGHT)  # 向右选择单词\n\n# 复杂序列的手动控制\nawait tab.keyboard.down(Key.SHIFT)\nawait tab.keyboard.press(Key.ARROWRIGHT)  # 按住 Shift 选择文本\nawait tab.keyboard.up(Key.SHIFT)\n```\n\n**主要改进：**\n- **集中化**：所有键盘操作通过 `tab.keyboard` 访问\n- **智能修饰键检测**：快捷键自动检测并应用修饰键（Ctrl、Shift、Alt、Meta）\n- **完整按键支持**：26个字母（A-Z）、10个数字（0-9）、所有功能键、数字键盘和特殊键\n- **页面级快捷键**：适用于 Ctrl+C、Ctrl+V、Ctrl+A 等（由于 CDP 限制，浏览器 UI 快捷键不起作用）\n\n> **⚠️ CDP 限制：** 浏览器 UI 快捷键（如 Ctrl+T 打开新标签，F12 打开开发者工具）通过 CDP 无法使用。请改用 Pydoll 的方法：`await browser.new_tab()`、`await tab.close()`。\n\n### Retry 装饰器：生产级错误恢复\n\n使用 `@retry` 装饰器将脆弱的脚本转变为强大的生产级爬虫。通过指数退避和自定义恢复策略，自动从网络故障、超时和临时错误中恢复：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound, NetworkError\n\nclass ProductScraper:\n    def __init__(self):\n        self.tab = None\n        self.retry_count = 0\n    \n    # 在每次重试前执行的恢复回调\n    async def recover_from_failure(self):\n        self.retry_count += 1\n        print(f\"尝试 {self.retry_count} 失败。恢复中...\")\n        \n        # 刷新页面并恢复状态\n        if self.tab:\n            await self.tab.refresh()\n            await asyncio.sleep(2)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound, NetworkError],\n        on_retry=recover_from_failure,  # 执行恢复逻辑\n        delay=2.0,\n        exponential_backoff=True\n    )\n    async def scrape_product(self, url: str):\n        if not self.tab:\n            browser = Chrome()\n            self.tab = await browser.start()\n        \n        await self.tab.go_to(url)\n        title = await self.tab.find(class_name='product-title', timeout=5)\n        return await title.text\n```\n\n**强大功能：**\n- **智能重试逻辑**：仅对您定义的特定异常重试\n- **指数退避**：逐步增加等待时间（1秒 → 2秒 → 4秒 → 8秒）\n- **恢复回调**：在重试之间执行自定义逻辑（刷新页面、切换代理、重启浏览器）\n- **生产验证**：自信地处理真实世界爬虫的混乱情况\n\n非常适合处理速率限制、网络不稳定、动态内容加载和验证码检测。将不可靠的爬虫转变为防弹自动化。\n\n[**📖 完整文档**](https://pydoll.tech/docs/zh/features/advanced/decorators/)\n\n### 通过 WebSocket 进行远程连接 —— 随时随地控制浏览器！\n\n现在你可以使用浏览器的 WebSocket 地址直接连接到已运行的实例，并立即使用完整的 Pydoll API：\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nchrome = Chrome()\ntab = await chrome.connect('ws://YOUR_HOST:9222/devtools/browser/XXXX')\n\n# 直接开干：导航、元素自动化、请求、事件…\nawait tab.go_to('https://example.com')\ntitle = await tab.execute_script('return document.title')\nprint(title)\n```\n\n这让你可以轻松对接远程/CI 浏览器、容器或共享调试目标——无需本地启动，只需指向 WS 端点即可自动化。\n\n### 像专业人士一样漫游 DOM：get_children_elements() 与 get_siblings_elements()\n\n两个让复杂布局遍历更优雅的小助手：\n\n```python\n# 获取容器的直接子元素\ncontainer = await tab.find(id='cards')\ncards = await container.get_children_elements(max_depth=1)\n\n# 想更深入？这将返回子元素的子元素（以此类推）\nelements = await container.get_children_elements(max_depth=2) \n\n# 在横向列表中无痛遍历兄弟元素\nactive = await tab.find(class_name='item--active')\nsiblings = await active.get_siblings_elements()\n\nprint(len(cards), len(siblings))\n```\n\n用更少样板代码表达更多意图，特别适合动态网格、列表与菜单的场景，让抓取/自动化逻辑更清晰、更可读。\n\n### WebElement：状态等待与新的公共 API\n\n- 新增 `wait_until(...)` 用于等待元素状态，使用更简单：\n\n```python\n# 等待元素变为可见，直到超时\nawait element.wait_until(is_visible=True, timeout=5)\n\n# 等待元素变为可交互（可见、位于顶层并可接收事件）\nawait element.wait_until(is_interactable=True, timeout=10)\n```\n\n- 以下 `WebElement` 方法现已公开：\n  - `is_visible()`\n    - 判断元素是否具有可见区域、未被 CSS 隐藏，并在需要时滚动进入视口。适用于交互前的快速校验。\n  - `is_interactable()`\n    - “可点击”状态：综合可见性、启用状态与指针事件命中等条件，适合构建更稳健的交互流程。\n  - `is_on_top()`\n    - 检查元素在点击位置是否为顶部命中目标，避免被覆盖导致点击失效。\n  - `execute_script(script: str, return_by_value: bool = False)`\n    - 在元素上下文中执行 JavaScript（this 指向该元素），便于细粒度调整与快速检查。\n\n```python\n# 使用 JS 高亮元素\nawait element.execute_script(\"this.style.outline='2px solid #22d3ee'\")\n\n# 校验状态\nvisible = await element.is_visible()\ninteractable = await element.is_interactable()\non_top = await element.is_on_top()\n```\n\n以上新增能力能显著简化“等待+验证”场景，降低自动化过程中的不稳定性，使用例更可预测。\n\n### 浏览器上下文 HTTP 请求 - 混合自动化的游戏规则改变者！\n你是否曾经希望能够发出自动继承浏览器所有会话状态的 HTTP 请求？**现在你可以了！**<br>\n`tab.request` 属性为你提供了一个美观的 `requests` 风格接口，可在浏览器的 JavaScript 上下文中直接执行 HTTP 调用。这意味着每个请求都会自动获得 cookies、身份验证标头、CORS 策略和会话状态，就像浏览器本身发出请求一样。\n\n**混合自动化的完美选择：**\n```python\n# 使用 PyDoll 正常导航到网站并登录\nawait tab.go_to('https://example.com/login')\nawait (await tab.find(id='username')).type_text('user@example.com')\nawait (await tab.find(id='password')).type_text('password')\nawait (await tab.find(id='login-btn')).click()\n\n# 现在发出继承已登录会话的 API 调用！\nresponse = await tab.request.get('https://example.com/api/user/profile')\nuser_data = response.json()\n\n# 在保持身份验证的同时 POST 数据\nresponse = await tab.request.post(\n    'https://example.com/api/settings', \n    json={'theme': 'dark', 'notifications': True}\n)\n\n# 以不同格式访问响应内容\nraw_data = response.content\ntext_data = response.text\njson_data = response.json()\n\n# 检查设置的 cookies\nfor cookie in response.cookies:\n    print(f\"Cookie: {cookie['name']} = {cookie['value']}\")\n\n# 向你的请求添加自定义标头\nheaders = [\n    {'name': 'X-Custom-Header', 'value': 'my-value'},\n    {'name': 'X-API-Version', 'value': '2.0'}\n]\n\nawait tab.request.get('https://api.example.com/data', headers=headers)\n\n```\n\n**为什么这很棒：**\n- **无需会话切换** - 请求自动继承浏览器 cookies\n- **CORS 无缝工作** - 请求遵循浏览器安全策略  \n- **现代 SPA 的完美选择** - 无缝混合 UI 自动化与 API 调用\n- **身份验证变得简单** - 通过 UI 登录一次，然后调用 API\n- **混合工作流** - 为每个步骤使用最佳工具（UI 或 API）\n\n这为需要浏览器交互和 API 效率的自动化场景开启了令人难以置信的可能性！\n\n### 使用自定义首选项完全控制浏览器！(感谢 [@LucasAlvws](https://github.com/LucasAlvws))\n想要完全自定义 Chrome 的行为？**现在你可以控制一切！**<br>\n新的 `browser_preferences` 系统让你可以访问数百个之前无法通过编程方式更改的内部 Chrome 设置。我们说的是远超命令行标志的深度浏览器自定义！\n\n**可能性是无限的：**\n```python\noptions = ChromiumOptions()\n\n# 创建完美的自动化环境\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/tmp/downloads',\n        'prompt_for_download': False,\n        'directory_upgrade': True,\n        'extensions_to_open': ''  # 不自动打开任何下载\n    },\n    'profile': {\n        'default_content_setting_values': {\n            'notifications': 2,        # 阻止所有通知\n            'geolocation': 2,         # 阻止位置请求\n            'media_stream_camera': 2, # 阻止摄像头访问\n            'media_stream_mic': 2,    # 阻止麦克风访问\n            'popups': 1               # 允许弹窗（对自动化有用）\n        },\n        'password_manager_enabled': False,  # 禁用密码提示\n        'exit_type': 'Normal'              # 始终正常退出\n    },\n    'intl': {\n        'accept_languages': 'zh-CN,zh,en-US,en',\n        'charset_default': 'UTF-8'\n    },\n    'browser': {\n        'check_default_browser': False,    # 不询问默认浏览器\n        'show_update_promotion_infobar': False\n    }\n}\n\n# 或使用便捷的辅助方法\noptions.set_default_download_directory('/tmp/downloads')\noptions.set_accept_languages('zh-CN,zh,en-US,en')  \noptions.prompt_for_download = False\n```\n\n**实际应用的强大示例：**\n- **静默下载** - 无提示、无对话框，只有自动化下载\n- **阻止所有干扰** - 通知、弹窗、摄像头请求，应有尽有\n- **CI/CD 的完美选择** - 禁用更新检查、默认浏览器提示、崩溃报告\n- **多区域测试** - 即时更改语言、时区和区域设置\n- **安全加固** - 锁定权限并禁用不必要的功能\n- **高级指纹控制** - 修改浏览器安装日期、参与历史和行为模式\n\n**用于隐蔽自动化的指纹自定义：**\n```python\nimport time\n\n# 模拟一个已经存在几个月的浏览器\nfake_engagement_time = int(time.time()) - (7 * 24 * 60 * 60)  # 7天前\n\noptions.browser_preferences = {\n    'settings': {\n        'touchpad': {\n            'natural_scroll': True,\n        }\n    },\n    'profile': {\n        'last_engagement_time': fake_engagement_time,\n        'exit_type': 'Normal',\n        'exited_cleanly': True\n    },\n    'newtab_page_location_override': 'https://www.google.com',\n    'session': {\n        'restore_on_startup': 1,  # 恢复上次会话\n        'startup_urls': ['https://www.google.com']\n    }\n}\n```\n\n这种控制级别以前只有 Chrome 扩展开发者才能使用 - 现在它在你的自动化工具包中！\n\n查看[文档](https://pydoll.tech/docs/zh/features/#custom-browser-preferences/)了解更多详情。\n\n### 新的 `get_parent_element()` 方法\n检索任何 WebElement 的父元素，使导航 DOM 结构更加容易：\n```python\nelement = await tab.find(id='button')\nparent = await element.get_parent_element()\n```\n\n### 新的 start_timeout 选项 (感谢 [@j0j1j2](https://github.com/j0j1j2))\n添加到 ChromiumOptions 来控制浏览器启动可以花费多长时间。在较慢的机器或 CI 环境中很有用。\n\n```python\noptions = ChromiumOptions()\noptions.start_timeout = 20  # 等待 20 秒\n```\n\n### 新的 expect_download() 上下文管理器 —— 稳健、优雅的文件下载！\n还在为不稳定的下载流程、丢失的文件或混乱的事件监听而头疼吗？`tab.expect_download()` 来了：一种可靠、简洁的下载方式。\n\n- 自动配置浏览器下载行为\n- 支持自定义下载目录或临时目录（自动清理！）\n- 内置超时等待，防止任务卡住\n- 提供便捷句柄：读取字节/BASE64，获取 `file_path`\n\n一个“开箱即用”的小示例：\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser import Chrome\n\nasync def download_report():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/reports')\n\n        target_dir = Path('/tmp/my-downloads')\n        async with tab.expect_download(keep_file_at=target_dir, timeout=10) as dl:\n            # 触发页面上的下载（按钮/链接等）\n            await (await tab.find(text='Download latest report')).click()\n\n            # 等待完成并读取内容\n            data = await dl.read_bytes()\n            print(f\"已下载 {len(data)} 字节，保存至: {dl.file_path}\")\n\nasyncio.run(download_report())\n```\n\n想要“零成本清理”？不传 `keep_file_at` 即可——我们会创建临时目录，并在上下文退出后自动清理。对测试场景非常友好。\n\n## 📦 安装\n\n```bash\npip install pydoll-python\n```\n\n就这么简单！安装即用，马上开始自动化\n\n## 🚀 快速上手\n\n### 你的第一个自动化任务\n\n让我们从一个实际例子开始：一个自动执行谷歌搜索并点击第一个结果的自动化流程。通过这个示例，你可以了解该库的工作原理，以及如何开始将日常任务自动化。\n\n```python\nimport asyncio\n\nfrom pydoll.browser import Chrome\nfrom pydoll.constants import Key\n\nasync def google_search(query: str):\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://www.google.com')\n        search_box = await tab.find(tag_name='textarea', name='q')\n        await search_box.insert_text(query)\n        await tab.keyboard.press(Key.ENTER)\n        await (await tab.find(\n            tag_name='h3',\n            text='autoscrape-labs/pydoll',\n            timeout=10,\n        )).click()\n        await tab.find(id='repository-container-header', timeout=10)\n\nasyncio.run(google_search('pydoll site:github.com'))\n```\n\n无需任何配置，只需一个简单脚本，我们就能完成一次完整的谷歌搜索！\n好了，现在让我们看看如何从网页中提取数据，依然沿用之前的示例。\n\n假设在以下代码中，我们已经进入了 Pydoll 项目页面。我们需要提取以下信息：\n\n- 项目描述\n- 星标数量\n- Fork 数量\n- Issue 数量\n- Pull Request 数量\n如果想要获取项目描述，我们将使用 XPath 查询。你可以查阅相关文档，学习如何构建自己的查询语句。\n\n```python\ndescription = await (await tab.query(\n    '//h2[contains(text(), \"About\")]/following-sibling::p',\n    timeout=10,\n)).text\n```\n\n下面让我们来理解这条查询语句的作用：\n\n1. `//h2[contains(text(), \"About\")]` - 选择第一个包含\"About\"的 `<h2>` 标签\n2. `/following-sibling::p` - 选择第一个在`<h2>` 标签之后的`<p>`标签\n\n然后你可以获取到剩下的数据：\n\n```python\nnumber_of_stars = await (await tab.find(\n    id='repo-stars-counter-star'\n)).text\n\nnumber_of_forks = await (await tab.find(\n    id='repo-network-counter'\n)).text\nnumber_of_issues = await (await tab.find(\n    id='issues-repo-tab-count',\n)).text\nnumber_of_pull_requests = await (await tab.find(\n    id='pull-requests-repo-tab-count',\n)).text\n\ndata = {\n    'description': description,\n    'number_of_stars': number_of_stars,\n    'number_of_forks': number_of_forks,\n    'number_of_issues': number_of_issues,\n    'number_of_pull_requests': number_of_pull_requests,\n}\nprint(data)\n\n```\n\n下图展示了本次自动化任务的执行速度与结果。\n（为演示需要，浏览器界面未显示。）\n\n![google_seach](./docs/images/google-search-example.gif)\n\n\n短短5秒内，我们就成功提取了所需数据！  \n这就是使用Pydoll进行自动化所能达到的速度。\n\n### 更多复杂的例子\n\n接下来我们来看一个你可能经常遇到的场景：类似Cloudflare的验证码防护。  \nPydoll提供了相应的处理方法，但需要说明的是，正如前文所述，其有效性会受到多种因素影响。  \n下面的代码展示了一个完整的Cloudflare验证码处理示例。\n\n```python\nimport asyncio\n\nfrom pydoll.browser import Chrome\nfrom pydoll.constants import By\n\nasync def cloudflare_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        async with tab.expect_and_bypass_cloudflare_captcha():\n            await tab.go_to('https://2captcha.com/demo/cloudflare-turnstile')\n        print('Captcha handled, continuing...')\n        await asyncio.sleep(5)  # just to see the result :)\n\nasyncio.run(cloudflare_example())\n\n```\n\n执行结果如下：\n\n![cloudflare_example](./docs/images/cloudflare-example.gif)\n\n\n仅需数行代码，我们就成功攻克了最棘手的验证码防护之一。\n而这仅仅是Pydoll所提供的众多强大功能之一。但这还远不是全部！\n\n\n### 自定义配置\n\n有时我们需要对浏览器进行更精细的控制。Pydoll提供了灵活的配置方式来实现这一点。下面我们来看具体示例：\n\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions as Options\n\nasync def custom_automation():\n    # Configure browser options\n    options = Options()\n    options.add_argument('--proxy-server=username:password@ip:port')\n    options.add_argument('--window-size=1920,1080')\n    options.binary_location = '/path/to/your/browser'\n    options.start_timeout = 20\n\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        # Your automation code here\n        await tab.go_to('https://example.com')\n        # The browser is now using your custom settings\n\nasyncio.run(custom_automation())\n```\n\n本示例中，我们配置浏览器使用代理服务器，并设置窗口分辨率为1920x1080。此外，还指定了Chrome二进制文件的自定义路径——适用于您的安装位置与常规默认路径不同的情况。\n\n## ⚡ 高级功能\n\nPydoll提供了一系列高级特性满足高端玩家的需求。\n\n\n### 高级元素定位\n\n我们提供多种页面元素定位方式。无论您偏好那种方法，都能找到适合您的解决方案：\n\n```python\nimport asyncio\nfrom pydoll.browser import Chrome\n\nasync def element_finding_examples():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n\n        # Find by attributes (most intuitive)\n        submit_btn = await tab.find(\n            tag_name='button',\n            class_name='btn-primary',\n            text='Submit'\n        )\n        # Find by ID\n        username_field = await tab.find(id='username')\n        # Find multiple elements\n        all_links = await tab.find(tag_name='a', find_all=True)\n        # CSS selectors and XPath\n        nav_menu = await tab.query('nav.main-menu')\n        specific_item = await tab.query('//div[@data-testid=\"item-123\"]')\n        # With timeout and error handling\n        delayed_element = await tab.find(\n            class_name='dynamic-content',\n            timeout=10,\n            raise_exc=False  # Returns None if not found\n        )\n        # Advanced: Custom attributes\n        custom_element = await tab.find(\n            data_testid='submit-button',\n            aria_label='Submit form'\n        )\n\nasyncio.run(element_finding_examples())\n```\n\nfind 方法更为友好。我们可以通过常见属性（如 id、tag_name、class_name 等）进行元素查找，甚至支持自定义属性（例如 data-testid）。\n\n如果这些基础方式仍不能满足需求，还可使用 query 方法，通过 CSS 选择器、XPath 查询语句等多种方式进行元素定位。Pydoll 会自动识别当前使用的查询类型。\n\n### 并发自动化\n\nPydoll 的一大优势在于其基于异步实现的多任务并行处理能力。我们可以同时自动化操作多个浏览器标签页！下面来看具体示例：\n\n```python\nimport asyncio\nfrom pydoll.browser import Chrome\n\nasync def scrape_page(url, tab):\n    await tab.go_to(url)\n    title = await tab.execute_script('return document.title')\n    links = await tab.find(tag_name='a', find_all=True)\n    return {\n        'url': url,\n        'title': title,\n        'link_count': len(links)\n    }\n\nasync def concurrent_scraping():\n    browser = Chrome()\n    tab_google = await browser.start()\n    tab_duckduckgo = await browser.new_tab()\n    tasks = [\n        scrape_page('https://google.com/', tab_google),\n        scrape_page('https://duckduckgo.com/', tab_duckduckgo)\n    ]\n    results = await asyncio.gather(*tasks)\n    print(results)\n    await browser.stop()\n\nasyncio.run(concurrent_scraping())\n```\n\n下方展示令人惊叹的执行速度：\n\n![concurrent_example](./docs/images/concurrent-example.gif)\n\n\n这个例子,我们成功实现了同时对两个页面的数据提取.\n还有更多强大功能！响应式自动化的事件系统、请求拦截与修改等等。赶快查阅文档!\n\n## 🔧 快速问题排查\n\n**找不到浏览器？**\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.binary_location = '/path/to/your/chrome'\nbrowser = Chrome(options=options)\n```\n\n**浏览器在 FailedToStartBrowser 错误后启动？**\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.start_timeout = 20  # 默认是 10 秒\n\nbrowser = Chrome(options=options)\n```\n\n**需要代理？**\n```python\noptions.add_argument('--proxy-server=your-proxy:port')\n```\n\n**在 Docker 中运行？**\n```python\noptions.add_argument('--no-sandbox')\noptions.add_argument('--disable-dev-shm-usage')\n```\n\n## 📚 文档\n\nPydoll 的完整文档、详细示例以及对所有功能的深入探讨可以通过以下链接访问： [官方文档](https://autoscrape-labs.github.io/pydoll/).\n\n文档包含以下部分:\n- **快速上手指南** - 分步教程\n- **API 参考** - 完整的方法文档\n- **高级技巧** - 网络拦截、事件处理、性能优化\n\n>此 README 的中文版本在[这里](README_zh.md)。\n\n## 🤝 贡献\n\n我们很乐意看到您的帮助让 Pydoll 变得更好！查看我们的[贡献指南](CONTRIBUTING.md)开始贡献。无论是修复错误、添加功能还是改进文档 - 所有贡献都受欢迎！\n\n请确保：\n- 为新功能或错误修复编写测试\n- 遵循代码风格和约定\n- 对拉取请求使用约定式提交\n- 在提交前运行 lint 检查和测试\n\n## 💖 支持我的工作\n\n如果您发现 Pydoll 有用，请考虑[在 GitHub 上支持我](https://github.com/sponsors/thalissonvs)。  \n您将获得独家优惠，如优先支持、自定义功能等等！\n\n现在无法赞助？没问题，您仍然可以通过以下方式提供很大帮助：\n- 为仓库加星\n- 在社交媒体上分享\n- 撰写文章或教程\n- 提供反馈或报告问题\n\n每一点支持都很重要/\n\n## 💬 传播消息\n\n如果 Pydoll 为您节省了时间、心理健康或者拯救了一个键盘免于被砸，请给它一个 ⭐，分享它，或者告诉您奇怪的开发者朋友。\n\n## 📄 许可证\n\nPydoll 在 [MIT 许可证](LICENSE) 下获得许可。\n\n<p align=\"center\">\n  <b>Pydoll</b> — 让浏览器自动化变得神奇！\n</p>\n"
  },
  {
    "path": "SPONSORS.md",
    "content": "# Sponsors\n\nPydoll is supported by these amazing sponsors. Their contributions help keep the project maintained and growing.\n\n## Top Sponsors\n\n<a href=\"https://substack.thewebscraping.club/p/pydoll-webdriver-scraping?utm_source=github&utm_medium=repo&utm_campaign=pydoll\">\n<img alt=\"The Web Scraping Club\" src=\"public/images/banner-the-webscraping-club.png\" />\n</a>\n\nRead a full review of Pydoll on **[The Web Scraping Club](https://substack.thewebscraping.club/p/pydoll-webdriver-scraping?utm_source=github&utm_medium=repo&utm_campaign=pydoll)**, the #1 newsletter dedicated to web scraping.\n\n---\n\n## Sponsors\n\n<a href=\"https://www.thordata.com/?ls=github&lk=pydoll\">\n<img alt=\"Thordata\" src=\"public/images/thordata.png\" />\n</a>\n\nPydoll is proudly sponsored by **[Thordata](https://www.thordata.com/?ls=github&lk=pydoll)**: a residential proxy network built for serious web scraping and automation. With **190+ real residential and ISP locations**, fully encrypted connections, and infrastructure optimized for high-performance workflows, Thordata is an excellent choice for scaling your Pydoll automations.\n\n**[Sign up through our link](https://www.thordata.com/?ls=github&lk=pydoll)** to support the project and get **1GB free** to get started.\n\n---\n\n<a href=\"https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc\">\n<img alt=\"CapSolver\" src=\"public/images/capsolver.jpeg\" />\n</a>\n\nPydoll excels at behavioral evasion, but it doesn't solve captchas. That's where **[CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc)** comes in. An AI-powered service that handles reCAPTCHA, Cloudflare challenges, and more, seamlessly integrating with your automation workflows.\n\n**[Register with our invite code](https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc)** and use code **PYDOLL** to get an extra **6% balance bonus**.\n\n---\n\nInterested in sponsoring Pydoll? [Become a sponsor](https://github.com/sponsors/thalissonvs).\n"
  },
  {
    "path": "codecov.yml",
    "content": "coverage:\n  status:\n    project: \n      default:\n        target: 90%\n        threshold: 0%\n        base: auto "
  },
  {
    "path": "cz.yaml",
    "content": "---\ncommitizen:\n  name: cz_conventional_commits\n  tag_format: $version\n  version: 2.21.3\n"
  },
  {
    "path": "docs/en/api/browser/chrome.md",
    "content": "# Chrome Browser\n \n::: pydoll.browser.chromium.Chrome\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2 "
  },
  {
    "path": "docs/en/api/browser/edge.md",
    "content": "# Edge Browser\n \n::: pydoll.browser.chromium.Edge\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2 "
  },
  {
    "path": "docs/en/api/browser/managers.md",
    "content": "# Browser Managers\n\nThe managers module provides specialized classes for managing different aspects of browser lifecycle and configuration.\n\n## Overview\n\nBrowser managers handle specific responsibilities in browser automation:\n\n::: pydoll.browser.managers\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Manager Classes\n\n### Browser Process Manager\nManages the browser process lifecycle, including starting, stopping, and monitoring browser processes.\n\n::: pydoll.browser.managers.browser_process_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n### Browser Options Manager\nHandles browser configuration options and command-line arguments.\n\n::: pydoll.browser.managers.browser_options_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n### Proxy Manager\nManages proxy configuration and authentication for browser instances.\n\n::: pydoll.browser.managers.proxy_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n### Temporary Directory Manager\nHandles creation and cleanup of temporary directories used by browser instances.\n\n::: pydoll.browser.managers.temp_dir_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## Usage\n\nManagers are typically used internally by browser classes like `Chrome` and `Edge`. They provide modular functionality that can be composed together:\n\n```python\nfrom pydoll.browser.managers.proxy_manager import ProxyManager\nfrom pydoll.browser.managers.temp_dir_manager import TempDirManager\n\n# Managers are used internally by browser classes\n# Direct usage is for advanced scenarios only\nproxy_manager = ProxyManager()\ntemp_manager = TempDirManager()\n```\n\n!!! note \"Internal Usage\"\n    These managers are primarily used internally by the browser classes. Direct usage is recommended only for advanced scenarios or when extending the library. "
  },
  {
    "path": "docs/en/api/browser/options.md",
    "content": "# Browser Options\n\n## ChromiumOptions\n\n::: pydoll.browser.options.ChromiumOptions\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## Options Interface\n\n::: pydoll.browser.interfaces.Options\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## BrowserOptionsManager Interface\n\n::: pydoll.browser.interfaces.BrowserOptionsManager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3 "
  },
  {
    "path": "docs/en/api/browser/requests.md",
    "content": "# Browser Requests\n\nThe requests module provides HTTP request capabilities within the browser context, enabling seamless API calls that inherit the browser's session state, cookies, and authentication.\n\n## Overview\n\nThe browser requests module offers a `requests`-like interface for making HTTP calls directly within the browser's JavaScript context. This approach provides several advantages over traditional HTTP libraries:\n\n- **Session inheritance**: Automatic cookie, authentication, and CORS handling\n- **Browser context**: Requests execute in the same security context as the page\n- **No session juggling**: Eliminate the need to transfer cookies and tokens between automation and API calls\n- **SPA compatibility**: Perfect for Single Page Applications with complex authentication flows\n\n## Request Class\n\nThe main interface for making HTTP requests within the browser context.\n\n::: pydoll.browser.requests.request.Request\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n      group_by_category: true\n      members_order: source\n      filters:\n        - \"!^__\"\n\n## Response Class\n\nRepresents the response from HTTP requests, providing a familiar interface similar to the `requests` library.\n\n::: pydoll.browser.requests.response.Response\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n      group_by_category: true\n      members_order: source\n      filters:\n        - \"!^__\"\n\n## Usage Examples\n\n### Basic HTTP Methods\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to(\"https://api.example.com\")\n    \n    # GET request\n    response = await tab.request.get(\"/users/123\")\n    user_data = await response.json()\n    \n    # POST request\n    response = await tab.request.post(\"/users\", json={\n        \"name\": \"John Doe\",\n        \"email\": \"john@example.com\"\n    })\n    \n    # PUT request with headers\n    response = await tab.request.put(\"/users/123\", \n        json={\"name\": \"Jane Doe\"},\n        headers={\"Authorization\": \"Bearer token123\"}\n    )\n```\n\n### Response Handling\n\n```python\n# Check response status\nif response.ok:\n    print(f\"Success: {response.status_code}\")\nelse:\n    print(f\"Error: {response.status_code}\")\n    response.raise_for_status()  # Raises HTTPError for 4xx/5xx\n\n# Access response data\ntext_data = response.text\njson_data = await response.json()\nraw_bytes = response.content\n\n# Inspect headers and cookies\nprint(\"Response headers:\", response.headers)\nprint(\"Request headers:\", response.request_headers)\nfor cookie in response.cookies:\n    print(f\"Cookie: {cookie.name}={cookie.value}\")\n```\n\n### Advanced Features\n\n```python\n# Request with custom headers and parameters\nresponse = await tab.request.get(\"/search\", \n    params={\"q\": \"python\", \"limit\": 10},\n    headers={\n        \"User-Agent\": \"Custom Bot 1.0\",\n        \"Accept\": \"application/json\"\n    }\n)\n\n# File upload simulation\nresponse = await tab.request.post(\"/upload\",\n    data={\"description\": \"Test file\"},\n    files={\"file\": (\"test.txt\", \"file content\", \"text/plain\")}\n)\n\n# Form data submission\nresponse = await tab.request.post(\"/login\",\n    data={\"username\": \"user\", \"password\": \"pass\"}\n)\n```\n\n## Integration with Tab\n\nThe request functionality is accessed through the `tab.request` property, which provides a singleton `Request` instance for each tab:\n\n```python\n# Each tab has its own request instance\ntab1 = await browser.get_tab(0)\ntab2 = await browser.new_tab()\n\n# These are separate Request instances\nrequest1 = tab1.request  # Request bound to tab1\nrequest2 = tab2.request  # Request bound to tab2\n\n# Requests inherit the tab's context\nawait tab1.go_to(\"https://site1.com\")\nawait tab2.go_to(\"https://site2.com\")\n\n# These requests will have different cookie/session contexts\nresponse1 = await tab1.request.get(\"/api/data\")  # Uses site1.com cookies\nresponse2 = await tab2.request.get(\"/api/data\")  # Uses site2.com cookies\n```\n\n!!! tip \"Hybrid Automation\"\n    This module is particularly powerful for hybrid automation scenarios where you need to combine UI interactions with API calls. For example, log in through the UI, then use the authenticated session for API calls without manually handling cookies or tokens."
  },
  {
    "path": "docs/en/api/browser/tab.md",
    "content": "# Tab\n\n::: pydoll.browser.tab.Tab\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/en/api/commands/browser.md",
    "content": "# Browser Commands\n\nBrowser commands provide low-level control over browser instances and their configuration.\n\n## Overview\n\nThe browser commands module handles browser-level operations such as version information, target management, and browser-wide settings.\n\n::: pydoll.commands.browser_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Usage\n\nBrowser commands are typically used internally by browser classes to manage browser instances:\n\n```python\nfrom pydoll.commands.browser_commands import get_version\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Get browser version information\nconnection = ConnectionHandler()\nversion_info = await get_version(connection)\n```\n\n## Available Commands\n\nThe browser commands module provides functions for:\n\n- Getting browser version and user agent information\n- Managing browser targets (tabs, windows)\n- Controlling browser-wide settings and permissions\n- Handling browser lifecycle events\n\n!!! note \"Internal Usage\"\n    These commands are primarily used internally by the `Chrome` and `Edge` browser classes. Direct usage is recommended only for advanced scenarios. "
  },
  {
    "path": "docs/en/api/commands/dom.md",
    "content": "# DOM Commands\n\nDOM commands provide comprehensive functionality for interacting with the Document Object Model of web pages.\n\n## Overview\n\nThe DOM commands module is one of the most important modules in Pydoll, providing all the functionality needed to find, interact with, and manipulate HTML elements on web pages.\n\n::: pydoll.commands.dom_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Usage\n\nDOM commands are used extensively by the `WebElement` class and element finding methods:\n\n```python\nfrom pydoll.commands.dom_commands import query_selector, get_attributes\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Find element and get its attributes\nconnection = ConnectionHandler()\nnode_id = await query_selector(connection, selector=\"#username\")\nattributes = await get_attributes(connection, node_id=node_id)\n```\n\n## Key Functionality\n\nThe DOM commands module provides functions for:\n\n### Element Finding\n- `query_selector()` - Find single element by CSS selector\n- `query_selector_all()` - Find multiple elements by CSS selector\n- `get_document()` - Get the document root node\n\n### Element Interaction\n- `click_element()` - Click on elements\n- `focus_element()` - Focus elements\n- `set_attribute_value()` - Set element attributes\n- `get_attributes()` - Get element attributes\n\n### Element Information\n- `get_box_model()` - Get element positioning and dimensions\n- `describe_node()` - Get detailed element information\n- `get_outer_html()` - Get element HTML content\n\n### DOM Manipulation\n- `remove_node()` - Remove elements from DOM\n- `set_node_value()` - Set element values\n- `request_child_nodes()` - Get child elements\n\n!!! tip \"High-Level APIs\"\n    While these commands provide powerful low-level access, most users should use the higher-level `WebElement` class methods like `click()`, `type_text()`, and `get_attribute()` which use these commands internally. "
  },
  {
    "path": "docs/en/api/commands/fetch.md",
    "content": "# Fetch Commands\n\nFetch commands provide advanced network request handling and interception capabilities using the Fetch API domain.\n\n## Overview\n\nThe fetch commands module enables sophisticated network request management, including request modification, response interception, and authentication handling.\n\n::: pydoll.commands.fetch_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Usage\n\nFetch commands are used for advanced network interception and request handling:\n\n```python\nfrom pydoll.commands.fetch_commands import enable, request_paused, continue_request\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Enable fetch domain\nconnection = ConnectionHandler()\nawait enable(connection, patterns=[{\n    \"urlPattern\": \"*\",\n    \"requestStage\": \"Request\"\n}])\n\n# Handle paused requests\nasync def handle_paused_request(request_id, request):\n    # Modify request or continue as-is\n    await continue_request(connection, request_id=request_id)\n```\n\n## Key Functionality\n\nThe fetch commands module provides functions for:\n\n### Request Interception\n- `enable()` - Enable fetch domain with patterns\n- `disable()` - Disable fetch domain\n- `continue_request()` - Continue intercepted requests\n- `fail_request()` - Fail requests with specific errors\n\n### Request Modification\n- Modify request headers\n- Change request URLs\n- Alter request methods (GET, POST, etc.)\n- Modify request bodies\n\n### Response Handling\n- `fulfill_request()` - Provide custom responses\n- `get_response_body()` - Get response content\n- Response header modification\n- Response status code control\n\n### Authentication\n- `continue_with_auth()` - Handle authentication challenges\n- Basic authentication support\n- Custom authentication flows\n\n## Advanced Features\n\n### Pattern-Based Interception\n```python\n# Intercept specific URL patterns\npatterns = [\n    {\"urlPattern\": \"*/api/*\", \"requestStage\": \"Request\"},\n    {\"urlPattern\": \"*.js\", \"requestStage\": \"Response\"},\n    {\"urlPattern\": \"https://example.com/*\", \"requestStage\": \"Request\"}\n]\n\nawait enable(connection, patterns=patterns)\n```\n\n### Request Modification\n```python\n# Modify intercepted requests\nasync def modify_request(request_id, request):\n    # Add authentication header\n    headers = request.headers.copy()\n    headers[\"Authorization\"] = \"Bearer token123\"\n    \n    # Continue with modified headers\n    await continue_request(\n        connection,\n        request_id=request_id,\n        headers=headers\n    )\n```\n\n### Response Mocking\n```python\n# Mock API responses\nawait fulfill_request(\n    connection,\n    request_id=request_id,\n    response_code=200,\n    response_headers=[\n        {\"name\": \"Content-Type\", \"value\": \"application/json\"},\n        {\"name\": \"Access-Control-Allow-Origin\", \"value\": \"*\"}\n    ],\n    body='{\"status\": \"success\", \"data\": {\"mocked\": true}}'\n)\n```\n\n### Authentication Handling\n```python\n# Handle authentication challenges\nawait continue_with_auth(\n    connection,\n    request_id=request_id,\n    auth_challenge_response={\n        \"response\": \"ProvideCredentials\",\n        \"username\": \"user\",\n        \"password\": \"pass\"\n    }\n)\n```\n\n## Request Stages\n\nFetch commands can intercept requests at different stages:\n\n| Stage | Description | Use Cases |\n|-------|-------------|-----------|\n| Request | Before request is sent | Modify headers, URL, method |\n| Response | After response received | Mock responses, modify content |\n\n## Error Handling\n\n```python\n# Fail requests with specific errors\nawait fail_request(\n    connection,\n    request_id=request_id,\n    error_reason=\"ConnectionRefused\"  # or \"AccessDenied\", \"TimedOut\", etc.\n)\n```\n\n## Integration with Network Commands\n\nFetch commands work alongside network commands but provide more granular control:\n\n- **Network Commands**: Broader network monitoring and control\n- **Fetch Commands**: Specific request/response interception and modification\n\n!!! tip \"Performance Considerations\"\n    Fetch interception can impact page loading performance. Use specific URL patterns and disable when not needed to minimize overhead. "
  },
  {
    "path": "docs/en/api/commands/index.md",
    "content": "# Commands Overview\n\nThe Commands module provides high-level interfaces for interacting with Chrome DevTools Protocol (CDP) domains. Each command module corresponds to a specific CDP domain and provides methods to execute various browser operations.\n\n## Available Command Modules\n\n### Browser Commands\n- **Module**: `browser_commands.py`\n- **Purpose**: Browser-level operations and window management\n- **Documentation**: [Browser Commands](browser.md)\n\n### DOM Commands\n- **Module**: `dom_commands.py`\n- **Purpose**: DOM tree manipulation and element operations\n- **Documentation**: [DOM Commands](dom.md)\n\n### Input Commands\n- **Module**: `input_commands.py`\n- **Purpose**: Input event simulation (keyboard, mouse, touch)\n- **Documentation**: [Input Commands](input.md)\n\n### Network Commands\n- **Module**: `network_commands.py`\n- **Purpose**: Network monitoring and request interception\n- **Documentation**: [Network Commands](network.md)\n\n### Page Commands\n- **Module**: `page_commands.py`\n- **Purpose**: Page lifecycle management and navigation\n- **Documentation**: [Page Commands](page.md)\n\n### Runtime Commands\n- **Module**: `runtime_commands.py`\n- **Purpose**: JavaScript execution and runtime management\n- **Documentation**: [Runtime Commands](runtime.md)\n\n### Storage Commands\n- **Module**: `storage_commands.py`\n- **Purpose**: Browser storage access (cookies, local storage, etc.)\n- **Documentation**: [Storage Commands](storage.md)\n\n### Target Commands\n- **Module**: `target_commands.py`\n- **Purpose**: Target management and tab operations\n- **Documentation**: [Target Commands](target.md)\n\n### Fetch Commands\n- **Module**: `fetch_commands.py`\n- **Purpose**: Network request interception and modification\n- **Documentation**: [Fetch Commands](fetch.md)\n\n## Usage Pattern\n\nCommands are typically accessed through the browser or tab instances:\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\n# Initialize browser\nbrowser = Chrome()\nawait browser.start()\n\n# Get active tab\ntab = await browser.get_active_tab()\n\n# Use commands through the tab\nawait tab.navigate(\"https://example.com\")\nelement = await tab.find(id=\"button\")\nawait element.click()\n```\n\n## Command Structure\n\nEach command module follows a consistent pattern:\n- **Static methods**: For direct command execution\n- **Type hints**: Full type safety with protocol types\n- **Error handling**: Proper exception handling for CDP errors\n- **Documentation**: Comprehensive docstrings with examples "
  },
  {
    "path": "docs/en/api/commands/input.md",
    "content": "# Input Commands\n\nInput commands handle mouse and keyboard interactions, providing human-like input simulation.\n\n## Overview\n\nThe input commands module provides functionality for simulating user input including mouse movements, clicks, keyboard typing, and key presses.\n\n::: pydoll.commands.input_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Usage\n\nInput commands are used by element interaction methods and can be used directly for advanced input scenarios:\n\n```python\nfrom pydoll.commands.input_commands import dispatch_mouse_event, dispatch_key_event\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Simulate mouse click\nconnection = ConnectionHandler()\nawait dispatch_mouse_event(\n    connection, \n    type=\"mousePressed\", \n    x=100, \n    y=200, \n    button=\"left\"\n)\n\n# Simulate keyboard typing\nawait dispatch_key_event(\n    connection,\n    type=\"keyDown\",\n    key=\"Enter\"\n)\n```\n\n## Key Functionality\n\nThe input commands module provides functions for:\n\n### Mouse Events\n- `dispatch_mouse_event()` - Mouse clicks, movements, and wheel events\n- Mouse button states (left, right, middle)\n- Coordinate-based positioning\n- Drag and drop operations\n\n### Keyboard Events\n- `dispatch_key_event()` - Key press and release events\n- `insert_text()` - Direct text insertion\n- Special key handling (Enter, Tab, Arrow keys, etc.)\n- Modifier keys (Ctrl, Alt, Shift)\n\n### Touch Events\n- Touch screen simulation\n- Multi-touch gestures\n- Touch coordinates and pressure\n\n## Human-like Behavior\n\nThe input commands support human-like behavior patterns:\n\n- Natural mouse movement curves\n- Realistic typing speeds and patterns\n- Random micro-delays between actions\n- Pressure-sensitive touch events\n\n!!! tip \"Element Methods\"\n    For most use cases, use the higher-level element methods like `element.click()` and `element.type_text()` which provide a more convenient API and handle common scenarios automatically. "
  },
  {
    "path": "docs/en/api/commands/network.md",
    "content": "# Network Commands\n\nNetwork commands provide comprehensive control over network requests, responses, and browser networking behavior.\n\n## Overview\n\nThe network commands module enables request interception, response modification, cookie management, and network monitoring capabilities.\n\n::: pydoll.commands.network_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Usage\n\nNetwork commands are used for advanced scenarios like request interception and network monitoring:\n\n```python\nfrom pydoll.commands.network_commands import enable, set_request_interception\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Enable network monitoring\nconnection = ConnectionHandler()\nawait enable(connection)\n\n# Enable request interception\nawait set_request_interception(connection, patterns=[{\"urlPattern\": \"*\"}])\n```\n\n## Key Functionality\n\nThe network commands module provides functions for:\n\n### Request Management\n- `enable()` / `disable()` - Enable/disable network monitoring\n- `set_request_interception()` - Intercept and modify requests\n- `continue_intercepted_request()` - Continue or modify intercepted requests\n- `get_request_post_data()` - Get request body data\n\n### Response Handling\n- `get_response_body()` - Get response content\n- `fulfill_request()` - Provide custom responses\n- `fail_request()` - Simulate network failures\n\n### Cookie Management\n- `get_cookies()` - Get browser cookies\n- `set_cookies()` - Set browser cookies\n- `delete_cookies()` - Delete specific cookies\n- `clear_browser_cookies()` - Clear all cookies\n\n### Cache Control\n- `clear_browser_cache()` - Clear browser cache\n- `set_cache_disabled()` - Disable browser cache\n- `get_response_body_for_interception()` - Get cached responses\n\n### Security & Headers\n- `set_user_agent_override()` - Override user agent\n- `set_extra_http_headers()` - Add custom headers\n- `emulate_network_conditions()` - Simulate network conditions\n\n## Advanced Use Cases\n\n### Request Interception\n```python\n# Intercept and modify requests\nawait set_request_interception(connection, patterns=[\n    {\"urlPattern\": \"*/api/*\", \"requestStage\": \"Request\"}\n])\n\n# Handle intercepted request\nasync def handle_request(request):\n    if \"api/login\" in request.url:\n        # Modify request headers\n        headers = request.headers.copy()\n        headers[\"Authorization\"] = \"Bearer token\"\n        await continue_intercepted_request(\n            connection, \n            request_id=request.request_id,\n            headers=headers\n        )\n```\n\n### Response Mocking\n```python\n# Mock API responses\nawait fulfill_request(\n    connection,\n    request_id=request_id,\n    response_code=200,\n    response_headers={\"Content-Type\": \"application/json\"},\n    body='{\"status\": \"success\"}'\n)\n```\n\n!!! warning \"Performance Impact\"\n    Network interception can impact page loading performance. Use selectively and disable when not needed. "
  },
  {
    "path": "docs/en/api/commands/page.md",
    "content": "# Page Commands\n\nPage commands handle page navigation, lifecycle events, and page-level operations.\n\n## Overview\n\nThe page commands module provides functionality for navigating between pages, managing page lifecycle, handling JavaScript execution, and controlling page behavior.\n\n::: pydoll.commands.page_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Usage\n\nPage commands are used extensively by the `Tab` class for navigation and page management:\n\n```python\nfrom pydoll.commands.page_commands import navigate, reload, enable\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Navigate to a URL\nconnection = ConnectionHandler()\nawait enable(connection)  # Enable page events\nawait navigate(connection, url=\"https://example.com\")\n\n# Reload the page\nawait reload(connection)\n```\n\n## Key Functionality\n\nThe page commands module provides functions for:\n\n### Navigation\n- `navigate()` - Navigate to URLs\n- `reload()` - Reload current page\n- `go_back()` - Navigate back in history\n- `go_forward()` - Navigate forward in history\n- `stop_loading()` - Stop page loading\n\n### Page Lifecycle\n- `enable()` / `disable()` - Enable/disable page events\n- `get_frame_tree()` - Get page frame structure\n- `get_navigation_history()` - Get navigation history\n\n### Content Management\n- `get_resource_content()` - Get page resource content\n- `search_in_resource()` - Search within page resources\n- `set_document_content()` - Set page HTML content\n\n### Screenshots & PDF\n- `capture_screenshot()` - Take page screenshots\n- `print_to_pdf()` - Generate PDF from page\n- `capture_snapshot()` - Capture page snapshots\n\n### JavaScript Execution\n- `add_script_to_evaluate_on_new_document()` - Add startup scripts\n- `remove_script_to_evaluate_on_new_document()` - Remove startup scripts\n\n### Page Settings\n- `set_lifecycle_events_enabled()` - Control lifecycle events\n- `set_ad_blocking_enabled()` - Enable/disable ad blocking\n- `set_bypass_csp()` - Bypass Content Security Policy\n\n## Advanced Features\n\n### Frame Management\n```python\n# Get all frames in the page\nframe_tree = await get_frame_tree(connection)\nfor frame in frame_tree.child_frames:\n    print(f\"Frame: {frame.frame.url}\")\n```\n\n### Resource Interception\n```python\n# Get resource content\ncontent = await get_resource_content(\n    connection, \n    frame_id=frame_id, \n    url=\"https://example.com/script.js\"\n)\n```\n\n### Page Events\nThe page commands work with various page events:\n- `Page.loadEventFired` - Page load completed\n- `Page.domContentEventFired` - DOM content loaded\n- `Page.frameNavigated` - Frame navigation\n- `Page.frameStartedLoading` - Frame loading started\n\n!!! tip \"Tab Class Integration\"\n    Most page operations are available through the `Tab` class methods like `tab.go_to()`, `tab.reload()`, and `tab.screenshot()` which provide a more convenient API. "
  },
  {
    "path": "docs/en/api/commands/runtime.md",
    "content": "# Runtime Commands\n\nRuntime commands provide JavaScript execution capabilities and runtime environment management.\n\n## Overview\n\nThe runtime commands module enables JavaScript code execution, object inspection, and runtime environment control within browser contexts.\n\n::: pydoll.commands.runtime_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Usage\n\nRuntime commands are used for JavaScript execution and runtime management:\n\n```python\nfrom pydoll.commands.runtime_commands import evaluate, enable\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Enable runtime events\nconnection = ConnectionHandler()\nawait enable(connection)\n\n# Execute JavaScript\nresult = await evaluate(\n    connection, \n    expression=\"document.title\",\n    return_by_value=True\n)\nprint(result.value)  # Page title\n```\n\n## Key Functionality\n\nThe runtime commands module provides functions for:\n\n### JavaScript Execution\n- `evaluate()` - Execute JavaScript expressions\n- `call_function_on()` - Call functions on objects\n- `compile_script()` - Compile JavaScript for reuse\n- `run_script()` - Run compiled scripts\n\n### Object Management\n- `get_properties()` - Get object properties\n- `release_object()` - Release object references\n- `release_object_group()` - Release object groups\n\n### Runtime Control\n- `enable()` / `disable()` - Enable/disable runtime events\n- `discard_console_entries()` - Clear console entries\n- `set_custom_object_formatter_enabled()` - Enable custom formatters\n\n### Exception Handling\n- `set_async_call_stack_depth()` - Set call stack depth\n- Exception capture and reporting\n- Error object inspection\n\n## Advanced Usage\n\n### Complex JavaScript Execution\n```python\n# Execute complex JavaScript with error handling\nscript = \"\"\"\ntry {\n    const elements = document.querySelectorAll('.item');\n    return Array.from(elements).map(el => ({\n        text: el.textContent,\n        href: el.href\n    }));\n} catch (error) {\n    return { error: error.message };\n}\n\"\"\"\n\nresult = await evaluate(\n    connection,\n    expression=script,\n    return_by_value=True,\n    await_promise=True\n)\n```\n\n### Object Inspection\n```python\n# Get detailed object properties\nproperties = await get_properties(\n    connection,\n    object_id=object_id,\n    own_properties=True,\n    accessor_properties_only=False\n)\n\nfor prop in properties:\n    print(f\"{prop.name}: {prop.value}\")\n```\n\n### Console Integration\nRuntime commands integrate with browser console:\n- Console messages and errors\n- Console API method calls\n- Custom console formatters\n\n!!! note \"Performance Considerations\"\n    JavaScript execution through runtime commands can be slower than native browser execution. Use judiciously for complex operations. "
  },
  {
    "path": "docs/en/api/commands/storage.md",
    "content": "# Storage Commands\n\nStorage commands provide comprehensive browser storage management including cookies, localStorage, sessionStorage, and IndexedDB.\n\n## Overview\n\nThe storage commands module enables management of all browser storage mechanisms, providing functionality for data persistence and retrieval.\n\n::: pydoll.commands.storage_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Usage\n\nStorage commands are used for managing browser storage across different mechanisms:\n\n```python\nfrom pydoll.commands.storage_commands import get_cookies, set_cookies, clear_data_for_origin\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Get cookies for a domain\nconnection = ConnectionHandler()\ncookies = await get_cookies(connection, urls=[\"https://example.com\"])\n\n# Set a new cookie\nawait set_cookies(connection, cookies=[{\n    \"name\": \"session_id\",\n    \"value\": \"abc123\",\n    \"domain\": \"example.com\",\n    \"path\": \"/\",\n    \"httpOnly\": True,\n    \"secure\": True\n}])\n\n# Clear all storage for an origin\nawait clear_data_for_origin(\n    connection,\n    origin=\"https://example.com\",\n    storage_types=\"all\"\n)\n```\n\n## Key Functionality\n\nThe storage commands module provides functions for:\n\n### Cookie Management\n- `get_cookies()` - Get cookies by URL or domain\n- `set_cookies()` - Set new cookies\n- `delete_cookies()` - Delete specific cookies\n- `clear_cookies()` - Clear all cookies\n\n### Local Storage\n- `get_dom_storage_items()` - Get localStorage items\n- `set_dom_storage_item()` - Set localStorage item\n- `remove_dom_storage_item()` - Remove localStorage item\n- `clear_dom_storage()` - Clear localStorage\n\n### Session Storage\n- Session storage operations (similar to localStorage)\n- Session-specific data management\n- Tab-isolated storage\n\n### IndexedDB\n- `get_database_names()` - Get IndexedDB databases\n- `request_database()` - Access database structure\n- `request_data()` - Query database data\n- `clear_object_store()` - Clear object stores\n\n### Cache Storage\n- `request_cache_names()` - Get cache names\n- `request_cached_response()` - Get cached responses\n- `delete_cache()` - Delete cache entries\n\n### Application Cache (Deprecated)\n- Legacy application cache support\n- Manifest-based caching\n\n## Advanced Features\n\n### Bulk Operations\n```python\n# Clear all storage types for multiple origins\norigins = [\"https://example.com\", \"https://api.example.com\"]\nfor origin in origins:\n    await clear_data_for_origin(\n        connection,\n        origin=origin,\n        storage_types=\"cookies,local_storage,session_storage,indexeddb\"\n    )\n```\n\n### Storage Quotas\n```python\n# Get storage quota information\nquota_info = await get_usage_and_quota(connection, origin=\"https://example.com\")\nprint(f\"Used: {quota_info.usage} bytes\")\nprint(f\"Quota: {quota_info.quota} bytes\")\n```\n\n### Cross-Origin Storage\n```python\n# Manage storage across different origins\nawait set_cookies(connection, cookies=[{\n    \"name\": \"cross_site_token\",\n    \"value\": \"token123\",\n    \"domain\": \".example.com\",  # Applies to all subdomains\n    \"sameSite\": \"None\",\n    \"secure\": True\n}])\n```\n\n## Storage Types\n\nThe module supports various storage mechanisms:\n\n| Storage Type | Persistence | Scope | Capacity |\n|--------------|-------------|-------|----------|\n| Cookies | Persistent | Domain/Path | ~4KB per cookie |\n| localStorage | Persistent | Origin | ~5-10MB |\n| sessionStorage | Session | Tab | ~5-10MB |\n| IndexedDB | Persistent | Origin | Large (GB+) |\n| Cache API | Persistent | Origin | Large |\n\n!!! warning \"Privacy Considerations\"\n    Storage operations can affect user privacy. Always handle storage data responsibly and in compliance with privacy regulations. "
  },
  {
    "path": "docs/en/api/commands/target.md",
    "content": "# Target Commands\n\nTarget commands manage browser targets including tabs, windows, and other browsing contexts.\n\n## Overview\n\nThe target commands module provides functionality for creating, managing, and controlling browser targets such as tabs, popup windows, and service workers.\n\n::: pydoll.commands.target_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Usage\n\nTarget commands are used internally by browser classes to manage tabs and windows:\n\n```python\nfrom pydoll.commands.target_commands import get_targets, create_target, close_target\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Get all browser targets\nconnection = ConnectionHandler()\ntargets = await get_targets(connection)\n\n# Create a new tab\nnew_target = await create_target(connection, url=\"https://example.com\")\n\n# Close a target\nawait close_target(connection, target_id=new_target.target_id)\n```\n\n## Key Functionality\n\nThe target commands module provides functions for:\n\n### Target Management\n- `get_targets()` - List all browser targets\n- `create_target()` - Create new tabs or windows\n- `close_target()` - Close specific targets\n- `activate_target()` - Bring target to foreground\n\n### Target Information\n- `get_target_info()` - Get detailed target information\n- Target types: page, background_page, service_worker, browser\n- Target states: attached, detached, crashed\n\n### Session Management\n- `attach_to_target()` - Attach to target for control\n- `detach_from_target()` - Detach from target\n- `send_message_to_target()` - Send commands to targets\n\n### Browser Context\n- `create_browser_context()` - Create isolated browser context\n- `dispose_browser_context()` - Remove browser context\n- `get_browser_contexts()` - List browser contexts\n\n## Target Types\n\nDifferent types of targets can be managed:\n\n### Page Targets\n```python\n# Create a new tab\npage_target = await create_target(\n    connection,\n    url=\"https://example.com\",\n    width=1920,\n    height=1080,\n    browser_context_id=None  # Default context\n)\n```\n\n### Popup Windows\n```python\n# Create a popup window\npopup_target = await create_target(\n    connection,\n    url=\"https://popup.example.com\",\n    width=800,\n    height=600,\n    new_window=True\n)\n```\n\n### Incognito Contexts\n```python\n# Create incognito browser context\nincognito_context = await create_browser_context(connection)\n\n# Create tab in incognito context\nincognito_tab = await create_target(\n    connection,\n    url=\"https://private.example.com\",\n    browser_context_id=incognito_context.browser_context_id\n)\n```\n\n!!! info \"Headless vs Headed: how contexts show up\"\n    Browser contexts are isolated logical environments. In headed mode, the first page created inside a new context will usually open in a new OS window. In headless mode, no window is shown — the isolation remains purely logical (cookies, storage, cache and auth state are still separate per context). Prefer contexts in headless/CI pipelines for performance and clean isolation.\n\n## Advanced Features\n\n### Target Events\nTarget commands work with various target events:\n- `Target.targetCreated` - New target created\n- `Target.targetDestroyed` - Target closed\n- `Target.targetInfoChanged` - Target information updated\n- `Target.targetCrashed` - Target crashed\n\n### Multi-Target Coordination\n```python\n# Manage multiple tabs\ntargets = await get_targets(connection)\npage_targets = [t for t in targets if t.type == \"page\"]\n\nfor target in page_targets:\n    # Perform operations on each tab\n    await activate_target(connection, target_id=target.target_id)\n    # ... do work in this tab\n```\n\n### Target Isolation\n```python\n# Create isolated browser context for testing\ntest_context = await create_browser_context(connection)\n\n# All targets in this context are isolated\ntest_tab1 = await create_target(\n    connection, \n    url=\"https://test1.com\",\n    browser_context_id=test_context.browser_context_id\n)\n\ntest_tab2 = await create_target(\n    connection,\n    url=\"https://test2.com\", \n    browser_context_id=test_context.browser_context_id\n)\n```\n\n!!! note \"Browser Integration\"\n    Target commands are primarily used internally by the `Chrome` and `Edge` browser classes. The high-level browser APIs provide more convenient methods for tab management. "
  },
  {
    "path": "docs/en/api/connection/connection.md",
    "content": "# Connection Handler\n\n::: pydoll.connection.connection_handler.ConnectionHandler\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/en/api/connection/managers.md",
    "content": "# Connection Managers\n\n## CommandsManager\n\n::: pydoll.connection.managers.commands_manager.CommandsManager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## EventsManager\n\n::: pydoll.connection.managers.events_manager.EventsManager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3 "
  },
  {
    "path": "docs/en/api/core/constants.md",
    "content": "# Constants\n\nThis section documents all constants, enums, and configuration values used throughout Pydoll.\n\n::: pydoll.constants\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      group_by_category: true\n      members_order: source "
  },
  {
    "path": "docs/en/api/core/exceptions.md",
    "content": "# Exceptions\n\nThis section documents all custom exceptions that can be raised by Pydoll operations.\n\n::: pydoll.exceptions\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      group_by_category: true\n      members_order: source "
  },
  {
    "path": "docs/en/api/core/utils.md",
    "content": "# Utilities\n\nThis section documents utility functions and helper classes used throughout Pydoll.\n\n::: pydoll.utils\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      group_by_category: true\n      members_order: source "
  },
  {
    "path": "docs/en/api/elements/mixins.md",
    "content": "# Element Mixins\n\nThe mixins module provides reusable functionality that can be mixed into element classes to extend their capabilities.\n\n## Find Elements Mixin\n\nThe `FindElementsMixin` provides element finding capabilities to classes that include it.\n\n::: pydoll.elements.mixins.find_elements_mixin\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Usage\n\nMixins are typically used internally by the library to compose functionality. The `FindElementsMixin` is used by classes like `Tab` and `WebElement` to provide element finding methods:\n\n```python\n# These methods come from FindElementsMixin\nelement = await tab.find(id=\"username\")\nelements = await tab.find(class_name=\"item\", find_all=True)\nelement = await tab.query(\"#submit-button\")\n```\n\n## Available Methods\n\nThe `FindElementsMixin` provides several methods for finding elements:\n\n- `find()` - Modern element finding with keyword arguments\n- `query()` - CSS selector and XPath queries\n- `find_element()` - Legacy element finding method\n- `find_elements()` - Legacy method for finding multiple elements\n\n!!! tip \"Modern vs Legacy\"\n    The `find()` method is the modern, recommended approach for finding elements. The `find_element()` and `find_elements()` methods are maintained for backward compatibility. "
  },
  {
    "path": "docs/en/api/elements/shadow_root.md",
    "content": "# ShadowRoot\n\n::: pydoll.elements.shadow_root.ShadowRoot\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      members_order: source\n      group_by_category: true\n"
  },
  {
    "path": "docs/en/api/elements/web_element.md",
    "content": "# WebElement\n\n::: pydoll.elements.web_element.WebElement\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      members_order: source\n      group_by_category: true "
  },
  {
    "path": "docs/en/api/index.md",
    "content": "# API Reference\n\nWelcome to the Pydoll API Reference! This section provides comprehensive documentation for all classes, methods, and functions available in the Pydoll library.\n\n## Overview\n\nPydoll is organized into several key modules, each serving a specific purpose in browser automation:\n\n### Browser Module\nThe browser module contains classes for managing browser instances and their lifecycle.\n\n- **[Chrome](browser/chrome.md)** - Chrome browser automation\n- **[Edge](browser/edge.md)** - Microsoft Edge browser automation  \n- **[Options](browser/options.md)** - Browser configuration options\n- **[Tab](browser/tab.md)** - Tab management and interaction\n- **[Requests](browser/requests.md)** - HTTP requests within browser context\n- **[Managers](browser/managers.md)** - Browser lifecycle managers\n\n### Elements Module\nThe elements module provides classes for interacting with web page elements.\n\n- **[WebElement](elements/web_element.md)** - Individual element interaction\n- **[Mixins](elements/mixins.md)** - Reusable element functionality\n\n### Connection Module\nThe connection module handles communication with the browser through the Chrome DevTools Protocol.\n\n- **[Connection Handler](connection/connection.md)** - WebSocket connection management\n- **[Managers](connection/managers.md)** - Connection lifecycle managers\n\n### Commands Module\nThe commands module provides low-level Chrome DevTools Protocol command implementations.\n\n- **[Commands Overview](commands/index.md)** - CDP command implementations by domain\n\n### Protocol Module\nThe protocol module implements the Chrome DevTools Protocol commands and events.\n\n- **[Base Types](protocol/base.md)** - Base types for Chrome DevTools Protocol\n- **[Browser](protocol/browser.md)** - Browser domain commands and events\n- **[DOM](protocol/dom.md)** - DOM domain commands and events\n- **[Fetch](protocol/fetch.md)** - Fetch domain commands and events\n- **[Input](protocol/input.md)** - Input domain commands and events\n- **[Network](protocol/network.md)** - Network domain commands and events\n- **[Page](protocol/page.md)** - Page domain commands and events\n- **[Runtime](protocol/runtime.md)** - Runtime domain commands and events\n- **[Storage](protocol/storage.md)** - Storage domain commands and events\n- **[Target](protocol/target.md)** - Target domain commands and events\n\n### Core Module\nThe core module contains fundamental utilities, constants, and exceptions.\n\n- **[Constants](core/constants.md)** - Library constants and enums\n- **[Exceptions](core/exceptions.md)** - Custom exception classes\n- **[Utils](core/utils.md)** - Utility functions\n\n## Quick Navigation\n\n### Most Common Classes\n\n| Class | Purpose | Module |\n|-------|---------|--------|\n| `Chrome` | Chrome browser automation | `pydoll.browser.chromium` |\n| `Edge` | Edge browser automation | `pydoll.browser.chromium` |\n| `Tab` | Tab interaction and control | `pydoll.browser.tab` |\n| `WebElement` | Element interaction | `pydoll.elements.web_element` |\n| `ChromiumOptions` | Browser configuration | `pydoll.browser.options` |\n\n### Key Enums and Constants\n\n| Name | Purpose | Module |\n|------|---------|--------|\n| `By` | Element selector strategies | `pydoll.constants` |\n| `Key` | Keyboard key constants | `pydoll.constants` |\n| `PermissionType` | Browser permission types | `pydoll.constants` |\n\n### Common Exceptions\n\n| Exception | When Raised | Module |\n|-----------|-------------|--------|\n| `ElementNotFound` | Element not found in DOM | `pydoll.exceptions` |\n| `WaitElementTimeout` | Element wait timeout | `pydoll.exceptions` |\n| `BrowserNotStarted` | Browser not started | `pydoll.exceptions` |\n\n## Usage Patterns\n\n### Basic Browser Automation\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to(\"https://example.com\")\n    element = await tab.find(id=\"my-element\")\n    await element.click()\n```\n\n### Element Finding\n\n```python\n# Using the modern find() method\nelement = await tab.find(id=\"username\")\nelement = await tab.find(tag_name=\"button\", class_name=\"submit\")\n\n# Using CSS selectors or XPath\nelement = await tab.query(\"#username\")\nelement = await tab.query(\"//button[@class='submit']\")\n```\n\n### Event Handling\n\n```python\nawait tab.enable_page_events()\nawait tab.on('Page.loadEventFired', handle_page_load)\n```\n\n## Type Hints\n\nPydoll is fully typed and provides comprehensive type hints for better IDE support and code safety. All public APIs include proper type annotations.\n\n```python\nfrom typing import Optional, List\nfrom pydoll.elements.web_element import WebElement\n\n# Methods return properly typed objects\nelement: Optional[WebElement] = await tab.find(id=\"test\", raise_exc=False)\nelements: List[WebElement] = await tab.find(class_name=\"item\", find_all=True)\n```\n\n## Async/Await Support\n\nAll Pydoll operations are asynchronous and must be used with `async`/`await`:\n\n```python\nimport asyncio\n\nasync def main():\n    # All Pydoll operations are async\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to(\"https://example.com\")\n        \nasyncio.run(main())\n```\n\nBrowse the sections below to explore the complete API documentation for each module. "
  },
  {
    "path": "docs/en/api/protocol/base.md",
    "content": "# Protocol Base Types\n\nBase types and structures for Chrome DevTools Protocol commands, responses, and events.\n\n## Base Types\n\n::: pydoll.protocol.base\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n      group_by_category: true\n      members_order: source\n      filters:\n        - \"!^__\""
  },
  {
    "path": "docs/en/api/protocol/browser.md",
    "content": "# Browser Protocol\n\nBrowser domain commands, events and types for Chrome DevTools Protocol.\n\n## Methods\n\n::: pydoll.protocol.browser.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Events  \n\n::: pydoll.protocol.browser.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Types\n\n::: pydoll.protocol.browser.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/en/api/protocol/dom.md",
    "content": "# DOM Protocol\n\nDOM domain commands and events for Chrome DevTools Protocol.\n\n## Methods\n\n::: pydoll.protocol.dom.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Events\n\n::: pydoll.protocol.dom.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Types\n\n::: pydoll.protocol.dom.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/en/api/protocol/fetch.md",
    "content": "# Fetch Protocol\n\nFetch domain commands, events and types for Chrome DevTools Protocol.\n\n## Methods\n\n::: pydoll.protocol.fetch.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Events\n\n::: pydoll.protocol.fetch.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Types\n\n::: pydoll.protocol.fetch.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/en/api/protocol/input.md",
    "content": "# Input Protocol\n\nInput domain commands, events and types for Chrome DevTools Protocol.\n\n## Methods\n\n::: pydoll.protocol.input.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Events\n\n::: pydoll.protocol.input.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Types\n\n::: pydoll.protocol.input.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/en/api/protocol/network.md",
    "content": "# Network Protocol\n\nNetwork domain commands and events for Chrome DevTools Protocol.\n\n## Methods\n\n::: pydoll.protocol.network.methods\n    options:\n      show_root_heading: false\n      show_source: false\n      heading_level: 2\n\n## Events\n\n::: pydoll.protocol.network.events\n    options:\n      show_root_heading: false\n      show_source: false\n      heading_level: 2\n\n## Types\n\n::: pydoll.protocol.network.types\n    options:\n      show_root_heading: false\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/en/api/protocol/page.md",
    "content": "# Page Protocol\n\nPage domain commands, events and types for Chrome DevTools Protocol.\n\n## Methods\n\n::: pydoll.protocol.page.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Events\n\n::: pydoll.protocol.page.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Types\n\n::: pydoll.protocol.page.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/en/api/protocol/runtime.md",
    "content": "# Runtime Protocol\n\nRuntime domain commands, events and types for Chrome DevTools Protocol.\n\n## Methods\n\n::: pydoll.protocol.runtime.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Events\n\n::: pydoll.protocol.runtime.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Types\n\n::: pydoll.protocol.runtime.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/en/api/protocol/storage.md",
    "content": "# Storage Protocol\n\nStorage domain commands, events and types for Chrome DevTools Protocol.\n\n## Methods\n\n::: pydoll.protocol.storage.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Events\n\n::: pydoll.protocol.storage.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Types\n\n::: pydoll.protocol.storage.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/en/api/protocol/target.md",
    "content": "# Target Protocol\n\nTarget domain commands and events for Chrome DevTools Protocol.\n\n## Methods\n\n::: pydoll.protocol.target.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Events\n\n::: pydoll.protocol.target.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Types\n\n::: pydoll.protocol.target.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/en/deep-dive/architecture/browser-domain.md",
    "content": "# Browser Domain Architecture\n\nThe Browser domain represents the highest level of Pydoll's automation hierarchy, managing the browser process lifecycle, CDP connections, context isolation, and global browser operations. This document explores the internal architecture, design decisions, and technical implementation of browser-level control.\n\n!!! info \"Practical Usage Guide\"\n    For practical examples and usage patterns, see the [Browser Management](../features/browser-management/tabs.md) and [Browser Contexts](../features/browser-management/contexts.md) guides.\n\n## Architectural Overview\n\nThe Browser domain sits at the intersection of process management, protocol communication, and resource coordination. It orchestrates multiple specialized components to provide a unified interface for browser automation:\n\n```mermaid\ngraph LR\n    Browser[Browser Instance]\n    Browser --> ProcessManager[Process Manager]\n    Browser --> ProxyManager[Proxy Manager]\n    Browser --> TempDirManager[Temp Directory Manager]\n    Browser --> TabRegistry[Tab Registry]\n    Browser --> ConnectionHandler[Connection Handler]\n    \n    ProcessManager --> |Manages| BrowserProcess[Browser Process]\n    ConnectionHandler <--> |WebSocket| CDP[Chrome DevTools Protocol]\n    TabRegistry --> |Manages| Tabs[Tab Instances]\n    CDP <--> BrowserProcess\n```\n\n### Hierarchy and Abstraction\n\nThe Browser domain is implemented as an **abstract base class** that defines the contract for all browser implementations:\n\n```python\nclass Browser(ABC):\n    \"\"\"Abstract base class for browser automation via CDP.\"\"\"\n    \n    @abstractmethod\n    def _get_default_binary_location(self) -> str:\n        \"\"\"Subclasses must provide browser-specific executable path.\"\"\"\n        pass\n    \n    async def start(self, headless: bool = False) -> Tab:\n        \"\"\"Concrete implementation shared by all browsers.\"\"\"\n        # 1. Resolve binary location\n        # 2. Setup user data directory\n        # 3. Start browser process\n        # 4. Verify CDP connection\n        # 5. Configure proxy (if needed)\n        # 6. Return initial tab\n```\n\nThis design enables **polymorphism** - Chrome, Edge, and other Chromium-based browsers share 99% of their code, differing only in executable paths and minor flag variations.\n\n## Component Architecture\n\nThe Browser class coordinates several specialized managers, each responsible for a specific aspect of browser automation. Understanding these components is key to understanding Pydoll's design.\n\n### Connection Handler\n\nThe ConnectionHandler is the **communication bridge** between Pydoll and the browser process. It manages:\n\n- **WebSocket lifecycle**: Connection establishment, keep-alive, reconnection\n- **Command execution**: Sending CDP commands and awaiting responses\n- **Event dispatching**: Routing CDP events to registered callbacks\n- **Callback registry**: Maintaining event listeners per connection\n\n```python\nclass Browser:\n    def __init__(self, ...):\n        # ConnectionHandler is initialized with port or WebSocket address\n        self._connection_handler = ConnectionHandler(self._connection_port)\n    \n    async def _execute_command(self, command, timeout=10):\n        \"\"\"All CDP commands flow through the connection handler.\"\"\"\n        return await self._connection_handler.execute_command(command, timeout)\n```\n\n!!! info \"Connection Layer Deep Dive\"\n    For detailed information on WebSocket communication, command/response flow, and async patterns, see [Connection Layer Architecture](./connection-layer.md).\n\n### Process Manager\n\nThe BrowserProcessManager handles **operating system process lifecycle**:\n\n```python\nclass BrowserProcessManager:\n    def start_browser_process(self, binary, port, arguments):\n        \"\"\"\n        1. Constructs command-line with binary path + arguments\n        2. Spawns subprocess with proper stdio handling\n        3. Monitors process startup\n        4. Stores process handle for later termination\n        \"\"\"\n        \n    def stop_process(self):\n        \"\"\"\n        1. Attempts graceful termination (SIGTERM)\n        2. Waits for process exit\n        3. Force-kills if timeout exceeded (SIGKILL)\n        4. Cleans up process resources\n        \"\"\"\n```\n\n**Why separate process management?**\n\n- **Testability**: Process manager can be mocked for unit tests\n- **Cross-platform**: Encapsulates OS-specific process handling\n- **Reliability**: Handles edge cases like zombie processes, orphaned children\n\n### Tab Registry\n\nThe Browser maintains a **registry of Tab instances** to ensure singleton behavior per target:\n\n```python\nclass Browser:\n    def __init__(self, ...):\n        self._tabs_opened: dict[str, Tab] = {}\n    \n    async def new_tab(self, url='', browser_context_id=None) -> Tab:\n        # Create target via CDP\n        response = await self._execute_command(\n            TargetCommands.create_target(browser_context_id=browser_context_id)\n        )\n        target_id = response['result']['targetId']\n        \n        # Check if tab already exists in registry\n        if target_id in self._tabs_opened:\n            return self._tabs_opened[target_id]\n        \n        # Create new Tab instance and register it\n        tab = Tab(self, target_id=target_id, ...)\n        self._tabs_opened[target_id] = tab\n        return tab\n```\n\n**Why singleton Tab instances?**\n\n- **State consistency**: Multiple references to same tab share state (enabled domains, callbacks)\n- **Memory efficiency**: Prevents duplicate Tab instances for same target\n- **Event routing**: Ensures events route to correct Tab instance\n\n### Proxy Authentication Architecture\n\nPydoll implements **automatic proxy authentication** via the Fetch domain to avoid exposing credentials in CDP commands. The implementation uses **two distinct mechanisms** depending on proxy scope:\n\n#### Mechanism 1: Browser-Level Proxy Auth (Global Proxy)\n\nWhen a proxy is configured via `ChromiumOptions` (applies to all tabs in the default context):\n\n```python\n# In Browser.start() -> _configure_proxy()\nasync def _configure_proxy(self, private_proxy, proxy_credentials):\n    # Enable Fetch AT BROWSER LEVEL\n    await self.enable_fetch_events(handle_auth_requests=True)\n    \n    # Register callbacks AT BROWSER LEVEL (affects ALL tabs)\n    await self.on(FetchEvent.REQUEST_PAUSED, self._continue_request_callback, temporary=True)\n    await self.on(FetchEvent.AUTH_REQUIRED, \n                  partial(self._continue_request_with_auth_callback,\n                          proxy_username=credentials[0],\n                          proxy_password=credentials[1]),\n                  temporary=True)\n```\n\n**Scope:** Browser-wide WebSocket connection → affects **all tabs in default context**\n\n#### Mechanism 2: Tab-Level Proxy Auth (Per-Context Proxy)\n\nWhen a proxy is configured per-context via `create_browser_context(proxy_server=...)`:\n\n```python\n# Store credentials per context\nasync def create_browser_context(self, proxy_server, ...):\n    sanitized_proxy, extracted_auth = self._sanitize_proxy_and_extract_auth(proxy_server)\n    \n    response = await self._execute_command(\n        TargetCommands.create_browser_context(proxy_server=sanitized_proxy)\n    )\n    context_id = response['result']['browserContextId']\n    \n    if extracted_auth:\n        self._context_proxy_auth[context_id] = extracted_auth  # Store per context\n    \n    return context_id\n\n# Setup auth for EACH tab in that context\nasync def _setup_context_proxy_auth_for_tab(self, tab, browser_context_id):\n    creds = self._context_proxy_auth.get(browser_context_id)\n    if not creds:\n        return\n    \n    # Enable Fetch ON THE TAB (tab-level WebSocket)\n    await tab.enable_fetch_events(handle_auth=True)\n    \n    # Register callbacks ON THE TAB (affects only this tab)\n    await tab.on(FetchEvent.REQUEST_PAUSED, \n                 partial(self._tab_continue_request_callback, tab=tab), \n                 temporary=True)\n    await tab.on(FetchEvent.AUTH_REQUIRED,\n                 partial(self._tab_continue_request_with_auth_callback,\n                         tab=tab,\n                         proxy_username=creds[0],\n                         proxy_password=creds[1]),\n                 temporary=True)\n```\n\n**Scope:** Tab-level WebSocket connection → affects **only that specific tab**\n\n#### Why Two Mechanisms?\n\n| Aspect | Browser-Level | Tab-Level |\n|--------|---------------|-----------|\n| **Trigger** | Proxy in `ChromiumOptions` | Proxy in `create_browser_context()` |\n| **WebSocket** | Browser-level connection | Tab-level connection |\n| **Scope** | All tabs in default context | Only tabs in that context |\n| **Efficiency** | One listener for all tabs | One listener per tab |\n| **Isolation** | No context separation | Each context has different credentials |\n\n**Design rationale for tab-level auth:**\n\n- **Context isolation**: Each context can have a **different proxy** with **different credentials**\n- **CDP limitation**: Fetch domain cannot be scoped to a specific context at browser level\n- **Tradeoff**: Slightly less efficient (one listener per tab), but necessary for per-context proxy support\n\nThis architecture ensures **credentials never appear in CDP logs** and authentication is handled transparently.\n\n!!! warning \"Fetch Domain Side Effects\"\n    - **Browser-level Fetch**: Temporarily pauses **all requests across all tabs** in the default context until auth completes\n    - **Tab-level Fetch**: Temporarily pauses **all requests in that specific tab** until auth completes\n    \n    This is a CDP limitation - Fetch enables request interception. After authentication completes, Fetch is disabled to minimize overhead.\n\n## Initialization and Lifecycle\n\n### Constructor Design\n\nThe Browser constructor initializes all internal components but **does not start the browser process**. This separation allows configuration before launch:\n\n```python\nclass Browser(ABC):\n    def __init__(\n        self,\n        options_manager: BrowserOptionsManager,\n        connection_port: Optional[int] = None,\n    ):\n        # 1. Validate parameters\n        self._validate_connection_port(connection_port)\n        \n        # 2. Initialize options via manager\n        self.options = options_manager.initialize_options()\n        \n        # 3. Determine CDP port (random if not specified)\n        self._connection_port = connection_port or randint(9223, 9322)\n        \n        # 4. Initialize specialized managers\n        self._proxy_manager = ProxyManager(self.options)\n        self._browser_process_manager = BrowserProcessManager()\n        self._temp_directory_manager = TempDirectoryManager()\n        self._connection_handler = ConnectionHandler(self._connection_port)\n        \n        # 5. Initialize state tracking\n        self._tabs_opened: dict[str, Tab] = {}\n        self._context_proxy_auth: dict[str, tuple[str, str]] = {}\n        self._ws_address: Optional[str] = None\n```\n\n**Key design decisions:**\n\n- **Lazy process start**: Constructor is synchronous; `start()` is async\n- **Port flexibility**: Random port prevents collisions in parallel automation\n- **Options manager pattern**: Strategy pattern for browser-specific configuration\n- **Component composition**: Specialized managers instead of monolithic class\n\n### Start Sequence\n\nThe `start()` method orchestrates browser launch and connection:\n\n```python\nasync def start(self, headless: bool = False) -> Tab:\n    # 1. Resolve binary location\n    binary_location = self.options.binary_location or self._get_default_binary_location()\n    \n    # 2. Setup user data directory (temp or persistent)\n    self._setup_user_dir()\n    \n    # 3. Extract proxy credentials (if private proxy)\n    proxy_config = self._proxy_manager.get_proxy_credentials()\n    \n    # 4. Start browser process with arguments\n    self._browser_process_manager.start_browser_process(\n        binary_location, self._connection_port, self.options.arguments\n    )\n    \n    # 5. Verify CDP endpoint is responsive\n    await self._verify_browser_running()\n    \n    # 6. Configure proxy authentication (via Fetch domain)\n    await self._configure_proxy(proxy_config[0], proxy_config[1])\n    \n    # 7. Get first valid target and create Tab\n    valid_tab_id = await self._get_valid_tab_id(await self.get_targets())\n    tab = Tab(self, target_id=valid_tab_id, connection_port=self._connection_port)\n    self._tabs_opened[valid_tab_id] = tab\n    \n    return tab\n```\n\n!!! tip \"Why start() Returns a Tab\"\n    This is a **design compromise** for ergonomics. Ideally, `start()` would only launch the browser, and users would call `new_tab()` separately. However, returning the initial tab reduces boilerplate for the 90% use case (single-tab automation). The tradeoff: the initial tab cannot be avoided even in multi-tab scenarios.\n\n### Context Manager Protocol\n\nThe Browser implements `__aenter__` and `__aexit__` for automatic cleanup:\n\n```python\nasync def __aexit__(self, exc_type, exc_val, exc_tb):\n    # 1. Restore backup preferences (if modified)\n    if self._backup_preferences_dir:\n        shutil.copy2(self._backup_preferences_dir, ...)\n    \n    # 2. Check if browser is still running\n    if await self._is_browser_running(timeout=2):\n        await self.stop()\n    \n    # 3. Close WebSocket connection\n    await self._connection_handler.close()\n```\n\nThis ensures proper cleanup even if exceptions occur during automation.\n\n## Browser Context Architecture\n\nBrowser contexts are Pydoll's most sophisticated isolation mechanism, providing **complete browsing environment separation** within a single browser process. Understanding their architecture is essential for advanced automation.\n\n### CDP Hierarchy: Browser, Context, Target\n\nCDP organizes browser structure into three levels:\n\n```mermaid\ngraph TB\n    Browser[Browser Process]\n    Browser --> DefaultContext[Default BrowserContext]\n    Browser --> Context1[BrowserContext ID: abc-123]\n    Browser --> Context2[BrowserContext ID: def-456]\n    \n    DefaultContext --> Target1[Target/Page ID: page-1]\n    DefaultContext --> Target2[Target/Page ID: page-2]\n    \n    Context1 --> Target3[Target/Page ID: page-3]\n    \n    Context2 --> Target4[Target/Page ID: page-4]\n    Context2 --> Target5[Target/Page ID: page-5]\n```\n\n**Key concepts:**\n\n1. **Browser Process**: Single Chromium instance with one CDP endpoint\n2. **BrowserContext**: Isolated storage/cache/permission boundary (similar to incognito mode)\n3. **Target**: Individual page, popup, worker, or background target\n\n### Context Isolation Boundaries\n\nEach browser context maintains **strict isolation** for:\n\n| Resource | Isolation Level | Implementation |\n|----------|----------------|----------------|\n| Cookies | Full | Separate cookie jar per context |\n| localStorage | Full | Separate storage per origin per context |\n| IndexedDB | Full | Separate database per origin per context |\n| Cache | Full | Independent HTTP cache per context |\n| Permissions | Full | Context-specific permission grants |\n| Network proxy | Full | Per-context proxy configuration |\n| Authentication | Full | Independent auth state per context |\n\n!!! info \"Why Contexts Are Lightweight\"\n    Unlike launching multiple browser processes, contexts share the **rendering engine, GPU process, and network stack**. Only storage and state are isolated. This makes contexts 10-100x faster to create than new browser instances.\n\n### Context Creation and Target Binding\n\nCreating a context and target involves two CDP commands:\n\n```python\n# Step 1: Create isolated browsing context\nresponse = await self._execute_command(\n    TargetCommands.create_browser_context(\n        proxy_server='http://proxy.example.com:8080',\n        proxy_bypass_list='localhost,127.0.0.1'\n    )\n)\ncontext_id = response['result']['browserContextId']\n\n# Step 2: Create target (page) within that context\nresponse = await self._execute_command(\n    TargetCommands.create_target(\n        browser_context_id=context_id  # Binds target to context\n    )\n)\ntarget_id = response['result']['targetId']\n```\n\n**Critical detail:** The `browser_context_id` parameter **binds the target to the context's isolation boundary**. Without it, the target is created in the default context.\n\n### Window Materialization in Headed Mode\n\nIn **headed mode** (visible UI), browser contexts have an important physical constraint:\n\n- A context initially exists only **in memory** (no window)\n- The **first target** created in a context **must** open a top-level window\n- **Subsequent targets** can open as tabs within that window\n\nThis is a **CDP/Chromium limitation**, not a Pydoll design choice:\n\n```python\n# First target in context: MUST create window\ntab1 = await browser.new_tab(browser_context_id=context_id)  # Opens new window\n\n# Subsequent targets: CAN open as tabs in existing window\ntab2 = await browser.new_tab(browser_context_id=context_id)  # Opens as tab\n```\n\n**Why does this matter?**\n\n- In **headless mode**: Completely irrelevant (no windows rendered)\n- In **headed mode**: First target per context will open a visible window\n- In **test environments**: Multiple contexts → multiple windows (can be confusing)\n\n!!! tip \"Headless Contexts Are Cleaner\"\n    For CI/CD, scraping, or batch automation, use headless mode. Context isolation works identically, but without window materialization overhead.\n\n### Context Deletion and Cleanup\n\nDeleting a context **immediately closes all targets** within it:\n\n```python\nawait browser.delete_browser_context(context_id)\n# All tabs in this context are now closed\n# All storage for this context is cleared\n# Context cannot be reused (ID is invalid)\n```\n\n**Cleanup sequence:**\n\n1. CDP sends `Target.disposeBrowserContext` command\n2. Browser closes all targets in that context\n3. Browser clears all storage for that context\n4. Browser invalidates the context ID\n5. Pydoll removes context from internal registries\n\n## Event System at Browser Level\n\nThe Browser domain supports **browser-wide event listeners** that operate across all tabs and contexts. This is distinct from tab-level events.\n\n### Browser vs Tab Event Scope\n\n```python\n# Browser-level event: applies to ALL tabs\nawait browser.on('Target.targetCreated', handle_new_target)\n\n# Tab-level event: applies to ONE tab\nawait tab.on('Page.loadEventFired', handle_page_load)\n```\n\n**Architectural difference:**\n\n- **Browser events** use the **browser-level WebSocket connection** (port-based or `ws://host/devtools/browser/...`)\n- **Tab events** use **tab-level WebSocket connections** (`ws://host/devtools/page/<target_id>`)\n\n### Fetch Domain: Global Request Interception\n\nThe Fetch domain can be enabled at **both** browser and tab levels, with different scopes:\n\n```python\n# Browser-level Fetch: intercepts requests for ALL tabs\nawait browser.enable_fetch_events(handle_auth_requests=True)\nawait browser.on('Fetch.requestPaused', handle_request)\n\n# Tab-level Fetch: intercepts requests for ONE tab\nawait tab.enable_fetch_events(handle_auth_requests=True)\nawait tab.on('Fetch.requestPaused', handle_request)\n```\n\n**When to use each:**\n\n| Use Case | Level | Reason |\n|----------|-------|--------|\n| Proxy authentication | Browser | Applies globally to all contexts |\n| Ad blocking | Browser | Block ads across all tabs |\n| API mocking | Tab | Mock specific API for specific test |\n| Request logging | Tab | Log only relevant tab's requests |\n\n!!! warning \"Fetch Performance Impact\"\n    Enabling Fetch at the browser level **pauses all requests** across all tabs until callbacks execute. This adds latency to every request. Use tab-level Fetch when possible to minimize impact.\n\n### Command Routing\n\nAll CDP commands flow through the Browser's connection handler:\n\n```python\nasync def _execute_command(self, command, timeout=10):\n    \"\"\"\n    Routes command to appropriate connection:\n    - Browser-level commands → browser WebSocket\n    - Tab-level commands → delegated to Tab instance\n    \"\"\"\n    return await self._connection_handler.execute_command(command, timeout)\n```\n\nThis centralized routing enables:\n\n- **Request/response correlation**: Match responses to requests via ID\n- **Timeout management**: Cancel commands that exceed timeout\n- **Error handling**: Convert CDP errors to Python exceptions\n\n## Resource Management\n\n### Cookie and Storage Operations\n\nThe Browser domain exposes **browser-wide** and **context-specific** storage operations:\n\n```python\n# Browser-level operations (all contexts)\nawait browser.set_cookies(cookies)\nawait browser.get_cookies()\nawait browser.delete_all_cookies()\n\n# Context-specific operations\nawait browser.set_cookies(cookies, browser_context_id=context_id)\nawait browser.get_cookies(browser_context_id=context_id)\nawait browser.delete_all_cookies(browser_context_id=context_id)\n```\n\nThese operations use the **Storage domain** under the hood:\n\n- `Storage.getCookies`: Retrieve cookies for context or all contexts\n- `Storage.setCookies`: Set cookies with domain/path/expiry\n- `Storage.clearCookies`: Clear cookies for context or all contexts\n\n!!! info \"Browser vs Tab Storage Scope\"\n    - **Browser-level**: Operates on entire browser or specific context\n    - **Tab-level**: Scoped to tab's current origin\n    \n    Use browser-level for global cookie management (e.g., setting session cookies for all domains). Use tab-level for origin-specific operations (e.g., clearing cookies after logout).\n\n### Permission Grants\n\nThe Browser domain provides **programmatic permission control**, bypassing browser prompts:\n\n```python\nawait browser.grant_permissions(\n    [PermissionType.GEOLOCATION, PermissionType.NOTIFICATIONS],\n    origin='https://example.com',\n    browser_context_id=context_id\n)\n```\n\n**Architecture:**\n\n- Permissions are granted via the `Browser.grantPermissions` CDP command\n- Permissions are **context-specific** (isolated per context)\n- Grants override default prompt behavior\n- `reset_permissions()` reverts to default behavior\n\n### Download Management\n\nDownload behavior is configured via the `Browser.setDownloadBehavior` command:\n\n```python\nawait browser.set_download_behavior(\n    behavior=DownloadBehavior.ALLOW,\n    download_path='/path/to/downloads',\n    events_enabled=True,  # Emit download progress events\n    browser_context_id=context_id\n)\n```\n\n**Options:**\n\n- `ALLOW`: Save to specified path\n- `DENY`: Cancel all downloads\n- `DEFAULT`: Show browser's default download UI\n\n### Window Management\n\nWindow operations apply to the **physical OS window** of a target:\n\n```python\nwindow_id = await browser.get_window_id_for_target(target_id)\nawait browser.set_window_bounds({\n    'left': 100, 'top': 100,\n    'width': 1920, 'height': 1080,\n    'windowState': 'normal'  # or 'minimized', 'maximized', 'fullscreen'\n})\n```\n\n**Implementation details:**\n\n- Uses `Browser.getWindowForTarget` to resolve window ID from target ID\n- `Browser.setWindowBounds` modifies window geometry\n- **Headless mode**: Window operations are no-ops (no physical windows exist)\n\n## Architectural Insights and Design Tradeoffs\n\n### Singleton Tab Registry: Why?\n\nThe tab registry pattern (`_tabs_opened: dict[str, Tab]`) ensures that:\n\n1. **Event routing works correctly**: CDP events contain a `targetId` but no Tab reference. The registry maps `targetId` → `Tab` for correct callback dispatch.\n2. **State consistency**: Multiple code paths that reference the same target get the **same Tab instance**, preventing state divergence.\n3. **Memory efficiency**: Without the registry, `get_opened_tabs()` would create duplicate Tab instances for every call.\n\n**Tradeoff:** Memory usage grows with tab count, but this is unavoidable for stateful Tab instances.\n\n### Why start() Returns a Tab\n\nThis design decision sacrifices purity for **ergonomics**:\n\n- **Downside**: Initial tab cannot be avoided, even in multi-tab automation\n- **Upside**: 90% of users (single-tab scripts) don't need boilerplate:\n\n```python\n# With start() returning Tab\ntab = await browser.start()\n\n# Without (pure design)\nawait browser.start()\ntab = await browser.new_tab()\n```\n\n**Alternative explored:** Auto-close initial tab in `new_tab()`. Rejected because it's surprising behavior (implicit side effects).\n\n### Proxy Authentication: Two-Level Architecture Tradeoff\n\nPydoll's proxy authentication uses two different Fetch domain strategies:\n\n**Browser-Level (Global Proxy):**\n- **Security benefit**: Credentials never logged in CDP traces\n- **Performance cost**: Fetch pauses **all requests across all tabs** until auth completes\n- **Efficiency**: Single listener for all tabs in default context\n- **Mitigation**: Fetch is disabled after first auth, minimizing overhead\n\n**Tab-Level (Per-Context Proxy):**\n- **Security benefit**: Credentials never logged in CDP traces\n- **Performance cost**: Fetch pauses **all requests in that tab** until auth completes\n- **Efficiency**: Separate listener per tab (less efficient, but necessary for isolation)\n- **Isolation benefit**: Each context can have different proxy credentials\n- **Mitigation**: Fetch is disabled after first auth per tab\n\n**Why not use Browser.setProxyAuth?** This CDP command doesn't exist. Fetch is the only mechanism for programmatic auth.\n\n**Why tab-level for contexts?** CDP's Fetch domain cannot be scoped to a specific BrowserContext. Since each context can have a different proxy with different credentials, Pydoll must handle auth at the tab level to respect context boundaries.\n\n### Port Randomization Strategy\n\nRandom CDP ports (9223-9322) prevent collisions when running parallel browser instances:\n\n```python\nself._connection_port = connection_port or randint(9223, 9322)\n```\n\n**Why not increment from 9222?**\n\n- Race conditions in multi-process environments (e.g., pytest-xdist)\n- Collision with user's manual port selection\n\n**Tradeoff:** Random ports are harder to debug (can't hardcode). Solution: `browser._connection_port` exposes the chosen port.\n\n### Component Separation: Why Managers?\n\nThe Browser class delegates to specialized managers (ProcessManager, ProxyManager, TempDirManager, ConnectionHandler) for:\n\n1. **Testability**: Managers can be mocked independently\n2. **Reusability**: ProxyManager logic shared across Browser implementations\n3. **Maintainability**: Each manager has single responsibility\n4. **Cross-platform**: OS-specific logic isolated in ProcessManager\n\n**Tradeoff:** More indirection, but significantly better code organization at scale.\n\n## Key Takeaways\n\n1. **Browser is a coordinator**, not a monolith. It orchestrates managers and handles CDP communication.\n2. **Tab registry ensures singleton instances** per target, critical for event routing and state consistency.\n3. **Browser contexts are lightweight isolation**, sharing browser process but separating storage/cache/auth.\n4. **Proxy auth via Fetch** is a security tradeoff - hides credentials but adds latency.\n5. **Event system has two levels**: Browser-wide and tab-specific, with different WebSocket connections.\n6. **Component separation** (managers) improves testability and cross-platform support.\n\n## Related Documentation\n\nFor deeper understanding of related architectural components:\n\n- **[Connection Layer](./connection-layer.md)**: WebSocket communication, command/response flow, async patterns\n- **[Event Architecture](./event-architecture.md)**: Event dispatch, callback management, domain enabling\n- **[Tab Domain](./tab-domain.md)**: Tab-level operations, page navigation, element finding\n- **[CDP Deep Dive](./cdp.md)**: Chrome DevTools Protocol fundamentals\n- **[Proxy Architecture](./proxy-architecture.md)**: Network-level proxy concepts and implementation\n\nFor practical usage patterns:\n\n- **[Tab Management](../features/browser-management/tabs.md)**: Multi-tab automation patterns\n- **[Browser Contexts](../features/browser-management/contexts.md)**: Context isolation in practice\n- **[Proxy Configuration](../features/configuration/proxy.md)**: Setting up proxies and authentication\n"
  },
  {
    "path": "docs/en/deep-dive/architecture/browser-requests-architecture.md",
    "content": "# Browser-Context Requests Architecture\n\nThis document explores the architectural design of Pydoll's browser-context HTTP request system, which enables making HTTP requests that seamlessly inherit the browser's session state, cookies, and authentication.\n\n!!! info \"Practical Guide Available\"\n    This is the architectural deep dive. For practical examples and use cases, see [HTTP Requests Guide](../features/network/http-requests.md).\n\n## Architectural Overview\n\nBrowser-context requests solve a fundamental problem in hybrid automation: maintaining session continuity between UI interactions and API calls. Traditional approaches require manually extracting cookies and headers, creating fragile coupling between browser and HTTP client.\n\nPydoll's architecture eliminates this complexity by executing HTTP requests **inside** the browser's JavaScript context, while leveraging CDP network events to capture comprehensive metadata that JavaScript alone cannot provide.\n\n### Why This Architecture?\n\n| Traditional Approach | Pydoll Architecture |\n|---------------------|---------------------|\n| Separate HTTP client (requests, aiohttp) | Unified browser-based execution |\n| Manual cookie extraction and sync | Automatic cookie inheritance |\n| Two separate session states | Single session state |\n| Limited CORS handling | Browser-native CORS enforcement |\n| Complex authentication flows | Transparent auth preservation |\n\n\n## Component Architecture\n\nThe browser-context request system consists of two primary classes that work together with Pydoll's event system:\n\n```mermaid\nclassDiagram\n    class Tab {\n        +request: Request\n        +enable_network_events()\n        +disable_network_events()\n        +get_network_response_body()\n        +on(event_name, callback)\n        +clear_callbacks()\n    }\n    \n    class Request {\n        -tab: Tab\n        -_network_events_enabled: bool\n        -_requests_sent: list\n        -_requests_received: list\n        +get(url, params, kwargs)\n        +post(url, data, json, kwargs)\n        +put(url, data, json, kwargs)\n        +patch(url, data, json, kwargs)\n        +delete(url, kwargs)\n        +head(url, kwargs)\n        +options(url, kwargs)\n        -_execute_fetch_request()\n        -_register_callbacks()\n        -_extract_headers()\n        -_extract_cookies()\n    }\n    \n    class Response {\n        -_status_code: int\n        -_content: bytes\n        -_text: str\n        -_json: dict\n        -_response_headers: list\n        -_request_headers: list\n        -_cookies: list\n        -_url: str\n        +ok: bool\n        +status_code: int\n        +text: str\n        +content: bytes\n        +url: str\n        +headers: list\n        +request_headers: list\n        +cookies: list\n        +json()\n        +raise_for_status()\n    }\n    \n    Tab *-- Request\n    Request ..> Response : creates\n    Request ..> Tab : uses events\n```\n\n### Request Class\n\nThe `Request` class serves as the interface layer, providing a familiar `requests`-like API while orchestrating the complex interaction between JavaScript execution and network event monitoring.\n\n**Key Responsibilities:**\n\n- Translate Python method calls to Fetch API JavaScript\n- Manage temporary network event listeners\n- Accumulate network events during request execution\n- Extract metadata from CDP events\n- Construct Response objects with complete information\n\n### Response Class\n\nThe `Response` class provides a `requests.Response`-compatible interface, making migration from traditional HTTP clients seamless.\n\n**Key Features:**\n\n- Multiple content accessors (text, bytes, JSON)\n- Lazy JSON parsing with caching\n- Comprehensive header information (both sent and received)\n- Cookie extraction from Set-Cookie headers\n- Final URL after redirects\n\n## Execution Flow\n\nThe request execution follows a six-phase pipeline:\n\n```mermaid\nflowchart TD\n    Start([tab.request.get#40;url#41;]) --> Phase1[<b>1. Preparation</b><br/>Build URL + options]\n    \n    Phase1 --> Phase2[<b>2. Event Registration</b><br/>Enable network events<br/>Register callbacks]\n    \n    Phase2 --> Phase3[<b>3. JavaScript Execution</b><br/>Runtime.evaluate&#40;fetch&#41;]\n    \n    Phase3 --> Phase4{<b>4. Network Activity</b>}\n    Phase4 -->|Request sent| Event1[REQUEST_WILL_BE_SENT]\n    Phase4 -->|Response received| Event2[RESPONSE_RECEIVED]\n    Phase4 -->|Extra info| Event3[*_EXTRA_INFO events]\n    \n    Event1 --> Collect[Collect metadata]\n    Event2 --> Collect\n    Event3 --> Collect\n    \n    Collect --> Phase5[<b>5. Construction</b><br/>Extract headers/cookies<br/>Build Response object]\n    \n    Phase5 --> Phase6[<b>6. Cleanup</b><br/>Clear callbacks<br/>Disable events]\n    \n    Phase6 --> End([Return Response])\n```\n\n### Phase Details\n\n| Phase | Layer | Key Operations | Asynchronous |\n|-------|-------|----------------|--------------|\n| **1. Preparation** | Request | URL building, options formatting | No |\n| **2. Event Registration** | Tab | Enable events, register callbacks | Yes |\n| **3. JavaScript Execution** | CDP/Browser | Execute fetch() in browser context | Yes |\n| **4. Network Activity** | Browser/CDP | HTTP request, emit CDP events | Yes (parallel) |\n| **5. Construction** | Request | Parse events, build Response | No |\n| **6. Cleanup** | Tab | Remove callbacks, disable events | Yes |\n\n## Event System Integration\n\nBrowser-context requests are tightly integrated with Pydoll's event system architecture. Understanding this relationship is crucial.\n\n### Temporary Event Lifecycle\n\n```mermaid\nstateDiagram-v2\n    [*] --> NoEvents: Request starts\n    NoEvents --> EventsEnabled: Enable network events\n    EventsEnabled --> CallbacksRegistered: Register callbacks\n    CallbacksRegistered --> ExecutingRequest: Execute fetch\n    ExecutingRequest --> CapturingEvents: Events fire\n    CapturingEvents --> ExecutingRequest: More events\n    ExecutingRequest --> CleaningUp: Fetch completes\n    CleaningUp --> CallbacksRemoved: Clear callbacks\n    CallbacksRemoved --> EventsDisabled: Disable if needed\n    EventsDisabled --> [*]: Request complete\n```\n\n### Why Both JavaScript and Events?\n\nA common question: if JavaScript can execute the request, why use network events?\n\n| Information Source | JavaScript (Fetch API) | Network Events (CDP) |\n|-------------------|------------------------|----------------------|\n| Response status | Available | Available |\n| Response body | Available | Not available |\n| Response headers | Partial (CORS restricted) | Complete |\n| Request headers | Not accessible | Complete |\n| Set-Cookie headers | Hidden by browser | Available |\n| Timing information | Limited | Comprehensive |\n| Redirect chain | Only final URL | Full chain |\n\n**The Solution:** Combine both sources for complete information.\n\n!!! tip \"Complementary Technologies\"\n    JavaScript provides the response body and triggers the request in the browser's context (with cookies, auth). Network events provide the metadata that JavaScript security policies hide.\n\n### CDP Network Event Types\n\nThe architecture uses four CDP event types to capture complete metadata:\n\n| Event | Purpose | Key Information |\n|-------|---------|----------------|\n| `REQUEST_WILL_BE_SENT` | Main outgoing request | URL, method, standard headers |\n| `REQUEST_WILL_BE_SENT_EXTRA_INFO` | Additional request metadata | Associated cookies, raw headers |\n| `RESPONSE_RECEIVED` | Main response received | Status, headers, MIME type, timing |\n| `RESPONSE_RECEIVED_EXTRA_INFO` | Additional response metadata | Set-Cookie headers, security info |\n\n!!! info \"Event Multiplicity\"\n    A single HTTP request generates multiple CDP events. The Request class accumulates all related events and extracts non-duplicate information during the construction phase.\n\n## Header and Cookie Architecture\n\n### Header Extraction Strategy\n\nHeaders exist in multiple CDP events with potential duplication. The architecture uses a deduplication strategy:\n\n```mermaid\nflowchart TD\n    A[Network Events] --> B{Event Type}\n    B -->|REQUEST events| C[Extract Sent Headers]\n    B -->|RESPONSE events| D[Extract Received Headers]\n    \n    C --> E[Deduplicate by name+value]\n    D --> F[Deduplicate by name+value]\n    \n    E --> G[Request Headers List]\n    F --> H[Response Headers List]\n    \n    G --> I[Response Object]\n    H --> I\n```\n\n**Deduplication Logic:**\n\n1. Events are processed in order\n2. Each header is identified by `(name, value)` tuple\n3. Only first occurrence of each tuple is kept\n4. Result: unique, non-redundant header list\n\n### Cookie Parsing Architecture\n\nCookies require special handling because they come from `Set-Cookie` headers in `RESPONSE_RECEIVED_EXTRA_INFO` events:\n\n```mermaid\nflowchart TD\n    A[RESPONSE_RECEIVED_EXTRA_INFO] --> B[Extract Set-Cookie headers]\n    B --> C{Multi-line header?}\n    C -->|Yes| D[Split by newline]\n    C -->|No| E[Parse single cookie]\n    D --> F[Parse each line]\n    F --> G[Extract name=value]\n    E --> G\n    G --> H{Valid name?}\n    H -->|Yes| I[Create CookieParam]\n    H -->|No| J[Discard]\n    I --> K[Add to cookie list]\n    K --> L[Deduplicate]\n    L --> M[Response Object]\n```\n\n**Cookie Extraction Principles:**\n\n- Only `EXTRA_INFO` events contain `Set-Cookie` headers\n- Cookie attributes (Path, Domain, Secure, HttpOnly) are ignored\n- Browser manages cookie attributes internally\n- Only name-value pairs are extracted for informational purposes\n\n!!! warning \"Cookie Scope\"\n    The `Response.cookies` property contains only **new or updated** cookies from this specific response. Existing browser cookies are managed automatically and not exposed through this interface.\n\n## JavaScript Execution Context\n\nThe Fetch API execution happens in the browser's JavaScript context, which is key to the architecture's power:\n\n### Fetch API Integration\n\nThe request is translated to JavaScript:\n\n```javascript\n// Simplified representation\n(async () => {\n    const response = await fetch(url, {\n        method: 'GET',\n        headers: {'X-Custom': 'value'},\n        // Browser automatically adds:\n        // - Cookie header\n        // - Authorization if set\n        // - Standard headers (User-Agent, Accept, etc.)\n    });\n    \n    return {\n        status: response.status,\n        url: response.url,  // Final URL after redirects\n        text: await response.text(),\n        content: new Uint8Array(await response.arrayBuffer()),\n        json: response.headers.get('Content-Type')?.includes('application/json')\n            ? await response.clone().json()\n            : null\n    };\n})()\n```\n\n### Browser Context Benefits\n\nExecuting in the browser context provides:\n\n| Benefit | Description |\n|---------|-------------|\n| **Automatic Cookie Inclusion** | Browser sends all applicable cookies automatically |\n| **Auth State Preservation** | Authentication headers maintained from browser session |\n| **CORS Enforcement** | Browser applies same CORS policies as user interactions |\n| **TLS/SSL Handling** | Browser's certificate validation and security policies apply |\n| **Compression** | Automatic handling of gzip, br, deflate |\n| **Redirects** | Browser follows redirects transparently |\n| **Same Security Context** | Request appears identical to user-initiated requests |\n\n!!! info \"Anti-Bot Detection\"\n    Requests executed in the browser context are indistinguishable from user-initiated requests, making them effective against anti-bot systems that analyze request patterns.\n\n## Performance Considerations\n\n### Event Overhead\n\nNetwork events add overhead to request execution:\n\n| Scenario | Overhead | Recommendation |\n|----------|----------|----------------|\n| Single request | Low | Acceptable |\n| Multiple sequential requests | Moderate | Enable events once |\n| Bulk requests (100+) | High | Consider enabling events at tab level |\n| Long-running automation | Memory concern | Disable when done |\n\n### Optimization Pattern\n\n```python\n# Inefficient - events enabled/disabled repeatedly\nfor url in urls:\n    response = await tab.request.get(url)\n\n# Efficient - events enabled once\nawait tab.enable_network_events()\nfor url in urls:\n    response = await tab.request.get(url)\nawait tab.disable_network_events()\n```\n\n!!! tip \"Automatic Optimization\"\n    The Request class checks if network events are already enabled and skips redundant enable/disable operations automatically.\n\n### JSON Parsing Strategy\n\nResponse JSON parsing uses lazy evaluation with caching:\n\n1. First call to `response.json()`: Parse and cache\n2. Subsequent calls: Return cached result\n3. If JSON pre-parsed during construction: Use that\n\nThis prevents redundant parsing overhead.\n\n## Security Architecture\n\n### CORS Policy Enforcement\n\nBrowser-context requests respect CORS policies:\n\n```mermaid\nflowchart TD\n    A[tab.request.get&#40;url&#41;] --> B{Same Origin?}\n    B -->|Yes| C[Request Allowed]\n    B -->|No| D{CORS Headers Present?}\n    D -->|Yes| E[Request Allowed]\n    D -->|No| F[Request Blocked]\n    \n    C --> G[Response Returned]\n    E --> G\n    F --> H[CORS Error]\n```\n\n**CORS Behavior:**\n\n- Requests to same origin: Always allowed\n- Cross-origin requests: Require CORS headers from server\n- Opaque responses: May be blocked by browser\n\n**Workaround for CORS Issues:**\n\nNavigate to the domain first to establish same-origin context:\n\n```python\nawait tab.go_to('https://different-domain.com')\nresponse = await tab.request.get('https://different-domain.com/api')\n```\n\n### Cookie Security\n\nCookies with security flags (`HttpOnly`, `Secure`, `SameSite`) are handled by the browser:\n\n- **HttpOnly cookies**: Sent automatically but not exposed to JavaScript or CDP\n- **Secure cookies**: Only sent over HTTPS\n- **SameSite cookies**: Browser enforces SameSite policies\n\nThe `Response.cookies` property may not show all cookies due to these security restrictions.\n\n### TLS/SSL Validation\n\nThe browser validates SSL certificates. Self-signed or invalid certificates cause requests to fail unless:\n\n```python\noptions = ChromiumOptions()\noptions.add_argument('--ignore-certificate-errors')\nbrowser = Chrome(options=options)\n```\n\n!!! warning \"Security Trade-off\"\n    Disabling certificate validation reduces security. Only use in controlled environments.\n\n## Limitations and Design Decisions\n\n### Request Body Size\n\nVery large request bodies (files, large datasets) have JavaScript memory constraints. For file uploads, use `WebElement.set_input_files()` or the file chooser interceptor instead.\n\n### Binary Response Handling\n\nBinary responses are converted through JavaScript's `ArrayBuffer` and `Uint8Array`, which adds some overhead for very large responses (>100MB).\n\n### Redirect Transparency\n\nThe Fetch API follows redirects automatically. Only the final URL is captured. If you need the redirect chain, use network monitoring separately.\n\n### Event Timing\n\nEvents must be registered **before** executing the fetch. The architecture ensures this through the registration phase, but manual event handling requires careful timing.\n\n## Architectural Principles\n\nThe browser-context request architecture adheres to these principles:\n\n1. **Session Continuity**: Never break the browser's session state\n2. **Zero Manual Sync**: No cookie/header extraction required\n3. **Complete Information**: Combine JavaScript + events for full metadata\n4. **Automatic Cleanup**: Resources freed after each request\n5. **Familiar Interface**: `requests`-compatible API for easy adoption\n6. **Performance Conscious**: Optimize for common use cases\n7. **Security Aware**: Respect browser security policies\n\n## Integration with Other Systems\n\n### Event System Dependency\n\nBrowser-context requests depend on the event system architecture:\n\n- Leverages `Tab.on()` for callback registration\n- Uses `Tab.clear_callbacks()` for cleanup\n- Respects existing network event enablement\n- Integrates with event lifecycle management\n\nSee [Event System Architecture](event-architecture.md) for details.\n\n### Type System Integration\n\nThe architecture uses Python's type system extensively:\n\n- `HeaderEntry` TypedDict for headers\n- `CookieParam` TypedDict for cookies\n- Event type definitions from `pydoll.protocol.network.events`\n- Provides IDE autocomplete and type safety\n\nSee [Typing System](typing-system.md) for details.\n\n## Further Reading\n\n- **[HTTP Requests Guide](../features/network/http-requests.md)** - Practical examples and use cases\n- **[Event System Architecture](event-architecture.md)** - Event system internal design\n- **[Network Monitoring](../features/network/monitoring.md)** - Passive network observation\n- **[Request Interception](../features/network/interception.md)** - Active request modification\n- **[Typing System](typing-system.md)** - Type system integration\n\n## Summary\n\nPydoll's browser-context request architecture achieves seamless HTTP communication by combining JavaScript Fetch API execution with CDP network event monitoring. This hybrid approach provides:\n\n- **Complete metadata** from both JavaScript and CDP events\n- **Automatic session continuity** through browser context execution  \n- **Familiar interface** compatible with the requests library\n- **Performance optimization** through event reuse\n- **Security compliance** with browser policies\n\nThe architecture demonstrates how combining complementary technologies (JavaScript + CDP events) can solve complex problems elegantly, providing power and convenience without compromising on completeness or security.\n\n"
  },
  {
    "path": "docs/en/deep-dive/architecture/event-architecture.md",
    "content": "# Event System Architecture\n\nThis document explores the internal architecture of Pydoll's event system, covering WebSocket communication, event flow, callback management, and performance considerations.\n\n!!! info \"Practical Usage Guide\"\n    For practical examples and usage patterns, see the [Event System Guide](../features/advanced/event-system.md).\n\n## WebSocket Communication and CDP\n\nAt the core of Pydoll's event system is the Chrome DevTools Protocol (CDP), which provides a structured way to interact with and monitor browser activities over WebSocket connections. This bidirectional communication channel allows your code to both send commands to the browser and receive events back.\n\n```mermaid\nsequenceDiagram\n    participant Client as Pydoll Code\n    participant Connection as ConnectionHandler\n    participant WebSocket\n    participant Browser\n    \n    Client->>Connection: Register callback for event\n    Connection->>Connection: Store callback in registry\n    \n    Client->>Connection: Enable event domain\n    Connection->>WebSocket: Send CDP command to enable domain\n    WebSocket->>Browser: Forward command\n    Browser-->>WebSocket: Acknowledge domain enabled\n    WebSocket-->>Connection: Forward response\n    Connection-->>Client: Domain enabled\n    \n    Browser->>WebSocket: Event occurs, sends CDP event message\n    WebSocket->>Connection: Forward event message\n    Connection->>Connection: Look up callbacks for this event\n    Connection->>Client: Execute registered callback\n```\n\n### WebSocket Communication Model\n\nThe WebSocket connection between Pydoll and the browser follows this pattern:\n\n1. **Connection Establishment**: When the browser starts, a WebSocket server is created, and Pydoll establishes a connection to it\n2. **Bidirectional Messaging**: Both Pydoll and the browser can send messages at any time\n3. **Message Types**:\n   - **Commands**: Sent from Pydoll to the browser (e.g., navigation, DOM manipulation)\n   - **Command Responses**: Sent from the browser to Pydoll in response to commands\n   - **Events**: Sent from the browser to Pydoll when something happens (e.g., page load, network activity)\n\n### Chrome DevTools Protocol Structure\n\nCDP organizes its functionality into domains, each responsible for a specific area of browser functionality:\n\n| Domain | Responsibility | Typical Events |\n|--------|----------------|----------------|\n| Page | Page lifecycle | Load events, navigation, dialogs |\n| Network | Network activity | Request/response monitoring, WebSockets |\n| DOM | Document structure | DOM changes, attribute modifications |\n| Fetch | Request interception | Request paused, authentication required |\n| Runtime | JavaScript execution | Console messages, exceptions |\n| Browser | Browser management | Window creation, tabs, contexts |\n\nEach domain must be explicitly enabled before it will emit events, which helps manage performance by only processing events that are actually needed.\n\n## Domain Architecture\n\n### The Enable/Disable Pattern\n\nThe explicit enable/disable pattern serves several important architectural purposes:\n\n1. **Performance Optimization**: By only enabling domains you're interested in, you reduce the overhead of event processing\n2. **Resource Management**: Some event domains (like Network or DOM monitoring) can generate large volumes of events that consume memory\n3. **Protocol Compliance**: CDP requires explicit domain enabling before events are emitted\n4. **Controlled Cleanup**: Explicitly disabling domains ensures proper cleanup when events are no longer needed\n\n```mermaid\nstateDiagram-v2\n    [*] --> Disabled: Initial State\n    Disabled --> Enabled: enable_xxx_events()\n    Enabled --> Disabled: disable_xxx_events()\n    Enabled --> [*]: Tab Closed\n    Disabled --> [*]: Tab Closed\n```\n\n!!! warning \"Event Leak Prevention\"\n    Failing to disable event domains when they're no longer needed can lead to memory leaks and performance degradation, especially in long-running automation. Always disable event domains when you're done with them, particularly for high-volume events like network monitoring.\n\n### Domain-Specific Enabling Methods\n\nDifferent domains are enabled through specific methods on the appropriate objects:\n\n| Domain | Enable Method | Disable Method | Available On |\n|--------|--------------|----------------|--------------|\n| Page | `enable_page_events()` | `disable_page_events()` | Tab |\n| Network | `enable_network_events()` | `disable_network_events()` | Tab |\n| DOM | `enable_dom_events()` | `disable_dom_events()` | Tab |\n| Fetch | `enable_fetch_events()` | `disable_fetch_events()` | Tab, Browser |\n| File Chooser | `enable_intercept_file_chooser_dialog()` | `disable_intercept_file_chooser_dialog()` | Tab |\n\n!!! info \"Domain Ownership\"\n    Events belong to specific domains based on their functionality. Some domains are only available at certain levels - for instance, Page events are available on the Tab instance but not directly at the Browser level.\n\n## Event Registration System\n\n### The `on()` Method\n\nThe central method for subscribing to events is the `on()` method, available on both Tab and Browser instances:\n\n```python\nasync def on(\n    self, event_name: str, callback: callable, temporary: bool = False\n) -> int:\n    \"\"\"\n    Registers an event listener.\n\n    Args:\n        event_name (str): The event name to listen for.\n        callback (callable): The callback function to execute when the\n            event is triggered.\n        temporary (bool): If True, the callback will be removed after it's\n            triggered once. Defaults to False.\n\n    Returns:\n        int: The ID of the registered callback.\n    \"\"\"\n```\n\nThis method returns a callback ID that can be used to remove the callback later if needed.\n\n### Callback Registry\n\nInternally, the `ConnectionHandler` maintains a callback registry:\n\n```python\n{\n    'Page.loadEventFired': [\n        (callback_id_1, callback_function_1, temporary=False),\n        (callback_id_2, callback_function_2, temporary=True),\n    ],\n    'Network.requestWillBeSent': [\n        (callback_id_3, callback_function_3, temporary=False),\n    ]\n}\n```\n\nWhen an event arrives via WebSocket:\n\n1. The event name is extracted from the message\n2. The registry is queried for matching callbacks\n3. Each callback is executed with the event data\n4. Temporary callbacks are removed after execution\n\n### Async Callback Handling\n\nCallbacks can be either synchronous or asynchronous. The event system handles both:\n\n```python\nasync def _trigger_callbacks(self, event_name: str, event_data: dict):\n    for cb_id, cb_data in self._event_callbacks.items():\n        if cb_data['event'] == event_name:\n            if asyncio.iscoroutinefunction(cb_data['callback']):\n                await cb_data['callback'](event_data)\n            else:\n                cb_data['callback'](event_data)\n```\n\nAsynchronous callbacks are awaited sequentially. This means each callback completes before the next one executes, which is important for:\n\n- **Predictable Execution Order**: Callbacks execute in registration order\n- **Error Handling**: Exceptions in one callback don't prevent others from executing\n- **State Consistency**: Callbacks can rely on sequential state changes\n\n!!! info \"Sequential vs Concurrent Execution\"\n    Callbacks execute sequentially within the same event. However, different events can be processed concurrently since the event loop handles multiple connections simultaneously.\n\n## Event Flow and Lifecycle\n\nThe event lifecycle follows these steps:\n\n```mermaid\nflowchart TD\n    A[Browser Activity] -->|Generates| B[CDP Event]\n    B -->|Sent via WebSocket| C[ConnectionHandler]\n    C -->|Filters by Event Name| D{Registered Callbacks?}\n    D -->|Yes| E[Process Event]\n    D -->|No| F[Discard Event]\n    E -->|For Each Callback| G[Execute Callback]\n    G -->|If Temporary| H[Remove Callback]\n    G -->|If Permanent| I[Retain for Future Events]\n```\n\n### Detailed Flow\n\n1. **Browser Activity**: Something happens in the browser (page loads, request sent, DOM changes)\n2. **CDP Event Generation**: Browser generates a CDP event message\n3. **WebSocket Transmission**: Message is sent over WebSocket to Pydoll\n4. **Event Reception**: The ConnectionHandler receives the event\n5. **Callback Lookup**: ConnectionHandler checks its registry for callbacks matching the event name\n6. **Callback Execution**: If callbacks exist, each is executed with the event data\n7. **Temporary Removal**: If a callback was registered as temporary, it's removed after execution\n\n## Browser-Level vs. Tab-Level Events\n\nPydoll's event system operates at both the browser and tab levels, with important distinctions:\n\n```mermaid\ngraph TD\n    Browser[Browser Instance] -->|\"Global Events (e.g., Target events)\"| BrowserCallbacks[Browser-Level Callbacks]\n    Browser -->|\"Creates\"| Tab1[Tab Instance 1]\n    Browser -->|\"Creates\"| Tab2[Tab Instance 2]\n    Tab1 -->|\"Tab-Specific Events\"| Tab1Callbacks[Tab 1 Callbacks]\n    Tab2 -->|\"Tab-Specific Events\"| Tab2Callbacks[Tab 2 Callbacks]\n```\n\n### Browser-Level Events\n\nBrowser-level events operate globally across all tabs. These are limited to specific domains like:\n\n- **Target Events**: Tab creation, destruction, crash\n- **Browser Events**: Window management, download coordination\n\n```python\n# Browser-level event registration\nawait browser.on('Target.targetCreated', handle_new_target)\n```\n\nBrowser-level event domains are limited, and trying to use tab-specific events will raise an exception.\n\n### Tab-Level Events\n\nTab-level events are specific to an individual tab:\n\n```python\n# Each tab has its own event context\ntab1 = await browser.start()\ntab2 = await browser.new_tab()\n\nawait tab1.enable_page_events()\nawait tab1.on(PageEvent.LOAD_EVENT_FIRED, handle_tab1_load)\n\nawait tab2.enable_page_events()\nawait tab2.on(PageEvent.LOAD_EVENT_FIRED, handle_tab2_load)\n```\n\nThis architecture allows for:\n\n- **Isolated Event Handling**: Events in one tab don't affect others\n- **Per-Tab Configuration**: Different tabs can monitor different event types\n- **Resource Efficiency**: Only enable events on tabs that need them\n\n!!! info \"Domain-Specific Scope\"\n    Not all event domains are available at both levels:\n    \n    - **Fetch Events**: Available at both browser and tab levels\n    - **Page Events**: Available only at the tab level\n    - **Target Events**: Available only at the browser level\n\n## Performance Architecture\n\n### Event System Overhead\n\nThe event system adds overhead to browser automation, especially for high-frequency events:\n\n| Event Domain | Typical Event Volume | Performance Impact |\n|--------------|---------------------|-------------------|\n| Page | Low | Minimal |\n| Network | High | Moderate to High |\n| DOM | Very High | High |\n| Fetch | Moderate | Moderate (higher if intercepting) |\n\n### Performance Optimization Strategies\n\n1. **Selective Domain Enabling**: Only enable event domains you're actively using\n2. **Strategic Scoping**: Use browser-level events only for truly browser-wide concerns\n3. **Timely Disabling**: Always disable event domains when you're finished with them\n4. **Early Filtering**: In callbacks, filter out irrelevant events as early as possible\n5. **Temporary Callbacks**: Use the `temporary=True` flag for one-time events\n\n### Memory Management\n\nThe event system manages memory through several mechanisms:\n\n1. **Callback Registry Cleanup**: Removing callbacks frees their references\n2. **Temporary Auto-Removal**: Temporary callbacks are automatically cleaned up\n3. **Domain Disabling**: Disabling a domain stops event generation\n4. **Tab Closure**: When a tab closes, all its callbacks are automatically removed\n\n!!! warning \"Memory Leak Prevention\"\n    In long-running automation, always clean up callbacks and disable domains when done. High-frequency events (especially DOM) can accumulate significant memory if left enabled.\n\n## Connection Handler Architecture\n\nThe `ConnectionHandler` is the central component managing WebSocket communication and event dispatching.\n\n### Key Responsibilities\n\n1. **WebSocket Management**: Establishing and maintaining the WebSocket connection\n2. **Message Routing**: Distinguishing between command responses and events\n3. **Callback Registry**: Maintaining the mapping of event names to callbacks\n4. **Event Dispatching**: Executing registered callbacks when events arrive\n5. **Cleanup**: Removing callbacks and closing connections\n\n### Internal Structure\n\n```python\nclass ConnectionHandler:\n    def __init__(self, ...):\n        self._events_handler = EventsManager()\n        self._websocket = None\n        # ... other attributes\n    \n    async def register_callback(self, event_name, callback, temporary):\n        return self._events_handler.register_callback(event_name, callback, temporary)\n\nclass EventsManager:\n    def __init__(self):\n        self._event_callbacks = {}  # Callback ID -> callback data\n        self._callback_id = 0\n    \n    def register_callback(self, event_name, callback, temporary):\n        self._callback_id += 1\n        self._event_callbacks[self._callback_id] = {\n            'event': event_name,\n            'callback': callback,\n            'temporary': temporary\n        }\n        return self._callback_id\n    \n    async def _trigger_callbacks(self, event_name, event_data):\n        callbacks_to_remove = []\n        \n        for cb_id, cb_data in self._event_callbacks.items():\n            if cb_data['event'] == event_name:\n                # Execute callback (await if async, call directly if sync)\n                if asyncio.iscoroutinefunction(cb_data['callback']):\n                    await cb_data['callback'](event_data)\n                else:\n                    cb_data['callback'](event_data)\n                \n                # Mark temporary callbacks for removal\n                if cb_data['temporary']:\n                    callbacks_to_remove.append(cb_id)\n        \n        # Remove temporary callbacks after all callbacks executed\n        for cb_id in callbacks_to_remove:\n            self.remove_callback(cb_id)\n```\n\nThis architecture ensures:\n\n- **Efficient Lookup**: Event names map directly to callback lists\n- **Minimal Overhead**: Only registered events are processed\n- **Automatic Cleanup**: Temporary callbacks are removed after execution\n- **Thread Safety**: Operations are async-safe\n\n## Event Message Format\n\nCDP events follow a standardized message format:\n\n```json\n{\n    \"method\": \"Network.requestWillBeSent\",\n    \"params\": {\n        \"requestId\": \"1234.56\",\n        \"loaderId\": \"7890.12\",\n        \"documentURL\": \"https://example.com\",\n        \"request\": {\n            \"url\": \"https://api.example.com/data\",\n            \"method\": \"GET\",\n            \"headers\": {...}\n        },\n        \"timestamp\": 123456.789,\n        \"wallTime\": 1234567890.123,\n        \"initiator\": {...},\n        \"type\": \"XHR\"\n    }\n}\n```\n\nKey components:\n\n- **`method`**: The event name in `Domain.eventName` format\n- **`params`**: Event-specific data, varies by event type\n- **No `id` field**: Unlike commands, events don't have request IDs\n\nThe event system extracts the `method` field to route to the appropriate callbacks, passing the entire message to each callback.\n\n## Multi-Tab Event Coordination\n\nPydoll's architecture supports sophisticated multi-tab event coordination:\n\n### Independent Tab Contexts\n\nEach tab maintains its own:\n\n- Event domain enablement state\n- Callback registry\n- Event communication channel\n- Network logs (if network events enabled)\n\n!!! info \"Communication Architecture\"\n    Each tab has its own event communication channel to the browser. For technical details on how WebSocket connections and target IDs work at the protocol level, see [Browser Domain Architecture](./browser-domain.md).\n\n### Shared Browser Context\n\nMultiple tabs can share:\n\n- Browser-level event listeners\n- Cookie storage\n- Cache\n- Browser process\n\nThis architecture allows for:\n\n1. **Parallel Event Processing**: Multiple tabs can process events simultaneously\n2. **Isolated Failures**: Issues in one tab don't affect others\n3. **Resource Sharing**: Common browser features are shared efficiently\n4. **Coordinated Actions**: Browser-level events can coordinate cross-tab activities\n\n## Conclusion\n\nPydoll's event system architecture is designed for:\n\n- **Performance**: Minimal overhead through selective domain enabling and efficient callback dispatch\n- **Flexibility**: Support for both browser-level and tab-level events\n- **Scalability**: Handle multiple tabs with independent event contexts\n- **Reliability**: Automatic cleanup and memory management\n\nUnderstanding this architecture helps you:\n\n- **Optimize Performance**: Know which domains have high overhead\n- **Debug Issues**: Understand the event flow when things don't work as expected\n- **Design Better Automation**: Leverage the architecture for efficient event-driven workflows\n- **Avoid Pitfalls**: Prevent memory leaks and performance degradation\n\nFor practical usage patterns and examples, see the [Event System Guide](../features/advanced/event-system.md).\n\n"
  },
  {
    "path": "docs/en/deep-dive/architecture/find-elements-mixin.md",
    "content": "# FindElements Mixin Architecture\n\nThe FindElementsMixin represents a critical architectural decision in Pydoll: using **composition over inheritance** to share element-finding capabilities between `Tab` and `WebElement` without coupling them through a common base class. This document explores the mixin pattern, its implementation, and the internal mechanics of element location.\n\n!!! info \"Practical Usage Guide\"\n    For practical examples and usage patterns, see the [Element Finding Guide](../features/automation/element-finding.md) and [Selectors Guide](./selectors-guide.md).\n\n## Mixin Pattern: Design Philosophy\n\n### What is a Mixin?\n\nA mixin is a class designed to **provide methods to other classes** without being a base class in a traditional inheritance hierarchy. Unlike standard inheritance (which models \"is-a\" relationships), mixins model **\"can-do\" capabilities**.\n\n```python\n# Traditional inheritance: \"is-a\"\nclass Animal:\n    def breathe(self): ...\n\nclass Dog(Animal):  # Dog IS-A Animal\n    def bark(self): ...\n\n# Mixin pattern: \"can-do\"\nclass FlyableMixin:\n    def fly(self): ...\n\nclass Bird(Animal, FlyableMixin):  # Bird IS-A Animal, CAN fly\n    pass\n```\n\n### Why Mixins Over Inheritance?\n\nPydoll faces a specific architectural challenge:\n\n- **`Tab`** needs to find elements in the **document context**\n- **`WebElement`** needs to find elements **relative to itself** (child elements)\n- Both need **identical selector logic** (CSS, XPath, attribute building)\n\n**Option 1: Shared Base Class**\n\n```python\nclass ElementLocator:\n    def find(...): ...\n\nclass Tab(ElementLocator):\n    pass\n\nclass WebElement(ElementLocator):\n    pass\n```\n\n**Problems:**\n- Tight coupling: `Tab` and `WebElement` now share inheritance hierarchy\n- Violates Single Responsibility: `Tab` shouldn't inherit from same class as `WebElement`\n- Hard to extend: Adding new capabilities requires modifying base class\n\n**Option 2: Mixin Pattern (Chosen Approach)**\n\n```python\nclass FindElementsMixin:\n    def find(...): ...\n    def query(...): ...\n\nclass Tab(FindElementsMixin):\n    # Tab-specific logic\n    pass\n\nclass WebElement(FindElementsMixin):\n    # WebElement-specific logic\n    pass\n```\n\n**Benefits:**\n\n- **Decoupling**: `Tab` and `WebElement` remain independent\n- **Reusability**: Same element-finding logic in both classes\n- **Composability**: Can add other mixins without conflicts\n- **Testability**: Mixin can be tested in isolation\n\n!!! tip \"Mixin Characteristics\"\n    1. **Stateless**: Mixins don't maintain their own state (no `__init__`)\n    2. **Dependency Injection**: Assumes consuming class provides dependencies (e.g., `_connection_handler`)\n    3. **Single Purpose**: Each mixin provides one cohesive capability\n    4. **Not Instantiable**: Never create `FindElementsMixin()` directly\n\n## Mixin Implementation in Pydoll\n\n### Class Structure\n\nThe FindElementsMixin uses **dependency injection** to work with any class that provides a `_connection_handler`:\n\n```python\nclass FindElementsMixin:\n    \"\"\"\n    Mixin providing element finding capabilities.\n    \n    Assumes the consuming class has:\n    - _connection_handler: ConnectionHandler instance for CDP commands\n    - _object_id: Optional[str] for context-relative searches (WebElement only)\n    \"\"\"\n    \n    if TYPE_CHECKING:\n        _connection_handler: ConnectionHandler  # Type hint, not actual attribute\n    \n    async def find(self, ...):\n        # Implementation uses self._connection_handler\n        # Checks for self._object_id to determine context\n```\n\n**Key insight:** The mixin doesn't define `_connection_handler` or `_object_id`. It **assumes** they exist via duck typing.\n\n### How Tab and WebElement Use the Mixin\n\n```python\n# Tab: Document-level searches\nclass Tab(FindElementsMixin):\n    def __init__(self, browser, target_id, connection_port):\n        self._connection_handler = ConnectionHandler(connection_port)\n        # No _object_id → searches from document root\n\n# WebElement: Element-relative searches\nclass WebElement(FindElementsMixin):\n    def __init__(self, object_id, connection_handler, ...):\n        self._object_id = object_id  # CDP object ID\n        self._connection_handler = connection_handler\n        # Has _object_id → searches relative to this element\n```\n\n**Critical distinction:**\n\n- **Tab**: `hasattr(self, '_object_id')` → `False` → uses `RuntimeCommands.evaluate()` (document context)\n- **WebElement**: `hasattr(self, '_object_id')` → `True` → uses `RuntimeCommands.call_function_on()` (element context)\n\n### Context Detection\n\nThe mixin dynamically detects search context:\n\n```python\nasync def _find_element(self, by, value, raise_exc=True):\n    if hasattr(self, '_object_id'):\n        # Relative search: call JavaScript function on THIS element\n        command = self._get_find_element_command(by, value, self._object_id)\n    else:\n        # Document search: evaluate JavaScript in global context\n        command = self._get_find_element_command(by, value)\n    \n    response = await self._execute_command(command)\n    # ...\n```\n\nThis single implementation handles both:\n\n- `tab.find(id='submit')` → searches entire document\n- `form_element.find(id='submit')` → searches within `form_element`\n\n!!! warning \"Mixin Dependency Coupling\"\n    The mixin is **tightly coupled** to CDP's object model. It assumes:\n    \n    - Elements are represented by `objectId` strings\n    - `Runtime.evaluate()` for document searches\n    - `Runtime.callFunctionOn()` for element-relative searches\n    \n    This is acceptable because Pydoll is **CDP-specific**. A more generic design would require abstraction layers.\n\n## Public API Design\n\nThe mixin exposes two high-level methods with distinct design philosophies:\n\n### find(): Attribute-Based Selection\n\n```python\n@overload\nasync def find(self, find_all: Literal[False], ...) -> WebElement: ...\n\n@overload\nasync def find(self, find_all: Literal[True], ...) -> list[WebElement]: ...\n\nasync def find(\n    self,\n    id: Optional[str] = None,\n    class_name: Optional[str] = None,\n    name: Optional[str] = None,\n    tag_name: Optional[str] = None,\n    text: Optional[str] = None,\n    timeout: int = 0,\n    find_all: bool = False,\n    raise_exc: bool = True,\n    **attributes,\n) -> Union[WebElement, list[WebElement], None]:\n```\n\n**Design decisions:**\n\n1. **Kwargs over positional By enum**:\n   ```python\n   # Pydoll (intuitive)\n   await tab.find(id='submit', class_name='primary')\n   \n   # Selenium (verbose)\n   driver.find_element(By.ID, 'submit')  # Can't combine attributes easily\n   ```\n\n2. **Auto-resolution to optimal selector**:\n   - Single attribute → uses `By.ID`, `By.CLASS_NAME`, etc. (fastest)\n   - Multiple attributes → builds XPath (flexible but slower)\n\n3. **`**attributes` for extensibility**:\n   ```python\n   await tab.find(data_testid='submit-btn', aria_label='Submit form')\n   # Builds: //\\*[@data-testid='submit-btn' and @aria-label='Submit form']\n   ```\n\n### query(): Expression-Based Selection\n\n```python\n@overload\nasync def query(self, expression, find_all: Literal[False], ...) -> WebElement: ...\n\n@overload\nasync def query(self, expression, find_all: Literal[True], ...) -> list[WebElement]: ...\n\nasync def query(\n    self, \n    expression: str, \n    timeout: int = 0, \n    find_all: bool = False, \n    raise_exc: bool = True\n) -> Union[WebElement, list[WebElement], None]:\n```\n\n**Design decisions:**\n\n1. **Auto-detect CSS vs XPath**:\n   ```python\n   # XPath detection (starts with / or ./)\n   await tab.query(\"//div[@id='content']\")\n   \n   # CSS detection (default)\n   await tab.query(\"div#content > p.intro\")\n   ```\n\n2. **Single expression parameter** (unlike `find()`):\n   - Assumes user knows selector syntax\n   - No abstraction overhead\n\n3. **Direct passthrough to browser**:\n   - `querySelector()` / `querySelectorAll()` for CSS\n   - `document.evaluate()` for XPath\n\n### Overload Pattern for Type Safety\n\nBoth methods use `@overload` to provide **precise return types**:\n\n```python\n# IDE knows return type is WebElement\nelement = await tab.find(id='submit')\n\n# IDE knows return type is list[WebElement]\nelements = await tab.find(class_name='item', find_all=True)\n\n# IDE knows return type is Optional[WebElement]\nmaybe_element = await tab.find(id='optional', raise_exc=False)\n```\n\nThis is critical for IDE autocomplete and type checking. See [Type System Deep Dive](./typing-system.md) for details.\n\n## Selector Resolution Architecture\n\nThe mixin converts user input into CDP commands through a resolution pipeline:\n\n| Stage | Input | Output | Key Decision |\n|-------|-------|--------|-------------|\n| **1. Method Selection** | `find()` kwargs or `query()` expression | Selector strategy | Attribute-based vs expression-based |\n| **2. Strategy Resolution** | Attributes or expression | `By` enum + value | Single attr → native method, Multiple → XPath |\n| **3. Context Detection** | `By` + value + `hasattr(_object_id)` | CDP command type | Document vs element-relative search |\n| **4. Command Generation** | CDP command type + selector | JavaScript + CDP method | `evaluate()` vs `callFunctionOn()` |\n| **5. Execution** | CDP command | `objectId` or array of `objectId`s | Via ConnectionHandler |\n| **6. WebElement Creation** | `objectId` + attributes | `WebElement` instance(s) | Factory function to avoid circular imports |\n\n### Key Architectural Decisions\n\n**1. Single vs Multiple Attributes**\n\n```python\n# Single attribute → Direct selector (fast)\nawait tab.find(id='username')  # Uses By.ID → getElementById()\n\n# Multiple attributes → XPath (flexible)\nawait tab.find(tag_name='input', type='password', name='pwd')\n# → //input[@type='password' and @name='pwd']\n```\n\n**Why this matters:**\n- Native methods (`getElementById`, `getElementsByClassName`) are 10-50% faster than XPath\n- XPath overhead is acceptable when combining attributes (no alternative)\n\n**2. Auto-Detection of Selector Type**\n\n```python\nawait tab.query(\"//div\")       # Starts with / → XPath\nawait tab.query(\"#login\")      # Default → CSS\n```\n\n**Implementation:**\n```python\nif expression.startswith(('./', '/', '(/')):\n    return By.XPATH\nreturn By.CSS_SELECTOR\n```\n\nHeuristic is **unambiguous** - CSS selectors cannot start with `/`.\n\n**3. XPath Relative Path Adjustment**\n\nFor element-relative searches, absolute XPath must be converted:\n\n```python\n# User provides: //div\n# For WebElement: .//div (relative to element, not document)\n\ndef _ensure_relative_xpath(xpath):\n    return f'.{xpath}' if not xpath.startswith('.') else xpath\n```\n\nWithout this, `element.find()` would search from document root.\n\n## CDP Command Generation\n\nThe mixin routes to different CDP methods based on search context:\n\n| Context | Selector Type | CDP Method | JavaScript Equivalent |\n|---------|--------------|------------|---------------------|\n| Document | CSS | `Runtime.evaluate` | `document.querySelector()` |\n| Document | XPath | `Runtime.evaluate` | `document.evaluate()` |\n| Element | CSS | `Runtime.callFunctionOn` | `this.querySelector()` |\n| Element | XPath | `Runtime.callFunctionOn` | `document.evaluate(..., this)` |\n\n**Key insight:** `Runtime.callFunctionOn` requires an `objectId` (the element to call on), while `Runtime.evaluate` executes in global scope.\n\n### JavaScript Templates\n\nPydoll uses pre-defined templates for consistency and performance:\n\n```python\n# CSS selectors\nScripts.QUERY_SELECTOR = 'document.querySelector(\"{selector}\")'\nScripts.RELATIVE_QUERY_SELECTOR = 'this.querySelector(\"{selector}\")'\n\n# XPath expressions\nScripts.FIND_XPATH_ELEMENT = '''\n    document.evaluate(\"{escaped_value}\", document, null,\n                      XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue\n'''\n```\n\nTemplates avoid runtime string concatenation and centralize JavaScript code.\n\n## Object ID Resolution and WebElement Creation\n\nCDP represents DOM nodes as **`objectId` strings**. The mixin abstracts this:\n\n**Single element flow:**\n1. Execute CDP command → Extract `objectId` from response\n2. Call `DOM.describeNode(objectId)` → Get attributes, tag name\n3. Create `WebElement(objectId, connection_handler, attributes)`\n\n**Multiple elements flow:**\n1. Execute CDP command → Returns **array as single remote object**\n2. Call `Runtime.getProperties(array_objectId)` → Enumerate array indices\n3. Extract individual `objectId` for each element\n4. Describe and create `WebElement` for each\n\n**Why `Runtime.getProperties`?** CDP doesn't return arrays directly - it returns a **reference to an array object**. We must enumerate its properties to extract individual elements.\n\n## Architectural Insights and Design Tradeoffs\n\n### Why Kwargs Instead of By Enum?\n\n**Pydoll's choice:**\n```python\nawait tab.find(id='submit', class_name='primary')\n```\n\n**Selenium's approach:**\n```python\ndriver.find_element(By.ID, 'submit')  # Can't combine attributes\n```\n\n**Rationale:**\n\n- **Discoverability**: IDE autocomplete shows all available parameters\n- **Composability**: Can combine multiple attributes in one call\n- **Readability**: `id='submit'` is more intuitive than `(By.ID, 'submit')`\n\n**Tradeoff:** Kwargs are less explicit about selector strategy. Solved by documentation and logging.\n\n### Why Auto-Detect CSS vs XPath?\n\nThe `_get_expression_type()` heuristic eliminates user burden:\n\n```python\nawait tab.query(\"//div\")       # Auto: XPath\nawait tab.query(\"#login\")      # Auto: CSS\nawait tab.query(\"div > p\")     # Auto: CSS\n```\n\n**Benefits:**\n\n- **Ergonomics**: Users don't need to specify selector type\n- **Correctness**: Impossible to misuse (XPath with CSS method, vice versa)\n\n**Limitation:** No way to force CSS interpretation of ambiguous selectors (rare edge case).\n\n### Circular Import Prevention: create_web_element()\n\nThe mixin uses a **factory function** to avoid circular imports:\n\n```python\ndef create_web_element(*args, **kwargs):\n    \"\"\"Dynamically import WebElement at runtime.\"\"\"\n    from pydoll.elements.web_element import WebElement  # Late import\n    return WebElement(*args, **kwargs)\n```\n\n**Why needed?**\n\n- `FindElementsMixin` → needs to create `WebElement`\n- `WebElement` → inherits from `FindElementsMixin`\n- Circular dependency!\n\n**Solution:** Late import inside factory function. Import only executes when function is called, breaking the cycle.\n\n### hasattr() for Context Detection: Elegant or Hacky?\n\nThe mixin uses `hasattr(self, '_object_id')` to detect Tab vs WebElement:\n\n```python\nif hasattr(self, '_object_id'):\n    # WebElement: element-relative search\nelse:\n    # Tab: document-level search\n```\n\n**Is this \"hacky\"?**\n\n- **No**: It's **duck typing** (Pythonic idiom)\n- Mixin doesn't need to know class hierarchy\n- Both Tab and WebElement provide `_connection_handler`\n- WebElement additionally provides `_object_id`\n\n**Alternative approaches:**\n\n1. **Type checking**: `if isinstance(self, WebElement)` → Couples mixin to WebElement\n2. **Abstract method**: Requires Tab/WebElement to implement `get_search_context()` → More boilerplate\n3. **Dependency injection**: Pass context as parameter → Breaks API ergonomics\n\n**Verdict:** `hasattr()` is the best solution for this use case.\n\n## Key Takeaways\n\n1. **Mixins enable code sharing** without coupling `Tab` and `WebElement` through inheritance\n2. **Context detection via duck typing** (`hasattr`) keeps mixin decoupled from class hierarchy\n3. **Auto-resolution optimizes performance** by using native methods for single attributes\n4. **XPath building provides composability** for multi-attribute queries\n5. **Polling-based waiting is simple** but trades CPU cycles for implementation simplicity\n6. **CDP object model complexity** is hidden behind WebElement abstraction\n7. **Type safety via overloads** provides precise return types for IDE support\n\n## Related Documentation\n\nFor deeper understanding of related architectural components:\n\n- **[Type System](./typing-system.md)**: Overload pattern, TypedDict, Generic types\n- **[WebElement Domain](./webelement-domain.md)**: WebElement architecture and interaction methods\n- **[Selectors Guide](./selectors-guide.md)**: CSS vs XPath syntax and best practices\n- **[Tab Domain](./tab-domain.md)**: Tab-level operations and context management\n\nFor practical usage patterns:\n\n- **[Element Finding Guide](../features/automation/element-finding.md)**: Practical examples and patterns\n- **[Human-Like Interactions](../features/automation/human-interactions.md)**: Realistic element interaction"
  },
  {
    "path": "docs/en/deep-dive/architecture/index.md",
    "content": "# Internal Architecture\n\n**Understand the design, then break the rules intentionally.**\n\nMost documentation shows you **what** a framework does. This section reveals **how** and **why** Pydoll is architected the way it is: the design patterns, architectural decisions, and tradeoffs that shape every line of code.\n\n## Why Architecture Matters\n\nYou can use Pydoll effectively without understanding its internal architecture. But when you need to:\n\n- **Debug** complex issues that span multiple components\n- **Optimize** performance bottlenecks in large-scale automation\n- **Extend** Pydoll with custom functionality\n- **Contribute** improvements to the codebase\n- **Build** similar tools for different use cases\n\n...architectural knowledge becomes **indispensable**.\n\n!!! quote \"Architecture as Language\"\n    **\"Architecture is frozen music.\"** - Johann Wolfgang von Goethe\n    \n    Good architecture isn't just about making code work, it's about making code **understandable**, **maintainable**, and **extensible**. Understanding Pydoll's architecture teaches you patterns you'll apply to every project.\n\n## The Six Architectural Domains\n\nPydoll's architecture is organized into **six cohesive domains**, each with clear responsibilities and interfaces:\n\n### 1. Browser Domain\n**[→ Explore Browser Architecture](./browser-domain.md)**\n\n**The orchestrator: managing processes, contexts, and global state.**\n\nThe Browser domain sits at the top of the hierarchy, coordinating:\n\n- **Process management**: Launching/terminating browser executables\n- **Browser contexts**: Isolated environments (like incognito windows)\n- **Tab registry**: Singleton pattern for Tab instances\n- **Proxy authentication**: Automatic auth via Fetch domain\n- **Global operations**: Downloads, permissions, window management\n\n**Key architectural patterns**:\n\n- **Abstract base class** for Chrome/Edge/other Chromium browsers\n- **Manager pattern** (ProcessManager, ProxyManager, TempDirManager)\n- **Singleton registry** for Tab instances (prevents duplicates)\n- **Context manager protocol** for automatic cleanup\n\n**Critical insight**: The Browser doesn't directly manipulate pages, it **coordinates** lower-level components. This separation of concerns enables multi-browser support and concurrent tab operations.\n\n---\n\n### 2. Tab Domain\n**[→ Explore Tab Architecture](./tab-domain.md)**\n\n**The workhorse: executing commands, managing state, coordinating automation.**\n\nThe Tab domain is Pydoll's primary interface, handling:\n\n- **Navigation**: Page loading with configurable wait states\n- **Element finding**: Delegated to FindElementsMixin\n- **JavaScript execution**: Both page and element contexts\n- **Event coordination**: Tab-specific event listeners\n- **Network monitoring**: Request/response capture and analysis\n- **IFrame handling**: Nested context management\n\n**Key architectural patterns**:\n\n- **Façade pattern**: Simplified interface to complex CDP operations\n- **Mixin composition**: FindElementsMixin for element location\n- **Per-tab WebSocket**: Independent connections for parallelism\n- **State flags**: Track enabled domains (network_events_enabled, etc.)\n- **Lazy initialization**: Request object created on first access\n\n**Critical insight**: Each Tab owns its **own ConnectionHandler**, enabling true parallel operations across tabs without contention or state leakage.\n\n---\n\n### 3. WebElement Domain\n**[→ Explore WebElement Architecture](./webelement-domain.md)**\n\n**The interactor: bridging Python code and DOM elements.**\n\nThe WebElement domain represents **individual DOM elements**, providing:\n\n- **Interaction methods**: Click, type, scroll, select\n- **Property access**: Text, HTML, bounds, attributes\n- **State queries**: Visibility, enabled status, value\n- **Screenshots**: Element-specific image capture\n- **Child finding**: Relative element location (also via FindElementsMixin)\n\n**Key architectural patterns**:\n\n- **Proxy pattern**: Python object representing remote browser element\n- **Object ID abstraction**: CDP's objectId hidden behind Python API\n- **Hybrid properties**: Sync (attributes) vs async (dynamic state)\n- **Command pattern**: Interaction methods wrap CDP commands\n- **Fallback strategies**: Multiple approaches for robustness\n\n**Critical insight**: WebElement maintains **both cached attributes** (from creation) and **dynamic state** (fetched on demand), balancing performance with freshness.\n\n---\n\n### 4. FindElements Mixin\n**[→ Explore FindElements Architecture](./find-elements-mixin.md)**\n\n**The locator: translating selectors into DOM queries.**\n\nThe FindElementsMixin provides element-finding capabilities to both Tab and WebElement through **composition**, not inheritance:\n\n- **Attribute-based finding**: `find(id='submit', class_name='btn')`\n- **Expression-based querying**: `query('div.container > p')`\n- **Strategy resolution**: Optimal selector for single vs. multiple attributes\n- **Waiting mechanisms**: Polling with configurable timeouts\n- **Context detection**: Document vs. element-relative searches\n\n**Key architectural patterns**:\n- **Mixin pattern**: Shared capability without inheritance hierarchy\n- **Strategy pattern**: Different selector strategies based on input\n- **Template method**: Common flow, strategy-specific implementation\n- **Factory function**: Late import to avoid circular dependencies\n- **Overload pattern**: Type-safe return types (WebElement vs list)\n\n**Critical insight**: The mixin uses **duck typing** (`hasattr(self, '_object_id')`) to detect Tab vs WebElement, enabling code reuse without tight coupling.\n\n---\n\n### 5. Event Architecture\n**[→ Explore Event Architecture](./event-architecture.md)**\n\n**The dispatcher: routing browser events to Python callbacks.**\n\nThe Event Architecture enables reactive automation through:\n\n- **Event registration**: `on()` method for subscribing to CDP events\n- **Callback dispatch**: Async execution without blocking\n- **Domain management**: Explicit enable/disable for performance\n- **Temporary callbacks**: Auto-removal after first invocation\n- **Multi-level scope**: Browser-wide vs tab-specific events\n\n**Key architectural patterns**:\n\n- **Observer pattern**: Subscribe/notify for event-driven code\n- **Registry pattern**: Event name → callback list mapping\n- **Wrapper pattern**: Auto-wrap sync callbacks for async execution\n- **Cleanup protocol**: Automatic callback removal on tab close\n- **Scope isolation**: Independent event contexts per tab\n\n**Critical insight**: Events are **push-based** (browser notifies Python), not poll-based, enabling low-latency reactive automation without busy-waiting.\n\n---\n\n### 6. Browser Requests Architecture\n**[→ Explore Requests Architecture](./browser-requests-architecture.md)**\n\n**The hybrid: HTTP requests with browser session state.**\n\nThe Browser Requests system bridges HTTP and browser automation:\n\n- **Session continuity**: Cookies and auth automatically included\n- **Dual data sources**: JavaScript Fetch API + CDP network events\n- **Complete metadata**: Headers, cookies, timing (not all available via JavaScript)\n- **`requests`-like API**: Familiar interface with browser power\n\n**Key architectural patterns**:\n\n- **Hybrid execution**: JavaScript for body, CDP for metadata\n- **Temporary event registration**: Enable/capture/disable pattern\n- **Lazy property initialization**: Request object created on first use\n- **Adapter pattern**: Requests-compatible interface to browser fetch\n\n**Critical insight**: Browser requests combine **two information sources** (JavaScript and CDP events). JavaScript provides the response body, CDP provides headers and cookies that JavaScript security policies hide.\n\n---\n\n## Architectural Principles\n\nThese six domains follow consistent principles:\n\n### 1. Separation of Concerns\nEach domain has a **single, well-defined responsibility**:\n\n- Browser → Process/context management\n- Tab → Command execution and state\n- WebElement → Element interaction\n- FindElements → Element location\n- Events → Reactive dispatch\n- Requests → HTTP in browser context\n\n**Benefit**: Changes in one domain rarely require changes in others.\n\n### 2. Composition Over Inheritance\nInstead of deep inheritance hierarchies, Pydoll uses:\n\n- **Mixins** (FindElementsMixin shared by Tab and WebElement)\n- **Managers** (ProcessManager, ProxyManager, TempDirManager)\n- **Dependency injection** (ConnectionHandler passed to components)\n\n**Benefit**: Flexible component reuse without tight coupling.\n\n### 3. Async by Default\nAll I/O operations are `async def` and must be `await`ed:\n\n- WebSocket communication\n- CDP command execution\n- Event callback dispatch\n- Network requests\n\n**Benefit**: Enables true concurrency with multiple tabs, parallel operations, and non-blocking I/O.\n\n### 4. Type Safety\nEvery public API has type annotations:\n\n- Function parameters and return types\n- CDP responses as `TypedDict`\n- Event types for callback parameters\n- Overloads for polymorphic methods\n\n**Benefit**: IDE autocomplete, static type checking, self-documenting code.\n\n### 5. Resource Management\nContext managers ensure cleanup:\n\n- `async with Browser()` → closes browser on exit\n- `async with tab.expect_file_chooser()` → disables interceptor\n- `async with tab.expect_download()` → cleans temp files\n\n**Benefit**: Automatic resource cleanup, prevents leaks even on exceptions.\n\n## Component Interaction\n\nUnderstanding how domains interact is key:\n\n```mermaid\ngraph TB\n    User[Your Python Code]\n    \n    User --> Browser[Browser Domain]\n    User --> Tab[Tab Domain]\n    User --> Element[WebElement Domain]\n    \n    Browser --> ProcessMgr[Process Manager]\n    Browser --> ContextMgr[Context Manager]\n    Browser --> TabRegistry[Tab Registry]\n    \n    Tab --> ConnHandler[Connection Handler]\n    Tab --> FindMixin[FindElements Mixin]\n    Tab --> EventSystem[Event System]\n    Tab --> RequestSystem[Request System]\n    \n    Element --> ConnHandler2[Connection Handler]\n    Element --> FindMixin2[FindElements Mixin]\n    \n    ConnHandler --> WebSocket[WebSocket to CDP]\n    ConnHandler2 --> WebSocket\n    EventSystem --> ConnHandler\n    RequestSystem --> ConnHandler\n    RequestSystem --> EventSystem\n    \n    WebSocket --> Chrome[Chrome Browser]\n```\n\n**Key interactions**:\n\n1. **Browser creates Tabs** → Tabs stored in registry\n2. **Tab and WebElement both use FindElementsMixin** → Shared element location\n3. **Each Tab owns a ConnectionHandler** → Independent WebSocket connections\n4. **Request system uses Event system** → Network events capture metadata\n5. **All components use ConnectionHandler** → Centralized CDP communication\n\n## Prerequisites\n\nTo fully benefit from this section:\n\n- **[Core Fundamentals](../fundamentals/cdp.md)** - Understand CDP, async, and types\n- **Python design patterns** - Familiarity with common patterns\n- **OOP concepts** - Classes, inheritance, composition, interfaces\n- **Async Python** - Comfortable with `async def` and `await`  \n\n**If you haven't read Fundamentals**, start there first. Architecture builds on those concepts.\n\n## Beyond Architecture\n\nAfter mastering internal architecture, you'll be ready for:\n\n- **Contributing code**: Understand where new features fit\n- **Performance optimization**: Identify bottlenecks and inefficiencies\n- **Custom extensions**: Build on Pydoll's patterns\n- **Similar tools**: Apply these patterns to other projects\n\n## Philosophy of Design\n\nGood architecture is **invisible**, it shouldn't get in your way. Pydoll's architecture prioritizes:\n\n1. **Simplicity**: Each component does one thing well\n2. **Consistency**: Similar operations have similar patterns\n3. **Explicitness**: No magic, no hidden behavior\n4. **Type safety**: Catch errors at design time, not runtime\n5. **Performance**: Async by default, parallelism without locks\n\nThese aren't arbitrary choices, they're **battle-tested principles** from decades of software engineering.\n\n---\n\n## Ready to Understand the Design?\n\nStart with **[Browser Domain](./browser-domain.md)** to understand how process management and context isolation work, then progress through the domains in order.\n\n**This is where usage becomes mastery.**\n\n---\n\n!!! success \"After Completing Architecture\"\n    Once you understand these patterns, you'll see them everywhere in software engineering, not just Pydoll. These are **universal patterns** applied to browser automation:\n    \n    - Façade (Tab simplifies CDP complexity)\n    - Observer (Event system for reactive code)\n    - Mixin (FindElementsMixin for code reuse)\n    - Registry (Browser tracks Tab instances)\n    - Strategy (FindElements resolves optimal selectors)\n    \n    Good architecture is **timeless knowledge**.\n"
  },
  {
    "path": "docs/en/deep-dive/architecture/shadow-dom.md",
    "content": "# Shadow DOM Architecture\n\nThe Shadow DOM is one of the most challenging aspects of modern web automation. Elements inside shadow trees are invisible to regular DOM queries, which breaks traditional automation approaches. This document explains how Shadow DOM works at the browser level, why conventional tools fail with closed shadow roots, and how Pydoll bypasses these restrictions through direct CDP access.\n\n!!! info \"Practical Usage Guide\"\n    For usage examples and quick-start patterns, see the [Element Finding Guide — Shadow DOM section](../../features/element-finding.md#shadow-dom-support).\n\n## What is Shadow DOM?\n\nShadow DOM is a web standard that enables **DOM encapsulation**. It allows a component to have its own isolated DOM tree (the \"shadow tree\") attached to a regular DOM element (the \"shadow host\"). Elements inside a shadow tree are hidden from the main document's queries.\n\n```mermaid\ngraph TB\n    subgraph \"Main DOM (Light DOM)\"\n        Document[\"document\"]\n        Host[\"div#my-component\\n(shadow host)\"]\n        Other[\"p.normal-content\"]\n    end\n\n    subgraph \"Shadow Tree (Encapsulated)\"\n        SR[\"#shadow-root (open)\"]\n        Style[\"style\"]\n        Button[\"button.internal\"]\n        Input[\"input.private\"]\n    end\n\n    Document --> Host\n    Document --> Other\n    Host -.->|\"attachShadow()\"| SR\n    SR --> Style\n    SR --> Button\n    SR --> Input\n```\n\n### Shadow Root Modes\n\nWhen a component creates a shadow root via `attachShadow()`, it specifies a **mode**:\n\n| Mode | JavaScript Access | CDP Access | Common Usage |\n|------|-------------------|------------|--------------|\n| `open` | `element.shadowRoot` returns the root | Full access via `backendNodeId` | Custom web components (Lit, Stencil) |\n| `closed` | `element.shadowRoot` returns `null` | Full access via `backendNodeId` | Security-sensitive components, payment forms |\n| `user-agent` | Not accessible via JS | Limited access | Browser-internal UI (input placeholders, video controls) |\n\nThis distinction is critical: **JavaScript-level access is restricted by mode, but CDP-level access is not.**\n\n### Why Regular Automation Fails\n\nTraditional automation tools rely on JavaScript execution in the page context:\n\n```javascript\n// WebDriver / Selenium approach\ndocument.querySelector('#my-component')        // ✓ Finds the host\ndocument.querySelector('#my-component button') // ✗ Cannot cross shadow boundary\nelement.shadowRoot                             // ✗ Returns null for closed roots\n```\n\nThe shadow boundary is enforced by the browser's JavaScript engine. Any automation tool that executes JavaScript to find elements will hit this wall. This includes Selenium, Playwright's `page.evaluate()`, and any tool using `Runtime.evaluate()` with `document.querySelector()` at the document level.\n\n## How Pydoll Bypasses Shadow Boundaries\n\nPydoll's approach works at a layer **below JavaScript**: the Chrome DevTools Protocol. CDP has direct access to the browser's internal DOM representation, which ignores shadow mode restrictions entirely.\n\n### The CDP Advantage\n\n```mermaid\nsequenceDiagram\n    participant User as User Code\n    participant SR as ShadowRoot\n    participant CH as ConnectionHandler\n    participant CDP as Chrome CDP\n    participant DOM as Browser DOM\n\n    User->>SR: shadow_root.query('.btn')\n    SR->>SR: _get_find_element_command(object_id)\n    SR->>CH: execute_command(Runtime.callFunctionOn)\n    CH->>CDP: WebSocket send\n    CDP->>DOM: Execute querySelector on shadow root object\n    DOM-->>CDP: Element result\n    CDP-->>CH: Response with objectId\n    CH-->>SR: Element data\n    SR-->>User: WebElement instance\n```\n\nThe key insight is in **how the shadow root object is obtained** and **how queries are executed against it**:\n\n1. **Discovery**: `DOM.describeNode` with `pierce=true` returns shadow root nodes with their `backendNodeId`, regardless of mode\n2. **Resolution**: `DOM.resolveNode` converts a `backendNodeId` to a JavaScript `objectId` that references the shadow root directly\n3. **Querying**: `Runtime.callFunctionOn` executes `this.querySelector()` on the shadow root's `objectId`; this works because the call is made **on the shadow root object itself**, not from the document context\n\n### Step-by-Step: Shadow Root Access\n\n```mermaid\nflowchart TD\n    A[\"WebElement\\n(shadow host)\"]\n    B[\"shadowRoots[] with\\nbackendNodeId\"]\n    C[\"JavaScript objectId\\nfor shadow root\"]\n    D[\"ShadowRoot instance\"]\n    E[\"WebElement\\n(inside shadow)\"]\n\n    A -->|\"DOM.describeNode\\ndepth=1, pierce=true\"| B\n    B -->|\"DOM.resolveNode\\nbackendNodeId\"| C\n    C -->|\"Create ShadowRoot\\nwith objectId\"| D\n    D -->|\"find() / query()\\nvia callFunctionOn\"| E\n```\n\n#### Step 1: Describe the Host Node\n\n```python\n# Pydoll sends this CDP command:\n{\n    \"method\": \"DOM.describeNode\",\n    \"params\": {\n        \"objectId\": \"<host-element-object-id>\",\n        \"depth\": 1,\n        \"pierce\": true  # ← This is the key flag\n    }\n}\n```\n\nThe `pierce` parameter tells CDP to traverse shadow boundaries when describing the node. The response includes shadow root information regardless of the shadow root mode:\n\n```json\n{\n    \"result\": {\n        \"node\": {\n            \"nodeName\": \"DIV\",\n            \"shadowRoots\": [\n                {\n                    \"nodeId\": 0,\n                    \"backendNodeId\": 5,\n                    \"shadowRootType\": \"closed\",\n                    \"childNodeCount\": 4\n                }\n            ]\n        }\n    }\n}\n```\n\n!!! warning \"nodeId vs backendNodeId\"\n    When the DOM domain is not explicitly enabled (which is Pydoll's default to minimize overhead), `nodeId` is always `0`. The `backendNodeId` is the stable, always-available identifier. Pydoll uses `backendNodeId` exclusively for shadow root resolution, which is why it works without requiring `DOM.enable()`.\n\n#### Step 2: Resolve to JavaScript Object\n\n```python\n# Convert backendNodeId to a usable objectId:\n{\n    \"method\": \"DOM.resolveNode\",\n    \"params\": {\n        \"backendNodeId\": 5\n    }\n}\n```\n\nThe response provides an `objectId`, a handle to the shadow root in JavaScript's object space:\n\n```json\n{\n    \"result\": {\n        \"object\": {\n            \"objectId\": \"-2296764575741119861.1.3\"\n        }\n    }\n}\n```\n\n#### Step 3: Query Within the Shadow Root\n\nWith the shadow root's `objectId`, Pydoll leverages `FindElementsMixin`'s existing relative search mechanism:\n\n```python\n# When ShadowRoot.query('.btn') is called:\n{\n    \"method\": \"Runtime.callFunctionOn\",\n    \"params\": {\n        \"functionDeclaration\": \"function() { return this.querySelector(\\\".btn\\\"); }\",\n        \"objectId\": \"-2296764575741119861.1.3\"\n    }\n}\n```\n\nThe function runs with `this` bound to the shadow root object. Since shadow roots implement the `querySelector()` and `querySelectorAll()` interfaces natively, CSS selectors work naturally within the shadow boundary.\n\n## ShadowRoot Architecture\n\n### Design Decision: Reuse FindElementsMixin\n\nThe most critical architectural decision was making `ShadowRoot` inherit from `FindElementsMixin`:\n\n```python\nclass ShadowRoot(FindElementsMixin):\n    def __init__(self, object_id, connection_handler, mode, host_element):\n        self._object_id = object_id               # Shadow root CDP reference\n        self._connection_handler = connection_handler  # For CDP communication\n        self._mode = mode                          # ShadowRootType enum\n        self._host_element = host_element          # Back-reference to host\n```\n\n**Why this works**: `FindElementsMixin._find_element()` checks `hasattr(self, '_object_id')`. When present, it uses `RELATIVE_QUERY_SELECTOR`, which calls `this.querySelector()` on the referenced object. Since shadow roots support `querySelector()` natively, `query()` with CSS selectors works automatically without any shadow-specific code.\n\n```python\n# This single line in FindElementsMixin enables shadow root searches:\nelif hasattr(self, '_object_id'):\n    command = self._get_find_element_command(by, value, self._object_id)\n```\n\n`ShadowRoot` inherits `query()` and `find_or_wait_element()` from `FindElementsMixin`. However, `find()` and XPath-based `query()` are explicitly **blocked** on `ShadowRoot` (via the `_css_only` class flag) because shadow roots only support `querySelector()` / `querySelectorAll()` — XPath does not work inside shadow boundaries.\n\n!!! tip \"Architectural Consistency\"\n    This is the same mechanism that makes `WebElement.find()` search within an element's children: the `_object_id` attribute signals \"search relative to me\" rather than \"search the whole document.\" `ShadowRoot`, `WebElement`, and `Tab` all share element-finding behavior through `FindElementsMixin`, with `ShadowRoot` restricted to CSS selectors only.\n\n### Class Relationships\n\n| Class | Has `_object_id` | Has `_connection_handler` | Find Scope |\n|-------|:-:|:-:|---|\n| `Tab` | No | Yes | Entire document |\n| `WebElement` | Yes | Yes | Within element's subtree |\n| `ShadowRoot` | Yes | Yes | Within shadow tree |\n\nAll three inherit from `FindElementsMixin`. The presence or absence of `_object_id` determines whether searches are document-global or scoped to a specific node.\n\n### Resolving Shadow Roots: backendNodeId Strategy\n\nPydoll deliberately uses `backendNodeId` instead of `nodeId` for shadow root resolution:\n\n| Property | `nodeId` | `backendNodeId` |\n|----------|----------|-----------------|\n| Requires `DOM.enable()` | Yes | No |\n| Stable across describe calls | No (0 when DOM not enabled) | Yes |\n| Works for shadow root resolution | Only when DOM enabled | Always |\n| Performance overhead | Higher (DOM domain tracking) | None |\n\nBy relying on `backendNodeId`, Pydoll avoids the overhead of enabling the DOM domain while maintaining reliable shadow root access. This is a pragmatic choice: most automation scenarios don't need the DOM domain's event stream, and enabling it adds memory and processing overhead for tracking every DOM mutation.\n\n## Closed Shadow Roots: Why CDP Access Works\n\nThis is the most commonly asked question: **if `element.shadowRoot` returns `null` for closed shadow roots in JavaScript, how can CDP access them?**\n\nThe answer lies in understanding the browser's architecture:\n\n```mermaid\ngraph TB\n    subgraph \"JavaScript Runtime\"\n        JS[\"JavaScript Code\"]\n        API[\"Web APIs\\n(shadowRoot property)\"]\n    end\n\n    subgraph \"Browser Internals\"\n        CDP_Layer[\"CDP Protocol Layer\"]\n        DOM_Internal[\"Internal DOM Tree\"]\n    end\n\n    JS -->|\"element.shadowRoot\"| API\n    API -->|\"mode == 'closed'\\n→ return null\"| JS\n    CDP_Layer -->|\"DOM.describeNode\\npierce=true\"| DOM_Internal\n    DOM_Internal -->|\"Always returns\\nfull shadow tree\"| CDP_Layer\n```\n\n**JavaScript access** goes through the Web API layer, which enforces the shadow mode restriction. When `mode='closed'`, the API returns `null`; this is an intentional access control boundary for web page code.\n\n**CDP access** operates below the Web API layer. It communicates directly with the browser's internal DOM representation. The `closed` mode restriction is a **JavaScript-level policy**, not a **DOM-level restriction**. The shadow tree still exists in the DOM; it's just hidden from JavaScript's view.\n\n!!! info \"Security Implications\"\n    This is by design in the DevTools Protocol. CDP is intended for debugging and automation tools that need full DOM access. The `closed` mode protects shadow contents from other scripts on the same page (e.g., third-party scripts), not from the browser's debugging interface. This is the same reason browser DevTools can inspect closed shadow roots in the Elements panel.\n\n### Practical Verification\n\nYou can verify this behavior yourself:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.dom.types import ShadowRootType\n\nasync def verify_closed_access():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('about:blank')\n\n        # Create a closed shadow root via JavaScript\n        await tab.execute_script(\"\"\"\n            const host = document.createElement('div');\n            host.id = 'test-host';\n            document.body.appendChild(host);\n            const shadow = host.attachShadow({ mode: 'closed' });\n            shadow.innerHTML = '<p class=\"secret\">Hidden content</p>';\n        \"\"\")\n\n        # JavaScript cannot access it:\n        result = await tab.execute_script(\n            \"return document.getElementById('test-host').shadowRoot\",\n            return_by_value=True,\n        )\n        js_value = result['result']['result'].get('value')\n        print(f\"JS shadowRoot: {js_value}\")  # None\n\n        # But Pydoll can:\n        host = await tab.find(id='test-host')\n        shadow = await host.get_shadow_root()\n        print(f\"Shadow mode: {shadow.mode}\")  # ShadowRootType.CLOSED\n\n        secret = await shadow.query('.secret')\n        text = await secret.text\n        print(f\"Content: {text}\")  # \"Hidden content\"\n\nasyncio.run(verify_closed_access())\n```\n\n## Nested Shadow Roots\n\nWeb components frequently compose other web components, creating multi-level shadow trees:\n\n```mermaid\ngraph TB\n    subgraph \"Light DOM\"\n        Host1[\"outer-component\\n(shadow host)\"]\n    end\n\n    subgraph \"Outer Shadow Tree\"\n        SR1[\"#shadow-root (open)\"]\n        Host2[\"inner-component\\n(shadow host)\"]\n        P1[\"p.outer-text\"]\n    end\n\n    subgraph \"Inner Shadow Tree\"\n        SR2[\"#shadow-root (closed)\"]\n        Button[\"button.deep-btn\"]\n        P2[\"p.inner-text\"]\n    end\n\n    Host1 -.-> SR1\n    SR1 --> P1\n    SR1 --> Host2\n    Host2 -.-> SR2\n    SR2 --> P2\n    SR2 --> Button\n```\n\nPydoll handles this naturally by chaining `get_shadow_root()` calls. Each `ShadowRoot` produces `WebElement` instances that can themselves have shadow roots:\n\n```python\nouter_host = await tab.find(tag_name='outer-component')\nouter_shadow = await outer_host.get_shadow_root()        # open\n\ninner_host = await outer_shadow.query('inner-component')\ninner_shadow = await inner_host.get_shadow_root()        # closed, still works\n\ndeep_button = await inner_shadow.query('.deep-btn')\nawait deep_button.click()\n```\n\nEach level follows the same CDP resolution flow: `describeNode` then `resolveNode` then `ShadowRoot` with `_object_id` then `querySelector` via `callFunctionOn`.\n\n## Shadow Roots Inside IFrames\n\nA common real-world scenario involves shadow roots inside cross-origin iframes — for example, Cloudflare Turnstile captchas. This combines two isolation mechanisms: the iframe boundary and the shadow boundary.\n\n```mermaid\ngraph TB\n    subgraph \"Main Page\"\n        Host[\"div.widget\\n(shadow host)\"]\n    end\n\n    subgraph \"Shadow Tree\"\n        SR1[\"#shadow-root\"]\n        IFrame[\"iframe\\n(cross-origin)\"]\n    end\n\n    subgraph \"IFrame (OOPIF)\"\n        Body[\"body\"]\n    end\n\n    subgraph \"IFrame Shadow Tree\"\n        SR2[\"#shadow-root\"]\n        Button[\"label.checkbox\"]\n    end\n\n    Host -.-> SR1\n    SR1 --> IFrame\n    IFrame -.->|\"separate process\"| Body\n    Body -.-> SR2\n    SR2 --> Button\n```\n\nPydoll handles this transparently through **iframe context propagation**. When a `ShadowRoot` is created, it inherits the iframe routing context from its host element:\n\n```python\n# The full chain: main page → shadow root → iframe → shadow root → element\nshadow_host = await tab.find(id='widget-container')\nfirst_shadow = await shadow_host.get_shadow_root()\n\niframe = await first_shadow.query('iframe')\nbody = await iframe.find(tag_name='body')\nsecond_shadow = await body.get_shadow_root()\n\n# click() works correctly — mouse events route through the OOPIF session\nbutton = await second_shadow.query('label.checkbox')\nawait button.click()\n```\n\n### How Context Propagation Works\n\nCross-origin iframes run in a separate browser process (Out-of-Process IFrame, or OOPIF). CDP commands for these iframes must be routed through a dedicated `sessionId`. Pydoll propagates this routing context automatically through the entire chain:\n\n1. **IFrame resolves its context**: `iframe.find()` establishes an `IFrameContext` with `session_id` and `session_handler` for the OOPIF\n2. **Child elements inherit context**: Elements found inside the iframe receive the `IFrameContext`\n3. **Shadow roots inherit from host**: `ShadowRoot` copies its host element's `_iframe_context`\n4. **Elements in shadow inherit from shadow root**: Elements found via `shadow.query()` receive the propagated context\n5. **Commands route correctly**: `_execute_command()` detects the inherited context and routes CDP commands (including `Input.dispatchMouseEvent` for `click()`) through the OOPIF session\n\nThis means coordinates from `DOM.getBoxModel` (which are relative to the iframe viewport) are correctly paired with mouse events dispatched to the same OOPIF session.\n\n## Finding Shadow Roots: find_shadow_roots()\n\n`Tab.find_shadow_roots()` traverses the entire DOM tree to collect all shadow roots found on the page.\n\n### How It Works\n\n```\nTab.find_shadow_roots()\n  ├─ DOM.getDocument(depth=-1, pierce=true)\n  │   └─ Returns full DOM tree with shadowRoots arrays\n  ├─ Recursive tree walk: _collect_shadow_roots_from_tree()\n  │   ├─ Collects shadowRoots entries with host backendNodeId\n  │   ├─ Traverses children recursively\n  │   └─ Traverses contentDocument (same-origin iframes)\n  ├─ For each shadow root entry:\n  │   ├─ DOM.resolveNode(backendNodeId) → objectId\n  │   └─ Resolve host element (best-effort)\n  └─ Returns list[ShadowRoot] with host references\n```\n\n### Timeout: Waiting for Shadow Roots\n\nShadow hosts are often injected asynchronously. `Tab.find_shadow_roots()` accepts a `timeout` parameter that polls every 0.5s until at least one shadow root is found or the timeout expires (raises `WaitElementTimeout`). Similarly, `WebElement.get_shadow_root()` also supports `timeout` for waiting on a specific element's shadow root:\n\n```python\n# Wait up to 10 seconds for shadow roots to appear\nshadow_roots = await tab.find_shadow_roots(timeout=10)\n\n# Wait for a shadow root on a specific element\nshadow = await element.get_shadow_root(timeout=5)\n```\n\n### Key Details\n\n- **`pierce=True`** in `DOM.getDocument` causes the browser to include `shadowRoots` arrays in node descriptions, allowing discovery of all shadow roots without navigating to each host individually.\n- **Same-origin iframe content** is included in the tree via `contentDocument` nodes. The traversal handles these.\n- Each returned `ShadowRoot` has a reference to its `host_element` (resolved best-effort via `DOM.resolveNode`).\n\n### Deep Traversal: Cross-Origin IFrames (OOPIFs)\n\nBy default, cross-origin iframes (OOPIFs) are **not** included in the DOM tree — their content lives in a separate browser process. Pass `deep=True` to also discover shadow roots inside OOPIFs:\n\n```python\nshadow_roots = await tab.find_shadow_roots(deep=True, timeout=10)\n```\n\nWhen `deep=True` is set, the method performs additional steps:\n\n```\nTab.find_shadow_roots(deep=True)\n  ├─ ... (main document traversal as above) ...\n  └─ _collect_oopif_shadow_roots()\n      ├─ Browser-level ConnectionHandler (no page_id → browser endpoint)\n      ├─ Target.getTargets() → filter type='iframe'\n      └─ For each iframe target:\n          ├─ Target.attachToTarget(targetId, flatten=True) → sessionId\n          ├─ DOM.getDocument(depth=-1, pierce=True) with sessionId\n          ├─ _collect_shadow_roots_from_tree() on OOPIF DOM\n          └─ For each shadow root found:\n              ├─ DOM.resolveNode(backendNodeId) with sessionId\n              ├─ Resolve host element (best-effort) with sessionId\n              ├─ Create IFrameContext(frame_id, session_handler, session_id)\n              └─ Set IFrameContext on host element (or ShadowRoot directly)\n```\n\nThe returned `ShadowRoot` objects carry the OOPIF routing context (`IFrameContext`), so elements found via `shadow_root.query()` will automatically route CDP commands through the correct OOPIF session. This is critical for scenarios like Cloudflare Turnstile captchas, where the checkbox lives inside a closed shadow root within a cross-origin iframe.\n\n## Limitations and Edge Cases\n\n### Selector Strategies Inside Shadow Roots\n\n!!! warning \"CSS Selectors Only Inside Shadow Roots\"\n    `find()` and XPath are **not supported** on `ShadowRoot` and will raise `NotImplementedError`. Always use `query()` with CSS selectors to search inside shadow roots.\n\nShadow roots natively implement `querySelector()` and `querySelectorAll()`, but **not** XPath evaluation. Pydoll enforces this by blocking `find()` (which may generate XPath internally) and XPath-based `query()` on `ShadowRoot`:\n\n| Method | Inside Shadow Root | Notes |\n|--------|:--:|---|\n| `query('css-selector')` | Supported | The only supported approach |\n| `find(...)` | Not supported | Raises `NotImplementedError` |\n| `query('//xpath')` | Not supported | Raises `NotImplementedError` |\n\n```python\nshadow = await host.get_shadow_root()\n\n# Supported: query() with CSS selectors\nbutton = await shadow.query('button.submit')\nemail = await shadow.query('#email-input')\nitems = await shadow.query('.item', find_all=True)\n\n# Not supported: find() and XPath raise NotImplementedError\n# shadow.find(id='email-input')       # NotImplementedError\n# shadow.query('.//button')            # NotImplementedError\n```\n\n### XPath Cannot Cross Shadow Boundaries\n\nXPath expressions from the document root cannot traverse shadow boundaries. This is a fundamental limitation of XPath, which was designed before Shadow DOM existed:\n\n```python\n# Won't find shadow content: document-level XPath cannot cross the boundary\nelement = await tab.find(xpath='//div[@id=\"host\"]//button')\n```\n\n### User-Agent Shadow Roots\n\nBrowser-internal shadow roots (e.g., `<input>` placeholder styling, `<video>` controls) are of type `user-agent`. These are accessible via CDP but their internal structure varies across browser versions and is not part of any web standard.\n\n```python\ninput_element = await tab.find(tag_name='input')\ntry:\n    ua_shadow = await input_element.get_shadow_root()\n    # ua_shadow.mode == ShadowRootType.USER_AGENT\n    # Internal structure is browser-specific\nexcept ShadowRootNotFound:\n    pass  # Not all inputs have user-agent shadow roots\n```\n\n!!! warning \"User-Agent Shadow Root Stability\"\n    Do not build automation logic that depends on the internal structure of user-agent shadow roots. Their DOM structure is an implementation detail that can change between browser versions without notice.\n\n### Stale Shadow Root References\n\nIf the host element is removed from the DOM and re-added (common in single-page applications), the shadow root's `objectId` becomes stale. The solution is to re-acquire the shadow root:\n\n```python\n# After a page navigation or DOM rebuild:\nhost = await tab.find(id='my-component', timeout=5)  # Re-find the host\nshadow = await host.get_shadow_root()                 # Fresh shadow root\n```\n\n## Key Takeaways\n\n- **Shadow DOM encapsulation** hides elements from document-level `querySelector()`, breaking traditional automation\n- **CDP operates below the JavaScript API layer**, bypassing shadow mode restrictions entirely\n- **`backendNodeId`** is the stable identifier used for shadow root resolution, avoiding the need to enable the DOM domain\n- **`ShadowRoot` inherits `FindElementsMixin`**, gaining `query()` with CSS selectors through the `_object_id` mechanism (`find()` and XPath are blocked)\n- **Closed shadow roots** are fully accessible because the `closed` mode is a JavaScript-level policy, not a DOM-level restriction\n- **Nested shadow roots** work naturally by chaining `get_shadow_root()` calls at each level\n- **Shadow roots inside iframes** work transparently through automatic iframe context propagation\n- **Use `query()` with CSS selectors** inside shadow roots; `find()` and XPath raise `NotImplementedError`\n- **`find_shadow_roots()`** discovers all shadow roots on the page; supports `timeout` for polling and `deep=True` for cross-origin iframes (OOPIFs)\n- **`get_shadow_root(timeout)`** waits for a shadow root to appear on a specific element\n\n## Related Documentation\n\n- **[Element Finding Guide](../../features/element-finding.md)**: Practical usage of `find()`, `query()`, and shadow root access\n- **[IFrames & Contexts](../fundamentals/iframes-and-contexts.md)**: How Pydoll resolves and routes commands to iframes, including OOPIF handling\n- **[FindElements Mixin Architecture](./find-elements-mixin.md)**: How the `_object_id` mechanism enables scoped searches\n- **[WebElement Domain](./webelement-domain.md)**: How elements interact with CDP\n- **[Connection Layer](../fundamentals/connection-layer.md)**: WebSocket communication with the browser\n"
  },
  {
    "path": "docs/en/deep-dive/architecture/tab-domain.md",
    "content": "# Tab Domain Architecture\n\nThe Tab domain is Pydoll's primary interface for browser automation, acting as an orchestration layer that integrates multiple CDP domains into a cohesive API. This document explores its internal architecture, design patterns, and the engineering decisions that shape its behavior.\n\n!!! info \"Practical Usage\"\n    For usage examples and practical patterns, see the [Tab Management Guide](../features/automation/tabs.md).\n\n## Architectural Overview\n\nThe `Tab` class serves as a **façade** over Chrome DevTools Protocol, abstracting the complexity of multi-domain coordination into a unified interface.\n\n### Component Structure\n\n| Component | Relationship | Purpose |\n|-----------|-------------|---------|\n| **Tab** | Core class | Primary automation interface |\n| ↳ **ConnectionHandler** | Composition (owned) | WebSocket communication with CDP |\n| ↳ **Browser** | Reference (parent) | Access to browser-level state and configuration |\n| ↳ **FindElementsMixin** | Inheritance | Element location capabilities |\n| ↳ **WebElement** | Factory (creates) | Individual DOM element representations |\n\n### CDP Domain Integration\n\nThe `ConnectionHandler` routes Tab operations to multiple CDP domains:\n\n```\nTab Methods                CDP Domain          Purpose\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ngo_to(), refresh()     →   Page            →  Navigation & lifecycle\nexecute_script()       →   Runtime         →  JavaScript execution\nfind(), query()        →   Runtime/DOM     →  Element location\nget_cookies()          →   Storage         →  Session state\nenable_network_events()→   Network         →  Traffic monitoring\nenable_fetch_events()  →   Fetch           →  Request interception\n```\n\n### Core Responsibilities\n\n1. **CDP Command Routing**: Translates high-level operations into domain-specific CDP commands\n2. **State Management**: Tracks enabled domains, active callbacks, and session state\n3. **Event Coordination**: Bridges CDP events to user-defined callbacks\n4. **Element Factory**: Creates `WebElement` instances from CDP `objectId` strings\n5. **Lifecycle Management**: Handles cleanup and resource deallocation\n\n## Composition vs Inheritance: The FindElementsMixin\n\nA key architectural decision in the Tab domain is **inheriting from `FindElementsMixin`** rather than using composition:\n\n```python\nclass Tab(FindElementsMixin):\n    def __init__(self, ...):\n        self._connection_handler = ConnectionHandler(...)\n        # Mixin methods now available on Tab\n```\n\n**Why inheritance here?**\n\n| Approach | Pros | Cons | Pydoll's Choice |\n|----------|------|------|-----------------|\n| **Inheritance** | Clean API (`tab.find()`), type compatibility | Tight coupling | Used |\n| Composition | Loose coupling, flexible | Verbose (`tab.finder.find()`), wrapper overhead | Not used |\n\n**Rationale:** The mixin pattern is justified because:\n\n- Element finding is **core to Tab identity** (every tab can find elements)\n- The mixin is **stateless** - it only requires `_connection_handler` (dependency injection via duck typing)\n- API ergonomics matter - `tab.find()` is more intuitive than `tab.elements.find()`\n\nSee [FindElements Mixin Deep Dive](./find-elements-mixin.md) for architectural details.\n\n## State Management Architecture\n\nThe Tab class manages **multiple layers of state**:\n\n###  1. Domain Enablement Flags\n\n```python\nclass Tab:\n    def __init__(self, ...):\n        self._page_events_enabled = False\n        self._network_events_enabled = False\n        self._fetch_events_enabled = False\n        self._dom_events_enabled = False\n        self._runtime_events_enabled = False\n        self._intercept_file_chooser_dialog_enabled = False\n```\n\n**Why explicit flags?**\n\n- **Idempotency**: Calling `enable_page_events()` twice doesn't double-register\n- **State inspection**: Properties like `tab.page_events_enabled` expose current state\n- **Cleanup tracking**: Know which domains need disabling on tab close\n\n**Alternative (not used):** Query CDP for enabled domains on each check → Too slow, adds latency.\n\n### 2. Target Identity\n\n```python\nself._target_id: str              # Unique CDP identifier\nself._browser_context_id: Optional[str]  # Isolation context\nself._connection_port: int        # WebSocket port\n```\n\n**Design decision:** `target_id` is the **primary identifier**, not the Tab instance itself. This enables:\n\n- **Browser-level Tab registry**: `Browser._tabs_opened[target_id] = tab`\n- **Singleton pattern**: Same `target_id` always returns same `Tab` instance\n- **Connection reuse**: Multiple operations on same tab share WebSocket\n\n### 3. Feature-Specific State\n\n```python\nself._cloudflare_captcha_callback_id: Optional[int] = None  # For cleanup\nself._request: Optional[Request] = None  # Lazy initialization\n```\n\n**Lazy initialization pattern:** `Request` is only created when `tab.request` is accessed:\n\n```python\n@property\ndef request(self) -> Request:\n    if self._request is None:\n        self._request = Request(self)\n    return self._request\n```\n\n**Why lazy?** Most automations don't use browser-context HTTP requests. Saves memory and initialization time.\n\n\n## JavaScript Execution: Dual Context Architecture\n\nThe `execute_script()` method implements **context polymorphism** - same interface, different CDP commands:\n\n| Context | CDP Method | Use Case |\n|---------|-----------|----------|\n| Global (no element) | `Runtime.evaluate` | `document.title`, global scripts |\n| Element-bound | `Runtime.callFunctionOn` | Element-specific operations |\n\n**Key architectural decision:** Auto-detect execution mode based on `element` parameter presence, eliminating separate APIs (`evaluate()` vs `call_function_on()`).\n\n**Script transformation pipeline:**\n\n1. Replace `argument` → `this` (Selenium compatibility)\n2. Detect if script is already wrapped in `function() { }`\n3. Wrap if needed: `script` → `function() { script }`\n4. Route to appropriate CDP command\n\n**Why `argument` keyword?** Migration path for Selenium users, API familiarity.\n\n!!! info \"Practical Usage\"\n    See [Human-Like Interactions](../features/automation/human-interactions.md) for real-world script execution patterns.\n\n## Event System Integration\n\nThe Tab acts as a **thin wrapper** over ConnectionHandler's event system, but adds an important layer: **non-blocking callback execution**.\n\n```python\nasync def on(self, event_name: str, callback: Callable, temporary: bool = False) -> int:\n    # Wrap async callbacks to execute in background\n    async def callback_wrapper(event):\n        asyncio.create_task(callback(event))\n    \n    if asyncio.iscoroutinefunction(callback):\n        function_to_register = callback_wrapper  # Non-blocking wrapper\n    else:\n        function_to_register = callback  # Sync callbacks execute directly\n    \n    # Delegate registration to ConnectionHandler\n    return await self._connection_handler.register_callback(\n        event_name, function_to_register, temporary\n    )\n```\n\n**Architectural role:** Tab provides tab-scoped event registration with non-blocking execution semantics, while ConnectionHandler handles WebSocket plumbing and sequential callback invocation.\n\n**Key features:**\n\n- **Background execution** via `asyncio.create_task()` for async callbacks (fire-and-forget)\n- **Sync/async callback auto-detection**\n- **Temporary callbacks** for one-shot handlers\n- **Callback ID** for explicit removal\n\n**Execution model:**\n\n| Layer | Behavior | Purpose |\n|-------|----------|---------|\n| **User callback** | Runs in background task | Never blocks other callbacks or CDP commands |\n| **Tab wrapper** | `create_task(callback())` | Launches background task, returns immediately |\n| **EventsManager** | `await wrapper()` | Sequentially invokes wrappers for the same event |\n\n**Why the wrapper?** Without it, a slow async callback would block other callbacks for the same event. The `create_task` wrapper ensures all callbacks start \"simultaneously\" (in separate tasks), preventing one slow callback from delaying others.\n\n!!! info \"Detailed Architecture\"\n    See [Event Architecture Deep Dive](./event-architecture.md) for internal event routing mechanisms and EventsManager's sequential invocation pattern.\n    \n    **Practical usage:** [Event System Guide](../features/advanced/event-system.md)\n\n## Session State: Cookie Management\n\n**Architectural separation:** Cookies route to **Storage domain** (manipulation), not Network domain (observation).\n\n```python\nasync def set_cookies(self, cookies: list[CookieParam]):\n    return await self._execute_command(\n        StorageCommands.set_cookies(cookies, self._browser_context_id)\n    )\n```\n\n**Context-aware design:** `browser_context_id` parameter ensures cookie isolation, enabling multi-account automation.\n\n!!! info \"Practical Cookie Management\"\n    See [Cookies & Sessions Guide](../features/browser-management/cookies-sessions.md) for usage patterns and anti-detection strategies.\n\n## Content Capture: CDP Target Restrictions\n\n**Critical limitation:** `Page.captureScreenshot` only works on **top-level targets**. Iframe tabs fail silently (no `data` field in response).\n\n```python\ntry:\n    screenshot_data = response['result']['data']\nexcept KeyError:\n    raise TopLevelTargetRequired(...)  # Guide users to WebElement.take_screenshot()\n```\n\n**Design implication:** Historically, Pydoll created dedicated Tab instances for iframes. The new model keeps iframe interaction inside `WebElement`, so screenshots and other helpers should target elements within the frame (for example, `await iframe_element.find(...).take_screenshot()`).\n\n**PDF Generation:** `Page.printToPDF` returns base64-encoded data. Pydoll abstracts file I/O, but underlying data is always base64 (CDP spec).\n\n!!! info \"Practical Usage\"\n    See [Screenshots & PDFs Guide](../features/automation/screenshots-and-pdfs.md) for parameters, formats, and real-world examples.\n\n## Network Monitoring: Stateful Design\n\n**Architectural principle:** Network methods require **enabled state** - runtime checks prevent accessing nonexistent data.\n\n**Storage separation:**\n\n- **Logs**: Buffered in `ConnectionHandler` (receives all CDP events)\n- **Tab**: Queries handler, no duplicate storage\n- **Response bodies**: Retrieved on-demand via `Network.getResponseBody(requestId)`\n\n**Critical timing constraint:** Response bodies must be fetched **within ~30s** after response (browser garbage collection).\n\n!!! info \"Network Monitoring in Practice\"\n    See [Network Monitoring Guide](../features/network/monitoring.md) for comprehensive event tracking and analysis patterns.\n    \n    **Request interception:** [Request Interception Guide](../features/network/interception.md)\n\n## Dialog Management: Event Capture Pattern\n\n**Critical CDP behavior:** JavaScript dialogs **block all CDP commands** until handled.\n\n**Architectural solution:** `ConnectionHandler` captures `Page.javascriptDialogOpening` events immediately, preventing automation hangs.\n\n```python\n# Handler stores dialog event before user code runs\nself._connection_handler.dialog  # Captured by handler\n# Tab queries stored event\nasync def has_dialog(self) -> bool:\n    return bool(self._connection_handler.dialog)\n```\n\n**Why this design?** Event fires before user callbacks execute. Without immediate capture, automation would deadlock waiting for blocked CDP responses.\n\n## IFrame Architecture: Tab Reuse Pattern\n\n**Key insight:** IFrames are **first-class CDP targets** → Represented as `Tab` instances.\n\n**Target resolution algorithm:**\n\n1. Extract `src` attribute from iframe element\n2. Query all CDP targets via `Target.getTargets()`\n3. Match iframe URL to target `targetId`\n4. Check singleton registry (`Browser._tabs_opened`)\n5. Return existing instance or create + register new Tab\n\n**Design tradeoff:** IFrame tabs inherit all Tab methods, but some fail (e.g., `take_screenshot()`). Alternative (dedicated `IFrame` class) would duplicate 90% of API for minimal benefit.\n\n!!! info \"Working with IFrames\"\n    See [IFrame Interaction Guide](../features/automation/iframes.md) for practical patterns, nested frames, and common pitfalls.\n\n## Context Managers: Automatic Resource Cleanup\n\n**Architectural pattern:** State restoration + optimistic resource acquisition.\n\n### Key Context Managers\n\n| Manager | Pattern | Key Feature |\n|---------|---------|-------------|\n| `expect_file_chooser()` | State restoration | Restores domain enablement after exit |\n| `expect_download()` | Temporary resources | Auto-cleanup temp directories |\n\n**File Chooser Design:**\n\n- Enable required domains (`Page`, file chooser interception)\n- Register **temporary callback** (auto-removes after first fire)\n- Restore original state on exit (if domains were disabled before, disable again)\n\n**Download Handling Design:**\n\n- Create temporary directory (or use provided path)\n- Use `asyncio.Future` for coordination (`will_begin_future`, `done_future`)\n- Browser-level configuration (downloads are per-context, not per-tab)\n- Guaranteed cleanup via `finally` block\n\n!!! info \"Practical File Operations\"\n    See [File Operations Guide](../features/automation/file-operations.md) for upload patterns, file chooser usage, and download handling.\n\n## Lifecycle: Tab Closure and Invalidation\n\n**Tab closure cascade:**\n\n1. CDP closes browser tab (`Page.close`)\n2. Tab unregisters from `Browser._tabs_opened`\n3. WebSocket auto-closes (CDP target destroyed)\n4. Event callbacks garbage-collected\n\n**Post-closure behavior:** Tab instance becomes **invalid** - further operations fail (closed WebSocket).\n\n**Design decision:** No explicit `_closed` flag. Users manage lifecycle. Alternative (state tracking) adds overhead for marginal safety benefit.\n\n## Key Architectural Decisions\n\n### Per-Tab WebSocket Strategy\n\n**Chosen design:** Each Tab creates its own ConnectionHandler with a dedicated WebSocket connection to `ws://localhost:port/devtools/page/{targetId}`.\n\n**Rationale:**\n\nCDP supports **two connection models**:\n\n1. **Browser-level**: Single connection to `ws://localhost:port/devtools/browser/...` (used by Browser instance)\n2. **Tab-level**: Per-tab connections to `ws://localhost:port/devtools/page/{targetId}` (used by Tab instances)\n\nPydoll uses **both**:\n\n- **Browser** has its own ConnectionHandler for browser-wide operations (contexts, downloads, browser-level events)\n- **Each Tab** has its own ConnectionHandler for tab-specific operations (navigation, element finding, tab events)\n\n**Benefits of per-tab WebSockets:**\n\n- **True parallelism**: Multiple tabs can execute CDP commands simultaneously without waiting\n- **Independent event streams**: Each tab receives only its own events (no filtering needed)\n- **Isolated failures**: Connection issues in one tab don't affect others\n- **Simplified routing**: No need to demultiplex messages by targetId\n\n**Tradeoff:** More open connections (one per tab), but CDP and browsers handle this efficiently. For 10 tabs, this is 11 connections total (1 browser + 10 tabs), which is negligible compared to the HTTP connections the tabs themselves create.\n\n!!! info \"Browser vs Tab Communication\"\n    See [Browser Domain Architecture](./browser-domain.md) for details on the browser-level ConnectionHandler and how Browser/Tab coordination works.\n\n### Browser Reference Necessity\n\n**Why Tab stores `_browser` reference:**\n- Context queries (`browser_context_id` for cookies)\n- Browser-level operations (download behavior, iframe registry)\n- Configuration access (`browser.options.page_load_state`)\n\n### API Design Choices\n\n| Choice | Rationale |\n|--------|-----------|\n| **Async properties** (`current_url`, `page_source`) | Signal live data + CDP cost |\n| **Separate `enable`/`disable` methods** | Explicit over implicit, matches CDP naming |\n| **No `_closed` flag** | Users manage lifecycle, reduces overhead |\n| **`argument` keyword in scripts** | Selenium compatibility, migration path |\n\n## Relationship to Other Domains\n\nThe Tab domain sits at the **center** of Pydoll's architecture:\n\n```mermaid\ngraph TD\n    Browser[Browser Domain<br/>Lifecycle & Process] -->|creates| Tab[Tab Domain<br/>Automation Interface]\n    Tab -->|uses| ConnectionHandler[ConnectionHandler<br/>CDP Communication]\n    Tab -->|creates| WebElement[WebElement Domain<br/>Element Interaction]\n    Tab -->|inherits| FindMixin[FindElementsMixin<br/>Locator Strategies]\n    Tab -->|uses| Commands[CDP Commands<br/>Typed Protocol]\n    \n    ConnectionHandler -->|dispatches| Events[Event System]\n    Tab -.->|references| Browser\n    WebElement -.->|references| ConnectionHandler\n```\n\n**Key relationships:**\n\n1. **Browser → Tab**: Parent-child. Browser manages Tab lifecycle and shared state.\n2. **Tab → ConnectionHandler**: Composition. Tab delegates CDP communication.\n3. **Tab → WebElement**: Factory. Tab creates elements from `objectId` strings.\n4. **Tab ← FindElementsMixin**: Inheritance. Tab gains element location methods.\n5. **Tab ↔ Browser**: Bidirectional reference. Tab queries browser for context info.\n\n## Summary: Design Philosophy\n\nThe Tab domain prioritizes **API ergonomics** and **correctness** over micro-optimizations:\n\n- **Façade pattern** abstracts CDP complexity\n- **State management** via explicit flags prevents double-enabling\n- **Resource management** through context managers\n- **Event coordination** with background execution (non-blocking)\n\n**Core tradeoffs:**\n\n| Decision | Benefit | Cost | Verdict |\n|----------|---------|------|---------|\n| Per-tab WebSocket | True parallelism | More connections | Justified |\n| Inherit FindElementsMixin | Clean API | Tight coupling | Justified |\n| Lazy Request init | Memory efficiency | Property overhead | Justified |\n\n## Further Reading\n\n**Practical guides:**\n\n- [Tab Management](../features/automation/tabs.md) - Multi-tab patterns, lifecycle, concurrency\n- [Element Finding](../features/element-finding.md) - Selectors and DOM traversal\n- [Event System](../features/advanced/event-system.md) - Real-time browser monitoring\n\n**Architectural deep-dives:**\n\n- [Event Architecture](./event-architecture.md) - WebSocket plumbing and event routing\n- [FindElements Mixin](./find-elements-mixin.md) - Selector resolution algorithms\n- [Browser Domain](./browser-domain.md) - Process management and contexts\n"
  },
  {
    "path": "docs/en/deep-dive/architecture/webelement-domain.md",
    "content": "# WebElement Domain Architecture\n\nThe WebElement domain bridges high-level automation code and low-level DOM interaction through Chrome DevTools Protocol. This document explores its internal architecture, design patterns, and engineering decisions.\n\n!!! info \"Practical Usage\"\n    For usage examples and interaction patterns, see:\n    \n    - [Element Finding Guide](../features/element-finding.md)\n    - [Human-Like Interactions](../features/automation/human-interactions.md)\n    - [File Operations](../features/automation/file-operations.md)\n\n## Architectural Overview\n\nWebElement represents a **remote object reference** to a DOM element via CDP's `objectId` mechanism:\n\n```\nUser Code → WebElement → ConnectionHandler → CDP Runtime → Browser DOM\n```\n\n**Key characteristics:**\n\n- **Async by design**: All operations follow Python's async/await pattern\n- **Remote reference**: Maintains CDP `objectId` for browser-side element\n- **Mixin inheritance**: Inherits `FindElementsMixin` for child element searches\n- **Hybrid state**: Combines cached attributes with live DOM queries\n\n### Core State\n\n```python\nclass WebElement(FindElementsMixin):\n    def __init__(self, object_id: str, connection_handler: ConnectionHandler, ...):\n        self._object_id = object_id              # CDP remote object reference\n        self._connection_handler = connection_handler  # WebSocket communication\n        self._attributes: dict[str, str] = {}    # Cached HTML attributes\n        self._search_method = method             # How element was found (debug)\n        self._selector = selector                # Original selector (debug)\n```\n\n**Why cache attributes?** Initial element location returns HTML attributes. Caching provides fast, synchronous access to common properties (`id`, `class`, `tag_name`) without additional CDP calls.\n\n## Design Patterns\n\n### 1. Command Pattern\n\nAll element interactions translate to CDP commands:\n\n| User Operation | CDP Domain | Command |\n|----------------|-----------|---------|\n| `element.click()` | Input | `Input.dispatchMouseEvent` |\n| `element.text` | Runtime | `Runtime.callFunctionOn` |\n| `element.bounds` | DOM | `DOM.getBoxModel` |\n| `element.take_screenshot()` | Page | `Page.captureScreenshot` |\n\n### 2. Bridge Pattern\n\nWebElement abstracts CDP protocol complexity:\n\n```python\nasync def click(self, x_offset=0, y_offset=0, hold_time=0.1):\n    # High-level API\n    \n    # → Translates to low-level CDP commands:\n    # 1. DOM.getBoxModel (get position)\n    # 2. Input.dispatchMouseEvent (press)\n    # 3. Input.dispatchMouseEvent (release)\n```\n\n### 3. Mixin Inheritance for Child Searches\n\n**Why inherit FindElementsMixin?** Enables element-relative searches:\n\n```python\nform = await tab.find(id='login-form')\nusername = await form.find(name='username')  # Search within form\n```\n\n**Design decision:** Composition (`form.finder.find()`) would be more flexible but less ergonomic. Inheritance chosen for API simplicity.\n\n## Hybrid Property System\n\n**Architectural innovation:** WebElement combines sync and async property access.\n\n### Synchronous Properties (Cached Attributes)\n\n```python\n@property\ndef id(self) -> str:\n    return self._attributes.get('id')  # From cached HTML attributes\n\n@property  \ndef class_name(self) -> str:\n    return self._attributes.get('class_name')  # 'class' → 'class_name' (Python keyword)\n```\n\n**Source:** Flat list from CDP element location response, parsed during `__init__`.\n\n### Asynchronous Properties (Live DOM State)\n\n```python\n@property\nasync def text(self) -> str:\n    outer_html = await self.inner_html  # CDP call\n    soup = BeautifulSoup(outer_html, 'html.parser')\n    return soup.get_text(strip=True)\n\n@property\nasync def bounds(self) -> dict:\n    response = await self._execute_command(DomCommands.get_box_model(self._object_id))\n    # Parse and return bounds\n```\n\n**Rationale:** Text and bounds are **dynamic** - they change as page updates. Attributes are **static** - captured at location time.\n\n| Property Type | Access | Source | Use Case |\n|--------------|--------|--------|----------|\n| Sync | `element.id` | Cached attributes | Fast access, static data |\n| Async | `await element.text` | Live CDP query | Current state, dynamic data |\n\n## Click Implementation: Multi-Stage Pipeline\n\nClick operations follow a sophisticated pipeline to ensure reliability:\n\n### 1. Special Element Detection\n\n```python\nasync def click(self, x_offset=0, y_offset=0, hold_time=0.1):\n    # Stage 1: Handle special elements\n    if self._is_option_tag():\n        return await self.click_option_tag()  # <option> needs JavaScript select\n```\n\n**Why special handling?** `<option>` elements inside `<select>` don't respond to mouse events. Requires JavaScript `selected = true`.\n\n### 2. Visibility Check\n\n```python\n    # Stage 2: Verify element is visible\n    if not await self.is_visible():\n        raise ElementNotVisible()\n```\n\n**Why check?** CDP mouse events target coordinates. Hidden elements would receive clicks at wrong positions or fail silently.\n\n### 3. Position Calculation\n\n```python\n    # Stage 3: Scroll into view and get position\n    await self.scroll_into_view()\n    bounds = await self.bounds\n    \n    # Stage 4: Calculate click coordinates\n    position_to_click = (\n        bounds['x'] + bounds['width'] / 2 + x_offset,\n        bounds['y'] + bounds['height'] / 2 + y_offset,\n    )\n```\n\n**Offset support:** Enables varied click positions for human-like behavior (anti-detection).\n\n### 4. Mouse Event Dispatch\n\n```python\n    # Stage 5: Send CDP mouse events\n    await self._execute_command(InputCommands.mouse_press(*position_to_click))\n    await asyncio.sleep(hold_time)  # Configurable hold (default 0.1s)\n    await self._execute_command(InputCommands.mouse_release(*position_to_click))\n```\n\n**Why two commands?** Simulates real mouse behavior (press → hold → release). Some sites detect instant clicks as bots.\n\n### Click Fallback: JavaScript Alternative\n\n```python\nasync def click_using_js(self):\n    \"\"\"Fallback for elements that can't be clicked via mouse events.\"\"\"\n    await self.execute_script('this.click()')\n```\n\n**When to use:**\n- Hidden elements (e.g., file inputs styled with CSS)\n- Elements behind overlays\n- Performance-critical scenarios (skips visibility/position checks)\n\n!!! info \"Mouse vs JavaScript Clicks\"\n    See [Human-Like Interactions](../features/automation/human-interactions.md) for when to use each approach and detection implications.\n\n## Screenshot Architecture: Clip Regions\n\n**Key mechanism:** `Page.captureScreenshot` with `clip` parameter.\n\n```python\nasync def take_screenshot(self, path: str, quality: int = 100):\n    # 1. Get element bounds (position + dimensions)\n    bounds = await self.get_bounds_using_js()\n    \n    # 2. Create clip region\n    clip = Viewport(x=bounds['x'], y=bounds['y'], \n                    width=bounds['width'], height=bounds['height'], scale=1)\n    \n    # 3. Capture only clipped region\n    screenshot = await self._execute_command(\n        PageCommands.capture_screenshot(format=ScreenshotFormat.JPEG, clip=clip, quality=quality)\n    )\n```\n\n**Why JavaScript bounds?** `DOM.getBoxModel` can fail for certain elements. JavaScript `getBoundingClientRect()` is more reliable fallback.\n\n**Format limitation:** Element screenshots always use JPEG (CDP restriction with clip regions).\n\n!!! info \"Screenshot Capabilities\"\n    See [Screenshots & PDFs](../features/automation/screenshots-and-pdfs.md) for full-page vs element screenshots comparison.\n\n## JavaScript Execution Context\n\n**Critical CDP feature:** `Runtime.callFunctionOn(objectId, ...)` executes JavaScript **in element context** (`this` = element).\n\n```python\nasync def execute_script(self, script: str, return_by_value=False):\n    return await self._execute_command(\n        RuntimeCommands.call_function_on(self._object_id, script, return_by_value)\n    )\n```\n\n**Use cases:**\n\n- Visibility checks: `await element.is_visible()` → JavaScript checks computed styles\n- Style manipulation: `await element.execute_script(\"this.style.border = '2px solid red'\")`\n- Attribute access: Some properties require JavaScript (e.g., `value` for inputs)\n\n**Alternative (not used):** Execute global script with element selector → Slower, risks stale references.\n\n## State Verification Pipeline\n\n**Reliability strategy:** Pre-check element state before interactions to prevent failures.\n\n| Check | Purpose | Implementation |\n|-------|---------|----------------|\n| `is_visible()` | Element in viewport, not hidden | JavaScript: `offsetWidth > 0 && offsetHeight > 0` |\n| `is_on_top()` | No overlays blocking element | JavaScript: `document.elementFromPoint(x, y) === this` |\n| `is_interactable()` | Visible + on top | Combines both checks |\n\n**Why JavaScript for visibility?** CSS `display: none`, `visibility: hidden`, `opacity: 0` all affect visibility differently. JavaScript provides unified check.\n\n## Performance Strategies\n\n### 1. Operation-Specific Optimization\n\n**Principle:** Choose the fastest approach for each operation type.\n\n| Operation | Primary Approach | Rationale |\n|-----------|-----------------|-----------|\n| Text extraction | BeautifulSoup parsing | More accurate than JavaScript `innerText` |\n| Visibility check | JavaScript | Single CDP call vs multiple DOM queries |\n| Click | CDP mouse events | Most realistic, required for anti-detection |\n| Bounds | `DOM.getBoxModel` | Faster than JavaScript, with JS fallback |\n\n### 2. Local Computation\n\n**Minimize CDP round-trips** by computing locally when possible:\n\n```python\n# Good: Single bounds query, local calculation\nbounds = await element.bounds\nclick_x = bounds['x'] + bounds['width'] / 2 + offset_x\nclick_y = bounds['y'] + bounds['height'] / 2 + offset_y\n\n# Bad: Multiple CDP calls for simple math\nclick_x = await element.execute_script('return this.offsetLeft + this.offsetWidth / 2')\nclick_y = await element.execute_script('return this.offsetTop + this.offsetHeight / 2')\n```\n\n### 3. Cached Attributes\n\n**Design decision:** Cache static attributes at creation time:\n\n```python\n# Fast synchronous access (no CDP call)\nelement_id = element.id\nelement_class = element.class_name\n```\n\n**Tradeoff:** Attributes won't reflect runtime changes. For dynamic properties, use async: `await element.text`.\n\n## Key Architectural Decisions\n\n| Decision | Rationale |\n|----------|-----------|\n| **Inherit FindElementsMixin** | Enables child searches, maintains API consistency |\n| **Hybrid sync/async properties** | Balances performance (sync) with freshness (async) |\n| **JavaScript fallbacks** | Reliability over performance for critical operations |\n| **Special element detection** | `<option>`, `<input type=\"file\">` require unique handling |\n| **Pre-click visibility checks** | Fail fast with clear errors vs silent failures |\n\n## Summary\n\nThe WebElement domain bridges Python automation code and browser DOM through:\n\n- **Remote object references** via CDP `objectId`\n- **Hybrid property system** balancing sync attributes and async state\n- **Multi-stage interaction pipelines** ensuring reliability\n- **Specialized handling** for element type variations\n\n**Core tradeoffs:**\n\n| Decision | Benefit | Cost | Verdict |\n|----------|---------|------|---------|\n| Mixin inheritance | Clean API | Tight coupling | Justified |\n| Cached attributes | Fast sync access | Stale data risk | Justified |\n| JavaScript fallbacks | Reliability | Performance hit | Justified |\n| Visibility pre-checks | Clear errors | Extra CDP calls | Justified |\n\n## Further Reading\n\n**Practical guides:**\n\n- [Element Finding](../features/element-finding.md) - Locating elements, selectors\n- [Human-Like Interactions](../features/automation/human-interactions.md) - Clicking, typing, realism\n- [File Operations](../features/automation/file-operations.md) - File uploads and downloads\n\n**Architectural deep-dives:**\n\n- [FindElements Mixin](./find-elements-mixin.md) - Selector resolution pipeline\n- [Tab Domain](./tab-domain.md) - Tab as element factory\n- [Connection Layer](./connection-layer.md) - WebSocket communication "
  },
  {
    "path": "docs/en/deep-dive/fingerprinting/behavioral-fingerprinting.md",
    "content": "# Behavioral Fingerprinting\n\nBehavioral fingerprinting analyzes how users interact with web applications rather than what tools they use. While network and browser fingerprints can be spoofed by setting the right values, human behavior follows biomechanical patterns that are difficult to replicate convincingly. Detection systems collect mouse movements, keystroke timing, scroll behavior, and interaction sequences, then use statistical models to distinguish humans from automation.\n\nThis document covers the detection techniques, the science behind them, and how Pydoll's humanization features address each vector.\n\n!!! info \"Module Navigation\"\n    - [Network Fingerprinting](./network-fingerprinting.md): TCP/IP, TLS, HTTP/2 protocol fingerprinting\n    - [Browser Fingerprinting](./browser-fingerprinting.md): Canvas, WebGL, navigator properties\n    - [Evasion Techniques](./evasion-techniques.md): Practical countermeasures\n\n## Mouse Movement Analysis\n\nMouse movement is one of the most powerful behavioral indicators because human motor control follows biomechanical laws that simple automation cannot replicate. Detection systems collect `mousemove` events (each containing x, y coordinates and a timestamp) and analyze the trajectory for properties that distinguish organic movement from programmatic cursor teleportation.\n\n### Fitts's Law\n\nFitts's Law describes the time required to move a pointer to a target. The Shannon formulation (MacKenzie, 1992), which is the most widely used version, states:\n\n```\nT = a + b * log2(D/W + 1)\n```\n\nWhere `T` is the movement time, `a` is a constant representing reaction/start time, `b` is a constant representing the inherent speed of the input device, `D` is the distance to the target, and `W` is the width (size) of the target. The logarithmic relationship means that doubling the distance adds a fixed amount of time, while halving the target size adds the same fixed amount.\n\nThe implications for bot detection are significant. Humans take longer to reach small, distant targets and reach large, nearby targets quickly. They accelerate at the start of a movement, reach peak velocity roughly mid-path, and decelerate as they approach the target. Bots that move the cursor in constant time regardless of distance and target size violate Fitts's Law and are trivially detectable.\n\nDetection systems measure the movement time for each click event, compute the expected time from the distance and target size, and flag movements that are significantly faster than Fitts's Law predicts or that show no correlation between distance/size and movement time.\n\n### Trajectory Shape\n\nHuman hand movements between two points are not straight lines. Research by Abend, Bizzi, and Morasso (1982) showed that hand paths are typically curved due to biomechanical constraints of the arm's joints and muscles. Flash and Hogan (1985) demonstrated that human reaching movements follow minimum-jerk trajectories, where the trajectory minimizes the integral of jerk (the derivative of acceleration) over the movement duration. The resulting velocity profile is bell-shaped and is described by a quintic (5th degree) polynomial:\n\n```\nx(t) = x0 + (xf - x0) * (10t^3 - 15t^4 + 6t^5)\n```\n\nwhere `t` is normalized time from 0 to 1, and `x0`/`xf` are the start and end positions. This produces smooth acceleration from rest, peak velocity at approximately mid-path, and smooth deceleration back to rest.\n\nDetection systems analyze trajectory curvature, velocity profiles, and acceleration patterns. The specific signals they look for include:\n\n**Straight-line detection.** A perfectly straight path between two points (zero curvature at every sample) is the most obvious bot signal. Human paths always have some curvature due to the arm's rotational joints.\n\n**Constant velocity.** Humans show a bell-shaped velocity profile (accelerate, peak, decelerate). A constant velocity throughout the movement indicates linear interpolation, which is the default behavior of most automation tools.\n\n**Absence of sub-movements.** Long movements are composed of multiple overlapping sub-movements (Meyer et al., 1988), each with its own velocity peak. A movement covering 500+ pixels with a single smooth velocity peak is suspicious; real movements of that distance typically show 2-4 velocity peaks.\n\n**No overshoot.** Humans frequently overshoot the target slightly (by 5-15 pixels) and make a small correction back. Perfectly precise movements that land exactly on target every time are statistically improbable.\n\n### Movement Entropy\n\nEntropy, in this context, measures the unpredictability of the mouse path. Detection systems divide the trajectory into segments, measure the direction change at each point, and compute Shannon entropy over the distribution of direction changes. A straight line has zero entropy (every segment points the same direction). A random walk has maximum entropy. Human movement has moderate-to-high entropy, reflecting the combination of intentional direction and involuntary variability.\n\nLow entropy across many mouse movements in a session is a strong bot signal, even if individual movements have plausible curvature.\n\n### Pydoll's Mouse Humanization\n\nPydoll implements comprehensive mouse humanization through the `humanize=True` parameter on click operations. When enabled, the mouse module generates movements that address each of the detection vectors described above:\n\nThe path follows a cubic Bezier curve with randomized control points, producing natural curvature rather than straight lines. The velocity along the path follows a minimum-jerk profile (`10t^3 - 15t^4 + 6t^5`), producing the bell-shaped velocity curve that Fitts's Law predicts. Movement duration is calculated using Fitts's Law with configurable constants (`a=0.070`, `b=0.150` by default).\n\nPhysiological tremor is simulated by adding Gaussian noise to cursor positions, with amplitude scaled inversely to velocity (tremor is more visible when the hand moves slowly, which matches real physiology). Overshoot occurs with 70% probability, overshooting the target by 3-12% of the total distance before making a correction movement. Micro-pauses (15-40ms) occur with 3% probability during the movement, simulating brief hesitations.\n\n```python\n# Basic humanized click\nawait element.click(humanize=True)\n\n# The Mouse class can also be used directly for more control\nfrom pydoll.interactions.mouse import Mouse\n\nmouse = Mouse(connection_handler)\nawait mouse.click(500, 300, humanize=True)\n```\n\n!!! note \"What Pydoll Does Not Do\"\n    Pydoll's mouse humanization does not currently model sub-movements for very long distances (the path is a single Bezier segment). For most web interactions, where distances are under 500 pixels, this is sufficient. Extremely long movements (full-screen diagonal traversals) may benefit from future multi-segment support.\n\n## Keystroke Dynamics\n\nKeystroke dynamics analyzes the timing patterns of keyboard input. The technique dates back to telegraph operators in the 1850s, who could identify each other by their Morse code \"fist\" (characteristic timing pattern). Modern systems measure timing at millisecond precision through `keydown` and `keyup` events.\n\n### Timing Features\n\nThe two fundamental measurements are dwell time (the duration between `keydown` and `keyup` for a single key, typically 50-200ms for humans) and flight time (the duration between releasing one key and pressing the next, typically 80-400ms). The combination of dwell and flight times for consecutive key pairs is called a digraph latency.\n\nDigraph latencies are not uniform. They depend on the specific key pair (bigram) being typed, because typing is a motor skill where common sequences are stored as procedural memory. The key biomechanical factors are:\n\n**Hand alternation.** Bigrams typed with alternating hands (like \"th\", where \"t\" is left hand and \"h\" is right hand on QWERTY) are generally faster than same-hand bigrams (like \"de\", where both keys are on the left hand). The alternating hand can begin its movement while the first hand is still completing its keystroke.\n\n**Finger distance.** Home-row to home-row transitions are fastest. Reaching to the top or bottom row adds time proportional to the physical distance the finger must travel.\n\n**Finger independence.** Ring finger and pinky combinations on the same hand are slower than index and middle finger combinations, because the ring and pinky fingers share tendons and have less independent motor control.\n\n**Frequency effects.** Frequently typed bigrams (like \"th\", \"er\", \"in\" in English) are executed faster due to motor memory, regardless of their physical layout.\n\n### Detection Signals\n\nDetection systems look for several signals that distinguish human typing from automation:\n\n**Zero or constant dwell time.** Many automation tools dispatch `keydown` and `keyup` events with zero or near-zero delay between them (under 5ms). Real key presses have measurable dwell times. Constant dwell time across all keys is equally suspicious.\n\n**Uniform flight time.** Setting a fixed interval between keystrokes (such as `type_text(\"hello\", interval=0.1)`) produces perfectly regular timing that is trivially detectable. Human flight times vary by bigram, fatigue, and cognitive load.\n\n**No typing errors.** In extended text input (50+ characters), the complete absence of backspace or delete presses is unusual. Humans make mistakes at a rate of roughly 1-5% depending on typing proficiency and text complexity.\n\n**Superhuman speed.** Sustained typing above 150 WPM is beyond the capability of all but elite competitive typists. Automation tools that dispatch characters faster than this are immediately flagged.\n\n### Pydoll's Keyboard Humanization\n\nPydoll's `type_text(humanize=True)` addresses each detection vector with configurable parameters:\n\nKeystroke delays are drawn from a uniform distribution (30-120ms by default) rather than a fixed interval. Punctuation characters (`.!?;:,`) receive additional delay (80-180ms), simulating the pause that occurs when a typist considers sentence structure. Thinking pauses (300-700ms) occur with 2% probability, simulating brief moments of thought. Distraction pauses (500-1200ms) occur with 0.5% probability, simulating the typist looking away or being briefly interrupted.\n\nRealistic typos occur with approximately 2% probability per character, with five distinct error types weighted by their real-world frequency: adjacent key errors (55%, pressing a neighboring key on QWERTY), transpositions (20%, swapping two consecutive characters), double presses (12%, hitting a key twice), skipped characters (8%, hesitating before typing correctly), and missed spaces (5%, forgetting a space between words). Each error type includes a realistic recovery sequence (pause, backspace, correction) with appropriate timing.\n\n```python\n# Humanized typing\nawait element.type_text(\"Hello, world!\", humanize=True)\n\n# With custom timing configuration\nfrom pydoll.interactions.keyboard import Keyboard, TimingConfig, TypoConfig\n\nconfig = TimingConfig(\n    keystroke_min=0.04,\n    keystroke_max=0.15,\n    thinking_probability=0.03,\n)\nkeyboard = Keyboard(connection_handler, timing_config=config)\nawait keyboard.type_text(\"Custom timing example\", humanize=True)\n```\n\n!!! note \"What Pydoll Does Not Do\"\n    Pydoll's keyboard humanization uses uniform random delays rather than bigram-aware timing. It does not model per-key dwell time variation or hand-alternation speed differences. For most automation scenarios (form filling, search queries), uniform variation is sufficient to pass behavioral detection. Applications requiring authentication-level keystroke biometric evasion would need custom timing models.\n\n## Scroll Behavior Analysis\n\nScroll fingerprinting analyzes how users navigate vertically (and horizontally) through page content. The distinction between human and automated scrolling is stark: programmatic `window.scrollTo()` calls produce instant, discrete jumps, while human scrolling via mouse wheel, trackpad, or touch produces a stream of small incremental events with momentum and deceleration.\n\n### Physical Scroll Characteristics\n\nMouse wheel scrolling produces discrete `wheel` events with consistent delta values (typically 100 or 120 pixels per notch, depending on OS and browser). The events arrive at irregular intervals reflecting how quickly the user turns the wheel. Trackpad scrolling produces many small events with decreasing deltas, simulating physical momentum. Touch scrolling is similar to trackpad but with larger initial deltas and longer deceleration tails.\n\nDetection systems analyze the delta distribution, inter-event timing, and deceleration curve. A `scrollTo(0, 5000)` call produces a single jump with no intermediate events, which is fundamentally different from the hundreds of incremental events that a human scroll generates.\n\n### Detection Signals\n\n**Instant scrolling.** Using `window.scrollTo()` or `window.scrollBy()` with large values produces zero intermediate scroll events. Detection systems that listen for `scroll` events see the scroll position change in a single frame.\n\n**Uniform deltas.** Programmatic scroll simulation that dispatches wheel events with constant delta values (e.g., always 100 pixels) lacks the natural variation in human scrolling, where delta values fluctuate by 10-30% due to inconsistent finger pressure.\n\n**No deceleration.** Human scrolling, especially on trackpads, has a momentum phase where the scroll continues after the user lifts their finger, with exponentially decreasing velocity. Automated scrolling that stops abruptly lacks this deceleration tail.\n\n**Absence of direction changes.** Humans frequently over-scroll and scroll back slightly, or pause partway down a page to read content. Automated scrolling that moves in one direction at constant speed without pauses or reversals is suspicious.\n\n### Pydoll's Scroll Humanization\n\nPydoll's scroll module implements humanized scrolling through `scroll.by(position, distance, humanize=True)`:\n\nThe scroll follows a cubic Bezier easing curve (control points `0.645, 0.045, 0.355, 1.0` by default), producing natural acceleration and deceleration. Per-frame jitter of ±3 pixels adds variation to delta values. Micro-pauses (20-50ms) occur with 5% probability, simulating brief reading stops. Overshoot occurs with 15% probability, scrolling 2-8% past the target and correcting back. For large distances, the scroll is broken into multiple \"flick\" gestures (100-1200 pixels each), simulating how a real user scrolls through a long page with repeated swipes rather than a single continuous motion.\n\n```python\nfrom pydoll.interactions.scroll import Scroll, ScrollPosition\n\nscroll = Scroll(connection_handler)\n\n# Humanized scroll down by 800 pixels\nawait scroll.by(ScrollPosition.Y, 800, humanize=True)\n\n# Scroll to top/bottom uses multiple human-like flicks\nawait scroll.to_bottom(humanize=True)\n```\n\n## Additional Detection Vectors\n\nBeyond mouse, keyboard, and scroll analysis, sophisticated detection systems monitor several other behavioral signals.\n\n### Focus and Visibility\n\nThe Page Visibility API (`document.visibilityState`) and focus events (`window.onfocus`, `window.onblur`) reveal whether the user is actively viewing the page. A real user's session includes tab switches, window minimizations, and periods of inactivity. An automation script that maintains continuous focus for hours without a single blur event is behaviorally anomalous. Similarly, `document.hasFocus()` returning `true` continuously for extended periods is unusual.\n\n### Idle Patterns\n\nReal users have natural idle periods: reading content, thinking before acting, being distracted. Detection systems measure the distribution of idle times between interactions. A session where every action follows the previous one within 100-500ms with no longer pauses follows a pattern that is statistically distinct from human browsing, where idle periods of 2-30 seconds between actions are normal.\n\n### Event Sequence Integrity\n\nBrowsers generate specific event sequences for user interactions. A mouse click produces `pointerdown`, `mousedown`, `pointerup`, `mouseup`, `click` in that order, preceded by `pointermove`/`mousemove` events showing the cursor approaching the click target. Automation tools that dispatch a bare `click` event without the preceding movement and pointer events are detectable through event sequence analysis.\n\nPydoll's CDP-based event dispatch generates complete event sequences because it uses Chrome's input simulation, which produces the same event chain as real user input.\n\n## Machine Learning Detection\n\nModern anti-bot systems (DataDome, Akamai Bot Manager, Cloudflare Bot Management, PerimeterX/HUMAN Security) do not use simple threshold rules. They train machine learning models on millions of real user sessions and millions of known bot sessions, learning to distinguish humans from automation based on 50+ features simultaneously.\n\nThese models capture statistical properties that are hard to enumerate as individual rules: the joint distribution of movement speed and curvature, the correlation between typing speed and error rate, the relationship between scroll depth and reading time, and the overall \"rhythm\" of a browsing session. A system that passes every individual check but has subtly wrong correlations between features can still be flagged by a well-trained model.\n\nThe practical implication is that behavioral evasion must be consistent across all interaction types, not just individually plausible. Pydoll's `humanize=True` parameter provides a coherent humanization layer across mouse, keyboard, and scroll interactions, but the developer is still responsible for higher-level behavioral plausibility: adding reading delays between page loads, varying the pace of a multi-page workflow, and including natural idle periods.\n\n## References\n\n- Fitts, P. M. (1954). The Information Capacity of the Human Motor System in Controlling the Amplitude of Movement. Journal of Experimental Psychology.\n- MacKenzie, I. S. (1992). Fitts' Law as a Research and Design Tool in Human-Computer Interaction. Human-Computer Interaction.\n- Flash, T., & Hogan, N. (1985). The Coordination of Arm Movements: An Experimentally Confirmed Mathematical Model. Journal of Neuroscience.\n- Abend, W., Bizzi, E., & Morasso, P. (1982). Human Arm Trajectory Formation. Brain.\n- Meyer, D. E., Abrams, R. A., Kornblum, S., Wright, C. E., & Smith, J. E. K. (1988). Optimality in Human Motor Performance. Psychological Review.\n- Ahmed, A. A. E., & Traore, I. (2007). A New Biometric Technology Based on Mouse Dynamics. IEEE TDSC.\n"
  },
  {
    "path": "docs/en/deep-dive/fingerprinting/browser-fingerprinting.md",
    "content": "# Browser Fingerprinting\n\nBrowser fingerprinting identifies clients by analyzing properties exposed through JavaScript APIs, HTTP headers, and rendering engines. Unlike network fingerprinting, which examines protocol-level signals from the OS kernel and TLS library, browser fingerprinting targets the application layer: the specific browser, its version, its configuration, and the hardware it runs on. These signals are accessible to any website through standard web APIs, and the combination of enough properties creates a fingerprint that is often unique across millions of visitors.\n\n!!! info \"Module Navigation\"\n    - [Network Fingerprinting](./network-fingerprinting.md): TCP/IP, TLS, HTTP/2 protocol fingerprinting\n    - [Behavioral Fingerprinting](./behavioral-fingerprinting.md): Mouse, keyboard, scroll analysis\n    - [Evasion Techniques](./evasion-techniques.md): Practical countermeasures\n\n## JavaScript Navigator Properties\n\nThe `navigator` object is the richest single source of browser fingerprinting data. It exposes dozens of properties that reveal the browser, its capabilities, and the system it runs on. Detection systems collect these properties, cross-reference them against each other and against HTTP headers, and flag inconsistencies.\n\nThe following JavaScript collects the core set of properties that fingerprinting systems typically examine:\n\n```javascript\nconst fingerprint = {\n    // Identity\n    userAgent: navigator.userAgent,\n    platform: navigator.platform,\n    vendor: navigator.vendor,\n\n    // Language and locale\n    language: navigator.language,\n    languages: navigator.languages,\n\n    // Hardware\n    hardwareConcurrency: navigator.hardwareConcurrency,\n    deviceMemory: navigator.deviceMemory,\n    maxTouchPoints: navigator.maxTouchPoints,\n\n    // Features\n    cookieEnabled: navigator.cookieEnabled,\n    doNotTrack: navigator.doNotTrack,\n    webdriver: navigator.webdriver,\n\n    // Screen\n    screenWidth: screen.width,\n    screenHeight: screen.height,\n    colorDepth: screen.colorDepth,\n    devicePixelRatio: window.devicePixelRatio,\n\n    // Window chrome (toolbar, scrollbar dimensions)\n    chromeHeight: window.outerHeight - window.innerHeight,\n    chromeWidth: window.outerWidth - window.innerWidth,\n\n    // Timezone\n    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n    timezoneOffset: new Date().getTimezoneOffset(),\n};\n```\n\nSeveral of these properties deserve individual attention because they carry more fingerprinting weight or are more commonly misconfigured by automation tools.\n\n### Platform and User-Agent Consistency\n\nThe `navigator.platform` property returns a string like `Win32`, `MacIntel`, or `Linux x86_64`. Detection systems compare this against the User-Agent header. If the HTTP User-Agent claims `Windows NT 10.0` but `navigator.platform` returns `Linux x86_64`, the mismatch is a strong signal. This is one of the most common mistakes in automation: setting a custom User-Agent via `--user-agent=` without also overriding the platform.\n\n### Hardware Properties\n\n`navigator.hardwareConcurrency` returns the number of logical CPU cores. A value of 1 or 2 suggests a minimal VM or container rather than a real user's machine. `navigator.deviceMemory` reports approximate RAM in gigabytes (0.25, 0.5, 1, 2, 4, 8). This property is only available in Chromium browsers; Firefox and Safari return `undefined`. Both values should be consistent with the claimed device: a User-Agent claiming a modern desktop but reporting 1 core and 0.5 GB of RAM is suspicious.\n\n### WebDriver Property\n\nThe `navigator.webdriver` property is `true` when the browser is controlled by WebDriver-based automation (Selenium, Playwright in WebDriver mode). This is the single most obvious automation indicator. Pydoll uses CDP (Chrome DevTools Protocol) directly, which does not set this flag. In a Pydoll-controlled browser, `navigator.webdriver` is `undefined`, matching the behavior of a normal user session.\n\n### Plugins\n\nThe `navigator.plugins` property was historically a strong fingerprinting vector because different browsers and OS configurations exposed different plugin lists. Modern Chromium browsers (Chrome 90+) return a fixed list of five PDF-related plugins regardless of actual plugin state:\n\n```javascript\n// Modern Chrome always returns these 5 plugins:\n// 1. PDF Viewer\n// 2. Chrome PDF Viewer\n// 3. Chromium PDF Viewer\n// 4. Microsoft Edge PDF Viewer\n// 5. WebKit built-in PDF\nconsole.log(navigator.plugins.length); // 5\n```\n\nA common misconception claims that modern browsers return empty arrays for `navigator.plugins`. This is incorrect. Returning an empty array is itself a detection signal that suggests headless mode or a non-browser HTTP client.\n\n### Screen and Window Dimensions\n\nThe gap between `window.outerWidth`/`outerHeight` and `window.innerWidth`/`innerHeight` represents the browser chrome (toolbars, scrollbars, window frame). Headless browsers often report zero difference because they have no visible UI. Detection systems flag clients where `outerWidth` equals `innerWidth` as potentially headless. Similarly, `screen.width` matching `innerWidth` exactly suggests a maximized headless window rather than a normal desktop session.\n\nThe `devicePixelRatio` varies by display: standard monitors report `1.0`, MacBook Retina displays report `2.0`, and smartphones report `2.0` to `3.0`. This value should be consistent with the claimed device in the User-Agent.\n\n## User-Agent Client Hints\n\nModern Chromium browsers (Chrome, Edge, Opera) supplement the traditional User-Agent string with Client Hints headers: `Sec-CH-UA`, `Sec-CH-UA-Platform`, `Sec-CH-UA-Mobile`, and (on request) higher-entropy values like `Sec-CH-UA-Full-Version-List`, `Sec-CH-UA-Arch`, and `Sec-CH-UA-Bitness`.\n\n```http\nSec-CH-UA: \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\", \"Not:A-Brand\";v=\"99\"\nSec-CH-UA-Mobile: ?0\nSec-CH-UA-Platform: \"Windows\"\n```\n\nClient Hints provide structured, machine-readable data that is harder to spoof inconsistently. A server can compare the `Sec-CH-UA-Platform` header against `navigator.platform`, the User-Agent string, and the TCP/IP fingerprint. Any inconsistency across these layers is a detection signal.\n\nThe JavaScript-side equivalent is `navigator.userAgentData`, which exposes `brands`, `mobile`, and `platform` as low-entropy values, and `getHighEntropyValues()` for detailed version, architecture, and bitness information:\n\n```javascript\n// Low-entropy (always available, no permission needed)\nconsole.log(navigator.userAgentData.brands);\n// [{brand: \"Chromium\", version: \"120\"}, {brand: \"Google Chrome\", version: \"120\"}, ...]\nconsole.log(navigator.userAgentData.platform); // \"Windows\"\nconsole.log(navigator.userAgentData.mobile);   // false\n\n// High-entropy (requires promise, may require permission)\nconst highEntropy = await navigator.userAgentData.getHighEntropyValues([\n    'architecture', 'bitness', 'platformVersion', 'uaFullVersion'\n]);\n// {architecture: \"x86\", bitness: \"64\", platformVersion: \"15.0.0\", ...}\n```\n\n!!! warning \"Browser Support\"\n    Client Hints are a Chromium-only feature. Firefox and Safari do not send `Sec-CH-UA` headers and do not expose `navigator.userAgentData`. If the User-Agent claims Firefox but the server receives Client Hints headers, the client is not Firefox.\n\n## Canvas Fingerprinting\n\nCanvas fingerprinting exploits the fact that the HTML5 Canvas API produces subtly different pixel output across different combinations of GPU, graphics driver, OS, and browser. The variation comes from differences in font rasterization (sub-pixel rendering, hinting, anti-aliasing), GPU-specific shader execution, floating-point precision in the graphics pipeline, and OS-level text rendering libraries (DirectWrite on Windows, Core Text on macOS, FreeType on Linux).\n\nThe technique draws text, shapes, and gradients onto a hidden canvas, extracts the pixel data, and hashes it:\n\n```javascript\nfunction generateCanvasFingerprint() {\n    const canvas = document.createElement('canvas');\n    canvas.width = 220;\n    canvas.height = 30;\n    const ctx = canvas.getContext('2d');\n\n    // Colored rectangle (exposes blending differences)\n    ctx.fillStyle = '#f60';\n    ctx.fillRect(125, 1, 62, 20);\n\n    // Text with emoji (maximizes rendering variation)\n    ctx.font = '14px Arial';\n    ctx.textBaseline = 'alphabetic';\n    ctx.fillStyle = '#069';\n    ctx.fillText('Cwm fjordbank glyphs vext quiz, 😃', 2, 15);\n\n    // Semi-transparent overlay (exposes alpha compositing differences)\n    ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';\n    ctx.fillText('Cwm fjordbank glyphs vext quiz, 😃', 4, 17);\n\n    return canvas.toDataURL();\n}\n```\n\nThe pangram \"Cwm fjordbank glyphs vext quiz\" is chosen because it uses unusual character combinations that stress font rendering. The emoji adds another dimension because emoji rendering varies significantly across operating systems. The semi-transparent overlay tests alpha compositing, which differs across GPU implementations.\n\nCanvas fingerprinting is effective for distinguishing broad categories of devices, but its uniqueness is sometimes overstated. Research by Laperdrix et al. (2016) found that canvas fingerprints alone provide moderate distinguishing power, and their real value comes from combining with other signals (WebGL, navigator properties, timezone) to achieve high uniqueness.\n\n!!! note \"Canvas Noise Injection\"\n    Some privacy tools inject random noise into canvas output to break fingerprinting. Detection systems counter this by requesting the canvas fingerprint multiple times in the same session. If the hash changes between requests, noise injection is present, which is itself a detection signal. Randomizing canvas output is therefore counterproductive: it does not prevent identification and it reveals the use of anti-fingerprinting tools.\n\nSince Pydoll controls a real Chrome instance with actual GPU rendering, the canvas fingerprint is authentic and consistent across repeated reads. No injection or spoofing is needed.\n\n## WebGL Fingerprinting\n\nWebGL fingerprinting extends canvas fingerprinting into the 3D rendering pipeline. It is more powerful because it directly exposes hardware identifiers that are difficult to spoof.\n\nThe most distinctive data comes from the `WEBGL_debug_renderer_info` extension, which reveals the GPU vendor and model:\n\n```javascript\nfunction getWebGLFingerprint() {\n    const canvas = document.createElement('canvas');\n    const gl = canvas.getContext('webgl');\n    if (!gl) return null;\n\n    // GPU identification (most distinctive)\n    const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');\n    const vendor = debugInfo\n        ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL)\n        : gl.getParameter(gl.VENDOR);\n    const renderer = debugInfo\n        ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)\n        : gl.getParameter(gl.RENDERER);\n\n    return {\n        vendor,    // e.g. \"Google Inc. (NVIDIA)\"\n        renderer,  // e.g. \"ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0)\"\n        version: gl.getParameter(gl.VERSION),\n        shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),\n        maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),\n        extensions: gl.getSupportedExtensions(),\n    };\n}\n```\n\nThe renderer string directly names the GPU hardware. A client claiming to be a mobile device but reporting a desktop GPU is obviously inconsistent. Virtual machines often report software renderers like \"SwiftShader\" or \"llvmpipe\", which real users almost never have.\n\nBeyond metadata, WebGL can render a 3D scene (a gradient triangle, for instance) and hash the pixel output, producing a render fingerprint analogous to canvas fingerprinting but in the 3D pipeline. The combination of GPU identifiers, supported extensions, parameter limits (`MAX_TEXTURE_SIZE`, `MAX_VIEWPORT_DIMS`), and shader precision formats creates a detailed fingerprint of the graphics stack.\n\n## AudioContext Fingerprinting\n\nThe Web Audio API generates fingerprints by processing audio and measuring the output. The standard technique creates an `OscillatorNode`, routes it through a `DynamicsCompressorNode`, and reads the resulting audio samples from an `AnalyserNode` or `OfflineAudioContext`. Differences in audio processing implementations across browsers and OS audio stacks produce distinct output.\n\n```javascript\nfunction getAudioFingerprint() {\n    const ctx = new OfflineAudioContext(1, 44100, 44100);\n    const oscillator = ctx.createOscillator();\n    oscillator.type = 'triangle';\n    oscillator.frequency.setValueAtTime(10000, ctx.currentTime);\n\n    const compressor = ctx.createDynamicsCompressor();\n    compressor.threshold.setValueAtTime(-50, ctx.currentTime);\n    compressor.knee.setValueAtTime(40, ctx.currentTime);\n    compressor.ratio.setValueAtTime(12, ctx.currentTime);\n    compressor.attack.setValueAtTime(0, ctx.currentTime);\n    compressor.release.setValueAtTime(0.25, ctx.currentTime);\n\n    oscillator.connect(compressor);\n    compressor.connect(ctx.destination);\n    oscillator.start(0);\n\n    return ctx.startRendering().then(buffer => {\n        const data = buffer.getChannelData(0);\n        // Hash a subset of the audio samples\n        let hash = 0;\n        for (let i = 4500; i < 5000; i++) {\n            hash += Math.abs(data[i]);\n        }\n        return hash;\n    });\n}\n```\n\nAudioContext fingerprinting is less widely deployed than canvas or WebGL fingerprinting, but it adds another dimension to the overall fingerprint. The signal is particularly useful for distinguishing browsers on the same OS, since audio processing varies more across browser engines than across OS versions.\n\n## Battery Status API\n\nThe Battery Status API (`navigator.getBattery()`) exposes the device's battery level, charging status, and estimated charge/discharge times. These values create a short-lived but unique fingerprint for the duration of a session.\n\nThis API is only available in Chromium browsers. Firefox removed it in version 52 (2017) citing privacy concerns, and Safari has never implemented it. Detection systems that see Battery API results from a client claiming to be Firefox or Safari know the client is misrepresenting its identity.\n\n## HTTP Header Fingerprinting\n\nBeyond JavaScript APIs, HTTP headers provide fingerprinting signals visible to the server before any JavaScript executes.\n\n### Header Order\n\nBrowsers send HTTP headers in a consistent, version-specific order. Chrome places `Sec-CH-UA` headers early, before `User-Agent`. Firefox leads with `User-Agent` followed by `Accept` and `Accept-Language`. Automated HTTP libraries like Python's `requests` or `httpx` send headers in yet another order, typically starting with `Host` and `Connection`.\n\nDetection systems record the order of the first 10-15 headers and compare against known browser signatures. Even if all individual header values are correct, sending them in the wrong order reveals that the request was not generated by the claimed browser. Since Pydoll controls a real Chrome instance, the header order is authentic.\n\n### Accept-Encoding\n\nModern browsers support Brotli compression (`br`) in addition to `gzip` and `deflate`. Chrome also supports `zstd`. The `Accept-Encoding` for modern Chrome looks like `gzip, deflate, br, zstd`. A client claiming to be Chrome but missing Brotli is either outdated or automated.\n\n### Accept-Language Consistency\n\nThe `Accept-Language` header should be consistent with `navigator.language`, `navigator.languages`, the timezone, and the IP geolocation. A request with `Accept-Language: en-US` from an IP in Tokyo with timezone `Asia/Tokyo` is plausible for a traveler but suspicious in combination with other signals. A request with `Accept-Language: zh-CN` and timezone `America/New_York` from a Chinese datacenter IP is a strong proxy indicator.\n\n## Implications for Pydoll\n\nBecause Pydoll drives a real Chromium browser through CDP, all browser-level fingerprints are authentic by default. The canvas, WebGL, and AudioContext fingerprints come from actual GPU and audio hardware. The navigator properties, plugins, and screen dimensions reflect the real browser state. HTTP headers, including their order, are generated by Chrome's networking stack.\n\nThe main risk in automation is inconsistency across layers. Setting a custom User-Agent without synchronizing related properties creates trivially detectable mismatches. Pydoll handles this automatically: when it detects `--user-agent=` in the browser arguments, it uses `Emulation.setUserAgentOverride` to synchronize the User-Agent string, platform, and full Client Hints metadata across all layers. It also injects `navigator.vendor` and `navigator.appVersion` overrides via `Page.addScriptToEvaluateOnNewDocument` to ensure consistency in newly opened tabs.\n\nFor timezone and geolocation consistency (to match a proxy IP's location), JavaScript overrides can set `Intl.DateTimeFormat().resolvedOptions().timeZone` and `Date.prototype.getTimezoneOffset`. The `--lang` flag and `set_accept_languages()` configure language headers. The `webrtc_leak_protection` option prevents WebRTC from exposing the real IP behind a proxy.\n\nThe general principle is that Pydoll provides the authentic browser fingerprint as a baseline, and the developer only needs to ensure that the configurable layers (User-Agent, timezone, language, geolocation) are consistent with each other and with the proxy's characteristics.\n\n## References\n\n- Laperdrix, P., Rudametkin, W., & Baudry, B. (2016). Beauty and the Beast: Diverting Modern Web Browsers to Build Unique Browser Fingerprints. IEEE S&P.\n- Mowery, K., & Shacham, H. (2012). Pixel Perfect: Fingerprinting Canvas in HTML5. USENIX Security.\n- Eckersley, P. (2010). How Unique Is Your Web Browser? Privacy Enhancing Technologies Symposium.\n- W3C Client Hints Infrastructure: https://wicg.github.io/client-hints-infrastructure/\n- BrowserLeaks: https://browserleaks.com/\n- CreepJS: https://abrahamjuliot.github.io/creepjs/\n"
  },
  {
    "path": "docs/en/deep-dive/fingerprinting/evasion-techniques.md",
    "content": "# Evasion Techniques\n\nThis document covers practical techniques for evading fingerprinting detection using Pydoll. The previous sections described how detection works at each layer: [network fingerprinting](./network-fingerprinting.md) (TCP/IP, TLS, HTTP/2), [browser fingerprinting](./browser-fingerprinting.md) (Canvas, WebGL, navigator properties), and [behavioral fingerprinting](./behavioral-fingerprinting.md) (mouse, keyboard, scroll). This section focuses on countermeasures.\n\nThe core principle is consistency across layers. Passing one detection layer while failing another still results in a flag. A residential IP with a mismatched TCP fingerprint, or a perfect browser fingerprint with robotic mouse movements, will be caught by any system that correlates signals.\n\n!!! info \"Module Navigation\"\n    - [Network Fingerprinting](./network-fingerprinting.md): Protocol-level identification\n    - [Browser Fingerprinting](./browser-fingerprinting.md): Application-layer detection\n    - [Behavioral Fingerprinting](./behavioral-fingerprinting.md): Human behavior analysis\n\n## What Pydoll Provides by Default\n\nBefore configuring anything, it helps to understand what Pydoll gives you for free by using a real Chrome instance via CDP.\n\n**Authentic network fingerprints.** Chrome's TCP/IP stack, TLS implementation (BoringSSL), and HTTP/2 stack produce genuine fingerprints. The TLS ClientHello, HTTP/2 SETTINGS frame, pseudo-header order, and stream priorities all match a real Chrome browser. Tools that construct HTTP requests programmatically (requests, httpx, curl) produce non-browser fingerprints at these layers. With Pydoll, they are authentic by default.\n\n**Authentic browser fingerprints.** Canvas, WebGL, and AudioContext fingerprints come from real GPU and audio hardware. Navigator properties, plugins (the standard 5 PDF plugins), and MIME types reflect genuine browser state. There is nothing to configure here.\n\n**No `navigator.webdriver`.** Selenium, Playwright, and Puppeteer set `navigator.webdriver` to `true`. Pydoll uses CDP directly, which does not set this flag. The property is `undefined`, matching a normal user session.\n\n**Complete event sequences.** When Pydoll dispatches input events through CDP's Input domain, Chrome generates the full event chain (pointermove, pointerdown, mousedown, pointerup, mouseup, click) exactly as it would for real user input.\n\n## User-Agent Consistency\n\nThe most common fingerprinting inconsistency in automation is a mismatch between the HTTP `User-Agent` header, `navigator.userAgent` in JavaScript, `navigator.platform`, and Client Hints headers (`Sec-CH-UA`, `Sec-CH-UA-Platform`). Setting `--user-agent=` as a Chrome flag only changes the HTTP header, leaving JavaScript properties and Client Hints unchanged.\n\nPydoll solves this automatically. When it detects `--user-agent=` in the browser arguments, it:\n\n1. Parses the UA string to extract browser name, version, and OS.\n2. Calls `Emulation.setUserAgentOverride` via CDP with the full `userAgent`, the correct `platform` value (e.g., `Win32` for Windows), and complete `userAgentMetadata` (Client Hints data including `Sec-CH-UA`, `Sec-CH-UA-Platform`, `Sec-CH-UA-Full-Version-List`).\n3. Injects `navigator.vendor` and `navigator.appVersion` overrides via `Page.addScriptToEvaluateOnNewDocument`, ensuring consistency even in newly opened tabs.\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.add_argument(\n    '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) '\n    'AppleWebKit/537.36 (KHTML, like Gecko) '\n    'Chrome/120.0.6099.109 Safari/537.36'\n)\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n    # All layers are now consistent:\n    # - HTTP User-Agent header\n    # - navigator.userAgent / navigator.platform / navigator.appVersion\n    # - Sec-CH-UA / Sec-CH-UA-Platform / Sec-CH-UA-Full-Version-List\n    # - navigator.userAgentData.brands / .platform\n    await tab.go_to('https://example.com')\n```\n\nThis override is applied automatically to the initial tab, new tabs from `browser.new_tab()`, and any tabs discovered via `browser.get_opened_tabs()`.\n\n!!! note \"Supported Platforms\"\n    The UA parser handles Chrome, Edge, Windows (NT 6.1 through 10.0), macOS, Linux, Android, iOS, and Chrome OS. It generates proper GREASE brand values following the Chromium specification.\n\n## Timezone and Locale Consistency\n\nWhen using a proxy, the browser's timezone and language should match the proxy IP's geographic location. An IP geolocated to Tokyo with a browser timezone of `America/New_York` and `Accept-Language: en-US` is a detectable inconsistency.\n\n### Language Configuration\n\nLanguage is configured through Chrome flags and Pydoll's options API:\n\n```python\noptions = ChromiumOptions()\noptions.add_argument('--lang=ja-JP')\noptions.set_accept_languages('ja-JP,ja;q=0.9,en;q=0.8')\n```\n\nThis sets both the `Accept-Language` HTTP header and `navigator.language` / `navigator.languages`.\n\n### Timezone Override\n\nPydoll does not currently wrap CDP's `Emulation.setTimezoneOverride` command, so timezone override requires JavaScript injection. The critical APIs to override are `Intl.DateTimeFormat().resolvedOptions().timeZone` and `Date.prototype.getTimezoneOffset()`:\n\n```python\nasync def set_timezone(tab, timezone_id: str, offset_minutes: int):\n    \"\"\"\n    Override timezone via JavaScript.\n\n    Args:\n        timezone_id: IANA timezone name (e.g., 'Asia/Tokyo')\n        offset_minutes: UTC offset in minutes (e.g., -540 for JST)\n    \"\"\"\n    script = f'''\n        const _origDTF = Intl.DateTimeFormat;\n        Intl.DateTimeFormat = function(...args) {{\n            const opts = args[1] || {{}};\n            opts.timeZone = '{timezone_id}';\n            return new _origDTF(args[0], opts);\n        }};\n        Object.defineProperty(Intl.DateTimeFormat, 'prototype', {{\n            value: _origDTF.prototype\n        }});\n        Date.prototype.getTimezoneOffset = function() {{ return {offset_minutes}; }};\n    '''\n    await tab.execute_script(script)\n```\n\n!!! warning \"`execute_script` vs `addScriptToEvaluateOnNewDocument`\"\n    `tab.execute_script()` runs JavaScript in the current page context. If the page navigates, the override is lost. For overrides that must persist across navigations, use CDP's `Page.addScriptToEvaluateOnNewDocument`, which injects the script before any page JavaScript runs on every new document load. Pydoll uses this internally for User-Agent overrides. For timezone, you can send the CDP command directly:\n\n    ```python\n    await tab._connection_handler.execute_command(\n        'Page.addScriptToEvaluateOnNewDocument',\n        {'source': script}\n    )\n    ```\n\n### Geolocation Override\n\nFor sites that request geolocation permission, the Geolocation API can be overridden via JavaScript:\n\n```python\nasync def set_geolocation(tab, latitude: float, longitude: float):\n    script = f'''\n        navigator.geolocation.getCurrentPosition = function(success) {{\n            success({{\n                coords: {{\n                    latitude: {latitude}, longitude: {longitude},\n                    accuracy: 1, altitude: null, altitudeAccuracy: null,\n                    heading: null, speed: null\n                }},\n                timestamp: Date.now()\n            }});\n        }};\n        navigator.geolocation.watchPosition = function(success) {{\n            return navigator.geolocation.getCurrentPosition(success);\n        }};\n    '''\n    await tab.execute_script(script)\n```\n\n## WebRTC Leak Protection\n\nWebRTC can expose the client's real IP address even when using a proxy, through STUN/TURN server requests that bypass the proxy tunnel. Pydoll provides a built-in option to prevent this:\n\n```python\noptions = ChromiumOptions()\noptions.webrtc_leak_protection = True\n# Adds: --force-webrtc-ip-handling-policy=disable_non_proxied_udp\n```\n\nThis forces Chrome to route all WebRTC traffic through the proxy, preventing IP leakage. It should be enabled whenever using a proxy for stealth automation.\n\n## Behavioral Humanization\n\nPydoll implements humanized interactions for mouse, keyboard, and scroll through the `humanize=True` parameter. These are not future features or manual workarounds; they are built into the framework.\n\n### Mouse\n\n```python\n# Humanized click: Bezier curve path, Fitts's Law timing,\n# minimum-jerk velocity, tremor, overshoot + correction\nawait element.click(humanize=True)\n```\n\nWhen `humanize=True` is passed to a WebElement's `click()`, Pydoll generates a complete mouse movement from the current cursor position to the element using a cubic Bezier curve with randomized control points. The velocity follows a minimum-jerk profile. Physiological tremor, overshoot (70% probability), and micro-pauses are added. The movement duration is computed from Fitts's Law based on the distance and target size. See [Behavioral Fingerprinting](./behavioral-fingerprinting.md#pydolls-mouse-humanization) for detailed parameter descriptions.\n\n### Keyboard\n\n```python\n# Humanized typing: variable delays, realistic typos (~2%),\n# punctuation pauses, thinking pauses, distraction pauses\nawait element.type_text(\"Hello, world!\", humanize=True)\n```\n\nHumanized typing uses variable inter-key delays (30-120ms uniform distribution), punctuation pauses, thinking pauses (2% probability), distraction pauses (0.5% probability), and realistic typos with five distinct error types and natural correction sequences. See [Behavioral Fingerprinting](./behavioral-fingerprinting.md#pydolls-keyboard-humanization) for the full parameter breakdown.\n\n### Scroll\n\n```python\nfrom pydoll.interactions.scroll import Scroll, ScrollPosition\n\nscroll = Scroll(connection_handler)\n# Humanized scroll: Bezier easing, jitter, micro-pauses, overshoot\nawait scroll.by(ScrollPosition.Y, 800, humanize=True)\n```\n\nHumanized scrolling uses Bezier easing curves, per-frame jitter (±3px), micro-pauses (5% probability), and overshoot correction (15% probability). Large distances are broken into multiple \"flick\" gestures. See [Behavioral Fingerprinting](./behavioral-fingerprinting.md#pydolls-scroll-humanization) for details.\n\n## Request Interception\n\nPydoll supports request interception via CDP's Fetch domain, allowing you to modify headers, block requests, or provide custom responses before they reach the server:\n\n```python\nfrom pydoll.protocol.fetch.events import FetchEvent\n\nasync def handle_request(event):\n    request_id = event['params']['requestId']\n    request = event['params']['request']\n    headers = request.get('headers', {})\n\n    # Example: ensure Brotli support is advertised\n    if 'Accept-Encoding' in headers and 'br' not in headers['Accept-Encoding']:\n        headers['Accept-Encoding'] = 'gzip, deflate, br, zstd'\n\n    header_list = [{'name': k, 'value': v} for k, v in headers.items()]\n    await tab.continue_request(request_id=request_id, headers=header_list)\n\nawait tab.enable_fetch_events()\nawait tab.on(FetchEvent.REQUEST_PAUSED, handle_request)\n```\n\nIn practice, header modification is rarely needed with Pydoll because Chrome generates correct headers natively. Request interception is more useful for blocking tracking scripts, modifying response content, or debugging.\n\n## Browser Preferences for Realism\n\nChrome stores user preferences that fingerprinting systems can inspect. A brand-new browser profile with no history, no saved preferences, and default-everything looks different from a profile that has been used for weeks. Pydoll's `browser_preferences` option lets you pre-populate these:\n\n```python\nimport time\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    'profile': {\n        'created_by_version': '120.0.6099.130',\n        'creation_time': str(time.time() - 90 * 86400),  # 90 days ago\n        'exit_type': 'Normal',\n    },\n    'profile.default_content_setting_values': {\n        'cookies': 1,\n        'images': 1,\n        'javascript': 1,\n        'notifications': 2,  # \"Ask\" (realistic default)\n    },\n}\n```\n\n## Common Mistakes\n\n### Randomizing Everything\n\nGenerating a random fingerprint from scratch (random hardwareConcurrency, random deviceMemory, random screen size) creates impossible combinations. Real devices have constrained configurations: a 4-core machine with 8 GB RAM, 1920x1080 screen, and Windows 10 is a plausible profile. A 17-core machine with 0.5 GB RAM, 3840x2160 screen, and `navigator.platform: Linux armv7l` is not. Use profiles captured from real browsers rather than random generation.\n\n### Canvas Noise Injection\n\nAdding random noise to canvas output to prevent fingerprinting is counterproductive. Detection systems request the fingerprint multiple times. If the hash changes between requests, noise injection is detected, which is itself a strong automation signal. With Pydoll, the canvas fingerprint is authentic and consistent. Leave it alone.\n\n### Outdated User-Agents\n\nUsing a User-Agent from a browser version that is 6+ months old is detectable because the version lacks features and Client Hints values that the current release would have. Keep User-Agent strings current within the last 2-3 major Chrome versions.\n\n### Ignoring Session-Level Behavior\n\nEven with perfect fingerprints and humanized interactions, session-level behavior matters. Loading 100 pages in 60 seconds, never scrolling, clicking only buttons (never links), and maintaining constant focus for hours without a single tab switch or idle period are all behavioral anomalies. Add reading delays between navigations, vary the pace of multi-page workflows, and include natural idle periods.\n\n## Verification\n\nBefore deploying automation at scale, verify your fingerprint using these tools:\n\n| Tool | URL | Tests |\n|------|-----|-------|\n| BrowserLeaks | https://browserleaks.com/ | Canvas, WebGL, fonts, IP, WebRTC, HTTP/2 |\n| CreepJS | https://abrahamjuliot.github.io/creepjs/ | Lie detection, consistency checks |\n| Fingerprint.com | https://fingerprint.com/demo/ | Commercial-grade identification |\n| PixelScan | https://pixelscan.net/ | Bot detection analysis |\n| IPLeak | https://ipleak.net/ | WebRTC, DNS, IP leaks |\n\nA basic verification script with Pydoll:\n\n```python\nasync def verify_fingerprint(tab):\n    result = await tab.execute_script('''\n        return {\n            userAgent: navigator.userAgent,\n            platform: navigator.platform,\n            webdriver: navigator.webdriver,\n            languages: navigator.languages,\n            plugins: navigator.plugins.length,\n            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n            colorDepth: screen.colorDepth,\n            deviceMemory: navigator.deviceMemory,\n            hardwareConcurrency: navigator.hardwareConcurrency,\n        };\n    ''')\n    fp = result['result']['result']['value']\n\n    # Check for obvious issues\n    assert fp['webdriver'] is None, 'navigator.webdriver should be undefined'\n    assert fp['plugins'] == 5, f'Expected 5 plugins, got {fp[\"plugins\"]}'\n    assert 'HeadlessChrome' not in fp['userAgent'], 'Headless detected in UA'\n```\n\n## References\n\n- Chrome DevTools Protocol, Emulation Domain: https://chromedevtools.github.io/devtools-protocol/tot/Emulation/\n- Chrome DevTools Protocol, Fetch Domain: https://chromedevtools.github.io/devtools-protocol/tot/Fetch/\n- Chromium Source, Inspector Emulation Agent: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc\n"
  },
  {
    "path": "docs/en/deep-dive/fingerprinting/index.md",
    "content": "# Browser & Network Fingerprinting\n\nThis module covers browser and network fingerprinting, a critical aspect of modern web automation and detection systems.\n\nFingerprinting sits at the intersection of network protocols, cryptography, browser internals, and behavioral analysis. It encompasses the techniques used to identify and track devices, browsers, and users across sessions without relying on traditional identifiers like cookies or IP addresses.\n\n## Why This Matters\n\nEvery browser connection to a website exposes multiple characteristics, from the precise order of TCP options in network packets, to GPU-specific canvas rendering, to JavaScript execution timing patterns. Individually, these characteristics may appear innocuous. Combined, they create a fingerprint that can uniquely identify a device or browser instance.\n\nFor automation engineers, bot developers, and privacy-conscious users, understanding fingerprinting is essential for building effective detection evasion systems and understanding how tracking mechanisms operate at a technical level.\n\n!!! danger \"Multi-Layer Detection Systems\"\n    Modern anti-bot systems employ comprehensive analysis across multiple layers:\n    \n    - **Network-level**: TCP/IP stack behavior, TLS handshake patterns, HTTP/2 settings\n    - **Browser-level**: Canvas rendering, WebGL vendor strings, JavaScript property enumeration\n    - **Behavioral**: Mouse movement entropy, keystroke timing, scroll patterns\n    \n    A single inconsistency (such as a Chrome User-Agent with Firefox TLS fingerprint) can trigger immediate blocking.\n\n## Module Scope and Methodology\n\nFingerprinting techniques are documented across multiple sources with varying levels of accessibility and reliability:\n\n- Academic papers (often paywalled and theoretical)\n- Browser source code (millions of lines to analyze)\n- Security researcher blogs (technical but fragmented)\n- Anti-bot vendor whitepapers (marketing-focused, details omitted)\n- Underground forums (practical but unreliable)\n\nThis module centralizes, validates, and organizes this knowledge into a cohesive technical guide. Every technique described here has been:\n\n- **Verified** against browser source code and RFCs\n- **Tested** in real automation scenarios\n- **Cited** with authoritative references\n- **Explained** from first principles to implementation  \n\n## Module Structure\n\nThis module is organized into three progressive layers, from network fundamentals to practical evasion techniques:\n\n### 1. Network-Level Fingerprinting\n**[Network Fingerprinting](./network-fingerprinting.md)**\n\nCovers device identification through network behavior at the transport and session layers, before browser rendering begins.\n\n- **TCP/IP fingerprinting**: TTL, window size, option ordering\n- **TLS fingerprinting**: JA3/JA4, cipher suites, ALPN negotiation\n- **HTTP/2 fingerprinting**: SETTINGS frames, priority patterns\n- **Tools & techniques**: p0f, Nmap, Scapy, tshark analysis\n\n**Technical significance**: Network fingerprints are the most challenging to spoof because they require OS-level modifications. Inconsistencies at this layer are detected before JavaScript execution begins.\n\n### 2. Browser-Level Fingerprinting\n**[Browser Fingerprinting](./browser-fingerprinting.md)**\n\nExamines browser identification through JavaScript APIs, rendering engines, and plugin ecosystems at the application layer.\n\n- **Canvas & WebGL fingerprinting**: GPU-specific rendering artifacts\n- **Audio fingerprinting**: Subtle differences in audio API output\n- **Font enumeration**: Installed fonts reveal OS and locale\n- **JavaScript properties**: Navigator object, screen dimensions, timezone\n- **Header analysis**: Accept-Language, User-Agent consistency\n\n**Technical significance**: This layer accounts for the majority of detection events. Even with correct network-level fingerprints, exposed automation properties (e.g., `navigator.webdriver`) can trigger blocking.\n\n### 3. Behavioral Fingerprinting\n**[Behavioral Fingerprinting](./behavioral-fingerprinting.md)**\n\nAnalyzes user interaction patterns to distinguish human behavior from automated systems.\n\n- **Mouse movement analysis**: Trajectory curvature, velocity profiles, Fitts's Law compliance\n- **Keystroke dynamics**: Typing rhythm, dwell time, flight time, bigram patterns\n- **Scroll patterns**: Momentum, inertia, deceleration curves\n- **Event sequences**: Natural interaction ordering (mousemove → click), timing analysis\n- **Machine learning**: ML models trained on billions of behavioral signals\n\n**Technical significance**: Behavioral analysis can detect automation even when network and browser fingerprints are correctly spoofed. This layer is particularly challenging because it requires replicating biomechanical human behavior patterns.\n\n### 4. Evasion Techniques\n**[Evasion Techniques](./evasion-techniques.md)**\n\nPractical implementation of fingerprinting evasion using Pydoll's CDP integration, JavaScript overrides, and architectural features.\n\n- **CDP-based spoofing**: Timezone, geolocation, device metrics\n- **JavaScript property overrides**: Redefining navigator objects, canvas poisoning\n- **Request interception**: Forcing header consistency\n- **Behavioral mimicry**: Human-like timing, entropy injection\n- **Detection testing**: Tools to validate your evasion setup\n\n**Technical significance**: This section demonstrates practical application of fingerprinting concepts to real automation scenarios, integrating techniques from all previous layers.\n\n## Who Should Read This\n\n### **You MUST read this if you're:**\n- Building automation that interacts with anti-bot protected sites\n- Developing scraping infrastructure at scale\n- Implementing privacy-preserving browser automation\n- Researching bot detection for offensive or defensive purposes\n\n### **This is advanced material if you're:**\n- New to network protocols (start with [Network Fundamentals](../network/network-fundamentals.md))\n- Unfamiliar with CDP (read [Chrome DevTools Protocol](../fundamentals/cdp.md) first)\n- Just learning Python typing (see [Type System](../fundamentals/typing-system.md))\n\n### **This is NOT:**\n- A \"silver bullet\" anti-detection solution (no such thing exists)\n- Legal advice on web scraping (consult [Legal & Ethical](../network/proxy-legal.md))\n- A replacement for respecting robots.txt and rate limits\n\n## The Technical Philosophy\n\nFingerprinting defense is **not about becoming invisible**—it's about becoming **indistinguishable from legitimate traffic**. This means:\n\n1. **Consistency over perfection**: A perfectly configured Firefox fingerprint is better than a \"perfect\" but inconsistent Chrome fingerprint\n2. **Holistic approach**: You must align network, browser, and behavioral layers\n3. **Continuous adaptation**: Fingerprinting techniques evolve monthly; this is a living document\n\n!!! tip \"The Golden Rule\"\n    **Every layer must tell the same story.** If your TLS fingerprint says \"Chrome 120\", your HTTP/2 settings must match Chrome 120, your User-Agent must say Chrome 120, and your canvas rendering must produce Chrome 120 artifacts. One mismatch = detection.\n\n## Ethical Considerations\n\nFingerprinting knowledge is **dual-use technology**:\n\n- **Defensive**: Protect your privacy from invasive tracking\n- **Offensive**: Evade detection systems for automation\n\nWe trust you to use this knowledge **responsibly and ethically**:\n\n**Recommended practices:**\n\n- Respect website terms of service\n- Implement rate limiting and respectful crawling patterns\n- Evaluate whether automation is necessary\n- Be transparent when appropriate\n\n**Prohibited uses:**\n\n- Fraud, account abuse, or illegal activities\n- Overwhelming servers with aggressive scraping\n- Weaponizing this knowledge without understanding consequences  \n\n## Ready to Dive Deep?\n\nFingerprinting is a complex and technical domain that requires systematic study. Understanding these techniques is essential for effective web automation in environments with detection systems.\n\nBegin with **[Network Fingerprinting](./network-fingerprinting.md)** to establish foundational knowledge, continue with **[Browser Fingerprinting](./browser-fingerprinting.md)** for application-layer understanding, and conclude with **[Evasion Techniques](./evasion-techniques.md)** for practical implementation.\n\n---\n\n!!! info \"Documentation Status\"\n    This module represents **extensive research** combining academic papers, browser source code, real-world testing, and community knowledge. Every claim is cited and validated. If you find inaccuracies or have updates, contributions are welcome.\n\n## Further Reading\n\nBefore diving in, consider these complementary topics:\n\n- **[Proxy Architecture](../network/http-proxies.md)**: Network-level anonymity fundamentals\n- **[Browser Preferences](../../features/configuration/browser-preferences.md)**: Practical fingerprint configuration\n- **[Behavioral Captcha Bypass](../../features/advanced/behavioral-captcha-bypass.md)**: Behavioral analysis and evasion\n"
  },
  {
    "path": "docs/en/deep-dive/fingerprinting/network-fingerprinting.md",
    "content": "# Network Fingerprinting\n\nNetwork fingerprinting identifies clients by analyzing characteristics of the TCP/IP stack, TLS handshake, and HTTP/2 connection. These signals are set by the operating system kernel and the TLS library, not by the browser's JavaScript environment, which makes them harder to spoof than browser-level fingerprints. A proxy or VPN changes your IP address but does not alter your TCP window size, your TLS cipher suite list, or your HTTP/2 SETTINGS frame. Detection systems exploit this gap.\n\n!!! info \"Module Navigation\"\n    - [Browser Fingerprinting](./browser-fingerprinting.md): Canvas, WebGL, AudioContext\n    - [Evasion Techniques](./evasion-techniques.md): Multi-layer countermeasures\n\n    For protocol fundamentals, see [Network Fundamentals](../network/network-fundamentals.md). For proxy detection context, see [Proxy Detection](../network/proxy-detection.md).\n\n## TCP/IP Fingerprinting\n\nEvery operating system implements the TCP/IP stack differently. The SYN packet that initiates a TCP connection carries enough information to identify the OS with high confidence: the initial TTL, the TCP window size, the Maximum Segment Size, and the order and selection of TCP options. None of these values are controlled by the browser. They come from the kernel.\n\n### TTL (Time To Live)\n\nThe initial TTL is one of the simplest OS identifiers. Linux and macOS set it to 64, Windows sets it to 128, and network devices (routers, firewalls) typically use 255. Each router hop decrements the TTL by one, so a packet arriving with TTL 118 likely started at 128 (Windows) and crossed 10 hops.\n\nThe fingerprinting value of TTL comes from cross-referencing it with the User-Agent. If the browser claims to be Chrome on Windows but the packet arrives with a TTL near 64, the connection is either proxied through a Linux server or the User-Agent is spoofed. Detection systems round the observed TTL up to the nearest known initial value (64, 128, 255) and compare it against the claimed OS.\n\nWhen traffic flows through a proxy, the TTL resets because the proxy's kernel generates a new TCP connection to the target. The target sees the proxy's TTL, not yours. This is why TTL mismatches are a proxy detection signal: the User-Agent says Windows (TTL 128) but the TCP fingerprint shows Linux (TTL 64).\n\n### TCP Window Size and Scaling\n\nThe initial TCP window size in the SYN packet varies by OS and kernel version. Modern Linux kernels (3.x and later) typically send an initial window of 29200 bytes, which is `20 * MSS` where MSS is 1460 for standard Ethernet. Some newer kernels (5.x, 6.x) may use 64240 depending on configuration and `initcwnd` settings. Windows 10 and 11 typically send 65535 with window scaling enabled, though the exact value depends on auto-tuning configuration and patch level. macOS also defaults to 65535.\n\nThe window scale factor (a TCP option) multiplies the 16-bit window size field to support larger receive windows. Linux commonly uses a scale factor of 7 (allowing windows up to 8MB), while Windows often uses 8. Combined with the base window size, the scale factor creates a more granular fingerprint than either value alone.\n\n### TCP Options Order\n\nThe selection and ordering of TCP options in the SYN packet is highly distinctive. Each OS arranges options in a fixed, version-specific order that the kernel does not expose as a configurable parameter. Linux sends `MSS, SACK_PERM, TIMESTAMP, NOP, WSCALE`. Windows sends `MSS, NOP, WSCALE, NOP, NOP, SACK_PERM` and notably omits the TIMESTAMP option in default configurations. macOS sends `MSS, NOP, WSCALE, NOP, NOP, TIMESTAMP, SACK_PERM`.\n\nThe presence or absence of specific options matters as much as the order. Windows historically omitted TCP timestamps, which Linux and macOS include by default. SACK (Selective Acknowledgment) is supported by all modern systems, but older or embedded systems may not advertise it. The combination of which options appear and in what order creates a signature that tools like p0f match against a database of known OS fingerprints.\n\n### p0f\n\n[p0f](https://lcamtuf.coredump.cx/p0f3/) is the standard tool for passive TCP/IP fingerprinting. It observes traffic without generating any packets, analyzing SYN and SYN+ACK packets against a signature database. Its signature format encodes the key fingerprinting fields:\n\n```\nversion:ittl:olen:mss:wsize,scale:olayout:quirks:pclass\n```\n\nThe `ittl` is the inferred initial TTL, `mss` is the Maximum Segment Size, `wsize,scale` is the window size (which can be absolute, or relative to MSS like `mss*20`), and `olayout` is the TCP options layout using shorthand names (`mss`, `nop`, `ws`, `sok`, `sack`, `ts`, `eol+N`). The `quirks` field captures unusual behaviors like the Don't Fragment flag (`df`) or non-zero IP ID on DF packets (`id+`).\n\nA typical Linux 4.x+ signature in p0f looks like `4:64:0:*:mss*20,7:mss,sok,ts,nop,ws:df,id+:0`. A Windows 10 signature might look like `4:128:0:*:65535,8:mss,nop,ws,nop,nop,sok:df,id+:0`. Anti-bot services maintain similar databases internally, matching incoming connections against known OS profiles and flagging mismatches with the declared User-Agent.\n\n## TLS Fingerprinting\n\nThe TLS ClientHello message is transmitted before encryption is established, so it is visible to any observer on the network path. It contains the TLS version, supported cipher suites, TLS extensions, supported elliptic curves (named groups), and EC point formats. Each browser and TLS library produces a characteristic combination of these fields.\n\n### JA3\n\nJA3, developed at Salesforce by John Althouse, Jeff Atkinson, and Josh Atkins, was the first widely adopted TLS fingerprinting method. It concatenates five fields from the ClientHello (TLS version, cipher suites, extensions, elliptic curves, EC point formats), joins values within each field with hyphens, separates the five fields with commas, and takes the MD5 hash of the resulting string.\n\n```\nJA3 string: 771,4865-4866-4867-49195-49199-49196-49200-52393-52392,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0\nJA3 hash:   cd08e31494b9531f560d64c695473da9\n```\n\nOne subtlety: the \"TLS version\" field in JA3 uses `ClientHello.legacy_version`, not the `supported_versions` extension. Since TLS 1.3 (RFC 8446) requires clients to set `legacy_version` to `0x0303` (TLS 1.2) for backwards compatibility, the JA3 version field is almost always `771` for modern clients, even when they support TLS 1.3. The actual TLS 1.3 negotiation happens through extension 43 (`supported_versions`), but JA3 uses the header field.\n\nJA3 must filter GREASE values before hashing. GREASE (RFC 8701) is a mechanism where browsers insert randomly selected reserved values into cipher suites, extensions, and other fields to prevent protocol ossification. The valid GREASE values are `0x0a0a`, `0x1a1a`, `0x2a2a`, and so on up to `0xfafa`. Each value has two identical bytes where the low nibble of each byte is `0x0a`. A correct GREASE filter checks both conditions:\n\n```python\ndef is_grease(value: int) -> bool:\n    return (value & 0x0f0f) == 0x0a0a and (value >> 8) == (value & 0xff)\n```\n\n!!! warning \"JA3 Limitations with Modern Browsers\"\n    Since Chrome 110 (January 2023) and Firefox 114, browsers randomize the order of TLS extensions in every connection. This means the same browser produces different JA3 hashes on every connection, making JA3 effectively useless for identifying modern browsers. JA3 remains useful for fingerprinting non-browser clients (Python `requests`, `curl`, custom bots) that do not implement extension randomization.\n\n### JA4\n\nJA4 is the successor to JA3, developed by the same lead author (John Althouse) at FoxIO. It was designed specifically to survive TLS extension randomization by sorting extensions and cipher suites before hashing. The format consists of three sections separated by underscores: `a_b_c`.\n\nSection `a` is a human-readable string of metadata: the protocol (`t` for TCP, `q` for QUIC), the TLS version (`12` or `13`), whether SNI is present (`d` for domain, `i` for IP), the number of cipher suites (two digits), the number of extensions (two digits), and the first and last ALPN value (`h2` for HTTP/2, `00` if none). For example, `t13d1516h2` means TCP TLS 1.3 with SNI, 15 cipher suites, 16 extensions, and HTTP/2 ALPN.\n\nSection `b` is a truncated SHA-256 hash of the sorted cipher suites. Section `c` is a truncated SHA-256 hash of the sorted extensions concatenated with the signature algorithms. Because both lists are sorted before hashing, extension randomization does not affect the output.\n\nCloudflare, AWS, and other major platforms have adopted JA4. The full JA4+ suite also includes JA4S (server fingerprinting), JA4H (HTTP client fingerprinting), JA4X (X.509 certificate fingerprinting), and JA4SSH (SSH fingerprinting). The specification and tools are available at [github.com/FoxIO-LLC/ja4](https://github.com/FoxIO-LLC/ja4).\n\n### JA3S (Server Fingerprinting)\n\nJA3S applies the same concept to the ServerHello message, but the format is simpler because the server selects a single cipher suite rather than offering a list. The JA3S string is `version,cipher,extensions` and its MD5 hash identifies the server's TLS implementation. Pairing JA3 (or JA4) with JA3S creates a bidirectional fingerprint: a specific client talking to a specific server produces a predictable JA3+JA3S pair, which is more distinctive than either fingerprint alone.\n\n### How Proxies Interact with TLS Fingerprints\n\nThe type of proxy determines whether the TLS fingerprint is preserved. SOCKS5 proxies and HTTP CONNECT tunnels relay the TCP stream without terminating TLS, so the target server sees the original client's TLS fingerprint unchanged. This is the main advantage of these proxy types for fingerprint consistency.\n\nMITM proxies (which terminate TLS and re-establish a new connection to the target) replace the client's TLS fingerprint with their own. The target sees the proxy software's cipher suites and extensions, not the browser's. If the proxy uses a standard TLS library like OpenSSL or BoringSSL with default settings, the fingerprint will not match any known browser, which is itself a detection signal.\n\nThis is why Pydoll's approach of using `--proxy-server` (which creates a CONNECT tunnel, preserving the browser's TLS fingerprint) is preferable to external MITM proxy setups for stealth automation.\n\n## HTTP/2 Fingerprinting\n\nHTTP/2 connections expose a separate set of fingerprinting signals that are distinct from TLS. The first frame sent by the client is a SETTINGS frame containing parameters like `HEADER_TABLE_SIZE`, `ENABLE_PUSH`, `MAX_CONCURRENT_STREAMS`, `INITIAL_WINDOW_SIZE`, `MAX_FRAME_SIZE`, and `MAX_HEADER_LIST_SIZE`. Each browser uses different default values and includes different subsets of these parameters.\n\nBeyond SETTINGS, the WINDOW_UPDATE frame size, the priority/weight of the initial stream, and the order of HTTP/2 pseudo-headers (`:method`, `:authority`, `:scheme`, `:path`) vary between implementations. Chrome, Firefox, and Safari each produce a distinctive combination of these values.\n\nAkamai published the foundational research on HTTP/2 fingerprinting at Black Hat Europe 2017. Their fingerprint format concatenates the SETTINGS values, WINDOW_UPDATE size, PRIORITY frames, and pseudo-header order. The JA4+ suite includes `JA4H` for HTTP-level fingerprinting, covering header order and values.\n\nHTTP/2 fingerprinting is particularly effective against automation tools because many bot frameworks and HTTP libraries implement their own HTTP/2 stacks with default parameters that do not match any real browser. Even when a tool correctly spoofs the TLS fingerprint (using curl-impersonate or similar), its HTTP/2 SETTINGS frame may betray it.\n\nYou can check your HTTP/2 fingerprint at [browserleaks.com/http2](https://browserleaks.com/http2). Because Pydoll controls a real Chrome instance via CDP, the HTTP/2 fingerprint is always authentic, which is an inherent advantage over tools that construct HTTP requests programmatically.\n\n## Implications for Browser Automation\n\nThe practical takeaway for automation with Pydoll is that network fingerprinting is one area where controlling a real browser provides a significant advantage. Chrome's TCP/IP stack, TLS implementation (BoringSSL), and HTTP/2 stack produce authentic fingerprints by default. The main risk is environmental mismatch: running Chrome on a Linux server while the User-Agent claims Windows creates a TCP/IP fingerprint inconsistency (TTL 64 instead of 128, Linux TCP options order instead of Windows).\n\nFor proxy-based setups, the fingerprint flow is: your machine's TCP/IP stack generates the connection to the proxy (which the proxy's operator can see but the target cannot), and the proxy's TCP/IP stack generates the connection to the target. The target sees the proxy server's TTL and TCP options. If the proxy runs Linux (as most do), the TCP fingerprint will indicate Linux regardless of the User-Agent. This is a well-known detection signal that residential proxies partially mitigate (the proxy endpoint is a real user's machine, so its TCP fingerprint is plausible) but datacenter proxies cannot.\n\nThe TLS and HTTP/2 fingerprints, on the other hand, pass through SOCKS5 and CONNECT tunnels unmodified. These are the browser's fingerprints, not the proxy's. So with Pydoll through a CONNECT tunnel, the target sees authentic Chrome TLS and HTTP/2 fingerprints paired with the proxy's TCP/IP fingerprint. This combination is consistent with a real user browsing through a VPN or corporate proxy, which is a common and legitimate pattern.\n\n## References\n\n- Salesforce Engineering: TLS Fingerprinting with JA3 and JA3S - https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/\n- FoxIO JA4+ Network Fingerprinting - https://github.com/FoxIO-LLC/ja4\n- Cloudflare: JA4 Signals - https://blog.cloudflare.com/ja4-signals/\n- Akamai: Passive Fingerprinting of HTTP/2 Clients (Black Hat EU 2017) - https://blackhat.com/docs/eu-17/materials/eu-17-Shuster-Passive-Fingerprinting-Of-HTTP2-Clients-wp.pdf\n- p0f v3: Passive OS Fingerprinting - https://lcamtuf.coredump.cx/p0f3/\n- RFC 8446: TLS 1.3 - https://datatracker.ietf.org/doc/html/rfc8446\n- RFC 8701: GREASE for TLS - https://datatracker.ietf.org/doc/html/rfc8701\n- RFC 6528: Defending against Sequence Number Attacks - https://datatracker.ietf.org/doc/html/rfc6528\n- BrowserLeaks HTTP/2 Fingerprint - https://browserleaks.com/http2\n- Stamus Networks: JA3 Fingerprints Fade as Browsers Embrace Extension Randomization - https://www.stamus-networks.com/blog/ja3-fingerprints-fade-browsers-embrace-tls-extension-randomization\n"
  },
  {
    "path": "docs/en/deep-dive/fundamentals/cdp.md",
    "content": "# Chrome DevTools Protocol (CDP)\n\nThe Chrome DevTools Protocol (CDP) is the foundation that enables Pydoll to control browsers without traditional webdrivers. Understanding how CDP works provides valuable insight into Pydoll's capabilities and internal architecture.\n\n\n## What is CDP?\n\nThe Chrome DevTools Protocol is a powerful interface developed by the Chromium team that allows programmatic interaction with Chromium-based browsers. It's the same protocol used by Chrome DevTools when you inspect a webpage, but exposed as a programmable API that can be leveraged by automation tools.\n\nAt its core, CDP provides a comprehensive set of methods and events for interfacing with browser internals. This allows for fine-grained control over every aspect of the browser, from navigating between pages to manipulating the DOM, intercepting network requests, and monitoring performance metrics.\n\n!!! info \"CDP Evolution\"\n    The Chrome DevTools Protocol has been continuously evolving since its introduction. Google maintains and updates the protocol with each Chrome release, regularly adding new functionality and improving existing features.\n    \n    While the protocol was initially designed for Chrome's DevTools, its comprehensive capabilities have made it the foundation for next-generation browser automation tools like Puppeteer, Playwright, and of course, Pydoll.\n\n## WebSocket Communication\n\nOne of the key architectural decisions in CDP is its use of WebSockets for communication. When a Chromium-based browser is started with the remote debugging flag enabled, it opens a WebSocket server on a specified port:\n\n```\nchrome --remote-debugging-port=9222\n```\n\nPydoll connects to this WebSocket endpoint to establish a bidirectional communication channel with the browser. This connection:\n\n1. **Remains persistent** throughout the automation session\n2. **Enables real-time events** from the browser to be pushed to the client\n3. **Allows commands** to be sent to the browser\n4. **Supports binary data** for efficient transfer of screenshots, PDFs, and other assets\n\nThe WebSocket protocol is particularly well-suited for browser automation because it provides:\n\n- **Low latency communication** - Necessary for responsive automation\n- **Bidirectional messaging** - Essential for event-driven architecture\n- **Persistent connections** - Eliminating connection setup overhead for each operation\n\nHere's a simplified view of how Pydoll's communication with the browser works:\n\n```mermaid\nsequenceDiagram\n    participant App as Pydoll Application\n    participant WS as WebSocket Connection\n    participant Browser as Chrome Browser\n\n    App ->> WS: Command: navigate to URL\n    WS ->> Browser: Execute navigation\n\n    Browser -->> WS: Send page load event\n    WS -->> App: Receive page load event\n```\n\n!!! info \"WebSocket vs HTTP\"\n    Earlier browser automation protocols often relied on HTTP endpoints for communication. CDP's switch to WebSockets represents a significant architectural improvement that enables more responsive automation and real-time event monitoring.\n    \n    HTTP-based protocols require continuous polling to detect changes, creating overhead and delays. WebSockets allow the browser to push notifications to your automation script exactly when events occur, with minimal latency.\n\n## Key CDP Domains\n\nCDP is organized into logical domains, each responsible for a specific aspect of browser functionality. Some of the most important domains include:\n\n\n| Domain | Responsibility | Example Use Cases |\n|--------|----------------|------------------|\n| **Browser** | Control of the browser application itself | Window management, browser context creation |\n| **Page** | Interaction with page lifecycle | Navigation, JavaScript execution, frame management |\n| **DOM** | Access to page structure | Query selectors, attribute modification, event listeners |\n| **Network** | Network traffic monitoring and control | Request interception, response examination, caching |\n| **Runtime** | JavaScript execution environment | Evaluate expressions, call functions, handle exceptions |\n| **Input** | Simulating user interactions | Mouse movements, keyboard input, touch events |\n| **Target** | Managing browser contexts and targets | Creating tabs, accessing iframes, handling popups |\n| **Fetch** | Low-level network interception | Modifying requests, simulating responses, authentication |\n\nPydoll maps these CDP domains to a more intuitive API structure while preserving the full capabilities of the underlying protocol.\n\n## Event-Driven Architecture\n\nOne of CDP's most powerful features is its event system. The protocol allows clients to subscribe to various events that the browser emits during normal operation. These events cover virtually every aspect of browser behavior:\n\n- **Lifecycle events**: Page loads, frame navigation, target creation\n- **DOM events**: Element changes, attribute modifications\n- **Network events**: Request/response cycles, WebSocket messages\n- **Execution events**: JavaScript exceptions, console messages\n- **Performance events**: Metrics for rendering, scripting, and more\n\n\nWhen you enable event monitoring in Pydoll (e.g., with `page.enable_network_events()`), the library sets up the necessary subscriptions with the browser and provides hooks for your code to react to these events.\n\n```python\nfrom pydoll.events.network import NetworkEvents\nfrom functools import partial\n\nasync def on_request(page, event):\n    url = event['params']['request']['url']\n    print(f\"Request to: {url}\")\n\n# Subscribe to network request events\nawait page.enable_network_events()\nawait page.on(NetworkEvents.REQUEST_WILL_BE_SENT, partial(on_request, page))\n```\n\nThis event-driven approach allows automation scripts to react immediately to browser state changes without relying on inefficient polling or arbitrary delays.\n\n## Performance Advantages of Direct CDP Integration\n\nUsing CDP directly, as Pydoll does, offers several performance advantages over traditional webdriver-based automation:\n\n### 1. Elimination of Protocol Translation Layer\n\nTraditional webdriver-based tools like Selenium use a multi-layered approach:\n\n```mermaid\ngraph LR\n    AS[Automation Script] --> WC[WebDriver Client]\n    WC --> WS[WebDriver Server]\n    WS --> B[Browser]\n```\n\nEach layer adds overhead, especially the WebDriver server, which acts as a translation layer between the WebDriver protocol and the browser's native APIs.\n\nPydoll's approach streamlines this to:\n\n```mermaid\ngraph LR\n    AS[Automation Script] --> P[Pydoll]\n    P --> B[Browser via CDP]\n```\n\nThis direct communication eliminates the computational and network overhead of the intermediate server, resulting in faster operations.\n\n### 2. Efficient Command Batching\n\nCDP allows for the batching of multiple commands in a single message, reducing the number of round trips required for complex operations. This is particularly valuable for operations that require several steps, such as finding an element and then interacting with it.\n\n### 3. Asynchronous Operation\n\nCDP's WebSocket-based, event-driven architecture aligns perfectly with Python's asyncio framework, enabling true asynchronous operation. This allows Pydoll to:\n\n- Execute multiple operations concurrently\n- Process events as they occur\n- Avoid blocking the main thread during I/O operations\n\n```mermaid\ngraph TD\n    subgraph \"Pydoll Async Architecture\"\n        EL[Event Loop]\n        \n        subgraph \"Concurrent Tasks\"\n            T1[Task 1: Navigate]\n            T2[Task 2: Wait for Element]\n            T3[Task 3: Handle Network Events]\n        end\n        \n        EL --> T1\n        EL --> T2\n        EL --> T3\n        \n        T1 --> WS[WebSocket Connection]\n        T2 --> WS\n        T3 --> WS\n        \n        WS --> B[Browser]\n    end\n```\n\n!!! info \"Async Performance Gains\"\n    The combination of asyncio and CDP creates a multiplicative effect on performance. In benchmark tests, Pydoll's asynchronous approach can process multiple pages in parallel with near-linear scaling, while traditional synchronous tools see diminishing returns as concurrency increases.\n    \n    For example, scraping 10 pages that each take 2 seconds to load might take over 20 seconds with a synchronous tool, but just over 2 seconds with Pydoll's async architecture (plus some minimal overhead).\n\n### 4. Fine-Grained Control\n\nCDP provides more granular control over browser behavior than the WebDriver protocol. This allows Pydoll to implement optimized strategies for common operations:\n\n- More precise waiting conditions (vs. arbitrary timeouts)\n- Direct access to browser caches and storage\n- Targeted JavaScript execution in specific contexts\n- Detailed network control for request optimization\n\n\n## Conclusion\n\nThe Chrome DevTools Protocol forms the foundation of Pydoll's zero-webdriver approach to browser automation. By leveraging CDP's WebSocket communication, comprehensive domain coverage, event-driven architecture, and direct browser integration, Pydoll achieves superior performance and reliability compared to traditional automation tools.\n\nIn the following sections, we'll dive deeper into how Pydoll implements specific CDP domains and transforms the low-level protocol into an intuitive, developer-friendly API. "
  },
  {
    "path": "docs/en/deep-dive/fundamentals/connection-layer.md",
    "content": "# Connection Handler\n\nThe Connection Handler is the foundational layer of Pydoll's architecture, serving as the bridge between your Python code and the browser's Chrome DevTools Protocol (CDP). This component manages the WebSocket connection to the browser, handles command execution, and processes events in a non-blocking, asynchronous manner.\n\n```mermaid\ngraph TD\n    A[Python Code] --> B[Connection Handler]\n    B <--> C[WebSocket]\n    C <--> D[Browser CDP Endpoint]\n\n    subgraph \"Connection Handler\"\n        E[Command Manager]\n        F[Events Handler]\n        G[WebSocket Client]\n    end\n\n    B --> E\n    B --> F\n    B --> G\n```\n\n## Asynchronous Programming Model\n\nPydoll is built on Python's `asyncio` framework, which enables non-blocking I/O operations. This design choice is critical for high-performance browser automation, as it allows multiple operations to occur concurrently without waiting for each to complete.\n\n### Understanding Async/Await\n\n\nTo understand how async/await works in practice, let's examine a more detailed example with two concurrent operations:\n\n```python\nimport asyncio\nfrom pydoll.browser.chrome import Chrome\n\nasync def fetch_page_data(url):\n    print(f\"Starting fetch for {url}\")\n    browser = Chrome()\n    await browser.start()\n    page = await browser.get_page()\n    \n    # Navigation takes time - this is where we yield control\n    await page.go_to(url)\n    \n    # Get page title\n    title = await page.execute_script(\"return document.title\")\n    \n    # Extract some data\n    description = await page.execute_script(\n        \"return document.querySelector('meta[name=\\\"description\\\"]')?.content || ''\"\n    )\n    \n    await browser.stop()\n    print(f\"Completed fetch for {url}\")\n    return {\"url\": url, \"title\": title, \"description\": description}\n\nasync def main():\n    # Start two page operations concurrently\n    task1 = asyncio.create_task(fetch_page_data(\"https://example.com\"))\n    task2 = asyncio.create_task(fetch_page_data(\"https://github.com\"))\n    \n    # Wait for both to complete and get results\n    result1 = await task1\n    result2 = await task2\n    \n    return [result1, result2]\n\n# Run the async function\nresults = asyncio.run(main())\n```\n\nThis example demonstrates how we can fetch data from two different websites concurrently, potentially cutting the overall execution time nearly in half compared to sequential execution.\n\n#### Async Execution Flow Diagram\n\nHere's what happens in the event loop when executing the code above:\n\n```mermaid\nsequenceDiagram\n    participant A as Main Code\n    participant B as Task 1<br/> (example.com)\n    participant C as Task 2<br/> (github.com)\n    participant D as Event Loop\n    \n    A->>B: Create task1\n    B->>D: Register in loop\n    A->>C: Create task2\n    C->>D: Register in loop\n    D->>B: Execute until browser.start()\n    D->>C: Execute until browser.start()\n    D-->>B: Resume after WebSocket connected\n    D-->>C: Resume after WebSocket connected\n    D->>B: Execute until page.go_to()\n    D->>C: Execute until page.go_to()\n    D-->>B: Resume after page loaded\n    D-->>C: Resume after page loaded\n    B-->>A: Return result\n    C-->>A: Return result\n```\n\nThis sequence diagram illustrates how Python's asyncio manages the two concurrent tasks in our example code:\n\n1. The main function creates two tasks for fetching data from different websites\n2. Both tasks are registered in the event loop\n3. The event loop executes each task until it hits an `await` statement (like `browser.start()`)\n4. When async operations complete (like a WebSocket connection being established), tasks resume\n5. The loop continues to switch between tasks at each `await` point\n6. When each task completes, it returns its result back to the main function\n\nIn the `fetch_page_data` example, this allows both browser instances to work concurrently - while one is waiting for a page to load, the other can be making progress. This is significantly more efficient than sequentially processing each website, as I/O wait times don't block the execution of other tasks.\n\n!!! info \"Cooperative Multitasking\"\n    Asyncio uses cooperative multitasking, where tasks voluntarily yield control at `await` points. This differs from preemptive multitasking (threads), where tasks can be interrupted at any time. Cooperative multitasking can provide better performance for I/O-bound operations but requires careful coding to avoid blocking the event loop.\n\n## Connection Handler Implementation\n\nThe `ConnectionHandler` class is designed to manage both command execution and event processing, providing a robust interface to the CDP WebSocket connection.\n\n### Class Initialization\n\n```python\ndef __init__(\n    self,\n    connection_port: int,\n    page_id: str = 'browser',\n    ws_address_resolver: Callable[[int], str] = get_browser_ws_address,\n    ws_connector: Callable = websockets.connect,\n):\n    # Initialize components...\n```\n\nThe ConnectionHandler accepts several parameters:\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `connection_port` | `int` | Port number where the browser's CDP endpoint is listening |\n| `page_id` | `str` | Identifier for the specific page/target (use 'browser' for browser-level connections) |\n| `ws_address_resolver` | `Callable` | Function to resolve the WebSocket URL from the port number |\n| `ws_connector` | `Callable` | Function to establish the WebSocket connection |\n\n### Internal Components\n\nThe ConnectionHandler orchestrates three primary components:\n\n1. **WebSocket Connection**: Manages the actual WebSocket communication with the browser\n2. **Command Manager**: Handles sending commands and receiving responses\n3. **Events Handler**: Processes events from the browser and triggers appropriate callbacks\n\n```mermaid\nclassDiagram\n    class ConnectionHandler {\n        -_connection_port: int\n        -_page_id: str\n        -_ws_connection\n        -_command_manager: CommandManager\n        -_events_handler: EventsHandler\n        +execute_command(command, timeout) async\n        +register_callback(event_name, callback) async\n        +remove_callback(callback_id) async\n        +ping() async\n        +close() async\n        -_receive_events() async\n    }\n\n    class CommandManager {\n        -_pending_commands: dict\n        +create_command_future(command)\n        +resolve_command(id, response)\n        +remove_pending_command(id)\n    }\n\n    class EventsHandler {\n        -_callbacks: dict\n        -_network_logs: list\n        -_dialog: dict\n        +register_callback(event_name, callback, temporary)\n        +remove_callback(callback_id)\n        +clear_callbacks()\n        +process_event(event) async\n    }\n\n    ConnectionHandler *-- CommandManager\n    ConnectionHandler *-- EventsHandler\n```\n\n## Command Execution Flow\n\nWhen executing a command through the CDP, the ConnectionHandler follows a specific pattern:\n\n1. Ensure an active WebSocket connection exists\n2. Create a Future object to represent the pending response\n3. Send the command over the WebSocket\n4. Await the Future to be resolved with the response\n5. Return the response to the caller\n\n```python\nasync def execute_command(self, command: dict, timeout: int = 10) -> dict:\n    # Validate command\n    if not isinstance(command, dict):\n        logger.error('Command must be a dictionary.')\n        raise exceptions.InvalidCommand('Command must be a dictionary')\n\n    # Ensure connection is active\n    await self._ensure_active_connection()\n    \n    # Create future for this command\n    future = self._command_manager.create_command_future(command)\n    command_str = json.dumps(command)\n\n    # Send command and await response\n    try:\n        await self._ws_connection.send(command_str)\n        response: str = await asyncio.wait_for(future, timeout)\n        return json.loads(response)\n    except asyncio.TimeoutError as exc:\n        self._command_manager.remove_pending_command(command['id'])\n        raise exc\n    except websockets.ConnectionClosed as exc:\n        await self._handle_connection_loss()\n        raise exc\n```\n\n!!! warning \"Command Timeout\"\n    Commands that don't receive a response within the specified timeout period will raise a `TimeoutError`. This prevents automation scripts from hanging indefinitely due to missing responses. The default timeout is 10 seconds, but can be adjusted based on expected response times for complex operations.\n\n## Event Processing System\n\nThe event system is a key architectural component that enables reactive programming patterns in Pydoll. It allows you to register callbacks for specific browser events and have them executed automatically when those events occur.\n\n### Event Flow\n\nThe event processing flow follows these steps:\n\n1. The `_receive_events` method runs as a background task, continuously receiving messages from the WebSocket\n2. Each message is parsed and classified as either a command response or an event\n3. Events are passed to the EventsHandler for processing\n4. The EventsHandler identifies registered callbacks for the event and invokes them\n\n```mermaid\nflowchart TD\n    A[WebSocket Message] --> B{Is Command Response?}\n    B -->|Yes| C[Resolve Command Future]\n    B -->|No| D[Process as Event]\n    D --> E[Find Matching Callbacks]\n    E --> F[Execute Callbacks]\n    F --> G{Is Temporary?}\n    G -->|Yes| H[Remove Callback]\n    G -->|No| I[Keep Callback]\n```\n\n### Callback Registration\n\nThe ConnectionHandler provides methods to register, remove, and manage event callbacks:\n\n```python\n# Register a callback for a specific event\ncallback_id = await connection.register_callback(\n    'Page.loadEventFired', \n    handle_page_load\n)\n\n# Remove a specific callback\nawait connection.remove_callback(callback_id)\n\n# Remove all callbacks\nawait connection.clear_callbacks()\n```\n\n!!! tip \"Temporary Callbacks\"\n    You can register a callback as temporary, which means it will be automatically removed after being triggered once. This is useful for one-time events like dialog handling:\n    \n    ```python\n    await connection.register_callback(\n        'Page.javascriptDialogOpening',\n        handle_dialog,\n        temporary=True\n    )\n    ```\n\n### Asynchronous Callback Execution\n\nCallbacks can be either synchronous functions or asynchronous coroutines. The EventsHandler (managed by the ConnectionHandler) handles both types properly:\n\n```python\n# Synchronous callback\ndef synchronous_callback(event):\n    print(f\"Event received: {event['method']}\")\n\n# Asynchronous callback\nasync def asynchronous_callback(event):\n    await asyncio.sleep(0.1)  # Perform some async operation\n    print(f\"Event processed asynchronously: {event['method']}\")\n\n# Both can be registered the same way\nawait connection.register_callback('Network.requestWillBeSent', synchronous_callback)\nawait connection.register_callback('Network.responseReceived', asynchronous_callback)\n```\n\n**Sequential Execution Model:**\n\nAsynchronous callbacks are **awaited sequentially** by the EventsManager. This ensures that for a single event, callbacks execute in the order they were registered, preventing race conditions when multiple callbacks modify shared state.\n\n```python\n# Inside EventsManager.process_event()\nfor callback_data in callbacks:\n    if asyncio.iscoroutinefunction(callback_data['callback']):\n        await callback_data['callback'](event_data)  # Sequential await\n    else:\n        callback_data['callback'](event_data)  # Sync execution\n```\n\n**Non-blocking execution** (for UI callbacks that should not block other operations) is achieved at a **higher level**, such as in the `Tab.on()` method, which wraps the user's callback in an `asyncio.create_task()` before registering it here. This architecture provides:\n\n- **Lower layer** (ConnectionHandler/EventsManager): Guarantees sequential execution and predictable order\n- **Higher layer** (Tab.on()): Provides non-blocking semantics when needed\n\n!!! info \"Event Architecture Details\"\n    See [Event Architecture Deep Dive](../architecture/event-architecture.md) for complete details on the multi-layer event system and the rationale behind sequential callback execution.\n\n## Connection Management\n\nThe ConnectionHandler implements several strategies to ensure robust connections:\n\n### Lazy Connection Establishment\n\nConnections are established only when needed, typically when the first command is executed or when explicitly requested. This lazy initialization approach conserves resources and allows for more flexible connection management.\n\n### Automatic Reconnection\n\nIf the WebSocket connection is lost or closed unexpectedly, the ConnectionHandler will attempt to re-establish it automatically when the next command is executed. This provides resilience against transient network issues.\n\n```python\nasync def _ensure_active_connection(self):\n    \"\"\"\n    Guarantees that an active connection exists before proceeding.\n    \"\"\"\n    if self._ws_connection is None or self._ws_connection.closed:\n        await self._establish_new_connection()\n```\n\n### Resource Cleanup\n\nThe ConnectionHandler implements both explicit cleanup methods and Python's asynchronous context manager protocol (`__aenter__` and `__aexit__`), ensuring resources are properly released when no longer needed:\n\n```python\nasync def close(self):\n    \"\"\"\n    Closes the WebSocket connection and clears all callbacks.\n    \"\"\"\n    await self.clear_callbacks()\n    if self._ws_connection is not None:\n        try:\n            await self._ws_connection.close()\n        except websockets.ConnectionClosed as e:\n            logger.info(f'WebSocket connection has closed: {e}')\n        logger.info('WebSocket connection closed.')\n```\n\n!!! info \"Context Manager Usage\"\n    Using the ConnectionHandler as a context manager is the recommended pattern for ensuring proper resource cleanup:\n    \n    ```python\n    async with ConnectionHandler(9222, 'browser') as connection:\n        # Work with the connection...\n        await connection.execute_command(...)\n    # Connection is automatically closed when exiting the context\n    ```\n\n## Message Processing Pipeline\n\nThe ConnectionHandler implements a sophisticated message processing pipeline that handles the continuous stream of messages from the WebSocket connection:\n\n```mermaid\nsequenceDiagram\n    participant WS as WebSocket\n    participant RCV as _receive_events\n    participant MSG as _process_single_message\n    participant PARSE as _parse_message\n    participant CMD as _handle_command_message\n    participant EVT as _handle_event_message\n    \n    loop While connected\n        WS->>RCV: message\n        RCV->>MSG: raw_message\n        MSG->>PARSE: raw_message\n        PARSE-->>MSG: parsed JSON or None\n        \n        alt Is command response\n            MSG->>CMD: message\n            CMD->>CMD: resolve command future\n        else Is event notification\n            MSG->>EVT: message\n            EVT->>EVT: process event & trigger callbacks\n        end\n    end\n```\n\nThis pipeline ensures efficient processing of both command responses and asynchronous events, allowing Pydoll to maintain responsive operation even under high message volume.\n\n## Advanced Usage\n\nThe ConnectionHandler is usually used indirectly through the Browser and Page classes, but it can also be used directly for advanced scenarios:\n\n### Direct Event Monitoring\n\nFor specialized use cases, you might want to bypass the higher-level APIs and directly monitor specific CDP events:\n\n```python\nfrom pydoll.connection.connection import ConnectionHandler\n\nasync def monitor_network():\n    connection = ConnectionHandler(9222)\n    \n    async def log_request(event):\n        url = event['params']['request']['url']\n        print(f\"Request: {url}\")\n    \n    await connection.register_callback(\n        'Network.requestWillBeSent', \n        log_request\n    )\n    \n    # Enable network events via CDP command\n    await connection.execute_command({\n        \"id\": 1,\n        \"method\": \"Network.enable\"\n    })\n    \n    # Keep running until interrupted\n    try:\n        while True:\n            await asyncio.sleep(1)\n    finally:\n        await connection.close()\n```\n\n### Custom Command Execution\n\nYou can execute arbitrary CDP commands directly:\n\n```python\nasync def custom_cdp_command(connection, method, params=None):\n    command = {\n        \"id\": random.randint(1, 10000),\n        \"method\": method,\n        \"params\": params or {}\n    }\n    return await connection.execute_command(command)\n\n# Example: Get document HTML without using Page class\nasync def get_html(connection):\n    result = await custom_cdp_command(\n        connection,\n        \"Runtime.evaluate\",\n        {\"expression\": \"document.documentElement.outerHTML\"}\n    )\n    return result['result']['result']['value']\n```\n\n!!! warning \"Advanced Interface\"\n    Direct use of the ConnectionHandler requires a deep understanding of the Chrome DevTools Protocol. For most use cases, the higher-level Browser and Page APIs provide a more intuitive and safer interface.\n\n\n## Advanced Concurrency Patterns\n\nThe ConnectionHandler's asynchronous design enables sophisticated concurrency patterns:\n\n### Parallel Command Execution\n\nExecute multiple commands concurrently and wait for all results:\n\n```python\nasync def get_page_metrics(connection):\n    commands = [\n        {\"id\": 1, \"method\": \"Performance.getMetrics\"},\n        {\"id\": 2, \"method\": \"Network.getResponseBody\", \"params\": {\"requestId\": \"...\"}},\n        {\"id\": 3, \"method\": \"DOM.getDocument\"}\n    ]\n    \n    results = await asyncio.gather(\n        *(connection.execute_command(cmd) for cmd in commands)\n    )\n    \n    return results\n```\n\n## Conclusion\n\nThe ConnectionHandler serves as the foundation of Pydoll's architecture, providing a robust, efficient interface to the Chrome DevTools Protocol. By leveraging Python's asyncio framework and WebSocket communication, it enables high-performance browser automation with elegant, event-driven programming patterns.\n\nUnderstanding the ConnectionHandler's design and operation provides valuable insights into Pydoll's internal workings and offers opportunities for advanced customization and optimization in specialized scenarios.\n\nFor most use cases, you'll interact with the ConnectionHandler indirectly through the higher-level Browser and Page APIs, which provide a more intuitive interface while leveraging the ConnectionHandler's powerful capabilities. "
  },
  {
    "path": "docs/en/deep-dive/fundamentals/iframes-and-contexts.md",
    "content": "# Iframes, OOPIFs and Execution Contexts (Deep Dive)\n\nUnderstanding how browser automation handles iframes is critical for building robust automation tools. This comprehensive guide explores the technical foundations of iframe handling in Pydoll, covering the Document Object Model (DOM), Chrome DevTools Protocol (CDP) mechanics, execution contexts, isolated worlds, and the sophisticated resolution pipeline that makes iframe interaction seamless.\n\n!!! info \"Practical usage first\"\n    If you just need to use iframes in your automation scripts, start with the feature guide: **Features → Automation → IFrames**.  \n    This deep dive explains the architectural decisions, protocol nuances, and internal implementation details.\n\n---\n\n## Table of Contents\n\n1. [Foundation: The Document Object Model (DOM)](#foundation-the-document-object-model-dom)\n2. [What are Iframes and Why They Matter](#what-are-iframes-and-why-they-matter)\n3. [The Challenge: Out-of-Process Iframes (OOPIFs)](#the-challenge-out-of-process-iframes-oopifs)\n4. [Chrome DevTools Protocol and Frame Management](#chrome-devtools-protocol-and-frame-management)\n5. [Execution Contexts and Isolated Worlds](#execution-contexts-and-isolated-worlds)\n6. [CDP Identifiers Reference](#cdp-identifiers-reference)\n7. [Pydoll's Resolution Pipeline](#pydolls-resolution-pipeline)\n8. [Session Routing and Flattened Mode](#session-routing-and-flattened-mode)\n9. [Implementation Deep Dive](#implementation-deep-dive)\n10. [Performance Considerations](#performance-considerations)\n11. [Failure Modes and Debugging](#failure-modes-and-debugging)\n\n---\n\n## Foundation: The Document Object Model (DOM)\n\nBefore diving into iframes, we must understand the DOM—the tree structure that represents an HTML document in memory.\n\n### What is the DOM?\n\nThe **Document Object Model** is a programming interface for HTML and XML documents. It represents the page structure as a tree of nodes, where each node corresponds to a part of the document:\n\n- **Element nodes**: HTML tags like `<div>`, `<iframe>`, `<button>`\n- **Text nodes**: The actual text content\n- **Attribute nodes**: Element attributes like `id`, `class`, `src`\n- **Document node**: The root of the tree\n\n```mermaid\ngraph TD\n    Document[Document] --> HTML[html element]\n    HTML --> Head[head element]\n    HTML --> Body[body element]\n    Body --> Div1[div element]\n    Body --> Div2[div element]\n    Div1 --> Text1[text node: 'Hello']\n    Div2 --> Iframe[iframe element]\n    Iframe --> IframeDoc[iframe's document]\n    IframeDoc --> IframeBody[iframe body]\n    IframeBody --> IframeContent[iframe content...]\n```\n\n### DOM Tree Properties\n\n1. **Hierarchical structure**: Every node has a parent (except Document) and can have children\n2. **Node identification**: Nodes can be identified by:\n   - `nodeId`: Internal identifier within a document context (DOM domain)\n   - `backendNodeId`: Stable identifier that can reference nodes across different documents\n3. **Live representation**: Changes to the DOM are reflected immediately in the tree\n\n### Why This Matters for Iframes\n\nEach `<iframe>` element creates a **new, independent DOM tree**. The iframe element itself exists in the parent's DOM, but the content loaded into the iframe has its own complete Document node and tree structure. This separation is the foundation of all iframe complexity.\n\n---\n\n## What are Iframes and Why They Matter\n\n### Definition\n\nAn **iframe** (inline frame) is an HTML element (`<iframe>`) that embeds another HTML document within the current page. The embedded document maintains its own context, including:\n\n- Independent HTML structure and DOM tree\n- Separate JavaScript execution environment\n- Its own CSS styling (unless explicitly shared)\n- Distinct navigation history\n\n```html\n<body>\n  <h1>Parent Page</h1>\n  <iframe src=\"https://example.com/embedded.html\" id=\"content-frame\"></iframe>\n  <p>More parent content</p>\n</body>\n```\n\n### Common Use Cases\n\n| Use Case | Description | Example |\n|----------|-------------|---------|\n| **Third-party widgets** | Embed external content safely | Payment forms, social media feeds, chat widgets |\n| **Content isolation** | Sandbox untrusted content | User-generated HTML, advertisements |\n| **Modular architecture** | Reusable components | Dashboard widgets, plugin systems |\n| **Cross-origin content** | Load resources from different domains | Maps, video players, analytics dashboards |\n\n### Security Model: Same-Origin Policy\n\nThe browser enforces a **Same-Origin Policy** for iframes:\n\n- **Same-origin iframes**: Parent can access iframe's DOM via JavaScript (`iframe.contentDocument`)\n- **Cross-origin iframes**: Parent cannot access iframe's DOM directly (security restriction)\n\nThis security boundary is why automation tools need special mechanisms (like CDP) to interact with iframe content.\n\n!!! warning \"Important for automation\"\n    Traditional JavaScript-based automation (like Selenium's early approaches) cannot directly access cross-origin iframe content due to browser security. CDP operates at a lower level, bypassing this limitation for debugging purposes.\n\n---\n\n## The Challenge: Out-of-Process Iframes (OOPIFs)\n\n### What are OOPIFs?\n\nModern Chromium uses **site isolation** for security and stability. This means different origins may be rendered in separate OS processes. An iframe from a different origin becomes an **Out-of-Process Iframe (OOPIF)**.\n\n```mermaid\ngraph LR\n    subgraph \"Process 1: example.com\"\n        MainPage[Main Page DOM]\n    end\n    \n    subgraph \"Process 2: widget.com\"\n        IframeDOM[Iframe DOM]\n    end\n    \n    MainPage -.Process boundary.-> IframeDOM\n```\n\n### Why OOPIFs Complicate Automation\n\n| Aspect | In-Process Iframe | Out-of-Process Iframe (OOPIF) |\n|--------|-------------------|-------------------------------|\n| **DOM access** | Shared document tree in memory | Separate target with own document |\n| **Command routing** | Single connection | Requires target attachment and session routing |\n| **Frame tree** | All frames in one tree | Root frame + separate targets for OOPIFs |\n| **JavaScript context** | Same execution context | Different execution context per process |\n| **CDP communication** | Direct commands | Commands must include `sessionId` |\n\n### The Traditional Approach (Manual Context Switching)\n\nWithout sophisticated handling, automating OOPIFs requires:\n\n```python\n# Traditional (manual) approach with other tools\nmain_page = browser.get_page()\niframe_element = main_page.find_element_by_id(\"iframe-id\")\n\n# Must manually switch context\ndriver.switch_to.frame(iframe_element)\n\n# Now commands target the iframe\nbutton = driver.find_element_by_id(\"button-in-iframe\")\nbutton.click()\n\n# Must manually switch back\ndriver.switch_to.default_content()\n```\n\n**Problems with this approach:**\n\n1. **Developer burden**: Every iframe requires explicit context management\n2. **Nested iframes**: Each level needs another switch\n3. **OOPIF detection**: Hard to know when manual attachment is needed\n4. **Error-prone**: Forget to switch back → subsequent commands fail\n5. **Not composable**: Helper functions must know their iframe context\n\n### Pydoll's Solution: Transparent Context Resolution\n\nPydoll eliminates manual context switching by resolving iframe contexts automatically:\n\n```python\n# Pydoll approach (no manual switching)\niframe = await tab.find(id=\"iframe-id\")\nbutton = await iframe.find(id=\"button-in-iframe\")\nawait button.click()\n\n# Nested iframes? Same pattern\nouter = await tab.find(id=\"outer-iframe\")\ninner = await outer.find(tag_name=\"iframe\")\nbutton = await inner.find(text=\"Submit\")\nawait button.click()\n```\n\nThe complexity is handled internally. Let's explore how.\n\n---\n\n## Chrome DevTools Protocol and Frame Management\n\nAs discussed in [Deep Dive → Fundamentals → Chrome DevTools Protocol](./cdp.md), CDP provides comprehensive browser control via WebSocket communication. Frame management is spread across multiple CDP domains.\n\n### Relevant CDP Domains\n\n#### 1. **Page Domain**\n\nManages page lifecycle, frames, and navigation.\n\n**Key methods:**\n\n- `Page.getFrameTree()`: Returns the hierarchical structure of all frames in a page\n  ```json\n  {\n    \"frameTree\": {\n      \"frame\": {\n        \"id\": \"main-frame-id\",\n        \"url\": \"https://example.com\",\n        \"securityOrigin\": \"https://example.com\",\n        \"mimeType\": \"text/html\"\n      },\n      \"childFrames\": [\n        {\n          \"frame\": {\n            \"id\": \"child-frame-id\",\n            \"parentId\": \"main-frame-id\",\n            \"url\": \"https://widget.com/embed\"\n          }\n        }\n      ]\n    }\n  }\n  ```\n\n- `Page.createIsolatedWorld(frameId, worldName)`: Creates a new JavaScript execution context in a specific frame\n  ```json\n  {\n    \"executionContextId\": 42\n  }\n  ```\n\n**Pydoll usage:** \n\n```python\n# From pydoll/elements/web_element.py\n@staticmethod\nasync def _get_frame_tree_for(\n    handler: ConnectionHandler, session_id: Optional[str]\n) -> FrameTree:\n    \"\"\"Get the Page frame tree for the given connection/target.\"\"\"\n    command = PageCommands.get_frame_tree()\n    if session_id:\n        command['sessionId'] = session_id\n    response: GetFrameTreeResponse = await handler.execute_command(command)\n    return response['result']['frameTree']\n```\n\n#### 2. **DOM Domain**\n\nProvides access to the DOM structure.\n\n**Key methods:**\n\n- `DOM.describeNode(objectId)`: Returns detailed information about a DOM node\n  ```json\n  {\n    \"node\": {\n      \"nodeId\": 123,\n      \"backendNodeId\": 456,\n      \"nodeName\": \"IFRAME\",\n      \"frameId\": \"parent-frame-id\",\n      \"contentDocument\": {\n        \"frameId\": \"iframe-frame-id\",\n        \"documentURL\": \"https://embedded.com/page.html\"\n      }\n    }\n  }\n  ```\n\n- `DOM.getFrameOwner(frameId)`: Returns the `backendNodeId` of the `<iframe>` element that owns a frame\n  ```json\n  {\n    \"backendNodeId\": 456\n  }\n  ```\n\n**Pydoll usage:**\n\n```python\n# From pydoll/elements/web_element.py\n@staticmethod\nasync def _owner_backend_for(\n    handler: ConnectionHandler, session_id: Optional[str], frame_id: str\n) -> Optional[int]:\n    \"\"\"Get the backendNodeId of the DOM element that owns the given frame.\"\"\"\n    command = DomCommands.get_frame_owner(frame_id=frame_id)\n    if session_id:\n        command['sessionId'] = session_id\n    response: GetFrameOwnerResponse = await handler.execute_command(command)\n    return response.get('result', {}).get('backendNodeId')\n```\n\n#### 3. **Target Domain**\n\nManages browser targets (pages, iframes, workers, etc.).\n\n**Key methods:**\n\n- `Target.getTargets()`: Lists all available targets\n  ```json\n  {\n    \"targetInfos\": [\n      {\n        \"targetId\": \"page-target-id\",\n        \"type\": \"page\",\n        \"title\": \"Main Page\",\n        \"url\": \"https://example.com\"\n      },\n      {\n        \"targetId\": \"iframe-target-id\",\n        \"type\": \"iframe\",\n        \"title\": \"\",\n        \"url\": \"https://widget.com/embed\",\n        \"parentFrameId\": \"main-frame-id\"\n      }\n    ]\n  }\n  ```\n\n- `Target.attachToTarget(targetId, flatten)`: Attaches to a target for debugging\n  - When `flatten=true`: Returns a `sessionId` for routing commands in flattened mode\n  - All communication happens over the same WebSocket, differentiated by `sessionId`\n\n**Pydoll usage:**\n\n```python\n# From pydoll/interactions/iframe.py (simplified)\nasync def _resolve_oopif_by_parent(self, content_frame_id: str, ...):\n    \"\"\"Resolve an OOPIF using the content frame id.\"\"\"\n    browser_handler = ConnectionHandler(...)\n    targets_response: GetTargetsResponse = await browser_handler.execute_command(\n        TargetCommands.get_targets()\n    )\n    target_infos = targets_response.get('result', {}).get('targetInfos', [])\n\n    # Find targets whose parentFrameId matches\n    direct_children = [\n        target_info for target_info in target_infos\n        if target_info.get('parentFrameId') == content_frame_id\n    ]\n    \n    if direct_children:\n        attach_response: AttachToTargetResponse = await browser_handler.execute_command(\n            TargetCommands.attach_to_target(\n                target_id=direct_children[0]['targetId'], \n                flatten=True\n            )\n        )\n        attached_session_id = attach_response.get('result', {}).get('sessionId')\n        # ... use session_id for subsequent commands\n```\n\n#### 4. **Runtime Domain**\n\nExecutes JavaScript and manages execution contexts.\n\n**Key methods:**\n\n- `Runtime.evaluate(expression, contextId)`: Evaluates JavaScript in a specific execution context\n- `Runtime.callFunctionOn(functionDeclaration, objectId)`: Calls a function with a specific object as `this`\n\n**Pydoll usage for iframe document access:**\n\n```python\n# From pydoll/elements/web_element.py\nasync def _set_iframe_document_object_id(self, execution_context_id: int):\n    \"\"\"Evaluate document.documentElement in iframe context and cache its object id.\"\"\"\n    evaluate_command = RuntimeCommands.evaluate(\n        expression='document.documentElement',\n        context_id=execution_context_id,\n    )\n    if self._iframe_context and self._iframe_context.session_id:\n        evaluate_command['sessionId'] = self._iframe_context.session_id\n    \n    evaluate_response: EvaluateResponse = await (\n        (self._iframe_context.session_handler if self._iframe_context else None)\n        or self._connection_handler\n    ).execute_command(evaluate_command)\n    \n    document_object_id = evaluate_response.get('result', {}).get('result', {}).get('objectId')\n    if self._iframe_context:\n        self._iframe_context.document_object_id = document_object_id\n```\n\n---\n\n## Execution Contexts and Isolated Worlds\n\n### What is an Execution Context?\n\nAn **execution context** is an environment where JavaScript code is executed. Every frame in a browser has at least one execution context. The context includes:\n\n- **Global object** (`window` in browsers)\n- **Scope chain**: How variables are resolved\n- **This binding**: What `this` refers to\n- **Variable environment**: All declared variables and functions\n\n### Multiple Contexts per Frame\n\nA single frame can have multiple execution contexts:\n\n1. **Main world (default context)**: Where the page's own JavaScript runs\n2. **Isolated worlds**: Separate contexts that share the same DOM but have different JavaScript global scopes\n\n```mermaid\ngraph TB\n    Frame[Frame: example.com/page]\n    Frame --> MainWorld[Main World<br/>Page's JavaScript]\n    Frame --> IsolatedWorld1[Isolated World 1<br/>Extension content script]\n    Frame --> IsolatedWorld2[Isolated World 2<br/>Pydoll automation]\n    \n    DOM[Shared DOM Tree]\n    MainWorld -.can access.-> DOM\n    IsolatedWorld1 -.can access.-> DOM\n    IsolatedWorld2 -.can access.-> DOM\n    \n    MainWorld -.cannot access.-> IsolatedWorld1\n    MainWorld -.cannot access.-> IsolatedWorld2\n```\n\n### What is an Isolated World?\n\nAn **isolated world** is a separate JavaScript execution context that:\n\n- **Shares the same DOM**: Can read/modify DOM elements\n- **Has a separate global object**: Variables/functions don't leak between worlds\n- **Prevents interference**: Page scripts cannot detect or interfere with isolated world scripts\n\n**Origin**: Isolated worlds were created for browser extensions. Content scripts run in isolated worlds so they can interact with the page DOM without:\n\n- Page scripts overwriting their variables\n- Being detected by anti-tamper code\n- Conflicting with page JavaScript\n\n### Why Pydoll Uses Isolated Worlds for Iframes\n\nWhen Pydoll interacts with iframe content, it creates an isolated world in that iframe's context. This provides:\n\n1. **Clean JavaScript environment**: No conflicts with iframe's own scripts\n2. **Consistent behavior**: Automation scripts work regardless of what JavaScript the iframe runs\n3. **Anti-detection**: The iframe's JavaScript cannot easily detect Pydoll's presence\n4. **Safe evaluation**: Automation code cannot accidentally trigger page logic\n\n**Implementation:**\n\n```python\n# From pydoll/elements/web_element.py\n@staticmethod\nasync def _create_isolated_world_for_frame(\n    frame_id: str,\n    handler: ConnectionHandler,\n    session_id: Optional[str],\n) -> int:\n    \"\"\"Create an isolated world (Page.createIsolatedWorld) for the given frame.\"\"\"\n    create_command = PageCommands.create_isolated_world(\n        frame_id=frame_id,\n        world_name=f'pydoll::iframe::{frame_id}',\n        grant_universal_access=True,\n    )\n    if session_id:\n        create_command['sessionId'] = session_id\n    \n    create_response: CreateIsolatedWorldResponse = await handler.execute_command(\n        create_command\n    )\n    execution_context_id = create_response.get('result', {}).get('executionContextId')\n    if not execution_context_id:\n        raise InvalidIFrame('Unable to create isolated world for iframe')\n    return execution_context_id\n```\n\nThe `grant_universal_access=True` parameter allows the isolated world to:\n\n- Access cross-origin frames (normally blocked by same-origin policy)\n- Perform privileged operations needed for automation\n\n!!! tip \"Isolated worlds in practice\"\n    Every time you use `await iframe.find(...)`, Pydoll evaluates the selector query in an isolated world created specifically for that iframe. This ensures your automation logic never conflicts with the iframe's own JavaScript, and the iframe cannot detect or block your automation.\n\n---\n\n## CDP Identifiers Reference\n\nUnderstanding CDP identifiers is crucial for iframe handling. Here's a comprehensive reference:\n\n| Identifier | Domain | Scope | Purpose | Example Use in Pydoll |\n|------------|--------|-------|---------|----------------------|\n| **`nodeId`** | DOM | Document-local | Identifies a DOM node within a specific document context | Internal CDP operations; not stable across navigations |\n| **`backendNodeId`** | DOM | Cross-document stable | Stable identifier for a DOM node; can map frames to owner elements | Used to match iframe elements to frame IDs via `DOM.getFrameOwner` |\n| **`frameId`** | Page | Frame | Identifies a frame in the page's frame tree | Used to specify which frame for `Page.createIsolatedWorld` and frame tree traversal |\n| **`targetId`** | Target | Global | Identifies a debugging target (page, iframe, worker, etc.) | Used for `Target.attachToTarget` to connect to OOPIFs |\n| **`sessionId`** | Target | Target-specific | Routes commands to a specific target in flattened mode | Injected into commands to route them to the correct OOPIF |\n| **`executionContextId`** | Runtime | Frame + world | Identifies a JavaScript execution context (including isolated worlds) | Returned by `Page.createIsolatedWorld`; used in `Runtime.evaluate` |\n| **`objectId`** | Runtime | Execution context | Remote object reference (e.g., DOM element, function, object) | Reference to iframe's `document.documentElement` for relative queries |\n\n### Identifier Relationships\n\nHere's how identifiers relate to each other during iframe resolution:\n\n```\n┌─────────────────────────────────────────────────────────────────────────┐\n│                         Resolution Flow                                 │\n└─────────────────────────────────────────────────────────────────────────┘\n\n1. Start: <iframe> Element\n   └─ backendNodeId: 789\n   \n2. Find Frame ──────────────[DOM.getFrameOwner]──────────────┐\n   └─ frameId: abc-123                                       │\n                                                             │\n3. OOPIF? Check Origin ─────[Different origin detected]──────┤\n   └─ targetId: xyz-456                                      │\n                                                             │\n4. Attach to Target ────────[Target.attachToTarget]──────────┤\n   └─ sessionId: session-789                                 │\n                                                             │\n5. Create Isolated World ───[Page.createIsolatedWorld]───────┤\n   └─ executionContextId: 42                                 │\n                                                             │\n6. Get Document ────────────[Runtime.evaluate]───────────────┘\n   └─ objectId: obj-999\n```\n\n**Key transformation points:**\n\n| From | Method | To | Purpose |\n|------|--------|-----|---------|\n| `backendNodeId` | `DOM.getFrameOwner` | `frameId` | Find which frame owns the iframe element |\n| `targetId` | `Target.attachToTarget(flatten=true)` | `sessionId` | Connect to OOPIF for command routing |\n| `frameId` | `Page.createIsolatedWorld` | `executionContextId` | Create safe JavaScript environment |\n| `executionContextId` | `Runtime.evaluate('document.documentElement')` | `objectId` | Get reference to iframe's document |\n\n### Code Representation in Pydoll\n\n```python\n# From pydoll/elements/web_element.py\n@dataclass\nclass _IFrameContext:\n    \"\"\"Encapsulates all identifiers and routing information for an iframe.\"\"\"\n    frame_id: str                                   # frameId: identifies the frame\n    document_url: Optional[str] = None              # frame's loaded URL\n    execution_context_id: Optional[int] = None      # executionContextId: isolated world\n    document_object_id: Optional[str] = None        # objectId: document.documentElement\n    session_handler: Optional[ConnectionHandler] = None  # for OOPIF targets\n    session_id: Optional[str] = None                # sessionId: routes commands to OOPIF\n```\n\nThis dataclass is cached on each `WebElement` representing an iframe, enabling automatic routing of all subsequent operations.\n\n---\n\n## Pydoll's Resolution Pipeline\n\nWhen you access an iframe in Pydoll (e.g., `await iframe.find(...)`), an elaborate resolution pipeline executes behind the scenes. This section breaks down every step.\n\n### High-Level Flow\n\n```mermaid\nsequenceDiagram\n    participant User\n    participant WebElement\n    participant Pipeline as Resolution Pipeline\n    participant CDP\n    \n    User->>WebElement: iframe.find(id='button')\n    WebElement->>WebElement: Check if iframe context cached\n    alt Context not cached\n        WebElement->>Pipeline: _ensure_iframe_context()\n        Pipeline->>CDP: DOM.describeNode(iframe)\n        CDP-->>Pipeline: Node info (frameId?, backendNodeId, etc.)\n        \n        alt frameId not in node info\n            Pipeline->>Pipeline: _resolve_frame_by_owner()\n            Pipeline->>CDP: Page.getFrameTree()\n            CDP-->>Pipeline: Frame tree\n            Pipeline->>CDP: DOM.getFrameOwner(each frame)\n            CDP-->>Pipeline: backendNodeId\n            Pipeline->>Pipeline: Match backendNodeId to find frameId\n        end\n        \n        alt frameId still missing (OOPIF)\n            Pipeline->>Pipeline: _resolve_oopif_by_parent()\n            Pipeline->>CDP: Target.getTargets()\n            CDP-->>Pipeline: List of targets\n            Pipeline->>CDP: Target.attachToTarget(targetId, flatten=true)\n            CDP-->>Pipeline: sessionId\n            Pipeline->>CDP: Page.getFrameTree(sessionId)\n            CDP-->>Pipeline: OOPIF frame tree\n        end\n        \n        Pipeline->>CDP: Page.createIsolatedWorld(frameId)\n        CDP-->>Pipeline: executionContextId\n        \n        Pipeline->>CDP: Runtime.evaluate('document.documentElement', contextId)\n        CDP-->>Pipeline: objectId (document reference)\n        \n        Pipeline->>WebElement: Cache _IFrameContext\n    end\n    \n    WebElement->>WebElement: Use cached context for find()\n    WebElement-->>User: Button element (with context)\n```\n\n### Step-by-Step Deep Dive\n\n#### **Step 1: Describe the Iframe Element**\n\n**Goal**: Extract metadata from the `<iframe>` DOM element.\n\n**Method**: `DOM.describeNode(objectId=iframe_object_id)`\n\n**What we get**:\n\n- `backendNodeId`: Stable identifier for the iframe element\n- `frameId` (from `contentDocument`): If the iframe's content is already loaded and in-process\n- `documentURL`: The URL loaded in the iframe\n- `parentFrameId` (from `frameId` field on the node): The frame containing this iframe element\n\n**Code**:\n\n```python\n# From pydoll/interactions/iframe.py\nasync def resolve(self) -> IFrameContext:\n    \"\"\"Resolve and return iframe context.\"\"\"\n    base_handler, base_session_id = self._get_base_session()\n    node_info = await self._describe_element_node(base_handler, base_session_id)\n    frame_id, document_url, content_frame_id, backend_node_id = self._extract_frame_metadata(\n        node_info\n    )\n    # ... continue resolution\n```\n\n**Helper**:\n\n```python\n@staticmethod\ndef _extract_frame_metadata(\n    node_info: Node,\n) -> tuple[Optional[str], Optional[str], Optional[str], Optional[int]]:\n    \"\"\"Extract iframe-related metadata from a DOM.describeNode Node.\"\"\"\n    content_document = node_info.get('contentDocument') or {}\n    content_frame_id = node_info.get('frameId')\n    backend_node_id = node_info.get('backendNodeId')\n    frame_id = content_document.get('frameId')\n    document_url = (\n        content_document.get('documentURL')\n        or content_document.get('baseURL')\n        or node_info.get('documentURL')\n        or node_info.get('baseURL')\n    )\n    return frame_id, document_url, content_frame_id, backend_node_id\n```\n\n**Outcome**:\n\n- **If `frame_id` is present**: Great! The iframe is in-process; proceed to Step 4.\n- **If `frame_id` is missing**: The iframe might be an OOPIF or not fully loaded; proceed to Step 2.\n\n---\n\n#### **Step 2: Resolve Frame by Owner (backendNodeId matching)**\n\n**Goal**: Find the `frameId` by matching the iframe element's `backendNodeId` to frame owners in the frame tree.\n\n**Strategy**:\n\n1. Fetch the page's frame tree (`Page.getFrameTree`)\n2. For each frame in the tree, call `DOM.getFrameOwner(frameId)` to get the `backendNodeId` of the owning iframe element\n3. Compare with our iframe's `backendNodeId`\n4. When they match, we've found the correct `frameId`\n\n**Code**:\n\n```python\n# From pydoll/elements/web_element.py\nasync def _resolve_frame_by_owner(\n    self,\n    base_handler: ConnectionHandler,\n    base_session_id: Optional[str],\n    backend_node_id: int,\n    current_document_url: Optional[str],\n) -> tuple[Optional[str], Optional[str]]:\n    \"\"\"Resolve a frame id and URL by matching the owner backend_node_id.\"\"\"\n    owner_frame_id, owner_url = await self._find_frame_by_owner(\n        base_handler, base_session_id, backend_node_id\n    )\n    if not owner_frame_id:\n        return None, current_document_url\n    return owner_frame_id, owner_url or current_document_url\n\nasync def _find_frame_by_owner(\n    self, handler: ConnectionHandler, session_id: Optional[str], backend_node_id: int\n) -> tuple[Optional[str], Optional[str]]:\n    \"\"\"Find a frame by matching the owner backend_node_id of the <iframe> element.\"\"\"\n    frame_tree = await self._get_frame_tree_for(handler, session_id)\n    for frame_node in WebElement._walk_frames(frame_tree):\n        candidate_frame_id = frame_node.get('id', '')\n        if not candidate_frame_id:\n            continue\n        owner_backend_id = await self._owner_backend_for(\n            handler, session_id, candidate_frame_id\n        )\n        if owner_backend_id == backend_node_id:\n            return candidate_frame_id, frame_node.get('url')\n    return None, None\n```\n\n**Why this is necessary**:\n\n- `DOM.describeNode` sometimes doesn't include the `contentDocument.frameId` for cross-origin or lazy-loaded iframes\n- The frame tree always contains all frames (even OOPIFs), so we can find it indirectly\n\n**Outcome**:\n\n- **If `frameId` found**: Proceed to Step 4.\n- **If still not found**: The iframe is likely an OOPIF in a separate target; proceed to Step 3.\n\n---\n\n#### **Step 3: Resolve OOPIF by Parent Frame**\n\n**Goal**: For Out-of-Process Iframes, find the correct target, attach to it, and obtain the `frameId` from the target's frame tree (and the routing `sessionId` when needed).\n\n**When this runs**:\n\n- Same-origin / in-process iframes that already have a `frameId` and **no** `backendNodeId` skip this step (they are handled directly).\n- Cross-origin / OOPIF iframes (with `backendNodeId`) or iframes whose `frameId` could not be resolved via Step 2 use this step.\n\n**Strategy**:\n\n**3a. Direct child target lookup (fast path)**:\n\n1. Call `Target.getTargets()` to list all debugging targets.\n2. Filter targets where `type` is `\"iframe\"` or `\"page\"` and `parentFrameId` matches our parent frame.\n3. If there is **exactly one** matching child **and we don't have a `backendNodeId`**, attach directly to that target with `Target.attachToTarget(targetId, flatten=true)`.\n4. Fetch `Page.getFrameTree(sessionId)` for that target; the root frame of this tree is our iframe's frame.\n\nWhen there are **multiple** direct children or we have a `backendNodeId` (typical OOPIF case), Pydoll iterates over each child target:\n\n1. Attach via `Target.attachToTarget(flatten=true)`.\n2. Fetch `Page.getFrameTree(sessionId)` and read the root `frame.id`.\n3. Call `DOM.getFrameOwner(frameId=root_id)` on the **main** connection.\n4. Compare the returned `backendNodeId` with our iframe element's `backendNodeId`.\n5. The child whose root owner matches is selected as the correct OOPIF target.\n\n**3b. Fallback: Scan all targets (root owner + child search)**:\n\nIf no suitable direct child is found (or when `parentFrameId` information is incomplete), Pydoll falls back to scanning **all** iframe/page targets:\n\n1. Iterate all iframe/page targets.\n2. Attach to each and fetch its frame tree.\n3. First, try to match the **root frame owner** via `DOM.getFrameOwner(root_frame_id)` against our iframe's `backendNodeId`.\n4. If that does not match, look for a **child frame** whose `parentId` equals our `content_frame_id` (this covers cases where the OOPIF is nested under an intermediate frame).\n\n**Code**:\n\n```python\n# From pydoll/interactions/iframe.py\nasync def _resolve_oopif_by_parent(\n    self,\n    content_frame_id: str,\n    backend_node_id: Optional[int],\n    base_handler: Optional[ConnectionHandler] = None,\n    base_session_id: Optional[str] = None,\n) -> tuple[Optional[ConnectionHandler], Optional[str], Optional[str], Optional[str]]:\n    \"\"\"Resolve an OOPIF using the content frame id.\"\"\"\n    browser_handler = ConnectionHandler(\n        connection_port=self._element._connection_handler._connection_port\n    )\n    targets_response: GetTargetsResponse = await browser_handler.execute_command(\n        TargetCommands.get_targets()\n    )\n    target_infos = targets_response.get('result', {}).get('targetInfos', [])\n\n    # The handler that can resolve DOM.getFrameOwner for the element's context.\n    # When the <iframe> lives inside a nested OOPIF, the Tab-level handler has\n    # no visibility; we must route through the session that originally found\n    # the element.\n    owner_handler = base_handler or self._element._connection_handler\n    owner_session_id = base_session_id\n\n    # Strategy 3a: Direct children (fast path)\n    direct_children = [\n        target_info\n        for target_info in target_infos\n        if target_info.get('type') in {'iframe', 'page'}\n        and target_info.get('parentFrameId') == content_frame_id\n    ]\n\n    is_single_child = len(direct_children) == 1\n    for child_target in direct_children:\n        attach_response: AttachToTargetResponse = await browser_handler.execute_command(\n            TargetCommands.attach_to_target(\n                target_id=child_target['targetId'], flatten=True\n            )\n        )\n        attached_session_id = attach_response.get('result', {}).get('sessionId')\n        if not attached_session_id:\n            continue\n\n        frame_tree = await self._get_frame_tree_for(browser_handler, attached_session_id)\n        root_frame = (frame_tree or {}).get('frame', {})\n        root_frame_id = root_frame.get('id', '')\n\n        # Same-origin / simple case: single child and no backend_node_id\n        if is_single_child and root_frame_id and backend_node_id is None:\n            return (\n                browser_handler,\n                attached_session_id,\n                root_frame_id,\n                root_frame.get('url'),\n            )\n\n        # OOPIF case: confirm ownership via DOM.getFrameOwner\n        if root_frame_id and backend_node_id is not None:\n            owner_backend_id = await self._owner_backend_for(\n                owner_handler, owner_session_id, root_frame_id\n            )\n            if owner_backend_id == backend_node_id:\n                return (\n                    browser_handler,\n                    attached_session_id,\n                    root_frame_id,\n                    root_frame.get('url'),\n                )\n\n    # Strategy 3b: Scan all targets (root owner + child search)\n    for target_info in target_infos:\n        if target_info.get('type') not in {'iframe', 'page'}:\n            continue\n        attach_response = await browser_handler.execute_command(\n            TargetCommands.attach_to_target(\n                target_id=target_info.get('targetId', ''), flatten=True\n            )\n        )\n        attached_session_id = attach_response.get('result', {}).get('sessionId')\n        if not attached_session_id:\n            continue\n\n        frame_tree = await self._get_frame_tree_for(browser_handler, attached_session_id)\n        root_frame = (frame_tree or {}).get('frame', {})\n        root_frame_id = root_frame.get('id', '')\n\n        # Direct match: content_frame_id equals this target's root frame ID\n        if root_frame_id and root_frame_id == content_frame_id:\n            return (\n                browser_handler,\n                attached_session_id,\n                root_frame_id,\n                root_frame.get('url'),\n            )\n\n        # Try matching root owner by backend_node_id\n        if root_frame_id and backend_node_id is not None:\n            owner_backend_id = await self._owner_backend_for(\n                owner_handler, owner_session_id, root_frame_id\n            )\n            if owner_backend_id == backend_node_id:\n                return (\n                    browser_handler,\n                    attached_session_id,\n                    root_frame_id,\n                    root_frame.get('url'),\n                )\n\n        # Fallback: match a child frame whose parentId equals content_frame_id\n        child_frame_id = IFrameContextResolver._find_child_by_parent(\n            frame_tree, content_frame_id\n        )\n        if child_frame_id:\n            return browser_handler, attached_session_id, child_frame_id, None\n\n    return None, None, None, None\n```\n\n**Outcome**:\n\n- **If OOPIF resolved**: We now have `sessionId`, `session_handler`, and `frameId`; proceed to Step 4.\n- **If resolution fails**: Raise `InvalidIFrame` exception (handled in `_ensure_iframe_context`).\n\n---\n\n#### **Step 4: Create Isolated World**\n\n**Goal**: Create a separate JavaScript execution context in the resolved frame.\n\n**Method**: `Page.createIsolatedWorld(frameId, worldName='pydoll::iframe::<frameId>', grantUniversalAccess=true)`\n\n**Parameters**:\n- `frameId`: The frame where the isolated world is created\n- `worldName`: Identifier for the world (useful for debugging)\n- `grantUniversalAccess`: Allows cross-origin access (needed for automation)\n\n**Response**: `{ executionContextId: 42 }`\n\n**Code**:\n\n```python\n# From pydoll/elements/web_element.py\n@staticmethod\nasync def _create_isolated_world_for_frame(\n    frame_id: str,\n    handler: ConnectionHandler,\n    session_id: Optional[str],\n) -> int:\n    \"\"\"Create an isolated world for the given frame.\"\"\"\n    create_command = PageCommands.create_isolated_world(\n        frame_id=frame_id,\n        world_name=f'pydoll::iframe::{frame_id}',\n        grant_universal_access=True,\n    )\n    if session_id:\n        create_command['sessionId'] = session_id\n    create_response: CreateIsolatedWorldResponse = await handler.execute_command(create_command)\n    execution_context_id = create_response.get('result', {}).get('executionContextId')\n    if not execution_context_id:\n        raise InvalidIFrame('Unable to create isolated world for iframe')\n    return execution_context_id\n```\n\n**Why isolated world**:\n\n- **Isolation**: Our automation JavaScript doesn't interfere with the iframe's JavaScript\n- **Anti-detection**: The iframe cannot detect our presence easily\n- **Consistency**: Behavior is predictable regardless of iframe's script environment\n\n**Outcome**: We have an `executionContextId` for running JavaScript in the iframe.\n\n---\n\n#### **Step 5: Pin the Iframe Document as a Runtime Object**\n\n**Goal**: Obtain an `objectId` reference to the iframe's `document.documentElement` (the `<html>` element of the iframe).\n\n**Method**: `Runtime.evaluate(expression='document.documentElement', contextId=executionContextId)`\n\n**Why we need this**:\n\n- To execute **relative queries** (like `element.querySelector()`) inside the iframe\n- The `objectId` allows using `Runtime.callFunctionOn(objectId, ...)` with `this` bound to the iframe's document\n\n**Code**:\n\n```python\n# From pydoll/elements/web_element.py\nasync def _set_iframe_document_object_id(self, execution_context_id: int) -> None:\n    \"\"\"Evaluate document.documentElement in the iframe context and cache its object id.\"\"\"\n    evaluate_command = RuntimeCommands.evaluate(\n        expression='document.documentElement',\n        context_id=execution_context_id,\n    )\n    if self._iframe_context and self._iframe_context.session_id:\n        evaluate_command['sessionId'] = self._iframe_context.session_id\n    evaluate_response: EvaluateResponse = await (\n        (self._iframe_context.session_handler if self._iframe_context else None)\n        or self._connection_handler\n    ).execute_command(evaluate_command)\n    result_object = evaluate_response.get('result', {}).get('result', {})\n    document_object_id = result_object.get('objectId')\n    if not document_object_id:\n        raise InvalidIFrame('Unable to obtain document reference for iframe')\n    if self._iframe_context:\n        self._iframe_context.document_object_id = document_object_id\n```\n\n**Outcome**: The `_IFrameContext` is now fully populated and cached on the `WebElement`.\n\n---\n\n#### **Step 6: Cache and Propagate Context**\n\n**Goal**: Store the resolved context on the iframe element and propagate it to all child elements found within the iframe.\n\n**Caching**:\n\n```python\n# From pydoll/elements/web_element.py\ndef _init_iframe_context(\n    self,\n    frame_id: str,\n    document_url: Optional[str],\n    session_handler: Optional[ConnectionHandler],\n    session_id: Optional[str],\n) -> None:\n    \"\"\"Initialize and cache iframe context on this element.\"\"\"\n    self._iframe_context = _IFrameContext(frame_id=frame_id, document_url=document_url)\n    # Clean up routing attributes (these were for nested iframes)\n    if hasattr(self, '_routing_session_handler'):\n        delattr(self, '_routing_session_handler')\n    if hasattr(self, '_routing_session_id'):\n        delattr(self, '_routing_session_id')\n    # Store OOPIF routing if needed\n    if session_handler and session_id:\n        self._iframe_context.session_handler = session_handler\n        self._iframe_context.session_id = session_id\n```\n\n**Propagation** (when finding elements inside the iframe):\n\n```python\n# From pydoll/elements/mixins/find_elements_mixin.py\ndef _apply_iframe_context_to_element(\n    self, element: WebElement, iframe_context: _IFrameContext | None\n) -> None:\n    \"\"\"Propagate iframe context to the newly created element.\"\"\"\n    if not iframe_context:\n        return\n    \n    # If the child element is also an iframe, set up routing\n    if getattr(element, 'is_iframe', False):\n        element._routing_session_handler = (\n            iframe_context.session_handler or self._connection_handler\n        )\n        element._routing_session_id = iframe_context.session_id\n        element._routing_parent_frame_id = iframe_context.frame_id\n        return\n    \n    # Otherwise, inject the parent iframe's context\n    element._iframe_context = iframe_context\n```\n\n**Why propagation matters**:\n\n- Elements found inside an iframe inherit the iframe's context\n- This ensures subsequent operations (click, type, find nested elements) automatically use the correct routing\n- Nested iframes receive routing information so they can resolve their own context relative to the parent iframe\n\n---\n\n## Session Routing and Flattened Mode\n\n### The Flattened Session Model\n\nAs discussed in [Deep Dive → Fundamentals → CDP](./cdp.md), traditional CDP uses separate WebSocket connections for each target. **Flattened mode** is an optimization where all targets share a single WebSocket connection, with commands routed using a `sessionId`.\n\n```mermaid\ngraph TB\n    subgraph \"Traditional Mode\"\n        WS1[WebSocket 1] --> MainPage[Main Page Target]\n        WS2[WebSocket 2] --> Iframe1[OOPIF Target 1]\n        WS3[WebSocket 3] --> Iframe2[OOPIF Target 2]\n    end\n    \n    subgraph \"Flattened Mode\"\n        WS[Single WebSocket] --> Router{CDP Router}\n        Router -->|sessionId: null| MainPage2[Main Page Target]\n        Router -->|sessionId: session-1| Iframe3[OOPIF Target 1]\n        Router -->|sessionId: session-2| Iframe4[OOPIF Target 2]\n    end\n```\n\n### How Session Routing Works\n\n**When attaching to an OOPIF**:\n\n```python\nresponse = await handler.execute_command(\n    TargetCommands.attach_to_target(targetId=\"iframe-target-id\", flatten=True)\n)\nsession_id = response['result']['sessionId']  # e.g., \"8E6C...-1234\"\n```\n\n**When sending a command to that OOPIF**:\n\n```python\ncommand = PageCommands.get_frame_tree()\ncommand['sessionId'] = 'session-1'  # Route to the OOPIF\nresponse = await handler.execute_command(command)\n```\n\nThe browser's CDP implementation routes the command to the correct target based on the `sessionId`.\n\n### Pydoll's Command Routing\n\nEvery command sent by Pydoll elements is automatically routed to the correct target:\n\n```python\n# From pydoll/elements/mixins/find_elements_mixin.py\ndef _resolve_routing(self) -> tuple[ConnectionHandler, Optional[str]]:\n    \"\"\"Resolve handler and sessionId for the current context.\"\"\"\n    # Check if element has an iframe context with OOPIF routing\n    iframe_context = getattr(self, '_iframe_context', None)\n    if iframe_context and getattr(iframe_context, 'session_handler', None):\n        return iframe_context.session_handler, getattr(iframe_context, 'session_id', None)\n    \n    # Check if element has inherited routing from a parent iframe\n    routing_handler = getattr(self, '_routing_session_handler', None)\n    if routing_handler is not None:\n        return routing_handler, getattr(self, '_routing_session_id', None)\n    \n    # Default: use the tab's main connection\n    return self._connection_handler, None\n\nasync def _execute_command(\n    self, command: Command[T_CommandParams, T_CommandResponse]\n) -> T_CommandResponse:\n    \"\"\"Execute CDP command via resolved handler (60s timeout).\"\"\"\n    handler, session_id = self._resolve_routing()\n    if session_id:\n        command['sessionId'] = session_id\n    return await handler.execute_command(command, timeout=60)\n```\n\n**Routing logic**:\n\n1. **Element inside OOPIF iframe**: Use `iframe_context.session_id` and `iframe_context.session_handler`\n2. **Nested iframe (child of OOPIF)**: Use inherited `_routing_session_id` and `_routing_session_handler`\n3. **Regular element or in-process iframe**: Use main connection (`_connection_handler`), no `sessionId`\n\n### Extended Command Typing\n\nTo make `sessionId` type-safe, Pydoll extended the `Command` TypedDict:\n\n```python\n# From pydoll/protocol/base.py\nclass Command(TypedDict, Generic[T_CommandParams, T_CommandResponse]):\n    \"\"\"Base structure for all commands.\"\"\"\n    id: NotRequired[int]\n    method: str\n    params: NotRequired[T_CommandParams]\n    sessionId: NotRequired[str]  # Added for flattened session routing\n```\n\nThis allows type-checkers to recognize `command['sessionId'] = '...'` as valid without suppressing type warnings.\n\n---\n\n## Performance Considerations\n\n### Caching Strategy\n\n**First access is expensive**:\n\n- `DOM.describeNode`: 1 round-trip\n- Frame tree retrieval: 1+ round-trips (main + OOPIF targets)\n- `DOM.getFrameOwner` per frame: N round-trips (in worst case)\n- `Target.getTargets` + attachments: 1 + M round-trips (M = number of OOPIF targets)\n- `Page.createIsolatedWorld`: 1 round-trip\n- `Runtime.evaluate` (document): 1 round-trip\n\n**Total**: Potentially 5-20+ round-trips depending on page structure.\n\n**Subsequent access is O(1)**:\n\n- `iframe_context` is cached on the `WebElement` instance\n- Accessing `await iframe.iframe_context` multiple times returns the cached value immediately\n- All elements found within the iframe inherit the context (no re-resolution)\n\n### Optimization: Direct Child Target Lookup\n\nIn `_resolve_oopif_by_parent`, Pydoll first checks for direct children by `parentFrameId`:\n\n```python\ndirect_children = [\n    target_info\n    for target_info in target_infos\n    if target_info.get('type') in {'iframe', 'page'}\n    and target_info.get('parentFrameId') == content_frame_id\n]\nif direct_children:\n    # Attach immediately, skip scanning all targets\n```\n\n**Why this helps**:\n\n- Most OOPIFs have `parentFrameId` correctly set\n- Avoids attaching to every target speculatively\n- Reduces round-trips from O(targets) to O(1) in the common case\n\n### Asynchronous Parallel Resolution (Future Enhancement)\n\nCurrently, frame owner matching is sequential (check each frame one by one). A future optimization could parallelize:\n\n```python\n# Current (sequential)\nfor frame_node in frames:\n    owner = await self._owner_backend_for(...)\n    if owner == backend_node_id:\n        return frame_node['id']\n\n# Potential (parallel)\nresults = await asyncio.gather(*(\n    self._owner_backend_for(..., frame['id'])\n    for frame in frames\n))\nfor i, owner in enumerate(results):\n    if owner == backend_node_id:\n        return frames[i]['id']\n```\n\nThis would reduce latency from `N * RTT` to `RTT` (where RTT = round-trip time).\n\n---\n\n## Failure Modes and Debugging\n\n### Common Failure Scenarios\n\n#### 1. **InvalidIFrame: Unable to resolve frameId**\n\n**Cause**:\n\n- The iframe is dynamically created and hasn't fully initialized\n- The iframe is sandboxed with restrictive policies\n- Network issues delayed iframe loading\n\n**Solutions**:\n\n- **Wait for iframe**: Use `await tab.find(id='iframe', timeout=10)` with a timeout\n- **Check sandbox attribute**: Restrictive sandboxing (`<iframe sandbox>`) may block some CDP operations\n- **Retry strategy**: Implement retry logic with exponential backoff\n\n**Debugging**:\n\n```python\ntry:\n    iframe = await tab.find(id='problem-iframe')\n    context = await iframe.iframe_context\nexcept InvalidIFrame as e:\n    # Inspect what we have\n    node_info = await iframe._describe_node(object_id=iframe._object_id)\n    print(f\"Node info: {node_info}\")\n    \n    # Check frame tree manually\n    frame_tree = await WebElement._get_frame_tree_for(tab._connection_handler, None)\n    print(f\"Frame tree: {frame_tree}\")\n```\n\n#### 2. **InvalidIFrame: Unable to create isolated world**\n\n**Cause**:\n\n- Frame has been destroyed/navigated away between resolution steps\n- Chrome bug (rare)\n\n**Solutions**:\n\n- **Re-resolve context**: Clear cached context and re-access\n- **Check navigation**: Ensure iframe isn't navigating during resolution\n\n**Debugging**:\n\n```python\n# Clear cache and retry\niframe._iframe_context = None\ncontext = await iframe.iframe_context\n```\n\n#### 3. **InvalidIFrame: Unable to obtain document reference**\n\n**Cause**:\n\n- The isolated world was created but the document isn't ready\n- Frame is about to navigate\n\n**Solutions**:\n\n- Wait for frame load: Use Page events to detect `Page.frameNavigated` or `Page.loadEventFired`\n- Retry with a small delay\n\n#### 4. **Session routing failures (command times out or returns error)**\n\n**Cause**:\n\n- OOPIF target was detached (page navigated, iframe removed)\n- `sessionId` is stale\n\n**Solutions**:\n\n- **Re-attach to target**: Create a new `ConnectionHandler` and re-resolve OOPIF\n- **Validate target**: Call `Target.getTargets()` to check if target still exists\n\n**Debugging**:\n\n```python\n# Check if session is still valid\ntargets = await handler.execute_command(TargetCommands.get_targets())\nactive_sessions = [t['targetId'] for t in targets['result']['targetInfos']]\nprint(f\"Active targets: {active_sessions}\")\n\nif iframe._iframe_context and iframe._iframe_context.session_id:\n    print(f\"Our session: {iframe._iframe_context.session_id}\")\n```\n\n### Diagnostic Tools\n\n#### Enable CDP logging\n\n```python\nimport logging\nlogging.basicConfig(level=logging.DEBUG)\nlogger = logging.getLogger('pydoll')\nlogger.setLevel(logging.DEBUG)\n```\n\nThis logs all CDP commands and responses, useful for tracing iframe resolution steps.\n\n#### Inspect iframe context\n\n```python\niframe = await tab.find(id='my-iframe')\nctx = await iframe.iframe_context\n\nprint(f\"Frame ID: {ctx.frame_id}\")\nprint(f\"Document URL: {ctx.document_url}\")\nprint(f\"Execution Context ID: {ctx.execution_context_id}\")\nprint(f\"Document Object ID: {ctx.document_object_id}\")\nprint(f\"Session ID (OOPIF): {ctx.session_id}\")\nprint(f\"Session Handler: {ctx.session_handler}\")\n```\n\n---\n\n## Conclusion\n\nPydoll's iframe handling represents a sophisticated implementation of CDP's frame management capabilities. By understanding:\n\n- **The DOM**: Tree structure and node identification\n- **Iframes**: Independent document contexts and security boundaries\n- **OOPIFs**: Site isolation and target-based architecture\n- **CDP domains**: Page, DOM, Target, Runtime coordination\n- **Execution contexts**: Isolated worlds for clean automation\n- **Identifiers**: backendNodeId, frameId, targetId, sessionId, executionContextId, objectId relationships\n- **Resolution pipeline**: Multi-stage fallback strategy for finding frames\n- **Session routing**: Flattened mode and automatic command routing\n\nyou can appreciate why manual context switching is eliminated. The complexity is real, but Pydoll abstracts it behind a simple, intuitive API:\n\n```python\niframe = await tab.find(id='login-frame')\nusername = await iframe.find(name='username')\nawait username.type_text('user@example.com')\n```\n\nThree lines. No context switching. No target attachment. No session management. It just works.\n\n---\n\n## Further Reading\n\n- **CDP Specification**: [Chrome DevTools Protocol - Page Domain](https://chromedevtools.github.io/devtools-protocol/tot/Page/)\n- **CDP Specification**: [Chrome DevTools Protocol - DOM Domain](https://chromedevtools.github.io/devtools-protocol/tot/DOM/)\n- **CDP Specification**: [Chrome DevTools Protocol - Target Domain](https://chromedevtools.github.io/devtools-protocol/tot/Target/)\n- **CDP Specification**: [Chrome DevTools Protocol - Runtime Domain](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/)\n- **Chromium Site Isolation**: [Site Isolation - The Chromium Projects](https://www.chromium.org/Home/chromium-security/site-isolation/)\n- **Content Scripts & Isolated Worlds**: [Chrome Extensions - Content Scripts](https://developer.chrome.com/docs/extensions/mv3/content_scripts/)\n- **Pydoll Documentation**: [Deep Dive → Fundamentals → Chrome DevTools Protocol](./cdp.md)\n- **Pydoll Documentation**: [Features → Automation → IFrames](../../features/automation/iframes.md)\n\n---\n\n!!! tip \"Design Philosophy\"\n    The goal of Pydoll's iframe handling is **ergonomic automation**: write code as if iframes don't exist, and let the library handle the complexity. This deep dive showed what happens behind the scenes—but you never have to think about it in your automation scripts.\n"
  },
  {
    "path": "docs/en/deep-dive/fundamentals/index.md",
    "content": "# Core Fundamentals\n\n**Master the foundation, everything else becomes easier.**\n\nThis section covers the **bedrock technologies** that power Pydoll: the Chrome DevTools Protocol (CDP), WebSocket-based async communication, and Python's type system integration. These aren't just implementation details, they're the **fundamental design decisions** that make Pydoll fast, powerful, and type-safe.\n\n## Why Fundamentals Matter\n\nMost automation frameworks abstract away their communication layer, leaving you with a \"black box\" that works until it doesn't. When something breaks, debugging and optimization become difficult without understanding the underlying mechanisms.\n\n**Pydoll takes a different approach**: we expose and explain the fundamentals, enabling you to work as both a **framework user** and a **protocol engineer**.\n\n!!! quote \"The Power of First Principles\"\n    **\"If you know the way broadly, you will see it in all things.\"** - Miyamoto Musashi\n    \n    Understanding CDP, async communication, and type systems isn't just about Pydoll, it's about understanding **how modern browser automation works at its core**. This knowledge transfers to any CDP-based tool and any async Python project.\n\n## The Three Pillars\n\n### 1. Chrome DevTools Protocol (CDP)\n**[→ Read CDP Deep Dive](./cdp.md)**\n\n**The protocol that powers modern browser automation.**\n\nCDP is Chrome's native debugging protocol, the same one Chrome DevTools (F12) uses. By communicating directly with CDP, Pydoll:\n\n- **Eliminates WebDriver** (no Selenium overhead, no geckodriver/chromedriver intermediaries)\n- **Gains deep control** (modify requests, intercept events, execute privileged operations)\n- **Achieves native speed** (direct WebSocket communication, no HTTP polling)\n- **Becomes undetectable** (no `navigator.webdriver`, no WebDriver fingerprints)\n\n**What you'll learn:**\n\n- How CDP organizes functionality into domains (Page, Network, DOM, Fetch, etc.)\n- The command/event architecture that powers reactive automation\n- Why CDP-based tools are **fundamentally more powerful** than Selenium\n- How to read CDP documentation and extend Pydoll\n\n**Why this matters**: CDP isn't just Pydoll's implementation detail, it's the foundation of modern browser automation. Puppeteer, Playwright, and similar tools all use CDP. Understanding it once provides knowledge applicable across multiple tools.\n\n---\n\n### 2. The Connection Layer\n**[→ Read Connection Layer Architecture](./connection-layer.md)**\n\n**Async communication done right.**\n\nWhile CDP defines **what** you can do, the Connection Layer defines **how** Pydoll communicates with the browser. This is where protocol messages become Python objects, where async/await patterns enable concurrency, and where WebSockets provide real-time bidirectional communication.\n\n**What you'll learn:**\n\n- WebSocket architecture: persistent connections, message framing, keep-alive\n- The async/await pattern: why `async def` and `await` enable concurrent automation\n- Command/response correlation: how Pydoll matches responses to requests\n- Event dispatching: how browser events trigger Python callbacks\n- Error handling: timeout management, connection failures, graceful degradation\n\n**Why this matters**: The connection layer is the communication backbone of Pydoll. Understanding it enables:\n- **Effective debugging**: Inspect messages flowing between Python and Chrome\n- **Performance optimization**: Identify latency sources and parallelize operations\n- **Extension capabilities**: Add custom CDP commands or modify existing behavior\n\n---\n\n### 3. Python Type System Integration\n**[→ Read Type System Deep Dive](./typing-system.md)**\n\n**Types provide both safety and productivity.**\n\nPython's type system (introduced in 3.5, enhanced in every version since) significantly improves development experience. Pydoll leverages `TypedDict`, `Literal`, `overload`, and generics to provide:\n\n- **IDE autocomplete** for CDP response fields\n- **Type checking** to catch bugs before runtime (`mypy`, `pyright`)\n- **Self-documenting code** (function signatures reveal structure)\n- **Refactoring safety** (rename a field, IDE updates all usages)\n\n**What you'll learn:**\n\n- How `TypedDict` models CDP event/response structures\n- Why `overload` provides precise return types for `find()`/`query()`\n- How generics (`TypeVar`, `Generic[T]`) enable flexible command construction\n- Practical patterns: annotating callbacks, typing async functions, using `Literal`\n- Tool integration: configuring mypy, leveraging IDE type inference\n\n**Why this matters**: Type hints have become increasingly important in modern Python. Pydoll's comprehensive type coverage means:\n- **Faster development**: Autocomplete reveals available fields and methods\n- **Fewer bugs**: Type checker catches errors before they reach production\n- **Better refactoring**: Change signatures confidently with IDE support\n\n---\n\n## How These Fundamentals Connect\n\nUnderstanding how CDP, async communication, and type systems work **together** is key:\n\n```mermaid\ngraph TB\n    Python[Python Code:<br/>await tab.go_to#40;url#41;]\n    \n    Python --> TypeSystem[Type System:<br/>Function signature reveals<br/>parameters & return type]\n    \n    TypeSystem --> ConnectionLayer[Connection Layer:<br/>Serialize command to JSON,<br/>send via WebSocket]\n    \n    ConnectionLayer --> CDP[CDP:<br/>Browser receives<br/>Page.navigate command]\n    \n    CDP --> Browser[Chrome:<br/>Executes navigation,<br/>emits events]\n    \n    Browser --> CDPEvents[CDP Events:<br/>Page.loadEventFired,<br/>Network.requestWillBeSent]\n    \n    CDPEvents --> ConnectionLayer2[Connection Layer:<br/>Deserialize events,<br/>dispatch to callbacks]\n    \n    ConnectionLayer2 --> TypedDicts[TypedDict:<br/>Event data as<br/>typed dictionary]\n    \n    TypedDicts --> PythonCallback[Python Callback:<br/>IDE shows available fields<br/>via type inference]\n```\n\n**The flow**:\n\n1. You write Python code with **type annotations** (Type System)\n2. Code serializes to JSON and sends via **WebSocket** (Connection Layer)\n3. Browser receives and executes **CDP commands** (CDP)\n4. Browser emits **CDP events** back (CDP)\n5. Events deserialize into **TypedDict instances** (Type System)\n6. Your callbacks receive **type-safe event objects** (Type System)\n\nEach layer **amplifies** the others:\n\n- Types make CDP responses discoverable\n- CDP's event model enables async patterns\n- Async communication makes types essential (what fields exist on this response?)\n\n## Learning Path\n\nWe recommend this progression:\n\n### Step 1: CDP\n**[Start Here: Chrome DevTools Protocol](./cdp.md)**\n\nUnderstand the protocol that powers everything. Learn domains, commands, events, and how to read CDP documentation.\n\n**Outcome**: You'll know how to find and use any CDP feature, not just what Pydoll exposes.\n\n### Step 2: Connection Layer\n**[Continue: Connection Layer Architecture](./connection-layer.md)**\n\nDeep dive into WebSocket communication, async patterns, and event dispatching.\n\n**Outcome**: You'll understand exactly how messages flow between Python and Chrome, enabling debugging and optimization.\n\n### Step 3: Type System\n**[Finish: Python Type System](./typing-system.md)**\n\nLearn how Pydoll uses modern Python typing for safety and productivity.\n\n**Outcome**: You'll write type-safe automation with full IDE support, catching bugs before they run.\n\n## Prerequisites\n\nTo get the most from this section:\n\n- **Python fundamentals** - Functions, classes, decorators\n- **Basic async/await** - Understand `async def` and `await` keywords\n- **JSON familiarity** - Know how objects/arrays serialize\n- **Browser DevTools** - Have used Chrome Inspector (F12)  \n\n**If you're new to async Python**, read this first: [Real Python: Async IO in Python](https://realpython.com/async-io-python/)\n\n## Beyond the Basics\n\nOnce you've mastered these fundamentals, you'll be ready for:\n\n- **[Internal Architecture](../architecture/browser-domain.md)** - How Pydoll's components fit together\n- **[Network & Security](../network/index.md)** - Protocol-level understanding for proxies\n- **[Fingerprinting](../fingerprinting/index.md)** - Detection techniques requiring CDP knowledge\n\n## Common Questions\n\n### \"Do I need to understand this to use Pydoll?\"\n\n**No**, but understanding these fundamentals will make you more effective. Basic usage works fine without this knowledge. However, when you need to:\n- Debug why something isn't working\n- Optimize slow automation\n- Extend Pydoll with custom CDP commands\n- Understand error messages\n- Contribute to the project\n\nThese fundamentals become very helpful.\n\n### \"Isn't this too low-level?\"\n\nThis level of detail is intentional. Most frameworks hide these fundamentals, but abstraction comes with tradeoffs:\n\n- Understanding enables better debugging\n- Visibility enables optimization\n- Knowledge enables extension\n\nBy teaching fundamentals, we enable you to go beyond what Pydoll provides out-of-the-box.\n\n### \"How much of this do I need to memorize?\"\n\n**None of it.** The goal is building mental models, not memorization. After reading these sections, you'll develop intuition for:\n\n- \"This needs CDP, let me check the protocol docs\"\n- \"This is slow because of sequential await, let me parallelize\"\n- \"This type error means I'm using the wrong field name\"\n\nThe specifics fade, but the understanding remains.\n\n## Philosophy\n\nThese fundamentals represent long-lasting knowledge:\n\n- **CDP** is Chrome's native protocol and continues to evolve\n- **Async/await** is Python's standard for concurrency\n- **Type systems** are increasingly important in Python (PEP 484 onwards)\n\nLearning these concepts provides value across your development career.\n\n---\n\n## Ready to Build Your Foundation?\n\nStart with **[Chrome DevTools Protocol](./cdp.md)** to understand the protocol that powers everything. Then progress through the Connection Layer and Type System to complete your fundamental understanding.\n\n**This is where automation becomes engineering.**\n\n---\n\n!!! tip \"After Completing Fundamentals\"\n    Once you've mastered these concepts, you'll see them **everywhere** in Pydoll's architecture:\n    \n    - Browser/Tab/WebElement all use the **Connection Layer**\n    - Network events all follow **CDP's event model**\n    - All responses use **TypedDict** for type safety\n    \n    The fundamentals aren't separate from Pydoll, they **are** Pydoll's foundation.\n"
  },
  {
    "path": "docs/en/deep-dive/fundamentals/typing-system.md",
    "content": "# Python's Type System & Pydoll\n\nPydoll leverages Python's type system extensively to provide excellent IDE support, catch errors early, and make the API self-documenting. This guide explains the basics of type hints and how Pydoll uses them to enhance your development experience.\n\n## Type Hints Basics\n\nType hints are optional annotations that specify what type of value a variable, parameter, or return value should be. They don't affect runtime behavior but enable powerful tooling.\n\n### Simple Type Hints\n\n```python\n# Basic types\nname: str = \"Pydoll\"\nport: int = 9222\nis_headless: bool = False\nquality: float = 0.85\n\n# Function annotations\ndef navigate(url: str, timeout: int = 30) -> bool:\n    # ... implementation\n    return True\n```\n\n### Container Types\n\n```python\nfrom typing import List, Dict, Optional\n\n# Lists and dictionaries\nurls: List[str] = ['https://example.com', 'https://google.com']\nheaders: Dict[str, str] = {'User-Agent': 'MyBot/1.0'}\n\n# Optional values (can be None)\ntarget_id: Optional[str] = None\n\n# Modern syntax (Python 3.9+)\nurls: list[str] = ['https://example.com']\nheaders: dict[str, str] = {'User-Agent': 'MyBot/1.0'}\n```\n\n!!! tip \"Python 3.9+ Syntax\"\n    Pydoll's codebase uses the older `List[]`, `Dict[]` syntax for backward compatibility, but you can use lowercase `list[]`, `dict[]` in your code if you're on Python 3.9+.\n\n## TypedDict: Structured Dictionaries\n\nTypedDict allows you to define dictionary structures with specific keys and value types. This is **heavily used** in Pydoll's CDP protocol definitions.\n\n### Basic TypedDict\n\n```python\nfrom typing import TypedDict\n\nclass UserInfo(TypedDict):\n    name: str\n    age: int\n    email: str\n\n# IDE knows exactly what keys exist\nuser: UserInfo = {\n    'name': 'Alice',\n    'age': 30,\n    'email': 'alice@example.com'\n}\n\n# Autocomplete works!\nprint(user['name'])  # IDE suggests: name, age, email\n```\n\n### How Pydoll Uses TypedDict\n\nPydoll defines **every CDP command, response, and event** as a TypedDict. This means your IDE knows exactly what properties are available:\n\n```python\n# From pydoll/protocol/page/methods.py\nclass CaptureScreenshotParams(TypedDict, total=False):\n    \"\"\"Parameters for captureScreenshot.\"\"\"\n    format: ScreenshotFormat\n    quality: int\n    clip: Viewport\n    fromSurface: bool\n    captureBeyondViewport: bool\n    optimizeForSpeed: bool\n\nclass CaptureScreenshotResult(TypedDict):\n    \"\"\"Result for captureScreenshot command.\"\"\"\n    data: str\n```\n\nWhen you call methods that return CDP responses, your IDE autocompletes the response keys:\n\n```python\nasync def example():\n    response = await tab.take_screenshot(as_base64=True)\n    \n    # IDE knows this is CaptureScreenshotResponse\n    # and suggests 'result' -> 'data'\n    screenshot_data = response['result']['data']  # Full autocomplete!\n```\n\n### Optional vs Required Fields\n\nTypedDict supports optional fields using `NotRequired[]`:\n\n```python\nfrom typing import TypedDict, NotRequired\n\n# From pydoll/protocol/network/methods.py\nclass GetCookiesParams(TypedDict):\n    \"\"\"Parameters for retrieving browser cookies.\"\"\"\n    urls: NotRequired[list[str]]  # This field is optional\n```\n\nThe `total=False` flag makes **all** fields optional:\n\n```python\nclass CaptureScreenshotParams(TypedDict, total=False):\n    format: ScreenshotFormat  # All fields optional\n    quality: int\n    clip: Viewport\n```\n\n!!! info \"Autocomplete Magic\"\n    When you type `response['`, your IDE shows you all available keys with their types. This is TypedDict's superpower in action!\n\n## Enums: Type-Safe Constants\n\nEnums provide type-safe constants that your IDE can autocomplete. Pydoll uses them extensively for CDP values.\n\n### Basic Enums\n\n```python\nfrom enum import Enum\n\nclass ScreenshotFormat(str, Enum):\n    JPEG = 'jpeg'\n    PNG = 'png'\n    WEBP = 'webp'\n\n# IDE autocompletes available formats\nformat = ScreenshotFormat.PNG  # Type is ScreenshotFormat\nprint(format.value)  # 'png'\n```\n\n### Pydoll's Enum Usage\n\n```python\nfrom pydoll.constants import Key\nfrom pydoll.protocol.page.types import ScreenshotFormat\nfrom pydoll.protocol.input.types import KeyModifier\n\n# Finding elements - uses kwargs, not enums\nelement = await tab.find(id='submit-btn')\nelement = await tab.find(class_name='btn-primary')\nelement = await tab.find(tag_name='button')\n\n# Keyboard input - IDE suggests all keys\nawait element.press_keyboard_key(Key.ENTER)\nawait element.press_keyboard_key(Key.TAB)\nawait element.press_keyboard_key(Key.ESCAPE)\n\n# Modifiers are integer enums (for special keys)\nawait element.press_keyboard_key(Key.TAB, modifiers=KeyModifier.SHIFT)\n\n# Screenshot format enum\nawait tab.take_screenshot('file.webp', format=ScreenshotFormat.WEBP)\n```\n\n!!! tip \"Enum Autocomplete\"\n    Type `Key.` or `ScreenshotFormat.` and your IDE shows all available options. No more memorizing strings!\n\n## Function Overloads\n\nOverloads allow a function to return different types based on its parameters. Pydoll uses this to provide precise type information.\n\n### Basic Overload Example\n\n```python\nfrom typing import overload\n\n# Overload signatures (not executed)\n@overload\ndef process(data: str) -> str: ...\n\n@overload\ndef process(data: int) -> int: ...\n\n# Actual implementation\ndef process(data):\n    return data * 2\n\n# IDE knows return types\nresult1 = process(\"hello\")  # Type: str\nresult2 = process(42)       # Type: int\n```\n\n### Pydoll's Overload Usage\n\nThe `find()` and `query()` methods return different types depending on the `find_all` parameter:\n\n```python\n# From pydoll/elements/mixins/find_elements_mixin.py\nclass FindElementsMixin:\n    @overload\n    async def find(\n        self, find_all: Literal[False] = False, **kwargs\n    ) -> WebElement: ...\n    \n    @overload\n    async def find(\n        self, find_all: Literal[True], **kwargs\n    ) -> list[WebElement]: ...\n    \n    async def find(\n        self, find_all: bool = False, **kwargs\n    ) -> Union[WebElement, list[WebElement]]:\n        # Implementation...\n```\n\nIn your code:\n\n```python\n# find_all=False (default) - IDE knows return type is WebElement\nbutton = await tab.find(id='submit-btn')\nawait button.click()  # Single element methods available!\n\n# find_all=True - IDE knows return type is list[WebElement]\nbuttons = await tab.find(class_name='btn', find_all=True)\nfor btn in buttons:  # IDE knows this is a list!\n    await btn.click()\n\n# Same with query()\nelement = await tab.query('#submit-btn')  # Type: WebElement\nelements = await tab.query('.btn', find_all=True)  # Type: list[WebElement]\n```\n\n!!! tip \"Smart Type Inference\"\n    Your IDE automatically knows whether you're getting a single element or a list based on the `find_all` parameter. No casting or type assertions needed!\n\n## Generic Types\n\nGenerics are like \"type containers\" that work with different types while preserving type information. Think of them as templates that adapt to whatever you put inside.\n\n### Understanding Generics: A Simple Analogy\n\nImagine a `Box` that can hold anything. Without generics:\n\n```python\n# Without generics - IDE doesn't know what's inside\nclass Box:\n    def __init__(self, content):\n        self.content = content\n    \n    def get(self):\n        return self.content\n\nmy_box = Box(\"hello\")\nitem = my_box.get()  # Type: Unknown - could be anything!\n```\n\nWith generics:\n\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar('T')  # T is a \"type placeholder\"\n\nclass Box(Generic[T]):\n    def __init__(self, content: T):\n        self.content = content\n    \n    def get(self) -> T:\n        return self.content\n\n# Now IDE knows exactly what's inside each box\nstring_box: Box[str] = Box(\"hello\")\nitem1 = string_box.get()  # Type: str\n\nnumber_box: Box[int] = Box(42)\nitem2 = number_box.get()  # Type: int\n\n# List is a built-in generic\nnumbers: list[int] = [1, 2, 3]  # List that contains ints\nnames: list[str] = [\"Alice\", \"Bob\"]  # List that contains strings\n```\n\n!!! tip \"Generics Simplify Type Hints\"\n    Instead of writing `Union[List[str], List[int], List[float], ...]` for every possible list type, generics let you write one reusable `list[T]` that adapts to whatever you put inside.\n\n### Real-World Generic Example\n\n```python\nfrom typing import TypeVar, Generic\n\nT = TypeVar('T')\n\nclass Response(Generic[T]):\n    \"\"\"A generic API response wrapper.\"\"\"\n    def __init__(self, data: T, status: int):\n        self.data = data\n        self.status = status\n    \n    def get_data(self) -> T:\n        return self.data\n\n# Each response preserves its data type\nuser_response: Response[dict] = Response({\"name\": \"Alice\"}, 200)\nuser_data = user_response.get_data()  # Type: dict\n\ncount_response: Response[int] = Response(42, 200)\ncount = count_response.get_data()  # Type: int\n```\n\n### How Pydoll Uses Generics\n\nPydoll's CDP command system uses generics to ensure the response type matches the command:\n\n```python\n# From pydoll/protocol/base.py\nfrom typing import Generic, TypeVar\n\nT_CommandParams = TypeVar('T_CommandParams')\nT_CommandResponse = TypeVar('T_CommandResponse')\n\nclass Command(TypedDict, Generic[T_CommandParams, T_CommandResponse]):\n    \"\"\"Base structure for all commands.\"\"\"\n    id: NotRequired[int]\n    method: str\n    params: NotRequired[T_CommandParams]\n\nclass Response(TypedDict, Generic[T_CommandResponse]):\n    \"\"\"Base structure for all responses.\"\"\"\n    id: int\n    result: T_CommandResponse\n```\n\nThis means when you execute a command, the response type is automatically inferred:\n\n```python\n# PageCommands.navigate returns Command[NavigateParams, NavigateResult]\ncommand = PageCommands.navigate('https://example.com')\n\n# ConnectionHandler.execute_command preserves the generic type\nresponse = await connection_handler.execute_command(command)\n\n# IDE knows response['result'] is NavigateResult (not just \"any dict\")\nframe_id = response['result']['frameId']  # Autocomplete works!\nloader_id = response['result']['loaderId']  # All fields are known!\n```\n\n!!! info \"Why Generics Matter in Pydoll\"\n    Without generics, every CDP response would just be typed as `dict[str, Any]`, and you'd lose all autocomplete. With generics, the IDE knows the exact structure of each response based on which command you sent.\n\n## Union Types\n\nUnions represent values that could be one of several types:\n\n```python\nfrom typing import Union\n\n# Can be string or int\nidentifier: Union[str, int] = \"user-123\"\nidentifier = 456  # Also valid\n\n# Modern syntax (Python 3.10+)\nidentifier: str | int = \"user-123\"\n```\n\n### Pydoll's Union Usage\n\n```python\n# File paths can be strings or Path objects\nfrom pathlib import Path\n\nasync def upload_file(files: Union[str, Path, list[Union[str, Path]]]):\n    # Handles multiple input types\n    pass\n\n# All of these work:\nawait tab.expect_file_chooser('/path/to/file.txt')\nawait tab.expect_file_chooser(Path('/path/to/file.txt'))\nawait tab.expect_file_chooser(['/file1.txt', Path('/file2.txt')])\n```\n\n## Practical Benefits in Pydoll\n\n### 1. Intelligent Autocomplete\n\nYour IDE suggests available keys, methods, and values:\n\n```python\nfrom pydoll.protocol.page.events import PageEvent\nfrom pydoll.protocol.network.types import ResourceType\nfrom pydoll.protocol.input.types import KeyModifier\nfrom pydoll.constants import Key\n\n# Autocomplete for event names\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, callback)\nawait tab.on(PageEvent.JAVASCRIPT_DIALOG_OPENING, callback)\n\n# Autocomplete for resource types\nawait tab.enable_fetch_events(resource_type=ResourceType.XHR)\nawait tab.enable_fetch_events(resource_type=ResourceType.DOCUMENT)\n\n# Autocomplete for keys\nawait element.press_keyboard_key(Key.ENTER)\nawait element.press_keyboard_key(Key.TAB, modifiers=KeyModifier.SHIFT)\n\n# Autocomplete for kwargs in find()\nelement = await tab.find(id='submit-btn')  # IDE suggests: id, class_name, tag_name, etc.\n```\n\n### 2. Catch Errors Early\n\nType checkers like mypy or Pylance catch errors before runtime:\n\n```python\n# Type checker catches this\nawait tab.take_screenshot('file.png', quality='high')  # Error: quality must be int\n\n# Type checker catches this\nevent = await tab.find(id='button')\nawait tab.on(event, callback)  # Error: event is WebElement, not str\n\n# Correct\nawait tab.take_screenshot('file.png', quality=90)\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, callback)\n```\n\n### 3. Self-Documenting Code\n\nTypes serve as inline documentation:\n\n```python\n# You immediately know what each parameter expects\nasync def take_screenshot(\n    self,\n    path: Optional[str] = None,\n    quality: int = 100,\n    beyond_viewport: bool = False,\n    as_base64: bool = False,\n) -> Optional[str]:\n    pass\n```\n\n### 4. CDP Response Navigation\n\nNavigate complex CDP responses with confidence:\n\n```python\n# From pydoll/protocol/browser/methods.py\nclass GetVersionResult(TypedDict):\n    protocolVersion: str\n    product: str\n    revision: str\n    userAgent: str\n    jsVersion: str\n\n# In your code\nversion_info = await browser.get_version()\n\n# IDE suggests all available keys\nprint(version_info['product'])         # Autocomplete!\nprint(version_info['userAgent'])       # Autocomplete!\nprint(version_info['protocolVersion']) # Autocomplete!\n```\n\n## Type Checking Your Code\n\n### Using Pylance (VS Code)\n\nPylance provides real-time type checking in VS Code:\n\n1. Install the Pylance extension\n2. Set type checking mode in settings:\n\n```json\n{\n    \"python.analysis.typeCheckingMode\": \"basic\"  // or \"strict\"\n}\n```\n\nNow you get instant feedback:\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Pylance shows parameter types as you type\n        await tab.go_to('https://example.com', timeout=30)\n        \n        # Pylance warns about wrong types\n        await tab.take_screenshot(quality='high')  # Warning!\n```\n\n### Using mypy\n\nRun mypy to check your entire project:\n\n```bash\npip install mypy\nmypy your_script.py\n```\n\nExample output:\n\n```\nyour_script.py:10: error: Argument \"quality\" to \"take_screenshot\" has incompatible type \"str\"; expected \"int\"\nFound 1 error in 1 file (checked 1 source file)\n```\n\n## Pydoll's Protocol Type System\n\nPydoll's `protocol/` directory contains comprehensive type definitions for the entire Chrome DevTools Protocol:\n\n```\npydoll/protocol/\n├── base.py              # Generic Command, Response, CDPEvent types\n├── browser/\n│   ├── events.py        # BrowserEvent enum, event parameter TypedDicts\n│   ├── methods.py       # Browser method enums, parameter/result TypedDicts\n│   └── types.py         # Browser domain types (Bounds, PermissionType, etc.)\n├── dom/\n│   ├── events.py        # DOM event definitions\n│   ├── methods.py       # DOM command definitions\n│   └── types.py         # DOM types (Node, BackendNode, etc.)\n├── page/\n│   ├── events.py        # Page events (LOAD_EVENT_FIRED, etc.)\n│   ├── methods.py       # Page methods (navigate, captureScreenshot, etc.)\n│   └── types.py         # Page types (Frame, ScreenshotFormat, etc.)\n├── network/\n│   └── ...              # Network domain types\n└── ...                  # Other CDP domains\n```\n\n### Example: Complete Type Flow\n\nLet's trace a complete type flow from command to response:\n\n```python\n# 1. Method enum (protocol/page/methods.py)\nclass PageMethod(str, Enum):\n    CAPTURE_SCREENSHOT = 'Page.captureScreenshot'\n\n# 2. Parameter TypedDict (protocol/page/methods.py)\nclass CaptureScreenshotParams(TypedDict, total=False):\n    format: ScreenshotFormat\n    quality: int\n    clip: Viewport\n\n# 3. Result TypedDict (protocol/page/methods.py)\nclass CaptureScreenshotResult(TypedDict):\n    data: str\n\n# 4. Command creation (commands/page_commands.py)\nclass PageCommands:\n    @staticmethod\n    def capture_screenshot(\n        format: Optional[ScreenshotFormat] = None,\n        quality: Optional[int] = None,\n        ...\n    ) -> Command[CaptureScreenshotParams, CaptureScreenshotResult]:\n        return {\n            'method': PageMethod.CAPTURE_SCREENSHOT,\n            'params': {...}\n        }\n\n# 5. Usage in Tab (browser/tab.py)\nclass Tab:\n    async def take_screenshot(...) -> Optional[str]:\n        response: CaptureScreenshotResponse = await self._execute_command(\n            PageCommands.capture_screenshot(...)\n        )\n        screenshot_data = response['result']['data']  # Fully typed!\n        return screenshot_data\n```\n\nEvery step maintains type information, giving you autocomplete and type checking throughout!\n\n## Best Practices\n\n### 1. Let Pydoll's Types Guide You\n\nDon't fight the types, they're there to help:\n\n```python\n# Good: Use kwargs (IDE autocompletes parameter names)\nelement = await tab.find(id='submit-btn')\nbutton = await tab.find(class_name='btn-primary')\n\n# Good: Use enums where applicable\nfrom pydoll.constants import Key\nawait element.press_keyboard_key(Key.ENTER)\n\n# Avoid: Magic strings\nawait element.press_keyboard_key('Enter')  # No autocomplete, error-prone\n```\n\n### 2. Explore Types in Your IDE\n\nHover over variables to see their types:\n\n```python\n# Hover over 'response' to see: Response[CaptureScreenshotResult]\nresponse = await tab._execute_command(PageCommands.capture_screenshot(...))\n\n# Hover over 'data' to see: str\ndata = response['result']['data']\n```\n\n\n### 3. Don't Over-Annotate\n\nPython's type inference is smart, don't annotate everything:\n\n```python\n# Too much\nname: str = \"Alice\"\ncount: int = 5\nis_active: bool = True\n\n# Let Python infer simple literals\nname = \"Alice\"\ncount = 5\nis_active = True\n\n# Annotate when type isn't obvious\nfrom typing import Optional\n\nresult: Optional[WebElement] = await tab.find(id='missing', raise_exc=False)\n```\n\n## Learn More\n\nFor deeper understanding of Python's type system and CDP protocol:\n\n- **[Python typing documentation](https://docs.python.org/3/library/typing.html)**: Official Python typing reference\n- **[PEP 484](https://peps.python.org/pep-0484/)**: The original type hints proposal\n- **[Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)**: CDP documentation\n- **[Deep Dive: CDP](./cdp.md)**: How Pydoll implements CDP\n- **[API Reference: Protocol](../api/protocol/base.md)**: Pydoll's protocol type definitions\n\nThe type system transforms Pydoll from a simple automation library into a **type-safe, self-documenting, IDE-friendly** framework. It catches bugs before they happen and makes exploring the API a breeze!\n\n"
  },
  {
    "path": "docs/en/deep-dive/guides/index.md",
    "content": "# Practical Guides\n\n**Theory meets practice, actionable patterns for real automation challenges.**\n\nWhile the other Deep Dive sections explore **fundamentals** and **architecture**, this section provides **practical, battle-tested guides** for common automation scenarios. These aren't academic exercises, they're patterns refined through production use.\n\n## The Purpose of Guides\n\nYou've learned:\n\n- **[Fundamentals](../fundamentals/cdp.md)** - CDP, async, types\n- **[Architecture](../architecture/browser-domain.md)** - Internal design patterns\n- **[Network](../network/index.md)** - Protocols and proxies\n- **[Fingerprinting](../fingerprinting/index.md)** - Detection and evasion\n\nNow what? **How do you apply this knowledge to real problems?**\n\nThat's what guides are for: **bridging theory and practice**.\n\n!!! quote \"Practical Wisdom\"\n    **\"In theory, theory and practice are the same. In practice, they are not.\"** - Yogi Berra\n    \n    Guides distill complex technical knowledge into **actionable patterns** you can use immediately. They show you **what works** in production, not just what's theoretically possible.\n\n## Current Guides\n\n### CSS Selectors vs XPath\n**[→ Read Selectors Guide](./selectors-guide.md)**\n\n**The eternal debate, solved with data and best practices.**\n\nChoosing between CSS selectors and XPath isn't about preference. It's about understanding **tradeoffs**, **performance characteristics**, and **maintainability**.\n\n**What you'll learn**:\n\n- **Syntax comparison** - Side-by-side examples for common patterns\n- **Performance benchmarks** - Real measurements, not myths\n- **Power vs simplicity** - When CSS isn't enough (text matching, axes)\n- **Browser support** - Compatibility and edge cases\n- **Best practices** - When to use each, anti-patterns to avoid\n- **Complex examples** - Real-world selector challenges solved\n\n**Why this matters**: Element location is the **foundation** of automation. Choose the wrong tool, and you'll fight your selectors forever. Choose wisely, and automation becomes straightforward.\n\n---\n\n## Coming Soon\n\n### Asyncio & Concurrent Automation\n**Coming in future releases**\n\n**Deep dive into Python's asyncio: event loop internals, practical concurrency patterns, and real-world examples.**\n\nUnderstanding asyncio is fundamental to Pydoll. This guide provides a comprehensive analysis of Python's event loop, concurrency primitives, and how to apply them to browser automation without footguns.\n\n**Will cover**:\n\n- **Event Loop Internals**: How `asyncio.run()` works, task scheduling, and execution flow\n- **Async/Await Deep Dive**: Coroutines, futures, and the async state machine\n- **Concurrency Primitives**: `gather()`, `create_task()`, `TaskGroup`, and when to use each\n- **Rate Limiting**: Semaphores, queues, and throttling strategies\n- **Real-World Examples**: Multi-tab scraping, parallel form filling, coordinated browser instances\n- **Common Pitfalls**: Blocking the event loop, task cancellation, exception propagation\n- **Performance Analysis**: Profiling async code, identifying bottlenecks, optimizing I/O\n\n**Why this matters**: Asyncio powers Pydoll's architecture. Master it, and you unlock true concurrent automation without race conditions or state corruption.\n\n---\n\n### Architectural Patterns & Robust Selectors\n**Coming in future releases**\n\n**PageObject pattern, maintainable selectors, and architectural approaches for scalable automation.**\n\nMove beyond ad-hoc scripts to structured, maintainable automation architectures. Learn patterns that scale from simple scripts to production systems.\n\n**Will cover**:\n\n- **PageObject Pattern**: Encapsulating page structure, reducing duplication, improving maintainability\n- **Robust Selector Strategies**: Building selectors that survive page changes, avoiding brittle locators\n- **Component Abstraction**: Reusable components for common UI patterns (modals, dropdowns, tables)\n- **Waiting Strategies**: Smart waiting patterns beyond simple timeouts\n- **State Management**: Managing automation state across pages and flows\n- **Testing Patterns**: How to structure automation code for testability\n- **Real-World Architecture**: Production-ready project structure and organization\n\n**Why this matters**: The difference between throwaway scripts and maintainable automation systems is architecture. Learn patterns that make your code resilient to change.\n\n---\n\n## Guide Philosophy\n\nGuides follow consistent principles:\n\n### 1. Production-Ready Code\nAll examples are **complete and tested**, not pseudocode or simplified demonstrations. You can copy-paste and adapt to your needs.\n\n### 2. Real-World Scenarios\nGuides address **actual problems** encountered in production automation, not contrived examples.\n\n### 3. Tradeoff Analysis\nWhen multiple approaches exist, guides **compare** them objectively with pros/cons, not just \"here's one way.\"\n\n### 4. Progressive Complexity\nStart simple, add complexity incrementally. Basic pattern first, then edge cases and advanced variations.\n\n### 5. Anti-Patterns Highlighted\nShow **what NOT to do** explicitly, common mistakes caught through code review or production debugging.\n\n## How to Use Guides\n\nGuides are **reference material**, not sequential tutorials:\n\n- **Skim** for patterns relevant to your current problem  \n- **Bookmark** guides you'll need repeatedly  \n- **Adapt** examples to your specific context  \n- **Combine** patterns from multiple guides  \n\nDon't read sequentially cover-to-cover.  \nDon't blindly copy without understanding tradeoffs.  \nDon't use outdated patterns (check publication date).  \n\n## Contributing Guides\n\nHave a pattern worth sharing? Guides are **community-driven**:\n\n**What makes a good guide**:\n\n- Solves a **real problem** encountered in production\n- Provides **working code**, not just concepts\n- Compares **multiple approaches** with tradeoffs\n- Highlights **common mistakes** explicitly\n- Explains **why**, not just **how**\n\nSee [Contributing](../../CONTRIBUTING.md) for submission guidelines.\n\n## Guides vs Features Documentation\n\n**Confused about the difference?**\n\n|| Features Documentation | Deep Dive Guides |\n|---|---|---|\n| **Purpose** | Teach what Pydoll can do | Show how to solve problems |\n| **Scope** | Single method/feature | Multiple features combined |\n| **Depth** | API reference + examples | Patterns + tradeoffs + best practices |\n| **Order** | Structured by component | Structured by problem |\n| **Examples** | Simple, isolated | Complex, production-ready |\n\n**Use Features for**: Learning Pydoll's API  \n**Use Guides for**: Solving real automation challenges\n\n## Beyond Guides\n\nAfter mastering practical patterns:\n\n- **[Architecture](../architecture/browser-domain.md)** - Understand why patterns work\n- **[Network](../network/index.md)** - Network-level optimization\n- **[Fingerprinting](../fingerprinting/evasion-techniques.md)** - Anti-detection techniques\n\nGuides provide **immediate value**. Architecture provides **deep understanding**. Both make you effective.\n\n---\n\n## Ready for Practical Patterns?\n\nStart with **[CSS Selectors vs XPath](./selectors-guide.md)** to master element location, the foundation of all automation.\n\n**More guides coming soon. Star the repo to stay updated!**\n\n---\n\n!!! tip \"Request a Guide\"\n    Have a automation pattern you'd like documented? Open an issue titled \"Guide Request: [Topic]\" describing:\n    \n    - The problem you're trying to solve\n    - What you've tried so far\n    - Why existing documentation doesn't cover it\n    \n    We prioritize guides based on community need.\n\n## Quick Reference\n\n**Available Now:**\n\n- [CSS Selectors vs XPath](./selectors-guide.md)\n\n**Coming Soon:**\n\n- Asyncio & Concurrent Automation\n- Architectural Patterns & Robust Selectors\n\n**Timeline**: New guides added based on community feedback and production learnings.\n"
  },
  {
    "path": "docs/en/deep-dive/guides/selectors-guide.md",
    "content": "# CSS Selectors vs XPath: A Complete Guide\n\nWhen using the `query()` method, you have two powerful selector languages at your disposal: CSS Selectors and XPath. Understanding when and how to use each is crucial for effective element location.\n\n## Fundamental Differences\n\n| Aspect | CSS Selector | XPath |\n|--------|--------------|-------|\n| **Syntax** | Simple, CSS-like | XML path language |\n| **Performance** | Faster (native browser support) | Slightly slower |\n| **Direction** | Only traverses down and sideways | Can traverse in any direction |\n| **Text Matching** | Limited (pseudo-selectors) | Powerful text functions |\n| **Complexity** | Best for simple to moderate cases | Excels at complex relationships |\n| **Readability** | More intuitive for web developers | Steeper learning curve |\n\n## When to Use CSS Selectors\n\nCSS selectors are ideal for:\n\n- Simple element selection by ID, class, or tag\n- Direct parent-child relationships\n- Attribute matching with simple patterns\n- Performance-critical scenarios\n- When traversing downward in the DOM\n\n```python\n# Clean and performant CSS examples\nawait tab.query(\"#login-form\")\nawait tab.query(\".submit-button\")\nawait tab.query(\"div.container > p.intro\")\nawait tab.query(\"input[type='email'][required]\")\nawait tab.query(\"ul.menu li:first-child\")\n```\n\n## When to Use XPath\n\nXPath is ideal for:\n\n- Complex text matching and partial text searches\n- Traversing upward to parent elements\n- Finding elements relative to siblings\n- Conditional logic in selectors\n- Complex DOM relationships\n\n```python\n# Powerful XPath examples\nawait tab.query(\"//button[contains(text(), 'Submit')]\")\nawait tab.query(\"//input[@name='email']/parent::div\")\nawait tab.query(\"//td[text()='John']/following-sibling::td[2]\")\nawait tab.query(\"//div[contains(@class, 'product') and @data-price > 100]\")\n```\n\n## CSS Selector Syntax Reference\n\n### Basic Selectors\n\n```python\n# Element selector\nawait tab.query(\"div\")              # First <div> element\nawait tab.query(\"div\", find_all=True)  # All <div> elements\nawait tab.query(\"button\")           # First <button> element\n\n# ID selector\nawait tab.query(\"#username\")        # Element with id=\"username\"\n\n# Class selector\nawait tab.query(\".submit-btn\")      # First element with class=\"submit-btn\"\nawait tab.query(\".submit-btn\", find_all=True)  # All elements with class\nawait tab.query(\".btn.primary\")     # First element with both classes\n\n# Universal selector\nawait tab.query(\"*\", find_all=True) # All elements\n```\n\n### Combinators\n\n```python\n# Descendant combinator (space)\nawait tab.query(\"div p\")            # First <p> inside <div>\nawait tab.query(\"div p\", find_all=True)  # All <p> inside <div> (any depth)\n\n# Child combinator (>)\nawait tab.query(\"div > p\")          # First <p> that is direct child of <div>\nawait tab.query(\"div > p\", find_all=True)  # All <p> that are direct children\n\n# Adjacent sibling combinator (+)\nawait tab.query(\"h1 + p\")           # <p> immediately after <h1>\n\n# General sibling combinator (~)\nawait tab.query(\"h1 ~ p\")           # First <p> sibling after <h1>\nawait tab.query(\"h1 ~ p\", find_all=True)  # All <p> siblings after <h1>\n```\n\n### Attribute Selectors\n\n```python\n# Attribute exists\nawait tab.query(\"input[required]\")                # First input with 'required'\nawait tab.query(\"input[required]\", find_all=True) # All inputs with 'required'\n\n# Attribute equals\nawait tab.query(\"input[type='email']\")            # First email input\nawait tab.query(\"input[type='email']\", find_all=True)  # All email inputs\n\n# Attribute contains word\nawait tab.query(\"div[class~='active']\")           # First div with 'active' class\n\n# Attribute starts with\nawait tab.query(\"a[href^='https://']\")            # First HTTPS link\nawait tab.query(\"a[href^='https://']\", find_all=True)  # All HTTPS links\n\n# Attribute ends with\nawait tab.query(\"img[src$='.png']\")               # First PNG image\nawait tab.query(\"img[src$='.png']\", find_all=True)     # All PNG images\n\n# Attribute contains substring\nawait tab.query(\"a[href*='example']\")             # First link with 'example'\nawait tab.query(\"a[href*='example']\", find_all=True)   # All links with 'example'\n\n# Case-insensitive matching\nawait tab.query(\"input[type='text' i]\")           # Case-insensitive match\n```\n\n### Pseudo-Classes\n\n```python\n# Structural pseudo-classes\nawait tab.query(\"li:first-child\")                 # First <li> that is first child\nawait tab.query(\"li:last-child\")                  # First <li> that is last child\nawait tab.query(\"li:nth-child(2)\")                # First <li> that is 2nd child\nawait tab.query(\"li:nth-child(odd)\", find_all=True)  # All odd-numbered <li>\nawait tab.query(\"li:nth-child(even)\", find_all=True)  # All even-numbered <li>\nawait tab.query(\"li:nth-child(3n)\", find_all=True)    # Every 3rd <li>\n\n# Type-based pseudo-classes\nawait tab.query(\"p:first-of-type\")                # First <p> among siblings\nawait tab.query(\"p:last-of-type\")                 # Last <p> among siblings\nawait tab.query(\"p:nth-of-type(2)\")               # Second <p> among siblings\n\n# State pseudo-classes\nawait tab.query(\"input:enabled\")                  # First enabled input\nawait tab.query(\"input:enabled\", find_all=True)   # All enabled inputs\nawait tab.query(\"input:disabled\")                 # First disabled input\nawait tab.query(\"input:checked\")                  # First checked checkbox/radio\nawait tab.query(\"input:focus\")                    # Currently focused input\n\n# Other useful pseudo-classes\nawait tab.query(\"div:empty\")                      # First empty element\nawait tab.query(\"div:empty\", find_all=True)       # All empty elements\nawait tab.query(\"div:not(.exclude)\")              # First div without class\nawait tab.query(\"div:not(.exclude)\", find_all=True)  # All divs without class\n```\n\n## XPath Syntax Reference\n\n### Basic Path Expressions\n\n```python\n# Absolute path (from root)\nawait tab.query(\"/html/body/div\")                 # First div at exact path\n\n# Relative path (from anywhere)\nawait tab.query(\"//div\")                          # First <div> element\nawait tab.query(\"//div\", find_all=True)           # All <div> elements\nawait tab.query(\"//div/p\")                        # First <p> inside any <div>\nawait tab.query(\"//div/p\", find_all=True)         # All <p> inside any <div>\n\n# Current node\nawait tab.query(\"./div\")                          # First <div> relative to current\n\n# Parent node\nawait tab.query(\"..\")                             # Parent of current node\n```\n\n### Attribute Selection\n\n```python\n# Basic attribute matching\nawait tab.query(\"//input[@type='email']\")         # First email input\nawait tab.query(\"//input[@type='email']\", find_all=True)  # All email inputs\nawait tab.query(\"//div[@id='content']\")           # Div with id='content'\n\n# Multiple attributes\nawait tab.query(\"//input[@type='text' and @required]\")  # First match\nawait tab.query(\"//input[@type='text' and @required]\", find_all=True)  # All matches\nawait tab.query(\"//div[@class='card' or @class='panel']\")  # First card or panel\n\n# Attribute exists\nawait tab.query(\"//button[@disabled]\")            # First disabled button\nawait tab.query(\"//button[@disabled]\", find_all=True)  # All disabled buttons\n```\n\n## XPath Axes (Directional Navigation)\n\nThe real power of XPath comes from its ability to navigate in any direction through the DOM tree.\n\n### Axes Reference Table\n\n| Axis | Direction | Description | Example |\n|------|-----------|-------------|---------|\n| `child::` | Down | Direct children only | `//div/child::p` |\n| `descendant::` | Down | All descendants (any depth) | `//div/descendant::a` |\n| `parent::` | Up | Immediate parent | `//input/parent::div` |\n| `ancestor::` | Up | All ancestors (any depth) | `//span/ancestor::div` |\n| `following-sibling::` | Sideways | Siblings after current | `//h1/following-sibling::p` |\n| `preceding-sibling::` | Sideways | Siblings before current | `//p/preceding-sibling::h1` |\n| `following::` | Forward | All nodes after current | `//h1/following::*` |\n| `preceding::` | Backward | All nodes before current | `//h1/preceding::*` |\n| `ancestor-or-self::` | Up | Ancestors + current | `//div/ancestor-or-self::*` |\n| `descendant-or-self::` | Down | Descendants + current | `//div/descendant-or-self::*` |\n| `self::` | Current | Current node only | `//div/self::div` |\n| `attribute::` | Attribute | Attributes of current | `//div/attribute::class` |\n\n!!! info \"Shorthand Syntax\"\n    - `//div` is short for `//descendant-or-self::div`\n    - `//div/p` is short for `//div/child::p`\n    - `@id` is short for `attribute::id`\n    - `..` is short for `parent::node()`\n\n### Practical Axis Examples\n\n```python\n# Navigate to parent\nawait tab.query(\"//input[@name='email']/parent::div\")\nawait tab.query(\"//span[@class='error']/..\")       # Shorthand\n\n# Find ancestor\nawait tab.query(\"//input/ancestor::form\")          # First ancestor <form>\nawait tab.query(\"//button/ancestor::div[@class='modal']\")\n\n# Sibling navigation\nawait tab.query(\"//label[text()='Email:']/following-sibling::input\")\nawait tab.query(\"//h2/following-sibling::p[1]\")    # First <p> after <h2>\nawait tab.query(\"//h2/following-sibling::p\", find_all=True)  # All <p> after <h2>\nawait tab.query(\"//button/preceding-sibling::input[last()]\")\n\n# Complex relationships\nawait tab.query(\"//tr/td[1]/following-sibling::td[2]\")  # 3rd cell in first row\nawait tab.query(\"//tr/td[1]/following-sibling::td[2]\", find_all=True)  # 3rd cell in all rows\n```\n\n## XPath Functions\n\n### Text Functions\n\n```python\n# Exact text match\nawait tab.query(\"//button[text()='Submit']\")\n\n# Contains text\nawait tab.query(\"//p[contains(text(), 'welcome')]\")\n\n# Starts with\nawait tab.query(\"//a[starts-with(@href, 'https://')]\")\n\n# Text normalization (removes extra whitespace)\nawait tab.query(\"//button[normalize-space(text())='Submit']\")\n\n# String length\nawait tab.query(\"//input[string-length(@value) > 5]\")\n\n# Concatenation\nawait tab.query(\"//div[concat(@data-first, @data-last)='JohnDoe']\")\n```\n\n### Numeric Functions\n\n```python\n# Position matching\nawait tab.query(\"//li[position()=1]\")              # First <li>\nawait tab.query(\"//li[position() > 3]\", find_all=True)  # All <li> after 3rd\nawait tab.query(\"//li[last()]\")                    # Last <li>\nawait tab.query(\"//li[last()-1]\")                  # Second to last\n\n# Counting\nawait tab.query(\"//ul[count(li) > 5]\")             # First <ul> with more than 5 items\nawait tab.query(\"//ul[count(li) > 5]\", find_all=True)  # All <ul> with > 5 items\n\n# Numeric operations\nawait tab.query(\"//div[@data-price > 100]\")        # First div with price > 100\nawait tab.query(\"//div[@data-price > 100]\", find_all=True)  # All divs\nawait tab.query(\"//div[number(@data-stock) = 0]\")  # First with stock = 0\n```\n\n### Boolean Functions\n\n```python\n# Boolean logic\nawait tab.query(\"//div[@visible='true' and @enabled='true']\")  # First match\nawait tab.query(\"//input[@type='text' or @type='email']\")  # First text or email\nawait tab.query(\"//input[@type='text' or @type='email']\", find_all=True)  # All\nawait tab.query(\"//button[not(@disabled)]\")        # First enabled button\nawait tab.query(\"//button[not(@disabled)]\", find_all=True)  # All enabled buttons\n\n# Existence checks\nawait tab.query(\"//div[child::p]\")                 # First div with <p> children\nawait tab.query(\"//div[child::p]\", find_all=True)  # All divs with <p> children\nawait tab.query(\"//div[not(child::*)]\")            # First empty div\nawait tab.query(\"//div[not(child::*)]\", find_all=True)  # All empty divs\n```\n\n## XPath Predicates\n\nPredicates filter node sets using conditions in square brackets `[]`.\n\n```python\n# Position predicates\nawait tab.query(\"(//div)[1]\")                      # First <div> in document\nawait tab.query(\"(//div)[last()]\")                 # Last <div> in document\nawait tab.query(\"//ul/li[3]\")                      # First 3rd <li> in a <ul>\nawait tab.query(\"//ul/li[3]\", find_all=True)       # All 3rd <li> in each <ul>\n\n# Multiple predicates (AND logic)\nawait tab.query(\"//input[@type='text'][@required]\")  # First match\nawait tab.query(\"//div[@class='product'][position() < 4]\", find_all=True)  # First 3\n\n# Attribute predicates\nawait tab.query(\"//div[@data-id='123']\")\nawait tab.query(\"//a[contains(@class, 'button')]\")  # First matching link\nawait tab.query(\"//input[starts-with(@name, 'user')]\")  # First matching input\n```\n\n## Real-World Examples: Complex Element Finding\n\nLet's work with a realistic HTML structure to demonstrate advanced selectors.\n\n### Sample HTML Structure\n\n```html\n<div class=\"dashboard\">\n    <header>\n        <h1>User Dashboard</h1>\n        <nav class=\"menu\">\n            <a href=\"/home\" class=\"active\">Home</a>\n            <a href=\"/profile\">Profile</a>\n            <a href=\"/settings\">Settings</a>\n        </nav>\n    </header>\n    \n    <main>\n        <section class=\"products\">\n            <h2>Available Products</h2>\n            <table id=\"products-table\">\n                <thead>\n                    <tr>\n                        <th>Product Name</th>\n                        <th>Price</th>\n                        <th>Stock</th>\n                        <th>Actions</th>\n                    </tr>\n                </thead>\n                <tbody>\n                    <tr data-product-id=\"101\">\n                        <td>Laptop</td>\n                        <td class=\"price\">$999</td>\n                        <td class=\"stock\">15</td>\n                        <td>\n                            <button class=\"btn-edit\">Edit</button>\n                            <button class=\"btn-delete\">Delete</button>\n                        </td>\n                    </tr>\n                    <tr data-product-id=\"102\">\n                        <td>Mouse</td>\n                        <td class=\"price\">$25</td>\n                        <td class=\"stock\">0</td>\n                        <td>\n                            <button class=\"btn-edit\">Edit</button>\n                            <button class=\"btn-delete\" disabled>Delete</button>\n                        </td>\n                    </tr>\n                    <tr data-product-id=\"103\">\n                        <td>Keyboard</td>\n                        <td class=\"price\">$75</td>\n                        <td class=\"stock\">8</td>\n                        <td>\n                            <button class=\"btn-edit\">Edit</button>\n                            <button class=\"btn-delete\">Delete</button>\n                        </td>\n                    </tr>\n                </tbody>\n            </table>\n        </section>\n        \n        <section class=\"user-form\">\n            <h2>User Information</h2>\n            <form id=\"user-form\">\n                <div class=\"form-group\">\n                    <label for=\"username\">Username:</label>\n                    <input type=\"text\" id=\"username\" name=\"username\" required>\n                    <span class=\"error-message\" style=\"display:none;\">Invalid username</span>\n                </div>\n                <div class=\"form-group\">\n                    <label for=\"email\">Email:</label>\n                    <input type=\"email\" id=\"email\" name=\"email\" required>\n                    <span class=\"error-message\" style=\"display:none;\">Invalid email</span>\n                </div>\n                <div class=\"form-group\">\n                    <input type=\"checkbox\" id=\"newsletter\" name=\"newsletter\">\n                    <label for=\"newsletter\">Subscribe to newsletter</label>\n                </div>\n                <button type=\"submit\" class=\"btn-primary\">Save Changes</button>\n                <button type=\"button\" class=\"btn-secondary\">Cancel</button>\n            </form>\n        </section>\n    </main>\n</div>\n```\n\n### Challenge 1: Find Active Navigation Link\n\n**Goal**: Find the currently active navigation link.\n\n```python\n# CSS Selector\nactive_link = await tab.query(\"nav.menu a.active\")\n\n# XPath\nactive_link = await tab.query(\"//nav[@class='menu']//a[@class='active']\")\n\n# Get its text\ntext = await active_link.text\nprint(text)  # \"Home\"\n```\n\n### Challenge 2: Find Edit Button for Specific Product\n\n**Goal**: Find the Edit button for the product \"Mouse\" (without knowing its row position).\n\n```python\n# XPath (recommended for this case)\nedit_button = await tab.query(\n    \"//tr[td[text()='Mouse']]//button[contains(@class, 'btn-edit')]\"\n)\n\n# Alternative: Using following-sibling\nedit_button = await tab.query(\n    \"//td[text()='Mouse']/following-sibling::td//button[@class='btn-edit']\"\n)\n```\n\n!!! tip \"Why XPath Here?\"\n    CSS selectors can't traverse upward to find the row and then back down to the button. XPath's ability to move freely through the DOM makes this trivial.\n\n### Challenge 3: Find All Products with Price Over $50\n\n**Goal**: Get all table rows where the price is greater than $50.\n\n```python\n# XPath with numeric comparison\nexpensive_products = await tab.query(\n    \"//tr[number(translate(td[@class='price'], '$,', '')) > 50]\",\n    find_all=True\n)\n\n# More readable version: using contains for simpler cases\n# This finds products with price containing specific amounts\nproducts = await tab.query(\"//tr[contains(td[@class='price'], '$75')]\", find_all=True)\n```\n\n!!! note \"Text to Number Conversion\"\n    The `translate()` function removes `$` and `,` characters, then `number()` converts to numeric for comparison.\n\n### Challenge 4: Find All Out-of-Stock Products\n\n**Goal**: Find all products where stock is 0.\n\n```python\n# XPath\nout_of_stock = await tab.query(\n    \"//tr[td[@class='stock' and text()='0']]\",\n    find_all=True\n)\n\n# Alternative: Find all rows and check stock\nrows = await tab.query(\"//tbody/tr[td[@class='stock']/text()='0']\", find_all=True)\n```\n\n### Challenge 5: Find Input Field by Its Label\n\n**Goal**: Find the email input by locating its label first.\n\n```python\n# XPath using label's 'for' attribute\nemail_input = await tab.query(\"//label[text()='Email:']/following-sibling::input\")\n\n# Alternative: Using the for attribute\nemail_input = await tab.query(\"//input[@id=(//label[text()='Email:']/@for)]\")\n\n# More generic: Find by label text\nusername_input = await tab.query(\n    \"//label[contains(text(), 'Username')]/following-sibling::input\"\n)\n```\n\n### Challenge 6: Find Error Message Next to Email Field\n\n**Goal**: Get the error message span that appears next to the email input.\n\n```python\n# XPath - find error sibling of email input\nerror_span = await tab.query(\n    \"//input[@id='email']/following-sibling::span[@class='error-message']\"\n)\n\n# Alternative: Navigate from parent div\nerror_span = await tab.query(\n    \"//input[@id='email']/parent::div//span[@class='error-message']\"\n)\n\n# Check visibility\nis_visible = await error_span.is_visible()\n```\n\n### Challenge 7: Find Submit Button (Not Cancel)\n\n**Goal**: Find the submit button, excluding the cancel button.\n\n```python\n# CSS Selector (simple)\nsubmit_button = await tab.query(\"button[type='submit']\")\nsubmit_button = await tab.query(\"button.btn-primary\")\n\n# XPath with text\nsubmit_button = await tab.query(\"//button[text()='Save Changes']\")\n\n# XPath excluding others\nsubmit_button = await tab.query(\n    \"//button[@type='submit' and not(@class='btn-secondary')]\"\n)\n```\n\n### Challenge 8: Find All Required Form Fields\n\n**Goal**: Get all required input fields in the form.\n\n```python\n# CSS Selector (clean)\nrequired_fields = await tab.query(\n    \"#user-form input[required]\",\n    find_all=True\n)\n\n# XPath\nrequired_fields = await tab.query(\n    \"//form[@id='user-form']//input[@required]\",\n    find_all=True\n)\n\n# Verify\nfor field in required_fields:\n    field_name = await field.get_attribute(\"name\")\n    print(f\"Required: {field_name}\")\n```\n\n### Challenge 9: Find First Non-Disabled Delete Button\n\n**Goal**: Find the first delete button that is not disabled.\n\n```python\n# CSS Selector\nfirst_enabled_delete = await tab.query(\"button.btn-delete:not([disabled])\")\n\n# XPath\nfirst_enabled_delete = await tab.query(\n    \"//button[contains(@class, 'btn-delete') and not(@disabled)]\"\n)\n\n# Get all enabled delete buttons\nall_enabled = await tab.query(\n    \"//button[@class='btn-delete' and not(@disabled)]\",\n    find_all=True\n)\n```\n\n### Challenge 10: Find Table Row by Multiple Conditions\n\n**Goal**: Find products with stock > 0 AND price < $100.\n\n```python\n# XPath with complex logic\navailable_affordable = await tab.query(\n    \"\"\"\n    //tr[\n        number(td[@class='stock']) > 0 \n        and \n        number(translate(td[@class='price'], '$', '')) < 100\n    ]\n    \"\"\",\n    find_all=True\n)\n\n# For each matching product\nfor row in available_affordable:\n    cells = await row.query(\"td\", find_all=True)\n    product_name = await cells[0].text\n    print(f\"Available: {product_name}\")\n```\n\n### Challenge 11: Navigate Complex Relationships\n\n**Goal**: From a delete button, get the product name in the same row.\n\n```python\n# Start with a delete button\ndelete_button = await tab.query(\"//tr[@data-product-id='101']//button[@class='btn-delete']\")\n\n# Navigate to parent row, then to first cell\nproduct_name_cell = await delete_button.query(\"./ancestor::tr/td[1]\")\nproduct_name = await product_name_cell.text\nprint(product_name)  # \"Laptop\"\n\n# Alternative: Get the entire row first\nrow = await delete_button.query(\"./ancestor::tr\")\nproduct_id = await row.get_attribute(\"data-product-id\")\nprint(product_id)  # \"101\"\n```\n\n### Challenge 12: Find Checkbox and Its Label Together\n\n**Goal**: Find the newsletter checkbox and verify its label.\n\n```python\n# Find checkbox\ncheckbox = await tab.query(\"#newsletter\")\n\n# Get associated label using 'for' attribute\nlabel = await tab.query(\"//label[@for='newsletter']\")\nlabel_text = await label.text\nprint(label_text)  # \"Subscribe to newsletter\"\n\n# Alternative: Navigate from checkbox to label\nlabel = await checkbox.query(\"//following::label[@for='newsletter']\")\n\n# Check if checked\nis_checked = await checkbox.is_checked()\n```\n\n## Advanced Pattern: Dynamic Selector Building\n\nWhen dealing with dynamic content, you might need to build selectors programmatically:\n\n```python\nasync def find_product_by_name(tab, product_name: str):\n    \"\"\"Find a product row by its name dynamically.\"\"\"\n    # Escape quotes in product name to prevent XPath injection\n    safe_name = product_name.replace(\"'\", \"\\\\'\")\n    \n    xpath = f\"//tr[td[text()='{safe_name}']]\"\n    return await tab.query(xpath)\n\nasync def find_table_cell(tab, row_text: str, column_index: int):\n    \"\"\"Find a specific cell by row content and column position.\"\"\"\n    xpath = f\"//tr[td[contains(text(), '{row_text}')]]/td[{column_index}]\"\n    return await tab.query(xpath)\n\n# Usage\nproduct_row = await find_product_by_name(tab, \"Laptop\")\nprice_cell = await find_table_cell(tab, \"Laptop\", 2)\nprice = await price_cell.text\nprint(price)  # \"$999\"\n```\n\n## Performance Comparison\n\n```python\nimport asyncio\nimport time\n\nasync def benchmark_selectors(tab):\n    \"\"\"Compare CSS vs XPath performance.\"\"\"\n    \n    # Warm up\n    await tab.query(\"#products-table\")\n    \n    # Benchmark CSS\n    start = time.time()\n    for _ in range(100):\n        await tab.query(\"#products-table tbody tr\", find_all=True)\n    css_time = time.time() - start\n    \n    # Benchmark XPath\n    start = time.time()\n    for _ in range(100):\n        await tab.query(\"//table[@id='products-table']//tbody//tr\", find_all=True)\n    xpath_time = time.time() - start\n    \n    print(f\"CSS: {css_time:.3f}s\")\n    print(f\"XPath: {xpath_time:.3f}s\")\n    print(f\"CSS is {xpath_time/css_time:.2f}x faster\")\n\n# Typical results: CSS is 1.2-1.5x faster for simple selectors\n```\n\n!!! warning \"Performance vs Readability\"\n    While CSS selectors are generally faster, the difference is usually negligible (milliseconds) for individual queries. Choose the selector that makes your code more readable and maintainable, especially for complex relationships where XPath excels.\n\n## Selector Best Practices\n\n### 1. Prefer Stable Selectors\n\n```python\n# Good: Using semantic attributes\nawait tab.query(\"#user-email\")\nawait tab.query(\"[data-testid='submit-button']\")\nawait tab.query(\"input[name='username']\")\n\n# Avoid: Brittle selectors based on structure\nawait tab.query(\"div > div > div:nth-child(3) > input\")\nawait tab.query(\"body > div:nth-child(2) > form > div:first-child\")\n```\n\n### 2. Use the Simplest Selector That Works\n\n```python\n# Good: Simple and efficient\nawait tab.query(\"#login-form\")\nawait tab.query(\".submit-button\")\n\n# Avoid: Over-complicated when unnecessary\nawait tab.query(\"//div[@id='content']/descendant::form[@id='login-form']\")\n```\n\n### 3. Combine find() and query() Appropriately\n\n```python\n# Use find() for simple attribute matching\nusername = await tab.find(id=\"username\")\nsubmit = await tab.find(tag_name=\"button\", type=\"submit\")\n\n# Use query() for complex patterns\nactive_link = await tab.query(\"nav.menu a.active\")\nerror_msg = await tab.query(\"//input[@name='email']/following-sibling::span[@class='error']\")\n```\n\n### 4. Add Comments for Complex Selectors\n\n```python\n# Find the \"Edit\" button in the row containing product \"Laptop\"\n# XPath: Navigate to row with \"Laptop\" text, then find edit button\nedit_button = await tab.query(\n    \"//tr[td[text()='Laptop']]//button[@class='btn-edit']\"\n)\n```\n\n## Conclusion\n\nBy understanding both CSS selectors and XPath, along with their respective strengths and use cases, you can create robust and maintainable browser automation that handles the complexities of modern web applications. Remember:\n\n- **Use CSS selectors** for simple, performance-critical selections\n- **Use XPath** for complex relationships, text matching, and upward navigation\n- **Choose stability** over brevity when writing selectors\n- **Comment complex queries** to maintain code readability\n\nFor more information about how these selectors are used internally by Pydoll, see the [FindElements Mixin](find-elements-mixin.md) documentation.\n\n"
  },
  {
    "path": "docs/en/deep-dive/index.md",
    "content": "# Deep Dive: Technical Foundation\n\n**Welcome to the technical heart of Pydoll, where we explore the systems and protocols that power browser automation.**\n\nThis section provides comprehensive technical education on web scraping, browser automation, network protocols, and anti-detection techniques. Rather than focusing solely on usage patterns, we explore the underlying mechanisms, from the first TCP packet to the final rendered pixel.\n\n## What Makes This Different\n\nMost automation documentation teaches you **how to use a tool**. This section teaches you **how the internet actually works**, and how to manipulate it at every layer:\n\n- **Network protocols** (TCP/IP, TLS, HTTP/2) - The invisible foundation of every request\n- **Browser internals** (CDP, rendering engines, JavaScript contexts) - What happens inside Chrome\n- **Detection systems** (fingerprinting, behavioral analysis, proxy detection) - How websites identify bots\n- **Evasion techniques** (CDP overrides, consistency enforcement, human mimicry) - How to become undetectable\n\n!!! quote \"Philosophy\"\n    **\"Any sufficiently advanced technology is indistinguishable from magic.\"**\n    \n    This section aims to demystify browser automation by explaining the underlying systems. Understanding these fundamentals provides better control and predictability in your automation work.\n\n## The Architecture of Knowledge\n\nThis section is organized into **five progressive layers**, each building on the previous:\n\n### Core Fundamentals\n**[→ Explore Fundamentals](./fundamentals/cdp.md)**\n\nStart at the foundation: understand the protocols and systems that power Pydoll.\n\n- **[Chrome DevTools Protocol](./fundamentals/cdp.md)** - How Pydoll talks to browsers, bypassing WebDriver\n- **[Connection Layer](./fundamentals/connection-layer.md)** - WebSocket architecture, async patterns, real-time CDP\n- **[Python Type System](./fundamentals/typing-system.md)** - Type safety, TypedDict for CDP, IDE integration\n\n**Why start here**: Understanding CDP and async communication provides the foundation for comprehending all other aspects of browser automation.\n\n---\n\n### Internal Architecture\n**[→ Explore Architecture](./architecture/browser-domain.md)**\n\nClimb to the next level: understand how Pydoll's internal components work together.\n\n- **[Browser Domain](./architecture/browser-domain.md)** - Process management, contexts, multi-profile automation\n- **[Tab Domain](./architecture/tab-domain.md)** - Tab lifecycle, concurrent operations, iframe handling\n- **[WebElement Domain](./architecture/webelement-domain.md)** - Element interactions, shadow DOM, attribute handling\n- **[FindElements Mixin](./architecture/find-elements-mixin.md)** - Selector strategies, DOM traversal, optimization\n- **[Event Architecture](./architecture/event-architecture.md)** - Reactive event system, callbacks, async dispatch\n- **[Browser Requests Architecture](./architecture/browser-requests-architecture.md)** - HTTP in browser context\n\n**Why this matters**: Understanding internal architecture reveals optimization opportunities and design patterns that aren't apparent from surface-level usage.\n\n---\n\n### Network & Security\n**[→ Explore Network & Security](./network/index.md)**\n\nDrop down to the protocol layer: understand how data flows across the internet.\n\n- **[Network Fundamentals](./network/network-fundamentals.md)** - OSI model, TCP/UDP, WebRTC leakage\n- **[HTTP/HTTPS Proxies](./network/http-proxies.md)** - Application-layer proxying, CONNECT tunneling\n- **[SOCKS Proxies](./network/socks-proxies.md)** - Session-layer proxying, UDP support, security\n- **[Proxy Detection](./network/proxy-detection.md)** - Anonymity levels, detection techniques, evasion\n- **[Building Proxy Servers](./network/build-proxy.md)** - Full HTTP & SOCKS5 implementations\n- **[Legal & Ethical](./network/proxy-legal.md)** - GDPR, CFAA, compliance, responsible usage\n\n**Critical insight**: Network characteristics are determined at the OS level. Mismatches between claimed browser identity and network-level fingerprints can be detected by sophisticated anti-bot systems.\n\n---\n\n### Fingerprinting\n**[→ Explore Fingerprinting](./fingerprinting/index.md)**\n\nUnderstanding detection systems and evasion techniques for browser automation.\n\n- **[Network Fingerprinting](./fingerprinting/network-fingerprinting.md)** - TCP/IP, TLS/JA3, p0f, Nmap, Scapy\n- **[Browser Fingerprinting](./fingerprinting/browser-fingerprinting.md)** - HTTP/2, Canvas, WebGL, JavaScript APIs\n- **[Evasion Techniques](./fingerprinting/evasion-techniques.md)** - CDP overrides, consistency, practical code\n\n**Key insight**: Every connection reveals numerous characteristics (canvas rendering, TCP window size, TLS cipher order). Effective stealth requires consistency across all detection layers.\n\n---\n\n### Practical Guides\n**[→ Explore Guides](./guides/selectors-guide.md)**\n\nApply your knowledge: practical guides for common automation challenges.\n\n- **[CSS Selectors vs XPath](./guides/selectors-guide.md)** - Selector syntax, performance, best practices\n\n**Coming soon**: More practical guides synthesizing the technical knowledge into actionable patterns.\n\n---\n\n## Learning Paths\n\nDifferent goals require different knowledge. Choose your path:\n\n### Path 1: Stealth Automation\n**Goal: Build undetectable scrapers**\n\n1. **[Fingerprinting Overview](./fingerprinting/index.md)** - Understand the detection landscape\n2. **[Network Fingerprinting](./fingerprinting/network-fingerprinting.md)** - TCP/IP, TLS signatures\n3. **[Browser Fingerprinting](./fingerprinting/browser-fingerprinting.md)** - Canvas, WebGL, HTTP/2\n4. **[Evasion Techniques](./fingerprinting/evasion-techniques.md)** - CDP-based countermeasures\n5. **[Network & Security](./network/index.md)** - Proxy selection and configuration\n6. **[Browser Domain](./architecture/browser-domain.md)** - Context isolation, process management\n\n**Time investment**: 12-16 hours of deep technical learning  \n**Payoff**: Ability to bypass sophisticated anti-bot systems\n\n---\n\n### Path 2: Architecture Mastery\n**Goal: Contribute to Pydoll or build similar tools**\n\n1. **[CDP Deep Dive](./fundamentals/cdp.md)** - Protocol fundamentals\n2. **[Connection Layer](./fundamentals/connection-layer.md)** - WebSocket async patterns\n3. **[Event Architecture](./architecture/event-architecture.md)** - Event-driven design\n4. **[Browser Domain](./architecture/browser-domain.md)** - Browser management\n5. **[Tab Domain](./architecture/tab-domain.md)** - Tab lifecycle\n6. **[WebElement Domain](./architecture/webelement-domain.md)** - Element interaction\n7. **[Python Type System](./fundamentals/typing-system.md)** - Type safety integration\n\n**Time investment**: 16-20 hours of architectural study  \n**Payoff**: Deep understanding of browser automation internals\n\n---\n\n### Path 3: Network Engineering\n**Goal: Master proxies, fingerprinting, and network-level stealth**\n\n1. **[Network Fundamentals](./network/network-fundamentals.md)** - OSI model, TCP/UDP, WebRTC\n2. **[Network Fingerprinting](./fingerprinting/network-fingerprinting.md)** - TCP/IP signatures, TLS/JA3\n3. **[HTTP/HTTPS Proxies](./network/http-proxies.md)** - Application-layer proxying\n4. **[SOCKS Proxies](./network/socks-proxies.md)** - Session-layer proxying\n5. **[Proxy Detection](./network/proxy-detection.md)** - Anonymity and evasion\n6. **[Building Proxy Servers](./network/build-proxy.md)** - Implementation from scratch\n\n**Time investment**: 14-18 hours of network protocol study  \n**Payoff**: Complete understanding of network-level anonymity and detection\n\n---\n\n## Prerequisites\n\nThis is advanced, technical material. Recommended prerequisites include:\n\n- **Python fundamentals** - Classes, async/await, context managers, decorators\n- **Basic networking** - IP addresses, ports, HTTP protocol\n- **Pydoll basics** - See [Features](../features/core-concepts.md) and [Getting Started](../index.md)\n- **Browser DevTools** - Chrome Inspector, Network tab, Console  \n\n**If you're new to these**, we recommend:\n1. Complete the [Features](../features/index.md) section first\n2. Practice basic automation with Pydoll\n3. Return here when you need deeper understanding\n\n## The Philosophy of Mastery\n\nWeb automation involves multiple areas of expertise:\n\n- **Protocol engineering** - Understanding TCP/IP, TLS, HTTP/2\n- **Systems programming** - Managing processes, async I/O, WebSockets\n- **Security research** - Fingerprinting, detection, evasion\n- **Browser internals** - Rendering, JavaScript contexts, CDP\n- **Operational security** - Legal compliance, ethical guidelines\n\nMost developers learn these independently, over time. This section consolidates that knowledge by:\n\n1. **Centralizing knowledge** - No more scattered blog posts and academic papers\n2. **Providing context** - Every technique explained from first principles\n3. **Offering working code** - All examples are production-ready\n4. **Citing sources** - Every claim backed by RFCs, documentation, or research\n5. **Progressive complexity** - Each section builds on previous knowledge\n\n## Documentation Standards\n\nThis documentation represents extensive research, testing, and validation:\n\n- Every protocol detail verified against RFCs\n- Every fingerprinting technique tested in production\n- Every code example runs without modification\n- Every claim cited with authoritative sources\n- Every diagram generated from real system behavior\n\nTechnical accuracy and practical applicability are prioritized throughout.\n\n## Ethical Use\n\nWith this knowledge comes responsibility:\n\n!!! danger \"Use Responsibly\"\n    The techniques described here can serve legitimate automation or malicious purposes. Responsible use includes:\n    \n    - Respecting website terms of service and robots.txt\n    - Implementing rate limiting and respectful crawling\n    - Considering whether automation is truly necessary\n    - Consulting legal counsel when uncertain\n    - Being transparent about your automation when appropriate\n    \n    Avoid using this knowledge for:\n    - Fraud, account abuse, or illegal activities\n    - Overwhelming servers with aggressive scraping\n    - Harmful activities without understanding consequences  \n\nFor detailed guidance, see **[Legal & Ethical Considerations](./network/proxy-legal.md)**.\n\n## Contributing\n\nFound an error? Have a suggestion? See something outdated?\n\nThis documentation is a **living project**. Fingerprinting techniques evolve, protocols update, and new evasion methods emerge. We welcome contributions that:\n\n- Correct technical inaccuracies\n- Add new fingerprinting techniques\n- Update protocol information\n- Improve code examples\n- Expand coverage of detection systems\n\nSee [Contributing](../CONTRIBUTING.md) for guidelines.\n\n---\n\n## Getting Started\n\nChoose a path based on your goals:\n\n**New to deep technical content?**  \n→ Start with **[Chrome DevTools Protocol](./fundamentals/cdp.md)** to understand Pydoll's foundation\n\n**Need stealth automation?**  \n→ Jump to **[Fingerprinting](./fingerprinting/index.md)** for detection and evasion techniques\n\n**Want network-level control?**  \n→ Explore **[Network & Security](./network/index.md)** for proxy architecture and protocols\n\n**Building automation infrastructure?**  \n→ Study **[Internal Architecture](./architecture/browser-domain.md)** for design patterns\n\n**Just want to browse?**  \n→ Pick any topic from the sidebar, each article is self-contained\n\n---\n\n!!! success \"Technical Deep Dive\"\n    This section provides comprehensive technical knowledge for browser automation, from fundamental protocols to advanced evasion techniques.\n    \n    Explore at your own pace.\n"
  },
  {
    "path": "docs/en/deep-dive/network/build-proxy.md",
    "content": "# Building Proxy Servers\n\nThis document implements HTTP and SOCKS5 proxy servers from scratch in Python using asyncio. The goal is not production readiness but protocol comprehension: seeing how each byte is parsed, where security boundaries lie, and why certain design decisions exist in real proxy software.\n\n!!! info \"Module Navigation\"\n    - [Network Fundamentals](./network-fundamentals.md): TCP/IP, UDP, WebRTC\n    - [HTTP/HTTPS Proxies](./http-proxies.md): Application-layer proxying\n    - [SOCKS Proxies](./socks-proxies.md): Session-layer proxying\n    - [Proxy Detection](./proxy-detection.md): Detection techniques and evasion\n\n    For practical proxy usage in Pydoll, see [Proxy Configuration](../../features/configuration/proxy.md).\n\n!!! warning \"Educational Code\"\n    These implementations prioritize clarity over robustness. They lack connection limits, access control lists, and many error recovery paths that a production proxy requires. Do not expose them to untrusted networks.\n\n## HTTP Proxy\n\nAn HTTP proxy operates in two modes. For plaintext HTTP, it receives the full request (with an absolute-form URL like `GET http://example.com/path HTTP/1.1`), rewrites the request-target to origin-form (`GET /path HTTP/1.1`), connects to the target server, forwards the request, and pipes the response back. For HTTPS, the client sends a `CONNECT host:port` request, the proxy opens a TCP connection to the target, responds with `200 Connection Established`, and then blindly relays bytes in both directions without inspecting the encrypted content.\n\nThe implementation below handles both modes. A few things to note as you read through it. The `_pipe_data` method calls `write_eof()` when one side closes, which sends a TCP FIN to the other side. Without this, the tunnel hangs indefinitely because the other `read()` never returns empty bytes. The HTTP forwarding path uses the same piping approach rather than a single `read()` call, because HTTP responses can be arbitrarily large and a fixed-size read would silently truncate them. The request-target rewrite preserves query strings, which `urlparse().path` alone would drop.\n\n```python\nimport asyncio\nimport base64\nimport contextlib\nimport logging\nfrom urllib.parse import urlparse\n\nlogger = logging.getLogger(__name__)\n\n\nclass HTTPProxy:\n    \"\"\"Async HTTP/HTTPS proxy with optional Basic authentication.\"\"\"\n\n    def __init__(self, host='0.0.0.0', port=8080, username=None, password=None):\n        self.host = host\n        self.port = port\n        self.username = username\n        self.password = password\n\n    async def start(self):\n        server = await asyncio.start_server(\n            self._handle_client, self.host, self.port\n        )\n        logger.info(f'HTTP proxy listening on {self.host}:{self.port}')\n        async with server:\n            await server.serve_forever()\n\n    async def _handle_client(self, reader, writer):\n        try:\n            request_line = await asyncio.wait_for(\n                reader.readline(), timeout=30\n            )\n            if not request_line:\n                return\n\n            parts = request_line.decode('latin-1').split()\n            if len(parts) != 3:\n                writer.write(b'HTTP/1.1 400 Bad Request\\r\\n\\r\\n')\n                await writer.drain()\n                return\n\n            method, url, _ = parts\n            headers = await self._read_headers(reader)\n\n            if not self._check_auth(headers):\n                writer.write(\n                    b'HTTP/1.1 407 Proxy Authentication Required\\r\\n'\n                    b'Proxy-Authenticate: Basic realm=\"Proxy\"\\r\\n'\n                    b'Content-Length: 0\\r\\n\\r\\n'\n                )\n                await writer.drain()\n                return\n\n            if method == 'CONNECT':\n                await self._handle_connect(url, reader, writer)\n            else:\n                await self._handle_http(method, url, headers, reader, writer)\n        except Exception as e:\n            logger.error(f'Client handler error: {e}')\n        finally:\n            writer.close()\n            await writer.wait_closed()\n\n    async def _read_headers(self, reader):\n        headers = {}\n        while True:\n            line = await reader.readline()\n            if line in (b'\\r\\n', b'\\n', b''):\n                break\n            if b':' in line:\n                key, value = line.decode('latin-1').split(':', 1)\n                headers[key.strip().lower()] = value.strip()\n        return headers\n\n    def _check_auth(self, headers):\n        if not self.username:\n            return True\n        auth = headers.get('proxy-authorization', '')\n        if not auth.startswith('Basic '):\n            return False\n        try:\n            decoded = base64.b64decode(auth[6:]).decode('utf-8')\n            if ':' not in decoded:\n                return False\n            user, pwd = decoded.split(':', 1)\n            return user == self.username and pwd == self.password\n        except Exception:\n            return False\n\n    async def _handle_connect(self, target, client_reader, client_writer):\n        \"\"\"Establish a blind TCP tunnel for HTTPS.\"\"\"\n        # Parse host:port, handling IPv6 literals like [::1]:443\n        if target.startswith('['):\n            bracket_end = target.index(']')\n            host = target[1:bracket_end]\n            port = int(target[bracket_end + 2:])\n        elif ':' in target:\n            host, port_str = target.rsplit(':', 1)\n            port = int(port_str)\n        else:\n            client_writer.write(b'HTTP/1.1 400 Bad Request\\r\\n\\r\\n')\n            await client_writer.drain()\n            return\n\n        try:\n            server_reader, server_writer = await asyncio.open_connection(\n                host, port\n            )\n        except OSError as e:\n            logger.error(f'CONNECT failed to {host}:{port}: {e}')\n            client_writer.write(b'HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n')\n            await client_writer.drain()\n            return\n\n        client_writer.write(b'HTTP/1.1 200 Connection Established\\r\\n\\r\\n')\n        await client_writer.drain()\n\n        await asyncio.gather(\n            self._pipe(client_reader, server_writer),\n            self._pipe(server_reader, client_writer),\n        )\n\n    async def _handle_http(self, method, url, headers, client_reader, client_writer):\n        \"\"\"Forward a plaintext HTTP request.\"\"\"\n        parsed = urlparse(url)\n        host = parsed.hostname\n        port = parsed.port or 80\n\n        # Preserve query string in the request-target\n        path = parsed.path or '/'\n        if parsed.query:\n            path += f'?{parsed.query}'\n\n        try:\n            server_reader, server_writer = await asyncio.open_connection(\n                host, port\n            )\n        except OSError as e:\n            logger.error(f'HTTP forward failed to {host}:{port}: {e}')\n            client_writer.write(b'HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n')\n            await client_writer.drain()\n            return\n\n        # Rewrite request-target from absolute-form to origin-form\n        request = f'{method} {path} HTTP/1.1\\r\\n'\n\n        # Host header must include the port if it is non-standard\n        if port != 80:\n            request += f'Host: {host}:{port}\\r\\n'\n        else:\n            request += f'Host: {host}\\r\\n'\n\n        # Remove hop-by-hop headers that must not be forwarded\n        hop_by_hop = {\n            'proxy-authorization', 'proxy-connection',\n            'connection', 'keep-alive', 'te', 'trailer', 'upgrade',\n        }\n        for key, value in headers.items():\n            if key not in hop_by_hop:\n                request += f'{key}: {value}\\r\\n'\n\n        # Force Connection: close so the server does not keep-alive,\n        # which would prevent the response stream from ending\n        request += 'Connection: close\\r\\n\\r\\n'\n\n        server_writer.write(request.encode('latin-1'))\n\n        # Forward request body if present\n        content_length = int(headers.get('content-length', 0))\n        if content_length > 0:\n            body = await client_reader.readexactly(content_length)\n            server_writer.write(body)\n\n        await server_writer.drain()\n\n        # Pipe the entire response back (not a single fixed-size read)\n        while True:\n            chunk = await server_reader.read(65536)\n            if not chunk:\n                break\n            client_writer.write(chunk)\n            await client_writer.drain()\n\n        server_writer.close()\n        await server_writer.wait_closed()\n\n    async def _pipe(self, reader, writer):\n        \"\"\"Bidirectional data relay with proper half-close.\"\"\"\n        try:\n            while True:\n                data = await reader.read(8192)\n                if not data:\n                    break\n                writer.write(data)\n                await writer.drain()\n        except (ConnectionResetError, BrokenPipeError, OSError):\n            pass\n        finally:\n            with contextlib.suppress(Exception):\n                if writer.can_write_eof():\n                    writer.write_eof()\n```\n\nA few protocol details worth understanding. HTTP headers are encoded as ISO-8859-1 (Latin-1), not UTF-8. Latin-1 maps every byte value 0-255 to a character, so `decode('latin-1')` never raises a `UnicodeDecodeError`, while `decode('utf-8')` would crash on certain header values. The `Proxy-Authorization` header uses Base64 encoding, but Base64 is not encryption: the credentials travel in cleartext (or rather, trivially reversible encoding) unless the connection between client and proxy is itself protected by TLS. The hop-by-hop headers (`Connection`, `Keep-Alive`, `TE`, `Trailer`, `Upgrade`, `Proxy-Connection`) are meant for the immediate connection between two nodes, not for end-to-end forwarding. RFC 9110 Section 7.6.1 requires proxies to strip them before forwarding.\n\n!!! warning \"SSRF Risk\"\n    This implementation does not validate destination addresses. A client could request `CONNECT 127.0.0.1:6379` to reach a local Redis instance, or `CONNECT 169.254.169.254:80` to access cloud instance metadata (AWS, GCP, Azure). Any proxy exposed to untrusted clients must validate destinations against a deny list of private and link-local ranges (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `::1`, `fc00::/7`).\n\n## SOCKS5 Proxy\n\nA SOCKS5 proxy operates at a lower level than HTTP. It uses a binary protocol defined in RFC 1928, consisting of three phases: method negotiation, optional authentication, and the connection request. The proxy does not parse HTTP at all. Once the tunnel is established, it relays raw bytes without understanding what protocol flows through it.\n\nThe binary nature of SOCKS5 means every read must receive exactly the expected number of bytes. TCP is a stream protocol and does not guarantee that `read(4)` returns 4 bytes: it may return 1, 2, or 3 bytes depending on network conditions. The implementation below uses `readexactly()` from asyncio, which buffers internally until the requested number of bytes arrives or the connection closes (raising `IncompleteReadError`).\n\n```python\nimport asyncio\nimport contextlib\nimport struct\nimport logging\n\nlogger = logging.getLogger(__name__)\n\n\nclass SOCKS5Proxy:\n    \"\"\"Async SOCKS5 proxy supporting CONNECT with optional auth (RFC 1928).\"\"\"\n\n    VERSION = 0x05\n\n    def __init__(self, host='0.0.0.0', port=1080, username=None, password=None):\n        self.host = host\n        self.port = port\n        self.username = username\n        self.password = password\n\n    async def start(self):\n        server = await asyncio.start_server(\n            self._handle_client, self.host, self.port\n        )\n        logger.info(f'SOCKS5 proxy listening on {self.host}:{self.port}')\n        async with server:\n            await server.serve_forever()\n\n    async def _handle_client(self, reader, writer):\n        try:\n            if not await self._negotiate_method(reader, writer):\n                return\n            if self.username and not await self._authenticate(reader, writer):\n                return\n            await self._handle_request(reader, writer)\n        except (asyncio.IncompleteReadError, ConnectionResetError):\n            pass\n        except Exception as e:\n            logger.error(f'SOCKS5 error: {e}')\n        finally:\n            writer.close()\n            await writer.wait_closed()\n\n    async def _negotiate_method(self, reader, writer):\n        \"\"\"Phase 1: client offers authentication methods, server picks one.\"\"\"\n        version = (await reader.readexactly(1))[0]\n        if version != self.VERSION:\n            return False\n\n        nmethods = (await reader.readexactly(1))[0]\n        methods = await reader.readexactly(nmethods)\n\n        if self.username:\n            if 0x02 not in methods:\n                writer.write(bytes([self.VERSION, 0xFF]))\n                await writer.drain()\n                return False\n            selected = 0x02\n        else:\n            selected = 0x00\n\n        writer.write(bytes([self.VERSION, selected]))\n        await writer.drain()\n        return True\n\n    async def _authenticate(self, reader, writer):\n        \"\"\"Phase 2: username/password sub-negotiation (RFC 1929).\"\"\"\n        auth_ver = (await reader.readexactly(1))[0]\n        if auth_ver != 0x01:\n            return False\n\n        ulen = (await reader.readexactly(1))[0]\n        username = (await reader.readexactly(ulen)).decode('utf-8')\n        plen = (await reader.readexactly(1))[0]\n        password = (await reader.readexactly(plen)).decode('utf-8')\n\n        ok = username == self.username and password == self.password\n        writer.write(bytes([0x01, 0x00 if ok else 0x01]))\n        await writer.drain()\n        return ok\n\n    async def _handle_request(self, reader, writer):\n        \"\"\"Phase 3: parse the CONNECT request and establish the tunnel.\"\"\"\n        header = await reader.readexactly(4)\n        version, command, _, atyp = header\n\n        # Parse destination address based on address type\n        if atyp == 0x01:  # IPv4\n            raw = await reader.readexactly(4)\n            address = '.'.join(str(b) for b in raw)\n        elif atyp == 0x03:  # Domain name\n            length = (await reader.readexactly(1))[0]\n            address = (await reader.readexactly(length)).decode('ascii')\n        elif atyp == 0x04:  # IPv6\n            raw = await reader.readexactly(16)\n            groups = [f'{raw[i]:02x}{raw[i+1]:02x}' for i in range(0, 16, 2)]\n            address = ':'.join(groups)\n        else:\n            await self._reply(writer, 0x08)\n            return\n\n        port = struct.unpack('!H', await reader.readexactly(2))[0]\n        logger.info(f'SOCKS5 CONNECT {address}:{port}')\n\n        if command != 0x01:  # Only CONNECT is implemented\n            await self._reply(writer, 0x07)\n            return\n\n        try:\n            server_reader, server_writer = await asyncio.open_connection(\n                address, port\n            )\n        except ConnectionRefusedError:\n            await self._reply(writer, 0x05)\n            return\n        except OSError:\n            await self._reply(writer, 0x04)\n            return\n\n        # BND.ADDR and BND.PORT should reflect the local socket address.\n        # Most clients ignore these for CONNECT, but filling them\n        # correctly satisfies RFC 1928.\n        local = server_writer.get_extra_info('sockname')\n        await self._reply(writer, 0x00, local[0], local[1])\n\n        await asyncio.gather(\n            self._pipe(reader, server_writer),\n            self._pipe(server_reader, writer),\n        )\n\n    async def _reply(self, writer, status, bind_addr='0.0.0.0', bind_port=0):\n        \"\"\"Send a SOCKS5 reply with the given status and bound address.\"\"\"\n        import socket\n        try:\n            packed_ip = socket.inet_aton(bind_addr)\n            atyp = 0x01\n        except OSError:\n            packed_ip = socket.inet_aton('0.0.0.0')\n            atyp = 0x01\n\n        writer.write(bytes([\n            self.VERSION, status, 0x00, atyp,\n            *packed_ip,\n            (bind_port >> 8) & 0xFF, bind_port & 0xFF,\n        ]))\n        await writer.drain()\n\n    async def _pipe(self, reader, writer):\n        try:\n            while True:\n                data = await reader.read(8192)\n                if not data:\n                    break\n                writer.write(data)\n                await writer.drain()\n        except (ConnectionResetError, BrokenPipeError, OSError):\n            pass\n        finally:\n            with contextlib.suppress(Exception):\n                if writer.can_write_eof():\n                    writer.write_eof()\n```\n\nWhen the address type is `0x03` (domain name), the proxy resolves DNS itself via `asyncio.open_connection()`. This is the defining privacy property of SOCKS5 proxying: the client sends the domain name rather than resolving it locally, which prevents DNS queries from leaking to the client's local network. This is the same behavior Chrome relies on when configured with `--proxy-server=socks5://...`, as discussed in [SOCKS Proxies](./socks-proxies.md).\n\nThe `_reply` method fills `BND.ADDR` and `BND.PORT` with the actual local socket address after a successful connection, as RFC 1928 requires. Many SOCKS5 implementations return `0.0.0.0:0` here because most clients ignore these fields for CONNECT commands, but filling them correctly costs nothing and avoids a protocol violation.\n\n## Running Both Proxies\n\n```python\nasync def main():\n    http_proxy = HTTPProxy(\n        port=8080, username='user', password='pass'\n    )\n    socks5_proxy = SOCKS5Proxy(\n        port=1080, username='user', password='pass'\n    )\n    await asyncio.gather(http_proxy.start(), socks5_proxy.start())\n\n# asyncio.run(main())\n```\n\nYou can test them with curl:\n\n```bash\n# HTTP proxy\ncurl -x http://user:pass@localhost:8080 http://httpbin.org/ip\n\n# HTTPS through HTTP proxy (CONNECT tunnel)\ncurl -x http://user:pass@localhost:8080 https://httpbin.org/ip\n\n# SOCKS5 proxy\ncurl --socks5 localhost:1080 --proxy-user user:pass https://httpbin.org/ip\n```\n\n## What the Code Does Not Handle\n\nThese implementations omit several things that production proxies handle. Understanding what is missing is as instructive as understanding what is present.\n\nThere are no connection limits. `asyncio.start_server` accepts connections without bound, so a single client opening thousands of connections would exhaust file descriptors. Production proxies use semaphores or connection pools to cap concurrency.\n\nThere is no destination validation. Both proxies connect to whatever address the client requests, including `127.0.0.1`, `169.254.169.254` (cloud metadata), and internal network ranges. This is a Server-Side Request Forgery (SSRF) vector. Production proxies maintain deny lists of private and link-local address ranges.\n\nThere is no traffic logging or metrics. Production proxies track request counts, bytes transferred, error rates, and latency percentiles, typically exporting to Prometheus or similar systems.\n\nThe HTTP proxy does not add a `Via` header. RFC 9110 Section 7.6.3 requires intermediaries to append a `Via` field to forwarded messages. This was omitted for simplicity, but a standards-compliant proxy must include it.\n\nNeither proxy implements graceful shutdown. When the server stops, active tunnels are terminated abruptly rather than being drained. Production proxies track active connections and wait for them to complete (with a deadline) before shutting down.\n\n## Proxy Chaining\n\nChaining proxies means routing traffic through multiple proxies in sequence: client to proxy A, proxy A to proxy B, proxy B to the target server. Each proxy in the chain only knows its immediate neighbors, not the full path.\n\nThe main use case is distributing trust. If you do not fully trust any single proxy provider, chaining two providers means neither one sees both your real IP and your destination. The tradeoff is latency: each hop adds its own connection setup time and forwarding delay. A single proxy typically adds 50 to 100ms of overhead. Two proxies roughly double that, and three proxies can push total overhead past 300ms.\n\nBeyond two hops, the marginal privacy gain diminishes while latency and failure probability increase. Most practical setups use one or two proxies. Tor uses three relays (guard, middle, exit) because its threat model assumes some relays are compromised, but Tor accepts the latency penalty as an explicit design tradeoff.\n\n```\nClient --> Proxy A (SOCKS5) --> Proxy B (SOCKS5) --> Target\n           sees: client IP          sees: Proxy A IP\n           sees: Proxy B addr       sees: target addr\n```\n\nChaining a SOCKS5 proxy through another SOCKS5 proxy works by having proxy A treat proxy B as the target. The client connects to proxy A and sends a CONNECT request for proxy B's address. Once that tunnel is established, the client sends a second SOCKS5 handshake through the tunnel, this time requesting the real target. Proxy A sees traffic flowing to proxy B but cannot read it if the inner connection is encrypted.\n\n## References\n\n- RFC 1928: SOCKS Protocol Version 5 - https://datatracker.ietf.org/doc/html/rfc1928\n- RFC 1929: Username/Password Authentication for SOCKS V5 - https://datatracker.ietf.org/doc/html/rfc1929\n- RFC 9110: HTTP Semantics - https://www.rfc-editor.org/rfc/rfc9110.html\n- RFC 9112: HTTP/1.1 - https://www.rfc-editor.org/rfc/rfc9112.html\n- OWASP SSRF Prevention Cheat Sheet - https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html\n- mitmproxy (Python HTTPS intercepting proxy) - https://mitmproxy.org/\n"
  },
  {
    "path": "docs/en/deep-dive/network/http-proxies.md",
    "content": "# HTTP/HTTPS Proxy Architecture\n\nHTTP proxies are the most common proxy protocol on the internet. Nearly every corporate network uses them, and most commercial proxy services offer them as the default option. They operate at Layer 7 (Application) of the OSI model, which means they understand HTTP and can parse, modify, cache, and filter traffic. This same deep integration with the protocol is also their biggest limitation: they can only handle HTTP traffic, they reveal proxy usage through identifiable headers, and they cannot proxy UDP, which leaves WebRTC and DNS vulnerable to leaks.\n\nThis document covers how HTTP proxies work at the protocol level, the CONNECT method for HTTPS tunneling, authentication mechanisms, and the implications of modern protocols like HTTP/2 and HTTP/3.\n\n!!! info \"Module Navigation\"\n    - [Network Fundamentals](./network-fundamentals.md): TCP/IP, UDP, OSI model\n    - [Network & Security Overview](./index.md): Module introduction\n    - [SOCKS Proxies](./socks-proxies.md): Protocol-agnostic alternative\n    - [Proxy Detection](./proxy-detection.md): How to avoid detection\n\n    For practical configuration, see [Proxy Configuration](../../features/configuration/proxy.md).\n\n## How HTTP Proxies Work\n\nAn HTTP proxy sits between the client and the target server, maintaining two separate TCP connections: one from the client to the proxy, and another from the proxy to the target server. Because the proxy understands HTTP, it can make intelligent decisions about the traffic passing through it.\n\n### Request Flow\n\nWhen a client is configured to use an HTTP proxy, it sends the full HTTP request to the proxy rather than directly to the target server. The key difference from a direct request is that the request line includes the absolute URI, not just the path. For example, instead of `GET /page HTTP/1.1`, the client sends `GET http://example.com/page HTTP/1.1`. This tells the proxy where to forward the request.\n\n```mermaid\nsequenceDiagram\n    participant Client as Client Browser\n    participant Proxy as HTTP Proxy\n    participant Server as Target Server\n\n    Client->>Proxy: GET http://example.com/page HTTP/1.1<br/>Host: example.com<br/>User-Agent: Mozilla/5.0\n    Note over Client,Proxy: TCP connection #1\n\n    Note over Proxy: Parse request, check auth,<br/>check cache, apply rules\n\n    Proxy->>Server: GET /page HTTP/1.1<br/>Host: example.com<br/>Via: 1.1 proxy.example.com<br/>X-Forwarded-For: 192.168.1.100\n    Note over Proxy,Server: TCP connection #2\n\n    Server->>Proxy: HTTP/1.1 200 OK<br/>[response body]\n\n    Note over Proxy: Cache response if allowed,<br/>filter content, log transaction\n\n    Proxy->>Client: HTTP/1.1 200 OK<br/>Via: 1.1 proxy.example.com<br/>[possibly modified body]\n```\n\nThe proxy receives the full HTTP request, parses the method, URL, and headers, then decides what to do. It may check authentication credentials, verify the URL against an access control list, look for a cached copy of the resource, and modify headers before forwarding. It then opens a separate TCP connection to the target server and sends the request, potentially with altered headers.\n\nWhen the response arrives, the proxy can cache it according to HTTP semantics (`Cache-Control`, `ETag`), filter the content for malware or blocked keywords, compress it if the client supports it, and log the transaction before forwarding the response back to the client.\n\n### Proxy Headers and Privacy\n\nHTTP proxies commonly add headers that reveal their presence and the client's real IP address. The `Via` header (RFC 9110) identifies the proxy in the request chain. The `X-Forwarded-For` header contains the original client IP, often forming a chain if multiple proxies are involved. The `X-Forwarded-Proto` header indicates whether the original request was HTTP or HTTPS. Some proxies also add `X-Real-IP` as a simpler alternative to `X-Forwarded-For`.\n\nThere is also a standardized `Forwarded` header (RFC 7239) that combines all of this information into a single field, for example `Forwarded: for=192.168.1.100;proto=http;by=proxy.example.com`. In practice, most proxies still use the `X-Forwarded-*` variants since they have broader support.\n\nLegacy clients and some older browsers may also send a `Proxy-Connection: keep-alive` header instead of `Connection: keep-alive` when routing through a proxy. This header is a well-known indicator of proxy usage and a classic detection signal.\n\n!!! danger \"Header Detection\"\n    Detection systems look for the presence of `Via`, `X-Forwarded-For`, or `Forwarded` headers to confirm proxy usage. If `X-Real-IP` does not match the connecting IP, the proxy is confirmed. Sophisticated proxies can strip these headers, but many commercial proxy services leave them in by default. Always verify your proxy's behavior using a tool like [browserleaks.com/ip](https://browserleaks.com/ip).\n\n### Capabilities and Limitations\n\nBecause HTTP proxies parse and understand the HTTP protocol, they can read and modify every part of an unencrypted HTTP request and response: URLs, headers, cookies, and bodies. This lets them cache responses intelligently, filter content by URL or keyword, inject or strip headers, authenticate users, and log all traffic in detail.\n\nThe trade-off is that this deep coupling with HTTP means the proxy is limited to HTTP traffic. It cannot natively proxy FTP, SSH, SMTP, or custom protocols (though the CONNECT method, described below, provides a tunneling workaround for any TCP-based protocol). It has no support for UDP, which means WebRTC, DNS queries, and QUIC/HTTP/3 traffic bypass it entirely. And inspecting HTTPS content requires TLS termination, which breaks end-to-end encryption.\n\n## The CONNECT Method: HTTPS Tunneling\n\nThe CONNECT method (RFC 9110, Section 9.3.6) solves a fundamental problem: how can an HTTP proxy forward encrypted traffic it cannot read? The answer is to become a blind TCP tunnel.\n\nWhen a client wants to access an HTTPS site through a proxy, it sends a `CONNECT` request asking the proxy to establish a raw TCP connection to the destination. Once the proxy confirms the tunnel is established, it stops being an HTTP proxy entirely and becomes a transparent TCP relay at Layer 4, forwarding bytes in both directions without interpreting them.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant Proxy\n    participant Server\n\n    Client->>Proxy: CONNECT example.com:443 HTTP/1.1<br/>Host: example.com:443<br/>Proxy-Authorization: Basic dXNlcjpwYXNz\n    Note over Client,Proxy: Unencrypted HTTP request\n\n    Proxy->>Server: TCP three-way handshake\n    Note over Proxy,Server: TCP connection established\n\n    Proxy->>Client: HTTP/1.1 200 Connection Established\n\n    Note right of Proxy: Proxy is now a transparent<br/>TCP relay (Layer 4)\n\n    Client->>Server: TLS ClientHello\n    Note over Client,Server: TLS handshake (proxy sees<br/>this in plaintext)\n    Server->>Client: TLS ServerHello, Certificate\n\n    Client->>Server: Encrypted HTTP/2 request\n    Server->>Client: Encrypted HTTP/2 response\n\n    Note over Proxy: Proxy blindly forwards<br/>all encrypted data\n```\n\n### The CONNECT Request\n\nThe CONNECT request is minimal. The method is `CONNECT`, the request URI is the destination `host:port` (not a path), and it includes authentication if the proxy requires it. There is no request body. The proxy validates the credentials, checks its access control rules, and opens a TCP connection to the specified host and port. If everything succeeds, it sends back `HTTP/1.1 200 Connection Established` followed by a blank line. After that blank line, the HTTP conversation ends and the proxy becomes a transparent relay.\n\n### Visibility After CONNECT\n\nOnce the tunnel is established, the proxy's visibility is limited. It knows the destination hostname and port from the CONNECT request. It can observe connection timing (when it was established and for how long), the volume of data transferred in each direction, and when either side terminates the connection. It can also observe the TLS handshake that follows, which is particularly relevant.\n\nThe TLS ClientHello message, sent immediately after the tunnel is established, is transmitted in plaintext. The proxy (and any network observer) can directly read the TLS version, the full list of supported cipher suites, the extensions and their parameters, the elliptic curves offered, and the SNI (Server Name Indication) extension that contains the target hostname. This is exactly the information used for TLS fingerprinting (JA3/JA4). See [Network Fingerprinting](../fingerprinting/network-fingerprinting.md) for details.\n\nWhat the proxy cannot see is the encrypted application data: HTTP methods, URLs, request and response headers, cookies, session tokens, and response content are all encrypted inside the TLS tunnel.\n\n!!! note \"SNI and Encrypted Client Hello (ECH)\"\n    The SNI extension in the ClientHello reveals the target hostname in plaintext, which is redundant with the CONNECT request in the proxy scenario but relevant for other network observers. Encrypted Client Hello (ECH), currently being deployed, aims to encrypt the SNI to address this leak. However, ECH adoption is still limited and requires both client and server support.\n\n### CONNECT for Non-HTTPS Protocols\n\nWhile CONNECT is primarily used for HTTPS, it can tunnel any TCP-based protocol. An IMAPS connection to port 993, an SSH connection to port 22, or FTP-over-TLS to port 990 all work through a CONNECT tunnel. The proxy does not need to understand these protocols because after the tunnel is established, it is simply relaying bytes.\n\nIn practice, many corporate proxies restrict CONNECT to port 443 (HTTPS) to prevent abuse. Attempting `CONNECT example.com:22` for SSH will often return `403 Forbidden`.\n\n### The HTTPS Dilemma\n\nHTTP proxies face a fundamental choice with encrypted traffic. With the CONNECT tunnel approach, end-to-end encryption is preserved, the client verifies the server's certificate directly, and certificate pinning works normally. But the proxy cannot inspect, cache, or filter the encrypted content.\n\nThe alternative is TLS termination (MITM), where the proxy decrypts HTTPS traffic, inspects the content, and re-encrypts it before forwarding. This requires installing the proxy's CA certificate on the client, breaks end-to-end encryption, and is detectable through certificate pinning and Certificate Transparency logs. Most corporate proxies use this approach for content filtering and security scanning, while privacy-focused proxies use blind CONNECT tunnels.\n\nFor web scraping and automation, this distinction matters for TLS fingerprinting. If the proxy performs TLS termination, the TLS fingerprint that the target server sees belongs to the proxy, not your browser. If you are using a CONNECT tunnel, the fingerprint is preserved end-to-end. Depending on your evasion strategy, one approach may be preferable to the other.\n\n| Aspect | HTTP (no CONNECT) | HTTPS (CONNECT tunnel) |\n|--------|-------------------|------------------------|\n| Proxy visibility | Full HTTP request/response | Only destination host:port + TLS ClientHello |\n| Encryption | None (unless TLS termination) | End-to-end TLS |\n| Caching | Yes, based on HTTP semantics | No (encrypted content) |\n| Content filtering | Yes | No (only hostname-based blocking) |\n| Header modification | Yes | No (encrypted headers) |\n| URL visibility | Full URL | Only hostname (via CONNECT and SNI) |\n| Protocol support | HTTP only | Any protocol over TCP |\n\n## HTTPS Proxies (TLS to Proxy)\n\nA distinction worth clarifying is the difference between proxying HTTPS traffic and connecting to the proxy itself over HTTPS. When you configure `--proxy-server=https://proxy:port` instead of `http://proxy:port`, the connection between your browser and the proxy is encrypted with TLS. This protects your proxy authentication credentials from being sniffed on the local network and hides even the CONNECT hostname from local observers, since it is encapsulated inside the TLS connection to the proxy.\n\nChrome supports this via the `https://` scheme in `--proxy-server`. It is particularly important when using a proxy over untrusted networks (public Wi-Fi, shared hosting), where the connection between you and the proxy is the weakest link.\n\n## Authentication\n\nHTTP proxy authentication uses standard HTTP status codes and headers, following RFC 9110. When a proxy requires authentication, it responds with `407 Proxy Authentication Required` and a `Proxy-Authenticate` header indicating which authentication schemes it supports. The client then retransmits the request with a `Proxy-Authorization` header containing the credentials.\n\n### Authentication Schemes\n\nThere are several authentication schemes, each with different security characteristics.\n\n**Basic** (RFC 7617) is the simplest. The client sends `Proxy-Authorization: Basic <base64(username:password)>`. Base64 is an encoding, not encryption, so credentials are trivially reversible. Anyone who intercepts the header can decode it instantly and reuse it indefinitely since there is no replay protection. Basic auth should only be used over TLS-encrypted connections.\n\n**Digest** (RFC 7616) uses a challenge-response mechanism. The proxy sends a random nonce, and the client computes a hash of the username, password, nonce, and request URI. The password is never transmitted, and the nonce provides replay protection. The original version uses MD5, which is fast enough to brute-force efficiently, though RFC 7616 added SHA-256 support. Digest auth is rarely implemented by modern proxy services.\n\n**NTLM** is Microsoft's proprietary challenge-response protocol, common in Windows enterprise environments. It uses a three-step negotiation (Type 1 negotiation, Type 2 challenge, Type 3 authentication) and integrates with Active Directory for single sign-on. NTLMv1 uses DES (broken), and NTLMv2 uses HMAC-MD5 (considered weak by modern standards). Microsoft recommends Kerberos over NTLM for new deployments. NTLM is connection-bound, which means it breaks with HTTP/2 multiplexing.\n\n**Negotiate** (RFC 4559) uses SPNEGO to select between Kerberos and NTLM, preferring Kerberos. Kerberos offers the strongest security (AES encryption, mutual authentication, time-limited tickets) but requires Active Directory infrastructure, domain-joined machines, and accurate clock synchronization. In browser automation, Kerberos is difficult to configure programmatically.\n\n| Scheme | Security | Mechanism | Practical Notes |\n|--------|----------|-----------|-----------------|\n| Basic | Low | Base64-encoded credentials | Universal support. Only use over TLS. |\n| Digest | Medium | Challenge-response with MD5/SHA-256 | Replay protection via nonce. Rarely implemented. |\n| NTLM | Medium | Challenge-response (NT hash) | Windows SSO. Proprietary, known vulnerabilities. |\n| Negotiate | High | Kerberos/SPNEGO | Strongest. Requires Active Directory. |\n\n### Authentication in Pydoll\n\nChrome does not support inline proxy credentials in the `--proxy-server` flag. Writing `--proxy-server=http://user:pass@proxy:port` will not work: Chrome silently ignores the `user:pass` portion and connects without authentication.\n\nPydoll solves this transparently through its `ProxyManager`. When you provide a proxy URL with embedded credentials, Pydoll extracts the username and password, strips them from the URL before passing it to Chrome, and uses the CDP Fetch domain to intercept `407 Proxy Authentication Required` responses and automatically supply the credentials via `Fetch.continueWithAuth`. This approach works for all authentication schemes that Chrome supports (Basic, Digest, NTLM, Negotiate) without Pydoll needing to implement the protocol-specific logic.\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\n# Pydoll extracts credentials, cleans the URL, and handles 407 via CDP\noptions.add_argument('--proxy-server=http://user:pass@proxy.example.com:8080')\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n```\n\n!!! tip \"Authentication Best Practices\"\n    Always use TLS-encrypted proxy connections (HTTPS proxy or SSH tunnel) to protect credentials in transit. Prefer Bearer tokens for API proxies since they are revocable and time-limited. Never use Basic auth over an unencrypted HTTP connection to the proxy. Do not hardcode credentials in source code; use environment variables instead.\n\n## Modern Protocols and Proxying\n\n### HTTP/2\n\nHTTP/2 introduced multiplexing, binary framing, and HPACK header compression, which fundamentally change how proxies handle connections. In HTTP/1.1, each request occupies a connection sequentially (pipelining exists but is disabled in practice, so browsers work around this by opening six parallel connections per host). In HTTP/2, a single TCP connection carries multiple concurrent streams, each with its own request and response.\n\nFor proxies, this means managing stream IDs, priorities, and flow control windows on both sides of the connection. The proxy must translate between stream IDs on the client side and the server side, maintain priority trees, and handle flow control per-stream. This is significantly more complex than the simple request-response forwarding of HTTP/1.1.\n\nFrom a fingerprinting perspective, HTTP/2 stream metadata (window sizes, priority settings, header ordering within HPACK) can fingerprint individual clients even when multiple users share the same proxy.\n\n| Feature | HTTP/1.1 | HTTP/2 |\n|---------|----------|--------|\n| Connections | Sequential per connection (browsers open 6 in parallel) | Multiple concurrent streams over one connection |\n| Multiplexing | No (head-of-line blocking) | Yes (stream-level only) |\n| Header Compression | None | HPACK |\n| Proxy Complexity | Simple request/response forwarding | Stream ID mapping, priority management |\n\nIn HTTP/2, the CONNECT method was extended by RFC 8441 to support a `:protocol` pseudo-header, enabling WebSocket tunneling and other protocol upgrades directly within HTTP/2 streams without requiring separate connections.\n\n### HTTP/3 and QUIC\n\nHTTP/3 runs over QUIC (RFC 9000), which is a UDP-based transport protocol. This introduces fundamental challenges for HTTP proxies. Traditional HTTP proxies operate over TCP and cannot handle QUIC's UDP traffic. QUIC connections can survive IP changes (connection migration), complicating proxy session management. And QUIC encrypts nearly everything, including transport-level metadata that was previously visible.\n\nProxying QUIC requires CONNECT-UDP (RFC 9298), a new method for establishing UDP tunnels through HTTP proxies. Most traditional proxies, including many commercial services, do not support this yet. Browsers fall back to HTTP/2 over TCP when the proxy does not support QUIC, which means more metadata may leak than expected if you were relying on HTTP/3's encrypted transport.\n\nIn automation scenarios, consider disabling QUIC with the `--disable-quic` Chrome flag to force HTTP/2 over TCP. This ensures all traffic passes through your proxy and eliminates the risk of UDP-based leaks from QUIC.\n\n| Aspect | TCP + TLS (HTTP/1.1, HTTP/2) | QUIC/UDP (HTTP/3) |\n|--------|------------------------------|-------------------|\n| Transport | TCP (connection-oriented) | UDP (connectionless) |\n| Handshake | Separate TCP + TLS (2 RTT) | Combined (0-1 RTT) |\n| Head-of-line blocking | Yes (TCP level) | No (stream-level only) |\n| Connection migration | Not supported | Supported (survives IP changes) |\n| Proxy compatibility | Excellent | Limited (requires UDP relay support) |\n\n!!! warning \"Protocol Downgrade\"\n    When a proxy does not support HTTP/3, the browser silently falls back to HTTP/2 or HTTP/1.1. This downgrade can expose metadata (headers, timing patterns) that HTTP/3 would have encrypted. Monitor your traffic to understand your actual protocol version, and be aware that HTTP/3 adoption varies by region and CDN.\n\n## Summary\n\nHTTP proxies provide rich functionality at the cost of limited scope and privacy concerns. They can inspect, cache, and filter HTTP traffic, but they cannot handle non-HTTP protocols, UDP traffic, or HTTPS content without breaking encryption. Their presence is revealed through identifiable headers unless explicitly stripped.\n\nFor automation, the CONNECT tunnel is the most relevant feature: it preserves end-to-end TLS encryption while giving the proxy only hostname-level visibility. Pydoll handles proxy authentication transparently through the CDP Fetch domain, supporting all schemes Chrome implements.\n\n### HTTP Proxy vs SOCKS5\n\n| Need | HTTP Proxy | SOCKS5 |\n|------|------------|--------|\n| Content filtering | Yes | No |\n| URL-based blocking | Yes | No (only IP:port) |\n| Caching | Yes | No |\n| UDP support | No | Yes |\n| Protocol flexibility | HTTP only (CONNECT for TCP tunneling) | Any TCP/UDP |\n| Privacy | Low (parses HTTP, adds revealing headers) | Medium (does not parse or modify traffic, but unencrypted content is still visible to operator) |\n| DNS resolution | Proxy resolves (remote) | Depends (SOCKS5: typically client resolves, SOCKS5h: proxy resolves. Chrome always resolves remotely for SOCKS5.) |\n\nFor corporate environments that need content control and caching, HTTP proxies are the right choice. For privacy-focused automation, SOCKS5 offers better stealth and protocol flexibility. For maximum security, use SOCKS5 over an SSH tunnel or VPN.\n\n**Next steps:**\n\n- [SOCKS Proxies](./socks-proxies.md): Protocol-agnostic, session-layer proxying\n- [Network Fundamentals](./network-fundamentals.md): TCP/IP, UDP, WebRTC\n- [Proxy Detection](./proxy-detection.md): How proxies are detected and how to avoid it\n- [Proxy Configuration](../../features/configuration/proxy.md): Practical Pydoll proxy setup\n- [Network Fingerprinting](../fingerprinting/network-fingerprinting.md): TCP/IP and TLS fingerprinting\n\n## References\n\n- RFC 9110: HTTP Semantics (2022, replaces RFC 7230-7237) - https://www.rfc-editor.org/rfc/rfc9110.html\n- RFC 9112: HTTP/1.1 (2022) - https://www.rfc-editor.org/rfc/rfc9112.html\n- RFC 9113: HTTP/2 (2022, replaces RFC 7540) - https://www.rfc-editor.org/rfc/rfc9113.html\n- RFC 9114: HTTP/3 (2022) - https://www.rfc-editor.org/rfc/rfc9114.html\n- RFC 9000: QUIC Transport Protocol (2021) - https://www.rfc-editor.org/rfc/rfc9000.html\n- RFC 9298: Proxying UDP in HTTP (CONNECT-UDP, 2022) - https://www.rfc-editor.org/rfc/rfc9298.html\n- RFC 8441: Bootstrapping WebSockets with HTTP/2 (2018) - https://www.rfc-editor.org/rfc/rfc8441.html\n- RFC 7617: Basic Authentication (2015) - https://www.rfc-editor.org/rfc/rfc7617.html\n- RFC 7616: Digest Authentication (2015) - https://www.rfc-editor.org/rfc/rfc7616.html\n- RFC 7239: Forwarded HTTP Extension (2014) - https://www.rfc-editor.org/rfc/rfc7239.html\n- RFC 4559: Negotiate Authentication (2006) - https://www.rfc-editor.org/rfc/rfc4559.html\n- MDN Web Docs: Proxy servers and tunneling - https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling\n- Chrome DevTools Protocol: Fetch domain - https://chromedevtools.github.io/devtools-protocol/tot/Fetch/\n"
  },
  {
    "path": "docs/en/deep-dive/network/index.md",
    "content": "# Network & Security Deep Dive\n\n**Welcome to the foundation of modern internet communication, the battleground of anonymity, detection, and evasion.**\n\nNetwork protocols are the invisible infrastructure that powers every web request, browser connection, and automation script. Understanding them deeply transforms you from a **tool user** into a **protocol engineer** capable of navigating the most sophisticated anti-bot systems.\n\n## Why Network Architecture Matters\n\nWhen you run `tab.go_to('https://example.com')`, a complex symphony of protocols springs into action:\n\n1. **DNS resolution** translates the domain to an IP address (potentially leaking your intent)\n2. **TCP handshake** establishes connection (revealing your OS through packet characteristics)\n3. **TLS negotiation** secures the channel (fingerprinting your browser via cipher suites)\n4. **HTTP/2 request** fetches the page (exposing browser version through SETTINGS frames)\n5. **WebRTC discovery** may probe your real IP (bypassing your VPN entirely)\n\n**Every single step is an opportunity for detection or evasion.**\n\n!!! danger \"The Network Layer Cannot Lie\"\n    Unlike browser-level characteristics (which JavaScript can modify), network-level fingerprints are **burned into the OS kernel and TCP/IP stack**. A mismatch here like a Chrome browser sending Linux TCP options while claiming to be Windows is instantly fatal to stealth automation.\n\n## The Architecture of Internet Privacy\n\nThis module explores the **technical foundations** that make privacy possible (and breakable) on the modern internet:\n\n### The OSI Model Reality\n\n```mermaid\ngraph TB\n    subgraph \"Application Layer 7\"\n        HTTP[HTTP/HTTPS Headers]\n        DNS[DNS Queries]\n    end\n    \n    subgraph \"Presentation Layer 6\"\n        TLS[TLS/SSL Fingerprinting]\n        Ciphers[Cipher Suites, Extensions]\n    end\n    \n    subgraph \"Session/Transport Layers 5-4\"\n        SOCKS[SOCKS Proxy Protocol]\n        TCP[TCP Window, Options, ISN]\n    end\n    \n    subgraph \"Network Layer 3\"\n        IP[IP TTL, Fragmentation]\n        Routing[Packet Routing, Hops]\n    end\n    \n    HTTP --> TLS\n    DNS --> TLS\n    TLS --> SOCKS\n    Ciphers --> TCP\n    SOCKS --> IP\n    TCP --> Routing\n```\n\n**Each layer is both a shield and a vulnerability:**\n\n- **Layer 7 (Application)**: Proxies can read and modify your HTTP traffic\n- **Layer 6 (Presentation)**: TLS encryption protects content but leaks metadata\n- **Layer 4 (Transport)**: TCP characteristics betray your operating system\n- **Layer 3 (Network)**: IP addresses reveal your physical location\n\n## What You'll Master\n\nThis module is structured as a **technical progression** from fundamentals to advanced exploitation:\n\n### 1. Network Fundamentals\n**[Network Fundamentals](./network-fundamentals.md)**\n\nBuild the foundation: understand the protocols that power the internet and how they reveal, or hide, your identity.\n\n- **OSI Model layers** and their fingerprinting implications\n- **TCP vs UDP**: Why your proxy might leak UDP traffic\n- **WebRTC IP leakage**: The hidden threat in modern browsers\n- **Network stack characteristics**: TTL, window size, option ordering\n\n**Why start here**: Without this foundation, proxy configuration is **cargo cult programming**, copying commands without understanding why they work (or don't).\n\n### 2. HTTP/HTTPS Proxies\n**[HTTP/HTTPS Proxies](./http-proxies.md)**\n\nMaster the most common proxy protocol and understand its fundamental limitations.\n\n- **HTTP proxy operation**: Request forwarding, caching, header injection\n- **CONNECT tunneling**: How HTTPS \"tunnels\" through HTTP proxies\n- **HTTP/2 complexities**: Multiplexing, stream priorities, SETTINGS fingerprinting\n- **HTTP/3 and QUIC**: UDP-based proxying challenges\n- **Authentication schemes**: Basic, Digest, NTLM, Bearer tokens\n\n**Critical insight**: HTTP proxies operate at Layer 7, they can **read, modify, and log** your unencrypted traffic. For true privacy, you need encryption **before** the proxy sees your data.\n\n### 3. SOCKS Proxies\n**[SOCKS Proxies](./socks-proxies.md)**\n\nUnderstand why SOCKS5 is the **gold standard** for privacy-conscious automation.\n\n- **SOCKS4 vs SOCKS5**: Protocol evolution and capabilities\n- **SOCKS5 handshake**: Binary protocol deep dive with packet structures\n- **UDP support**: Gaming, VoIP, and WebRTC over SOCKS5\n- **DNS resolution**: Why proxy-side DNS prevents leaks\n- **Why SOCKS5 > HTTP proxies**: Protocol-level comparison\n\n**Key advantage**: SOCKS operates at Layer 5 (Session), **below** the application layer. It can't read your HTTP traffic, only see destination IPs, vastly reducing the trust surface area.\n\n### 4. Proxy Detection\n**[Proxy Detection & Anonymity](./proxy-detection.md)**\n\nLearn how websites **detect proxy usage** and how to evade detection.\n\n- **Anonymity levels**: Transparent, anonymous, elite proxies\n- **IP reputation databases**: How your datacenter IP betrays you\n- **Header analysis**: X-Forwarded-For, Via, Forwarded headers\n- **Consistency checks**: DNS reverse lookup, geolocation mismatches\n- **Network fingerprinting integration**: Combining proxy detection with TCP/TLS analysis\n\n**Harsh reality**: Most \"anonymous\" proxies are trivially detectable. True stealth requires **elite residential proxies** + **consistent browser fingerprinting** + **human-like behavior**.\n\n### 5. Building Proxy Servers\n**[Building Your Own Proxy](./build-proxy.md)**\n\nImplement HTTP and SOCKS5 proxies from scratch in Python, the ultimate learning experience.\n\n- **HTTP proxy server**: Complete async implementation with authentication\n- **SOCKS5 proxy server**: Binary protocol handling, TCP tunneling\n- **Proxy chaining**: Layered anonymity (and latency tradeoffs)\n- **Rotating proxy pools**: Health checking, failover, load balancing\n- **Advanced topics**: Transparent proxies, MITM SSL interception\n\n**Why build your own**: Understanding implementation details reveals **attack vectors** and **optimization opportunities** invisible from the outside.\n\n### 6. Legal & Ethical Considerations\n**[Legal & Ethical Guidelines](./proxy-legal.md)**\n\nNavigate the legal minefield of proxy usage and web automation.\n\n- **Regulatory compliance**: GDPR, CFAA, international laws\n- **Terms of Service**: What constitutes violation\n- **Ethical guidelines**: robots.txt, rate limiting, transparency\n- **Case studies**: Legal precedents (hiQ vs LinkedIn, QVC vs Resultly)\n- **When to avoid proxies**: High-risk scenarios\n\n**Disclaimer**: This is **educational information**, not legal advice. The law varies wildly by jurisdiction and use case. Consult qualified counsel.\n\n## The Proxy Paradox\n\nHere's the uncomfortable truth about proxies:\n\n!!! warning \"Proxies Don't Make You Anonymous. They Make You **Different**\"\n    A proxy changes your IP address, but it also:\n    \n    - Adds **latency** (detectible via timing analysis)\n    - Resets **TTL** values (revealing proxy hops)\n    - Introduces **TCP fingerprint** mismatches (proxy OS ≠ your OS)\n    - May inject **headers** (X-Forwarded-For, Via)\n    - Creates **geolocation** inconsistencies (browser timezone ≠ IP location)\n    \n    Proxies are a **tool**, not a solution. True stealth requires **holistic consistency**.\n\n## Prerequisites\n\nThis is **advanced material**. You should be comfortable with:\n\nBasic networking concepts (IP addresses, ports, protocols)  \nTCP/IP fundamentals (three-way handshake, packets, routing)  \nAsynchronous Python programming (asyncio, async/await)  \nPydoll basics (see [Core Concepts](../../features/core-concepts.md))  \n\n**If you're new to networking**, we highly recommend:\n\n1. Read a TCP/IP fundamentals guide first\n2. Experiment with Wireshark to visualize network traffic\n3. Try the code examples with packet captures running\n4. Build the proxy servers and test them locally\n\n## Integration with Other Modules\n\nNetwork architecture doesn't exist in isolation. It integrates deeply with:\n\n- **[Fingerprinting](../fingerprinting/network-fingerprinting.md)**: How TCP/IP and TLS characteristics identify you\n- **[Browser Configuration](../../features/configuration/browser-preferences.md)**: Aligning browser behavior with proxy characteristics\n- **[Connection Layer](../fundamentals/connection-layer.md)**: How Pydoll manages WebSocket connections over proxies\n\n## The Learning Path\n\nWe recommend this progression:\n\n**Phase 1: Foundation**\n\n1. Read [Network Fundamentals](./network-fundamentals.md)\n2. Understand OSI model and protocol layering\n3. Learn about WebRTC leaks and UDP tunneling\n\n**Phase 2: Protocol Deep Dive**\n\n4. Study [HTTP/HTTPS Proxies](./http-proxies.md)\n5. Master [SOCKS Proxies](./socks-proxies.md)\n6. Compare protocols and understand tradeoffs\n\n**Phase 3: Adversarial Thinking**\n\n7. Explore [Proxy Detection](./proxy-detection.md)\n8. Learn detection techniques from the defender's perspective\n9. Apply evasion strategies\n\n**Phase 4: Hands-On Implementation**\n\n10. Build proxy servers from [Building Proxies](./build-proxy.md)\n11. Capture and analyze traffic with Wireshark\n12. Test proxy chains and rotation strategies\n\n**Phase 5: Operational Security**\n\n13. Review [Legal & Ethical](./proxy-legal.md) guidelines\n14. Understand compliance requirements\n15. Develop responsible automation policies\n\n\n## The Philosophy\n\nNetwork and security knowledge is **foundational power**. Unlike framework-specific skills (which become obsolete), protocol knowledge is **timeless**:\n\n- TCP hasn't fundamentally changed since RFC 793 (1981)\n- TLS builds on concepts from SSL (1995)\n- HTTP/2 (2015) and HTTP/3 (2022) are evolutions, not revolutions\n\nMaster these fundamentals once, and you'll understand **every network-based system** you encounter for the rest of your career.\n\n## Ethical Commitment\n\nBefore proceeding, acknowledge:\n\nI understand proxies can be used for both legitimate and malicious purposes  \nI will respect website terms of service and robots.txt  \nI will implement rate limiting and respectful crawling  \nI will not use this knowledge for fraud, abuse, or illegal activities  \nI will consult legal counsel when uncertain about compliance  \n\n**With great power comes great responsibility.** Use this knowledge wisely.\n\n---\n\n## Ready to Begin?\n\nStart your journey with **[Network Fundamentals](./network-fundamentals.md)** to build the foundation, then progress through the modules in order. Each document builds on the previous, creating a comprehensive understanding of network architecture for automation.\n\n---\n\n!!! info \"Documentation Status\"\n    This module synthesizes knowledge from RFCs, protocol specifications, security research, and real-world testing. Every code example is production-ready. If you find inaccuracies or have improvements, contributions are welcome.\n\n## Quick Navigation\n\n**Core Protocols:**\n\n- [Network Fundamentals](./network-fundamentals.md) - TCP/IP, UDP, WebRTC\n- [HTTP/HTTPS Proxies](./http-proxies.md) - Application-layer proxying\n- [SOCKS Proxies](./socks-proxies.md) - Session-layer proxying\n\n**Advanced Topics:**\n\n- [Proxy Detection](./proxy-detection.md) - Anonymity and evasion\n- [Building Proxies](./build-proxy.md) - Implementation from scratch\n- [Legal & Ethical](./proxy-legal.md) - Compliance and responsibility\n\n**Related Modules:**\n\n- [Fingerprinting](../fingerprinting/index.md) - Detection techniques\n- [Browser Configuration](../../features/configuration/browser-options.md) - Practical setup\n"
  },
  {
    "path": "docs/en/deep-dive/network/network-fundamentals.md",
    "content": "# Network Fundamentals\n\nThis document covers the foundational network protocols that power the internet and how they can expose or protect your identity in automation scenarios. A working understanding of TCP, UDP, the OSI model, and WebRTC will make proxy configuration far less mysterious and far more effective.\n\n!!! info \"Module Navigation\"\n    - [Network & Security Overview](./index.md): Module introduction and learning path\n    - [HTTP/HTTPS Proxies](./http-proxies.md): Application-layer proxying\n    - [SOCKS Proxies](./socks-proxies.md): Session-layer proxying\n\n    For practical Pydoll usage, see [Proxy Configuration](../../features/configuration/proxy.md) and [Browser Options](../../features/configuration/browser-options.md).\n\n## The Network Stack\n\nEvery HTTP request your browser makes travels through a layered network stack. Each layer has specific responsibilities, protocols, and security implications. Proxies operate at different layers, and the layer determines what the proxy can see, modify, and hide. Network characteristics at lower layers can fingerprint your real system even through proxies, so understanding the stack helps you see where identity leaks happen and how to prevent them.\n\n### The OSI Model\n\nThe OSI (Open Systems Interconnection) model, developed by ISO in 1984, provides a conceptual framework for understanding how network protocols interact. Real-world networks use the TCP/IP model (which predates OSI and has only 4 layers), but OSI terminology remains the standard way to describe where proxies operate and what they can access.\n\n```mermaid\ngraph TD\n    L7[Layer 7: Application - HTTP, FTP, SMTP, DNS]\n    L6[Layer 6: Presentation - Encryption, Compression]\n    L5[Layer 5: Session - SOCKS]\n    L4[Layer 4: Transport - TCP, UDP]\n    L3[Layer 3: Network - IP, ICMP]\n    L2[Layer 2: Data Link - Ethernet, WiFi]\n    L1[Layer 1: Physical - Cables, Radio Waves]\n\n    L7 --> L6 --> L5 --> L4 --> L3 --> L2 --> L1\n```\n\nLayer 7 (Application) is where user-facing protocols live: HTTP, HTTPS, FTP, SMTP, and DNS all operate here. This layer contains the actual data your application cares about, such as HTML documents, JSON responses, and file transfers. HTTP proxies operate at this layer, which gives them full visibility into request and response content.\n\nLayer 6 (Presentation) handles data format translation, encryption, and compression. SSL/TLS is commonly associated with this layer for its encryption role, though in practice TLS straddles Layers 4 through 6 and does not map cleanly to any single OSI layer. What matters for automation is that HTTPS encryption happens here, encrypting Layer 7 data before it moves down the stack.\n\nLayer 5 (Session) manages connections between applications. SOCKS proxies operate here, below the application layer but above transport. This position makes SOCKS protocol-agnostic: it can proxy any Layer 7 protocol (HTTP, FTP, SMTP, SSH) without needing to understand their specifics.\n\nLayer 4 (Transport) provides end-to-end data delivery. TCP (connection-oriented, reliable) and UDP (connectionless, fast) are the dominant protocols here. This layer handles port numbers, flow control, and error correction. All proxies ultimately rely on Layer 4 for actual data transmission.\n\nLayer 3 (Network) handles routing and addressing between networks. IP (Internet Protocol) operates here, managing IP addresses and routing decisions. This is where your real IP address lives, and where proxies aim to substitute it.\n\nLayer 2 (Data Link) manages communication on the same physical network segment. Ethernet, Wi-Fi, and PPP operate here, handling MAC addresses and frame transmission. MAC addresses are only visible on the local network segment and are not directly accessible by remote servers, though they can be exposed through protocols like IPv6 SLAAC (which embeds the MAC in the address).\n\nLayer 1 (Physical) is the actual hardware: cables, radio waves, and voltage levels. Rarely relevant to software automation.\n\n!!! tip \"OSI vs TCP/IP\"\n    The TCP/IP model (4 layers: Link, Internet, Transport, Application) is what networks actually use. OSI (7 layers) is a teaching tool and reference model. When people say \"Layer 7 proxy,\" they are using OSI terminology, but the actual implementation runs on TCP/IP.\n\n### How Layer Positioning Affects Proxies\n\nThe layer where a proxy operates determines what it can and cannot do.\n\nHTTP/HTTPS proxies operate at Layer 7 (Application). Because they understand HTTP, they can read and modify URLs, headers, cookies, and request bodies. They can cache responses intelligently based on HTTP semantics, filter content by URL or keyword, and inject authentication headers. The trade-off is that they only understand HTTP. They cannot proxy FTP, SMTP, SSH, or other protocols, and inspecting HTTPS content requires TLS termination, which means decrypting and re-encrypting the traffic.\n\nSOCKS proxies operate at Layer 5 (Session). Because they sit below the application layer, they are protocol-agnostic and can proxy any Layer 7 protocol without modification. HTTPS traffic passes through encrypted end-to-end, since the SOCKS proxy never needs to decrypt it. SOCKS5 also supports UDP, enabling it to proxy DNS queries, VoIP, and other UDP-based protocols. The trade-off is that SOCKS proxies have no visibility into application-layer data: they cannot cache, filter by URL, or inspect content. They can only filter by IP and port.\n\n!!! note \"The Fundamental Tradeoff\"\n    Higher layers (Layer 7) give you more control but less flexibility. Lower layers (Layer 5) give you less control but more flexibility. Choose HTTP proxies when you need content control, and SOCKS proxies when you need protocol flexibility or end-to-end encryption.\n\n### The Layer Leak Problem\n\nEven with a perfect Layer 7 proxy, lower-layer characteristics can expose your real identity. Your operating system's TCP stack at Layer 4 has a unique fingerprint defined by window size, options order, and TTL values. IP header fields at Layer 3 such as TTL and fragmentation behavior reveal your OS and network topology.\n\nFor example, if you configure a proxy to present a \"Windows 10\" User-Agent but your actual Linux system's TCP fingerprint contradicts this at Layer 4, sophisticated detection systems can flag this inconsistency as a strong bot indicator. This is why network-level fingerprinting (covered in [Network Fingerprinting](../fingerprinting/network-fingerprinting.md)) is so dangerous: it operates below the proxy layer, exposing your real system even when application-layer proxying is flawless.\n\n## TCP vs UDP\n\nAt Layer 4 (Transport), two fundamentally different protocols dominate internet communication. They represent opposite design philosophies: reliability versus speed.\n\nTCP is connection-oriented. Think of it like a phone call: you establish a connection, verify the other party is listening, exchange data reliably, then hang up. Every byte is acknowledged, ordered, and guaranteed to arrive. UDP is connectionless. You send your data and hope it arrives. No handshake, no acknowledgments, no guarantees. Just raw speed with minimal overhead.\n\n| Feature | TCP | UDP |\n|---------|-----|-----|\n| Connection | Connection-oriented (handshake required) | Connectionless (no handshake) |\n| Reliability | Guaranteed delivery, ordered packets | Best-effort delivery, packets may be lost |\n| Speed | Slower (overhead from reliability mechanisms) | Faster (minimal overhead) |\n| Use Cases | Web browsing, file transfer, email | Video streaming, DNS queries, gaming |\n| Header Size | 20 bytes minimum (up to 60 with options) | 8 bytes fixed |\n| Flow Control | Yes (sliding window, receiver-driven) | No (sender transmits at will) |\n| Congestion Control | Yes (slows down when network is congested) | No (application's responsibility) |\n| Error Checking | Extensive (checksum + acknowledgments) | Basic (checksum only; optional in IPv4, mandatory in IPv6) |\n| Ordering | Packets reordered if received out-of-sequence | No ordering, packets delivered as received |\n| Retransmission | Automatic (lost packets retransmitted) | None (application must handle) |\n\n### TCP and Proxies\n\nAll proxy protocols (HTTP, HTTPS, SOCKS4, SOCKS5) use TCP for their control channel. This is because proxy authentication and command exchange require guaranteed delivery, proxy protocols have strict command sequences (handshake, then auth, then data), and proxies need persistent connections to track client state.\n\nHowever, SOCKS5 can also proxy UDP traffic, unlike SOCKS4 or HTTP proxies. This makes SOCKS5 essential for proxying DNS queries, WebRTC audio/video, VoIP, and gaming protocols.\n\n!!! danger \"UDP and IP Leakage\"\n    Most browser connections use TCP (HTTP, WebSocket, etc.), but WebRTC uses UDP directly, bypassing the browser's proxy configuration. This is the most common cause of IP leakage in proxied browser automation: your TCP traffic goes through the proxy while your UDP traffic leaks your real IP.\n\n### The TCP Three-Way Handshake\n\nBefore any data can be transmitted, TCP requires a three-way handshake to establish a connection. This negotiation synchronizes sequence numbers, agrees on window sizes, and establishes connection state on both ends.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant Server\n\n    Client->>Server: SYN (Synchronize, seq=x)\n    Note over Client,Server: Client requests connection\n\n    Server->>Client: SYN-ACK (seq=y, ack=x+1)\n    Note over Client,Server: Server acknowledges and sends its own SYN\n\n    Client->>Server: ACK (ack=y+1)\n    Note over Client,Server: Connection established, data transfer begins\n```\n\nThe process starts when the client sends a SYN (Synchronize) packet containing a random Initial Sequence Number (ISN), for example `seq=1000`. Along with the ISN, TCP options are negotiated: window size, Maximum Segment Size (MSS), timestamps, and SACK support.\n\nThe server responds with a SYN-ACK: it picks its own random ISN (e.g., `seq=5000`) and acknowledges the client's ISN by setting `ack=1001` (client's ISN + 1). This single packet both establishes the server-to-client direction (SYN) and confirms the client-to-server direction (ACK). The server also returns its own TCP options.\n\nThe client then sends a final ACK, acknowledging the server's ISN (`ack=5001`). At this point the connection is fully established in both directions and data transmission can begin.\n\nThe ISN is randomized rather than starting from zero to prevent TCP hijacking attacks. If ISNs were predictable, an attacker could inject packets into an existing connection by guessing the sequence numbers. Modern systems use cryptographic randomness for ISN selection (RFC 6528).\n\n### TCP Fingerprinting\n\nThe TCP handshake reveals characteristics that fingerprint your operating system. Different OSes use different default values for the initial window size, TCP options order, TTL (Time To Live), window scale factor, and timestamp behavior. These values are set by the kernel, not the browser, so a proxy cannot change them.\n\nHere are illustrative examples for modern operating systems. Note that actual values vary across OS versions, kernel configurations, and network tuning:\n\n```\nWindows 10/11 (modern builds):\n    Window Size: 65535\n    MSS: 1460\n    Options: MSS, NOP, WS, NOP, NOP, SACK_PERM\n    TTL: 128\n\nLinux (kernel 5.x+, Ubuntu 20.04+):\n    Window Size: 29200\n    MSS: 1460\n    Options: MSS, SACK_PERM, TS, NOP, WS\n    TTL: 64\n\nmacOS (Monterey+):\n    Window Size: 65535\n    TTL: 64\n```\n\nThese differences are burned into the kernel. A proxy cannot change them because they are set by your operating system, not your browser. This is how sophisticated detection systems can identify you even through proxies.\n\n!!! warning \"Proxy Limitation\"\n    HTTP and SOCKS proxies operate above the TCP layer. They cannot modify TCP handshake characteristics. Your OS's TCP fingerprint is always exposed to the proxy server and any network observers between you and the proxy. Only VPN-level solutions or OS-level TCP stack configuration can address this.\n\n!!! note \"Beyond TCP Fingerprinting\"\n    The TCP handshake is just the first fingerprinting opportunity. Immediately after, the TLS handshake reveals another unique fingerprint known as JA3/JA4. See [Network Fingerprinting](../fingerprinting/network-fingerprinting.md) for details on TLS and HTTP/2 fingerprinting.\n\n### UDP\n\nUnlike TCP's reliable, connection-oriented approach, UDP is a fire-and-forget protocol. It trades reliability for minimal latency and overhead, making it ideal for real-time applications where speed matters more than perfect delivery.\n\nA UDP datagram has only an 8-byte header (compared to TCP's 20-60 bytes), containing source port, destination port, length, and a checksum. There is no connection establishment, no reliability guarantee, no flow control, and no congestion control. If a packet is lost, the application must decide whether and how to handle it.\n\nUDP is the right choice for real-time communication (voice/video calls via WebRTC and VoIP), gaming (low-latency state updates), streaming (where occasional frame loss is acceptable), and DNS queries (small request/response pairs where the application handles retries). It is a poor choice for file transfers, web browsing, email, or databases, all of which need reliable, ordered delivery.\n\nDNS is a particularly important example in the context of automation. DNS uses UDP because queries are typically small and benefit from UDP's zero-handshake overhead. While EDNS0 (RFC 6891) increased the maximum UDP DNS payload beyond the original 512-byte limit, most queries remain compact. The DNS client handles retries at the application level if a response does not arrive within a timeout.\n\nFor browser automation, the key concern with UDP is that WebRTC uses it for real-time audio and video, DNS queries use it for domain resolution, and most proxies (HTTP, HTTPS, SOCKS4) only handle TCP. Unless you explicitly configure UDP proxying, this traffic bypasses your proxy and leaks your real IP.\n\n| Proxy Type | UDP Support | Notes |\n|------------|-------------|-------|\n| HTTP Proxy | No | Only proxies TCP-based HTTP/HTTPS |\n| HTTPS Proxy (CONNECT) | No | CONNECT method only establishes TCP tunnels |\n| SOCKS4 | No | TCP-only protocol |\n| SOCKS5 | Yes | Supports UDP relay via `UDP ASSOCIATE` command |\n| VPN | Yes | Tunnels all IP traffic (TCP and UDP) |\n\nFor true anonymity in browser automation, you need either a SOCKS5 proxy with UDP support and WebRTC configured to use it, WebRTC disabled entirely (which breaks video conferencing), a VPN that tunnels all traffic, or the browser flag `--force-webrtc-ip-handling-policy=disable_non_proxied_udp`.\n\n### QUIC and HTTP/3\n\nModern browsers increasingly use QUIC (RFC 9000), a UDP-based transport protocol that powers HTTP/3. Since QUIC runs over UDP, it shares the same proxy bypass issues as WebRTC and DNS: most HTTP proxies cannot handle QUIC traffic, and it may leak outside your proxy configuration.\n\nIn automation scenarios, consider disabling QUIC with the `--disable-quic` Chrome flag to force HTTP/2 over TCP, ensuring all web traffic passes through your proxy. QUIC also has its own fingerprinting characteristics, similar to JA3 for TLS, which adds another vector for detection.\n\n## WebRTC and IP Leakage\n\nWebRTC (Web Real-Time Communication) is a browser API standardized by the W3C that enables peer-to-peer audio, video, and data communication directly between browsers without plugins or intermediary servers. While powerful for real-time applications, WebRTC is the single biggest source of IP leakage in proxied browser automation.\n\n### How WebRTC Leaks Your IP\n\nWebRTC was designed for direct peer-to-peer connections, optimizing for low latency over privacy. To establish P2P connections, WebRTC must discover your real public IP address and share it with the remote peer, even if your browser is configured to use a proxy.\n\nThe problem unfolds like this: your browser uses a proxy for HTTP/HTTPS traffic (which is TCP), but WebRTC uses STUN servers to discover your real public IP over UDP. STUN queries bypass the proxy because most proxies only handle TCP. Your real IP is discovered and shared with remote peers as part of the connection negotiation. JavaScript on the page can read these \"ICE candidates\" and send your real IP to the website's server.\n\n!!! danger \"Severity of WebRTC Leaks\"\n    Even with an HTTP proxy configured correctly, HTTPS proxy working, DNS queries proxied, User-Agent spoofed, and canvas fingerprinting mitigated, WebRTC can still leak your real IP in milliseconds. This is because WebRTC operates below the browser's proxy layer, directly interfacing with the OS network stack.\n\n### The ICE Process\n\nWebRTC uses ICE (Interactive Connectivity Establishment, RFC 8445) to discover possible connection paths and select the best one. This process inherently reveals your network topology by gathering three types of candidates.\n\n```mermaid\nsequenceDiagram\n    participant Browser\n    participant STUN as STUN Server\n    participant TURN as TURN Relay\n    participant Peer as Remote Peer\n\n    Note over Browser: WebRTC connection initiated\n\n    Browser->>Browser: Gather local IP addresses<br/>(LAN interfaces)\n    Note over Browser: Local candidate:<br/>192.168.1.100:54321\n\n    Browser->>STUN: STUN Binding Request (over UDP)\n    Note over STUN: STUN server discovers public IP<br/>(bypasses proxy!)\n    STUN->>Browser: STUN Response with real public IP\n    Note over Browser: Server reflexive candidate:<br/>203.0.113.45:54321\n\n    Browser->>TURN: Allocate relay (if needed)\n    TURN->>Browser: Relay address assigned\n    Note over Browser: Relay candidate:<br/>198.51.100.10:61234\n\n    Browser->>Peer: Send all ICE candidates<br/>(local + public + relay)\n    Note over Peer: Now knows your:<br/>- LAN IP<br/>- Real public IP<br/>- Relay address\n\n    Peer->>Browser: Send ICE candidates\n\n    Note over Browser,Peer: ICE negotiation: try direct P2P first\n\n    alt Direct P2P succeeds\n        Browser<<->>Peer: Direct connection (bypasses proxy entirely!)\n    else Direct P2P fails (firewall/NAT)\n        Browser->>TURN: Use TURN relay\n        TURN<<->>Peer: Relayed connection\n        Note over Browser,Peer: Higher latency, but works\n    end\n```\n\n### ICE Candidate Types\n\nICE discovers three types of candidates (possible connection endpoints), each revealing different information about your network.\n\n**Host candidates** are your local LAN IP addresses. The browser enumerates all local network interfaces and creates candidates for each. This reveals your local IP addresses on private networks, your network topology (presence of VPN interfaces, VM bridges), and the number of network interfaces.\n\n```javascript\n// Example host candidates\ncandidate:1 1 UDP 2130706431 192.168.1.100 54321 typ host\ncandidate:2 1 UDP 2130706431 10.0.0.5 54322 typ host\n```\n\nModern browsers (Chrome 75+, Firefox 78+, Safari) mitigate host candidate leaks by replacing local IP addresses with ephemeral mDNS names (e.g., `a1b2c3d4.local`) when media permissions (camera/microphone) have not been granted. However, server reflexive candidates (your public IP) remain exposed regardless of mDNS.\n\n**Server reflexive candidates** are your public IP as seen by a STUN server. The browser sends a STUN request to a public server, which replies with your public IP address. This is the leak everyone talks about: your proxy shows one IP but WebRTC reveals your real one, along with your NAT type, external port mapping, and ISP information.\n\n```javascript\n// Server reflexive candidate (your real public IP)\ncandidate:4 1 UDP 1694498815 203.0.113.45 54321 typ srflx raddr 192.168.1.100 rport 54321\n```\n\n**Relay candidates** are TURN server addresses used as fallback when direct P2P fails. The relay candidate may still contain your real IP in the `raddr` (remote address) field, depending on the TURN server implementation.\n\n```javascript\n// Relay candidate (TURN server address)\ncandidate:5 1 UDP 16777215 198.51.100.10 61234 typ relay raddr 203.0.113.45 rport 54321\n```\n\n### The STUN Protocol\n\nSTUN (Session Traversal Utilities for NAT, RFC 8489) is a simple request-response protocol over UDP. Its job is straightforward: the client asks \"what IP do you see me as?\" and the server replies with the client's public IP and port.\n\nThe client sends a Binding Request containing a magic cookie (`0x2112A442`, a fixed value defined by the RFC) and a random 12-byte transaction ID. The server responds with a Binding Success Response that includes an `XOR-MAPPED-ADDRESS` attribute containing the client's public IP and port as seen from the server's perspective.\n\nThe IP address in the response is XOR'ed with the magic cookie and transaction ID. This is not for security but for NAT compatibility: some NAT devices incorrectly modify IP addresses in packet payloads, and XOR'ing obfuscates the address to prevent this interference.\n\nCommon public STUN servers used by browsers include `stun.l.google.com:19302` (Google), `stun1.l.google.com:19302` (Google), `stun.services.mozilla.com` (Mozilla), and `stun.stunprotocol.org:3478`.\n\n### Why Proxies Cannot Stop WebRTC Leaks\n\nWebRTC leaks happen for several reinforcing reasons. First, WebRTC uses UDP, and most proxies (HTTP, HTTPS CONNECT, SOCKS4) only handle TCP. Only SOCKS5 supports UDP, and even then the browser must be explicitly configured to route WebRTC through it.\n\nSecond, WebRTC is a browser API that operates below the HTTP layer. It directly accesses the OS network stack, bypassing proxy settings configured for HTTP/HTTPS. STUN queries go directly to the network interface, and the OS routing table determines their path, not the browser's proxy configuration. Only VPN-level routing can intercept them.\n\nThird, WebRTC enumerates all network interfaces (physical ethernet, Wi-Fi, VPN adapters, VM bridges), including interfaces not used for regular browsing. This leaks your internal network topology.\n\nFinally, web pages can read ICE candidates via JavaScript using the `RTCPeerConnection.onicecandidate` event, extract IP addresses from candidate strings with a simple regex, and send your real IP to their tracking server.\n\n### Preventing WebRTC Leaks in Pydoll\n\nPydoll provides multiple strategies for preventing WebRTC IP leaks.\n\n**Method 1: Force WebRTC to only use proxied routes (recommended)**\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.webrtc_leak_protection = True  # Adds --force-webrtc-ip-handling-policy=disable_non_proxied_udp\n```\n\nPydoll provides a convenient `webrtc_leak_protection` property that manages the underlying Chrome flag for you. This disables UDP if no proxy supports it, forces WebRTC to use TURN relays only (no direct P2P), and prevents STUN queries to public servers. The trade-off is higher latency for video calls since direct P2P connections are disabled.\n\n**Method 2: Disable WebRTC entirely**\n\n```python\noptions.add_argument('--disable-features=WebRTC')\n```\n\nThis completely disables the WebRTC API, eliminating any possibility of IP leaks through this vector. The trade-off is that all WebRTC-dependent sites (video conferencing, voice calls) will break. Note that this flag should be tested with your specific Chrome version, as feature flag names can vary between releases.\n\n**Method 3: Restrict WebRTC via browser preferences**\n\n```python\noptions.browser_preferences = {\n    'webrtc': {\n        'ip_handling_policy': 'disable_non_proxied_udp',\n        'multiple_routes_enabled': False,\n        'nonproxied_udp_enabled': False,\n        'allow_legacy_tls_protocols': False\n    }\n}\n```\n\nThis achieves the same effect as Method 1 but through preferences rather than command-line flags. `multiple_routes_enabled` prevents using multiple network paths, and `nonproxied_udp_enabled` blocks UDP that does not go through the proxy.\n\n**Method 4: Use a SOCKS5 proxy with UDP support**\n\n```python\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\noptions.add_argument('--force-webrtc-ip-handling-policy=default_public_interface_only')\n```\n\nSOCKS5 can proxy UDP via its `UDP ASSOCIATE` command, allowing WebRTC's STUN queries to go through the proxy. This requires a SOCKS5 proxy that actually supports UDP relay, which not all do.\n\n!!! warning \"SOCKS5 Authentication\"\n    Chrome does not support SOCKS5 authentication inline (e.g., `socks5://user:pass@host:port`) via the `--proxy-server` flag. Pydoll provides a built-in `SOCKS5Forwarder` that works around this limitation by running a local unauthenticated SOCKS5 proxy that forwards traffic to the remote authenticated proxy, handling the username/password handshake on Chrome's behalf. See [Proxy Configuration](../../features/configuration/proxy.md) for usage details.\n\n### Testing for WebRTC Leaks\n\nYou can test manually by visiting [browserleaks.com/webrtc](https://browserleaks.com/webrtc) and checking whether your real IP appears in the \"Public IP Address\" section. If you see your real IP instead of your proxy IP, you are leaking.\n\nFor automated testing with Pydoll:\n\n```python\nimport asyncio\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def test_webrtc_leak():\n    options = ChromiumOptions()\n    options.add_argument('--proxy-server=http://proxy.example.com:8080')\n    options.add_argument('--force-webrtc-ip-handling-policy=disable_non_proxied_udp')\n\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://browserleaks.com/webrtc')\n\n        await asyncio.sleep(3)\n\n        ips = await tab.execute_script('''\n            return Array.from(document.querySelectorAll('.ip-address'))\n                .map(el => el.textContent.trim());\n        ''')\n\n        print(\"Detected IPs:\", ips)\n        # Should only show proxy IP, not your real IP\n\nasyncio.run(test_webrtc_leak())\n```\n\n!!! danger \"Always Test WebRTC Leaks\"\n    Never assume your proxy configuration prevents WebRTC leaks. Always verify with [browserleaks.com/webrtc](https://browserleaks.com/webrtc) or [ipleak.net](https://ipleak.net). Even a single WebRTC leak instantly compromises your entire proxy setup, since the website now knows your real location, ISP, and network topology.\n\n### How Websites Exploit WebRTC Leaks\n\nWebsites can intentionally trigger WebRTC to extract your real IP using a few lines of JavaScript:\n\n```javascript\nconst pc = new RTCPeerConnection({\n    iceServers: [{urls: 'stun:stun.l.google.com:19302'}]\n});\n\npc.createDataChannel('');\npc.createOffer().then(offer => pc.setLocalDescription(offer));\n\npc.onicecandidate = (event) => {\n    if (event.candidate) {\n        const ipRegex = /([0-9]{1,3}(\\.[0-9]{1,3}){3})/;\n        const ipMatch = event.candidate.candidate.match(ipRegex);\n\n        if (ipMatch) {\n            const realIP = ipMatch[1];\n            fetch(`/track?real_ip=${realIP}&proxy_ip=${window.clientIP}`);\n        }\n    }\n};\n```\n\nThis code creates an RTCPeerConnection, triggers ICE candidate gathering (which contacts STUN servers), extracts IP addresses from the candidates with a regex, and sends your real IP to a tracking server. Disabling WebRTC or forcing proxied-only routes as described above prevents this.\n\n## Summary\n\nProxies operate at specific layers of the network stack: HTTP at Layer 7, SOCKS at Layer 5. The layer determines what the proxy can see, modify, and hide. TCP fingerprints (window size, options, TTL) leak from lower layers and reveal your real OS even through a proxy. UDP traffic, including WebRTC and DNS, often bypasses proxies unless explicitly configured. WebRTC is the most common source of IP leakage, and only SOCKS5 or a VPN can proxy UDP traffic effectively. Modern browsers also use QUIC (HTTP/3 over UDP), which adds another potential bypass vector.\n\n**Next steps:**\n\n- [HTTP/HTTPS Proxies](./http-proxies.md): Application-layer proxying\n- [SOCKS Proxies](./socks-proxies.md): Session-layer, protocol-agnostic proxying\n- [Network Fingerprinting](../fingerprinting/network-fingerprinting.md): TCP/IP and TLS fingerprinting techniques\n- [Proxy Configuration](../../features/configuration/proxy.md): Practical Pydoll proxy setup\n\n## References\n\n- RFC 793: Transmission Control Protocol (TCP) - https://tools.ietf.org/html/rfc793\n- RFC 768: User Datagram Protocol (UDP) - https://tools.ietf.org/html/rfc768\n- RFC 8489: Session Traversal Utilities for NAT (STUN) - https://tools.ietf.org/html/rfc8489\n- RFC 8445: Interactive Connectivity Establishment (ICE) - https://tools.ietf.org/html/rfc8445\n- RFC 8656: Traversal Using Relays around NAT (TURN) - https://tools.ietf.org/html/rfc8656\n- RFC 6528: Defending Against Sequence Number Attacks - https://tools.ietf.org/html/rfc6528\n- RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport - https://tools.ietf.org/html/rfc9000\n- W3C WebRTC 1.0: Real-Time Communication Between Browsers - https://www.w3.org/TR/webrtc/\n- BrowserLeaks: WebRTC Leak Test - https://browserleaks.com/webrtc\n- IPLeak: Comprehensive Leak Testing - https://ipleak.net\n"
  },
  {
    "path": "docs/en/deep-dive/network/proxy-detection.md",
    "content": "# Proxy Detection\n\nProxy detection is a probabilistic process. Websites combine dozens of signals to assess whether a connection is proxied, ranging from simple IP reputation lookups to TCP/IP stack analysis and behavioral profiling. No single signal provides definitive proof, but combining enough weak signals produces high-confidence decisions.\n\nThis document covers the main detection techniques, how they work at a technical level, and what they mean for browser automation with Pydoll.\n\n!!! info \"Module Navigation\"\n    - [SOCKS Proxies](./socks-proxies.md): Session-layer proxying\n    - [HTTP/HTTPS Proxies](./http-proxies.md): Application-layer proxying\n    - [Network Fundamentals](./network-fundamentals.md): TCP/IP, UDP, WebRTC\n\n    For fingerprinting details, see [Network Fingerprinting](../fingerprinting/network-fingerprinting.md) and [Browser Fingerprinting](../fingerprinting/browser-fingerprinting.md).\n\n## IP Reputation\n\nIP reputation analysis is the most widely deployed proxy detection technique. It combines publicly available data (ASN records, WHOIS, geolocation databases) with proprietary intelligence to classify IP addresses into risk categories.\n\n### ASN Classification\n\nEvery IP address belongs to an Autonomous System (AS), identified by an ASN. The type of AS that owns an IP is the strongest single indicator of whether it is a proxy.\n\nIPs belonging to cloud and hosting providers (AWS, DigitalOcean, OVH, Hetzner) are flagged as high risk because real users do not browse the web from datacenter servers. IPs from residential ISPs (Comcast, Deutsche Telekom, BT) are low risk because they look like normal home connections. Mobile carrier IPs (Verizon Wireless, AT&T Mobility) are the lowest risk because they are the hardest to distinguish from real mobile users.\n\nSome ASNs are associated with known proxy infrastructure, though this is more nuanced than it might seem. Large residential proxy providers like BrightData or Smartproxy do not operate their own ASNs; they route traffic through real residential IPs belonging to ISP ASNs. This is precisely what makes residential proxies harder to detect than datacenter proxies.\n\nDetection systems query ASN databases (Team Cymru, RIPE NCC, ARIN) and commercial IP intelligence APIs to classify each connecting IP. Datacenter IPs are detected with roughly 95%+ accuracy because the ASN classification is unambiguous. Residential proxy detection is much harder (roughly 40-70% accuracy) because the IPs genuinely belong to ISPs. Mobile proxy detection is the most difficult (roughly 20-40%) because mobile carrier NAT makes many real users share IPs.\n\nThis accuracy gradient is why residential and mobile proxies command 10-100x the price of datacenter proxies.\n\n### Known Proxy Databases\n\nBeyond ASN classification, specialized databases track IPs that have been observed participating in proxy networks. Services like IPQualityScore, proxycheck.io, and Spur.us maintain real-time databases of known proxy, VPN, and Tor exit node IPs. The Tor exit node list is publicly available at [check.torproject.org](https://check.torproject.org/torbulkexitlist).\n\nThese databases also track behavioral signals: IPs that rotate frequently (typical of proxy pools), IPs with abnormally high concurrent session counts (a residential IP normally has 1-5 concurrent connections, not 100+), and IPs previously associated with bot-like activity.\n\n### Geolocation Consistency\n\nProxies often reveal themselves through geographic inconsistencies. The IP address points to one location, but browser-reported signals point to another.\n\nThe most common mismatches are between the IP's geolocation and the browser's timezone (collected via JavaScript's `Intl.DateTimeFormat().resolvedOptions().timeZone`), between the IP's country and the `Accept-Language` header, and between the current session's location and a previous session's location. A user appearing in Los Angeles with a browser timezone of `Europe/Berlin` is suspicious. A user appearing in Tokyo 10 minutes after their last session was in New York is physically impossible.\n\nDetection systems also check whether the IP's geolocation matches the locale configuration of the browser. A US datacenter IP with `Accept-Language: zh-CN` and timezone `Asia/Shanghai` strongly suggests a Chinese user routing through a US proxy.\n\n!!! note \"False Positives\"\n    Legitimate scenarios trigger geolocation alarms: travelers using VPNs, expats with browser settings from their home country, corporate users connecting through company VPNs, and multilingual users with non-default language preferences. Sophisticated systems use risk scoring rather than binary blocking to account for these cases.\n\n## HTTP Header Analysis\n\nHTTP headers are the simplest detection vector. Transparent and anonymous proxies add headers like `Via`, `X-Forwarded-For`, `X-Real-IP`, and `Forwarded` (RFC 7239) that directly reveal proxy usage. Elite proxies strip these headers, but their absence alone is not proof of a direct connection.\n\nDetection goes beyond looking for proxy-specific headers. Missing headers that real browsers always send (like `Accept-Language`, `Accept-Encoding`, or a realistic `User-Agent`) are suspicious. Header ordering matters too: browsers send headers in a consistent, version-specific order, and proxies or automation tools that construct headers manually often get the order wrong.\n\nThe legacy `Proxy-Connection: keep-alive` header, sent by some older clients when routing through a proxy, is another classic detection signal.\n\n### Proxy Anonymity Levels\n\nProxies are traditionally classified into three anonymity levels based on their header behavior:\n\n| Level | Behavior | Detection |\n|-------|----------|-----------|\n| Transparent | Forwards your real IP in `X-Forwarded-For`, adds `Via` header | Trivial |\n| Anonymous | Hides your IP but adds `Via` or other proxy headers | Easy |\n| Elite | Strips all proxy-identifying headers | Requires deeper analysis |\n\nThis classification dates from an era when header analysis was the primary detection method. Modern detection systems use IP reputation, fingerprinting, and behavioral analysis, making the transparent/anonymous/elite distinction less meaningful. An elite proxy with a datacenter IP is detected instantly through ASN lookup. A transparent proxy on a residential IP might still pass under the radar on less sophisticated sites.\n\n## Network Fingerprinting\n\nNetwork-layer fingerprinting operates below the proxy layer, which means it can detect proxies even when the proxy itself is configured perfectly.\n\n### TCP/IP Fingerprinting\n\nEvery operating system has a unique TCP stack implementation that reveals itself during the TCP handshake. The initial window size, TCP options order, TTL (Time To Live), and window scale factor are all set by the kernel, not the browser, and cannot be changed by a proxy.\n\nDetection systems compare these TCP characteristics against the `User-Agent` header. If the User-Agent claims Windows 10 but the TCP fingerprint shows Linux characteristics (TTL of 64, window size of 29200), the mismatch is a strong proxy indicator. Windows uses a default TTL of 128 and modern builds typically show a window size of 65535, while Linux uses TTL 64 and window sizes around 29200.\n\nTTL analysis adds another layer. The TTL decreases by 1 at each network hop. If a Windows connection arrives with a TTL of 128, the client is likely on the same network. If it arrives with a TTL of 115, it has crossed roughly 13 hops. If the TTL value does not align with the expected hop count for the IP's geographic location, proxy routing is likely.\n\nFor detailed TCP fingerprint values and their implications, see [Network Fingerprinting](../fingerprinting/network-fingerprinting.md).\n\n### TLS Fingerprinting (JA3/JA4)\n\nThe TLS ClientHello message is transmitted in plaintext and contains enough parameters to uniquely identify the client application: TLS version, supported cipher suites, extensions, elliptic curves, and signature algorithms. The JA3 fingerprint is an MD5 hash of these parameters concatenated in a specific order. JA4 is a newer, more granular alternative.\n\nEach browser version produces a distinctive JA3/JA4 fingerprint. Detection systems maintain databases of known fingerprints for Chrome, Firefox, Safari, and other browsers. If the JA3 fingerprint does not match any known browser, or does not match the browser claimed in the User-Agent, the connection is flagged.\n\nAn important nuance: SOCKS5 proxies and HTTP CONNECT tunnels pass the TLS ClientHello through unmodified, so the target server sees the real browser fingerprint. The proxy does not alter TLS parameters in these configurations. Only MITM proxies (which terminate and re-establish TLS) change the fingerprint, and in that case the fingerprint belongs to the proxy software, not a real browser, which is itself a detection signal.\n\n### HTTP/2 Fingerprinting\n\nHTTP/2 connections expose fingerprinting signals that are distinct from TLS. The SETTINGS frame sent at the beginning of an HTTP/2 connection contains parameters like `HEADER_TABLE_SIZE`, `MAX_CONCURRENT_STREAMS`, `INITIAL_WINDOW_SIZE`, and `MAX_HEADER_LIST_SIZE`. Each browser uses different default values for these settings.\n\nThe order and priority of pseudo-headers (`:method`, `:authority`, `:scheme`, `:path`), the HPACK compression behavior, and stream priority weights also vary between browsers. Tools like [browserleaks.com/http2](https://browserleaks.com/http2) show what your HTTP/2 fingerprint looks like.\n\nAutomation frameworks and proxy software that implement their own HTTP/2 stacks often produce fingerprints that do not match any real browser, making this an effective detection vector.\n\n### Latency-Based Detection\n\nThe network latency between a client and a server reveals information about the physical network path. If the IP geolocates to New York but the round-trip time suggests a path through Asia, the connection is likely proxied.\n\nDetection systems measure RTT (round-trip time) during the TCP handshake and compare it against expected latencies for the IP's geographic location. They may also issue JavaScript-based timing challenges that measure latency from the browser's perspective, then compare this with the server-observed latency. A significant discrepancy between the two suggests an intermediary (proxy) in the path.\n\nClock skew analysis adds another dimension: by measuring the client's clock offset via JavaScript (`Date.now()`) or HTTP `Date` headers, detection systems can infer the client's actual timezone and compare it against the IP's expected timezone.\n\n## Behavioral Detection\n\nThe most advanced detection systems go beyond network and protocol analysis to examine user behavior. This includes request timing (are requests evenly spaced, suggesting automation?), mouse movement patterns (analyzed via JavaScript event listeners), scrolling behavior, keyboard input cadence, and overall browsing patterns.\n\nMachine learning models trained on millions of real user sessions can distinguish human behavior from automation with high accuracy. These models typically combine 50+ features including navigation patterns, session duration distribution, click positions, form interaction timing, and JavaScript execution characteristics.\n\nPydoll's humanized interactions (Bezier curve mouse movement, Fitts's Law timing, realistic typing) are designed specifically to pass behavioral analysis. See [Evasion Techniques](../fingerprinting/evasion-techniques.md) for the full multi-layer evasion strategy.\n\n## Multi-Signal Risk Scoring\n\nModern detection systems do not rely on any single technique. They combine all available signals into a risk score, typically 0-100, and apply thresholds that vary by industry and context.\n\nThe weight of each signal category varies, but a rough approximation is that IP reputation accounts for the largest share (it is the cheapest and most reliable signal), followed by network fingerprinting (TCP/IP, TLS, HTTP/2), header and protocol analysis, behavioral scoring, and consistency checks (geolocation, timezone, language).\n\nThresholds depend on the business context. Banking sites block aggressively (risk score above 50), e-commerce sites present CAPTCHAs at moderate scores (above 70), and content sites tend to be more permissive (blocking only above 80) since they rely on ad impressions.\n\nThe implication for automation is that passing one layer of detection is not enough. A residential IP (good IP reputation) with a mismatched TCP fingerprint and robotic behavior will still be flagged. Effective evasion requires consistency across all layers.\n\n## Detection by Proxy Type\n\n| Proxy Type | Detection Difficulty | Primary Detection Methods |\n|------------|----------------------|---------------------------|\n| Transparent HTTP | Trivial | HTTP headers (`Via`, `X-Forwarded-For`) |\n| Anonymous HTTP | Easy | HTTP headers + IP reputation |\n| Elite HTTP (datacenter) | Medium | IP reputation (ASN analysis) |\n| Datacenter SOCKS5 | Medium | IP reputation (ASN analysis) |\n| Residential proxies | Difficult | Behavioral analysis, connection patterns, latency |\n| Mobile proxies | Very difficult | Mostly behavioral, limited network signals |\n| Rotating proxies | Difficult | Session inconsistencies, IP rotation patterns |\n\n## Evasion Principles\n\nEffective evasion is about consistency across all detection layers, not perfecting any single one.\n\nUse residential or mobile IPs when stealth matters. They are harder to detect because the IPs genuinely belong to ISPs, and the cost premium reflects this advantage. Match the browser's geolocation signals (timezone, language, locale) to the proxy IP's location. Maintain session persistence by not rotating IPs mid-session, which creates detectable discontinuities. Ensure your TCP/IP fingerprint matches your User-Agent claim by running automation on the same OS you are impersonating. Use Pydoll's humanized interactions to pass behavioral analysis. And always test for leaks (WebRTC, DNS, timezone) before running automation at scale.\n\nThe goal is not to make detection impossible but to make it expensive and uncertain. Force the detection system to use multiple correlated signals, blend in with legitimate traffic patterns, and create plausible deniability.\n\n!!! warning \"No Proxy is Undetectable\"\n    With sufficient resources, any proxy can be detected. Even top-tier residential proxies achieve roughly 70-90% success rates against sophisticated anti-bot systems like Akamai, Cloudflare Enterprise, and DataDome. The practical question is whether detection is economically worthwhile for the target site.\n\n**Next steps:**\n\n- [Network Fingerprinting](../fingerprinting/network-fingerprinting.md): TCP/IP and TLS fingerprinting in detail\n- [Browser Fingerprinting](../fingerprinting/browser-fingerprinting.md): Canvas, WebGL, HTTP/2 fingerprinting\n- [Evasion Techniques](../fingerprinting/evasion-techniques.md): Multi-layer evasion strategy\n- [Proxy Configuration](../../features/configuration/proxy.md): Practical Pydoll proxy setup\n\n## References\n\n- MaxMind GeoIP2: https://www.maxmind.com/en/geoip2-services-and-databases\n- IPQualityScore Proxy Detection: https://www.ipqualityscore.com/proxy-vpn-tor-detection-service\n- Spur.us (Anonymous IP Detection): https://spur.us/\n- Team Cymru IP to ASN Mapping: https://www.team-cymru.com/ip-asn-mapping\n- Salesforce Engineering: TLS Fingerprinting with JA3 and JA3S - https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/\n- Akamai: Passive Fingerprinting of HTTP/2 Clients (Black Hat EU 2017) - https://blackhat.com/docs/eu-17/materials/eu-17-Shuster-Passive-Fingerprinting-Of-HTTP2-Clients-wp.pdf\n- Incolumitas: TCP/IP Fingerprinting for VPN and Proxy Detection - https://incolumitas.com/2021/03/13/tcp-ip-fingerprinting-for-vpn-and-proxy-detection/\n- Incolumitas: Detecting Proxies and VPNs with Latencies - https://incolumitas.com/2021/06/07/detecting-proxies-and-vpn-with-latencies/\n- BrowserLeaks HTTP/2 Fingerprint: https://browserleaks.com/http2\n- BrowserLeaks IP: https://browserleaks.com/ip\n- RFC 7239: Forwarded HTTP Extension - https://www.rfc-editor.org/rfc/rfc7239.html\n- RFC 9110: HTTP Semantics - https://www.rfc-editor.org/rfc/rfc9110.html\n"
  },
  {
    "path": "docs/en/deep-dive/network/proxy-legal.md",
    "content": "# Legal and Ethical Considerations\n\nThis document provides **general information** about the legal and ethical landscape of proxy usage and web automation. Laws vary wildly by jurisdiction and use case. This is **not legal advice**. Always consult qualified legal counsel for your specific situation.\n\n!!! info \"Module Navigation\"\n    - **[← Building Proxies](./build-proxy.md)** - Implementation and advanced topics\n    - **[← Proxy Detection](./proxy-detection.md)** - Anonymity and evasion\n    - **[← Network & Security Overview](./index.md)** - Module introduction\n    \n    For responsible automation, see **[Behavioral Captcha Bypass](../../features/advanced/behavioral-captcha-bypass.md)** and **[Human-Like Interactions](../../features/automation/human-interactions.md)**.\n\n!!! danger \"Legal Disclaimer\"\n    This document provides **educational information only**. It is **not legal advice**. Laws regarding web scraping, automation, and proxy usage vary by jurisdiction and are subject to interpretation. Consult qualified legal counsel before engaging in activities that may have legal implications.\n\n## Legal and Ethical Considerations\n\nProxy usage sits at the intersection of privacy, security, and compliance. Understanding the legal landscape is essential for responsible automation.\n\n### Regulatory Compliance\n\nDifferent jurisdictions have varying rules regarding proxy usage and data collection:\n\n| Region | Key Regulation | Proxy Implications |\n|--------|----------------|-------------------|\n| **European Union** | GDPR | IP addresses are personal data; proxy exit nodes in EU must comply |\n| **United States** | CFAA, State Laws | Circumventing access controls may violate computer fraud laws |\n| **China** | Cybersecurity Law | VPN/proxy usage heavily regulated; only approved services permitted |\n| **Russia** | VPN Law | VPN providers must register and log user activity |\n| **Australia** | Privacy Act | Data collection through proxies subject to privacy principles |\n\n**GDPR-specific considerations:**\n\n**IP addresses as personal data (Article 4):**\n\nWhen scraping EU-based websites through proxies:\n\n- Your proxy's EU IP is considered personal data\n- Websites must handle it per GDPR requirements  \n- You must have lawful basis for data collection\n- Data minimization principle applies\n\n**Lawful bases for processing (Article 6):**\n\n1. **Consent** - Hard to obtain for scraping\n2. **Contract** - Legitimate if you're a customer\n3. **Legal obligation** - Rare for scraping use cases\n4. **Vital interests** - Not applicable to scraping\n5. **Public task** - Not applicable to scraping\n6. **Legitimate interests** - Most applicable for scraping (balance test required)\n\n### Terms of Service and Access Restrictions\n\nProxies don't exempt you from website ToS:\n\n**Common ToS violations:**\n\n1. **Automated Access**: Many sites prohibit bots/scrapers regardless of IP\n2. **Rate Limiting Circumvention**: Using rotating proxies to bypass rate limits\n3. **Geographic Restrictions**: Bypassing geo-blocks may violate content licensing agreements\n4. **Account Sharing**: Using proxies to mask multiple users as one\n\n**Legal precedent examples:**\n\n```python\n# Notable cases (simplified, not legal advice)\ncases = {\n    'hiQ Labs v. LinkedIn (2022)': {\n        'issue': 'Scraping public data after access revoked',\n        'outcome': 'Scraping publicly available data generally permitted',\n        'caveat': 'But circumventing technological barriers may violate CFAA'\n    },\n    \n    'QVC v. Resultly (2020)': {\n        'issue': 'Aggressive scraping causing server load',\n        'outcome': 'Excessive requests constitute trespass to chattels',\n        'implication': 'Volume and impact matter, not just technical access'\n    }\n}\n```\n\n### Ethical Guidelines for Proxy Usage\n\nBeyond legal compliance, consider these ethical principles:\n\n**1. Respect robots.txt**\n```python\n# Even with proxies, honor site guidelines\nasync def ethical_scraping(url):\n    # Check robots.txt regardless of proxy anonymity\n    if not is_allowed_by_robots(url):\n        return None  # Respect the site's wishes\n```\n\n**2. Rate Limiting**\n```python\n# Don't abuse proxy rotation to overwhelm servers\nMINIMUM_DELAY = 1.0  # seconds between requests\nMAX_CONCURRENT = 5   # concurrent connections per site\n\n# Bad: Rotating proxies to scrape at 1000 req/sec\n# Good: Respectful scraping even with proxy rotation\n```\n\n**3. Transparency**\n```python\n# Identify yourself in User-Agent when appropriate\nheaders = {\n    'User-Agent': 'MyBot/1.0 (contact@example.com)',  # Honest identification\n    # Not: 'Mozilla/5.0...'  # Deceptive when not a browser\n}\n```\n\n**4. Data Minimization**\n```python\n# Collect only what you need\n# Just because you can scrape everything doesn't mean you should\ndata_to_collect = {\n    'product_name': True,\n    'price': True,\n    'user_emails': False,      # PII - don't collect unless necessary\n    'user_addresses': False,   # PII - privacy concerns\n}\n```\n\n### Compliance Checklist\n\nBefore deploying proxy-based automation:\n\n- [ ] **Legal Review**: Consult legal counsel for your jurisdiction\n- [ ] **ToS Compliance**: Review target website terms of service\n- [ ] **Data Protection**: Ensure GDPR/CCPA compliance if handling personal data\n- [ ] **Access Rights**: Verify you have permission to access the data\n- [ ] **Rate Limiting**: Implement respectful request rates\n- [ ] **Error Handling**: Handle 429 (Too Many Requests) appropriately\n- [ ] **Logging**: Maintain audit trails for compliance purposes\n- [ ] **Data Retention**: Implement appropriate data retention/deletion policies\n- [ ] **Security**: Protect collected data with appropriate measures\n- [ ] **Transparency**: Be honest about your scraping activities when questioned\n\n!!! warning \"This is Not Legal Advice\"\n    This section provides general information only. Proxy usage legality varies by jurisdiction, context, and specific circumstances. Always consult qualified legal counsel for your specific situation.\n\n!!! tip \"Responsible Proxy Usage\"\n    The most defensible proxy usage is:\n    \n    - **Transparent**: You can explain why you're doing it\n    - **Necessary**: You have a legitimate reason (research, monitoring, etc.)\n    - **Proportional**: Your methods match your needs (not excessive)\n    - **Documented**: You keep records of your activities\n    - **Compliant**: You follow all applicable laws and ToS\n\n### When to Avoid Proxies\n\nSome scenarios where proxy usage is problematic:\n\n| Scenario | Risk | Alternative |\n|----------|------|-------------|\n| **Banking/Financial Sites** | Fraud detection, account suspension | Use legitimate access only |\n| **Government Portals** | Legal penalties, security investigations | Direct access from authorized locations |\n| **Healthcare Data** | HIPAA violations, severe penalties | Use authorized API access |\n| **Internal Corporate Systems** | Policy violations, termination | Follow company IT policies |\n| **E-commerce Account Creation** | Fraud flags, permanent bans | Use single, verified identity |\n\n## Conclusion\n\nUnderstanding proxy architecture deeply enables you to:\n\n**Make Informed Decisions:**\n- Choose the right proxy type for your use case\n- Understand security implications\n- Identify when proxies are necessary vs optional\n\n**Troubleshoot Effectively:**\n- Debug connection issues\n- Identify DNS leaks or IP leakage\n- Diagnose performance problems\n\n**Optimize Performance:**\n- Configure appropriate timeouts\n- Implement connection pooling\n- Monitor proxy health\n\n**Build Better Automation:**\n- Combine proxies with anti-detection techniques\n- Implement robust error handling\n- Scale proxy usage efficiently\n\nThe proxy landscape is complex, but with this foundation, you're equipped to navigate it successfully.\n\n## Further Reading\n\n- **[RFC 1928](https://tools.ietf.org/html/rfc1928)**: SOCKS5 Protocol specification\n- **[RFC 1929](https://tools.ietf.org/html/rfc1929)**: SOCKS5 Username/Password Authentication\n- **[RFC 2616](https://tools.ietf.org/html/rfc2616)**: HTTP/1.1 (CONNECT method)\n- **[RFC 5389](https://tools.ietf.org/html/rfc5389)**: STUN Protocol\n- **[RFC 9298](https://tools.ietf.org/html/rfc9298)**: CONNECT-UDP (HTTP/3 proxying)\n- **[Proxy Configuration Guide](../features/configuration/proxy.md)**: Practical Pydoll proxy usage, authentication, rotation, and testing\n- **[Request Interception](../features/network/interception.md)**: How Pydoll implements proxy authentication internally\n- **[Network Capabilities Deep Dive](./network-capabilities.md)**: How Pydoll handles network operations\n\n!!! tip \"Experimentation\"\n    The best way to truly understand proxies is to:\n    \n    1. Set up your own proxy server (use the code above)\n    2. Capture traffic with Wireshark to see raw packets\n    3. Test different proxy types with real automation\n    4. Intentionally create leaks and learn to detect them\n    \n    Hands-on experience solidifies theoretical knowledge!\n\n"
  },
  {
    "path": "docs/en/deep-dive/network/socks-proxies.md",
    "content": "# SOCKS Protocol Architecture\n\nSOCKS (SOCKet Secure) is a proxying protocol that operates between the transport and application layers of the network stack (commonly described as Layer 5 in the OSI model). Unlike HTTP proxies, which parse and understand HTTP traffic, SOCKS proxies forward raw TCP and UDP connections without inspecting their content. This protocol-agnostic design makes SOCKS the preferred choice for privacy-focused automation: the proxy never needs to parse your requests, inject headers, or terminate TLS connections.\n\nThis document covers how SOCKS works at the protocol level, the differences between SOCKS4 and SOCKS5, authentication handling in Chrome, DNS resolution behavior, and practical configuration in Pydoll.\n\n!!! info \"Module Navigation\"\n    - [HTTP/HTTPS Proxies](./http-proxies.md): Application-layer proxying\n    - [Network Fundamentals](./network-fundamentals.md): TCP/IP, UDP, OSI model\n    - [Network & Security Overview](./index.md): Module introduction\n    - [Proxy Detection](./proxy-detection.md): Anonymity levels and detection evasion\n    - [Building Proxies](./build-proxy.md): SOCKS5 implementation from scratch\n\n    For practical configuration, see [Proxy Configuration](../../features/configuration/proxy.md).\n\n## How SOCKS Differs from HTTP Proxies\n\nThe fundamental difference lies in what each proxy can see and do. An HTTP proxy operates at the application layer and understands HTTP: it can read URLs, headers, cookies, and request bodies (for unencrypted traffic), modify them in transit, cache responses, and inject its own headers like `Via` and `X-Forwarded-For`. This is powerful for content filtering but means you must trust the proxy operator with your application data.\n\nA SOCKS proxy operates below the application layer. It sees only the destination address, port, and the volume of data being transferred. It does not parse, modify, or even understand what protocol is flowing through it. HTTP, HTTPS, FTP, SSH, WebSocket, or any custom protocol all look the same to a SOCKS proxy: just bytes being relayed between two endpoints.\n\nThis has a direct practical implication. When you send an HTTPS request through a SOCKS5 proxy, the proxy sees `example.com:443` and the encrypted TLS stream. It cannot read the URL, headers, cookies, or response content. It does not add identifying headers. It does not need to terminate TLS. The encrypted tunnel runs end-to-end between your browser and the target server.\n\nHowever, it is important to understand what SOCKS does not provide. SOCKS is a proxying protocol, not an encryption protocol. The name \"SOCKet Secure\" refers to secure firewall traversal, not cryptographic security. If you send unencrypted HTTP traffic through a SOCKS5 proxy, the proxy operator can read the bytes passing through, even though the proxy is not designed to inspect them. For actual encryption, you need TLS/HTTPS on top of SOCKS, or an encrypted tunnel (SSH, VPN) wrapping the SOCKS connection.\n\n!!! note \"Trust Model\"\n    With HTTP proxies, you trust the proxy operator not to log your browsing history, steal tokens, modify responses, or perform MITM attacks. With SOCKS5, you trust the proxy only to forward packets correctly and not log connection metadata. The attack surface is smaller, but it is not zero.\n\n## SOCKS4 vs SOCKS5\n\nSOCKS has two versions in common use. SOCKS4 was developed by NEC in the early 1990s as an informal standard with no RFC. SOCKS5 was standardized as RFC 1928 in 1996 to address SOCKS4's limitations.\n\n| Feature | SOCKS4 | SOCKS5 |\n|---------|--------|--------|\n| Standard | No official RFC (de facto, 1992) | RFC 1928 (1996) |\n| Authentication | Identification only (USERID field, no password) | Multiple methods (none, username/password, GSSAPI) |\n| IP version | IPv4 only | IPv4 and IPv6 |\n| UDP support | No | Yes (UDP ASSOCIATE command) |\n| DNS resolution | Client-side (SOCKS4A extension adds server-side) | Server-side when using domain names (ATYP=0x03) |\n| Protocol support | TCP only | TCP and UDP |\n\nSOCKS5 is superior in every practical way. Use SOCKS4 only if the proxy does not support SOCKS5.\n\n## The SOCKS5 Handshake\n\nThe SOCKS5 connection process follows RFC 1928 and consists of three phases: method negotiation, optional authentication, and the connection request.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant SOCKS5 as SOCKS5 Proxy\n    participant Server as Target Server\n\n    Note over Client,SOCKS5: Phase 1: Method Negotiation\n    Client->>SOCKS5: Hello [VER=5, NMETHODS, METHODS]\n    SOCKS5->>Client: Method Selected [VER=5, METHOD]\n\n    Note over Client,SOCKS5: Phase 2: Authentication (if required)\n    Client->>SOCKS5: Auth Request [VER=1, ULEN, UNAME, PLEN, PASSWD]\n    SOCKS5->>Client: Auth Response [VER=1, STATUS]\n\n    Note over Client,SOCKS5: Phase 3: Connection Request\n    Client->>SOCKS5: Connect [VER=5, CMD=CONNECT, DST.ADDR, DST.PORT]\n    SOCKS5->>Server: Establish TCP connection\n    Server-->>SOCKS5: Connection established\n    SOCKS5->>Client: Reply [VER=5, REP=SUCCESS, BND.ADDR, BND.PORT]\n\n    Note over Client,Server: Data relay (proxied)\n    Client->>SOCKS5: Application data\n    SOCKS5->>Server: Forward data\n    Server->>SOCKS5: Response data\n    SOCKS5->>Client: Forward response\n```\n\n### Phase 1: Method Negotiation\n\nThe client opens a TCP connection to the proxy and sends a greeting containing the protocol version (always `0x05` for SOCKS5) and a list of authentication methods it supports.\n\n```python\n# Client Hello\n[\n    0x05,        # VER: Protocol version (5)\n    0x02,        # NMETHODS: Number of methods offered\n    0x00, 0x02   # METHODS: No auth (0x00) and Username/Password (0x02)\n]\n```\n\nThe proxy responds with the method it selects. If the proxy requires authentication and the client offered `0x02` (username/password), the proxy selects it. If no acceptable method was offered, the proxy responds with `0xFF` and closes the connection.\n\n```python\n# Server response\n[\n    0x05,   # VER: Protocol version (5)\n    0x02    # METHOD: Username/Password selected\n]\n```\n\nMethod codes defined by RFC 1928: `0x00` = no authentication, `0x01` = GSSAPI, `0x02` = username/password (RFC 1929), `0x03-0x7F` = IANA assigned, `0x80-0xFE` = reserved for private methods, `0xFF` = no acceptable methods.\n\n### Phase 2: Authentication\n\nIf the proxy selected method `0x02`, the client sends credentials following RFC 1929. The subnegotiation uses its own version number (`0x01`, not `0x05`).\n\n```python\n# Client authentication\n[\n    0x01,              # VER: Subnegotiation version (1)\n    len(username),     # ULEN: Username length (max 255)\n    *username_bytes,   # UNAME: Username\n    len(password),     # PLEN: Password length (max 255)\n    *password_bytes    # PASSWD: Password\n]\n\n# Server response\n[\n    0x01,   # VER: Subnegotiation version (1)\n    0x00    # STATUS: 0 = success, non-zero = failure\n]\n```\n\nCredentials are transmitted in plaintext during this handshake. This is inherent to the SOCKS5 protocol (RFC 1929). For sensitive environments, wrap the SOCKS connection in an SSH tunnel or VPN.\n\n### Phase 3: Connection Request\n\nAfter authentication succeeds (or if no authentication was required), the client sends a connection request specifying the command, destination address, and port.\n\n```python\n[\n    0x05,          # VER: Protocol version (5)\n    0x01,          # CMD: 1=CONNECT, 2=BIND, 3=UDP ASSOCIATE\n    0x00,          # RSV: Reserved\n    0x03,          # ATYP: 1=IPv4 (4 bytes), 3=Domain (length+name), 4=IPv6 (16 bytes)\n    len(domain),   # Domain length (only for ATYP=0x03)\n    *domain_bytes, # Domain name\n    *port_bytes    # Port (2 bytes, big-endian)\n]\n```\n\nThe address type (ATYP) determines the format: `0x01` means 4 bytes of IPv4 address follow, `0x04` means 16 bytes of IPv6, and `0x03` means a length byte followed by the domain name. When the client sends a domain name (ATYP=0x03), the proxy resolves DNS on its side, which prevents DNS leaks to the client's local network.\n\nThe proxy connects to the destination and responds with a reply:\n\n```python\n[\n    0x05,       # VER: Protocol version (5)\n    0x00,       # REP: 0x00=success, 0x01-0x08=various errors\n    0x00,       # RSV: Reserved\n    0x01,       # ATYP: Address type of bound address\n    *bind_addr, # BND.ADDR: Address the proxy bound to\n    *bind_port  # BND.PORT: Port the proxy bound to\n]\n```\n\nReply codes: `0x00` succeeded, `0x01` general failure, `0x02` connection not allowed, `0x03` network unreachable, `0x04` host unreachable, `0x05` connection refused, `0x06` TTL expired, `0x07` command not supported, `0x08` address type not supported.\n\nAfter a successful reply, the proxy begins relaying data bidirectionally. The entire SOCKS5 handshake is a binary protocol, making it more efficient than text-based HTTP but harder to debug without hex dumps.\n\n## UDP Support\n\nSOCKS5 supports UDP proxying through the `UDP ASSOCIATE` command (CMD=0x03). This works differently from TCP proxying: the client sends a UDP ASSOCIATE request over the TCP control connection, and the proxy responds with a relay address and port. The client then sends UDP datagrams to this relay, and the proxy forwards them to their destinations.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant SOCKS5\n    participant UDP_Server as UDP Server\n\n    Note over Client,SOCKS5: TCP control connection (handshake + auth)\n    Client->>SOCKS5: UDP ASSOCIATE request (CMD=0x03)\n    SOCKS5->>Client: Relay address and port\n\n    Note over Client,SOCKS5: UDP data transfer\n    Client->>SOCKS5: UDP datagram to relay\n    SOCKS5->>UDP_Server: Forward datagram\n    UDP_Server->>SOCKS5: Response datagram\n    SOCKS5->>Client: Forward response\n\n    Note over Client,SOCKS5: TCP control connection stays open\n```\n\nEach UDP datagram sent through the relay includes a small header with the destination address and port:\n\n```python\n[\n    0x00, 0x00,    # RSV: Reserved\n    0x00,          # FRAG: Fragment number (0 = no fragmentation)\n    0x01,          # ATYP: Address type\n    *dst_addr,     # DST.ADDR: Destination address\n    *dst_port,     # DST.PORT: Destination port\n    *data          # DATA: Application data\n]\n```\n\nThe TCP control connection must remain open for the duration of the UDP association. If it closes, the proxy drops the UDP relay.\n\n!!! warning \"UDP in Chrome\"\n    Chrome does not use SOCKS5 UDP ASSOCIATE for any traffic. Even when configured with a SOCKS5 proxy, Chrome only proxies TCP connections. WebRTC, DNS-over-UDP, and other UDP traffic are not routed through the SOCKS5 proxy. This means WebRTC IP leaks are still possible with SOCKS5 in Chrome. Use `--force-webrtc-ip-handling-policy=disable_non_proxied_udp` or Pydoll's `webrtc_leak_protection = True` to mitigate this. For more details, see [Network Fundamentals: WebRTC and IP Leakage](./network-fundamentals.md#webrtc-and-ip-leakage).\n\n!!! tip \"Modern UDP Proxying Alternatives\"\n    For scenarios requiring full UDP support beyond what Chrome's SOCKS5 implementation provides, consider Shadowsocks (encrypted SOCKS-like protocol with native UDP), WireGuard (VPN with excellent performance), or V2Ray/VMess (flexible proxy framework with comprehensive UDP handling).\n\n## DNS Resolution\n\nA common misconception is that HTTP proxies leak DNS queries while SOCKS5 proxies do not. The reality in Chrome is more nuanced.\n\nWhen Chrome is configured with any proxy (HTTP, HTTPS, or SOCKS5), it sends hostnames to the proxy rather than resolving DNS locally. For HTTP proxies, the hostname appears in the `CONNECT host:443` request. For SOCKS5, it appears in the connection request with ATYP=0x03 (domain name). In both cases, the proxy resolves DNS on its side, and Chrome does not make local DNS queries for proxied traffic.\n\nThe real DNS privacy difference between the two proxy types is not who resolves DNS, but what the proxy sees at the application layer. An HTTP proxy sees the full URL for unencrypted requests and the hostname for CONNECT requests. A SOCKS5 proxy sees only the destination hostname and port as opaque connection parameters.\n\nHowever, there is an important caveat: Chrome's DNS prefetcher can make local DNS queries for hostnames found in page content, even when a proxy is configured. This can leak the domains you are browsing to your local DNS resolver. To prevent this, disable DNS prefetching or use the flag `--host-resolver-rules=\"MAP * ~NOTFOUND , EXCLUDE 127.0.0.1\"`.\n\n!!! note \"`socks5://` vs `socks5h://`\"\n    Many tools outside Chrome distinguish between `socks5://` (client resolves DNS) and `socks5h://` (proxy resolves DNS, the \"h\" stands for hostname). Chrome always resolves DNS proxy-side for SOCKS5, behaving like `socks5h://` regardless of which scheme you use. But if you use tools like `curl`, Firefox, or Python libraries alongside Pydoll, the distinction matters: always use `socks5h://` to prevent DNS leaks.\n\n## SOCKS5 and MITM Resistance\n\nSOCKS5 is often described as \"MITM-resistant.\" This is true in a specific sense: because SOCKS5 does not understand or interact with TLS, it has no mechanism to terminate a TLS connection and re-encrypt it. A SOCKS5 proxy simply relays encrypted bytes without modification.\n\nAn HTTP proxy, by contrast, can perform TLS termination (MITM) by presenting its own certificate to the client, decrypting the traffic, inspecting or modifying it, and re-encrypting it toward the server. This requires the client to trust the proxy's CA certificate, and it is detectable through certificate pinning and Certificate Transparency logs. The normal behavior of an HTTP proxy with HTTPS (using CONNECT) is to create a transparent tunnel without termination, but the architectural possibility of MITM exists.\n\nWith SOCKS5, TLS termination is not possible at the protocol level. The proxy cannot inject itself into the TLS handshake because it does not parse the application data flowing through it. The end-to-end encryption between client and server is preserved by design.\n\nIt is worth noting that TLS is what provides the actual cryptographic protection, not SOCKS5 itself. If you send unencrypted HTTP through a SOCKS5 proxy, the proxy operator can read everything. The security advantage of SOCKS5 is architectural (it does not require or enable TLS termination), not cryptographic.\n\n## TLS and Browser Fingerprinting Through SOCKS5\n\nAn important limitation to understand: SOCKS5 does not change your browser's fingerprint. The TLS handshake (ClientHello) passes through the SOCKS5 proxy byte-for-byte, which means the target server sees your browser's exact JA3/JA4 fingerprint. The same applies to HTTP/2 SETTINGS frames, browser-specific header ordering, and all other application-layer fingerprinting signals.\n\nSOCKS5 hides your IP address and prevents the proxy from injecting identifying headers. It does not help with any form of browser or behavioral fingerprinting. For a complete evasion strategy, you need to address fingerprinting at multiple layers. See [Evasion Techniques](../fingerprinting/evasion-techniques.md) for details.\n\n## SOCKS5 Authentication in Chrome\n\nChrome does not support SOCKS5 username/password authentication. This is a longstanding limitation tracked as [Chromium Issue #40323993](https://issues.chromium.org/issues/40323993). When Chrome performs the SOCKS5 method negotiation, it only offers method `0x00` (no authentication). If the proxy requires authentication, the connection fails silently.\n\nThis is fundamentally different from HTTP proxy authentication. HTTP proxies authenticate via HTTP status codes (`407 Proxy Authentication Required`), which Chrome handles through the Fetch domain in CDP. Pydoll intercepts these `Fetch.authRequired` events and responds with stored credentials automatically. SOCKS5 authentication, on the other hand, happens during a binary protocol handshake at the session layer, before any HTTP traffic exists. There is no HTTP 407, no `Fetch.authRequired` event, and no way for CDP-based tools to inject credentials into this process.\n\nConfiguring `--proxy-server=socks5://user:pass@proxy:1080` does not work. Chrome silently ignores the embedded credentials.\n\n### Pydoll's SOCKS5Forwarder\n\nThe standard solution is a local proxy forwarder: a lightweight SOCKS5 server running on localhost that accepts unauthenticated connections from Chrome and forwards them to the remote proxy with full authentication.\n\n```mermaid\nsequenceDiagram\n    participant Chrome\n    participant Forwarder as Local Forwarder<br/>(127.0.0.1:1081)\n    participant Remote as Remote SOCKS5 Proxy<br/>(proxy:1080)\n    participant Server as Destination Server\n\n    Note over Chrome,Forwarder: No authentication\n    Chrome->>Forwarder: SOCKS5 Hello [methods: 0x00]\n    Forwarder->>Chrome: Method selected [0x00]\n    Chrome->>Forwarder: CONNECT example.com:443\n\n    Note over Forwarder,Remote: With authentication\n    Forwarder->>Remote: SOCKS5 Hello [methods: 0x02]\n    Remote->>Forwarder: Method selected [0x02]\n    Forwarder->>Remote: Auth [username, password]\n    Remote->>Forwarder: Auth OK\n    Forwarder->>Remote: CONNECT example.com:443\n    Remote->>Server: TCP connection\n    Remote->>Forwarder: Connect OK\n\n    Forwarder->>Chrome: Connect OK\n\n    Note over Chrome,Server: Bidirectional data relay\n    Chrome->>Forwarder: TLS + application data\n    Forwarder->>Remote: Forward\n    Remote->>Server: Forward\n    Server->>Remote: Response\n    Remote->>Forwarder: Forward\n    Forwarder->>Chrome: Forward\n```\n\nPydoll provides a built-in `SOCKS5Forwarder` in the `pydoll.utils` module. It is a pure-Python, zero-dependency async implementation that handles the full SOCKS5 handshake with the remote proxy, including username/password authentication (RFC 1929), IPv4, IPv6, and domain address types.\n\n```python\nimport asyncio\nfrom pydoll.utils import SOCKS5Forwarder\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def main():\n    forwarder = SOCKS5Forwarder(\n        remote_host='proxy.example.com',\n        remote_port=1080,\n        username='myuser',\n        password='mypass',\n        local_port=1081,  # Use 0 for auto-assigned port\n    )\n    async with forwarder:\n        options = ChromiumOptions()\n        options.add_argument(f'--proxy-server=socks5://127.0.0.1:{forwarder.local_port}')\n\n        async with Chrome(options=options) as browser:\n            tab = await browser.start()\n            await tab.go_to('https://httpbin.org/ip')\n\nasyncio.run(main())\n```\n\nThe forwarder can also run as a standalone CLI tool for testing or use with other applications:\n\n```bash\npython -m pydoll.utils.socks5_proxy_forwarder \\\n    --remote-host proxy.example.com \\\n    --remote-port 1080 \\\n    --username myuser \\\n    --password mypass \\\n    --local-port 1081\n```\n\nThe forwarder binds to `127.0.0.1` by default, making it accessible only from your machine. Never bind to `0.0.0.0` in production, as this would expose an unauthenticated SOCKS5 proxy to the network. Credentials are never logged in plaintext. The forwarder adds sub-millisecond latency since all communication happens over the local loopback interface.\n\n!!! tip \"Restricted Environments\"\n    Some environments (Docker containers, serverless platforms, hardened VMs) may restrict binding to local ports. Use `local_port=0` to let the OS assign an available port. If local binding is completely blocked, consider using an HTTP CONNECT proxy instead, which Chrome supports natively with authentication via Pydoll's ProxyManager.\n\n## Practical Configuration\n\n**Basic SOCKS5 (no authentication):**\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n```\n\n**SOCKS5 with authentication (via SOCKS5Forwarder):**\n\nSee the [SOCKS5Forwarder section](#pydolls-socks5forwarder) above.\n\n**Preventing leaks:**\n\nFor a complete SOCKS5 setup, you should also prevent WebRTC and DNS prefetch leaks:\n\n```python\noptions = ChromiumOptions()\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\noptions.webrtc_leak_protection = True  # Prevents WebRTC IP leaks\noptions.add_argument('--disable-quic')  # Forces HTTP/2 over TCP through proxy\n```\n\n**Testing your setup:**\n\nAlways verify your proxy configuration with leak tests. Visit [browserleaks.com/ip](https://browserleaks.com/ip) to confirm your IP, [browserleaks.com/webrtc](https://browserleaks.com/webrtc) to check for WebRTC leaks, and [dnsleaktest.com](https://dnsleaktest.com/) to verify DNS is not leaking.\n\n## Summary\n\nSOCKS5 provides protocol-agnostic proxying with a smaller trust surface than HTTP proxies. It does not parse, modify, or inject anything into your traffic. DNS resolution happens proxy-side in Chrome. TLS encryption is preserved end-to-end. The main limitation in Chrome is the lack of native SOCKS5 authentication (solved by Pydoll's `SOCKS5Forwarder`) and the absence of UDP proxying (mitigated by disabling WebRTC or using the appropriate browser flags).\n\nSOCKS5 does not change your browser's TLS fingerprint, HTTP/2 settings, or any application-layer characteristics. For complete evasion, combine SOCKS5 with browser fingerprint management and behavioral simulation.\n\n**Next steps:**\n\n- [Proxy Detection](./proxy-detection.md): How even SOCKS5 proxies can be detected\n- [Building Proxies](./build-proxy.md): Implement your own SOCKS5 server\n- [Proxy Configuration](../../features/configuration/proxy.md): Practical Pydoll proxy setup\n- [Evasion Techniques](../fingerprinting/evasion-techniques.md): Multi-layer evasion strategy\n\n## References\n\n- RFC 1928: SOCKS Protocol Version 5 (1996) - https://datatracker.ietf.org/doc/html/rfc1928\n- RFC 1929: Username/Password Authentication for SOCKS V5 (1996) - https://datatracker.ietf.org/doc/html/rfc1929\n- RFC 1961: GSS-API Authentication Method for SOCKS V5 (1996) - https://datatracker.ietf.org/doc/html/rfc1961\n- RFC 3089: SOCKS-based IPv6/IPv4 Gateway Mechanism (2001) - https://datatracker.ietf.org/doc/html/rfc3089\n- Chromium Proxy Documentation - https://chromium.googlesource.com/chromium/src/+/689912289c/net/docs/proxy.md\n- Chromium Issue #40323993: SOCKS5 Authentication - https://issues.chromium.org/issues/40323993\n- BrowserLeaks: WebRTC Leak Test - https://browserleaks.com/webrtc\n- DNS Leak Test - https://dnsleaktest.com/\n- IPLeak: Comprehensive Leak Testing - https://ipleak.net\n"
  },
  {
    "path": "docs/en/features/advanced/behavioral-captcha-bypass.md",
    "content": "# Cloudflare Turnstile Interaction\n\nPydoll provides native support for interacting with Cloudflare Turnstile captchas by performing realistic browser clicks. This is **not a bypass or circumvention**. It simply automates the same click action a human would perform on the captcha checkbox.\n\n!!! warning \"What This Feature Actually Does\"\n    This feature **clicks** on the Cloudflare Turnstile captcha checkbox using standard browser interactions. That's it. There is no:\n    \n    - **NO**: Magic bypass or circumvention\n    - **NO**: Challenge solving (image selection, puzzles, etc.)\n    - **NO**: Score manipulation or fingerprint spoofing\n    - **YES**: Just a realistic click on the captcha container\n    \n    **Success depends entirely on your environment** (IP reputation, browser fingerprint, behavior patterns). Pydoll provides the mechanism to click; your environment determines if the click is accepted.\n\n!!! info \"What Is Cloudflare Turnstile?\"\n    Cloudflare Turnstile is a modern captcha system that analyzes browser environment and behavioral signals to determine if you're human. It typically shows as a checkbox that users must click. The system analyzes:\n    \n    - **IP reputation**: Is your IP address flagged or suspicious?\n    - **Browser fingerprint**: Does your browser look legitimate?\n    - **Behavioral patterns**: Do you behave like a human?\n    \n    When trust score is high enough, the checkbox click is accepted. When it's too low, Turnstile may show a challenge (which Pydoll **cannot solve**) or block you entirely. For image or puzzle challenges, consider using **[CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc)**.\n\n## Quick Start\n\n### Context Manager (Recommended)\n\nThe context manager waits for the captcha to appear, clicks it, and waits for resolution before continuing:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def turnstile_example():\n    options = ChromiumOptions()\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Context manager handles captcha automatically\n        async with tab.expect_and_bypass_cloudflare_captcha():\n            await tab.go_to('https://site-with-turnstile.com')\n        \n        # This code only runs after captcha is clicked\n        print(\"Turnstile captcha interaction complete!\")\n        \n        # Continue with your automation\n        content = await tab.find(id='protected-content')\n        print(await content.text)\n\nasyncio.run(turnstile_example())\n```\n\n### Background Processing\n\nEnable automatic captcha clicking in the background:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def background_turnstile():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Enable automatic clicking before navigating\n        await tab.enable_auto_solve_cloudflare_captcha()\n        \n        # Navigate to protected site\n        await tab.go_to('https://site-with-turnstile.com')\n        \n        # Wait for captcha to be processed in background\n        await asyncio.sleep(5)\n        \n        print(\"Page loaded with background captcha handling\")\n        \n        # Disable when no longer needed\n        await tab.disable_auto_solve_cloudflare_captcha()\n\nasyncio.run(background_turnstile())\n```\n\n## Customizing Captcha Interaction\n\n### How It Works\n\nPydoll automatically detects Cloudflare Turnstile by traversing the page's shadow DOM. It looks for a shadow root containing `challenges.cloudflare.com`, navigates into its cross-origin iframe, finds the inner shadow root, and clicks the actual checkbox element. No manual selector configuration is needed.\n\n### Timing Configuration\n\nThe captcha shadow root doesn't always appear immediately. Adjust the timeout to match the site's behavior:\n\n```python\nasync def timing_configuration_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n\n        async with tab.expect_and_bypass_cloudflare_captcha(\n            time_to_wait_captcha=10   # Wait up to 10 seconds for captcha to appear (default: 5)\n        ):\n            await tab.go_to('https://site-with-slow-turnstile.com')\n\n        print(\"Captcha interaction complete with custom timing!\")\n\nasyncio.run(timing_configuration_example())\n```\n\n**Parameter Reference:**\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `time_to_wait_captcha` | `float` | `5` | Maximum seconds to wait for captcha to appear |\n\n!!! info \"Why Timing Matters\"\n    Some sites load the captcha asynchronously. If the Cloudflare shadow root doesn't appear within `time_to_wait_captcha`, the interaction is skipped.\n\n## Other Captcha Systems\n\n### reCAPTCHA v3 (Invisible)\n\nreCAPTCHA v3 is **completely invisible** and requires **no interaction**. Just navigate normally:\n\n```python\nasync def recaptcha_v3_example():\n    options = ChromiumOptions()\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # No special handling needed - just navigate\n        await tab.go_to('https://site-with-recaptcha-v3.com')\n        \n        # reCAPTCHA v3 runs in background, analyzing your behavior\n        await asyncio.sleep(3)\n        \n        # Continue with form submission\n        submit_button = await tab.find(id='submit-btn')\n        await submit_button.click()\n\nasyncio.run(recaptcha_v3_example())\n```\n\n!!! note \"reCAPTCHA v3 Success Factors\"\n    Since reCAPTCHA v3 is entirely passive (no interaction), success depends on:\n    \n    - **IP reputation**: Use residential proxies with good reputation\n    - **Browser fingerprint**: Configure realistic browser preferences\n    - **Behavioral patterns**: Spend time on page, scroll naturally, type realistically\n    \n    If your score is too low, some sites may show a reCAPTCHA v2 challenge (which Pydoll **cannot solve**).\n\n## What Determines Success?\n\nThe success of captcha interaction depends **entirely on your environment**, not on Pydoll. The captcha system analyzes:\n\n### 1. IP Reputation (Most Critical)\n\n| IP Type | Trust Level | Expected Behavior |\n|---------|-------------|-------------------|\n| **Residential IP (clean)** | High | Generally accepted without challenges |\n| **Mobile IP** | High | Generally accepted without challenges |\n| **Datacenter IP** | Low | Often blocked or challenged |\n| **Previously blocked IP** | Very Low | Almost always blocked or challenged |\n\n!!! danger \"IP Reputation is Everything\"\n    **No tool can overcome a bad IP address.** If your IP is flagged, you will be blocked or challenged regardless of how realistic your browser looks.\n    \n    Use residential proxies with good reputation for best results.\n\n### 2. Browser Fingerprint\n\nConfigure your browser to look legitimate:\n\n```python\nimport time\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def stealth_configuration():\n    options = ChromiumOptions()\n    \n    # Stealth arguments\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    options.add_argument('--window-size=1920,1080')\n    \n    # Realistic browser preferences\n    current_time = int(time.time())\n    options.browser_preferences = {\n        'profile': {\n            'last_engagement_time': str(current_time - (3 * 60 * 60)),  # 3 hours ago\n            'exited_cleanly': True,\n            'exit_type': 'Normal',\n        },\n        'safebrowsing': {'enabled': True},\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        async with tab.expect_and_bypass_cloudflare_captcha():\n            await tab.go_to('https://site-with-turnstile.com')\n\nasyncio.run(stealth_configuration())\n```\n\n### 3. Behavioral Patterns\n\nCaptcha systems analyze how you interact with the page:\n\n```python\nasync def realistic_behavior():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://site-with-turnstile.com')\n        \n        # Simulate human behavior before captcha appears\n        await asyncio.sleep(2)  # Read page content\n        await tab.execute_script('window.scrollBy(0, 300)')  # Scroll\n        await asyncio.sleep(1)\n        \n        # Now interact with captcha\n        async with tab.expect_and_bypass_cloudflare_captcha():\n            # The captcha interaction happens here\n            pass\n        \n        print(\"Captcha passed with realistic behavior!\")\n\nasyncio.run(realistic_behavior())\n```\n\n!!! tip \"Behavioral Fingerprinting\"\n    For in-depth understanding of how behavioral patterns affect captcha success, see **[Behavioral Fingerprinting](../../deep-dive/fingerprinting/behavioral-fingerprinting.md)**. This guide explains:\n    \n    - Mouse movement patterns and detection\n    - Keystroke timing analysis\n    - Scroll behavior physics\n    - Event sequence analysis\n    \n    Understanding these concepts can help you build more realistic automation that achieves higher success rates.\n\n## Troubleshooting\n\n### Captcha Not Being Clicked\n\n**Symptoms**: Captcha appears but is never clicked, page stays on challenge.\n\n**Possible Causes:**\n\n1. **Timing too short**: Captcha hasn't loaded yet when Pydoll tries to click\n2. **Shadow root not found**: The Cloudflare Turnstile shadow root hasn't appeared in the DOM yet\n\n**Solutions:**\n\n```python\nasync def troubleshooting_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n\n        # Increase wait times\n        async with tab.expect_and_bypass_cloudflare_captcha(\n            time_before_click=5,     # Longer delay before clicking\n            time_to_wait_captcha=15  # More time to find captcha\n        ):\n            await tab.go_to('https://problematic-site.com')\n\nasyncio.run(troubleshooting_example())\n```\n\n### Captcha Clicked but Shows Challenge\n\n**Symptoms**: Checkbox shows checkmark briefly, then presents an image/puzzle challenge.\n\n**Root Cause**: Your environment's trust score is too low.\n\n**Solutions:**\n\n- Use residential proxies with good reputation\n- Configure realistic browser fingerprint\n- Add more realistic behavioral patterns (scrolling, mouse movement, delays)\n- **Note**: Pydoll cannot solve the challenge itself. If you need automated captcha solving, consider integrating with **[CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc)**\n\n### \"Access Denied\" or Immediate Block\n\n**Symptoms**: Site immediately shows \"Access Denied\" or blocks you without showing captcha.\n\n**Root Cause**: **Your IP address is flagged.**\n\n**Solutions:**\n\n- Use different residential proxy with good reputation\n- Rotate IPs between requests\n- Test your IP at `https://www.cloudflare.com/cdn-cgi/trace`\n- **Note**: No amount of browser configuration will fix a flagged IP\n\n### Works Locally but Fails in Docker/CI\n\n**Symptoms**: Captcha interaction works on your machine but fails in Docker/CI environments.\n\n**Root Cause**: Datacenter IPs are heavily scrutinized by captcha systems.\n\n**Solutions:**\n\n1. **Use headless mode with proper display** (for full rendering):\n   ```dockerfile\n   FROM python:3.11-slim\n   \n   RUN apt-get update && apt-get install -y \\\n       chromium \\\n       chromium-driver \\\n       xvfb \\\n       && rm -rf /var/lib/apt/lists/*\n   \n   ENV DISPLAY=:99\n   \n   CMD Xvfb :99 -screen 0 1920x1080x24 & python your_script.py\n   ```\n\n2. **Use residential proxy** even in CI/CD:\n   ```python\n   options = ChromiumOptions()\n   options.add_argument('--proxy-server=http://user:pass@residential-proxy.com:8080')\n   ```\n\n## Best Practices\n\n1. **Use residential proxies**: IP reputation is the most critical factor\n2. **Configure stealth options**: Remove automation indicators\n3. **Add behavioral patterns**: Scroll, wait, move mouse before clicking\n4. **Adjust timing**: Give captcha time to load before attempting click\n5. **Handle failures gracefully**: Have fallback logic when captcha cannot be passed\n6. **Test your environment**: Verify IP reputation and browser fingerprint before automation\n\n## Ethical Guidelines\n\n!!! danger \"Terms of Service and Legal Compliance\"\n    Interacting with captchas may violate a website's Terms of Service even if technically possible. **Always check and respect ToS** before automating any website.\n    \n    This feature is provided for **legitimate automation purposes only**:\n    \n    **Appropriate use cases:**\n    - Automated testing of your own applications\n    - Monitoring services you have permission to monitor\n    - Research and security analysis with proper authorization\n    \n    **Inappropriate use cases:**\n    - Scraping content you don't have permission to access\n    - Circumventing paywalls or subscription systems\n    - Denial-of-service attacks or aggressive scraping\n    - Any activity that violates Terms of Service\n\n## See Also\n\n- **[Browser Options](../configuration/browser-options.md)** - Stealth configuration\n- **[Browser Preferences](../configuration/browser-preferences.md)** - Advanced fingerprinting\n- **[Proxy Configuration](../configuration/proxy.md)** - Setting up proxies\n- **[Behavioral Fingerprinting](../../deep-dive/fingerprinting/behavioral-fingerprinting.md)** - Understanding behavioral detection\n- **[Human-Like Interactions](../automation/human-interactions.md)** - Realistic behavior patterns\n\n---\n\n**Remember**: Pydoll provides the mechanism to click on captchas, but your environment (IP, fingerprint, behavior) determines success. This is not a magic solution, it's a tool that works when used in the right environment with proper configuration. For challenges that require image recognition or puzzle solving, consider using **[CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc)** — use code **PYDOLL** for an extra 6% balance bonus.\n"
  },
  {
    "path": "docs/en/features/advanced/decorators.md",
    "content": "# Retry Decorator\n\nWeb scraping is inherently unpredictable. Networks fail, pages load slowly, elements appear and disappear, rate limits kick in, and CAPTCHAs show up unexpectedly. The `@retry` decorator provides a robust, battle-tested solution for handling these inevitable failures gracefully.\n\n## Why Use the Retry Decorator?\n\nIn production scraping, failures aren't exceptions, they're the norm. Instead of letting your entire scraping job crash because of a temporary network hiccup or a missing element, the retry decorator allows you to:\n\n- **Recover automatically** from transient failures\n- **Implement sophisticated retry strategies** with exponential backoff\n- **Execute recovery logic** before retrying (refresh page, switch proxy, restart browser)\n- **Keep your business logic clean** without polluting it with error handling code\n\n## Quick Start\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import WaitElementTimeout, NetworkError\n\n@retry(max_retries=3, exceptions=[WaitElementTimeout, NetworkError])\nasync def scrape_product_page(url: str):\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to(url)\n        \n        # This might fail due to network issues or slow loading\n        product_title = await tab.find(class_name='product-title', timeout=5)\n        return await product_title.text\n\nasyncio.run(scrape_product_page('https://example.com/product/123'))\n```\n\nIf `scrape_product_page` fails with a `WaitElementTimeout` or `NetworkError`, it will automatically retry up to 3 times before giving up.\n\n## Best Practice: Always Specify Exceptions\n\n!!! warning \"Critical Best Practice\"\n    **ALWAYS** specify which exceptions should trigger a retry. Using the default `exceptions=Exception` will catch **everything**, including bugs in your code that should fail immediately.\n\n**Bad (catches everything, including bugs):**\n\n```python\n@retry(max_retries=3)  # DON'T DO THIS\nasync def scrape_data():\n    data = response['items'][0]  # If 'items' doesn't exist, retries won't help!\n    return data\n```\n\n**Good (only retries on expected failures):**\n\n```python\nfrom pydoll.exceptions import ElementNotFound, WaitElementTimeout, NetworkError\n\n@retry(\n    max_retries=3,\n    exceptions=[ElementNotFound, WaitElementTimeout, NetworkError]\n)\nasync def scrape_data():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        return await tab.find(id='data-container', timeout=10)\n```\n\nBy specifying exceptions, you ensure that:\n\n- **Logic errors fail fast** (typos, wrong selectors, code bugs)\n- **Only recoverable errors are retried** (network issues, timeouts, missing elements)\n- **Debugging is easier** (you know exactly what went wrong)\n\n## Parameters\n\n### max_retries\n\nMaximum number of retry attempts before giving up.\n\n```python\nfrom pydoll.exceptions import WaitElementTimeout\n\n@retry(max_retries=5, exceptions=[WaitElementTimeout])\nasync def fetch_data():\n    # Will try up to 5 times total\n    pass\n```\n\n### exceptions\n\nException types that should trigger a retry. Can be a single exception or a list.\n\n```python\nfrom pydoll.exceptions import (\n    ElementNotFound,\n    WaitElementTimeout,\n    NetworkError,\n    ElementNotInteractable\n)\n\n# Single exception\n@retry(exceptions=[WaitElementTimeout])\nasync def example1():\n    pass\n\n# Multiple exceptions\n@retry(exceptions=[WaitElementTimeout, NetworkError, ElementNotFound, ElementNotInteractable])\nasync def example2():\n    pass\n```\n\n!!! tip \"Common Scraping Exceptions\"\n    For web scraping with Pydoll, you'll typically want to retry on:\n\n    - `WaitElementTimeout` - Timeout waiting for element to appear\n    - `ElementNotFound` - Element doesn't exist in DOM\n    - `ElementNotVisible` - Element exists but is not visible\n    - `ElementNotInteractable` - Element cannot receive interaction\n    - `NetworkError` - Network connectivity issues\n    - `ConnectionFailed` - Failed to connect to browser\n    - `PageLoadTimeout` - Page load timed out\n    - `ClickIntercepted` - Click was intercepted by another element\n\n### delay\n\nTime to wait between retry attempts (in seconds).\n\n```python\nfrom pydoll.exceptions import WaitElementTimeout\n\n@retry(max_retries=3, exceptions=[WaitElementTimeout], delay=2.0)\nasync def scrape_with_delay():\n    # Waits 2 seconds between each retry\n    pass\n```\n\n### exponential_backoff\n\nWhen `True`, increases the delay exponentially with each retry attempt.\n\n```python\nfrom pydoll.exceptions import NetworkError\n\n@retry(\n    max_retries=5,\n    exceptions=[NetworkError],\n    delay=1.0,\n    exponential_backoff=True\n)\nasync def scrape_with_backoff():\n    # Attempt 1: fails → wait 1 second\n    # Attempt 2: fails → wait 2 seconds\n    # Attempt 3: fails → wait 4 seconds\n    # Attempt 4: fails → wait 8 seconds\n    # Attempt 5: fails → raise exception\n    pass\n```\n\n**What is Exponential Backoff?**\n\nExponential backoff is a retry strategy where the wait time between attempts increases exponentially. Instead of hammering a server with requests every second, you give it progressively more time to recover:\n\n- **Attempt 1**: Wait `delay` seconds (e.g., 1s)\n- **Attempt 2**: Wait `delay * 2` seconds (e.g., 2s)\n- **Attempt 3**: Wait `delay * 4` seconds (e.g., 4s)\n- **Attempt 4**: Wait `delay * 8` seconds (e.g., 8s)\n\nThis is especially useful when:\n\n- Dealing with **rate limits** (give the server time to reset)\n- Handling **temporary server overload** (don't make it worse)\n- Waiting for **slow-loading dynamic content**\n- Avoiding **detection as a bot** (natural-looking retry patterns)\n\n### on_retry\n\nA callback function executed after each failed attempt, before the next retry. Must be an **async function**.\n\n```python\nfrom pydoll.exceptions import WaitElementTimeout\n\n@retry(\n    max_retries=3,\n    exceptions=[WaitElementTimeout],\n    on_retry=my_recovery_function\n)\nasync def scrape_data():\n    pass\n```\n\nThe callback can be:\n\n- **A standalone async function**\n- **A class method** (receives `self` automatically)\n\n## The on_retry Callback: Your Recovery Mechanism\n\nThe `on_retry` callback is where the real magic happens. This is your opportunity to **restore the application state** before the next retry attempt.\n\n### Standalone Function\n\n```python\nimport asyncio\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import WaitElementTimeout\n\nasync def log_retry():\n    print(\"Retry attempt failed, waiting before next attempt...\")\n    await asyncio.sleep(1)\n\n@retry(max_retries=3, exceptions=[WaitElementTimeout], on_retry=log_retry)\nasync def scrape_page():\n    # Your scraping logic\n    pass\n```\n\n### Class Method\n\nWhen using the decorator inside a class, the callback can be a class method. It will automatically receive `self` as the first argument.\n\n```python\nimport asyncio\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import WaitElementTimeout\n\nclass DataCollector:\n    def __init__(self):\n        self.retry_count = 0\n    \n    # IMPORTANT: Define callback BEFORE the decorated method\n    async def log_retry(self):\n        self.retry_count += 1\n        print(f\"Attempt {self.retry_count} failed, retrying...\")\n        await asyncio.sleep(1)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[WaitElementTimeout],\n        on_retry=log_retry  # No 'self.' prefix needed\n    )\n    async def fetch_data(self):\n        # Your scraping logic here\n        pass\n```\n\n!!! warning \"Method Definition Order Matters\"\n    When using `on_retry` with class methods, **you must define the callback method BEFORE the decorated method** in your class definition. Python needs to know about the callback when the decorator is applied.\n\n    **Wrong (will fail):**\n\n    ```python\n    class Scraper:\n        @retry(on_retry=handle_retry)  # handle_retry doesn't exist yet!\n        async def scrape(self):\n            pass\n        \n        async def handle_retry(self):  # Defined too late\n            pass\n    ```\n\n    **Correct:**\n\n    ```python\n    class Scraper:\n        async def handle_retry(self):  # Defined first\n            pass\n        \n        @retry(on_retry=handle_retry)  # Now it exists\n        async def scrape(self):\n            pass\n    ```\n\n## Real-World Use Cases\n\n### 1. Page Refresh and State Recovery\n\n**This is the most powerful use of `on_retry`**: recovering from failures by refreshing the page and restoring your application state. This example demonstrates why the retry decorator is so valuable for production scraping.\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound, WaitElementTimeout\nfrom pydoll.constants import Key\nimport asyncio\n\nclass DataScraper:\n    def __init__(self):\n        self.browser = None\n        self.tab = None\n        self.current_page = 1\n    \n    async def recover_from_failure(self):\n        \"\"\"Refresh page and restore state before retry\"\"\"\n        print(f\"Recovering... refreshing page {self.current_page}\")\n        \n        if self.tab:\n            # Refresh the page to recover from stale elements or bad state\n            await self.tab.refresh()\n            await asyncio.sleep(2)  # Wait for page to load\n            \n            # Restore state: navigate back to the correct page\n            if self.current_page > 1:\n                page_input = await self.tab.find(id='page-number')\n                await page_input.insert_text(str(self.current_page))\n                await self.tab.keyboard.press(Key.ENTER)\n                await asyncio.sleep(1)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound, WaitElementTimeout],\n        on_retry=recover_from_failure,\n        delay=1.0\n    )\n    async def scrape_page_data(self):\n        \"\"\"Scrape data from the current page\"\"\"\n        if not self.browser:\n            self.browser = Chrome()\n            self.tab = await self.browser.start()\n            await self.tab.go_to('https://example.com/data')\n        \n        # Navigate to specific page\n        page_input = await self.tab.find(id='page-number')\n        await page_input.insert_text(str(self.current_page))\n        await self.tab.keyboard.press(Key.ENTER)\n        await asyncio.sleep(1)\n        \n        # Scrape data (might fail if elements become stale)\n        items = await self.tab.find(class_name='data-item', find_all=True)\n        return [await item.text for item in items]\n    \n    async def scrape_multiple_pages(self, start_page: int, end_page: int):\n        \"\"\"Scrape multiple pages with automatic retry on failures\"\"\"\n        results = []\n        for page_num in range(start_page, end_page + 1):\n            self.current_page = page_num\n            data = await self.scrape_page_data()\n            results.extend(data)\n        return results\n\n# Usage\nasync def main():\n    scraper = DataScraper()\n    try:\n        # Scrape pages 1-10 with automatic recovery on failures\n        all_data = await scraper.scrape_multiple_pages(1, 10)\n        print(f\"Scraped {len(all_data)} items\")\n    finally:\n        if scraper.browser:\n            await scraper.browser.stop()\n```\n\n**What makes this powerful:**\n\n- `recover_from_failure()` actually **restores the state** by refreshing and navigating back\n- The `scrape_page_data()` method stays clean, focused only on scraping logic\n- If elements become stale or disappear, the retry mechanism handles recovery automatically\n- The browser persists across retries via `self.browser` and `self.tab`\n\n### 2. Modal Dialog Recovery\n\nSometimes a modal or overlay appears unexpectedly and blocks your automation. Close it and retry.\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound\n\nclass ModalAwareScraper:\n    def __init__(self):\n        self.tab = None\n    \n    async def close_modals(self):\n        \"\"\"Close any blocking modals before retry\"\"\"\n        print(\"Checking for blocking modals...\")\n        \n        # Try to find and close common modals\n        modal_close = await self.tab.find(\n            class_name='modal-close',\n            timeout=2,\n            raise_exc=False\n        )\n        if modal_close:\n            print(\"Found modal, closing it...\")\n            await modal_close.click()\n            await asyncio.sleep(0.5)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound],\n        on_retry=close_modals,\n        delay=0.5\n    )\n    async def click_button(self, button_id: str):\n        button = await self.tab.find(id=button_id)\n        await button.click()\n```\n\n### 3. Browser Restart and Proxy Rotation\n\nFor heavy scraping jobs, you might need to completely restart the browser and switch proxies after failures.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import NetworkError, PageLoadTimeout\n\nclass RobustScraper:\n    def __init__(self):\n        self.browser = None\n        self.tab = None\n        self.proxy_list = [\n            'proxy1.example.com:8080',\n            'proxy2.example.com:8080',\n            'proxy3.example.com:8080',\n        ]\n        self.current_proxy_index = 0\n    \n    async def restart_with_new_proxy(self):\n        \"\"\"Restart browser with a different proxy\"\"\"\n        print(\"Restarting browser with new proxy...\")\n        \n        # Close current browser\n        if self.browser:\n            await self.browser.stop()\n            await asyncio.sleep(2)\n        \n        # Rotate to next proxy\n        self.current_proxy_index = (self.current_proxy_index + 1) % len(self.proxy_list)\n        proxy = self.proxy_list[self.current_proxy_index]\n        \n        print(f\"Using proxy: {proxy}\")\n        \n        # Start new browser with new proxy\n        options = ChromiumOptions()\n        options.add_argument(f'--proxy-server={proxy}')\n        \n        self.browser = Chrome(options=options)\n        self.tab = await self.browser.start()\n    \n    @retry(\n        max_retries=3,\n        exceptions=[NetworkError, PageLoadTimeout],\n        on_retry=restart_with_new_proxy,\n        delay=5.0,\n        exponential_backoff=True\n    )\n    async def scrape_protected_site(self, url: str):\n        if not self.browser:\n            await self.restart_with_new_proxy()\n        \n        await self.tab.go_to(url)\n        await asyncio.sleep(3)\n        \n        # Your scraping logic here\n        content = await self.tab.find(id='content')\n        return await content.text\n```\n\n### 4. Network Idle Detection with Retry\n\nWait for all network activity to complete, with retry logic if the page never stabilizes.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import TimeoutException\n\nclass NetworkAwareScraper:\n    def __init__(self):\n        self.tab = None\n    \n    async def reload_page(self):\n        \"\"\"Reload page if network never stabilized\"\"\"\n        print(\"Page didn't stabilize, reloading...\")\n        if self.tab:\n            await self.tab.refresh()\n            await asyncio.sleep(2)\n    \n    @retry(\n        max_retries=2,\n        exceptions=[TimeoutException],\n        on_retry=reload_page,\n        delay=3.0\n    )\n    async def wait_for_page_ready(self):\n        \"\"\"Wait for all network requests to complete\"\"\"\n        await self.tab.enable_network_events()\n        \n        # Wait for network idle (no requests for 2 seconds)\n        idle_time = 0\n        max_wait = 10\n        \n        while idle_time < max_wait:\n            # Check if any requests are in flight\n            # (Implementation depends on your event tracking)\n            await asyncio.sleep(0.5)\n            idle_time += 0.5\n        \n        if idle_time >= max_wait:\n            raise TimeoutException(\"Network never stabilized\")\n```\n\n### 5. CAPTCHA Detection and Recovery\n\nDetect when a CAPTCHA appears and take appropriate action.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound\n\nclass CaptchaScraper:\n    def __init__(self):\n        self.tab = None\n        self.captcha_count = 0\n    \n    async def handle_captcha(self):\n        \"\"\"Handle CAPTCHA by waiting or switching strategy\"\"\"\n        self.captcha_count += 1\n        print(f\"CAPTCHA detected (count: {self.captcha_count})\")\n        \n        if self.captcha_count > 2:\n            print(\"Too many CAPTCHAs, might need to change strategy...\")\n            # Could switch to a different approach here\n        \n        # Wait longer between attempts\n        await asyncio.sleep(30)\n        \n        # Refresh the page\n        await self.tab.refresh()\n        await asyncio.sleep(5)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound],\n        on_retry=handle_captcha,\n        delay=10.0,\n        exponential_backoff=True\n    )\n    async def scrape_protected_content(self, url: str):\n        if not self.tab:\n            browser = Chrome()\n            self.tab = await browser.start()\n        \n        await self.tab.go_to(url)\n        \n        # Check for CAPTCHA\n        captcha = await self.tab.find(\n            class_name='g-recaptcha',\n            timeout=2,\n            raise_exc=False\n        )\n        \n        if captcha:\n            raise ElementNotFound(\"CAPTCHA detected\")\n        \n        # Normal scraping logic\n        content = await self.tab.find(class_name='article-content')\n        return await content.text\n```\n\n## Advanced Patterns\n\n### Combining Multiple Recovery Strategies\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound, WaitElementTimeout, NetworkError\n\nclass AdvancedScraper:\n    def __init__(self):\n        self.tab = None\n        self.attempt = 0\n        self.strategies = [\n            self.strategy_refresh,\n            self.strategy_clear_cache,\n            self.strategy_restart_browser,\n        ]\n    \n    async def strategy_refresh(self):\n        \"\"\"Strategy 1: Simple refresh\"\"\"\n        print(\"Strategy 1: Refreshing page\")\n        await self.tab.refresh()\n        await asyncio.sleep(2)\n    \n    async def strategy_clear_cache(self):\n        \"\"\"Strategy 2: Clear cache and refresh\"\"\"\n        print(\"Strategy 2: Clearing cache\")\n        await self.tab.execute_command('Network.clearBrowserCache')\n        await self.tab.refresh()\n        await asyncio.sleep(3)\n    \n    async def strategy_restart_browser(self):\n        \"\"\"Strategy 3: Full browser restart\"\"\"\n        print(\"Strategy 3: Restarting browser\")\n        if self.tab:\n            await self.tab._browser.stop()\n        \n        browser = Chrome()\n        self.tab = await browser.start()\n    \n    async def adaptive_recovery(self):\n        \"\"\"Try different recovery strategies based on attempt number\"\"\"\n        strategy_index = min(self.attempt, len(self.strategies) - 1)\n        strategy = self.strategies[strategy_index]\n        \n        print(f\"Attempt {self.attempt + 1}: Using {strategy.__name__}\")\n        await strategy()\n        \n        self.attempt += 1\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound, WaitElementTimeout, NetworkError],\n        on_retry=adaptive_recovery,\n        delay=2.0\n    )\n    async def scrape_with_adaptive_retry(self, url: str):\n        await self.tab.go_to(url)\n        return await self.tab.find(id='target-content')\n```\n\n### Custom Exception for Specific Failure\n\n```python\nimport asyncio\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import PydollException\n\nclass RateLimitError(PydollException):\n    \"\"\"Raised when rate limit is detected\"\"\"\n    message = \"API rate limit exceeded\"\n\nclass APIScraper:\n    async def wait_for_rate_limit_reset(self):\n        \"\"\"Wait longer when rate limited\"\"\"\n        print(\"Rate limit detected, waiting 60 seconds...\")\n        await asyncio.sleep(60)\n    \n    @retry(\n        max_retries=5,\n        exceptions=[RateLimitError],\n        on_retry=wait_for_rate_limit_reset,\n        delay=10.0,\n        exponential_backoff=True\n    )\n    async def fetch_api_data(self, endpoint: str):\n        response = await self.tab.request.get(endpoint)\n        \n        if response.status == 429:  # Too Many Requests\n            raise RateLimitError(\"API rate limit exceeded\")\n        \n        return response.json()\n```\n\n## Best Practices Summary\n\n1. **Always specify exceptions explicitly** - Never use the default `exceptions=Exception`\n2. **Use exponential backoff for external services** - Give servers time to recover\n3. **Keep retry counts reasonable** - Usually 3-5 attempts is enough\n4. **Log retry attempts** - Use `on_retry` to log what's happening\n5. **Define callbacks before decorated methods** - Order matters in class definitions\n6. **Make callbacks async** - The decorator requires async callbacks\n7. **Restore state in callbacks** - Use `on_retry` to navigate back to where you were\n8. **Consider the cost of retries** - Each retry consumes time and resources\n9. **Combine with other error handling** - Retries don't replace try/except blocks\n10. **Test your retry logic** - Ensure recovery callbacks actually work\n\n## Learn More\n\n- **[Exception Handling](../core-concepts.md#error-handling)** - Understanding Pydoll exceptions\n- **[Network Events](../network/monitoring.md)** - Track and handle network failures\n- **[Browser Options](../configuration/browser-options.md)** - Configure proxies and other settings\n- **[Event System](event-system.md)** - Build reactive retry strategies\n\nThe retry decorator is a powerful tool that turns fragile scraping scripts into production-ready applications. By combining it with thoughtful recovery strategies, you can build scrapers that gracefully handle the chaos of the real web.\n\n"
  },
  {
    "path": "docs/en/features/advanced/event-system.md",
    "content": "# Event System\n\nPydoll's event system allows you to listen and react to browser activities in real-time. This is essential for building dynamic automation, monitoring network requests, detecting page changes, and creating reactive workflows.\n\n!!! info \"Deep Dive Available\"\n    This guide focuses on practical usage. For architectural details and internal implementation, see [Event Architecture Deep Dive](../../deep-dive/event-architecture.md).\n\n## Prerequisites\n\nBefore working with events, you need to enable the corresponding CDP domain:\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    \n    # Enable the domain before listening to events\n    await tab.enable_page_events()     # For page lifecycle events\n    await tab.enable_network_events()  # For network activity\n    await tab.enable_dom_events()      # For DOM changes\n```\n\n!!! warning \"Events Won't Fire Without Enabling\"\n    If you register a callback but forget to enable the domain, your callback will never be triggered. Always enable the domain first!\n\n## Basic Event Listening\n\nThe `on()` method registers event listeners:\n\n```python\nfrom pydoll.protocol.page.events import PageEvent, LoadEventFiredEvent\n\nasync def handle_page_load(event: LoadEventFiredEvent):\n    print(f\"Page loaded at {event['params']['timestamp']}\")\n\n# Register the callback\nawait tab.enable_page_events()\ncallback_id = await tab.on(PageEvent.LOAD_EVENT_FIRED, handle_page_load)\n```\n\n### Event Structure\n\nAll events follow the same structure:\n\n```python\n{\n    'method': 'Page.loadEventFired',  # Event name\n    'params': {                        # Event-specific data\n        'timestamp': 123456.789\n    }\n}\n```\n\nAccess event data through `event['params']`:\n\n```python\nfrom pydoll.protocol.network.events import RequestWillBeSentEvent\n\nasync def handle_request(event: RequestWillBeSentEvent):\n    url = event['params']['request']['url']\n    method = event['params']['request']['method']\n    print(f\"{method} {url}\")\n```\n\n### Using Type Hints for Better IDE Support\n\nUse type hints with event parameter types to get autocomplete for event keys:\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\nfrom pydoll.protocol.page.events import PageEvent, LoadEventFiredEvent\n\n# With type hints - IDE knows all available keys!\nasync def handle_request(event: RequestWillBeSentEvent):\n    # IDE will autocomplete 'params', 'request', 'url', etc.\n    url = event['params']['request']['url']\n    method = event['params']['request']['method']\n    timestamp = event['params']['timestamp']\n    print(f\"{method} {url} at {timestamp}\")\n\nasync def handle_load(event: LoadEventFiredEvent):\n    # IDE knows this event has 'timestamp' in params\n    timestamp = event['params']['timestamp']\n    print(f\"Page loaded at {timestamp}\")\n\nawait tab.enable_network_events()\nawait tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, handle_request)\n\nawait tab.enable_page_events()\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, handle_load)\n```\n\n!!! tip \"Type Hints for Event Parameters\"\n    All event types are defined in `pydoll.protocol.<domain>.events`. Using them gives you:\n    \n    - **Autocomplete**: IDE suggests available keys in `event['params']`\n    - **Type safety**: Catch typos before running code\n    - **Documentation**: See what data each event provides\n    \n    Event types follow the pattern: `<EventName>Event` (e.g., `RequestWillBeSentEvent`, `ResponseReceivedEvent`)\n\n## Common Event Domains\n\n### Page Events\n\nMonitor page lifecycle and dialogs:\n\n```python\nfrom pydoll.protocol.page.events import PageEvent, JavascriptDialogOpeningEvent\n\nawait tab.enable_page_events()\n\n# Page loaded\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, lambda e: print(\"Page loaded!\"))\n\n# DOM ready\nawait tab.on(PageEvent.DOM_CONTENT_EVENT_FIRED, lambda e: print(\"DOM ready!\"))\n\n# JavaScript dialog\nasync def handle_dialog(event: JavascriptDialogOpeningEvent):\n    message = event['params']['message']\n    dialog_type = event['params']['type']\n    print(f\"Dialog ({dialog_type}): {message}\")\n    \n    # Handle it automatically\n    if await tab.has_dialog():\n        await tab.handle_dialog(accept=True)\n\nawait tab.on(PageEvent.JAVASCRIPT_DIALOG_OPENING, handle_dialog)\n```\n\n### Network Events\n\nMonitor requests and responses:\n\n```python\nfrom pydoll.protocol.network.events import (\n    NetworkEvent,\n    RequestWillBeSentEvent,\n    ResponseReceivedEvent,\n    LoadingFailedEvent\n)\n\nawait tab.enable_network_events()\n\n# Track requests\nasync def log_request(event: RequestWillBeSentEvent):\n    request = event['params']['request']\n    print(f\"→ {request['method']} {request['url']}\")\n\nawait tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, log_request)\n\n# Track responses\nasync def log_response(event: ResponseReceivedEvent):\n    response = event['params']['response']\n    print(f\"← {response['status']} {response['url']}\")\n\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, log_response)\n\n# Track failures\nasync def log_failure(event: LoadingFailedEvent):\n    url = event['params']['type']\n    error = event['params']['errorText']\n    print(f\"[FAILED] {url} - {error}\")\n\nawait tab.on(NetworkEvent.LOADING_FAILED, log_failure)\n```\n\n### DOM Events\n\nReact to DOM changes:\n\n```python\nfrom pydoll.protocol.dom.events import DomEvent, AttributeModifiedEvent\n\nawait tab.enable_dom_events()\n\n# Track attribute changes\nasync def on_attribute_change(event: AttributeModifiedEvent):\n    node_id = event['params']['nodeId']\n    attr_name = event['params']['name']\n    attr_value = event['params']['value']\n    print(f\"Node {node_id}: {attr_name}={attr_value}\")\n\nawait tab.on(DomEvent.ATTRIBUTE_MODIFIED, on_attribute_change)\n\n# Track document updates\nawait tab.on(DomEvent.DOCUMENT_UPDATED, lambda e: print(\"Document updated!\"))\n```\n\n## Temporary Callbacks\n\nUse `temporary=True` for one-time listeners:\n\n```python\nfrom pydoll.protocol.page.events import PageEvent\n\n# This will only fire once and then auto-remove\nawait tab.on(\n    PageEvent.LOAD_EVENT_FIRED,\n    lambda e: print(\"First load!\"),\n    temporary=True\n)\n\nawait tab.go_to(\"https://example.com\")  # Fires callback\nawait tab.refresh()                      # Callback won't fire again\n```\n\n!!! tip \"Perfect for One-Time Setup\"\n    Temporary callbacks are ideal for initialization tasks that should only happen once.\n\n## Accessing Tab in Callbacks\n\nUse `functools.partial` to pass the tab to your callbacks:\n\n```python\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def process_response(tab, event: ResponseReceivedEvent):\n    # Now we can use the tab object!\n    request_id = event['params']['requestId']\n    \n    # Get response body\n    body = await tab.get_network_response_body(request_id)\n    print(f\"Response body: {body[:100]}...\")\n\nawait tab.enable_network_events()\nawait tab.on(\n    NetworkEvent.RESPONSE_RECEIVED,\n    partial(process_response, tab)\n)\n```\n\n!!! info \"Why Use Partial?\"\n    The event system only passes the event data to callbacks. `partial` lets you bind additional parameters like the tab instance.\n\n## Managing Callbacks\n\n### Removing Callbacks\n\n```python\nfrom pydoll.protocol.page.events import PageEvent\n\n# Save the callback ID\ncallback_id = await tab.on(PageEvent.LOAD_EVENT_FIRED, my_callback)\n\n# Remove it later\nawait tab.remove_callback(callback_id)\n```\n\n### Clearing All Callbacks\n\n```python\n# Remove all registered callbacks for this tab\nawait tab.clear_callbacks()\n```\n\n## Practical Examples\n\n### Monitor API Calls\n\n```python\nimport asyncio\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def monitor_api_calls(tab):\n    collected_data = []\n    \n    # Type hint helps IDE autocomplete event keys\n    async def capture_api_response(tab, data_list, event: ResponseReceivedEvent):\n        url = event['params']['response']['url']\n        \n        # Filter only API calls\n        if '/api/' not in url:\n            return\n        \n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        \n        data_list.append({\n            'url': url,\n            'body': body,\n            'status': event['params']['response']['status']\n        })\n        print(f\"Captured API call: {url}\")\n    \n    await tab.enable_network_events()\n    await tab.on(\n        NetworkEvent.RESPONSE_RECEIVED,\n        partial(capture_api_response, tab, collected_data)\n    )\n    \n    # Navigate and collect\n    await tab.go_to(\"https://example.com\")\n    await asyncio.sleep(3)  # Wait for requests to complete\n    \n    return collected_data\n```\n\n### Wait for Specific Event\n\n```python\nimport asyncio\nfrom pydoll.protocol.page.events import PageEvent, FrameNavigatedEvent\n\nasync def wait_for_navigation():\n    navigation_done = asyncio.Event()\n    \n    async def on_navigated(event: FrameNavigatedEvent):\n        navigation_done.set()\n    \n    await tab.enable_page_events()\n    await tab.on(PageEvent.FRAME_NAVIGATED, on_navigated, temporary=True)\n    \n    # Trigger navigation\n    button = await tab.find(id='next-page')\n    await button.click()\n    \n    # Wait for it to complete\n    await navigation_done.wait()\n    print(\"Navigation completed!\")\n```\n\n### Network Idle Detection\n\n```python\nimport asyncio\nfrom pydoll.protocol.network.events import (\n    NetworkEvent,\n    RequestWillBeSentEvent,\n    LoadingFinishedEvent,\n    LoadingFailedEvent\n)\n\nasync def wait_for_network_idle(tab, timeout=5):\n    in_flight = 0\n    idle_event = asyncio.Event()\n    last_activity = asyncio.get_event_loop().time()\n    \n    async def on_request(event: RequestWillBeSentEvent):\n        nonlocal in_flight, last_activity\n        in_flight += 1\n        last_activity = asyncio.get_event_loop().time()\n    \n    async def on_finished(event: LoadingFinishedEvent | LoadingFailedEvent):\n        nonlocal in_flight, last_activity\n        in_flight -= 1\n        last_activity = asyncio.get_event_loop().time()\n        \n        if in_flight == 0:\n            idle_event.set()\n    \n    await tab.enable_network_events()\n    req_id = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, on_request)\n    fin_id = await tab.on(NetworkEvent.LOADING_FINISHED, on_finished)\n    fail_id = await tab.on(NetworkEvent.LOADING_FAILED, on_finished)\n    \n    try:\n        await asyncio.wait_for(idle_event.wait(), timeout=timeout)\n        print(\"Network is idle!\")\n    except asyncio.TimeoutError:\n        print(f\"Network still active after {timeout}s\")\n    finally:\n        # Cleanup\n        await tab.remove_callback(req_id)\n        await tab.remove_callback(fin_id)\n        await tab.remove_callback(fail_id)\n```\n\n### Dynamic Content Scraping\n\n```python\nimport asyncio\nimport json\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def scrape_infinite_scroll(tab, max_items=100):\n    items = []\n    \n    async def capture_products(tab, items_list, event: ResponseReceivedEvent):\n        url = event['params']['response']['url']\n        \n        # Look for product API endpoint\n        if '/products' not in url:\n            return\n        \n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        \n        try:\n            data = json.loads(body)\n            if 'items' in data:\n                items_list.extend(data['items'])\n                print(f\"Collected {len(data['items'])} items (total: {len(items_list)})\")\n        except json.JSONDecodeError:\n            pass\n    \n    await tab.enable_network_events()\n    await tab.on(\n        NetworkEvent.RESPONSE_RECEIVED,\n        partial(capture_products, tab, items)\n    )\n    \n    await tab.go_to(\"https://example.com/products\")\n    \n    # Scroll to trigger infinite loading\n    while len(items) < max_items:\n        await tab.execute_script(\"window.scrollTo(0, document.body.scrollHeight)\")\n        await asyncio.sleep(1)\n    \n    return items[:max_items]\n```\n\n## Event Reference Tables\n\n### Available Domains\n\n| Domain | Enable Method | Common Use Cases |\n|--------|--------------|------------------|\n| Page | `enable_page_events()` | Page lifecycle, navigation, dialogs |\n| Network | `enable_network_events()` | Request/response monitoring, API tracking |\n| DOM | `enable_dom_events()` | DOM structure changes, attribute modifications |\n| Fetch | `enable_fetch_events()` | Request interception and modification |\n| Runtime | `enable_runtime_events()` | Console messages, JavaScript exceptions |\n\n### Key Page Events\n\n| Event | When It Fires | Use Case |\n|-------|---------------|----------|\n| `LOAD_EVENT_FIRED` | Page load complete | Wait for full page load |\n| `DOM_CONTENT_EVENT_FIRED` | DOM ready | Start DOM manipulation |\n| `JAVASCRIPT_DIALOG_OPENING` | Alert/confirm/prompt | Auto-handle dialogs |\n| `FRAME_NAVIGATED` | Navigation complete | Track SPA navigation |\n| `FILE_CHOOSER_OPENED` | File input clicked | Automated file uploads |\n\n### Key Network Events\n\n| Event | When It Fires | Use Case |\n|-------|---------------|----------|\n| `REQUEST_WILL_BE_SENT` | Before request sent | Log/modify outgoing requests |\n| `RESPONSE_RECEIVED` | Response headers received | Capture API responses |\n| `LOADING_FINISHED` | Response body loaded | Get full response data |\n| `LOADING_FAILED` | Request failed | Track errors and retries |\n| `WEB_SOCKET_CREATED` | WebSocket opened | Monitor real-time connections |\n\n### Key DOM Events\n\n| Event | When It Fires | Use Case |\n|-------|---------------|----------|\n| `DOCUMENT_UPDATED` | DOM rebuilt | Refresh element references |\n| `ATTRIBUTE_MODIFIED` | Element attribute changed | Track dynamic attribute changes |\n| `CHILD_NODE_INSERTED` | New element added | Detect dynamically added content |\n| `CHILD_NODE_REMOVED` | Element removed | Detect removed content |\n\n### Event Type Reference\n\nAll event types and their parameter structures are defined in the protocol modules:\n\n| Domain | Import Path | Example Types |\n|--------|-------------|---------------|\n| Page | `pydoll.protocol.page.events` | `LoadEventFiredEvent`, `FrameNavigatedEvent`, `JavascriptDialogOpeningEvent` |\n| Network | `pydoll.protocol.network.events` | `RequestWillBeSentEvent`, `ResponseReceivedEvent`, `LoadingFinishedEvent` |\n| DOM | `pydoll.protocol.dom.events` | `DocumentUpdatedEvent`, `AttributeModifiedEvent`, `ChildNodeInsertedEvent` |\n| Fetch | `pydoll.protocol.fetch.events` | `RequestPausedEvent`, `AuthRequiredEvent` |\n| Runtime | `pydoll.protocol.runtime.events` | `ConsoleAPICalledEvent`, `ExceptionThrownEvent` |\n\nEach event type is a `TypedDict` that defines the exact structure of the event, including all available keys in the `params` dictionary.\n\n## Best Practices\n\n### 1. Always Enable Domains First\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent\n\n# Good\nawait tab.enable_network_events()\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, callback)\n\n# Bad: callback will never fire\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, callback)\nawait tab.enable_network_events()\n```\n\n### 2. Clean Up When Done\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent\n\n# Enable for specific task\nawait tab.enable_network_events()\ncallback_id = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, log_request)\n\n# Do your work...\nawait tab.go_to(\"https://example.com\")\n\n# Clean up\nawait tab.remove_callback(callback_id)\nawait tab.disable_network_events()\n```\n\n### 3. Use Early Filtering\n\n```python\nfrom pydoll.protocol.network.events import RequestWillBeSentEvent\n\n# Good: filter early\nasync def handle_api_request(event: RequestWillBeSentEvent):\n    url = event['params']['request']['url']\n    if '/api/' not in url:\n        return  # Exit early\n    \n    # Process only API requests\n    process_request(event)\n\n# Bad: processes everything\nasync def handle_all_requests(event: RequestWillBeSentEvent):\n    url = event['params']['request']['url']\n    process_request(event)\n    if '/api/' in url:\n        do_extra_work(event)\n```\n\n### 4. Handle Errors Gracefully\n\n```python\nfrom pydoll.protocol.network.events import ResponseReceivedEvent\n\nasync def safe_callback(event: ResponseReceivedEvent):\n    try:\n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        process_body(body)\n    except KeyError:\n        # Event might not have requestId\n        pass\n    except Exception as e:\n        print(f\"Error in callback: {e}\")\n        # Continue without breaking event loop\n```\n\n## Performance Considerations\n\n!!! warning \"High-Frequency Events\"\n    DOM events can fire **very frequently** on dynamic pages. Use filtering and debouncing to avoid performance issues.\n\n### Event Volume by Domain\n\n| Domain | Event Frequency | Performance Impact |\n|--------|----------------|-------------------|\n| Page | Low | Minimal |\n| Network | Moderate-High | Moderate |\n| DOM | Very High | High |\n| Fetch | Moderate | Moderate |\n\n### Optimization Tips\n\n1. **Enable only what you need**: Don't enable all domains at once\n2. **Use temporary callbacks**: Auto-cleanup when possible\n3. **Filter early**: Check conditions before expensive operations\n4. **Disable when done**: Free up resources\n5. **Avoid heavy processing**: Keep callbacks fast, offload work to separate tasks\n\n```python\nimport asyncio\nfrom pydoll.protocol.network.events import ResponseReceivedEvent\n\n# Good: fast callback, offload heavy work\nasync def handle_response(event: ResponseReceivedEvent):\n    if should_process(event):\n        asyncio.create_task(heavy_processing(event))  # Don't block\n\n# Bad: blocks event loop\nasync def handle_response(event: ResponseReceivedEvent):\n    await heavy_processing(event)  # Blocks other events\n```\n\n## Common Patterns\n\n### Context Manager for Events\n\n```python\nfrom contextlib import asynccontextmanager\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\n\n@asynccontextmanager\nasync def monitor_requests(tab):\n    \"\"\"Context manager to monitor requests during a block.\"\"\"\n    requests = []\n    \n    async def capture(event: RequestWillBeSentEvent):\n        requests.append(event['params']['request'])\n    \n    await tab.enable_network_events()\n    cb_id = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, capture)\n    \n    try:\n        yield requests\n    finally:\n        await tab.remove_callback(cb_id)\n        await tab.disable_network_events()\n\n# Usage\nasync with monitor_requests(tab) as requests:\n    await tab.go_to(\"https://example.com\")\n    # All requests are captured\n\nprint(f\"Captured {len(requests)} requests\")\n```\n\n### Conditional Event Registration\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent\nfrom pydoll.protocol.dom.events import DomEvent\n\nasync def setup_monitoring(tab, track_network=False, track_dom=False):\n    \"\"\"Enable only specified monitoring.\"\"\"\n    callbacks = []\n    \n    if track_network:\n        await tab.enable_network_events()\n        cb = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, log_request)\n        callbacks.append(('network', cb))\n    \n    if track_dom:\n        await tab.enable_dom_events()\n        cb = await tab.on(DomEvent.ATTRIBUTE_MODIFIED, log_dom_change)\n        callbacks.append(('dom', cb))\n    \n    return callbacks\n```\n\n## Further Reading\n\n- **[Event Architecture Deep Dive](../../deep-dive/event-architecture.md)** - Internal implementation and WebSocket communication\n- **[Network Monitoring](../network/monitoring.md)** - Advanced network analysis techniques\n- **[Reactive Automation](reactive-automation.md)** - Building event-driven workflows\n\n!!! tip \"Start Simple\"\n    Begin with Page events to understand the basics, then move to Network and DOM events as needed. The event system is powerful but can be overwhelming at first.\n"
  },
  {
    "path": "docs/en/features/advanced/remote-connections.md",
    "content": "# Remote Connections & Hybrid Automation\n\nPydoll allows you to connect to already-running browsers via WebSocket, enabling remote control and hybrid automation scenarios. This is perfect for CI/CD pipelines, containerized environments, debugging sessions, and integrating Pydoll with existing CDP tooling.\n\n!!! info \"Zero Setup Required\"\n    Unlike traditional automation that launches browsers, remote connections let you control browsers that are already running. No process management needed!\n\n## Why Remote Connections?\n\nRemote connections unlock powerful automation scenarios:\n\n| Use Case | Benefit |\n|----------|---------|\n| **CI/CD Pipelines** | Connect to browser containers without managing processes |\n| **Docker Environments** | Control browsers running in separate containers |\n| **Remote Debugging** | Automate browsers on remote servers or VMs |\n| **Hybrid Tooling** | Integrate Pydoll with your existing CDP infrastructure |\n| **Development** | Attach to your local browser for quick testing |\n| **Multi-Tool Automation** | Share browser sessions between different tools |\n\n## Setting Up a Remote Browser Server\n\n!!! tip \"Already Have a Remote Browser Service?\"\n    If you're using a cloud browser service (BrowserStack, Selenium Grid, LambdaTest, etc.) or already have a Chrome instance running with a WebSocket URL, you can **skip this entire section** and jump directly to [Connection Methods](#connection-methods) to learn how to connect with Pydoll.\n\nBefore you can connect remotely, you need to start Chrome with debugging enabled and properly configured to accept external connections.\n\n### Basic Server Setup (Linux)\n\nStart Chrome with remote debugging on a server:\n\n```bash\n# Basic setup - only accessible from localhost\ngoogle-chrome \\\n  --remote-debugging-port=9222 \\\n  --headless=new \\\n  --no-sandbox \\\n  --disable-dev-shm-usage \\\n  --user-data-dir=/tmp/chrome-profile\n\n# Server setup - accessible from other machines\ngoogle-chrome \\\n  --remote-debugging-port=9222 \\\n  --remote-debugging-address=0.0.0.0 \\\n  --headless=new \\\n  --no-sandbox \\\n  --disable-dev-shm-usage \\\n  --user-data-dir=/tmp/chrome-profile\n```\n\n!!! warning \"Security Critical\"\n    Using `--remote-debugging-address=0.0.0.0` makes the debugging port accessible from **any network interface**. This is necessary for remote connections but creates a significant security risk if exposed to the internet.\n\n### Recommended Server Configuration\n\n```bash\n# Production-ready configuration\ngoogle-chrome \\\n  --remote-debugging-port=9222 \\\n  --remote-debugging-address=0.0.0.0 \\\n  --headless=new \\\n  --no-sandbox \\\n  --disable-dev-shm-usage \\\n  --disable-gpu \\\n  --disable-software-rasterizer \\\n  --disable-extensions \\\n  --disable-background-networking \\\n  --disable-background-timer-throttling \\\n  --disable-client-side-phishing-detection \\\n  --disable-popup-blocking \\\n  --disable-prompt-on-repost \\\n  --disable-sync \\\n  --metrics-recording-only \\\n  --no-first-run \\\n  --safebrowsing-disable-auto-update \\\n  --user-data-dir=/tmp/chrome-remote-$(date +%s)\n```\n\n**Key flags explained:**\n\n| Flag | Purpose |\n|------|---------|\n| `--remote-debugging-port=9222` | Enable CDP on port 9222 |\n| `--remote-debugging-address=0.0.0.0` | Allow external connections (security risk!) |\n| `--headless=new` | Run without GUI (server mode) |\n| `--no-sandbox` | Required in Docker/containers (security tradeoff) |\n| `--disable-dev-shm-usage` | Prevent /dev/shm memory issues in containers |\n| `--disable-gpu` | No GPU acceleration (recommended for headless) |\n| `--user-data-dir=/tmp/...` | Isolated profile per instance |\n\n!!! warning \"About --no-sandbox Flag\"\n    The `--no-sandbox` flag disables Chrome's security sandbox, which isolates the browser process from the system. This flag is **required** in most Docker/container environments due to kernel capability restrictions, but it comes with security implications:\n    \n    - **Risk**: Removes isolation between browser and system\n    - **When to use**: Docker containers, restricted environments\n    - **Mitigation**: Ensure container-level isolation (namespaces, cgroups) and avoid running as root\n    \n    Consider using `--no-sandbox` only when absolutely necessary and implement additional security layers at the container level.\n\n### Docker Setup\n\nCreate a containerized Chrome server:\n\n!!! tip \"Using Pre-built Images\"\n    For production, consider using official pre-built images instead of building your own:\n    \n    - **Selenium Images**: `selenium/standalone-chrome` (includes WebDriver)\n    - **Zenika Alpine Chrome**: `zenika/alpine-chrome` (lightweight, ~200MB)\n    - **Browserless**: `browserless/chrome` (production-ready with monitoring)\n    \n    These images are regularly updated, security-tested, and optimized for container environments.\n\n**Dockerfile (Custom Build):**\n```dockerfile\nFROM ubuntu:22.04\n\n# Install Chrome\nRUN apt-get update && apt-get install -y \\\n    wget \\\n    gnupg \\\n    ca-certificates \\\n    && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \\\n    && echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list \\\n    && apt-get update \\\n    && apt-get install -y google-chrome-stable \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Expose debugging port\nEXPOSE 9222\n\n# Start Chrome with remote debugging\nCMD [\"google-chrome\", \\\n     \"--remote-debugging-port=9222\", \\\n     \"--remote-debugging-address=0.0.0.0\", \\\n     \"--headless=new\", \\\n     \"--no-sandbox\", \\\n     \"--disable-dev-shm-usage\", \\\n     \"--disable-gpu\", \\\n     \"--user-data-dir=/tmp/chrome-profile\"]\n```\n\n**docker-compose.yml:**\n```yaml\nservices:\n  chrome-server:\n    build: .\n    ports:\n      - \"127.0.0.1:9222:9222\"\n    \n    # Uncomment the line below ONLY if you need remote access \n    # AND have secured the port with a firewall or proxy.\n    # - \"9222:9222\"\n\n    shm_size: '2gb'  # Critical: Chrome uses /dev/shm for shared memory\n                      # Default Docker shm_size (64MB) is insufficient\n    restart: unless-stopped\n    environment:\n      - DISPLAY=:99\n    networks:\n      - automation-network\n    # Optional: Resource limits for production\n    # deploy:\n    #   resources:\n    #     limits:\n    #       cpus: '2'\n    #       memory: 4G\n\n  automation-client:\n    image: python:3.11\n    depends_on:\n      - chrome-server\n    volumes:\n      - ./:/app\n    working_dir: /app\n    command: python automation_script.py\n    environment:\n      - CHROME_WS=ws://chrome-server:9222/devtools/browser\n    networks:\n      - automation-network\n\nnetworks:\n  automation-network:\n    driver: bridge\n```\n\n**Usage:**\n```bash\n# Start the stack\ndocker-compose up -d\n\n# Check Chrome is running\ncurl http://localhost:9222/json/version\n\n# Connect from automation client (inside Docker network)\n# ws://chrome-server:9222/devtools/browser/...\n```\n\n### Systemd Service (Linux Server)\n\nCreate a persistent Chrome service:\n\n**/etc/systemd/system/chrome-remote.service:**\n```ini\n[Unit]\nDescription=Chrome Remote Debugging Server\nAfter=network.target\n\n[Service]\nType=simple\nUser=chrome-user\nGroup=chrome-user\nEnvironment=\"DISPLAY=:99\"\nExecStart=/usr/bin/google-chrome \\\n    --remote-debugging-port=9222 \\\n    --remote-debugging-address=0.0.0.0 \\\n    --headless=new \\\n    --no-sandbox \\\n    --disable-dev-shm-usage \\\n    --disable-gpu \\\n    --user-data-dir=/var/lib/chrome-remote\nRestart=always\nRestartSec=10\n\n[Install]\nWantedBy=multi-user.target\n```\n\n**Setup and management:**\n```bash\n# Create dedicated user\nsudo useradd -r -s /bin/false chrome-user\nsudo mkdir -p /var/lib/chrome-remote\nsudo chown chrome-user:chrome-user /var/lib/chrome-remote\n\n# Install and enable service\nsudo systemctl daemon-reload\nsudo systemctl enable chrome-remote\nsudo systemctl start chrome-remote\n\n# Check status\nsudo systemctl status chrome-remote\n\n# View logs\nsudo journalctl -u chrome-remote -f\n\n# Restart service\nsudo systemctl restart chrome-remote\n```\n\n### Network Security Configuration\n\n#### Firewall Rules (iptables)\n\n```bash\n# Allow only specific IPs to access port 9222\nsudo iptables -A INPUT -p tcp --dport 9222 -s 192.168.1.100 -j ACCEPT\nsudo iptables -A INPUT -p tcp --dport 9222 -j DROP\n\n# Save rules\nsudo iptables-save > /etc/iptables/rules.v4\n```\n\n#### Firewall Rules (ufw)\n\n```bash\n# Deny all access to port 9222 by default\nsudo ufw deny 9222\n\n# Allow specific IP\nsudo ufw allow from 192.168.1.100 to any port 9222\n\n# Allow specific subnet\nsudo ufw allow from 192.168.1.0/24 to any port 9222\n\n# Enable firewall\nsudo ufw enable\n```\n\n#### Nginx Reverse Proxy (with Authentication)\n\nProtect Chrome debugging with HTTP authentication:\n\n**/etc/nginx/sites-available/chrome-remote:**\n```nginx\nserver {\n    listen 80;\n    server_name chrome.example.com;\n\n    # Basic authentication\n    auth_basic \"Chrome Remote Debugging\";\n    auth_basic_user_file /etc/nginx/.htpasswd;\n\n    location / {\n        proxy_pass http://localhost:9222;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_read_timeout 86400;\n    }\n}\n```\n\n**Setup:**\n```bash\n# Create password file\nsudo htpasswd -c /etc/nginx/.htpasswd admin\n\n# Enable site\nsudo ln -s /etc/nginx/sites-available/chrome-remote /etc/nginx/sites-enabled/\nsudo nginx -t\nsudo systemctl reload nginx\n\n# Connect with authentication\n# ws://admin:password@chrome.example.com/devtools/browser/...\n```\n\n### Connecting from Another Computer\n\nOnce your server is configured, connect from your client machine:\n\n```python\nimport asyncio\nimport aiohttp\nfrom pydoll.browser.chromium import Chrome\n\nasync def connect_to_remote_server():\n    \"\"\"Connect to Chrome running on a remote server.\"\"\"\n    # Server IP and port\n    server_ip = \"192.168.1.100\"\n    server_port = 9222\n\n    async with aiohttp.ClientSession() as session:\n        # Query the server for available targets\n        url = f\"http://{server_ip}:{server_port}/json/version\"\n        \n        async with session.get(url) as response:\n            data = await response.json()\n            ws_url = data['webSocketDebuggerUrl']\n            \n            print(f\"Server info:\")\n            print(f\"  Browser: {data.get('Browser')}\")\n            print(f\"  Protocol: {data.get('Protocol-Version')}\")\n            print(f\"  WebSocket: {ws_url}\")\n    \n    # 2. Connect to the browser\n    chrome = Chrome()\n    tab = await chrome.connect(ws_url)\n    \n    print(f\"\\n[SUCCESS] Connected to remote Chrome server!\")\n    \n    # 3. Use normally\n    await tab.go_to('https://example.com')\n    title = await tab.execute_script('return document.title')\n    print(f\"Page title: {title}\")\n    \n    # 4. Cleanup\n    await chrome.close()\n\nasyncio.run(connect_to_remote_server())\n```\n\n### Testing Your Server Setup\n\n```bash\n# 1. Check if Chrome is running\nps aux | grep chrome\n\n# 2. Check if port is listening\nnetstat -tulpn | grep 9222\n# Or\nss -tulpn | grep 9222\n\n# 3. Test local access\ncurl http://localhost:9222/json/version\n\n# 4. Test remote access (from client machine)\ncurl http://SERVER_IP:9222/json/version\n\n# 5. Check WebSocket URL\ncurl http://SERVER_IP:9222/json/version | jq -r '.webSocketDebuggerUrl'\n\n# 6. List all available targets (tabs/pages)\ncurl http://SERVER_IP:9222/json/list\n```\n\n### Multi-Instance Setup\n\nRun multiple Chrome instances on different ports:\n\n```bash\n#!/bin/bash\n# start-chrome-pool.sh\n\nfor port in 9222 9223 9224 9225; do\n    google-chrome \\\n        --remote-debugging-port=$port \\\n        --remote-debugging-address=0.0.0.0 \\\n        --headless=new \\\n        --no-sandbox \\\n        --disable-dev-shm-usage \\\n        --user-data-dir=/tmp/chrome-$port &\n    \n    echo \"Started Chrome on port $port\"\ndone\n\necho \"Chrome pool ready. Ports: 9222-9225\"\n```\n\n**Python client with pool:**\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nimport aiohttp\n\nasync def connect_to_pool(server_ip: str, ports: list[int]):\n    \"\"\"Connect to multiple Chrome instances.\"\"\"\n    tasks = []\n    \n    for port in ports:\n        task = connect_to_instance(server_ip, port)\n        tasks.append(task)\n    \n    results = await asyncio.gather(*tasks)\n    return results\n\nasync def connect_to_instance(server_ip: str, port: int):\n    \"\"\"Connect to a single Chrome instance.\"\"\"\n    # Get WebSocket URL\n    async with aiohttp.ClientSession() as session:\n        url = f\"http://{server_ip}:{port}/json/version\"\n        async with session.get(url) as response:\n            data = await response.json()\n            ws_url = data['webSocketDebuggerUrl']\n    \n    # Connect\n    chrome = Chrome()\n    tab = await chrome.connect(ws_url)\n    \n    # Run automation\n    await tab.go_to('https://example.com')\n    title = await tab.execute_script('return document.title')\n    \n    print(f\"Port {port}: {title}\")\n    \n    await chrome.close()\n    return title\n\n# Usage\nasyncio.run(connect_to_pool('192.168.1.100', [9222, 9223, 9224, 9225]))\n```\n\n## Connection Methods\n\nPydoll provides two approaches for remote connections, each suited for different scenarios.\n\n### Method 1: Browser-Level Connection\n\nConnect to a running browser using its WebSocket endpoint and get access to all opened tabs:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def connect_to_remote_browser():\n    chrome = Chrome()\n    \n    # Connect to remote browser via WebSocket\n    tab = await chrome.connect('ws://localhost:9222/devtools/browser/XXXX')\n    \n    # The tab returned is the first available tab\n    print(f\"Connected to tab: {await tab.execute_script('return document.title')}\")\n    \n    # You can get all other tabs too\n    all_tabs = await chrome.get_opened_tabs()\n    print(f\"Total tabs available: {len(all_tabs)}\")\n    \n    # Use the tab normally\n    await tab.go_to('https://example.com')\n    element = await tab.find(id='main-content')\n    text = await element.text\n    print(f\"Content: {text}\")\n    \n    # Cleanup\n    await chrome.close()\n\nasyncio.run(connect_to_remote_browser())\n```\n\n!!! tip \"Getting the WebSocket URL\"\n    Start Chrome with debugging enabled:\n    ```bash\n    # Linux/Mac\n    google-chrome --remote-debugging-port=9222\n    \n    # Windows\n    \"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" --remote-debugging-port=9222\n    ```\n    \n    **For local connections** (same machine):\n    \n    - Visit `http://localhost:9222/json/version` in your browser to get the WebSocket URL in the `webSocketDebuggerUrl` field\n    - Or programmatically query it as shown in the example above using `aiohttp`\n    - For quick debugging, you can also check `browser._connection_port` after starting a local browser instance\n    \n    **For remote connections** (different machine):\n    \n    - Query `http://SERVER_IP:9222/json/version` from your client machine\n    - Use the `webSocketDebuggerUrl` from the response, replacing `localhost` with the actual server IP if needed\n\n### Method 2: Direct Element Control (Hybrid Approach)\n\nIf you already have your own CDP integration or low-level tooling, you can wrap existing elements with Pydoll's high-level API:\n\n```python\nimport asyncio\nimport json\nfrom pydoll.connection.connection_handler import ConnectionHandler\nfrom pydoll.elements.web_element import WebElement\n\nasync def custom_cdp_integration():\n    \"\"\"Use Pydoll alongside your custom CDP implementation.\"\"\"\n    # Your existing CDP setup has found an element\n    page_ws = 'ws://localhost:9222/devtools/page/ABC123'\n    \n    # You've used Runtime.evaluate to find an element\n    # and got its objectId\n    element_object_id = '{\\\"injectedScriptId\\\":1,\\\"id\\\":1}'\n    \n    # Create Pydoll connection\n    connection = ConnectionHandler(ws_address=page_ws)\n    \n    # Wrap the element\n    button = WebElement(\n        object_id=element_object_id,\n        connection_handler=connection\n    )\n    \n    # Use Pydoll's high-level methods\n    await button.wait_until(is_visible=True, timeout=5)\n    await button.wait_until(is_interactable=True)\n    \n    # Click with realistic offset\n    await button.click(offset_x=5, offset_y=5)\n    \n    # Get computed properties easily\n    is_enabled = await button.is_enabled()\n    bounds = await button.bounds\n    \n    print(f\"Button clicked! Enabled: {is_enabled}, Bounds: {bounds}\")\n    \n    # Cleanup\n    await connection.close()\n\nasyncio.run(custom_cdp_integration())\n```\n\n!!! tip \"Object ID Format\"\n    The `objectId` is a string returned by CDP commands like `Runtime.evaluate` or `DOM.resolveNode`. It's usually a JSON string with fields like `injectedScriptId` and `id`.\n\n\n!!! info \"Best of Both Worlds\"\n    This hybrid approach lets you leverage your existing CDP infrastructure while benefiting from Pydoll's ergonomic element API for interactions, waits, and property access.\n\n## Security Considerations\n\n!!! danger \"Production Environments\"\n    Remote debugging ports expose **full control** over the browser, including:\n    \n    - Access to all pages and data\n    - Ability to execute arbitrary JavaScript\n    - Cookie and session access\n    - File system access via downloads\n    \n    **Never expose debugging ports to the internet without proper authentication and network security!**\n\n### Recommended Security Practices\n\n| Practice | Why | How |\n|----------|-----|-----|\n| **SSH Tunnels** | Encrypt traffic and authenticate | `ssh -L 9222:localhost:9222 user@host` |\n| **VPN** | Network-level security | Connect via corporate/private VPN |\n| **Firewall Rules** | Restrict access | Allow only specific IPs |\n| **Docker Networks** | Container isolation | Use private Docker networks |\n| **No Public Exposure** | Prevent attacks | Never bind to `0.0.0.0` in production |\n\n## Further Reading\n\n- **[Event System](event-system.md)** - Monitor remote browser events\n- **[Network Monitoring](../network/monitoring.md)** - Track requests in remote browsers\n- **[Browser Options](../configuration/browser-options.md)** - Configure local browsers before starting\n\n!!! tip \"Start Local, Scale Remote\"\n    Develop your automation locally with `browser.start()` for quick iterations, then deploy with `browser.connect()` for production CI/CD pipelines and containerized environments.\n"
  },
  {
    "path": "docs/en/features/automation/file-operations.md",
    "content": "# File Operations\n\nFile uploads are one of the most challenging aspects of browser automation. Traditional tools often struggle with OS-level file dialogs, requiring complex workarounds or external libraries. Pydoll provides two straightforward approaches for handling file uploads, each suited for different scenarios.\n\n## Upload Methods\n\nPydoll supports two primary methods for file uploads:\n\n1. **Direct file input** (`set_input_files()`): Fast and direct, works with `<input type=\"file\">` elements\n2. **File chooser context manager** (`expect_file_chooser()`): Intercepts the file dialog, works with any upload trigger\n\n## Direct File Input\n\nThe simplest approach is using `set_input_files()` directly on file input elements. This method is fast, reliable, and bypasses the OS file dialog entirely.\n\n### Basic Usage\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def direct_file_upload():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/upload')\n        \n        # Find the file input element\n        file_input = await tab.find(tag_name='input', type='file')\n        \n        # Set the file directly\n        file_path = Path('path/to/document.pdf')\n        await file_input.set_input_files(file_path)\n        \n        # Submit the form\n        submit_button = await tab.find(id='submit-button')\n        await submit_button.click()\n        \n        print(\"File uploaded successfully!\")\n\nasyncio.run(direct_file_upload())\n```\n\n!!! tip \"Path vs String\"\n    While `Path` objects from `pathlib` are recommended as best practice for better path handling and cross-platform compatibility, you can also use plain strings if preferred:\n    ```python\n    await file_input.set_input_files('path/to/document.pdf')  # Also works!\n    ```\n\n### Multiple Files\n\nFor inputs that accept multiple files (`<input type=\"file\" multiple>`), pass a list of file paths:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def upload_multiple_files():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/multi-upload')\n        \n        file_input = await tab.find(tag_name='input', type='file')\n        \n        # Upload multiple files at once\n        files = [\n            Path('documents/report.pdf'),\n            Path('images/screenshot.png'),\n            Path('data/results.csv')\n        ]\n        await file_input.set_input_files(files)\n        \n        # Process as normal\n        upload_btn = await tab.find(id='upload-btn')\n        await upload_btn.click()\n\nasyncio.run(upload_multiple_files())\n```\n\n### Dynamic Path Resolution\n\n`Path` objects make it easy to build paths dynamically and handle cross-platform compatibility:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def upload_with_dynamic_paths():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/upload')\n        \n        file_input = await tab.find(tag_name='input', type='file')\n        \n        # Build paths dynamically\n        project_dir = Path(__file__).parent\n        file_path = project_dir / 'uploads' / 'data.json'\n\n        await file_input.set_input_files(file_path)\n        # Or use home directory\n        user_file = Path.home() / 'Documents' / 'report.pdf'\n        await file_input.set_input_files(user_file)\n\nasyncio.run(upload_with_dynamic_paths())\n```\n\n!!! tip \"When to Use Direct File Input\"\n    Use `set_input_files()` when:\n    \n    - The file input is directly accessible in the DOM\n    - You want maximum speed and simplicity\n    - The upload doesn't trigger a file chooser dialog\n    - You're working with standard `<input type=\"file\">` elements\n\n## File Chooser Context Manager\n\nSome websites hide the file input and use custom buttons or drag-and-drop areas that trigger the OS file chooser dialog. For these cases, use the `expect_file_chooser()` context manager.\n\n### How It Works\n\nThe `expect_file_chooser()` context manager:\n\n1. Enables file chooser interception\n2. Waits for the file chooser dialog to open\n3. Automatically sets the files when the dialog appears\n4. Cleans up after the operation completes\n\n### Basic Usage\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def file_chooser_upload():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/custom-upload')\n        \n        # Prepare the file path\n        file_path = Path.cwd() / 'document.pdf'\n        \n        # Use context manager to handle file chooser\n        async with tab.expect_file_chooser(files=file_path):\n            # Click the custom upload button\n            upload_button = await tab.find(class_name='custom-upload-btn')\n            await upload_button.click()\n            # File is automatically set when dialog opens\n        \n        # Continue with your automation\n        print(\"File selected via chooser!\")\n\nasyncio.run(file_chooser_upload())\n```\n\n### Multiple Files with File Chooser\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def multiple_files_chooser():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/gallery-upload')\n        \n        # Prepare multiple files\n        photos_dir = Path.home() / 'photos'\n        files = [\n            photos_dir / 'img1.jpg',\n            photos_dir / 'img2.jpg',\n            photos_dir / 'img3.jpg'\n        ]\n        \n        async with tab.expect_file_chooser(files=files):\n            # Trigger upload via custom button\n            add_photos_btn = await tab.find(text='Add Photos')\n            await add_photos_btn.click()\n        \n        print(f\"{len(files)} files selected!\")\n\nasyncio.run(multiple_files_chooser())\n```\n\n### Dynamic File Selection\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def dynamic_file_selection():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/batch-upload')\n        \n        # Find all CSV files in a directory using Path.glob()\n        data_dir = Path('data')\n        csv_files = list(data_dir.glob('*.csv'))\n        \n        async with tab.expect_file_chooser(files=csv_files):\n            upload_area = await tab.find(class_name='drop-zone')\n            await upload_area.click()\n        \n        print(f\"Selected {len(csv_files)} CSV files\")\n\nasyncio.run(dynamic_file_selection())\n```\n\n!!! tip \"When to Use File Chooser\"\n    Use `expect_file_chooser()` when:\n    \n    - The file input is hidden or not directly accessible\n    - Custom buttons trigger the file chooser dialog\n    - Working with drag-and-drop upload areas\n    - The site uses JavaScript to open file dialogs\n\n## Comparison: Direct vs File Chooser\n\n| Feature | `set_input_files()` | `expect_file_chooser()` |\n|---------|---------------------|-------------------------|\n| **Speed** | ⚡ Instant | 🕐 Waits for dialog |\n| **Complexity** | Simple | Requires context manager |\n| **Requirements** | Visible file input | Any upload trigger |\n| **Use Case** | Standard forms | Custom upload UIs |\n| **Event Handling** | Not needed | Uses page events |\n\n## Complete Example\n\nHere's a comprehensive example combining both approaches:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def comprehensive_upload_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/upload-form')\n        \n        # Scenario 1: Direct input for profile picture (single file)\n        avatar_input = await tab.find(id='avatar-upload')\n        avatar_path = Path.home() / 'Pictures' / 'profile.jpg'\n        await avatar_input.set_input_files(avatar_path)\n        \n        # Wait a bit for preview to load\n        await asyncio.sleep(1)\n        \n        # Scenario 2: File chooser for document upload\n        document_path = Path.cwd() / 'documents' / 'resume.pdf'\n        async with tab.expect_file_chooser(files=document_path):\n            # Custom styled button that triggers file chooser\n            upload_btn = await tab.find(class_name='btn-upload-document')\n            await upload_btn.click()\n        \n        # Wait for upload confirmation\n        await asyncio.sleep(2)\n        \n        # Scenario 3: Multiple files via file chooser\n        certs_dir = Path('certs')\n        certificates = [\n            certs_dir / 'certificate1.pdf',\n            certs_dir / 'certificate2.pdf',\n            certs_dir / 'certificate3.pdf'\n        ]\n        async with tab.expect_file_chooser(files=certificates):\n            add_certs_btn = await tab.find(text='Add Certificates')\n            await add_certs_btn.click()\n        \n        # Submit the complete form\n        submit_button = await tab.find(type='submit')\n        await submit_button.click()\n        \n        # Wait for success message\n        success_msg = await tab.find(class_name='success-message', timeout=10)\n        message_text = await success_msg.text\n        print(f\"Upload result: {message_text}\")\n\nasyncio.run(comprehensive_upload_example())\n```\n\n!!! info \"Method Summary\"\n    This example demonstrates the flexibility of Pydoll's file upload system:\n    \n    - **Single files**: Pass `Path` or `str` directly (no list needed)\n    - **Multiple files**: Pass a list of `Path` or `str` objects\n    - **Direct input**: Fast for visible `<input>` elements\n    - **File chooser**: Works with custom upload buttons and hidden inputs\n\n## Learn More\n\nFor deeper understanding of the file upload mechanisms:\n\n- **[Event System](../advanced/event-system.md)**: Learn about the page events used by `expect_file_chooser()`\n- **[Deep Dive: Tab Domain](../../deep-dive/tab-domain.md#file-chooser-handling)**: Technical details on file chooser interception\n- **[Deep Dive: Event System](../../deep-dive/event-system.md#file-chooser-events)**: How file chooser events work under the hood\n\nFile operations in Pydoll eliminate one of the biggest pain points in browser automation, providing clean, reliable methods for both simple and complex upload scenarios.\n"
  },
  {
    "path": "docs/en/features/automation/human-interactions.md",
    "content": "# Human-Like Interactions\n\nOne of the key differentiators between successful automation and easily-detected bots is how realistic the interactions are. Pydoll provides sophisticated tools to make your automation virtually indistinguishable from human behavior.\n\n!!! info \"Feature Status\"\n    **Already Implemented:**\n\n    - **Humanized Keyboard**: Variable typing speed, realistic typos with auto-correction (pass `humanize=True`)\n    - **Humanized Scroll**: Physics-based scrolling with momentum, friction, jitter, and overshoot (pass `humanize=True`)\n    - **Humanized Mouse**: Bezier curve paths, Fitts's Law timing, minimum-jerk velocity, tremor, and overshoot (pass `humanize=True`)\n\n    **Coming Soon:**\n\n    - **Automatic random click offsets**: Optional parameter to automatically randomize click positions within elements\n    - **Hover behavior**: Realistic delays and movement when hovering over elements\n\n## Why Human-Like Interactions Matter\n\nModern websites employ sophisticated bot detection techniques:\n\n- **Event timing analysis**: Detecting impossibly fast or perfectly timed actions\n- **Mouse movement tracking**: Identifying straight-line movements or instant teleportation\n- **Keyboard patterns**: Spotting instant text insertion without individual keystrokes\n- **Click positions**: Detecting clicks always at exact center of elements\n- **Action sequences**: Identifying non-human patterns in user behavior\n\nPydoll helps you avoid detection by providing realistic interaction methods that mimic real user behavior.\n\n## Realistic Mouse Movement\n\nThe Mouse API (`tab.mouse`) provides humanized cursor control with multiple layers of realism. When `humanize=True`, mouse movements follow natural Bezier curve paths with Fitts's Law timing, minimum-jerk velocity profiles, physiological tremor, and overshoot correction.\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n\n    # Move with natural curved path\n    await tab.mouse.move(500, 300, humanize=True)\n\n    # Click with realistic movement, offset, and timing\n    await tab.mouse.click(500, 300, humanize=True)\n\n    # Drag with natural movement\n    await tab.mouse.drag(100, 200, 500, 400, humanize=True)\n```\n\nKey techniques applied during humanized mouse operations:\n\n- **Bezier curve paths**: Curved trajectories with asymmetric control points (more curvature early in the movement)\n- **Fitts's Law timing**: Movement duration scales with distance: `MT = a + b × log₂(D/W + 1)`\n- **Minimum-jerk velocity**: Bell-shaped speed profile, slow start, peak in the middle, slow end\n- **Physiological tremor**: Gaussian noise (σ ≈ 1px) scaled inversely with velocity\n- **Overshoot and correction**: ~70% chance of overshooting fast movements by 3–12%, then correcting back\n!!! info \"Dedicated Mouse Control Documentation\"\n    For comprehensive mouse control documentation, including all methods, custom timing configuration, position tracking, and debug mode, see **[Mouse Control](mouse-control.md)**.\n\n## Realistic Clicking\n\n### Basic Click with Simulated Mouse Events\n\nThe `click()` method simulates real mouse press and release events, unlike JavaScript-based clicking:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def realistic_clicking():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        button = await tab.find(id=\"submit-button\")\n        \n        # Basic realistic click\n        await button.click()\n        \n        # The click includes:\n        # - Mouse move to element\n        # - Mouse press event\n        # - Configurable hold time\n        # - Mouse release event\n\nasyncio.run(realistic_clicking())\n```\n\n### Click with Position Offset\n\nReal users rarely click at the exact center of elements. Use offsets to vary click positions:\n\n!!! info \"Current State: Manual Offset Calculation\"\n    Currently, you must manually calculate and randomize click offsets for each interaction. Future versions will include an optional parameter to automatically randomize click positions within element bounds.\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\nasync def click_with_offset():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/form')\n        \n        submit_button = await tab.find(tag_name=\"button\", type=\"submit\")\n        \n        # Click slightly off-center (more natural)\n        await submit_button.click(\n            x_offset=5,   # 5 pixels right of center\n            y_offset=-3   # 3 pixels above center\n        )\n        \n        # Currently: Manually vary the offset for each click to appear more human\n        for item in await tab.find(class_name=\"clickable-item\", find_all=True):\n            offset_x = random.randint(-10, 10)\n            offset_y = random.randint(-10, 10)\n            await item.click(x_offset=offset_x, y_offset=offset_y)\n            await asyncio.sleep(random.uniform(0.5, 2.0))\n\nasyncio.run(click_with_offset())\n```\n\n### Adjustable Click Hold Time\n\nVary the duration of mouse button press to simulate different click styles:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def variable_hold_time():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        button = await tab.find(class_name=\"action-button\")\n        \n        # Quick click (default is 0.1s)\n        await button.click(hold_time=0.05)\n        \n        # Normal click\n        await button.click(hold_time=0.1)\n        \n        # Slower, more deliberate click\n        await button.click(hold_time=0.2)\n        \n        # Simulate user hesitation\n        await asyncio.sleep(0.8)\n        await button.click(hold_time=0.15)\n\nasyncio.run(variable_hold_time())\n```\n\n### When to Use click() vs click_using_js()\n\nUnderstanding the difference is crucial for avoiding detection:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def click_methods_comparison():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        button = await tab.find(id=\"interactive-button\")\n        \n        # Method 1: click() - Simulates real mouse events\n        # ✅ Triggers all mouse events (mousedown, mouseup, click)\n        # ✅ Respects element positioning\n        # ✅ More realistic and harder to detect\n        # ❌ Requires element to be visible and in viewport\n        await button.click()\n        \n        # Method 2: click_using_js() - Uses JavaScript click()\n        # ✅ Works on hidden elements\n        # ✅ Faster execution\n        # ✅ Bypasses visual overlays\n        # ❌ May be detected as automation\n        # ❌ Doesn't trigger same event sequence as real user\n        await button.click_using_js()\n\nasyncio.run(click_methods_comparison())\n```\n\n!!! tip \"Best Practice: Prefer Mouse Events\"\n    Use `click()` for user-facing interactions to maintain realism. Reserve `click_using_js()` for backend operations, hidden elements, or when speed is critical and detection isn't a concern.\n\n## Realistic Text Input\n\nPydoll's keyboard API provides two typing modes to balance speed and stealth.\n\n!!! info \"Understanding Typing Modes\"\n    | Mode | Parameters | Behavior | Use Case |\n    |------|------------|----------|----------|\n    | **Default (Fast)** | `humanize=False` | Fixed 50ms intervals, no typos | Speed-critical, low-risk scenarios (default) |\n    | **Humanized** | `humanize=True` | Variable timing, ~2% typo rate with auto-correction | **Anti-bot evasion** |\n\n    The `interval` parameter is deprecated. Pass `humanize=True` for realistic typing.\n\n### Natural Typing with Humanization\n\nWhen `humanize=True` is passed, `type_text()` uses humanized mode, simulating realistic human typing with variable speeds and occasional typos that are automatically corrected:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def natural_typing():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/login')\n        \n        username_field = await tab.find(id=\"username\")\n        password_field = await tab.find(id=\"password\")\n\n        # Variable speed: 30-120ms between keystrokes\n        # ~2% typo rate with realistic correction behavior\n        await username_field.type_text(\"john.doe@example.com\", humanize=True)\n        await password_field.type_text(\"MyC0mpl3xP@ssw0rd!\", humanize=True)\n\nasyncio.run(natural_typing())\n```\n\n### Fast Input for Non-Visible Fields\n\nFor fields that don't require realism (like hidden fields or backend operations), use `insert_text()`:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def fast_vs_realistic_input():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/form')\n        \n        username = await tab.find(id=\"username\")\n        await username.click()\n        await username.type_text(\"john_doe\", humanize=True)\n        \n        hidden_field = await tab.find(id=\"hidden-token\")\n        await hidden_field.insert_text(\"very-long-generated-token-12345678\")\n        \n        comment = await tab.find(id=\"comment-box\")\n        await comment.click()\n        await comment.type_text(\"This looks like human input!\", humanize=True)\n\nasyncio.run(fast_vs_realistic_input())\n```\n\n!!! info \"Advanced Keyboard Control\"\n    For comprehensive keyboard control documentation, including special keys, key combinations, modifiers, and complete key reference tables, see **[Keyboard Control](keyboard-control.md)**.\n\n## Realistic Page Scrolling\n\nPydoll provides a dedicated scroll API that waits for scroll completion before proceeding, making your automations more realistic and reliable.\n\n!!! info \"Understanding Scroll Modes\"\n    Pydoll's scroll API offers **three distinct modes**:\n\n    | Mode | Parameters | Behavior | Use Case |\n    |------|------------|----------|----------|\n    | **Smooth (Default)** | `smooth=True` | CSS-based animation, predictable | General browsing simulation (default) |\n    | **Humanized** | `humanize=True` | Physics engine with momentum, jitter, overshoot | **Anti-bot evasion** |\n    | **Instant** | `smooth=False` | Teleports to position immediately | Speed-critical operations |\n\n    Pass `humanize=True` for physics-based humanized scrolling to evade bot detection.\n\n### Basic Directional Scrolling\n\nUse the `scroll.by()` method to scroll the page in any direction with precise control:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import ScrollPosition\n\nasync def basic_scrolling():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/long-page')\n        \n        # Humanized - physics engine with Bezier curves\n        # Includes: momentum, friction, jitter, micro-pauses, overshoot\n        await tab.scroll.by(ScrollPosition.DOWN, 500, humanize=True)\n        await tab.scroll.by(ScrollPosition.UP, 300, humanize=True)\n\n        # CSS-based animation - looks nice but predictable timing\n        await tab.scroll.by(ScrollPosition.DOWN, 500, humanize=False, smooth=True)\n\n        # Teleports instantly - fastest but easily detectable\n        await tab.scroll.by(ScrollPosition.DOWN, 1000, humanize=False, smooth=False)\n\nasyncio.run(basic_scrolling())\n```\n\n### Scrolling to Specific Positions\n\nNavigate to the top or bottom of the page with control over realism:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scroll_to_positions():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/article')\n        \n        # Read the beginning of the article\n        await asyncio.sleep(2.0)\n        \n        # Humanized scroll (physics engine, anti-bot evasion)\n        await tab.scroll.to_bottom(humanize=True)\n        await asyncio.sleep(1.5)\n        await tab.scroll.to_top(humanize=True)\n\n        # CSS smooth scroll (predictable animation)\n        await tab.scroll.to_bottom(humanize=False, smooth=True)\n        await asyncio.sleep(1.5)\n        await tab.scroll.to_top(humanize=False, smooth=True)\n\nasyncio.run(scroll_to_positions())\n```\n\n!!! tip \"Choosing the Right Mode\"\n    - **`humanize=True`**: Best for anti-bot evasion\n    - **Default** (`smooth=True`): Good for demos, screenshots, and general automation\n    - **`smooth=False`**: Maximum speed when stealth is not a concern\n\n### Human-Like Scrolling Patterns\n\nPydoll's scroll engine uses **Cubic Bezier curves** to simulate the physics of human scrolling. This includes:\n\n- **Momentum**: Initial burst of speed followed by gradual deceleration.\n- **Friction**: Natural slowing down based on \"physical\" resistance.\n- **Micro-pauses**: Brief stops during long scrolls, mimicking reading or eye movement.\n- **Overshoot**: Occasional scrolling past the target and correcting back.\n\nThis behavior is automatically enabled when you use `humanize=True`.\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import ScrollPosition\n\nasync def human_like_scrolling():\n    \"\"\"Simulate natural scrolling patterns while reading an article.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/article')\n        \n        # User starts reading from top\n        await asyncio.sleep(random.uniform(2.0, 4.0))\n        \n        # Gradually scroll while reading\n        # The scroll engine handles the physics (acceleration/deceleration)\n        for _ in range(random.randint(5, 8)):\n            # Varied scroll distances (simulates reading speed)\n            scroll_distance = random.randint(300, 600)\n            await tab.scroll.by(\n                ScrollPosition.DOWN, \n                scroll_distance, \n                humanize=True # Enables Bezier curve physics\n            )\n            \n            # Pause to \"read\" content\n            await asyncio.sleep(random.uniform(2.0, 5.0))\n        \n        # Quick scroll to check the end\n        await tab.scroll.to_bottom(humanize=True)\n        await asyncio.sleep(random.uniform(1.0, 2.0))\n        \n        # Scroll back to top to re-read something\n        await tab.scroll.to_top(humanize=True)\n\nasyncio.run(human_like_scrolling())\n```\n\n### Scrolling Elements into View\n\nUse `scroll_into_view()` to ensure elements are visible before taking page screenshots:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scroll_for_screenshots():\n    \"\"\"Scroll elements into view before capturing page screenshots.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/product')\n        \n        # Scroll to pricing section before taking full page screenshot\n        pricing_section = await tab.find(id=\"pricing\")\n        await pricing_section.scroll_into_view()\n        await tab.take_screenshot(path=\"page_with_pricing.png\")\n        \n        # Scroll to reviews section before screenshot\n        reviews = await tab.find(class_name=\"reviews\")\n        await reviews.scroll_into_view()\n        await tab.take_screenshot(path=\"page_with_reviews.png\")\n        \n        # Scroll to footer to capture complete page state\n        footer = await tab.find(tag_name=\"footer\")\n        await footer.scroll_into_view()\n        await tab.take_screenshot(path=\"page_with_footer.png\")\n        \n        # Note: click() already scrolls automatically, so no need for:\n        # await button.scroll_into_view()  # Unnecessary!\n        # await button.click()  # This already scrolls the button into view\n\nasyncio.run(scroll_for_screenshots())\n```\n\n### Handling Infinite Scroll Content\n\nImplement scrolling patterns to load lazy-loaded content:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import ScrollPosition\n\nasync def infinite_scroll_loading():\n    \"\"\"Load content on infinite scroll pages.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/feed')\n        \n        items_loaded = 0\n        max_scrolls = 10\n        \n        for scroll_num in range(max_scrolls):\n            # Scroll to bottom to trigger loading\n            await tab.scroll.to_bottom(smooth=True)\n            \n            # Wait for content to load\n            await asyncio.sleep(random.uniform(2.0, 3.0))\n            \n            # Check if new items were loaded\n            items = await tab.find(class_name=\"feed-item\", find_all=True)\n            new_count = len(items)\n            \n            if new_count == items_loaded:\n                print(\"No more content to load\")\n                break\n            \n            items_loaded = new_count\n            print(f\"Scroll {scroll_num + 1}: {items_loaded} items loaded\")\n            \n            # Small scroll up (human behavior)\n            if random.random() > 0.7:\n                await tab.scroll.by(ScrollPosition.UP, 200, smooth=True)\n                await asyncio.sleep(random.uniform(0.5, 1.0))\n\nasyncio.run(infinite_scroll_loading())\n```\n\n!!! success \"Automatic Completion Waiting\"\n    Unlike `execute_script(\"window.scrollBy(...)\")` which returns immediately, the `scroll` API uses CDP's `awaitPromise` parameter to wait for the browser's `scrollend` event. This ensures your subsequent actions only execute after scrolling completely finishes.\n\n## Combining Techniques for Maximum Realism\n\n### Complete Form Filling Example\n\nHere's a comprehensive example combining all human-like interaction techniques. **This demonstrates the current manual approach** for achieving maximum realism. Future versions will automate much of this randomization:\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import Key\n\nasync def human_like_form_filling():\n    \"\"\"Fill a form with maximum realism to avoid detection.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/registration')\n        \n        # Wait a bit (user reading the page)\n        await asyncio.sleep(random.uniform(1.5, 3.0))\n        \n        # Fill first name with variable typing speed\n        first_name = await tab.find(id=\"first-name\")\n        await first_name.click(\n            x_offset=random.randint(-5, 5),\n            y_offset=random.randint(-5, 5)\n        )\n        await asyncio.sleep(random.uniform(0.2, 0.5))\n        \n        # Manual character-by-character typing with randomized delays\n        # (This will be automated in future versions)\n        name_text = \"John\"\n        for char in name_text:\n            await first_name.type_text(char, interval=0)\n            await asyncio.sleep(random.uniform(0.08, 0.22))\n        \n        # Tab to next field\n        await asyncio.sleep(random.uniform(0.3, 0.8))\n        await first_name.press_keyboard_key(Key.TAB)\n        \n        # Fill last name\n        await asyncio.sleep(random.uniform(0.2, 0.5))\n        last_name = await tab.find(id=\"last-name\")\n        await last_name.type_text(\"Doe\", interval=random.uniform(0.1, 0.18))\n        \n        # Tab to email\n        await asyncio.sleep(random.uniform(0.4, 1.0))\n        await last_name.press_keyboard_key(Key.TAB)\n        \n        # Fill email with realistic pauses\n        await asyncio.sleep(random.uniform(0.2, 0.5))\n        email = await tab.find(id=\"email\")\n        \n        email_text = \"john.doe@example.com\"\n        for i, char in enumerate(email_text):\n            await email.type_text(char, interval=0)\n            # Longer pause at @ and . symbols (natural)\n            if char in ['@', '.']:\n                await asyncio.sleep(random.uniform(0.2, 0.4))\n            else:\n                await asyncio.sleep(random.uniform(0.08, 0.2))\n        \n        # Simulate user reviewing what they typed\n        await asyncio.sleep(random.uniform(1.0, 2.5))\n        \n        # Accept terms checkbox with offset\n        terms_checkbox = await tab.find(id=\"accept-terms\")\n        await terms_checkbox.click(\n            x_offset=random.randint(-3, 3),\n            y_offset=random.randint(-3, 3),\n            hold_time=random.uniform(0.08, 0.15)\n        )\n        \n        # Pause before submitting (user reviewing form)\n        await asyncio.sleep(random.uniform(1.5, 3.0))\n        \n        # Click submit with realistic parameters\n        submit_button = await tab.find(tag_name=\"button\", type=\"submit\")\n        await submit_button.click(\n            x_offset=random.randint(-8, 8),\n            y_offset=random.randint(-5, 5),\n            hold_time=random.uniform(0.1, 0.2)\n        )\n        \n        print(\"Form submitted with human-like behavior\")\n\nasyncio.run(human_like_form_filling())\n```\n\n## Best Practices for Avoiding Detection\n\n!!! tip \"Manual Randomization Currently Required\"\n    The following best practices represent the **current state of Pydoll**, where you must manually implement randomization. While this requires more code, it gives you fine-grained control over behavior. Future versions will automate these patterns while maintaining the same level of realism.\n\n### 1. Always Add Random Delays\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\n# Bad: Predictable timing\nawait element1.click()\nawait element2.click()\nawait element3.click()\n\n# Good: Variable timing (currently required)\nawait element1.click()\nawait asyncio.sleep(random.uniform(0.5, 1.5))\nawait element2.click()\nawait asyncio.sleep(random.uniform(0.8, 2.0))\nawait element3.click()\n```\n\n### 2. Vary Click Positions\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\n# Bad: Always center clicks\nfor button in buttons:\n    await button.click()\n\n# Good: Varied positions (currently manual)\nfor button in buttons:\n    await button.click(\n        x_offset=random.randint(-10, 10),\n        y_offset=random.randint(-10, 10)\n    )\n```\n\n### 3. Simulate Natural User Behavior\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\nasync def natural_user_simulation(tab):\n    # User arrives at page\n    await tab.go_to('https://example.com')\n    \n    # User reads page content (1-3 seconds)\n    await asyncio.sleep(random.uniform(1.0, 3.0))\n    \n    # User scrolls down to see more\n    await tab.scroll.by(ScrollPosition.DOWN, 300, smooth=True)\n    await asyncio.sleep(random.uniform(0.5, 1.5))\n    \n    # User finds and clicks button\n    button = await tab.find(class_name=\"cta-button\")\n    await button.click(\n        x_offset=random.randint(-5, 5),\n        y_offset=random.randint(-5, 5)\n    )\n    \n    # User waits for content to load\n    await asyncio.sleep(random.uniform(0.8, 1.5))\n```\n\n### 4. Combine Multiple Techniques\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\nasync def advanced_stealth_automation():\n    \"\"\"Combine multiple techniques for maximum stealth.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Use human-like page load waiting\n        await tab.go_to('https://example.com/sensitive-page')\n        await asyncio.sleep(random.uniform(2.0, 4.0))\n        \n        # Scroll realistically with the dedicated API\n        for _ in range(random.randint(2, 4)):\n            scroll_amount = random.randint(200, 500)\n            await tab.scroll.by(ScrollPosition.DOWN, scroll_amount, smooth=True)\n            await asyncio.sleep(random.uniform(0.8, 2.0))\n        \n        # Find element with timeout (simulating user search)\n        target = await tab.find(\n            class_name=\"target-element\",\n            timeout=random.randint(3, 7)\n        )\n        \n        # Click with all realistic parameters\n        await target.click(\n            x_offset=random.randint(-12, 12),\n            y_offset=random.randint(-8, 8),\n            hold_time=random.uniform(0.09, 0.18)\n        )\n        \n        # Human reaction time\n        await asyncio.sleep(random.uniform(0.5, 1.2))\n\nasyncio.run(advanced_stealth_automation())\n```\n\n## Performance vs Realism Trade-offs\n\nSometimes you need to balance speed with realism:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def balanced_automation():\n    \"\"\"Choose appropriate realism level based on context.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/scraping-target')\n        \n        # Phase 1: Initial interaction (high realism)\n        # This is when detection systems are most active\n        login_button = await tab.find(text=\"Login\")\n        await asyncio.sleep(random.uniform(1.0, 2.0))\n        await login_button.click(\n            x_offset=random.randint(-5, 5),\n            y_offset=random.randint(-5, 5)\n        )\n        \n        await asyncio.sleep(random.uniform(0.5, 1.0))\n        \n        username = await tab.find(id=\"username\")\n        await username.type_text(\"user@example.com\", interval=0.12)\n        \n        await asyncio.sleep(random.uniform(0.3, 0.7))\n        \n        password = await tab.find(id=\"password\")\n        await password.type_text(\"password123\", interval=0.10)\n        \n        submit = await tab.find(type=\"submit\")\n        await asyncio.sleep(random.uniform(0.8, 1.5))\n        await submit.click()\n        \n        # Phase 2: Authenticated data extraction (lower realism, higher speed)\n        # Less scrutiny after successful authentication\n        await asyncio.sleep(2)\n        \n        # Fast navigation through pages\n        items = await tab.find(class_name=\"data-item\", find_all=True)\n        \n        for item in items:\n            # Quick click without offsets\n            await item.click_using_js()\n            await asyncio.sleep(0.3)  # Minimal delay\n            \n            # Extract data\n            title = await tab.find(class_name=\"title\")\n            data = await title.text\n            \n            # Fast navigation\n            await tab.execute_script(\"window.history.back()\")\n            await asyncio.sleep(0.5)\n\nasyncio.run(balanced_automation())\n```\n\n## Monitoring and Adjusting\n\nTest your automation's realism:\n\n```python\nimport asyncio\nimport random\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def test_interaction_timing():\n    \"\"\"Log timing to ensure realistic patterns.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/test-page')\n        \n        # Measure and log interaction timing\n        elements = await tab.find(class_name=\"clickable\", find_all=True)\n        \n        timings = []\n        last_time = time.time()\n        \n        for i, element in enumerate(elements):\n            await element.click(\n                x_offset=random.randint(-8, 8),\n                y_offset=random.randint(-8, 8)\n            )\n            \n            current_time = time.time()\n            elapsed = current_time - last_time\n            timings.append(elapsed)\n            \n            print(f\"Click {i+1}: {elapsed:.3f}s since last action\")\n            last_time = current_time\n            \n            await asyncio.sleep(random.uniform(0.5, 2.0))\n        \n        # Analyze timing distribution\n        avg_time = sum(timings) / len(timings)\n        print(f\"\\nAverage time between actions: {avg_time:.3f}s\")\n        print(f\"Min: {min(timings):.3f}s, Max: {max(timings):.3f}s\")\n        \n        # Good: Variable timing with realistic average (1-2 seconds)\n        # Bad: Constant timing or unrealistically fast (<0.1s)\n\nasyncio.run(test_interaction_timing())\n```\n\n## Learn More\n\nFor more information about element interaction methods:\n\n- **[Element Finding](../element-finding.md)**: Locate elements to interact with\n- **[WebElement Domain](../../deep-dive/webelement-domain.md)**: Deep dive into WebElement capabilities\n- **[File Operations](file-operations.md)**: Upload files and handle downloads\n\nMaster human-like interactions, and your automation will be more reliable, harder to detect, and more closely mirror real user behavior.\n"
  },
  {
    "path": "docs/en/features/automation/iframes.md",
    "content": "# Working with IFrames\n\nModern web pages embed content from other documents using `<iframe>`. In previous versions of Pydoll you had to convert an iframe into a `Tab` using `tab.get_frame()` and keep track of CDP targets manually. **That is no longer necessary.**  \nAn iframe nowadays behaves like any other `WebElement`: you can call `find()`, `query()`, `execute_script()`, `inner_html`, `text`, and all element helpers directly—Pydoll will transparently execute the request inside the correct browsing context.\n\n!!! info \"Simpler mental model\"\n    Treat an iframe exactly like a div: locate it once and use it as the starting point for new element searches. Pydoll handles cross-origin frames, isolated execution contexts, and nested frames behind the scenes.\n\n## Quick Start\n\n### Interact with the first iframe on the page\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def interact_with_iframe():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/page-with-iframe')\n\n        iframe = await tab.find(tag_name='iframe', id='content-frame')\n\n        # These methods execute inside the iframe automatically\n        title = await iframe.find(tag_name='h1')\n        await title.click()\n\n        form = await iframe.find(id='login-form')\n        username = await form.find(name='username')\n        await username.type_text('john_doe')\n\nasyncio.run(interact_with_iframe())\n```\n\n### Nested iframes\n\nNeed to reach a frame inside another frame? Chain your searches:\n\n```python\nouter = await tab.find(id='outer-frame')\ninner = await outer.find(tag_name='iframe')   # Search inside the outer iframe\n\nsubmit_button = await inner.find(id='submit')\nawait submit_button.click()\n```\n\nThe algorithm is always the same:\n\n1. Find the iframe element.\n2. Use that `WebElement` to continue searching.\n3. Repeat for deeper levels if required.\n\nThere is no need to cache frame targets or create additional `Tab` instances.\n\n### Execute JavaScript in an iframe\n\n```python\niframe = await tab.find(tag_name='iframe')\nresult = await iframe.execute_script('return document.title', return_by_value=True)\nprint(result['result']['result']['value'])\n```\n\nPydoll automatically runs the script within the iframe’s isolated execution context (cross-origin and same-origin frames work the same way).\n\n## Why this is better\n\n- **Intuitive:** what you see in the DOM tree is what you code—if you can select the `iframe` element, you can interact with everything inside it.\n- **Cross-origin friendly:** Pydoll spins up an isolated world for you; no more manual target resolution.\n- **Nested by design:** each search is scoped to the element you call it on, so deep hierarchies stay manageable.\n- **No API split:** you do not have to switch between `Tab` and `WebElement` methods—one set of primitives is enough.\n\n!!! tip \"Deprecation notice\"\n    `Tab.get_frame()` now emits a `DeprecationWarning` and will be removed in a future release. Update existing snippets to work directly with iframe elements as shown above.\n\n## Frequently used patterns\n\n### Take a screenshot from inside an iframe\n\n```python\niframe = await tab.find(tag_name='iframe')\nchart = await iframe.find(id='sales-chart')\nawait chart.take_screenshot('chart.png')\n```\n\n\n## Cross-iframe Selectors\n\nInstead of manually finding each iframe and then searching inside it, you can write a **single selector** that crosses iframe boundaries. Pydoll automatically detects `iframe` steps in your XPath or CSS selector, splits them into segments, and walks the iframe chain for you.\n\n### CSS selectors\n\nUse any standard combinator (`>`, space) after an `iframe` compound:\n\n```python\n# Single iframe crossing\nbutton = await tab.query('iframe > .submit-btn')\n\n# With attribute selectors on the iframe\nbutton = await tab.query('iframe[src*=\"checkout\"] > #pay-button')\n\n# Nested iframes\nelement = await tab.query('iframe.outer > iframe.inner > div.content')\n\n# Multiple steps after the iframe\nlink = await tab.query('iframe > nav > a.home-link')\n\n# Iframe inside another element (not at root)\nbutton = await tab.query('div > iframe > button.submit')\ncontent = await tab.query('.wrapper iframe > div.content')\n```\n\n### XPath expressions\n\nUse `/` after an `iframe` step — Pydoll splits at the iframe node:\n\n```python\n# Single iframe crossing\nbutton = await tab.query('//iframe/body/button[@id=\"submit\"]')\n\n# Iframe inside another element (not at root)\ndiv = await tab.query('//div/iframe/div')\nitem = await tab.query('//div[@class=\"wrapper\"]/iframe/body/div')\n\n# With predicates on the iframe\nheading = await tab.query('//iframe[@src*=\"cloudflare\"]//h1')\n\n# Nested iframes\nelement = await tab.query('//iframe[@id=\"outer\"]//iframe[@id=\"inner\"]//div')\n```\n\n### How it works\n\nWhen Pydoll encounters a selector like `iframe[src*=\"checkout\"] > form > button`:\n\n1. **Parses** the selector into segments: `iframe[src*=\"checkout\"]` and `form > button`\n2. **Finds** the iframe element using the first segment\n3. **Searches inside** the iframe using the second segment\n4. For nested iframes, repeats the process at each boundary\n\nThis is equivalent to the manual approach but in a single call:\n\n```python\n# Manual (still works)\niframe = await tab.find(tag_name='iframe', src='*checkout*')\nbutton = await iframe.query('form > button')\n\n# Automatic (same result, one line)\nbutton = await tab.query('iframe[src*=\"checkout\"] > form > button')\n```\n\n### When splitting does NOT happen\n\nSelectors are only split when `iframe` appears as a **tag name**. These selectors pass through unchanged:\n\n- `.iframe > body` — class selector, not a tag\n- `#iframe > body` — ID selector\n- `div.iframe > body` — tag is `div`, not `iframe`\n- `[data-type=\"iframe\"] > body` — attribute selector\n- `iframe` or `//iframe` — no content after iframe (nothing to search inside)\n\n### find_all support\n\nThe last segment respects `find_all=True`, returning all matching elements inside the final iframe:\n\n```python\n# Get all links inside an iframe\nlinks = await tab.query('iframe > a', find_all=True)\n```\n\n## Best practices\n\n- **Use the iframe element as scope:** call `find`, `query`, or other helpers on the iframe itself.\n- **Avoid `tab.find` for inner content:** it only sees the top-level document.\n- **Remember partial results:** if you need the same iframe repeatedly, store the `WebElement` reference; Pydoll keeps the underlying context cached.\n- **Keep existing element workflows:** everything that works for a normal element (scrolling, screenshot, scripts, waiting) works for an iframe element too.\n\n## Further reading\n\n- **[Element Finding](../element-finding.md)** – covers scoped searches and chaining.\n- **[Screenshots & PDFs](screenshots-and-pdfs.md)** – details about capturing visual output.\n- **[Event System](../advanced/event-system.md)** – reactively monitor page activity, including frames.\n\nOnce you adapt to the new model, iframes become just another part of the DOM tree. Focus on building your automation logic—Pydoll takes care of the frame plumbing for you.\n"
  },
  {
    "path": "docs/en/features/automation/keyboard-control.md",
    "content": "# Keyboard Control\n\nThe Keyboard API provides complete control over keyboard input at the page level, enabling you to simulate realistic typing, execute shortcuts, and control complex key sequences. Unlike element-level keyboard methods, the Keyboard API operates globally on the page, giving you the flexibility to interact with any focused element or trigger page-level keyboard actions.\n\n!!! info \"Centralized Keyboard Interface\"\n    All keyboard operations are accessible via `tab.keyboard`, providing a clean, unified API for all keyboard interactions.\n\n!!! warning \"Important CDP Limitation: Browser UI Shortcuts Don't Work\"\n    **Known Issue**: Events injected via Chrome DevTools Protocol are marked as \"untrusted\" and do **not** trigger browser UI actions or create user gestures.\n    \n    **What DOESN'T work:**\n\n    - Browser shortcuts (Ctrl+T, Ctrl+W, Ctrl+N)\n    - DevTools shortcuts (F12, Ctrl+Shift+I)\n    - Browser navigation (Ctrl+Shift+T to reopen tabs)\n    - Any shortcut that modifies browser UI or windows\n    \n    **What WORKS perfectly:**\n\n    - Page-level shortcuts (Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+F)\n    - Text selection and manipulation\n    - Form navigation (Tab, Enter, Arrow keys)\n    - Input field interactions\n    - Custom application shortcuts (in web apps)\n    \n    **Technical reason**: CDP events don't create \"user gestures\" required by browser security. See [chromium issue #615341](https://bugs.chromium.org/p/chromium/issues/detail?id=615341) and [CDP documentation](https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent).\n    \n    For browser-level automation, use CDP browser commands directly (like `tab.close()`, `browser.new_tab()`) instead of keyboard shortcuts.\n\n## Quick Start\n\nThe Keyboard API provides three primary methods:\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import Key\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n\n    # Press and release a key\n    await tab.keyboard.press(Key.ENTER)\n    \n    # Execute a hotkey combination\n    await tab.keyboard.hotkey(Key.CONTROL, Key.S)  # Ctrl+S\n    \n    # Manual control\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.ARROWRIGHT)\n    await tab.keyboard.up(Key.SHIFT)\n```\n\n## Core Methods\n\n### Press: Complete Key Action\n\nThe `press()` method executes a full key press cycle (down → wait → up):\n\n```python\nfrom pydoll.constants import Key\n\n# Basic key press\nawait tab.keyboard.press(Key.ENTER)\nawait tab.keyboard.press(Key.TAB)\nawait tab.keyboard.press(Key.ESCAPE)\n\n# Press with modifiers\nawait tab.keyboard.press(Key.S, modifiers=2)  # Ctrl+S (manual modifier)\n\n# Custom hold duration\nawait tab.keyboard.press(Key.SPACE, interval=0.5)  # Hold for 500ms\n```\n\n**Parameters:**\n\n- `key`: Key to press (from `Key` enum)\n- `modifiers` (optional): Modifier flags (Alt=1, Ctrl=2, Meta=4, Shift=8)\n- `interval` (optional): Duration to hold key in seconds (default: 0.1)\n\n### Down: Press Key Without Releasing\n\nThe `down()` method presses a key without releasing it, useful for holding modifiers or creating key sequences:\n\n```python\nfrom pydoll.constants import Key\n\n# Hold Shift while pressing other keys\nawait tab.keyboard.down(Key.SHIFT)\nawait tab.keyboard.press(Key.ARROWRIGHT)  # Select text\nawait tab.keyboard.press(Key.ARROWRIGHT)  # Continue selecting\nawait tab.keyboard.up(Key.SHIFT)\n\n# Press with modifier flags\nawait tab.keyboard.down(Key.A, modifiers=2)  # Ctrl+A (select all)\n```\n\n**Parameters:**\n\n- `key`: Key to press down\n- `modifiers` (optional): Modifier flags to apply\n\n### Up: Release a Key\n\nThe `up()` method releases a previously pressed key:\n\n```python\nfrom pydoll.constants import Key\n\n# Manual key sequence\nawait tab.keyboard.down(Key.CONTROL)\nawait tab.keyboard.down(Key.SHIFT)\nawait tab.keyboard.press(Key.T)  # Ctrl+Shift+T\nawait tab.keyboard.up(Key.SHIFT)\nawait tab.keyboard.up(Key.CONTROL)\n```\n\n**Parameters:**\n\n- `key`: Key to release\n\n!!! tip \"When to Use Each Method\"\n\n    - **`press()`**: Single key actions (Enter, Tab, letters)\n    - **`hotkey()`**: Keyboard shortcuts (Ctrl+C, Ctrl+Shift+T)\n    - **`down()`/`up()`**: Complex sequences, holding modifiers, custom timing\n\n## Hotkeys: Keyboard Shortcuts Made Easy\n\nThe `hotkey()` method automatically detects modifier keys and executes shortcuts correctly:\n\n### Basic Hotkeys\n\n```python\nfrom pydoll.constants import Key\n\n# Common shortcuts\nawait tab.keyboard.hotkey(Key.CONTROL, Key.C)  # Copy\nawait tab.keyboard.hotkey(Key.CONTROL, Key.V)  # Paste\nawait tab.keyboard.hotkey(Key.CONTROL, Key.X)  # Cut\nawait tab.keyboard.hotkey(Key.CONTROL, Key.Z)  # Undo\nawait tab.keyboard.hotkey(Key.CONTROL, Key.Y)  # Redo\nawait tab.keyboard.hotkey(Key.CONTROL, Key.A)  # Select all\nawait tab.keyboard.hotkey(Key.CONTROL, Key.S)  # Save\n\n```\n\n### Three-Key Combinations\n\n```python\nfrom pydoll.constants import Key\n\n# Text editing shortcuts (these work!)\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.ARROWLEFT)  # Select word left\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.ARROWRIGHT)  # Select word right\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.HOME)  # Select to start of document\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.END)  # Select to end of document\n\n# Application-specific shortcuts (if supported by the web app)\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.Z)  # Redo in many apps\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.S)  # Save As (if app supports it)\n```\n\n### Platform-Specific Shortcuts\n\n```python\nimport sys\nfrom pydoll.constants import Key\n\n# Use Meta (Command) on macOS, Control on Windows/Linux\nmodifier = Key.META if sys.platform == 'darwin' else Key.CONTROL\n\nawait tab.keyboard.hotkey(modifier, Key.C)  # Copy (platform-aware)\nawait tab.keyboard.hotkey(modifier, Key.V)  # Paste (platform-aware)\n```\n\n### How Hotkeys Work\n\nThe `hotkey()` method intelligently handles modifier keys:\n\n1. **Detects modifiers**: Automatically identifies Ctrl, Shift, Alt, Meta\n2. **Calculates flags**: Combines modifiers using bitwise OR (Ctrl=2, Shift=8 → 10)\n3. **Applies correctly**: Presses non-modifier keys with modifier flags applied\n4. **Clean release**: Releases keys in reverse order\n\n```python\nfrom pydoll.constants import Key\n\n# Behind the scenes for hotkey(Key.CONTROL, Key.SHIFT, Key.T):\n# 1. Detect: modifiers=[CONTROL, SHIFT], keys=[T]\n# 2. Calculate: modifier_value = 2 | 8 = 10\n# 3. Execute: press T with modifiers=10\n# 4. Release: release T\n```\n\n!!! tip \"Modifier Values\"\n    When using `modifiers` parameter manually:\n\n    - Alt = 1\n    - Ctrl = 2\n    - Meta/Command = 4\n    - Shift = 8\n    \n    Combine them: Ctrl+Shift = 2 + 8 = 10\n\n## Available Keys\n\nThe `Key` enum provides comprehensive keyboard coverage:\n\n### Letter Keys (A-Z)\n\n```python\nfrom pydoll.constants import Key\n\n# All letters A through Z\nawait tab.keyboard.press(Key.A)\nawait tab.keyboard.press(Key.Z)\n```\n\n### Number Keys\n\n```python\nfrom pydoll.constants import Key\n\n# Top row numbers (0-9)\nawait tab.keyboard.press(Key.DIGIT0)\nawait tab.keyboard.press(Key.DIGIT9)\n\n# Numpad numbers\nawait tab.keyboard.press(Key.NUMPAD0)\nawait tab.keyboard.press(Key.NUMPAD9)\n```\n\n### Function Keys\n\n```python\nfrom pydoll.constants import Key\n\n# F1 through F12\nawait tab.keyboard.press(Key.F1)\nawait tab.keyboard.press(Key.F12)\n```\n\n### Navigation Keys\n\n```python\nfrom pydoll.constants import Key\n\nawait tab.keyboard.press(Key.ARROWUP)\nawait tab.keyboard.press(Key.ARROWDOWN)\nawait tab.keyboard.press(Key.ARROWLEFT)\nawait tab.keyboard.press(Key.ARROWRIGHT)\nawait tab.keyboard.press(Key.HOME)\nawait tab.keyboard.press(Key.END)\nawait tab.keyboard.press(Key.PAGEUP)\nawait tab.keyboard.press(Key.PAGEDOWN)\n```\n\n### Modifier Keys\n\n```python\nfrom pydoll.constants import Key\n\nawait tab.keyboard.press(Key.CONTROL)\nawait tab.keyboard.press(Key.SHIFT)\nawait tab.keyboard.press(Key.ALT)\nawait tab.keyboard.press(Key.META)  # Command on macOS, Windows key on Windows\n```\n\n### Special Keys\n\n```python\nfrom pydoll.constants import Key\n\nawait tab.keyboard.press(Key.ENTER)\nawait tab.keyboard.press(Key.TAB)\nawait tab.keyboard.press(Key.SPACE)\nawait tab.keyboard.press(Key.BACKSPACE)\nawait tab.keyboard.press(Key.DELETE)\nawait tab.keyboard.press(Key.ESCAPE)\nawait tab.keyboard.press(Key.INSERT)\n```\n\n## Practical Examples\n\n### Form Navigation\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.constants import Key\n\nasync def fill_form_with_keyboard():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/form')\n        \n        # Focus first field and type\n        first_field = await tab.find(id='name')\n        await first_field.click()\n        await first_field.insert_text('John Doe')\n        \n        # Navigate to next field with Tab\n        await tab.keyboard.press(Key.TAB)\n        await tab.keyboard.press(Key.TAB)  # Skip a field\n        \n        # Type in current focused field\n        second_field = await tab.find(id='email')\n        await second_field.insert_text('john@example.com')\n        \n        # Submit with Enter\n        await tab.keyboard.press(Key.ENTER)\n```\n\n### Text Selection and Manipulation\n\n```python\nfrom pydoll.constants import Key\n\nasync def select_and_replace_text():\n    # Select all text\n    await tab.keyboard.hotkey(Key.CONTROL, Key.A)\n    \n    # Copy selection\n    await tab.keyboard.hotkey(Key.CONTROL, Key.C)\n    \n    # Move to end\n    await tab.keyboard.press(Key.END)\n    \n    # Select word by word\n    await tab.keyboard.down(Key.CONTROL)\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.ARROWLEFT)\n    await tab.keyboard.press(Key.ARROWLEFT)\n    await tab.keyboard.up(Key.SHIFT)\n    await tab.keyboard.up(Key.CONTROL)\n    \n    # Delete selection\n    await tab.keyboard.press(Key.DELETE)\n```\n\n### Dropdown and Select Navigation\n\n```python\nfrom pydoll.constants import Key\n\nasync def navigate_dropdown():\n    # Open dropdown\n    select = await tab.find(tag_name='select')\n    await select.click()\n    \n    # Navigate options with arrow keys\n    await tab.keyboard.press(Key.ARROWDOWN)\n    await tab.keyboard.press(Key.ARROWDOWN)\n    \n    # Select with Enter\n    await tab.keyboard.press(Key.ENTER)\n    \n    # Or cancel with Escape\n    await tab.keyboard.press(Key.ESCAPE)\n```\n\n### Complex Key Sequences\n\n```python\nfrom pydoll.constants import Key\nimport asyncio\n\nasync def complex_editing():\n    # Select line\n    await tab.keyboard.press(Key.HOME)  # Go to start\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.END)  # Select to end\n    await tab.keyboard.up(Key.SHIFT)\n    \n    # Cut\n    await tab.keyboard.hotkey(Key.CONTROL, Key.X)\n    \n    # Move down and paste\n    await tab.keyboard.press(Key.ARROWDOWN)\n    await tab.keyboard.hotkey(Key.CONTROL, Key.V)\n    \n    # Undo if needed\n    await tab.keyboard.hotkey(Key.CONTROL, Key.Z)\n```\n\n## Best Practices\n\n### 1. Add Delays for Reliability\n\n```python\nfrom pydoll.constants import Key\nimport asyncio\n\n# Good: Wait for UI to update\nawait tab.keyboard.hotkey(Key.CONTROL, Key.F)  # Open find\nawait asyncio.sleep(0.2)  # Wait for dialog\nawait tab.keyboard.press(Key.ESCAPE)  # Close it\n\n# Bad: No delay, it might not work\nawait tab.keyboard.hotkey(Key.CONTROL, Key.F)\nawait tab.keyboard.press(Key.ESCAPE)  # Might be too fast\n```\n\n### 2. Focus Elements Before Typing\n\n```python\nfrom pydoll.constants import Key\n\n# Good: Ensure element is focused\ninput_field = await tab.find(id='search')\nawait input_field.click()  # Focus it\nawait input_field.insert_text('query')\n\n# Bad: Keyboard input goes to wrong element\nawait tab.keyboard.press(Key.A)  # Where does this go?\n```\n\n### 3. Use Platform-Aware Shortcuts\n\n```python\nimport sys\nfrom pydoll.constants import Key\n\n# Good: Platform-aware\ncmd_key = Key.META if sys.platform == 'darwin' else Key.CONTROL\nawait tab.keyboard.hotkey(cmd_key, Key.C)\n\n# Bad: Hardcoded (won't work on macOS)\nawait tab.keyboard.hotkey(Key.CONTROL, Key.C)\n```\n\n### 4. Clean Up Long Sequences\n\n```python\nfrom pydoll.constants import Key\n\n# Good: Ensure modifiers are released\ntry:\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.ARROWRIGHT)\n    # ... more operations\nfinally:\n    await tab.keyboard.up(Key.SHIFT)  # Always release\n\n# Bad: Modifier stays pressed on error\nawait tab.keyboard.down(Key.SHIFT)\nawait tab.keyboard.press(Key.ARROWRIGHT)\n# Error here leaves Shift pressed!\n```\n\n## Key Reference Tables\n\n### Common Page-Level Shortcuts (These Work!)\n\n| Action | Windows/Linux | macOS | Notes |\n|--------|--------------|-------|-------|\n| Copy | Ctrl+C | Cmd+C | Works |\n| Paste | Ctrl+V | Cmd+V | Works |\n| Cut | Ctrl+X | Cmd+X | Works |\n| Undo | Ctrl+Z | Cmd+Z | Works |\n| Redo | Ctrl+Y | Cmd+Y | Works |\n| Select All | Ctrl+A | Cmd+A | Works |\n| Find | Ctrl+F | Cmd+F | Only if web app implements it |\n| Save | Ctrl+S | Cmd+S | Only if web app implements it |\n| Refresh | F5 or Ctrl+R | Cmd+R | Use `await tab.refresh()` instead |\n\n### Browser Shortcuts (These DON'T Work via CDP)\n\n| Action | Shortcut | Use Instead |\n|--------|----------|-------------|\n| New Tab | Ctrl+T | `await browser.new_tab()` |\n| Close Tab | Ctrl+W | `await tab.close()` |\n| Reopen Tab | Ctrl+Shift+T | Track tabs manually |\n| DevTools | F12, Ctrl+Shift+I | Already available via CDP! |\n| Address Bar | Ctrl+L | `await tab.go_to(url)` |\n\n### All Available Keys\n\n| Category | Keys |\n|----------|------|\n| **Letters** | `Key.A` through `Key.Z` (26 keys) |\n| **Numbers** | `Key.DIGIT0` through `Key.DIGIT9` (10 keys) |\n| **Numpad** | `Key.NUMPAD0` through `Key.NUMPAD9`, `NUMPADMULTIPLY`, `NUMPADADD`, `NUMPADSUBTRACT`, `NUMPADDECIMAL`, `NUMPADDIVIDE` |\n| **Function** | `Key.F1` through `Key.F12` (12 keys) |\n| **Navigation** | `ARROWUP`, `ARROWDOWN`, `ARROWLEFT`, `ARROWRIGHT`, `HOME`, `END`, `PAGEUP`, `PAGEDOWN` |\n| **Modifiers** | `CONTROL`, `SHIFT`, `ALT`, `META` |\n| **Special** | `ENTER`, `TAB`, `SPACE`, `BACKSPACE`, `DELETE`, `ESCAPE`, `INSERT` |\n| **Locks** | `CAPSLOCK`, `NUMLOCK`, `SCROLLLOCK` |\n| **Symbols** | `SEMICOLON`, `EQUALSIGN`, `COMMA`, `MINUS`, `PERIOD`, `SLASH`, `GRAVEACCENT`, `BRACKETLEFT`, `BACKSLASH`, `BRACKETRIGHT`, `QUOTE` |\n\n### Modifier Flag Values\n\n| Modifier | Value | Binary | Usage |\n|----------|-------|--------|-------|\n| Alt | 1 | 0001 | `modifiers=1` |\n| Ctrl | 2 | 0010 | `modifiers=2` |\n| Meta | 4 | 0100 | `modifiers=4` |\n| Shift | 8 | 1000 | `modifiers=8` |\n| Ctrl+Shift | 10 | 1010 | `modifiers=10` |\n| Ctrl+Alt | 3 | 0011 | `modifiers=3` |\n| Ctrl+Shift+Alt | 11 | 1011 | `modifiers=11` |\n\n## Migration from WebElement Methods\n\nPrevious keyboard methods on `WebElement` are deprecated. Here's how to migrate:\n\n### Old vs New\n\n```python\nfrom pydoll.constants import Key\n\n# Old (deprecated)\nelement = await tab.find(id='input')\nawait element.key_down(Key.A, modifiers=2)\nawait element.key_up(Key.A)\nawait element.press_keyboard_key(Key.ENTER)\n\n# New (recommended)\nawait tab.keyboard.down(Key.A, modifiers=2)\nawait tab.keyboard.up(Key.A)\nawait tab.keyboard.press(Key.ENTER)\n```\n\n!!! warning \"Deprecation Notice\"\n    The following `WebElement` methods are deprecated:\n\n    - `key_down()` → Use `tab.keyboard.down()`\n    - `key_up()` → Use `tab.keyboard.up()`\n    - `press_keyboard_key()` → Use `tab.keyboard.press()`\n    \n    These methods still work for backward compatibility but will show deprecation warnings.\n\n### Why Migrate?\n\n- **Centralized**: All keyboard operations in one place\n- **Cleaner API**: Consistent interface for all keyboard actions\n- **More powerful**: Hotkey support, smart modifier detection\n- **Better typed**: Full IDE autocomplete support\n\n## Learn More\n\nFor additional automation capabilities:\n\n- **[Human Interactions](human-interactions.md)**: Realistic clicking, scrolling, and mouse movement\n- **[Form Handling](form-handling.md)**: Complete form automation workflows\n- **[File Operations](file-operations.md)**: File upload automation\n\nThe Keyboard API eliminates the complexity of keyboard automation, providing clean, reliable methods for everything from simple key presses to complex shortcuts and sequences.\n"
  },
  {
    "path": "docs/en/features/automation/mouse-control.md",
    "content": "# Mouse Control\n\nThe Mouse API provides complete control over mouse input at the page level, enabling you to simulate realistic cursor movement, clicks, double-clicks, and drag operations. When `humanize=True` is passed, mouse operations use humanized simulation: paths follow natural Bezier curves with Fitts's Law timing, minimum-jerk velocity profiles, physiological tremor, and overshoot correction, making automation virtually indistinguishable from human behavior.\n\n!!! info \"Centralized Mouse Interface\"\n    All mouse operations are accessible via `tab.mouse`, providing a clean, unified API for all mouse interactions.\n\n## Quick Start\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.input.types import MouseButton\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n\n    # Move cursor to position\n    await tab.mouse.move(500, 300)\n\n    # Click at position\n    await tab.mouse.click(500, 300)\n\n    # Right-click\n    await tab.mouse.click(500, 300, button=MouseButton.RIGHT)\n\n    # Double-click\n    await tab.mouse.double_click(500, 300)\n\n    # Drag from one position to another\n    await tab.mouse.drag(100, 200, 500, 400)\n```\n\n## Core Methods\n\n### move: Move Cursor\n\nMove the mouse cursor to a specific position on the page:\n\n```python\n# Default move (single CDP event, no simulation)\nawait tab.mouse.move(500, 300)\n\n# Humanized move (curved path with natural timing)\nawait tab.mouse.move(500, 300, humanize=True)\n```\n\n**Parameters:**\n\n- `x`: Target X coordinate (CSS pixels)\n- `y`: Target Y coordinate (CSS pixels)\n- `humanize` (keyword-only): Simulate human-like curved movement (default: `False`)\n\n### click: Click at Position\n\nMove to position and perform a mouse click:\n\n```python\nfrom pydoll.protocol.input.types import MouseButton\n\n# Left click (default, instant)\nawait tab.mouse.click(500, 300)\n\n# Right click\nawait tab.mouse.click(500, 300, button=MouseButton.RIGHT)\n\n# Double click via click_count\nawait tab.mouse.click(500, 300, click_count=2)\n\n# Humanized click with natural movement\nawait tab.mouse.click(500, 300, humanize=True)\n```\n\n**Parameters:**\n\n- `x`: Target X coordinate\n- `y`: Target Y coordinate\n- `button` (keyword-only): Mouse button, one of `LEFT`, `RIGHT`, `MIDDLE` (default: `LEFT`)\n- `click_count` (keyword-only): Number of clicks (default: `1`)\n- `humanize` (keyword-only): Simulate human-like behavior (default: `False`)\n\n### double_click: Double-Click at Position\n\nConvenience method equivalent to `click(x, y, click_count=2)`:\n\n```python\nawait tab.mouse.double_click(500, 300)\nawait tab.mouse.double_click(500, 300, humanize=False)\n```\n\n### down / up: Low-Level Button Control\n\nPress or release mouse buttons independently:\n\n```python\n# Press left button at current position\nawait tab.mouse.down()\n\n# Release left button\nawait tab.mouse.up()\n\n# Right button\nawait tab.mouse.down(button=MouseButton.RIGHT)\nawait tab.mouse.up(button=MouseButton.RIGHT)\n```\n\nThese are primitives that operate at the current cursor position and have no `humanize` parameter.\n\n### drag: Drag and Drop\n\nMove from start to end while holding the mouse button:\n\n```python\n# Default drag (instant)\nawait tab.mouse.drag(100, 200, 500, 400)\n\n# Humanized drag with natural movement\nawait tab.mouse.drag(100, 200, 500, 400, humanize=True)\n```\n\n**Parameters:**\n\n- `start_x`, `start_y`: Start coordinates\n- `end_x`, `end_y`: End coordinates\n- `humanize` (keyword-only): Simulate human-like drag (default: `False`)\n\n## Enabling Humanization\n\nAll mouse methods default to `humanize=False`. To enable humanized simulation with natural Bezier curve paths and realistic timing, pass `humanize=True`:\n\n```python\n# Humanized move, natural curved path with Fitts's Law timing\nawait tab.mouse.move(500, 300, humanize=True)\n\n# Humanized click: curved movement + pre-click pause + press + release\nawait tab.mouse.click(500, 300, humanize=True)\n\n# Humanized drag, natural curves and pauses\nawait tab.mouse.drag(100, 200, 500, 400, humanize=True)\n```\n\nThis is recommended when detection evasion is important, for example when interacting with sites that employ bot detection.\n\n## Humanized Mode\n\nWhen `humanize=True` is passed, the mouse module applies multiple layers of realism:\n\n### Bezier Curve Paths\n\nMouse follows a natural curved trajectory instead of a straight line. Control points are randomly offset perpendicular to the start→end line, with asymmetric placement (more curvature early in the movement, like a real ballistic reach).\n\n### Fitts's Law Timing\n\nMovement duration follows Fitts's Law: `MT = a + b × log₂(D/W + 1)`. Longer distances take proportionally more time, matching human motor control behavior.\n\n### Minimum-Jerk Velocity Profile\n\nThe cursor follows a bell-shaped speed profile, starting slow, accelerating to peak velocity in the middle, then decelerating at the end. This matches the smoothest possible human movement trajectory.\n\n### Physiological Tremor\n\nSmall Gaussian noise (σ ≈ 1px) is added to each frame, simulating hand tremor. The tremor amplitude scales inversely with velocity, with more tremor when the cursor is slow or hovering and less during fast ballistic movements.\n\n### Overshoot and Correction\n\nFor fast, long-distance movements (~70% probability), the cursor overshoots the target by 3–12% of the distance, then makes a small corrective sub-movement back to the target. This matches real human motor control data.\n\n### Pre-Click Pause\n\nHumanized clicks include a pre-click pause (50–200ms) that simulates the natural settle time before pressing the button.\n\n## Automatic Humanized Element Clicks\n\nWhen you use `element.click(humanize=True)`, the Mouse API is used to produce a realistic Bezier curve movement from the current cursor position to the element center before clicking, making element clicks indistinguishable from human behavior.\n\n```python\n# Default click: raw CDP press/release\nbutton = await tab.find(id='submit')\nawait button.click()\n\n# With offset from center\nawait button.click(x_offset=10, y_offset=5)\n\n# Humanized click: Bezier curve movement + click\nawait button.click(humanize=True)\n```\n\nPosition tracking is maintained across element clicks. Clicking element A, then element B, produces a natural curved path from A's position to B.\n\n## Custom Timing Configuration\n\nAll humanization parameters are configurable via `MouseTimingConfig`:\n\n```python\nfrom pydoll.interactions.mouse import MouseTimingConfig\n\nconfig = MouseTimingConfig(\n    fitts_a=0.070,              # Fitts's Law intercept (seconds)\n    fitts_b=0.150,              # Fitts's Law slope (seconds/bit)\n    frame_interval=0.012,       # Base interval between mouseMoved events\n    curvature_min=0.10,         # Min path curvature as fraction of distance\n    curvature_max=0.30,         # Max path curvature\n    tremor_amplitude=1.0,       # Tremor sigma in pixels\n    overshoot_probability=0.70, # Chance of overshoot on fast moves\n    min_duration=0.08,          # Minimum movement duration\n    max_duration=2.5,           # Maximum movement duration\n)\n\n# Apply to the tab's mouse instance\ntab.mouse.timing = config\n```\n\nSee the `MouseTimingConfig` dataclass for all available parameters.\n\n## Position Tracking\n\nThe Mouse API tracks the cursor position across operations:\n\n```python\n# Initial position is (0, 0)\nawait tab.mouse.move(100, 200)\n# Position is now (100, 200)\n\nawait tab.mouse.click(300, 400)\n# Position is now (300, 400)\n\n# Low-level methods use the tracked position\nawait tab.mouse.down()   # Presses at (300, 400)\nawait tab.mouse.up()     # Releases at (300, 400)\n```\n\n!!! note \"Position State\"\n    The mouse position is tracked internally. `WebElement.click()` automatically uses `tab.mouse` when available, so position tracking is maintained across element clicks.\n\n## Debug Mode\n\nEnable debug mode to visualize mouse movement on the page. When active, colored dots are drawn on a transparent overlay canvas:\n\n- **Blue dots**: cursor path during movement\n- **Red dots**: click positions\n\n```python\n# Enable at runtime via property\ntab.mouse.debug = True\n\n# Now all movements draw colored dots\nawait tab.mouse.click(500, 300)\n\n# Disable when done\ntab.mouse.debug = False\n```\n\nThis is useful for tuning timing parameters and verifying that paths look natural.\n\n## Practical Examples\n\n### Click a Button with Realistic Movement\n\n```python\nasync def click_button_naturally(tab):\n    # element.click() automatically uses tab.mouse for humanized movement\n    button = await tab.find(id='submit')\n    await button.click()\n```\n\n### Drag a Slider\n\n```python\nasync def drag_slider(tab):\n    slider = await tab.find(css_selector='.slider-handle')\n    bounds = await slider.get_bounds_using_js()\n\n    start_x = bounds['x'] + bounds['width'] / 2\n    start_y = bounds['y'] + bounds['height'] / 2\n    end_x = start_x + 200  # Drag 200px to the right\n\n    await tab.mouse.drag(start_x, start_y, end_x, start_y)\n```\n\n### Hover Over Elements\n\n```python\nasync def hover_menu(tab):\n    menu = await tab.find(css_selector='.dropdown-trigger')\n    bounds = await menu.get_bounds_using_js()\n\n    await tab.mouse.move(\n        bounds['x'] + bounds['width'] / 2,\n        bounds['y'] + bounds['height'] / 2,\n    )\n    # Menu should now be visible via CSS :hover\n```\n\n## Learn More\n\n- **[Human Interactions](human-interactions.md)**: Overview of all humanized interactions\n- **[Keyboard Control](keyboard-control.md)**: Realistic keyboard simulation\n"
  },
  {
    "path": "docs/en/features/automation/screenshots-and-pdfs.md",
    "content": "# Screenshots and PDFs\n\nPydoll provides powerful screenshot and PDF generation capabilities through direct Chrome DevTools Protocol commands. Capture full pages, specific elements, or generate PDFs with fine-grained control.\n\n## Screenshots\n\n### Basic Page Screenshot\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def take_page_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Save screenshot to file\n        await tab.take_screenshot('page.png', quality=100)\n\nasyncio.run(take_page_screenshot())\n```\n\n### Supported Formats\n\nPydoll supports three image formats based on file extension:\n\n```python\n# PNG format (lossless, larger file size)\nawait tab.take_screenshot('screenshot.png', quality=100)\n\n# JPEG format (lossy, smaller file size)\nawait tab.take_screenshot('screenshot.jpeg', quality=85)\n\n# WebP format (modern, efficient)\nawait tab.take_screenshot('screenshot.webp', quality=90)\n```\n\n!!! info \"Format Detection\"\n    The image format is automatically determined by the file extension. Using an unsupported extension raises `InvalidFileExtension`.\n    \n    Both `.jpg` and `.jpeg` are supported for JPEG format (`.jpg` is automatically normalized to `.jpeg` internally to match CDP requirements).\n\n### Screenshot Parameters\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `path` | `Optional[str]` | `None` | File path to save screenshot. Required if `as_base64=False`. |\n| `quality` | `int` | `100` | Image quality (0-100). Higher values mean better quality and larger files. |\n| `beyond_viewport` | `bool` | `False` | Capture entire scrollable page, not just visible area. |\n| `as_base64` | `bool` | `False` | Return base64-encoded string instead of saving to file. |\n\n### Full Page Screenshot\n\nCapture content beyond the visible viewport:\n\n```python\nasync def full_page_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/long-page')\n        \n        # Capture entire page including content below the fold\n        await tab.take_screenshot(\n            'full-page.png',\n            beyond_viewport=True,\n            quality=90\n        )\n```\n\n!!! warning \"Performance Note\"\n    Using `beyond_viewport=True` on very long pages can consume significant memory and take longer to process.\n\n### Base64 Screenshot\n\nGet screenshot as base64 string for embedding or sending via API:\n\n```python\nasync def base64_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Get screenshot as base64 string\n        screenshot_base64 = await tab.take_screenshot(\n            as_base64=True\n        )\n        \n        # Use in HTML img tag\n        html = f'<img src=\"data:image/png;base64,{screenshot_base64}\" />'\n        \n        # Or send via API\n        import aiohttp\n        async with aiohttp.ClientSession() as session:\n            await session.post(\n                'https://api.example.com/upload',\n                json={'image': screenshot_base64}\n            )\n```\n\n### Element Screenshot\n\nCapture specific elements instead of the entire page:\n\n```python\nasync def element_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Screenshot a specific element (PNG)\n        header = await tab.find(tag_name='header')\n        await header.take_screenshot('header.png', quality=100)\n        \n        # Screenshot a form (JPEG)\n        form = await tab.find(id='login-form')\n        await form.take_screenshot('login-form.jpeg', quality=85)\n        \n        # Screenshot a chart or graph (WebP)\n        chart = await tab.find(class_name='data-visualization')\n        await chart.take_screenshot('chart.webp', quality=90)\n```\n\n!!! info \"Format Detection\"\n    The image format is automatically detected from the file extension (`.png`, `.jpeg`/`.jpg`, or `.webp`). Using an unsupported extension raises `InvalidFileExtension`.\n\n!!! tip \"Automatic Scrolling\"\n    When capturing element screenshots, Pydoll automatically scrolls the element into view before taking the screenshot.\n\n### Element vs Page Screenshots\n\n| Feature | `tab.take_screenshot()` | `element.take_screenshot()` |\n|---------|------------------------|----------------------------|\n| **Scope** | Entire viewport or page | Specific element only |\n| **Format Support** | PNG, JPEG, WebP | PNG, JPEG, WebP |\n| **Beyond Viewport** | ✅ Supported | ❌ Not applicable |\n| **Base64 Output** | ✅ Supported | ✅ Supported |\n| **Auto-Scroll** | ❌ Not applicable | ✅ Yes |\n| **Use Case** | Full page captures | Component isolation, testing |\n\n\n## PDF Generation\n\n### Basic PDF Export\n\nConvert pages to PDF with print-quality output:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def generate_pdf():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/document')\n        \n        # Generate PDF with Path\n        await tab.print_to_pdf(Path('document.pdf'))\n        \n        # Or with string\n        await tab.print_to_pdf('document.pdf')\n\nasyncio.run(generate_pdf())\n```\n\n### PDF Parameters\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `path` | `Optional[str \\| Path]` | `None` | File path to save PDF. Required if `as_base64=False`. |\n| `landscape` | `bool` | `False` | Use landscape orientation (vs portrait). |\n| `display_header_footer` | `bool` | `False` | Include browser-generated header/footer with title, URL, page numbers. |\n| `print_background` | `bool` | `True` | Include background graphics and colors. |\n| `scale` | `float` | `1.0` | Page scale factor (0.1-2.0). Useful for zoom/shrink effects. |\n| `as_base64` | `bool` | `False` | Return base64-encoded string instead of saving to file. |\n\n!!! tip \"Path vs String\"\n    While `Path` objects from `pathlib` are recommended as best practice for better path handling and cross-platform compatibility, you can also use plain strings if preferred.\n\n### Advanced PDF Options\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def advanced_pdf():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/report')\n        \n        # Landscape PDF with headers/footers\n        await tab.print_to_pdf(\n            Path('report-landscape.pdf'),\n            landscape=True,\n            display_header_footer=True,\n            print_background=True,\n            scale=0.9\n        )\n        \n        # Portrait PDF without backgrounds (ink-friendly)\n        await tab.print_to_pdf(\n            Path('report-ink-friendly.pdf'),\n            landscape=False,\n            print_background=False,\n            scale=1.0\n        )\n\nasyncio.run(advanced_pdf())\n```\n\n### PDF Scale Factor\n\nControl the zoom level of PDF output:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def scaled_pdfs():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/content')\n        \n        # Shrink content to fit more on each page\n        await tab.print_to_pdf(Path('compact.pdf'), scale=0.7)\n        \n        # Normal scale\n        await tab.print_to_pdf(Path('normal.pdf'), scale=1.0)\n        \n        # Enlarge content (fewer pages)\n        await tab.print_to_pdf(Path('large.pdf'), scale=1.5)\n\nasyncio.run(scaled_pdfs())\n```\n\n!!! warning \"Scale Limits\"\n    The `scale` parameter accepts values between `0.1` and `2.0`. Values outside this range may produce unexpected results.\n\n### Base64 PDF\n\nGenerate PDF as base64 string for API transmission:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def base64_pdf():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/invoice')\n        \n        # Get PDF as base64 (no path needed)\n        pdf_base64 = await tab.print_to_pdf(as_base64=True)\n        \n        # Send via API\n        import aiohttp\n        async with aiohttp.ClientSession() as session:\n            await session.post(\n                'https://api.example.com/invoices',\n                json={'pdf': pdf_base64}\n            )\n\nasyncio.run(base64_pdf())\n```\n\n\n!!! info \"CDP Reference\"\n    For complete CDP documentation on these commands, see:\n    \n    - [Page.captureScreenshot](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot)\n    - [Page.printToPDF](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF)\n\n### Error Handling\n\n```python\nfrom pydoll.exceptions import (\n    InvalidFileExtension,\n    MissingScreenshotPath,\n    TopLevelTargetRequired\n)\n\nasync def safe_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        try:\n            # Missing path and as_base64=False\n            await tab.take_screenshot()\n        except MissingScreenshotPath:\n            print(\"Error: Must provide path or set as_base64=True\")\n        \n        try:\n            # Invalid extension\n            await tab.take_screenshot('image.bmp')\n        except InvalidFileExtension as e:\n            print(f\"Error: {e}\")\n        \n        # IFrame screenshot limitation\n        iframe_element = await tab.find(tag_name='iframe')\n\n        # This still won't work: top-level screenshots ignore iframe content\n        # await tab.take_screenshot('frame.png')\n\n        # Screenshot an element inside the iframe WebElement\n        content = await iframe_element.find(id='content')\n        await content.take_screenshot('iframe-content.png')\n```\n\n## Page Bundle Export\n\nSave an entire page with all its assets (CSS, JS, images, fonts) as a `.zip` archive for offline viewing.\n\n### Basic Usage\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def save_page():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n\n        # Save page with assets as separate files\n        await tab.save_bundle('page.zip')\n\nasyncio.run(save_page())\n```\n\nThe resulting zip contains an `index.html` with all URLs rewritten to reference local files under an `assets/` directory.\n\n### Inline Mode\n\nEmbed everything directly into a single `index.html` using data URIs, `<style>`, and `<script>` tags:\n\n```python\n# Single self-contained HTML file inside the zip\nawait tab.save_bundle('page-inline.zip', inline_assets=True)\n```\n\n### Parameters\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `path` | `str \\| Path` | *(required)* | Destination path. Must end with `.zip`. |\n| `inline_assets` | `bool` | `False` | Embed all assets inline instead of saving as separate files. |\n\n!!! info \"What Gets Bundled\"\n    The bundle includes resources of type: Document, Stylesheet, Script, Image, Font, and Media. Resources that failed to load, were canceled, or use `data:` URIs are automatically skipped.\n\n## Learn More\n\nFor additional context on how screenshots and PDFs integrate with Pydoll's architecture:\n\n- **[Deep Dive: CDP](../../deep-dive/cdp.md)**: Understanding Chrome DevTools Protocol commands\n- **[API Reference: Tab](../../api/browser/tab.md#take_screenshot)**: Complete method signatures and parameters\n- **[API Reference: WebElement](../../api/elements/web-element.md#take_screenshot)**: Element-specific screenshot capabilities\n\nScreenshots and PDFs are essential tools for automation, testing, and documentation. Pydoll's direct CDP integration provides professional-grade output with fine-grained control.\n\n"
  },
  {
    "path": "docs/en/features/browser-management/contexts.md",
    "content": "# Browser Contexts\n\nBrowser Contexts are Pydoll's solution for creating completely isolated browsing environments within a single browser process. Think of them as separate \"incognito windows\" but with full programmatic control, each context maintains its own cookies, storage, cache, and authentication state.\n\n## Quick Start\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def basic_context_example():\n    async with Chrome() as browser:\n        # Start browser with initial tab in default context\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        \n        # Create isolated context\n        context_id = await browser.create_browser_context()\n        \n        # New tab in isolated context\n        isolated_tab = await browser.new_tab('https://example.com', browser_context_id=context_id)\n        \n        # Both tabs are completely isolated - different cookies, storage, etc.\n        await initial_tab.execute_script(\"localStorage.setItem('user', 'Alice')\")\n        await isolated_tab.execute_script(\"localStorage.setItem('user', 'Bob')\")\n        \n        # Verify isolation\n        user_default = await initial_tab.execute_script(\"return localStorage.getItem('user')\")\n        user_isolated = await isolated_tab.execute_script(\"return localStorage.getItem('user')\")\n        \n        print(f\"Default context: {user_default}\")  # Alice\n        print(f\"Isolated context: {user_isolated}\")  # Bob\n\nasyncio.run(basic_context_example())\n```\n\n## What Are Browser Contexts?\n\nA browser context is an isolated browsing environment within a single browser process. Each context maintains completely separate:\n\n| Component | Description | Isolation Level |\n|-----------|-------------|-----------------|\n| **Cookies** | HTTP cookies and session data | ✓ Fully isolated |\n| **Local Storage** | `localStorage` and `sessionStorage` | ✓ Fully isolated |\n| **IndexedDB** | Client-side database | ✓ Fully isolated |\n| **Cache** | HTTP cache and resources | ✓ Fully isolated |\n| **Permissions** | Geolocation, notifications, camera, etc. | ✓ Fully isolated |\n| **Authentication** | Login sessions and auth tokens | ✓ Fully isolated |\n| **Service Workers** | Background scripts | ✓ Fully isolated |\n\n```mermaid\ngraph LR\n    Browser[Browser Process] --> Default[Default Context]\n    Browser --> Context1[Context 1]\n    Browser --> Context2[Context 2]\n    \n    Default --> T1[Tab A]\n    Default --> T2[Tab B]\n    Context1 --> T3[Tab C]\n    Context2 --> T4[Tab D]\n```\n\n## Why Use Browser Contexts?\n\n### 1. Multi-Account Testing\n\nTest different user accounts simultaneously without interference:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def perform_login(tab, email, password):\n    \"\"\"\n    Helper function to navigate to the login page\n    and submit account credentials.\n    \"\"\"\n    print(f\"Attempting login with: {email}...\")\n    await tab.go_to('https://app.example.com/login')\n\n    # Find elements\n    email_field = await tab.find(id='email')\n    password_field = await tab.find(id='password')\n    login_btn = await tab.find(id='login-btn')\n\n    # Fill credentials and click\n    await email_field.type_text(email)\n    await password_field.type_text(password)\n    await login_btn.click()\n\n    # Wait for login to process\n    await asyncio.sleep(2)\n    print(f\"Login successful for {email}.\")\n\n\nasync def multi_account_test():\n    \"\"\"\n    Main script to test simultaneous logins\n    using isolated browser contexts.\n    \"\"\"\n    accounts = [\n        {\"email\": \"user1@example.com\", \"password\": \"pass1\"},\n        {\"email\": \"user2@example.com\", \"password\": \"pass2\"},\n        {\"email\": \"admin@example.com\", \"password\": \"admin_pass\"}\n    ]\n\n    # This list will store information for each active user session.\n    user_sessions = []\n\n    async with Chrome() as browser:\n        first_account = accounts[0]\n        initial_tab = await browser.start()\n        await perform_login(initial_tab, first_account['email'], first_account['password'])\n        user_sessions.append({\n            \"email\": first_account['email'],\n            \"tab\": initial_tab,\n            \"context_id\": None  # 'None' represents the default browser context\n        })\n\n        # Iterate over the rest of the accounts\n        for account in accounts[1:]:\n            context_id = await browser.create_browser_context()\n            new_tab = await browser.new_tab(browser_context_id=context_id)\n            await perform_login(new_tab, account['email'], account['password'])\n\n            # Add this new session info to the list\n            user_sessions.append({\n                \"email\": account['email'],\n                \"tab\": new_tab,\n                \"context_id\": context_id\n            })\n\n        print(\"\\n--- Verifying all active sessions ---\")\n        for session in user_sessions:\n            tab = session[\"tab\"]\n            email = session[\"email\"]\n            await tab.go_to('https://app.example.com/dashboard')\n            username = await tab.find(class_name='username')\n            username_text = await username.text\n            print(f\"[Account: {email}] -> Logged in as: {username_text}\")\n            await asyncio.sleep(0.5)\n\n        print(\"\\n--- Cleaning up contexts ---\")\n        for session in user_sessions:\n            # Only close contexts we created (non-None)\n            if session[\"context_id\"] is not None:\n                print(f\"Closing context for: {session['email']}\")\n                await session[\"tab\"].close()\n                await browser.delete_browser_context(session[\"context_id\"])\n        \n        # The default context (None) is closed automatically\n        # by the 'async with Chrome() as browser'\n\nasyncio.run(multi_account_test())\n```\n\n### 2. Geo-Location Testing with Context-Specific Proxies\n\nEach context can have its own proxy configuration:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def geo_location_testing():\n    async with Chrome() as browser:\n        # Start browser and use initial tab for first test (default context, no proxy)\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://api.ipify.org')\n        await asyncio.sleep(2)\n        default_ip = await initial_tab.execute_script('return document.body.textContent')\n        print(f\"Default IP (no proxy): {default_ip}\")\n        \n        # US context with US proxy\n        us_context = await browser.create_browser_context(\n            proxy_server='http://us-proxy.example.com:8080'\n        )\n        us_tab = await browser.new_tab('https://api.ipify.org', browser_context_id=us_context)\n        await asyncio.sleep(2)\n        us_ip = await us_tab.execute_script('return document.body.textContent')\n        print(f\"US IP: {us_ip}\")\n        \n        # EU context with EU proxy\n        eu_context = await browser.create_browser_context(\n            proxy_server='http://eu-proxy.example.com:8080'\n        )\n        eu_tab = await browser.new_tab('https://api.ipify.org', browser_context_id=eu_context)\n        await asyncio.sleep(2)\n        eu_ip = await eu_tab.execute_script('return document.body.textContent')\n        print(f\"EU IP: {eu_ip}\")\n        \n        # Cleanup (skip initial tab)\n        await us_tab.close()\n        await eu_tab.close()\n        await browser.delete_browser_context(us_context)\n        await browser.delete_browser_context(eu_context)\n\nasyncio.run(geo_location_testing())\n```\n\n!!! tip \"Proxy Authentication\"\n    Pydoll handles proxy authentication automatically for contexts. Just include credentials in the URL:\n    ```python\n    context_id = await browser.create_browser_context(\n        proxy_server='http://username:password@proxy.example.com:8080'\n    )\n    ```\n    The credentials are sanitized from CDP commands and only used when the browser challenges for auth.\n\n### 3. A/B Testing\n\nCompare different user experiences in parallel:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def ab_testing():\n    async with Chrome() as browser:\n        # Start browser with initial tab (Control group in default context)\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        await initial_tab.execute_script(\"localStorage.setItem('experiment', 'control')\")\n        \n        # Treatment group in isolated context\n        context_b = await browser.create_browser_context()\n        tab_b = await browser.new_tab('https://example.com', browser_context_id=context_b)\n        await tab_b.execute_script(\"localStorage.setItem('experiment', 'treatment')\")\n        \n        # Navigate both to the feature page\n        await initial_tab.go_to('https://example.com/feature')\n        await tab_b.go_to('https://example.com/feature')\n        \n        # Compare results\n        result_a = await initial_tab.find(class_name='experiment-result')\n        result_b = await tab_b.find(class_name='experiment-result')\n        \n        print(f\"Control group result: {await result_a.text}\")\n        print(f\"Treatment group result: {await result_b.text}\")\n        \n        # Cleanup\n        await tab_b.close()\n        await browser.delete_browser_context(context_b)\n\nasyncio.run(ab_testing())\n```\n\n### 4. Parallel Web Scraping\n\nScrape multiple sites with different configurations:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def parallel_scraping():\n    websites = [\n        {'url': 'https://news.ycombinator.com', 'selector': '.storylink'},\n        {'url': 'https://reddit.com/r/python', 'selector': '.title'},\n        {'url': 'https://github.com/trending', 'selector': '.h3'},\n    ]\n    \n    async with Chrome() as browser:\n        # Start browser and get initial tab\n        initial_tab = await browser.start()\n        \n        # Create contexts for remaining sites (first uses default context)\n        contexts = [None] + [await browser.create_browser_context() for _ in websites[1:]]\n        \n        # Create tabs (reusing initial tab for first site)\n        tabs = [initial_tab] + [\n            await browser.new_tab(browser_context_id=ctx) for ctx in contexts[1:]\n        ]\n        \n        async def scrape_site(tab, site, context_id):\n            \"\"\"Scrape a single site within the given tab and context.\"\"\"\n            try:\n                await tab.go_to(site['url'])\n                await asyncio.sleep(3)\n                \n                # Extract titles using CSS selector\n                elements = await tab.query(site['selector'], find_all=True)\n                titles = [await elem.text for elem in elements[:5]]\n                \n                return {'url': site['url'], 'titles': titles}\n            finally:\n                # Clean up context (skip default context for initial tab)\n                if context_id is not None:\n                    await tab.close()\n                    await browser.delete_browser_context(context_id)\n        \n        # Scrape all sites concurrently\n        results = await asyncio.gather(*[\n            scrape_site(tab, site, ctx) for tab, site, ctx in zip(tabs, websites, contexts)\n        ])\n        \n        # Display results\n        for result in results:\n            print(f\"\\n{result['url']}:\")\n            for i, title in enumerate(result['titles'], 1):\n                print(f\"  {i}. {title}\")\n\nasyncio.run(parallel_scraping())\n```\n\n## Understanding Context Performance\n\n### Contexts Are Lightweight\n\n!!! info \"Performance Characteristics\"\n    Creating a browser context is **significantly faster and lighter** than launching a new browser process:\n    \n    - **Context creation**: ~50-100ms, minimal memory overhead\n    - **New browser process**: ~2-5 seconds, 50-150 MB base memory\n    \n    For 10 isolated environments:\n\n    - **10 contexts in 1 browser**: ~500ms startup, ~500 MB total\n    - **10 separate browsers**: ~30 seconds startup, ~1-1.5 GB total\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def benchmark_contexts_vs_browsers():\n    # Benchmark contexts\n    start = time.time()\n    async with Chrome() as browser:\n        # Start browser (initial tab not used in this example)\n        await browser.start()\n        \n        contexts = []\n        for i in range(10):\n            context_id = await browser.create_browser_context()\n            contexts.append(context_id)\n        \n        print(f\"10 contexts created in: {time.time() - start:.2f}s\")\n        \n        # Cleanup\n        for context_id in contexts:\n            await browser.delete_browser_context(context_id)\n\nasyncio.run(benchmark_contexts_vs_browsers())\n```\n\n### Headless vs Headed: The Window Behavior\n\n!!! warning \"Important: Context Windows in Headed Mode\"\n    When running in **headed mode** (visible browser UI), there's an important behavior to understand:\n    \n    **The first tab created in a new context will open a new OS window.**\n    \n    - This happens because the context needs a \"host window\" to render its first page\n    - Subsequent tabs in that context can open as tabs within that window\n    - This is a CDP/Chromium limitation, not a Pydoll design choice\n    \n    **In headless mode**, this doesn't matter—no windows are created, everything runs in the background.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def demonstrate_window_behavior():\n    # Headed mode - will see windows\n    options_headed = ChromiumOptions()\n    options_headed.headless = False\n    \n    async with Chrome(options=options_headed) as browser:\n        # Start browser with initial tab (opens first window in default context)\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        \n        # Create new context - first tab will open a NEW window\n        context = await browser.create_browser_context()\n        tab2 = await browser.new_tab('https://github.com', browser_context_id=context)\n        \n        # Second tab in same context - opens as tab in existing window\n        tab3 = await browser.new_tab('https://google.com', browser_context_id=context)\n        \n        await asyncio.sleep(10)  # Observe the windows\n        \n        await tab2.close()\n        await tab3.close()\n        await browser.delete_browser_context(context)\n\n# Headless mode - no windows, contexts are invisible but still isolated\nasync def headless_contexts():\n    options = ChromiumOptions()\n    options.headless = True  # No visible windows\n    \n    async with Chrome(options=options) as browser:\n        # Start browser with initial tab in default context\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com/page0')\n        \n        # Create 4 more contexts - no windows opened, all in background\n        contexts = []\n        for i in range(1, 5):\n            context_id = await browser.create_browser_context()\n            tab = await browser.new_tab(f'https://example.com/page{i}', browser_context_id=context_id)\n            contexts.append((context_id, tab))\n        \n        print(f\"Created {len(contexts) + 1} isolated contexts (1 default + {len(contexts)} custom, invisible)\")\n        \n        # Cleanup\n        for context_id, tab in contexts:\n            await tab.close()\n            await browser.delete_browser_context(context_id)\n\nasyncio.run(headless_contexts())\n```\n\n!!! tip \"Best Practice: Use Headless for Contexts\"\n    For maximum efficiency with multiple contexts:\n    \n    - **Development/Debugging**: Use headed mode to see what's happening\n    - **Production/CI/CD**: Use headless mode for faster, lighter execution\n    - **Multiple contexts**: Strongly prefer headless to avoid window management complexity\n\n## Context Management\n\n### Creating Contexts\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def create_context_example():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Create basic context\n        context_id = await browser.create_browser_context()\n        print(f\"Created context: {context_id}\")\n        \n        # Create context with proxy\n        proxied_context = await browser.create_browser_context(\n            proxy_server='http://proxy.example.com:8080',\n            proxy_bypass_list='localhost,127.0.0.1'\n        )\n        print(f\"Created proxied context: {proxied_context}\")\n        \n        # Create context with authenticated proxy\n        auth_context = await browser.create_browser_context(\n            proxy_server='http://user:pass@proxy.example.com:8080'\n        )\n        print(f\"Created auth context: {auth_context}\")\n\nasyncio.run(create_context_example())\n```\n\n### Listing Contexts\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def list_contexts():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Get all contexts (includes default)\n        contexts = await browser.get_browser_contexts()\n        print(f\"Initial contexts: {len(contexts)}\")  # Usually 1 (default)\n        \n        # Create additional contexts\n        context1 = await browser.create_browser_context()\n        context2 = await browser.create_browser_context()\n        \n        # List again\n        contexts = await browser.get_browser_contexts()\n        print(f\"After creating 2 new contexts: {len(contexts)}\")  # 3 total\n        \n        for i, context_id in enumerate(contexts):\n            print(f\"  Context {i+1}: {context_id}\")\n        \n        # Cleanup\n        await browser.delete_browser_context(context1)\n        await browser.delete_browser_context(context2)\n\nasyncio.run(list_contexts())\n```\n\n### Deleting Contexts\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def delete_context_example():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Create context with tabs\n        context_id = await browser.create_browser_context()\n        tab1 = await browser.new_tab('https://example.com', browser_context_id=context_id)\n        tab2 = await browser.new_tab('https://github.com', browser_context_id=context_id)\n        \n        print(f\"Created context {context_id} with 2 tabs\")\n        \n        # Deleting context closes all its tabs automatically\n        await browser.delete_browser_context(context_id)\n        print(\"Context deleted (all tabs closed automatically)\")\n\nasyncio.run(delete_context_example())\n```\n\n!!! warning \"Deleting Contexts Closes All Tabs\"\n    When you delete a browser context, **all tabs belonging to that context are automatically closed**. This is an efficient way to clean up multiple tabs at once, but make sure you've saved any important data first.\n\n## Default Context\n\nEvery browser starts with a **default context** that contains the initial tab:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def default_context_example():\n    async with Chrome() as browser:\n        # Initial tab is in the default context\n        initial_tab = await browser.start()\n        \n        # Create tab without specifying context - uses default\n        default_tab = await browser.new_tab('https://example.com')\n        \n        # Create custom context\n        custom_context = await browser.create_browser_context()\n        custom_tab = await browser.new_tab('https://github.com', browser_context_id=custom_context)\n        \n        # Default and custom contexts are isolated\n        await default_tab.execute_script(\"localStorage.setItem('type', 'default')\")\n        await custom_tab.execute_script(\"localStorage.setItem('type', 'custom')\")\n        \n        # Verify isolation\n        default_type = await default_tab.execute_script(\"return localStorage.getItem('type')\")\n        custom_type = await custom_tab.execute_script(\"return localStorage.getItem('type')\")\n        \n        print(f\"Default context: {default_type}\")  # 'default'\n        print(f\"Custom context: {custom_type}\")    # 'custom'\n        \n        # Cleanup custom context\n        await browser.delete_browser_context(custom_context)\n\nasyncio.run(default_context_example())\n```\n\n!!! info \"You Cannot Delete the Default Context\"\n    The default browser context is permanent and cannot be deleted. It exists for the entire browser session. Only custom contexts created with `create_browser_context()` can be deleted.\n\n## Advanced Patterns\n\n### Context Pool for Reusable Isolation\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nclass ContextPool:\n    def __init__(self, browser, size=5):\n        self.browser = browser\n        self.size = size\n        self.contexts = []\n        self.in_use = set()\n    \n    async def initialize(self):\n        \"\"\"Create pool of contexts\"\"\"\n        for _ in range(self.size):\n            context_id = await self.browser.create_browser_context()\n            self.contexts.append(context_id)\n        print(f\"Context pool initialized with {self.size} contexts\")\n    \n    async def acquire(self):\n        \"\"\"Get available context from pool\"\"\"\n        for context_id in self.contexts:\n            if context_id not in self.in_use:\n                self.in_use.add(context_id)\n                return context_id\n        raise Exception(\"No available contexts in pool\")\n    \n    def release(self, context_id):\n        \"\"\"Return context to pool\"\"\"\n        self.in_use.discard(context_id)\n    \n    async def cleanup(self):\n        \"\"\"Delete all contexts in pool\"\"\"\n        for context_id in self.contexts:\n            await self.browser.delete_browser_context(context_id)\n\nasync def use_context_pool():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Create pool\n        pool = ContextPool(browser, size=3)\n        await pool.initialize()\n        \n        # Use contexts from pool\n        async def scrape_with_pool(url):\n            context_id = await pool.acquire()\n            try:\n                tab = await browser.new_tab(url, browser_context_id=context_id)\n                await asyncio.sleep(2)\n                title = await tab.execute_script('return document.title')\n                await tab.close()\n                return title\n            finally:\n                pool.release(context_id)\n        \n        # Scrape multiple URLs using the pool\n        urls = [f'https://example.com/page{i}' for i in range(10)]\n        results = await asyncio.gather(*[scrape_with_pool(url) for url in urls])\n        \n        for i, title in enumerate(results):\n            print(f\"{urls[i]}: {title}\")\n        \n        # Cleanup\n        await pool.cleanup()\n\nasyncio.run(use_context_pool())\n```\n\n### Per-Context Configuration Manager\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def context_config_manager():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Define configurations for different scenarios\n        configs = {\n            'us_user': {\n                'proxy': 'http://us-proxy.example.com:8080',\n                'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'\n            },\n            'eu_user': {\n                'proxy': 'http://eu-proxy.example.com:8080',\n                'user_agent': 'Mozilla/5.0 (X11; Linux x86_64)'\n            },\n            'mobile_user': {\n                'proxy': None,\n                'user_agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)'\n            }\n        }\n        \n        contexts = {}\n        \n        # Create context for each configuration\n        for name, config in configs.items():\n            if config['proxy']:\n                context_id = await browser.create_browser_context(\n                    proxy_server=config['proxy']\n                )\n            else:\n                context_id = await browser.create_browser_context()\n            \n            # Create tab and set user agent\n            tab = await browser.new_tab(browser_context_id=context_id)\n            # Note: User agent would be set via CDP or options, simplified here\n            \n            contexts[name] = {'context_id': context_id, 'tab': tab}\n        \n        # Use different contexts for different scenarios\n        for name, data in contexts.items():\n            tab = data['tab']\n            await tab.go_to('https://httpbin.org/headers')\n            await asyncio.sleep(2)\n            print(f\"\\n{name} configuration active\")\n        \n        # Cleanup\n        for data in contexts.values():\n            await data['tab'].close()\n            await browser.delete_browser_context(data['context_id'])\n\nasyncio.run(context_config_manager())\n```\n\n## Best Practices\n\n1. **Use headless mode for multiple contexts** to avoid window management complexity\n2. **Always delete contexts when done** to prevent memory leaks\n3. **Group related operations in the same context** for better organization\n4. **Prefer contexts over multiple browser processes** for better performance\n5. **Use context pools** for scenarios requiring many short-lived isolated environments\n6. **Close tabs before deleting contexts** for cleaner cleanup (though not strictly required)\n\n## See Also\n\n- **[Multi-Tab Management](tabs.md)** - Managing multiple tabs within contexts\n- **[Deep Dive: Browser Domain](../../deep-dive/browser-domain.md)** - Architectural details on contexts\n- **[Network: HTTP Requests](../network/http-requests.md)** - Browser-context requests inherit context state\n- **[Core Concepts](../core-concepts.md)** - Understanding Pydoll's architecture\n\nBrowser Contexts are one of Pydoll's most powerful features for creating sophisticated automation workflows. By understanding how they work—especially the window behavior in headed mode and their lightweight nature—you can build efficient, scalable automation that handles complex multi-environment scenarios with ease.\n\n"
  },
  {
    "path": "docs/en/features/browser-management/cookies-sessions.md",
    "content": "# Cookies & Sessions\n\nManaging cookies and sessions effectively is crucial for realistic browser automation. Websites use cookies to track authentication, preferences, and user behavior, and they expect browsers to behave accordingly.\n\n## Why Cookies Matter for Automation\n\nCookies are more than just stored data: they're a fingerprint of browser activity:\n\n- **Authentication**: Session cookies maintain login state across requests\n- **Tracking Prevention**: Anti-bot systems analyze cookie patterns\n- **Realistic Behavior**: A browser without cookies looks suspicious\n- **Session Persistence**: Reusing cookies can save time on repeated logins\n\n!!! warning \"The Cookie Paradox\"\n    - **Too clean**: A browser with no cookies or history appears bot-like\n    - **Too stale**: Using the same session for weeks triggers security alerts\n    - **Sweet spot**: Fresh cookies with occasional rotation and realistic activity patterns\n\n## Quick Start\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def basic_cookie_management():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Set a cookie (using a simple dict)\n        cookies = [\n            {\n                'name': 'session_id',\n                'value': 'abc123xyz',\n                'domain': 'example.com',\n                'path': '/',\n                'secure': True,\n                'httpOnly': True\n            }\n        ]\n        await tab.set_cookies(cookies)\n        \n        # Get all cookies\n        all_cookies = await browser.get_cookies()\n        print(f\"Total cookies: {len(all_cookies)}\")\n        \n        # Delete all cookies\n        await tab.delete_all_cookies()\n\nasyncio.run(basic_cookie_management())\n```\n\n## Understanding Cookie Types\n\n!!! info \"TypedDict: Use Regular Dicts in Practice\"\n    Throughout this documentation, you'll see references to `CookieParam` and `Cookie`. These are **TypedDict** types, they're just regular Python dicts with type hints for IDE autocomplete and type checking.\n    \n    **In practice, you use regular dicts:**\n    ```python\n    # This is what you actually write:\n    cookie = {'name': 'session', 'value': 'abc123', 'domain': 'example.com'}\n    \n    # The type annotation is just for your IDE:\n    from pydoll.protocol.network.types import CookieParam\n    cookie: CookieParam = {'name': 'session', 'value': 'abc123'}\n    ```\n    \n    All examples below use plain dicts for simplicity.\n\n### Cookie Structure\n\nThe `Cookie` type (retrieved from browser) contains full cookie information:\n\n```python\n{\n    \"name\": str,           # Cookie name\n    \"value\": str,          # Cookie value\n    \"domain\": str,         # Domain where cookie is valid\n    \"path\": str,           # Path where cookie is valid\n    \"expires\": float,      # Unix timestamp (0 = session cookie)\n    \"size\": int,           # Size in bytes\n    \"httpOnly\": bool,      # Accessible only via HTTP (not JavaScript)\n    \"secure\": bool,        # Sent only over HTTPS\n    \"session\": bool,       # True if expires when browser closes\n    \"sameSite\": str,       # \"Strict\", \"Lax\", or \"None\"\n    \"priority\": str,       # \"Low\", \"Medium\", or \"High\"\n    \"sourceScheme\": str,   # \"Unset\", \"NonSecure\", or \"Secure\"\n    \"sourcePort\": int,     # Port where cookie was set\n}\n```\n\n### CookieParam Structure\n\nWhen **setting** cookies, use a dict (only `name` and `value` are required):\n\n```python\n# Simple cookie with just required fields\ncookie = {\n    'name': 'user_token',\n    'value': 'token_value'\n}\n\n# Full cookie with all optional fields\ncookie = {\n    'name': 'user_token',       # Required\n    'value': 'token_value',     # Required\n    'domain': 'example.com',    # Optional: defaults to current page domain\n    'path': '/',                # Optional: defaults to /\n    'secure': True,             # Optional: HTTPS only\n    'httpOnly': True,           # Optional: no JS access\n    'sameSite': 'Lax',          # Optional: 'Strict', 'Lax', or 'None'\n    'expires': 1735689600,      # Optional: Unix timestamp\n    'priority': 'High',         # Optional: 'Low', 'Medium', or 'High'\n}\n```\n\n!!! info \"Optional Fields Default Behavior\"\n    When you omit optional fields:\n    \n    - `domain`: Uses the domain of the current page\n    - `path`: Defaults to `/`\n    - `secure`: Defaults to `False`\n    - `httpOnly`: Defaults to `False`\n    - `sameSite`: Browser's default (usually `Lax`)\n    - `expires`: Session cookie (deleted when browser closes)\n\n## Cookie Management Operations\n\n### Setting Cookies\n\n#### Set Multiple Cookies at Once\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def set_multiple_cookies():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        cookies = [\n            {\n                'name': 'session_id',\n                'value': 'xyz789',\n                'domain': 'example.com',\n                'secure': True,\n                'httpOnly': True,\n                'sameSite': 'Strict'\n            },\n            {\n                'name': 'preferences',\n                'value': 'dark_mode=true',\n                'domain': 'example.com',\n                'path': '/settings'\n            },\n            {\n                'name': 'analytics',\n                'value': 'tracking_id_12345',\n                'domain': 'example.com',\n                'expires': 1735689600  # Expires on specific date\n            }\n        ]\n        \n        await tab.set_cookies(cookies)\n        print(f\"Set {len(cookies)} cookies\")\n\nasyncio.run(set_multiple_cookies())\n```\n\n#### Set Cookies in Specific Context\n\n```python\n# Set cookies in a specific browser context\ncontext_id = await browser.create_browser_context()\nawait browser.set_cookies(cookies, browser_context_id=context_id)\n```\n\n!!! tip \"Tab vs Browser Methods for Setting Cookies\"\n    - `tab.set_cookies(cookies)`: Sets cookies in the tab's browser context (convenient shortcut)\n    - `browser.set_cookies(cookies, browser_context_id=...)`: Sets cookies with explicit context control\n    \n    Both methods add cookies to the **entire context**, not just the current page. The cookies will be available to all tabs in that context.\n\n### Retrieving Cookies\n\n#### Get All Cookies (Context-Wide)\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def get_cookies_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://github.com')\n        \n        # Wait for page to set cookies\n        await asyncio.sleep(2)\n        \n        # Option 1: Get cookies via tab (shortcut for current context)\n        cookies = await tab.get_cookies()\n        \n        # Option 2: Get cookies via browser (explicit context control)\n        # cookies = await browser.get_cookies()  # Same as tab.get_cookies() for default context\n        \n        print(f\"Found {len(cookies)} cookies:\")\n        for cookie in cookies:\n            print(f\"  - {cookie['name']}: {cookie['value'][:20]}...\")\n            print(f\"    Domain: {cookie['domain']}, Secure: {cookie['secure']}\")\n\nasyncio.run(get_cookies_example())\n```\n\n!!! tip \"Tab vs Browser Methods\"\n    - `tab.get_cookies()`: Returns cookies from the tab's browser context (convenient shortcut)\n    - `browser.get_cookies()`: Returns cookies from the default context (or specify `browser_context_id`)\n    \n    Both methods return **all cookies** from the context, not just cookies for the current page domain.\n\n!!! warning \"Incognito Mode Limitation\"\n    `browser.get_cookies()` does **not work** with native incognito mode (`--incognito` flag). This is a Chrome DevTools Protocol limitation where `Storage.getCookies` cannot access cookies in native incognito mode.\n    \n    **Workaround:** Use `tab.get_cookies()` instead, which uses `Network.getCookies` and works correctly in incognito mode.\n\n#### Get Cookies from Specific Context\n\n```python\n# Get cookies from specific browser context\ncontext_id = await browser.create_browser_context()\ncookies = await browser.get_cookies(browser_context_id=context_id)\n```\n\n### Deleting Cookies\n\n#### Delete All Cookies\n\n```python\n# Delete all cookies from current tab's context\nawait tab.delete_all_cookies()\n\n# Delete all cookies from specific context\nawait browser.delete_all_cookies(browser_context_id=context_id)\n```\n\n!!! warning \"Cookies Are Deleted Immediately\"\n    When you delete cookies, they're removed from the browser immediately. The website may not detect this until the next request or page reload.\n\n## Practical Use Cases\n\n### 1. Persistent Login Sessions\n\nReuse authentication cookies across script runs:\n\n```python\nimport asyncio\nimport json\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nCOOKIE_FILE = Path('cookies.json')\n\nasync def save_cookies_after_login():\n    \"\"\"Log in and save cookies for future use.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/login')\n        \n        # Perform login (simplified)\n        email = await tab.find(id='email')\n        password = await tab.find(id='password')\n        await email.type_text('user@example.com')\n        await password.type_text('secret')\n        \n        login_btn = await tab.find(id='login')\n        await login_btn.click()\n        await asyncio.sleep(3)\n        \n        # Save cookies\n        cookies = await browser.get_cookies()\n        COOKIE_FILE.write_text(json.dumps(cookies, indent=2))\n        print(f\"Saved {len(cookies)} cookies to {COOKIE_FILE}\")\n\nasync def reuse_saved_cookies():\n    \"\"\"Load saved cookies to skip login.\"\"\"\n    if not COOKIE_FILE.exists():\n        print(\"No saved cookies found. Run save_cookies_after_login() first.\")\n        return\n    \n    # Load cookies from file\n    saved_cookies = json.loads(COOKIE_FILE.read_text())\n    \n    # Convert to simplified format (only required fields)\n    # Note: get_cookies() returns detailed Cookie objects with read-only fields\n    # (size, session, sourceScheme, etc.). set_cookies() expects CookieParam\n    # format with only the settable fields.\n    cookies_to_set = [\n        {\n            'name': c['name'],\n            'value': c['value'],\n            'domain': c['domain'],\n            'path': c.get('path', '/'),\n            'secure': c.get('secure', False),\n            'httpOnly': c.get('httpOnly', False)\n        }\n        for c in saved_cookies\n    ]\n    \n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Set cookies before navigating\n        await tab.set_cookies(cookies_to_set)\n        print(f\"Loaded {len(cookies_to_set)} cookies from file\")\n        \n        # Navigate - should be logged in already\n        await tab.go_to('https://example.com/dashboard')\n        await asyncio.sleep(2)\n        \n        # Verify login\n        try:\n            username = await tab.find(class_name='username')\n            print(f\"Logged in as: {await username.text}\")\n        except Exception:\n            print(\"Login failed - cookies may have expired\")\n\n# First run: log in and save cookies\n# asyncio.run(save_cookies_after_login())\n\n# Subsequent runs: reuse cookies\nasyncio.run(reuse_saved_cookies())\n```\n\n!!! note \"Cookie Reformatting Required\"\n    `get_cookies()` returns **detailed `Cookie` objects** with read-only attributes like `size`, `session`, `sourceScheme`, and `sourcePort`. When using `set_cookies()`, you must provide **`CookieParam` format** containing only the settable fields (`name`, `value`, `domain`, `path`, `secure`, `httpOnly`, `sameSite`, `expires`, `priority`).\n    \n    The reformatting step in the example above is **essential**. Passing raw `Cookie` objects to `set_cookies()` may cause errors or unexpected behavior.\n\n!!! tip \"Cookie Expiration\"\n    Always check if saved cookies have expired. Session cookies (`session=True`) expire when the browser closes, while persistent cookies have an `expires` timestamp you can validate.\n\n### 2. Multi-Account Testing with Isolated Cookies\n\nEach browser context maintains separate cookies:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def test_multiple_accounts():\n    accounts = [\n        {'email': 'user1@example.com', 'cookie_value': 'session_user1'},\n        {'email': 'user2@example.com', 'cookie_value': 'session_user2'},\n    ]\n    \n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # First account in default context\n        cookies_user1 = [{\n            'name': 'session',\n            'value': accounts[0]['cookie_value'],\n            'domain': 'example.com',\n            'secure': True,\n            'httpOnly': True\n        }]\n        await initial_tab.set_cookies(cookies_user1)\n        await initial_tab.go_to('https://example.com/dashboard')\n        \n        # Second account in isolated context\n        context2 = await browser.create_browser_context()\n        tab2 = await browser.new_tab(browser_context_id=context2)\n        \n        cookies_user2 = [{\n            'name': 'session',\n            'value': accounts[1]['cookie_value'],\n            'domain': 'example.com',\n            'secure': True,\n            'httpOnly': True\n        }]\n        await browser.set_cookies(cookies_user2, browser_context_id=context2)\n        await tab2.go_to('https://example.com/dashboard')\n        \n        # Both users are logged in simultaneously with different sessions\n        print(\"User 1 and User 2 logged in with isolated cookies\")\n        \n        await asyncio.sleep(5)\n        \n        # Cleanup\n        await tab2.close()\n        await browser.delete_browser_context(context2)\n\nasyncio.run(test_multiple_accounts())\n```\n\n### 3. Cookie Rotation for Long-Running Scripts\n\nRefresh cookies periodically to avoid detection:\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_with_cookie_rotation():\n    urls = [f'https://example.com/page{i}' for i in range(100)]\n    \n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Log in initially\n        await tab.go_to('https://example.com/login')\n        # ... perform login ...\n        await asyncio.sleep(2)\n        \n        last_rotation = time.time()\n        rotation_interval = 600  # Rotate every 10 minutes\n        \n        for url in urls:\n            # Check if it's time to rotate cookies\n            if time.time() - last_rotation > rotation_interval:\n                print(\"Rotating session...\")\n                \n                # Delete old cookies\n                await tab.delete_all_cookies()\n                \n                # Re-login or load fresh cookies\n                await tab.go_to('https://example.com/login')\n                # ... perform login again ...\n                \n                last_rotation = time.time()\n            \n            # Scrape page\n            await tab.go_to(url)\n            await asyncio.sleep(2)\n            # ... extract data ...\n\nasyncio.run(scrape_with_cookie_rotation())\n```\n\n!!! tip \"Rotation Frequency\"\n    The ideal rotation frequency depends on your use case:\n    \n    - **High-security sites**: Rotate every 5-15 minutes\n    - **Normal sites**: Rotate every 30-60 minutes\n    - **Low-risk scraping**: Rotate every few hours\n\n\n## Cookie Attributes Reference\n\n| Attribute | Type | Description | Default |\n|-----------|------|-------------|---------|\n| `name` | `str` | Cookie name | *Required* |\n| `value` | `str` | Cookie value | *Required* |\n| `domain` | `str` | Domain where cookie is valid | Current page domain |\n| `path` | `str` | Path where cookie is valid | `/` |\n| `secure` | `bool` | Send only over HTTPS | `False` |\n| `httpOnly` | `bool` | Not accessible via JavaScript | `False` |\n| `sameSite` | `CookieSameSite` | CSRF protection: `Strict`, `Lax`, `None` | Browser default (`Lax`) |\n| `expires` | `float` | Unix timestamp (0 = session cookie) | `0` (session) |\n| `priority` | `CookiePriority` | Cookie priority: `Low`, `Medium`, `High` | `Medium` |\n\n### SameSite Values\n\n```python\n# Use string values directly in your cookie dict:\n\n'sameSite': 'Strict'  # Cookie sent only for same-site requests\n'sameSite': 'Lax'     # Cookie sent for top-level navigation (default)\n'sameSite': 'None'    # Cookie sent for all requests (requires secure=True)\n\n# Or use the enum for IDE autocomplete:\nfrom pydoll.protocol.network.types import CookieSameSite\n\ncookie = {\n    'name': 'session',\n    'value': 'xyz',\n    'sameSite': CookieSameSite.STRICT  # IDE will autocomplete: STRICT, LAX, NONE\n}\n```\n\n### Priority Values\n\n```python\n# Use string values directly:\n\n'priority': 'Low'     # Low priority (deleted first when space is needed)\n'priority': 'Medium'  # Medium priority (default)\n'priority': 'High'    # High priority (deleted last)\n\n# Or use the enum:\nfrom pydoll.protocol.network.types import CookiePriority\n\ncookie = {\n    'name': 'session',\n    'value': 'xyz',\n    'priority': CookiePriority.HIGH  # IDE will autocomplete: LOW, MEDIUM, HIGH\n}\n```\n\n## Common Patterns\n\n### Context Manager for Temporary Cookies\n\n```python\nfrom contextlib import asynccontextmanager\n\n@asynccontextmanager\nasync def temporary_cookies(browser, tab, cookies):\n    \"\"\"Set cookies, execute code, then restore original cookies.\"\"\"\n    # Save current cookies\n    original_cookies = await browser.get_cookies()\n    \n    try:\n        # Set temporary cookies\n        await tab.delete_all_cookies()\n        await tab.set_cookies(cookies)\n        yield tab\n    finally:\n        # Restore original cookies\n        await tab.delete_all_cookies()\n        cookies_to_restore = [\n            {\n                'name': c['name'],\n                'value': c['value'],\n                'domain': c['domain'],\n                'path': c.get('path', '/')\n            }\n            for c in original_cookies\n        ]\n        await tab.set_cookies(cookies_to_restore)\n\n# Usage\nasync with temporary_cookies(browser, tab, test_cookies):\n    await tab.go_to('https://example.com')\n    # ... perform actions with temporary cookies ...\n# Original cookies restored automatically\n```\n\n!!! tip \"Using Public APIs\"\n    This context manager accepts both `browser` and `tab` as parameters to use public APIs. Since `tab` doesn't expose its parent `browser` as a public property, passing it explicitly is the recommended approach for accessing browser-level methods.\n\n### Cookie Fingerprint Comparison\n\n```python\ndef cookie_fingerprint(cookies):\n    \"\"\"Generate a simple fingerprint of cookie state.\"\"\"\n    return {\n        'count': len(cookies),\n        'domains': set(c['domain'] for c in cookies),\n        'names': sorted(c['name'] for c in cookies),\n        'secure_count': sum(1 for c in cookies if c.get('secure')),\n        'httponly_count': sum(1 for c in cookies if c.get('httpOnly')),\n    }\n\n# Compare cookie states\nbefore = await browser.get_cookies()\nawait tab.go_to('https://example.com')\nafter = await browser.get_cookies()\n\nprint(f\"Before: {cookie_fingerprint(before)}\")\nprint(f\"After: {cookie_fingerprint(after)}\")\n```\n\n## Security Considerations\n\n!!! danger \"Never Hardcode Sensitive Cookies\"\n    Always load authentication cookies from secure storage (environment variables, encrypted files, secrets managers).\n    \n    ```python\n    # ❌ Bad - hardcoded in code\n    cookies = [{'name': 'session', 'value': 'abc123secret'}]\n    \n    # ✅ Good - loaded from environment\n    import os\n    cookies = [{\n        'name': 'session',\n        'value': os.getenv('SESSION_COOKIE'),\n        'domain': os.getenv('COOKIE_DOMAIN')\n    }]\n    ```\n\n!!! warning \"Cookie Theft Protection\"\n    When saving cookies to disk:\n    \n    - Use encrypted storage (e.g., `cryptography` library)\n    - Set restrictive file permissions\n    - Never commit cookie files to version control\n    - Rotate cookies regularly\n\n## Best Practices Summary\n\n1. **Start with realistic cookies** - Don't run automation with a completely clean browser\n2. **Rotate sessions periodically** - Avoid using the same cookies for extended periods\n3. **Respect cookie security attributes** - Use `secure`, `httpOnly`, `sameSite` appropriately\n4. **Save and reuse authentication cookies** - Skip repetitive logins when appropriate\n5. **Isolate contexts for multi-account testing** - Each context has independent cookies\n6. **Monitor cookie evolution** - Real browsing accumulates cookies naturally\n7. **Clean up expired cookies** - Remove invalid cookies before reuse\n8. **Use secure storage** - Encrypt saved cookies, never hardcode secrets\n\n## See Also\n\n- **[Browser Contexts](contexts.md)** - Isolated cookie environments\n- **[HTTP Requests](../network/http-requests.md)** - Browser-context requests inherit cookies automatically\n- **[Human-Like Interactions](../automation/human-interactions.md)** - Combine cookies with realistic behavior\n- **[API Reference: Storage Commands](/api/commands/storage_commands/)** - Full CDP cookie methods\n\nEffective cookie management is the foundation of realistic browser automation. By balancing freshness with persistence and respecting security attributes, you can build automation that behaves like a real user while remaining efficient and maintainable.\n"
  },
  {
    "path": "docs/en/features/browser-management/tabs.md",
    "content": "# Multi-Tab Management\n\nPydoll provides sophisticated multi-tab capabilities that enable complex automation workflows spanning multiple browser tabs simultaneously. Understanding how tabs work in Pydoll is essential for building robust, scalable automation.\n\n## Understanding Tabs in Pydoll\n\nIn Pydoll, a `Tab` instance represents a single browser tab (or window) and provides the primary interface for all page automation operations. Each tab maintains its own:\n\n- **Independent execution context**: JavaScript, DOM, and page state\n- **Isolated event handlers**: Callbacks registered on one tab don't affect others\n- **Separate network monitoring**: Each tab can track its own network activity\n- **Unique CDP connection**: Direct WebSocket communication with the browser\n\n```mermaid\ngraph LR\n    Browser[Browser Instance] --> Tab1[Tab 1]\n    Browser --> Tab2[Tab 2]\n    Browser --> Tab3[...]\n    \n    Tab1 --> Features1[Independent<br/>Context]\n    Tab2 --> Features2[Independent<br/>Context]\n```\n\n| Tab Component | Description | Independence |\n|---------------|-------------|--------------|\n| **Execution Context** | JavaScript runtime, DOM, page state | ✓ Each tab has its own |\n| **Event Handlers** | Registered callbacks for CDP events | ✓ Isolated per tab |\n| **Network Monitoring** | HTTP requests, responses, timing | ✓ Track separately |\n| **CDP Connection** | WebSocket communication channel | ✓ Direct connection |\n\n### What is a Browser Tab?\n\nA browser tab is technically a **CDP target** - an isolated browsing context with its own:\n\n- Document Object Model (DOM)\n- JavaScript execution environment\n- Network connection pool\n- Cookie storage (shared with other tabs in the same context)\n- Event loop and rendering engine\n\nEach tab has a unique `target_id` assigned by the browser, which Pydoll uses to route commands and events correctly.\n\n## Tab Instance Management\n\nPydoll's `Browser` class maintains a registry of Tab instances based on each tab's `target_id`. This ensures that multiple references to the same browser tab always return the same Tab object. The Browser stores these instances in an internal `_tabs_opened` dictionary.\n\n| Benefit | Description |\n|---------|-------------|\n| **Resource Efficiency** | One Tab instance per browser tab, no duplicates |\n| **Consistent State** | All references share the same event handlers and state |\n| **Memory Safety** | Prevents multiple WebSocket connections to the same target |\n| **Predictable Behavior** | Changes in one reference affect all references |\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def tab_registry_demonstration():\n    async with Chrome() as browser:\n        # Start the browser with initial tab\n        tab1 = await browser.start()\n\n        # Get the same tab through different methods\n        # Note: get_opened_tabs() returns tabs in reversed order (newest first)\n        # So the initial tab (oldest) is at the end\n        opened_tabs = await browser.get_opened_tabs()\n        tab2 = opened_tabs[-1]  # Initial tab is the oldest, so it's last\n\n        # Both references point to the same object\n        # because Browser returns the same instance from its registry\n        print(f\"Same instance? {tab1 is tab2}\")  # True\n        print(f\"Same target ID? {tab1._target_id == tab2._target_id}\")  # True\n\n        # Registering event on one reference affects the other\n        await tab1.enable_network_events()\n        print(f\"Network events on tab2? {tab2.network_events_enabled}\")  # True\n\n        # Browser maintains the registry internally\n        print(f\"Tab registered in browser? {tab1._target_id in browser._tabs_opened}\")  # True\n\nasyncio.run(tab_registry_demonstration())\n```\n\n!!! info \"Browser-Managed Registry\"\n    The Browser class manages a `_tabs_opened` dictionary keyed by `target_id`. When you request a tab (via `new_tab()` or `get_opened_tabs()`), the Browser checks this registry first. If a Tab instance already exists for that `target_id`, it returns the existing instance; otherwise, it creates a new one and stores it in the registry. (IFrames no longer create Tab entries—interact with them as regular elements.)\n\n## Creating and Managing Tabs\n\n### Starting the Browser\n\nWhen you start the browser, Pydoll automatically creates and returns a Tab instance for the initial browser tab:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def start_browser():\n    async with Chrome() as browser:\n        # Initial tab is created automatically\n        tab = await browser.start()\n        \n        print(f\"Tab created with target ID: {tab._target_id}\")\n        await tab.go_to('https://example.com')\n        \n        title = await tab.execute_script('return document.title')\n        print(f\"Page title: {title}\")\n\nasyncio.run(start_browser())\n```\n\n### Creating Additional Tabs Programmatically\n\nUse `browser.new_tab()` to create additional tabs with full control:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def create_multiple_tabs():\n    async with Chrome() as browser:\n        # Start with initial tab\n        main_tab = await browser.start()\n        \n        # Create additional tabs with specific URLs\n        search_tab = await browser.new_tab('https://google.com')\n        docs_tab = await browser.new_tab('https://docs.python.org')\n        news_tab = await browser.new_tab('https://news.ycombinator.com')\n        \n        # Each tab can be controlled independently\n        await search_tab.find(name='q')  # Google search box\n        await docs_tab.find(id='search-field')  # Python docs search\n        await news_tab.find(class_name='storylink', find_all=True)  # HN stories\n        \n        # Get all opened tabs\n        all_tabs = await browser.get_opened_tabs()\n        print(f\"Total tabs: {len(all_tabs)}\")  # 4 (initial + 3 new)\n        \n        # Close specific tabs when done\n        await search_tab.close()\n        await docs_tab.close()\n        await news_tab.close()\n\nasyncio.run(create_multiple_tabs())\n```\n\n!!! tip \"URL Parameter Optional\"\n    You can create tabs without specifying a URL: `await browser.new_tab()`. The tab will open with a blank page (`about:blank`), ready for navigation.\n\n### Handling User-Opened Tabs\n\nWhen users click links with `target=\"_blank\"` or use \"Open in new tab\", Pydoll can detect and manage these tabs:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def handle_user_tabs():\n    async with Chrome() as browser:\n        main_tab = await browser.start()\n        await main_tab.go_to('https://example.com')\n        \n        # Record initial tab count\n        initial_tabs = await browser.get_opened_tabs()\n        print(f\"Initial tabs: {len(initial_tabs)}\")\n        \n        # Click a link that opens a new tab (target=\"_blank\")\n        external_link = await main_tab.find(text='Open in New Tab')\n        await external_link.click()\n        \n        # Wait for new tab to open\n        await asyncio.sleep(2)\n        \n        # Detect new tabs\n        current_tabs = await browser.get_opened_tabs()\n        print(f\"Current tabs: {len(current_tabs)}\")\n        \n        # Find the newly opened tab (last in the list)\n        if len(current_tabs) > len(initial_tabs):\n            new_tab = current_tabs[-1]\n            \n            # Work with the new tab\n            url = await new_tab.current_url\n            print(f\"New tab URL: {url}\")\n            \n            await new_tab.go_to('https://different-site.com')\n            title = await new_tab.execute_script('return document.title')\n            print(f\"New tab title: {title}\")\n            \n            # Close it when done\n            await new_tab.close()\n\nasyncio.run(handle_user_tabs())\n```\n\n### Listing All Open Tabs\n\nUse `browser.get_opened_tabs()` to retrieve all currently open tabs:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def list_tabs():\n    async with Chrome() as browser:\n        # Use the initial tab returned by start()\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        \n        # Open several more tabs\n        await browser.new_tab('https://github.com')\n        await browser.new_tab('https://stackoverflow.com')\n        await browser.new_tab('https://reddit.com')\n        \n        # Get all tabs\n        all_tabs = await browser.get_opened_tabs()\n        \n        # Inspect each tab\n        for i, tab in enumerate(all_tabs, 1):\n            url = await tab.current_url\n            title = await tab.execute_script('return document.title')\n            print(f\"Tab {i}: {title} - {url}\")\n\nasyncio.run(list_tabs())\n```\n\n## Concurrent Tab Operations\n\nPydoll's async architecture enables powerful concurrent workflows across multiple tabs:\n\n### Parallel Data Collection\n\nProcess multiple pages simultaneously for maximum efficiency:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_page(tab, url):\n    \"\"\"Scrape a single page within a given tab.\"\"\"\n    await tab.go_to(url)\n    title = await tab.execute_script('return document.title')\n    articles = await tab.find(class_name='article', find_all=True)\n    content = [await article.text for article in articles[:5]]\n\n    return {\n        'url': url,\n        'title': title,\n        'articles_count': len(articles),\n        'sample_content': content\n    }\n\nasync def concurrent_scraping():\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n        'https://example.com/page4',\n    ]\n\n    async with Chrome() as browser:\n        # Start browser and open the first tab\n        initial_tab = await browser.start()\n        # Create one tab per URL\n        tabs = [initial_tab] + [await browser.new_tab() for _ in urls[1:]]\n\n        # Run all scrapers concurrently\n        results = await asyncio.gather(*[\n            scrape_page(tab, url) for tab, url in zip(tabs, urls)\n        ])\n\n        # Display results\n        for result in results:\n            print(f\"\\n{result['title']}\")\n            print(f\"  URL: {result['url']}\")\n            print(f\"  Articles: {result['articles_count']}\")\n            if result['sample_content']:\n                print(f\"  Sample: {result['sample_content'][0][:100]}...\")\n\nasyncio.run(concurrent_scraping())\n```\n\n!!! tip \"Performance Boost\"\n    Concurrent scraping can reduce total execution time by 5-10x compared to sequential processing, especially for I/O-bound tasks like page loading.\n\n### Coordinated Multi-Tab Workflows\n\nOrchestrate complex workflows that require multiple tabs to interact:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\n\nasync def multi_tab_workflow():\n    async with Chrome() as browser:\n        # Use the initial tab for login\n        login_tab = await browser.start()\n        await login_tab.go_to('https://app.example.com/login')\n        await asyncio.sleep(2)\n        \n        username = await login_tab.find(id='username')\n        password = await login_tab.find(id='password')\n        \n        await username.type_text('admin@example.com')\n        await password.type_text('secure_password')\n        \n        login_btn = await login_tab.find(id='login')\n        await login_btn.click()\n        await asyncio.sleep(3)\n        \n        # Tab 2: Navigate to data export page\n        export_tab = await browser.new_tab('https://app.example.com/export')\n        await asyncio.sleep(2)\n        \n        export_btn = await export_tab.find(text='Export Data')\n        await export_btn.click()\n        \n        # Tab 3: Monitor API calls in a dashboard\n        monitor_tab = await browser.new_tab('https://app.example.com/dashboard')\n        await monitor_tab.enable_network_events()\n        \n        # Track API calls\n        api_calls = []\n        async def track_api(event: RequestWillBeSentEvent):\n            url = event['params']['request']['url']\n            if '/api/' in url:\n                api_calls.append(url)\n        \n        await monitor_tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, track_api)\n        await asyncio.sleep(5)\n        \n        print(f\"Tracked {len(api_calls)} API calls:\")\n        for call in api_calls[:10]:\n            print(f\"  - {call}\")\n        \n        # Clean up\n        await login_tab.close()\n        await export_tab.close()\n        await monitor_tab.close()\n\nasyncio.run(multi_tab_workflow())\n```\n\n## Tab Lifecycle and Cleanup\n\n### Explicit Tab Closure\n\nAlways close tabs when you're done to free browser resources:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def explicit_cleanup():\n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # Create tabs for different tasks\n        tab1 = await browser.new_tab('https://example.com')\n        tab2 = await browser.new_tab('https://example.org')\n        \n        # Do work with tabs\n        await tab1.go_to('https://different-site.com')\n        await tab2.take_screenshot('/tmp/screenshot.png')\n        \n        # Explicitly close tabs\n        await tab1.close()\n        await tab2.close()\n        \n        # Verify tabs are closed\n        remaining = await browser.get_opened_tabs()\n        print(f\"Remaining tabs: {len(remaining)}\")  # Should be 1 (initial)\n\nasyncio.run(explicit_cleanup())\n```\n\n!!! warning \"Memory Leaks\"\n    Failing to close tabs in long-running automation can lead to memory exhaustion. Each tab consumes browser resources (memory, file handles, network connections).\n\n### Using Context Managers for Automatic Cleanup\n\nWhile Pydoll doesn't provide a built-in tab context manager, you can create your own:\n\n```python\nimport asyncio\nfrom contextlib import asynccontextmanager\nfrom pydoll.browser.chromium import Chrome\n\n@asynccontextmanager\nasync def managed_tab(browser, url=None):\n    \"\"\"Context manager for automatic tab cleanup.\"\"\"\n    tab = await browser.new_tab(url)\n    try:\n        yield tab\n    finally:\n        await tab.close()\n\nasync def auto_cleanup_example():\n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # Tab automatically closes when exiting the context\n        async with managed_tab(browser, 'https://example.com') as tab:\n            title = await tab.execute_script('return document.title')\n            print(f\"Title: {title}\")\n            \n            await tab.take_screenshot('/tmp/page.png')\n        # Tab is automatically closed here\n        \n        tabs = await browser.get_opened_tabs()\n        print(f\"Tabs after context exit: {len(tabs)}\")  # 1 (initial_tab only)\n\nasyncio.run(auto_cleanup_example())\n```\n\n### Browser Cleanup\n\nWhen the browser closes, all tabs are automatically closed:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def browser_cleanup():\n    # Using context manager - automatic cleanup\n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # Create multiple tabs\n        await browser.new_tab('https://example.com')\n        await browser.new_tab('https://github.com')\n        await browser.new_tab('https://stackoverflow.com')\n        \n        tabs = await browser.get_opened_tabs()\n        print(f\"Tabs open: {len(tabs)}\")  # 4 (initial + 3 new)\n    \n    # All tabs automatically closed when browser exits\n    print(\"Browser closed, all tabs cleaned up\")\n\nasyncio.run(browser_cleanup())\n```\n\n## Tab State Management\n\n### Checking Tab State\n\nQuery various aspects of a tab's current state:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def check_tab_state():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Check current URL\n        url = await tab.current_url\n        print(f\"Current URL: {url}\")\n        \n        # Check page source\n        source = await tab.page_source\n        print(f\"Page source length: {len(source)} characters\")\n        \n        # Check enabled event domains\n        print(f\"Page events enabled: {tab.page_events_enabled}\")\n        print(f\"Network events enabled: {tab.network_events_enabled}\")\n        print(f\"DOM events enabled: {tab.dom_events_enabled}\")\n        \n        # Enable events and check again\n        await tab.enable_network_events()\n        print(f\"Network events enabled: {tab.network_events_enabled}\")  # True\n\nasyncio.run(check_tab_state())\n```\n\n### Tab Identification\n\nEach tab has unique identifiers:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def tab_identification():\n    async with Chrome() as browser:\n        tab1 = await browser.start()\n        tab2 = await browser.new_tab()\n        \n        # Target ID - unique identifier assigned by browser\n        print(f\"Tab 1 target ID: {tab1._target_id}\")\n        print(f\"Tab 2 target ID: {tab2._target_id}\")\n        \n        # Connection details\n        print(f\"Tab 1 connection port: {tab1._connection_port}\")\n        print(f\"Tab 2 connection port: {tab2._connection_port}\")\n        \n        # Browser context ID (usually None for default context)\n        print(f\"Tab 1 context ID: {tab1._browser_context_id}\")\n        print(f\"Tab 2 context ID: {tab2._browser_context_id}\")\n\nasyncio.run(tab_identification())\n```\n\n## Advanced Tab Features\n\n### Bringing Tabs to Front\n\nMake a specific tab visible (bring to foreground):\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def bring_to_front():\n    async with Chrome() as browser:\n        tab1 = await browser.start()\n        tab2 = await browser.new_tab('https://github.com')\n        tab3 = await browser.new_tab('https://stackoverflow.com')\n        \n        # tab3 is currently in front (last created)\n        await asyncio.sleep(2)\n        \n        # Bring tab1 to front\n        await tab1.bring_to_front()\n        print(\"Tab 1 brought to front\")\n        \n        await asyncio.sleep(2)\n        \n        # Bring tab2 to front\n        await tab2.bring_to_front()\n        print(\"Tab 2 brought to front\")\n\nasyncio.run(bring_to_front())\n```\n\n### Tab-Specific Network Monitoring\n\nEach tab can independently monitor its own network activity:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def tab_network_monitoring():\n    async with Chrome() as browser:\n        # Use initial tab for monitored navigation\n        tab1 = await browser.start()\n        await tab1.go_to('https://example.com')\n        \n        # Create second tab without monitoring\n        tab2 = await browser.new_tab('https://github.com')\n        \n        # Enable network monitoring only on tab1\n        await tab1.enable_network_events()\n        \n        # Navigate both tabs\n        await tab1.go_to('https://example.com/page1')\n        await tab2.go_to('https://github.com/explore')\n        \n        await asyncio.sleep(3)\n        \n        # Get network logs only from tab1\n        tab1_logs = await tab1.get_network_logs()\n        print(f\"Tab 1 network requests: {len(tab1_logs)}\")\n        \n        # tab2 has no network monitoring\n        print(f\"Tab 2 network events enabled: {tab2.network_events_enabled}\")  # False\n\nasyncio.run(tab_network_monitoring())\n```\n\n### Tab-Specific Event Handlers\n\nRegister different event handlers on different tabs:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.page.events import PageEvent\n\nasync def tab_specific_events():\n    async with Chrome() as browser:\n        # Use initial tab as first tab\n        tab1 = await browser.start()\n        tab2 = await browser.new_tab()\n        \n        # Enable page events on both\n        await tab1.enable_page_events()\n        await tab2.enable_page_events()\n        \n        # Different handlers for each tab\n        async def tab1_handler(event):\n            print(\"Tab 1 loaded!\")\n        \n        async def tab2_handler(event):\n            print(\"Tab 2 loaded!\")\n        \n        await tab1.on(PageEvent.LOAD_EVENT_FIRED, tab1_handler)\n        await tab2.on(PageEvent.LOAD_EVENT_FIRED, tab2_handler)\n        \n        # Navigate both tabs\n        await tab1.go_to('https://example.com')\n        await tab2.go_to('https://github.com')\n        \n        await asyncio.sleep(2)\n\nasyncio.run(tab_specific_events())\n```\n\n## Performance Considerations\n\n| Scenario | Resource Impact | Recommendation |\n|----------|----------------|----------------|\n| **1-5 tabs** | Low | Direct management, no special handling |\n| **5-20 tabs** | Moderate | Use semaphores to limit concurrency |\n| **20-50 tabs** | High | Batch processing, close tabs aggressively |\n| **50+ tabs** | Very High | Consider sequential processing or multiple browsers |\n\n### Memory Usage\n\nEach tab consumes approximately:\n\n- **Base memory**: 50-100 MB\n- **With network events**: +10-20 MB\n- **With DOM events**: +20-50 MB\n- **Complex page (SPA)**: +100-300 MB\n\nFor 20 tabs with network monitoring: ~1.5-3 GB of memory.\n\n## Common Patterns\n\n### Sequential Processing with Single Tab\n\n```python\nasync def sequential_pattern():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        for url in urls:\n            await tab.go_to(url)\n            # Extract data\n            await tab.clear_callbacks()  # Clean up events\n\nasyncio.run(sequential_pattern())\n```\n\n### Parallel Processing with Multiple Tabs\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def parallel_pattern():\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n        'https://example.com/page4',\n    ]\n\n    async with Chrome() as browser:\n        # Start browser and get initial tab\n        initial_tab = await browser.start()\n        # Create one tab per URL (reusing initial tab for the first)\n        tabs = [initial_tab] + [await browser.new_tab() for _ in urls[1:]]\n\n        async def process_page(tab, url):\n            \"\"\"Process a single page within the given tab.\"\"\"\n            try:\n                await tab.go_to(url)\n                await asyncio.sleep(2)\n                title = await tab.evaluate('document.title')\n                print(f\"[{url}] {title}\")\n            finally:\n                if tab is not initial_tab:\n                    await tab.close()\n\n        # Run all tabs concurrently\n        await asyncio.gather(*[\n            process_page(tab, url) for tab, url in zip(tabs, urls)\n        ])\n\nasyncio.run(parallel_pattern())\n```\n\n### Worker Pool Pattern\n\n```python\nasync def worker_pool_pattern():\n    async with Chrome() as browser:\n        # Use initial tab as first worker\n        initial_tab = await browser.start()\n        \n        # Create additional worker tabs (5 workers total: 1 initial + 4 new)\n        workers = [initial_tab] + [await browser.new_tab() for _ in range(4)]\n        \n        # Distribute work across all workers\n        for url in urls:\n            worker = workers[urls.index(url) % len(workers)]\n            await worker.go_to(url)\n            # Process...\n        \n        # Cleanup all workers (including initial tab)\n        for worker in workers:\n            await worker.close()\n\nasyncio.run(worker_pool_pattern())\n```\n\n!!! tip \"Reusing the Initial Tab\"\n    Always use the tab returned by `browser.start()` instead of letting it sit idle. This saves browser resources and improves performance. In the examples above, the initial tab is reused as the first worker or for the first URL in the batch.\n\n## See Also\n\n- **[Browser Contexts](contexts.md)** - Isolated browser sessions\n- **[Cookies & Sessions](cookies-sessions.md)** - Managing cookies across tabs\n- **[Event System](../advanced/event-system.md)** - Tab-specific event handling\n- **[Concurrent Scraping](../../features.md#concurrent-scraping)** - Real-world examples\n\nMulti-tab management in Pydoll provides the foundation for building scalable, efficient browser automation. By understanding the tab lifecycle, singleton pattern, and best practices, you can create robust automation workflows that handle complex multi-page scenarios with ease.\n"
  },
  {
    "path": "docs/en/features/configuration/browser-options.md",
    "content": "# Browser Options (ChromiumOptions)\n\n`ChromiumOptions` is your central configuration hub for customizing browser behavior. It controls everything from command-line arguments and binary location to page load states and content preferences.\n\n!!! info \"Related Documentation\"\n    - **[Browser Preferences](browser-preferences.md)** - Deep dive into Chromium's internal preference system\n    - **[Browser Management](../browser-management/tabs.md)** - Working with browser instances and tabs\n    - **[Contexts](../browser-management/contexts.md)** - Isolated browsing contexts\n\n## Quick Start\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\nasync def main():\n    # Create and configure options\n    options = ChromiumOptions()\n    \n    # Basic configuration\n    options.headless = True\n    options.start_timeout = 15\n    options.page_load_state = PageLoadState.INTERACTIVE\n    \n    # Add command-line arguments\n    options.add_argument('--disable-gpu')\n    options.add_argument('--window-size=1920,1080')\n    \n    # Helper methods for common settings\n    options.block_notifications = True\n    options.block_popups = True\n    options.set_default_download_directory('/tmp/downloads')\n    \n    # Use the configured options\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n\nasyncio.run(main())\n```\n\n## Core Properties\n\n### Command-Line Arguments\n\nChromium supports hundreds of command-line switches that control browser behavior at the deepest level. Use `add_argument()` to pass flags directly to the browser process.\n\n```python\noptions = ChromiumOptions()\n\n# Add single argument\noptions.add_argument('--disable-blink-features=AutomationControlled')\n\n# Add argument with value\noptions.add_argument('--window-size=1920,1080')\noptions.add_argument('--user-agent=Mozilla/5.0 ...')\n\n# Remove argument if needed\noptions.remove_argument('--window-size=1920,1080')\n\n# Get all arguments\nall_args = options.arguments\n```\n\n!!! tip \"Argument Format\"\n    - Arguments starting with `--` are flags: `--headless`, `--disable-gpu`\n    - Arguments with `=` have values: `--window-size=1920,1080`\n    - Some accept multiple values: `--disable-features=Feature1,Feature2`\n\n**See [Command-Line Arguments Reference](#command-line-arguments-reference) below for comprehensive lists.**\n\n### Binary Location\n\nSpecify a custom browser executable instead of using the system default:\n\n```python\noptions = ChromiumOptions()\n\n# Linux\noptions.binary_location = '/opt/google/chrome-beta/chrome'\n\n# macOS\noptions.binary_location = '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'\n\n# Windows\noptions.binary_location = r'C:\\Program Files\\Google\\Chrome Beta\\Application\\chrome.exe'\n```\n\n!!! info \"When to Set Binary Location\"\n    - Testing different Chrome versions (Stable, Beta, Canary)\n    - Using Chromium instead of Chrome\n    - Using portable browser installations\n    - Running specific builds for debugging\n\n### Start Timeout\n\nControl how long Pydoll waits for the browser to start and respond:\n\n```python\noptions = ChromiumOptions()\noptions.start_timeout = 20  # seconds (default: 10)\n```\n\n!!! warning \"Timeout Considerations\"\n    - **Too low**: Browser may not fully initialize, causing startup failures\n    - **Too high**: Hangs will block your automation for longer\n    - **Recommended**: 10-15s for most cases, 20-30s for slow systems or heavy browser profiles\n\n### Headless Mode\n\nRun the browser without a visible UI:\n\n```python\noptions = ChromiumOptions()\noptions.headless = True  # Automatically adds --headless argument\n\n# Or manually\noptions.add_argument('--headless')\noptions.add_argument('--headless=new')  # New headless mode (Chrome 109+)\n```\n\n| Mode | Argument | Description |\n|------|----------|-------------|\n| **Headful** | (none) | Visible browser window (default) |\n| **Classic Headless** | `--headless` | Legacy headless mode |\n| **New Headless** | `--headless=new` | Modern headless (Chrome 109+, better compatibility) |\n\n!!! tip \"New Headless Mode\"\n    The `--headless=new` mode (Chrome 109+) provides better compatibility with modern web features and is harder to detect. Use it for production automation.\n\n### Page Load State\n\nControl when `tab.go_to()` considers a page \"loaded\":\n\n```python\nfrom pydoll.constants import PageLoadState\n\noptions = ChromiumOptions()\noptions.page_load_state = PageLoadState.INTERACTIVE  # or PageLoadState.COMPLETE\n```\n\n| State | When Navigation Completes | Use Case |\n|-------|---------------------------|----------|\n| `COMPLETE` (default) | `load` event fired, all resources loaded | Wait for images, fonts, scripts |\n| `INTERACTIVE` | `DOMContentLoaded` fired, DOM ready | Faster navigation, interact with DOM immediately |\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\nasync def compare_load_states():\n    # Complete mode - waits for everything\n    options_complete = ChromiumOptions()\n    options_complete.page_load_state = PageLoadState.COMPLETE\n    \n    async with Chrome(options=options_complete) as browser:\n        tab = await browser.start()\n        \n        import time\n        start = time.time()\n        await tab.go_to('https://example.com')\n        complete_time = time.time() - start\n        print(f\"COMPLETE mode: {complete_time:.2f}s\")\n    \n    # Interactive mode - DOM ready is enough\n    options_interactive = ChromiumOptions()\n    options_interactive.page_load_state = PageLoadState.INTERACTIVE\n    \n    async with Chrome(options=options_interactive) as browser:\n        tab = await browser.start()\n        \n        start = time.time()\n        await tab.go_to('https://example.com')\n        interactive_time = time.time() - start\n        print(f\"INTERACTIVE mode: {interactive_time:.2f}s\")\n\nasyncio.run(compare_load_states())\n```\n\n!!! tip \"When to Use INTERACTIVE\"\n    Use `INTERACTIVE` when:\n    \n    - You only need DOM access, not images/fonts\n    - Scraping text content and structure\n    - Speed is critical\n    - The page has many slow-loading resources\n    \n    Stick with `COMPLETE` (default) when:\n    \n    - Taking screenshots (need images loaded)\n    - Waiting for JavaScript-heavy apps to fully initialize\n    - Testing page load performance\n\n## Command-Line Arguments Reference\n\nChromium supports hundreds of command-line switches. Below are the most useful for automation, organized by category.\n\n!!! info \"Full Reference\"\n    Complete list of all Chromium switches: [Peter Beverloo's Chromium Command Line Switches](https://peter.sh/experiments/chromium-command-line-switches/)\n\n### Performance & Resource Management\n\nOptimize browser performance for faster automation:\n\n```python\noptions = ChromiumOptions()\n\n# Disable GPU acceleration (headless, Docker, CI/CD)\noptions.add_argument('--disable-gpu')\noptions.add_argument('--disable-software-rasterizer')\n\n# Reduce memory usage\noptions.add_argument('--disable-dev-shm-usage')  # Docker: overcome /dev/shm size limit\noptions.add_argument('--disable-extensions')\noptions.add_argument('--disable-background-networking')\n\n# Disable unnecessary features\noptions.add_argument('--disable-sync')  # Google account sync\noptions.add_argument('--disable-translate')\noptions.add_argument('--disable-background-timer-throttling')\noptions.add_argument('--disable-backgrounding-occluded-windows')\noptions.add_argument('--disable-renderer-backgrounding')\n\n# Network optimizations\noptions.add_argument('--disable-features=NetworkPrediction')\noptions.add_argument('--dns-prefetch-disable')\n\n# Window and rendering\noptions.add_argument('--window-size=1920,1080')\noptions.add_argument('--window-position=0,0')\noptions.add_argument('--force-device-scale-factor=1')\n```\n\n| Argument | Effect | When to Use |\n|----------|--------|-------------|\n| `--disable-gpu` | No GPU acceleration | Headless, Docker, servers without GPU |\n| `--disable-dev-shm-usage` | Use `/tmp` instead of `/dev/shm` | Docker containers with small shared memory |\n| `--disable-extensions` | Don't load any extensions | Clean, fast browser for automation |\n| `--window-size=W,H` | Set initial window dimensions | Screenshots, consistent viewport |\n| `--force-device-scale-factor=1` | Disable high-DPI scaling | Consistent rendering across systems |\n\n### Stealth & Fingerprinting\n\nMake your automation harder to detect with these command-line arguments:\n\n| Argument | Purpose | Example |\n|----------|---------|---------|\n| `--disable-blink-features=AutomationControlled` | Remove `navigator.webdriver` flag | Essential for stealth |\n| `--user-agent=...` | Set realistic, common user agent | Match target region/device |\n| `--use-gl=swiftshader` | Software WebGL renderer | Avoid unique GPU fingerprints |\n| `--force-webrtc-ip-handling-policy=...` | Prevent WebRTC IP leaks | Use `disable_non_proxied_udp` |\n| `--lang=en-US` | Set browser language | Match target locale |\n| `--accept-lang=en-US,en;q=0.9` | Accept-Language header | Realistic language preferences |\n| `--tz=America/New_York` | Set timezone | Match target region |\n| `--no-first-run` | Skip first-run wizards | Cleaner automation |\n| `--no-default-browser-check` | Skip default browser prompt | Avoid UI interruptions |\n| `--disable-reading-from-canvas` | Canvas fingerprinting mitigation | Reduce uniqueness |\n| `--disable-features=AudioServiceOutOfProcess` | Audio fingerprinting mitigation | Reduce uniqueness |\n\n!!! warning \"Detection Arms Race\"\n    No single technique guarantees undetectability. Combine multiple strategies:\n    \n    1. **Command-line arguments** (this table)\n    2. **Browser preferences** - [Browser Preferences - Stealth & Fingerprinting](browser-preferences.md#stealth-fingerprinting)\n    3. **Human-like interactions** - [Human-Like Interactions](../automation/human-interactions.md)\n    4. **Good IP reputation** - Use residential proxies with clean history\n\n### Security & Privacy\n\nControl security features and privacy settings:\n\n```python\noptions = ChromiumOptions()\n\n# Sandbox (disable for Docker/CI only)\noptions.add_argument('--no-sandbox')  # SECURITY RISK - use only in controlled environments\noptions.add_argument('--disable-setuid-sandbox')\n\n# HTTPS/SSL\noptions.add_argument('--ignore-certificate-errors')  # Ignore SSL errors\noptions.add_argument('--ignore-ssl-errors')\noptions.add_argument('--allow-insecure-localhost')\n\n# Privacy\noptions.add_argument('--disable-features=Translate')\noptions.add_argument('--disable-sync')\noptions.add_argument('--incognito')  # Open in incognito mode\n\n# Permissions auto-grant (for testing)\noptions.add_argument('--use-fake-ui-for-media-stream')  # Auto-grant camera/mic\noptions.add_argument('--use-fake-device-for-media-stream')  # Use fake devices\n```\n\n!!! danger \"Sandbox Warnings\"\n    **`--no-sandbox` is a security risk!** Only use it when:\n    \n    - Running in Docker containers (sandbox conflicts with container isolation)\n    - CI/CD environments with restricted permissions\n    - You fully trust the content being loaded\n    \n    **Never** use `--no-sandbox` when:\n    \n    - Visiting untrusted websites\n    - Running user-submitted code\n    - In production environments with external input\n\n| Argument | Effect | Security Impact |\n|----------|--------|-----------------|\n| `--no-sandbox` | Disable Chrome sandbox | **HIGH RISK** - Allows code execution |\n| `--ignore-certificate-errors` | Skip SSL validation | **MEDIUM RISK** - MITM attacks possible |\n| `--incognito` | Private browsing mode | Safer - no persistent state |\n\n### Debugging & Development\n\nTools for debugging automation and development:\n\n```python\noptions = ChromiumOptions()\n\n# DevTools\noptions.add_argument('--auto-open-devtools-for-tabs')\n\n# Logging\noptions.add_argument('--enable-logging')\noptions.add_argument('--v=1')  # Verbosity level (0-3)\noptions.add_argument('--log-level=0')  # 0=INFO, 1=WARNING, 2=ERROR\n\n# Crash handling\noptions.add_argument('--disable-crash-reporter')\noptions.add_argument('--no-crash-upload')\n\n# Enable experimental features\noptions.add_argument('--enable-features=NetworkService,NetworkServiceInProcess')\noptions.add_argument('--enable-experimental-web-platform-features')\n\n# JavaScript debugging\noptions.add_argument('--js-flags=--expose-gc')  # Expose garbage collector\n```\n\n!!! tip \"Remote Debugging\"\n    Pydoll automatically manages the remote debugging port. To access Chrome DevTools:\n    \n    ```python\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Get the debugging port\n        port = browser._connection_port\n        print(f\"DevTools available at: http://localhost:{port}\")\n        \n        # Open this URL in your browser to access DevTools\n    ```\n    \n    **Do not** use `--remote-debugging-port` argument - it will conflict with Pydoll's internal management!\n\n### Display & Rendering\n\nControl how the browser renders content:\n\n```python\noptions = ChromiumOptions()\n\n# Viewport and window\noptions.add_argument('--window-size=1920,1080')\noptions.add_argument('--window-position=0,0')\noptions.add_argument('--start-maximized')\noptions.add_argument('--start-fullscreen')\n\n# High DPI displays\noptions.add_argument('--force-device-scale-factor=1')\noptions.add_argument('--high-dpi-support=1')\n\n# Color and rendering\noptions.add_argument('--force-color-profile=srgb')\noptions.add_argument('--disable-accelerated-2d-canvas')\noptions.add_argument('--disable-accelerated-video-decode')\n\n# Font rendering\noptions.add_argument('--font-render-hinting=none')\noptions.add_argument('--disable-font-subpixel-positioning')\n\n# Animations\noptions.add_argument('--disable-animations')\noptions.add_argument('--wm-window-animations-disabled')\n```\n\n| Argument | Effect | Use Case |\n|----------|--------|----------|\n| `--window-size=W,H` | Set window dimensions | Screenshots, consistent viewport |\n| `--start-maximized` | Open maximized window | UI testing, full-screen captures |\n| `--force-device-scale-factor=1` | Disable DPI scaling | Consistent rendering across systems |\n| `--disable-animations` | No CSS/UI animations | Faster testing, reduce flakiness |\n\n### Proxy Configuration\n\nConfigure proxies for all network traffic:\n\n```python\noptions = ChromiumOptions()\n\n# HTTP/HTTPS proxy\noptions.add_argument('--proxy-server=http://proxy.example.com:8080')\n\n# Authenticated proxy\noptions.add_argument('--proxy-server=http://user:pass@proxy.example.com:8080')\n\n# SOCKS proxy\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\n\n# Bypass proxy for specific hosts\noptions.add_argument('--proxy-bypass-list=localhost,127.0.0.1,*.local')\n\n# Proxy auto-config (PAC) file\noptions.add_argument('--proxy-pac-url=http://proxy.example.com/proxy.pac')\n```\n\n!!! info \"Proxy Authentication\"\n    For proxies requiring authentication, Pydoll automatically handles auth challenges when using the `--proxy-server` argument with credentials.\n    \n    See **[Request Interception](../network/interception.md)** for details on the Fetch domain interaction with proxies.\n\n## Helper Methods\n\n`ChromiumOptions` provides convenient methods for common configuration tasks:\n\n### Download Management\n\n```python\noptions = ChromiumOptions()\n\n# Set download directory\noptions.set_default_download_directory('/home/user/downloads')\n\n# Prompt for download location\noptions.prompt_for_download = True  # Ask user where to save\noptions.prompt_for_download = False  # Download silently (default)\n\n# Allow multiple automatic downloads\noptions.allow_automatic_downloads = True  # Allow without prompt\noptions.allow_automatic_downloads = False  # Block or ask (default)\n```\n\n### Content Blocking\n\n```python\noptions = ChromiumOptions()\n\n# Block pop-ups\noptions.block_popups = True  # Block (default in most cases)\noptions.block_popups = False  # Allow\n\n# Block notifications\noptions.block_notifications = True  # Block requests\noptions.block_notifications = False  # Allow sites to ask\n```\n\n### Privacy Controls\n\n```python\noptions = ChromiumOptions()\n\n# Password manager\noptions.password_manager_enabled = False  # Disable save password prompts\noptions.password_manager_enabled = True  # Enable (default)\n\n# WebRTC leak protection (prevents real IP exposure through WebRTC)\noptions.webrtc_leak_protection = True  # Adds --force-webrtc-ip-handling-policy=disable_non_proxied_udp\noptions.webrtc_leak_protection = False  # Disable (default)\n```\n\n!!! tip \"WebRTC Leak Protection\"\n    WebRTC can leak your real IP address even when using a proxy. Enable `webrtc_leak_protection` to block non-proxied UDP connections, preventing STUN requests from bypassing your proxy. This is **essential** when using proxies for anonymity. See **[Network Fundamentals - WebRTC](../../deep-dive/network/network-fundamentals.md#webrtc-and-ip-leakage)** for details.\n\n### File Handling\n\n```python\noptions = ChromiumOptions()\n\n# PDF behavior\noptions.open_pdf_externally = True  # Download PDFs instead of viewing\noptions.open_pdf_externally = False  # View in browser (default)\n```\n\n### Internationalization\n\n```python\noptions = ChromiumOptions()\n\n# Accept languages (affects Content-Language header)\noptions.set_accept_languages('en-US,en;q=0.9,pt-BR;q=0.8')\n```\n\n## Complete Configuration Examples\n\n### Fast Scraping Configuration\n\nOptimized for speed and resource efficiency:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\ndef create_fast_scraping_options() -> ChromiumOptions:\n    \"\"\"Ultra-fast configuration for web scraping.\"\"\"\n    options = ChromiumOptions()\n    \n    # Headless for speed\n    options.headless = True\n    \n    # Faster page loads (DOM ready is enough for scraping)\n    options.page_load_state = PageLoadState.INTERACTIVE\n    \n    # Disable unnecessary features\n    options.add_argument('--disable-extensions')\n    options.add_argument('--disable-gpu')\n    options.add_argument('--disable-dev-shm-usage')\n    options.add_argument('--disable-background-networking')\n    options.add_argument('--disable-sync')\n    options.add_argument('--disable-translate')\n    \n    # Block content that slows down loading\n    options.block_notifications = True\n    options.block_popups = True\n    \n    # Disable images for even faster loading (if you don't need them)\n    options.add_argument('--blink-settings=imagesEnabled=false')\n    \n    # Network optimizations\n    options.add_argument('--disable-features=NetworkPrediction')\n    options.add_argument('--dns-prefetch-disable')\n    \n    return options\n\nasync def fast_scraping_example():\n    options = create_fast_scraping_options()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Blazingly fast navigation and scraping\n        urls = ['https://example.com', 'https://example.org', 'https://example.net']\n        \n        for url in urls:\n            await tab.go_to(url)\n            title = await tab.execute_script('return document.title')\n            print(f\"{url}: {title}\")\n\nasyncio.run(fast_scraping_example())\n```\n\n### Full Stealth Configuration\n\nFor maximum undetectability, combine command-line arguments with browser preferences:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\ndef create_full_stealth_options() -> ChromiumOptions:\n    \"\"\"Complete stealth configuration combining arguments and preferences.\"\"\"\n    options = ChromiumOptions()\n    \n    # ===== Command-Line Arguments =====\n    \n    # Core stealth\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    options.add_argument('--disable-features=IsolateOrigins,site-per-process')\n    \n    # User agent (use a recent, common one)\n    options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36')\n    \n    # Language and locale\n    options.add_argument('--lang=en-US')\n    options.add_argument('--accept-lang=en-US,en;q=0.9')\n    \n    # WebGL (software renderer to avoid unique GPU signatures)\n    options.add_argument('--use-gl=swiftshader')\n    options.add_argument('--disable-features=WebGLDraftExtensions')\n    \n    # WebRTC IP leak prevention\n    options.webrtc_leak_protection = True\n\n    # Permissions and first-run\n    options.add_argument('--no-first-run')\n    options.add_argument('--no-default-browser-check')\n    \n    # Window size (common resolution)\n    options.add_argument('--window-size=1920,1080')\n    \n    # ===== Browser Preferences =====\n    # For comprehensive browser preferences configuration, see:\n    # https://pydoll.tech/docs/features/configuration/browser-preferences/#stealth-fingerprinting\n    \n    return options\n\nasync def stealth_automation_example():\n    options = create_full_stealth_options()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Test on bot detection sites\n        await tab.go_to('https://bot.sannysoft.com')\n        await asyncio.sleep(5)\n        \n        # Your automation here...\n\nasyncio.run(stealth_automation_example())\n```\n\n!!! warning \"User-Agent Consistency is Critical\"\n    Setting `--user-agent` only changes the **HTTP header**, but detection systems also check `navigator.userAgent`, `navigator.platform`, `navigator.vendor`, and other JavaScript properties. **Inconsistencies between these values are a strong bot indicator.**\n    \n    For example, if your HTTP User-Agent says \"Windows\" but `navigator.platform` says \"Linux\", you'll be flagged immediately.\n    \n    **Solution**: You must also override JavaScript properties via CDP to maintain consistency. See **[Browser Fingerprinting - User-Agent Consistency](../../deep-dive/fingerprinting/browser-fingerprinting.md#user-agent-consistency)** for detailed explanation and implementation using `Page.addScriptToEvaluateOnNewDocument`.\n    \n    This is why comprehensive stealth requires both command-line arguments AND browser preferences configuration.\n\n!!! tip \"Complete Stealth Strategy\"\n    Command-line arguments are only part of the solution. For maximum stealth:\n    \n    1. **Use arguments above** (navigator.webdriver, WebGL, WebRTC)\n    2. **Configure browser preferences** - See [Browser Preferences - Stealth & Fingerprinting](browser-preferences.md#stealth-fingerprinting)\n    3. **Human-like interactions** - See [Human-Like Interactions](../automation/human-interactions.md)\n    4. **Good proxy/IP reputation** - Use residential proxies\n\n### Docker/CI Configuration\n\nFor containerized environments:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\ndef create_docker_options() -> ChromiumOptions:\n    \"\"\"Configuration for Docker containers and CI/CD.\"\"\"\n    options = ChromiumOptions()\n    \n    # Required for Docker\n    options.headless = True\n    options.add_argument('--no-sandbox')  # Sandbox conflicts with container isolation\n    options.add_argument('--disable-dev-shm-usage')  # Overcome /dev/shm size limit\n    \n    # Stability\n    options.add_argument('--disable-gpu')\n    options.add_argument('--disable-software-rasterizer')\n    \n    # Memory optimization\n    options.add_argument('--disable-extensions')\n    options.add_argument('--disable-background-networking')\n    \n    # Faster page loads for CI\n    options.page_load_state = PageLoadState.INTERACTIVE\n    \n    # Increase timeout for slow CI runners\n    options.start_timeout = 20\n    \n    # Crash handling\n    options.add_argument('--disable-crash-reporter')\n    \n    return options\n\nasync def ci_testing_example():\n    options = create_docker_options()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Run your tests...\n        await tab.go_to('https://example.com')\n        assert await tab.execute_script('return document.title') == 'Example Domain'\n\nasyncio.run(ci_testing_example())\n```\n\n## Troubleshooting\n\n### Browser Won't Start\n\n```python\n# Increase timeout\noptions.start_timeout = 30\n\n# Check binary location\noptions.binary_location = '/path/to/chrome'\n\n# Docker/CI issues\noptions.add_argument('--no-sandbox')\noptions.add_argument('--disable-dev-shm-usage')\n```\n\n### Slow Performance\n\n```python\n# Disable GPU if not needed\noptions.add_argument('--disable-gpu')\n\n# Disable images\noptions.add_argument('--blink-settings=imagesEnabled=false')\n\n# Use INTERACTIVE load state\noptions.page_load_state = PageLoadState.INTERACTIVE\n\n# Disable unnecessary features\noptions.add_argument('--disable-extensions')\noptions.add_argument('--disable-background-networking')\n```\n\n### Memory Issues in Docker\n\n```python\n# Essential for Docker\noptions.add_argument('--disable-dev-shm-usage')\n\n# Reduce memory footprint\noptions.add_argument('--disable-extensions')\noptions.add_argument('--disable-gpu')\noptions.add_argument('--single-process')  # Last resort (can be unstable)\n```\n\n## Further Reading\n\n- **[Browser Preferences](browser-preferences.md)** - Chromium's internal preference system\n- **[Stealth Automation](../automation/human-interactions.md)** - Human-like interactions\n- **[Contexts](../browser-management/contexts.md)** - Isolated browsing contexts\n- **[Network Interception](../network/interception.md)** - Request/response manipulation\n\n!!! tip \"Experimentation is Key\"\n    Browser configuration is highly dependent on your specific use case. Start with the examples here, then adjust based on your needs. Use `browser._connection_port` to access DevTools and inspect what's happening inside the browser.\n"
  },
  {
    "path": "docs/en/features/configuration/browser-preferences.md",
    "content": "# Custom Browser Preferences\n\nOne of Pydoll's most powerful features is direct access to Chromium's internal preference system. Unlike traditional browser automation tools that only expose a limited set of options, Pydoll gives you the same level of control that extensions and enterprise administrators have, allowing you to configure **any** browser setting available in Chromium's source code.\n\n## Why Browser Preferences Matter\n\nBrowser preferences control every aspect of how Chromium behaves:\n\n- **Performance**: Disable features you don't need for faster page loads\n- **Privacy**: Control what data the browser collects and sends\n- **Automation**: Remove user prompts and confirmations that break workflows\n- **Stealth**: Create realistic browser fingerprints to avoid detection\n- **Enterprise**: Apply policies typically only available through Group Policy\n\n!!! info \"The Power of Direct Access\"\n    Most automation tools only expose 10-20 common settings. Pydoll gives you access to **hundreds** of preferences, from download behavior to search suggestions, from network prediction to plugin management. If Chromium can do it, you can configure it.\n\n## Quick Start\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def preferences_example():\n    options = ChromiumOptions()\n    \n    # Set preferences using a dict\n    options.browser_preferences = {\n        'download': {\n            'default_directory': '/tmp/downloads',\n            'prompt_for_download': False\n        },\n        'profile': {\n            'default_content_setting_values': {\n                'notifications': 2  # Block notifications\n            }\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Downloads go to /tmp/downloads automatically\n        # No notification prompts will appear\n\nasyncio.run(preferences_example())\n```\n\n## Understanding Browser Preferences\n\n### What Are Preferences?\n\nChromium stores all user-configurable settings in a JSON file called `Preferences`, located in the browser's user data directory. This file contains **everything** from your homepage URL to whether images load automatically.\n\n**Typical location:**\n\n- **Linux**: `~/.config/google-chrome/Default/Preferences`\n- **macOS**: `~/Library/Application Support/Google/Chrome/Default/Preferences`\n- **Windows**: `%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Preferences`\n\n### Preferences File Structure\n\nThe Preferences file is a nested JSON object:\n\n```json\n{\n  \"download\": {\n    \"default_directory\": \"/home/user/Downloads\",\n    \"prompt_for_download\": true\n  },\n  \"profile\": {\n    \"default_content_setting_values\": {\n      \"notifications\": 1,\n      \"popups\": 0\n    },\n    \"password_manager_enabled\": true\n  },\n  \"search\": {\n    \"suggest_enabled\": true\n  },\n  \"net\": {\n    \"network_prediction_options\": 1\n  }\n}\n```\n\nEach dot-separated preference name in Chromium's source maps to a nested JSON path:\n\n- `download.default_directory` → `{'download': {'default_directory': ...}}`\n- `profile.password_manager_enabled` → `{'profile': {'password_manager_enabled': ...}}`\n\n### How Chromium Uses Preferences\n\nWhen Chromium starts:\n\n1. **Reads** the Preferences file from disk\n2. **Applies** these settings to configure browser behavior\n3. **Updates** the file when users change settings via UI\n4. **Falls back** to defaults if preferences are missing\n\nPydoll intercepts step 1 by pre-populating the Preferences file before the browser starts, ensuring your custom settings are applied from the very first page load.\n\n## How It Works in Pydoll\n\n### Setting Preferences\n\nUse the `browser_preferences` property to set any preference:\n\n```python\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\n\n# Direct assignment - merges with existing preferences\noptions.browser_preferences = {\n    'download': {'default_directory': '/tmp'},\n    'intl': {'accept_languages': 'pt-BR,en-US'}\n}\n\n# Multiple assignments are merged, not replaced\noptions.browser_preferences = {\n    'profile': {'password_manager_enabled': False}\n}\n\n# Both sets of preferences are now active\n```\n\n!!! warning \"Preferences Are Merged, Not Replaced\"\n    When you set `browser_preferences` multiple times, the new preferences are **merged** with existing ones. Only the specific keys you set are updated; everything else is preserved.\n    \n    ```python\n    options.browser_preferences = {'download': {'prompt': False}}\n    options.browser_preferences = {'profile': {'password_manager_enabled': False}}\n    \n    # Result: BOTH preferences are set\n    # {'download': {'prompt': False}, 'profile': {'password_manager_enabled': False}}\n    ```\n\n### Nested Path Syntax\n\nPreferences use nested dictionaries that mirror Chromium's dot-notation:\n\n```python\n# Chromium source code constant:\n# const char kDownloadDefaultDirectory[] = \"download.default_directory\";\n\n# Translates to Python dict:\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/path/to/downloads'\n    }\n}\n```\n\nThe deeper the nesting, the more specific the preference:\n\n```python\n# Top-level: profile\n# Second-level: default_content_setting_values  \n# Third-level: notifications\n\noptions.browser_preferences = {\n    'profile': {\n        'default_content_setting_values': {\n            'notifications': 2,  # Block\n            'geolocation': 2,    # Block\n            'media_stream': 2    # Block\n        }\n    }\n}\n```\n\n## Practical Use Cases\n\n### 1. Performance Optimization\n\nDisable resource-intensive features for faster automation:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def performance_optimized_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        # Disable network prediction and prefetching\n        'net': {\n            'network_prediction_options': 2  # 2 = Never predict\n        },\n        # Disable image loading\n        'profile': {\n            'default_content_setting_values': {\n                'images': 2  # 2 = Block, 1 = Allow\n            }\n        },\n        # Disable plugins\n        'webkit': {\n            'webprefs': {\n                'plugins_enabled': False\n            }\n        },\n        # Disable spell check\n        'browser': {\n            'enable_spellchecking': False\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Pages load 3-5x faster without images and unnecessary features\n        await tab.go_to('https://example.com')\n        print(\"Fast loading complete!\")\n\nasyncio.run(performance_optimized_browser())\n```\n\n!!! tip \"Performance Impact\"\n    Disabling images alone can reduce page load time by 50-70% for image-heavy sites. Combine with disabling prefetch, spell check, and plugins for maximum speed.\n\n### 2. Privacy & Anti-Tracking\n\nCreate a privacy-focused browser configuration:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def privacy_focused_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        # Enable Do Not Track\n        'enable_do_not_track': True,\n        \n        # Disable referrers\n        'enable_referrers': False,\n        \n        # Disable Safe Browsing (sends URLs to Google)\n        'safebrowsing': {\n            'enabled': False\n        },\n        \n        # Disable password manager\n        'profile': {\n            'password_manager_enabled': False\n        },\n        \n        # Disable autofill\n        'autofill': {\n            'enabled': False,\n            'profile_enabled': False\n        },\n        \n        # Disable search suggestions (sends queries to search engine)\n        'search': {\n            'suggest_enabled': False\n        },\n        \n        # Disable telemetry and metrics\n        'user_experience_metrics': {\n            'reporting_enabled': False\n        },\n        \n        # Block third-party cookies\n        'profile': {\n            'block_third_party_cookies': True,\n            'cookie_controls_mode': 1\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        print(\"Privacy-focused browser ready!\")\n\nasyncio.run(privacy_focused_browser())\n```\n\n### 3. Silent Downloads\n\nAutomate file downloads without user interaction:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def silent_download_automation():\n    download_dir = Path.home() / 'automation_downloads'\n    download_dir.mkdir(exist_ok=True)\n    \n    options = ChromiumOptions()\n    options.browser_preferences = {\n        'download': {\n            'default_directory': str(download_dir),\n            'prompt_for_download': False,\n            'directory_upgrade': True\n        },\n        'profile': {\n            'default_content_setting_values': {\n                'automatic_downloads': 1  # 1 = Allow, 2 = Block\n            }\n        },\n        # Always download PDFs instead of opening in viewer\n        'plugins': {\n            'always_open_pdf_externally': True\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/downloads')\n        \n        # Click download links - files save automatically\n        download_link = await tab.find(text='Download Report')\n        await download_link.click()\n        \n        await asyncio.sleep(3)\n        print(f\"File downloaded to: {download_dir}\")\n\nasyncio.run(silent_download_automation())\n```\n\n### 4. Block Intrusive UI Elements\n\nRemove popups, notifications, and prompts that break automation:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def clean_ui_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        'profile': {\n            'default_content_setting_values': {\n                'notifications': 2,      # Block notifications\n                'popups': 0,             # Block popups\n                'geolocation': 2,        # Block location requests\n                'media_stream': 2,       # Block camera/mic access\n                'media_stream_mic': 2,   # Block microphone\n                'media_stream_camera': 2 # Block camera\n            }\n        },\n        # Disable translation prompts\n        'translate': {\n            'enabled': False\n        },\n        # Disable save password prompt\n        'credentials_enable_service': False,\n        \n        # Disable \"Chrome is being controlled by automation\" infobar\n        'devtools': {\n            'preferences': {\n                'currentDockState': '\"undocked\"'\n            }\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        # No popups, no prompts, clean automation!\n\nasyncio.run(clean_ui_browser())\n```\n\n### 5. Internationalization & Localization\n\nConfigure language and locale preferences:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def localized_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        # Accept languages (priority order)\n        'intl': {\n            'accept_languages': 'pt-BR,pt,en-US,en'\n        },\n        \n        # Spellcheck languages\n        'spellcheck': {\n            'dictionaries': ['pt-BR', 'en-US']\n        },\n        \n        # Translate settings\n        'translate': {\n            'enabled': True\n        },\n        'translate_blocked_languages': ['en'],  # Don't offer to translate English\n        \n        # Default character encoding\n        'default_charset': 'UTF-8'\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        # Browser configured for Brazilian Portuguese\n\nasyncio.run(localized_browser())\n```\n\n## Helper Methods\n\nFor common scenarios, Pydoll provides convenience methods:\n\n```python\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\n\n# Download management\noptions.set_default_download_directory('/tmp/downloads')\noptions.prompt_for_download = False\noptions.allow_automatic_downloads = True\noptions.open_pdf_externally = True\n\n# Content blocking\noptions.block_notifications = True\noptions.block_popups = True\n\n# Privacy\noptions.password_manager_enabled = False\n\n# Internationalization\noptions.set_accept_languages('pt-BR,en-US,en')\n```\n\nThese methods are shortcuts that set the correct nested preferences for you:\n\n```python\n# This helper:\noptions.set_default_download_directory('/tmp')\n\n# Is equivalent to:\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/tmp'\n    }\n}\n```\n\n!!! tip \"Combine Helpers with Direct Preferences\"\n    Use helpers for common settings and `browser_preferences` for advanced configuration:\n    \n    ```python\n    # Start with helpers\n    options.block_notifications = True\n    options.prompt_for_download = False\n    \n    # Add advanced preferences\n    options.browser_preferences = {\n        'net': {'network_prediction_options': 2},\n        'webkit': {'webprefs': {'plugins_enabled': False}}\n    }\n    ```\n\n## Finding Preferences in Chromium Source\n\n### Source Code Reference\n\nChromium defines all preference constants in `pref_names.cc`:\n\n**Official source**: [chromium/src/+/main/chrome/common/pref_names.cc](https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.cc)\n\n### Reading the Source\n\nPreference constants use dot-notation that maps directly to nested dicts:\n\n```cpp\n// From Chromium source (pref_names.cc):\nconst char kDownloadDefaultDirectory[] = \"download.default_directory\";\nconst char kPromptForDownload[] = \"download.prompt_for_download\";\nconst char kSafeBrowsingEnabled[] = \"safebrowsing.enabled\";\nconst char kBlockThirdPartyCookies[] = \"profile.block_third_party_cookies\";\n```\n\n**Converts to Python:**\n\n```python\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/path/to/dir',\n        'prompt_for_download': False\n    },\n    'safebrowsing': {\n        'enabled': False\n    },\n    'profile': {\n        'block_third_party_cookies': True\n    }\n}\n```\n\n### Discovery Process\n\n1. **Search the source**: Go to [pref_names.cc](https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.cc)\n2. **Find your preference**: Search for keywords (e.g., \"download\", \"password\", \"notification\")\n3. **Note the constant name**: e.g., `kDownloadDefaultDirectory[] = \"download.default_directory\"`\n4. **Convert to dict**: Split by dots and create nested structure\n\n**Example - Finding notification preferences:**\n\n```cpp\n// Search for \"notification\" in pref_names.cc:\nconst char kPushMessagingAppIdentifierMap[] = \n    \"gcm.push_messaging_application_id_map\";\nconst char kDefaultNotificationsSetting[] = \n    \"profile.default_content_setting_values.notifications\";\n```\n\n```python\n# Becomes:\noptions.browser_preferences = {\n    'profile': {\n        'default_content_setting_values': {\n            'notifications': 2  # 2 = block, 1 = allow, 0 = ask\n        }\n    }\n}\n```\n\n### Common Preference Patterns\n\n| Category | Example Constant | Python Dict Path |\n|----------|-----------------|------------------|\n| Downloads | `download.default_directory` | `{'download': {'default_directory': ...}}` |\n| Content Settings | `profile.default_content_setting_values.X` | `{'profile': {'default_content_setting_values': {'X': ...}}}` |\n| Network | `net.network_prediction_options` | `{'net': {'network_prediction_options': ...}}` |\n| Privacy | `safebrowsing.enabled` | `{'safebrowsing': {'enabled': ...}}` |\n| Session | `session.restore_on_startup` | `{'session': {'restore_on_startup': ...}}` |\n\n!!! warning \"Undocumented Preferences\"\n    Not all preferences are documented. Some are:\n    \n    - **Experimental**: May change or be removed in future Chromium versions\n    - **Internal**: Used by Chromium's internal systems\n    - **Platform-specific**: Only work on certain operating systems\n    \n    Test thoroughly before relying on undocumented preferences.\n\n## Useful Preferences Reference\n\nHere's a curated list of interesting and useful preferences from Chromium's `pref_names.cc`:\n\n### Content & Media Settings\n\n```python\noptions.browser_preferences = {\n    'profile': {\n        'default_content_setting_values': {\n            # Content control (0=ask, 1=allow, 2=block)\n            'cookies': 1,                    # Allow cookies\n            'images': 1,                     # Allow images (2 to block)\n            'javascript': 1,                 # Allow JavaScript (2 to block)\n            'plugins': 2,                    # Block plugins (Flash, etc.)\n            'popups': 0,                     # Block popups\n            'geolocation': 2,                # Block location requests\n            'notifications': 2,              # Block notifications\n            'media_stream': 2,               # Block camera/microphone\n            'media_stream_mic': 2,           # Block microphone only\n            'media_stream_camera': 2,        # Block camera only\n            'automatic_downloads': 1,        # Allow automatic downloads\n            'midi_sysex': 2,                 # Block MIDI access\n            'clipboard': 1,                  # Allow clipboard access\n            'sensors': 2,                    # Block motion sensors\n            'usb_guard': 2,                  # Block USB device access\n            'serial_guard': 2,               # Block serial port access\n            'bluetooth_guard': 2,            # Block Bluetooth\n            'file_system_write_guard': 2,    # Block file system writes\n        }\n    }\n}\n```\n\n### Network & Performance\n\n```python\noptions.browser_preferences = {\n    'net': {\n        # Network prediction: 0=always, 1=wifi only, 2=never\n        'network_prediction_options': 2,\n        \n        # Quick check for server reachability\n        'quick_check_enabled': False\n    },\n    \n    # DNS prefetching\n    'dns_prefetching': {\n        'enabled': False  # Disable to reduce network traffic\n    },\n    \n    # Preconnect to search results\n    'search': {\n        'suggest_enabled': False,           # Disable search suggestions\n        'instant_enabled': False            # Disable instant results\n    },\n    \n    # Alternate error pages\n    'alternate_error_pages': {\n        'enabled': False  # Don't suggest alternatives for 404s\n    }\n}\n```\n\n### Download Preferences\n\n```python\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/path/to/downloads',\n        'prompt_for_download': False,\n        'directory_upgrade': True,\n        'extensions_to_open': '',           # File types to auto-open\n        'open_pdf_externally': True,        # Don't use internal PDF viewer\n    },\n    \n    'download_bubble': {\n        'partial_view_enabled': True        # Show download progress bubble\n    },\n    \n    'safebrowsing': {\n        'enabled': False  # Disable Safe Browsing download warnings\n    }\n}\n```\n\n### Privacy & Security\n\n```python\noptions.browser_preferences = {\n    # Do Not Track\n    'enable_do_not_track': True,\n    \n    # Referrers\n    'enable_referrers': False,\n    \n    # Safe Browsing\n    'safebrowsing': {\n        'enabled': False,                   # Disable Safe Browsing\n        'enhanced': False                   # Disable enhanced protection\n    },\n    \n    # Privacy Sandbox (Google's cookie replacement)\n    'privacy_sandbox': {\n        'apis_enabled': False,\n        'topics_enabled': False,\n        'fledge_enabled': False\n    },\n    \n    # Third-party cookies\n    'profile': {\n        'block_third_party_cookies': True,\n        'cookie_controls_mode': 1,          # Block third-party in incognito\n        \n        # Content settings\n        'default_content_setting_values': {\n            'cookies': 1,\n            'third_party_cookie_blocking_enabled': True\n        }\n    },\n    \n    # WebRTC (can leak real IP)\n    'webrtc': {\n        'ip_handling_policy': 'default_public_interface_only',\n        'multiple_routes_enabled': False,\n        'nonproxied_udp_enabled': False\n    }\n}\n```\n\n### Autofill & Passwords\n\n```python\noptions.browser_preferences = {\n    'autofill': {\n        'enabled': False,                   # Disable form autofill\n        'profile_enabled': False,           # Disable address autofill\n        'credit_card_enabled': False,       # Disable credit card autofill\n        'credit_card_fido_auth_enabled': False\n    },\n    \n    'profile': {\n        'password_manager_enabled': False,\n        'password_manager_leak_detection': False\n    },\n    \n    'credentials_enable_service': False,\n    'credentials_enable_autosignin': False\n}\n```\n\n### Browser Behavior & UI\n\n```python\nimport time\n\noptions.browser_preferences = {\n    # Homepage and startup\n    'homepage': 'https://www.google.com',\n    'homepage_is_newtabpage': False,\n    'newtab_page_location_override': 'https://www.google.com',\n    \n    'session': {\n        'restore_on_startup': 1,            # 0=new tab, 1=restore, 4=specific URLs, 5=new tab page\n        'startup_urls': ['https://www.google.com'],\n        'session_data_status': 3            # Session data status (internal)\n    },\n    \n    # Welcome page and window\n    'browser': {\n        'has_seen_welcome_page': True,      # Skip welcome screen\n        'window_placement': {\n            'bottom': 1032,                 # Window bottom position\n            'left': 2247,                   # Window left position\n            'right': 3192,                  # Window right position\n            'top': 31,                      # Window top position\n            'maximized': False,             # Window is maximized\n            'work_area_bottom': 1080,       # Screen work area bottom\n            'work_area_left': 1920,         # Screen work area left\n            'work_area_right': 3840,        # Screen work area right\n            'work_area_top': 0              # Screen work area top\n        }\n    },\n    \n    # Extensions\n    'extensions': {\n        'ui': {\n            'developer_mode': False\n        },\n        'alerts': {\n            'initialized': True\n        },\n        'theme': {\n            'system_theme': 2               # 0=default, 1=light, 2=dark\n        },\n        'last_chrome_version': '130.0.6723.91'  # Must match your version\n    },\n    \n    # Translate\n    'translate': {\n        'enabled': False                    # Disable translation prompts\n    },\n    'translate_blocked_languages': ['en'],  # Never translate English\n    'translate_site_blacklist': [],         # Legacy (use blocklist_with_time)\n    \n    # Bookmarks\n    'bookmark_bar': {\n        'show_on_all_tabs': False\n    },\n    \n    # Tabs\n    'tabs': {\n        'new_tab_position': 0               # 0=right, 1=after current\n    },\n    'pinned_tabs': [],                      # List of pinned tab URLs\n    \n    # New Tab Page (timestamps in Chrome format)\n    'NewTabPage': {\n        'PrevNavigationTime': str(int(time.time() * 1000000) + 11644473600000000)  # Chrome timestamp\n    },\n    'ntp': {\n        'num_personal_suggestions': 6       # Number of suggestions (0-10)\n    },\n    \n    # Toolbar customization\n    'toolbar': {\n        'pinned_chrome_labs_migration_complete': True\n    }\n}\n```\n\n!!! info \"Chrome Timestamp Format\"\n    Chrome uses Windows FILETIME format: microseconds since January 1, 1601 UTC.\n    \n    Convert Python timestamp:\n    ```python\n    import time\n    chrome_time = int(time.time() * 1000000) + 11644473600000000\n    ```\n\n### Spelling & Language\n\n```python\noptions.browser_preferences = {\n    'browser': {\n        'enable_spellchecking': False       # Disable spell check\n    },\n    \n    'spellcheck': {\n        'dictionaries': ['en-US', 'pt-BR'], # Spell check languages\n        'dictionary': '',                   # Legacy preference (keep empty)\n        'use_spelling_service': False       # Don't send to Google\n    },\n    \n    'intl': {\n        'accept_languages': 'pt-BR,pt,en-US,en',\n        'selected_languages': 'pt-BR,pt,en-US,en'  # Explicitly selected\n    },\n    \n    # Translation behavior and history\n    'translate': {\n        'enabled': True\n    },\n    'translate_accepted_count': {\n        'pt-BR': 0,\n        'es': 5                             # Accepted 5 Spanish translations\n    },\n    'translate_denied_count_for_language': {\n        'en': 10                            # Never translate English\n    },\n    'translate_ignored_count_for_language': {\n        'en': 1\n    },\n    'translate_site_blocklist_with_time': {},  # Sites never to translate\n    \n    # Accessibility caption language\n    'accessibility': {\n        'captions': {\n            'live_caption_language': 'pt-BR'\n        }\n    },\n    \n    # Language model counters (usage statistics)\n    'language_model_counters': {\n        'en': 2,                            # English word count\n        'pt': 10                            # Portuguese word count\n    }\n}\n```\n\n!!! info \"Language Model Counters\"\n    These counters track language usage statistics for Chrome's machine learning models:\n    \n    - Used for predicting user language preferences\n    - Affects search suggestions and autocomplete\n    - Higher counts indicate more frequent use\n    - Realistic values: 0-1000 for occasional use, 1000+ for heavy use\n\n### Accessibility\n\n```python\noptions.browser_preferences = {\n    'accessibility': {\n        'image_labels_enabled': False       # Don't get image labels from Google\n    },\n    \n    # Font settings\n    'webkit': {\n        'webprefs': {\n            'default_font_size': 16,\n            'default_fixed_font_size': 13,\n            'minimum_font_size': 0,\n            'minimum_logical_font_size': 6,\n            'fonts': {\n                'standard': {\n                    'Zyyy': 'Arial'\n                },\n                'serif': {\n                    'Zyyy': 'Times New Roman'\n                }\n            }\n        }\n    }\n}\n```\n\n### Media & Audio\n\n```python\noptions.browser_preferences = {\n    # Audio\n    'audio': {\n        'mute_enabled': False               # Start with audio on/off\n    },\n    \n    # Autoplay\n    'media': {\n        'autoplay_policy': 0,               # 0=allow, 1=user gesture, 2=document user activation\n        'video_fullscreen_orientation_lock': False\n    },\n    \n    # WebGL\n    'webkit': {\n        'webprefs': {\n            'webgl_enabled': True,          # Enable/disable WebGL\n            'webgl2_enabled': True\n        }\n    }\n}\n```\n\n### Printing\n\n```python\noptions.browser_preferences = {\n    'printing': {\n        'print_preview_sticky_settings': {\n            'appState': '{\\\"version\\\":2,\\\"recentDestinations\\\":[{\\\"id\\\":\\\"Save as PDF\\\",\\\"origin\\\":\\\"local\\\"}],\\\"marginsType\\\":3,\\\"customMargins\\\":{\\\"marginTop\\\":63,\\\"marginRight\\\":192,\\\"marginBottom\\\":240,\\\"marginLeft\\\":260}}'\n        }\n    },\n    \n    'savefile': {\n        'default_directory': '/tmp'         # Default save location for PDFs\n    }\n}\n```\n\n!!! tip \"Printing appState Format\"\n    The `appState` is a JSON-encoded string. For easier manipulation:\n    \n    ```python\n    import json\n    \n    app_state = {\n        'version': 2,\n        'recentDestinations': [{\n            'id': 'Save as PDF',\n            'origin': 'local'\n        }],\n        'marginsType': 3,                   # 0=default, 1=no margins, 2=minimum, 3=custom\n        'customMargins': {\n            'marginTop': 63,\n            'marginRight': 192,\n            'marginBottom': 240,\n            'marginLeft': 260\n        },\n        'isHeaderFooterEnabled': False,\n        'scaling': '100',\n        'scalingType': 3,                   # 0=default, 1=fit to page, 2=fit to paper, 3=custom\n        'isColorEnabled': True,\n        'isDuplexEnabled': False,\n        'isCssBackgroundEnabled': True,\n        'dpi': {\n            'horizontal_dpi': 300,\n            'vertical_dpi': 300,\n            'is_default': True\n        },\n        'mediaSize': {\n            'name': 'ISO_A4',\n            'width_microns': 210000,\n            'height_microns': 297000,\n            'custom_display_name': 'A4',\n            'is_default': True\n        }\n    }\n    \n    # Convert to string for appState\n    options.browser_preferences = {\n        'printing': {\n            'print_preview_sticky_settings': {\n                'appState': json.dumps(app_state)\n            }\n        }\n    }\n    ```\n\n### WebRTC & Peer-to-Peer\n\n```python\noptions.browser_preferences = {\n    'webrtc': {\n        # IP handling policy\n        'ip_handling_policy': 'default_public_interface_only',\n        \n        # UDP transport options\n        'udp_port_range': '10000-10100',    # Restrict UDP port range\n        \n        # Disable peer-to-peer\n        'multiple_routes_enabled': False,\n        'nonproxied_udp_enabled': False,\n        \n        # Text log collection\n        'text_log_collection_allowed': False\n    }\n}\n```\n\n### Site Isolation & Security\n\n```python\noptions.browser_preferences = {\n    # Site isolation\n    'site_isolation': {\n        'isolate_origins': '',              # Comma-separated origins to isolate\n        'site_per_process': True            # Full site isolation\n    },\n    \n    # Mixed content\n    'mixed_content': {\n        'auto_upgrade_enabled': True        # Upgrade HTTP to HTTPS\n    },\n    \n    # SSL/TLS\n    'ssl': {\n        'rev_checking': {\n            'enabled': True                 # Check certificate revocation\n        }\n    }\n}\n```\n\n### Installation & Country Metadata\n\n```python\nimport uuid\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    # Country ID at install (affects default settings and locale)\n    'countryid_at_install': 16978,          # Varies by country (e.g., 16978 for Brazil)\n    \n    # Default apps installation state\n    'default_apps_install_state': 3,        # 0=not installed, 1=installed, 3=migrated\n    \n    # Enterprise profile GUID (for managed browsers)\n    'enterprise_profile_guid': str(uuid.uuid4()),\n    \n    # Default search provider\n    'default_search_provider': {\n        'guid': ''                          # Empty for default (Google)\n    }\n}\n```\n\n!!! info \"Country ID Values\"\n    `countryid_at_install` is a numeric code representing the country where Chrome was first installed:\n    \n    - **16978**: Brazil (BR)\n    - **16965**: United States (US)\n    - **16967**: Great Britain (GB)\n    - **16966**: Germany (DE)\n    - **16972**: Japan (JP)\n    - And many others...\n    \n    This affects default language, currency, and regional settings. For realistic fingerprinting, match this to your target region.\n\n### Experimental Features\n\n```python\noptions.browser_preferences = {\n    # Chrome Labs experiments\n    'browser': {\n        'labs': {\n            'enabled': False\n        }\n    },\n    \n    # Preloading\n    'preload': {\n        'enabled': False                    # Disable page preloading\n    },\n    \n    # Smooth scrolling\n    'smooth_scrolling': {\n        'enabled': True\n    },\n    \n    # Hardware acceleration\n    'hardware_acceleration_mode': {\n        'enabled': True                     # Disable for headless performance\n    }\n}\n```\n\n### DevTools & Developer Options\n\n```python\noptions.browser_preferences = {\n    'devtools': {\n        'preferences': {\n            # DevTools appearance\n            'currentDockState': '\"right\"',              # \"bottom\", \"right\", \"undocked\"\n            'uiTheme': '\"dark\"',                        # \"dark\", \"light\", \"system\"\n            \n            # Console settings\n            'consoleTimestampsEnabled': 'true',\n            'preserveConsoleLog': 'true',\n            \n            # Network panel\n            'network.disableCache': 'false',\n            'network.color-code-resource-types': 'true',\n            'network-panel-split-view-state': '{\"vertical\":{\"size\":0}}',\n            \n            # Source maps\n            'cssSourceMapsEnabled': 'true',\n            'jsSourceMapsEnabled': 'true',\n            \n            # Elements panel\n            'elements.styles.sidebar.width': '{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}',\n            \n            # Inspector versioning\n            'inspectorVersion': '37',\n            \n            # Selected panel\n            'panel-selected-tab': '\"network\"',          # Last opened panel\n            \n            # Request info expanded categories\n            'request-info-general-category-expanded': 'true',\n            'request-info-request-headers-category-expanded': 'true',\n            'request-info-response-headers-category-expanded': 'true'\n        },\n        'synced_preferences_sync_disabled': {\n            'adorner-settings': '[{\"adorner\":\"grid\",\"isEnabled\":true},{\"adorner\":\"flex\",\"isEnabled\":true}]',\n            'syncedInspectorVersion': '37'\n        }\n    },\n    \n    # GCM (Google Cloud Messaging)\n    'gcm': {\n        'product_category_for_subtypes': 'com.chrome.linux'  # com.chrome.windows, com.chrome.macos\n    }\n}\n```\n\n!!! tip \"DevTools Preferences Format\"\n    DevTools preferences use a unique format where boolean and string values are stored as **JSON-encoded strings** (e.g., `'true'` not `True`, `'\"dark\"'` not `'dark'`). This is because DevTools settings are serialized directly to JSON.\n    \n    For complex objects, double-encode:\n    ```python\n    import json\n    \n    # Create the object\n    split_view = {'vertical': {'size': 0}}\n    \n    # Double-encode for DevTools\n    devtools_value = json.dumps(json.dumps(split_view))\n    # Result: '\"{\\\\\"vertical\\\\\":{\\\\\"size\\\\\":0}}\"'\n    ```\n\n### Sync & Sign-In Control\n\n```python\nimport time\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    'signin': {\n        'allowed': True,                        # Allow sign-in to Google\n        'cookie_clear_on_exit_migration_notice_complete': True\n    },\n    \n    'sync': {\n        'data_type_status_for_sync_to_signin': {\n            'bookmarks': False,\n            'history': False,\n            'passwords': False,\n            'preferences': False\n        },\n        'encryption_bootstrap_token_per_account_migration_done': True,\n        'passwords_per_account_pref_migration_done': True,\n        'feature_status_for_sync_to_signin': 5\n    },\n    \n    # Google services\n    'google': {\n        'services': {\n            'signin_scoped_device_id': '<your-device-id>'  # Generate unique ID\n        }\n    },\n    \n    # GAIA (Google Accounts Infrastructure)\n    'gaia_cookie': {\n        'changed_time': str(int(time.time())),\n        'hash': '',\n        'last_list_accounts_data': '[]'\n    }\n}\n```\n\n### Optimization & Performance Tracking\n\n```python\nimport time\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    # Optimization guide (Google's performance hints)\n    'optimization_guide': {\n        'hintsfetcher': {\n            'hosts_successfully_fetched': {}\n        },\n        'predictionmodelfetcher': {\n            'last_fetch_attempt': str(int(time.time())),\n            'last_fetch_success': str(int(time.time()))\n        },\n        'previously_registered_optimization_types': {}\n    },\n    \n    # History clusters (grouping related browsing)\n    'history_clusters': {\n        'all_cache': {\n            'all_keywords': {},\n            'all_timestamp': str(int(time.time()))\n        },\n        'last_selected_tab': 0,\n        'short_cache': {\n            'short_keywords': {},\n            'short_timestamp': '0'\n        }\n    },\n    \n    # Domain diversity metrics\n    'domain_diversity': {\n        'last_reporting_timestamp': str(int(time.time()))\n    },\n    \n    # Segmentation platform (user behavior analysis)\n    'segmentation_platform': {\n        'device_switcher_util': {\n            'result': {\n                'labels': ['NotSynced']\n            }\n        },\n        'last_db_compaction_time': str(int(time.time()))\n    },\n    \n    # Zero suggest (omnibox predictions)\n    'zerosuggest': {\n        'cachedresults': '',\n        'cachedresults_with_url': {}\n    }\n}\n```\n\n!!! info \"Performance Tracking Preferences\"\n    These preferences are typically used by Chrome to track and optimize performance. For automation, you can leave them empty or set realistic values to appear more like a normal browser.\n\n### Session Events & Crash Handling\n\nChrome tracks session history for recovery and telemetry:\n\n```python\nimport time\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    'sessions': {\n        'event_log': [\n            {\n                'crashed': False,\n                'time': str(int(time.time() * 1000000) + 11644473600000000),\n                'type': 0                   # 0=session start\n            },\n            {\n                'crashed': False,\n                'did_schedule_command': True,\n                'first_session_service': True,\n                'tab_count': 1,\n                'time': str(int(time.time() * 1000000) + 11644473600000000),\n                'type': 2,                  # 2=session data saved\n                'window_count': 1\n            }\n        ],\n        'session_data_status': 3            # 0=unknown, 1=no data, 2=some data, 3=full data\n    },\n    \n    # Profile exit type (important for fingerprinting)\n    'profile': {\n        'exit_type': 'Crashed'              # 'Normal', 'Crashed', 'SessionEnded'\n    }\n}\n```\n\n!!! warning \"Crashed vs Normal\"\n    Most real browsers **crash occasionally**. Always showing `'Normal'` exit is suspicious.\n    \n    **Realistic strategy**: Set `'Crashed'` for ~10-20% of profiles to simulate normal user experience. Ironically, having occasional \"crashes\" makes your automation look more human.\n\n!!! tip \"Session Event Types\"\n    - **Type 0**: Session start\n    - **Type 1**: Session ended normally\n    - **Type 2**: Session data saved (tabs, windows)\n    - **Type 3**: Session restored\n    \n    The `event_log` builds a history of browser sessions over time.\n\n## Stealth & Fingerprinting\n\nCreating a realistic browser fingerprint is crucial for avoiding bot detection systems. This section covers both basic and advanced techniques.\n\n### Quick Stealth Setup\n\nFor most use cases, this simple configuration provides good anti-detection:\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def quick_stealth():\n    options = ChromiumOptions()\n    \n    # Simulate a 60-day-old browser\n    fake_timestamp = int(time.time()) - (60 * 24 * 60 * 60)\n    \n    options.browser_preferences = {\n        # Fake usage history\n        'profile': {\n            'last_engagement_time': fake_timestamp,\n            'exited_cleanly': True,\n            'exit_type': 'Normal'\n        },\n        \n        # Realistic homepage\n        'homepage': 'https://www.google.com',\n        'session': {\n            'restore_on_startup': 1,\n            'startup_urls': ['https://www.google.com']\n        },\n        \n        # Enable features real users have\n        'enable_do_not_track': False,  # Most users don't enable this\n        'safebrowsing': {'enabled': True},\n        'autofill': {'enabled': True},\n        'search': {'suggest_enabled': True},\n        'dns_prefetching': {'enabled': True}\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://bot-detection-site.com')\n        print(\"Stealth mode activated!\")\n\nasyncio.run(quick_stealth())\n```\n\n!!! tip \"Key Stealth Principles\"\n    **Enable, don't disable**: Real users have Safe Browsing, autofill, and search suggestions enabled. Disabling everything looks suspicious.\n    \n    **Age your profile**: Fresh installs are a red flag. Simulate a browser that's been used for weeks or months.\n    \n    **Match the majority**: Use default settings that 90% of users have, not privacy-focused configurations.\n\n### Advanced Fingerprinting\n\nFor maximum realism, simulate detailed browser usage history:\n\n```python\nimport time\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\ndef create_realistic_browser() -> ChromiumOptions:\n    \"\"\"Create a browser with comprehensive fingerprinting resistance.\"\"\"\n    options = ChromiumOptions()\n    \n    # Timestamps\n    current_time = int(time.time())\n    install_time = current_time - (90 * 24 * 60 * 60)  # 90 days ago\n    last_use = current_time - (3 * 60 * 60)            # 3 hours ago\n    \n    options.browser_preferences = {\n        # Profile metadata (critical for fingerprinting)\n        'profile': {\n            'created_by_version': '130.0.6723.91',      # Must match your Chrome version\n            'creation_time': str(install_time),\n            'last_engagement_time': str(last_use),\n            'exit_type': 'Crashed',                     # 'Normal', 'Crashed', 'SessionEnded'\n            'name': 'Pessoa 1',                         # Realistic profile name\n            'avatar_index': 26,                         # 0-26 available avatars\n            \n            # Realistic content settings\n            'default_content_setting_values': {\n                'cookies': 1,\n                'images': 1,\n                'javascript': 1,\n                'popups': 0,\n                'notifications': 2,\n                'geolocation': 0,           # Ask (not block)\n                'media_stream': 0           # Ask (realistic)\n            },\n            \n            'password_manager_enabled': False,\n            'cookie_controls_mode': 0,\n            'content_settings': {\n                'pref_version': 1,\n                'enable_quiet_permission_ui': {\n                    'notifications': False\n                },\n                'enable_quiet_permission_ui_enabling_method': {\n                    'notifications': 1\n                }\n            },\n            \n            # Security metadata\n            'family_member_role': 'not_in_family',\n            'managed_user_id': '',\n            'were_old_google_logins_removed': True\n        },\n        \n        # Browser usage metadata\n        'browser': {\n            'has_seen_welcome_page': True,\n            'window_placement': {\n                'work_area_bottom': 1080,\n                'work_area_left': 0,\n                'work_area_right': 1920,\n                'work_area_top': 0\n            }\n        },\n        \n        # Installation metadata\n        'countryid_at_install': 16978,              # Varies by country\n        'default_apps_install_state': 3,\n        \n        # Extensions metadata\n        'extensions': {\n            'last_chrome_version': '130.0.6723.91',  # Must match your version\n            'alerts': {'initialized': True},\n            'theme': {'system_theme': 2}\n        },\n        \n        # Session activity (shows regular usage)\n        'in_product_help': {\n            'session_start_time': str(current_time),\n            'session_last_active_time': str(current_time),\n            'recent_session_start_times': [\n                str(current_time - (24 * 60 * 60)),\n                str(current_time - (48 * 60 * 60)),\n                str(current_time - (72 * 60 * 60))\n            ]\n        },\n        \n        # Session restore\n        'session': {\n            'restore_on_startup': 1,\n            'startup_urls': ['https://www.google.com']\n        },\n        \n        # Homepage\n        'homepage': 'https://www.google.com',\n        'homepage_is_newtabpage': False,\n        \n        # Translation history (shows multilingual usage)\n        'translate': {'enabled': True},\n        'translate_accepted_count': {'es': 2, 'fr': 1},\n        'translate_denied_count_for_language': {'en': 1},\n        \n        # Spell check\n        'spellcheck': {\n            'dictionaries': ['en-US', 'pt-BR'],\n            'dictionary': ''\n        },\n        \n        # Languages\n        'intl': {\n            'selected_languages': 'en-US,en,pt-BR'\n        },\n        \n        # Sign-in metadata\n        'signin': {\n            'allowed': True,\n            'cookie_clear_on_exit_migration_notice_complete': True\n        },\n        \n        # Safe Browsing (most users have this)\n        'safebrowsing': {\n            'enabled': True,\n            'enhanced': False\n        },\n        \n        # Autofill (common for real users)\n        'autofill': {\n            'enabled': True,\n            'profile_enabled': True\n        },\n        \n        # Search suggestions\n        'search': {'suggest_enabled': True},\n        \n        # DNS prefetch\n        'dns_prefetching': {'enabled': True},\n        \n        # Do NOT Track (usually off)\n        'enable_do_not_track': False,\n        \n        # WebRTC (default settings)\n        'webrtc': {\n            'ip_handling_policy': 'default',\n            'multiple_routes_enabled': True\n        },\n        \n        # Privacy Sandbox (Google's cookie replacement - realistic users have this)\n        'privacy_sandbox': {\n            'first_party_sets_data_access_allowed_initialized': True,\n            'm1': {\n                'ad_measurement_enabled': True,\n                'fledge_enabled': True,\n                'row_notice_acknowledged': True,\n                'topics_enabled': True\n            }\n        },\n        \n        # Media engagement\n        'media': {\n            'engagement': {'schema_version': 5}\n        },\n        \n        # Web apps\n        'web_apps': {\n            'did_migrate_default_chrome_apps': ['app-id'],\n            'last_preinstall_synchronize_version': '130'\n        }\n    }\n    \n    return options\n\n# Usage\nasync def advanced_stealth():\n    options = create_realistic_browser()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://advanced-bot-detection.com')\n        # Browser appears as a genuine 90-day-old installation\n```\n\n!!! warning \"Version Consistency is Critical\"\n    **Always match Chrome versions**: Ensure `profile.created_by_version` and `extensions.last_chrome_version` match your actual Chrome version. Mismatched versions are an instant red flag.\n    \n    ```python\n    # Get your Chrome version programmatically:\n    async with Chrome() as browser:\n        tab = await browser.start()\n        version = await browser.get_version()\n        chrome_version = version['product'].split('/')[1]  # e.g., '130.0.6723.91'\n        print(f\"Use this version: {chrome_version}\")\n    ```\n\n!!! info \"What Fingerprinting Preferences Do\"\n    **Profile age**: `creation_time` and `last_engagement_time` prove the browser isn't a fresh install.\n    \n    **Usage history**: `recent_session_start_times` shows regular browsing patterns.\n    \n    **Translation history**: `translate_accepted_count` indicates a real person using multiple languages.\n    \n    **Window placement**: Realistic screen dimensions that match actual monitor resolutions.\n    \n    **Privacy Sandbox**: Google's new tracking system. Disabling it is unusual and suspicious.\n\n## Performance Impact\n\nUnderstanding the performance implications of browser preferences helps you optimize for your specific use case:\n\n| Preference Category | Expected Impact | Use Case |\n|---------------------|----------------|----------|\n| Disable images | 50-70% faster loads | Scraping text content |\n| Disable prefetch | 10-20% faster loads | Reduce bandwidth usage |\n| Disable plugins | 5-10% faster loads | Security and performance |\n| Block notifications | Eliminates popups | Clean automation |\n| Silent downloads | Eliminates prompts | Automated file downloads |\n\n!!! tip \"Speed vs Stealth Trade-off\"\n    **For speed**: Disable images, prefetch, plugins, and spell check.\n    \n    **For stealth**: Enable Safe Browsing, autofill, search suggestions, and DNS prefetch (even though they slow things down).\n    \n    **Balanced approach**: Enable stealth features but disable images and plugins. This gives 40-50% speedup while maintaining realistic fingerprint.\n\n## See Also\n\n- **[Deep Dive: Browser Preferences](../../deep-dive/browser-preferences.md)** - Architectural details and internals\n- **[Page Load State](page-load-state.md)** - Control when pages are considered loaded\n- **[Proxy Configuration](proxy.md)** - Configure network proxies\n- **[Cookies & Sessions](../browser-management/cookies-sessions.md)** - Manage browser state\n- **[Chromium Source: pref_names.cc](https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.cc)** - Official preference constants\n- **[Chromium Source: pref_names.h](https://github.com/chromium/chromium/blob/main/chrome/common/pref_names.h)** - Header file with definitions\n\nCustom browser preferences give you unprecedented control over browser behavior, enabling sophisticated automation, performance optimization, and privacy configuration that simply isn't possible with traditional automation tools. This level of access transforms Pydoll from a simple automation library into a complete browser control system.\n"
  },
  {
    "path": "docs/en/features/configuration/proxy.md",
    "content": "# Proxy Configuration\n\nProxies are essential for professional web automation, enabling you to bypass rate limits, access geo-restricted content, and maintain anonymity. Pydoll provides native proxy support with automatic authentication handling.\n\n!!! info \"Related Documentation\"\n    - **[Browser Options](browser-options.md)** - Command-line proxy arguments\n    - **[Request Interception](../network/interception.md)** - How proxy authentication works internally\n    - **[Stealth Automation](../automation/human-interactions.md)** - Combine proxies with anti-detection\n    - **[Proxy Architecture Deep Dive](../../deep-dive/proxy-architecture.md)** - Network fundamentals, protocols, security, and building your own proxy\n\n## Why Use Proxies?\n\nProxies provide critical capabilities for automation:\n\n| Benefit | Description | Use Case |\n|---------|-------------|----------|\n| **IP Rotation** | Distribute requests across multiple IPs | Avoid rate limits, scrape at scale |\n| **Geographic Access** | Access region-locked content | Test geo-targeted features, bypass restrictions |\n| **Anonymity** | Hide your real IP address | Privacy-focused automation, competitor analysis |\n| **Load Distribution** | Spread traffic across multiple endpoints | High-volume scraping, stress testing |\n| **Ban Avoidance** | Prevent permanent IP bans | Long-running automation, aggressive scraping |\n\n!!! tip \"When to Use Proxies\"\n    **Always use proxies for:**\n    \n    - Production web scraping (>100 requests/hour)\n    - Accessing geo-restricted content\n    - Bypassing rate limits or IP-based blocks\n    - Testing from different regions\n    - Maintaining anonymity\n    \n    **You may skip proxies for:**\n    \n    - Local development and testing\n    - Internal/corporate automation\n    - Low-volume automation (<50 requests/day)\n    - When scraping your own infrastructure\n\n## Proxy Types\n\nDifferent proxy protocols serve different purposes:\n\n| Type | Port | Authentication | Speed | Security | Use Case |\n|------|------|----------------|-------|----------|----------|\n| **HTTP** | 80, 8080 | Optional | Fast | Low | Basic web scraping, non-sensitive data |\n| **HTTPS** | 443, 8443 | Optional | Fast | Medium | Secure web scraping, encrypted traffic |\n| **SOCKS5** | 1080, 1081 | Optional | Medium | High | Full TCP/UDP support, advanced use cases |\n\n### HTTP/HTTPS Proxies\n\nStandard web proxies, ideal for most automation tasks:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def http_proxy_example():\n    options = ChromiumOptions()\n    \n    # HTTP proxy (unencrypted)\n    options.add_argument('--proxy-server=http://proxy.example.com:8080')\n    \n    # Or HTTPS proxy (encrypted)\n    # options.add_argument('--proxy-server=https://proxy.example.com:8443')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # All traffic goes through proxy\n        await tab.go_to('https://httpbin.org/ip')\n        \n        # Verify proxy IP\n        ip = await tab.execute_script('return document.body.textContent')\n        print(f\"Current IP: {ip}\")\n\nasyncio.run(http_proxy_example())\n```\n\n**Pros:**\n\n- Fast and efficient\n- Wide support across services\n- Easy to configure\n\n**Cons:**\n\n- HTTP: No encryption (traffic visible to proxy)\n- Can be detected more easily than SOCKS5\n\n### SOCKS5 Proxies\n\nAdvanced proxies with full TCP/UDP support:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def socks5_proxy_example():\n    options = ChromiumOptions()\n    \n    # SOCKS5 proxy\n    options.add_argument('--proxy-server=socks5://proxy.example.com:1080')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://httpbin.org/ip')\n\nasyncio.run(socks5_proxy_example())\n```\n\n**Pros:**\n\n- Protocol-agnostic (works with any TCP/UDP traffic)\n- Better for advanced use cases (WebSockets, WebRTC)\n- More stealthy (harder to detect)\n\n**Cons:**\n\n- Slightly slower than HTTP/HTTPS\n- Less common in free/cheap proxy services\n\n!!! info \"SOCKS4 vs SOCKS5\"\n    **SOCKS5** is recommended over SOCKS4 because it:\n    \n    - Supports authentication (username/password)\n    - Handles UDP traffic (for WebRTC, DNS, etc.)\n    - Provides better error handling\n    \n    Use `socks5://` unless you specifically need SOCKS4 (`socks4://`).\n\n## Authenticated Proxies\n\nPydoll automatically handles proxy authentication without manual intervention.\n\n### How Authentication Works\n\nWhen you provide credentials in the proxy URL, Pydoll:\n\n1. **Intercepts the authentication challenge** using the Fetch domain\n2. **Automatically responds** with credentials\n3. **Continues navigation** sea@mlessly\n\nThis happens transparently, you don't need to handle authentication manually!\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def authenticated_proxy_example():\n    options = ChromiumOptions()\n    \n    # Proxy with authentication (username:password)\n    options.add_argument('--proxy-server=http://user:pass@proxy.example.com:8080')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Authentication handled automatically!\n        await tab.go_to('https://example.com')\n        print(\"Connected through authenticated proxy\")\n\nasyncio.run(authenticated_proxy_example())\n```\n\n!!! tip \"Credential Format\"\n    Include credentials directly in the proxy URL:\n\n    - HTTP: `http://username:password@host:port`\n    - HTTPS: `https://username:password@host:port`\n    - SOCKS5: `socks5://username:password@host:port`\n\n    Pydoll automatically extracts and uses these credentials.\n\n!!! warning \"SOCKS5 Authentication Limitation\"\n    **Chrome does not support SOCKS5 authentication natively** ([Chromium Issue #40323993](https://issues.chromium.org/issues/40323993)). Credentials embedded in `socks5://user:pass@host:port` are silently ignored — Chrome only sends a \"no authentication\" greeting to the SOCKS5 proxy.\n\n    This means Pydoll's automatic proxy auth (via `Fetch.authRequired`) **does not work for SOCKS5**, because Chrome never issues an HTTP 407 challenge for SOCKS5 connections.\n\n    **Workaround — Local proxy forwarder:**\n\n    Run a local SOCKS5 proxy (no auth) that forwards to the remote authenticated proxy. Pydoll provides a ready-to-use script for this:\n\n    ```python\n    import asyncio\n    from pydoll.utils import SOCKS5Forwarder\n    from pydoll.browser.chromium import Chrome\n    from pydoll.browser.options import ChromiumOptions\n\n    async def main():\n        forwarder = SOCKS5Forwarder(\n            remote_host='proxy.example.com',\n            remote_port=1080,\n            username='myuser',\n            password='mypass',\n            local_port=1081,\n        )\n        async with forwarder:\n            options = ChromiumOptions()\n            options.add_argument('--proxy-server=socks5://127.0.0.1:1081')\n\n            async with Chrome(options=options) as browser:\n                tab = await browser.start()\n                await tab.go_to('https://httpbin.org/ip')\n\n    asyncio.run(main())\n    ```\n\n    The forwarder handles the username/password handshake with the remote proxy while Chrome connects to localhost without authentication.\n\n    For the full technical explanation of why this happens, see **[SOCKS5 Authentication Deep Dive](../../deep-dive/network/socks-proxies.md#socks5-authentication-and-chrome)**.\n\n### Authentication Implementation Details\n\nPydoll uses Chrome's **Fetch domain** at the browser level to intercept and handle authentication challenges:\n\n```python\n# This is handled internally by Pydoll\n# You don't need to write this code!\n\nasync def _handle_proxy_auth(event):\n    \"\"\"Pydoll's internal proxy authentication handler.\"\"\"\n    if event['params']['authChallenge']['source'] == 'Proxy':\n        await browser.continue_request_with_auth(\n            request_id=event['params']['requestId'],\n            username='user',\n            password='pass'\n        )\n```\n\n!!! info \"Under the Hood\"\n    For technical details on how Pydoll intercepts and handles proxy authentication, see:\n    \n    - **[Request Interception](../network/interception.md)** - Fetch domain and request handling\n    - **[Event System](../advanced/event-system.md)** - Event-driven authentication\n\n!!! warning \"Fetch Domain Conflicts\"\n    When using **authenticated proxies** + **tab-level request interception**, be aware:\n    \n    - Pydoll enables Fetch at the **Browser level** for proxy auth\n    - If you enable Fetch at the **Tab level**, they share the same domain\n    - **Solution**: Call `tab.go_to()` once before enabling tab-level interception\n    \n    ```python\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # 1. First navigation triggers proxy auth (Browser-level Fetch)\n        await tab.go_to('https://example.com')\n        \n        # 2. Then enable tab-level interception safely\n        await tab.enable_fetch_events()\n        await tab.on('Fetch.requestPaused', my_interceptor)\n        \n        # 3. Continue with your automation\n        await tab.go_to('https://example.com/page2')\n    ```\n    \n    See [Request Interception - Proxy + Interception](../network/interception.md#private-proxy-request-interception-fetch) for details.\n\n## Proxy Bypass List\n\nExclude specific domains from using the proxy:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def proxy_bypass_example():\n    options = ChromiumOptions()\n    \n    # Use proxy for most traffic\n    options.add_argument('--proxy-server=http://proxy.example.com:8080')\n    \n    # But bypass proxy for these domains\n    options.add_argument('--proxy-bypass-list=localhost,127.0.0.1,*.local,internal.company.com')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Uses proxy\n        await tab.go_to('https://external-site.com')\n        \n        # Bypasses proxy (direct connection)\n        await tab.go_to('http://localhost:8000')\n        await tab.go_to('http://internal.company.com')\n\nasyncio.run(proxy_bypass_example())\n```\n\n**Bypass list patterns:**\n\n| Pattern | Matches | Example |\n|---------|---------|---------|\n| `localhost` | Localhost only | `http://localhost` |\n| `127.0.0.1` | Loopback IP | `http://127.0.0.1` |\n| `*.local` | All `.local` domains | `http://server.local` |\n| `internal.company.com` | Specific domain | `http://internal.company.com` |\n| `192.168.1.*` | IP range | `http://192.168.1.100` |\n\n!!! tip \"When to Use Bypass List\"\n    Bypass proxy for:\n    \n    - **Local development servers** (`localhost`, `127.0.0.1`)\n    - **Internal company resources** (VPN, intranet)\n    - **Testing environments** (`.local`, `.test` domains)\n    - **High-bandwidth resources** (when proxy is slow)\n\n## PAC (Proxy Auto-Config)\n\nUse a PAC file for complex proxy routing rules:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def pac_proxy_example():\n    options = ChromiumOptions()\n    \n    # Load PAC file from URL\n    options.add_argument('--proxy-pac-url=http://proxy.example.com/proxy.pac')\n    \n    # Or use local PAC file\n    # options.add_argument('--proxy-pac-url=file:///path/to/proxy.pac')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n\nasyncio.run(pac_proxy_example())\n```\n\n**Example PAC file:**\n\n```javascript\nfunction FindProxyForURL(url, host) {\n    // Direct connection for local addresses\n    if (isInNet(host, \"192.168.0.0\", \"255.255.0.0\") ||\n        isInNet(host, \"127.0.0.0\", \"255.0.0.0\")) {\n        return \"DIRECT\";\n    }\n    \n    // Use specific proxy for certain domains\n    if (dnsDomainIs(host, \".example.com\")) {\n        return \"PROXY proxy1.example.com:8080\";\n    }\n    \n    // Default proxy for everything else\n    return \"PROXY proxy2.example.com:8080\";\n}\n```\n\n!!! info \"PAC File Use Cases\"\n    PAC files are useful for:\n    \n    - **Complex routing rules** (domain-based, IP-based)\n    - **Proxy failover** (try multiple proxies)\n    - **Load balancing** (distribute across proxy pool)\n    - **Enterprise environments** (centralized proxy management)\n\n## Rotating Proxies\n\nRotate through multiple proxies for better distribution:\n\n```python\nimport asyncio\nfrom itertools import cycle\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def rotating_proxy_example():\n    # List of proxies\n    proxies = [\n        'http://user:pass@proxy1.example.com:8080',\n        'http://user:pass@proxy2.example.com:8080',\n        'http://user:pass@proxy3.example.com:8080',\n    ]\n    \n    # Cycle through proxies\n    proxy_pool = cycle(proxies)\n    \n    # Scrape multiple URLs with different proxies\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n    ]\n    \n    for url in urls:\n        # Get next proxy\n        proxy = next(proxy_pool)\n        \n        # Configure options with this proxy\n        options = ChromiumOptions()\n        options.add_argument(f'--proxy-server={proxy}')\n        \n        # Use proxy for this browser instance\n        async with Chrome(options=options) as browser:\n            tab = await browser.start()\n            await tab.go_to(url)\n            \n            title = await tab.execute_script('return document.title')\n            print(f\"[{proxy.split('@')[1]}] {url}: {title}\")\n\nasyncio.run(rotating_proxy_example())\n```\n\n!!! tip \"Proxy Rotation Strategies\"\n    **Per-browser rotation** (above):\n\n    - Each browser instance uses a different proxy\n    - Best for isolation and avoiding session conflicts\n    \n    **Per-request rotation**:\n\n    - More complex, requires request interception\n    - See [Request Interception](../network/interception.md) for implementation\n\n## Residential vs Datacenter Proxies\n\nUnderstanding proxy types helps you choose the right service:\n\n| Feature | Residential | Datacenter |\n|---------|------------|------------|\n| **IP Source** | Real residential ISPs | Data centers |\n| **Legitimacy** | High (real users) | Low (known ranges) |\n| **Detection Risk** | Very low | High |\n| **Speed** | Medium (150-500ms) | Very fast (<50ms) |\n| **Cost** | Expensive ($5-15/GB) | Cheap ($0.10-1/GB) |\n| **Best For** | Anti-bot sites, e-commerce | APIs, internal tools |\n\n### Residential Proxies\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def residential_proxy_example():\n    \"\"\"Use residential proxy for anti-bot sites.\"\"\"\n    options = ChromiumOptions()\n    \n    # Residential proxy with high trust score\n    options.add_argument('--proxy-server=http://user:pass@residential.proxy.com:8080')\n    \n    # Combine with stealth options\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Access protected site\n        await tab.go_to('https://protected-site.com')\n        print(\"Successfully accessed through residential proxy\")\n\nasyncio.run(residential_proxy_example())\n```\n\n**When to use Residential:**\n\n- Sites with strong anti-bot protection (Cloudflare, DataDome)\n- E-commerce scraping (Amazon, eBay, etc.)\n- Social media automation\n- Financial services\n- Any site that actively blocks datacenter IPs\n\n### Datacenter Proxies\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def datacenter_proxy_example():\n    \"\"\"Use fast datacenter proxy for APIs and unprotected sites.\"\"\"\n    options = ChromiumOptions()\n    \n    # Fast datacenter proxy\n    options.add_argument('--proxy-server=http://user:pass@datacenter.proxy.com:8080')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Fast API scraping\n        await tab.go_to('https://api.example.com/data')\n\nasyncio.run(datacenter_proxy_example())\n```\n\n**When to use Datacenter:**\n\n- Public APIs without rate limits\n- Internal/corporate automation\n- Sites without anti-bot measures\n- High-volume, speed-critical scraping\n- Development and testing\n\n!!! warning \"Proxy Quality Matters\"\n    **Bad proxies** cause more problems than they solve:\n    \n    - Slow response times (timeouts)\n    - Connection failures (error rates)\n    - Blacklisted IPs (immediate bans)\n    - Leaked real IP (privacy breach)\n    \n    **Invest in quality proxies** from reputable providers. Free proxies are almost never worth it.\n\n## Testing Your Proxy\n\nVerify proxy configuration before running production automation:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def test_proxy():\n    \"\"\"Test proxy connection and configuration.\"\"\"\n    proxy_url = 'http://user:pass@proxy.example.com:8080'\n    \n    options = ChromiumOptions()\n    options.add_argument(f'--proxy-server={proxy_url}')\n    \n    try:\n        async with Chrome(options=options) as browser:\n            tab = await browser.start()\n            \n            # Test 1: Connection\n            print(\"Testing proxy connection...\")\n            await tab.go_to('https://httpbin.org/ip', timeout=10)\n            \n            # Test 2: IP verification\n            print(\"Verifying proxy IP...\")\n            ip_response = await tab.execute_script('return document.body.textContent')\n            print(f\"[OK] Proxy IP: {ip_response}\")\n            \n            # Test 3: Geographic location (if available)\n            await tab.go_to('https://ipapi.co/json/')\n            geo_data = await tab.execute_script('return document.body.textContent')\n            print(f\"[OK] Geographic data: {geo_data}\")\n            \n            # Test 4: Speed test\n            import time\n            start = time.time()\n            await tab.go_to('https://example.com')\n            load_time = time.time() - start\n            print(f\"[OK] Load time: {load_time:.2f}s\")\n            \n            if load_time > 5:\n                print(\"[WARNING] Slow proxy response time\")\n            \n            print(\"\\n[SUCCESS] All proxy tests passed!\")\n            \n    except asyncio.TimeoutError:\n        print(\"[ERROR] Proxy connection timeout\")\n    except Exception as e:\n        print(f\"[ERROR] Proxy test failed: {e}\")\n\nasyncio.run(test_proxy())\n```\n\n## Further Reading\n\n- **[Proxy Architecture Deep Dive](../../deep-dive/proxy-architecture.md)** - Network fundamentals, TCP/UDP, HTTP/2/3, SOCKS5 internals, security analysis, and building your own proxy server\n- **[Browser Options](browser-options.md)** - Command-line arguments and configuration\n- **[Request Interception](../network/interception.md)** - How proxy authentication works\n- **[Browser Preferences](browser-preferences.md)** - Stealth and fingerprinting\n- **[Contexts](../browser-management/contexts.md)** - Using different proxies per context\n\n!!! tip \"Start Simple\"\n    Begin with a simple proxy setup, test thoroughly, then add complexity (rotation, retry logic, monitoring) as needed. Quality proxies are more important than complex rotation strategies.\n    \n    For those interested in understanding proxies at a deeper level, the **[Proxy Architecture Deep Dive](../../deep-dive/proxy-architecture.md)** provides comprehensive coverage of network protocols, security considerations, and even guides you through building your own proxy server.\n"
  },
  {
    "path": "docs/en/features/core-concepts.md",
    "content": "# Core Concepts\n\nUnderstanding what makes Pydoll different starts with its foundational design decisions. These aren't just technical choices, they directly impact how you write automation scripts, what problems you can solve, and how reliable your solutions will be.\n\n## Zero WebDrivers\n\nOne of Pydoll's most significant advantages is the complete elimination of WebDriver dependencies. If you've ever fought with \"chromedriver version doesn't match Chrome version\" errors or dealt with mysterious driver crashes, you'll appreciate this approach.\n\n### How It Works\n\nTraditional browser automation tools like Selenium rely on WebDriver executables that act as intermediaries between your code and the browser. Pydoll takes a different path by connecting directly to browsers through the Chrome DevTools Protocol (CDP).\n\n```mermaid\ngraph LR\n    %% Pydoll Flow\n    subgraph P[\"Pydoll Flow\"]\n        direction LR\n        P1[\"💻 Your Code\"] --> P2[\"🪄 Pydoll\"]\n        P2 --> P3[\"🌐 Browser (via CDP)\"]\n    end\n\n    %% Traditional Selenium Flow\n    subgraph S[\"Traditional Selenium Flow\"]\n        direction LR\n        S1[\"💻 Your Code\"] --> S2[\"🔌 WebDriver Client\"]\n        S2 --> S3[\"⚙️ WebDriver Executable\"]\n        S3 --> S4[\"🌐 Browser\"]\n    end\n\n```\n\nWhen you start a browser with Pydoll, here's what happens under the hood:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    # This creates a Browser instance\n    browser = Chrome()\n    \n    # start() launches Chrome with --remote-debugging-port\n    # and establishes a WebSocket connection to the CDP endpoint\n    tab = await browser.start()\n    \n    # Now you can control the browser through CDP commands\n    await tab.go_to('https://example.com')\n    \n    await browser.stop()\n\nasyncio.run(main())\n```\n\nBehind the scenes, `browser.start()` does the following:\n\n1. **Launches the browser process** with `--remote-debugging-port=<port>` flag\n2. **Waits for the CDP server** to become available on that port\n3. **Establishes a WebSocket connection** to `ws://localhost:<port>/devtools/...`\n4. **Returns a Tab instance** ready for automation\n\n!!! info \"Want to Know More?\"\n    For technical details on how the browser process is managed internally, see the [Browser Domain](../../deep-dive/browser-domain.md#browser-process-manager) deep dive.\n\n### Benefits You'll Notice\n\n**No Version Management Headaches**\n```python\n# With Selenium, you might see:\n# SessionNotCreatedException: This version of ChromeDriver only supports Chrome version 120\n\n# With Pydoll, you just need Chrome installed:\nasync with Chrome() as browser:\n    tab = await browser.start()  # Works with any Chrome version\n```\n\n**Simpler Setup**\n```bash\n# Selenium setup:\n$ pip install selenium\n$ brew install chromedriver  # or download, chmod +x, add to PATH...\n$ chromedriver --version     # does it match your Chrome?\n\n# Pydoll setup:\n$ pip install pydoll-python  # That's it!\n```\n\n**More Reliable**\n\nWithout WebDriver as a middle layer, there are fewer points of failure. Your code communicates directly with the browser through a well-defined protocol that Chromium developers themselves use and maintain.\n\n### CDP: The Protocol Behind the Magic\n\nThe Chrome DevTools Protocol isn't just for Pydoll; it's the same protocol that powers Chrome DevTools when you open the inspector. This means:\n\n- **Battle-tested reliability**: Used by millions of developers daily\n- **Rich capabilities**: Everything DevTools can do, Pydoll can do\n- **Active development**: Google maintains and evolves CDP continuously\n\n!!! tip \"Deep Dive: Understanding CDP\"\n    For a comprehensive understanding of how CDP works and why it's superior to WebDriver, see our [Chrome DevTools Protocol](../../deep-dive/cdp.md) deep dive.\n\n## Async-First Architecture\n\nPydoll isn't just async-compatible; it's designed from the ground up to leverage Python's `asyncio` framework. This isn't a checkbox feature; it's fundamental to how Pydoll achieves high performance.\n\n!!! info \"New to Async Programming?\"\n    If you're not familiar with Python's `async`/`await` syntax or asyncio concepts, we strongly recommend reading our [Understanding Async/Await](../../deep-dive/connection-layer.md#understanding-asyncawait) guide first. It explains the fundamentals with practical examples that will help you understand how Pydoll's async architecture works and why it's so powerful for browser automation.\n\n### Why Async Matters for Browser Automation\n\nBrowser automation involves a lot of waiting: pages loading, elements appearing, network requests completing. Traditional synchronous tools waste CPU time during these waits. Async architecture lets you do useful work while waiting.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_page(browser, url):\n    \"\"\"Scrape a single page.\"\"\"\n    tab = await browser.new_tab()\n    await tab.go_to(url)\n    title = await tab.execute_script('return document.title')\n    await tab.close()\n    return title\n\nasync def main():\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n    ]\n    \n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Process all URLs concurrently!\n        titles = await asyncio.gather(\n            *(scrape_page(browser, url) for url in urls)\n        )\n        \n        print(titles)\n\nasyncio.run(main())\n```\n\nIn this example, instead of scraping pages one after another (which might take 3 × 2 seconds = 6 seconds), all three pages are scraped concurrently, taking roughly 2 seconds total.\n\n### True Concurrency vs Threading\n\nUnlike threading-based approaches, Pydoll's async architecture provides true concurrent execution without the complexity of thread management:\n\n```mermaid\nsequenceDiagram\n    participant Main as Main Task\n    participant Tab1 as Tab 1\n    participant Tab2 as Tab 2\n    participant Tab3 as Tab 3\n    \n    Main->>Tab1: go_to(url1)\n    Main->>Tab2: go_to(url2)\n    Main->>Tab3: go_to(url3)\n    \n    Note over Tab1,Tab3: All tabs navigate concurrently\n    \n    Tab1-->>Main: Page 1 loaded\n    Tab2-->>Main: Page 2 loaded\n    Tab3-->>Main: Page 3 loaded\n    \n    Main->>Main: Process results\n```\n\n### Modern Python Patterns\n\nPydoll embraces modern Python idioms throughout:\n\n**Context Managers**\n```python\n# Automatic resource cleanup\nasync with Chrome() as browser:\n    tab = await browser.start()\n    # ... do work ...\n# Browser is automatically stopped when exiting context\n```\n\n**Async Context Managers for Operations**\n```python\n# Wait for and handle downloads\nasync with tab.expect_download(keep_file_at='/downloads') as dl:\n    await (await tab.find(text='Download PDF')).click()\n    pdf_data = await dl.read_bytes()\n```\n\n!!! tip \"Deep Dive\"\n    Want to understand how async operations work under the hood? Check out the [Connection Layer](../../deep-dive/connection-layer.md) deep dive for implementation details.\n\n### Performance Implications\n\nThe async-first design delivers measurable performance improvements:\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def benchmark_concurrent():\n    \"\"\"Scrape 10 pages concurrently.\"\"\"\n    async with Chrome() as browser:\n        await browser.start()\n        \n        start = time.time()\n        tasks = [\n            browser.new_tab(f'https://example.com/page{i}')\n            for i in range(10)\n        ]\n        await asyncio.gather(*tasks)\n        elapsed = time.time() - start\n        \n        print(f\"10 pages loaded in {elapsed:.2f}s\")\n        # Typical result: ~2-3 seconds vs 20+ seconds sequentially\n\nasyncio.run(benchmark_concurrent())\n```\n\n## Multi-Browser Support\n\nPydoll provides a unified API across all Chromium-based browsers. Write your automation once, run it anywhere.\n\n### Supported Browsers\n\n**Google Chrome**: Primary target with full feature support.\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n```\n\n**Microsoft Edge**: Full support including Edge-specific features.\n```python\nfrom pydoll.browser.chromium import Edge\n\nasync with Edge() as browser:\n    tab = await browser.start()\n```\n\n**Other Chromium Browsers**: Brave, Vivaldi, Opera, etc.\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.binary_location = '/path/to/brave-browser'  # or any Chromium browser\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n```\n\nThe key benefit: all Chromium-based browsers share the same API. Write your automation once, and it works across Chrome, Edge, Brave, or any other Chromium browser without code changes.\n\n### Cross-Browser Testing\n\nTest your automation across multiple browsers without changing code:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome, Edge\n\nasync def test_login(browser_class, browser_name):\n    \"\"\"Test login flow in a specific browser.\"\"\"\n    async with browser_class() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://app.example.com/login')\n        \n        await (await tab.find(id='username')).type_text('user@example.com')\n        await (await tab.find(id='password')).type_text('password123')\n        await (await tab.find(id='login-btn')).click()\n        \n        # Verify login success\n        success = await tab.find(id='dashboard', raise_exc=False)\n        print(f\"{browser_name} login: {'✓' if success else '✗'}\")\n\nasync def main():\n    # Test in both Chrome and Edge\n    await test_login(Chrome, \"Chrome\")\n    await test_login(Edge, \"Edge\")\n\nasyncio.run(main())\n```\n\n## Human-Like Behavior\n\nAutomated browsers are often detectable because they behave robotically. Pydoll includes built-in features to make interactions appear more human.\n\n### Natural Typing\n\nReal users don't type at perfectly consistent speeds. Pydoll's `type_text()` method includes randomized delays between keystrokes:\n\n```python\n# Type with human-like timing\nusername_field = await tab.find(id='username')\nawait username_field.type_text(\n    'user@example.com',\n    interval=0.1  # Average 100ms between keys, with randomization\n)\n\n# Faster typing (still human-like)\nawait username_field.type_text(\n    'user@example.com',\n    interval=0.05  # Faster but still varies\n)\n\n# Instant (robotic; use only when speed matters more than stealth)\nawait username_field.type_text(\n    'user@example.com',\n    interval=0\n)\n```\n\nThe `interval` parameter sets the average delay, but Pydoll adds random variance to make the timing more natural.\n\n### Realistic Clicking\n\nClicks aren't just \"fire and forget\". Pydoll automatically dispatches all mouse events that a real user would trigger:\n\n```python\nbutton = await tab.find(id='submit-button')\n\n# Default behavior: clicks center of element\n# Automatically fires: mouseover, mouseenter, mousemove, mousedown, mouseup, click\nawait button.click()\n\n# Click with offset (useful for avoiding detection on larger elements)\nawait button.click(offset_x=10, offset_y=5)\n```\n\n!!! info \"Mouse Events\"\n    Pydoll dispatches the complete sequence of mouse events in the correct order, simulating how real browsers handle user clicks. This makes clicks more realistic compared to simple JavaScript `.click()` calls.\n\n!!! warning \"Detection Considerations\"\n    While human-like behavior helps avoid basic bot detection, sophisticated anti-automation systems use many signals. Combine these features with:\n    \n    - Realistic browser fingerprints (via browser preferences)\n    - Proper proxy configuration\n    - Reasonable delays between actions\n    - Varied navigation patterns\n\n## Event-Driven Design\n\nUnlike traditional polling-based automation, Pydoll lets you react to browser events as they happen. This is more efficient and enables sophisticated interaction patterns.\n\n### Real-Time Event Monitoring\n\nSubscribe to browser events and execute callbacks when they fire:\n\n```python\nimport asyncio\nfrom functools import partial\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.page.events import PageEvent\nfrom pydoll.protocol.network.events import NetworkEvent\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # React to page load events\n        async def on_page_load(event):\n            print(f\"Page loaded: {await tab.current_url}\")\n        \n        await tab.enable_page_events()\n        await tab.on(PageEvent.LOAD_EVENT_FIRED, on_page_load)\n        \n        # Monitor network requests\n        async def on_request(tab, event):\n            url = event['params']['request']['url']\n            if '/api/' in url:\n                print(f\"API call: {url}\")\n        \n        await tab.enable_network_events()\n        await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, partial(on_request, tab))\n        \n        # Navigate and watch events fire\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)  # Let events process\n\nasyncio.run(main())\n```\n\n### Event Categories\n\nPydoll exposes several CDP event domains that you can subscribe to:\n\n| Domain | Example Events |\n|--------|----------------|\n| **Page Events** | Load completed, navigation, JavaScript dialogs |\n| **Network Events** | Request sent, response received, WebSocket activity |\n| **DOM Events** | DOM changes, attribute modifications |\n| **Fetch Events** | Request paused, authentication required |\n| **Runtime Events** | Console messages, exceptions |\n\n### Practical Event-Driven Patterns\n\n**Capture API Responses**\n```python\nimport json\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent\n\napi_data = []\n\nasync def capture_api(tab, event):\n    url = event['params']['response']['url']\n    if '/api/data' in url:\n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        api_data.append(json.loads(body))\n\nawait tab.enable_network_events()\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, partial(capture_api, tab))\n\n# Navigate and automatically capture API responses\nawait tab.go_to('https://app.example.com')\nawait asyncio.sleep(2)\n\nprint(f\"Captured {len(api_data)} API responses\")\n```\n\n**Wait for Specific Conditions**\n```python\nimport asyncio\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent\n\nasync def wait_for_api_call(tab, endpoint):\n    \"\"\"Wait for a specific API endpoint to be called.\"\"\"\n    event_occurred = asyncio.Event()\n    \n    async def check_endpoint(tab, event):\n        url = event['params']['request']['url']\n        if endpoint in url:\n            event_occurred.set()\n    \n    await tab.enable_network_events()\n    callback_id = await tab.on(\n        NetworkEvent.REQUEST_WILL_BE_SENT,\n        partial(check_endpoint, tab),\n        temporary=True  # Auto-remove after first trigger\n    )\n\n    await event_occurred.wait()\n    print(f\"API endpoint {endpoint} was called!\")\n\n# Usage\nawait wait_for_api_call(tab, '/api/users')\n```\n\n!!! info \"Deep Dive: Event System Details\"\n    For a comprehensive guide to event handling, callback patterns, and performance considerations, see the [Event System](../../deep-dive/event-system.md) deep dive.\n\n### Event Performance\n\nEvents are powerful but come with overhead. Best practices:\n\n```python\n# ✓ Good: Enable only what you need\nawait tab.enable_network_events()\n\n# ✗ Avoid: Enabling all events unnecessarily\nawait tab.enable_page_events()\nawait tab.enable_network_events()\nawait tab.enable_dom_events()\nawait tab.enable_fetch_events()\nawait tab.enable_runtime_events()\n\n# ✓ Good: Filter early in callbacks\nasync def handle_request(event):\n    url = event['params']['request']['url']\n    if '/api/' not in url:\n        return  # Skip non-API requests early\n    # Process API request...\n\n# ✓ Good: Disable when done\nawait tab.disable_network_events()\n```\n\n## Bringing It All Together\n\nThese core concepts work together to create a powerful automation framework:\n\n```python\nimport asyncio\nimport json\nfrom functools import partial\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import NetworkEvent\nfrom pydoll.constants import Keys\n\nasync def advanced_scraping():\n    \"\"\"Demonstrates multiple core concepts working together.\"\"\"\n    async with Chrome() as browser:  # Async context manager\n        tab = await browser.start()\n        \n        # Event-driven: Capture API data\n        api_responses = []\n        \n        async def capture_data(tab, event):\n            url = event['params']['response']['url']\n            if '/api/products' in url:\n                request_id = event['params']['requestId']\n                body = await tab.get_network_response_body(request_id)\n                api_responses.append(json.loads(body))\n        \n        await tab.enable_network_events()\n        await tab.on(NetworkEvent.RESPONSE_RECEIVED, partial(capture_data, tab))\n        \n        # Navigate with zero-webdriver simplicity\n        await tab.go_to('https://example.com/products')\n        \n        # Human-like interaction\n        search = await tab.find(id='search')\n        await search.type_text('laptop', interval=0.1)  # Natural typing\n        await search.press_keyboard_key(Keys.ENTER)\n        \n        # Wait for API responses (async efficiency)\n        await asyncio.sleep(2)\n        \n        print(f\"Captured {len(api_responses)} products from API\")\n        return api_responses\n\n# Multi-browser support: works with Chrome, Edge, etc.\nasyncio.run(advanced_scraping())\n```\n\nThese foundational concepts inform everything else in Pydoll. As you explore specific features, you'll see these principles in action, working together to create reliable, efficient, and maintainable browser automation.\n\n---\n\n## What's Next?\n\nNow that you understand Pydoll's core design, you're ready to explore specific features:\n\n- **[Element Finding](element-finding.md)** - Learn Pydoll's intuitive element location APIs\n- **[Network Features](../network/monitoring.md)** - Leverage the event system for network analysis\n- **[Browser Management](../browser-management/tabs.md)** - Use async patterns for concurrent operations\n\nFor deeper technical understanding, explore the [Deep Dive](../../deep-dive/index.md) section.\n"
  },
  {
    "path": "docs/en/features/element-finding.md",
    "content": "# Element Finding\n\nFinding elements on a web page is the foundation of browser automation. Pydoll introduces a revolutionary, intuitive approach that makes element location both more powerful and easier to use than traditional selector-based methods.\n\n## Why Pydoll's Approach is Different\n\nTraditional browser automation tools force you to think in terms of CSS selectors and XPath expressions from the start. Pydoll inverts this: you describe what you're looking for using natural HTML attributes, and Pydoll figures out the optimal selector strategy.\n\n```python\n# Traditional approach (other tools)\nelement = driver.find_element(By.XPATH, \"//input[@type='email' and @name='username']\")\n\n# Pydoll's approach\nelement = await tab.find(tag_name=\"input\", type=\"email\", name=\"username\")\n```\n\nBoth find the same element, but Pydoll's syntax is clearer, more maintainable, and less error-prone.\n\n### Element Finding Methods Overview\n\nPydoll offers three main approaches to find elements:\n\n| Method | Use When | Example |\n|--------|----------|---------|\n| **`find()`** | You know HTML attributes | `await tab.find(id=\"username\")` |\n| **`query()`** | You have CSS/XPath selector | `await tab.query(\"div.content\")` |\n| **Traversal** | You want to explore from a known element | `await element.get_children_elements()` |\n\n```mermaid\nflowchart LR\n    A[Need Element?] --> B{What do you have?}\n    B -->|HTML Attributes| C[find method]\n    B -->|CSS/XPath| D[query method]\n    B -->|Parent Element| E[Traversal]\n    \n    C --> F[WebElement]\n    D --> F\n    E --> G[List of WebElements]\n```\n\n!!! info \"Deep Dive: How It Works\"\n    Curious about how Pydoll implements element finding under the hood? Check out the [FindElements Mixin](../deep-dive/find-elements-mixin.md) documentation to learn about the architecture, performance optimizations, and internal selector strategies.\n\n## The find() Method: Natural Element Selection\n\nThe `find()` method is your primary tool for locating elements. It accepts common HTML attributes as parameters and automatically builds the most efficient selector.\n\n### Basic Usage\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def basic_finding():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Find by ID (most common and fastest)\n        username = await tab.find(id=\"username\")\n        \n        # Find by class name\n        submit_button = await tab.find(class_name=\"btn-primary\")\n        \n        # Find by tag name\n        first_paragraph = await tab.find(tag_name=\"p\")\n        \n        # Find by name attribute\n        email_field = await tab.find(name=\"email\")\n        \n        # Find by text content\n        login_link = await tab.find(text=\"Login\")\n\nasyncio.run(basic_finding())\n```\n\n### Combining Attributes for Precision\n\nThe real power of `find()` comes from combining multiple attributes to create precise selectors:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def precise_finding():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/form')\n        \n        # Combine tag name with type\n        password_input = await tab.find(tag_name=\"input\", type=\"password\")\n        \n        # Combine tag, class, and custom attributes\n        submit_button = await tab.find(\n            tag_name=\"button\",\n            class_name=\"btn\",\n            type=\"submit\"\n        )\n        \n        # Use data attributes\n        product_card = await tab.find(\n            tag_name=\"div\",\n            data_testid=\"product-card\",\n            data_category=\"electronics\"\n        )\n        \n        # Combine multiple conditions\n        specific_link = await tab.find(\n            tag_name=\"a\",\n            class_name=\"nav-link\",\n            href=\"/dashboard\"\n        )\n\nasyncio.run(precise_finding())\n```\n\n!!! info \"Combination Logic: AND\"\n    Combining attributes in `find()` works as an AND operation. The element must match **all** provided attributes.\n    \n    For more complex scenarios requiring OR logic—like finding an element that may have either an `id` or a different `name`—the correct approach is to chain multiple `find()` calls, as demonstrated in the \"Complete Example\" section.\n\n!!! tip \"Attribute Naming Convention\"\n    Use underscores for attribute names with hyphens. For example, `data-testid` becomes `data_testid`, and `aria-label` becomes `aria_label`. Pydoll automatically converts them to the correct format.\n\n### How find() Selects the Optimal Strategy\n\nPydoll automatically chooses the most efficient selector based on the attributes you provide:\n\n| Attributes Provided | Strategy Used | Performance |\n|---------------------|---------------|-------------|\n| Single: `id` | `By.ID` | ⚡ Fastest |\n| Single: `class_name` | `By.CLASS_NAME` | ⚡ Fast |\n| Single: `name` | `By.NAME` | ⚡ Fast |\n| Single: `tag_name` | `By.TAG_NAME` | ⚡ Fast |\n| Single: `text` | `By.XPATH` | ⚡ Fast |\n| Multiple attributes | XPath Expression | ✓ Efficient |\n\n```mermaid\nflowchart LR\n    A[find attributes] --> B{Single or Multiple?}\n    B -->|Single| C[Direct Selector]\n    B -->|Multiple| D[Build XPath]\n    C --> E[Fast Execution]\n    D --> E\n```\n\n### Finding Multiple Elements\n\nUse `find_all=True` to get a list of all matching elements:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def find_multiple():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/products')\n        \n        # Find all product cards\n        products = await tab.find(class_name=\"product-card\", find_all=True)\n        print(f\"Found {len(products)} products\")\n        \n        # Find all links in navigation\n        nav_links = await tab.find(\n            tag_name=\"a\",\n            class_name=\"nav-link\",\n            find_all=True\n        )\n        \n        # Process each element\n        for link in nav_links:\n            text = await link.text\n            href = await link.get_attribute(\"href\")\n            print(f\"Link: {text} → {href}\")\n\nasyncio.run(find_multiple())\n```\n\n### Waiting for Dynamic Elements\n\nModern web applications load content dynamically. Use `timeout` to wait for elements to appear:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def wait_for_elements():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/dashboard')\n        \n        # Wait up to 10 seconds for element to appear\n        dynamic_content = await tab.find(\n            class_name=\"dynamic-content\",\n            timeout=10\n        )\n        \n        # Wait for AJAX-loaded data\n        user_profile = await tab.find(\n            id=\"user-profile\",\n            timeout=15\n        )\n        \n        # Handle elements that might not appear\n        optional_banner = await tab.find(\n            class_name=\"promo-banner\",\n            timeout=3,\n            raise_exc=False  # Returns None if not found\n        )\n        \n        if optional_banner:\n            await optional_banner.click()\n        else:\n            print(\"No promotional banner present\")\n\nasyncio.run(wait_for_elements())\n```\n\n!!! warning \"Timeout Best Practices\"\n    Use reasonable timeout values. Too short and you'll miss slow-loading elements; too long and you'll waste time waiting for elements that don't exist. Start with 5-10 seconds for most dynamic content.\n\n## The query() Method: Direct Selector Access\n\nFor developers who prefer traditional selectors or need more complex selection logic, the `query()` method provides direct access to CSS selectors and XPath expressions.\n\n### CSS Selectors\n\nCSS selectors are fast, widely understood, and perfect for most use cases:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def css_selector_examples():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Simple selectors\n        main_nav = await tab.query(\"nav.main-menu\")\n        first_article = await tab.query(\"article:first-child\")\n        \n        # Attribute selectors\n        submit_button = await tab.query(\"button[type='submit']\")\n        required_inputs = await tab.query(\"input[required]\", find_all=True)\n        \n        # Complex selectors\n        nested = await tab.query(\"div.container > .content .item:nth-child(2)\")\n        \n        # Pseudo-classes\n        first_enabled_button = await tab.query(\"button:not([disabled])\")\n\nasyncio.run(css_selector_examples())\n```\n\n### XPath Expressions\n\nXPath excels at complex relationships and text matching:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def xpath_examples():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/table')\n        \n        # Text matching\n        button = await tab.query(\"//button[contains(text(), 'Submit')]\")\n        \n        # Navigate to parent\n        input_parent = await tab.query(\"//input[@name='email']/parent::div\")\n        \n        # Find sibling elements\n        label_input = await tab.query(\n            \"//label[text()='Email:']/following-sibling::input\"\n        )\n        \n        # Complex table queries\n        edit_button = await tab.query(\n            \"//tr[td[text()='John Doe']]//button[@class='btn-edit']\"\n        )\n\nasyncio.run(xpath_examples())\n```\n\n!!! info \"CSS vs XPath: Which to Use?\"\n    For a comprehensive guide on choosing between CSS selectors and XPath, including syntax references and real-world examples, see the [Selectors Guide](../deep-dive/selectors-guide.md).\n\n## DOM Traversal: Children and Siblings\n\nSometimes you need to explore the DOM tree from a known starting point. Pydoll provides dedicated methods for traversing element relationships.\n\n### DOM Tree Structure\n\nUnderstanding the DOM tree structure helps you choose the right traversal method:\n\n```mermaid\ngraph TB\n    Root[Document Root]\n    Root --> Container[div id='container']\n    \n    Container --> Child1[div class='card']\n    Container --> Child2[div class='card']\n    Container --> Child3[div class='card']\n    \n    Child1 --> GrandChild1[h2 title]\n    Child1 --> GrandChild2[p description]\n    Child1 --> GrandChild3[button action]\n    \n    Child2 --> GrandChild4[h2 title]\n    Child2 --> GrandChild5[p description]\n    \n    Child3 --> GrandChild6[h2 title]\n```\n\n### Getting Child Elements\n\nThe `get_children_elements()` method retrieves descendants of an element:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def traverse_children():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/cards')\n        \n        # Get container\n        container = await tab.find(id=\"cards-container\")\n        \n        # Get direct children only (max_depth=1)\n        direct_children = await container.get_children_elements(max_depth=1)\n        print(f\"Container has {len(direct_children)} direct children\")\n        \n        # Include grandchildren (max_depth=2)\n        descendants = await container.get_children_elements(max_depth=2)\n        print(f\"Found {len(descendants)} elements up to 2 levels deep\")\n        \n        # Filter by tag name\n        links = await container.get_children_elements(\n            max_depth=3,\n            tag_filter=[\"a\"]\n        )\n        print(f\"Found {len(links)} links in container\")\n        \n        # Combine filters for specific elements\n        nav_links = await container.get_children_elements(\n            max_depth=2,\n            tag_filter=[\"a\", \"button\"]\n        )\n\nasyncio.run(traverse_children())\n```\n\n### Getting Sibling Elements\n\nThe `get_siblings_elements()` method finds elements at the same level:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def traverse_siblings():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/list')\n        \n        # Find active item\n        active_item = await tab.find(class_name=\"item-active\")\n        \n        # Get all siblings (excluding active_item itself)\n        all_siblings = await active_item.get_siblings_elements()\n        print(f\"Active item has {len(all_siblings)} siblings\")\n        \n        # Filter siblings by tag\n        link_siblings = await active_item.get_siblings_elements(\n            tag_filter=[\"a\"]\n        )\n        \n        # Process sibling elements\n        for sibling in all_siblings:\n            text = await sibling.text\n            print(f\"Sibling: {text}\")\n\nasyncio.run(traverse_siblings())\n```\n\n!!! tip \"Performance Considerations\"\n    DOM traversal can be expensive for large trees. Prefer shallow `max_depth` values and specific `tag_filter` parameters to minimize the number of nodes processed. For deeply nested structures, consider multiple targeted `find()` calls instead of a single deep traversal.\n\n## Finding Elements Within Elements\n\nOnce you have an element, you can search within its scope using the same `find()` and `query()` methods.\n\n!!! warning \"Important: Search Depth Behavior\"\n    When you call `element.find()` or `element.query()`, Pydoll searches through **ALL descendants** (children, grandchildren, great-grandchildren, etc.), not just direct children. This is the standard behavior of `querySelector()` and matches what most developers expect.\n\n### Understanding Search Scope\n\n```mermaid\ngraph TB\n    Container[div id='container']\n    \n    Container --> Child1[div class='card' ✓]\n    Container --> Child2[div class='card' ✓]\n    Container --> Child3[div class='other']\n    \n    Child1 --> GrandChild1[div class='card' ✓]\n    Child1 --> GrandChild2[p class='text']\n    \n    Child3 --> GrandChild3[div class='card' ✓]\n    Child3 --> GrandChild4[div class='card' ✓]\n```\n\n```python\n# This finds ALL 5 elements with class='card' in the tree\n# (2 direct children + 3 nested descendants)\ncards = await container.find(class_name=\"card\", find_all=True)\nprint(len(cards))  # Output: 5\n```\n\n### Basic Scoped Search\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scoped_search():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/products')\n        \n        # Find a product container\n        product_card = await tab.find(class_name=\"product-card\")\n        \n        # Search within the product card (searches ALL descendants, returns only the first match)\n        product_title = await product_card.find(class_name=\"title\")\n        product_price = await product_card.find(class_name=\"price\")\n        add_button = await product_card.find(tag_name=\"button\", text=\"Add to Cart\")\n        \n        # Query within scope\n        product_image = await product_card.query(\"img.product-image\")\n        \n        # Find all items within a container (ALL descendants)\n        nav_menu = await tab.find(class_name=\"nav-menu\")\n        menu_items = await nav_menu.find(tag_name=\"li\", find_all=True)\n        \n        print(f\"Menu has {len(menu_items)} items\")\n\nasyncio.run(scoped_search())\n```\n\n### Finding Only Direct Children\n\nIf you need to find **only direct children** (depth 1), use CSS child combinator `>` or XPath:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def direct_children_only():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/cards')\n        \n        container = await tab.find(id=\"cards-container\")\n        \n        # Method 1: CSS child combinator (>)\n        # Finds ONLY direct children with class='card'\n        direct_cards = await container.query(\"> .card\", find_all=True)\n        print(f\"Direct children: {len(direct_cards)}\")\n        \n        # Method 2: XPath direct child\n        direct_divs = await container.query(\"./div[@class='card']\", find_all=True)\n        \n        # Method 3: Use get_children_elements() with max_depth=1\n        # (but this only filters by tag, not by other attributes)\n        direct_children = await container.get_children_elements(\n            max_depth=1,\n            tag_filter=[\"div\"]\n        )\n        \n        # Then filter manually by class\n        cards_only = [\n            child for child in direct_children\n            if 'card' in (await child.get_attribute('class') or '')\n        ]\n\nasyncio.run(direct_children_only())\n```\n\n### Comparison: find() vs get_children_elements()\n\n| Feature | `find()` / `query()` | `get_children_elements()` |\n|---------|---------------------|---------------------------|\n| **Search Depth** | ALL descendants | Configurable with `max_depth` |\n| **Filter By** | Any HTML attribute | Only tag name |\n| **Use Case** | Find specific elements anywhere in subtree | Explore DOM structure, get direct children |\n| **Performance** | Optimized for single attribute | Good for broad exploration |\n| **Parameter** | `tag_name=\"a\"` (string) | `tag_filter=[\"a\"]` (list) |\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def comparison_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        container = await tab.find(id=\"container\")\n        \n        # Scenario 1: I want ALL links anywhere in container\n        # Use find() - searches all descendants\n        all_links = await container.find(tag_name=\"a\", find_all=True)\n        \n        # Scenario 2: I want ONLY direct child links\n        # Use CSS child combinator\n        direct_links = await container.query(\"> a\", find_all=True)\n        \n        # Scenario 3: I want direct children with specific class\n        # Use CSS child combinator\n        direct_cards = await container.query(\"> .card\", find_all=True)\n        \n        # Scenario 4: I want to explore the DOM structure\n        # Use get_children_elements()\n        direct_children = await container.get_children_elements(max_depth=1)\n        \n        # Scenario 5: I want all descendants up to depth 2, filtered by tag\n        # Use get_children_elements()\n        shallow_links = await container.get_children_elements(\n            max_depth=2,\n            tag_filter=[\"a\"]\n        )\n\nasyncio.run(comparison_example())\n```\n\n!!! tip \"When to Use Each Method\"\n    - **Use `find()`**: When you know the attributes (class, id, etc.) and want to search the entire subtree\n    - **Use `query(\"> .class\")`**: When you need only direct children with specific attributes\n    - **Use `get_children_elements()`**: When exploring DOM structure or filtering by tag only\n\n### Common Use Cases\n\nThis scoped searching is incredibly useful for working with repeating patterns like:\n\n- Product cards in e-commerce sites\n- Table rows with multiple cells\n- Form sections with multiple fields\n- Navigation menus with nested items\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def practical_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/products')\n        \n        # Find all product cards on the page\n        product_cards = await tab.find(class_name=\"product-card\", find_all=True)\n        \n        for card in product_cards:\n            # Within each card, find ALL descendants with these classes\n            title = await card.find(class_name=\"product-title\")\n            price = await card.find(class_name=\"product-price\")\n            \n            # Get the button that's anywhere inside this card\n            buy_button = await card.find(tag_name=\"button\", text=\"Buy Now\")\n            \n            title_text = await title.text\n            price_text = await price.text\n            \n            print(f\"Product: {title_text}, Price: {price_text}\")\n            \n            # Click buy button\n            await buy_button.click()\n\nasyncio.run(practical_example())\n```\n\n\n## Shadow DOM Support\n\nMany modern web applications use [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) to encapsulate component internals. Pydoll provides seamless access to elements inside shadow trees through the `ShadowRoot` class.\n\n### How Shadow DOM Works\n\n```mermaid\ngraph TB\n    Host[\"div#my-component (shadow host)\"]\n    SR[\"ShadowRoot (open)\"]\n    Internal1[\"button.internal-btn\"]\n    Internal2[\"input.internal-input\"]\n\n    Host --> SR\n    SR --> Internal1\n    SR --> Internal2\n```\n\nElements inside a shadow root are hidden from regular DOM queries. You need to first access the shadow root, then search within it.\n\n### Accessing Shadow Roots\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def shadow_dom_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/web-components')\n\n        # Find the shadow host element\n        shadow_host = await tab.find(id='my-component')\n\n        # Access its shadow root\n        shadow_root = await shadow_host.get_shadow_root()\n\n        # Find elements inside the shadow root using query() with CSS selectors\n        button = await shadow_root.query('.internal-btn')\n        await button.click()\n\n        input_field = await shadow_root.query('input[type=\"email\"]')\n        await input_field.type_text('user@example.com')\n\nasyncio.run(shadow_dom_example())\n```\n\n!!! warning \"Use `query()` with CSS selectors inside shadow roots\"\n    `find()` and XPath are **not supported** on `ShadowRoot` and will raise `NotImplementedError`. Always use `query()` with CSS selectors to search inside shadow roots.\n\n### query() with CSS Selectors\n\n`ShadowRoot` supports `query()` with CSS selectors for element finding:\n\n```python\n# query() with CSS selectors\nelement = await shadow_root.query('#inner-id')\nelement = await shadow_root.query('button.primary')\n\n# Complex selectors\nelement = await shadow_root.query('div.container > .content')\n\n# find_all for multiple elements\nitems = await shadow_root.query('.item', find_all=True)\n\n# Waiting with timeout\nelement = await shadow_root.query('#dynamic', timeout=5)\n```\n\n### Nested Shadow Roots\n\nWeb components can contain other web components with their own shadow roots:\n\n```python\nasync def nested_shadow():\n    outer_host = await tab.find(tag_name='outer-component')\n    outer_shadow = await outer_host.get_shadow_root()\n\n    inner_host = await outer_shadow.query('inner-component')\n    inner_shadow = await inner_host.get_shadow_root()\n\n    deep_button = await inner_shadow.query('.deep-btn')\n    await deep_button.click()\n```\n\n### Finding Shadow Roots: find_shadow_roots()\n\nWhen you need to explore which shadow roots exist on a page (useful for debugging or dynamic pages like Cloudflare challenges), use `find_shadow_roots()`:\n\n```python\n# Find all shadow roots in the page\nshadow_roots = await tab.find_shadow_roots()\n\nfor sr in shadow_roots:\n    print(f'Mode: {sr.mode}, Host: {sr.host_element}')\n    # Search inside each shadow root\n    btn = await sr.query('button', raise_exc=False)\n    if btn:\n        await btn.click()\n```\n\n#### Waiting for Shadow Roots: `timeout`\n\nShadow hosts are often injected asynchronously (e.g., Cloudflare Turnstile loading inside an OOPIF). Use `timeout` to poll until shadow roots appear:\n\n```python\n# Wait up to 10 seconds for shadow roots to appear\nshadow_roots = await tab.find_shadow_roots(timeout=10)\n```\n\nThe `get_shadow_root()` method on elements also supports `timeout`:\n\n```python\n# Wait for a shadow root to be attached to an element\nhost = await tab.find(id='my-component', timeout=5)\nshadow = await host.get_shadow_root(timeout=5)\n```\n\n#### Deep Traversal: Cross-Origin IFrames (OOPIFs)\n\nBy default, `find_shadow_roots()` only traverses the main document's DOM tree (which includes same-origin iframes via `contentDocument` but **not** cross-origin iframes). Pass `deep=True` to also discover shadow roots inside cross-origin iframes (OOPIFs):\n\n```python\n# Include shadow roots from cross-origin iframes (e.g., Cloudflare Turnstile)\nshadow_roots = await tab.find_shadow_roots(deep=True, timeout=10)\n\nfor sr in shadow_roots:\n    print(f'Mode: {sr.mode}, Host: {sr.host_element}')\n    # Elements found inside these shadow roots automatically route\n    # CDP commands through the correct OOPIF session\n    btn = await sr.query('input[type=\"checkbox\"]', raise_exc=False)\n    if btn:\n        await btn.click()\n```\n\n!!! tip \"When to use `deep=True`\"\n    Use `deep=True` when automating pages with cross-origin embedded widgets such as Cloudflare Turnstile captchas, third-party payment forms, or social login buttons. These widgets typically use cross-origin iframes with closed shadow roots inside them.\n\n### Shadow Root Properties\n\n```python\nshadow_root = await element.get_shadow_root()\n\n# Check the shadow root mode (open, closed, or user-agent)\nprint(shadow_root.mode)  # ShadowRootType.OPEN\n\n# Access the host element\nhost = shadow_root.host_element\n\n# Get the shadow root inner HTML\nhtml = await shadow_root.inner_html\n```\n\n!!! note \"Closed Shadow Roots\"\n    Closed shadow roots (`mode='closed'`) are accessible via CDP since the protocol bypasses JavaScript restrictions. However, some browser-internal shadow roots (user-agent) may have limited accessibility.\n\n## Working with iFrames\n\n!!! info \"Complete IFrame Guide Available\"\n    This section covers basic iframe interaction for element finding. For a comprehensive guide including nested iframes, CAPTCHA handling, technical deep dives, and troubleshooting, see **[Working with IFrames](automation/iframes.md)**.\n\niFrames present a special challenge in browser automation because they have separate DOM contexts. Pydoll makes iframe interaction seamless:\n\n```mermaid\nflowchart TB\n    Main[tab]\n    Frame[\"iframe WebElement\"]\n    Content[\"elements inside iframe\"]\n\n    Main -->|\"find('iframe')\"| Frame\n    Frame -->|\"find('button#submit')\"| Content\n```\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def iframe_interaction():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/page-with-iframe')\n\n        iframe = await tab.query(\"iframe.embedded-content\", timeout=10)\n\n        # WebElement helpers run inside the iframe automatically\n        iframe_button = await iframe.find(tag_name=\"button\", class_name=\"submit\")\n        await iframe_button.click()\n\n        iframe_input = await iframe.find(id=\"captcha-input\")\n        await iframe_input.type_text(\"verification-code\")\n\n        # Nested iframe? Keep chaining\n        inner_iframe = await iframe.find(tag_name=\"iframe\")\n        download_link = await inner_iframe.find(text=\"Download PDF\")\n        await download_link.click()\n\nasyncio.run(iframe_interaction())\n```\n!!! note \"Screenshots in iframes\"\n    `tab.take_screenshot()` only works on the top-level target. Capture iframe content by targeting an element inside the frame and calling `element.take_screenshot()`.\n\n## Error Handling Strategies\n\nRobust automation requires handling cases where elements don't exist or take longer to appear than expected.\n\n### Element Finding Flow with Error Handling\n\n```mermaid\nflowchart TB\n    Start[Start Finding Element] --> Immediate[Try Immediate Find]\n    \n    Immediate --> Found1{Element Found?}\n    Found1 -->|Yes| Return1[Return WebElement]\n    Found1 -->|No & timeout=0| Check1{raise_exc=True?}\n    Found1 -->|No & timeout>0| Wait[Start Waiting Loop]\n    \n    Check1 -->|Yes| Error1[Raise ElementNotFound]\n    Check1 -->|No| ReturnNone[Return None]\n    \n    Wait --> Sleep[Wait 0.5 seconds]\n    Sleep --> TryAgain[Try Finding Again]\n    TryAgain --> Found2{Element Found?}\n    \n    Found2 -->|Yes| Return2[Return WebElement]\n    Found2 -->|No| TimeCheck{Timeout Exceeded?}\n    \n    TimeCheck -->|No| Sleep\n    TimeCheck -->|Yes| Check2{raise_exc=True?}\n    \n    Check2 -->|Yes| Error2[Raise WaitElementTimeout]\n    Check2 -->|No| ReturnNone2[Return None]\n```\n\n### Using raise_exc Parameter\n\nControl whether to raise an exception when elements aren't found:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.exceptions import ElementNotFound\n\nasync def error_handling():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Raise exception if not found (default behavior)\n        try:\n            critical_element = await tab.find(id=\"must-exist\")\n        except ElementNotFound:\n            print(\"Critical element missing! Cannot continue.\")\n            return\n        \n        # Return None if not found (optional elements)\n        optional_banner = await tab.find(\n            class_name=\"promo-banner\",\n            raise_exc=False\n        )\n        \n        if optional_banner:\n            print(\"Banner found, closing it\")\n            close_button = await optional_banner.find(class_name=\"close-btn\")\n            await close_button.click()\n        else:\n            print(\"No banner present, continuing\")\n\nasyncio.run(error_handling())\n```\n\n## Best Practices\n\n### 1. Prefer Stable Selectors\n\nUse attributes that are unlikely to change:\n\n```python\n# Good: Semantic attributes\nawait tab.find(id=\"user-profile\")  # IDs are usually stable\nawait tab.find(data_testid=\"submit-button\")  # Test IDs are designed for automation\nawait tab.find(name=\"username\")  # Form names are stable\n\n# Avoid: Structural dependencies\nawait tab.query(\"div > div > div:nth-child(3) > input\")  # Brittle, breaks easily\n```\n\n### 2. Use the Simplest Selector That Works\n\nStart simple and add complexity only when needed:\n\n```python\n# Good: Simple and clear\nawait tab.find(id=\"login-form\")\n\n# Unnecessary: Over-complicated\nawait tab.query(\"//div[@id='content']/descendant::form[@id='login-form']\")\n```\n\n### 3. Choose the Right Method\n\n- Use `find()` for simple attribute-based searches\n- Use `query()` for complex CSS or XPath patterns\n- Use traversal methods for exploring from known anchors\n\n```python\n# Use find() for straightforward cases\nusername = await tab.find(id=\"username\")\n\n# Use query() for complex patterns\nactive_nav_link = await tab.query(\"nav.menu a.active\")\n\n# Use traversal for relationship-based searches\ncontainer = await tab.find(id=\"cards\")\nchild_links = await container.get_children_elements(tag_filter=[\"a\"])\n```\n\n### 4. Add Meaningful Timeouts\n\nDon't use zero timeouts for dynamic content, and don't wait forever for optional elements:\n\n```python\n# Good: Reasonable timeouts\ncritical_data = await tab.find(id=\"data\", timeout=10)\noptional_popup = await tab.find(class_name=\"popup\", timeout=2, raise_exc=False)\n\n# Bad: No timeout for dynamic content\ndynamic_element = await tab.find(class_name=\"ajax-loaded\")  # Will fail immediately\n\n# Bad: Very long timeout for optional element\nbanner = await tab.find(class_name=\"ad-banner\", timeout=60)  # Wastes time\n```\n\n### 5. Handle Errors Gracefully\n\nPlan for elements that might not exist:\n\n```python\n# Critical elements: let exceptions bubble up\nsubmit_button = await tab.find(id=\"submit-btn\")\n\n# Optional elements: handle explicitly\ncookie_notice = await tab.find(class_name=\"cookie-notice\", raise_exc=False)\nif cookie_notice:\n    accept_button = await cookie_notice.find(text=\"Accept\")\n    await accept_button.click()\n```\n\n## Complete Example: Form Automation\n\nHere's a complete example combining multiple element finding techniques:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.exceptions import ElementNotFound\n\nasync def automate_registration_form():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        try:\n            # Navigate to registration page\n            await tab.go_to('https://example.com/register', timeout=10)\n            \n            # Handle optional cookie banner\n            cookie_banner = await tab.find(\n                class_name=\"cookie-banner\",\n                timeout=2,\n                raise_exc=False\n            )\n            if cookie_banner:\n                accept = await cookie_banner.find(text=\"Accept\")\n                await accept.click()\n                await asyncio.sleep(1)\n            \n            # Fill out the registration form\n            # Find form fields\n            username_field = await tab.find(name=\"username\", timeout=5)\n            email_field = await tab.find(name=\"email\")\n            password_field = await tab.find(type=\"password\", name=\"password\")\n            confirm_password = await tab.find(type=\"password\", name=\"confirm_password\")\n            \n            # Enter information\n            await username_field.type_text(\"john_doe_2024\", interval=0.1)\n            await email_field.type_text(\"john@example.com\", interval=0.1)\n            await password_field.type_text(\"SecurePass123!\", interval=0.1)\n            await confirm_password.type_text(\"SecurePass123!\", interval=0.1)\n            \n            # Find and check terms checkbox\n            # Try multiple strategies\n            terms_checkbox = await tab.find(id=\"terms\", raise_exc=False)\n            if not terms_checkbox:\n                terms_checkbox = await tab.find(name=\"accept_terms\", raise_exc=False)\n            if not terms_checkbox:\n                terms_checkbox = await tab.query(\"input[type='checkbox']\")\n            \n            await terms_checkbox.click()\n            \n            # Find and click submit button\n            submit_button = await tab.find(\n                tag_name=\"button\",\n                type=\"submit\",\n                timeout=2\n            )\n            await submit_button.click()\n            \n            # Wait for success message with longer timeout (form processing)\n            success_message = await tab.find(\n                class_name=\"success-message\",\n                timeout=15\n            )\n            \n            message_text = await success_message.text\n            print(f\"Registration successful: {message_text}\")\n            \n            # Verify redirect to dashboard\n            await asyncio.sleep(2)\n            current_url = await tab.current_url\n            \n            if \"dashboard\" in current_url:\n                print(\"Successfully redirected to dashboard\")\n                \n                # Find welcome message\n                welcome = await tab.find(class_name=\"welcome-message\", timeout=5)\n                welcome_text = await welcome.text\n                print(f\"Welcome message: {welcome_text}\")\n            else:\n                print(f\"Unexpected URL after registration: {current_url}\")\n                \n        except ElementNotFound as e:\n            print(f\"Element not found: {e}\")\n            # Take screenshot for debugging\n            await tab.take_screenshot(\"error_screenshot.png\")\n        except Exception as e:\n            print(f\"Unexpected error: {e}\")\n            await tab.take_screenshot(\"unexpected_error.png\")\n\nasyncio.run(automate_registration_form())\n```\n\n## Learn More\n\nWant to dive deeper into element finding?\n\n- **[FindElements Mixin Deep Dive](../deep-dive/find-elements-mixin.md)**: Learn about the architecture, internal selector strategies, and performance optimizations\n- **[Selectors Guide](../deep-dive/selectors-guide.md)**: Comprehensive guide to CSS selectors and XPath with syntax references and real-world examples\n- **[WebElement Domain](../deep-dive/webelement-domain.md)**: Understand what you can do with elements once you've found them\n\nElement finding is the foundation of successful browser automation. Master these techniques, and you'll be able to reliably locate any element on any web page, no matter how complex the structure.\n"
  },
  {
    "path": "docs/en/features/index.md",
    "content": "# Features Guide\n\nWelcome to Pydoll's comprehensive features documentation! This is where you'll discover everything that makes Pydoll a powerful and flexible browser automation tool. Whether you're just starting out or looking to leverage advanced capabilities, you'll find detailed guides, practical examples, and best practices for each feature.\n\n## What You'll Find Here\n\nThis guide is organized into logical sections that reflect your automation journey: from basic concepts to advanced techniques. Each page is designed to be self-contained, so you can jump directly to what interests you or follow along sequentially.\n\n## Core Concepts\n\nBefore diving into specific features, it's worth understanding what sets Pydoll apart. These foundational concepts inform how the entire library works.\n\n**[Core Concepts](core-concepts.md)**: Discover the architectural decisions that make Pydoll different: the zero-webdriver approach that eliminates compatibility headaches, the async-first design that enables true concurrent operations, and native support for multiple Chromium-based browsers.\n\n## Element Finding & Interaction\n\nFinding and interacting with page elements is the bread and butter of automation. Pydoll makes this surprisingly intuitive with modern APIs that just make sense.\n\n**[Element Finding](element-finding.md)**: Master Pydoll's element location strategies, from the intuitive `find()` method that uses natural HTML attributes, to the powerful `query()` method for CSS selectors and XPath. You'll also learn about DOM traversal helpers that let you navigate the page structure efficiently.\n\n## Automation Capabilities\n\nThese are the features that bring your automation to life: simulating user interactions, keyboard control, handling file operations, working with iframes, and capturing visual content.\n\n**[Human-Like Interactions](automation/human-interactions.md)**: Learn how to create interactions that feel genuinely human: typing with natural timing variations, clicking with realistic mouse movements, and using keyboard shortcuts just like a real user would. This is crucial for avoiding detection in automation-sensitive sites.\n\n**[Keyboard Control](automation/keyboard-control.md)**: Master keyboard interactions with comprehensive support for key combinations, modifiers, and special keys. Essential for forms, shortcuts, and accessibility testing.\n\n**[File Operations](automation/file-operations.md)**: File handling can be tricky in browser automation. Pydoll provides robust solutions for both uploads and downloads, with the `expect_download` context manager offering elegant handling of asynchronous download completion.\n\n**[IFrame Interaction](automation/iframes.md)**: Treat iframes like regular elements—find the iframe and keep searching inside it. No extra targets, no extra tabs.\n\n**[Screenshots & PDF](automation/screenshots-and-pdfs.md)**: Capture visual content from your automation sessions. Whether you need full-page screenshots for visual regression testing, element-specific captures for debugging, or PDF exports for archival, Pydoll has you covered.\n\n## Network Features\n\nPydoll's network capabilities are where it truly shines, giving you unprecedented visibility and control over HTTP traffic.\n\n**[Network Monitoring](network/monitoring.md)**: Observe and analyze all network activity in your browser session. Extract API responses, track request timing, identify failed requests, and understand exactly what data is being exchanged. Essential for debugging, testing, and data extraction.\n\n**[Request Interception](network/interception.md)**: Go beyond observation to actively modify network behavior. Block unwanted resources, inject custom headers, modify request payloads, or even fulfill requests with mock data. This is powerful for testing, optimization, and privacy control.\n\n**[Browser-Context HTTP Requests](network/http-requests.md)**: Make HTTP requests that execute within the browser's JavaScript context, automatically inheriting session state, cookies, and authentication. This hybrid approach combines the familiarity of Python's `requests` library with browser-context execution benefits.\n\n## Browser Management\n\nEffective browser and tab management is essential for complex automation scenarios, parallel processing, and multi-user testing.\n\n**[Multi-Tab Management](browser-management/tabs.md)**: Work with multiple browser tabs simultaneously, ensuring efficient resource usage while giving you full control over tab lifecycle, detection of user-opened tabs, and concurrent scraping operations.\n\n**[Browser Contexts](browser-management/contexts.md)**: Create completely isolated browsing environments within a single browser process. Each context maintains separate cookies, storage, cache, and permissions: perfect for multi-account testing, A/B testing, or parallel scraping with different configurations.\n\n\n**[Cookies & Sessions](browser-management/cookies-sessions.md)**: Manage session state at both browser and tab levels. Set cookies programmatically, extract session data, and maintain different sessions across browser contexts for sophisticated testing scenarios.\n\n\n## Configuration\n\nCustomize every aspect of browser behavior to match your automation needs, from low-level Chromium preferences to command-line arguments and page loading strategies.\n\n**[Browser Options](configuration/browser-options.md)**: Configure Chromium's launch parameters, command-line arguments, and page load state control. Fine-tune browser behavior, enable experimental features, and optimize performance for your automation needs.\n\n**[Browser Preferences](configuration/browser-preferences.md)**: Direct access to Chromium's internal preference system gives you control over hundreds of settings. Configure downloads, disable features, optimize performance, or create realistic browser fingerprints for stealth automation.\n\n**[Proxy Configuration](configuration/proxy.md)**: Native proxy support with full authentication capabilities. Essential for web scraping projects requiring IP rotation, geo-targeted testing, or privacy-focused automation.\n\n\n## Advanced Features\n\nThese sophisticated capabilities address complex automation challenges and specialized use cases.\n\n**[Behavioral Captcha Bypass](advanced/behavioral-captcha-bypass.md)**: Pydoll's native behavioral captcha handling is one of its most requested features. Learn how to interact with Cloudflare Turnstile, reCAPTCHA v3, and hCaptcha invisible challenges using two approaches - synchronous context manager for guaranteed completion, and background processing for non-blocking operation.\n\n**[Event System](advanced/event-system.md)**: Build reactive automation that responds to browser events in real-time. Monitor page loads, network activity, DOM changes, and JavaScript execution to create intelligent, adaptive automation scripts.\n\n**[Remote Connections](advanced/remote-connections.md)**: Connect to already-running browsers via WebSocket for hybrid automation scenarios. Perfect for CI/CD pipelines, containerized environments, or integrating Pydoll into existing CDP tooling.\n\n\n## How to Use This Guide\n\nEach feature page follows a consistent structure:\n\n1. **Overview** - What the feature does and why it matters\n2. **Basic Usage** - Get started quickly with simple examples\n3. **Advanced Patterns** - Leverage the feature's full potential\n4. **Best Practices** - Tips for effective and efficient usage\n5. **Common Pitfalls** - Learn from common mistakes\n\nFeel free to explore features in any order based on your needs. Code examples are complete and ready to run - just copy, paste, and adapt to your use case.\n\nReady to dive deep into Pydoll's capabilities? Pick a feature that interests you and start exploring! 🚀\n\n"
  },
  {
    "path": "docs/en/features/network/http-requests.md",
    "content": "# Browser-Context HTTP Requests\n\nMake HTTP requests that automatically inherit your browser's session state, cookies, and authentication. Perfect for hybrid automation combining UI navigation with API efficiency.\n\n!!! tip \"Game Changer for Hybrid Automation\"\n    Ever wished you could make HTTP requests that automatically get all your browser's cookies and authentication? Now you can! The `tab.request` property gives you a beautiful `requests`-like interface that executes HTTP calls **directly in the browser's JavaScript context**.\n\n## Why Use Browser-Context Requests?\n\nTraditional automation often requires you to extract cookies and headers manually to make API calls. Browser-context requests eliminate this hassle:\n\n| Traditional Approach | Browser-Context Requests |\n|---------------------|-------------------------|\n| Extract cookies manually | Cookies inherited automatically |\n| Manage session tokens | Session state preserved |\n| Handle CORS separately | CORS policies respected |\n| Juggle two HTTP clients | One unified interface |\n| Sync authentication state | Always authenticated |\n\n**Perfect for:**\n\n- Scraping authenticated APIs after login via UI\n- Hybrid workflows mixing browser interaction and API calls\n- Testing authenticated endpoints without token management\n- Bypassing complex authentication flows\n- Working with single-page applications (SPAs)\n\n## Quick Start\n\nThe simplest example: +login via UI, then make authenticated API calls:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def hybrid_automation():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 1. Login normally through the UI\n        await tab.go_to('https://example.com/login')\n        await (await tab.find(id='username')).type_text('user@example.com')\n        await (await tab.find(id='password')).type_text('password123')\n        await (await tab.find(id='login-btn')).click()\n        \n        # Wait for redirect after login\n        await asyncio.sleep(2)\n        \n        # 2. Now make API calls with the authenticated session!\n        response = await tab.request.get('https://example.com/api/user/profile')\n        user_data = response.json()\n        \n        print(f\"Logged in as: {user_data['name']}\")\n        print(f\"Email: {user_data['email']}\")\n\nasyncio.run(hybrid_automation())\n```\n\n!!! success \"No Cookie Management Required\"\n    Notice how we didn't extract or pass any cookies? The request automatically inherited the browser's authenticated session!\n\n## Common Use Cases\n\n### 1. Scraping Authenticated APIs\n\nUse the UI to login, then hammer APIs for data extraction:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_user_data():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Login via UI (handles complex auth flows)\n        await tab.go_to('https://app.example.com/login')\n        await (await tab.find(id='email')).type_text('user@example.com')\n        await (await tab.find(id='password')).type_text('password')\n        await (await tab.find(type='submit')).click()\n        await asyncio.sleep(2)\n        \n        # Now extract data via API (much faster than scraping UI)\n        all_users = []\n        for page in range(1, 6):\n            response = await tab.request.get(\n                f'https://app.example.com/api/users',\n                params={'page': str(page), 'limit': '100'}\n            )\n            users = response.json()['users']\n            all_users.extend(users)\n            print(f\"Page {page}: fetched {len(users)} users\")\n        \n        print(f\"Total users scraped: {len(all_users)}\")\n\nasyncio.run(scrape_user_data())\n```\n\n### 2. Testing Protected Endpoints\n\nTest API endpoints without managing authentication tokens:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def test_api_endpoints():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Authenticate once\n        await tab.go_to('https://api.example.com/login')\n        # ... perform login ...\n        await asyncio.sleep(2)\n        \n        # Test multiple endpoints\n        endpoints = [\n            '/api/users/me',\n            '/api/settings',\n            '/api/notifications',\n            '/api/dashboard/stats'\n        ]\n        \n        for endpoint in endpoints:\n            response = await tab.request.get(f'https://api.example.com{endpoint}')\n            \n            if response.ok:\n                print(f\"Success {endpoint}: {response.status_code}\")\n            else:\n                print(f\"Failed {endpoint}: {response.status_code}\")\n                print(f\"   Error: {response.text[:100]}\")\n\nasyncio.run(test_api_endpoints())\n```\n\n### 3. Submitting Forms via API\n\nFill forms faster by posting directly to the API:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def bulk_form_submission():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Login first\n        await tab.go_to('https://crm.example.com/login')\n        # ... login logic ...\n        await asyncio.sleep(2)\n        \n        # Submit multiple entries via API (much faster than filling forms)\n        contacts = [\n            {'name': 'John Doe', 'email': 'john@example.com', 'company': 'Acme Inc'},\n            {'name': 'Jane Smith', 'email': 'jane@example.com', 'company': 'Tech Corp'},\n            {'name': 'Bob Wilson', 'email': 'bob@example.com', 'company': 'StartupXYZ'},\n        ]\n        \n        for contact in contacts:\n            response = await tab.request.post(\n                'https://crm.example.com/api/contacts',\n                json=contact\n            )\n            \n            if response.ok:\n                print(f\"Added: {contact['name']}\")\n            else:\n                print(f\"Failed: {contact['name']} - {response.status_code}\")\n\nasyncio.run(bulk_form_submission())\n```\n\n### 4. Downloading Files with Session\n\nDownload files that require authentication:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def download_authenticated_file():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Authenticate\n        await tab.go_to('https://portal.example.com/login')\n        # ... login logic ...\n        await asyncio.sleep(2)\n        \n        # Download file that requires authentication\n        response = await tab.request.get(\n            'https://portal.example.com/api/reports/monthly.pdf'\n        )\n        \n        if response.ok:\n            # Save the file\n            output_path = Path('/tmp/monthly_report.pdf')\n            output_path.write_bytes(response.content)\n            print(f\"Downloaded: {output_path} ({len(response.content)} bytes)\")\n        else:\n            print(f\"Download failed: {response.status_code}\")\n\nasyncio.run(download_authenticated_file())\n```\n\n### 5. Working with Custom Headers\n\nAdd custom headers to your requests:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def custom_headers_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Login first\n        await tab.go_to('https://api.example.com/login')\n        # ... login logic ...\n        \n        # Make request with custom headers\n        headers: list[HeaderEntry] = [\n            {'name': 'X-API-Version', 'value': '2.0'},\n            {'name': 'X-Request-ID', 'value': 'unique-id-123'},\n            {'name': 'Accept-Language', 'value': 'pt-BR,pt;q=0.9'},\n        ]\n        \n        response = await tab.request.get(\n            'https://api.example.com/data',\n            headers=headers\n        )\n        \n        print(f\"Status: {response.status_code}\")\n        print(f\"Data: {response.json()}\")\n\nasyncio.run(custom_headers_example())\n```\n\n### 6. Handling Different Response Types\n\nAccess response data in multiple formats:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def response_formats():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://api.example.com')\n        \n        # JSON response\n        json_response = await tab.request.get('/api/users/1')\n        user = json_response.json()\n        print(f\"JSON: {user}\")\n        \n        # Text response\n        text_response = await tab.request.get('/api/status')\n        status_text = text_response.text\n        print(f\"Text: {status_text}\")\n        \n        # Binary response (e.g., image)\n        image_response = await tab.request.get('/api/avatar/1')\n        image_bytes = image_response.content\n        print(f\"Binary: {len(image_bytes)} bytes\")\n        \n        # Check response status\n        if json_response.ok:\n            print(\"Request successful!\")\n        \n        # Access response URL (useful after redirects)\n        print(f\"Final URL: {json_response.url}\")\n\nasyncio.run(response_formats())\n```\n\n## HTTP Methods\n\nAll standard HTTP methods are supported:\n\n### GET - Retrieve Data\n\n```python\n# Simple GET\nresponse = await tab.request.get('https://api.example.com/users')\n\n# GET with query parameters\nresponse = await tab.request.get(\n    'https://api.example.com/search',\n    params={'q': 'python', 'limit': '10'}\n)\n```\n\n### POST - Create Resources\n\n```python\n# POST with JSON data\nresponse = await tab.request.post(\n    'https://api.example.com/users',\n    json={'name': 'John Doe', 'email': 'john@example.com'}\n)\n\n# POST with form data\nresponse = await tab.request.post(\n    'https://api.example.com/login',\n    data={'username': 'john', 'password': 'secret'}\n)\n```\n\n### PUT - Update Resources\n\n```python\n# Update entire resource\nresponse = await tab.request.put(\n    'https://api.example.com/users/123',\n    json={'name': 'Jane Doe', 'email': 'jane@example.com', 'role': 'admin'}\n)\n```\n\n### PATCH - Partial Updates\n\n```python\n# Update specific fields\nresponse = await tab.request.patch(\n    'https://api.example.com/users/123',\n    json={'email': 'newemail@example.com'}\n)\n```\n\n### DELETE - Remove Resources\n\n```python\n# Delete a resource\nresponse = await tab.request.delete('https://api.example.com/users/123')\n```\n\n### HEAD - Get Headers Only\n\n```python\n# Check if resource exists without downloading it\nresponse = await tab.request.head('https://example.com/large-file.zip')\nprint(f\"Content-Length: {response.headers}\")\n```\n\n### OPTIONS - Check Capabilities\n\n```python\n# Check allowed methods\nresponse = await tab.request.options('https://api.example.com/users')\nprint(f\"Allowed methods: {response.headers}\")\n```\n\n!!! info \"How Does This Work?\"\n    Browser-context requests execute HTTP calls directly in the browser's JavaScript context using the Fetch API, while monitoring CDP network events to capture comprehensive metadata (headers, cookies, timing).\n    \n    For a detailed explanation of the internal architecture, event monitoring, and implementation details, see [Browser Requests Architecture](../../deep-dive/browser-requests-architecture.md).\n\n## Response Object\n\nThe `Response` object provides a familiar interface similar to `requests.Response`:\n\n```python\nresponse = await tab.request.get('https://api.example.com/users')\n\n# Status code\nprint(response.status_code)  # 200, 404, 500, etc.\n\n# Check if successful (2xx or 3xx)\nif response.ok:\n    print(\"Success!\")\n\n# Response body\ntext_data = response.text      # As string\nbyte_data = response.content   # As bytes\njson_data = response.json()    # Parsed JSON\n\n# Headers\nfor header in response.headers:\n    print(f\"{header['name']}: {header['value']}\")\n\n# Request headers (what was actually sent)\nfor header in response.request_headers:\n    print(f\"{header['name']}: {header['value']}\")\n\n# Cookies set by the response\nfor cookie in response.cookies:\n    print(f\"{cookie['name']} = {cookie['value']}\")\n\n# Final URL (after redirects)\nprint(response.url)\n\n# Raise exception for error status codes\nresponse.raise_for_status()  # Raises HTTPError if 4xx or 5xx\n```\n\n!!! note \"Redirects and URL Tracking\"\n    The `response.url` property contains only the **final URL** after all redirects. If you need to track the complete redirect chain (intermediate URLs, status codes, timing), use [Network Monitoring](monitoring.md) to observe all requests in detail.\n\n## Headers and Cookies\n\n### Working with Headers\n\nHeaders are represented as `HeaderEntry` objects:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def header_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Using HeaderEntry type for IDE autocomplete and type checking\n        headers: list[HeaderEntry] = [\n            {'name': 'Authorization', 'value': 'Bearer token-123'},\n            {'name': 'X-Custom-Header', 'value': 'custom-value'},\n        ]\n        \n        response = await tab.request.get(\n            'https://api.example.com/protected',\n            headers=headers\n        )\n        \n        # Inspect response headers (also HeaderEntry typed dicts)\n        for header in response.headers:\n            if header['name'] == 'Content-Type':\n                print(f\"Content-Type: {header['value']}\")\n\nasyncio.run(header_example())\n```\n\n!!! tip \"Type Hints for Headers\"\n    `HeaderEntry` is a `TypedDict` from `pydoll.protocol.fetch.types`. Using it as a type hint gives you:\n    \n    - **Autocomplete**: IDE suggests `name` and `value` keys\n    - **Type safety**: Catch typos and missing keys before running\n    - **Documentation**: Clear structure for headers\n    \n    While you can pass plain dictionaries, using the type hint improves code quality and IDE support.\n\n!!! tip \"Custom Headers Behavior\"\n    Custom headers are sent **alongside** the browser's automatic headers (like `User-Agent`, `Accept`, `Referer`, etc.). \n    \n    If you try to set a standard browser header (e.g., `User-Agent`), the behavior depends on the specific header; some may be overridden, others ignored, and some may cause conflicts. For most use cases, stick to custom headers (e.g., `X-API-Key`, `Authorization`) to avoid unexpected behavior.\n\n### Understanding Cookies\n\nCookies are automatically managed by the browser:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def cookie_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # First request sets cookies\n        login_response = await tab.request.post(\n            'https://api.example.com/login',\n            json={'username': 'user', 'password': 'pass'}\n        )\n        \n        # Check cookies set by server\n        print(\"Cookies set by server:\")\n        for cookie in login_response.cookies:\n            print(f\"  {cookie['name']} = {cookie['value']}\")\n        \n        # Subsequent requests automatically include cookies\n        profile_response = await tab.request.get(\n            'https://api.example.com/profile'\n        )\n        # No need to pass cookies - browser handles it!\n        \n        print(f\"Profile data: {profile_response.json()}\")\n\nasyncio.run(cookie_example())\n```\n\n## Comparison with Traditional Requests\n\n| Feature | `requests` Library | Browser-Context Requests |\n|---------|-------------------|-------------------------|\n| **Session Management** | Manual cookie handling | Automatic via browser |\n| **Authentication** | Extract and pass tokens | Inherited from browser |\n| **CORS** | Not applicable | Browser enforces policies |\n| **JavaScript** | Cannot execute | Full access to browser context |\n| **Cookie Jar** | Separate instance | Browser's native cookie store |\n| **Headers** | Manually set | Browser auto-adds standard headers |\n| **Use Case** | Server-side scripts | Browser automation |\n| **Setup** | External library | Built into Pydoll |\n\n## See Also\n\n- **[Browser Requests Architecture](../../deep-dive/browser-requests-architecture.md)** - Internal implementation and architecture\n- **[Network Monitoring](monitoring.md)** - Observe all network traffic\n- **[Request Interception](interception.md)** - Modify requests before they're sent\n- **[Event System](../advanced/event-system.md)** - React to browser events\n- **[Deep Dive: Network Capabilities](../../deep-dive/network-capabilities.md)** - Technical details\n\nBrowser-context requests are a game-changer for hybrid automation. Combine the power of UI automation with the speed of direct API calls, all while maintaining perfect session continuity!\n"
  },
  {
    "path": "docs/en/features/network/interception.md",
    "content": "# Request Interception\n\nRequest interception allows you to intercept, modify, block, or mock HTTP requests and responses in real-time. This is essential for testing, performance optimization, content filtering, and simulating various network conditions.\n\n!!! info \"Network vs Fetch Domain\"\n    **Network domain** is for passive monitoring (observing traffic). **Fetch domain** is for active interception (modifying/blocking requests). This guide focuses on interception. For passive monitoring, see [Network Monitoring](monitoring.md).\n\n## Understanding Request Interception\n\nWhen you enable request interception, Pydoll pauses matching requests before they're sent to the server (or after receiving the response). You then have three options:\n\n1. **Continue**: Let the request proceed (optionally with modifications)\n2. **Block**: Fail the request with an error\n3. **Mock**: Fulfill the request with a custom response\n\n```mermaid\nsequenceDiagram\n    participant Browser\n    participant Pydoll\n    participant Server\n    \n    Browser->>Pydoll: Request initiated\n    Note over Pydoll: Request Paused\n    Pydoll->>Pydoll: Callback executed\n    \n    alt Continue\n        Pydoll->>Server: Forward request\n        Server-->>Browser: Response\n    else Block\n        Pydoll-->>Browser: Error response\n    else Mock\n        Pydoll-->>Browser: Custom response\n    end\n```\n\n!!! warning \"Performance Impact\"\n    Request interception adds latency to every matching request. Only intercept what you need and disable when done to avoid slowing down page loads.\n\n## Enabling Request Interception\n\nBefore intercepting requests, you must enable the Fetch domain:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Enable fetch events (intercepts all requests by default)\n        await tab.enable_fetch_events()\n        \n        await tab.go_to('https://example.com')\n        \n        # Disable when done\n        await tab.disable_fetch_events()\n\nasyncio.run(main())\n```\n\n### Selective Interception\n\nYou can filter which requests to intercept by resource type:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def selective_interception():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Only intercept images and stylesheets\n        await tab.enable_fetch_events(\n            resource_type='Image'  # Or 'Stylesheet', 'Script', etc.\n        )\n        \n        await tab.go_to('https://example.com')\n        await tab.disable_fetch_events()\n\nasyncio.run(selective_interception())\n```\n\n!!! tip \"Resource Types\"\n    See the [Resource Types Reference](#resource-types-reference) section for a complete list of interceptable resource types.\n\n## Intercepting Requests\n\nUse the `RequestPaused` event to intercept requests:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\n\nasync def basic_interception():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Callback with type hint for IDE support\n        async def handle_request(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            print(f\"Intercepted: {url}\")\n            \n            # Continue the request without modifications\n            await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, handle_request)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(basic_interception())\n```\n\n!!! info \"Type Hints for Better IDE Support\"\n    Use type hints like `RequestPausedEvent` to get autocomplete for event keys. All event types are in `pydoll.protocol.fetch.events`.\n\n!!! note \"Production-Ready Waiting\"\n    The examples in this guide use `asyncio.sleep()` for simplicity. In production code, consider using more explicit waiting strategies like waiting for specific elements or implementing network idle detection. See the [Network Monitoring](monitoring.md) guide for advanced techniques.\n\n## Common Use Cases\n\n### 1. Blocking Resources to Save Bandwidth\n\nBlock images, stylesheets, or other resources to speed up page loads:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.network.types import ErrorReason\n\nasync def block_images():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        blocked_count = 0\n        \n        async def block_resource(event: RequestPausedEvent):\n            nonlocal blocked_count\n            request_id = event['params']['requestId']\n            resource_type = event['params']['resourceType']\n            url = event['params']['request']['url']\n            \n            # Block images and stylesheets\n            if resource_type in ['Image', 'Stylesheet']:\n                blocked_count += 1\n                print(f\"🚫 Blocked {resource_type}: {url[:60]}\")\n                await tab.fail_request(request_id, ErrorReason.BLOCKED_BY_CLIENT)\n            else:\n                # Continue other requests\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, block_resource)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        print(f\"\\n📊 Total blocked: {blocked_count} resources\")\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(block_images())\n```\n\n### 2. Modifying Request Headers\n\nAdd, modify, or remove headers before requests are sent:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def modify_headers():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def add_custom_headers(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            # Only modify API requests\n            if '/api/' in url:\n                # Build custom headers (using HeaderEntry type hint for IDE support)\n                headers: list[HeaderEntry] = [\n                    {'name': 'X-Custom-Header', 'value': 'MyValue'},\n                    {'name': 'Authorization', 'value': 'Bearer my-token-123'},\n                ]\n                \n                print(f\"✨ Modified headers for: {url}\")\n                await tab.continue_request(request_id, headers=headers)\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, add_custom_headers)\n        \n        await tab.go_to('https://your-app.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(modify_headers())\n```\n\n!!! tip \"Type Hints for Headers\"\n    `HeaderEntry` is a `TypedDict` from `pydoll.protocol.fetch.types`. Using it as a type hint gives you IDE autocomplete for `name` and `value` keys. You can also use plain dictionaries without the type hint.\n\n!!! tip \"Header Management\"\n    When you provide custom headers, they **replace** all existing headers. Make sure to include necessary headers like `User-Agent`, `Accept`, etc., if needed.\n\n### 3. Mocking API Responses\n\nReplace real API responses with custom mock data:\n\n```python\nimport asyncio\nimport json\nimport base64\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def mock_api_responses():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def mock_response(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            # Mock specific API endpoint\n            if '/api/users' in url:\n                # Create mock response data\n                mock_data = {\n                    'users': [\n                        {'id': 1, 'name': 'Mock User 1'},\n                        {'id': 2, 'name': 'Mock User 2'},\n                    ],\n                    'total': 2\n                }\n                \n                # Convert to JSON and base64-encode\n                body_json = json.dumps(mock_data)\n                body_base64 = base64.b64encode(body_json.encode()).decode()\n                \n                # Response headers\n                headers: list[HeaderEntry] = [\n                    {'name': 'Content-Type', 'value': 'application/json'},\n                    {'name': 'Access-Control-Allow-Origin', 'value': '*'},\n                ]\n                \n                print(f\"🎭 Mocked response for: {url}\")\n                await tab.fulfill_request(\n                    request_id=request_id,\n                    response_code=200,\n                    response_headers=headers,\n                    body=body_base64,\n                    response_phrase='OK'\n                )\n            else:\n                # Continue other requests normally\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, mock_response)\n        \n        await tab.go_to('https://your-app.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(mock_api_responses())\n```\n\n!!! warning \"Base64 Encoding Required\"\n    The `body` parameter in `fulfill_request()` must be base64-encoded. Use Python's `base64` module to encode your response data.\n\n### 4. Modifying Request URLs\n\nRedirect requests to different URLs:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\n\nasync def redirect_requests():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def redirect_url(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            original_url = event['params']['request']['url']\n            \n            # Redirect CDN requests to local server\n            if 'cdn.example.com' in original_url:\n                new_url = original_url.replace(\n                    'cdn.example.com',\n                    'localhost:8080'\n                )\n                print(f\"🔀 Redirected: {original_url} → {new_url}\")\n                await tab.continue_request(request_id, url=new_url)\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, redirect_url)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(redirect_requests())\n```\n\n### 5. Modifying Request Body\n\nModify POST data before sending:\n\n```python\nimport asyncio\nimport base64\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\n\nasync def modify_post_data():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def modify_body(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            method = event['params']['request']['method']\n            url = event['params']['request']['url']\n            \n            # Modify POST requests\n            if method == 'POST' and '/api/submit' in url:\n                # Create new POST data\n                new_data = '{\"modified\": true, \"timestamp\": 123456789}'\n                post_data_base64 = base64.b64encode(new_data.encode()).decode()\n                \n                print(f\"✏️  Modified POST data for: {url}\")\n                await tab.continue_request(\n                    request_id,\n                    post_data=post_data_base64\n                )\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, modify_body)\n        \n        await tab.go_to('https://your-app.com/form')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(modify_post_data())\n```\n\n### 6. Handling Authentication Challenges\n\nManually respond to HTTP authentication challenges (Basic Auth, Digest Auth, etc.):\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, AuthRequiredEvent\nfrom pydoll.protocol.fetch.types import AuthChallengeResponseType\n\nasync def handle_auth():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def respond_to_auth(event: AuthRequiredEvent):\n            request_id = event['params']['requestId']\n            auth_challenge = event['params']['authChallenge']\n            \n            print(f\"🔐 Auth challenge from: {auth_challenge['origin']}\")\n            print(f\"   Scheme: {auth_challenge['scheme']}\")\n            print(f\"   Realm: {auth_challenge.get('realm', 'N/A')}\")\n            \n            # Provide credentials for the authentication challenge\n            await tab.continue_with_auth(\n                request_id=request_id,\n                auth_challenge_response=AuthChallengeResponseType.PROVIDE_CREDENTIALS,\n                proxy_username='myuser',\n                proxy_password='mypassword'\n            )\n        \n        # Enable with auth handling\n        await tab.enable_fetch_events(handle_auth=True)\n        await tab.on(FetchEvent.AUTH_REQUIRED, respond_to_auth)\n        \n        await tab.go_to('https://httpbin.org/basic-auth/myuser/mypassword')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(handle_auth())\n```\n\n!!! note \"Automatic Proxy Authentication\"\n    **Pydoll automatically handles proxy authentication** (407 Proxy Authentication Required) when you configure proxy credentials via browser options. This example demonstrates **manual handling** of authentication challenges, which is useful for:\n    \n    - HTTP Basic/Digest Authentication from servers (401 Unauthorized)\n    - Custom authentication flows\n    - Dynamic credential selection based on the challenge\n    - Testing authentication failure scenarios\n    \n    For standard proxy usage, simply configure your proxy credentials in browser options - no manual handling needed!\n\n### 7. Simulating Network Errors\n\nTest how your application handles network failures:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.network.types import ErrorReason\n\nasync def simulate_errors():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        request_count = 0\n        \n        async def fail_some_requests(event: RequestPausedEvent):\n            nonlocal request_count\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            request_count += 1\n            \n            # Fail every 3rd request\n            if request_count % 3 == 0:\n                print(f\"❌ Simulating timeout for: {url[:60]}\")\n                await tab.fail_request(request_id, ErrorReason.TIMED_OUT)\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, fail_some_requests)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(simulate_errors())\n```\n\n## Request Stages\n\nYou can intercept requests at different stages:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.types import RequestStage\n\nasync def intercept_responses():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Intercept responses instead of requests\n        await tab.enable_fetch_events(request_stage=RequestStage.RESPONSE)\n        \n        # Now you can modify responses before they reach the page\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(intercept_responses())\n```\n\n| Stage | When Intercepted | Use Cases |\n|-------|------------------|-----------|\n| `Request` (default) | Before request is sent | Modify headers, block requests, change URL |\n| `Response` | After response received | Modify response body, change status codes |\n\n!!! tip \"Response Interception\"\n    When intercepting responses, you can use `intercept_response=True` in `continue_request()` to also intercept the response for that specific request.\n\n## Resource Types Reference\n\n| Resource Type | Description | Common File Extensions |\n|---------------|-------------|------------------------|\n| `Document` | HTML documents | `.html` |\n| `Stylesheet` | CSS files | `.css` |\n| `Image` | Image resources | `.jpg`, `.png`, `.gif`, `.webp`, `.svg` |\n| `Media` | Audio/video | `.mp4`, `.webm`, `.mp3`, `.ogg` |\n| `Font` | Web fonts | `.woff`, `.woff2`, `.ttf`, `.otf` |\n| `Script` | JavaScript | `.js` |\n| `TextTrack` | Subtitles | `.vtt`, `.srt` |\n| `XHR` | XMLHttpRequest | AJAX requests |\n| `Fetch` | Fetch API | Modern API calls |\n| `EventSource` | Server-Sent Events | Real-time streams |\n| `WebSocket` | WebSocket | Bidirectional communication |\n| `Manifest` | Web app manifest | PWA configuration |\n| `Other` | Other types | Miscellaneous |\n\n## Error Reasons Reference\n\nUse these with `fail_request()` to simulate different network failures:\n\n| Error Reason | Description | Use Case |\n|--------------|-------------|----------|\n| `FAILED` | Generic failure | General error |\n| `ABORTED` | Request aborted | User cancelled |\n| `TIMED_OUT` | Request timeout | Network timeout |\n| `ACCESS_DENIED` | Access denied | Permission error |\n| `CONNECTION_CLOSED` | Connection closed | Server disconnect |\n| `CONNECTION_RESET` | Connection reset | Network reset |\n| `CONNECTION_REFUSED` | Connection refused | Server unreachable |\n| `NAME_NOT_RESOLVED` | DNS failure | Invalid hostname |\n| `INTERNET_DISCONNECTED` | No internet | Offline mode |\n| `BLOCKED_BY_CLIENT` | Client blocked | Ad blocker simulation |\n| `BLOCKED_BY_RESPONSE` | Response blocked | CORS/CSP violation |\n\n## Best Practices\n\n### 1. Always Continue or Fail Requests\n\n```python\n# Good: Every paused request is handled\nasync def handle_request(event: RequestPausedEvent):\n    request_id = event['params']['requestId']\n    try:\n        # Your logic here\n        await tab.continue_request(request_id)\n    except Exception as e:\n        # Fail on error to prevent hanging\n        await tab.fail_request(request_id, ErrorReason.FAILED)\n\n# Bad: Request might hang if callback raises exception\nasync def handle_request(event: RequestPausedEvent):\n    request_id = event['params']['requestId']\n    # If this raises, request hangs forever\n    await tab.continue_request(request_id)\n```\n\n### 2. Use Selective Interception\n\n```python\n# Good: Only intercept what you need\nawait tab.enable_fetch_events(resource_type='Image')\n\n# Bad: Intercepts everything, slows down all requests\nawait tab.enable_fetch_events()\n```\n\n### 3. Disable When Done\n\n```python\n# Good: Clean up after yourself\nawait tab.enable_fetch_events()\n# ... do work ...\nawait tab.disable_fetch_events()\n\n# Bad: Leaves interception enabled\nawait tab.enable_fetch_events()\n# ... do work ...\n# (never disabled)\n```\n\n### 4. Handle Errors Gracefully\n\n```python\n# Good: Wrap in try/except\nasync def safe_handler(event: RequestPausedEvent):\n    request_id = event['params']['requestId']\n    try:\n        # Complex logic that might fail\n        modified_url = transform_url(event['params']['request']['url'])\n        await tab.continue_request(request_id, url=modified_url)\n    except Exception as e:\n        print(f\"Error handling request: {e}\")\n        # Continue without modifications on error\n        await tab.continue_request(request_id)\n```\n\n## Complete Example: Advanced Request Control\n\nHere's a complete example combining multiple interception techniques:\n\n```python\nimport asyncio\nimport base64\nimport json\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.fetch.types import HeaderEntry\nfrom pydoll.protocol.network.types import ErrorReason\n\nasync def advanced_interception():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        stats = {\n            'blocked': 0,\n            'mocked': 0,\n            'modified': 0,\n            'continued': 0\n        }\n        \n        async def intelligent_handler(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            resource_type = event['params']['resourceType']\n            method = event['params']['request']['method']\n            \n            try:\n                # Block ads and trackers\n                if any(tracker in url for tracker in ['analytics', 'ads', 'tracking']):\n                    stats['blocked'] += 1\n                    print(f\"🚫 Blocked tracker: {url[:50]}\")\n                    await tab.fail_request(request_id, ErrorReason.BLOCKED_BY_CLIENT)\n                \n                # Mock API responses\n                elif '/api/config' in url:\n                    stats['mocked'] += 1\n                    mock_config = {'feature_x': True, 'debug_mode': False}\n                    body = base64.b64encode(json.dumps(mock_config).encode()).decode()\n                    headers: list[HeaderEntry] = [\n                        {'name': 'Content-Type', 'value': 'application/json'},\n                    ]\n                    print(f\"🎭 Mocked config API\")\n                    await tab.fulfill_request(\n                        request_id, 200, headers, body, 'OK'\n                    )\n                \n                # Add auth headers to API requests\n                elif '/api/' in url and method == 'GET':\n                    stats['modified'] += 1\n                    headers: list[HeaderEntry] = [\n                        {'name': 'Authorization', 'value': 'Bearer token-123'},\n                    ]\n                    print(f\"✨ Added auth to: {url[:50]}\")\n                    await tab.continue_request(request_id, headers=headers)\n                \n                # Continue everything else normally\n                else:\n                    stats['continued'] += 1\n                    await tab.continue_request(request_id)\n                    \n            except Exception as e:\n                print(f\"⚠️  Error handling request: {e}\")\n                # Always continue on error to prevent hanging\n                await tab.continue_request(request_id)\n        \n        # Enable interception\n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, intelligent_handler)\n        \n        # Navigate\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(5)\n        \n        # Print stats\n        print(f\"\\n📊 Interception Statistics:\")\n        print(f\"   Blocked: {stats['blocked']}\")\n        print(f\"   Mocked: {stats['mocked']}\")\n        print(f\"   Modified: {stats['modified']}\")\n        print(f\"   Continued: {stats['continued']}\")\n        print(f\"   Total: {sum(stats.values())}\")\n        \n        # Cleanup\n        await tab.disable_fetch_events()\n\nasyncio.run(advanced_interception())\n```\n\n## See Also\n\n- **[Network Monitoring](monitoring.md)** - Passive network traffic observation\n- **[CDP Fetch Domain](../../deep-dive/network-capabilities.md#fetch-domain)** - Deep dive into the Fetch domain\n- **[Event System](../advanced/event-system.md)** - Understanding Pydoll's event architecture\n\nRequest interception is a powerful tool for testing, optimization, and mocking. Master these techniques to build robust, efficient browser automation scripts.\n"
  },
  {
    "path": "docs/en/features/network/monitoring.md",
    "content": "# Network Monitoring\n\nNetwork monitoring in Pydoll allows you to observe and analyze HTTP requests, responses, and other network activity during browser automation. This is essential for debugging, performance analysis, API testing, and understanding how web applications communicate with servers.\n\n!!! info \"Network vs Fetch Domain\"\n    **Network domain** is for passive monitoring (observing traffic). **Fetch domain** is for active interception (modifying requests/responses). This guide focuses on monitoring. For request interception, see the advanced documentation.\n\n## Enabling Network Events\n\nBefore you can monitor network activity, you must enable the Network domain:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Enable network monitoring\n        await tab.enable_network_events()\n        \n        # Now navigate\n        await tab.go_to('https://api.github.com')\n        \n        # Don't forget to disable when done (optional but recommended)\n        await tab.disable_network_events()\n\nasyncio.run(main())\n```\n\n!!! warning \"Enable Before Navigation\"\n    Always enable network events **before** navigating to capture all requests. Requests made before enabling won't be captured.\n\n## Getting Network Logs\n\nPydoll automatically stores network logs when network events are enabled. You can retrieve them using `get_network_logs()`:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def analyze_requests():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # Navigate to a page\n        await tab.go_to('https://httpbin.org/json')\n        \n        # Wait for page to fully load\n        await asyncio.sleep(2)\n        \n        # Get all network logs\n        logs = await tab.get_network_logs()\n        \n        print(f\"Total requests captured: {len(logs)}\")\n        \n        for log in logs:\n            request = log['params']['request']\n            print(f\"→ {request['method']} {request['url']}\")\n\nasyncio.run(analyze_requests())\n```\n\n!!! note \"Production-Ready Waiting\"\n    The examples above use `asyncio.sleep(2)` for simplicity. In production code, consider using more explicit waiting strategies:\n    \n    - Wait for specific elements to appear\n    - Use the [Event System](../advanced/event-system.md) to detect when all resources have loaded\n    - Implement network idle detection (see Real-Time Network Monitoring section)\n    \n    This ensures your automation waits exactly as long as needed, no more, no less.\n\n### Filtering Network Logs\n\nYou can filter logs by URL pattern:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def filter_logs_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        \n        # Get all logs\n        all_logs = await tab.get_network_logs()\n        \n        # Get logs for specific domain\n        api_logs = await tab.get_network_logs(filter='api.example.com')\n        \n        # Get logs for specific endpoint\n        user_logs = await tab.get_network_logs(filter='/api/users')\n\nasyncio.run(filter_logs_example())\n```\n\n## Understanding Network Event Structure\n\nNetwork logs contain detailed information about each request. Here's the structure:\n\n### RequestWillBeSentEvent\n\nThis event is fired when a request is about to be sent:\n\n```python\n{\n    'method': 'Network.requestWillBeSent',\n    'params': {\n        'requestId': 'unique-request-id',\n        'loaderId': 'loader-id',\n        'documentURL': 'https://example.com',\n        'request': {\n            'url': 'https://api.example.com/data',\n            'method': 'GET',  # or 'POST', 'PUT', 'DELETE', etc.\n            'headers': {\n                'User-Agent': 'Chrome/...',\n                'Accept': 'application/json',\n                ...\n            },\n            'postData': '...',  # Only present for POST/PUT requests\n            'initialPriority': 'High',\n            'referrerPolicy': 'strict-origin-when-cross-origin'\n        },\n        'timestamp': 1234567890.123,\n        'wallTime': 1234567890.123,\n        'initiator': {\n            'type': 'script',  # or 'parser', 'other'\n            'stack': {...}  # Call stack if initiated from script\n        },\n        'type': 'XHR',  # Resource type: Document, Script, Image, XHR, etc.\n        'frameId': 'frame-id',\n        'hasUserGesture': False\n    }\n}\n```\n\n### Key Fields Reference\n\n| Field | Location | Type | Description |\n|-------|----------|------|-------------|\n| `requestId` | `params.requestId` | `str` | Unique identifier for this request |\n| `url` | `params.request.url` | `str` | Complete request URL |\n| `method` | `params.request.method` | `str` | HTTP method (GET, POST, etc.) |\n| `headers` | `params.request.headers` | `dict` | Request headers |\n| `postData` | `params.request.postData` | `str` | Request body (POST/PUT) |\n| `timestamp` | `params.timestamp` | `float` | Monotonic time when request started |\n| `type` | `params.type` | `str` | Resource type (Document, XHR, Image, etc.) |\n| `initiator` | `params.initiator` | `dict` | What triggered this request |\n\n## Getting Response Bodies\n\nTo get the actual response content, use `get_network_response_body()`:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def fetch_api_response():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # Navigate to API endpoint\n        await tab.go_to('https://httpbin.org/json')\n        await asyncio.sleep(2)\n        \n        # Get all requests\n        logs = await tab.get_network_logs()\n        \n        for log in logs:\n            request_id = log['params']['requestId']\n            url = log['params']['request']['url']\n            \n            # Only get response for JSON endpoint\n            if 'httpbin.org/json' in url:\n                try:\n                    # Get response body\n                    response_body = await tab.get_network_response_body(request_id)\n                    print(f\"Response from {url}:\")\n                    print(response_body)\n                except Exception as e:\n                    print(f\"Could not get response body: {e}\")\n\nasyncio.run(fetch_api_response())\n```\n\n!!! warning \"Response Body Availability\"\n    Response bodies are only available for requests that have completed. Also, some response types (like images or redirects) may not have accessible bodies.\n\n## Practical Use Cases\n\n### 1. API Testing and Validation\n\nMonitor API calls to verify correct requests are being made:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def validate_api_calls():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # Navigate to your app\n        await tab.go_to('https://your-app.com')\n        \n        # Trigger some action that makes API calls\n        button = await tab.find(id='load-data-button')\n        await button.click()\n        await asyncio.sleep(2)\n        \n        # Get API logs\n        api_logs = await tab.get_network_logs(filter='/api/')\n        \n        print(f\"\\n📊 API Calls Summary:\")\n        print(f\"Total API calls: {len(api_logs)}\")\n        \n        for log in api_logs:\n            request = log['params']['request']\n            method = request['method']\n            url = request['url']\n            \n            # Check if correct auth header is present\n            headers = request.get('headers', {})\n            has_auth = 'Authorization' in headers or 'authorization' in headers\n            \n            print(f\"\\n{method} {url}\")\n            print(f\"  ✓ Has Authorization: {has_auth}\")\n            \n            # Validate POST data if applicable\n            if method == 'POST' and 'postData' in request:\n                print(f\"  📤 Body: {request['postData'][:100]}...\")\n\nasyncio.run(validate_api_calls())\n```\n\n### 2. Performance Analysis\n\nAnalyze request timing and identify slow resources:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def analyze_performance():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(5)\n        \n        logs = await tab.get_network_logs()\n        \n        # Store timing data\n        timings = []\n        \n        for log in logs:\n            params = log['params']\n            request_id = params['requestId']\n            url = params['request']['url']\n            resource_type = params.get('type', 'Other')\n            \n            timings.append({\n                'url': url,\n                'type': resource_type,\n                'timestamp': params['timestamp']\n            })\n        \n        # Sort by timestamp\n        timings.sort(key=lambda x: x['timestamp'])\n        \n        print(\"\\n⏱️  Request Timeline:\")\n        start_time = timings[0]['timestamp'] if timings else 0\n        \n        for timing in timings[:20]:  # Show first 20\n            elapsed = (timing['timestamp'] - start_time) * 1000  # Convert to ms\n            print(f\"{elapsed:7.0f}ms | {timing['type']:12} | {timing['url'][:80]}\")\n\nasyncio.run(analyze_performance())\n```\n\n### 3. Detecting External Resources\n\nFind all external domains your page connects to:\n\n```python\nimport asyncio\nfrom urllib.parse import urlparse\nfrom collections import Counter\nfrom pydoll.browser.chromium import Chrome\n\nasync def analyze_domains():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        await tab.go_to('https://news.ycombinator.com')\n        await asyncio.sleep(5)\n        \n        logs = await tab.get_network_logs()\n        \n        # Count requests per domain\n        domains = Counter()\n        \n        for log in logs:\n            url = log['params']['request']['url']\n            try:\n                domain = urlparse(url).netloc\n                if domain:\n                    domains[domain] += 1\n            except:\n                pass\n        \n        print(\"\\n🌐 External Domains:\")\n        for domain, count in domains.most_common(10):\n            print(f\"  {count:3} requests | {domain}\")\n\nasyncio.run(analyze_domains())\n```\n\n### 4. Monitoring Specific Resource Types\n\nTrack specific types of resources like images or scripts:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def track_resource_types():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        logs = await tab.get_network_logs()\n        \n        # Group by resource type\n        by_type = {}\n        \n        for log in logs:\n            params = log['params']\n            resource_type = params.get('type', 'Other')\n            url = params['request']['url']\n            \n            if resource_type not in by_type:\n                by_type[resource_type] = []\n            \n            by_type[resource_type].append(url)\n        \n        print(\"\\n📦 Resources by Type:\")\n        for rtype in sorted(by_type.keys()):\n            urls = by_type[rtype]\n            print(f\"\\n{rtype}: {len(urls)} resource(s)\")\n            for url in urls[:3]:  # Show first 3\n                print(f\"  • {url}\")\n            if len(urls) > 3:\n                print(f\"  ... and {len(urls) - 3} more\")\n\nasyncio.run(track_resource_types())\n```\n\n## Real-Time Network Monitoring\n\nFor real-time monitoring, use event callbacks instead of polling `get_network_logs()`:\n\n!!! info \"Understanding Events\"\n    Real-time monitoring uses Pydoll's event system to react to network activity as it happens. For a deep dive into how events work, see **[Event System](../advanced/event-system.md)**.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import (\n    NetworkEvent,\n    RequestWillBeSentEvent,\n    ResponseReceivedEvent,\n    LoadingFailedEvent\n)\n\nasync def real_time_monitoring():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Statistics\n        stats = {\n            'requests': 0,\n            'responses': 0,\n            'failed': 0\n        }\n        \n        # Request callback\n        async def on_request(event: RequestWillBeSentEvent):\n            stats['requests'] += 1\n            url = event['params']['request']['url']\n            method = event['params']['request']['method']\n            print(f\"→ {method:6} | {url}\")\n        \n        # Response callback\n        async def on_response(event: ResponseReceivedEvent):\n            stats['responses'] += 1\n            response = event['params']['response']\n            status = response['status']\n            url = response['url']\n            \n            # Color code by status\n            if 200 <= status < 300:\n                color = '\\033[92m'  # Green\n            elif 300 <= status < 400:\n                color = '\\033[93m'  # Yellow\n            else:\n                color = '\\033[91m'  # Red\n            reset = '\\033[0m'\n            \n            print(f\"← {color}{status}{reset} | {url}\")\n        \n        # Failed callback\n        async def on_failed(event: LoadingFailedEvent):\n            stats['failed'] += 1\n            error = event['params']['errorText']\n            print(f\"✗ FAILED: {error}\")\n        \n        # Enable and register callbacks\n        await tab.enable_network_events()\n        await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, on_request)\n        await tab.on(NetworkEvent.RESPONSE_RECEIVED, on_response)\n        await tab.on(NetworkEvent.LOADING_FAILED, on_failed)\n        \n        # Navigate\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(5)\n        \n        print(f\"\\n📊 Summary:\")\n        print(f\"  Requests: {stats['requests']}\")\n        print(f\"  Responses: {stats['responses']}\")\n        print(f\"  Failed: {stats['failed']}\")\n\nasyncio.run(real_time_monitoring())\n```\n\n## Resource Types Reference\n\nPydoll captures the following resource types:\n\n| Type | Description | Examples |\n|------|-------------|----------|\n| `Document` | Main HTML documents | Page loads, iframe sources |\n| `Stylesheet` | CSS files | External .css, inline styles |\n| `Image` | Image resources | .jpg, .png, .gif, .webp, .svg |\n| `Media` | Audio/video files | .mp4, .webm, .mp3, .ogg |\n| `Font` | Web fonts | .woff, .woff2, .ttf, .otf |\n| `Script` | JavaScript files | .js files, inline scripts |\n| `TextTrack` | Subtitle files | .vtt, .srt |\n| `XHR` | XMLHttpRequest | AJAX requests, legacy API calls |\n| `Fetch` | Fetch API requests | Modern API calls |\n| `EventSource` | Server-Sent Events | Real-time streams |\n| `WebSocket` | WebSocket connections | Bidirectional communication |\n| `Manifest` | Web app manifests | PWA configuration |\n| `Other` | Other resource types | Miscellaneous |\n\n## Advanced: Extracting Response Timing\n\nNetwork events include detailed timing information:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def analyze_timing():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # Custom callback to capture timing\n        timing_data = []\n        \n        async def on_response(event: ResponseReceivedEvent):\n            response = event['params']['response']\n            timing = response.get('timing')\n            \n            if timing:\n                # Calculate different phases\n                dns_time = timing.get('dnsEnd', 0) - timing.get('dnsStart', 0)\n                connect_time = timing.get('connectEnd', 0) - timing.get('connectStart', 0)\n                ssl_time = timing.get('sslEnd', 0) - timing.get('sslStart', 0)\n                send_time = timing.get('sendEnd', 0) - timing.get('sendStart', 0)\n                wait_time = timing.get('receiveHeadersStart', 0) - timing.get('sendEnd', 0)\n                receive_time = timing.get('receiveHeadersEnd', 0) - timing.get('receiveHeadersStart', 0)\n                \n                timing_data.append({\n                    'url': response['url'][:50],\n                    'dns': dns_time if dns_time > 0 else 0,\n                    'connect': connect_time if connect_time > 0 else 0,\n                    'ssl': ssl_time if ssl_time > 0 else 0,\n                    'send': send_time,\n                    'wait': wait_time,\n                    'receive': receive_time,\n                    'total': receive_time + wait_time + send_time\n                })\n        \n        await tab.on(NetworkEvent.RESPONSE_RECEIVED, on_response)\n        await tab.go_to('https://github.com')\n        await asyncio.sleep(5)\n        \n        # Print timing breakdown\n        print(\"\\n⏱️  Request Timing Breakdown (ms):\")\n        print(f\"{'URL':<50} | {'DNS':>6} | {'Connect':>8} | {'SSL':>6} | {'Send':>6} | {'Wait':>6} | {'Receive':>8} | {'Total':>7}\")\n        print(\"-\" * 120)\n        \n        for data in sorted(timing_data, key=lambda x: x['total'], reverse=True)[:10]:\n            print(f\"{data['url']:<50} | {data['dns']:6.1f} | {data['connect']:8.1f} | {data['ssl']:6.1f} | \"\n                  f\"{data['send']:6.1f} | {data['wait']:6.1f} | {data['receive']:8.1f} | {data['total']:7.1f}\")\n\nasyncio.run(analyze_timing())\n```\n\n## Timing Fields Explanation\n\n| Phase | Fields | Description |\n|-------|--------|-------------|\n| **DNS** | `dnsStart` → `dnsEnd` | DNS lookup time |\n| **Connect** | `connectStart` → `connectEnd` | TCP connection establishment |\n| **SSL** | `sslStart` → `sslEnd` | SSL/TLS handshake |\n| **Send** | `sendStart` → `sendEnd` | Time to send request |\n| **Wait** | `sendEnd` → `receiveHeadersStart` | Waiting for server response (TTFB) |\n| **Receive** | `receiveHeadersStart` → `receiveHeadersEnd` | Time to receive response headers |\n\n!!! tip \"Time to First Byte (TTFB)\"\n    TTFB is the \"Wait\" phase - the time between sending the request and receiving the first byte of the response. This is crucial for performance analysis.\n\n## Best Practices\n\n### 1. Enable Network Events Only When Needed\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def best_practice_enable():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # ✅ Good: Enable before navigation, disable after\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        logs = await tab.get_network_logs()\n        await tab.disable_network_events()\n        \n        # ❌ Bad: Leaving it enabled throughout entire session\n        # await tab.enable_network_events()\n        # ... long automation session ...\n```\n\n### 2. Filter Logs to Reduce Memory Usage\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def best_practice_filter():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        \n        # ✅ Good: Filter for specific requests\n        api_logs = await tab.get_network_logs(filter='/api/')\n        \n        # ❌ Bad: Getting all logs when you only need specific ones\n        all_logs = await tab.get_network_logs()\n        filtered = [log for log in all_logs if '/api/' in log['params']['request']['url']]\n```\n\n### 3. Handle Missing Fields Safely\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def best_practice_safe_access():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        \n        logs = await tab.get_network_logs()\n        \n        # ✅ Good: Safe access with .get()\n        for log in logs:\n            params = log.get('params', {})\n            request = params.get('request', {})\n            url = request.get('url', 'Unknown')\n            post_data = request.get('postData')  # May be None\n            \n            if post_data:\n                print(f\"POST data: {post_data}\")\n        \n        # ❌ Bad: Direct access can raise KeyError\n        # url = log['params']['request']['url']\n        # post_data = log['params']['request']['postData']  # May not exist!\n```\n\n### 4. Use Event Callbacks for Real-Time Needs\n\n```python\nimport asyncio\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\n\n# ✅ Good: Real-time monitoring with callbacks\nasync def on_request(event: RequestWillBeSentEvent):\n    print(f\"New request: {event['params']['request']['url']}\")\n\nawait tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, on_request)\n\n# ❌ Bad: Polling logs repeatedly (inefficient)\nwhile True:\n    logs = await tab.get_network_logs()\n    # Process logs...\n    await asyncio.sleep(0.5)  # Wasteful!\n```\n\n## See Also\n\n- **[CDP Network Domain](../../deep-dive/network-capabilities.md)** - Deep dive into network capabilities\n- **[Event System](../advanced/event-system.md)** - Understanding Pydoll's event architecture\n- **[Request Interception](interception.md)** - Modifying requests and responses\n\n"
  },
  {
    "path": "docs/en/features/network/network-recording.md",
    "content": "# HAR Network Recording\n\nCapture all network activity during a browser session and export it as a standard HAR (HTTP Archive) 1.2 file. Perfect for debugging, performance analysis, and test fixtures.\n\n!!! tip \"Debug Like a Pro\"\n    HAR files are the industry standard for recording network traffic. You can import them directly into Chrome DevTools, Charles Proxy, or any HAR viewer for detailed analysis.\n\n## Why Use HAR Recording?\n\n| Use Case | Benefit |\n|----------|---------|\n| Debugging failed requests | See exact headers, timing, and response bodies |\n| Performance analysis | Identify slow requests and bottlenecks |\n| API documentation | Capture real request/response pairs |\n| Test fixtures | Record real traffic for test mocking |\n\n## Quick Start\n\nRecord all network traffic during a page navigation:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def record_traffic():\n    async with Chrome() as browser:\n        tab = await browser.start()\n\n        async with tab.request.record() as capture:\n            await tab.go_to('https://example.com')\n\n        # Save the capture as a HAR file\n        capture.save('flow.har')\n        print(f'Captured {len(capture.entries)} requests')\n\nasyncio.run(record_traffic())\n```\n\n## Recording API\n\n### `tab.request.record(resource_types=None)`\n\nContext manager that captures network traffic on the tab.\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `resource_types` | `list[ResourceType] \\| None` | Optional list of resource types to capture. When `None` (default), all types are captured. |\n\n```python\nasync with tab.request.record() as capture:\n    # All network activity inside this block is captured\n    await tab.go_to('https://example.com')\n    await (await tab.find(id='search')).type_text('pydoll')\n    await (await tab.find(type='submit')).click()\n```\n\nThe `capture` object (`HarCapture`) provides:\n\n| Property/Method | Description |\n|----------------|-------------|\n| `capture.entries` | List of captured HAR entries |\n| `capture.to_dict()` | Full HAR 1.2 dict (for custom processing) |\n| `capture.save(path)` | Save as HAR JSON file |\n\n### Filtering by Resource Type\n\nRecord only specific resource types instead of all traffic:\n\n```python\nfrom pydoll.protocol.network.types import ResourceType\n\n# Record only fetch/XHR requests (skip documents, images, etc.)\nasync with tab.request.record(\n    resource_types=[ResourceType.FETCH, ResourceType.XHR]\n) as capture:\n    await tab.go_to('https://example.com')\n\n# Record only document and stylesheet requests\nasync with tab.request.record(\n    resource_types=[ResourceType.DOCUMENT, ResourceType.STYLESHEET]\n) as capture:\n    await tab.go_to('https://example.com')\n```\n\nAvailable `ResourceType` values:\n\n| Value | Description |\n|-------|-------------|\n| `ResourceType.DOCUMENT` | HTML documents |\n| `ResourceType.STYLESHEET` | CSS stylesheets |\n| `ResourceType.SCRIPT` | JavaScript files |\n| `ResourceType.IMAGE` | Images |\n| `ResourceType.FONT` | Web fonts |\n| `ResourceType.MEDIA` | Audio/video |\n| `ResourceType.FETCH` | Fetch API requests |\n| `ResourceType.XHR` | XMLHttpRequest calls |\n| `ResourceType.WEB_SOCKET` | WebSocket connections |\n| `ResourceType.OTHER` | Other resource types |\n\n### Saving Captures\n\n```python\n# Save as HAR file (can be opened in Chrome DevTools)\ncapture.save('flow.har')\n\n# Save to a nested directory (created automatically)\ncapture.save('recordings/session1/flow.har')\n\n# Access the raw HAR dict for custom processing\nhar_dict = capture.to_dict()\nprint(har_dict['log']['version'])  # \"1.2\"\n```\n\n### Inspecting Entries\n\n```python\nasync with tab.request.record() as capture:\n    await tab.go_to('https://example.com')\n\nfor entry in capture.entries:\n    req = entry['request']\n    resp = entry['response']\n    print(f\"{req['method']} {req['url']} -> {resp['status']}\")\n```\n\n## Advanced Usage\n\n### Filtering Captured Entries\n\n```python\nasync with tab.request.record() as capture:\n    await tab.go_to('https://example.com')\n\n# Filter only API calls\napi_entries = [\n    e for e in capture.entries\n    if '/api/' in e['request']['url']\n]\n\n# Filter only failed requests\nfailed = [\n    e for e in capture.entries\n    if e['response']['status'] >= 400\n]\n```\n\n### Custom HAR Processing\n\n```python\nhar = capture.to_dict()\n\n# Count requests by type\nfrom collections import Counter\ntypes = Counter(\n    e.get('_resourceType', 'Other')\n    for e in har['log']['entries']\n)\nprint(types)  # Counter({'Document': 1, 'Script': 5, 'Stylesheet': 3, ...})\n```\n\n## HAR File Format\n\nThe exported HAR follows the [HAR 1.2 specification](http://www.softwareishard.com/blog/har-12-spec/). Each entry contains:\n\n- **Request**: method, URL, headers, query parameters, POST data\n- **Response**: status, headers, body content (text or base64-encoded)\n- **Timings**: DNS, connect, SSL, send, wait (TTFB), receive\n- **Metadata**: server IP, connection ID, resource type\n\n!!! note \"Response Bodies\"\n    Response bodies are captured automatically after each request completes. Binary content (images, fonts, etc.) is stored as base64-encoded strings.\n"
  },
  {
    "path": "docs/en/index.md",
    "content": "<p align=\"center\">\n    <img src=\"resources/images/logo.png\" alt=\"Pydoll Logo\" /> <br><br>\n</p>\n\n<p align=\"center\">\n    <a href=\"https://codecov.io/gh/autoscrape-labs/pydoll\">\n        <img src=\"https://codecov.io/gh/autoscrape-labs/pydoll/graph/badge.svg?token=40I938OGM9\"/> \n    </a>\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/tests.yml/badge.svg\" alt=\"Tests\">\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/ruff-ci.yml/badge.svg\" alt=\"Ruff CI\">\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/release.yml/badge.svg\" alt=\"Release\">\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/mypy.yml/badge.svg\" alt=\"MyPy CI\">\n</p>\n\n\n# Welcome to Pydoll\n\nHey there! Thanks for checking out Pydoll, the next generation of browser automation for Python. If you're tired of wrestling with webdrivers and looking for a smoother, more reliable way to automate browsers, you're in the right place.\n\n## What is Pydoll?\n\nPydoll is revolutionizing browser automation by **eliminating the need for webdrivers** completely! Unlike other solutions that rely on external dependencies, Pydoll connects directly to browsers using their DevTools Protocol, providing a seamless and reliable automation experience with native asynchronous performance.\n\nWhether you're scraping data, [testing web applications](https://www.lambdatest.com/web-testing), or automating repetitive tasks, Pydoll makes it surprisingly easy with its intuitive API and powerful features.\n\n## Installation\n\nCreate and activate a [virtual environment](https://docs.python.org/3/tutorial/venv.html) first, then install Pydoll:\n\n<div class=\"termy\">\n```bash\n$ pip install pydoll-python\n\n---> 100%\n```\n</div>\n\nFor the latest development version, you can install directly from GitHub:\n\n```bash\n$ pip install git+https://github.com/autoscrape-labs/pydoll.git\n```\n\n## Why Choose Pydoll?\n\n- **Genuine Simplicity**: We don't want you wasting time configuring drivers or dealing with compatibility issues. With Pydoll, you install and you're ready to automate.\n- **Truly Human Interactions**: Our algorithms simulate real human behavior patterns - from timing between clicks to how the mouse moves across the screen.\n- **Native Async Performance**: Built from the ground up with `asyncio`, Pydoll doesn't just support asynchronous operations - it was designed for them.\n- **Integrated Intelligence**: Automatic bypass of Cloudflare Turnstile and reCAPTCHA v3 captchas, without external services or complex configurations.\n- **Powerful Network Monitoring**: Intercept, modify, and analyze all network traffic with ease, giving you complete control over requests.\n- **Event-Driven Architecture**: React to page events, network requests, and user interactions in real-time.\n- **Intuitive Element Finding**: Modern `find()` and `query()` methods that make sense and work as you'd expect.\n- **Robust Type Safety**: Comprehensive type system for better IDE support and error prevention.\n\n\nReady to dive in? The following pages will guide you through installation, basic usage, and advanced features to help you get the most out of Pydoll.\n\nLet's start automating the web, the right way! 🚀\n\n## Quick Start Guide: A simple example\n\nLet's start with a practical example. The following script will open the Pydoll GitHub repository and star it:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://github.com/autoscrape-labs/pydoll')\n        \n        star_button = await tab.find(\n            tag_name='button',\n            timeout=5,\n            raise_exc=False\n        )\n        if not star_button:\n            print(\"Ops! The button was not found.\")\n            return\n\n        await star_button.click()\n        await asyncio.sleep(3)\n\nasyncio.run(main())\n```\n\nThis example demonstrates how to navigate to a website, wait for an element to appear, and interact with it. You can adapt this pattern to automate many different web tasks.\n\n??? note \"Or use without context manager...\"\n    If you prefer not to use the context manager pattern, you can manually manage the browser instance:\n    \n    ```python\n    import asyncio\n    from pydoll.browser.chromium import Chrome\n    \n    async def main():\n        browser = Chrome()\n        tab = await browser.start()\n        await tab.go_to('https://github.com/autoscrape-labs/pydoll')\n        \n        star_button = await tab.find(\n            tag_name='button',\n            timeout=5,\n            raise_exc=False\n        )\n        if not star_button:\n            print(\"Ops! The button was not found.\")\n            return\n\n        await star_button.click()\n        await asyncio.sleep(3)\n        await browser.stop()\n    \n    asyncio.run(main())\n    ```\n    \n    Note that when not using the context manager, you'll need to explicitly call `browser.stop()` to release resources.\n\n## Extended Example: Custom Browser Configuration\n\nFor more advanced usage scenarios, Pydoll allows you to customize your browser configuration using the `ChromiumOptions` class. This is useful when you need to:\n\n- Run in headless mode (no visible browser window)\n- Specify a custom browser executable path\n- Configure proxies, user agents, or other browser settings\n- Set window dimensions or startup arguments\n\nHere's an example showing how to use custom options for Chrome:\n\n```python hl_lines=\"8-12 30-32 34-38\"\nimport asyncio\nimport os\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def main():\n    options = ChromiumOptions()\n    options.binary_location = '/usr/bin/google-chrome-stable'\n    options.add_argument('--headless=new')\n    options.add_argument('--start-maximized')\n    options.add_argument('--disable-notifications')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://github.com/autoscrape-labs/pydoll')\n        \n        star_button = await tab.find(\n            tag_name='button',\n            timeout=5,\n            raise_exc=False\n        )\n        if not star_button:\n            print(\"Ops! The button was not found.\")\n            return\n\n        await star_button.click()\n        await asyncio.sleep(3)\n\n        screenshot_path = os.path.join(os.getcwd(), 'pydoll_repo.png')\n        await tab.take_screenshot(path=screenshot_path)\n        print(f\"Screenshot saved to: {screenshot_path}\")\n\n        base64_screenshot = await tab.take_screenshot(as_base64=True)\n\n        repo_description_element = await tab.find(\n            class_name='f4.my-3'\n        )\n        repo_description = await repo_description_element.text\n        print(f\"Repository description: {repo_description}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nThis extended example demonstrates:\n\n1. Creating and configuring browser options\n2. Setting a custom Chrome binary path\n3. Enabling headless mode for invisible operation\n4. Setting additional browser flags\n5. Taking screenshots (especially useful in headless mode)\n\n??? info \"About Chromium Options\"\n    The `options.add_argument()` method allows you to pass any Chromium command-line argument to customize browser behavior. There are hundreds of available options to control everything from networking to rendering behavior.\n    \n    Common Chrome Options\n    \n    ```python\n    # Performance & Behavior Options\n    options.add_argument('--headless=new')         # Run Chrome in headless mode\n    options.add_argument('--disable-gpu')          # Disable GPU hardware acceleration\n    options.add_argument('--no-sandbox')           # Disable sandbox (use with caution)\n    options.add_argument('--disable-dev-shm-usage') # Overcome limited resource issues\n    \n    # Appearance Options\n    options.add_argument('--start-maximized')      # Start with maximized window\n    options.add_argument('--window-size=1920,1080') # Set specific window size\n    options.add_argument('--hide-scrollbars')      # Hide scrollbars\n    \n    # Network Options\n    options.add_argument('--proxy-server=socks5://127.0.0.1:9050') # Use proxy\n    options.add_argument('--disable-extensions')   # Disable extensions\n    options.add_argument('--disable-notifications') # Disable notifications\n    \n    # Privacy & Security\n    options.add_argument('--incognito')            # Run in incognito mode\n    options.add_argument('--disable-infobars')     # Disable infobars\n    ```\n    \n    Complete Reference Guides\n    \n    For a comprehensive list of all available Chrome command-line arguments, refer to these resources:\n    \n    - [Chromium Command Line Switches](https://peter.sh/experiments/chromium-command-line-switches/) - Complete reference list\n    - [Chrome Flags](chrome://flags) - Enter this in your Chrome browser address bar to see experimental features\n    - [Chromium Source Code Flags](https://source.chromium.org/chromium/chromium/src/+/main:chrome/common/chrome_switches.cc) - Direct source code reference\n    \n    Remember that some options may behave differently across Chrome versions, so it's a good practice to test your configuration when upgrading Chrome.\n\nWith these configurations, you can run Pydoll in various environments, including CI/CD pipelines, servers without displays, or Docker containers.\n\nContinue reading the documentation to explore Pydoll's powerful features for handling captchas, working with multiple tabs, interacting with elements, and more.\n\n## Minimal Dependencies\n\nOne of Pydoll's advantages is its lightweight footprint. Unlike other browser automation tools that require numerous dependencies, Pydoll is intentionally designed to be minimalist while maintaining powerful capabilities.\n\n### Core Dependencies\n\nPydoll relies on just a few carefully selected packages:\n\n```\npython = \"^3.10\"\nwebsockets = \"^13.1\"\naiohttp = \"^3.9.5\"\naiofiles = \"^23.2.1\"\nbs4 = \"^0.0.2\"\n```\n\nThat's it! This minimal dependency approach means:\n\n- **Faster installation** - No complex dependency tree to resolve\n- **Fewer conflicts** - Reduced chance of version conflicts with other packages\n- **Smaller footprint** - Lower disk space usage\n- **Better security** - Smaller attack surface and dependency-related vulnerabilities\n- **Easier updates** - Simpler maintenance and fewer breaking changes\n\nThe small number of dependencies also contributes to Pydoll's reliability and performance, as there are fewer external factors that could impact its operation.\n\n## Top Sponsors\n\n<a href=\"https://substack.thewebscraping.club/p/pydoll-webdriver-scraping?utm_source=github&utm_medium=repo&utm_campaign=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n  <img src=\"resources/images/banner-the-webscraping-club.png\" alt=\"The Web Scraping Club\" />\n</a>\n\n<sub>Read a full review of Pydoll on <b><a href=\"https://substack.thewebscraping.club/p/pydoll-webdriver-scraping?utm_source=github&utm_medium=repo&utm_campaign=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">The Web Scraping Club</a></b>, the #1 newsletter dedicated to web scraping.</sub>\n\n## Sponsors\n\nThe support from sponsors is essential to keep the project alive, evolving, and accessible to the entire community. Each partnership helps cover costs, drive new features, and ensure ongoing development. We are truly grateful to everyone who believes in and supports the project!\n\n<div class=\"sponsors-grid\">\n  <a href=\"https://www.thordata.com/?ls=github&lk=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n    <img src=\"resources/images/Thordata-logo.png\" alt=\"Thordata\" />\n  </a>\n  <a href=\"https://www.testmuai.com/?utm_medium=sponsor&utm_source=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n    <img src=\"resources/images/logo-lamda-test.svg\" alt=\"LambdaTest\" />\n  </a>\n  <a href=\"https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n    <img src=\"resources/images/capsolver-logo.png\" alt=\"CapSolver\" />\n  </a>\n</div>\n\n<p>\n  <a href=\"https://github.com/sponsors/thalissonvs\" target=\"_blank\" rel=\"noopener\">Become a sponsor</a>\n</p>\n\n## License\n\nPydoll is released under the MIT License, which gives you the freedom to use, modify, and distribute the code with minimal restrictions. This permissive license makes Pydoll suitable for both personal and commercial projects.\n\n??? info \"View Full MIT License Text\"\n    ```\n    MIT License\n    \n    Copyright (c) 2023 Pydoll Contributors\n    \n    Permission is hereby granted, free of charge, to any person obtaining a copy\n    of this software and associated documentation files (the \"Software\"), to deal\n    in the Software without restriction, including without limitation the rights\n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n    copies of the Software, and to permit persons to whom the Software is\n    furnished to do so, subject to the following conditions:\n    \n    The above copyright notice and this permission notice shall be included in all\n    copies or substantial portions of the Software.\n    \n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n    SOFTWARE.\n    ```\n"
  },
  {
    "path": "docs/pt/api/browser/chrome.md",
    "content": "# Brower Chrome\n \n::: pydoll.browser.chromium.Chrome\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2 "
  },
  {
    "path": "docs/pt/api/browser/edge.md",
    "content": "# Brower Edge\n \n::: pydoll.browser.chromium.Edge\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2 "
  },
  {
    "path": "docs/pt/api/browser/managers.md",
    "content": "# Gerenciadores do Navegador\n\nO módulo de gerenciadores (managers) fornece classes especializadas para gerenciar diferentes aspectos do ciclo de vida e configuração do navegador.\n\n## Visão Geral\n\nOs gerenciadores do navegador lidam com responsabilidades específicas na automação do navegador:\n\n::: pydoll.browser.managers\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Classes de Gerenciadores\n\n### Gerenciador de Processo do Navegador\nGerencia o ciclo de vida do processo do navegador, incluindo iniciar, parar e monitorar os processos do navegador.\n\n::: pydoll.browser.managers.browser_process_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n### Gerenciador de Opções do Navegador\nLida com as opções de configuração do navegador e argumentos de linha de comando.\n\n::: pydoll.browser.managers.browser_options_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n### Gerenciador de Proxy\nGerencia a configuração de proxy e autenticação para instâncias do navegador.\n\n::: pydoll.browser.managers.proxy_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n### Gerenciador de Diretório Temporário\nLida com a criação e limpeza de diretórios temporários usados pelas instâncias do navegador.\n\n::: pydoll.browser.managers.temp_dir_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## Uso\n\nOs gerenciadores são normalmente usados internamente pelas classes do navegador, como `Chrome` e `Edge`. Eles fornecem funcionalidade modular que pode ser composta:\n\n```python\nfrom pydoll.browser.managers.proxy_manager import ProxyManager\nfrom pydoll.browser.managers.temp_dir_manager import TempDirManager\n\n# Gerenciadores são usados internamente pelas classes do navegador\n# O uso direto é apenas para cenários avançados\nproxy_manager = ProxyManager()\ntemp_manager = TempDirManager()\n```\n\n!!! note \"Uso Interno\"\n    Esses gerenciadores são usados principalmente internamente pelas classes do navegador. O uso direto é recomendado apenas para cenários avançados ou ao estender a biblioteca.\n"
  },
  {
    "path": "docs/pt/api/browser/options.md",
    "content": "# Browser Options\n\n## ChromiumOptions\n\n::: pydoll.browser.options.ChromiumOptions\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## Interface Options\n\n::: pydoll.browser.interfaces.Options\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## Interface BrowserOptionsManager \n\n::: pydoll.browser.interfaces.BrowserOptionsManager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3 "
  },
  {
    "path": "docs/pt/api/browser/requests.md",
    "content": "# Requisições do Navegador\n\nO módulo de requisições (requests) fornece capacidades de requisição HTTP dentro do contexto do navegador, permitindo chamadas de API contínuas que herdam o estado de sessão, cookies e autenticação do navegador.\n\n## Visão Geral\n\nO módulo de requisições do navegador oferece uma interface semelhante à do `requests` para fazer chamadas HTTP diretamente dentro do contexto JavaScript do navegador. Esta abordagem oferece várias vantagens sobre as bibliotecas HTTP tradicionais:\n\n- **Herança de sessão**: Manipulação automática de cookies, autenticação e CORS\n- **Contexto do navegador**: As requisições são executadas no mesmo contexto de segurança da página\n- **Sem malabarismo de sessão**: Elimina a necessidade de transferir cookies e tokens entre a automação e as chamadas de API\n- **Compatibilidade com SPA**: Perfeito para Single Page Applications (Aplicações de Página Única) com fluxos de autenticação complexos\n\n## Classe Request\n\nA interface principal para fazer requisições HTTP dentro do contexto do navegador.\n\n::: pydoll.browser.requests.request.Request\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n      group_by_category: true\n      members_order: source\n      filters:\n        - \"!^__\"\n\n## Classe Response\n\nRepresenta a resposta de requisições HTTP, fornecendo uma interface familiar semelhante à biblioteca `requests`.\n\n::: pydoll.browser.requests.response.Response\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n      group_by_category: true\n      members_order: source\n      filters:\n        - \"!^__\"\n\n## Exemplos de Uso\n\n### Métodos HTTP Básicos\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to(\"https://api.example.com\")\n    \n    # Requisição GET\n    response = await tab.request.get(\"/users/123\")\n    user_data = await response.json()\n    \n    # Requisição POST\n    response = await tab.request.post(\"/users\", json={\n        \"name\": \"John Doe\",\n        \"email\": \"john@example.com\"\n    })\n    \n    # Requisição PUT com cabeçalhos\n    response = await tab.request.put(\"/users/123\", \n        json={\"name\": \"Jane Doe\"},\n        headers={\"Authorization\": \"Bearer token123\"}\n    )\n```\n\n### Manipulação de Resposta\n\n```python\n# Verificar status da resposta\nif response.ok:\n    print(f\"Sucesso: {response.status_code}\")\nelse:\n    print(f\"Erro: {response.status_code}\")\n    response.raise_for_status()  # Levanta HTTPError para 4xx/5xx\n\n# Acessar dados da resposta\ntext_data = response.text\njson_data = await response.json()\nraw_bytes = response.content\n\n# Inspecionar cabeçalhos e cookies\nprint(\"Cabeçalhos da resposta:\", response.headers)\nprint(\"Cabeçalhos da requisição:\", response.request_headers)\nfor cookie in response.cookies:\n    print(f\"Cookie: {cookie.name}={cookie.value}\")\n```\n\n### Recursos Avançados\n\n```python\n# Requisição com cabeçalhos e parâmetros customizados\nresponse = await tab.request.get(\"/search\", \n    params={\"q\": \"python\", \"limit\": 10},\n    headers={\n        \"User-Agent\": \"Custom Bot 1.0\",\n        \"Accept\": \"application/json\"\n    }\n)\n\n# Simulação de upload de arquivo\nresponse = await tab.request.post(\"/upload\",\n    data={\"description\": \"Test file\"},\n    files={\"file\": (\"test.txt\", \"file content\", \"text/plain\")}\n)\n\n# Submissão de dados de formulário\nresponse = await tab.request.post(\"/login\",\n    data={\"username\": \"user\", \"password\": \"pass\"}\n)\n```\n\n## Integração com a Aba (Tab)\n\nA funcionalidade de requisição é acessada através da propriedade `tab.request`, que fornece uma instância `Request` singleton para cada aba:\n\n```python\n# Cada aba tem sua própria instância de requisição\ntab1 = await browser.get_tab(0)\ntab2 = await browser.new_tab()\n\n# Estas são instâncias de Request separadas\nrequest1 = tab1.request  # Requisição vinculada à tab1\nrequest2 = tab2.request  # Requisição vinculada à tab2\n\n# Requisições herdam o contexto da aba\nawait tab1.go_to(\"https://site1.com\")\nawait tab2.go_to(\"https://site2.com\")\n\n# Estas requisições terão contextos de cookie/sessão diferentes\nresponse1 = await tab1.request.get(\"/api/data\")  # Usa cookies de site1.com\nresponse2 = await tab2.request.get(\"/api/data\")  # Usa cookies de site2.com\n```\n\n!!! tip \"Automação Híbrida\"\n    Este módulo é particularmente poderoso para cenários de automação híbrida onde você precisa combinar interações de UI com chamadas de API. Por exemplo, faça login através da UI, depois use a sessão autenticada para chamadas de API sem manipular manualmente cookies ou tokens."
  },
  {
    "path": "docs/pt/api/browser/tab.md",
    "content": "# Tab\n\n::: pydoll.browser.tab.Tab\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/pt/api/commands/browser.md",
    "content": "# Comandos do Navegador\n\nOs comandos do navegador fornecem controle de baixo nível sobre as instâncias do navegador e sua configuração.\n\n## Visão Geral\n\nO módulo de comandos do navegador lida com operações em nível de navegador, como informações de versão, gerenciamento de alvos (targets) e configurações globais do navegador.\n\n::: pydoll.commands.browser_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Uso\n\nOs comandos do navegador são tipicamente usados internamente pelas classes do navegador para gerenciar instâncias do navegador:\n\n```python\nfrom pydoll.commands.browser_commands import get_version\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Obter informações da versão do navegador\nconnection = ConnectionHandler()\nversion_info = await get_version(connection)\n```\n\n## Comandos Disponíveis\n\nO módulo de comandos do navegador fornece funções para:\n\n- Obter informações de versão do navegador e user agent\n- Gerenciar alvos (targets) do navegador (abas, janelas)\n- Controlar configurações e permissões globais do navegador\n- Lidar com eventos do ciclo de vida do navegador\n\n!!! note \"Uso Interno\"\n    Esses comandos são usados principalmente internamente pelas classes de navegador `Chrome` e `Edge`. O uso direto é recomendado apenas para cenários avançados."
  },
  {
    "path": "docs/pt/api/commands/dom.md",
    "content": "# Comandos DOM\n\nOs comandos DOM fornecem funcionalidade abrangente para interagir com o Document Object Model (Modelo de Objeto de Documento) das páginas web.\n\n## Visão Geral\n\nO módulo de comandos DOM é um dos módulos mais importantes no Pydoll, fornecendo toda a funcionalidade necessária para encontrar, interagir com e manipular elementos HTML em páginas web.\n\n::: pydoll.commands.dom_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Uso\n\nOs comandos DOM são usados extensivamente pela classe `WebElement` e pelos métodos de localização de elementos:\n\n```python\nfrom pydoll.commands.dom_commands import query_selector, get_attributes\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Encontrar elemento e obter seus atributos\nconnection = ConnectionHandler()\nnode_id = await query_selector(connection, selector=\"#username\")\nattributes = await get_attributes(connection, node_id=node_id)\n```\n\n## Funcionalidades Principais\n\nO módulo de comandos DOM fornece funções para:\n\n### Localização de Elementos\n- `query_selector()` - Encontrar elemento único por seletor CSS\n- `query_selector_all()` - Encontrar múltiplos elementos por seletor CSS\n- `get_document()` - Obter o nó raiz do documento\n\n### Interação com Elementos\n- `click_element()` - Clicar em elementos\n- `focus_element()` - Focar em elementos\n- `set_attribute_value()` - Definir atributos do elemento\n- `get_attributes()` - Obter atributos do elemento\n\n### Informações do Elemento\n- `get_box_model()` - Obter posicionamento e dimensões do elemento\n- `describe_node()` - Obter informações detalhadas do elemento\n- `get_outer_html()` - Obter o conteúdo HTML do elemento\n\n### Manipulação do DOM\n- `remove_node()` - Remover elementos do DOM\n- `set_node_value()` - Definir valores do elemento\n- `request_child_nodes()` - Obter elementos filhos\n\n!!! tip \"APIs de Alto Nível\"\n    Embora esses comandos forneçam acesso poderoso de baixo nível, a maioria dos usuários deve usar os métodos de nível superior da classe `WebElement`, como `click()`, `type_text()` e `get_attribute()`, que usam esses comandos internamente."
  },
  {
    "path": "docs/pt/api/commands/fetch.md",
    "content": "# Comandos Fetch\n\nOs comandos Fetch fornecem capacidades avançadas de manipulação e interceptação de requisições de rede usando o domínio da API Fetch.\n\n## Visão Geral\n\nO módulo de comandos fetch permite o gerenciamento sofisticado de requisições de rede, incluindo modificação de requisições, interceptação de respostas e manipulação de autenticação.\n\n::: pydoll.commands.fetch_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Uso\n\nOs comandos Fetch são usados para interceptação avançada de rede e manipulação de requisições:\n\n```python\nfrom pydoll.commands.fetch_commands import enable, request_paused, continue_request\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Habilitar domínio fetch\nconnection = ConnectionHandler()\nawait enable(connection, patterns=[{\n    \"urlPattern\": \"*\",\n    \"requestStage\": \"Request\"\n}])\n\n# Lidar com requisições pausadas\nasync def handle_paused_request(request_id, request):\n    # Modificar requisição ou continuar como está\n    await continue_request(connection, request_id=request_id)\n```\n\n## Funcionalidades Principais\n\nO módulo de comandos fetch fornece funções para:\n\n### Interceptação de Requisição\n- `enable()` - Habilitar domínio fetch com padrões\n- `disable()` - Desabilitar domínio fetch\n- `continue_request()` - Continuar requisições interceptadas\n- `fail_request()` - Falhar requisições com erros específicos\n\n### Modificação de Requisição\n- Modificar cabeçalhos da requisição\n- Alterar URLs da requisição\n- Alterar métodos da requisição (GET, POST, etc.)\n- Modificar corpos (bodies) da requisição\n\n### Manipulação de Resposta\n- `fulfill_request()` - Fornecer respostas customizadas\n- `get_response_body()` - Obter conteúdo da resposta\n- Modificação de cabeçalho de resposta\n- Controle do código de status da resposta\n\n### Autenticação\n- `continue_with_auth()` - Lidar com desafios de autenticação\n- Suporte a autenticação básica\n- Fluxos de autenticação customizados\n\n## Recursos Avançados\n\n### Interceptação Baseada em Padrões\n```python\n# Interceptar padrões de URL específicos\npatterns = [\n    {\"urlPattern\": \"*/api/*\", \"requestStage\": \"Request\"},\n    {\"urlPattern\": \"*.js\", \"requestStage\": \"Response\"},\n    {\"urlPattern\": \"https://example.com/*\", \"requestStage\": \"Request\"}\n]\n\nawait enable(connection, patterns=patterns)\n```\n\n### Modificação de Requisição\n```python\n# Modificar requisições interceptadas\nasync def modify_request(request_id, request):\n    # Adicionar cabeçalho de autenticação\n    headers = request.headers.copy()\n    headers[\"Authorization\"] = \"Bearer token123\"\n    \n    # Continuar com cabeçalhos modificados\n    await continue_request(\n        connection,\n        request_id=request_id,\n        headers=headers\n    )\n```\n\n### Simulação de Resposta (Mocking)\n```python\n# Simular (mockar) respostas de API\nawait fulfill_request(\n    connection,\n    request_id=request_id,\n    response_code=200,\n    response_headers=[\n        {\"name\": \"Content-Type\", \"value\": \"application/json\"},\n        {\"name\": \"Access-Control-Allow-Origin\", \"value\": \"*\"}\n    ],\n    body='{\"status\": \"success\", \"data\": {\"mocked\": true}}'\n)\n```\n\n### Manipulação de Autenticação\n```python\n# Lidar com desafios de autenticação\nawait continue_with_auth(\n    connection,\n    request_id=request_id,\n    auth_challenge_response={\n        \"response\": \"ProvideCredentials\",\n        \"username\": \"user\",\n        \"password\": \"pass\"\n    }\n)\n```\n\n## Estágios da Requisição\n\nOs comandos Fetch podem interceptar requisições em diferentes estágios:\n\n| Estágio | Descrição | Casos de Uso |\n|-------|-------------|-----------|\n| Requisição | Antes da requisição ser enviada | Modificar cabeçalhos, URL, método |\n| Resposta | Após a resposta ser recebida | Simular respostas, modificar conteúdo |\n\n## Manipulação de Erros\n\n```python\n# Falhar requisições com erros específicos\nawait fail_request(\n    connection,\n    request_id=request_id,\n    error_reason=\"ConnectionRefused\"  # ou \"AccessDenied\", \"TimedOut\", etc.\n)\n```\n\n## Integração com Comandos de Rede (Network)\n\nOs comandos Fetch trabalham em conjunto com os comandos de rede (Network), mas fornecem controle mais granular:\n\n- **Comandos de Rede (Network)**: Monitoramento e controle de rede mais amplos\n- **Comandos Fetch**: Interceptação e modificação específicas de requisição/resposta\n\n!!! tip \"Considerações de Performance\"\n    A interceptação do Fetch pode impactar a performance de carregamento da página. Use padrões de URL específicos e desabilite quando não for necessário para minimizar a sobrecarga (overhead)."
  },
  {
    "path": "docs/pt/api/commands/index.md",
    "content": "# Visão Geral dos Comandos\n\nO módulo de Comandos (Commands) fornece interfaces de alto nível para interagir com os domínios do Chrome DevTools Protocol (CDP). Cada módulo de comando corresponde a um domínio CDP específico e fornece métodos para executar várias operações do navegador.\n\n## Módulos de Comando Disponíveis\n\n### Comandos do Navegador (Browser)\n- **Módulo**: `browser_commands.py`\n- **Propósito**: Operações em nível de navegador e gerenciamento de janelas\n- **Documentação**: [Comandos do Navegador](browser.md)\n\n### Comandos DOM\n- **Módulo**: `dom_commands.py`\n- **Propósito**: Manipulação da árvore DOM e operações de elementos\n- **Documentação**: [Comandos DOM](dom.md)\n\n### Comandos de Entrada (Input)\n- **Módulo**: `input_commands.py`\n- **Propósito**: Simulação de eventos de entrada (teclado, mouse, toque)\n- **Documentação**: [Comandos de Entrada](input.md)\n\n### Comandos de Rede (Network)\n- **Módulo**: `network_commands.py`\n- **Propósito**: Monitoramento de rede e interceptação de requisições\n- **Documentação**: [Comandos de Rede](network.md)\n\n### Comandos de Página (Page)\n- **Módulo**: `page_commands.py`\n- **Propósito**: Gerenciamento do ciclo de vida da página e navegação\n- **Documentação**: [Comandos de Página](page.md)\n\n### Comandos de Tempo de Execução (Runtime)\n- **Módulo**: `runtime_commands.py`\n- **Propósito**: Execução de JavaScript e gerenciamento de tempo de execução\n- **Documentação**: [Comandos de Tempo de Execução](runtime.md)\n\n### Comandos de Armazenamento (Storage)\n- **Módulo**: `storage_commands.py`\n- **Propósito**: Acesso ao armazenamento do navegador (cookies, local storage, etc.)\n- **Documentação**: [Comandos de Armazenamento](storage.md)\n\n### Comandos de Alvo (Target)\n- **Módulo**: `target_commands.py`\n- **Propósito**: Gerenciamento de alvos (targets) e operações de aba\n- **Documentação**: [Comandos de Alvo](target.md)\n\n### Comandos Fetch\n- **Módulo**: `fetch_commands.py`\n- **Propósito**: Interceptação e modificação de requisições de rede\n- **Documentação**: [Comandos Fetch](fetch.md)\n\n## Padrão de Uso\n\nOs comandos são tipicamente acessados através das instâncias do navegador (browser) ou aba (tab):\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\n# Inicializa o navegador\nbrowser = Chrome()\nawait browser.start()\n\n# Obtém a aba ativa\ntab = await browser.get_active_tab()\n\n# Usa comandos através da aba\nawait tab.navigate(\"https://example.com\")\nelement = await tab.find(id=\"button\")\nawait element.click()\n```\n\n## Estrutura dos Comandos\n\nCada módulo de comando segue um padrão consistente:\n- **Métodos estáticos**: Para execução direta de comandos\n- **Dicas de tipo (Type hints)**: Segurança de tipo (type safety) completa com tipos de protocolo\n- **Tratamento de erros**: Tratamento de exceção adequado para erros CDP\n- **Documentação**: Docstrings abrangentes com exemplos"
  },
  {
    "path": "docs/pt/api/commands/input.md",
    "content": "# Comandos de Entrada (Input)\n\nOs comandos de entrada lidam com interações de mouse e teclado, fornecendo simulação de entrada semelhante à humana.\n\n## Visão Geral\n\nO módulo de comandos de entrada fornece funcionalidade para simular a entrada do usuário, incluindo movimentos do mouse, cliques, digitação no teclado e pressionamento de teclas.\n\n::: pydoll.commands.input_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Uso\n\nOs comandos de entrada são usados por métodos de interação de elementos e podem ser usados diretamente para cenários de entrada avançados:\n\n```python\nfrom pydoll.commands.input_commands import dispatch_mouse_event, dispatch_key_event\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Simular clique do mouse\nconnection = ConnectionHandler()\nawait dispatch_mouse_event(\n    connection, \n    type=\"mousePressed\", \n    x=100, \n    y=200, \n    button=\"left\"\n)\n\n# Simular digitação do teclado\nawait dispatch_key_event(\n    connection,\n    type=\"keyDown\",\n    key=\"Enter\"\n)\n```\n\n## Funcionalidades Principais\n\nO módulo de comandos de entrada fornece funções para:\n\n### Eventos de Mouse\n- `dispatch_mouse_event()` - Cliques, movimentos e eventos de roda do mouse\n- Estados dos botões do mouse (esquerdo, direito, meio)\n- Posicionamento baseado em coordenadas\n- Operações de arrastar e soltar (drag and drop)\n\n### Eventos de Teclado\n- `dispatch_key_event()` - Eventos de pressionar e soltar tecla\n- `insert_text()` - Inserção direta de texto\n- Manipulação de teclas especiais (Enter, Tab, teclas de seta, etc.)\n- Teclas modificadoras (Ctrl, Alt, Shift)\n\n### Eventos de Toque (Touch)\n- Simulação de tela de toque\n- Gestos multitoque (multi-touch)\n- Coordenadas e pressão do toque\n\n## Comportamento Semelhante ao Humano\n\nOs comandos de entrada suportam padrões de comportamento semelhantes ao humano:\n\n- Curvas naturais de movimento do mouse\n- Velocidades e padrões de digitação realistas\n- Micro-atrasos aleatórios entre ações\n- Eventos de toque sensíveis à pressão\n\n!!! tip \"Métodos de Elemento\"\n    Para a maioria dos casos de uso, utilize os métodos de elemento de nível superior, como `element.click()` e `element.type_text()`, que fornecem uma API mais conveniente e lidam com cenários comuns automaticamente."
  },
  {
    "path": "docs/pt/api/commands/network.md",
    "content": "# Comandos de Rede (Network)\n\nOs comandos de rede fornecem controle abrangente sobre requisições de rede, respostas e comportamento de rede do navegador.\n\n## Visão Geral\n\nO módulo de comandos de rede habilita a interceptação de requisições, modificação de respostas, gerenciamento de cookies e capacidades de monitoramento de rede.\n\n::: pydoll.commands.network_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Uso\n\nOs comandos de rede são usados para cenários avançados como interceptação de requisições e monitoramento de rede:\n\n```python\nfrom pydoll.commands.network_commands import enable, set_request_interception\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Habilitar monitoramento de rede\nconnection = ConnectionHandler()\nawait enable(connection)\n\n# Habilitar interceptação de requisições\nawait set_request_interception(connection, patterns=[{\"urlPattern\": \"*\"}])\n```\n\n## Funcionalidades Principais\n\nO módulo de comandos de rede fornece funções para:\n\n### Gerenciamento de Requisições\n- `enable()` / `disable()` - Habilitar/desabilitar monitoramento de rede\n- `set_request_interception()` - Interceptar e modificar requisições\n- `continue_intercepted_request()` - Continuar ou modificar requisições interceptadas\n- `get_request_post_data()` - Obter dados do corpo (body) da requisição\n\n### Manipulação de Respostas\n- `get_response_body()` - Obter conteúdo da resposta\n- `fulfill_request()` - Fornecer respostas customizadas\n- `fail_request()` - Simular falhas de rede\n\n### Gerenciamento de Cookies\n- `get_cookies()` - Obter cookies do navegador\n- `set_cookies()` - Definir cookies do navegador\n- `delete_cookies()` - Deletar cookies específicos\n- `clear_browser_cookies()` - Limpar todos os cookies\n\n### Controle de Cache\n- `clear_browser_cache()` - Limpar cache do navegador\n- `set_cache_disabled()` - Desabilitar cache do navegador\n- `get_response_body_for_interception()` - Obter respostas em cache\n\n### Segurança & Cabeçalhos\n- `set_user_agent_override()` - Sobrescrever user agent\n- `set_extra_http_headers()` - Adicionar cabeçalhos customizados\n- `emulate_network_conditions()` - Simular condições de rede\n\n## Casos de Uso Avançados\n\n### Interceptação de Requisição\n```python\n# Interceptar e modificar requisições\nawait set_request_interception(connection, patterns=[\n    {\"urlPattern\": \"*/api/*\", \"requestStage\": \"Request\"}\n])\n\n# Lidar com requisição interceptada\nasync def handle_request(request):\n    if \"api/login\" in request.url:\n        # Modificar cabeçalhos da requisição\n        headers = request.headers.copy()\n        headers[\"Authorization\"] = \"Bearer token\"\n        await continue_intercepted_request(\n            connection, \n            request_id=request.request_id,\n            headers=headers\n        )\n```\n\n### Simulação de Resposta (Mocking)\n```python\n# Simular (mockar) respostas de API\nawait fulfill_request(\n    connection,\n    request_id=request_id,\n    response_code=200,\n    response_headers={\"Content-Type\": \"application/json\"},\n    body='{\"status\": \"success\"}'\n)\n```\n\n!!! warning \"Impacto na Performance\"\n    A interceptação de rede pode impactar a performance de carregamento da página. Use seletivamente e desabilite quando não for necessário."
  },
  {
    "path": "docs/pt/api/commands/page.md",
    "content": "# Comandos de Página (Page)\n\nOs comandos de página lidam com a navegação da página, eventos do ciclo de vida e operações em nível de página.\n\n## Visão Geral\n\nO módulo de comandos de página fornece funcionalidade para navegar entre páginas, gerenciar o ciclo de vida da página, lidar com a execução de JavaScript e controlar o comportamento da página.\n\n::: pydoll.commands.page_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Uso\n\nOs comandos de página são usados extensivamente pela classe `Tab` para navegação e gerenciamento da página:\n\n```python\nfrom pydoll.commands.page_commands import navigate, reload, enable\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Navegar para uma URL\nconnection = ConnectionHandler()\nawait enable(connection)  # Habilitar eventos da página\nawait navigate(connection, url=\"https://example.com\")\n\n# Recarregar a página\nawait reload(connection)\n```\n\n## Funcionalidades Principais\n\nO módulo de comandos de página fornece funções para:\n\n### Navegação\n- `Maps()` - Navegar para URLs\n- `reload()` - Recarregar página atual\n- `go_back()` - Navegar para trás no histórico\n- `go_forward()` - Navegar para frente no histórico\n- `stop_loading()` - Parar carregamento da página\n\n### Ciclo de Vida da Página\n- `enable()` / `disable()` - Habilitar/desabilitar eventos da página\n- `get_frame_tree()` - Obter estrutura de frames da página\n- `get_navigation_history()` - Obter histórico de navegação\n\n### Gerenciamento de Conteúdo\n- `get_resource_content()` - Obter conteúdo de recurso da página\n- `search_in_resource()` - Pesquisar dentro de recursos da página\n- `set_document_content()` - Definir conteúdo HTML da página\n\n### Capturas de Tela & PDF\n- `capture_screenshot()` - Tirar capturas de tela da página\n- `print_to_pdf()` - Gerar PDF a partir da página\n- `capture_snapshot()` - Capturar snapshots da página\n\n### Execução de JavaScript\n- `add_script_to_evaluate_on_new_document()` - Adicionar scripts para avaliar em novo documento (scripts de inicialização)\n- `remove_script_to_evaluate_on_new_document()` - Remover scripts de inicialização\n\n### Configurações da Página\n- `set_lifecycle_events_enabled()` - Controlar eventos do ciclo de vida\n- `set_ad_blocking_enabled()` - Habilitar/desabilitar bloqueio de anúncios\n- `set_bypass_csp()` - Contornar (Bypass) Política de Segurança de Conteúdo (CSP)\n\n## Recursos Avançados\n\n### Gerenciamento de Frames\n```python\n# Obter todos os frames na página\nframe_tree = await get_frame_tree(connection)\nfor frame in frame_tree.child_frames:\n    print(f\"Frame: {frame.frame.url}\")\n```\n\n### Interceptação de Recursos\n```python\n# Obter conteúdo do recurso\ncontent = await get_resource_content(\n    connection, \n    frame_id=frame_id, \n    url=\"https://example.com/script.js\"\n)\n```\n\n### Eventos da Página\nOs comandos de página funcionam com vários eventos de página:\n- `Page.loadEventFired` - Carregamento da página concluído\n- `Page.domContentEventFired` - Conteúdo DOM carregado\n- `Page.frameNavigated` - Navegação do frame\n- `Page.frameStartedLoading` - Carregamento do frame iniciado\n\n!!! tip \"Integração com a Classe Tab\"\n    A maioria das operações de página está disponível através dos métodos da classe `Tab`, como `tab.go_to()`, `tab.reload()` e `tab.screenshot()`, que fornecem uma API mais conveniente."
  },
  {
    "path": "docs/pt/api/commands/runtime.md",
    "content": "# Comandos de Tempo de Execução (Runtime)\n\nOs comandos de tempo de execução fornecem capacidades de execução de JavaScript e gerenciamento do ambiente de tempo de execução.\n\n## Visão Geral\n\nO módulo de comandos de tempo de execução habilita a execução de código JavaScript, inspeção de objetos e controle do ambiente de tempo de execução dentro dos contextos do navegador.\n\n::: pydoll.commands.runtime_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Uso\n\nOs comandos de tempo de execução são usados para execução de JavaScript e gerenciamento do tempo de execução:\n\n```python\nfrom pydoll.commands.runtime_commands import evaluate, enable\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Habilitar eventos de tempo de execução\nconnection = ConnectionHandler()\nawait enable(connection)\n\n# Executar JavaScript\nresult = await evaluate(\n    connection, \n    expression=\"document.title\",\n    return_by_value=True\n)\nprint(result.value)  # Título da página\n```\n\n## Funcionalidades Principais\n\nO módulo de comandos de tempo de execução fornece funções para:\n\n### Execução de JavaScript\n- `evaluate()` - Executar expressões JavaScript\n- `call_function_on()` - Chamar funções em objetos\n- `compile_script()` - Compilar JavaScript para reutilização\n- `run_script()` - Executar scripts compilados\n\n### Gerenciamento de Objetos\n- `get_properties()` - Obter propriedades do objeto\n- `release_object()` - Liberar referências de objeto\n- `release_object_group()` - Liberar grupos de objetos\n\n### Controle de Tempo de Execução\n- `enable()` / `disable()` - Habilitar/desabilitar eventos de tempo de execução\n- `discard_console_entries()` - Limpar entradas do console\n- `set_custom_object_formatter_enabled()` - Habilitar formatadores customizados\n\n### Manipulação de Exceções\n- `set_async_call_stack_depth()` - Definir profundidade da pilha de chamadas assíncronas\n- Captura e relatório de exceções\n- Inspeção de objeto de erro\n\n## Uso Avançado\n\n### Execução de JavaScript Complexo\n```python\n# Executar JavaScript complexo com tratamento de erros\nscript = \"\"\"\ntry {\n    const elements = document.querySelectorAll('.item');\n    return Array.from(elements).map(el => ({\n        text: el.textContent,\n        href: el.href\n    }));\n} catch (error) {\n    return { error: error.message };\n}\n\"\"\"\n\nresult = await evaluate(\n    connection,\n    expression=script,\n    return_by_value=True,\n    await_promise=True\n)\n```\n\n### Inspeção de Objeto\n```python\n# Obter propriedades detalhadas do objeto\nproperties = await get_properties(\n    connection,\n    object_id=object_id,\n    own_properties=True,\n    accessor_properties_only=False\n)\n\nfor prop in properties:\n    print(f\"{prop.name}: {prop.value}\")\n```\n\n### Integração com Console\nOs comandos de tempo de execução se integram ao console do navegador:\n- Mensagens e erros do console\n- Chamadas de método da API Console\n- Formatadores de console customizados\n\n!!! note \"Considerações de Performance\"\n    A execução de JavaScript através dos comandos de tempo de execução pode ser mais lenta do que a execução nativa do navegador. Use com moderação para operações complexas."
  },
  {
    "path": "docs/pt/api/commands/storage.md",
    "content": "# Comandos de Armazenamento (Storage)\n\nOs comandos de armazenamento fornecem gerenciamento abrangente do armazenamento do navegador, incluindo cookies, localStorage, sessionStorage e IndexedDB.\n\n## Visão Geral\n\nO módulo de comandos de armazenamento permite o gerenciamento de todos os mecanismos de armazenamento do navegador, fornecendo funcionalidade para persistência e recuperação de dados.\n\n::: pydoll.commands.storage_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Uso\n\nOs comandos de armazenamento são usados para gerenciar o armazenamento do navegador em diferentes mecanismos:\n\n```python\nfrom pydoll.commands.storage_commands import get_cookies, set_cookies, clear_data_for_origin\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Obter cookies para um domínio\nconnection = ConnectionHandler()\ncookies = await get_cookies(connection, urls=[\"https://example.com\"])\n\n# Definir um novo cookie\nawait set_cookies(connection, cookies=[{\n    \"name\": \"session_id\",\n    \"value\": \"abc123\",\n    \"domain\": \"example.com\",\n    \"path\": \"/\",\n    \"httpOnly\": True,\n    \"secure\": True\n}])\n\n# Limpar todo o armazenamento para uma origem\nawait clear_data_for_origin(\n    connection,\n    origin=\"https://example.com\",\n    storage_types=\"all\"\n)\n```\n\n## Funcionalidades Principais\n\nO módulo de comandos de armazenamento fornece funções para:\n\n### Gerenciamento de Cookies\n- `get_cookies()` - Obter cookies por URL ou domínio\n- `set_cookies()` - Definir novos cookies\n- `delete_cookies()` - Deletar cookies específicos\n- `clear_cookies()` - Limpar todos os cookies\n\n### Local Storage\n- `get_dom_storage_items()` - Obter itens do localStorage\n- `set_dom_storage_item()` - Definir item do localStorage\n- `remove_dom_storage_item()` - Remover item do localStorage\n- `clear_dom_storage()` - Limpar localStorage\n\n### Session Storage\n- Operações de session storage (semelhantes ao localStorage)\n- Gerenciamento de dados específicos da sessão\n- Armazenamento isolado por aba\n\n### IndexedDB\n- `get_database_names()` - Obter bancos de dados IndexedDB\n- `request_database()` - Acessar a estrutura do banco de dados\n- `request_data()` - Consultar dados do banco de dados\n- `clear_object_store()` - Limpar object stores\n\n### Cache Storage\n- `request_cache_names()` - Obter nomes de cache\n- `request_cached_response()` - Obter respostas em cache\n- `delete_cache()` - Deletar entradas de cache\n\n### Application Cache (Obsoleto)\n- Suporte a cache de aplicação legado\n- Cache baseado em manifesto\n\n## Recursos Avançados\n\n### Operações em Massa\n```python\n# Limpar todos os tipos de armazenamento para múltiplas origens\norigins = [\"https://example.com\", \"https://api.example.com\"]\nfor origin in origins:\n    await clear_data_for_origin(\n        connection,\n        origin=origin,\n        storage_types=\"cookies,local_storage,session_storage,indexeddb\"\n    )\n```\n\n### Cotas de Armazenamento\n```python\n# Obter informações de uso e cota de armazenamento\nquota_info = await get_usage_and_quota(connection, origin=\"https://example.com\")\nprint(f\"Usado: {quota_info.usage} bytes\")\nprint(f\"Cota: {quota_info.quota} bytes\")\n```\n\n### Armazenamento Cross-Origin\n```python\n# Gerenciar armazenamento entre diferentes origens\nawait set_cookies(connection, cookies=[{\n    \"name\": \"cross_site_token\",\n    \"value\": \"token123\",\n    \"domain\": \".example.com\",  # Aplica-se a todos os subdomínios\n    \"sameSite\": \"None\",\n    \"secure\": True\n}])\n```\n\n## Tipos de Armazenamento\n\nO módulo suporta vários mecanismos de armazenamento:\n\n| Tipo de Armazenamento | Persistência | Escopo | Capacidade |\n|--------------|-------------|-------|----------|\n| Cookies | Persistente | Domínio/Caminho | ~4KB por cookie |\n| localStorage | Persistente | Origem | ~5-10MB |\n| sessionStorage | Sessão | Aba | ~5-10MB |\n| IndexedDB | Persistente | Origem | Grande (GB+) |\n| Cache API | Persistente | Origem | Grande |\n\n!!! warning \"Considerações de Privacidade\"\n    Operações de armazenamento podem afetar a privacidade do usuário. Sempre lide com dados de armazenamento de forma responsável e em conformidade com as regulamentações de privacidade."
  },
  {
    "path": "docs/pt/api/commands/target.md",
    "content": "# Comandos de Alvo (Target)\n\nOs comandos de alvo (Target) gerenciam os alvos do navegador, incluindo abas, janelas e outros contextos de navegação.\n\n## Visão Geral\n\nO módulo de comandos de alvo fornece funcionalidade para criar, gerenciar e controlar os alvos do navegador, como abas, janelas pop-up e service workers.\n\n::: pydoll.commands.target_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Uso\n\nOs comandos de alvo são usados internamente pelas classes do navegador para gerenciar abas e janelas:\n\n```python\nfrom pydoll.commands.target_commands import get_targets, create_target, close_target\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Obter todos os alvos do navegador\nconnection = ConnectionHandler()\ntargets = await get_targets(connection)\n\n# Criar uma nova aba\nnew_target = await create_target(connection, url=\"https://example.com\")\n\n# Fechar um alvo\nawait close_target(connection, target_id=new_target.target_id)\n```\n\n## Funcionalidades Principais\n\nO módulo de comandos de alvo fornece funções para:\n\n### Gerenciamento de Alvo\n- `get_targets()` - Listar todos os alvos do navegador\n- `create_target()` - Criar novas abas ou janelas\n- `close_target()` - Fechar alvos específicos\n- `activate_target()` - Trazer alvo para o primeiro plano\n\n### Informações do Alvo\n- `get_target_info()` - Obter informações detalhadas do alvo\n- Tipos de alvo: page, background_page, service_worker, browser\n- Estados do alvo: attached, detached, crashed\n\n### Gerenciamento de Sessão\n- `attach_to_target()` - Anexar a um alvo para controle\n- `detach_from_target()` - Desanexar de um alvo\n- `send_message_to_target()` - Enviar comandos para alvos\n\n### Contexto do Navegador\n- `create_browser_context()` - Criar contexto de navegador isolado\n- `dispose_browser_context()` - Remover contexto de navegador\n- `get_browser_contexts()` - Listar contextos de navegador\n\n## Tipos de Alvos\n\nDiferentes tipos de alvos podem ser gerenciados:\n\n### Alvos de Página\n```python\n# Criar uma nova aba\npage_target = await create_target(\n    connection,\n    url=\"https://example.com\",\n    width=1920,\n    height=1080,\n    browser_context_id=None  # Contexto padrão\n)\n```\n\n### Janelas Pop-up\n```python\n# Criar uma janela pop-up\npopup_target = await create_target(\n    connection,\n    url=\"https://popup.example.com\",\n    width=800,\n    height=600,\n    new_window=True\n)\n```\n\n### Contextos Anônimos (Incognito)\n```python\n# Criar contexto de navegador anônimo\nincognito_context = await create_browser_context(connection)\n\n# Criar aba no contexto anônimo\nincognito_tab = await create_target(\n    connection,\n    url=\"https://private.example.com\",\n    browser_context_id=incognito_context.browser_context_id\n)\n```\n\n!!! info \"Headless vs Headed: como os contextos se manifestam\"\n    Contextos de navegador são ambientes lógicos isolados. No modo **headed** (com interface gráfica), a primeira página criada dentro de um novo contexto geralmente abrirá em uma nova janela do SO. No modo **headless** (sem interface gráfica), nenhuma janela é mostrada — o isolamento permanece puramente lógico (cookies, armazenamento, cache e estado de autenticação ainda são separados por contexto). Prefira contextos em pipelines headless/CI para performance e isolamento limpo.\n\n## Recursos Avançados\n\n### Eventos de Alvo\nOs comandos de alvo funcionam com vários eventos de alvo:\n- `Target.targetCreated` - Novo alvo criado\n- `Target.targetDestroyed` - Alvo fechado\n- `Target.targetInfoChanged` - Informações do alvo atualizadas\n- `Target.targetCrashed` - Alvo falhou (crashed)\n\n### Coordenação Multi-Alvo\n```python\n# Gerenciar múltiplas abas\ntargets = await get_targets(connection)\npage_targets = [t for t in targets if t.type == \"page\"]\n\nfor target in page_targets:\n    # Realizar operações em cada aba\n    await activate_target(connection, target_id=target.target_id)\n    # ... fazer o trabalho nesta aba\n```\n\n### Isolamento de Alvo\n```python\n# Criar contexto de navegador isolado para testes\ntest_context = await create_browser_context(connection)\n\n# Todos os alvos neste contexto estão isolados\ntest_tab1 = await create_target(\n    connection, \n    url=\"https://test1.com\",\n    browser_context_id=test_context.browser_context_id\n)\n\ntest_tab2 = await create_target(\n    connection,\n    url=\"https://test2.com\", \n    browser_context_id=test_context.browser_context_id\n)\n```\n\n!!! note \"Integração com o Navegador\"\n    Os comandos de alvo são usados principalmente internamente pelas classes de navegador `Chrome` e `Edge`. As APIs de navegador de alto nível fornecem métodos mais convenientes para o gerenciamento de abas."
  },
  {
    "path": "docs/pt/api/connection/connection.md",
    "content": "# Connection Handler\n\n::: pydoll.connection.connection_handler.ConnectionHandler\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/pt/api/connection/managers.md",
    "content": "# Connection Managers\n\n## CommandsManager\n\n::: pydoll.connection.managers.commands_manager.CommandsManager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## EventsManager\n\n::: pydoll.connection.managers.events_manager.EventsManager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3 "
  },
  {
    "path": "docs/pt/api/core/constants.md",
    "content": "# Constants\n\nO módulo de constantes fornece valores predefinidos e configurações padrão para o navegador.\n\n::: pydoll.constants\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      group_by_category: true\n      members_order: source "
  },
  {
    "path": "docs/pt/api/core/exceptions.md",
    "content": "# Exceptions\n\nO módulo de exceções fornece classes de exceção personalizadas que podem ser lançadas por operações do Pydoll.\n\n::: pydoll.exceptions\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      group_by_category: true\n      members_order: source "
  },
  {
    "path": "docs/pt/api/core/utils.md",
    "content": "# Utilities\n\nO módulo de utilitários fornece funções e classes auxiliares usadas em todo o Pydoll.\n\n::: pydoll.utils\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      group_by_category: true\n      members_order: source "
  },
  {
    "path": "docs/pt/api/elements/mixins.md",
    "content": "# Mixins de Elementos\n\nO módulo de mixins (mixins) fornece funcionalidade reutilizável que pode ser misturada em classes de elementos para estender suas capacidades.\n\n## Mixin Find Elements\n\nO `FindElementsMixin` fornece capacidades de localização de elementos para as classes que o incluem.\n\n::: pydoll.elements.mixins.find_elements_mixin\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Uso\n\nMixins são tipicamente usados internamente pela biblioteca para compor funcionalidades. O `FindElementsMixin` é usado por classes como `Tab` e `WebElement` para fornecer métodos de localização de elementos:\n\n```python\n# Estes métodos vêm do FindElementsMixin\nelement = await tab.find(id=\"username\")\nelements = await tab.find(class_name=\"item\", find_all=True)\nelement = await tab.query(\"#submit-button\")\n```\n\n## Métodos Disponíveis\n\nO `FindElementsMixin` fornece vários métodos para encontrar elementos:\n\n- `find()` - Localização de elementos moderna com argumentos nomeados (keyword arguments)\n- `query()` - Consultas de seletor CSS e XPath\n- `find_element()` - Método de localização de elemento legado\n- `find_elements()` - Método legado para encontrar múltiplos elementos\n\n!!! tip \"Moderno vs. Legado\"\n    O método `find()` é a abordagem moderna e recomendada para encontrar elementos. Os métodos `find_element()` e `find_elements()` são mantidos para compatibilidade com versões anteriores."
  },
  {
    "path": "docs/pt/api/elements/shadow_root.md",
    "content": "# ShadowRoot\n\n::: pydoll.elements.shadow_root.ShadowRoot\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      members_order: source\n      group_by_category: true\n"
  },
  {
    "path": "docs/pt/api/elements/web_element.md",
    "content": "# WebElement\n\n::: pydoll.elements.web_element.WebElement\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      members_order: source\n      group_by_category: true "
  },
  {
    "path": "docs/pt/api/index.md",
    "content": "# Referência da API\n\nBem-vindo à Referência da API do Pydoll! Esta seção fornece documentação abrangente para todas as classes, métodos e funções disponíveis na biblioteca Pydoll.\n\n---\n\n## Visão Geral\n\nO Pydoll está organizado em vários módulos chave, cada um servindo a um propósito específico na automação do navegador:\n\n### Módulo Browser (Navegador)\nO módulo `browser` contém classes para gerenciar instâncias de navegador e seu ciclo de vida.\n\n* **[Chrome](browser/chrome.md)** - Automação do navegador Chrome\n* **[Edge](browser/edge.md)** - Automação do navegador Microsoft Edge\n* **[Options](browser/options.md)** - Opções de configuração do navegador\n* **[Tab](browser/tab.md)** - Gerenciamento e interação de abas\n* **[Requests](browser/requests.md)** - Requisições HTTP dentro do contexto do navegador\n* **[Managers](browser/managers.md)** - Gerenciadores do ciclo de vida do navegador\n\n### Módulo Elements (Elementos)\nO módulo `elements` fornece classes para interagir com elementos de página web.\n\n* **[WebElement](elements/web_element.md)** - Interação com elemento individual\n* **[Mixins](elements/mixins.md)** - Funcionalidade de elemento reutilizável\n\n### Módulo Connection (Conexão)\nO módulo `connection` lida com a comunicação com o navegador através do Chrome DevTools Protocol.\n\n* **[Connection Handler](connection/connection.md)** - Gerenciamento de conexão WebSocket\n* **[Managers](connection/managers.md)** - Gerenciadores do ciclo de vida da conexão\n\n### Módulo Commands (Comandos)\nO módulo `commands` fornece implementações de comando de baixo nível do Chrome DevTools Protocol.\n\n* **[Visão Geral dos Comandos](commands/index.md)** - Implementações de comando CDP por domínio\n\n### Módulo Protocol (Protocolo)\nO módulo `protocol` implementa os comandos e eventos do Chrome DevTools Protocol.\n\n* **[Tipos Base](protocol/base.md)** - Tipos base para o Chrome DevTools Protocol\n* **[Browser](protocol/browser.md)** - Comandos e eventos do domínio Browser\n* **[DOM](protocol/dom.md)** - Comandos e eventos do domínio DOM\n* **[Fetch](protocol/fetch.md)** - Comandos e eventos do domínio Fetch\n* **[Input](protocol/input.md)** - Comandos e eventos do domínio Input\n* **[Network](protocol/network.md)** - Comandos e eventos do domínio Network\n* **[Page](protocol/page.md)** - Comandos e eventos do domínio Page\n* **[Runtime](protocol/runtime.md)** - Comandos e eventos do domínio Runtime\n* **[Storage](protocol/storage.md)** - Comandos e eventos do domínio Storage\n* **[Target](protocol/target.md)** - Comandos e eventos do domínio Target\n\n### Módulo Core (Núcleo)\nO módulo `core` contém utilitários fundamentais, constantes e exceções.\n\n* **[Constants](core/constants.md)** - Constantes e enums da biblioteca\n* **[Exceptions](core/exceptions.md)** - Classes de exceção customizadas\n* **[Utils](core/utils.md)** - Funções de utilidade\n\n---\n\n## Navegação Rápida\n\n### Classes Mais Comuns\n\n| Classe | Propósito | Módulo |\n|-------|---------|--------|\n| `Chrome` | Automação do navegador Chrome | `pydoll.browser.chromium` |\n| `Edge` | Automação do navegador Edge | `pydoll.browser.chromium` |\n| `Tab` | Interação e controle de abas | `pydoll.browser.tab` |\n| `WebElement` | Interação com elementos | `pydoll.elements.web_element` |\n| `ChromiumOptions` | Configuração do navegador | `pydoll.browser.options` |\n\n### Enums e Constantes Chave\n\n| Nome | Propósito | Módulo |\n|------|---------|--------|\n| `By` | Estratégias de seletor de elemento | `pydoll.constants` |\n| `Key` | Constantes de tecla do teclado | `pydoll.constants` |\n| `PermissionType` | Tipos de permissão do navegador | `pydoll.constants` |\n\n### Exceções Comuns\n\n| Exceção | Quando Levantada | Módulo |\n|-----------|-------------|--------|\n| `ElementNotFound` | Elemento não encontrado no DOM | `pydoll.exceptions` |\n| `WaitElementTimeout` | Timeout de espera de elemento | `pydoll.exceptions` |\n| `BrowserNotStarted` | Navegador não iniciado | `pydoll.exceptions` |\n\n---\n\n## Padrões de Uso\n\n### Automação Básica do Navegador\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to(\"https://example.com\")\n    element = await tab.find(id=\"my-element\")\n    await element.click()\n```\n\n### Localização de Elementos\n\n```python\n# Usando o método moderno find()\nelement = await tab.find(id=\"username\")\nelement = await tab.find(tag_name=\"button\", class_name=\"submit\")\n\n# Usando seletores CSS ou XPath\nelement = await tab.query(\"#username\")\nelement = await tab.query(\"//button[@class='submit']\")\n```\n\n### Manipulação de Eventos\n\n```python\nawait tab.enable_page_events()\nawait tab.on('Page.loadEventFired', handle_page_load)\n```\n\n---\n\n## Suporte a Tipagem e Assincronismo\n\n### Dicas de Tipo (Type Hints)\nO Pydoll é totalmente tipado e fornece **dicas de tipo** abrangentes para melhor suporte da IDE e segurança de código. Todas as APIs públicas incluem anotações de tipo adequadas.\n\n```python\nfrom typing import Optional, List\nfrom pydoll.elements.web_element import WebElement\n\n# Métodos retornam objetos tipados corretamente\nelement: Optional[WebElement] = await tab.find(id=\"test\", raise_exc=False)\nelements: List[WebElement] = await tab.find(class_name=\"item\", find_all=True)\n```\n\n### Suporte Async/Await\nTodas as operações do Pydoll são **assíncronas** e devem ser usadas com **`async`**/**`await`**:\n\n```python\nimport asyncio\n\nasync def main():\n    # Todas as operações do Pydoll são assíncronas\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to(\"https://example.com\")\n        \nasyncio.run(main())\n```\n\nNavegue pelas seções abaixo para explorar a documentação completa da API para cada módulo."
  },
  {
    "path": "docs/pt/api/protocol/base.md",
    "content": "# Protocol Base Types\n\nTipos e estruturas base para comandos, respostas e eventos do Chrome DevTools Protocol.\n\n## Base Types\n\n::: pydoll.protocol.base\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n      group_by_category: true\n      members_order: source\n      filters:\n        - \"!^__\""
  },
  {
    "path": "docs/pt/api/protocol/browser.md",
    "content": "# Protocolo do Navegador\n\nDomínio de comandos, eventos e tipos para o Chrome DevTools Protocol.\n\n## Métodos\n\n::: pydoll.protocol.browser.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Eventos  \n\n::: pydoll.protocol.browser.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Tipos\n\n::: pydoll.protocol.browser.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/pt/api/protocol/dom.md",
    "content": "# Protocolo DOM\n\nDomínio de comandos e eventos para o Chrome DevTools Protocol.\n\n## Métodos\n\n::: pydoll.protocol.dom.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Eventos\n\n::: pydoll.protocol.dom.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Tipos\n\n::: pydoll.protocol.dom.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/pt/api/protocol/fetch.md",
    "content": "# Protocolo Fetch\n\nDomínio de comandos, eventos e tipos para o Chrome DevTools Protocol.\n\n## Métodos\n\n::: pydoll.protocol.fetch.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Eventos\n\n::: pydoll.protocol.fetch.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Tipos\n\n::: pydoll.protocol.fetch.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/pt/api/protocol/input.md",
    "content": "# Protocolo Input\n\nDomínio de comandos, eventos e tipos para o Chrome DevTools Protocol.\n\n## Métodos\n\n::: pydoll.protocol.input.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Eventos\n\n::: pydoll.protocol.input.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Tipos\n\n::: pydoll.protocol.input.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/pt/api/protocol/network.md",
    "content": "# Protocolo Network\n\nDomínio de comandos, eventos e tipos para o Chrome DevTools Protocol.\n\n## Métodos\n\n::: pydoll.protocol.network.methods\n    options:\n      show_root_heading: false\n      show_source: false\n      heading_level: 2\n\n## Eventos\n\n::: pydoll.protocol.network.events\n    options:\n      show_root_heading: false\n      show_source: false\n      heading_level: 2\n\n## Tipos\n\n::: pydoll.protocol.network.types\n    options:\n      show_root_heading: false\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/pt/api/protocol/page.md",
    "content": "# Protocolo Page\n\nDomínio de comandos, eventos e tipos para o Chrome DevTools Protocol.\n\n## Métodos\n\n::: pydoll.protocol.page.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Eventos\n\n::: pydoll.protocol.page.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Tipos\n\n::: pydoll.protocol.page.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/pt/api/protocol/runtime.md",
    "content": "# Protocolo Runtime\n\nDomínio de comandos, eventos e tipos para o Chrome DevTools Protocol.\n\n## Métodos\n\n::: pydoll.protocol.runtime.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Eventos\n\n::: pydoll.protocol.runtime.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Tipos\n\n::: pydoll.protocol.runtime.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/pt/api/protocol/storage.md",
    "content": "# Protocolo Storage\n\nDomínio de comandos, eventos e tipos para o Chrome DevTools Protocol.\n\n## Métodos\n\n::: pydoll.protocol.storage.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Eventos\n\n::: pydoll.protocol.storage.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Tipos\n\n::: pydoll.protocol.storage.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/pt/api/protocol/target.md",
    "content": "# Protocolo Target\n\nDomínio de comandos e eventos para o Chrome DevTools Protocol.\n\n## Métodos\n\n::: pydoll.protocol.target.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Eventos\n\n::: pydoll.protocol.target.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## Tipos\n\n::: pydoll.protocol.target.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/pt/deep-dive/architecture/browser-domain.md",
    "content": "# Arquitetura do Domínio do Navegador\n\nO domínio do Navegador (Browser) representa o nível mais alto da hierarquia de automação do Pydoll, gerenciando o ciclo de vida do processo do navegador, conexões CDP, isolamento de contexto e operações globais do navegador. Este documento explora a arquitetura interna, as decisões de design e a implementação técnica do controle em nível de navegador.\n\n!!! info \"Guia de Uso Prático\"\n    Para exemplos práticos e padrões de uso, consulte os guias [Gerenciamento do Navegador](../features/browser-management/tabs.md) e [Contextos do Navegador](../features/browser-management/contexts.md).\n\n## Visão Geral da Arquitetura\n\nO domínio do Navegador situa-se na interseção do gerenciamento de processos, comunicação de protocolo e coordenação de recursos. Ele orquestra múltiplos componentes especializados para fornecer uma interface unificada para a automação do navegador:\n\n```mermaid\ngraph LR\n    Browser[Instancia do Navegador]\n    Browser --> ProcessManager[Gerenciador de Processo]\n    Browser --> ProxyManager[Gerenciador de Proxy]\n    Browser --> TempDirManager[Gerenciador de Diretorio Temporario]\n    Browser --> TabRegistry[Registro de Abas]\n    Browser --> ConnectionHandler[Manipulador de Conexao]\n    \n    ProcessManager --> |Gerencia| BrowserProcess[Processo do Navegador]\n    ConnectionHandler <--> |WebSocket| CDP[Chrome DevTools Protocol]\n    TabRegistry --> |Gerencia| Tabs[Instancias de Abas]\n    CDP <--> BrowserProcess\n```\n\n### Hierarquia e Abstração\n\nO domínio do Navegador é implementado como uma **classe base abstrata** (abstract base class) que define o contrato para todas as implementações de navegador:\n\n```python\nclass Browser(ABC):\n    \"\"\"Classe base abstrata para automação de navegador via CDP.\"\"\"\n    \n    @abstractmethod\n    def _get_default_binary_location(self) -> str:\n        \"\"\"Subclasses devem fornecer o caminho do executável específico do navegador.\"\"\"\n        pass\n    \n    async def start(self, headless: bool = False) -> Tab:\n        \"\"\"Implementação concreta compartilhada por todos os navegadores.\"\"\"\n        # 1. Resolver localização do binário\n        # 2. Configurar diretório de dados do usuário\n        # 3. Iniciar processo do navegador\n        # 4. Verificar conexão CDP\n        # 5. Configurar proxy (se necessário)\n        # 6. Retornar aba inicial\n```\n\nEste design permite **polimorfismo** - Chrome, Edge e outros navegadores baseados em Chromium compartilham 99% de seu código, diferindo apenas nos caminhos dos executáveis e pequenas variações de flags.\n\n## Arquitetura de Componentes\n\nA classe Browser coordena vários gerenciadores especializados, cada um responsável por um aspecto específico da automação do navegador. Entender esses componentes é fundamental para entender o design do Pydoll.\n\n### Manipulador de Conexão (Connection Handler)\n\nO ConnectionHandler é a **ponte de comunicação** entre o Pydoll e o processo do navegador. Ele gerencia:\n\n- **Ciclo de vida do WebSocket**: Estabelecimento da conexão, keep-alive, reconexão\n- **Execução de comandos**: Envio de comandos CDP e aguardo de respostas\n- **Despacho de eventos**: Roteamento de eventos CDP para callbacks registrados\n- **Registro de callbacks**: Manutenção de ouvintes de eventos por conexão\n\n```python\nclass Browser:\n    def __init__(self, ...):\n        # ConnectionHandler é inicializado com a porta ou endereço WebSocket\n        self._connection_handler = ConnectionHandler(self._connection_port)\n    \n    async def _execute_command(self, command, timeout=10):\n        \"\"\"Todos os comandos CDP fluem através do manipulador de conexão.\"\"\"\n        return await self._connection_handler.execute_command(command, timeout)\n```\n\n!!! info \"Análise Profunda da Camada de Conexão\"\n    Para informações detalhadas sobre comunicação WebSocket, fluxo de comando/resposta e padrões assíncronos, consulte [Arquitetura da Camada de Conexão](./connection-layer.md).\n\n### Gerenciador de Processo (Process Manager)\n\nO BrowserProcessManager lida com o **ciclo de vida do processo do sistema operacional**:\n\n```python\nclass BrowserProcessManager:\n    def start_browser_process(self, binary, port, arguments):\n        \"\"\"\n        1. Constrói a linha de comando com caminho do binário + argumentos\n        2. Inicia o subprocesso com manipulação adequada de stdio\n        3. Monitora a inicialização do processo\n        4. Armazena o handle do processo para terminação posterior\n        \"\"\"\n        \n    def stop_process(self):\n        \"\"\"\n        1. Tenta terminação graciosa (SIGTERM)\n        2. Aguarda a saída do processo\n        3. Mata forçadamente se o tempo limite for excedido (SIGKILL)\n        4. Limpa os recursos do processo\n        \"\"\"\n```\n\n**Por que separar o gerenciamento de processos?**\n\n- **Testabilidade**: O gerenciador de processos pode ser mockado para testes unitários\n- **Multiplataforma**: Encapsula o manuseio de processos específico do SO\n- **Confiabilidade**: Lida com casos extremos como processos zumbis, filhos órfãos\n\n### Registro de Abas (Tab Registry)\n\nO Navegador mantém um **registro de instâncias de Abas** (Tab) para garantir o comportamento singleton por alvo (target):\n\n```python\nclass Browser:\n    def __init__(self, ...):\n        self._tabs_opened: dict[str, Tab] = {}\n    \n    async def new_tab(self, url='', browser_context_id=None) -> Tab:\n        # Criar alvo via CDP\n        response = await self._execute_command(\n            TargetCommands.create_target(browser_context_id=browser_context_id)\n        )\n        target_id = response['result']['targetId']\n        \n        # Verificar se a aba já existe no registro\n        if target_id in self._tabs_opened:\n            return self._tabs_opened[target_id]\n        \n        # Criar nova instância de Aba e registrá-la\n        tab = Tab(self, target_id=target_id, ...)\n        self._tabs_opened[target_id] = tab\n        return tab\n```\n\n**Por que instâncias de Aba singleton?**\n\n- **Consistência de estado**: Múltiplas referências à mesma aba compartilham estado (domínios habilitados, callbacks)\n- **Eficiência de memória**: Evita instâncias duplicadas de Aba para o mesmo alvo\n- **Roteamento de eventos**: Garante que os eventos sejam roteados para a instância de Aba correta\n\n### Arquitetura de Autenticação de Proxy\n\nO Pydoll implementa **autenticação automática de proxy** através do domínio Fetch para evitar a exposição de credenciais em comandos CDP. A implementação usa **dois mecanismos distintos** dependendo do escopo do proxy:\n\n#### Mecanismo 1: Autenticação de Proxy em Nível de Navegador (Proxy Global)\n\nQuando um proxy é configurado via `ChromiumOptions` (aplica-se a todas as abas no contexto padrão):\n\n```python\n# Em Browser.start() -> _configure_proxy()\nasync def _configure_proxy(self, private_proxy, proxy_credentials):\n    # Habilitar Fetch EM NÍVEL DE NAVEGADOR\n    await self.enable_fetch_events(handle_auth_requests=True)\n    \n    # Registrar callbacks EM NÍVEL DE NAVEGADOR (afeta TODAS as abas)\n    await self.on(FetchEvent.REQUEST_PAUSED, self._continue_request_callback, temporary=True)\n    await self.on(FetchEvent.AUTH_REQUIRED, \n                  partial(self._continue_request_with_auth_callback,\n                          proxy_username=credentials[0],\n                          proxy_password=credentials[1]),\n                  temporary=True)\n```\n\n**Escopo:** Conexão WebSocket em nível de navegador → afeta **todas as abas no contexto padrão**\n\n#### Mecanismo 2: Autenticação de Proxy em Nível de Aba (Proxy por Contexto)\n\nQuando um proxy é configurado por contexto via `create_browser_context(proxy_server=...)`:\n\n```python\n# Armazenar credenciais por contexto\nasync def create_browser_context(self, proxy_server, ...):\n    sanitized_proxy, extracted_auth = self._sanitize_proxy_and_extract_auth(proxy_server)\n    \n    response = await self._execute_command(\n        TargetCommands.create_browser_context(proxy_server=sanitized_proxy)\n    )\n    context_id = response['result']['browserContextId']\n    \n    if extracted_auth:\n        self._context_proxy_auth[context_id] = extracted_auth  # Armazena por contexto\n    \n    return context_id\n\n# Configurar autenticação para CADA aba nesse contexto\nasync def _setup_context_proxy_auth_for_tab(self, tab, browser_context_id):\n    creds = self._context_proxy_auth.get(browser_context_id)\n    if not creds:\n        return\n    \n    # Habilitar Fetch NA ABA (WebSocket em nível de aba)\n    await tab.enable_fetch_events(handle_auth=True)\n    \n    # Registrar callbacks NA ABA (afeta apenas esta aba)\n    await tab.on(FetchEvent.REQUEST_PAUSED, \n                 partial(self._tab_continue_request_callback, tab=tab), \n                 temporary=True)\n    await tab.on(FetchEvent.AUTH_REQUIRED,\n                 partial(self._tab_continue_request_with_auth_callback,\n                         tab=tab,\n                         proxy_username=creds[0],\n                         proxy_password=creds[1]),\n                 temporary=True)\n```\n\n**Escopo:** Conexão WebSocket em nível de aba → afeta **apenas aquela aba específica**\n\n#### Por Que Dois Mecanismos?\n\n| Aspecto | Nível do Navegador | Nível da Aba |\n|--------|---------------|-----------|\n| **Gatilho** | Proxy em `ChromiumOptions` | Proxy em `create_browser_context()` |\n| **WebSocket** | Conexão em nível de navegador | Conexão em nível de aba |\n| **Escopo** | Todas as abas no contexto padrão | Apenas abas naquele contexto |\n| **Eficiência** | Um ouvinte para todas as abas | Um ouvinte por aba |\n| **Isolamento** | Sem separação de contexto | Cada contexto tem credenciais diferentes |\n\n**Justificativa de design para autenticação em nível de aba:**\n\n- **Isolamento de contexto**: Cada contexto pode ter um **proxy diferente** com **credenciais diferentes**\n- **Limitação do CDP**: O domínio Fetch não pode ser escopado para um contexto específico no nível do navegador\n- **Tradeoff**: Ligeiramente menos eficiente (um ouvinte por aba), mas necessário para suporte a proxy por contexto\n\nEsta arquitetura garante que **credenciais nunca apareçam nos logs do CDP** e a autenticação seja tratada de forma transparente.\n\n!!! warning \"Efeitos Colaterais do Domínio Fetch\"\n    - **Fetch em Nível de Navegador**: Pausa temporariamente **todas as requisições em todas as abas** no contexto padrão até que a autenticação seja concluída\n    - **Fetch em Nível de Aba**: Pausa temporariamente **todas as requisições naquela aba específica** até que a autenticação seja concluída\n    \n    Esta é uma limitação do CDP - o Fetch habilita a interceptação de requisições. Após a conclusão da autenticação, o Fetch é desabilitado para minimizar a sobrecarga.\n\n## Inicialização e Ciclo de Vida\n\n### Design do Construtor\n\nO construtor do Navegador inicializa todos os componentes internos, mas **não inicia o processo do navegador**. Essa separação permite a configuração antes do lançamento:\n\n```python\nclass Browser(ABC):\n    def __init__(\n        self,\n        options_manager: BrowserOptionsManager,\n        connection_port: Optional[int] = None,\n    ):\n        # 1. Validar parâmetros\n        self._validate_connection_port(connection_port)\n        \n        # 2. Inicializar opções via gerenciador\n        self.options = options_manager.initialize_options()\n        \n        # 3. Determinar porta CDP (aleatória se não especificada)\n        self._connection_port = connection_port or randint(9223, 9322)\n        \n        # 4. Inicializar gerenciadores especializados\n        self._proxy_manager = ProxyManager(self.options)\n        self._browser_process_manager = BrowserProcessManager()\n        self._temp_directory_manager = TempDirectoryManager()\n        self._connection_handler = ConnectionHandler(self._connection_port)\n        \n        # 5. Inicializar rastreamento de estado\n        self._tabs_opened: dict[str, Tab] = {}\n        self._context_proxy_auth: dict[str, tuple[str, str]] = {}\n        self._ws_address: Optional[str] = None\n```\n\n**Principais decisões de design:**\n\n- **Início tardio do processo**: Construtor é síncrono; `start()` é assíncrono\n- **Flexibilidade de porta**: Porta aleatória previne colisões em automação paralela\n- **Padrão de gerenciador de opções**: Padrão Strategy para configuração específica do navegador\n- **Composição de componentes**: Gerenciadores especializados em vez de classe monolítica\n\n### Sequência de Início (Start)\n\nO método `start()` orquestra o lançamento e conexão do navegador:\n\n```python\nasync def start(self, headless: bool = False) -> Tab:\n    # 1. Resolver localização do binário\n    binary_location = self.options.binary_location or self._get_default_binary_location()\n    \n    # 2. Configurar diretório de dados do usuário (temporário ou persistente)\n    self._setup_user_dir()\n    \n    # 3. Extrair credenciais do proxy (se proxy privado)\n    proxy_config = self._proxy_manager.get_proxy_credentials()\n    \n    # 4. Iniciar processo do navegador com argumentos\n    self._browser_process_manager.start_browser_process(\n        binary_location, self._connection_port, self.options.arguments\n    )\n    \n    # 5. Verificar se o endpoint CDP está responsivo\n    await self._verify_browser_running()\n    \n    # 6. Configurar autenticação de proxy (via domínio Fetch)\n    await self._configure_proxy(proxy_config[0], proxy_config[1])\n    \n    # 7. Obter primeiro alvo válido e criar Aba\n    valid_tab_id = await self._get_valid_tab_id(await self.get_targets())\n    tab = Tab(self, target_id=valid_tab_id, connection_port=self._connection_port)\n    self._tabs_opened[valid_tab_id] = tab\n    \n    return tab\n```\n\n!!! tip \"Por que start() Retorna uma Aba\"\n    Este é um **compromisso de design** para ergonomia. Idealmente, `start()` apenas iniciaria o navegador, e os usuários chamariam `new_tab()` separadamente. No entanto, retornar a aba inicial reduz o código boilerplate para o caso de uso de 90% (automação de aba única). O tradeoff: a aba inicial não pode ser evitada mesmo em cenários de múltiplas abas.\n\n### Protocolo de Gerenciador de Contexto\n\nO Navegador implementa `__aenter__` e `__aexit__` para limpeza automática:\n\n```python\nasync def __aexit__(self, exc_type, exc_val, exc_tb):\n    # 1. Restaurar preferências de backup (se modificadas)\n    if self._backup_preferences_dir:\n        shutil.copy2(self._backup_preferences_dir, ...)\n    \n    # 2. Verificar se o navegador ainda está em execução\n    if await self._is_browser_running(timeout=2):\n        await self.stop()\n    \n    # 3. Fechar conexão WebSocket\n    await self._connection_handler.close()\n```\n\nIsso garante uma limpeza adequada mesmo se ocorrerem exceções durante a automação.\n\n## Arquitetura de Contexto do Navegador\n\nContextos de navegador são o mecanismo de isolamento mais sofisticado do Pydoll, fornecendo **separação completa do ambiente de navegação** dentro de um único processo de navegador. Entender sua arquitetura é essencial para automação avançada.\n\n### Hierarquia CDP: Navegador, Contexto, Alvo\n\nO CDP organiza a estrutura do navegador em três níveis:\n\n```mermaid\ngraph TB\n    Browser[Processo do Navegador]\n    Browser --> DefaultContext[Contexto de Navegador Padrao]\n    Browser --> Context1[Contexto de Navegador ID: abc-123]\n    Browser --> Context2[Contexto de Navegador ID: def-456]\n    \n    DefaultContext --> Target1[Alvo/Pagina ID: page-1]\n    DefaultContext --> Target2[Alvo/Pagina ID: page-2]\n    \n    Context1 --> Target3[Alvo/Pagina ID: page-3]\n    \n    Context2 --> Target4[Alvo/Pagina ID: page-4]\n    Context2 --> Target5[Alvo/Pagina ID: page-5]\n```\n\n**Conceitos-chave:**\n\n1.  **Processo do Navegador**: Única instância do Chromium com um endpoint CDP\n2.  **Contexto do Navegador (BrowserContext)**: Limite isolado de armazenamento/cache/permissão (semelhante ao modo anônimo)\n3.  **Alvo (Target)**: Página individual, popup, worker ou alvo de background\n\n### Limites de Isolamento de Contexto\n\nCada contexto de navegador mantém **isolamento estrito** para:\n\n| Recurso | Nível de Isolamento | Implementação |\n|----------|----------------|----------------|\n| Cookies | Completo | Jarra de cookies separada por contexto |\n| localStorage | Completo | Armazenamento separado por origem por contexto |\n| IndexedDB | Completo | Banco de dados separado por origem por contexto |\n| Cache | Completo | Cache HTTP independente por contexto |\n| Permissões | Completo | Concessões de permissão específicas do contexto |\n| Proxy de rede | Completo | Configuração de proxy por contexto |\n| Autenticação | Completo | Estado de autenticação independente por contexto |\n\n!!! info \"Por Que Contextos São Leves\"\n    Ao contrário de iniciar múltiplos processos de navegador, os contextos compartilham o **mecanismo de renderização, processo de GPU e pilha de rede**. Apenas armazenamento e estado são isolados. Isso torna os contextos 10-100x mais rápidos de criar do que novas instâncias de navegador.\n\n### Criação de Contexto e Vinculação de Alvo\n\nCriar um contexto e um alvo envolve dois comandos CDP:\n\n```python\n# Passo 1: Criar contexto de navegação isolado\nresponse = await self._execute_command(\n    TargetCommands.create_browser_context(\n        proxy_server='http://proxy.example.com:8080',\n        proxy_bypass_list='localhost,127.0.0.1'\n    )\n)\ncontext_id = response['result']['browserContextId']\n\n# Passo 2: Criar alvo (página) dentro desse contexto\nresponse = await self._execute_command(\n    TargetCommands.create_target(\n        browser_context_id=context_id  # Vincula o alvo ao contexto\n    )\n)\ntarget_id = response['result']['targetId']\n```\n\n**Detalhe crítico:** O parâmetro `browser_context_id` **vincula o alvo ao limite de isolamento do contexto**. Sem ele, o alvo é criado no contexto padrão.\n\n### Materialização de Janela no Modo Headed (com interface gráfica)\n\nNo **modo headed** (UI visível), os contextos do navegador têm uma restrição física importante:\n\n-   Um contexto inicialmente existe apenas **em memória** (sem janela)\n-   O **primeiro alvo** criado em um contexto **deve** abrir uma janela de nível superior\n-   **Alvos subsequentes** podem abrir como abas dentro dessa janela\n\nEsta é uma **limitação do CDP/Chromium**, não uma escolha de design do Pydoll:\n\n```python\n# Primeiro alvo no contexto: DEVE criar janela\ntab1 = await browser.new_tab(browser_context_id=context_id)  # Abre nova janela\n\n# Alvos subsequentes: PODEM abrir como abas na janela existente\ntab2 = await browser.new_tab(browser_context_id=context_id)  # Abre como aba\n```\n\n**Por que isso importa?**\n\n-   No **modo headless**: Completamente irrelevante (sem janelas renderizadas)\n-   No **modo headed**: O primeiro alvo por contexto abrirá uma janela visível\n-   Em **ambientes de teste**: Múltiplos contextos → múltiplas janelas (pode ser confuso)\n\n!!! tip \"Contextos Headless São Mais Limpos\"\n    Para CI/CD, scraping ou automação em lote, use o modo headless. O isolamento de contexto funciona identicamente, mas sem a sobrecarga de materialização de janela.\n\n### Exclusão e Limpeza de Contexto\n\nExcluir um contexto **fecha imediatamente todos os alvos** dentro dele:\n\n```python\nawait browser.delete_browser_context(context_id)\n# Todas as abas neste contexto agora estão fechadas\n# Todo o armazenamento para este contexto é limpo\n# O contexto não pode ser reutilizado (ID é inválido)\n```\n\n**Sequência de limpeza:**\n\n1.  CDP envia o comando `Target.disposeBrowserContext`\n2.  Navegador fecha todos os alvos naquele contexto\n3.  Navegador limpa todo o armazenamento para aquele contexto\n4.  Navegador invalida o ID do contexto\n5.  Pydoll remove o contexto dos registros internos\n\n## Sistema de Eventos em Nível de Navegador\n\nO domínio do Navegador suporta **ouvintes de eventos em todo o navegador** que operam em todas as abas e contextos. Isso é distinto dos eventos em nível de aba.\n\n### Escopo de Evento Navegador vs. Aba\n\n```python\n# Evento em nível de navegador: aplica-se a TODAS as abas\nawait browser.on('Target.targetCreated', handle_new_target)\n\n# Evento em nível de aba: aplica-se a UMA aba\nawait tab.on('Page.loadEventFired', handle_page_load)\n```\n\n**Diferença arquitetural:**\n\n-   **Eventos do navegador** usam a **conexão WebSocket em nível de navegador** (baseada em porta ou `ws://host/devtools/browser/...`)\n-   **Eventos de aba** usam **conexões WebSocket em nível de aba** (`ws://host/devtools/page/<target_id>`)\n\n### Domínio Fetch: Interceptação Global de Requisições\n\nO domínio Fetch pode ser habilitado nos níveis de **navegador e aba**, com escopos diferentes:\n\n```python\n# Fetch em nível de navegador: intercepta requisições para TODAS as abas\nawait browser.enable_fetch_events(handle_auth_requests=True)\nawait browser.on('Fetch.requestPaused', handle_request)\n\n# Fetch em nível de aba: intercepta requisições para UMA aba\nawait tab.enable_fetch_events(handle_auth_requests=True)\nawait tab.on('Fetch.requestPaused', handle_request)\n```\n\n**Quando usar cada um:**\n\n| Caso de Uso | Nível | Razão |\n|----------|-------|--------|\n| Autenticação de proxy | Navegador | Aplica-se globalmente a todos os contextos |\n| Bloqueio de anúncios | Navegador | Bloquear anúncios em todas as abas |\n| Mocking de API | Aba | Mockar API específica para teste específico |\n| Log de requisições | Aba | Registrar apenas requisições da aba relevante |\n\n!!! warning \"Impacto de Performance do Fetch\"\n    Habilitar o Fetch no nível do navegador **pausa todas as requisições** em todas as abas até que os callbacks sejam executados. Isso adiciona latência a cada requisição. Use o Fetch em nível de aba quando possível para minimizar o impacto.\n\n### Roteamento de Comandos\n\nTodos os comandos CDP fluem através do manipulador de conexão do Navegador:\n\n```python\nasync def _execute_command(self, command, timeout=10):\n    \"\"\"\n    Roteia o comando para a conexão apropriada:\n    - Comandos em nível de navegador → WebSocket do navegador\n    - Comandos em nível de aba → delegados para a instância da Aba\n    \"\"\"\n    return await self._connection_handler.execute_command(command, timeout)\n```\n\nEste roteamento centralizado permite:\n\n-   **Correlação requisição/resposta**: Corresponder respostas a requisições via ID\n-   **Gerenciamento de timeout**: Cancelar comandos que excedem o tempo limite\n-   **Tratamento de erros**: Converter erros CDP em exceções Python\n\n## Gerenciamento de Recursos\n\n### Operações de Cookies e Armazenamento\n\nO domínio do Navegador expõe operações de armazenamento **em todo o navegador** e **específicas do contexto**:\n\n```python\n# Operações em nível de navegador (todos os contextos)\nawait browser.set_cookies(cookies)\nawait browser.get_cookies()\nawait browser.delete_all_cookies()\n\n# Operações específicas do contexto\nawait browser.set_cookies(cookies, browser_context_id=context_id)\nawait browser.get_cookies(browser_context_id=context_id)\nawait browser.delete_all_cookies(browser_context_id=context_id)\n```\n\nEssas operações usam o **domínio Storage** internamente:\n\n-   `Storage.getCookies`: Recupera cookies para o contexto ou todos os contextos\n-   `Storage.setCookies`: Define cookies com domínio/caminho/validade\n-   `Storage.clearCookies`: Limpa cookies para o contexto ou todos os contextos\n\n!!! info \"Escopo de Armazenamento Navegador vs. Aba\"\n    - **Nível do Navegador**: Opera no navegador inteiro ou contexto específico\n    - **Nível da Aba**: Escopado para a origem atual da aba\n    \n    Use o nível do navegador para gerenciamento global de cookies (ex: definir cookies de sessão para todos os domínios). Use o nível da aba para operações específicas da origem (ex: limpar cookies após logout).\n\n### Concessões de Permissão\n\nO domínio do Navegador fornece **controle programático de permissões**, contornando os prompts do navegador:\n\n```python\nawait browser.grant_permissions(\n    [PermissionType.GEOLOCATION, PermissionType.NOTIFICATIONS],\n    origin='https://example.com',\n    browser_context_id=context_id\n)\n```\n\n**Arquitetura:**\n\n-   Permissões são concedidas via comando CDP `Browser.grantPermissions`\n-   Permissões são **específicas do contexto** (isoladas por contexto)\n-   Concessões sobrescrevem o comportamento padrão de prompt\n-   `reset_permissions()` reverte para o comportamento padrão\n\n### Gerenciamento de Download\n\nO comportamento de download é configurado através do comando `Browser.setDownloadBehavior`:\n\n```python\nawait browser.set_download_behavior(\n    behavior=DownloadBehavior.ALLOW,\n    download_path='/path/to/downloads',\n    events_enabled=True,  # Emitir eventos de progresso de download\n    browser_context_id=context_id\n)\n```\n\n**Opções:**\n\n-   `ALLOW`: Salvar no caminho especificado\n-   `DENY`: Cancelar todos os downloads\n-   `DEFAULT`: Mostrar UI de download padrão do navegador\n\n### Gerenciamento de Janela\n\nOperações de janela aplicam-se à **janela física do SO** de um alvo:\n\n```python\nwindow_id = await browser.get_window_id_for_target(target_id)\nawait browser.set_window_bounds({\n    'left': 100, 'top': 100,\n    'width': 1920, 'height': 1080,\n    'windowState': 'normal'  # ou 'minimized', 'maximized', 'fullscreen'\n})\n```\n\n**Detalhes da implementação:**\n\n-   Usa `Browser.getWindowForTarget` para resolver o ID da janela a partir do ID do alvo\n-   `Browser.setWindowBounds` modifica a geometria da janela\n-   **Modo headless**: Operações de janela são no-ops (não existem janelas físicas)\n\n## Insights Arquiteturais e Tradeoffs de Design\n\n### Registro Singleton de Abas: Por quê?\n\nO padrão de registro de abas (`_tabs_opened: dict[str, Tab]`) garante que:\n\n1.  **Roteamento de eventos funcione corretamente**: Eventos CDP contêm um `targetId`, mas nenhuma referência de Aba. O registro mapeia `targetId` → `Tab` para o despacho correto do callback.\n2.  **Consistência de estado**: Múltiplos caminhos de código que referenciam o mesmo alvo obtêm a **mesma instância de Aba**, prevenindo divergência de estado.\n3.  **Eficiência de memória**: Sem o registro, `get_opened_tabs()` criaria instâncias duplicadas de Aba a cada chamada.\n\n**Tradeoff:** O uso de memória cresce com a contagem de abas, mas isso é inevitável para instâncias de Aba com estado (stateful).\n\n### Por que start() Retorna uma Aba\n\nEsta decisão de design sacrifica a pureza pela **ergonomia**:\n\n-   **Desvantagem**: A aba inicial não pode ser evitada, mesmo em automação de múltiplas abas\n-   **Vantagem**: 90% dos usuários (scripts de aba única) não precisam de boilerplate:\n\n```python\n# Com start() retornando Aba\ntab = await browser.start()\n\n# Sem (design puro)\nawait browser.start()\ntab = await browser.new_tab()\n```\n\n**Alternativa explorada:** Fechar automaticamente a aba inicial em `new_tab()`. Rejeitada porque é um comportamento surpreendente (efeitos colaterais implícitos).\n\n### Autenticação de Proxy: Tradeoff da Arquitetura de Dois Níveis\n\nA autenticação de proxy do Pydoll usa duas estratégias diferentes do domínio Fetch:\n\n**Nível do Navegador (Proxy Global):**\n-   **Benefício de segurança**: Credenciais nunca registradas em logs CDP\n-   **Custo de performance**: Fetch pausa **todas as requisições em todas as abas** até que a autenticação seja concluída\n-   **Eficiência**: Único ouvinte para todas as abas no contexto padrão\n-   **Mitigação**: Fetch é desabilitado após a primeira autenticação, minimizando a sobrecarga\n\n**Nível da Aba (Proxy por Contexto):**\n-   **Benefício de segurança**: Credenciais nunca registradas em logs CDP\n-   **Custo de performance**: Fetch pausa **todas as requisições naquela aba** até que a autenticação seja concluída\n-   **Eficiência**: Ouvinte separado por aba (menos eficiente, mas necessário para isolamento)\n-   **Benefício de isolamento**: Cada contexto pode ter credenciais de proxy diferentes\n-   **Mitigação**: Fetch é desabilitado após a primeira autenticação por aba\n\n**Por que não usar `Browser.setProxyAuth`?** Este comando CDP não existe. Fetch é o único mecanismo para autenticação programática.\n\n**Por que em nível de aba para contextos?** O domínio Fetch do CDP não pode ser escopado para um BrowserContext específico. Como cada contexto pode ter um proxy diferente com credenciais diferentes, o Pydoll deve lidar com a autenticação no nível da aba para respeitar os limites do contexto.\n\n### Estratégia de Randomização de Porta\n\nPortas CDP aleatórias (9223-9322) previnem colisões ao executar instâncias paralelas de navegador:\n\n```python\nself._connection_port = connection_port or randint(9223, 9322)\n```\n\n**Por que não incrementar a partir de 9222?**\n\n-   Condições de corrida em ambientes multiprocesso (ex: pytest-xdist)\n-   Colisão com a seleção manual de porta do usuário\n\n**Tradeoff:** Portas aleatórias são mais difíceis de depurar (não podem ser hardcoded). Solução: `browser._connection_port` expõe a porta escolhida.\n\n### Separação de Componentes: Por que Gerenciadores?\n\nA classe Browser delega para gerenciadores especializados (ProcessManager, ProxyManager, TempDirManager, ConnectionHandler) para:\n\n1.  **Testabilidade**: Gerenciadores podem ser mockados independentemente\n2.  **Reusabilidade**: Lógica do ProxyManager compartilhada entre implementações do Browser\n3.  **Manutenibilidade**: Cada gerenciador tem responsabilidade única\n4.  **Multiplataforma**: Lógica específica do SO isolada no ProcessManager\n\n**Tradeoff:** Mais indireção, mas organização de código significativamente melhor em escala.\n\n## Principais Tópicos\n\n1.  **Navegador é um coordenador**, não um monolito. Ele orquestra gerenciadores e lida com a comunicação CDP.\n2.  **Registro de abas garante instâncias singleton** por alvo, crítico para roteamento de eventos e consistência de estado.\n3.  **Contextos de navegador são isolamento leve**, compartilhando o processo do navegador mas separando armazenamento/cache/autenticação.\n4.  **Autenticação de proxy via Fetch** é um tradeoff de segurança - esconde credenciais mas adiciona latência.\n5.  **Sistema de eventos tem dois níveis**: Em todo o navegador e específico da aba, com diferentes conexões WebSocket.\n6.  **Separação de componentes** (gerenciadores) melhora a testabilidade e o suporte multiplataforma.\n\n## Documentação Relacionada\n\nPara um entendimento mais profundo dos componentes arquiteturais relacionados:\n\n- **[Camada de Conexão](./connection-layer.md)**: Comunicação WebSocket, fluxo de comando/resposta, padrões assíncronos\n- **[Arquitetura de Eventos](./event-architecture.md)**: Despacho de eventos, gerenciamento de callback, habilitação de domínio\n- **[Domínio da Aba](./tab-domain.md)**: Operações em nível de aba, navegação de página, localização de elementos\n- **[Análise Profunda do CDP](./cdp.md)**: Fundamentos do Chrome DevTools Protocol\n- **[Arquitetura de Proxy](./proxy-architecture.md)**: Conceitos e implementação de proxy em nível de rede\n\nPara padrões de uso prático:\n\n- **[Gerenciamento de Abas](../features/browser-management/tabs.md)**: Padrões de automação de múltiplas abas\n- **[Contextos do Navegador](../features/browser-management/contexts.md)**: Isolamento de contexto na prática\n- **[Configuração de Proxy](../features/configuration/proxy.md)**: Configurando proxies e autenticação"
  },
  {
    "path": "docs/pt/deep-dive/architecture/browser-requests-architecture.md",
    "content": "# Arquitetura de Requisições no Contexto do Navegador\n\nEste documento explora o design arquitetural do sistema de requisições HTTP no contexto do navegador do Pydoll, que permite fazer requisições HTTP que herdam perfeitamente o estado de sessão, cookies e autenticação do navegador.\n\n!!! info \"Guia Prático Disponível\"\n    Esta é a análise profunda da arquitetura. Para exemplos práticos e casos de uso, consulte o [Guia de Requisições HTTP](../features/network/http-requests.md).\n\n## Visão Geral da Arquitetura\n\nRequisições no contexto do navegador resolvem um problema fundamental na automação híbrida: manter a continuidade da sessão entre interações de UI e chamadas de API. Abordagens tradicionais exigem a extração manual de cookies e cabeçalhos, criando um acoplamento frágil entre o navegador e o cliente HTTP.\n\nA arquitetura do Pydoll elimina essa complexidade executando requisições HTTP **dentro** do contexto JavaScript do navegador, enquanto aproveita os eventos de rede do CDP para capturar metadados abrangentes que o JavaScript sozinho não pode fornecer.\n\n### Por Que Essa Arquitetura?\n\n| Abordagem Tradicional | Arquitetura Pydoll |\n|---------------------|---------------------|\n| Cliente HTTP separado (requests, aiohttp) | Execução unificada baseada no navegador |\n| Extração e sincronização manual de cookies | Herança automática de cookies |\n| Dois estados de sessão separados | Estado de sessão único |\n| Manipulação limitada de CORS | Aplicação nativa de CORS do navegador |\n| Fluxos de autenticação complexos | Preservação transparente da autenticação |\n\n\n## Arquitetura de Componentes\n\nO sistema de requisições no contexto do navegador consiste em duas classes principais que trabalham juntas com o sistema de eventos do Pydoll:\n\n```mermaid\nclassDiagram\n    class Tab {\n        +request: Request\n        +enable_network_events()\n        +disable_network_events()\n        +get_network_response_body()\n        +on(event_name, callback)\n        +clear_callbacks()\n    }\n    \n    class Request {\n        -tab: Tab\n        -_network_events_enabled: bool\n        -_requests_sent: list\n        -_requests_received: list\n        +get(url, params, kwargs)\n        +post(url, data, json, kwargs)\n        +put(url, data, json, kwargs)\n        +patch(url, data, json, kwargs)\n        +delete(url, kwargs)\n        +head(url, kwargs)\n        +options(url, kwargs)\n        -_execute_fetch_request()\n        -_register_callbacks()\n        -_extract_headers()\n        -_extract_cookies()\n    }\n    \n    class Response {\n        -_status_code: int\n        -_content: bytes\n        -_text: str\n        -_json: dict\n        -_response_headers: list\n        -_request_headers: list\n        -_cookies: list\n        -_url: str\n        +ok: bool\n        +status_code: int\n        +text: str\n        +content: bytes\n        +url: str\n        +headers: list\n        +request_headers: list\n        +cookies: list\n        +json()\n        +raise_for_status()\n    }\n    \n    Tab *-- Request\n    Request ..> Response : cria\n    Request ..> Tab : usa eventos\n```\n\n### Classe Request\n\nA classe `Request` serve como a camada de interface, fornecendo uma API familiar semelhante à do `requests` enquanto orquestra a interação complexa entre a execução de JavaScript e o monitoramento de eventos de rede.\n\n**Principais Responsabilidades:**\n\n- Traduzir chamadas de método Python para JavaScript da API Fetch\n- Gerenciar ouvintes (listeners) de eventos de rede temporários\n- Acumular eventos de rede durante a execução da requisição\n- Extrair metadados de eventos CDP\n- Construir objetos Response com informações completas\n\n### Classe Response\n\nA classe `Response` fornece uma interface compatível com `requests.Response`, tornando a migração de clientes HTTP tradicionais contínua.\n\n**Principais Características:**\n\n- Múltiplos acessadores de conteúdo (texto, bytes, JSON)\n- Análise (parsing) preguiçosa (lazy) de JSON com cache\n- Informações abrangentes de cabeçalho (enviados e recebidos)\n- Extração de cookies dos cabeçalhos Set-Cookie\n- URL final após redirecionamentos\n\n## Fluxo de Execução\n\nA execução da requisição segue um pipeline de seis fases:\n\n```mermaid\nflowchart TD\n    Start([tab.request.get#40;url#41;]) --> Phase1[<b>1. Preparação</b><br/>Construir URL + opções]\n    \n    Phase1 --> Phase2[<b>2. Registro de Eventos</b><br/>Habilitar eventos de rede<br/>Registrar callbacks]\n    \n    Phase2 --> Phase3[<b>3. Execução JavaScript</b><br/>Runtime.evaluate&#40;fetch&#41;]\n    \n    Phase3 --> Phase4{<b>4. Atividade de Rede</b>}\n    Phase4 -->|Requisição enviada| Event1[REQUEST_WILL_BE_SENT]\n    Phase4 -->|Resposta recebida| Event2[RESPONSE_RECEIVED]\n    Phase4 -->|Informação extra| Event3[Eventos *_EXTRA_INFO]\n    \n    Event1 --> Collect[Coletar metadados]\n    Event2 --> Collect\n    Event3 --> Collect\n    \n    Collect --> Phase5[<b>5. Construção</b><br/>Extrair cabeçalhos/cookies<br/>Construir objeto Response]\n    \n    Phase5 --> Phase6[<b>6. Limpeza</b><br/>Limpar callbacks<br/>Desabilitar eventos]\n    \n    Phase6 --> End([Retornar Response])\n```\n\n### Detalhes das Fases\n\n| Fase | Camada | Operações Principais | Assíncrono |\n|-------|-------|----------------|--------------|\n| **1. Preparação** | Request | Construção de URL, formatação de opções | Não |\n| **2. Registro de Eventos** | Tab | Habilitar eventos, registrar callbacks | Sim |\n| **3. Execução JavaScript** | CDP/Navegador | Executar fetch() no contexto do navegador | Sim |\n| **4. Atividade de Rede** | Navegador/CDP | Requisição HTTP, emitir eventos CDP | Sim (paralelo) |\n| **5. Construção** | Request | Analisar eventos, construir Response | Não |\n| **6. Limpeza** | Tab | Remover callbacks, desabilitar eventos | Sim |\n\n## Integração com o Sistema de Eventos\n\nRequisições no contexto do navegador são fortemente integradas com a arquitetura do sistema de eventos do Pydoll. Entender essa relação é crucial.\n\n### Ciclo de Vida de Eventos Temporários\n\n```mermaid\nstateDiagram-v2\n    [*] --> NoEvents: Requisição inicia\n    NoEvents --> EventsEnabled: Habilitar eventos de rede\n    EventsEnabled --> CallbacksRegistered: Registrar callbacks\n    CallbacksRegistered --> ExecutingRequest: Executar fetch\n    ExecutingRequest --> CapturingEvents: Eventos disparam\n    CapturingEvents --> ExecutingRequest: Mais eventos\n    ExecutingRequest --> CleaningUp: Fetch completa\n    CleaningUp --> CallbacksRemoved: Limpar callbacks\n    CallbacksRemoved --> EventsDisabled: Desabilitar se necessário\n    EventsDisabled --> [*]: Requisição completa\n```\n\n### Por Que Usar Ambos, JavaScript e Eventos?\n\nUma pergunta comum: se o JavaScript pode executar a requisição, por que usar eventos de rede?\n\n| Fonte da Informação | JavaScript (API Fetch) | Eventos de Rede (CDP) |\n|-------------------|------------------------|----------------------|\n| Status da resposta | Disponível | Disponível |\n| Corpo da resposta | Disponível | Não disponível |\n| Cabeçalhos da resposta | Parcial (restrito por CORS) | Completo |\n| Cabeçalhos da requisição | Não acessível | Completo |\n| Cabeçalhos Set-Cookie | Ocultos pelo navegador | Disponível |\n| Informações de tempo (timing) | Limitadas | Abrangentes |\n| Cadeia de redirecionamento | Apenas URL final | Cadeia completa |\n\n**A Solução:** Combinar ambas as fontes para informações completas.\n\n!!! tip \"Tecnologias Complementares\"\n    O JavaScript fornece o corpo da resposta e dispara a requisição no contexto do navegador (com cookies, autenticação). Os eventos de rede fornecem os metadados que as políticas de segurança do JavaScript ocultam.\n\n### Tipos de Eventos de Rede CDP\n\nA arquitetura usa quatro tipos de eventos CDP para capturar metadados completos:\n\n| Evento | Propósito | Informação Chave |\n|-------|---------|----------------|\n| `REQUEST_WILL_BE_SENT` | Requisição principal de saída | URL, método, cabeçalhos padrão |\n| `REQUEST_WILL_BE_SENT_EXTRA_INFO` | Metadados adicionais da requisição | Cookies associados, cabeçalhos brutos |\n| `RESPONSE_RECEIVED` | Resposta principal recebida | Status, cabeçalhos, tipo MIME, tempo |\n| `RESPONSE_RECEIVED_EXTRA_INFO` | Metadados adicionais da resposta | Cabeçalhos Set-Cookie, informações de segurança |\n\n!!! info \"Multiplicidade de Eventos\"\n    Uma única requisição HTTP gera múltiplos eventos CDP. A classe Request acumula todos os eventos relacionados e extrai informações não duplicadas durante a fase de construção.\n\n## Arquitetura de Cabeçalhos e Cookies\n\n### Estratégia de Extração de Cabeçalhos\n\nCabeçalhos existem em múltiplos eventos CDP com potencial duplicação. A arquitetura usa uma estratégia de desduplicação:\n\n```mermaid\nflowchart TD\n    A[Eventos de Rede] --> B{Tipo de Evento}\n    B -->|Eventos REQUEST| C[Extrair Cabeçalhos Enviados]\n    B -->|Eventos RESPONSE| D[Extrair Cabeçalhos Recebidos]\n    \n    C --> E[Desduplicar por nome+valor]\n    D --> F[Desduplicar por nome+valor]\n    \n    E --> G[Lista de Cabeçalhos da Requisição]\n    F --> H[Lista de Cabeçalhos da Resposta]\n    \n    G --> I[Objeto Response]\n    H --> I\n```\n\n**Lógica de Desduplicação:**\n\n1. Eventos são processados em ordem\n2. Cada cabeçalho é identificado pela tupla `(nome, valor)`\n3. Apenas a primeira ocorrência de cada tupla é mantida\n4. Resultado: lista de cabeçalhos única e não redundante\n\n### Arquitetura de Análise de Cookies\n\nCookies exigem tratamento especial porque vêm dos cabeçalhos `Set-Cookie` nos eventos `RESPONSE_RECEIVED_EXTRA_INFO`:\n\n```mermaid\nflowchart TD\n    A[RESPONSE_RECEIVED_EXTRA_INFO] --> B[Extrair cabeçalhos Set-Cookie]\n    B --> C{Cabeçalho multi-linha?}\n    C -->|Sim| D[Dividir por nova linha]\n    C -->|Não| E[Analisar cookie único]\n    D --> F[Analisar cada linha]\n    F --> G[Extrair nome=valor]\n    E --> G\n    G --> H{Nome válido?}\n    H -->|Sim| I[Criar CookieParam]\n    H -->|Não| J[Descartar]\n    I --> K[Adicionar à lista de cookies]\n    K --> L[Desduplicar]\n    L --> M[Objeto Response]\n```\n\n**Princípios de Extração de Cookies:**\n\n- Apenas eventos `EXTRA_INFO` contêm cabeçalhos `Set-Cookie`\n- Atributos de cookie (Path, Domain, Secure, HttpOnly) são ignorados\n- O navegador gerencia atributos de cookie internamente\n- Apenas pares nome-valor são extraídos para fins informativos\n\n!!! warning \"Escopo dos Cookies\"\n    A propriedade `Response.cookies` contém apenas cookies **novos ou atualizados** desta resposta específica. Cookies existentes do navegador são gerenciados automaticamente e não expostos através desta interface.\n\n## Contexto de Execução JavaScript\n\nA execução da API Fetch acontece no contexto JavaScript do navegador, o que é fundamental para o poder da arquitetura:\n\n### Integração com a API Fetch\n\nA requisição é traduzida para JavaScript:\n\n```javascript\n// Representação simplificada\n(async () => {\n    const response = await fetch(url, {\n        method: 'GET',\n        headers: {'X-Custom': 'value'},\n        // O navegador adiciona automaticamente:\n        // - Cabeçalho Cookie\n        // - Authorization se definido\n        // - Cabeçalhos padrão (User-Agent, Accept, etc.)\n    });\n    \n    return {\n        status: response.status,\n        url: response.url,  // URL final após redirecionamentos\n        text: await response.text(),\n        content: new Uint8Array(await response.arrayBuffer()),\n        json: response.headers.get('Content-Type')?.includes('application/json')\n            ? await response.clone().json()\n            : null\n    };\n})()\n```\n\n### Benefícios do Contexto do Navegador\n\nExecutar no contexto do navegador fornece:\n\n| Benefício | Descrição |\n|---------|-------------|\n| **Inclusão Automática de Cookies** | O navegador envia todos os cookies aplicáveis automaticamente |\n| **Preservação do Estado de Autenticação** | Cabeçalhos de autenticação mantidos da sessão do navegador |\n| **Aplicação de CORS** | O navegador aplica as mesmas políticas CORS das interações do usuário |\n| **Manipulação de TLS/SSL** | A validação de certificado e políticas de segurança do navegador se aplicam |\n| **Compressão** | Manipulação automática de gzip, br, deflate |\n| **Redirecionamentos** | O navegador segue redirecionamentos transparentemente |\n| **Mesmo Contexto de Segurança** | A requisição parece idêntica às requisições iniciadas pelo usuário |\n\n!!! info \"Detecção Anti-Bot\"\n    Requisições executadas no contexto do navegador são indistinguíveis de requisições iniciadas pelo usuário, tornando-as eficazes contra sistemas anti-bot que analisam padrões de requisição.\n\n## Considerações de Performance\n\n### Sobrecarga de Eventos\n\nEventos de rede adicionam sobrecarga à execução da requisição:\n\n| Cenário | Sobrecarga | Recomendação |\n|----------|----------|----------------|\n| Requisição única | Baixa | Aceitável |\n| Múltiplas requisições sequenciais | Moderada | Habilitar eventos uma vez |\n| Requisições em massa (100+) | Alta | Considere habilitar eventos no nível da aba |\n| Automação de longa duração | Preocupação com memória | Desabilitar quando terminar |\n\n### Padrão de Otimização\n\n```python\n# Ineficiente - eventos habilitados/desabilitados repetidamente\nfor url in urls:\n    response = await tab.request.get(url)\n\n# Eficiente - eventos habilitados uma vez\nawait tab.enable_network_events()\nfor url in urls:\n    response = await tab.request.get(url)\nawait tab.disable_network_events()\n```\n\n!!! tip \"Otimização Automática\"\n    A classe Request verifica se os eventos de rede já estão habilitados e pula operações redundantes de habilitar/desabilitar automaticamente.\n\n### Estratégia de Análise JSON\n\nA análise JSON da resposta usa avaliação preguiçosa (lazy) com cache:\n\n1. Primeira chamada a `response.json()`: Analisa e armazena em cache\n2. Chamadas subsequentes: Retorna resultado do cache\n3. Se o JSON foi pré-analisado durante a construção: Usa esse\n\nIsso previne sobrecarga de análise redundante.\n\n## Arquitetura de Segurança\n\n### Aplicação da Política CORS\n\nRequisições no contexto do navegador respeitam as políticas CORS:\n\n```mermaid\nflowchart TD\n    A[tab.request.get&#40;url&#41;] --> B{Mesma Origem?}\n    B -->|Sim| C[Requisição Permitida]\n    B -->|Não| D{Cabeçalhos CORS Presentes?}\n    D -->|Sim| E[Requisição Permitida]\n    D -->|Não| F[Requisição Bloqueada]\n    \n    C --> G[Resposta Retornada]\n    E --> G\n    F --> H[Erro CORS]\n```\n\n**Comportamento do CORS:**\n\n- Requisições para mesma origem: Sempre permitidas\n- Requisições cross-origin: Exigem cabeçalhos CORS do servidor\n- Respostas opacas: Podem ser bloqueadas pelo navegador\n\n**Solução para problemas de CORS:**\n\nNavegue para o domínio primeiro para estabelecer um contexto de mesma origem:\n\n```python\nawait tab.go_to('https://different-domain.com')\nresponse = await tab.request.get('https://different-domain.com/api')\n```\n\n### Segurança de Cookies\n\nCookies com flags de segurança (`HttpOnly`, `Secure`, `SameSite`) são manipulados pelo navegador:\n\n- **Cookies HttpOnly**: Enviados automaticamente, mas não expostos ao JavaScript ou CDP\n- **Cookies Secure**: Enviados apenas sobre HTTPS\n- **Cookies SameSite**: O navegador aplica as políticas SameSite\n\nA propriedade `Response.cookies` pode não mostrar todos os cookies devido a essas restrições de segurança.\n\n### Validação TLS/SSL\n\nO navegador valida certificados SSL. Certificados autoassinados ou inválidos fazem com que as requisições falhem, a menos que:\n\n```python\noptions = ChromiumOptions()\noptions.add_argument('--ignore-certificate-errors')\nbrowser = Chrome(options=options)\n```\n\n!!! warning \"Compromisso de Segurança\"\n    Desabilitar a validação de certificados reduz a segurança. Use apenas em ambientes controlados.\n\n## Limitações e Decisões de Design\n\n### Tamanho do Corpo da Requisição\n\nCorpos de requisição muito grandes (arquivos, grandes conjuntos de dados) têm restrições de memória do JavaScript. Para uploads de arquivos, use `WebElement.set_input_files()` ou o interceptador de seletor de arquivos.\n\n### Manipulação de Resposta Binária\n\nRespostas binárias são convertidas através do `ArrayBuffer` e `Uint8Array` do JavaScript, o que adiciona alguma sobrecarga para respostas muito grandes (>100MB).\n\n### Transparência de Redirecionamento\n\nA API Fetch segue redirecionamentos automaticamente. Apenas a URL final é capturada. Se você precisar da cadeia de redirecionamento, use o monitoramento de rede separadamente.\n\n### Temporização de Eventos\n\nEventos devem ser registrados **antes** de executar o fetch. A arquitetura garante isso através da fase de registro, mas o manuseio manual de eventos requer uma temporização cuidadosa.\n\n## Princípios Arquiteturais\n\nA arquitetura de requisições no contexto do navegador adere a estes princípios:\n\n1. **Continuidade da Sessão**: Nunca quebrar o estado de sessão do navegador\n2. **Sincronização Manual Zero**: Nenhuma extração de cookie/cabeçalho necessária\n3. **Informação Completa**: Combinar JavaScript + eventos para metadados completos\n4. **Limpeza Automática**: Recursos liberados após cada requisição\n5. **Interface Familiar**: API compatível com `requests` para fácil adoção\n6. **Consciente de Performance**: Otimizar para casos de uso comuns\n7. **Consciente de Segurança**: Respeitar as políticas de segurança do navegador\n\n## Integração com Outros Sistemas\n\n### Dependência do Sistema de Eventos\n\nRequisições no contexto do navegador dependem da arquitetura do sistema de eventos:\n\n- Utiliza `Tab.on()` para registro de callback\n- Usa `Tab.clear_callbacks()` para limpeza\n- Respeita a habilitação existente de eventos de rede\n- Integra-se com o gerenciamento do ciclo de vida dos eventos\n\nVeja [Arquitetura do Sistema de Eventos](event-architecture.md) para detalhes.\n\n### Integração com o Sistema de Tipos\n\nA arquitetura usa o sistema de tipos do Python extensivamente:\n\n- `HeaderEntry` TypedDict para cabeçalhos\n- `CookieParam` TypedDict para cookies\n- Definições de tipo de evento de `pydoll.protocol.network.events`\n- Fornece autocomplete na IDE e segurança de tipos\n\nVeja [Sistema de Tipagem](typing-system.md) para detalhes.\n\n## Leitura Adicional\n\n- **[Guia de Requisições HTTP](../features/network/http-requests.md)** - Exemplos práticos e casos de uso\n- **[Arquitetura do Sistema de Eventos](event-architecture.md)** - Design interno do sistema de eventos\n- **[Monitoramento de Rede](../features/network/monitoring.md)** - Observação passiva de rede\n- **[Interceptação de Requisições](../features/network/interception.md)** - Modificação ativa de requisições\n- **[Sistema de Tipagem](typing-system.md)** - Integração do sistema de tipos\n\n## Resumo\n\nA arquitetura de requisições no contexto do navegador do Pydoll alcança comunicação HTTP contínua combinando a execução da API Fetch do JavaScript com o monitoramento de eventos de rede do CDP. Esta abordagem híbrida fornece:\n\n- **Metadados completos** de ambos os eventos JavaScript e CDP\n- **Continuidade automática da sessão** através da execução no contexto do navegador  \n- **Interface familiar** compatível com a biblioteca requests\n- **Otimização de performance** através da reutilização de eventos\n- **Conformidade de segurança** com as políticas do navegador\n\nA arquitetura demonstra como a combinação de tecnologias complementares (JavaScript + eventos CDP) pode resolver problemas complexos de forma elegante, fornecendo poder e conveniência sem comprometer a completude ou a segurança."
  },
  {
    "path": "docs/pt/deep-dive/architecture/event-architecture.md",
    "content": "# Arquitetura do Sistema de Eventos\n\nEste documento explora a arquitetura interna do sistema de eventos do Pydoll, cobrindo comunicação WebSocket, fluxo de eventos, gerenciamento de callbacks e considerações de performance.\n\n!!! info \"Guia de Uso Prático\"\n    Para exemplos práticos e padrões de uso, consulte o [Guia do Sistema de Eventos](../features/advanced/event-system.md).\n\n## Comunicação WebSocket e CDP\n\nNo núcleo do sistema de eventos do Pydoll está o Chrome DevTools Protocol (CDP), que fornece uma maneira estruturada de interagir e monitorar atividades do navegador através de conexões WebSocket. Este canal de comunicação bidirecional permite que seu código tanto envie comandos para o navegador quanto receba eventos de volta.\n\n```mermaid\nsequenceDiagram\n    participant Client as Código Pydoll\n    participant Connection as Manipulador de Conexão\n    participant WebSocket\n    participant Browser as Navegador\n    \n    Client->>Connection: Registra callback para evento\n    Connection->>Connection: Armazena callback no registro\n    \n    Client->>Connection: Habilita domínio do evento\n    Connection->>WebSocket: Envia comando CDP para habilitar domínio\n    WebSocket->>Browser: Encaminha comando\n    Browser-->>WebSocket: Confirma domínio habilitado\n    WebSocket-->>Connection: Encaminha resposta\n    Connection-->>Client: Domínio habilitado\n    \n    Browser->>WebSocket: Evento ocorre, envia mensagem de evento CDP\n    WebSocket->>Connection: Encaminha mensagem de evento\n    Connection->>Connection: Procura callbacks para este evento\n    Connection->>Client: Executa callback registrado\n```\n\n### Modelo de Comunicação WebSocket\n\nA conexão WebSocket entre o Pydoll e o navegador segue este padrão:\n\n1.  **Estabelecimento da Conexão**: Quando o navegador inicia, um servidor WebSocket é criado, e o Pydoll estabelece uma conexão com ele\n2.  **Mensagens Bidirecionais**: Tanto o Pydoll quanto o navegador podem enviar mensagens a qualquer momento\n3.  **Tipos de Mensagem**:\n    -   **Comandos**: Enviados do Pydoll para o navegador (ex: navegação, manipulação do DOM)\n    -   **Respostas de Comandos**: Enviadas do navegador para o Pydoll em resposta a comandos\n    -   **Eventos**: Enviados do navegador para o Pydoll quando algo acontece (ex: carregamento da página, atividade de rede)\n\n### Estrutura do Chrome DevTools Protocol\n\nO CDP organiza sua funcionalidade em domínios, cada um responsável por uma área específica da funcionalidade do navegador:\n\n| Domínio | Responsabilidade | Eventos Típicos |\n|--------|----------------|----------------|\n| Page | Ciclo de vida da página | Eventos de carregamento, navegação, diálogos |\n| Network | Atividade de rede | Monitoramento de requisição/resposta, WebSockets |\n| DOM | Estrutura do documento | Mudanças no DOM, modificações de atributos |\n| Fetch | Interceptação de requisição | Requisição pausada, autenticação necessária |\n| Runtime | Execução JavaScript | Mensagens do console, exceções |\n| Browser | Gerenciamento do navegador | Criação de janelas, abas, contextos |\n\nCada domínio must ser explicitamente habilitado antes de começar a emitir eventos, o que ajuda a gerenciar a performance processando apenas os eventos que são realmente necessários.\n\n## Arquitetura de Domínio\n\n### O Padrão Habilitar/Desabilitar (Enable/Disable)\n\nO padrão explícito de habilitar/desabilitar atende a vários propósitos arquiteturais importantes:\n\n1.  **Otimização de Performance**: Ao habilitar apenas os domínios nos quais você está interessado, você reduz a sobrecarga (overhead) do processamento de eventos\n2.  **Gerenciamento de Recursos**: Alguns domínios de eventos (como monitoramento de Rede ou DOM) podem gerar grandes volumes de eventos que consomem memória\n3.  **Conformidade com o Protocolo**: O CDP exige a habilitação explícita do domínio antes que os eventos sejam emitidos\n4.  **Limpeza Controlada**: Desabilitar explicitamente os domínios garante uma limpeza adequada quando os eventos não são mais necessários\n\n```mermaid\nstateDiagram-v2\n    [*] --> Disabled: Estado Inicial\n    Disabled --> Enabled: enable_xxx_events()\n    Enabled --> Disabled: disable_xxx_events()\n    Enabled --> [*]: Aba Fechada\n    Disabled --> [*]: Aba Fechada\n```\n\n!!! warning \"Prevenção de Vazamento de Eventos\"\n    A falha em desabilitar domínios de eventos quando eles não são mais necessários pode levar a vazamentos de memória e degradação de performance, especialmente em automações de longa duração. Sempre desabilite os domínios de eventos quando terminar de usá-los, particularmente para eventos de alto volume, como monitoramento de rede.\n\n### Métodos de Habilitação Específicos do Domínio\n\nDiferentes domínios são habilitados através de métodos específicos nos objetos apropriados:\n\n| Domínio | Método de Habilitação | Método de Desabilitação | Disponível Em |\n|--------|--------------|----------------|--------------|\n| Page | `enable_page_events()` | `disable_page_events()` | Aba |\n| Network | `enable_network_events()` | `disable_network_events()` | Aba |\n| DOM | `enable_dom_events()` | `disable_dom_events()` | Aba |\n| Fetch | `enable_fetch_events()` | `disable_fetch_events()` | Aba, Navegador |\n| File Chooser | `enable_intercept_file_chooser_dialog()` | `disable_intercept_file_chooser_dialog()` | Aba |\n\n!!! info \"Propriedade do Domínio\"\n    Eventos pertencem a domínios específicos com base em sua funcionalidade. Alguns domínios estão disponíveis apenas em certos níveis - por exemplo, eventos de Página (Page) estão disponíveis na instância da Aba (Tab), mas não diretamente noível do Navegador (Browser).\n\n## Sistema de Registro de Eventos\n\n### O Método `on()`\n\nO método central para se inscrever (subscribing) em eventos é o método `on()`, disponível tanto nas instâncias de Aba (Tab) quanto de Navegador (Browser):\n\n```python\nasync def on(\n    self, event_name: str, callback: callable, temporary: bool = False\n) -> int:\n    \"\"\"\n    Registra um ouvinte (listener) de evento.\n\n    Args:\n        event_name (str): O nome do evento a ser ouvido.\n        callback (callable): A função de callback a ser executada quando o\n            evento é disparado.\n        temporary (bool): Se True, o callback será removido após ser\n            disparado uma vez. O padrão é False.\n\n    Returns:\n        int: O ID do callback registrado.\n    \"\"\"\n```\n\nEste método retorna um ID de callback que pode ser usado para remover o callback posteriormente, se necessário.\n\n### Registro de Callback\n\nInternamente, o `ConnectionHandler` (Manipulador de Conexão) mantém um registro de callbacks:\n\n```python\n{\n    'Page.loadEventFired': [\n        (callback_id_1, callback_function_1, temporary=False),\n        (callback_id_2, callback_function_2, temporary=True),\n    ],\n    'Network.requestWillBeSent': [\n        (callback_id_3, callback_function_3, temporary=False),\n    ]\n}\n```\n\nQuando um evento chega via WebSocket:\n\n1.  O nome do evento é extraído da mensagem\n2.  O registro é consultado por callbacks correspondentes\n3.  Cada callback é executado com os dados do evento\n4.  Callbacks temporários são removidos após a execução\n\n### Manipulação de Callback Assíncrono\n\nCallbacks podem ser síncronos ou assíncronos. O sistema de eventos lida com ambos:\n\n```python\nasync def _trigger_callbacks(self, event_name: str, event_data: dict):\n    for cb_id, cb_data in self._event_callbacks.items():\n        if cb_data['event'] == event_name:\n            if asyncio.iscoroutinefunction(cb_data['callback']):\n                await cb_data['callback'](event_data)\n            else:\n                cb_data['callback'](event_data)\n```\n\nCallbacks assíncronos são aguardados (awaited) sequencialmente. Isso significa que cada callback é concluído antes que o próximo seja executado, o que é importante para:\n\n-   **Ordem de Execução Previsível**: Callbacks executam na ordem de registro\n-   **Tratamento de Erros**: Exceções em um callback não impedem que outros sejam executados\n-   **Consistência de Estado**: Callbacks podem confiar em mudanças de estado sequenciais\n\n!!! info \"Execução Sequencial vs. Concorrente\"\n    Callbacks são executados sequencialmente dentro do mesmo evento. No entanto, eventos diferentes podem ser processados concorrentemente, já que o loop de eventos lida com múltiplas conexões simultaneamente.\n\n## Fluxo e Ciclo de Vida do Evento\n\nO ciclo de vida do evento segue estes passos:\n\n```mermaid\nflowchart TD\n    A[Atividade do Navegador] -->|Gera| B[Evento CDP]\n    B -->|Enviado via WebSocket| C[Manipulador de Conexão]\n    C -->|Filtra por Nome de Evento| D{Callbacks Registrados?}\n    D -->|Sim| E[Processar Evento]\n    D -->|Não| F[Descartar Evento]\n    E -->|Para Cada Callback| G[Executar Callback]\n    G -->|Se Temporário| H[Remover Callback]\n    G -->|Se Permanente| I[Manter para Eventos Futuros]\n```\n\n### Fluxo Detalhado\n\n1.  **Atividade do Navegador**: Algo acontece no navegador (página carrega, requisição enviada, DOM muda)\n2.  **Geração de Evento CDP**: O navegador gera uma mensagem de evento CDP\n3.  **Transmissão WebSocket**: A mensagem é enviada pelo WebSocket para o Pydoll\n4.  **Recepção do Evento**: O ConnectionHandler recebe o evento\n5.  **Busca de Callback**: O ConnectionHandler verifica seu registro por callbacks que correspondem ao nome do evento\n6.  **Execução do Callback**: Se callbacks existirem, cada um é executado com os dados do evento\n7.  **Remoção Temporária**: Se um callback foi registrado como temporário, ele é removido após a execução\n\n## Eventos em Nível de Navegador vs. Nível de Aba\n\nO sistema de eventos do Pydoll opera tanto no nível do navegador quanto no nível da aba, com distinções importantes:\n\n```mermaid\ngraph TD\n    Browser[Instância do Navegador] -->|\"Eventos Globais (ex: eventos de Target)\"| BrowserCallbacks[Callbacks de Nível de Navegador]\n    Browser -->|\"Cria\"| Tab1[Instância de Aba 1]\n    Browser -->|\"Cria\"| Tab2[Instância de Aba 2]\n    Tab1 -->|\"Eventos Específicos da Aba\"| Tab1Callbacks[Callbacks da Aba 1]\n    Tab2 -->|\"Eventos Específicos da Aba\"| Tab2Callbacks[Callbacks da Aba 2]\n```\n\n### Eventos em Nível de Navegador\n\nEventos em nível de navegador operam globalmente em todas as abas. Estes são limitados a domínios específicos como:\n\n-   **Eventos de Alvo (Target)**: Criação, destruição, falha (crash) de abas\n-   **Eventos do Navegador**: Gerenciamento de janelas, coordenação de downloads\n\n```python\n# Registro de evento em nível de navegador\nawait browser.on('Target.targetCreated', handle_new_target)\n```\n\nOs domínios de eventos em nível de navegador são limitados, e tentar usar eventos específicos de abas levantará uma exceção.\n\n### Eventos em Nível de Aba\n\nEventos em nível de aba são específicos para uma aba individual:\n\n```python\n# Cada aba tem seu próprio contexto de evento\ntab1 = await browser.start()\ntab2 = await browser.new_tab()\n\nawait tab1.enable_page_events()\nawait tab1.on(PageEvent.LOAD_EVENT_FIRED, handle_tab1_load)\n\nawait tab2.enable_page_events()\nawait tab2.on(PageEvent.LOAD_EVENT_FIRED, handle_tab2_load)\n```\n\nEsta arquitetura permite:\n\n-   **Manipulação Isolada de Eventos**: Eventos em uma aba não afetam outras\n-   **Configuração por Aba**: Abas diferentes podem monitorar tipos de eventos diferentes\n-   **Eficiência de Recursos**: Habilite eventos apenas nas abas que precisam deles\n\n!!! info \"Escopo Específico do Domínio\"\n    Nem todos os domínios de eventos estão disponíveis em ambos os níveis:\n    \n    -   **Eventos Fetch**: Disponíveis tanto no nível do navegador quanto da aba\n    -   **Eventos de Página (Page)**: Disponíveis apenas no nível da aba\n    -   **Eventos de Alvo (Target)**: Disponíveis apenas no nível do navegador\n\n## Arquitetura de Performance\n\n### Sobrecarga (Overhead) do Sistema de Eventos\n\nO sistema de eventos adiciona sobrecarga (overhead) à automação do navegador, especialmente para eventos de alta frequência:\n\n| Domínio do Evento | Volume Típico de Eventos | Impacto na Performance |\n|--------------|---------------------|-------------------|\n| Page | Baixo | Mínimo |\n| Network | Alto | Moderado a Alto |\n| DOM | Muito Alto | Alto |\n| Fetch | Moderado | Moderado (maior se estiver interceptando) |\n\n### Estratégias de Otimização de Performance\n\n1.  **Habilitação Seletiva de Domínio**: Apenas habilite domínios de eventos que você está usando ativamente\n2.  **Definição Estratégica de Escopo**: Use eventos em nível de navegador apenas para preocupações que sejam verdadeiramente globais\n3.  **Desabilitação Oportuna**: Sempre desabilite os domínios de eventos quando terminar de usá-los\n4.  **Filtragem Precoce**: Nos callbacks, filtre eventos irrelevantes o mais cedo possível\n5.  **Callbacks Temporários**: Use a flag `temporary=True` para eventos de ocorrência única\n\n### Gerenciamento de Memória\n\nO sistema de eventos gerencia a memória através de vários mecanismos:\n\n1.  **Limpeza do Registro de Callbacks**: Remover callbacks libera suas referências\n2.  **Auto-Remoção Temporária**: Callbacks temporários são limpos automaticamente\n3.  **Desabilitação de Domínio**: Desabilitar um domínio interrompe a geração de eventos\n4.  **Fechamento da Aba**: Quando uma aba fecha, todos os seus callbacks são removidos automaticamente\n\n!!! warning \"Prevenção de Vazamento de Memória\"\n    Em automações de longa duração, sempre limpe os callbacks e desabilite os domínios quando terminar. Eventos de alta frequência (especialmente DOM) podem acumular memória significativa se deixados habilitados.\n\n## Arquitetura do Manipulador de Conexão (Connection Handler)\n\nO `ConnectionHandler` é o componente central que gerencia a comunicação WebSocket e o despacho de eventos.\n\n### Principais Responsabilidades\n\n1.  **Gerenciamento de WebSocket**: Estabelecer e manter a conexão WebSocket\n2.  **Roteamento de Mensagens**: Distinguir entre respostas de comandos e eventos\n3.  **Registro de Callbacks**: Manter o mapeamento de nomes de eventos para callbacks\n4.  **Despacho de Eventos**: Executar callbacks registrados quando os eventos chegam\n5.  **Limpeza**: Remover callbacks e fechar conexões\n\n### Estrutura Interna\n\n```python\nclass ConnectionHandler:\n    def __init__(self, ...):\n        self._events_handler = EventsManager()\n        self._websocket = None\n        # ... outros atributos\n    \n    async def register_callback(self, event_name, callback, temporary):\n        return self._events_handler.register_callback(event_name, callback, temporary)\n\nclass EventsManager:\n    def __init__(self):\n        self._event_callbacks = {}  # ID do Callback -> dados do callback\n        self._callback_id = 0\n    \n    def register_callback(self, event_name, callback, temporary):\n        self._callback_id += 1\n        self._event_callbacks[self._callback_id] = {\n            'event': event_name,\n            'callback': callback,\n            'temporary': temporary\n        }\n        return self._callback_id\n    \n    async def _trigger_callbacks(self, event_name, event_data):\n        callbacks_to_remove = []\n        \n        for cb_id, cb_data in self._event_callbacks.items():\n            if cb_data['event'] == event_name:\n                # Executa callback (await se assíncrono, chama diretamente se síncrono)\n                if asyncio.iscoroutinefunction(cb_data['callback']):\n                    await cb_data['callback'](event_data)\n                else:\n                    cb_data['callback'](event_data)\n                \n                # Marca callbacks temporários para remoção\n                if cb_data['temporary']:\n                    callbacks_to_remove.append(cb_id)\n        \n        # Remove callbacks temporários após todos os callbacks serem executados\n        for cb_id in callbacks_to_remove:\n            self.remove_callback(cb_id)\n```\n\nEsta arquitetura garante:\n\n-   **Busca Eficiente**: Nomes de eventos mapeiam diretamente para listas de callbacks\n-   **Sobrecarga Mínima**: Apenas eventos registrados são processados\n-   **Limpeza Automática**: Callbacks temporários são removidos após a execução\n-   **Segurança Assíncrona (Async-safe)**: Operações são seguras em ambientes assíncronos\n\n## Formato da Mensagem de Evento\n\nEventos CDP seguem um formato de mensagem padronizado:\n\n```json\n{\n    \"method\": \"Network.requestWillBeSent\",\n    \"params\": {\n        \"requestId\": \"1234.56\",\n        \"loaderId\": \"7890.12\",\n        \"documentURL\": \"https://example.com\",\n        \"request\": {\n            \"url\": \"https://api.example.com/data\",\n            \"method\": \"GET\",\n            \"headers\": {...}\n        },\n        \"timestamp\": 123456.789,\n        \"wallTime\": 1234567890.123,\n        \"initiator\": {...},\n        \"type\": \"XHR\"\n    }\n}\n```\n\nComponentes principais:\n\n-   **`method`**: O nome do evento no formato `Dominio.nomeDoEvento`\n-   **`params`**: Dados específicos do evento, variam por tipo de evento\n-   **Sem campo `id`**: Diferente dos comandos, eventos não têm IDs de requisição\n\nO sistema de eventos extrai o campo `method` para rotear para os callbacks apropriados, passando a mensagem inteira para cada callback.\n\n## Coordenação de Eventos Multi-Aba\n\nA arquitetura do Pydoll suporta coordenação sofisticada de eventos multi-aba:\n\n### Contextos de Aba Independentes\n\nCada aba mantém seus próprios:\n\n-   Estado de habilitação de domínio de evento\n-   Registro de callbacks\n-   Canal de comunicação de evento\n-   Logs de rede (se eventos de rede estiverem habilitados)\n\n!!! info \"Arquitetura de Comunicação\"\n    Cada aba tem seu próprio canal de comunicação de eventos para o navegador. Para detalhes técnicos sobre como conexões WebSocket e IDs de alvo (target) funcionam no nível do protocolo, consulte [Arquitetura do Domínio do Navegador](./browser-domain.md).\n\n### Contexto de Navegador Compartilhado\n\nMúltiplas abas podem compartilhar:\n\n-   Ouvintes de eventos em nível de navegador\n-   Armazenamento de cookies\n-   Cache\n-   Processo do navegador\n\nEsta arquitetura permite:\n\n1.  **Processamento Paralelo de Eventos**: Múltiplas abas podem processar eventos simultaneamente\n2.  **Falhas Isoladas**: Problemas em uma aba não afetam outras\n3.  **Compartilhamento de Recursos**: Recursos comuns do navegador são compartilhados eficientemente\n4.  **Ações Coordenadas**: Eventos em nível de navegador podem coordenar atividades entre abas\n\n## Conclusão\n\nA arquitetura do sistema de eventos do Pydoll é projetada para:\n\n-   **Performance**: Sobrecarga mínima através de habilitação seletiva de domínio e despacho eficiente de callbacks\n-   **Flexibilidade**: Suporte para eventos tanto em nível de navegador quanto de aba\n-   **Escalabilidade**: Lidar com múltiplas abas com contextos de eventos independentes\n-   **Confiabilidade**: Limpeza automática e gerenciamento de memória\n\nEntender esta arquitetura ajuda você a:\n\n-   **Otimizar Performance**: Saber quais domínios têm alta sobrecarga\n-   **Depurar Problemas**: Entender o fluxo de eventos quando as coisas não funcionam como esperado\n-   **Projetar Automação Melhor**: Alavancar a arquitetura para fluxos de trabalho eficientes orientados a eventos\n-   **Evitar Armadilhas**: Prevenir vazamentos de memória e degradação de performance\n\nPara padrões de uso práticos e exemplos, consulte o [Guia do Sistema de Eventos](../features/advanced/event-system.md)."
  },
  {
    "path": "docs/pt/deep-dive/architecture/find-elements-mixin.md",
    "content": "# Arquitetura do Mixin FindElements\n\nO FindElementsMixin representa uma decisão arquitetural crítica no Pydoll: usar **composição sobre herança** para compartilhar capacidades de localização de elementos entre `Tab` e `WebElement` sem acoplá-los através de uma classe base comum. Este documento explora o padrão mixin, sua implementação e a mecânica interna de localização de elementos.\n\n!!! info \"Guia de Uso Prático\"\n    Para exemplos práticos e padrões de uso, consulte o [Guia de Localização de Elementos](../features/automation/element-finding.md) e o [Guia de Seletores](./selectors-guide.md).\n\n## Padrão Mixin: Filosofia de Design\n\n### O que é um Mixin?\n\nUm mixin é uma classe projetada para **fornecer métodos a outras classes** sem ser uma classe base em uma hierarquia de herança tradicional. Diferente da herança padrão (que modela relações \"é-um\" (is-a)), mixins modelam **capacidades \"pode-fazer\" (can-do)**.\n\n```python\n# Herança tradicional: \"é-um\" (is-a)\nclass Animal:\n    def breathe(self): ...\n\nclass Dog(Animal):  # Dog É-UM Animal\n    def bark(self): ...\n\n# Padrão Mixin: \"pode-fazer\" (can-do)\nclass FlyableMixin:\n    def fly(self): ...\n\nclass Bird(Animal, FlyableMixin):  # Bird É-UM Animal, PODE voar\n    pass\n```\n\n### Por que Mixins em vez de Herança?\n\nO Pydoll enfrenta um desafio arquitetural específico:\n\n- **`Tab`** precisa encontrar elementos no **contexto do documento**\n- **`WebElement`** precisa encontrar elementos **relativos a si mesmo** (elementos filhos)\n- Ambos precisam de **lógica de seletor idêntica** (CSS, XPath, construção de atributos)\n\n**Opção 1: Classe Base Compartilhada**\n\n```python\nclass ElementLocator:\n    def find(...): ...\n\nclass Tab(ElementLocator):\n    pass\n\nclass WebElement(ElementLocator):\n    pass\n```\n\n**Problemas:**\n- Alto acoplamento: `Tab` e `WebElement` agora compartilham a hierarquia de herança\n- Viola a Responsabilidade Única: `Tab` não deveria herdar da mesma classe que `WebElement`\n- Difícil de estender: Adicionar novas capacidades requer modificar a classe base\n\n**Opção 2: Padrão Mixin (Abordagem Escolhida)**\n\n```python\nclass FindElementsMixin:\n    def find(...): ...\n    def query(...): ...\n\nclass Tab(FindElementsMixin):\n    # Lógica específica do Tab\n    pass\n\nclass WebElement(FindElementsMixin):\n    # Lógica específica do WebElement\n    pass\n```\n\n**Benefícios:**\n\n- **Desacoplamento**: `Tab` e `WebElement` permanecem independentes\n- **Reutilização**: Mesma lógica de localização de elementos em ambas as classes\n- **Componibilidade**: Pode adicionar outros mixins sem conflitos\n- **Testabilidade**: O Mixin pode ser testado isoladamente\n\n!!! tip \"Características do Mixin\"\n    1. **Sem Estado (Stateless)**: Mixins não mantêm seu próprio estado (sem `__init__`)\n    2. **Injeção de Dependência**: Assume que a classe consumidora fornece dependências (ex: `_connection_handler`)\n    3. **Propósito Único**: Cada mixin fornece uma capacidade coesa\n    4. **Não Instanciável**: Nunca crie `FindElementsMixin()` diretamente\n\n## Implementação do Mixin no Pydoll\n\n### Estrutura da Classe\n\nO FindElementsMixin usa **injeção de dependência** para funcionar com qualquer classe que forneça um `_connection_handler`:\n\n```python\nclass FindElementsMixin:\n    \"\"\"\n    Mixin que fornece capacidades de localização de elementos.\n    \n    Assume que a classe consumidora possui:\n    - _connection_handler: Instância de ConnectionHandler para comandos CDP\n    - _object_id: Optional[str] para buscas relativas ao contexto (apenas WebElement)\n    \"\"\"\n    \n    if TYPE_CHECKING:\n        _connection_handler: ConnectionHandler  # Dica de tipo (type hint), não um atributo real\n    \n    async def find(self, ...):\n        # Implementação usa self._connection_handler\n        # Verifica self._object_id para determinar o contexto\n```\n\n**Insight principal:** O mixin não define `_connection_handler` ou `_object_id`. Ele **assume** que eles existem via duck typing.\n\n### Como Tab e WebElement Usam o Mixin\n\n```python\n# Tab: buscas em nível de documento\nclass Tab(FindElementsMixin):\n    def __init__(self, browser, target_id, connection_port):\n        self._connection_handler = ConnectionHandler(connection_port)\n        # Sem _object_id → busca a partir da raiz do documento\n\n# WebElement: buscas relativas ao elemento\nclass WebElement(FindElementsMixin):\n    def __init__(self, object_id, connection_handler, ...):\n        self._object_id = object_id  # ID do objeto CDP\n        self._connection_handler = connection_handler\n        # Tem _object_id → busca relativa a este elemento\n```\n\n**Distinção crítica:**\n\n- **Tab**: `hasattr(self, '_object_id')` → `False` → usa `RuntimeCommands.evaluate()` (contexto do documento)\n- **WebElement**: `hasattr(self, '_object_id')` → `True` → usa `RuntimeCommands.call_function_on()` (contexto do elemento)\n\n### Detecção de Contexto\n\nO mixin detecta dinamicamente o contexto da busca:\n\n```python\nasync def _find_element(self, by, value, raise_exc=True):\n    if hasattr(self, '_object_id'):\n        # Busca relativa: chama a função JavaScript NESTE elemento\n        command = self._get_find_element_command(by, value, self._object_id)\n    else:\n        # Busca no documento: avalia o JavaScript no contexto global\n        command = self._get_find_element_command(by, value)\n    \n    response = await self._execute_command(command)\n    # ...\n```\n\nEsta implementação única lida com ambos:\n\n- `tab.find(id='submit')` → busca no documento inteiro\n- `form_element.find(id='submit')` → busca dentro do `form_element`\n\n!!! warning \"Acoplamento de Dependência do Mixin\"\n    O mixin é **fortemente acoplado** ao modelo de objeto do CDP. Ele assume que:\n    \n    - Elementos são representados por strings `objectId`\n    - `Runtime.evaluate()` para buscas no documento\n    - `Runtime.callFunctionOn()` para buscas relativas a elementos\n    \n    Isso é aceitável porque o Pydoll é **específico do CDP**. Um design mais genérico exigiria camadas de abstração.\n\n## Design da API Pública\n\nO mixin expõe dois métodos de alto nível com filosofias de design distintas:\n\n### find(): Seleção Baseada em Atributos\n\n```python\n@overload\nasync def find(self, find_all: Literal[False], ...) -> WebElement: ...\n\n@overload\nasync def find(self, find_all: Literal[True], ...) -> list[WebElement]: ...\n\nasync def find(\n    self,\n    id: Optional[str] = None,\n    class_name: Optional[str] = None,\n    name: Optional[str] = None,\n    tag_name: Optional[str] = None,\n    text: Optional[str] = None,\n    timeout: int = 0,\n    find_all: bool = False,\n    raise_exc: bool = True,\n    **attributes,\n) -> Union[WebElement, list[WebElement], None]:\n```\n\n**Decisões de design:**\n\n1. **Kwargs (argumentos nomeados) em vez de Enum By posicional**:\n   ```python\n   # Pydoll (intuitivo)\n   await tab.find(id='submit', class_name='primary')\n   \n   # Selenium (verboso)\n   driver.find_element(By.ID, 'submit')  # Não pode combinar atributos facilmente\n   ```\n\n2. **Resolução automática para o seletor ideal**:\n   - Atributo único → usa `By.ID`, `By.CLASS_NAME`, etc. (mais rápido)\n   - Múltiplos atributos → constrói XPath (flexível, mas mais lento)\n\n3. **`**attributes` para extensibilidade**:\n   ```python\n   await tab.find(data_testid='submit-btn', aria_label='Submit form')\n   # Constrói: //\\*[@data-testid='submit-btn' and @aria-label='Submit form']\n   ```\n\n### query(): Seleção Baseada em Expressão\n\n```python\n@overload\nasync def query(self, expression, find_all: Literal[False], ...) -> WebElement: ...\n\n@overload\nasync def query(self, expression, find_all: Literal[True], ...) -> list[WebElement]: ...\n\nasync def query(\n    self, \n    expression: str, \n    timeout: int = 0, \n    find_all: bool = False, \n    raise_exc: bool = True\n) -> Union[WebElement, list[WebElement], None]:\n```\n\n**Decisões de design:**\n\n1. **Detecção automática de CSS vs XPath**:\n   ```python\n   # Detecção de XPath (começa com / ou ./)\n   await tab.query(\"//div[@id='content']\")\n   \n   # Detecção de CSS (padrão)\n   await tab.query(\"div#content > p.intro\")\n   ```\n\n2. **Parâmetro de expressão única** (diferente do `find()`):\n   - Assume que o usuário conhece a sintaxe do seletor\n   - Sem sobrecarga de abstração\n\n3. **Passagem direta (passthrough) para o navegador**:\n   - `querySelector()` / `querySelectorAll()` para CSS\n   - `document.evaluate()` para XPath\n\n### Padrão de Sobrecarga (Overload) para Segurança de Tipos\n\nAmbos os métodos usam `@overload` para fornecer **tipos de retorno precisos**:\n\n```python\n# A IDE sabe que o tipo de retorno é WebElement\nelement = await tab.find(id='submit')\n\n# A IDE sabe que o tipo de retorno é list[WebElement]\nelements = await tab.find(class_name='item', find_all=True)\n\n# A IDE sabe que o tipo de retorno é Optional[WebElement]\nmaybe_element = await tab.find(id='optional', raise_exc=False)\n```\n\nIsso é crítico para o autocomplete da IDE e verificação de tipos. Veja [Análise Profunda do Sistema de Tipos](./typing-system.md) para detalhes.\n\n## Arquitetura de Resolução de Seletor\n\nO mixin converte a entrada do usuário em comandos CDP através de um pipeline de resolução:\n\n| Estágio | Entrada | Saída | Decisão Chave |\n|-------|-------|--------|-------------|\n| **1. Seleção de Método** | `find()` kwargs ou `query()` expressão | Estratégia de seletor | Baseado em atributo vs. baseado em expressão |\n| **2. Resolução da Estratégia** | Atributos ou expressão | Enum `By` + valor | Atributo único → método nativo, Múltiplos → XPath |\n| **3. Detecção de Contexto** | `By` + valor + `hasattr(_object_id)` | Tipo de comando CDP | Documento vs. busca relativa ao elemento |\n| **4. Geração do Comando** | Tipo de comando CDP + seletor | JavaScript + método CDP | `evaluate()` vs `callFunctionOn()` |\n| **5. Execução** | Comando CDP | `objectId` ou array de `objectId`s | Via ConnectionHandler |\n| **6. Criação do WebElement** | `objectId` + atributos | Instância(s) de `WebElement` | Função de fábrica (factory) para evitar importações circulares |\n\n### Principais Decisões Arquiteturais\n\n**1. Atributos Únicos vs. Múltiplos**\n\n```python\n# Atributo único → Seletor direto (rápido)\nawait tab.find(id='username')  # Usa By.ID → getElementById()\n\n# Múltiplos atributos → XPath (flexível)\nawait tab.find(tag_name='input', type='password', name='pwd')\n# → //input[@type='password' and @name='pwd']\n```\n\n**Por que isso importa:**\n- Métodos nativos (`getElementById`, `getElementsByClassName`) são 10-50% mais rápidos que XPath\n- A sobrecarga do XPath é aceitável ao combinar atributos (não há alternativa)\n\n**2. Detecção Automática do Tipo de Seletor**\n\n```python\nawait tab.query(\"//div\")       # Começa com / → XPath\nawait tab.query(\"#login\")      # Padrão → CSS\n```\n\n**Implementação:**\n```python\nif expression.startswith(('./', '/', '(/')):\n    return By.XPATH\nreturn By.CSS_SELECTOR\n```\n\nA heurística é **inequívoca** - seletores CSS não podem começar com `/`.\n\n**3. Ajuste de Caminho Relativo do XPath**\n\nPara buscas relativas a elementos, o XPath absoluto deve ser convertido:\n\n```python\n# Usuário fornece: //div\n# Para WebElement: .//div (relativo ao elemento, não ao documento)\n\ndef _ensure_relative_xpath(xpath):\n    return f'.{xpath}' if not xpath.startswith('.') else xpath\n```\n\nSem isso, `element.find()` buscaria a partir da raiz do documento.\n\n## Geração de Comando CDP\n\nO mixin roteia para diferentes métodos CDP com base no contexto da busca:\n\n| Contexto | Tipo de Seletor | Método CDP | Equivalente JavaScript |\n|---------|--------------|------------|---------------------|\n| Documento | CSS | `Runtime.evaluate` | `document.querySelector()` |\n| Documento | XPath | `Runtime.evaluate` | `document.evaluate()` |\n| Elemento | CSS | `Runtime.callFunctionOn` | `this.querySelector()` |\n| Elemento | XPath | `Runtime.callFunctionOn` | `document.evaluate(..., this)` |\n\n**Insight principal:** `Runtime.callFunctionOn` requer um `objectId` (o elemento no qual a função será chamada), enquanto `Runtime.evaluate` executa no escopo global.\n\n### Modelos (Templates) JavaScript\n\nO Pydoll usa modelos pré-definidos para consistência e performance:\n\n```python\n# Seletores CSS\nScripts.QUERY_SELECTOR = 'document.querySelector(\"{selector}\")'\nScripts.RELATIVE_QUERY_SELECTOR = 'this.querySelector(\"{selector}\")'\n\n# Expressões XPath\nScripts.FIND_XPATH_ELEMENT = '''\n    document.evaluate(\"{escaped_value}\", document, null,\n                      XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue\n'''\n```\n\nModelos evitam concatenação de strings em tempo de execução e centralizam o código JavaScript.\n\n## Resolução de ObjectID e Criação de WebElement\n\nO CDP representa nós DOM como **strings `objectId`**. O mixin abstrai isso:\n\n**Fluxo de elemento único:**\n1. Executar comando CDP → Extrair `objectId` da resposta\n2. Chamar `DOM.describeNode(objectId)` → Obter atributos, nome da tag\n3. Criar `WebElement(objectId, connection_handler, attributes)`\n\n**Fluxo de múltiplos elementos:**\n1. Executar comando CDP → Retorna **array como um único objeto remoto**\n2. Chamar `Runtime.getProperties(array_objectId)` → Enumerar índices do array\n3. Extrair `objectId` individual para cada elemento\n4. Descrever e criar `WebElement` para cada\n\n**Por que `Runtime.getProperties`?** O CDP não retorna arrays diretamente - ele retorna uma **referência a um objeto array**. Devemos enumerar suas propriedades para extrair os elementos individuais.\n\n## Insights Arquiteturais e Tradeoffs de Design\n\n### Por que Kwargs em vez de Enum By?\n\n**A escolha do Pydoll:**\n```python\nawait tab.find(id='submit', class_name='primary')\n```\n\n**A abordagem do Selenium:**\n```python\ndriver.find_element(By.ID, 'submit')  # Não pode combinar atributos\n```\n\n**Justificativa:**\n\n- **Descoberta (Discoverability)**: O autocomplete da IDE mostra todos os parâmetros disponíveis\n- **Componibilidade**: Pode combinar múltiplos atributos em uma chamada\n- **Legibilidade**: `id='submit'` é mais intuitivo do que `(By.ID, 'submit')`\n\n**Tradeoff:** Kwargs são menos explícitos sobre a estratégia do seletor. Resolvido com documentação e logs.\n\n### Por que Detectar Automaticamente CSS vs. XPath?\n\nA heurística `_get_expression_type()` elimina o fardo do usuário:\n\n```python\nawait tab.query(\"//div\")       # Auto: XPath\nawait tab.query(\"#login\")      # Auto: CSS\nawait tab.query(\"div > p\")     # Auto: CSS\n```\n\n**Benefícios:**\n\n- **Ergonomia**: Usuários não precisam especificar o tipo de seletor\n- **Correção**: Impossível usar incorretamente (XPath com método CSS, vice-versa)\n\n**Limitação:** Nenhuma maneira de forçar a interpretação de CSS para seletores ambíguos (caso extremo raro).\n\n### Prevenção de Importação Circular: create_web_element()\n\nO mixin usa uma **função de fábrica (factory function)** para evitar importações circulares:\n\n```python\ndef create_web_element(*args, **kwargs):\n    \"\"\"Importa WebElement dinamicamente em tempo de execução.\"\"\"\n    from pydoll.elements.web_element import WebElement  # Importação tardia\n    return WebElement(*args, **kwargs)\n```\n\n**Por que é necessário?**\n\n- `FindElementsMixin` → precisa criar `WebElement`\n- `WebElement` → herda de `FindElementsMixin`\n- Dependência circular!\n\n**Solução:** Importação tardia (late import) dentro da função de fábrica. A importação só é executada quando a função é chamada, quebrando o ciclo.\n\n### hasattr() para Detecção de Contexto: Elegante ou Hacky?\n\nO mixin usa `hasattr(self, '_object_id')` para detectar Tab vs WebElement:\n\n```python\nif hasattr(self, '_object_id'):\n    # WebElement: busca relativa ao elemento\nelse:\n    # Tab: busca em nível de documento\n```\n\n**Isso é \"hacky\" (gambiarra)?**\n\n- **Não**: É **duck typing** (um idioma Pythônico)\n- O Mixin não precisa saber a hierarquia de classes\n- Tanto Tab quanto WebElement fornecem `_connection_handler`\n- WebElement adicionalmente fornece `_object_id`\n\n**Abordagens alternativas:**\n\n1. **Verificação de tipo**: `if isinstance(self, WebElement)` → Acopla o mixin ao WebElement\n2. **Método abstrato**: Exigiria que Tab/WebElement implementassem `get_search_context()` → Mais código boilerplate\n3. **Injeção de dependência**: Passar o contexto como parâmetro → Quebra a ergonomia da API\n\n**Veredito:** `hasattr()` é a melhor solução para este caso de uso.\n\n## Principais Conclusões\n\n1. **Mixins permitem o compartilhamento de código** sem acoplar `Tab` e `WebElement` através de herança\n2. **Detecção de contexto via duck typing** (`hasattr`) mantém o mixin desacoplado da hierarquia de classes\n3. **Resolução automática otimiza a performance** usando métodos nativos para atributos únicos\n4. **Construção de XPath fornece componibilidade** para consultas com múltiplos atributos\n5. **Espera baseada em polling (sondagem) é simples**, mas troca ciclos de CPU por simplicidade de implementação\n6. **Complexidade do modelo de objeto CDP** é escondida atrás da abstração do WebElement\n7. **Segurança de tipos via sobrecargas (overloads)** fornece tipos de retorno precisos para suporte da IDE\n\n## Documentação Relacionada\n\nPara um entendimento mais profundo dos componentes arquiteturais relacionados:\n\n- **[Sistema de Tipos](./typing-system.md)**: Padrão Overload, TypedDict, tipos Genéricos\n- **[Domínio do WebElement](./webelement-domain.md)**: Arquitetura do WebElement e métodos de interação\n- **[Guia de Seletores](./selectors-guide.md)**: Sintaxe e boas práticas de CSS vs XPath\n- **[Domínio da Tab](./tab-domain.md)**: Operações em nível de aba e gerenciamento de contexto\n\nPara padrões de uso prático:\n\n- **[Guia de Localização de Elementos](../features/automation/element-finding.md)**: Exemplos práticos e padrões\n- **[Interações Humanizadas](../features/automation/human-interactions.md)**: Interação realista com elementos"
  },
  {
    "path": "docs/pt/deep-dive/architecture/index.md",
    "content": "# Arquitetura Interna\n\n**Entenda o design, depois quebre as regras intencionalmente.**\n\nA maioria da documentação mostra **o que** um framework faz. Esta seção revela **como** e **por que** o Pydoll é arquitetado da maneira que é: os padrões de design, as decisões arquiteturais e os tradeoffs (compromissos) que moldam cada linha de código.\n\n## Por que a Arquitetura Importa\n\nVocê pode usar o Pydoll eficazmente sem entender sua arquitetura interna. Mas quando você precisar:\n\n- **Depurar** problemas complexos que abrangem múltiplos componentes\n- **Otimizar** gargalos de performance em automação de grande escala\n- **Estender** o Pydoll com funcionalidade personalizada\n- **Contribuir** com melhorias para a base de código\n- **Construir** ferramentas similares para diferentes casos de uso\n\n...o conhecimento arquitetural se torna **indispensável**.\n\n!!! quote \"Arquitetura como Linguagem\"\n    **\"Arquitetura é música congelada.\"** - Johann Wolfgang von Goethe\n    \n    Uma boa arquitetura não é apenas sobre fazer o código funcionar, é sobre tornar o código **compreensível**, **manutenível** e **extensível**. Entender a arquitetura do Pydoll ensina padrões que você aplicará em todos os projetos.\n\n## Os Seis Domínios Arquiteturais\n\nA arquitetura do Pydoll é organizada em **seis domínios coesos**, cada um com responsabilidades e interfaces claras:\n\n### 1. Domínio do Navegador (Browser)\n**[→ Explore a Arquitetura do Navegador](./browser-domain.md)**\n\n**O orquestrador: gerenciando processos, contextos e estado global.**\n\nO domínio do Navegador (Browser) fica no topo da hierarquia, coordenando:\n\n- **Gerenciamento de processos**: Iniciar/terminar executáveis do navegador\n- **Contextos do navegador**: Ambientes isolados (como janelas anônimas)\n- **Registro de abas**: Padrão Singleton para instâncias de Abas (Tab)\n- **Autenticação de proxy**: Autenticação automática via domínio Fetch\n- **Operações globais**: Downloads, permissões, gerenciamento de janelas\n\n**Principais padrões arquiteturais**:\n\n- **Classe base abstrata** para Chrome/Edge/outros navegadores Chromium\n- **Padrão Gerenciador (Manager)** (ProcessManager, ProxyManager, TempDirManager)\n- **Registro Singleton** para instâncias de Aba (previne duplicatas)\n- **Protocolo de gerenciador de contexto** para limpeza automática\n\n**Insight crítico**: O Navegador não manipula páginas diretamente, ele **coordena** componentes de nível inferior. Essa separação de responsabilidades permite suporte a múltiplos navegadores e operações concorrentes em abas.\n\n---\n\n### 2. Domínio da Aba (Tab)\n**[→ Explore a Arquitetura da Aba](./tab-domain.md)**\n\n**O cavalo de batalha: executando comandos, gerenciando estado, coordenando automação.**\n\nO domínio da Aba (Tab) é a interface primária do Pydoll, lidando com:\n\n- **Navegação**: Carregamento de página com estados de espera configuráveis\n- **Localização de elementos**: Delegado ao FindElementsMixin\n- **Execução de JavaScript**: Contextos tanto de página quanto de elemento\n- **Coordenação de eventos**: Ouvintes (listeners) de eventos específicos da aba\n- **Monitoramento de rede**: Captura e análise de requisição/resposta\n- **Manipulação de IFrame**: Gerenciamento de contexto aninhado\n\n**Principais padrões arquiteturais**:\n\n- **Padrão Façade (Fachada)**: Interface simplificada para operações complexas do CDP\n- **Composição de Mixin**: FindElementsMixin para localização de elementos\n- **WebSocket por aba**: Conexões independentes para paralelismo\n- **Flags de estado**: Rastreia domínios habilitados (network_events_enabled, etc.)\n- **Inicialização preguiçosa (Lazy)**: Objeto Request criado no primeiro acesso\n\n**Insight crítico**: Cada Aba (Tab) possui seu **próprio ConnectionHandler**, permitindo operações paralelas verdadeiras entre abas sem contenção ou vazamento de estado.\n\n---\n\n### 3. Domínio do WebElement\n**[→ Explore a Arquitetura do WebElement](./webelement-domain.md)**\n\n**O interator: fazendo a ponte entre código Python e elementos DOM.**\n\nO domínio WebElement representa **elementos DOM individuais**, fornecendo:\n\n- **Métodos de interação**: Clique, digitação, rolagem, seleção\n- **Acesso a propriedades**: Texto, HTML, limites (bounds), atributos\n- **Consultas de estado**: Visibilidade, status de habilitado, valor\n- **Capturas de tela (Screenshots)**: Captura de imagem específica do elemento\n- **Localização de filhos**: Localização de elementos relativos (também via FindElementsMixin)\n\n**Principais padrões arquiteturais**:\n\n- **Padrão Proxy**: Objeto Python representando um elemento remoto do navegador\n- **Abstração de Object ID**: O objectId do CDP oculto atrás da API Python\n- **Propriedades híbridas**: Síncronas (atributos) vs. assíncronas (estado dinâmico)\n- **Padrão Command**: Métodos de interação encapsulam comandos CDP\n- **Estratégias de fallback**: Múltiplas abordagens para robustez\n\n**Insight crítico**: O WebElement mantém **ambos os atributos em cache** (da criação) e **estado dinâmico** (buscado sob demanda), equilibrando performance com dados atualizados.\n\n---\n\n### 4. Mixin FindElements\n**[→ Explore a Arquitetura do FindElements](./find-elements-mixin.md)**\n\n**O localizador: traduzindo seletores em consultas DOM.**\n\nO FindElementsMixin fornece capacidades de localização de elementos tanto para a Aba (Tab) quanto para o WebElement através de **composição**, não herança:\n\n- **Localização baseada em atributos**: `find(id='submit', class_name='btn')`\n- **Consulta baseada em expressão**: `query('div.container > p')`\n- **Resolução de estratégia**: Seletor ideal para atributos únicos vs. múltiplos\n- **Mecanismos de espera**: Polling (sondagem) com timeouts configuráveis\n- **Detecção de contexto**: Buscas no documento vs. relativas ao elemento\n\n**Principais padrões arquiteturais**:\n- **Padrão Mixin**: Capacidade compartilhada sem hierarquia de herança\n- **Padrão Strategy**: Diferentes estratégias de seletor baseadas na entrada\n- **Padrão Template Method**: Fluxo comum, implementação específica da estratégia\n- **Função de Fábrica (Factory)**: Importação tardia (late import) para evitar dependências circulares\n- **Padrão Overload**: Tipos de retorno seguros (WebElement vs. lista)\n\n**Insight crítico**: O mixin usa **duck typing** (`hasattr(self, '_object_id')`) para detectar Tab vs. WebElement, permitindo reuso de código sem acoplamento forte.\n\n---\n\n### 5. Arquitetura de Eventos\n**[→ Explore a Arquitetura de Eventos](./event-architecture.md)**\n\n**O despachante: roteando eventos do navegador para callbacks Python.**\n\nA Arquitetura de Eventos permite automação reativa através de:\n\n- **Registro de eventos**: Método `on()` para se inscrever (subscribe) em eventos CDP\n- **Despacho de callbacks**: Execução assíncrona sem bloqueio\n- **Gerenciamento de domínio**: Habilitação/desabilitação explícita para performance\n- **Callbacks temporários**: Auto-remoção após a primeira invocação\n- **Escopo multi-nível**: Eventos em todo o navegador vs. específicos da aba\n\n**Principais padrões arquiteturais**:\n\n- **Padrão Observer**: Inscrever/notificar para código orientado a eventos\n- **Padrão Registry**: Mapeamento de nome do evento → lista de callbacks\n- **Padrão Wrapper**: Encapsula callbacks síncronos para execução assíncrona\n- **Protocolo de limpeza**: Remoção automática de callbacks no fechamento da aba\n- **Isolamento de escopo**: Contextos de eventos independentes por aba\n\n**Insight crítico**: Eventos são baseados em **push** (navegador notifica o Python), não em poll (sondagem), permitindo automação reativa de baixa latência sem espera ocupada (busy-waiting).\n\n---\n\n### 6. Arquitetura de Requisições do Navegador\n**[→ Explore a Arquitetura de Requisições](./browser-requests-architecture.md)**\n\n**O híbrido: requisições HTTP com o estado de sessão do navegador.**\n\nO sistema de Requisições do Navegador (Browser Requests) faz a ponte entre HTTP e automação de navegador:\n\n- **Continuidade de sessão**: Cookies e autenticação incluídos automaticamente\n- **Fontes de dados duplas**: API Fetch do JavaScript + eventos de rede do CDP\n- **Metadados completos**: Cabeçalhos, cookies, tempo (timing) (nem tudo disponível via JavaScript)\n- **API semelhante à `requests`**: Interface familiar com o poder do navegador\n\n**Principais padrões arquiteturais**:\n\n- **Execução híbrida**: JavaScript para o corpo (body), CDP para metadados\n- **Registro temporário de eventos**: Padrão Habilitar/capturar/desabilitar\n- **Inicialização preguiçosa (lazy) de propriedade**: Objeto Request criado no primeiro uso\n- **Padrão Adapter**: Interface compatível com `requests` para o fetch do navegador\n\n**Insight crítico**: As requisições do navegador combinam **duas fontes de informação** (JavaScript e eventos CDP). O JavaScript fornece o corpo da resposta, o CDP fornece cabeçalhos e cookies que as políticas de segurança do JavaScript ocultam.\n\n---\n\n## Princípios Arquiteturais\n\nEsses seis domínios seguem princípios consistentes:\n\n### 1. Separação de Responsabilidades (Separation of Concerns)\nCada domínio tem uma **responsabilidade única e bem definida**:\n\n- Navegador → Gerenciamento de processo/contexto\n- Aba → Execução de comando e estado\n- WebElement → Interação com elemento\n- FindElements → Localização de elemento\n- Eventos → Despacho reativo\n- Requisições → HTTP no contexto do navegador\n\n**Benefício**: Mudanças em um domínio raramente exigem mudanças em outros.\n\n### 2. Composição Sobre Herança\nEm vez de hierarquias de herança profundas, o Pydoll usa:\n\n- **Mixins** (FindElementsMixin compartilhado por Tab e WebElement)\n- **Gerenciadores (Managers)** (ProcessManager, ProxyManager, TempDirManager)\n- **Injeção de dependência** (ConnectionHandler passado para os componentes)\n\n**Benefício**: Reutilização flexível de componentes sem acoplamento forte.\n\n### 3. Assíncrono por Padrão (Async by Default)\nTodas as operações de E/S (I/O) são `async def` e devem ser `await`ed:\n\n- Comunicação WebSocket\n- Execução de comando CDP\n- Despacho de callback de evento\n- Requisições de rede\n\n**Benefício**: Permite concorrência verdadeira com múltiplas abas, operações paralelas e E/S não bloqueante.\n\n### 4. Segurança de Tipos (Type Safety)\nToda API pública tem anotações de tipo (type annotations):\n\n- Parâmetros de função e tipos de retorno\n- Respostas CDP como `TypedDict`\n- Tipos de eventos para parâmetros de callback\n- Sobrecargas (Overloads) para métodos polimórficos\n\n**Benefício**: Autocomplete da IDE, verificação estática de tipos, código autodocumentado.\n\n### 5. Gerenciamento de Recursos\nGerenciadores de contexto garantem a limpeza:\n\n- `async with Browser()` → fecha o navegador ao sair\n- `async with tab.expect_file_chooser()` → desabilita o interceptador\n- `async with tab.expect_download()` → limpa arquivos temporários\n\n**Benefício**: Limpeza automática de recursos, previne vazamentos mesmo em exceções.\n\n## Interação de Componentes\n\nEntender como os domínios interagem é fundamental:\n\n```mermaid\ngraph TB\n    User[Seu Código Python]\n    \n    User --> Browser[Domínio do Navegador]\n    User --> Tab[Domínio da Aba]\n    User --> Element[Domínio do WebElement]\n    \n    Browser --> ProcessMgr[Gerenciador de Processo]\n    Browser --> ContextMgr[Gerenciador de Contexto]\n    Browser --> TabRegistry[Registro de Abas]\n    \n    Tab --> ConnHandler[Manipulador de Conexão]\n    Tab --> FindMixin[Mixin FindElements]\n    Tab --> EventSystem[Sistema de Eventos]\n    Tab --> RequestSystem[Sistema de Requisições]\n    \n    Element --> ConnHandler2[Manipulador de Conexão]\n    Element --> FindMixin2[Mixin FindElements]\n    \n    ConnHandler --> WebSocket[WebSocket para CDP]\n    ConnHandler2 --> WebSocket\n    EventSystem --> ConnHandler\n    RequestSystem --> ConnHandler\n    RequestSystem --> EventSystem\n    \n    WebSocket --> Chrome[Navegador Chrome]\n```\n\n**Principais interações**:\n\n1. **Navegador cria Abas** → Abas armazenadas no registro\n2. **Aba e WebElement usam FindElementsMixin** → Localização de elementos compartilhada\n3. **Cada Aba possui um ConnectionHandler** → Conexões WebSocket independentes\n4. **Sistema de requisições usa Sistema de eventos** → Eventos de rede capturam metadados\n5. **Todos os componentes usam ConnectionHandler** → Comunicação CDP centralizada\n\n## Pré-requisitos\n\nPara se beneficiar totalmente desta seção:\n\n- **[Fundamentos Essenciais](../fundamentals/cdp.md)** - Entender CDP, assincronismo e tipos\n- **Padrões de design Python** - Familiaridade com padrões comuns\n- **Conceitos de OOP** - Classes, herança, composição, interfaces\n- **Python Assíncrono** - Confortável com `async def` e `await`  \n\n**Se você não leu os Fundamentos**, comece por lá primeiro. A arquitetura se baseia nesses conceitos.\n\n## Além da Arquitetura\n\nDepois de dominar a arquitetura interna, você estará pronto para:\n\n- **Contribuir com código**: Entender onde novos recursos se encaixam\n- **Otimização de performance**: Identificar gargalos e ineficiências\n- **Extensões personalizadas**: Construir sobre os padrões do Pydoll\n- **Ferramentas similares**: Aplicar esses padrões a outros projetos\n\n## Filosofia de Design\n\nUma boa arquitetura é **invisível**, ela não deve atrapalhar seu caminho. A arquitetura do Pydoll prioriza:\n\n1. **Simplicidade**: Cada componente faz uma coisa bem feita\n2. **Consistência**: Operações similares têm padrões similares\n3. **Explicitude**: Sem mágica, sem comportamento oculto\n4. **Segurança de tipos**: Capturar erros em tempo de design, não em tempo de execução\n5. **Performance**: Assíncrono por padrão, paralelismo sem bloqueios (locks)\n\nEstas não são escolhas arbitrárias, são **princípios testados em batalha** de décadas de engenharia de software.\n\n---\n\n## Pronto para Entender o Design?\n\nComece com o **[Domínio do Navegador](./browser-domain.md)** para entender como o gerenciamento de processos e o isolamento de contexto funcionam, depois progrida através dos domínios em ordem.\n\n**É aqui que o uso se torna maestria.**\n\n---\n\n!!! success \"Após Completar a Arquitetura\"\n    Depois de entender esses padrões, você os verá em toda parte na engenharia de software, não apenas no Pydoll. Estes são **padrões universais** aplicados à automação de navegadores:\n    \n    - Façade (Aba simplifica a complexidade do CDP)\n    - Observer (Sistema de eventos para código reativo)\n    - Mixin (FindElementsMixin para reuso de código)\n    - Registry (Navegador rastreia instâncias de Aba)\n    - Strategy (FindElements resolve seletores ideais)\n    \n    Boa arquitetura é **conhecimento atemporal**."
  },
  {
    "path": "docs/pt/deep-dive/architecture/shadow-dom.md",
    "content": "# Arquitetura do Shadow DOM\n\nO Shadow DOM e um dos aspectos mais desafiadores da automacao web moderna. Elementos dentro de shadow trees sao invisiveis para consultas DOM regulares, o que quebra abordagens tradicionais de automacao. Este documento explica como o Shadow DOM funciona no nivel do navegador, por que ferramentas convencionais falham com shadow roots fechados, e como o Pydoll contorna essas restricoes atraves de acesso direto via CDP.\n\n!!! info \"Guia de Uso Pratico\"\n    Para exemplos de uso e padroes de inicio rapido, consulte o [Guia de Pesquisa de Elementos — secao Shadow DOM](../../features/element-finding.md#suporte-a-shadow-dom).\n\n## O que e Shadow DOM?\n\nShadow DOM e um padrao web que permite **encapsulamento DOM**. Ele permite que um componente tenha sua propria arvore DOM isolada (a \"shadow tree\") anexada a um elemento DOM regular (o \"shadow host\"). Elementos dentro de uma shadow tree ficam ocultos das consultas do documento principal.\n\n```mermaid\ngraph TB\n    subgraph \"DOM Principal (Light DOM)\"\n        Document[\"document\"]\n        Host[\"div#my-component\\n(shadow host)\"]\n        Other[\"p.normal-content\"]\n    end\n\n    subgraph \"Shadow Tree (Encapsulada)\"\n        SR[\"#shadow-root (open)\"]\n        Style[\"style\"]\n        Button[\"button.internal\"]\n        Input[\"input.private\"]\n    end\n\n    Document --> Host\n    Document --> Other\n    Host -.->|\"attachShadow()\"| SR\n    SR --> Style\n    SR --> Button\n    SR --> Input\n```\n\n### Modos do Shadow Root\n\nQuando um componente cria um shadow root via `attachShadow()`, ele especifica um **modo**:\n\n| Modo | Acesso JavaScript | Acesso CDP | Uso Comum |\n|------|-------------------|------------|-----------|\n| `open` | `element.shadowRoot` retorna o root | Acesso total via `backendNodeId` | Web components customizados (Lit, Stencil) |\n| `closed` | `element.shadowRoot` retorna `null` | Acesso total via `backendNodeId` | Componentes sensiveis, formularios de pagamento |\n| `user-agent` | Nao acessivel via JS | Acesso limitado | UI interna do navegador (placeholders, controles de video) |\n\nEssa distincao e critica: **o acesso no nivel JavaScript e restrito pelo modo, mas o acesso no nivel CDP nao e.**\n\n### Por que a Automacao Tradicional Falha\n\nFerramentas de automacao tradicionais dependem da execucao de JavaScript no contexto da pagina:\n\n```javascript\n// Abordagem WebDriver / Selenium\ndocument.querySelector('#my-component')        // ✓ Encontra o host\ndocument.querySelector('#my-component button') // ✗ Nao cruza a fronteira do shadow\nelement.shadowRoot                             // ✗ Retorna null para roots fechados\n```\n\nA fronteira do shadow e imposta pelo motor JavaScript do navegador. Qualquer ferramenta de automacao que executa JavaScript para encontrar elementos vai encontrar essa barreira. Isso inclui Selenium, `page.evaluate()` do Playwright, e qualquer ferramenta usando `Runtime.evaluate()` com `document.querySelector()` no nivel do documento.\n\n## Como o Pydoll Contorna as Fronteiras do Shadow\n\nA abordagem do Pydoll funciona em uma camada **abaixo do JavaScript**: o Chrome DevTools Protocol. O CDP tem acesso direto a representacao interna do DOM do navegador, que ignora restricoes de modo do shadow completamente.\n\n### A Vantagem do CDP\n\n```mermaid\nsequenceDiagram\n    participant User as Codigo do Usuario\n    participant SR as ShadowRoot\n    participant CH as ConnectionHandler\n    participant CDP as Chrome CDP\n    participant DOM as DOM do Navegador\n\n    User->>SR: shadow_root.query('.btn')\n    SR->>SR: _get_find_element_command(object_id)\n    SR->>CH: execute_command(Runtime.callFunctionOn)\n    CH->>CDP: WebSocket send\n    CDP->>DOM: Executa querySelector no objeto shadow root\n    DOM-->>CDP: Resultado do elemento\n    CDP-->>CH: Resposta com objectId\n    CH-->>SR: Dados do elemento\n    SR-->>User: Instancia WebElement\n```\n\nO insight chave esta em **como o objeto shadow root e obtido** e **como as consultas sao executadas contra ele**:\n\n1. **Descoberta**: `DOM.describeNode` com `pierce=true` retorna nos de shadow root com seu `backendNodeId`, independente do modo\n2. **Resolucao**: `DOM.resolveNode` converte um `backendNodeId` em um `objectId` JavaScript que referencia o shadow root diretamente\n3. **Consulta**: `Runtime.callFunctionOn` executa `this.querySelector()` no `objectId` do shadow root; isso funciona porque a chamada e feita **no proprio objeto shadow root**, nao a partir do contexto do documento\n\n### Passo a Passo: Acesso ao Shadow Root\n\n```mermaid\nflowchart TD\n    A[\"WebElement\\n(shadow host)\"]\n    B[\"shadowRoots[] com\\nbackendNodeId\"]\n    C[\"objectId JavaScript\\npara o shadow root\"]\n    D[\"Instancia ShadowRoot\"]\n    E[\"WebElement\\n(dentro do shadow)\"]\n\n    A -->|\"DOM.describeNode\\ndepth=1, pierce=true\"| B\n    B -->|\"DOM.resolveNode\\nbackendNodeId\"| C\n    C -->|\"Criar ShadowRoot\\ncom objectId\"| D\n    D -->|\"find() / query()\\nvia callFunctionOn\"| E\n```\n\n#### Passo 1: Descrever o No Host\n\n```python\n# Pydoll envia este comando CDP:\n{\n    \"method\": \"DOM.describeNode\",\n    \"params\": {\n        \"objectId\": \"<host-element-object-id>\",\n        \"depth\": 1,\n        \"pierce\": true  # ← Esta e a flag chave\n    }\n}\n```\n\nO parametro `pierce` diz ao CDP para atravessar fronteiras do shadow ao descrever o no. A resposta inclui informacoes do shadow root independente do modo do shadow root:\n\n```json\n{\n    \"result\": {\n        \"node\": {\n            \"nodeName\": \"DIV\",\n            \"shadowRoots\": [\n                {\n                    \"nodeId\": 0,\n                    \"backendNodeId\": 5,\n                    \"shadowRootType\": \"closed\",\n                    \"childNodeCount\": 4\n                }\n            ]\n        }\n    }\n}\n```\n\n!!! warning \"nodeId vs backendNodeId\"\n    Quando o dominio DOM nao esta explicitamente habilitado (que e o padrao do Pydoll para minimizar overhead), `nodeId` e sempre `0`. O `backendNodeId` e o identificador estavel e sempre disponivel. O Pydoll usa `backendNodeId` exclusivamente para resolucao de shadow root, e por isso funciona sem necessitar de `DOM.enable()`.\n\n#### Passo 2: Resolver para Objeto JavaScript\n\n```python\n# Converter backendNodeId em um objectId utilizavel:\n{\n    \"method\": \"DOM.resolveNode\",\n    \"params\": {\n        \"backendNodeId\": 5\n    }\n}\n```\n\nA resposta fornece um `objectId`, um handle para o shadow root no espaco de objetos do JavaScript:\n\n```json\n{\n    \"result\": {\n        \"object\": {\n            \"objectId\": \"-2296764575741119861.1.3\"\n        }\n    }\n}\n```\n\n#### Passo 3: Consultar Dentro do Shadow Root\n\nCom o `objectId` do shadow root, o Pydoll aproveita o mecanismo de busca relativa existente do `FindElementsMixin`:\n\n```python\n# Quando ShadowRoot.query('.btn') e chamado:\n{\n    \"method\": \"Runtime.callFunctionOn\",\n    \"params\": {\n        \"functionDeclaration\": \"function() { return this.querySelector(\\\".btn\\\"); }\",\n        \"objectId\": \"-2296764575741119861.1.3\"\n    }\n}\n```\n\nA funcao executa com `this` vinculado ao objeto shadow root. Como shadow roots implementam as interfaces `querySelector()` e `querySelectorAll()` nativamente, seletores CSS funcionam naturalmente dentro da fronteira do shadow.\n\n## Arquitetura do ShadowRoot\n\n### Decisao de Design: Reutilizar FindElementsMixin\n\nA decisao arquitetural mais critica foi fazer `ShadowRoot` herdar de `FindElementsMixin`:\n\n```python\nclass ShadowRoot(FindElementsMixin):\n    def __init__(self, object_id, connection_handler, mode, host_element):\n        self._object_id = object_id               # Referencia CDP do shadow root\n        self._connection_handler = connection_handler  # Para comunicacao CDP\n        self._mode = mode                          # Enum ShadowRootType\n        self._host_element = host_element          # Referencia de volta ao host\n```\n\n**Por que isso funciona**: `FindElementsMixin._find_element()` verifica `hasattr(self, '_object_id')`. Quando presente, usa `RELATIVE_QUERY_SELECTOR`, que chama `this.querySelector()` no objeto referenciado. Como shadow roots suportam `querySelector()` nativamente, `query()` com seletores CSS funciona automaticamente. A flag `_css_only = True` no `ShadowRoot` bloqueia `find()` e `query()` com XPath, lancando `NotImplementedError`.\n\n```python\n# Esta unica linha no FindElementsMixin habilita buscas em shadow root:\nelif hasattr(self, '_object_id'):\n    command = self._get_find_element_command(by, value, self._object_id)\n```\n\nIsso significa que `ShadowRoot` herda `query()` e `find_or_wait_element()` do mixin. Porem, a flag `_css_only = True` restringe o uso a apenas `query()` com seletores CSS; `find()` e XPath lancam `NotImplementedError`.\n\n!!! tip \"Consistencia Arquitetural\"\n    Este e o mesmo mecanismo que faz `WebElement.find()` buscar dentro dos filhos de um elemento: o atributo `_object_id` sinaliza \"busque relativo a mim\" em vez de \"busque no documento inteiro.\" `ShadowRoot`, `WebElement` e `Tab` compartilham comportamento identico de busca de elementos atraves do `FindElementsMixin`.\n\n### Relacionamento entre Classes\n\n| Classe | Tem `_object_id` | Tem `_connection_handler` | Escopo de Busca |\n|--------|:-:|:-:|---|\n| `Tab` | Nao | Sim | Documento inteiro |\n| `WebElement` | Sim | Sim | Dentro da subarvore do elemento |\n| `ShadowRoot` | Sim | Sim | Dentro da shadow tree |\n\nTodos os tres herdam de `FindElementsMixin`. A presenca ou ausencia de `_object_id` determina se as buscas sao globais no documento ou com escopo para um no especifico.\n\n### Resolvendo Shadow Roots: Estrategia backendNodeId\n\nO Pydoll deliberadamente usa `backendNodeId` em vez de `nodeId` para resolucao de shadow root:\n\n| Propriedade | `nodeId` | `backendNodeId` |\n|-------------|----------|-----------------|\n| Requer `DOM.enable()` | Sim | Nao |\n| Estavel entre chamadas describe | Nao (0 quando DOM nao habilitado) | Sim |\n| Funciona para resolucao de shadow root | Apenas com DOM habilitado | Sempre |\n| Overhead de performance | Maior (rastreamento do dominio DOM) | Nenhum |\n\nAo confiar no `backendNodeId`, o Pydoll evita o overhead de habilitar o dominio DOM enquanto mantem acesso confiavel ao shadow root. Esta e uma escolha pragmatica: a maioria dos cenarios de automacao nao precisa do stream de eventos do dominio DOM, e habilita-lo adiciona overhead de memoria e processamento para rastrear cada mutacao do DOM.\n\n## Shadow Roots Fechados: Por que o Acesso CDP Funciona\n\nEsta e a pergunta mais frequente: **se `element.shadowRoot` retorna `null` para shadow roots fechados em JavaScript, como o CDP pode acessa-los?**\n\nA resposta esta em entender a arquitetura do navegador:\n\n```mermaid\ngraph TB\n    subgraph \"Runtime JavaScript\"\n        JS[\"Codigo JavaScript\"]\n        API[\"Web APIs\\n(propriedade shadowRoot)\"]\n    end\n\n    subgraph \"Internos do Navegador\"\n        CDP_Layer[\"Camada CDP\"]\n        DOM_Internal[\"Arvore DOM Interna\"]\n    end\n\n    JS -->|\"element.shadowRoot\"| API\n    API -->|\"mode == 'closed'\\n→ retorna null\"| JS\n    CDP_Layer -->|\"DOM.describeNode\\npierce=true\"| DOM_Internal\n    DOM_Internal -->|\"Sempre retorna\\nshadow tree completa\"| CDP_Layer\n```\n\n**Acesso JavaScript** passa pela camada de Web API, que impoe a restricao de modo do shadow. Quando `mode='closed'`, a API retorna `null`; esta e uma fronteira de controle de acesso intencional para codigo de paginas web.\n\n**Acesso CDP** opera abaixo da camada de Web API. Ele se comunica diretamente com a representacao interna do DOM do navegador. A restricao do modo `closed` e uma **politica no nivel JavaScript**, nao uma **restricao no nivel DOM**. A shadow tree ainda existe no DOM; ela apenas esta oculta da visao do JavaScript.\n\n!!! info \"Implicacoes de Seguranca\"\n    Isso e por design no DevTools Protocol. O CDP e destinado a ferramentas de depuracao e automacao que precisam de acesso total ao DOM. O modo `closed` protege conteudos do shadow de outros scripts na mesma pagina (ex: scripts de terceiros), nao da interface de depuracao do navegador. Esta e a mesma razao pela qual o DevTools do navegador consegue inspecionar shadow roots fechados no painel Elements.\n\n### Verificacao Pratica\n\nVoce pode verificar esse comportamento:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.dom.types import ShadowRootType\n\nasync def verify_closed_access():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('about:blank')\n\n        # Criar um shadow root fechado via JavaScript\n        await tab.execute_script(\"\"\"\n            const host = document.createElement('div');\n            host.id = 'test-host';\n            document.body.appendChild(host);\n            const shadow = host.attachShadow({ mode: 'closed' });\n            shadow.innerHTML = '<p class=\"secret\">Conteudo oculto</p>';\n        \"\"\")\n\n        # JavaScript nao consegue acessar:\n        result = await tab.execute_script(\n            \"return document.getElementById('test-host').shadowRoot\",\n            return_by_value=True,\n        )\n        js_value = result['result']['result'].get('value')\n        print(f\"JS shadowRoot: {js_value}\")  # None\n\n        # Mas o Pydoll consegue:\n        host = await tab.find(id='test-host')\n        shadow = await host.get_shadow_root()\n        print(f\"Modo do shadow: {shadow.mode}\")  # ShadowRootType.CLOSED\n\n        secret = await shadow.query('.secret')\n        text = await secret.text\n        print(f\"Conteudo: {text}\")  # \"Conteudo oculto\"\n\nasyncio.run(verify_closed_access())\n```\n\n## Shadow Roots Aninhados\n\nWeb components frequentemente compoem outros web components, criando shadow trees em multiplos niveis:\n\n```mermaid\ngraph TB\n    subgraph \"Light DOM\"\n        Host1[\"outer-component\\n(shadow host)\"]\n    end\n\n    subgraph \"Shadow Tree Externa\"\n        SR1[\"#shadow-root (open)\"]\n        Host2[\"inner-component\\n(shadow host)\"]\n        P1[\"p.outer-text\"]\n    end\n\n    subgraph \"Shadow Tree Interna\"\n        SR2[\"#shadow-root (closed)\"]\n        Button[\"button.deep-btn\"]\n        P2[\"p.inner-text\"]\n    end\n\n    Host1 -.-> SR1\n    SR1 --> P1\n    SR1 --> Host2\n    Host2 -.-> SR2\n    SR2 --> P2\n    SR2 --> Button\n```\n\nO Pydoll lida com isso naturalmente encadeando chamadas `get_shadow_root()`. Cada `ShadowRoot` produz instancias `WebElement` que podem ter seus proprios shadow roots:\n\n```python\nouter_host = await tab.find(tag_name='outer-component')\nouter_shadow = await outer_host.get_shadow_root()        # open\n\ninner_host = await outer_shadow.query('inner-component')\ninner_shadow = await inner_host.get_shadow_root()        # closed, ainda funciona\n\ndeep_button = await inner_shadow.query('.deep-btn')\nawait deep_button.click()\n```\n\nCada nivel segue o mesmo fluxo de resolucao CDP: `describeNode` depois `resolveNode` depois `ShadowRoot` com `_object_id` depois `querySelector` via `callFunctionOn`.\n\n## Shadow Roots Dentro de IFrames\n\nUm cenario comum no mundo real envolve shadow roots dentro de iframes cross-origin — por exemplo, captchas Cloudflare Turnstile. Isso combina dois mecanismos de isolamento: a fronteira do iframe e a fronteira do shadow.\n\n```mermaid\ngraph TB\n    subgraph \"Pagina Principal\"\n        Host[\"div.widget\\n(shadow host)\"]\n    end\n\n    subgraph \"Shadow Tree\"\n        SR1[\"#shadow-root\"]\n        IFrame[\"iframe\\n(cross-origin)\"]\n    end\n\n    subgraph \"IFrame (OOPIF)\"\n        Body[\"body\"]\n    end\n\n    subgraph \"Shadow Tree do IFrame\"\n        SR2[\"#shadow-root\"]\n        Button[\"label.checkbox\"]\n    end\n\n    Host -.-> SR1\n    SR1 --> IFrame\n    IFrame -.->|\"processo separado\"| Body\n    Body -.-> SR2\n    SR2 --> Button\n```\n\nO Pydoll lida com isso de forma transparente atraves da **propagacao de contexto do iframe**. Quando um `ShadowRoot` e criado, ele herda o contexto de roteamento do iframe do seu elemento host:\n\n```python\n# A cadeia completa: pagina principal → shadow root → iframe → shadow root → elemento\nshadow_host = await tab.find(id='widget-container')\nfirst_shadow = await shadow_host.get_shadow_root()\n\niframe = await first_shadow.query('iframe')\nbody = await iframe.find(tag_name='body')\nsecond_shadow = await body.get_shadow_root()\n\n# click() funciona corretamente — eventos de mouse roteados pela sessao OOPIF\nbutton = await second_shadow.query('label.checkbox')\nawait button.click()\n```\n\n### Como a Propagacao de Contexto Funciona\n\nIFrames cross-origin rodam em um processo separado do navegador (Out-of-Process IFrame, ou OOPIF). Comandos CDP para esses iframes devem ser roteados atraves de um `sessionId` dedicado. O Pydoll propaga esse contexto de roteamento automaticamente por toda a cadeia:\n\n1. **IFrame resolve seu contexto**: `iframe.find()` estabelece um `IFrameContext` com `session_id` e `session_handler` para o OOPIF\n2. **Elementos filhos herdam o contexto**: Elementos encontrados dentro do iframe recebem o `IFrameContext`\n3. **Shadow roots herdam do host**: `ShadowRoot` copia o `_iframe_context` do seu elemento host\n4. **Elementos no shadow herdam do shadow root**: Elementos encontrados via `shadow.query()` recebem o contexto propagado\n5. **Comandos roteiam corretamente**: `_execute_command()` detecta o contexto herdado e roteia comandos CDP (incluindo `Input.dispatchMouseEvent` para `click()`) pela sessao OOPIF\n\nIsso significa que coordenadas de `DOM.getBoxModel` (que sao relativas ao viewport do iframe) sao corretamente pareadas com eventos de mouse despachados para a mesma sessao OOPIF.\n\n## Buscando Shadow Roots: find_shadow_roots()\n\n`Tab.find_shadow_roots()` percorre toda a arvore DOM para coletar todos os shadow roots encontrados na pagina.\n\n### Como Funciona\n\n```\nTab.find_shadow_roots()\n  ├─ DOM.getDocument(depth=-1, pierce=true)\n  │   └─ Retorna arvore DOM completa com arrays shadowRoots\n  ├─ Percurso recursivo da arvore: _collect_shadow_roots_from_tree()\n  │   ├─ Coleta entradas shadowRoots com backendNodeId do host\n  │   ├─ Percorre filhos recursivamente\n  │   └─ Percorre contentDocument (iframes same-origin)\n  ├─ Para cada entrada de shadow root:\n  │   ├─ DOM.resolveNode(backendNodeId) → objectId\n  │   └─ Resolver elemento host (melhor esforco)\n  └─ Retorna list[ShadowRoot] com referencias de host\n```\n\n### Timeout: Esperando Shadow Roots\n\nShadow hosts sao frequentemente injetados de forma assincrona. `Tab.find_shadow_roots()` aceita um parametro `timeout` que faz polling a cada 0.5s ate que pelo menos um shadow root seja encontrado ou o timeout expire (lanca `WaitElementTimeout`). Da mesma forma, `WebElement.get_shadow_root()` tambem suporta `timeout` para esperar pelo shadow root de um elemento especifico:\n\n```python\n# Esperar ate 10 segundos pelos shadow roots\nshadow_roots = await tab.find_shadow_roots(timeout=10)\n\n# Esperar pelo shadow root de um elemento especifico\nshadow = await element.get_shadow_root(timeout=5)\n```\n\n### Detalhes Importantes\n\n- **`pierce=True`** em `DOM.getDocument` faz o navegador incluir arrays `shadowRoots` nas descricoes de nos, permitindo a descoberta de todos os shadow roots sem navegar individualmente ate cada host.\n- **Conteudo de iframes same-origin** e incluido na arvore via nos `contentDocument`. A travessia os manipula.\n- Cada `ShadowRoot` retornado tem uma referencia ao seu `host_element` (resolvido por melhor esforco via `DOM.resolveNode`).\n\n### Travessia Profunda: IFrames Cross-Origin (OOPIFs)\n\nPor padrao, iframes cross-origin (OOPIFs) **nao** sao incluidos na arvore DOM — seu conteudo vive em um processo separado do navegador. Passe `deep=True` para tambem descobrir shadow roots dentro de OOPIFs:\n\n```python\nshadow_roots = await tab.find_shadow_roots(deep=True, timeout=10)\n```\n\nQuando `deep=True` e definido, o metodo executa etapas adicionais:\n\n```\nTab.find_shadow_roots(deep=True)\n  ├─ ... (travessia do documento principal como acima) ...\n  └─ _collect_oopif_shadow_roots()\n      ├─ ConnectionHandler de nivel browser (sem page_id → endpoint do browser)\n      ├─ Target.getTargets() → filtrar type='iframe'\n      └─ Para cada target iframe:\n          ├─ Target.attachToTarget(targetId, flatten=True) → sessionId\n          ├─ DOM.getDocument(depth=-1, pierce=True) com sessionId\n          ├─ _collect_shadow_roots_from_tree() no DOM do OOPIF\n          └─ Para cada shadow root encontrado:\n              ├─ DOM.resolveNode(backendNodeId) com sessionId\n              ├─ Resolver elemento host (melhor esforco) com sessionId\n              ├─ Criar IFrameContext(frame_id, session_handler, session_id)\n              └─ Definir IFrameContext no elemento host (ou diretamente no ShadowRoot)\n```\n\nOs objetos `ShadowRoot` retornados carregam o contexto de roteamento OOPIF (`IFrameContext`), entao elementos encontrados via `shadow_root.query()` roteiam automaticamente comandos CDP pela sessao OOPIF correta. Isso e critico para cenarios como captchas Cloudflare Turnstile, onde o checkbox esta dentro de um shadow root fechado dentro de um iframe cross-origin.\n\n## Limitacoes e Casos Especiais\n\n### Estrategias de Seletores Dentro de Shadow Roots\n\n!!! warning \"Use Apenas query() com CSS Dentro de Shadow Roots\"\n    `ShadowRoot` define `_css_only = True`, o que significa que apenas `query()` com seletores CSS e suportado. `find()` e `query()` com XPath lancam `NotImplementedError`.\n\nShadow roots implementam nativamente `querySelector()` e `querySelectorAll()`, tornando seletores CSS a escolha natural e confiavel:\n\n| Metodo | Dentro do Shadow Root | Notas |\n|--------|:--:|---|\n| `query('seletor-css')` | Totalmente suportado | Abordagem recomendada |\n| `query('seletor-css', find_all=True)` | Totalmente suportado | Retorna lista de elementos |\n| `find()` | Nao suportado | Lanca `NotImplementedError` |\n| `query('//xpath')` | Nao suportado | Lanca `NotImplementedError` |\n\n```python\nshadow = await host.get_shadow_root()\n\n# ✓ Recomendado: query() com seletores CSS\nbutton = await shadow.query('button.submit')\nemail = await shadow.query('#email-input')\nitems = await shadow.query('.item', find_all=True)\n\n# ✗ Nao suportado: find() e XPath lancam NotImplementedError\n# shadow.find(id='email-input')        # NotImplementedError\n# shadow.query('//button')             # NotImplementedError\n```\n\n### XPath Nao Cruza Fronteiras do Shadow\n\nExpressoes XPath a partir da raiz do documento nao conseguem atravessar fronteiras do shadow. Esta e uma limitacao fundamental do XPath, que foi projetado antes do Shadow DOM existir:\n\n```python\n# Nao encontra conteudo shadow: XPath no nivel do documento nao cruza a fronteira\nelement = await tab.find(xpath='//div[@id=\"host\"]//button')\n```\n\n### Shadow Roots User-Agent\n\nShadow roots internos do navegador (ex: estilizacao de placeholder de `<input>`, controles de `<video>`) sao do tipo `user-agent`. Eles sao acessiveis via CDP, mas sua estrutura interna varia entre versoes do navegador e nao faz parte de nenhum padrao web.\n\n```python\ninput_element = await tab.find(tag_name='input')\ntry:\n    ua_shadow = await input_element.get_shadow_root()\n    # ua_shadow.mode == ShadowRootType.USER_AGENT\n    # Estrutura interna e especifica do navegador\nexcept ShadowRootNotFound:\n    pass  # Nem todos os inputs tem shadow roots user-agent\n```\n\n!!! warning \"Estabilidade de Shadow Roots User-Agent\"\n    Nao construa logica de automacao que dependa da estrutura interna de shadow roots user-agent. Sua estrutura DOM e um detalhe de implementacao que pode mudar entre versoes do navegador sem aviso.\n\n### Referencias de Shadow Root Obsoletas\n\nSe o elemento host for removido do DOM e re-adicionado (comum em aplicacoes single-page), o `objectId` do shadow root se torna obsoleto. A solucao e re-adquirir o shadow root:\n\n```python\n# Apos uma navegacao de pagina ou reconstrucao do DOM:\nhost = await tab.find(id='my-component', timeout=5)  # Re-encontrar o host\nshadow = await host.get_shadow_root()                 # Shadow root atualizado\n```\n\n## Pontos-Chave\n\n- **Encapsulamento Shadow DOM** oculta elementos do `querySelector()` no nivel do documento, quebrando automacao tradicional\n- **CDP opera abaixo da camada de API JavaScript**, contornando restricoes de modo do shadow completamente\n- **`backendNodeId`** e o identificador estavel usado para resolucao de shadow root, evitando a necessidade de habilitar o dominio DOM\n- **`ShadowRoot` herda `FindElementsMixin`** com `_css_only = True`, suportando apenas `query()` com seletores CSS; `find()` e XPath lancam `NotImplementedError`\n- **Shadow roots fechados** sao totalmente acessiveis porque o modo `closed` e uma politica no nivel JavaScript, nao uma restricao no nivel DOM\n- **Shadow roots aninhados** funcionam naturalmente encadeando chamadas `get_shadow_root()` em cada nivel\n- **Shadow roots dentro de iframes** funcionam de forma transparente atraves da propagacao automatica de contexto do iframe\n- **Use seletores CSS** (`query()`) dentro de shadow roots; `find()` e XPath nao sao suportados\n- **`find_shadow_roots()`** descobre todos os shadow roots na pagina; suporta `timeout` para polling e `deep=True` para iframes cross-origin (OOPIFs)\n- **`get_shadow_root(timeout)`** espera pelo shadow root de um elemento especifico\n\n## Documentacao Relacionada\n\n- **[Guia de Pesquisa de Elementos](../../features/element-finding.md)**: Uso pratico de `find()`, `query()`, e acesso a shadow root\n- **[IFrames e Contextos](../fundamentals/iframes-and-contexts.md)**: Como o Pydoll resolve e roteia comandos para iframes, incluindo tratamento de OOPIF\n- **[Arquitetura do FindElements Mixin](./find-elements-mixin.md)**: Como o mecanismo `_object_id` habilita buscas com escopo\n- **[Dominio WebElement](./webelement-domain.md)**: Como elementos interagem com CDP\n- **[Camada de Conexao](../fundamentals/connection-layer.md)**: Comunicacao WebSocket com o navegador\n"
  },
  {
    "path": "docs/pt/deep-dive/architecture/tab-domain.md",
    "content": "# Arquitetura do Domínio da Aba (Tab)\n\nO domínio da Aba (Tab) é a interface principal do Pydoll para automação de navegador, atuando como uma camada de orquestração que integra múltiplos domínios CDP em uma API coesa. Este documento explora sua arquitetura interna, padrões de design e as decisões de engenharia que moldam seu comportamento.\n\n!!! info \"Uso Prático\"\n    Para exemplos de uso e padrões práticos, consulte o [Guia de Gerenciamento de Abas](../features/automation/tabs.md).\n\n## Visão Geral da Arquitetura\n\nA classe `Tab` serve como uma **façade (fachada)** sobre o Chrome DevTools Protocol, abstraindo a complexidade da coordenação de múltiplos domínios em uma interface unificada.\n\n### Estrutura de Componentes\n\n| Componente | Relacionamento | Propósito |\n|-----------|-------------|---------|\n| **Tab** | Classe principal | Interface de automação primária |\n| ↳ **ConnectionHandler** | Composição (própria) | Comunicação WebSocket com CDP |\n| ↳ **Browser** | Referência (pai) | Acesso ao estado e configuração em nível de navegador |\n| ↳ **FindElementsMixin** | Herança | Capacidades de localização de elementos |\n| ↳ **WebElement** | Fábrica (cria) | Representações individuais de elementos DOM |\n\n### Integração de Domínio CDP\n\nO `ConnectionHandler` roteia operações da Aba (Tab) para múltiplos domínios CDP:\n\n```\nMétodos da Aba            Domínio CDP          Propósito\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ngo_to(), refresh()     →   Page            →  Navegação & ciclo de vida\nexecute_script()       →   Runtime         →  Execução JavaScript\nfind(), query()        →   Runtime/DOM     →  Localização de elementos\nget_cookies()          →   Storage         →  Estado da sessão\nenable_network_events()→   Network         →  Monitoramento de tráfego\nenable_fetch_events()  →   Fetch           →  Interceptação de requisição\n```\n\n### Responsabilidades Principais\n\n1. **Roteamento de Comandos CDP**: Traduz operações de alto nível em comandos CDP específicos do domínio\n2. **Gerenciamento de Estado**: Rastreia domínios habilitados, callbacks ativos e estado da sessão\n3. **Coordenação de Eventos**: Faz a ponte entre eventos CDP e callbacks definidos pelo usuário\n4. **Fábrica de Elementos**: Cria instâncias de `WebElement` a partir de strings `objectId` do CDP\n5. **Gerenciamento do Ciclo de Vida**: Lida com limpeza e desalocação de recursos\n\n## Composição vs. Herança: O FindElementsMixin\n\nUma decisão arquitetural chave no domínio da Aba é **herdar de `FindElementsMixin`** em vez de usar composição:\n\n```python\nclass Tab(FindElementsMixin):\n    def __init__(self, ...):\n        self._connection_handler = ConnectionHandler(...)\n        # Métodos do Mixin agora estão disponíveis na Aba\n```\n\n**Por que herança aqui?**\n\n| Abordagem | Prós | Contras | Escolha do Pydoll |\n|----------|------|------|-----------------|\n| **Herança** | API limpa (`tab.find()`), compatibilidade de tipo | Acoplamento forte | Usado |\n| Composição | Baixo acoplamento, flexível | Verboso (`tab.finder.find()`), sobrecarga de wrapper | Não usado |\n\n**Justificativa:** O padrão mixin se justifica porque:\n\n- A localização de elementos é **central para a identidade da Aba** (toda aba pode encontrar elementos)\n- O mixin é **sem estado (stateless)** - ele requer apenas `_connection_handler` (injeção de dependência via duck typing)\n- A ergonomia da API importa - `tab.find()` é mais intuitivo do que `tab.elements.find()`\n\nVeja a [Análise Profunda do FindElements Mixin](./find-elements-mixin.md) para detalhes arquiteturais.\n\n## Arquitetura de Gerenciamento de Estado\n\nA classe Tab gerencia **múltiplas camadas de estado**:\n\n###  1. Flags de Habilitação de Domínio\n\n```python\nclass Tab:\n    def __init__(self, ...):\n        self._page_events_enabled = False\n        self._network_events_enabled = False\n        self._fetch_events_enabled = False\n        self._dom_events_enabled = False\n        self._runtime_events_enabled = False\n        self._intercept_file_chooser_dialog_enabled = False\n```\n\n**Por que flags explícitas?**\n\n- **Idempotência**: Chamar `enable_page_events()` duas vezes não registra duplamente\n- **Inspeção de estado**: Propriedades como `tab.page_events_enabled` expõem o estado atual\n- **Rastreamento de limpeza**: Sabe quais domínios precisam ser desabilitados no fechamento da aba\n\n**Alternativa (não usada):** Consultar o CDP sobre domínios habilitados a cada verificação → Muito lento, adiciona latência.\n\n### 2. Identidade do Alvo (Target)\n\n```python\nself._target_id: str              # Identificador CDP único\nself._browser_context_id: Optional[str]  # Contexto de isolamento\nself._connection_port: int        # Porta WebSocket\n```\n\n**Decisão de design:** `target_id` é o **identificador primário**, não a instância da Aba em si. Isso permite:\n\n- **Registro de Abas em nível de Navegador**: `Browser._tabs_opened[target_id] = tab`\n- **Padrão Singleton**: O mesmo `target_id` sempre retorna a mesma instância de `Tab`\n- **Reutilização de conexão**: Múltiplas operações na mesma aba compartilham o WebSocket\n\n### 3. Estado Específico de Funcionalidades\n\n```python\nself._cloudflare_captcha_callback_id: Optional[int] = None  # Para limpeza\nself._request: Optional[Request] = None  # Inicialização preguiçosa (lazy)\n```\n\n**Padrão de inicialização preguiçosa (lazy):** `Request` é criado apenas quando `tab.request` é acessado:\n\n```python\n@property\ndef request(self) -> Request:\n    if self._request is None:\n        self._request = Request(self)\n    return self._request\n```\n\n**Por que preguiçosa (lazy)?** A maioria das automações não usa requisições HTTP no contexto do navegador. Economiza memória e tempo de inicialização.\n\n\n## Execução JavaScript: Arquitetura de Contexto Duplo\n\nO método `execute_script()` implementa **polimorfismo de contexto** - mesma interface, diferentes comandos CDP:\n\n| Contexto | Método CDP | Caso de Uso |\n|---------|-----------|----------|\n| Global (sem elemento) | `Runtime.evaluate` | `document.title`, scripts globais |\n| Vinculado ao elemento | `Runtime.callFunctionOn` | Operações específicas do elemento |\n\n**Decisão arquitetural chave:** Autodetectar o modo de execução com base na presença do parâmetro `element`, eliminando APIs separadas (`evaluate()` vs. `call_function_on()`).\n\n**Pipeline de transformação de script:**\n\n1. Substitui `argument` → `this` (compatibilidade com Selenium)\n2. Detecta se o script já está encapsulado em `function() { }`\n3. Encapsula se necessário: `script` → `function() { script }`\n4. Roteia para o comando CDP apropriado\n\n**Por que a palavra-chave `argument`?** Caminho de migração para usuários do Selenium, familiaridade da API.\n\n!!! info \"Uso Prático\"\n    Veja [Interações Humanizadas](../features/automation/human-interactions.md) para padrões de execução de script do mundo real.\n\n## Integração com o Sistema de Eventos\n\nA Aba (Tab) atua como um **wrapper (invólucro) fino** sobre o sistema de eventos do ConnectionHandler, mas adiciona uma camada importante: **execução de callback não-bloqueante**.\n\n```python\nasync def on(self, event_name: str, callback: Callable, temporary: bool = False) -> int:\n    # Encapsula callbacks assíncronos para executar em background\n    async def callback_wrapper(event):\n        asyncio.create_task(callback(event))\n    \n    if asyncio.iscoroutinefunction(callback):\n        function_to_register = callback_wrapper  # Wrapper não-bloqueante\n    else:\n        function_to_register = callback  # Callbacks síncronos executam diretamente\n    \n    # Delega o registro ao ConnectionHandler\n    return await self._connection_handler.register_callback(\n        event_name, function_to_register, temporary\n    )\n```\n\n**Papel arquitetural:** A Aba fornece registro de eventos com escopo de aba e semântica de execução não-bloqueante, enquanto o ConnectionHandler lida com os mecanismos internos do WebSocket e a invocação sequencial de callbacks.\n\n**Principais características:**\n\n- **Execução em background** via `asyncio.create_task()` para callbacks assíncronos (disparar e esquecer)\n- **Autodetecção de callback síncrono/assíncrono**\n- **Callbacks temporários** para manipuladores de uso único\n- **ID de Callback** para remoção explícita\n\n**Modelo de execução:**\n\n| Camada | Comportamento | Propósito |\n|-------|----------|---------|\n| **Callback do usuário** | Executa em tarefa de background | Nunca bloqueia outros callbacks ou comandos CDP |\n| **Wrapper da Aba** | `create_task(callback())` | Inicia tarefa de background, retorna imediatamente |\n| **EventsManager** | `await wrapper()` | Invoca wrappers sequencialmente para o mesmo evento |\n\n**Por que o wrapper?** Sem ele, um callback assíncrono lento bloquearia outros callbacks para o mesmo evento. O wrapper `create_task` garante que todos os callbacks iniciem \"simultaneamente\" (em tarefas separadas), impedindo que um callback lento atrase os outros.\n\n!!! info \"Arquitetura Detalhada\"\n    Veja a [Análise Profunda da Arquitetura de Eventos](./event-architecture.md) para mecanismos internos de roteamento de eventos e o padrão de invocação sequencial do EventsManager.\n    \n    **Uso prático:** [Guia do Sistema de Eventos](../features/advanced/event-system.md)\n\n## Estado da Sessão: Gerenciamento de Cookies\n\n**Separação arquitetural:** Cookies são roteados para o **domínio Storage** (manipulação), não para o domínio Network (observação).\n\n```python\nasync def set_cookies(self, cookies: list[CookieParam]):\n    return await self._execute_command(\n        StorageCommands.set_cookies(cookies, self._browser_context_id)\n    )\n```\n\n**Design consciente de contexto:** O parâmetro `browser_context_id` garante o isolamento de cookies, permitindo automação multi-conta.\n\n!!! info \"Gerenciamento Prático de Cookies\"\n    Veja o [Guia de Cookies & Sessões](../features/browser-management/cookies-sessions.md) para padrões de uso e estratégias anti-detecção.\n\n## Captura de Conteúdo: Restrições de Alvo (Target) CDP\n\n**Limitação crítica:** `Page.captureScreenshot` só funciona em **alvos (targets) de nível superior**. Abas de Iframe falham silenciosamente (sem campo `data` na resposta).\n\n```python\ntry:\n    screenshot_data = response['result']['data']\nexcept KeyError:\n    raise TopLevelTargetRequired(...)  # Guia os usuários para WebElement.take_screenshot()\n```\n\n**Implicação de design:** Antes, o Pydoll criava instâncias de Tab dedicadas para iframes. Com o novo modelo, toda interação acontece no próprio `WebElement`, então capturas e outros utilitários devem ser executados nos elementos internos (por exemplo, `await iframe_element.find(...).take_screenshot()`).\n\n**Geração de PDF:** `Page.printToPDF` retorna dados codificados em base64. O Pydoll abstrai a E/S (I/O) de arquivo, mas os dados subjacentes são sempre base64 (especificação CDP).\n\n!!! info \"Uso Prático\"\n    Veja o [Guia de Screenshots & PDFs](../features/automation/screenshots-and-pdfs.md) para parâmetros, formatos e exemplos do mundo real.\n\n## Monitoramento de Rede: Design com Estado (Stateful)\n\n**Princípio arquitetural:** Métodos de rede exigem **estado habilitado** - verificações em tempo de execução impedem o acesso a dados inexistentes.\n\n**Separação de armazenamento:**\n\n- **Logs**: Armazenados em buffer no `ConnectionHandler` (recebe todos os eventos CDP)\n- **Aba (Tab)**: Consulta o manipulador, sem armazenamento duplicado\n- **Corpos de resposta (Response bodies)**: Recuperados sob demanda via `Network.getResponseBody(requestId)`\n\n**Restrição de tempo crítica:** Corpos de resposta devem ser buscados **dentro de ~30s** após a resposta (coleta de lixo do navegador).\n\n!!! info \"Monitoramento de Rede na Prática\"\n    Veja o [Guia de Monitoramento de Rede](../features/network/monitoring.md) para rastreamento abrangente de eventos e padrões de análise.\n    \n    **Interceptação de requisições:** [Guia de Interceptação de Requisições](../features/network/interception.md)\n\n## Gerenciamento de Diálogos: Padrão de Captura de Evento\n\n**Comportamento crítico do CDP:** Diálogos JavaScript **bloqueiam todos os comandos CDP** até serem tratados.\n\n**Solução arquitetural:** O `ConnectionHandler` captura eventos `Page.javascriptDialogOpening` imediatamente, evitando que a automação trave.\n\n```python\n# O Manipulador (Handler) armazena o evento de diálogo antes que o código do usuário rode\nself._connection_handler.dialog  # Capturado pelo manipulador\n# A Aba (Tab) consulta o evento armazenado\nasync def has_dialog(self) -> bool:\n    return bool(self._connection_handler.dialog)\n```\n\n**Por que esse design?** O evento dispara antes que os callbacks do usuário sejam executados. Sem captura imediata, a automação entraria em impasse (deadlock) aguardando respostas CDP bloqueadas.\n\n## Arquitetura de IFrame: Padrão de Reutilização de Aba\n\n**Insight chave:** IFrames são **alvos (targets) CDP de primeira classe** → Representados como instâncias de `Tab`.\n\n**Algoritmo de resolução de alvo:**\n\n1. Extrai o atributo `src` do elemento iframe\n2. Consulta todos os alvos CDP via `Target.getTargets()`\n3. Corresponde a URL do iframe ao `targetId` do alvo\n4. Verifica o registro singleton (`Browser._tabs_opened`)\n5. Retorna a instância existente ou cria + registra uma nova Aba (Tab)\n\n**Tradeoff (compromisso) de design:** Abas de Iframe herdam todos os métodos da Aba (Tab), mas alguns falham (ex: `take_screenshot()`). A alternativa (classe `IFrame` dedicada) duplicaria 90% da API para um benefício mínimo.\n\n!!! info \"Trabalhando com IFrames\"\n    Veja o [Guia de Interação com IFrame](../features/automation/iframes.md) para padrões práticos, frames aninhados e armadilhas comuns.\n\n## Gerenciadores de Contexto: Limpeza Automática de Recursos\n\n**Padrão arquitetural:** Restauração de estado + aquisição otimista de recursos.\n\n### Principais Gerenciadores de Contexto\n\n| Gerenciador | Padrão | Característica Chave |\n|---------|---------|-------------|\n| `expect_file_chooser()` | Restauração de estado | Restaura a habilitação do domínio ao sair |\n| `expect_download()` | Recursos temporários | Limpeza automática de diretórios temporários |\n\n**Design do Seletor de Arquivo (File Chooser):**\n\n- Habilita domínios necessários (`Page`, interceptação de seletor de arquivo)\n- Registra **callback temporário** (auto-remove após o primeiro disparo)\n- Restaura o estado original ao sair (se os domínios estavam desabilitados antes, desabilita novamente)\n\n**Design do Manipulador de Download:**\n\n- Cria diretório temporário (ou usa o caminho fornecido)\n- Usa `asyncio.Future` para coordenação (`will_begin_future`, `done_future`)\n- Configuração em nível de navegador (downloads são por contexto, não por aba)\n- Limpeza garantida via bloco `finally`\n\n!!! info \"Operações Práticas de Arquivo\"\n    Veja o [Guia de Operações de Arquivo](../features/automation/file-operations.md) para padrões de upload, uso do seletor de arquivos e manipulação de downloads.\n\n## Ciclo de Vida: Fechamento e Invalidação da Aba\n\n**Cascata de fechamento da aba:**\n\n1. CDP fecha a aba do navegador (`Page.close`)\n2. A Aba (Tab) desregistra-se de `Browser._tabs_opened`\n3. O WebSocket fecha automaticamente (alvo CDP destruído)\n4. Callbacks de evento sofrem coleta de lixo (garbage-collected)\n\n**Comportamento pós-fechamento:** A instância da Aba se torna **inválida** - operações futuras falharão (WebSocket fechado).\n\n**Decisão de design:** Sem flag `_closed` explícita. Os usuários gerenciam o ciclo de vida. A alternativa (rastreamento de estado) adiciona sobrecarga (overhead) para um benefício marginal de segurança.\n\n## Principais Decisões Arquiteturais\n\n### Estratégia de WebSocket por Aba\n\n**Design escolhido:** Cada Aba (Tab) cria seu próprio ConnectionHandler com uma conexão WebSocket dedicada para `ws://localhost:port/devtools/page/{targetId}`.\n\n**Justificativa:**\n\nO CDP suporta **dois modelos de conexão**:\n\n1. **Nível de Navegador**: Conexão única para `ws://localhost:port/devtools/browser/...` (usada pela instância do Navegador)\n2. **Nível de Aba**: Conexões por aba para `ws://localhost:port/devtools/page/{targetId}` (usadas pelas instâncias de Aba)\n\nO Pydoll usa **ambos**:\n\n- O **Navegador (Browser)** tem seu próprio ConnectionHandler para operações em todo o navegador (contextos, downloads, eventos em nível de navegador)\n- **Cada Aba (Tab)** tem seu próprio ConnectionHandler para operações específicas da aba (navegação, localização de elementos, eventos de aba)\n\n**Benefícios de WebSockets por aba:**\n\n- **Paralelismo verdadeiro**: Múltiplas abas podem executar comandos CDP simultaneamente sem esperar\n- **Fluxos de eventos independentes**: Cada aba recebe apenas seus próprios eventos (sem necessidade de filtragem)\n- **Falhas isoladas**: Problemas de conexão em uma aba não afetam outras\n- **Roteamento simplificado**: Sem necessidade de demultiplexar mensagens por targetId\n\n**Tradeoff (Compromisso):** Mais conexões abertas (uma por aba), mas o CDP e os navegadores lidam com isso eficientemente. Para 10 abas, são 11 conexões no total (1 navegador + 10 abas), o que é insignificante comparado às conexões HTTP que as próprias abas criam.\n\n!!! info \"Comunicação Navegador vs. Aba\"\n    Veja a [Arquitetura do Domínio do Navegador](./browser-domain.md) para detalhes sobre o ConnectionHandler em nível de navegador e como funciona a coordenação Navegador/Aba.\n\n### Necessidade da Referência ao Navegador\n\n**Por que a Aba armazena a referência `_browser`:**\n- Consultas de contexto (`browser_context_id` para cookies)\n- Operações em nível de navegador (comportamento de download, registro de iframe)\n- Acesso à configuração (`browser.options.page_load_state`)\n\n### Escolhas de Design da API\n\n| Escolha | Justificativa |\n|--------|-----------|\n| **Propriedades Assíncronas** (`current_url`, `page_source`) | Sinaliza dados ao vivo + custo CDP |\n| **Métodos `enable`/`disable` separados** | Explícito sobre implícito, corresponde à nomenclatura CDP |\n| **Sem flag `_closed`** | Usuários gerenciam ciclo de vida, reduz sobrecarga |\n| **Palavra-chave `argument` em scripts** | Compatibilidade com Selenium, caminho de migração |\n\n## Relacionamento com Outros Domínios\n\nO domínio da Aba (Tab) fica no **centro** da arquitetura do Pydoll:\n\n```mermaid\ngraph TD\n    Browser[Domínio do Navegador<br/>Ciclo de Vida & Processo] -->|cria| Tab[Domínio da Aba<br/>Interface de Automação]\n    Tab -->|usa| ConnectionHandler[ConnectionHandler<br/>Comunicação CDP]\n    Tab -->|cria| WebElement[Domínio do WebElement<br/>Interação com Elemento]\n    Tab -->|herda| FindMixin[FindElementsMixin<br/>Estratégias de Localização]\n    Tab -->|usa| Commands[Comandos CDP<br/>Protocolo Tipado]\n    \n    ConnectionHandler -->|despacha| Events[Sistema de Eventos]\n    Tab -.->|referencia| Browser\n    WebElement -.->|referencia| ConnectionHandler\n```\n\n**Relacionamentos chave:**\n\n1. **Navegador → Aba**: Pai-filho. O Navegador gerencia o ciclo de vida da Aba e o estado compartilhado.\n2. **Aba → ConnectionHandler**: Composição. A Aba delega a comunicação CDP.\n3. **Aba → WebElement**: Fábrica. A Aba cria elementos a partir de strings `objectId`.\n4. **Aba ← FindElementsMixin**: Herança. A Aba ganha métodos de localização de elementos.\n5. **Aba ↔ Navegador**: Referência bidirecional. A Aba consulta o navegador para informações de contexto.\n\n## Resumo: Filosofia de Design\n\nO domínio da Aba (Tab) prioriza a **ergonomia da API** e a **correção (correctness)** sobre micro-otimizações:\n\n- **Padrão Façade** abstrai a complexidade do CDP\n- **Gerenciamento de estado** via flags explícitas previne habilitação dupla\n- **Gerenciamento de recursos** através de gerenciadores de contexto\n- **Coordenação de eventos** com execução em background (não-bloqueante)\n\n**Principais tradeoffs (compromissos):**\n\n| Decisão | Benefício | Custo | Veredito |\n|----------|---------|------|---------|\n| WebSocket por aba | Paralelismo verdadeiro | Mais conexões | Justificado |\n| Herdar FindElementsMixin | API limpa | Acoplamento forte | Justificado |\n| Inicialização preguiçosa (lazy) de Request | Eficiência de memória | Sobrecarga (overhead) de propriedade | Justificado |\n\n## Leitura Adicional\n\n**Guias práticos:**\n\n- [Gerenciamento de Abas](../features/automation/tabs.md) - Padrões multi-aba, ciclo de vida, concorrência\n- [Localização de Elementos](../features/element-finding.md) - Seletores e travessia do DOM\n- [Sistema de Eventos](../features/advanced/event-system.md) - Monitoramento de navegador em tempo real\n\n**Análises profundas de arquitetura:**\n\n- [Arquitetura de Eventos](./event-architecture.md) - Mecanismos internos de WebSocket e roteamento de eventos\n- [FindElements Mixin](./find-elements-mixin.md) - Algoritmos de resolução de seletores\n- [Domínio do Navegador](./browser-domain.md) - Gerenciamento de processos e contextos"
  },
  {
    "path": "docs/pt/deep-dive/architecture/webelement-domain.md",
    "content": "# Arquitetura do Domínio WebElement\n\nO domínio WebElement faz a ponte entre o código de automação de alto nível e a interação DOM de baixo nível através do Chrome DevTools Protocol. Este documento explora sua arquitetura interna, padrões de design e decisões de engenharia.\n\n!!! info \"Uso Prático\"\n    Para exemplos de uso e padrões de interação, veja:\n    \n    - [Guia de Localização de Elementos](../features/element-finding.md)\n    - [Interações Humanizadas](../features/automation/human-interactions.md)\n    - [Operações com Arquivos](../features/automation/file-operations.md)\n\n## Visão Geral da Arquitetura\n\nO WebElement representa uma **referência de objeto remoto** para um elemento DOM através do mecanismo `objectId` do CDP:\n\n```\nCódigo do Usuário → WebElement → ConnectionHandler → CDP Runtime → DOM do Navegador\n```\n\n**Principais características:**\n\n- **Assíncrono por design**: Todas as operações seguem o padrão async/await do Python\n- **Referência remota**: Mantém o `objectId` do CDP para o elemento no lado do navegador\n- **Herança de Mixin**: Herda `FindElementsMixin` para buscas de elementos filhos\n- **Estado híbrido**: Combina atributos em cache com consultas DOM em tempo real\n\n### Estado Principal (Core)\n\n```python\nclass WebElement(FindElementsMixin):\n    def __init__(self, object_id: str, connection_handler: ConnectionHandler, ...):\n        self._object_id = object_id              # Referência de objeto remoto CDP\n        self._connection_handler = connection_handler  # Comunicação WebSocket\n        self._attributes: dict[str, str] = {}    # Atributos HTML em cache\n        self._search_method = method             # Como o elemento foi encontrado (debug)\n        self._selector = selector                # Seletor original (debug)\n```\n\n**Por que atributos em cache?** A localização inicial do elemento retorna atributos HTML. O cache fornece acesso síncrono rápido a propriedades comuns (`id`, `class`, `tag_name`) sem chamadas CDP adicionais.\n\n## Padrões de Design\n\n### 1. Padrão de Comando (Command Pattern)\n\nTodas as interações de elementos são traduzidas para comandos CDP:\n\n| Operação do Usuário | Domínio CDP | Comando |\n|----------------|-----------|---------|\n| `element.click()` | Input | `Input.dispatchMouseEvent` |\n| `element.text` | Runtime | `Runtime.callFunctionOn` |\n| `element.bounds` | DOM | `DOM.getBoxModel` |\n| `element.take_screenshot()` | Page | `Page.captureScreenshot` |\n\n### 2. Padrão de Ponte (Bridge Pattern)\n\nO WebElement abstrai a complexidade do protocolo CDP:\n\n```python\nasync def click(self, x_offset=0, y_offset=0, hold_time=0.1):\n    # API de alto nível\n    \n    # → Traduz para comandos CDP de baixo nível:\n    # 1. DOM.getBoxModel (obter posição)\n    # 2. Input.dispatchMouseEvent (pressionar)\n    # 3. Input.dispatchMouseEvent (soltar)\n```\n\n### 3. Herança de Mixin para Buscas de Filhos\n\n**Por que herdar FindElementsMixin?** Permite buscas relativas ao elemento:\n\n```python\nform = await tab.find(id='login-form')\nusername = await form.find(name='username')  # Busca dentro do formulário\n```\n\n**Decisão de design:** Composição (`form.finder.find()`) seria mais flexível, mas menos ergonômica. A herança foi escolhida pela simplicidade da API.\n\n## Sistema de Propriedades Híbrido\n\n**Inovação arquitetural:** O WebElement combina acesso a propriedades síncronas e assíncronas.\n\n### Propriedades Síncronas (Atributos em Cache)\n\n```python\n@property\ndef id(self) -> str:\n    return self._attributes.get('id')  # Dos atributos HTML em cache\n\n@property  \ndef class_name(self) -> str:\n    return self._attributes.get('class_name')  # 'class' → 'class_name' (palavra-chave do Python)\n```\n\n**Fonte:** Lista plana da resposta de localização do elemento CDP, analisada durante o `__init__`.\n\n### Propriedades Assíncronas (Estado DOM em Tempo Real)\n\n```python\n@property\nasync def text(self) -> str:\n    outer_html = await self.inner_html  # Chamada CDP\n    soup = BeautifulSoup(outer_html, 'html.parser')\n    return soup.get_text(strip=True)\n\n@property\nasync def bounds(self) -> dict:\n    response = await self._execute_command(DomCommands.get_box_model(self._object_id))\n    # Analisar e retornar limites (bounds)\n```\n\n**Justificativa:** Texto e limites (bounds) são **dinâmicos** - eles mudam conforme a página é atualizada. Atributos são **estáticos** - capturados no momento da localização.\n\n| Tipo de Propriedade | Acesso | Fonte | Caso de Uso |\n|--------------|--------|--------|----------|\n| Síncrona | `element.id` | Atributos em cache | Acesso rápido, dados estáticos |\n| Assíncrona | `await element.text` | Consulta CDP ao vivo | Estado atual, dados dinâmicos |\n\n## Implementação do Clique: Pipeline Multi-Estágio\n\nOperações de clique seguem um pipeline sofisticado para garantir confiabilidade:\n\n### 1. Detecção de Elemento Especial\n\n```python\nasync def click(self, x_offset=0, y_offset=0, hold_time=0.1):\n    # Estágio 1: Lidar com elementos especiais\n    if self._is_option_tag():\n        return await self.click_option_tag()  # <option> precisa de JavaScript para selecionar\n```\n\n**Por que tratamento especial?** Elementos `<option>` dentro de `<select>` não respondem a eventos de mouse. Requer JavaScript `selected = true`.\n\n### 2. Verificação de Visibilidade\n\n```python\n    # Estágio 2: Verificar se o elemento está visível\n    if not await self.is_visible():\n        raise ElementNotVisible()\n```\n\n**Por que verificar?** Eventos de mouse do CDP miram coordenadas. Elementos ocultos receberiam cliques em posições erradas ou falhariam silenciosamente.\n\n### 3. Cálculo de Posição\n\n```python\n    # Estágio 3: Rolar para visualização e obter posição\n    await self.scroll_into_view()\n    bounds = await self.bounds\n    \n    # Estágio 4: Calcular coordenadas do clique\n    position_to_click = (\n        bounds['x'] + bounds['width'] / 2 + x_offset,\n        bounds['y'] + bounds['height'] / 2 + y_offset,\n    )\n```\n\n**Suporte a offset (deslocamento):** Permite posições de clique variadas para comportamento semelhante ao humano (anti-detecção).\n\n### 4. Despacho de Evento de Mouse\n\n```python\n    # Estágio 5: Enviar eventos de mouse CDP\n    await self._execute_command(InputCommands.mouse_press(*position_to_click))\n    await asyncio.sleep(hold_time)  # Espera configurável (padrão 0.1s)\n    await self._execute_command(InputCommands.mouse_release(*position_to_click))\n```\n\n**Por que dois comandos?** Simula o comportamento real do mouse (pressionar → segurar → soltar). Alguns sites detectam cliques instantâneos como bots.\n\n### Alternativa de Clique (Fallback): JavaScript\n\n```python\nasync def click_using_js(self):\n    \"\"\"Fallback para elementos que não podem ser clicados via eventos de mouse.\"\"\"\n    await self.execute_script('this.click()')\n```\n\n**Quando usar:**\n- Elementos ocultos (ex: inputs de arquivo estilizados com CSS)\n- Elementos atrás de sobreposições (overlays)\n- Cenários críticos de performance (pula verificações de visibilidade/posição)\n\n!!! info \"Cliques de Mouse vs. JavaScript\"\n    Veja [Interações Humanizadas](../features/automation/human-interactions.md) para saber quando usar cada abordagem e as implicações de detecção.\n\n## Arquitetura de Screenshot: Regiões de Corte (Clip)\n\n**Mecanismo chave:** `Page.captureScreenshot` com parâmetro `clip`.\n\n```python\nasync def take_screenshot(self, path: str, quality: int = 100):\n    # 1. Obter limites (bounds) do elemento (posição + dimensões)\n    bounds = await self.get_bounds_using_js()\n    \n    # 2. Criar região de corte (clip)\n    clip = Viewport(x=bounds['x'], y=bounds['y'], \n                    width=bounds['width'], height=bounds['height'], scale=1)\n    \n    # 3. Capturar apenas a região cortada\n    screenshot = await self._execute_command(\n        PageCommands.capture_screenshot(format=ScreenshotFormat.JPEG, clip=clip, quality=quality)\n    )\n```\n\n**Por que limites (bounds) com JavaScript?** `DOM.getBoxModel` pode falhar para certos elementos. `getBoundingClientRect()` do JavaScript é uma alternativa (fallback) mais confiável.\n\n**Limitação de formato:** Screenshots de elementos sempre usam JPEG (restrição do CDP com regiões de corte).\n\n!!! info \"Capacidades de Screenshot\"\n    Veja [Screenshots & PDFs](../features/automation/screenshots-and-pdfs.md) para comparação entre screenshots de página inteira vs. elementos.\n\n## Contexto de Execução JavaScript\n\n**Recurso crítico do CDP:** `Runtime.callFunctionOn(objectId, ...)` executa JavaScript **no contexto do elemento** (`this` = elemento).\n\n```python\nasync def execute_script(self, script: str, return_by_value=False):\n    return await self._execute_command(\n        RuntimeCommands.call_function_on(self._object_id, script, return_by_value)\n    )\n```\n\n**Casos de uso:**\n\n- Verificações de visibilidade: `await element.is_visible()` → JavaScript verifica estilos computados\n- Manipulação de estilo: `await element.execute_script(\"this.style.border = '2px solid red'\")`\n- Acesso a atributos: Algumas propriedades exigem JavaScript (ex: `value` para inputs)\n\n**Alternativa (não usada):** Executar script global com seletor de elemento → Mais lento, arrisca referências obsoletas.\n\n## Pipeline de Verificação de Estado\n\n**Estratégia de confiabilidade:** Pré-verificar o estado do elemento antes de interações para prevenir falhas.\n\n| Verificação | Propósito | Implementação |\n|-------|---------|----------------|\n| `is_visible()` | Elemento na viewport, não oculto | JavaScript: `offsetWidth > 0 && offsetHeight > 0` |\n| `is_on_top()` | Sem sobreposições (overlays) bloqueando o elemento | JavaScript: `document.elementFromPoint(x, y) === this` |\n| `is_interactable()` | Visível + no topo | Combina ambas as verificações |\n\n**Por que JavaScript para visibilidade?** CSS `display: none`, `visibility: hidden`, `opacity: 0` todos afetam a visibilidade de formas diferentes. JavaScript fornece uma verificação unificada.\n\n## Estratégias de Performance\n\n### 1. Otimização Específica da Operação\n\n**Princípio:** Escolher a abordagem mais rápida para cada tipo de operação.\n\n| Operação | Abordagem Primária | Justificativa |\n|-----------|-----------------|-----------|\n| Extração de texto | Análise (parsing) com BeautifulSoup | Mais preciso que o `innerText` do JavaScript |\n| Verificação de visibilidade | JavaScript | Chamada CDP única vs. múltiplas consultas DOM |\n| Clique | Eventos de mouse CDP | Mais realista, necessário para anti-detecção |\n| Limites (Bounds) | `DOM.getBoxModel` | Mais rápido que JavaScript, com JS como fallback |\n\n### 2. Computação Local\n\n**Minimizar viagens de ida e volta ao CDP (round-trips)** computando localmente quando possível:\n\n```python\n# Bom: Consulta única de limites (bounds), cálculo local\nbounds = await element.bounds\nclick_x = bounds['x'] + bounds['width'] / 2 + offset_x\nclick_y = bounds['y'] + bounds['height'] / 2 + offset_y\n\n# Ruim: Múltiplas chamadas CDP para matemática simples\nclick_x = await element.execute_script('return this.offsetLeft + this.offsetWidth / 2')\nclick_y = await element.execute_script('return this.offsetTop + this.offsetHeight / 2')\n```\n\n### 3. Atributos em Cache\n\n**Decisão de design:** Armazenar atributos estáticos em cache no momento da criação:\n\n```python\n# Acesso síncrono rápido (sem chamada CDP)\nelement_id = element.id\nelement_class = element.class_name\n```\n\n**Tradeoff (Compromisso):** Atributos não refletirão mudanças em tempo de execução. Para propriedades dinâmicas, use assíncrono: `await element.text`.\n\n## Principais Decisões Arquiteturais\n\n| Decisão | Justificativa |\n|----------|-----------|\n| **Herdar FindElementsMixin** | Permite buscas de filhos, mantém consistência da API |\n| **Propriedades híbridas síncronas/assíncronas** | Equilibra performance (síncrono) com dados atualizados (assíncrono) |\n| **Alternativas (fallbacks) com JavaScript** | Confiabilidade acima da performance para operações críticas |\n| **Detecção de elementos especiais** | `<option>`, `<input type=\"file\">` exigem tratamento único |\n| **Verificações de visibilidade pré-clique** | Falhar rápido (fail fast) com erros claros vs. falhas silenciosas |\n\n## Resumo\n\nO domínio WebElement faz a ponte entre o código de automação Python e o DOM do navegador através de:\n\n- **Referências de objetos remotos** via `objectId` do CDP\n- **Sistema de propriedades híbrido** equilibrando atributos síncronos e estado assíncrono\n- **Pipelines de interação multi-estágio** garantindo confiabilidade\n- **Tratamento especializado** para variações de tipos de elementos\n\n**Principais tradeoffs (compromissos):**\n\n| Decisão | Benefício | Custo | Veredito |\n|----------|---------|------|---------|\n| Herança de Mixin | API limpa | Acoplamento forte | Justificado |\n| Atributos em cache | Acesso síncrono rápido | Risco de dados obsoletos | Justificado |\n| Alternativas (fallbacks) com JavaScript | Confiabilidade | Perda de performance | Justificado |\n| Pré-verificações de visibilidade | Erros claros | Chamadas CDP extras | Justificado |\n\n## Leitura Adicional\n\n**Guias práticos:**\n\n- [Localização de Elementos](../features/element-finding.md) - Localizando elementos, seletores\n- [Interações Humanizadas](../features.automation/human-interactions.md) - Clicar, digitar, realismo\n- [Operações com Arquivos](../features/automation/file-operations.md) - Uploads e downloads de arquivos\n\n**Análises profundas de arquitetura:**\n\n- [FindElements Mixin](./find-elements-mixin.md) - Pipeline de resolução de seletores\n- [Domínio da Aba (Tab)](./tab-domain.md) - A Aba como fábrica de elementos\n- [Camada de Conexão](./connection-layer.md) - Comunicação WebSocket"
  },
  {
    "path": "docs/pt/deep-dive/fingerprinting/behavioral-fingerprinting.md",
    "content": "# Fingerprinting Comportamental\n\nO fingerprinting comportamental analisa como os usuários interagem com aplicações web, em vez de quais ferramentas eles usam. Enquanto fingerprints de rede e navegador podem ser falsificados definindo os valores corretos, o comportamento humano segue padrões biomecânicos difíceis de replicar de forma convincente. Sistemas de detecção coletam movimentos de mouse, tempos de digitação, comportamento de scroll e sequências de interação, e então usam modelos estatísticos para distinguir humanos de automação.\n\nEste documento cobre as técnicas de detecção, a ciência por trás delas, e como os recursos de humanização do Pydoll abordam cada vetor.\n\n!!! info \"Navegação do Módulo\"\n    - [Network Fingerprinting](./network-fingerprinting.md): Fingerprinting de protocolo TCP/IP, TLS, HTTP/2\n    - [Browser Fingerprinting](./browser-fingerprinting.md): Canvas, WebGL, propriedades do navigator\n    - [Técnicas de Evasão](./evasion-techniques.md): Contramedidas práticas\n\n## Análise de Movimento do Mouse\n\nO movimento do mouse é um dos indicadores comportamentais mais poderosos porque o controle motor humano segue leis biomecânicas que automação simples não consegue replicar. Sistemas de detecção coletam eventos `mousemove` (cada um contendo coordenadas x, y e um timestamp) e analisam a trajetória em busca de propriedades que distinguem movimento orgânico de teleporte programático do cursor.\n\n### Lei de Fitts\n\nA Lei de Fitts descreve o tempo necessário para mover um ponteiro até um alvo. A formulação de Shannon (MacKenzie, 1992), que é a versão mais amplamente utilizada, estabelece:\n\n```\nT = a + b * log2(D/W + 1)\n```\n\nOnde `T` é o tempo de movimento, `a` é uma constante representando tempo de reação/início, `b` é uma constante representando a velocidade inerente do dispositivo de entrada, `D` é a distância até o alvo, e `W` é a largura (tamanho) do alvo. A relação logarítmica significa que dobrar a distância adiciona uma quantidade fixa de tempo, enquanto reduzir pela metade o tamanho do alvo adiciona a mesma quantidade fixa.\n\nAs implicações para detecção de bots são significativas. Humanos levam mais tempo para alcançar alvos pequenos e distantes e alcançam alvos grandes e próximos rapidamente. Eles aceleram no início de um movimento, atingem velocidade máxima aproximadamente no meio do caminho e desaceleram ao se aproximar do alvo. Bots que movem o cursor em tempo constante independentemente da distância e tamanho do alvo violam a Lei de Fitts e são trivialmente detectáveis.\n\nSistemas de detecção medem o tempo de movimento para cada evento de clique, calculam o tempo esperado a partir da distância e tamanho do alvo, e sinalizam movimentos significativamente mais rápidos do que a Lei de Fitts prevê ou que não mostram correlação entre distância/tamanho e tempo de movimento.\n\n### Forma da Trajetória\n\nMovimentos humanos da mão entre dois pontos não são linhas retas. A pesquisa de Abend, Bizzi e Morasso (1982) mostrou que os caminhos das mãos são tipicamente curvados devido a restrições biomecânicas das articulações e músculos do braço. Flash e Hogan (1985) demonstraram que movimentos de alcance humanos seguem trajetórias de jerk mínimo, onde a trajetória minimiza a integral do jerk (a derivada da aceleração) ao longo da duração do movimento. O perfil de velocidade resultante tem forma de sino e é descrito por um polinômio quíntico (grau 5):\n\n```\nx(t) = x0 + (xf - x0) * (10t^3 - 15t^4 + 6t^5)\n```\n\nonde `t` é o tempo normalizado de 0 a 1, e `x0`/`xf` são as posições inicial e final. Isso produz aceleração suave a partir do repouso, velocidade máxima aproximadamente no meio do caminho e desaceleração suave de volta ao repouso.\n\nSistemas de detecção analisam curvatura da trajetória, perfis de velocidade e padrões de aceleração. Os sinais específicos que procuram incluem:\n\n**Detecção de linha reta.** Um caminho perfeitamente reto entre dois pontos (curvatura zero em cada amostra) é o sinal de bot mais óbvio. Caminhos humanos sempre têm alguma curvatura devido às articulações rotacionais do braço.\n\n**Velocidade constante.** Humanos mostram um perfil de velocidade em forma de sino (acelerar, pico, desacelerar). Uma velocidade constante durante todo o movimento indica interpolação linear, que é o comportamento padrão da maioria das ferramentas de automação.\n\n**Ausência de sub-movimentos.** Movimentos longos são compostos por múltiplos sub-movimentos sobrepostos (Meyer et al., 1988), cada um com seu próprio pico de velocidade. Um movimento cobrindo 500+ pixels com um único pico de velocidade suave é suspeito; movimentos reais dessa distância tipicamente mostram 2-4 picos de velocidade.\n\n**Sem overshoot.** Humanos frequentemente ultrapassam o alvo ligeiramente (por 5-15 pixels) e fazem uma pequena correção de volta. Movimentos perfeitamente precisos que acertam exatamente no alvo toda vez são estatisticamente improváveis.\n\n### Entropia de Movimento\n\nEntropia, neste contexto, mede a imprevisibilidade do caminho do mouse. Sistemas de detecção dividem a trajetória em segmentos, medem a mudança de direção em cada ponto e calculam a entropia de Shannon sobre a distribuição de mudanças de direção. Uma linha reta tem entropia zero (cada segmento aponta na mesma direção). Uma caminhada aleatória tem entropia máxima. O movimento humano tem entropia moderada a alta, refletindo a combinação de direção intencional e variabilidade involuntária.\n\nEntropia baixa em muitos movimentos de mouse em uma sessão é um sinal forte de bot, mesmo que movimentos individuais tenham curvatura plausível.\n\n### Humanização de Mouse do Pydoll\n\nO Pydoll implementa humanização abrangente do mouse através do parâmetro `humanize=True` em operações de clique. Quando habilitado, o módulo de mouse gera movimentos que abordam cada um dos vetores de detecção descritos acima:\n\nO caminho segue uma curva Bezier cúbica com pontos de controle aleatorizados, produzindo curvatura natural em vez de linhas retas. A velocidade ao longo do caminho segue um perfil de jerk mínimo (`10t^3 - 15t^4 + 6t^5`), produzindo a curva de velocidade em forma de sino que a Lei de Fitts prevê. A duração do movimento é calculada usando a Lei de Fitts com constantes configuráveis (`a=0.070`, `b=0.150` por padrão).\n\nTremor fisiológico é simulado adicionando ruído Gaussiano às posições do cursor, com amplitude inversamente proporcional à velocidade (tremor é mais visível quando a mão se move lentamente, o que corresponde à fisiologia real). Overshoot ocorre com 70% de probabilidade, ultrapassando o alvo em 3-12% da distância total antes de fazer um movimento de correção. Micro-pausas (15-40ms) ocorrem com 3% de probabilidade durante o movimento, simulando hesitações breves.\n\n```python\n# Clique humanizado básico\nawait element.click(humanize=True)\n\n# A classe Mouse também pode ser usada diretamente para mais controle\nfrom pydoll.interactions.mouse import Mouse\n\nmouse = Mouse(connection_handler)\nawait mouse.click(500, 300, humanize=True)\n```\n\n!!! note \"O que o Pydoll Não Faz\"\n    A humanização de mouse do Pydoll atualmente não modela sub-movimentos para distâncias muito longas (o caminho é um único segmento Bezier). Para a maioria das interações web, onde distâncias são menores que 500 pixels, isso é suficiente. Movimentos extremamente longos (travessias diagonais de tela inteira) podem se beneficiar de suporte futuro a múltiplos segmentos.\n\n## Dinâmica de Digitação\n\nA dinâmica de digitação analisa os padrões de tempo da entrada do teclado. A técnica remonta aos operadores de telégrafo na década de 1850, que podiam identificar uns aos outros pelo \"punho\" do código Morse (padrão de tempo característico). Sistemas modernos medem o tempo com precisão de milissegundos através de eventos `keydown` e `keyup`.\n\n### Características de Tempo\n\nAs duas medições fundamentais são tempo de permanência (a duração entre `keydown` e `keyup` para uma única tecla, tipicamente 50-200ms para humanos) e tempo de voo (a duração entre soltar uma tecla e pressionar a próxima, tipicamente 80-400ms). A combinação de tempos de permanência e voo para pares de teclas consecutivas é chamada de latência de digrafo.\n\nLatências de digrafo não são uniformes. Elas dependem do par de teclas específico (bigrama) sendo digitado, porque digitação é uma habilidade motora onde sequências comuns são armazenadas como memória procedural. Os fatores biomecânicos chave são:\n\n**Alternância de mãos.** Bigramas digitados com mãos alternadas (como \"th\", onde \"t\" é mão esquerda e \"h\" é mão direita no QWERTY) são geralmente mais rápidos que bigramas da mesma mão (como \"de\", onde ambas as teclas são na mão esquerda). A mão alternada pode começar seu movimento enquanto a primeira mão ainda está completando sua tecla.\n\n**Distância dos dedos.** Transições de tecla inicial para tecla inicial são mais rápidas. Alcançar a fileira superior ou inferior adiciona tempo proporcional à distância física que o dedo deve percorrer.\n\n**Independência dos dedos.** Combinações de dedo anelar e mínimo na mesma mão são mais lentas que combinações de indicador e médio, porque o anelar e o mínimo compartilham tendões e têm menos controle motor independente.\n\n**Efeitos de frequência.** Bigramas frequentemente digitados (como \"th\", \"er\", \"in\" em inglês) são executados mais rapidamente devido à memória motora, independentemente de seu layout físico.\n\n### Sinais de Detecção\n\nSistemas de detecção procuram vários sinais que distinguem digitação humana de automação:\n\n**Tempo de permanência zero ou constante.** Muitas ferramentas de automação despacham eventos `keydown` e `keyup` com atraso zero ou quase zero entre eles (menos de 5ms). Pressionamentos reais de teclas têm tempos de permanência mensuráveis. Tempo de permanência constante em todas as teclas é igualmente suspeito.\n\n**Tempo de voo uniforme.** Definir um intervalo fixo entre teclas (como `type_text(\"hello\", interval=0.1)`) produz tempo perfeitamente regular que é trivialmente detectável. Tempos de voo humanos variam por bigrama, fadiga e carga cognitiva.\n\n**Sem erros de digitação.** Em entrada de texto extensa (50+ caracteres), a ausência completa de pressionamentos de backspace ou delete é incomum. Humanos cometem erros a uma taxa de aproximadamente 1-5% dependendo da proficiência de digitação e complexidade do texto.\n\n**Velocidade sobre-humana.** Digitação sustentada acima de 150 WPM está além da capacidade de todos exceto digitadores competitivos de elite. Ferramentas de automação que despacham caracteres mais rápido que isso são imediatamente sinalizadas.\n\n### Humanização de Teclado do Pydoll\n\nO `type_text(humanize=True)` do Pydoll aborda cada vetor de detecção com parâmetros configuráveis:\n\nAtrasos entre teclas são extraídos de uma distribuição uniforme (30-120ms por padrão) em vez de um intervalo fixo. Caracteres de pontuação (`.!?;:,`) recebem atraso adicional (80-180ms), simulando a pausa que ocorre quando um digitador considera a estrutura da frase. Pausas de pensamento (300-700ms) ocorrem com 2% de probabilidade, simulando breves momentos de reflexão. Pausas de distração (500-1200ms) ocorrem com 0.5% de probabilidade, simulando o digitador desviando o olhar ou sendo brevemente interrompido.\n\nErros de digitação realistas ocorrem com aproximadamente 2% de probabilidade por caractere, com cinco tipos de erro distintos ponderados por sua frequência no mundo real: erros de tecla adjacente (55%, pressionar uma tecla vizinha no QWERTY), transposições (20%, trocar dois caracteres consecutivos), pressionamentos duplos (12%, pressionar uma tecla duas vezes), caracteres pulados (8%, hesitar antes de digitar corretamente) e espaços esquecidos (5%, esquecer um espaço entre palavras). Cada tipo de erro inclui uma sequência de recuperação realista (pausa, backspace, correção) com tempo apropriado.\n\n```python\n# Digitação humanizada\nawait element.type_text(\"Hello, world!\", humanize=True)\n\n# Com configuração de tempo personalizada\nfrom pydoll.interactions.keyboard import Keyboard, TimingConfig, TypoConfig\n\nconfig = TimingConfig(\n    keystroke_min=0.04,\n    keystroke_max=0.15,\n    thinking_probability=0.03,\n)\nkeyboard = Keyboard(connection_handler, timing_config=config)\nawait keyboard.type_text(\"Custom timing example\", humanize=True)\n```\n\n!!! note \"O que o Pydoll Não Faz\"\n    A humanização de teclado do Pydoll usa atrasos aleatórios uniformes em vez de temporização ciente de bigramas. Não modela variação de tempo de permanência por tecla ou diferenças de velocidade de alternância de mãos. Para a maioria dos cenários de automação (preenchimento de formulários, consultas de busca), variação uniforme é suficiente para passar na detecção comportamental. Aplicações que requerem evasão de biometria de digitação em nível de autenticação precisariam de modelos de tempo personalizados.\n\n## Análise de Comportamento de Scroll\n\nO fingerprinting de scroll analisa como os usuários navegam verticalmente (e horizontalmente) pelo conteúdo da página. A distinção entre scroll humano e automatizado é marcante: chamadas programáticas `window.scrollTo()` produzem saltos instantâneos e discretos, enquanto scroll humano via roda do mouse, trackpad ou toque produz um fluxo de pequenos eventos incrementais com momentum e desaceleração.\n\n### Características Físicas do Scroll\n\nScroll por roda do mouse produz eventos `wheel` discretos com valores de delta consistentes (tipicamente 100 ou 120 pixels por notch, dependendo do SO e navegador). Os eventos chegam em intervalos irregulares refletindo quão rapidamente o usuário gira a roda. Scroll por trackpad produz muitos eventos pequenos com deltas decrescentes, simulando momentum físico. Scroll por toque é similar ao trackpad mas com deltas iniciais maiores e caudas de desaceleração mais longas.\n\nSistemas de detecção analisam a distribuição de delta, timing entre eventos e curva de desaceleração. Uma chamada `scrollTo(0, 5000)` produz um único salto sem eventos intermediários, que é fundamentalmente diferente das centenas de eventos incrementais que um scroll humano gera.\n\n### Sinais de Detecção\n\n**Scroll instantâneo.** Usar `window.scrollTo()` ou `window.scrollBy()` com valores grandes produz zero eventos de scroll intermediários. Sistemas de detecção que escutam eventos `scroll` veem a posição de scroll mudar em um único frame.\n\n**Deltas uniformes.** Simulação programática de scroll que despacha eventos wheel com valores de delta constantes (ex: sempre 100 pixels) carece da variação natural no scroll humano, onde valores de delta flutuam em 10-30% devido à pressão inconsistente dos dedos.\n\n**Sem desaceleração.** Scroll humano, especialmente em trackpads, tem uma fase de momentum onde o scroll continua após o usuário levantar o dedo, com velocidade exponencialmente decrescente. Scroll automatizado que para abruptamente carece dessa cauda de desaceleração.\n\n**Ausência de mudanças de direção.** Humanos frequentemente scrollam demais e scrollam de volta ligeiramente, ou pausam no meio de uma página para ler conteúdo. Scroll automatizado que se move em uma direção com velocidade constante sem pausas ou reversões é suspeito.\n\n### Humanização de Scroll do Pydoll\n\nO módulo de scroll do Pydoll implementa scroll humanizado através de `scroll.by(position, distance, humanize=True)`:\n\nO scroll segue uma curva de easing Bezier cúbica (pontos de controle `0.645, 0.045, 0.355, 1.0` por padrão), produzindo aceleração e desaceleração naturais. Jitter por frame de ±3 pixels adiciona variação aos valores de delta. Micro-pausas (20-50ms) ocorrem com 5% de probabilidade, simulando paradas breves de leitura. Overshoot ocorre com 15% de probabilidade, scrollando 2-8% além do alvo e corrigindo de volta. Para grandes distâncias, o scroll é dividido em múltiplos gestos de \"flick\" (100-1200 pixels cada), simulando como um usuário real scrolla por uma página longa com deslizes repetidos em vez de um único movimento contínuo.\n\n```python\nfrom pydoll.interactions.scroll import Scroll, ScrollPosition\n\nscroll = Scroll(connection_handler)\n\n# Scroll humanizado para baixo 800 pixels\nawait scroll.by(ScrollPosition.Y, 800, humanize=True)\n\n# Scroll até o topo/fundo usa múltiplos flicks semelhantes a humanos\nawait scroll.to_bottom(humanize=True)\n```\n\n## Vetores de Detecção Adicionais\n\nAlém da análise de mouse, teclado e scroll, sistemas de detecção sofisticados monitoram vários outros sinais comportamentais.\n\n### Foco e Visibilidade\n\nA API de Visibilidade de Página (`document.visibilityState`) e eventos de foco (`window.onfocus`, `window.onblur`) revelam se o usuário está ativamente visualizando a página. Uma sessão de usuário real inclui trocas de aba, minimizações de janela e períodos de inatividade. Um script de automação que mantém foco contínuo por horas sem um único evento blur é comportamentalmente anômalo. Da mesma forma, `document.hasFocus()` retornando `true` continuamente por períodos prolongados é incomum.\n\n### Padrões de Inatividade\n\nUsuários reais têm períodos naturais de inatividade: lendo conteúdo, pensando antes de agir, sendo distraídos. Sistemas de detecção medem a distribuição de tempos de inatividade entre interações. Uma sessão onde cada ação segue a anterior dentro de 100-500ms sem pausas mais longas segue um padrão que é estatisticamente distinto da navegação humana, onde períodos de inatividade de 2-30 segundos entre ações são normais.\n\n### Integridade de Sequência de Eventos\n\nNavegadores geram sequências de eventos específicas para interações do usuário. Um clique de mouse produz `pointerdown`, `mousedown`, `pointerup`, `mouseup`, `click` nessa ordem, precedido por eventos `pointermove`/`mousemove` mostrando o cursor se aproximando do alvo do clique. Ferramentas de automação que despacham um evento `click` sem o movimento precedente e eventos de ponteiro são detectáveis através de análise de sequência de eventos.\n\nO despacho de eventos baseado em CDP do Pydoll gera sequências completas de eventos porque usa a simulação de entrada do Chrome, que produz a mesma cadeia de eventos que entrada real do usuário.\n\n## Detecção por Machine Learning\n\nSistemas anti-bot modernos (DataDome, Akamai Bot Manager, Cloudflare Bot Management, PerimeterX/HUMAN Security) não usam regras de limiar simples. Eles treinam modelos de machine learning em milhões de sessões de usuários reais e milhões de sessões de bots conhecidos, aprendendo a distinguir humanos de automação com base em 50+ características simultaneamente.\n\nEsses modelos capturam propriedades estatísticas difíceis de enumerar como regras individuais: a distribuição conjunta de velocidade de movimento e curvatura, a correlação entre velocidade de digitação e taxa de erro, a relação entre profundidade de scroll e tempo de leitura, e o \"ritmo\" geral de uma sessão de navegação. Um sistema que passa em cada verificação individual mas tem correlações sutilmente erradas entre características ainda pode ser sinalizado por um modelo bem treinado.\n\nA implicação prática é que a evasão comportamental deve ser consistente em todos os tipos de interação, não apenas individualmente plausível. O parâmetro `humanize=True` do Pydoll fornece uma camada de humanização coerente entre interações de mouse, teclado e scroll, mas o desenvolvedor ainda é responsável pela plausibilidade comportamental de nível mais alto: adicionar atrasos de leitura entre carregamentos de página, variar o ritmo de workflows de múltiplas páginas e incluir períodos naturais de inatividade.\n\n## Referências\n\n- Fitts, P. M. (1954). The Information Capacity of the Human Motor System in Controlling the Amplitude of Movement. Journal of Experimental Psychology.\n- MacKenzie, I. S. (1992). Fitts' Law as a Research and Design Tool in Human-Computer Interaction. Human-Computer Interaction.\n- Flash, T., & Hogan, N. (1985). The Coordination of Arm Movements: An Experimentally Confirmed Mathematical Model. Journal of Neuroscience.\n- Abend, W., Bizzi, E., & Morasso, P. (1982). Human Arm Trajectory Formation. Brain.\n- Meyer, D. E., Abrams, R. A., Kornblum, S., Wright, C. E., & Smith, J. E. K. (1988). Optimality in Human Motor Performance. Psychological Review.\n- Ahmed, A. A. E., & Traore, I. (2007). A New Biometric Technology Based on Mouse Dynamics. IEEE TDSC.\n"
  },
  {
    "path": "docs/pt/deep-dive/fingerprinting/browser-fingerprinting.md",
    "content": "# Browser Fingerprinting\n\nO browser fingerprinting identifica clientes analisando propriedades expostas através de APIs JavaScript, cabeçalhos HTTP e motores de renderização. Diferentemente do network fingerprinting, que examina sinais de nível de protocolo do kernel do SO e biblioteca TLS, o browser fingerprinting tem como alvo a camada de aplicação: o navegador específico, sua versão, sua configuração e o hardware em que roda. Esses sinais são acessíveis a qualquer site através de APIs web padrão, e a combinação de propriedades suficientes cria um fingerprint que é frequentemente único entre milhões de visitantes.\n\n!!! info \"Navegação do Módulo\"\n    - [Network Fingerprinting](./network-fingerprinting.md): Fingerprinting de protocolo TCP/IP, TLS, HTTP/2\n    - [Behavioral Fingerprinting](./behavioral-fingerprinting.md): Análise de mouse, teclado, scroll\n    - [Técnicas de Evasão](./evasion-techniques.md): Contramedidas práticas\n\n## Propriedades JavaScript do Navigator\n\nO objeto `navigator` é a fonte mais rica de dados de fingerprinting de navegador. Ele expõe dezenas de propriedades que revelam o navegador, suas capacidades e o sistema em que roda. Sistemas de detecção coletam essas propriedades, fazem referência cruzada entre elas e contra cabeçalhos HTTP, e sinalizam inconsistências.\n\nO seguinte JavaScript coleta o conjunto central de propriedades que sistemas de fingerprinting tipicamente examinam:\n\n```javascript\nconst fingerprint = {\n    // Identidade\n    userAgent: navigator.userAgent,\n    platform: navigator.platform,\n    vendor: navigator.vendor,\n\n    // Idioma e locale\n    language: navigator.language,\n    languages: navigator.languages,\n\n    // Hardware\n    hardwareConcurrency: navigator.hardwareConcurrency,\n    deviceMemory: navigator.deviceMemory,\n    maxTouchPoints: navigator.maxTouchPoints,\n\n    // Recursos\n    cookieEnabled: navigator.cookieEnabled,\n    doNotTrack: navigator.doNotTrack,\n    webdriver: navigator.webdriver,\n\n    // Tela\n    screenWidth: screen.width,\n    screenHeight: screen.height,\n    colorDepth: screen.colorDepth,\n    devicePixelRatio: window.devicePixelRatio,\n\n    // Chrome do navegador (barra de ferramentas, dimensões da scrollbar)\n    chromeHeight: window.outerHeight - window.innerHeight,\n    chromeWidth: window.outerWidth - window.innerWidth,\n\n    // Timezone\n    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n    timezoneOffset: new Date().getTimezoneOffset(),\n};\n```\n\nVárias dessas propriedades merecem atenção individual porque carregam mais peso de fingerprinting ou são mais comumente mal configuradas por ferramentas de automação.\n\n### Consistência de Platform e User-Agent\n\nA propriedade `navigator.platform` retorna uma string como `Win32`, `MacIntel` ou `Linux x86_64`. Sistemas de detecção comparam isso com o cabeçalho User-Agent. Se o User-Agent HTTP afirma `Windows NT 10.0` mas `navigator.platform` retorna `Linux x86_64`, a inconsistência é um sinal forte. Este é um dos erros mais comuns em automação: definir um User-Agent personalizado via `--user-agent=` sem também sobrescrever a plataforma.\n\n### Propriedades de Hardware\n\n`navigator.hardwareConcurrency` retorna o número de núcleos lógicos de CPU. Um valor de 1 ou 2 sugere uma VM ou container mínimo em vez de uma máquina real de usuário. `navigator.deviceMemory` reporta RAM aproximada em gigabytes (0.25, 0.5, 1, 2, 4, 8). Esta propriedade só está disponível em navegadores Chromium; Firefox e Safari retornam `undefined`. Ambos os valores devem ser consistentes com o dispositivo declarado: um User-Agent alegando um desktop moderno mas reportando 1 núcleo e 0.5 GB de RAM é suspeito.\n\n### Propriedade WebDriver\n\nA propriedade `navigator.webdriver` é `true` quando o navegador é controlado por automação baseada em WebDriver (Selenium, Playwright em modo WebDriver). Este é o indicador de automação mais óbvio. O Pydoll usa CDP (Chrome DevTools Protocol) diretamente, que não define esta flag. Em um navegador controlado pelo Pydoll, `navigator.webdriver` é `undefined`, correspondendo ao comportamento de uma sessão normal de usuário.\n\n### Plugins\n\nA propriedade `navigator.plugins` foi historicamente um forte vetor de fingerprinting porque diferentes navegadores e configurações de SO expunham diferentes listas de plugins. Navegadores Chromium modernos (Chrome 90+) retornam uma lista fixa de cinco plugins relacionados a PDF independentemente do estado real dos plugins:\n\n```javascript\n// Chrome moderno sempre retorna estes 5 plugins:\n// 1. PDF Viewer\n// 2. Chrome PDF Viewer\n// 3. Chromium PDF Viewer\n// 4. Microsoft Edge PDF Viewer\n// 5. WebKit built-in PDF\nconsole.log(navigator.plugins.length); // 5\n```\n\nUm equívoco comum alega que navegadores modernos retornam arrays vazios para `navigator.plugins`. Isto é incorreto. Retornar um array vazio é em si um sinal de detecção que sugere modo headless ou um cliente HTTP não-navegador.\n\n### Dimensões de Tela e Janela\n\nA diferença entre `window.outerWidth`/`outerHeight` e `window.innerWidth`/`innerHeight` representa o chrome do navegador (barras de ferramentas, scrollbars, moldura da janela). Navegadores headless frequentemente reportam diferença zero porque não têm UI visível. Sistemas de detecção sinalizam clientes onde `outerWidth` é igual a `innerWidth` como potencialmente headless. Da mesma forma, `screen.width` correspondendo a `innerWidth` exatamente sugere uma janela headless maximizada em vez de uma sessão desktop normal.\n\nO `devicePixelRatio` varia por display: monitores padrão reportam `1.0`, displays Retina de MacBook reportam `2.0`, e smartphones reportam `2.0` a `3.0`. Este valor deve ser consistente com o dispositivo declarado no User-Agent.\n\n## User-Agent Client Hints\n\nNavegadores Chromium modernos (Chrome, Edge, Opera) complementam a string User-Agent tradicional com cabeçalhos Client Hints: `Sec-CH-UA`, `Sec-CH-UA-Platform`, `Sec-CH-UA-Mobile`, e (sob demanda) valores de maior entropia como `Sec-CH-UA-Full-Version-List`, `Sec-CH-UA-Arch` e `Sec-CH-UA-Bitness`.\n\n```http\nSec-CH-UA: \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\", \"Not:A-Brand\";v=\"99\"\nSec-CH-UA-Mobile: ?0\nSec-CH-UA-Platform: \"Windows\"\n```\n\nClient Hints fornecem dados estruturados e legíveis por máquina que são mais difíceis de falsificar de forma inconsistente. Um servidor pode comparar o cabeçalho `Sec-CH-UA-Platform` com `navigator.platform`, a string User-Agent e o fingerprint TCP/IP. Qualquer inconsistência entre essas camadas é um sinal de detecção.\n\nO equivalente no lado JavaScript é `navigator.userAgentData`, que expõe `brands`, `mobile` e `platform` como valores de baixa entropia, e `getHighEntropyValues()` para informações detalhadas de versão, arquitetura e bitness:\n\n```javascript\n// Baixa entropia (sempre disponível, sem necessidade de permissão)\nconsole.log(navigator.userAgentData.brands);\n// [{brand: \"Chromium\", version: \"120\"}, {brand: \"Google Chrome\", version: \"120\"}, ...]\nconsole.log(navigator.userAgentData.platform); // \"Windows\"\nconsole.log(navigator.userAgentData.mobile);   // false\n\n// Alta entropia (requer promise, pode requerer permissão)\nconst highEntropy = await navigator.userAgentData.getHighEntropyValues([\n    'architecture', 'bitness', 'platformVersion', 'uaFullVersion'\n]);\n// {architecture: \"x86\", bitness: \"64\", platformVersion: \"15.0.0\", ...}\n```\n\n!!! warning \"Suporte de Navegador\"\n    Client Hints são um recurso exclusivo do Chromium. Firefox e Safari não enviam cabeçalhos `Sec-CH-UA` e não expõem `navigator.userAgentData`. Se o User-Agent alega Firefox mas o servidor recebe cabeçalhos Client Hints, o cliente não é Firefox.\n\n## Canvas Fingerprinting\n\nO canvas fingerprinting explora o fato de que a API Canvas do HTML5 produz saída de pixels sutilmente diferente em diferentes combinações de GPU, driver gráfico, SO e navegador. A variação vem de diferenças na rasterização de fontes (renderização sub-pixel, hinting, anti-aliasing), execução de shader específica da GPU, precisão de ponto flutuante no pipeline gráfico e bibliotecas de renderização de texto no nível do SO (DirectWrite no Windows, Core Text no macOS, FreeType no Linux).\n\nA técnica desenha texto, formas e gradientes em um canvas oculto, extrai os dados de pixel e faz hash:\n\n```javascript\nfunction generateCanvasFingerprint() {\n    const canvas = document.createElement('canvas');\n    canvas.width = 220;\n    canvas.height = 30;\n    const ctx = canvas.getContext('2d');\n\n    // Retângulo colorido (expõe diferenças de blending)\n    ctx.fillStyle = '#f60';\n    ctx.fillRect(125, 1, 62, 20);\n\n    // Texto com emoji (maximiza variação de renderização)\n    ctx.font = '14px Arial';\n    ctx.textBaseline = 'alphabetic';\n    ctx.fillStyle = '#069';\n    ctx.fillText('Cwm fjordbank glyphs vext quiz, 😃', 2, 15);\n\n    // Sobreposição semi-transparente (expõe diferenças de composição alfa)\n    ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';\n    ctx.fillText('Cwm fjordbank glyphs vext quiz, 😃', 4, 17);\n\n    return canvas.toDataURL();\n}\n```\n\nO pangrama \"Cwm fjordbank glyphs vext quiz\" é escolhido porque usa combinações incomuns de caracteres que estressam a renderização de fontes. O emoji adiciona outra dimensão porque a renderização de emoji varia significativamente entre sistemas operacionais. A sobreposição semi-transparente testa composição alfa, que difere entre implementações de GPU.\n\nO canvas fingerprinting é eficaz para distinguir categorias amplas de dispositivos, mas sua unicidade é às vezes exagerada. A pesquisa de Laperdrix et al. (2016) encontrou que fingerprints de canvas sozinhos fornecem poder de distinção moderado, e seu verdadeiro valor vem da combinação com outros sinais (WebGL, propriedades do navigator, timezone) para alcançar alta unicidade.\n\n!!! note \"Injeção de Ruído no Canvas\"\n    Algumas ferramentas de privacidade injetam ruído aleatório na saída do canvas para quebrar o fingerprinting. Sistemas de detecção contra-atacam solicitando o fingerprint do canvas múltiplas vezes na mesma sessão. Se o hash muda entre requisições, injeção de ruído está presente, o que é em si um sinal de detecção. Randomizar a saída do canvas é, portanto, contraproducente: não previne a identificação e revela o uso de ferramentas anti-fingerprinting.\n\nComo o Pydoll controla uma instância real do Chrome com renderização GPU real, o fingerprint de canvas é autêntico e consistente entre leituras repetidas. Nenhuma injeção ou falsificação é necessária.\n\n## WebGL Fingerprinting\n\nO WebGL fingerprinting estende o canvas fingerprinting para o pipeline de renderização 3D. É mais poderoso porque expõe diretamente identificadores de hardware que são difíceis de falsificar.\n\nOs dados mais distintivos vêm da extensão `WEBGL_debug_renderer_info`, que revela o fabricante e modelo da GPU:\n\n```javascript\nfunction getWebGLFingerprint() {\n    const canvas = document.createElement('canvas');\n    const gl = canvas.getContext('webgl');\n    if (!gl) return null;\n\n    // Identificação da GPU (mais distintivo)\n    const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');\n    const vendor = debugInfo\n        ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL)\n        : gl.getParameter(gl.VENDOR);\n    const renderer = debugInfo\n        ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)\n        : gl.getParameter(gl.RENDERER);\n\n    return {\n        vendor,    // ex: \"Google Inc. (NVIDIA)\"\n        renderer,  // ex: \"ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0)\"\n        version: gl.getParameter(gl.VERSION),\n        shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),\n        maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),\n        extensions: gl.getSupportedExtensions(),\n    };\n}\n```\n\nA string do renderer nomeia diretamente o hardware da GPU. Um cliente alegando ser um dispositivo móvel mas reportando uma GPU desktop é obviamente inconsistente. Máquinas virtuais frequentemente reportam renderizadores de software como \"SwiftShader\" ou \"llvmpipe\", que usuários reais quase nunca têm.\n\nAlém de metadados, o WebGL pode renderizar uma cena 3D (um triângulo gradiente, por exemplo) e fazer hash da saída de pixels, produzindo um fingerprint de renderização análogo ao canvas fingerprinting mas no pipeline 3D. A combinação de identificadores de GPU, extensões suportadas, limites de parâmetros (`MAX_TEXTURE_SIZE`, `MAX_VIEWPORT_DIMS`) e formatos de precisão de shader cria um fingerprint detalhado da pilha gráfica.\n\n## AudioContext Fingerprinting\n\nA Web Audio API gera fingerprints processando áudio e medindo a saída. A técnica padrão cria um `OscillatorNode`, roteia através de um `DynamicsCompressorNode`, e lê as amostras de áudio resultantes de um `AnalyserNode` ou `OfflineAudioContext`. Diferenças nas implementações de processamento de áudio entre navegadores e pilhas de áudio do SO produzem saída distinta.\n\n```javascript\nfunction getAudioFingerprint() {\n    const ctx = new OfflineAudioContext(1, 44100, 44100);\n    const oscillator = ctx.createOscillator();\n    oscillator.type = 'triangle';\n    oscillator.frequency.setValueAtTime(10000, ctx.currentTime);\n\n    const compressor = ctx.createDynamicsCompressor();\n    compressor.threshold.setValueAtTime(-50, ctx.currentTime);\n    compressor.knee.setValueAtTime(40, ctx.currentTime);\n    compressor.ratio.setValueAtTime(12, ctx.currentTime);\n    compressor.attack.setValueAtTime(0, ctx.currentTime);\n    compressor.release.setValueAtTime(0.25, ctx.currentTime);\n\n    oscillator.connect(compressor);\n    compressor.connect(ctx.destination);\n    oscillator.start(0);\n\n    return ctx.startRendering().then(buffer => {\n        const data = buffer.getChannelData(0);\n        // Hash de um subconjunto das amostras de áudio\n        let hash = 0;\n        for (let i = 4500; i < 5000; i++) {\n            hash += Math.abs(data[i]);\n        }\n        return hash;\n    });\n}\n```\n\nO AudioContext fingerprinting é menos amplamente implantado que canvas ou WebGL fingerprinting, mas adiciona outra dimensão ao fingerprint geral. O sinal é particularmente útil para distinguir navegadores no mesmo SO, já que o processamento de áudio varia mais entre motores de navegador do que entre versões de SO.\n\n## Battery Status API\n\nA Battery Status API (`navigator.getBattery()`) expõe o nível de bateria do dispositivo, status de carregamento e tempos estimados de carga/descarga. Esses valores criam um fingerprint de curta duração mas único para a duração de uma sessão.\n\nEsta API só está disponível em navegadores Chromium. O Firefox a removeu na versão 52 (2017) citando preocupações de privacidade, e o Safari nunca a implementou. Sistemas de detecção que veem resultados da Battery API de um cliente alegando ser Firefox ou Safari sabem que o cliente está representando falsamente sua identidade.\n\n## Fingerprinting de Cabeçalhos HTTP\n\nAlém de APIs JavaScript, cabeçalhos HTTP fornecem sinais de fingerprinting visíveis ao servidor antes de qualquer JavaScript executar.\n\n### Ordem dos Cabeçalhos\n\nNavegadores enviam cabeçalhos HTTP em uma ordem consistente e específica por versão. O Chrome coloca cabeçalhos `Sec-CH-UA` cedo, antes de `User-Agent`. O Firefox lidera com `User-Agent` seguido por `Accept` e `Accept-Language`. Bibliotecas HTTP automatizadas como `requests` ou `httpx` do Python enviam cabeçalhos em outra ordem, tipicamente começando com `Host` e `Connection`.\n\nSistemas de detecção registram a ordem dos primeiros 10-15 cabeçalhos e comparam contra assinaturas de navegadores conhecidos. Mesmo que todos os valores de cabeçalho individuais estejam corretos, enviá-los na ordem errada revela que a requisição não foi gerada pelo navegador declarado. Como o Pydoll controla uma instância real do Chrome, a ordem dos cabeçalhos é autêntica.\n\n### Accept-Encoding\n\nNavegadores modernos suportam compressão Brotli (`br`) além de `gzip` e `deflate`. O Chrome também suporta `zstd`. O `Accept-Encoding` do Chrome moderno se parece com `gzip, deflate, br, zstd`. Um cliente alegando ser Chrome mas sem Brotli é desatualizado ou automatizado.\n\n### Consistência de Accept-Language\n\nO cabeçalho `Accept-Language` deve ser consistente com `navigator.language`, `navigator.languages`, o timezone e a geolocalização do IP. Uma requisição com `Accept-Language: en-US` de um IP em Tóquio com timezone `Asia/Tokyo` é plausível para um viajante mas suspeita em combinação com outros sinais. Uma requisição com `Accept-Language: zh-CN` e timezone `America/New_York` de um IP de datacenter chinês é um forte indicador de proxy.\n\n## Implicações para o Pydoll\n\nPorque o Pydoll controla um navegador Chromium real através do CDP, todos os fingerprints de nível de navegador são autênticos por padrão. Os fingerprints de canvas, WebGL e AudioContext vêm de hardware real de GPU e áudio. As propriedades do navigator, plugins e dimensões de tela refletem o estado real do navegador. Cabeçalhos HTTP, incluindo sua ordem, são gerados pela pilha de rede do Chrome.\n\nO principal risco na automação é inconsistência entre camadas. Definir um User-Agent personalizado sem sincronizar propriedades relacionadas cria incompatibilidades trivialmente detectáveis. O Pydoll lida com isso automaticamente: quando detecta `--user-agent=` nos argumentos do navegador, usa `Emulation.setUserAgentOverride` para sincronizar a string User-Agent, plataforma e metadados completos de Client Hints em todas as camadas. Também injeta sobrescritas de `navigator.vendor` e `navigator.appVersion` via `Page.addScriptToEvaluateOnNewDocument` para garantir consistência em abas recém-abertas.\n\nPara consistência de timezone e geolocalização (para corresponder à localização do IP do proxy), sobrescritas JavaScript podem definir `Intl.DateTimeFormat().resolvedOptions().timeZone` e `Date.prototype.getTimezoneOffset`. A flag `--lang` e `set_accept_languages()` configuram cabeçalhos de idioma. A opção `webrtc_leak_protection` previne que o WebRTC exponha o IP real por trás de um proxy.\n\nO princípio geral é que o Pydoll fornece o fingerprint autêntico do navegador como linha de base, e o desenvolvedor só precisa garantir que as camadas configuráveis (User-Agent, timezone, idioma, geolocalização) sejam consistentes entre si e com as características do proxy.\n\n## Referências\n\n- Laperdrix, P., Rudametkin, W., & Baudry, B. (2016). Beauty and the Beast: Diverting Modern Web Browsers to Build Unique Browser Fingerprints. IEEE S&P.\n- Mowery, K., & Shacham, H. (2012). Pixel Perfect: Fingerprinting Canvas in HTML5. USENIX Security.\n- Eckersley, P. (2010). How Unique Is Your Web Browser? Privacy Enhancing Technologies Symposium.\n- W3C Client Hints Infrastructure: https://wicg.github.io/client-hints-infrastructure/\n- BrowserLeaks: https://browserleaks.com/\n- CreepJS: https://abrahamjuliot.github.io/creepjs/\n"
  },
  {
    "path": "docs/pt/deep-dive/fingerprinting/evasion-techniques.md",
    "content": "# Técnicas de Evasão\n\nEste documento cobre técnicas práticas para evadir detecção de fingerprinting usando o Pydoll. As seções anteriores descreveram como a detecção funciona em cada camada: [network fingerprinting](./network-fingerprinting.md) (TCP/IP, TLS, HTTP/2), [browser fingerprinting](./browser-fingerprinting.md) (Canvas, WebGL, propriedades do navigator) e [behavioral fingerprinting](./behavioral-fingerprinting.md) (mouse, teclado, scroll). Esta seção foca em contramedidas.\n\nO princípio central é consistência entre camadas. Passar em uma camada de detecção enquanto falha em outra ainda resulta em sinalização. Um IP residencial com um fingerprint TCP incompatível, ou um fingerprint de navegador perfeito com movimentos de mouse robóticos, será detectado por qualquer sistema que correlacione sinais.\n\n!!! info \"Navegação do Módulo\"\n    - [Network Fingerprinting](./network-fingerprinting.md): Identificação em nível de protocolo\n    - [Browser Fingerprinting](./browser-fingerprinting.md): Detecção na camada de aplicação\n    - [Behavioral Fingerprinting](./behavioral-fingerprinting.md): Análise de comportamento humano\n\n## O que o Pydoll Fornece por Padrão\n\nAntes de configurar qualquer coisa, é útil entender o que o Pydoll te dá gratuitamente ao usar uma instância real do Chrome via CDP.\n\n**Fingerprints de rede autênticos.** A pilha TCP/IP do Chrome, implementação TLS (BoringSSL) e pilha HTTP/2 produzem fingerprints genuínos. O TLS ClientHello, frame HTTP/2 SETTINGS, ordem de pseudo-cabeçalhos e prioridades de stream correspondem a um navegador Chrome real. Ferramentas que constroem requisições HTTP programaticamente (requests, httpx, curl) produzem fingerprints não-navegador nessas camadas. Com o Pydoll, eles são autênticos por padrão.\n\n**Fingerprints de navegador autênticos.** Fingerprints de Canvas, WebGL e AudioContext vêm de hardware real de GPU e áudio. Propriedades do navigator, plugins (os 5 plugins PDF padrão) e tipos MIME refletem estado genuíno do navegador. Não há nada para configurar aqui.\n\n**Sem `navigator.webdriver`.** Selenium, Playwright e Puppeteer definem `navigator.webdriver` como `true`. O Pydoll usa CDP diretamente, que não define esta flag. A propriedade é `undefined`, correspondendo a uma sessão normal de usuário.\n\n**Sequências de eventos completas.** Quando o Pydoll despacha eventos de entrada através do domínio Input do CDP, o Chrome gera a cadeia completa de eventos (pointermove, pointerdown, mousedown, pointerup, mouseup, click) exatamente como faria para entrada real do usuário.\n\n## Consistência de User-Agent\n\nA inconsistência de fingerprinting mais comum em automação é uma incompatibilidade entre o cabeçalho HTTP `User-Agent`, `navigator.userAgent` no JavaScript, `navigator.platform` e cabeçalhos Client Hints (`Sec-CH-UA`, `Sec-CH-UA-Platform`). Definir `--user-agent=` como flag do Chrome apenas muda o cabeçalho HTTP, deixando propriedades JavaScript e Client Hints inalterados.\n\nO Pydoll resolve isso automaticamente. Quando detecta `--user-agent=` nos argumentos do navegador, ele:\n\n1. Analisa a string UA para extrair nome do navegador, versão e SO.\n2. Chama `Emulation.setUserAgentOverride` via CDP com o `userAgent` completo, o valor correto de `platform` (ex: `Win32` para Windows) e `userAgentMetadata` completo (dados de Client Hints incluindo `Sec-CH-UA`, `Sec-CH-UA-Platform`, `Sec-CH-UA-Full-Version-List`).\n3. Injeta sobrescritas de `navigator.vendor` e `navigator.appVersion` via `Page.addScriptToEvaluateOnNewDocument`, garantindo consistência mesmo em abas recém-abertas.\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.add_argument(\n    '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) '\n    'AppleWebKit/537.36 (KHTML, like Gecko) '\n    'Chrome/120.0.6099.109 Safari/537.36'\n)\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n    # Todas as camadas agora são consistentes:\n    # - Cabeçalho HTTP User-Agent\n    # - navigator.userAgent / navigator.platform / navigator.appVersion\n    # - Sec-CH-UA / Sec-CH-UA-Platform / Sec-CH-UA-Full-Version-List\n    # - navigator.userAgentData.brands / .platform\n    await tab.go_to('https://example.com')\n```\n\nEssa sobrescrita é aplicada automaticamente à aba inicial, novas abas de `browser.new_tab()`, e quaisquer abas descobertas via `browser.get_opened_tabs()`.\n\n!!! note \"Plataformas Suportadas\"\n    O parser de UA lida com Chrome, Edge, Windows (NT 6.1 até 10.0), macOS, Linux, Android, iOS e Chrome OS. Ele gera valores de marca GREASE adequados seguindo a especificação do Chromium.\n\n## Consistência de Timezone e Locale\n\nAo usar um proxy, o timezone e idioma do navegador devem corresponder à localização geográfica do IP do proxy. Um IP geolocalizado em Tóquio com timezone `America/New_York` e `Accept-Language: en-US` é uma inconsistência detectável.\n\n### Configuração de Idioma\n\nO idioma é configurado através de flags do Chrome e da API de opções do Pydoll:\n\n```python\noptions = ChromiumOptions()\noptions.add_argument('--lang=ja-JP')\noptions.set_accept_languages('ja-JP,ja;q=0.9,en;q=0.8')\n```\n\nIsso define tanto o cabeçalho HTTP `Accept-Language` quanto `navigator.language` / `navigator.languages`.\n\n### Sobrescrita de Timezone\n\nO Pydoll atualmente não encapsula o comando `Emulation.setTimezoneOverride` do CDP, então a sobrescrita de timezone requer injeção de JavaScript. As APIs críticas para sobrescrever são `Intl.DateTimeFormat().resolvedOptions().timeZone` e `Date.prototype.getTimezoneOffset()`:\n\n```python\nasync def set_timezone(tab, timezone_id: str, offset_minutes: int):\n    \"\"\"\n    Sobrescreve timezone via JavaScript.\n\n    Args:\n        timezone_id: Nome de timezone IANA (ex: 'Asia/Tokyo')\n        offset_minutes: Offset UTC em minutos (ex: -540 para JST)\n    \"\"\"\n    script = f'''\n        const _origDTF = Intl.DateTimeFormat;\n        Intl.DateTimeFormat = function(...args) {{\n            const opts = args[1] || {{}};\n            opts.timeZone = '{timezone_id}';\n            return new _origDTF(args[0], opts);\n        }};\n        Object.defineProperty(Intl.DateTimeFormat, 'prototype', {{\n            value: _origDTF.prototype\n        }});\n        Date.prototype.getTimezoneOffset = function() {{ return {offset_minutes}; }};\n    '''\n    await tab.execute_script(script)\n```\n\n!!! warning \"`execute_script` vs `addScriptToEvaluateOnNewDocument`\"\n    `tab.execute_script()` executa JavaScript no contexto da página atual. Se a página navegar, a sobrescrita é perdida. Para sobrescritas que devem persistir entre navegações, use `Page.addScriptToEvaluateOnNewDocument` do CDP, que injeta o script antes de qualquer JavaScript da página executar em cada novo carregamento de documento. O Pydoll usa isso internamente para sobrescritas de User-Agent. Para timezone, você pode enviar o comando CDP diretamente:\n\n    ```python\n    await tab._connection_handler.execute_command(\n        'Page.addScriptToEvaluateOnNewDocument',\n        {'source': script}\n    )\n    ```\n\n### Sobrescrita de Geolocalização\n\nPara sites que solicitam permissão de geolocalização, a API de Geolocation pode ser sobrescrita via JavaScript:\n\n```python\nasync def set_geolocation(tab, latitude: float, longitude: float):\n    script = f'''\n        navigator.geolocation.getCurrentPosition = function(success) {{\n            success({{\n                coords: {{\n                    latitude: {latitude}, longitude: {longitude},\n                    accuracy: 1, altitude: null, altitudeAccuracy: null,\n                    heading: null, speed: null\n                }},\n                timestamp: Date.now()\n            }});\n        }};\n        navigator.geolocation.watchPosition = function(success) {{\n            return navigator.geolocation.getCurrentPosition(success);\n        }};\n    '''\n    await tab.execute_script(script)\n```\n\n## Proteção contra Vazamento WebRTC\n\nO WebRTC pode expor o endereço IP real do cliente mesmo ao usar um proxy, através de requisições a servidores STUN/TURN que ignoram o túnel do proxy. O Pydoll fornece uma opção integrada para prevenir isso:\n\n```python\noptions = ChromiumOptions()\noptions.webrtc_leak_protection = True\n# Adiciona: --force-webrtc-ip-handling-policy=disable_non_proxied_udp\n```\n\nIsso força o Chrome a rotear todo o tráfego WebRTC através do proxy, prevenindo vazamento de IP. Deve ser habilitado sempre que usar um proxy para automação stealth.\n\n## Humanização Comportamental\n\nO Pydoll implementa interações humanizadas para mouse, teclado e scroll através do parâmetro `humanize=True`. Estes não são recursos futuros ou soluções manuais; estão integrados ao framework.\n\n### Mouse\n\n```python\n# Clique humanizado: caminho com curva Bezier, tempo pela Lei de Fitts,\n# velocidade de jerk mínimo, tremor, overshoot + correção\nawait element.click(humanize=True)\n```\n\nQuando `humanize=True` é passado para o `click()` de um WebElement, o Pydoll gera um movimento completo do mouse da posição atual do cursor até o elemento usando uma curva Bezier cúbica com pontos de controle aleatorizados. A velocidade segue um perfil de jerk mínimo. Tremor fisiológico, overshoot (70% de probabilidade) e micro-pausas são adicionados. A duração do movimento é calculada pela Lei de Fitts baseada na distância e tamanho do alvo. Veja [Behavioral Fingerprinting](./behavioral-fingerprinting.md#humanização-de-mouse-do-pydoll) para descrições detalhadas dos parâmetros.\n\n### Teclado\n\n```python\n# Digitação humanizada: atrasos variáveis, erros realistas (~2%),\n# pausas de pontuação, pausas de pensamento, pausas de distração\nawait element.type_text(\"Hello, world!\", humanize=True)\n```\n\nA digitação humanizada usa atrasos inter-tecla variáveis (distribuição uniforme de 30-120ms), pausas de pontuação, pausas de pensamento (2% de probabilidade), pausas de distração (0.5% de probabilidade) e erros de digitação realistas com cinco tipos de erro distintos e sequências de correção naturais. Veja [Behavioral Fingerprinting](./behavioral-fingerprinting.md#humanização-de-teclado-do-pydoll) para o detalhamento completo dos parâmetros.\n\n### Scroll\n\n```python\nfrom pydoll.interactions.scroll import Scroll, ScrollPosition\n\nscroll = Scroll(connection_handler)\n# Scroll humanizado: easing Bezier, jitter, micro-pausas, overshoot\nawait scroll.by(ScrollPosition.Y, 800, humanize=True)\n```\n\nO scroll humanizado usa curvas de easing Bezier, jitter por frame (±3px), micro-pausas (5% de probabilidade) e correção de overshoot (15% de probabilidade). Grandes distâncias são divididas em múltiplos gestos de \"flick\". Veja [Behavioral Fingerprinting](./behavioral-fingerprinting.md#humanização-de-scroll-do-pydoll) para detalhes.\n\n## Interceptação de Requisições\n\nO Pydoll suporta interceptação de requisições via domínio Fetch do CDP, permitindo modificar cabeçalhos, bloquear requisições ou fornecer respostas personalizadas antes que cheguem ao servidor:\n\n```python\nfrom pydoll.protocol.fetch.events import FetchEvent\n\nasync def handle_request(event):\n    request_id = event['params']['requestId']\n    request = event['params']['request']\n    headers = request.get('headers', {})\n\n    # Exemplo: garantir que suporte a Brotli é anunciado\n    if 'Accept-Encoding' in headers and 'br' not in headers['Accept-Encoding']:\n        headers['Accept-Encoding'] = 'gzip, deflate, br, zstd'\n\n    header_list = [{'name': k, 'value': v} for k, v in headers.items()]\n    await tab.continue_request(request_id=request_id, headers=header_list)\n\nawait tab.enable_fetch_events()\nawait tab.on(FetchEvent.REQUEST_PAUSED, handle_request)\n```\n\nNa prática, modificação de cabeçalhos é raramente necessária com o Pydoll porque o Chrome gera cabeçalhos corretos nativamente. A interceptação de requisições é mais útil para bloquear scripts de rastreamento, modificar conteúdo de resposta ou depuração.\n\n## Preferências do Navegador para Realismo\n\nO Chrome armazena preferências do usuário que sistemas de fingerprinting podem inspecionar. Um perfil de navegador novo sem histórico, sem preferências salvas e tudo padrão parece diferente de um perfil que foi usado por semanas. A opção `browser_preferences` do Pydoll permite pré-popular estas:\n\n```python\nimport time\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    'profile': {\n        'created_by_version': '120.0.6099.130',\n        'creation_time': str(time.time() - 90 * 86400),  # 90 dias atrás\n        'exit_type': 'Normal',\n    },\n    'profile.default_content_setting_values': {\n        'cookies': 1,\n        'images': 1,\n        'javascript': 1,\n        'notifications': 2,  # \"Perguntar\" (padrão realista)\n    },\n}\n```\n\n## Erros Comuns\n\n### Randomizar Tudo\n\nGerar um fingerprint aleatório do zero (hardwareConcurrency aleatório, deviceMemory aleatório, tamanho de tela aleatório) cria combinações impossíveis. Dispositivos reais têm configurações restritas: uma máquina de 4 núcleos com 8 GB de RAM, tela 1920x1080 e Windows 10 é um perfil plausível. Uma máquina de 17 núcleos com 0.5 GB de RAM, tela 3840x2160 e `navigator.platform: Linux armv7l` não é. Use perfis capturados de navegadores reais em vez de geração aleatória.\n\n### Injeção de Ruído no Canvas\n\nAdicionar ruído aleatório à saída do canvas para prevenir fingerprinting é contraproducente. Sistemas de detecção solicitam o fingerprint múltiplas vezes. Se o hash muda entre requisições, injeção de ruído é detectada, o que é em si um sinal forte de automação. Com o Pydoll, o fingerprint de canvas é autêntico e consistente. Deixe-o como está.\n\n### User-Agents Desatualizados\n\nUsar um User-Agent de uma versão de navegador com 6+ meses é detectável porque a versão carece de recursos e valores de Client Hints que a versão atual teria. Mantenha strings de User-Agent atuais dentro das últimas 2-3 versões principais do Chrome.\n\n### Ignorar Comportamento em Nível de Sessão\n\nMesmo com fingerprints perfeitos e interações humanizadas, o comportamento em nível de sessão importa. Carregar 100 páginas em 60 segundos, nunca scrollar, clicar apenas em botões (nunca links) e manter foco constante por horas sem uma única troca de aba ou período ocioso são todas anomalias comportamentais. Adicione atrasos de leitura entre navegações, varie o ritmo de workflows de múltiplas páginas e inclua períodos naturais de inatividade.\n\n## Verificação\n\nAntes de implantar automação em escala, verifique seu fingerprint usando estas ferramentas:\n\n| Ferramenta | URL | Testes |\n|------|-----|-------|\n| BrowserLeaks | https://browserleaks.com/ | Canvas, WebGL, fontes, IP, WebRTC, HTTP/2 |\n| CreepJS | https://abrahamjuliot.github.io/creepjs/ | Detecção de mentiras, verificações de consistência |\n| Fingerprint.com | https://fingerprint.com/demo/ | Identificação de nível comercial |\n| PixelScan | https://pixelscan.net/ | Análise de detecção de bots |\n| IPLeak | https://ipleak.net/ | WebRTC, DNS, vazamentos de IP |\n\nUm script básico de verificação com o Pydoll:\n\n```python\nasync def verify_fingerprint(tab):\n    result = await tab.execute_script('''\n        return {\n            userAgent: navigator.userAgent,\n            platform: navigator.platform,\n            webdriver: navigator.webdriver,\n            languages: navigator.languages,\n            plugins: navigator.plugins.length,\n            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n            colorDepth: screen.colorDepth,\n            deviceMemory: navigator.deviceMemory,\n            hardwareConcurrency: navigator.hardwareConcurrency,\n        };\n    ''')\n    fp = result['result']['result']['value']\n\n    # Verificar problemas óbvios\n    assert fp['webdriver'] is None, 'navigator.webdriver deveria ser undefined'\n    assert fp['plugins'] == 5, f'Esperados 5 plugins, obtidos {fp[\"plugins\"]}'\n    assert 'HeadlessChrome' not in fp['userAgent'], 'Headless detectado no UA'\n```\n\n## Referências\n\n- Chrome DevTools Protocol, Emulation Domain: https://chromedevtools.github.io/devtools-protocol/tot/Emulation/\n- Chrome DevTools Protocol, Fetch Domain: https://chromedevtools.github.io/devtools-protocol/tot/Fetch/\n- Chromium Source, Inspector Emulation Agent: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc\n"
  },
  {
    "path": "docs/pt/deep-dive/fingerprinting/index.md",
    "content": "# Análise Profunda de Fingerprinting de Navegador e Rede\n\nEste módulo cobre fingerprinting de navegador e rede, um aspecto crítico dos sistemas modernos de automação web e detecção.\n\nO fingerprinting situa-se na interseção de protocolos de rede, criptografia, componentes internos do navegador e análise comportamental. Ele engloba as técnicas usadas para identificar e rastrear dispositivos, navegadores e usuários através de sessões sem depender de identificadores tradicionais como cookies ou endereços IP.\n\n## Por que Isso Importa\n\nCada conexão de navegador a um site expõe múltiplas características, desde a ordem precisa das opções TCP em pacotes de rede, até a renderização de canvas específica da GPU, e padrões de tempo de execução de JavaScript. Individualmente, essas características podem parecer inócuas. Combinadas, elas criam um fingerprint (impressão digital) que pode identificar unicamente um dispositivo ou instância de navegador.\n\nPara engenheiros de automação, desenvolvedores de bots e usuários conscientes da privacidade, entender o fingerprinting é essencial para construir sistemas eficazes de evasão de detecção e para compreender como os mecanismos de rastreamento operam em um nível técnico.\n\n!!! danger \"Sistemas de Detecção Multi-Camada\"\n    Sistemas anti-bot modernos empregam análise abrangente em múltiplas camadas:\n    \n    - **Nível de Rede**: Comportamento da pilha TCP/IP, padrões de handshake TLS, configurações HTTP/2\n    - **Nível de Navegador**: Renderização de Canvas, strings de fornecedor WebGL, enumeração de propriedades JavaScript\n    - **Comportamental**: Entropia de movimento do mouse, tempo de digitação, padrões de rolagem\n    \n    Uma única inconsistência (como um User-Agent do Chrome com um fingerprint TLS do Firefox) pode disparar um bloqueio imediato.\n\n## Escopo e Metodologia do Módulo\n\nTécnicas de fingerprinting estão documentadas em múltiplas fontes com níveis variados de acessibilidade e confiabilidade:\n\n- Artigos acadêmicos (frequentemente com acesso pago e teóricos)\n- Código-fonte de navegadores (milhões de linhas para analisar)\n- Blogs de pesquisadores de segurança (técnicos, mas fragmentados)\n- Whitepapers de fornecedores anti-bot (focados em marketing, detalhes omitidos)\n- Fóruns underground (práticos, mas não confiáveis)\n\nEste módulo centraliza, valida e organiza esse conhecimento em um guia técnico coeso. Cada técnica descrita aqui foi:\n\n- **Verificada** contra código-fonte de navegadores e RFCs\n- **Testada** em cenários reais de automação\n- **Citada** com referências de autoridade\n- **Explicada** desde os primeiros princípios até a implementação\n\n## Estrutura do Módulo\n\nEste módulo é organizado em três camadas progressivas, desde fundamentos de rede até técnicas práticas de evasão:\n\n### 1. Fingerprinting em Nível de Rede\n**[Network Fingerprinting (Fingerprinting de Rede)](./network-fingerprinting.md)**\n\nCobre a identificação de dispositivos através do comportamento de rede nas camadas de transporte e sessão, antes que a renderização do navegador comece.\n\n- **Fingerprinting de TCP/IP**: TTL, tamanho da janela, ordenação de opções\n- **Fingerprinting de TLS**: JA3/JA4, suítes de cifras, negociação ALPN\n- **Fingerprinting de HTTP/2**: Frames SETTINGS, padrões de prioridade\n- **Ferramentas e técnicas**: p0f, Nmap, Scapy, análise tshark\n\n**Significância técnica**: Fingerprints de rede são os mais desafiadores de falsificar (spoof) porque exigem modificações em nível de SO. Inconsistências nesta camada são detectadas antes que a execução de JavaScript comece.\n\n### 2. Fingerprinting em Nível de Navegador\n**[Browser Fingerprinting (Fingerprinting de Navegador)](./browser-fingerprinting.md)**\n\nExamina a identificação do navegador através de APIs JavaScript, motores de renderização e ecossistemas de plugins na camada de aplicação.\n\n- **Fingerprinting de Canvas e WebGL**: Artefatos de renderização específicos da GPU\n- **Fingerprinting de Áudio**: Diferenças sutis na saída da API de áudio\n- **Enumeração de Fontes**: Fontes instaladas revelam SO e localidade\n- **Propriedades JavaScript**: Objeto Navigator, dimensões da tela, fuso horário\n- **Análise de Cabeçalhos**: Consistência de Accept-Language, User-Agent\n\n**Significância técnica**: Esta camada é responsável pela maioria dos eventos de detecção. Mesmo com fingerprints de nível de rede corretos, propriedades de automação expostas (ex: `navigator.webdriver`) podem disparar o bloqueio.\n\n### 3. Fingerprinting Comportamental\n**[Behavioral Fingerprinting (Fingerprinting Comportamental)](./behavioral-fingerprinting.md)**\n\nAnalisa padrões de interação do usuário para distinguir comportamento humano de sistemas automatizados.\n\n- **Análise de movimento do mouse**: Curvatura da trajetória, perfis de velocidade, conformidade com a Lei de Fitts\n- **Dinâmica de teclado**: Ritmo de digitação, tempo de permanência (dwell time), tempo de voo (flight time), padrões de bigramas\n- **Padrões de rolagem**: Momentum, inércia, curvas de desaceleração\n- **Sequências de eventos**: Ordem natural de interação (mousemove → click), análise de tempo\n- **Machine learning**: Modelos de ML treinados em bilhões de sinais comportamentais\n\n**Significância técnica**: A análise comportamental pode detectar automação mesmo quando os fingerprints de rede e navegador estão corretamente falsificados. Esta camada é particularmente desafiadora porque requer a replicação de padrões de comportamento biomecânico humano.\n\n### 4. Técnicas de Evasão\n**[Evasion Techniques (Técnicas de Evasão)](./evasion-techniques.md)**\n\nImplementação prática de evasão de fingerprinting usando a integração CDP do Pydoll, sobrescritas de JavaScript e recursos arquitetônicos.\n\n- **Falsificação (Spoofing) baseada em CDP**: Fuso horário, geolocalização, métricas do dispositivo\n- **Sobrescrita de propriedades JavaScript**: Redefinindo objetos navigator, envenenamento de canvas (canvas poisoning)\n- **Interceptação de requisições**: Forçando consistência de cabeçalhos\n- **Imitação comportamental**: Tempo semelhante ao humano, injeção de entropia\n- **Testes de detecção**: Ferramentas para validar sua configuração de evasão\n\n**Significância técnica**: Esta seção demonstra a aplicação prática de conceitos de fingerprinting em cenários reais de automação, integrando técnicas de todas as camadas anteriores.\n\n## Quem Deve Ler Isto\n\n### **Você DEVE ler isto se você está:**\n- Construindo automação que interage com sites protegidos por anti-bots\n- Desenvolvendo infraestrutura de scraping em escala\n- Implementando automação de navegador que preserva a privacidade\n- Pesquisando detecção de bots para fins ofensivos ou defensivos\n\n### **Isto é material avançado se você:**\n- É novo em protocolos de rede (comece com [Fundamentos de Rede](../network/network-fundamentals.md))\n- Não está familiarizado com CDP (leia [Chrome DevTools Protocol](../fundamentals/cdp.md) primeiro)\n- Está apenas aprendendo tipagem em Python (veja [Sistema de Tipos](../fundamentals/typing-system.md))\n\n### **Isto NÃO é:**\n- Uma \"bala de prata\" como solução anti-detecção (tal coisa não existe)\n- Aconselhamento jurídico sobre web scraping (consulte [Legal e Ético](../network/proxy-legal.md))\n- Um substituto para respeitar o robots.txt e limites de taxa (rate limits)\n\n## A Filosofia Técnica\n\nA defesa contra fingerprinting **não é sobre se tornar invisível** — é sobre se tornar **indistinguível do tráfego legítimo**. Isso significa:\n\n1.  **Consistência acima da perfeição**: Um fingerprint de Firefox perfeitamente configurado é melhor que um fingerprint \"perfeito\" mas inconsistente do Chrome\n2.  **Abordagem holística**: Você deve alinhar as camadas de rede, navegador e comportamental\n3.  **Adaptação contínua**: Técnicas de fingerprinting evoluem mensalmente; este é um documento vivo\n\n!!! tip \"A Regra de Ouro\"\n    **Cada camada deve contar a mesma história.** Se seu fingerprint TLS diz \"Chrome 120\", suas configurações HTTP/2 devem corresponder ao Chrome 120, seu User-Agent deve dizer Chrome 120, e sua renderização de canvas deve produzir artefatos do Chrome 120. Um desencontro = detecção.\n\n## Considerações Éticas\n\nO conhecimento sobre fingerprinting é **tecnologia de uso dual**:\n\n- **Defensivo**: Proteger sua privacidade de rastreamento invasivo\n- **Ofensivo**: Evadir sistemas de detecção para automação\n\nConfiamos que você usará este conhecimento de forma **responsável e ética**:\n\n**Práticas recomendadas:**\n- Respeitar os termos de serviço dos sites\n- Implementar limitação de taxa (rate limiting) e padrões de rastreamento respeitosos\n- Avaliar se a automação é necessária\n- Ser transparente quando apropriado\n\n**Usos proibidos:**\n- Fraude, abuso de contas ou atividades ilegais\n- Sobrecarregar servidores com scraping agressivo\n- Usar este conhecimento como arma sem entender as consequências\n\n## Pronto para Mergulhar Fundo?\n\nFingerprinting é um domínio complexo e técnico que requer estudo sistemático. Entender essas técnicas é essencial para automação web eficaz em ambientes com sistemas de detecção.\n\nComece com **[Network Fingerprinting (Fingerprinting de Rede)](./network-fingerprinting.md)** para estabelecer conhecimento fundamental, continue com **[Browser Fingerprinting (Fingerprinting de Navegador)](./browser-fingerprinting.md)** para entendimento da camada de aplicação, e conclua com **[Evasion Techniques (Técnicas de Evasão)](./evasion-techniques.md)** para implementação prática.\n\n---\n\n!!! info \"Status da Documentação\"\n    Este módulo representa **pesquisa extensiva** combinando artigos acadêmicos, código-fonte de navegadores, testes do mundo real e conhecimento da comunidade. Cada alegação é citada e validada. Se você encontrar imprecisões ou tiver atualizações, contribuições são bem-vindas.\n\n## Leitura Adicional\n\nAntes de mergulhar, considere estes tópicos complementares:\n\n- **[Arquitetura de Proxy](../network/http-proxies.md)**: Fundamentos de anonimato em nível de rede\n- **[Preferências do Navegador](../../features/configuration/browser-preferences.md)**: Configuração prática de fingerprint\n- **[Contorno de Captcha Comportamental](../../features/advanced/behavioral-captcha-bypass.md)**: Análise e evasão comportamental"
  },
  {
    "path": "docs/pt/deep-dive/fingerprinting/network-fingerprinting.md",
    "content": "# Network Fingerprinting\n\nO network fingerprinting identifica clientes analisando características da pilha TCP/IP, handshake TLS e conexão HTTP/2. Esses sinais são definidos pelo kernel do sistema operacional e pela biblioteca TLS, não pelo ambiente JavaScript do navegador, o que os torna mais difíceis de falsificar que fingerprints de nível de navegador. Um proxy ou VPN muda seu endereço IP mas não altera seu tamanho de janela TCP, sua lista de cipher suites TLS ou seu frame HTTP/2 SETTINGS. Sistemas de detecção exploram essa lacuna.\n\n!!! info \"Navegação do Módulo\"\n    - [Browser Fingerprinting](./browser-fingerprinting.md): Canvas, WebGL, AudioContext\n    - [Técnicas de Evasão](./evasion-techniques.md): Contramedidas multi-camada\n\n    Para fundamentos de protocolo, veja [Fundamentos de Rede](../network/network-fundamentals.md). Para contexto de detecção de proxy, veja [Detecção de Proxy](../network/proxy-detection.md).\n\n## TCP/IP Fingerprinting\n\nCada sistema operacional implementa a pilha TCP/IP de forma diferente. O pacote SYN que inicia uma conexão TCP carrega informação suficiente para identificar o SO com alta confiança: o TTL inicial, o tamanho da janela TCP, o Maximum Segment Size e a ordem e seleção de opções TCP. Nenhum desses valores é controlado pelo navegador. Eles vêm do kernel.\n\n### TTL (Time To Live)\n\nO TTL inicial é um dos identificadores de SO mais simples. Linux e macOS definem como 64, Windows define como 128, e dispositivos de rede (roteadores, firewalls) tipicamente usam 255. Cada salto de roteador decrementa o TTL em um, então um pacote chegando com TTL 118 provavelmente começou em 128 (Windows) e cruzou 10 saltos.\n\nO valor de fingerprinting do TTL vem da referência cruzada com o User-Agent. Se o navegador alega ser Chrome no Windows mas o pacote chega com TTL próximo de 64, a conexão está ou sendo proxy através de um servidor Linux ou o User-Agent está falsificado. Sistemas de detecção arredondam o TTL observado para cima até o valor inicial conhecido mais próximo (64, 128, 255) e comparam contra o SO declarado.\n\nQuando o tráfego flui através de um proxy, o TTL reinicia porque o kernel do proxy gera uma nova conexão TCP para o destino. O destino vê o TTL do proxy, não o seu. É por isso que incompatibilidades de TTL são um sinal de detecção de proxy: o User-Agent diz Windows (TTL 128) mas o fingerprint TCP mostra Linux (TTL 64).\n\n### Tamanho da Janela TCP e Escalonamento\n\nO tamanho inicial da janela TCP no pacote SYN varia por SO e versão do kernel. Kernels Linux modernos (3.x e posteriores) tipicamente enviam uma janela inicial de 29200 bytes, que é `20 * MSS` onde MSS é 1460 para Ethernet padrão. Alguns kernels mais novos (5.x, 6.x) podem usar 64240 dependendo da configuração e ajustes de `initcwnd`. Windows 10 e 11 tipicamente enviam 65535 com escalonamento de janela habilitado, embora o valor exato dependa da configuração de auto-tuning e nível de patch. macOS também usa 65535 como padrão.\n\nO fator de escala de janela (uma opção TCP) multiplica o campo de tamanho de janela de 16 bits para suportar janelas de recebimento maiores. Linux comumente usa fator de escala 7 (permitindo janelas de até 8MB), enquanto Windows frequentemente usa 8. Combinado com o tamanho base da janela, o fator de escala cria um fingerprint mais granular do que qualquer valor isolado.\n\n### Ordem de Opções TCP\n\nA seleção e ordenação de opções TCP no pacote SYN é altamente distintiva. Cada SO organiza as opções em uma ordem fixa e específica por versão que o kernel não expõe como parâmetro configurável. Linux envia `MSS, SACK_PERM, TIMESTAMP, NOP, WSCALE`. Windows envia `MSS, NOP, WSCALE, NOP, NOP, SACK_PERM` e notavelmente omite a opção TIMESTAMP nas configurações padrão. macOS envia `MSS, NOP, WSCALE, NOP, NOP, TIMESTAMP, SACK_PERM`.\n\nA presença ou ausência de opções específicas importa tanto quanto a ordem. Windows historicamente omitiu timestamps TCP, que Linux e macOS incluem por padrão. SACK (Selective Acknowledgment) é suportado por todos os sistemas modernos, mas sistemas mais antigos ou embarcados podem não anunciá-lo. A combinação de quais opções aparecem e em que ordem cria uma assinatura que ferramentas como p0f comparam contra um banco de dados de fingerprints de SO conhecidos.\n\n### p0f\n\n[p0f](https://lcamtuf.coredump.cx/p0f3/) é a ferramenta padrão para fingerprinting TCP/IP passivo. Ele observa tráfego sem gerar nenhum pacote, analisando pacotes SYN e SYN+ACK contra um banco de dados de assinaturas. Seu formato de assinatura codifica os campos chave de fingerprinting:\n\n```\nversion:ittl:olen:mss:wsize,scale:olayout:quirks:pclass\n```\n\nO `ittl` é o TTL inicial inferido, `mss` é o Maximum Segment Size, `wsize,scale` é o tamanho da janela (que pode ser absoluto, ou relativo ao MSS como `mss*20`), e `olayout` é o layout de opções TCP usando nomes abreviados (`mss`, `nop`, `ws`, `sok`, `sack`, `ts`, `eol+N`). O campo `quirks` captura comportamentos incomuns como a flag Don't Fragment (`df`) ou IP ID não-zero em pacotes DF (`id+`).\n\nUma assinatura típica de Linux 4.x+ no p0f se parece com `4:64:0:*:mss*20,7:mss,sok,ts,nop,ws:df,id+:0`. Uma assinatura de Windows 10 pode parecer `4:128:0:*:65535,8:mss,nop,ws,nop,nop,sok:df,id+:0`. Serviços anti-bot mantêm bancos de dados similares internamente, comparando conexões de entrada contra perfis de SO conhecidos e sinalizando incompatibilidades com o User-Agent declarado.\n\n## TLS Fingerprinting\n\nA mensagem TLS ClientHello é transmitida antes da criptografia ser estabelecida, então é visível para qualquer observador no caminho de rede. Ela contém a versão TLS, cipher suites suportadas, extensões TLS, curvas elípticas suportadas (named groups) e formatos de ponto EC. Cada navegador e biblioteca TLS produz uma combinação característica desses campos.\n\n### JA3\n\nJA3, desenvolvido na Salesforce por John Althouse, Jeff Atkinson e Josh Atkins, foi o primeiro método de fingerprinting TLS amplamente adotado. Ele concatena cinco campos do ClientHello (versão TLS, cipher suites, extensões, curvas elípticas, formatos de ponto EC), junta valores dentro de cada campo com hífens, separa os cinco campos com vírgulas e tira o hash MD5 da string resultante.\n\n```\nString JA3: 771,4865-4866-4867-49195-49199-49196-49200-52393-52392,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0\nHash JA3:   cd08e31494b9531f560d64c695473da9\n```\n\nUma sutileza: o campo \"versão TLS\" no JA3 usa `ClientHello.legacy_version`, não a extensão `supported_versions`. Como TLS 1.3 (RFC 8446) requer que clientes definam `legacy_version` como `0x0303` (TLS 1.2) para compatibilidade retroativa, o campo de versão JA3 é quase sempre `771` para clientes modernos, mesmo quando suportam TLS 1.3. A negociação real de TLS 1.3 acontece através da extensão 43 (`supported_versions`), mas o JA3 usa o campo do cabeçalho.\n\nO JA3 deve filtrar valores GREASE antes do hashing. GREASE (RFC 8701) é um mecanismo onde navegadores inserem valores reservados selecionados aleatoriamente em cipher suites, extensões e outros campos para prevenir ossificação de protocolo. Os valores GREASE válidos são `0x0a0a`, `0x1a1a`, `0x2a2a` e assim por diante até `0xfafa`. Cada valor tem dois bytes idênticos onde o nibble inferior de cada byte é `0x0a`. Um filtro GREASE correto verifica ambas as condições:\n\n```python\ndef is_grease(value: int) -> bool:\n    return (value & 0x0f0f) == 0x0a0a and (value >> 8) == (value & 0xff)\n```\n\n!!! warning \"Limitações do JA3 com Navegadores Modernos\"\n    Desde o Chrome 110 (janeiro 2023) e Firefox 114, navegadores randomizam a ordem das extensões TLS em cada conexão. Isso significa que o mesmo navegador produz hashes JA3 diferentes em cada conexão, tornando o JA3 efetivamente inútil para identificar navegadores modernos. O JA3 permanece útil para fingerprinting de clientes não-navegador (Python `requests`, `curl`, bots personalizados) que não implementam randomização de extensões.\n\n### JA4\n\nJA4 é o sucessor do JA3, desenvolvido pelo mesmo autor principal (John Althouse) na FoxIO. Foi projetado especificamente para sobreviver à randomização de extensões TLS ordenando extensões e cipher suites antes do hashing. O formato consiste em três seções separadas por underscores: `a_b_c`.\n\nSeção `a` é uma string legível de metadados: o protocolo (`t` para TCP, `q` para QUIC), a versão TLS (`12` ou `13`), se SNI está presente (`d` para domínio, `i` para IP), o número de cipher suites (dois dígitos), o número de extensões (dois dígitos) e o primeiro e último valor ALPN (`h2` para HTTP/2, `00` se nenhum). Por exemplo, `t13d1516h2` significa TCP TLS 1.3 com SNI, 15 cipher suites, 16 extensões e HTTP/2 ALPN.\n\nSeção `b` é um hash SHA-256 truncado das cipher suites ordenadas. Seção `c` é um hash SHA-256 truncado das extensões ordenadas concatenadas com os algoritmos de assinatura. Como ambas as listas são ordenadas antes do hashing, a randomização de extensões não afeta a saída.\n\nCloudflare, AWS e outras plataformas principais adotaram o JA4. A suíte completa JA4+ também inclui JA4S (fingerprinting de servidor), JA4H (fingerprinting de cliente HTTP), JA4X (fingerprinting de certificado X.509) e JA4SSH (fingerprinting SSH). A especificação e ferramentas estão disponíveis em [github.com/FoxIO-LLC/ja4](https://github.com/FoxIO-LLC/ja4).\n\n### JA3S (Fingerprinting de Servidor)\n\nJA3S aplica o mesmo conceito à mensagem ServerHello, mas o formato é mais simples porque o servidor seleciona uma única cipher suite em vez de oferecer uma lista. A string JA3S é `version,cipher,extensions` e seu hash MD5 identifica a implementação TLS do servidor. Parear JA3 (ou JA4) com JA3S cria um fingerprint bidirecional: um cliente específico conversando com um servidor específico produz um par JA3+JA3S previsível, que é mais distintivo do que qualquer fingerprint isolado.\n\n### Como Proxies Interagem com Fingerprints TLS\n\nO tipo de proxy determina se o fingerprint TLS é preservado. Proxies SOCKS5 e túneis HTTP CONNECT retransmitem o stream TCP sem encerrar o TLS, então o servidor destino vê o fingerprint TLS original do cliente inalterado. Esta é a principal vantagem desses tipos de proxy para consistência de fingerprint.\n\nProxies MITM (que encerram o TLS e reestabelecem uma nova conexão para o destino) substituem o fingerprint TLS do cliente pelo seu próprio. O destino vê as cipher suites e extensões do software proxy, não as do navegador. Se o proxy usa uma biblioteca TLS padrão como OpenSSL ou BoringSSL com configurações padrão, o fingerprint não corresponderá a nenhum navegador conhecido, o que é em si um sinal de detecção.\n\nÉ por isso que a abordagem do Pydoll de usar `--proxy-server` (que cria um túnel CONNECT, preservando o fingerprint TLS do navegador) é preferível a configurações de proxy MITM externo para automação stealth.\n\n## HTTP/2 Fingerprinting\n\nConexões HTTP/2 expõem um conjunto separado de sinais de fingerprinting distintos do TLS. O primeiro frame enviado pelo cliente é um frame SETTINGS contendo parâmetros como `HEADER_TABLE_SIZE`, `ENABLE_PUSH`, `MAX_CONCURRENT_STREAMS`, `INITIAL_WINDOW_SIZE`, `MAX_FRAME_SIZE` e `MAX_HEADER_LIST_SIZE`. Cada navegador usa valores padrão diferentes e inclui subconjuntos diferentes desses parâmetros.\n\nAlém do SETTINGS, o tamanho do frame WINDOW_UPDATE, a prioridade/peso do stream inicial e a ordem dos pseudo-cabeçalhos HTTP/2 (`:method`, `:authority`, `:scheme`, `:path`) variam entre implementações. Chrome, Firefox e Safari cada um produz uma combinação distintiva desses valores.\n\nA Akamai publicou a pesquisa fundamental sobre fingerprinting HTTP/2 no Black Hat Europe 2017. Seu formato de fingerprint concatena os valores SETTINGS, tamanho do WINDOW_UPDATE, frames PRIORITY e ordem dos pseudo-cabeçalhos. A suíte JA4+ inclui `JA4H` para fingerprinting em nível HTTP, cobrindo ordem e valores de cabeçalhos.\n\nO fingerprinting HTTP/2 é particularmente eficaz contra ferramentas de automação porque muitos frameworks de bot e bibliotecas HTTP implementam suas próprias pilhas HTTP/2 com parâmetros padrão que não correspondem a nenhum navegador real. Mesmo quando uma ferramenta falsifica corretamente o fingerprint TLS (usando curl-impersonate ou similar), seu frame HTTP/2 SETTINGS pode traí-la.\n\nVocê pode verificar seu fingerprint HTTP/2 em [browserleaks.com/http2](https://browserleaks.com/http2). Como o Pydoll controla uma instância real do Chrome via CDP, o fingerprint HTTP/2 é sempre autêntico, o que é uma vantagem inerente sobre ferramentas que constroem requisições HTTP programaticamente.\n\n## Implicações para Automação de Navegador\n\nA conclusão prática para automação com o Pydoll é que o network fingerprinting é uma área onde controlar um navegador real fornece uma vantagem significativa. A pilha TCP/IP do Chrome, implementação TLS (BoringSSL) e pilha HTTP/2 produzem fingerprints autênticos por padrão. O principal risco é incompatibilidade ambiental: executar o Chrome em um servidor Linux enquanto o User-Agent alega Windows cria uma inconsistência de fingerprint TCP/IP (TTL 64 ao invés de 128, ordem de opções TCP do Linux ao invés do Windows).\n\nPara configurações baseadas em proxy, o fluxo de fingerprint é: a pilha TCP/IP da sua máquina gera a conexão para o proxy (que o operador do proxy pode ver mas o destino não), e a pilha TCP/IP do proxy gera a conexão para o destino. O destino vê o TTL e opções TCP do servidor proxy. Se o proxy roda Linux (como a maioria faz), o fingerprint TCP indicará Linux independentemente do User-Agent. Este é um sinal de detecção bem conhecido que proxies residenciais mitigam parcialmente (o endpoint do proxy é a máquina de um usuário real, então seu fingerprint TCP é plausível) mas proxies de datacenter não podem.\n\nOs fingerprints TLS e HTTP/2, por outro lado, passam por túneis SOCKS5 e CONNECT sem modificação. Estes são os fingerprints do navegador, não do proxy. Então com o Pydoll através de um túnel CONNECT, o destino vê fingerprints TLS e HTTP/2 autênticos do Chrome pareados com o fingerprint TCP/IP do proxy. Esta combinação é consistente com um usuário real navegando através de uma VPN ou proxy corporativo, que é um padrão comum e legítimo.\n\n## Referências\n\n- Salesforce Engineering: TLS Fingerprinting with JA3 and JA3S - https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/\n- FoxIO JA4+ Network Fingerprinting - https://github.com/FoxIO-LLC/ja4\n- Cloudflare: JA4 Signals - https://blog.cloudflare.com/ja4-signals/\n- Akamai: Passive Fingerprinting of HTTP/2 Clients (Black Hat EU 2017) - https://blackhat.com/docs/eu-17/materials/eu-17-Shuster-Passive-Fingerprinting-Of-HTTP2-Clients-wp.pdf\n- p0f v3: Passive OS Fingerprinting - https://lcamtuf.coredump.cx/p0f3/\n- RFC 8446: TLS 1.3 - https://datatracker.ietf.org/doc/html/rfc8446\n- RFC 8701: GREASE for TLS - https://datatracker.ietf.org/doc/html/rfc8701\n- RFC 6528: Defending against Sequence Number Attacks - https://datatracker.ietf.org/doc/html/rfc6528\n- BrowserLeaks HTTP/2 Fingerprint - https://browserleaks.com/http2\n- Stamus Networks: JA3 Fingerprints Fade as Browsers Embrace Extension Randomization - https://www.stamus-networks.com/blog/ja3-fingerprints-fade-browsers-embrace-tls-extension-randomization\n"
  },
  {
    "path": "docs/pt/deep-dive/fundamentals/cdp.md",
    "content": "# Chrome DevTools Protocol (CDP)\n\nO Chrome DevTools Protocol (CDP) é a fundação que permite ao Pydoll controlar navegadores sem os webdrivers tradicionais. Entender como o CDP funciona fornece insights valiosos sobre as capacidades e a arquitetura interna do Pydoll.\n\n\n## O que é o CDP?\n\nO Chrome DevTools Protocol é uma interface poderosa desenvolvida pela equipe do Chromium que permite a interação programática com navegadores baseados no Chromium. É o mesmo protocolo usado pelo Chrome DevTools quando você inspeciona uma página web, mas exposto como uma API programável que pode ser aproveitada por ferramentas de automação.\n\nEm sua essência, o CDP fornece um conjunto abrangente de métodos e eventos para interagir com os componentes internos do navegador. Isso permite um controle refinado sobre todos os aspectos do navegador, desde navegar entre páginas até manipular o DOM, interceptar requisições de rede e monitorar métricas de desempenho.\n\n!!! info \"Evolução do CDP\"\n    O Chrome DevTools Protocol tem evoluído continuamente desde sua introdução. O Google mantém e atualiza o protocolo a cada lançamento do Chrome, adicionando regularmente novas funcionalidades e melhorando recursos existentes.\n    \n    Embora o protocolo tenha sido inicialmente projetado para o DevTools do Chrome, suas capacidades abrangentes o tornaram a fundação para ferramentas de automação de navegador de próxima geração como Puppeteer, Playwright e, claro, o Pydoll.\n\n## Comunicação via WebSocket\n\nUma das principais decisões arquitetônicas no CDP é o uso de WebSockets para comunicação. Quando um navegador baseado no Chromium é iniciado com a flag de depuração remota habilitada, ele abre um servidor WebSocket em uma porta especificada:\n\n```\nchrome --remote-debugging-port=9222\n```\n\nO Pydoll se conecta a este endpoint WebSocket para estabelecer um canal de comunicação bidirecional com o navegador. Esta conexão:\n\n1.  **Permanece persistente** durante toda a sessão de automação\n2.  **Habilita eventos em tempo real** do navegador para serem enviados (push) ao cliente\n3.  **Permite que comandos** sejam enviados ao navegador\n4.  **Suporta dados binários** para transferência eficiente de capturas de tela, PDFs e outros ativos\n\nO protocolo WebSocket é particularmente adequado para automação de navegador porque fornece:\n\n- **Comunicação de baixa latência** - Necessária para automação responsiva\n- **Mensagens bidirecionais** - Essencial para arquitetura orientada a eventos\n- **Conexões persistentes** - Eliminando a sobrecarga de configuração de conexão para cada operação\n\nAqui está uma visão simplificada de como funciona a comunicação do Pydoll com o navegador:\n\n```mermaid\nsequenceDiagram\n    participant App as Aplicação Pydoll\n    participant WS as Conexão WebSocket\n    participant Browser as Navegador Chrome\n\n    App ->> WS: Comando: navegar para URL\n    WS ->> Browser: Executar navegação\n\n    Browser -->> WS: Enviar evento de carregamento de página\n    WS -->> App: Receber evento de carregamento de página\n```\n\n!!! info \"WebSocket vs HTTP\"\n    Protocolos de automação de navegador anteriores frequentemente dependiam de endpoints HTTP para comunicação. A mudança do CDP para WebSockets representa uma melhoria arquitetônica significativa que permite automação mais responsiva e monitoramento de eventos em tempo real.\n    \n    Protocolos baseados em HTTP exigem \"polling\" (consultas periódicas) contínuo para detectar mudanças, criando sobrecarga e atrasos. WebSockets permitem que o navegador envie notificações (push) para seu script de automação exatamente quando os eventos ocorrem, com latência mínima.\n\n## Domínios Chave do CDP\n\nO CDP é organizado em domínios lógicos, cada um responsável por um aspecto específico da funcionalidade do navegador. Alguns dos domínios mais importantes incluem:\n\n\n| Domínio | Responsabilidade | Exemplos de Casos de Uso |\n|---|---|---|\n| **Browser** | Controle da própria aplicação do navegador | Gerenciamento de janelas, criação de contexto de navegador |\n| **Page** | Interação com o ciclo de vida da página | Navegação, execução de JavaScript, gerenciamento de frames |\n| **DOM** | Acesso à estrutura da página | Seletores de consulta, modificação de atributos, ouvintes de eventos |\n| **Network** | Monitoramento e controle de tráfego de rede | Interceptação de requisições, exame de respostas, cache |\n| **Runtime** | Ambiente de execução JavaScript | Avaliar expressões, chamar funções, lidar com exceções |\n| **Input** | Simulação de interações do usuário | Movimentos do mouse, entrada de teclado, eventos de toque |\n| **Target** | Gerenciamento de contextos e alvos do navegador | Criar abas, acessar iframes, lidar com popups |\n| **Fetch** | Interceptação de rede de baixo nível | Modificar requisições, simular respostas, autenticação |\n\nO Pydoll mapeia esses domínios CDP para uma estrutura de API mais intuitiva, preservando ao mesmo tempo todas as capacidades do protocolo subjacente.\n\n## Arquitetura Orientada a Eventos\n\nUma das funcionalidades mais poderosas do CDP é seu sistema de eventos. O protocolo permite que clientes se inscrevam em vários eventos que o navegador emite durante a operação normal. Esses eventos cobrem virtualmente todos os aspectos do comportamento do navegador:\n\n- **Eventos de ciclo de vida**: Carregamentos de página, navegação de frames, criação de alvos\n- **Eventos DOM**: Mudanças de elementos, modificações de atributos\n- **Eventos de rede**: Ciclos de requisição/resposta, mensagens WebSocket\n- **Eventos de execução**: Exceções JavaScript, mensagens do console\n- **Eventos de desempenho**: Métricas de renderização, script e mais\n\n\nQuando você habilita o monitoramento de eventos no Pydoll (ex: com `page.enable_network_events()`), a biblioteca configura as inscrições necessárias com o navegador e fornece \"ganchos\" (hooks) para seu código reagir a esses eventos.\n\n```python\nfrom pydoll.events.network import NetworkEvents\nfrom functools import partial\n\nasync def on_request(page, event):\n    url = event['params']['request']['url']\n    print(f\"Requisição para: {url}\")\n\n# Inscrever-se em eventos de requisição de rede\nawait page.enable_network_events()\nawait page.on(NetworkEvents.REQUEST_WILL_BE_SENT, partial(on_request, page))\n```\n\nEssa abordagem orientada a eventos permite que scripts de automação reajam imediatamente a mudanças de estado do navegador, sem depender de polling ineficiente ou atrasos arbitrários.\n\n## Vantagens de Desempenho da Integração Direta com CDP\n\nUsar o CDP diretamente, como o Pydoll faz, oferece várias vantagens de desempenho em relação à automação tradicional baseada em webdriver:\n\n### 1. Eliminação da Camada de Tradução de Protocolo\n\nFerramentas tradicionais baseadas em webdriver, como o Selenium, usam uma abordagem multicamada:\n\n```mermaid\ngraph LR\n    AS[Script de Automação] --> WC[Cliente WebDriver]\n    WC --> WS[Servidor WebDriver]\n    WS --> B[Navegador]\n```\n\nCada camada adiciona sobrecarga, especialmente o servidor WebDriver, que atua como uma camada de tradução entre o protocolo WebDriver e as APIs nativas do navegador.\n\nA abordagem do Pydoll simplifica isso para:\n\n```mermaid\ngraph LR\n    AS[Script de Automação] --> P[Pydoll]\n    P --> B[Navegador via CDP]\n```\n\nEssa comunicação direta elimina a sobrecarga computacional e de rede do servidor intermediário, resultando em operações mais rápidas.\n\n### 2. Agrupamento Eficiente de Comandos (Batching)\n\nO CDP permite o agrupamento de múltiplos comandos em uma única mensagem, reduzindo o número de viagens de ida e volta (round trips) necessárias para operações complexas. Isso é particularmente valioso para operações que exigem várias etapas, como encontrar um elemento e depois interagir com ele.\n\n### 3. Operação Assíncrona\n\nA arquitetura orientada a eventos e baseada em WebSocket do CDP alinha-se perfeitamente com o framework asyncio do Python, permitindo uma verdadeira operação assíncrona. Isso permite ao Pydoll:\n\n- Executar múltiplas operações concorrentemente\n- Processar eventos à medida que ocorrem\n- Evitar o bloqueio da thread principal durante operações de I/O\n\n```mermaid\ngraph TD\n    subgraph \"Arquitetura Assíncrona Pydoll\"\n        EL[Loop de Eventos]\n        \n        subgraph \"Tarefas Concorrentes\"\n            T1[Tarefa 1: Navegar]\n            T2[Tarefa 2: Esperar por Elemento]\n            T3[Tarefa 3: Lidar com Eventos de Rede]\n        end\n        \n        EL --> T1\n        EL --> T2\n        EL --> T3\n        \n        T1 --> WS[Conexão WebSocket]\n        T2 --> WS\n        T3 --> WS\n        \n        WS --> B[Navegador]\n    end\n```\n\n!!! info \"Ganhos de Desempenho Assíncrono\"\n    A combinação de asyncio e CDP cria um efeito multiplicador no desempenho. Em testes de benchmark, a abordagem assíncrona do Pydoll pode processar múltiplas páginas em paralelo com escalabilidade quase linear, enquanto ferramentas síncronas tradicionais veem retornos decrescentes à medida que a concorrência aumenta.\n    \n    Por exemplo, raspar 10 páginas que levam 2 segundos cada para carregar pode levar mais de 20 segundos com uma ferramenta síncrona, mas pouco mais de 2 segundos com a arquitetura assíncrona do Pydoll (mais uma sobrecarga mínima).\n\n### 4. Controle Refinado (Fine-Grained)\n\nO CDP fornece controle mais granular sobre o comportamento do navegador do que o protocolo WebDriver. Isso permite ao Pydoll implementar estratégias otimizadas para operações comuns:\n\n- Condições de espera mais precisas (vs. timeouts arbitrários)\n- Acesso direto a caches e armazenamento do navegador\n- Execução direcionada de JavaScript em contextos específicos\n- Controle detalhado da rede para otimização de requisições\n\n\n## Conclusão\n\nO Chrome DevTools Protocol forma a base da abordagem \"zero-webdriver\" do Pydoll para automação de navegador. Ao alavancar a comunicação WebSocket do CDP, a cobertura abrangente de domínios, a arquitetura orientada a eventos e a integração direta com o navegador, o Pydoll alcança desempenho e confiabilidade superiores em comparação com as ferramentas de automação tradicionais.\n\nNas seções seguintes, mergulharemos mais fundo em como o Pydoll implementa domínios CDP específicos e transforma o protocolo de baixo nível em uma API intuitiva e amigável ao desenvolvedor.\n"
  },
  {
    "path": "docs/pt/deep-dive/fundamentals/connection-layer.md",
    "content": "# Connection Handler (Gerenciador de Conexão)\n\nO Connection Handler é a camada fundamental da arquitetura do Pydoll, servindo como a ponte entre seu código Python e o Chrome DevTools Protocol (CDP) do navegador. Este componente gerencia a conexão WebSocket com o navegador, lida com a execução de comandos e processa eventos de maneira assíncrona e não bloqueante.\n\n```mermaid\ngraph TD\n    A[Código Python] --> B[Connection Handler]\n    B <--> C[WebSocket]\n    C <--> D[Endpoint CDP do Navegador]\n\n    subgraph \"Connection Handler\"\n        E[Gerenciador de Comandos]\n        F[Gerenciador de Eventos]\n        G[Cliente WebSocket]\n    end\n\n    B --> E\n    B --> F\n    B --> G\n```\n\n## Modelo de Programação Assíncrona\n\nO Pydoll é construído sobre o framework `asyncio` do Python, que permite operações de I/O (Entrada/Saída) não bloqueantes. Essa escolha de design é crítica para a automação de navegador de alto desempenho, pois permite que múltiplas operações ocorram concorrentemente sem esperar que cada uma seja concluída.\n\n### Entendendo Async/Await\n\n\nPara entender como async/await funciona na prática, vamos examinar um exemplo mais detalhado com duas operações concorrentes:\n\n```python\nimport asyncio\nfrom pydoll.browser.chrome import Chrome\n\nasync def fetch_page_data(url):\n    print(f\"Iniciando busca por {url}\")\n    browser = Chrome()\n    await browser.start()\n    page = await browser.get_page()\n    \n    # Navegação leva tempo - é aqui que cedemos o controle\n    await page.go_to(url)\n    \n    # Obter título da página\n    title = await page.execute_script(\"return document.title\")\n    \n    # Extrair alguns dados\n    description = await page.execute_script(\n        \"return document.querySelector('meta[name=\\\"description\\\"]')?.content || ''\"\n    )\n    \n    await browser.stop()\n    print(f\"Busca por {url} concluída\")\n    return {\"url\": url, \"title\": title, \"description\": description}\n\nasync def main():\n    # Iniciar duas operações de página concorrentemente\n    task1 = asyncio.create_task(fetch_page_data(\"https://example.com\"))\n    task2 = asyncio.create_task(fetch_page_data(\"https://github.com\"))\n    \n    # Esperar que ambas terminem e obter resultados\n    result1 = await task1\n    result2 = await task2\n    \n    return [result1, result2]\n\n# Rodar a função assíncrona\nresults = asyncio.run(main())\n```\n\nEste exemplo demonstra como podemos buscar dados de dois sites diferentes concorrentemente, potencialmente cortando o tempo total de execução quase pela metade em comparação com a execução sequencial.\n\n#### Diagrama de Fluxo de Execução Assíncrona\n\nAqui está o que acontece no loop de eventos ao executar o código acima:\n\n```mermaid\nsequenceDiagram\n    participant A as Código Principal\n    participant B as Tarefa 1<br/> (example.com)\n    participant C as Tarefa 2<br/> (github.com)\n    participant D as Loop de Eventos\n    \n    A->>B: Criar tarefa1\n    B->>D: Registrar no loop\n    A->>C: Criar tarefa2\n    C->>D: Registrar no loop\n    D->>B: Executar até browser.start()\n    D->>C: Executar até browser.start()\n    D-->>B: Retomar após WebSocket conectado\n    D-->>C: Retomar após WebSocket conectado\n    D->>B: Executar até page.go_to()\n    D->>C: Executar até page.go_to()\n    D-->>B: Retomar após página carregada\n    D-->>C: Retomar após página carregada\n    B-->>A: Retornar resultado\n    C-->>A: Retornar resultado\n```\n\nEste diagrama de sequência ilustra como o asyncio do Python gerencia as duas tarefas concorrentes em nosso código de exemplo:\n\n1.  A função principal cria duas tarefas para buscar dados de sites diferentes\n2.  Ambas as tarefas são registradas no loop de eventos\n3.  O loop de eventos executa cada tarefa até encontrar uma declaração `await` (como `browser.start()`)\n4.  Quando as operações assíncronas terminam (como uma conexão WebSocket sendo estabelecida), as tarefas retomam\n5.  O loop continua a alternar entre as tarefas em cada ponto `await`\n6.  Quando cada tarefa termina, ela retorna seu resultado para a função principal\n\nNo exemplo `fetch_page_data`, isso permite que ambas as instâncias do navegador trabalhem concorrentemente - enquanto uma está esperando uma página carregar, a outra pode estar progredindo. Isso é significativamente mais eficiente do que processar sequencialmente cada site, já que os tempos de espera de I/O não bloqueiam a execução de outras tarefas.\n\n!!! info \"Multitarefa Cooperativa\"\n    O Asyncio usa multitarefa cooperativa, onde as tarefas voluntariamente cedem o controle nos pontos `await`. Isso difere da multitarefa preemptiva (threads), onde as tarefas podem ser interrompidas a qualquer momento. A multitarefa cooperativa pode fornecer melhor desempenho para operações ligadas a I/O, mas requer codificação cuidadosa para evitar bloquear o loop de eventos.\n\n## Implementação do Connection Handler\n\nA classe `ConnectionHandler` é projetada para gerenciar tanto a execução de comandos quanto o processamento de eventos, fornecendo uma interface robusta para a conexão WebSocket do CDP.\n\n### Inicialização da Classe\n\n```python\ndef __init__(\n    self,\n    connection_port: int,\n    page_id: str = 'browser',\n    ws_address_resolver: Callable[[int], str] = get_browser_ws_address,\n    ws_connector: Callable = websockets.connect,\n):\n    # Inicializar componentes...\n```\n\nO ConnectionHandler aceita vários parâmetros:\n\n| Parâmetro | Tipo | Descrição |\n|---|---|---|\n| `connection_port` | `int` | Número da porta onde o endpoint CDP do navegador está escutando |\n| `page_id` | `str` | Identificador para a página/alvo específico (use 'browser' para conexões em nível de navegador) |\n| `ws_address_resolver` | `Callable` | Função para resolver a URL do WebSocket a partir do número da porta |\n| `ws_connector` | `Callable` | Função para estabelecer a conexão WebSocket |\n\n### Componentes Internos\n\nO ConnectionHandler orquestra três componentes primários:\n\n1.  **Conexão WebSocket**: Gerencia a comunicação WebSocket real com o navegador\n2.  **Gerenciador de Comandos**: Lida com o envio de comandos e recebimento de respostas\n3.  **Gerenciador de Eventos**: Processa eventos do navegador e dispara callbacks apropriados\n\n```mermaid\nclassDiagram\n    class ConnectionHandler {\n        -_connection_port: int\n        -_page_id: str\n        -_ws_connection\n        -_command_manager: CommandManager\n        -_events_handler: EventsHandler\n        +execute_command(command, timeout) async\n        +register_callback(event_name, callback) async\n        +remove_callback(callback_id) async\n        +ping() async\n        +close() async\n        -_receive_events() async\n    }\n\n    class CommandManager {\n        -_pending_commands: dict\n        +create_command_future(command)\n        +resolve_command(id, response)\n        +remove_pending_command(id)\n    }\n\n    class EventsHandler {\n        -_callbacks: dict\n        -_network_logs: list\n        -_dialog: dict\n        +register_callback(event_name, callback, temporary)\n        +remove_callback(callback_id)\n        +clear_callbacks()\n        +process_event(event) async\n    }\n\n    ConnectionHandler *-- CommandManager\n    ConnectionHandler *-- EventsHandler\n```\n\n## Fluxo de Execução de Comando\n\nAo executar um comando através do CDP, o ConnectionHandler segue um padrão específico:\n\n1.  Garantir que uma conexão WebSocket ativa exista\n2.  Criar um objeto Future para representar a resposta pendente\n3.  Enviar o comando pelo WebSocket\n4.  Aguardar (await) o Future ser resolvido com a resposta\n5.  Retornar a resposta ao chamador\n\n```python\nasync def execute_command(self, command: dict, timeout: int = 10) -> dict:\n    # Validar comando\n    if not isinstance(command, dict):\n        logger.error('Comando deve ser um dicionário.')\n        raise exceptions.InvalidCommand('Comando deve ser um dicionário')\n\n    # Garantir que a conexão está ativa\n    await self._ensure_active_connection()\n    \n    # Criar future para este comando\n    future = self._command_manager.create_command_future(command)\n    command_str = json.dumps(command)\n\n    # Enviar comando e aguardar resposta\n    try:\n        await self._ws_connection.send(command_str)\n        response: str = await asyncio.wait_for(future, timeout)\n        return json.loads(response)\n    except asyncio.TimeoutError as exc:\n        self._command_manager.remove_pending_command(command['id'])\n        raise exc\n    except websockets.ConnectionClosed as exc:\n        await self._handle_connection_loss()\n        raise exc\n```\n\n!!! warning \"Timeout de Comando\"\n    Comandos que não recebem uma resposta dentro do período de timeout especificado lançarão um `TimeoutError`. Isso impede que scripts de automação fiquem travados indefinidamente devido a respostas ausentes. O timeout padrão é de 10 segundos, mas pode ser ajustado com base nos tempos de resposta esperados para operações complexas.\n\n## Sistema de Processamento de Eventos\n\nO sistema de eventos é um componente arquitetônico chave que permite padrões de programação reativa no Pydoll. Ele permite que você registre callbacks para eventos específicos do navegador e os execute automaticamente quando esses eventos ocorrem.\n\n### Fluxo de Eventos\n\nO fluxo de processamento de eventos segue estas etapas:\n\n1.  O método `_receive_events` roda como uma tarefa em segundo plano, recebendo continuamente mensagens do WebSocket\n2.  Cada mensagem é analisada e classificada como uma resposta de comando ou um evento\n3.  Eventos são passados para o EventsHandler para processamento\n4.  O EventsHandler identifica callbacks registrados para o evento e os invoca\n\n```mermaid\nflowchart TD\n    A[Mensagem WebSocket] --> B{É Resposta de Comando?}\n    B -->|Sim| C[Resolver Future do Comando]\n    B -->|Não| D[Processar como Evento]\n    D --> E[Encontrar Callbacks Correspondentes]\n    E --> F[Executar Callbacks]\n    F --> G{É Temporário?}\n    G -->|Sim| H[Remover Callback]\n    G -->|Não| I[Manter Callback]\n```\n\n### Registro de Callback\n\nO ConnectionHandler fornece métodos para registrar, remover e gerenciar callbacks de eventos:\n\n```python\n# Registrar um callback para um evento específico\ncallback_id = await connection.register_callback(\n    'Page.loadEventFired', \n    handle_page_load\n)\n\n# Remover um callback específico\nawait connection.remove_callback(callback_id)\n\n# Remover todos os callbacks\nawait connection.clear_callbacks()\n```\n\n!!! tip \"Callbacks Temporários\"\n    Você pode registrar um callback como temporário, o que significa que ele será automaticamente removido após ser acionado uma vez. Isso é útil para eventos únicos, como o manuseio de diálogos:\n    \n    ```python\n    await connection.register_callback(\n        'Page.javascriptDialogOpening',\n        handle_dialog,\n        temporary=True\n    )\n    ```\n\n### Execução Assíncrona de Callback\n\nCallbacks podem ser funções síncronas ou corrotinas assíncronas. O EventsHandler (gerenciado pelo ConnectionHandler) lida com ambos os tipos adequadamente:\n\n```python\n# Callback síncrono\ndef synchronous_callback(event):\n    print(f\"Evento recebido: {event['method']}\")\n\n# Callback assíncrono\nasync def asynchronous_callback(event):\n    await asyncio.sleep(0.1)  # Realizar alguma operação assíncrona\n    print(f\"Evento processado assincronamente: {event['method']}\")\n\n# Ambos podem ser registrados da mesma forma\nawait connection.register_callback('Network.requestWillBeSent', synchronous_callback)\nawait connection.register_callback('Network.responseReceived', asynchronous_callback)\n```\n\n**Modelo de Execução Sequencial:**\n\nCallbacks assíncronos são **aguardados (awaited) sequencialmente** pelo EventsManager. Isso garante que, para um único evento, os callbacks sejam executados na ordem em que foram registrados, prevenindo condições de corrida (race conditions) quando múltiplos callbacks modificam estado compartilhado.\n\n```python\n# Dentro de EventsManager.process_event()\nfor callback_data in callbacks:\n    if asyncio.iscoroutinefunction(callback_data['callback']):\n        await callback_data['callback'](event_data)  # Await sequencial\n    else:\n        callback_data['callback'](event_data)  # Execução síncrona\n```\n\nA **execução não bloqueante** (para callbacks de UI que não devem bloquear outras operações) é alcançada em um **nível mais alto**, como no método `Tab.on()`, que envolve o callback do usuário em um `asyncio.create_task()` antes de registrá-lo aqui. Esta arquitetura fornece:\n\n- **Camada inferior** (ConnectionHandler/EventsManager): Garante execução sequencial e ordem previsível\n- **Camada superior** (Tab.on()): Fornece semântica não bloqueante quando necessário\n\n!!! info \"Detalhes da Arquitetura de Eventos\"\n    Veja [Análise Profunda da Arquitetura de Eventos](../architecture/event-architecture.md) para detalhes completos sobre o sistema de eventos multicamada e a lógica por trás da execução sequencial de callbacks.\n\n## Gerenciamento de Conexão\n\nO ConnectionHandler implementa várias estratégias para garantir conexões robustas:\n\n### Estabelecimento Lento de Conexão (Lazy)\n\nConexões são estabelecidas apenas quando necessário, tipicamente quando o primeiro comando é executado ou quando explicitamente solicitado. Esta abordagem de inicialização lenta economiza recursos e permite um gerenciamento de conexão mais flexível.\n\n### Reconexão Automática\n\nSe a conexão WebSocket for perdida ou fechada inesperadamente, o ConnectionHandler tentará reestabelecê-la automaticamente quando o próximo comando for executado. Isso fornece resiliência contra problemas transitórios de rede.\n\n```python\nasync def _ensure_active_connection(self):\n    \"\"\"\n    Garante que uma conexão ativa exista antes de prosseguir.\n    \"\"\"\n    if self._ws_connection is None or self._ws_connection.closed:\n        await self._establish_new_connection()\n```\n\n### Limpeza de Recursos\n\nO ConnectionHandler implementa tanto métodos de limpeza explícitos quanto o protocolo de gerenciador de contexto assíncrono do Python (`__aenter__` e `__aexit__`), garantindo que os recursos sejam devidamente liberados quando não mais necessários:\n\n```python\nasync def close(self):\n    \"\"\"\n    Fecha a conexão WebSocket e limpa todos os callbacks.\n    \"\"\"\n    await self.clear_callbacks()\n    if self._ws_connection is not None:\n        try:\n            await self._ws_connection.close()\n        except websockets.ConnectionClosed as e:\n            logger.info(f'Conexão WebSocket foi fechada: {e}')\n        logger.info('Conexão WebSocket fechada.')\n```\n\n!!! info \"Uso do Gerenciador de Contexto\"\n    Usar o ConnectionHandler como um gerenciador de contexto é o padrão recomendado para garantir a limpeza adequada dos recursos:\n    \n    ```python\n    async with ConnectionHandler(9222, 'browser') as connection:\n        # Trabalhar com a conexão...\n        await connection.execute_command(...)\n    # Conexão é automaticamente fechada ao sair do contexto\n    ```\n\n## Pipeline de Processamento de Mensagens\n\nO ConnectionHandler implementa um pipeline sofisticado de processamento de mensagens que lida com o fluxo contínuo de mensagens da conexão WebSocket:\n\n```mermaid\nsequenceDiagram\n    participant WS as WebSocket\n    participant RCV as _receive_events\n    participant MSG as _process_single_message\n    participant PARSE as _parse_message\n    participant CMD as _handle_command_message\n    participant EVT as _handle_event_message\n    \n    loop Enquanto conectado\n        WS->>RCV: mensagem\n        RCV->>MSG: raw_message\n        MSG->>PARSE: raw_message\n        PARSE-->>MSG: JSON parseado ou None\n        \n        alt É resposta de comando\n            MSG->>CMD: mensagem\n            CMD->>CMD: resolve future do comando\n        else É notificação de evento\n            MSG->>EVT: mensagem\n            EVT->>EVT: processa evento & dispara callbacks\n        end\n    end\n```\n\nEste pipeline garante o processamento eficiente tanto de respostas de comandos quanto de eventos assíncronos, permitindo ao Pydoll manter uma operação responsiva mesmo sob alto volume de mensagens.\n\n## Uso Avançado\n\nO ConnectionHandler é geralmente usado indiretamente através das classes Browser e Page, mas também pode ser usado diretamente para cenários avançados:\n\n### Monitoramento Direto de Eventos\n\nPara casos de uso especializados, você pode querer contornar as APIs de nível superior e monitorar diretamente eventos CDP específicos:\n\n```python\nfrom pydoll.connection.connection import ConnectionHandler\n\nasync def monitor_network():\n    connection = ConnectionHandler(9222)\n    \n    async def log_request(event):\n        url = event['params']['request']['url']\n        print(f\"Requisição: {url}\")\n    \n    await connection.register_callback(\n        'Network.requestWillBeSent', \n        log_request\n    )\n    \n    # Habilitar eventos de rede via comando CDP\n    await connection.execute_command({\n        \"id\": 1,\n        \"method\": \"Network.enable\"\n    })\n    \n    # Manter rodando até ser interrompido\n    try:\n        while True:\n            await asyncio.sleep(1)\n    finally:\n        await connection.close()\n```\n\n### Execução de Comando Personalizado\n\nVocê pode executar comandos CDP arbitrários diretamente:\n\n```python\nasync def custom_cdp_command(connection, method, params=None):\n    command = {\n        \"id\": random.randint(1, 10000),\n        \"method\": method,\n        \"params\": params or {}\n    }\n    return await connection.execute_command(command)\n\n# Exemplo: Obter HTML do documento sem usar a classe Page\nasync def get_html(connection):\n    result = await custom_cdp_command(\n        connection,\n        \"Runtime.evaluate\",\n        {\"expression\": \"document.documentElement.outerHTML\"}\n    )\n    return result['result']['result']['value']\n```\n\n!!! warning \"Interface Avançada\"\n    O uso direto do ConnectionHandler requer um entendimento profundo do Chrome DevTools Protocol. Para a maioria dos casos de uso, as APIs de nível superior Browser e Page fornecem uma interface mais intuitiva e segura.\n\n\n## Padrões Avançados de Concorrência\n\nO design assíncrono do ConnectionHandler permite padrões sofisticados de concorrência:\n\n### Execução Paralela de Comandos\n\nExecute múltiplos comandos concorrentemente e espere por todos os resultados:\n\n```python\nasync def get_page_metrics(connection):\n    commands = [\n        {\"id\": 1, \"method\": \"Performance.getMetrics\"},\n        {\"id\": 2, \"method\": \"Network.getResponseBody\", \"params\": {\"requestId\": \"...\"}},\n        {\"id\": 3, \"method\": \"DOM.getDocument\"}\n    ]\n    \n    results = await asyncio.gather(\n        *(connection.execute_command(cmd) for cmd in commands)\n    )\n    \n    return results\n```\n\n## Conclusão\n\nO ConnectionHandler serve como a fundação da arquitetura do Pydoll, fornecendo uma interface robusta e eficiente para o Chrome DevTools Protocol. Ao alavancar o framework asyncio do Python e a comunicação WebSocket, ele permite automação de navegador de alto desempenho com padrões de programação elegantes e orientados a eventos.\n\nEntender o design e a operação do ConnectionHandler fornece insights valiosos sobre o funcionamento interno do Pydoll e oferece oportunidades para personalização avançada e otimização em cenários especializados.\n\nPara a maioria dos casos de uso, você interagirá com o ConnectionHandler indiretamente através das APIs de nível superior Browser e Page, que fornecem uma interface mais intuitiva enquanto aproveitam as poderosas capacidades do ConnectionHandler."
  },
  {
    "path": "docs/pt/deep-dive/fundamentals/iframes-and-contexts.md",
    "content": "# Iframes, OOPIFs e Contextos de Execução (Análise Aprofundada)\n\nEntender como a automação de navegador lida com iframes é crucial para construir ferramentas de automação robustas. Este guia abrangente explora os fundamentos técnicos do manuseio de iframes no Pydoll, cobrindo o Document Object Model (DOM), mecânicas do Chrome DevTools Protocol (CDP), contextos de execução, mundos isolados e o sofisticado pipeline de resolução que torna a interação com iframes fluida.\n\n!!! info \"Primeiro o uso prático\"\n    Se você só precisa usar iframes em seus scripts de automação, comece com o guia de funcionalidades: **Funcionalidades → Automação → IFrames**.\n    Esta análise aprofundada explica as decisões de arquitetura, nuances do protocolo e detalhes de implementação interna.\n\n---\n\n## Tabela de Conteúdos\n\n1. [Fundação: O Modelo de Objeto de Documento (DOM)](#fundação-o-modelo-de-objeto-de-documento-dom)\n2. [O que são Iframes e Por que Eles Importam](#o-que-são-iframes-e-por-que-eles-importam)\n3. [O Desafio: Iframes Fora de Processo (OOPIFs)](#o-desafio-iframes-fora-de-processo-oopifs)\n4. [Protocolo Chrome DevTools e Gerenciamento de Frames](#protocolo-chrome-devtools-e-gerenciamento-de-frames)\n5. [Contextos de Execução e Mundos Isolados](#contextos-de-execução-e-mundos-isolados)\n6. [Referência de Identificadores CDP](#referência-de-identificadores-cdp)\n7. [Pipeline de Resolução do Pydoll](#pipeline-de-resolução-do-pydoll)\n8. [Roteamento de Sessão e Modo \"Flattened\"](#roteamento-de-sessão-e-modo-flattened)\n9. [Análise Aprofundada da Implementação](#análise-aprofundada-da-implementação)\n10. [Considerações de Performance](#considerações-de-performance)\n11. [Modos de Falha e Depuração](#modos-de-falha-e-depuração)\n\n---\n\n## Fundação: O Modelo de Objeto de Documento (DOM)\n\nAntes de mergulhar nos iframes, precisamos entender o DOM — a estrutura em árvore que representa um documento HTML na memória.\n\n### O que é o DOM?\n\nO **Modelo de Objeto de Documento** (Document Object Model) é uma interface de programação para documentos HTML e XML. Ele representa a estrutura da página como uma árvore de nós, onde cada nó corresponde a uma parte do documento:\n\n- **Nós de elemento**: Tags HTML como `<div>`, `<iframe>`, `<button>`\n- **Nós de texto**: O conteúdo de texto real\n- **Nós de atributo**: Atributos de elemento como `id`, `class`, `src`\n- **Nó do documento**: A raiz da árvore\n\n```mermaid\ngraph TD\n    Document[Documento] --> HTML[elemento html]\n    HTML --> Head[elemento head]\n    HTML --> Body[elemento body]\n    Body --> Div1[elemento div]\n    Body --> Div2[elemento div]\n    Div1 --> Text1[nó de texto: 'Olá']\n    Div2 --> Iframe[elemento iframe]\n    Iframe --> IframeDoc[documento do iframe]\n    IframeDoc --> IframeBody[body do iframe]\n    IframeBody --> IframeContent[conteúdo do iframe...]\n```\n\n### Propriedades da Árvore DOM\n\n1. **Estrutura hierárquica**: Todo nó tem um pai (exceto o Documento) e pode ter filhos\n2. **Identificação de nós**: Nós podem ser identificados por:\n   - `nodeId`: Identificador interno dentro de um contexto de documento (domínio DOM)\n   - `backendNodeId`: Identificador estável que pode referenciar nós através de diferentes documentos\n3. **Representação viva**: Mudanças no DOM são refletidas imediatamente na árvore\n\n### Por que Isso Importa para Iframes\n\nCada elemento `<iframe>` cria uma **árvore DOM nova e independente**. O próprio elemento iframe existe no DOM do pai, mas o conteúdo carregado no iframe tem seu próprio nó Documento completo e estrutura de árvore. Essa separação é a base de toda a complexidade dos iframes.\n\n---\n\n## O que são Iframes e Por que Eles Importam\n\n### Definição\n\nUm **iframe** (quadro em linha) é um elemento HTML (`<iframe>`) que incorpora outro documento HTML dentro da página atual. O documento incorporado mantém seu próprio contexto, incluindo:\n\n- Estrutura HTML e árvore DOM independentes\n- Ambiente de execução JavaScript separado\n- Estilização CSS própria (a menos que explicitamente compartilhada)\n- Histórico de navegação distinto\n\n```html\n<body>\n  <h1>Página Pai</h1>\n  <iframe src=\"https://example.com/embedded.html\" id=\"content-frame\"></iframe>\n  <p>Mais conteúdo do pai</p>\n</body>\n```\n\n### Casos de Uso Comuns\n\n| Caso de Uso | Descrição | Exemplo |\n|----------|-------------|---------|\n| **Widgets de terceiros** | Incorpora conteúdo externo com segurança | Formulários de pagamento, feeds de mídia social, widgets de chat |\n| **Isolamento de conteúdo** | Coloca conteúdo não confiável em sandbox | HTML gerado por usuário, anúncios |\n| **Arquitetura modular** | Componentes reutilizáveis | Widgets de dashboard, sistemas de plugins |\n| **Conteúdo de origem cruzada** | Carrega recursos de domínios diferentes | Mapas, players de vídeo, dashboards de analytics |\n\n### Modelo de Segurança: Política de Mesma Origem (Same-Origin Policy)\n\nO navegador impõe uma **Política de Mesma Origem** para iframes:\n\n- **Iframes de mesma origem**: O pai pode acessar o DOM do iframe via JavaScript (`iframe.contentDocument`)\n- **Iframes de origem cruzada**: O pai não pode acessar o DOM do iframe diretamente (restrição de segurança)\n\nEssa barreira de segurança é o motivo pelo qual ferramentas de automação precisam de mecanismos especiais (como o CDP) para interagir com o conteúdo do iframe.\n\n!!! warning \"Importante para automação\"\n    Automação tradicional baseada em JavaScript (como as primeiras abordagens do Selenium) não pode acessar diretamente o conteúdo de iframes de origem cruzada devido à segurança do navegador. O CDP opera em um nível mais baixo, contornando essa limitação para fins de depuração.\n\n---\n\n## O Desafio: Iframes Fora de Processo (OOPIFs)\n\n### O que são OOPIFs?\n\nO Chromium moderno usa **isolamento de site** (site isolation) para segurança e estabilidade. Isso significa que origens diferentes podem ser renderizadas em processos separados do SO. Um iframe de uma origem diferente torna-se um **Iframe Fora de Processo (OOPIF)**.\n\n```mermaid\ngraph LR\n    subgraph \"Processo 1: example.com\"\n        MainPage[DOM da Página Principal]\n    end\n    \n    subgraph \"Processo 2: widget.com\"\n        IframeDOM[DOM do Iframe]\n    end\n    \n    MainPage -.Fronteira do Processo.-> IframeDOM\n```\n\n### Por que OOPIFs Complicam a Automação\n\n| Aspecto | Iframe no Mesmo Processo | Iframe Fora de Processo (OOPIF) |\n|--------|-------------------|-------------------------------|\n| **Acesso ao DOM** | Árvore de documento compartilhada na memória | Alvo (target) separado com seu próprio documento |\n| **Roteamento de comandos** | Conexão única | Requer anexação ao alvo e roteamento de sessão |\n| **Árvore de frames** | Todos os frames em uma árvore | Frame raiz + alvos separados para OOPIFs |\n| **Contexto JavaScript** | Mesmo contexto de execution | Contexto de execução diferente por processo |\n| **Comunicação CDP** | Comandos diretos | Comandos devem incluir `sessionId` |\n\n### A Abordagem Tradicional (Troca Manual de Contexto)\n\nSem um manuseio sofisticado, automatizar OOPIFs requer:\n\n```python\n# Abordagem tradicional (manual) com outras ferramentas\nmain_page = browser.get_page()\niframe_element = main_page.find_element_by_id(\"iframe-id\")\n\n# Deve trocar manualmente o contexto\ndriver.switch_to.frame(iframe_element)\n\n# Agora os comandos miram o iframe\nbutton = driver.find_element_by_id(\"button-in-iframe\")\nbutton.click()\n\n# Deve trocar manualmente de volta\ndriver.switch_to.default_content()\n```\n\n**Problemas com esta abordagem:**\n\n1. **Carga para o desenvolvedor**: Todo iframe requer gerenciamento explícito de contexto\n2. **Iframes aninhados**: Cada nível precisa de outra troca\n3. **Detecção de OOPIF**: Difícil saber quando a anexação manual é necessária\n4. **Propenso a erros**: Esquecer de trocar de volta → comandos subsequentes falham\n5. **Não componentizável**: Funções auxiliares precisam saber seu contexto de iframe\n\n### A Solução do Pydoll: Resolução Transparente de Contexto\n\nO Pydoll elimina a troca manual de contexto resolvendo os contextos de iframe automaticamente:\n\n```python\n# Abordagem Pydoll (sem troca manual)\niframe = await tab.find(id=\"iframe-id\")\nbutton = await iframe.find(id=\"button-in-iframe\")\nawait button.click()\n\n# Iframes aninhados? Mesmo padrão\nouter = await tab.find(id=\"outer-iframe\")\ninner = await outer.find(tag_name=\"iframe\")\nbutton = await inner.find(text=\"Submit\")\nawait button.click()\n```\n\nA complexidade é tratada internamente. Vamos explorar como.\n\n---\n\n## Protocolo Chrome DevTools e Gerenciamento de Frames\n\nComo discutido em [Análise Aprofundada → Fundamentos → Protocolo Chrome DevTools](./cdp.md), o CDP fornece controle abrangente do navegador via comunicação WebSocket. O gerenciamento de frames é distribuído por múltiplos domínios do CDP.\n\n### Domínios CDP Relevantes\n\n#### 1. **Domínio Page**\n\nGerencia o ciclo de vida da página, frames e navegação.\n\n**Métodos principais:**\n\n- `Page.getFrameTree()`: Retorna a estrutura hierárquica de todos os frames em uma página\n  ```json\n  {\n    \"frameTree\": {\n      \"frame\": {\n        \"id\": \"main-frame-id\",\n        \"url\": \"https://example.com\",\n        \"securityOrigin\": \"https://example.com\",\n        \"mimeType\": \"text/html\"\n      },\n      \"childFrames\": [\n        {\n          \"frame\": {\n            \"id\": \"child-frame-id\",\n            \"parentId\": \"main-frame-id\",\n            \"url\": \"https://widget.com/embed\"\n          }\n        }\n      ]\n    }\n  }\n  ```\n\n- `Page.createIsolatedWorld(frameId, worldName)`: Cria um novo contexto de execução JavaScript em um frame específico\n  ```json\n  {\n    \"executionContextId\": 42\n  }\n  ```\n\n**Uso no Pydoll:**\n\n```python\n# De pydoll/elements/web_element.py\n@staticmethod\nasync def _get_frame_tree_for(\n    handler: ConnectionHandler, session_id: Optional[str]\n) -> FrameTree:\n    \"\"\"Pega a árvore de frames da Página para a conexão/alvo dados.\"\"\"\n    command = PageCommands.get_frame_tree()\n    if session_id:\n        command['sessionId'] = session_id\n    response: GetFrameTreeResponse = await handler.execute_command(command)\n    return response['result']['frameTree']\n```\n\n#### 2. **Domínio DOM**\n\nFornece acesso à estrutura do DOM.\n\n**Métodos principais:**\n\n- `DOM.describeNode(objectId)`: Retorna informação detalhada sobre um nó DOM\n  ```json\n  {\n    \"node\": {\n      \"nodeId\": 123,\n      \"backendNodeId\": 456,\n      \"nodeName\": \"IFRAME\",\n      \"frameId\": \"parent-frame-id\",\n      \"contentDocument\": {\n        \"frameId\": \"iframe-frame-id\",\n        \"documentURL\": \"https://embedded.com/page.html\"\n      }\n    }\n  }\n  ```\n\n- `DOM.getFrameOwner(frameId)`: Retorna o `backendNodeId` do elemento `<iframe>` que possui um frame\n  ```json\n  {\n    \"backendNodeId\": 456\n  }\n  ```\n\n**Uso no Pydoll:**\n\n```python\n# De pydoll/elements/web_element.py\n@staticmethod\nasync def _owner_backend_for(\n    handler: ConnectionHandler, session_id: Optional[str], frame_id: str\n) -> Optional[int]:\n    \"\"\"Pega o backendNodeId do elemento DOM que possui o frame dado.\"\"\"\n    command = DomCommands.get_frame_owner(frame_id=frame_id)\n    if session_id:\n        command['sessionId'] = session_id\n    response: GetFrameOwnerResponse = await handler.execute_command(command)\n    return response.get('result', {}).get('backendNodeId')\n```\n\n#### 3. **Domínio Target**\n\nGerencia alvos (targets) do navegador (páginas, iframes, workers, etc.).\n\n**Métodos principais:**\n\n- `Target.getTargets()`: Lista todos os alvos disponíveis\n  ```json\n  {\n    \"targetInfos\": [\n      {\n        \"targetId\": \"page-target-id\",\n        \"type\": \"page\",\n        \"title\": \"Main Page\",\n        \"url\": \"https://example.com\"\n      },\n      {\n        \"targetId\": \"iframe-target-id\",\n        \"type\": \"iframe\",\n        \"title\": \"\",\n        \"url\": \"https://widget.com/embed\",\n        \"parentFrameId\": \"main-frame-id\"\n      }\n    ]\n  }\n  ```\n\n- `Target.attachToTarget(targetId, flatten)`: Anexa a um alvo para depuração\n  - Quando `flatten=true`: Retorna um `sessionId` para rotear comandos no modo \"flattened\"\n  - Toda comunicação acontece sobre o mesmo WebSocket, diferenciada por `sessionId`\n\n**Uso no Pydoll:**\n\n```python\n# De pydoll/interactions/iframe.py (simplificado)\nasync def _resolve_oopif_by_parent(self, content_frame_id: str, ...):\n    \"\"\"Resolve um OOPIF usando o content frame id.\"\"\"\n    browser_handler = ConnectionHandler(...)\n    targets_response: GetTargetsResponse = await browser_handler.execute_command(\n        TargetCommands.get_targets()\n    )\n    target_infos = targets_response.get('result', {}).get('targetInfos', [])\n\n    # Encontra alvos cujo parentFrameId bate\n    direct_children = [\n        target_info for target_info in target_infos\n        if target_info.get('parentFrameId') == content_frame_id\n    ]\n    \n    if direct_children:\n        attach_response: AttachToTargetResponse = await browser_handler.execute_command(\n            TargetCommands.attach_to_target(\n                target_id=direct_children[0]['targetId'], \n                flatten=True\n            )\n        )\n        attached_session_id = attach_response.get('result', {}).get('sessionId')\n        # ... usa session_id para comandos subsequentes\n```\n\n#### 4. **Domínio Runtime**\n\nExecuta JavaScript e gerencia contextos de execução.\n\n**Métodos principais:**\n\n- `Runtime.evaluate(expression, contextId)`: Avalia JavaScript em um contexto de execução específico\n- `Runtime.callFunctionOn(functionDeclaration, objectId)`: Chama uma função com um objeto específico como `this`\n\n**Uso no Pydoll para acesso ao documento do iframe:**\n\n```python\n# De pydoll/elements/web_element.py\nasync def _set_iframe_document_object_id(self, execution_context_id: int):\n    \"\"\"Avalia document.documentElement no contexto do iframe e cacheia seu object id.\"\"\"\n    evaluate_command = RuntimeCommands.evaluate(\n        expression='document.documentElement',\n        context_id=execution_context_id,\n    )\n    if self._iframe_context and self._iframe_context.session_id:\n        evaluate_command['sessionId'] = self._iframe_context.session_id\n    \n    evaluate_response: EvaluateResponse = await (\n        (self._iframe_context.session_handler if self._iframe_context else None)\n        or self._connection_handler\n    ).execute_command(evaluate_command)\n    \n    document_object_id = evaluate_response.get('result', {}).get('result', {}).get('objectId')\n    if self._iframe_context:\n        self._iframe_context.document_object_id = document_object_id\n```\n\n---\n\n## Contextos de Execução e Mundos Isolados\n\n### O que é um Contexto de Execução?\n\nUm **contexto de execução** é um ambiente onde o código JavaScript é executado. Todo frame em um navegador tem pelo menos um contexto de execution. O contexto inclui:\n\n- **Objeto global** (`window` em navegadores)\n- **Cadeia de escopo**: Como variáveis são resolvidas\n- **Vínculo 'this'**: A o que `this` se refere\n- **Ambiente de variáveis**: Todas as variáveis e funções declaradas\n\n### Múltiplos Contextos por Frame\n\nUm único frame pode ter múltiplos contextos de execução:\n\n1. **Mundo principal (contexto padrão)**: Onde o JavaScript da própria página roda\n2. **Mundos isolados**: Contextos separados que share o mesmo DOM mas têm escopos globais JavaScript diferentes\n\n```mermaid\ngraph TB\n    Frame[Frame: example.com/page]\n    Frame --> MainWorld[Mundo Principal<br/>JavaScript da Página]\n    Frame --> IsolatedWorld1[Mundo Isolado 1<br/>Script de conteúdo de extensão]\n    Frame --> IsolatedWorld2[Mundo Isolado 2<br/>Automação Pydoll]\n    \n    DOM[Árvore DOM Compartilhada]\n    MainWorld -.pode acessar.-> DOM\n    IsolatedWorld1 -.pode acessar.-> DOM\n    IsolatedWorld2 -.pode acessar.-> DOM\n    \n    MainWorld -.não pode acessar.-> IsolatedWorld1\n    MainWorld -.não pode acessar.-> IsolatedWorld2\n```\n\n### O que é um Mundo Isolado?\n\nUm **mundo isolado** é um contexto de execução JavaScript separado que:\n\n- **Compartilha o mesmo DOM**: Pode ler/modificar elementos DOM\n- **Tem um objeto global separado**: Variáveis/funções não vazam entre mundos\n- **Previne interferência**: Scripts da página não podem detectar ou interferir com scripts do mundo isolado\n\n**Origem**: Mundos isolados foram criados para extensões de navegador. Scripts de conteúdo (content scripts) rodam em mundos isolados para que possam interagir com o DOM da página sem:\n\n- Scripts da página sobrescrevendo suas variáveis\n- Serem detectados por código anti-tamper (anti-adulteração)\n- Conflitar com o JavaScript da página\n\n### Por que o Pydoll Usa Mundos Isolados para Iframes\n\nQuando o Pydoll interage com o conteúdo de um iframe, ele cria um mundo isolado no contexto desse iframe. Isso fornece:\n\n1. **Ambiente JavaScript limpo**: Sem conflitos com os scripts do próprio iframe\n2. **Comportamento consistente**: Scripts de automação funcionam independentemente de qual JavaScript o iframe roda\n3. **Anti-detecção**: O JavaScript do iframe não pode detectar facilmente a presença do Pydoll\n4. **Avaliação segura**: Código de automação não pode acidentalmente disparar lógica da página\n\n**Implementação:**\n\n```python\n# De pydoll/elements/web_element.py\n@staticmethod\nasync def _create_isolated_world_for_frame(\n    frame_id: str,\n    handler: ConnectionHandler,\n    session_id: Optional[str],\n) -> int:\n    \"\"\"Cria um mundo isolado (Page.createIsolatedWorld) para o frame dado.\"\"\"\n    create_command = PageCommands.create_isolated_world(\n        frame_id=frame_id,\n        world_name=f'pydoll::iframe::{frame_id}',\n        grant_universal_access=True,\n    )\n    if session_id:\n        create_command['sessionId'] = session_id\n    \n    create_response: CreateIsolatedWorldResponse = await handler.execute_command(\n        create_command\n    )\n    execution_context_id = create_response.get('result', {}).get('executionContextId')\n    if not execution_context_id:\n        raise InvalidIFrame('Incapaz de criar mundo isolado para o iframe')\n    return execution_context_id\n```\n\nO parâmetro `grant_universal_access=True` permite ao mundo isolado:\n\n- Acessar frames de origem cruzada (normalmente bloqueado pela política de mesma origem)\n- Realizar operações privilegiadas necessárias para automação\n\n!!! tip \"Mundos isolados na prática\"\n    Toda vez que você usa `await iframe.find(...)`, o Pydoll avalia a consulta do seletor em um mundo isolado criado especificamente para aquele iframe. Isso garante que sua lógica de automação nunca conflite com o JavaScript do próprio iframe, e o iframe não possa detectar ou bloquear sua automação.\n\n---\n\n## Referência de Identificadores CDP\n\nEntender os identificadores do CDP é crucial para o manuseio de iframes. Aqui está uma referência abrangente:\n\n| Identificador | Domínio | Escopo | Propósito | Exemplo de Uso no Pydoll |\n|------------|--------|-------|---------|----------------------|\n| **`nodeId`** | DOM | Local do Documento | Identifica um nó DOM dentro de um contexto de documento específico | Operações internas do CDP; não é estável entre navegações |\n| **`backendNodeId`** | DOM | Estável entre documentos | Identificador estável para um nó DOM; pode mapear frames para elementos donos | Usado para parear elementos iframe com IDs de frame via `DOM.getFrameOwner` |\n| **`frameId`** | Page | Frame | Identifica um frame na árvore de frames da página | Usado para especificar qual frame para `Page.createIsolatedWorld` e travessia da árvore de frames |\n| **`targetId`** | Target | Global | Identifica um alvo (target) de depuração (página, iframe, worker, etc.) | Usado para `Target.attachToTarget` para conectar a OOPIFs |\n| **`sessionId`** | Target | Específico do Alvo | Roteia comandos para um alvo específico no modo \"flattened\" | Injetado em comandos para roteá-los ao OOPIF correto |\n| **`executionContextId`** | Runtime | Frame + Mundo | Identifica um contexto de execução JavaScript (incluindo mundos isolados) | Retornado por `Page.createIsolatedWorld`; usado em `Runtime.evaluate` |\n| **`objectId`** | Runtime | Contexto de Execução | Referência de objeto remoto (ex: elemento DOM, função, objeto) | Referência ao `document.documentElement` do iframe para consultas relativas |\n\n### Relacionamentos dos Identificadores\n\nVeja como os identificadores se relacionam durante a resolução do iframe:\n\n```\n┌─────────────────────────────────────────────────────────────────────────┐\n│                         Fluxo de Resolução                              │\n└─────────────────────────────────────────────────────────────────────────┘\n\n1. Início: Elemento <iframe>\n   └─ backendNodeId: 789\n   \n2. Encontrar Frame ─────────[DOM.getFrameOwner]──────────────┐\n   └─ frameId: abc-123                                       │\n                                                             │\n3. OOPIF? Checar Origem ────[Origem diferente detectada]─────┤\n   └─ targetId: xyz-456                                      │\n                                                             │\n4. Anexar ao Alvo ──────────[Target.attachToTarget]──────────┤\n   └─ sessionId: session-789                                 │\n                                                             │\n5. Criar Mundo Isolado ─────[Page.createIsolatedWorld]───────┤\n   └─ executionContextId: 42                                 │\n                                                             │\n6. Obter Documento ─────────[Runtime.evaluate]───────────────┘\n   └─ objectId: obj-999\n```\n\n**Pontos chave de transformação:**\n\n| De | Método | Para | Propósito |\n|------|--------|-----|---------|\n| `backendNodeId` | `DOM.getFrameOwner` | `frameId` | Encontrar qual frame é dono do elemento iframe |\n| `targetId` | `Target.attachToTarget(flatten=true)` | `sessionId` | Conectar ao OOPIF para roteamento de comandos |\n| `frameId` | `Page.createIsolatedWorld` | `executionContextId` | Criar ambiente JavaScript seguro |\n| `executionContextId` | `Runtime.evaluate('document.documentElement')` | `objectId` | Obter referência ao documento do iframe |\n\n### Representação no Código do Pydoll\n\n```python\n# De pydoll/elements/web_element.py\n@dataclass\nclass _IFrameContext:\n    \"\"\"Encapsula todos os identificadores e informação de roteamento para um iframe.\"\"\"\n    frame_id: str                                   # frameId: identifica o frame\n    document_url: Optional[str] = None              # URL carregada do frame\n    execution_context_id: Optional[int] = None      # executionContextId: mundo isolado\n    document_object_id: Optional[str] = None        # objectId: document.documentElement\n    session_handler: Optional[ConnectionHandler] = None  # para alvos OOPIF\n    session_id: Optional[str] = None                # sessionId: roteia comandos para OOPIF\n```\n\nEste dataclass é cacheado em cada `WebElement` representando um iframe, permitindo o roteamento automático de todas as operações subsequentes.\n\n---\n\n## Pipeline de Resolução do Pydoll\n\nQuando você acessa um iframe no Pydoll (ex: `await iframe.find(...)`), um elaborado pipeline de resolução é executado nos bastidores. Esta seção detalha cada passo.\n\n### Fluxo de Alto Nível\n\n```mermaid\nsequenceDiagram\n    participant User as Usuário\n    participant WebElement\n    participant Pipeline as Pipeline de Resolução\n    participant CDP\n    \n    Usuário->>WebElement: iframe.find(id='button')\n    WebElement->>WebElement: Verifica se contexto do iframe está em cache\n    alt Contexto não cacheado\n        WebElement->>Pipeline: _ensure_iframe_context()\n        Pipeline->>CDP: DOM.describeNode(iframe)\n        CDP-->>Pipeline: Info do Nó (frameId?, backendNodeId, etc.)\n        \n        alt frameId não está na info do nó\n            Pipeline->>Pipeline: _resolve_frame_by_owner()\n            Pipeline->>CDP: Page.getFrameTree()\n            CDP-->>Pipeline: Árvore de frames\n            Pipeline->>CDP: DOM.getFrameOwner(cada frame)\n            CDP-->>Pipeline: backendNodeId\n            Pipeline->>Pipeline: Compara backendNodeId para achar frameId\n        end\n        \n        alt frameId ainda faltando (OOPIF)\n            Pipeline->>Pipeline: _resolve_oopif_by_parent()\n            Pipeline->>CDP: Target.getTargets()\n            CDP-->>Pipeline: Lista de alvos\n            Pipeline->>CDP: Target.attachToTarget(targetId, flatten=true)\n            CDP-->>Pipeline: sessionId\n            Pipeline->>CDP: Page.getFrameTree(sessionId)\n            CDP-->>Pipeline: Árvore de frames do OOPIF\n        end\n        \n        Pipeline->>CDP: Page.createIsolatedWorld(frameId)\n        CDP-->>Pipeline: executionContextId\n        \n        Pipeline->>CDP: Runtime.evaluate('document.documentElement', contextId)\n        CDP-->>Pipeline: objectId (referência do documento)\n        \n        Pipeline->>WebElement: Cacheia _IFrameContext\n    end\n    \n    WebElement->>WebElement: Usa contexto cacheado para find()\n    WebElement-->>Usuário: Elemento Button (com contexto)\n```\n\n### Análise Aprofundada Passo a Passo\n\n#### **Passo 1: Descrever o Elemento Iframe**\n\n**Objetivo**: Extrair metadados do elemento DOM `<iframe>`.\n\n**Método**: `DOM.describeNode(objectId=iframe_object_id)`\n\n**O que obtemos**:\n\n- `backendNodeId`: Identificador estável para o elemento iframe\n- `frameId` (de `contentDocument`): Se o conteúdo do iframe já está carregado e no mesmo processo\n- `documentURL`: A URL carregada no iframe\n- `parentFrameId` (do campo `frameId` no nó): O frame contendo este elemento iframe\n\n**Código**:\n\n```python\n# De pydoll/interactions/iframe.py\nasync def resolve(self) -> IFrameContext:\n    \"\"\"Resolve e retorna o contexto do iframe.\"\"\"\n    base_handler, base_session_id = self._get_base_session()\n    node_info = await self._describe_element_node(base_handler, base_session_id)\n    frame_id, document_url, content_frame_id, backend_node_id = self._extract_frame_metadata(\n        node_info\n    )\n    # ... continua resolução\n```\n\n**Auxiliar**:\n\n```python\n@staticmethod\ndef _extract_frame_metadata(\n    node_info: Node,\n) -> tuple[Optional[str], Optional[str], Optional[str], Optional[int]]:\n    \"\"\"Extrai metadados relacionados a iframe de um Nó DOM.describeNode.\"\"\"\n    content_document = node_info.get('contentDocument') or {}\n    content_frame_id = node_info.get('frameId')\n    backend_node_id = node_info.get('backendNodeId')\n    frame_id = content_document.get('frameId')\n    document_url = (\n        content_document.get('documentURL')\n        or content_document.get('baseURL')\n        or node_info.get('documentURL')\n        or node_info.get('baseURL')\n    )\n    return frame_id, document_url, content_frame_id, backend_node_id\n```\n\n**Resultado**:\n\n- **Se `frame_id` está presente**: Ótimo! O iframe está no mesmo processo; prossiga para o Passo 4.\n- **Se `frame_id` está faltando**: O iframe pode ser um OOPIF ou não totalmente carregado; prossiga para o Passo 2.\n\n---\n\n#### **Passo 2: Resolver Frame pelo Dono (comparação de backendNodeId)**\n\n**Objetivo**: Encontrar o `frameId` comparando o `backendNodeId` do elemento iframe com os donos de frames na árvore de frames.\n\n**Estratégia**:\n\n1. Buscar a árvore de frames da página (`Page.getFrameTree`)\n2. Para cada frame na árvore, chamar `DOM.getFrameOwner(frameId)` para obter o `backendNodeId` do elemento iframe dono\n3. Comparar com o `backendNodeId` do nosso iframe\n4. Quando eles baterem, encontramos o `frameId` correto\n\n**Código**:\n\n```python\n# De pydoll/elements/web_element.py\nasync def _resolve_frame_by_owner(\n    self,\n    base_handler: ConnectionHandler,\n    base_session_id: Optional[str],\n    backend_node_id: int,\n    current_document_url: Optional[str],\n) -> tuple[Optional[str], Optional[str]]:\n    \"\"\"Resolve um ID de frame e URL comparando o backend_node_id do dono.\"\"\"\n    owner_frame_id, owner_url = await self._find_frame_by_owner(\n        base_handler, base_session_id, backend_node_id\n    )\n    if not owner_frame_id:\n        return None, current_document_url\n    return owner_frame_id, owner_url or current_document_url\n\nasync def _find_frame_by_owner(\n    self, handler: ConnectionHandler, session_id: Optional[str], backend_node_id: int\n) -> tuple[Optional[str], Optional[str]]:\n    \"\"\"Encontra um frame comparando o backend_node_id do dono do elemento <iframe>.\"\"\"\n    frame_tree = await self._get_frame_tree_for(handler, session_id)\n    for frame_node in WebElement._walk_frames(frame_tree):\n        candidate_frame_id = frame_node.get('id', '')\n        if not candidate_frame_id:\n            continue\n        owner_backend_id = await self._owner_backend_for(\n            handler, session_id, candidate_frame_id\n        )\n        if owner_backend_id == backend_node_id:\n            return candidate_frame_id, frame_node.get('url')\n    return None, None\n```\n\n**Por que isso é necessário**:\n\n- `DOM.describeNode` às vezes não inclui o `contentDocument.frameId` para iframes de origem cruzada ou carregados tardiamente\n- A árvore de frames sempre contém todos os frames (mesmo OOPIFs), então podemos achá-lo indiretamente\n\n**Resultado**:\n\n- **Se `frameId` encontrado**: Prossiga para o Passo 4.\n- **Se ainda não encontrado**: O iframe é provavelmente um OOPIF em um alvo separado; prossiga para o Passo 3.\n\n---\n\n#### **Passo 3: Resolver OOPIF pelo Frame Pai**\n\n**Objetivo**: Para Iframes Fora de Processo, encontrar o alvo correto, anexar a ele e obter o `frameId` da árvore de frames do alvo (e o `sessionId` de roteamento quando necessário).\n\n**Quando esse passo roda**:\n\n- Iframes de **mesma origem** / in-process que já têm um `frameId` e **não** têm `backendNodeId` pulam esse passo (são tratados diretamente).\n- Iframes **cross-origin / OOPIF** (com `backendNodeId`) ou iframes cujo `frameId` não pôde ser resolvido no Passo 2 usam esse passo.\n\n**Estratégia**:\n\n**3a. Busca por alvo filho direto (caminho rápido)**:\n\n1. Chamar `Target.getTargets()` para listar todos os alvos de depuração.\n2. Filtrar alvos onde `type` é `\"iframe\"` ou `\"page\"` e `parentFrameId` bate com nosso frame pai.\n3. Se houver **apenas um** filho direto **e não houver `backendNodeId`**, anexar diretamente a esse alvo com `Target.attachToTarget(targetId, flatten=true)`.\n4. Buscar `Page.getFrameTree(sessionId)` para aquele alvo; o frame raiz dessa árvore é o frame do nosso iframe.\n\nQuando existem **múltiplos** filhos diretos ou temos um `backendNodeId` (caso típico de OOPIF), o Pydoll itera sobre cada alvo filho:\n\n1. Anexa com `Target.attachToTarget(flatten=true)`.\n2. Busca `Page.getFrameTree(sessionId)` e lê o `frame.id` raiz.\n3. Chama `DOM.getFrameOwner(frameId=root_id)` na conexão principal.\n4. Compara o `backendNodeId` retornado com o `backendNodeId` do elemento `<iframe>` original.\n5. O filho cujo dono raiz coincide é selecionado como o alvo OOPIF correto.\n\n**3b. Fallback: Escanear todos os alvos (dono raiz + busca por filho)**:\n\nSe nenhum filho direto adequado for encontrado (ou se `parentFrameId` estiver incompleto), o Pydoll recorre a escanear **todos** os alvos iframe/page:\n\n1. Iterar todos os alvos iframe/page.\n2. Anexar a cada um e buscar sua árvore de frames.\n3. Primeiro, tentar casar o **dono do frame raiz** via `DOM.getFrameOwner(root_frame_id)` com o `backendNodeId` do iframe.\n4. Se isso não bater, procurar um **frame filho** cujo `parentId` seja o `content_frame_id` (isso cobre casos em que o OOPIF está aninhado sob um frame intermediário).\n\n**Código**:\n\n```python\n# De pydoll/interactions/iframe.py\nasync def _resolve_oopif_by_parent(\n    self,\n    content_frame_id: str,\n    backend_node_id: Optional[int],\n    base_handler: Optional[ConnectionHandler] = None,\n    base_session_id: Optional[str] = None,\n) -> tuple[Optional[ConnectionHandler], Optional[str], Optional[str], Optional[str]]:\n    \"\"\"Resolve um OOPIF usando o content frame id.\"\"\"\n    browser_handler = ConnectionHandler(\n        connection_port=self._element._connection_handler._connection_port\n    )\n    targets_response: GetTargetsResponse = await browser_handler.execute_command(\n        TargetCommands.get_targets()\n    )\n    target_infos = targets_response.get('result', {}).get('targetInfos', [])\n\n    # O handler que pode resolver DOM.getFrameOwner para o contexto do elemento.\n    # Quando o <iframe> está dentro de um OOPIF aninhado, o handler do Tab\n    # não tem visibilidade; devemos rotear pela sessão que originalmente\n    # encontrou o elemento.\n    owner_handler = base_handler or self._element._connection_handler\n    owner_session_id = base_session_id\n\n    # Estratégia 3a: Filhos diretos (caminho rápido)\n    direct_children = [\n        target_info\n        for target_info in target_infos\n        if target_info.get('type') in {'iframe', 'page'}\n        and target_info.get('parentFrameId') == content_frame_id\n    ]\n\n    is_single_child = len(direct_children) == 1\n    for child_target in direct_children:\n        attach_response: AttachToTargetResponse = await browser_handler.execute_command(\n            TargetCommands.attach_to_target(\n                target_id=child_target['targetId'], flatten=True\n            )\n        )\n        attached_session_id = attach_response.get('result', {}).get('sessionId')\n        if not attached_session_id:\n            continue\n\n        frame_tree = await self._get_frame_tree_for(browser_handler, attached_session_id)\n        root_frame = (frame_tree or {}).get('frame', {})\n        root_frame_id = root_frame.get('id', '')\n\n        # Caso simples / mesma origem: filho único e sem backend_node_id\n        if is_single_child and root_frame_id and backend_node_id is None:\n            return (\n                browser_handler,\n                attached_session_id,\n                root_frame_id,\n                root_frame.get('url'),\n            )\n\n        # Caso OOPIF: confirmar propriedade via DOM.getFrameOwner\n        if root_frame_id and backend_node_id is not None:\n            owner_backend_id = await self._owner_backend_for(\n                owner_handler, owner_session_id, root_frame_id\n            )\n            if owner_backend_id == backend_node_id:\n                return (\n                    browser_handler,\n                    attached_session_id,\n                    root_frame_id,\n                    root_frame.get('url'),\n                )\n\n    # Estratégia 3b: Escanear todos os alvos (dono raiz + busca por filho)\n    for target_info in target_infos:\n        if target_info.get('type') not in {'iframe', 'page'}:\n            continue\n        attach_response = await browser_handler.execute_command(\n            TargetCommands.attach_to_target(\n                target_id=target_info.get('targetId', ''), flatten=True\n            )\n        )\n        attached_session_id = attach_response.get('result', {}).get('sessionId')\n        if not attached_session_id:\n            continue\n\n        frame_tree = await self._get_frame_tree_for(browser_handler, attached_session_id)\n        root_frame = (frame_tree or {}).get('frame', {})\n        root_frame_id = root_frame.get('id', '')\n\n        # Match direto: content_frame_id igual ao root frame ID do alvo\n        if root_frame_id and root_frame_id == content_frame_id:\n            return (\n                browser_handler,\n                attached_session_id,\n                root_frame_id,\n                root_frame.get('url'),\n            )\n\n        # Primeiro tenta casar o dono do frame raiz via backend_node_id\n        if root_frame_id and backend_node_id is not None:\n            owner_backend_id = await self._owner_backend_for(\n                owner_handler, owner_session_id, root_frame_id\n            )\n            if owner_backend_id == backend_node_id:\n                return (\n                    browser_handler,\n                    attached_session_id,\n                    root_frame_id,\n                    root_frame.get('url'),\n                )\n\n        # Fallback: procurar frame filho cujo parentId seja content_frame_id\n        child_frame_id = IFrameContextResolver._find_child_by_parent(\n            frame_tree, content_frame_id\n        )\n        if child_frame_id:\n            return browser_handler, attached_session_id, child_frame_id, None\n\n    return None, None, None, None\n```\n\n**Resultado**:\n\n- **Se OOPIF resolvido**: Agora temos `sessionId`, `session_handler`, e `frameId`; prossiga para o Passo 4.\n- **Se resolução falhar**: Lança exceção `InvalidIFrame` (tratada em `_ensure_iframe_context`).\n\n---\n\n#### **Passo 4: Criar Mundo Isolado**\n\n**Objetivo**: Criar um contexto de execução JavaScript separado no frame resolvido.\n\n**Método**: `Page.createIsolatedWorld(frameId, worldName='pydoll::iframe::<frameId>', grantUniversalAccess=true)`\n\n**Parâmetros**:\n- `frameId`: O frame onde o mundo isolado é criado\n- `worldName`: Identificador para o mundo (útil para depuração)\n- `grantUniversalAccess`: Permite acesso de origem cruzada (necessário para automação)\n\n**Resposta**: `{ executionContextId: 42 }`\n\n**Código**:\n\n```python\n# De pydoll/elements/web_element.py\n@staticmethod\nasync def _create_isolated_world_for_frame(\n    frame_id: str,\n    handler: ConnectionHandler,\n    session_id: Optional[str],\n) -> int:\n    \"\"\"Cria um mundo isolado para o frame dado.\"\"\"\n    create_command = PageCommands.create_isolated_world(\n        frame_id=frame_id,\n        world_name=f'pydoll::iframe::{frame_id}',\n        grant_universal_access=True,\n    )\n    if session_id:\n        create_command['sessionId'] = session_id\n    create_response: CreateIsolatedWorldResponse = await handler.execute_command(create_command)\n    execution_context_id = create_response.get('result', {}).get('executionContextId')\n    if not execution_context_id:\n        raise InvalidIFrame('Incapaz de criar mundo isolado para o iframe')\n    return execution_context_id\n```\n\n**Por que mundo isolado**:\n\n- **Isolamento**: Nosso JavaScript de automação não interfere com o JavaScript do iframe\n- **Anti-detecção**: O iframe não pode detectar nossa presença facilmente\n- **Consistência**: Comportamento é previsível independentemente do ambiente de script do iframe\n\n**Resultado**: Temos um `executionContextId` para rodar JavaScript no iframe.\n\n---\n\n#### **Passo 5: Fixar o Documento do Iframe como um Objeto Runtime**\n\n**Objetivo**: Obter uma referência `objectId` ao `document.documentElement` do iframe (o elemento `<html>` do iframe).\n\n**Método**: `Runtime.evaluate(expression='document.documentElement', contextId=executionContextId)`\n\n**Por que precisamos disso**:\n\n- Para executar **consultas relativas** (como `element.querySelector()`) dentro do iframe\n- O `objectId` permite usar `Runtime.callFunctionOn(objectId, ...)` com `this` vinculado ao documento do iframe\n\n**Código**:\n\n```python\n# De pydoll/elements/web_element.py\nasync def _set_iframe_document_object_id(self, execution_context_id: int) -> None:\n    \"\"\"Avalia document.documentElement no contexto do iframe e cacheia seu object id.\"\"\"\n    evaluate_command = RuntimeCommands.evaluate(\n        expression='document.documentElement',\n        context_id=execution_context_id,\n    )\n    if self._iframe_context and self._iframe_context.session_id:\n        evaluate_command['sessionId'] = self._iframe_context.session_id\n    evaluate_response: EvaluateResponse = await (\n        (self._iframe_context.session_handler if self._iframe_context else None)\n        or self._connection_handler\n    ).execute_command(evaluate_command)\n    result_object = evaluate_response.get('result', {}).get('result', {})\n    document_object_id = result_object.get('objectId')\n    if not document_object_id:\n        raise InvalidIFrame('Incapaz de obter referência do documento para o iframe')\n    if self._iframe_context:\n        self._iframe_context.document_object_id = document_object_id\n```\n\n**Resultado**: O `_IFrameContext` está agora totalmente populado e cacheado no `WebElement`.\n\n---\n\n#### **Passo 6: Cachear e Propagar Contexto**\n\n**Objetivo**: Armazenar o contexto resolvido no elemento iframe e propagá-lo para todos os elementos filhos encontrados dentro do iframe.\n\n**Cacheando**:\n\n```python\n# De pydoll/elements/web_element.py\ndef _init_iframe_context(\n    self,\n    frame_id: str,\n    document_url: Optional[str],\n    session_handler: Optional[ConnectionHandler],\n    session_id: Optional[str],\n) -> None:\n    \"\"\"Inicializa e cacheia contexto de iframe neste elemento.\"\"\"\n    self._iframe_context = _IFrameContext(frame_id=frame_id, document_url=document_url)\n    # Limpa atributos de roteamento (estes eram para iframes aninhados)\n    if hasattr(self, '_routing_session_handler'):\n        delattr(self, '_routing_session_handler')\n    if hasattr(self, '_routing_session_id'):\n        delattr(self, '_routing_session_id')\n    # Armazena roteamento OOPIF se necessário\n    if session_handler and session_id:\n        self._iframe_context.session_handler = session_handler\n        self._iframe_context.session_id = session_id\n```\n\n**Propagação** (ao encontrar elementos dentro do iframe):\n\n```python\n# De pydoll/elements/mixins/find_elements_mixin.py\ndef _apply_iframe_context_to_element(\n    self, element: WebElement, iframe_context: _IFrameContext | None\n) -> None:\n    \"\"\"Propaga contexto de iframe para o elemento recém-criado.\"\"\"\n    if not iframe_context:\n        return\n    \n    # Se o elemento filho também é um iframe, configura roteamento\n    if getattr(element, 'is_iframe', False):\n        element._routing_session_handler = (\n            iframe_context.session_handler or self._connection_handler\n        )\n        element._routing_session_id = iframe_context.session_id\n        element._routing_parent_frame_id = iframe_context.frame_id\n        return\n    \n    # Caso contrário, injeta o contexto do iframe pai\n    element._iframe_context = iframe_context\n```\n\n**Por que propagação importa**:\n\n- Elementos encontrados dentro de um iframe herdam o contexto do iframe\n- Isso garante que operações subsequentes (clicar, digitar, encontrar elementos aninhados) automaticamente usem o roteamento correto\n- Iframes aninhados recebem informação de roteamento para que possam resolver seu próprio contexto relativo ao iframe pai\n\n---\n\n## Roteamento de Sessão e Modo \"Flattened\"\n\n### O Modelo de Sessão \"Flattened\"\n\nComo discutido em [Análise Aprofundada → Fundamentos → CDP](./cdp.md), o CDP tradicional usa conexões WebSocket separadas para cada alvo. O **Modo \"Flattened\"** (unificado) é uma otimização onde todos os alvos compartilham uma única conexão WebSocket, com comandos roteados usando um `sessionId`.\n\n```mermaid\ngraph TB\n    subgraph \"Modo Tradicional\"\n        WS1[WebSocket 1] --> MainPage[Alvo Página Principal]\n        WS2[WebSocket 2] --> Iframe1[Alvo OOPIF 1]\n        WS3[WebSocket 3] --> Iframe2[Alvo OOPIF 2]\n    end\n    \n    subgraph \"Modo Flattened\"\n        WS[WebSocket Único] --> Router{Roteador CDP}\n        Router -->|sessionId: null| MainPage2[Alvo Página Principal]\n        Router -->|sessionId: session-1| Iframe3[Alvo OOPIF 1]\n        Router -->|sessionId: session-2| Iframe4[Alvo OOPIF 2]\n    end\n```\n\n### Como Funciona o Roteamento de Sessão\n\n**Ao anexar a um OOPIF**:\n\n```python\nresponse = await handler.execute_command(\n    TargetCommands.attach_to_target(targetId=\"iframe-target-id\", flatten=True)\n)\nsession_id = response['result']['sessionId']  # ex: \"8E6C...-1234\"\n```\n\n**Ao enviar um comando para aquele OOPIF**:\n\n```python\ncommand = PageCommands.get_frame_tree()\ncommand['sessionId'] = 'session-1'  # Roteia para o OOPIF\nresponse = await handler.execute_command(command)\n```\n\nA implementação CDP do navegador roteia o comando para o alvo correto baseado no `sessionId`.\n\n### Roteamento de Comandos do Pydoll\n\nTodo comando enviado por elementos Pydoll é automaticamente roteado para o alvo correto:\n\n```python\n# De pydoll/elements/mixins/find_elements_mixin.py\ndef _resolve_routing(self) -> tuple[ConnectionHandler, Optional[str]]:\n    \"\"\"Resolve handler e sessionId para o contexto atual.\"\"\"\n    # Verifica se elemento tem um contexto iframe com roteamento OOPIF\n    iframe_context = getattr(self, '_iframe_context', None)\n    if iframe_context and getattr(iframe_context, 'session_handler', None):\n        return iframe_context.session_handler, getattr(iframe_context, 'session_id', None)\n    \n    # Verifica se elemento herdou roteamento de um iframe pai\n    routing_handler = getattr(self, '_routing_session_handler', None)\n    if routing_handler is not None:\n        return routing_handler, getattr(self, '_routing_session_id', None)\n    \n    # Padrão: usa a conexão principal da aba\n    return self._connection_handler, None\n\nasync def _execute_command(\n    self, command: Command[T_CommandParams, T_CommandResponse]\n) -> T_CommandResponse:\n    \"\"\"Executa comando CDP via handler resolvido (timeout 60s).\"\"\"\n    handler, session_id = self._resolve_routing()\n    if session_id:\n        command['sessionId'] = session_id\n    return await handler.execute_command(command, timeout=60)\n```\n\n**Lógica de roteamento**:\n\n1. **Elemento dentro de iframe OOPIF**: Usa `iframe_context.session_id` e `iframe_context.session_handler`\n2. **Iframe aninhado (filho de OOPIF)**: Usa `_routing_session_id` e `_routing_session_handler` herdados\n3. **Elemento regular ou iframe no mesmo processo**: Usa conexão principal (`_connection_handler`), sem `sessionId`\n\n### Tipagem de Comando Estendida\n\nPara tornar `sessionId` seguro em termos de tipo (type-safe), o Pydoll estendeu o `Command` TypedDict:\n\n```python\n# De pydoll/protocol/base.py\nclass Command(TypedDict, Generic[T_CommandParams, T_CommandResponse]):\n    \"\"\"Estrutura base para todos os comandos.\"\"\"\n    id: NotRequired[int]\n    method: str\n    params: NotRequired[T_CommandParams]\n    sessionId: NotRequired[str]  # Adicionado para roteamento de sessão flattened\n```\n\nIsso permite que checadores de tipo (type-checkers) reconheçam `command['sessionId'] = '...'` como válido sem suprimir avisos de tipo.\n\n---\n\n## Considerações de Performance\n\n### Estratégia de Cache\n\n**O primeiro acesso é caro**:\n\n- `DOM.describeNode`: 1 ida e volta (round-trip)\n- Recuperação da árvore de frames: 1+ idas e voltas (principal + alvos OOPIF)\n- `DOM.getFrameOwner` por frame: N idas e voltas (no pior caso)\n- `Target.getTargets` + anexações: 1 + M idas e voltas (M = número de alvos OOPIF)\n- `Page.createIsolatedWorld`: 1 ida e volta\n- `Runtime.evaluate` (documento): 1 ida e volta\n\n**Total**: Potencialmente 5-20+ idas e voltas dependendo da estrutura da página.\n\n**Acessos subsequentes são O(1)**:\n\n- `iframe_context` é cacheado na instância `WebElement`\n- Acessar `await iframe.iframe_context` múltiplas vezes retorna o valor cacheado imediatamente\n- Todos os elementos encontrados dentro do iframe herdam o contexto (sem re-resolução)\n\n### Otimização: Busca de Alvo \"Filho Direto\"\n\nEm `_resolve_oopif_by_parent`, o Pydoll primeiro checa por filhos diretos por `parentFrameId`:\n\n```python\ndirect_children = [\n    target_info\n    for target_info in target_infos\n    if target_info.get('type') in {'iframe', 'page'}\n    and target_info.get('parentFrameId') == content_frame_id\n]\nif direct_children:\n    # Anexa hatchery, pula o escaneamento de todos os alvos\n```\n\n**Por que isso ajuda**:\n\n- A maioria dos OOPIFs tem `parentFrameId` definido corretamente\n- Evita anexar a cada alvo especulativamente\n- Reduz idas e voltas de O(alvos) para O(1) no caso comum\n\n### Resolução Paralela Assíncrona (Melhoria Futura)\n\nAtualmente, a correspondência de dono de frame é sequencial (checa cada frame um por um). Uma otimização futura poderia paralelizar:\n\n```python\n# Atual (sequencial)\nfor frame_node in frames:\n    owner = await self._owner_backend_for(...)\n    if owner == backend_node_id:\n        return frame_node['id']\n\n# Potencial (paralelo)\nresults = await asyncio.gather(*(\n    self._owner_backend_for(..., frame['id'])\n    for frame in frames\n))\nfor i, owner in enumerate(results):\n    if owner == backend_node_id:\n        return frames[i]['id']\n```\n\nIsso reduziria a latência de `N * RTT` para `RTT` (onde RTT = tempo de ida e volta).\n\n---\n\n## Modos de Falha e Depuração\n\n### Cenários Comuns de Falha\n\n#### 1. **InvalidIFrame: Incapaz de resolver frameId**\n\n**Causa**:\n\n- O iframe é criado dinamicamente e não inicializou completamente\n- O iframe está em sandbox com políticas restritivas\n- Problemas de rede atrasaram o carregamento do iframe\n\n**Soluções**:\n\n- **Esperar pelo iframe**: Use `await tab.find(id='iframe', timeout=10)` com um timeout\n- **Verificar atributo sandbox**: Sandbox restritivo (`<iframe sandbox>`) pode bloquear algumas operações CDP\n- **Estratégia de retentativa**: Implementar lógica de retentativa com backoff exponencial\n\n**Depuração**:\n\n```python\ntry:\n    iframe = await tab.find(id='problem-iframe')\n    context = await iframe.iframe_context\nexcept InvalidIFrame as e:\n    # Inspeciona o que temos\n    node_info = await iframe._describe_node(object_id=iframe._object_id)\n    print(f\"Info do nó: {node_info}\")\n    \n    # Checa árvore de frames manualmente\n    frame_tree = await WebElement._get_frame_tree_for(tab._connection_handler, None)\n    print(f\"Árvore de frames: {frame_tree}\")\n```\n\n#### 2. **InvalidIFrame: Incapaz de criar mundo isolado**\n\n**Causa**:\n\n- Frame foi destruído/navegou para longe entre os passos de resolução\n- Bug do Chrome (raro)\n\n**Soluções**:\n\n- **Re-resolver contexto**: Limpar contexto cacheado e re-acessar\n- **Verificar navegação**: Garantir que o iframe não esteja navegando durante a resolução\n\n**Depuração**:\n\n```python\n# Limpa cache e retenta\niframe._iframe_context = None\ncontext = await iframe.iframe_context\n```\n\n#### 3. **InvalidIFrame: Incapaz de obter referência do documento**\n\n**Causa**:\n\n- O mundo isolado foi criado mas o documento não está pronto\n- O frame está prestes a navegar\n\n**Soluções**:\n\n- Esperar pelo carregamento do frame: Usar eventos Page para detectar `Page.frameNavigated` ou `Page.loadEventFired`\n- Retentar com um pequeno atraso\n\n#### 4. **Falhas de roteamento de sessão (comando expira ou retorna erro)**\n\n**Causa**:\n\n- Alvo OOPIF foi destacado (página navegou, iframe removido)\n- `sessionId` está obsoleto\n\n**Soluções**:\n\n- **Re-anexar ao alvo**: Criar um novo `ConnectionHandler` e re-resolver OOPIF\n- **Validar alvo**: Chamar `Target.getTargets()` para checar se o alvo ainda existe\n\n**Depuração**:\n\n```python\n# Checa se sessão ainda é válida\ntargets = await handler.execute_command(TargetCommands.get_targets())\nactive_sessions = [t['targetId'] for t in targets['result']['targetInfos']]\nprint(f\"Alvos ativos: {active_sessions}\")\n\nif iframe._iframe_context and iframe._iframe_context.session_id:\n    print(f\"Nossa sessão: {iframe._iframe_context.session_id}\")\n```\n\n### Ferramentas de Diagnóstico\n\n#### Habilitar logs do CDP\n\n```python\nimport logging\nlogging.basicConfig(level=logging.DEBUG)\nlogger = logging.getLogger('pydoll')\nlogger.setLevel(logging.DEBUG)\n```\n\nIsso registra todos os comandos e respostas CDP, útil para rastrear os passos de resolução do iframe.\n\n#### Inspecionar contexto do iframe\n\n```python\niframe = await tab.find(id='my-iframe')\nctx = await iframe.iframe_context\n\nprint(f\"Frame ID: {ctx.frame_id}\")\nprint(f\"Document URL: {ctx.document_url}\")\nprint(f\"Execution Context ID: {ctx.execution_context_id}\")\nprint(f\"Document Object ID: {ctx.document_object_id}\")\nprint(f\"Session ID (OOPIF): {ctx.session_id}\")\nprint(f\"Session Handler: {ctx.session_handler}\")\n```\n\n---\n\n## Conclusão\n\nO manuseio de iframes do Pydoll representa uma implementação sofisticada das capacidades de gerenciamento de frames do CDP. Ao entender:\n\n- **O DOM**: Estrutura em árvore e identificação de nós\n- **Iframes**: Contextos de documento independentes e barreiras de segurança\n- **OOPIFs**: Isolamento de site e arquitetura baseada em alvos\n- **Domínios CDP**: Coordenação de Page, DOM, Target, Runtime\n- **Contextos de Execução**: Mundos isolados para automação limpa\n- **Identificadores**: Relacionamentos entre backendNodeId, frameId, targetId, sessionId, executionContextId, objectId\n- **Pipeline de resolução**: Estratégia de fallback em múltiplos estágios para encontrar frames\n- **Roteamento de sessão**: Modo \"flattened\" e roteamento automático de comandos\n\nvocê pode apreciar por que a troca manual de contexto é eliminada. A complexidade é real, mas o Pydoll a abstrai por trás de uma API simples e intuitiva:\n\n```python\niframe = await tab.find(id='login-frame')\nusername = await iframe.find(name='username')\nawait username.type_text('user@example.com')\n```\n\nTrês linhas. Sem troca de contexto. Sem anexar alvos. Sem gerenciamento de sessão. Apenas funciona.\n\n---\n\n## Leitura Adicional\n\n- **Especificação do CDP**: [Chrome DevTools Protocol - Domínio Page](https://chromedevtools.github.io/devtools-protocol/tot/Page/)\n- **Especificação do CDP**: [Chrome DevTools Protocol - Domínio DOM](https://chromedevtools.github.io/devtools-protocol/tot/DOM/)\n- **Especificação do CDP**: [Chrome DevTools Protocol - Domínio Target](https://chromedevtools.github.io/devtools-protocol/tot/Target/)\n- **Especificação do CDP**: [Chrome DevTools Protocol - Domínio Runtime](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/)\n- **Isolamento de Site do Chromium**: [Site Isolation - The Chromium Projects](https://www.chromium.org/Home/chromium-security/site-isolation/)\n- **Scripts de Conteúdo e Mundos Isolados**: [Chrome Extensions - Content Scripts](https://developer.chrome.com/docs/extensions/mv3/content_scripts/)\n- **Documentação do Pydoll**: [Análise Aprofundada → Fundamentos → Protocolo Chrome DevTools](./cdp.md)\n- **Documentação do Pydoll**: [Funcionalidades → Automação → IFrames](../../features/automation/iframes.md)\n\n---\n\n!!! tip \"Filosofia de Design\"\n    O objetivo do manuseio de iframes do Pydoll é a **automação ergonômica**: escreva código como se iframes não existissem, e deixe a biblioteca lidar com a complexidade. Esta análise aprofundada mostrou o que acontece nos bastidores—mas você nunca precisa pensar sobre isso em seus scripts de automação."
  },
  {
    "path": "docs/pt/deep-dive/fundamentals/index.md",
    "content": "# Análise Profunda: Fundamentos Essenciais\n\n**Domine a base, e todo o resto se torna mais fácil.**\n\nEsta seção cobre as **tecnologias fundamentais** que impulsionam o Pydoll: o Chrome DevTools Protocol (CDP), a comunicação assíncrona baseada em WebSocket e a integração do sistema de tipos do Python. Estes não são apenas detalhes de implementação, são as **decisões de design fundamentais** que tornam o Pydoll rápido, poderoso e seguro em tipos (type-safe).\n\n## Por que os Fundamentos Importam\n\nA maioria dos frameworks de automação abstrai sua camada de comunicação, deixando você com uma \"caixa preta\" que funciona até deixar de funcionar. Quando algo quebra, a depuração e a otimização tornam-se difíceis sem entender os mecanismos subjacentes.\n\n**O Pydoll adota uma abordagem diferente**: expomos e explicamos os fundamentos, permitindo que você trabalhe tanto como um **usuário do framework** quanto como um **engenheiro de protocolo**.\n\n!!! quote \"O Poder dos Primeiros Princípios\"\n    **\"Se você conhece o caminho amplamente, você o verá em todas as coisas.\"** - Miyamoto Musashi\n    \n    Entender o CDP, a comunicação assíncrona e os sistemas de tipos não é apenas sobre o Pydoll, é sobre entender **como a automação de navegador moderna funciona em sua essência**. Esse conhecimento se transfere para qualquer ferramenta baseada em CDP e qualquer projeto Python assíncrono.\n\n## Os Três Pilares\n\n### 1. Chrome DevTools Protocol (CDP)\n**[→ Leia a Análise Profunda do CDP](./cdp.md)**\n\n**O protocolo que impulsiona a automação de navegador moderna.**\n\nO CDP é o protocolo de depuração nativo do Chrome, o mesmo que o Chrome DevTools (F12) usa. Ao se comunicar diretamente com o CDP, o Pydoll:\n\n- **Elimina o WebDriver** (sem sobrecarga do Selenium, sem intermediários geckodriver/chromedriver)\n- **Ganha controle profundo** (modifica requisições, intercepta eventos, executa operações privilegiadas)\n- **Alcança velocidade nativa** (comunicação direta via WebSocket, sem polling HTTP)\n- **Torna-se indetectável** (sem `navigator.webdriver`, sem fingerprints de WebDriver)\n\n**O que você aprenderá:**\n\n- Como o CDP organiza a funcionalidade em domínios (Page, Network, DOM, Fetch, etc.)\n- A arquitetura de comando/evento que impulsiona a automação reativa\n- Por que ferramentas baseadas em CDP são **fundamentalmente mais poderosas** que o Selenium\n- Como ler a documentação do CDP e estender o Pydoll\n\n**Por que isso importa**: O CDP não é apenas um detalhe de implementação do Pydoll, é a fundação da automação de navegador moderna. Puppeteer, Playwright e ferramentas similares, todas usam CDP. Entendê-lo uma vez fornece conhecimento aplicável a múltiplas ferramentas.\n\n---\n\n### 2. A Camada de Conexão\n**[→ Leia a Arquitetura da Camada de Conexão](./connection-layer.md)**\n\n**Comunicação assíncrona feita da maneira certa.**\n\nEnquanto o CDP define **o que** você pode fazer, a Camada de Conexão define **como** o Pydoll se comunica com o navegador. É aqui que as mensagens de protocolo se tornam objetos Python, onde os padrões async/await permitem concorrência, e onde os WebSockets fornecem comunicação bidirecional em tempo real.\n\n**O que você aprenderá:**\n\n- Arquitetura WebSocket: conexões persistentes, enquadramento de mensagens, keep-alive\n- O padrão async/await: por que `async def` e `await` permitem automação concorrente\n- Correlação comando/resposta: como o Pydoll associa respostas a requisições\n- Despacho de eventos: como eventos do navegador disparam callbacks Python\n- Tratamento de erros: gerenciamento de timeout, falhas de conexão, degradação graciosa\n\n**Por que isso importa**: A camada de conexão é a espinha dorsal da comunicação do Pydoll. Entendê-la permite:\n- **Depuração eficaz**: Inspecionar mensagens fluindo entre Python e Chrome\n- **Otimização de desempenho**: Identificar fontes de latência e paralelizar operações\n- **Capacidades de extensão**: Adicionar comandos CDP personalizados ou modificar comportamento existente\n\n---\n\n### 3. Integração com Sistema de Tipos do Python\n**[→ Leia a Análise Profunda do Sistema de Tipos](./typing-system.md)**\n\n**Tipos fornecem tanto segurança quanto produtividade.**\n\nO sistema de tipos do Python (introduzido no 3.5, melhorado em cada versão desde então) melhora significativamente a experiência de desenvolvimento. O Pydoll utiliza `TypedDict`, `Literal`, `overload` e genéricos para fornecer:\n\n- **Autocompletar da IDE** para campos de resposta do CDP\n- **Verificação de tipos (Type checking)** para pegar bugs antes do tempo de execução (`mypy`, `pyright`)\n- **Código autodocumentado** (assinaturas de função revelam a estrutura)\n- **Segurança na refatoração** (renomeie um campo, a IDE atualiza todos os usos)\n\n**O que você aprenderá:**\n\n- Como `TypedDict` modela estruturas de eventos/respostas do CDP\n- Por que `overload` fornece tipos de retorno precisos para `find()`/`query()`\n- Como genéricos (`TypeVar`, `Generic[T]`) permitem construção flexível de comandos\n- Padrões práticos: anotar callbacks, tipar funções assíncronas, usar `Literal`\n- Integração de ferramentas: configurar mypy, aproveitar a inferência de tipos da IDE\n\n**Por que isso importa**: Dicas de tipo (type hints) tornaram-se cada vez mais importantes no Python moderno. A cobertura abrangente de tipos do Pydoll significa:\n- **Desenvolvimento mais rápido**: Autocompletar revela campos e métodos disponíveis\n- **Menos bugs**: Verificador de tipos pega erros antes que cheguem à produção\n- **Melhor refatoração**: Mude assinaturas com confiança com suporte da IDE\n\n---\n\n## Como Esses Fundamentos se Conectam\n\nEntender como CDP, comunicação assíncrona e sistemas de tipos funcionam **juntos** é a chave:\n\n```mermaid\ngraph TB\n    Python[Código Python:<br/>await tab.go_to#40;url#41;]\n    \n    Python --> TypeSystem[Sistema de Tipos:<br/>Assinatura da função revela<br/>parâmetros e tipo de retorno]\n    \n    TypeSystem --> ConnectionLayer[Camada de Conexão:<br/>Serializa comando para JSON,<br/>envia via WebSocket]\n    \n    ConnectionLayer --> CDP[CDP:<br/>Navegador recebe<br/>comando Page.navigate]\n    \n    CDP --> Browser[Chrome:<br/>Executa navegação,<br/>emite eventos]\n    \n    Browser --> CDPEvents[Eventos CDP:<br/>Page.loadEventFired,<br/>Network.requestWillBeSent]\n    \n    CDPEvents --> ConnectionLayer2[Camada de Conexão:<br/>Desserializa eventos,<br/>despacha para callbacks]\n    \n    ConnectionLayer2 --> TypedDicts[TypedDict:<br/>Dados do evento como<br/>dicionário tipado]\n    \n    TypedDicts --> PythonCallback[Callback Python:<br/>IDE mostra campos disponíveis<br/>via inferência de tipo]\n```\n\n**O fluxo**:\n\n1.  Você escreve código Python com **anotações de tipo** (Sistema de Tipos)\n2.  O código serializa para JSON e envia via **WebSocket** (Camada de Conexão)\n3.  O navegador recebe e executa **comandos CDP** (CDP)\n4.  O navegador emite **eventos CDP** de volta (CDP)\n5.  Eventos desserializam em **instâncias de TypedDict** (Sistema de Tipos)\n6.  Seus callbacks recebem **objetos de evento com tipos seguros** (Sistema de Tipos)\n\nCada camada **amplifica** as outras:\n\n- Tipos tornam as respostas do CDP descobríveis\n- O modelo de eventos do CDP permite padrões assíncronos\n- A comunicação assíncrona torna os tipos essenciais (quais campos existem nesta resposta?)\n\n## Trilha de Aprendizagem\n\nRecomendamos esta progressão:\n\n### Passo 1: CDP\n**[Comece Aqui: Chrome DevTools Protocol](./cdp.md)**\n\nEntenda o protocolo que impulsiona tudo. Aprenda domínios, comandos, eventos e como ler a documentação do CDP.\n\n**Resultado**: Você saberá como encontrar e usar qualquer recurso do CDP, não apenas o que o Pydoll expõe.\n\n### Passo 2: Camada de Conexão\n**[Continue: Arquitetura da Camada de Conexão](./connection-layer.md)**\n\nAnálise profunda da comunicação WebSocket, padrões assíncronos e despacho de eventos.\n\n**Resultado**: Você entenderá exatamente como as mensagens fluem entre Python e Chrome, permitindo depuração e otimização.\n\n### Passo 3: Sistema de Tipos\n**[Termine: Sistema de Tipos do Python](./typing-system.md)**\n\nAprenda como o Pydoll usa a tipagem moderna do Python para segurança e produtividade.\n\n**Resultado**: Você escreverá automação com segurança de tipos e suporte total da IDE, pegando bugs antes que eles rodem.\n\n## Pré-requisitos\n\nPara tirar o máximo proveito desta seção:\n\n- **Fundamentos de Python** - Funções, classes, decoradores\n- **Básico de async/await** - Entender as palavras-chave `async def` e `await`\n- **Familiaridade com JSON** - Saber como objetos/arrays serializam\n- **Browser DevTools** - Ter usado o Inspetor do Chrome (F12)\n\n**Se você é novo em Python assíncrono**, leia isto primeiro: [Real Python: Async IO in Python](https://realpython.com/async-io-python/)\n\n## Além do Básico\n\nUma vez que você dominar esses fundamentos, estará pronto para:\n\n- **[Arquitetura Interna](../architecture/browser-domain.md)** - Como os componentes do Pydoll se encaixam\n- **[Rede e Segurança](../network/index.md)** - Entendimento em nível de protocolo para proxies\n- **[Fingerprinting](../fingerprinting/index.md)** - Técnicas de detecção que exigem conhecimento de CDP\n\n## Perguntas Comuns\n\n### \"Preciso entender isso para usar o Pydoll?\"\n\n**Não**, mas entender esses fundamentos o tornará mais eficaz. O uso básico funciona bem sem esse conhecimento. No entanto, quando você precisar:\n- Depurar por que algo não está funcionando\n- Otimizar automação lenta\n- Estender o Pydoll com comandos CDP personalizados\n- Entender mensagens de erro\n- Contribuir para o projeto\n\nEsses fundamentos se tornam muito úteis.\n\n### \"Isso não é muito baixo nível?\"\n\nEste nível de detalhe é intencional. A maioria dos frameworks esconde esses fundamentos, mas a abstração vem com trocas:\n\n- Entendimento permite melhor depuração\n- Visibilidade permite otimização\n- Conhecimento permite extensão\n\nAo ensinar os fundamentos, permitimos que você vá além do que o Pydoll oferece de fábrica.\n\n### \"Quanto disso eu preciso memorizar?\"\n\n**Nada.** O objetivo é construir modelos mentais, não memorização. Após ler estas seções, você desenvolverá intuição para:\n\n- \"Isso precisa de CDP, deixe-me checar a documentação do protocolo\"\n- \"Isso está lento por causa de awaits sequenciais, deixe-me paralelizar\"\n- \"Este erro de tipo significa que estou usando o nome de campo errado\"\n\nOs detalhes específicos desaparecem, mas o entendimento permanece.\n\n## Filosofia\n\nEsses fundamentos representam conhecimento duradouro:\n\n- **CDP** é o protocolo nativo do Chrome e continua a evoluir\n- **Async/await** é o padrão do Python para concorrência\n- **Sistemas de tipos** são cada vez mais importantes no Python (PEP 484 em diante)\n\nAprender esses conceitos agrega valor ao longo de sua carreira de desenvolvimento.\n\n---\n\n## Pronto para Construir Sua Base?\n\nComece com **[Chrome DevTools Protocol](./cdp.md)** para entender o protocolo que impulsiona tudo. Em seguida, progrida pela Camada de Conexão e Sistema de Tipos para completar seu entendimento fundamental.\n\n**É aqui que a automação se torna engenharia.**\n\n---\n\n!!! tip \"Após Completar os Fundamentos\"\n    Uma vez que você dominar esses conceitos, você os verá em **toda parte** na arquitetura do Pydoll:\n    \n    - Browser/Tab/WebElement todos usam a **Camada de Conexão**\n    - Eventos de rede todos seguem o **modelo de eventos do CDP**\n    - Todas as respostas usam **TypedDict** para segurança de tipos\n    \n    Os fundamentos não estão separados do Pydoll, eles **são** a fundação do Pydoll."
  },
  {
    "path": "docs/pt/deep-dive/fundamentals/typing-system.md",
    "content": "# O Sistema de Tipos do Python e o Pydoll\n\nO Pydoll utiliza extensivamente o sistema de tipos do Python para fornecer excelente suporte de IDE, detectar erros precocemente e tornar a API autodocumentada. Este guia explica o básico das dicas de tipo (type hints) e como o Pydoll as utiliza para aprimorar sua experiência de desenvolvimento.\n\n## O Básico de Dicas de Tipo (Type Hints)\n\nDicas de tipo são anotações opcionais que especificam o tipo de valor que uma variável, parâmetro ou valor de retorno deve ter. Elas não afetam o comportamento em tempo de execução, mas habilitam ferramentas poderosas.\n\n### Dicas de Tipo Simples\n\n```python\n# Tipos básicos\nname: str = \"Pydoll\"\nport: int = 9222\nis_headless: bool = False\nquality: float = 0.85\n\n# Anotações de função\ndef navigate(url: str, timeout: int = 30) -> bool:\n    # ... implementação\n    return True\n```\n\n### Tipos de Contêineres\n\n```python\nfrom typing import List, Dict, Optional\n\n# Listas e dicionários\nurls: List[str] = ['https://example.com', 'https://google.com']\nheaders: Dict[str, str] = {'User-Agent': 'MyBot/1.0'}\n\n# Valores opcionais (podem ser None)\ntarget_id: Optional[str] = None\n\n# Sintaxe moderna (Python 3.9+)\nurls: list[str] = ['https://example.com']\nheaders: dict[str, str] = {'User-Agent': 'MyBot/1.0'}\n```\n\n!!! tip \"Sintaxe do Python 3.9+\"\n    O código-fonte do Pydoll usa a sintaxe mais antiga `List[]`, `Dict[]` para compatibilidade retroativa, mas você pode usar `list[]`, `dict[]` em minúsculas no seu código se estiver usando Python 3.9+.\n\n## TypedDict: Dicionários Estruturados\n\nO `TypedDict` permite definir estruturas de dicionário com chaves e tipos de valor específicos. Isso é **amplamente utilizado** nas definições de protocolo CDP do Pydoll.\n\n### TypedDict Básico\n\n```python\nfrom typing import TypedDict\n\nclass UserInfo(TypedDict):\n    name: str\n    age: int\n    email: str\n\n# A IDE sabe exatamente quais chaves existem\nuser: UserInfo = {\n    'name': 'Alice',\n    'age': 30,\n    'email': 'alice@example.com'\n}\n\n# O Autocomplete funciona!\nprint(user['name'])  # IDE sugere: name, age, email\n```\n\n### Como o Pydoll Usa o TypedDict\n\nO Pydoll define **cada comando, resposta e evento do CDP** como um TypedDict. Isso significa que sua IDE sabe exatamente quais propriedades estão disponíveis:\n\n```python\n# De pydoll/protocol/page/methods.py\nclass CaptureScreenshotParams(TypedDict, total=False):\n    \"\"\"Parâmetros para captureScreenshot.\"\"\"\n    format: ScreenshotFormat\n    quality: int\n    clip: Viewport\n    fromSurface: bool\n    captureBeyondViewport: bool\n    optimizeForSpeed: bool\n\nclass CaptureScreenshotResult(TypedDict):\n    \"\"\"Resultado para o comando captureScreenshot.\"\"\"\n    data: str\n```\n\nQuando você chama métodos que retornam respostas do CDP, sua IDE autocompleta as chaves da resposta:\n\n```python\nasync def example():\n    response = await tab.take_screenshot(as_base64=True)\n    \n    # A IDE sabe que esta é uma CaptureScreenshotResponse\n    # e sugere 'result' -> 'data'\n    screenshot_data = response['result']['data']  # Autocomplete completo!\n```\n\n### Campos Opcionais vs Obrigatórios\n\nO TypedDict suporta campos opcionais usando `NotRequired[]`:\n\n```python\nfrom typing import TypedDict, NotRequired\n\n# De pydoll/protocol/network/methods.py\nclass GetCookiesParams(TypedDict):\n    \"\"\"Parâmetros para recuperar cookies do navegador.\"\"\"\n    urls: NotRequired[list[str]]  # Este campo é opcional\n```\n\nA flag `total=False` torna **todos** os campos opcionais:\n\n```python\nclass CaptureScreenshotParams(TypedDict, total=False):\n    format: ScreenshotFormat  # Todos os campos são opcionais\n    quality: int\n    clip: Viewport\n```\n\n!!! info \"Mágica do Autocomplete\"\n    Quando você digita `response['`, sua IDE mostra todas as chaves disponíveis com seus tipos. Este é o superpoder do TypedDict em ação!\n\n## Enums: Constantes com Tipo Seguro (Type-Safe)\n\nEnums (enumerações) fornecem constantes com tipo seguro que sua IDE pode autocompletar. O Pydoll os usa extensivamente para valores CDP.\n\n### Enums Básicos\n\n```python\nfrom enum import Enum\n\nclass ScreenshotFormat(str, Enum):\n    JPEG = 'jpeg'\n    PNG = 'png'\n    WEBP = 'webp'\n\n# IDE autocompleta os formatos disponíveis\nformat = ScreenshotFormat.PNG  # O tipo é ScreenshotFormat\nprint(format.value)  # 'png'\n```\n\n### Uso de Enums no Pydoll\n\n```python\nfrom pydoll.constants import Key\nfrom pydoll.protocol.page.types import ScreenshotFormat\nfrom pydoll.protocol.input.types import KeyModifier\n\n# Encontrando elementos - usa kwargs, não enums\nelement = await tab.find(id='submit-btn')\nelement = await tab.find(class_name='btn-primary')\nelement = await tab.find(tag_name='button')\n\n# Entrada de teclado - IDE sugere todas as teclas\nawait element.press_keyboard_key(Key.ENTER)\nawait element.press_keyboard_key(Key.TAB)\nawait element.press_keyboard_key(Key.ESCAPE)\n\n# Modificadores são enums inteiros (para teclas especiais)\nawait element.press_keyboard_key(Key.TAB, modifiers=KeyModifier.SHIFT)\n\n# Enum de formato de captura de tela\nawait tab.take_screenshot('file.webp', format=ScreenshotFormat.WEBP)\n```\n\n!!! tip \"Autocomplete de Enum\"\n    Digite `Key.` ou `ScreenshotFormat.` e sua IDE mostrará todas as opções disponíveis. Chega de memorizar strings!\n\n## Sobrecarga de Funções (Function Overloads)\n\nSobrecargas permitem que uma função retorne tipos diferentes com base em seus parâmetros. O Pydoll usa isso para fornecer informações de tipo precisas.\n\n### Exemplo Básico de Sobrecarga\n\n```python\nfrom typing import overload\n\n# Assinaturas de sobrecarga (não executadas)\n@overload\ndef process(data: str) -> str: ...\n\n@overload\ndef process(data: int) -> int: ...\n\n# Implementação real\ndef process(data):\n    return data * 2\n\n# A IDE conhece os tipos de retorno\nresult1 = process(\"hello\")  # Tipo: str\nresult2 = process(42)       # Tipo: int\n```\n\n### Uso de Sobrecarga no Pydoll\n\nOs métodos `find()` e `query()` retornam tipos diferentes dependendo do parâmetro `find_all`:\n\n```python\n# De pydoll/elements/mixins/find_elements_mixin.py\nclass FindElementsMixin:\n    @overload\n    async def find(\n        self, find_all: Literal[False] = False, **kwargs\n    ) -> WebElement: ...\n    \n    @overload\n    async def find(\n        self, find_all: Literal[True], **kwargs\n    ) -> list[WebElement]: ...\n    \n    async def find(\n        self, find_all: bool = False, **kwargs\n    ) -> Union[WebElement, list[WebElement]]:\n        # Implementação...\n```\n\nNo seu código:\n\n```python\n# find_all=False (padrão) - A IDE sabe que o tipo de retorno é WebElement\nbutton = await tab.find(id='submit-btn')\nawait button.click()  # Métodos de elemento único disponíveis!\n\n# find_all=True - A IDE sabe que o tipo de retorno é list[WebElement]\nbuttons = await tab.find(class_name='btn', find_all=True)\nfor btn in buttons:  # A IDE sabe que isso é uma lista!\n    await btn.click()\n\n# O mesmo com query()\nelement = await tab.query('#submit-btn')  # Tipo: WebElement\nelements = await tab.query('.btn', find_all=True)  # Tipo: list[WebElement]\n```\n\n!!! tip \"Inferência de Tipo Inteligente\"\n    Sua IDE sabe automaticamente se você está obtendo um único elemento ou uma lista com base no parâmetro `find_all`. Não é necessário casting ou asserções de tipo!\n\n## Tipos Genéricos (Generic Types)\n\nGenéricos são como \"contêineres de tipo\" que funcionam com tipos diferentes enquanto preservam a informação de tipo. Pense neles como modelos que se adaptam ao que você coloca dentro.\n\n### Entendendo Genéricos: Uma Analogia Simples\n\nImagine uma `Caixa` que pode conter qualquer coisa. Sem genéricos:\n\n```python\n# Sem genéricos - A IDE não sabe o que está dentro\nclass Box:\n    def __init__(self, content):\n        self.content = content\n    \n    def get(self):\n        return self.content\n\nmy_box = Box(\"hello\")\nitem = my_box.get()  # Tipo: Unknown - poderia ser qualquer coisa!\n```\n\nCom genéricos:\n\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar('T')  # T é um \"marcador de posição de tipo\"\n\nclass Box(Generic[T]):\n    def __init__(self, content: T):\n        self.content = content\n    \n    def get(self) -> T:\n        return self.content\n\n# Agora a IDE sabe exatamente o que está dentro de cada caixa\nstring_box: Box[str] = Box(\"hello\")\nitem1 = string_box.get()  # Tipo: str\n\nnumber_box: Box[int] = Box(42)\nitem2 = number_box.get()  # Tipo: int\n\n# List é um genérico embutido\nnumbers: list[int] = [1, 2, 3]  # Lista que contém ints\nnames: list[str] = [\"Alice\", \"Bob\"]  # Lista que contém strs\n```\n\n!!! tip \"Genéricos Simplificam Dicas de Tipo\"\n    Em vez de escrever `Union[List[str], List[int], List[float], ...]` para todo tipo de lista possível, genéricos permitem que você escreva um `list[T]` reutilizável que se adapta ao que você coloca dentro.\n\n### Exemplo de Genérico do Mundo Real\n\n```python\nfrom typing import TypeVar, Generic\n\nT = TypeVar('T')\n\nclass Response(Generic[T]):\n    \"\"\"Um wrapper de resposta de API genérico.\"\"\"\n    def __init__(self, data: T, status: int):\n        self.data = data\n        self.status = status\n    \n    def get_data(self) -> T:\n        return self.data\n\n# Cada resposta preserva seu tipo de dado\nuser_response: Response[dict] = Response({\"name\": \"Alice\"}, 200)\nuser_data = user_response.get_data()  # Tipo: dict\n\ncount_response: Response[int] = Response(42, 200)\ncount = count_response.get_data()  # Tipo: int\n```\n\n### Como o Pydoll Usa Genéricos\n\nO sistema de comandos CDP do Pydoll usa genéricos para garantir que o tipo de resposta corresponda ao comando:\n\n```python\n# De pydoll/protocol/base.py\nfrom typing import Generic, TypeVar\n\nT_CommandParams = TypeVar('T_CommandParams')\nT_CommandResponse = TypeVar('T_CommandResponse')\n\nclass Command(TypedDict, Generic[T_CommandParams, T_CommandResponse]):\n    \"\"\"Estrutura base para todos os comandos.\"\"\"\n    id: NotRequired[int]\n    method: str\n    params: NotRequired[T_CommandParams]\n\nclass Response(TypedDict, Generic[T_CommandResponse]):\n    \"\"\"Estrutura base para todas as respostas.\"\"\"\n    id: int\n    result: T_CommandResponse\n```\n\nIsso significa que quando você executa um comando, o tipo de resposta é automaticamente inferido:\n\n```python\n# PageCommands.navigate retorna Command[NavigateParams, NavigateResult]\ncommand = PageCommands.navigate('https://example.com')\n\n# ConnectionHandler.execute_command preserva o tipo genérico\nresponse = await connection_handler.execute_command(command)\n\n# A IDE sabe que response['result'] é NavigateResult (não apenas \"qualquer dict\")\nframe_id = response['result']['frameId']  # Autocomplete funciona!\nloader_id = response['result']['loaderId']  # Todos os campos são conhecidos!\n```\n\n!!! info \"Por que Genéricos Importam no Pydoll\"\n    Sem genéricos, cada resposta CDP seria apenas tipada como `dict[str, Any]`, e você perderia todo o autocomplete. Com genéricos, a IDE sabe a estrutura exata de cada resposta com base em qual comando você enviou.\n\n## Tipos de União (Union Types)\n\nUniões representam valores que podem ser de um de vários tipos:\n\n```python\nfrom typing import Union\n\n# Pode ser string ou int\nidentifier: Union[str, int] = \"user-123\"\nidentifier = 456  # Também válido\n\n# Sintaxe moderna (Python 3.10+)\nidentifier: str | int = \"user-123\"\n```\n\n### Uso de União no Pydoll\n\n```python\n# Caminhos de arquivo podem ser strings ou objetos Path\nfrom pathlib import Path\n\nasync def upload_file(files: Union[str, Path, list[Union[str, Path]]]):\n    # Lida com múltiplos tipos de entrada\n    pass\n\n# Todos estes funcionam:\nawait tab.expect_file_chooser('/path/to/file.txt')\nawait tab.expect_file_chooser(Path('/path/to/file.txt'))\nawait tab.expect_file_chooser(['/file1.txt', Path('/file2.txt')])\n```\n\n## Benefícios Práticos no Pydoll\n\n### 1. Autocomplete Inteligente\n\nSua IDE sugere chaves, métodos e valores disponíveis:\n\n```python\nfrom pydoll.protocol.page.events import PageEvent\nfrom pydoll.protocol.network.types import ResourceType\nfrom pydoll.protocol.input.types import KeyModifier\nfrom pydoll.constants import Key\n\n# Autocomplete para nomes de eventos\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, callback)\nawait tab.on(PageEvent.JAVASCRIPT_DIALOG_OPENING, callback)\n\n# Autocomplete para tipos de recursos\nawait tab.enable_fetch_events(resource_type=ResourceType.XHR)\nawait tab.enable_fetch_events(resource_type=ResourceType.DOCUMENT)\n\n# Autocomplete para teclas\nawait element.press_keyboard_key(Key.ENTER)\nawait element.press_keyboard_key(Key.TAB, modifiers=KeyModifier.SHIFT)\n\n# Autocomplete para kwargs em find()\nelement = await tab.find(id='submit-btn')  # IDE sugere: id, class_name, tag_name, etc.\n```\n\n### 2. Pegue Erros Cedo\n\nVerificadores de tipo como mypy ou Pylance pegam erros antes do tempo de execução:\n\n```python\n# Verificador de tipo pega isso\nawait tab.take_screenshot('file.png', quality='high')  # Erro: quality deve ser int\n\n# Verificador de tipo pega isso\nevent = await tab.find(id='button')\nawait tab.on(event, callback)  # Erro: event é WebElement, não str\n\n# Correto\nawait tab.take_screenshot('file.png', quality=90)\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, callback)\n```\n\n### 3. Código Autodocumentado\n\nOs tipos servem como documentação embutida:\n\n```python\n# Você sabe imediatamente o que cada parâmetro espera\nasync def take_screenshot(\n    self,\n    path: Optional[str] = None,\n    quality: int = 100,\n    beyond_viewport: bool = False,\n    as_base64: bool = False,\n) -> Optional[str]:\n    pass\n```\n\n### 4. Navegação em Respostas CDP\n\nNavegue em respostas CDP complexas com confiança:\n\n```python\n# De pydoll/protocol/browser/methods.py\nclass GetVersionResult(TypedDict):\n    protocolVersion: str\n    product: str\n    revision: str\n    userAgent: str\n    jsVersion: str\n\n# No seu código\nversion_info = await browser.get_version()\n\n# IDE sugere todas as chaves disponíveis\nprint(version_info['product'])         # Autocomplete!\nprint(version_info['userAgent'])       # Autocomplete!\nprint(version_info['protocolVersion']) # Autocomplete!\n```\n\n## Verificando Tipos no Seu Código\n\n### Usando Pylance (VS Code)\n\nO Pylance fornece verificação de tipos em tempo real no VS Code:\n\n1.  Instale a extensão Pylance\n2.  Defina o modo de verificação de tipos nas configurações:\n\n```json\n{\n    \"python.analysis.typeCheckingMode\": \"basic\"  // ou \"strict\"\n}\n```\n\nAgora você obtém feedback instantâneo:\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Pylance mostra os tipos dos parâmetros enquanto você digita\n        await tab.go_to('https://example.com', timeout=30)\n        \n        # Pylance avisa sobre tipos errados\n        await tab.take_screenshot(quality='high')  # Aviso!\n```\n\n### Usando mypy\n\nExecute o mypy para verificar seu projeto inteiro:\n\n```bash\npip install mypy\nmypy your_script.py\n```\n\nExemplo de saída:\n\n```\nyour_script.py:10: error: Argument \"quality\" to \"take_screenshot\" has incompatible type \"str\"; expected \"int\"\nFound 1 error in 1 file (checked 1 source file)\n```\n\n## Sistema de Tipos de Protocolo do Pydoll\n\nO diretório `protocol/` do Pydoll contém definições de tipo abrangentes para todo o Chrome DevTools Protocol:\n\n```\npydoll/protocol/\n├── base.py              # Tipos genéricos Command, Response, CDPEvent\n├── browser/\n│   ├── events.py        # Enum BrowserEvent, TypedDicts de parâmetros de evento\n│   ├── methods.py       # Enums de métodos do Browser, TypedDicts de parâmetro/resultado\n│   └── types.py         # Tipos do domínio Browser (Bounds, PermissionType, etc.)\n├── dom/\n│   ├── events.py        # Definições de eventos DOM\n│   ├── methods.py       # Definições de comandos DOM\n│   └── types.py         # Tipos DOM (Node, BackendNode, etc.)\n├── page/\n│   ├── events.py        # Eventos de Page (LOAD_EVENT_FIRED, etc.)\n│   ├── methods.py       # Métodos de Page (navigate, captureScreenshot, etc.)\n│   └── types.py         # Tipos de Page (Frame, ScreenshotFormat, etc.)\n├── network/\n│   └── ...              # Tipos do domínio Network\n└── ...                  # Outros domínios CDP\n```\n\n### Exemplo: Fluxo de Tipo Completo\n\nVamos rastrear um fluxo de tipo completo, do comando à resposta:\n\n```python\n# 1. Enum de Método (protocol/page/methods.py)\nclass PageMethod(str, Enum):\n    CAPTURE_SCREENSHOT = 'Page.captureScreenshot'\n\n# 2. TypedDict de Parâmetro (protocol/page/methods.py)\nclass CaptureScreenshotParams(TypedDict, total=False):\n    format: ScreenshotFormat\n    quality: int\n    clip: Viewport\n\n# 3. TypedDict de Resultado (protocol/page/methods.py)\nclass CaptureScreenshotResult(TypedDict):\n    data: str\n\n# 4. Criação do Comando (commands/page_commands.py)\nclass PageCommands:\n    @staticmethod\n    def capture_screenshot(\n        format: Optional[ScreenshotFormat] = None,\n        quality: Optional[int] = None,\n        ...\n    ) -> Command[CaptureScreenshotParams, CaptureScreenshotResult]:\n        return {\n            'method': PageMethod.CAPTURE_SCREENSHOT,\n            'params': {...}\n        }\n\n# 5. Uso na Tab (browser/tab.py)\nclass Tab:\n    async def take_screenshot(...) -> Optional[str]:\n        response: CaptureScreenshotResponse = await self._execute_command(\n            PageCommands.capture_screenshot(...)\n        )\n        screenshot_data = response['result']['data']  # Totalmente tipado!\n        return screenshot_data\n```\n\nCada etapa mantém a informação de tipo, dando a você autocomplete e verificação de tipos por toda parte!\n\n## Melhores Práticas\n\n### 1. Deixe os Tipos do Pydoll Guiarem Você\n\nNão lute contra os tipos, eles estão lá para ajudar:\n\n```python\n# Bom: Use kwargs (IDE autocompleta nomes de parâmetros)\nelement = await tab.find(id='submit-btn')\nbutton = await tab.find(class_name='btn-primary')\n\n# Bom: Use enums onde aplicável\nfrom pydoll.constants import Key\nawait element.press_keyboard_key(Key.ENTER)\n\n# Evite: Strings mágicas\nawait element.press_keyboard_key('Enter')  # Sem autocomplete, propenso a erros\n```\n\n### 2. Explore os Tipos na Sua IDE\n\nPasse o mouse sobre as variáveis para ver seus tipos:\n\n```python\n# Passe o mouse sobre 'response' para ver: Response[CaptureScreenshotResult]\nresponse = await tab._execute_command(PageCommands.capture_screenshot(...))\n\n# Passe o mouse sobre 'data' para ver: str\ndata = response['result']['data']\n```\n\n\n### 3. Não Anote em Excesso\n\nA inferência de tipos do Python é inteligente, não anote tudo:\n\n```python\n# Demais\nname: str = \"Alice\"\ncount: int = 5\nis_active: bool = True\n\n# Deixe o Python inferir literais simples\nname = \"Alice\"\ncount = 5\nis_active = True\n\n# Anote quando o tipo não for óbvio\nfrom typing import Optional\n\nresult: Optional[WebElement] = await tab.find(id='missing', raise_exc=False)\n```\n\n## Aprenda Mais\n\nPara um entendimento mais profundo do sistema de tipos do Python e do protocolo CDP:\n\n- **[Documentação de typing do Python](https://docs.python.org/3/library/typing.html)**: Referência oficial de typing do Python\n- **[PEP 484](https://peps.python.org/pep-0484/)**: A proposta original das dicas de tipo\n- **[Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)**: Documentação do CDP\n- **[Análise Profunda: CDP](./cdp.md)**: Como o Pydoll implementa o CDP\n- **[Referência da API: Protocol](../api/protocol/base.md)**: Definições de tipo de protocolo do Pydoll\n\nO sistema de tipos transforma o Pydoll de uma simples biblioteca de automação em um framework **seguro em tipos, autodocumentado e amigável à IDE**. Ele pega bugs antes que aconteçam e torna a exploração da API muito mais fácil!"
  },
  {
    "path": "docs/pt/deep-dive/guides/index.md",
    "content": "# Guias Práticos\n\n**A teoria encontra a prática, padrões acionáveis para desafios reais de automação.**\n\nEnquanto as outras seções de Análise Profunda exploram **fundamentos** e **arquitetura**, esta seção fornece **guias práticos e testados em batalha** para cenários comuns de automação. Estes não são exercícios acadêmicos, são padrões refinados através do uso em produção.\n\n## O Propósito dos Guias\n\nVocê aprendeu:\n\n- **[Fundamentos](../fundamentals/cdp.md)** - CDP, async, tipos\n- **[Arquitetura](../architecture/browser-domain.md)** - Padrões de design interno\n- **[Rede](../network/index.md)** - Protocolos e proxies\n- **[Fingerprinting](../fingerprinting/index.md)** - Detecção e evasão\n\nE agora? **Como você aplica esse conhecimento a problemas reais?**\n\nÉ para isso que servem os guias: **fazer a ponte entre teoria e prática**.\n\n!!! quote \"Sabedoria Prática\"\n    **\"Na teoria, teoria e prática são a mesma coisa. Na prática, não são.\"** - Yogi Berra\n    \n    Os guias destilam conhecimento técnico complexo em **padrões acionáveis** que você pode usar imediatamente. Eles mostram **o que funciona** em produção, não apenas o que é teoricamente possível.\n\n## Guias Atuais\n\n### Seletores CSS vs XPath\n**[→ Leia o Guia de Seletores](./selectors-guide.md)**\n\n**O eterno debate, resolvido com dados e melhores práticas.**\n\nEscolher entre seletores CSS e XPath não é sobre preferência. É sobre entender **trocas (tradeoffs)**, **características de desempenho** e **manutenibilidade**.\n\n**O que você aprenderá**:\n\n- **Comparação de sintaxe** - Exemplos lado a lado para padrões comuns\n- **Benchmarks de desempenho** - Medições reais, não mitos\n- **Poder vs simplicidade** - Quando o CSS não é suficiente (correspondência de texto, eixos)\n- **Suporte do navegador** - Compatibilidade e casos extremos\n- **Melhores práticas** - Quando usar cada um, anti-padrões a evitar\n- **Exemplos complexos** - Desafios de seletores do mundo real resolvidos\n\n**Por que isso importa**: A localização de elementos é a **base** da automação. Escolha a ferramenta errada, e você lutará com seus seletores para sempre. Escolha sabiamente, e a automação se torna direta.\n\n---\n\n## Em Breve\n\n### Asyncio e Automação Concorrente\n**Chegando em futuras versões**\n\n**Análise profunda do asyncio do Python: internos do loop de eventos, padrões práticos de concorrência e exemplos do mundo real.**\n\nEntender o asyncio é fundamental para o Pydoll. Este guia fornece uma análise abrangente do loop de eventos do Python, primitivas de concorrência e como aplicá-las à automação de navegador sem armadilhas (footguns).\n\n**Cobrirá**:\n\n- **Internos do Loop de Eventos**: Como `asyncio.run()` funciona, agendamento de tarefas e fluxo de execução\n- **Análise Profunda de Async/Await**: Corrotinas, futuros (futures) e a máquina de estados assíncrona\n- **Primitivas de Concorrência**: `gather()`, `create_task()`, `TaskGroup`, e quando usar cada um\n- **Limitação de Taxa (Rate Limiting)**: Semáforos, filas e estratégias de throttling\n- **Exemplos do Mundo Real**: Raspagem multi-abas, preenchimento de formulário paralelo, instâncias de navegador coordenadas\n- **Armadilhas Comuns**: Bloquear o loop de eventos, cancelamento de tarefas, propagação de exceções\n- **Análise de Desempenho**: Profiling de código assíncrono, identificando gargalos, otimizando I/O\n\n**Por que isso importa**: O Asyncio move a arquitetura do Pydoll. Domine-o, e você desbloqueará a verdadeira automação concorrente sem condições de corrida (race conditions) ou corrupção de estado.\n\n---\n\n### Padrões Arquiteturais e Seletores Robustos\n**Chegando em futuras versões**\n\n**Padrão PageObject, seletores sustentáveis e abordagens arquiteturais para automação escalável.**\n\nVá além de scripts ad-hoc para arquiteturas de automação estruturadas e sustentáveis. Aprenda padrões que escalam de scripts simples para sistemas de produção.\n\n**Cobrirá**:\n\n- **Padrão PageObject**: Encapsulando estrutura da página, reduzindo duplicação, melhorando manutenibilidade\n- **Estratégias de Seletores Robustos**: Construindo seletores que sobrevivem a mudanças na página, evitando localizadores frágeis\n- **Abstração de Componentes**: Componentes reutilizáveis para padrões comuns de UI (modais, dropdowns, tabelas)\n- **Estratégias de Espera**: Padrões de espera inteligentes além de simples timeouts\n- **Gerenciamento de Estado**: Gerenciando o estado da automação através de páginas e fluxos\n- **Padrões de Teste**: Como estruturar código de automação para testabilidade\n- **Arquitetura do Mundo Real**: Estrutura e organização de projeto prontas para produção\n\n**Por que isso importa**: A diferença entre scripts descartáveis e sistemas de automação sustentáveis é a arquitetura. Aprenda padrões que tornam seu código resiliente a mudanças.\n\n---\n\n## Filosofia dos Guias\n\nOs guias seguem princípios consistentes:\n\n### 1. Código Pronto para Produção\nTodos os exemplos são **completos e testados**, não pseudocódigo ou demonstrações simplificadas. Você pode copiar, colar e adaptar às suas necessidades.\n\n### 2. Cenários do Mundo Real\nOs guias abordam **problemas reais** encontrados na automação de produção, não exemplos artificiais.\n\n### 3. Análise de Tradeoffs (Trocas)\nQuando existem múltiplas abordagens, os guias as **comparam** objetivamente com prós/contras, não apenas \"aqui está uma maneira\".\n\n### 4. Complexidade Progressiva\nComece simples, adicione complexidade incrementalmente. Padrão básico primeiro, depois casos extremos e variações avançadas.\n\n### 5. Anti-Padrões Destacados\nMostra **o que NÃO fazer** explicitamente, erros comuns pegos através de revisão de código ou depuração em produção.\n\n## Como Usar os Guias\n\nGuias são **material de referência**, não tutoriais sequenciais:\n\n- **Examine** em busca de padrões relevantes para o seu problema atual\n- **Marque (Bookmark)** guias que você precisará repetidamente\n- **Adapte** exemplos ao seu contexto específico\n- **Combine** padrões de múltiplos guias\n\nNão leia sequencialmente de capa a capa.\nNão copie cegamente sem entender os tradeoffs.\nNão use padrões desatualizados (verifique a data de publicação).\n\n## Contribuindo com Guias\n\nTem um padrão que vale a pena compartilhar? Os guias são **impulsionados pela comunidade**:\n\n**O que faz um bom guia**:\n\n- Resolve um **problema real** encontrado em produção\n- Fornece **código funcional**, não apenas conceitos\n- Compara **múltiplas abordagens** com tradeoffs\n- Destaca **erros comuns** explicitamente\n- Explica **por quê**, não apenas **como**\n\nVeja [Contribuindo](../../CONTRIBUTING.md) para diretrizes de submissão.\n\n## Guias vs Documentação de Funcionalidades\n\n**Confuso sobre a diferença?**\n\n|| Documentação de Funcionalidades | Guias de Análise Profunda |\n|---|---|---|\n| **Propósito** | Ensinar o que o Pydoll pode fazer | Mostrar como resolver problemas |\n| **Escopo** | Método/funcionalidade única | Múltiplas funcionalidades combinadas |\n| **Profundidade** | Referência de API + exemplos | Padrões + tradeoffs + melhores práticas |\n| **Ordem** | Estruturado por componente | Estruturado por problema |\n| **Exemplos** | Simples, isolados | Complexos, prontos para produção |\n\n**Use Funcionalidades para**: Aprender a API do Pydoll\n**Use Guias para**: Resolver desafios reais de automação\n\n## Além dos Guias\n\nApós dominar os padrões práticos:\n\n- **[Arquitetura](../architecture/browser-domain.md)** - Entenda por que os padrões funcionam\n- **[Rede](../network/index.md)** - Otimização em nível de rede\n- **[Fingerprinting](../fingerprinting/evasion-techniques.md)** - Técnicas anti-detecção\n\nGuias fornecem **valor imediato**. Arquitetura fornece **entendimento profundo**. Ambos tornam você eficaz.\n\n---\n\n## Pronto para Padrões Práticos?\n\nComece com **[Seletores CSS vs XPath](./selectors-guide.md)** para dominar a localização de elementos, a fundação de toda automação.\n\n**Mais guias em breve. Marque o repositório com estrela (Star) para se manter atualizado!**\n\n---\n\n!!! tip \"Solicite um Guia\"\n    Tem um padrão de automação que você gostaria de ver documentado? Abra uma issue intitulada \"Solicitação de Guia: [Tópico]\" descrevendo:\n    \n    - O problema que você está tentando resolver\n    - O que você tentou até agora\n    - Por que a documentação existente não cobre isso\n    \n    Nós priorizamos guias com base na necessidade da comunidade.\n\n## Referência Rápida\n\n**Disponível Agora:**\n\n- [Seletores CSS vs XPath](./selectors-guide.md)\n\n**Em Breve:**\n\n- Asyncio e Automação Concorrente\n- Padrões Arquiteturais e Seletores Robustos\n\n**Cronograma**: Novos guias adicionados com base no feedback da comunidade e aprendizados de produção."
  },
  {
    "path": "docs/pt/deep-dive/guides/selectors-guide.md",
    "content": "# Seletores CSS vs XPath: Um Guia Completo\n\nAo usar o método `query()`, você tem duas poderosas linguagens de seletores à sua disposição: Seletores CSS e XPath. Entender quando e como usar cada um é crucial para a localização eficaz de elementos.\n\n## Diferenças Fundamentais\n\n| Aspecto | Seletor CSS | XPath |\n|---|---|---|\n| **Sintaxe** | Simples, semelhante a CSS | Linguagem de caminho XML |\n| **Desempenho** | Mais rápido (suporte nativo do navegador) | Ligeiramente mais lento |\n| **Direção** | Atravessa apenas para baixo e lateralmente | Pode atravessar em qualquer direção |\n| **Correspondência de Texto** | Limitada (pseudo-seletores) | Funções de texto poderosas |\n| **Complexidade** | Melhor para casos simples a moderados | Excelente em relacionamentos complexos |\n| **Legibilidade** | Mais intuitivo para desenvolvedores web | Curva de aprendizado mais íngreme |\n\n## Quando Usar Seletores CSS\n\nSeletores CSS são ideais para:\n\n- Seleção simples de elementos por ID, classe ou tag\n- Relacionamentos diretos pai-filho\n- Correspondência de atributos com padrões simples\n- Cenários críticos de desempenho\n- Ao atravessar para baixo no DOM\n\n```python\n# Exemplos de CSS limpos e performáticos\nawait tab.query(\"#login-form\")\nawait tab.query(\".submit-button\")\nawait tab.query(\"div.container > p.intro\")\nawait tab.query(\"input[type='email'][required]\")\nawait tab.query(\"ul.menu li:first-child\")\n```\n\n## Quando Usar XPath\n\nXPath é ideal para:\n\n- Correspondência de texto complexa e buscas parciais de texto\n- Atravessar para cima até elementos pais\n- Encontrar elementos relativos a irmãos\n- Lógica condicional em seletores\n- Relacionamentos DOM complexos\n\n```python\n# Exemplos poderosos de XPath\nawait tab.query(\"//button[contains(text(), 'Submit')]\")\nawait tab.query(\"//input[@name='email']/parent::div\")\nawait tab.query(\"//td[text()='John']/following-sibling::td[2]\")\nawait tab.query(\"//div[contains(@class, 'product') and @data-price > 100]\")\n```\n\n## Referência de Sintaxe de Seletor CSS\n\n### Seletores Básicos\n\n```python\n# Seletor de elemento\nawait tab.query(\"div\")              # Primeiro elemento <div>\nawait tab.query(\"div\", find_all=True)  # Todos os elementos <div>\nawait tab.query(\"button\")           # Primeiro elemento <button>\n\n# Seletor de ID\nawait tab.query(\"#username\")        # Elemento com id=\"username\"\n\n# Seletor de classe\nawait tab.query(\".submit-btn\")      # Primeiro elemento com class=\"submit-btn\"\nawait tab.query(\".submit-btn\", find_all=True)  # Todos os elementos com a classe\nawait tab.query(\".btn.primary\")     # Primeiro elemento com ambas as classes\n\n# Seletor universal\nawait tab.query(\"*\", find_all=True) # Todos os elementos\n```\n\n### Combinadores\n\n```python\n# Combinador descendente (espaço)\nawait tab.query(\"div p\")            # Primeiro <p> dentro de <div>\nawait tab.query(\"div p\", find_all=True)  # Todos os <p> dentro de <div> (qualquer profundidade)\n\n# Combinador filho (>)\nawait tab.query(\"div > p\")          # Primeiro <p> que é filho direto de <div>\nawait tab.query(\"div > p\", find_all=True)  # Todos os <p> que são filhos diretos\n\n# Combinador irmão adjacente (+)\nawait tab.query(\"h1 + p\")           # <p> imediatamente após <h1>\n\n# Combinador irmão geral (~)\nawait tab.query(\"h1 ~ p\")           # Primeiro <p> irmão após <h1>\nawait tab.query(\"h1 ~ p\", find_all=True)  # Todos os <p> irmãos após <h1>\n```\n\n### Seletores de Atributo\n\n```python\n# Atributo existe\nawait tab.query(\"input[required]\")                # Primeiro input com 'required'\nawait tab.query(\"input[required]\", find_all=True) # Todos os inputs com 'required'\n\n# Atributo igual\nawait tab.query(\"input[type='email']\")            # Primeiro input de email\nawait tab.query(\"input[type='email']\", find_all=True)  # Todos os inputs de email\n\n# Atributo contém palavra\nawait tab.query(\"div[class~='active']\")           # Primeiro div com classe 'active'\n\n# Atributo começa com\nawait tab.query(\"a[href^='https://']\")            # Primeiro link HTTPS\nawait tab.query(\"a[href^='https://']\", find_all=True)  # Todos os links HTTPS\n\n# Atributo termina com\nawait tab.query(\"img[src$='.png']\")               # Primeira imagem PNG\nawait tab.query(\"img[src$='.png']\", find_all=True)     # Todas as imagens PNG\n\n# Atributo contém substring\nawait tab.query(\"a[href*='example']\")             # Primeiro link com 'example'\nawait tab.query(\"a[href*='example']\", find_all=True)   # Todos os links com 'example'\n\n# Correspondência insensível a maiúsculas/minúsculas (case-insensitive)\nawait tab.query(\"input[type='text' i]\")           # Correspondência case-insensitive\n```\n\n### Pseudo-classes\n\n```python\n# Pseudo-classes estruturais\nawait tab.query(\"li:first-child\")                 # Primeiro <li> que é primeiro filho\nawait tab.query(\"li:last-child\")                  # Primeiro <li> que é último filho\nawait tab.query(\"li:nth-child(2)\")                # Primeiro <li> que é 2º filho\nawait tab.query(\"li:nth-child(odd)\", find_all=True)  # Todos os <li> ímpares\nawait tab.query(\"li:nth-child(even)\", find_all=True)  # Todos os <li> pares\nawait tab.query(\"li:nth-child(3n)\", find_all=True)    # A cada 3º <li>\n\n# Pseudo-classes baseadas em tipo\nawait tab.query(\"p:first-of-type\")                # Primeiro <p> entre irmãos\nawait tab.query(\"p:last-of-type\")                 # Último <p> entre irmãos\nawait tab.query(\"p:nth-of-type(2)\")               # Segundo <p> entre irmãos\n\n# Pseudo-classes de estado\nawait tab.query(\"input:enabled\")                  # Primeiro input habilitado\nawait tab.query(\"input:enabled\", find_all=True)   # Todos os inputs habilitados\nawait tab.query(\"input:disabled\")                 # Primeiro input desabilitado\nawait tab.query(\"input:checked\")                  # Primeiro checkbox/radio marcado\nawait tab.query(\"input:focus\")                    # Input atualmente focado\n\n# Outras pseudo-classes úteis\nawait tab.query(\"div:empty\")                      # Primeiro elemento vazio\nawait tab.query(\"div:empty\", find_all=True)       # Todos os elementos vazios\nawait tab.query(\"div:not(.exclude)\")              # Primeiro div sem a classe\nawait tab.query(\"div:not(.exclude)\", find_all=True)  # Todos os divs sem a classe\n```\n\n## Referência de Sintaxe XPath\n\n### Expressões de Caminho Básicas\n\n```python\n# Caminho absoluto (da raiz)\nawait tab.query(\"/html/body/div\")                 # Primeiro div no caminho exato\n\n# Caminho relativo (de qualquer lugar)\nawait tab.query(\"//div\")                          # Primeiro elemento <div>\nawait tab.query(\"//div\", find_all=True)           # Todos os elementos <div>\nawait tab.query(\"//div/p\")                        # Primeiro <p> dentro de qualquer <div>\nawait tab.query(\"//div/p\", find_all=True)         # Todos os <p> dentro de qualquer <div>\n\n# Nó atual\nawait tab.query(\"./div\")                          # Primeiro <div> relativo ao atual\n\n# Nó pai\nawait tab.query(\"..\")                             # Pai do nó atual\n```\n\n### Seleção de Atributo\n\n```python\n# Correspondência básica de atributo\nawait tab.query(\"//input[@type='email']\")         # Primeiro input de email\nawait tab.query(\"//input[@type='email']\", find_all=True)  # Todos os inputs de email\nawait tab.query(\"//div[@id='content']\")           # Div com id='content'\n\n# Múltiplos atributos\nawait tab.query(\"//input[@type='text' and @required]\")  # Primeira correspondência\nawait tab.query(\"//input[@type='text' and @required]\", find_all=True)  # Todas as correspondências\nawait tab.query(\"//div[@class='card' or @class='panel']\")  # Primeiro card ou panel\n\n# Atributo existe\nawait tab.query(\"//button[@disabled]\")            # Primeiro botão desabilitado\nawait tab.query(\"//button[@disabled]\", find_all=True)  # Todos os botões desabilitados\n```\n\n## Eixos (Axes) XPath (Navegação Direcional)\n\nO poder real do XPath vem de sua habilidade de navegar em qualquer direção através da árvore DOM.\n\n### Tabela de Referência de Eixos\n\n| Eixo | Direção | Descrição | Exemplo |\n|---|---|---|---|\n| `child::` | Para baixo | Apenas filhos diretos | `//div/child::p` |\n| `descendant::` | Para baixo | Todos os descendentes (qualquer profundidade) | `//div/descendant::a` |\n| `parent::` | Para cima | Pai imediato | `//input/parent::div` |\n| `ancestor::` | Para cima | Todos os ancestrais (qualquer profundidade) | `//span/ancestor::div` |\n| `following-sibling::` | Lateralmente | Irmãos após o atual | `//h1/following-sibling::p` |\n| `preceding-sibling::` | Lateralmente | Irmãos antes do atual | `//p/preceding-sibling::h1` |\n| `following::` | Para frente | Todos os nós após o atual | `//h1/following::*` |\n| `preceding::` | Para trás | Todos os nós antes do atual | `//h1/preceding::*` |\n| `ancestor-or-self::` | Para cima | Ancestrais + atual | `//div/ancestor-or-self::*` |\n| `descendant-or-self::` | Para baixo | Descendentes + atual | `//div/descendant-or-self::*` |\n| `self::` | Atual | Apenas o nó atual | `//div/self::div` |\n| `attribute::` | Atributo | Atributos do atual | `//div/attribute::class` |\n\n!!! info \"Sintaxe Abreviada\"\n    - `//div` é abreviação de `//descendant-or-self::div`\n    - `//div/p` é abreviação de `//div/child::p`\n    - `@id` é abreviação de `attribute::id`\n    - `..` é abreviação de `parent::node()`\n\n### Exemplos Práticos de Eixos\n\n```python\n# Navegar para o pai\nawait tab.query(\"//input[@name='email']/parent::div\")\nawait tab.query(\"//span[@class='error']/..\")       # Abreviação\n\n# Encontrar ancestral\nawait tab.query(\"//input/ancestor::form\")          # Primeiro <form> ancestral\nawait tab.query(\"//button/ancestor::div[@class='modal']\")\n\n# Navegação entre irmãos\nawait tab.query(\"//label[text()='Email:']/following-sibling::input\")\nawait tab.query(\"//h2/following-sibling::p[1]\")    # Primeiro <p> após <h2>\nawait tab.query(\"//h2/following-sibling::p\", find_all=True)  # Todos os <p> após <h2>\nawait tab.query(\"//button/preceding-sibling::input[last()]\")\n\n# Relacionamentos complexos\nawait tab.query(\"//tr/td[1]/following-sibling::td[2]\")  # 3ª célula na primeira linha\nawait tab.query(\"//tr/td[1]/following-sibling::td[2]\", find_all=True)  # 3ª célula em todas as linhas\n```\n\n## Funções XPath\n\n### Funções de Texto\n\n```python\n# Correspondência exata de texto\nawait tab.query(\"//button[text()='Submit']\")\n\n# Contém texto\nawait tab.query(\"//p[contains(text(), 'welcome')]\")\n\n# Começa com\nawait tab.query(\"//a[starts-with(@href, 'https://')]\")\n\n# Normalização de texto (remove espaços em branco extras)\nawait tab.query(\"//button[normalize-space(text())='Submit']\")\n\n# Comprimento da string\nawait tab.query(\"//input[string-length(@value) > 5]\")\n\n# Concatenação\nawait tab.query(\"//div[concat(@data-first, @data-last)='JohnDoe']\")\n```\n\n### Funções Numéricas\n\n```python\n# Correspondência de posição\nawait tab.query(\"//li[position()=1]\")              # Primeiro <li>\nawait tab.query(\"//li[position() > 3]\", find_all=True)  # Todos os <li> após o 3º\nawait tab.query(\"//li[last()]\")                    # Último <li>\nawait tab.query(\"//li[last()-1]\")                  # Penúltimo\n\n# Contagem\nawait tab.query(\"//ul[count(li) > 5]\")             # Primeiro <ul> com mais de 5 itens\nawait tab.query(\"//ul[count(li) > 5]\", find_all=True)  # Todos os <ul> com > 5 itens\n\n# Operações numéricas\nawait tab.query(\"//div[@data-price > 100]\")        # Primeiro div com preço > 100\nawait tab.query(\"//div[@data-price > 100]\", find_all=True)  # Todos os divs\nawait tab.query(\"//div[number(@data-stock) = 0]\")  # Primeiro com estoque = 0\n```\n\n### Funções Booleanas\n\n```python\n# Lógica booleana\nawait tab.query(\"//div[@visible='true' and @enabled='true']\")  # Primeira correspondência\nawait tab.query(\"//input[@type='text' or @type='email']\")  # Primeiro text ou email\nawait tab.query(\"//input[@type='text' or @type='email']\", find_all=True)  # Todos\nawait tab.query(\"//button[not(@disabled)]\")        # Primeiro botão habilitado\nawait tab.query(\"//button[not(@disabled)]\", find_all=True)  # Todos os botões habilitados\n\n# Verificações de existência\nawait tab.query(\"//div[child::p]\")                 # Primeiro div com filhos <p>\nawait tab.query(\"//div[child::p]\", find_all=True)  # Todos os divs com filhos <p>\nawait tab.query(\"//div[not(child::*)]\")            # Primeiro div vazio\nawait tab.query(\"//div[not(child::*)]\", find_all=True)  # Todos os divs vazios\n```\n\n## Predicados XPath\n\nPredicados filtram conjuntos de nós usando condições entre colchetes `[]`.\n\n```python\n# Predicados de posição\nawait tab.query(\"(//div)[1]\")                      # Primeiro <div> no documento\nawait tab.query(\"(//div)[last()]\")                 # Último <div> no documento\nawait tab.query(\"//ul/li[3]\")                      # Primeiro 3º <li> em um <ul>\nawait tab.query(\"//ul/li[3]\", find_all=True)       # Todos os 3º <li> em cada <ul>\n\n# Múltiplos predicados (lógica E)\nawait tab.query(\"//input[@type='text'][@required]\")  # Primeira correspondência\nawait tab.query(\"//div[@class='product'][position() < 4]\", find_all=True)  # Os 3 primeiros\n\n# Predicados de atributo\nawait tab.query(\"//div[@data-id='123']\")\nawait tab.query(\"//a[contains(@class, 'button')]\")  # Primeiro link correspondente\nawait tab.query(\"//input[starts-with(@name, 'user')]\")  # Primeiro input correspondente\n```\n\n## Exemplos do Mundo Real: Localização Complexa de Elementos\n\nVamos trabalhar com uma estrutura HTML realista para demonstrar seletores avançados.\n\n### Estrutura HTML de Exemplo\n\n```html\n<div class=\"dashboard\">\n    <header>\n        <h1>User Dashboard</h1>\n        <nav class=\"menu\">\n            <a href=\"/home\" class=\"active\">Home</a>\n            <a href=\"/profile\">Profile</a>\n            <a href=\"/settings\">Settings</a>\n        </nav>\n    </header>\n    \n    <main>\n        <section class=\"products\">\n            <h2>Available Products</h2>\n            <table id=\"products-table\">\n                <thead>\n                    <tr>\n                        <th>Product Name</th>\n                        <th>Price</th>\n                        <th>Stock</th>\n                        <th>Actions</th>\n                    </tr>\n                </thead>\n                <tbody>\n                    <tr data-product-id=\"101\">\n                        <td>Laptop</td>\n                        <td class=\"price\">$999</td>\n                        <td class=\"stock\">15</td>\n                        <td>\n                            <button class=\"btn-edit\">Edit</button>\n                            <button class=\"btn-delete\">Delete</button>\n                        </td>\n                    </tr>\n                    <tr data-product-id=\"102\">\n                        <td>Mouse</td>\n                        <td class=\"price\">$25</td>\n                        <td class=\"stock\">0</td>\n                        <td>\n                            <button class=\"btn-edit\">Edit</button>\n                            <button class=\"btn-delete\" disabled>Delete</button>\n                        </td>\n                    </tr>\n                    <tr data-product-id=\"103\">\n                        <td>Keyboard</td>\n                        <td class=\"price\">$75</td>\n                        <td class=\"stock\">8</td>\n                        <td>\n                            <button class=\"btn-edit\">Edit</button>\n                            <button class=\"btn-delete\">Delete</button>\n                        </td>\n                    </tr>\n                </tbody>\n            </table>\n        </section>\n        \n        <section class=\"user-form\">\n            <h2>User Information</h2>\n            <form id=\"user-form\">\n                <div class=\"form-group\">\n                    <label for=\"username\">Username:</label>\n                    <input type=\"text\" id=\"username\" name=\"username\" required>\n                    <span class=\"error-message\" style=\"display:none;\">Invalid username</span>\n                </div>\n                <div class=\"form-group\">\n                    <label for=\"email\">Email:</label>\n                    <input type=\"email\" id=\"email\" name=\"email\" required>\n                    <span class=\"error-message\" style=\"display:none;\">Invalid email</span>\n                </div>\n                <div class=\"form-group\">\n                    <input type=\"checkbox\" id=\"newsletter\" name=\"newsletter\">\n                    <label for=\"newsletter\">Subscribe to newsletter</label>\n                </div>\n                <button type=\"submit\" class=\"btn-primary\">Save Changes</button>\n                <button type=\"button\" class=\"btn-secondary\">Cancel</button>\n            </form>\n        </section>\n    </main>\n</div>\n```\n\n### Desafio 1: Encontrar Link de Navegação Ativo\n\n**Objetivo**: Encontrar o link de navegação atualmente ativo.\n\n```python\n# Seletor CSS\nactive_link = await tab.query(\"nav.menu a.active\")\n\n# XPath\nactive_link = await tab.query(\"//nav[@class='menu']//a[@class='active']\")\n\n# Obter seu texto\ntext = await active_link.text\nprint(text)  # \"Home\"\n```\n\n### Desafio 2: Encontrar Botão de Edição para Produto Específico\n\n**Objetivo**: Encontrar o botão \"Edit\" para o produto \"Mouse\" (sem saber sua posição na linha).\n\n```python\n# XPath (recomendado para este caso)\nedit_button = await tab.query(\n    \"//tr[td[text()='Mouse']]//button[contains(@class, 'btn-edit')]\"\n)\n\n# Alternativa: Usando following-sibling\nedit_button = await tab.query(\n    \"//td[text()='Mouse']/following-sibling::td//button[@class='btn-edit']\"\n)\n```\n\n!!! tip \"Por que XPath Aqui?\"\n    Seletores CSS não podem atravessar para cima para encontrar a linha e depois para baixo até o botão. A habilidade do XPath de se mover livremente pelo DOM torna isso trivial.\n\n### Desafio 3: Encontrar Todos os Produtos com Preço Acima de $50\n\n**Objetivo**: Obter todas as linhas da tabela onde o preço é maior que $50.\n\n```python\n# XPath com comparação numérica\nexpensive_products = await tab.query(\n    \"//tr[number(translate(td[@class='price'], '$,', '')) > 50]\",\n    find_all=True\n)\n\n# Versão mais legível: usando contains para casos mais simples\n# Isso encontra produtos com preço contendo valores específicos\nproducts = await tab.query(\"//tr[contains(td[@class='price'], '$75')]\", find_all=True)\n```\n\n!!! note \"Conversão de Texto para Número\"\n    A função `translate()` remove os caracteres `$` e `,`, então `number()` converte para numérico para comparação.\n\n### Desafio 4: Encontrar Todos os Produtos Fora de Estoque\n\n**Objetivo**: Encontrar todos os produtos onde o estoque é 0.\n\n```python\n# XPath\nout_of_stock = await tab.query(\n    \"//tr[td[@class='stock' and text()='0']]\",\n    find_all=True\n)\n\n# Alternativa: Encontrar todas as linhas e checar o estoque\nrows = await tab.query(\"//tbody/tr[td[@class='stock']/text()='0']\", find_all=True)\n```\n\n### Desafio 5: Encontrar Campo de Input pelo Seu Label\n\n**Objetivo**: Encontrar o input de email localizando seu label primeiro.\n\n```python\n# XPath usando atributo 'for' do label\nemail_input = await tab.query(\"//label[text()='Email:']/following-sibling::input\")\n\n# Alternativa: Usando o atributo for\nemail_input = await tab.query(\"//input[@id=(//label[text()='Email:']/@for)]\")\n\n# Mais genérico: Encontrar pelo texto do label\nusername_input = await tab.query(\n    \"//label[contains(text(), 'Username')]/following-sibling::input\"\n)\n```\n\n### Desafio 6: Encontrar Mensagem de Erro Próxima ao Campo de Email\n\n**Objetivo**: Obter o span de mensagem de erro que aparece ao lado do input de email.\n\n```python\n# XPath - encontrar irmão de erro do input de email\nerror_span = await tab.query(\n    \"//input[@id='email']/following-sibling::span[@class='error-message']\"\n)\n\n# Alternativa: Navegar a partir da div pai\nerror_span = await tab.query(\n    \"//input[@id='email']/parent::div//span[@class='error-message']\"\n)\n\n# Checar visibilidade\nis_visible = await error_span.is_visible()\n```\n\n### Desafio 7: Encontrar Botão de Envio (Não o de Cancelar)\n\n**Objetivo**: Encontrar o botão de envio, excluindo o botão de cancelar.\n\n```python\n# Seletor CSS (simples)\nsubmit_button = await tab.query(\"button[type='submit']\")\nsubmit_button = await tab.query(\"button.btn-primary\")\n\n# XPath com texto\nsubmit_button = await tab.query(\"//button[text()='Save Changes']\")\n\n# XPath excluindo outros\nsubmit_button = await tab.query(\n    \"//button[@type='submit' and not(@class='btn-secondary')]\"\n)\n```\n\n### Desafio 8: Encontrar Todos os Campos Obrigatórios do Formulário\n\n**Objetivo**: Obter todos os campos de input obrigatórios no formulário.\n\n```python\n# Seletor CSS (limpo)\nrequired_fields = await tab.query(\n    \"#user-form input[required]\",\n    find_all=True\n)\n\n# XPath\nrequired_fields = await tab.query(\n    \"//form[@id='user-form']//input[@required]\",\n    find_all=True\n)\n\n# Verificar\nfor field in required_fields:\n    field_name = await field.get_attribute(\"name\")\n    print(f\"Obrigatório: {field_name}\")\n```\n\n### Desafio 9: Encontrar Primeiro Botão de Deletar Não Desabilitado\n\n**Objetivo**: Encontrar o primeiro botão de deletar que não está desabilitado.\n\n```python\n# Seletor CSS\nfirst_enabled_delete = await tab.query(\"button.btn-delete:not([disabled])\")\n\n# XPath\nfirst_enabled_delete = await tab.query(\n    \"//button[contains(@class, 'btn-delete') and not(@disabled)]\"\n)\n\n# Obter todos os botões de deletar habilitados\nall_enabled = await tab.query(\n    \"//button[@class='btn-delete' and not(@disabled)]\",\n    find_all=True\n)\n```\n\n### Desafio 10: Encontrar Linha da Tabela por Múltiplas Condições\n\n**Objetivo**: Encontrar produtos com estoque > 0 E preço < $100.\n\n```python\n# XPath com lógica complexa\navailable_affordable = await tab.query(\n    \"\"\"\n    //tr[\n        number(td[@class='stock']) > 0 \n        and \n        number(translate(td[@class='price'], '$', '')) < 100\n    ]\n    \"\"\",\n    find_all=True\n)\n\n# Para cada produto correspondente\nfor row in available_affordable:\n    cells = await row.query(\"td\", find_all=True)\n    product_name = await cells[0].text\n    print(f\"Disponível: {product_name}\")\n```\n\n### Desafio 11: Navegar em Relacionamentos Complexos\n\n**Objetivo**: A partir de um botão de deletar, obter o nome do produto na mesma linha.\n\n```python\n# Começar com um botão de deletar\ndelete_button = await tab.query(\"//tr[@data-product-id='101']//button[@class='btn-delete']\")\n\n# Navegar para a linha pai, depois para a primeira célula\nproduct_name_cell = await delete_button.query(\"./ancestor::tr/td[1]\")\nproduct_name = await product_name_cell.text\nprint(product_name)  # \"Laptop\"\n\n# Alternativa: Obter a linha inteira primeiro\nrow = await delete_button.query(\"./ancestor::tr\")\nproduct_id = await row.get_attribute(\"data-product-id\")\nprint(product_id)  # \"101\"\n```\n\n### Desafio 12: Encontrar Checkbox e Seu Label Juntos\n\n**Objetivo**: Encontrar o checkbox da newsletter e verificar seu label.\n\n```python\n# Encontrar checkbox\ncheckbox = await tab.query(\"#newsletter\")\n\n# Obter label associado usando atributo 'for'\nlabel = await tab.query(\"//label[@for='newsletter']\")\nlabel_text = await label.text\nprint(label_text)  # \"Subscribe to newsletter\"\n\n# Alternativa: Navegar do checkbox para o label\nlabel = await checkbox.query(\"//following::label[@for='newsletter']\")\n\n# Checar se está marcado\nis_checked = await checkbox.is_checked()\n```\n\n## Padrão Avançado: Construção Dinâmica de Seletor\n\nAo lidar com conteúdo dinâmico, você pode precisar construir seletores programaticamente:\n\n```python\nasync def find_product_by_name(tab, product_name: str):\n    \"\"\"Encontra uma linha de produto pelo nome dinamicamente.\"\"\"\n    # Escapar aspas no nome do produto para prevenir injeção de XPath\n    safe_name = product_name.replace(\"'\", \"\\\\'\")\n    \n    xpath = f\"//tr[td[text()='{safe_name}']]\"\n    return await tab.query(xpath)\n\nasync def find_table_cell(tab, row_text: str, column_index: int):\n    \"\"\"Encontra uma célula específica pelo conteúdo da linha e posição da coluna.\"\"\"\n    xpath = f\"//tr[td[contains(text(), '{row_text}')]]/td[{column_index}]\"\n    return await tab.query(xpath)\n\n# Uso\nproduct_row = await find_product_by_name(tab, \"Laptop\")\nprice_cell = await find_table_cell(tab, \"Laptop\", 2)\nprice = await price_cell.text\nprint(price)  # \"$999\"\n```\n\n## Comparação de Desempenho\n\n```python\nimport asyncio\nimport time\n\nasync def benchmark_selectors(tab):\n    \"\"\"Comparar desempenho de CSS vs XPath.\"\"\"\n    \n    # Aquecimento\n    await tab.query(\"#products-table\")\n    \n    # Benchmark CSS\n    start = time.time()\n    for _ in range(100):\n        await tab.query(\"#products-table tbody tr\", find_all=True)\n    css_time = time.time() - start\n    \n    # Benchmark XPath\n    start = time.time()\n    for _ in range(100):\n        await tab.query(\"//table[@id='products-table']//tbody//tr\", find_all=True)\n    xpath_time = time.time() - start\n    \n    print(f\"CSS: {css_time:.3f}s\")\n    print(f\"XPath: {xpath_time:.3f}s\")\n    print(f\"CSS é {xpath_time/css_time:.2f}x mais rápido\")\n\n# Resultados típicos: CSS é 1.2-1.5x mais rápido para seletores simples\n```\n\n!!! warning \"Desempenho vs Legibilidade\"\n    Embora seletores CSS sejam geralmente mais rápidos, a diferença é usualmente negligenciável (milissegundos) para consultas individuais. Escolha o seletor que torna seu código mais legível e sustentável, especialmente para relacionamentos complexos onde o XPath se destaca.\n\n## Melhores Práticas de Seletores\n\n### 1. Prefira Seletores Estáveis\n\n```python\n# Bom: Usando atributos semânticos\nawait tab.query(\"#user-email\")\nawait tab.query(\"[data-testid='submit-button']\")\nawait tab.query(\"input[name='username']\")\n\n# Evite: Seletores frágeis baseados na estrutura\nawait tab.query(\"div > div > div:nth-child(3) > input\")\nawait tab.query(\"body > div:nth-child(2) > form > div:first-child\")\n```\n\n### 2. Use o Seletor Mais Simples que Funciona\n\n```python\n# Bom: Simples e eficiente\nawait tab.query(\"#login-form\")\nawait tab.query(\".submit-button\")\n\n# Evite: Complicado demais quando desnecessário\nawait tab.query(\"//div[@id='content']/descendant::form[@id='login-form']\")\n```\n\n### 3. Combine find() e query() Apropriadamente\n\n```python\n# Use find() para correspondência simples de atributos\nusername = await tab.find(id=\"username\")\nsubmit = await tab.find(tag_name=\"button\", type=\"submit\")\n\n# Use query() para padrões complexos\nactive_link = await tab.query(\"nav.menu a.active\")\nerror_msg = await tab.query(\"//input[@name='email']/following-sibling::span[@class='error']\")\n```\n\n### 4. Adicione Comentários para Seletores Complexos\n\n```python\n# Encontrar o botão \"Edit\" na linha que contém o produto \"Laptop\"\n# XPath: Navega para a linha com texto \"Laptop\", depois encontra o botão de edição\nedit_button = await tab.query(\n    \"//tr[td[text()='Laptop']]//button[@class='btn-edit']\"\n)\n```\n\n## Conclusão\n\nAo entender tanto seletores CSS quanto XPath, juntamente com suas respectivas forças e casos de uso, você pode criar uma automação de navegador robusta e sustentável que lida com as complexidades das aplicações web modernas. Lembre-se:\n\n- **Use seletores CSS** para seleções simples e críticas de desempenho\n- **Use XPath** para relacionamentos complexos, correspondência de texto e navegação ascendente\n- **Escolha estabilidade** em vez de brevidade ao escrever seletores\n- **Comente consultas complexas** para manter a legibilidade do código"
  },
  {
    "path": "docs/pt/deep-dive/index.md",
    "content": "# Análise Profunda: Fundamentos Técnicos\n\n**Bem-vindo ao coração técnico do Pydoll, onde exploramos os sistemas e protocolos que impulsionam a automação de navegadores.**\n\nEsta seção fornece educação técnica abrangente sobre web scraping, automação de navegadores, protocolos de rede e técnicas anti-detecção. Em vez de focar apenas em padrões de uso, exploramos os mecanismos subjacentes, desde o primeiro pacote TCP até o pixel final renderizado.\n\n## O que Torna Isto Diferente\n\nA maioria das documentações de automação ensina **como usar uma ferramenta**. Esta seção ensina **como a internet realmente funciona**, e como manipulá-la em cada camada:\n\n- **Protocolos de rede** (TCP/IP, TLS, HTTP/2) - A fundação invisível de cada requisição\n- **Componentes internos do navegador** (CDP, motores de renderização, contextos JavaScript) - O que acontece dentro do Chrome\n- **Sistemas de detecção** (fingerprinting, análise comportamental, detecção de proxy) - Como os sites identificam bots\n- **Técnicas de evasão** (sobrescritas de CDP, aplicação de consistência, imitação humana) - Como se tornar indetectável\n\n!!! quote \"Filosofia\"\n    **\"Qualquer tecnologia suficientemente avançada é indistinguível da mágica.\"**\n    \n    Esta seção visa desmistificar a automação de navegadores explicando os sistemas subjacentes. Entender esses fundamentos proporciona melhor controle e previsibilidade em seu trabalho de automação.\n\n## A Arquitetura do Conhecimento\n\nEsta seção está organizada em **cinco camadas progressivas**, cada uma construindo sobre a anterior:\n\n### Fundamentos Essenciais\n**[→ Explore os Fundamentos](./fundamentals/cdp.md)**\n\nComece pela base: entenda os protocolos e sistemas que impulsionam o Pydoll.\n\n- **[Chrome DevTools Protocol](./fundamentals/cdp.md)** - Como o Pydoll conversa com os navegadores, contornando o WebDriver\n- **[Camada de Conexão](./fundamentals/connection-layer.md)** - Arquitetura WebSocket, padrões assíncronos, CDP em tempo real\n- **[Sistema de Tipos do Python](./fundamentals/typing-system.md)** - Segurança de tipos, TypedDict para CDP, integração com IDE\n\n**Por que começar aqui**: Entender o CDP e a comunicação assíncrona fornece a base para compreender todos os outros aspectos da automação de navegadores.\n\n---\n\n### Arquitetura Interna\n**[→ Explore a Arquitetura](./architecture/browser-domain.md)**\n\nSuba para o próximo nível: entenda como os componentes internos do Pydoll trabalham juntos.\n\n- **[Domínio do Navegador (Browser)](./architecture/browser-domain.md)** - Gerenciamento de processos, contextos, automação multi-perfil\n- **[Domínio da Aba (Tab)](./architecture/tab-domain.md)** - Ciclo de vida da aba, operações concorrentes, manipulação de iframes\n- **[Domínio do WebElement](./architecture/webelement-domain.md)** - Interações de elementos, shadow DOM, manipulação de atributos\n- **[Mixin FindElements](./architecture/find-elements-mixin.md)** - Estratégias de seletores, travessia do DOM, otimização\n- **[Arquitetura de Eventos](./architecture/event-architecture.md)** - Sistema de eventos reativo, callbacks, despacho assíncrono\n- **[Arquitetura de Requisições do Navegador](./architecture/browser-requests-architecture.md)** - HTTP no contexto do navegador\n\n**Por que isso importa**: Entender a arquitetura interna revela oportunidades de otimização e padrões de design que não são aparentes no uso superficial.\n\n---\n\n### Rede e Segurança\n**[→ Explore Rede e Segurança](./network/index.md)**\n\nDesça para a camada de protocolo: entenda como os dados fluem pela internet.\n\n- **[Fundamentos de Rede](./network/network-fundamentals.md)** - Modelo OSI, TCP/UDP, vazamento de WebRTC\n- **[Proxies HTTP/HTTPS](./network/http-proxies.md)** - Proxy de camada de aplicação, tunelamento CONNECT\n- **[Proxies SOCKS](./network/socks-proxies.md)** - Proxy de camada de sessão, suporte UDP, segurança\n- **[Detecção de Proxy](./network/proxy-detection.md)** - Níveis de anonimato, técnicas de detecção, evasão\n- **[Construindo Servidores Proxy](./network/build-proxy.md)** - Implementações completas de HTTP e SOCKS5\n- **[Questões Legais e Éticas](./network/proxy-legal.md)** - GDPR, CFAA, conformidade, uso responsável\n\n**Visão crítica**: Características de rede são determinadas no nível do SO. Incompatibilidades entre a identidade do navegador declarada e os fingerprints de nível de rede podem ser detectadas por sistemas anti-bot sofisticados.\n\n---\n\n### Fingerprinting (Impressão Digital)\n**[→ Explore Fingerprinting](./fingerprinting/index.md)**\n\nEntendendo sistemas de detecção e técnicas de evasão para automação de navegadores.\n\n- **[Network Fingerprinting](./fingerprinting/network-fingerprinting.md)** - TCP/IP, TLS/JA3, p0f, Nmap, Scapy\n- **[Browser Fingerprinting](./fingerprinting/browser-fingerprinting.md)** - HTTP/2, Canvas, WebGL, APIs JavaScript\n- **[Técnicas de Evasão](./fingerprinting/evasion-techniques.md)** - Sobrescritas de CDP, consistência, código prático\n\n**Visão chave**: Cada conexão revela numerosas características (renderização de canvas, tamanho da janela TCP, ordem de cifras TLS). Furtividade eficaz requer consistência em todas as camadas de detecção.\n\n---\n\n### Guias Práticos\n**[→ Explore os Guias](./guides/selectors-guide.md)**\n\nAplique seu conhecimento: guias práticos para desafios comuns de automação.\n\n- **[Seletores CSS vs XPath](./guides/selectors-guide.md)** - Sintaxe de seletores, desempenho, melhores práticas\n\n**Em breve**: Mais guias práticos sintetizando o conhecimento técnico em padrões acionáveis.\n\n---\n\n## Trilhas de Aprendizagem\n\nObjetivos diferentes exigem conhecimentos diferentes. Escolha sua trilha:\n\n### Trilha 1: Automação Furtiva (Stealth)\n**Objetivo: Construir scrapers indetectáveis**\n\n1.  **[Visão Geral de Fingerprinting](./fingerprinting/index.md)** - Entenda o cenário de detecção\n2.  **[Network Fingerprinting](./fingerprinting/network-fingerprinting.md)** - Assinaturas TCP/IP, TLS\n3.  **[Browser Fingerprinting](./fingerprinting/browser-fingerprinting.md)** - Canvas, WebGL, HTTP/2\n4.  **[Técnicas de Evasão](./fingerprinting/evasion-techniques.md)** - Contramedidas baseadas em CDP\n5.  **[Rede e Segurança](./network/index.md)** - Seleção e configuração de proxy\n6.  **[Domínio do Navegador (Browser)](./architecture/browser-domain.md)** - Isolamento de contexto, gerenciamento de processos\n\n**Investimento de tempo**: 12-16 horas de aprendizado técnico profundo\n**Recompensa**: Capacidade de contornar sistemas anti-bot sofisticados\n\n---\n\n### Trilha 2: Maestria em Arquitetura\n**Objetivo: Contribuir para o Pydoll ou construir ferramentas similares**\n\n1.  **[Análise Profunda do CDP](./fundamentals/cdp.md)** - Fundamentos do protocolo\n2.  **[Camada de Conexão](./fundamentals/connection-layer.md)** - Padrões assíncronos WebSocket\n3.  **[Arquitetura de Eventos](./architecture/event-architecture.md)** - Design orientado a eventos\n4.  **[Domínio do Navegador (Browser)](./architecture/browser-domain.md)** - Gerenciamento do navegador\n5.  **[Domínio da Aba (Tab)](./architecture/tab-domain.md)** - Ciclo de vida da aba\n6.  **[Domínio do WebElement](./architecture/webelement-domain.md)** - Interação de elementos\n7.  **[Sistema de Tipos do Python](./fundamentals/typing-system.md)** - Integração de segurança de tipos\n\n**Investimento de tempo**: 16-20 horas de estudo arquitetural\n**Recompensa**: Entendimento profundo dos componentes internos da automação de navegadores\n\n---\n\n### Trilha 3: Engenharia de Rede\n**Objetivo: Dominar proxies, fingerprinting e furtividade em nível de rede**\n\n1.  **[Fundamentos de Rede](./network/network-fundamentals.md)** - Modelo OSI, TCP/UDP, WebRTC\n2.  **[Network Fingerprinting](./fingerprinting/network-fingerprinting.md)** - Assinaturas TCP/IP, TLS/JA3\n3.  **[Proxies HTTP/HTTPS](./network/http-proxies.md)** - Proxy de camada de aplicação\n4.  **[Proxies SOCKS](./network/socks-proxies.md)** - Proxy de camada de sessão\n5.  **[Detecção de Proxy](./network/proxy-detection.md)** - Anonimato e evasão\n6.  **[Construindo Servidores Proxy](./network/build-proxy.md)** - Implementação do zero\n\n**Investimento de tempo**: 14-18 horas de estudo de protocolos de rede\n**Recompensa**: Entendimento completo de anonimato e detecção em nível de rede\n\n---\n\n## Pré-requisitos\n\nEste é um material técnico **avançado**. Os pré-requisitos recomendados incluem:\n\n- **Fundamentos de Python** - Classes, async/await, gerenciadores de contexto, decoradores\n- **Redes básicas** - Endereços IP, portas, protocolo HTTP\n- **Básico de Pydoll** - Veja [Funcionalidades](../features/core-concepts.md) e [Começando](../index.md)\n- **Browser DevTools** - Inspetor do Chrome, aba Rede, Console\n\n**Se você é novo nisso**, recomendamos:\n1.  Completar a seção [Funcionalidades](../features/index.md) primeiro\n2.  Praticar automação básica com o Pydoll\n3.  Retornar aqui quando precisar de um entendimento mais profundo\n\n## A Filosofia da Maestria\n\nAutomação web envolve múltiplas áreas de especialização:\n\n- **Engenharia de protocolos** - Entender TCP/IP, TLS, HTTP/2\n- **Programação de sistemas** - Gerenciar processos, I/O assíncrono, WebSockets\n- **Pesquisa em segurança** - Fingerprinting, detecção, evasão\n- **Componentes internos do navegador** - Renderização, contextos JavaScript, CDP\n- **Segurança operacional** - Conformidade legal, diretrizes éticas\n\nA maioria dos desenvolvedores aprende isso independentemente, ao longo do tempo. Esta seção consolida esse conhecimento ao:\n\n1.  **Centralizar conhecimento** - Chega de posts de blog espalhados e artigos acadêmicos\n2.  **Fornecer contexto** - Cada técnica explicada desde os primeiros princípios\n3.  **Oferecer código funcional** - Todos os exemplos estão prontos para produção\n4.  **Citar fontes** - Cada alegação é apoiada por RFCs, documentação ou pesquisa\n5.  **Complexidade progressiva** - Cada seção constrói sobre o conhecimento anterior\n\n## Padrões da Documentação\n\nEsta documentação representa extensa pesquisa, testes e validação:\n\n- Cada detalhe de protocolo verificado contra RFCs\n- Cada técnica de fingerprinting testada em produção\n- Cada exemplo de código roda sem modificação\n- Cada alegação citada com fontes autoritativas\n- Cada diagrama gerado a partir do comportamento real do sistema\n\nPrecisão técnica e aplicabilidade prática são priorizadas em todo o conteúdo.\n\n## Uso Ético\n\nCom este conhecimento vem a responsabilidade:\n\n!!! danger \"Use com Responsabilidade\"\n    As técnicas descritas aqui podem servir tanto para automação legítima quanto para fins maliciosos. O uso responsável inclui:\n    \n    - Respeitar os termos de serviço dos sites e o robots.txt\n    - Implementar limitação de taxa (rate limiting) e rastreamento respeitoso\n    - Considerar se a automação é realmente necessária\n    - Consultar aconselhamento jurídico em caso de incerteza\n    - Ser transparente sobre sua automação quando apropriado\n    \n    Evite usar este conhecimento para:\n    - Fraude, abuso de contas ou atividades ilegais\n    - Sobrecarregar servidores com scraping agressivo\n    - Atividades prejudiciais sem entender as consequências\n\nPara orientação detalhada, veja **[Considerações Legais e Éticas](./network/proxy-legal.md)**.\n\n## Contribuindo\n\nEncontrou um erro? Tem uma sugestão? Viu algo desatualizado?\n\nEsta documentação é um **projeto vivo**. Técnicas de fingerprinting evoluem, protocolos atualizam e novos métodos de evasão emergem. Aceitamos contribuições que:\n\n- Corrijam imprecisões técnicas\n- Adicionem novas técnicas de fingerprinting\n- Atualizem informações de protocolo\n- Melhorem exemplos de código\n- Expandam a cobertura de sistemas de detecção\n\nVeja [Contribuindo](../CONTRIBUTING.md) para diretrizes de submissão.\n\n---\n\n## Começando\n\nEscolha uma trilha com base em seus objetivos:\n\n**Novo em conteúdo técnico profundo?**\n→ Comece com **[Chrome DevTools Protocol](./fundamentals/cdp.md)** para entender a fundação do Pydoll\n\n**Precisa de automação furtiva?**\n→ Pule para **[Fingerprinting](./fingerprinting/index.md)** para técnicas de detecção e evasão\n\n**Quer controle em nível de rede?**\n→ Explore **[Rede e Segurança](./network/index.md)** para arquitetura de proxy e protocolos\n\n**Construindo infraestrutura de automação?**\n→ Estude **[Arquitetura Interna](./architecture/browser-domain.md)** para padrões de design\n\n**Só quer dar uma olhada?**\n→ Escolha qualquer tópico da barra lateral, cada artigo é autocontido\n\n---\n\n!!! success \"Análise Profunda Técnica\"\n    Esta seção fornece conhecimento técnico abrangente para automação de navegadores, desde protocolos fundamentais até técnicas avançadas de evasão.\n    \n    Explore no seu próprio ritmo."
  },
  {
    "path": "docs/pt/deep-dive/network/build-proxy.md",
    "content": "# Construindo Servidores Proxy\n\nEste documento implementa servidores proxy HTTP e SOCKS5 do zero em Python usando asyncio. O objetivo não é prontidão para produção, mas compreensão de protocolo: ver como cada byte é analisado, onde estão os limites de segurança e por que certas decisões de design existem em software proxy real.\n\n!!! info \"Navegação do Módulo\"\n    - [Fundamentos de Rede](./network-fundamentals.md): TCP/IP, UDP, WebRTC\n    - [Proxies HTTP/HTTPS](./http-proxies.md): Proxy na camada de aplicação\n    - [Proxies SOCKS](./socks-proxies.md): Proxy na camada de sessão\n    - [Detecção de Proxy](./proxy-detection.md): Técnicas de detecção e evasão\n\n    Para uso prático de proxy no Pydoll, veja [Configuração de Proxy](../../features/configuration/proxy.md).\n\n!!! warning \"Código Educacional\"\n    Estas implementações priorizam clareza sobre robustez. Elas não possuem limites de conexão, listas de controle de acesso e muitos caminhos de recuperação de erro que um proxy de produção requer. Não as exponha a redes não confiáveis.\n\n## Proxy HTTP\n\nUm proxy HTTP opera em dois modos. Para HTTP em texto plano, ele recebe a requisição completa (com uma URL em formato absoluto como `GET http://example.com/path HTTP/1.1`), reescreve o request-target para formato de origem (`GET /path HTTP/1.1`), conecta ao servidor destino, encaminha a requisição e retorna a resposta. Para HTTPS, o cliente envia uma requisição `CONNECT host:port`, o proxy abre uma conexão TCP para o destino, responde com `200 Connection Established`, e então retransmite bytes cegamente em ambas as direções sem inspecionar o conteúdo criptografado.\n\nA implementação abaixo lida com ambos os modos. Algumas coisas para notar enquanto lê. O método `_pipe_data` chama `write_eof()` quando um lado fecha, que envia um TCP FIN para o outro lado. Sem isso, o túnel fica pendurado indefinidamente porque o outro `read()` nunca retorna bytes vazios. O caminho de encaminhamento HTTP usa a mesma abordagem de piping em vez de uma única chamada `read()`, porque respostas HTTP podem ser arbitrariamente grandes e um read de tamanho fixo as truncaria silenciosamente. A reescrita do request-target preserva query strings, que `urlparse().path` sozinho descartaria.\n\n```python\nimport asyncio\nimport base64\nimport contextlib\nimport logging\nfrom urllib.parse import urlparse\n\nlogger = logging.getLogger(__name__)\n\n\nclass HTTPProxy:\n    \"\"\"Proxy HTTP/HTTPS assíncrono com autenticação Basic opcional.\"\"\"\n\n    def __init__(self, host='0.0.0.0', port=8080, username=None, password=None):\n        self.host = host\n        self.port = port\n        self.username = username\n        self.password = password\n\n    async def start(self):\n        server = await asyncio.start_server(\n            self._handle_client, self.host, self.port\n        )\n        logger.info(f'HTTP proxy listening on {self.host}:{self.port}')\n        async with server:\n            await server.serve_forever()\n\n    async def _handle_client(self, reader, writer):\n        try:\n            request_line = await asyncio.wait_for(\n                reader.readline(), timeout=30\n            )\n            if not request_line:\n                return\n\n            parts = request_line.decode('latin-1').split()\n            if len(parts) != 3:\n                writer.write(b'HTTP/1.1 400 Bad Request\\r\\n\\r\\n')\n                await writer.drain()\n                return\n\n            method, url, _ = parts\n            headers = await self._read_headers(reader)\n\n            if not self._check_auth(headers):\n                writer.write(\n                    b'HTTP/1.1 407 Proxy Authentication Required\\r\\n'\n                    b'Proxy-Authenticate: Basic realm=\"Proxy\"\\r\\n'\n                    b'Content-Length: 0\\r\\n\\r\\n'\n                )\n                await writer.drain()\n                return\n\n            if method == 'CONNECT':\n                await self._handle_connect(url, reader, writer)\n            else:\n                await self._handle_http(method, url, headers, reader, writer)\n        except Exception as e:\n            logger.error(f'Client handler error: {e}')\n        finally:\n            writer.close()\n            await writer.wait_closed()\n\n    async def _read_headers(self, reader):\n        headers = {}\n        while True:\n            line = await reader.readline()\n            if line in (b'\\r\\n', b'\\n', b''):\n                break\n            if b':' in line:\n                key, value = line.decode('latin-1').split(':', 1)\n                headers[key.strip().lower()] = value.strip()\n        return headers\n\n    def _check_auth(self, headers):\n        if not self.username:\n            return True\n        auth = headers.get('proxy-authorization', '')\n        if not auth.startswith('Basic '):\n            return False\n        try:\n            decoded = base64.b64decode(auth[6:]).decode('utf-8')\n            if ':' not in decoded:\n                return False\n            user, pwd = decoded.split(':', 1)\n            return user == self.username and pwd == self.password\n        except Exception:\n            return False\n\n    async def _handle_connect(self, target, client_reader, client_writer):\n        \"\"\"Estabelece um túnel TCP cego para HTTPS.\"\"\"\n        # Analisa host:port, lidando com literais IPv6 como [::1]:443\n        if target.startswith('['):\n            bracket_end = target.index(']')\n            host = target[1:bracket_end]\n            port = int(target[bracket_end + 2:])\n        elif ':' in target:\n            host, port_str = target.rsplit(':', 1)\n            port = int(port_str)\n        else:\n            client_writer.write(b'HTTP/1.1 400 Bad Request\\r\\n\\r\\n')\n            await client_writer.drain()\n            return\n\n        try:\n            server_reader, server_writer = await asyncio.open_connection(\n                host, port\n            )\n        except OSError as e:\n            logger.error(f'CONNECT failed to {host}:{port}: {e}')\n            client_writer.write(b'HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n')\n            await client_writer.drain()\n            return\n\n        client_writer.write(b'HTTP/1.1 200 Connection Established\\r\\n\\r\\n')\n        await client_writer.drain()\n\n        await asyncio.gather(\n            self._pipe(client_reader, server_writer),\n            self._pipe(server_reader, client_writer),\n        )\n\n    async def _handle_http(self, method, url, headers, client_reader, client_writer):\n        \"\"\"Encaminha uma requisição HTTP em texto plano.\"\"\"\n        parsed = urlparse(url)\n        host = parsed.hostname\n        port = parsed.port or 80\n\n        # Preserva query string no request-target\n        path = parsed.path or '/'\n        if parsed.query:\n            path += f'?{parsed.query}'\n\n        try:\n            server_reader, server_writer = await asyncio.open_connection(\n                host, port\n            )\n        except OSError as e:\n            logger.error(f'HTTP forward failed to {host}:{port}: {e}')\n            client_writer.write(b'HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n')\n            await client_writer.drain()\n            return\n\n        # Reescreve request-target de formato absoluto para formato de origem\n        request = f'{method} {path} HTTP/1.1\\r\\n'\n\n        # Cabeçalho Host deve incluir a porta se for não-padrão\n        if port != 80:\n            request += f'Host: {host}:{port}\\r\\n'\n        else:\n            request += f'Host: {host}\\r\\n'\n\n        # Remove cabeçalhos hop-by-hop que não devem ser encaminhados\n        hop_by_hop = {\n            'proxy-authorization', 'proxy-connection',\n            'connection', 'keep-alive', 'te', 'trailer', 'upgrade',\n        }\n        for key, value in headers.items():\n            if key not in hop_by_hop:\n                request += f'{key}: {value}\\r\\n'\n\n        # Força Connection: close para que o servidor não mantenha keep-alive,\n        # o que impediria o stream de resposta de terminar\n        request += 'Connection: close\\r\\n\\r\\n'\n\n        server_writer.write(request.encode('latin-1'))\n\n        # Encaminha corpo da requisição se presente\n        content_length = int(headers.get('content-length', 0))\n        if content_length > 0:\n            body = await client_reader.readexactly(content_length)\n            server_writer.write(body)\n\n        await server_writer.drain()\n\n        # Retransmite a resposta inteira de volta (não um único read de tamanho fixo)\n        while True:\n            chunk = await server_reader.read(65536)\n            if not chunk:\n                break\n            client_writer.write(chunk)\n            await client_writer.drain()\n\n        server_writer.close()\n        await server_writer.wait_closed()\n\n    async def _pipe(self, reader, writer):\n        \"\"\"Retransmissão bidirecional de dados com half-close adequado.\"\"\"\n        try:\n            while True:\n                data = await reader.read(8192)\n                if not data:\n                    break\n                writer.write(data)\n                await writer.drain()\n        except (ConnectionResetError, BrokenPipeError, OSError):\n            pass\n        finally:\n            with contextlib.suppress(Exception):\n                if writer.can_write_eof():\n                    writer.write_eof()\n```\n\nAlguns detalhes de protocolo que vale entender. Cabeçalhos HTTP são codificados como ISO-8859-1 (Latin-1), não UTF-8. Latin-1 mapeia cada valor de byte 0-255 para um caractere, então `decode('latin-1')` nunca levanta um `UnicodeDecodeError`, enquanto `decode('utf-8')` quebraria em certos valores de cabeçalho. O cabeçalho `Proxy-Authorization` usa codificação Base64, mas Base64 não é criptografia: as credenciais trafegam em texto claro (ou melhor, codificação trivialmente reversível) a menos que a conexão entre cliente e proxy esteja protegida por TLS. Os cabeçalhos hop-by-hop (`Connection`, `Keep-Alive`, `TE`, `Trailer`, `Upgrade`, `Proxy-Connection`) são destinados à conexão imediata entre dois nós, não para encaminhamento de ponta a ponta. A RFC 9110 Seção 7.6.1 requer que proxies os removam antes de encaminhar.\n\n!!! warning \"Risco de SSRF\"\n    Esta implementação não valida endereços de destino. Um cliente poderia solicitar `CONNECT 127.0.0.1:6379` para alcançar uma instância Redis local, ou `CONNECT 169.254.169.254:80` para acessar metadados de instância cloud (AWS, GCP, Azure). Qualquer proxy exposto a clientes não confiáveis deve validar destinos contra uma lista de negação de faixas privadas e link-local (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `::1`, `fc00::/7`).\n\n## Proxy SOCKS5\n\nUm proxy SOCKS5 opera em um nível mais baixo que o HTTP. Ele usa um protocolo binário definido na RFC 1928, consistindo de três fases: negociação de método, autenticação opcional e a requisição de conexão. O proxy não analisa HTTP de forma alguma. Uma vez que o túnel é estabelecido, ele retransmite bytes brutos sem entender qual protocolo flui por ele.\n\nA natureza binária do SOCKS5 significa que cada leitura deve receber exatamente o número esperado de bytes. TCP é um protocolo de stream e não garante que `read(4)` retorne 4 bytes: pode retornar 1, 2 ou 3 bytes dependendo das condições de rede. A implementação abaixo usa `readexactly()` do asyncio, que bufferiza internamente até que o número solicitado de bytes chegue ou a conexão feche (levantando `IncompleteReadError`).\n\n```python\nimport asyncio\nimport contextlib\nimport struct\nimport logging\n\nlogger = logging.getLogger(__name__)\n\n\nclass SOCKS5Proxy:\n    \"\"\"Proxy SOCKS5 assíncrono com suporte a CONNECT e autenticação opcional (RFC 1928).\"\"\"\n\n    VERSION = 0x05\n\n    def __init__(self, host='0.0.0.0', port=1080, username=None, password=None):\n        self.host = host\n        self.port = port\n        self.username = username\n        self.password = password\n\n    async def start(self):\n        server = await asyncio.start_server(\n            self._handle_client, self.host, self.port\n        )\n        logger.info(f'SOCKS5 proxy listening on {self.host}:{self.port}')\n        async with server:\n            await server.serve_forever()\n\n    async def _handle_client(self, reader, writer):\n        try:\n            if not await self._negotiate_method(reader, writer):\n                return\n            if self.username and not await self._authenticate(reader, writer):\n                return\n            await self._handle_request(reader, writer)\n        except (asyncio.IncompleteReadError, ConnectionResetError):\n            pass\n        except Exception as e:\n            logger.error(f'SOCKS5 error: {e}')\n        finally:\n            writer.close()\n            await writer.wait_closed()\n\n    async def _negotiate_method(self, reader, writer):\n        \"\"\"Fase 1: cliente oferece métodos de autenticação, servidor escolhe um.\"\"\"\n        version = (await reader.readexactly(1))[0]\n        if version != self.VERSION:\n            return False\n\n        nmethods = (await reader.readexactly(1))[0]\n        methods = await reader.readexactly(nmethods)\n\n        if self.username:\n            if 0x02 not in methods:\n                writer.write(bytes([self.VERSION, 0xFF]))\n                await writer.drain()\n                return False\n            selected = 0x02\n        else:\n            selected = 0x00\n\n        writer.write(bytes([self.VERSION, selected]))\n        await writer.drain()\n        return True\n\n    async def _authenticate(self, reader, writer):\n        \"\"\"Fase 2: sub-negociação de usuário/senha (RFC 1929).\"\"\"\n        auth_ver = (await reader.readexactly(1))[0]\n        if auth_ver != 0x01:\n            return False\n\n        ulen = (await reader.readexactly(1))[0]\n        username = (await reader.readexactly(ulen)).decode('utf-8')\n        plen = (await reader.readexactly(1))[0]\n        password = (await reader.readexactly(plen)).decode('utf-8')\n\n        ok = username == self.username and password == self.password\n        writer.write(bytes([0x01, 0x00 if ok else 0x01]))\n        await writer.drain()\n        return ok\n\n    async def _handle_request(self, reader, writer):\n        \"\"\"Fase 3: analisa a requisição CONNECT e estabelece o túnel.\"\"\"\n        header = await reader.readexactly(4)\n        version, command, _, atyp = header\n\n        # Analisa endereço de destino baseado no tipo de endereço\n        if atyp == 0x01:  # IPv4\n            raw = await reader.readexactly(4)\n            address = '.'.join(str(b) for b in raw)\n        elif atyp == 0x03:  # Nome de domínio\n            length = (await reader.readexactly(1))[0]\n            address = (await reader.readexactly(length)).decode('ascii')\n        elif atyp == 0x04:  # IPv6\n            raw = await reader.readexactly(16)\n            groups = [f'{raw[i]:02x}{raw[i+1]:02x}' for i in range(0, 16, 2)]\n            address = ':'.join(groups)\n        else:\n            await self._reply(writer, 0x08)\n            return\n\n        port = struct.unpack('!H', await reader.readexactly(2))[0]\n        logger.info(f'SOCKS5 CONNECT {address}:{port}')\n\n        if command != 0x01:  # Apenas CONNECT é implementado\n            await self._reply(writer, 0x07)\n            return\n\n        try:\n            server_reader, server_writer = await asyncio.open_connection(\n                address, port\n            )\n        except ConnectionRefusedError:\n            await self._reply(writer, 0x05)\n            return\n        except OSError:\n            await self._reply(writer, 0x04)\n            return\n\n        # BND.ADDR e BND.PORT devem refletir o endereço do socket local.\n        # A maioria dos clientes ignora estes para CONNECT, mas preenchê-los\n        # corretamente satisfaz a RFC 1928.\n        local = server_writer.get_extra_info('sockname')\n        await self._reply(writer, 0x00, local[0], local[1])\n\n        await asyncio.gather(\n            self._pipe(reader, server_writer),\n            self._pipe(server_reader, writer),\n        )\n\n    async def _reply(self, writer, status, bind_addr='0.0.0.0', bind_port=0):\n        \"\"\"Envia uma resposta SOCKS5 com o status e endereço vinculado dados.\"\"\"\n        import socket\n        try:\n            packed_ip = socket.inet_aton(bind_addr)\n            atyp = 0x01\n        except OSError:\n            packed_ip = socket.inet_aton('0.0.0.0')\n            atyp = 0x01\n\n        writer.write(bytes([\n            self.VERSION, status, 0x00, atyp,\n            *packed_ip,\n            (bind_port >> 8) & 0xFF, bind_port & 0xFF,\n        ]))\n        await writer.drain()\n\n    async def _pipe(self, reader, writer):\n        try:\n            while True:\n                data = await reader.read(8192)\n                if not data:\n                    break\n                writer.write(data)\n                await writer.drain()\n        except (ConnectionResetError, BrokenPipeError, OSError):\n            pass\n        finally:\n            with contextlib.suppress(Exception):\n                if writer.can_write_eof():\n                    writer.write_eof()\n```\n\nQuando o tipo de endereço é `0x03` (nome de domínio), o proxy resolve DNS ele mesmo via `asyncio.open_connection()`. Esta é a propriedade de privacidade definidora do proxy SOCKS5: o cliente envia o nome de domínio em vez de resolvê-lo localmente, o que previne que consultas DNS vazem para a rede local do cliente. Este é o mesmo comportamento em que o Chrome se baseia quando configurado com `--proxy-server=socks5://...`, como discutido em [Proxies SOCKS](./socks-proxies.md).\n\nO método `_reply` preenche `BND.ADDR` e `BND.PORT` com o endereço real do socket local após uma conexão bem-sucedida, como a RFC 1928 requer. Muitas implementações SOCKS5 retornam `0.0.0.0:0` aqui porque a maioria dos clientes ignora esses campos para comandos CONNECT, mas preenchê-los corretamente não custa nada e evita uma violação de protocolo.\n\n## Executando Ambos os Proxies\n\n```python\nasync def main():\n    http_proxy = HTTPProxy(\n        port=8080, username='user', password='pass'\n    )\n    socks5_proxy = SOCKS5Proxy(\n        port=1080, username='user', password='pass'\n    )\n    await asyncio.gather(http_proxy.start(), socks5_proxy.start())\n\n# asyncio.run(main())\n```\n\nVocê pode testá-los com curl:\n\n```bash\n# Proxy HTTP\ncurl -x http://user:pass@localhost:8080 http://httpbin.org/ip\n\n# HTTPS através de proxy HTTP (túnel CONNECT)\ncurl -x http://user:pass@localhost:8080 https://httpbin.org/ip\n\n# Proxy SOCKS5\ncurl --socks5 localhost:1080 --proxy-user user:pass https://httpbin.org/ip\n```\n\n## O que o Código Não Lida\n\nEstas implementações omitem várias coisas que proxies de produção lidam. Entender o que está faltando é tão instrutivo quanto entender o que está presente.\n\nNão há limites de conexão. `asyncio.start_server` aceita conexões sem limite, então um único cliente abrindo milhares de conexões esgotaria descritores de arquivo. Proxies de produção usam semáforos ou pools de conexão para limitar concorrência.\n\nNão há validação de destino. Ambos os proxies conectam a qualquer endereço que o cliente solicite, incluindo `127.0.0.1`, `169.254.169.254` (metadados cloud) e faixas de rede interna. Este é um vetor de Server-Side Request Forgery (SSRF). Proxies de produção mantêm listas de negação de faixas de endereços privados e link-local.\n\nNão há logging de tráfego ou métricas. Proxies de produção rastreiam contagem de requisições, bytes transferidos, taxas de erro e percentis de latência, tipicamente exportando para Prometheus ou sistemas similares.\n\nO proxy HTTP não adiciona um cabeçalho `Via`. A RFC 9110 Seção 7.6.3 requer que intermediários adicionem um campo `Via` às mensagens encaminhadas. Isso foi omitido por simplicidade, mas um proxy em conformidade com os padrões deve incluí-lo.\n\nNenhum dos proxies implementa shutdown gracioso. Quando o servidor para, túneis ativos são terminados abruptamente em vez de serem drenados. Proxies de produção rastreiam conexões ativas e aguardam que completem (com um prazo) antes de encerrar.\n\n## Encadeamento de Proxy\n\nEncadear proxies significa rotear tráfego através de múltiplos proxies em sequência: cliente para proxy A, proxy A para proxy B, proxy B para o servidor destino. Cada proxy na cadeia só conhece seus vizinhos imediatos, não o caminho completo.\n\nO principal caso de uso é distribuir confiança. Se você não confia totalmente em nenhum provedor de proxy individual, encadear dois provedores significa que nenhum deles vê tanto seu IP real quanto seu destino. O tradeoff é latência: cada salto adiciona seu próprio tempo de setup de conexão e atraso de encaminhamento. Um único proxy tipicamente adiciona 50 a 100ms de overhead. Dois proxies aproximadamente dobram isso, e três proxies podem empurrar o overhead total além de 300ms.\n\nAlém de dois saltos, o ganho marginal de privacidade diminui enquanto latência e probabilidade de falha aumentam. A maioria das configurações práticas usa um ou dois proxies. O Tor usa três relays (guard, middle, exit) porque seu modelo de ameaça assume que alguns relays estão comprometidos, mas o Tor aceita a penalidade de latência como um tradeoff de design explícito.\n\n```\nClient --> Proxy A (SOCKS5) --> Proxy B (SOCKS5) --> Target\n           vê: IP do cliente       vê: IP do Proxy A\n           vê: endereço do Proxy B  vê: endereço do destino\n```\n\nEncadear um proxy SOCKS5 através de outro proxy SOCKS5 funciona fazendo o proxy A tratar o proxy B como o destino. O cliente conecta ao proxy A e envia uma requisição CONNECT para o endereço do proxy B. Uma vez que esse túnel é estabelecido, o cliente envia um segundo handshake SOCKS5 através do túnel, desta vez solicitando o destino real. O proxy A vê tráfego fluindo para o proxy B mas não pode lê-lo se a conexão interna estiver criptografada.\n\n## Referências\n\n- RFC 1928: SOCKS Protocol Version 5 - https://datatracker.ietf.org/doc/html/rfc1928\n- RFC 1929: Username/Password Authentication for SOCKS V5 - https://datatracker.ietf.org/doc/html/rfc1929\n- RFC 9110: HTTP Semantics - https://www.rfc-editor.org/rfc/rfc9110.html\n- RFC 9112: HTTP/1.1 - https://www.rfc-editor.org/rfc/rfc9112.html\n- OWASP SSRF Prevention Cheat Sheet - https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html\n- mitmproxy (Python HTTPS intercepting proxy) - https://mitmproxy.org/\n"
  },
  {
    "path": "docs/pt/deep-dive/network/http-proxies.md",
    "content": "# Arquitetura de Proxy HTTP/HTTPS\n\nProxies HTTP são o protocolo de proxy mais comum na internet. Quase toda rede corporativa os utiliza, e a maioria dos serviços de proxy comerciais os oferece como opção padrão. Eles operam na Camada 7 (Aplicação) do modelo OSI, o que significa que entendem HTTP e podem analisar, modificar, cachear e filtrar tráfego. Essa mesma integração profunda com o protocolo também é sua maior limitação: só podem lidar com tráfego HTTP, revelam uso de proxy através de cabeçalhos identificáveis, e não podem fazer proxy de UDP, o que deixa WebRTC e DNS vulneráveis a vazamentos.\n\nEste documento cobre como proxies HTTP funcionam no nível do protocolo, o método CONNECT para tunelamento HTTPS, mecanismos de autenticação e as implicações de protocolos modernos como HTTP/2 e HTTP/3.\n\n!!! info \"Navegação do Módulo\"\n    - [Fundamentos de Rede](./network-fundamentals.md): TCP/IP, UDP, modelo OSI\n    - [Proxies SOCKS](./socks-proxies.md): Alternativa agnóstica a protocolo\n    - [Detecção de Proxy](./proxy-detection.md): Como evitar detecção\n\n    Para configuração prática, veja [Configuração de Proxy](../../features/configuration/proxy.md).\n\n## Como Proxies HTTP Funcionam\n\nUm proxy HTTP fica entre o cliente e o servidor destino, mantendo duas conexões TCP separadas: uma do cliente para o proxy, e outra do proxy para o servidor destino. Como o proxy entende HTTP, ele pode tomar decisões inteligentes sobre o tráfego que passa por ele.\n\n### Fluxo de Requisição\n\nQuando um cliente é configurado para usar um proxy HTTP, ele envia a requisição HTTP completa para o proxy em vez de diretamente para o servidor destino. A diferença chave de uma requisição direta é que a linha de requisição inclui a URI absoluta, não apenas o caminho. Por exemplo, em vez de `GET /page HTTP/1.1`, o cliente envia `GET http://example.com/page HTTP/1.1`. Isso diz ao proxy para onde encaminhar a requisição.\n\n```mermaid\nsequenceDiagram\n    participant Client as Navegador Cliente\n    participant Proxy as Proxy HTTP\n    participant Server as Servidor Destino\n\n    Client->>Proxy: GET http://example.com/page HTTP/1.1<br/>Host: example.com<br/>User-Agent: Mozilla/5.0\n    Note over Client,Proxy: Conexão TCP #1\n\n    Note over Proxy: Analisa requisição, verifica auth,<br/>verifica cache, aplica regras\n\n    Proxy->>Server: GET /page HTTP/1.1<br/>Host: example.com<br/>Via: 1.1 proxy.example.com<br/>X-Forwarded-For: 192.168.1.100\n    Note over Proxy,Server: Conexão TCP #2\n\n    Server->>Proxy: HTTP/1.1 200 OK<br/>[corpo da resposta]\n\n    Note over Proxy: Cacheia resposta se permitido,<br/>filtra conteúdo, registra transação\n\n    Proxy->>Client: HTTP/1.1 200 OK<br/>Via: 1.1 proxy.example.com<br/>[corpo possivelmente modificado]\n```\n\nO proxy recebe a requisição HTTP completa, analisa o método, URL e cabeçalhos, e decide o que fazer. Ele pode verificar credenciais de autenticação, verificar a URL contra uma lista de controle de acesso, procurar uma cópia em cache do recurso e modificar cabeçalhos antes de encaminhar. Então abre uma conexão TCP separada para o servidor destino e envia a requisição, potencialmente com cabeçalhos alterados.\n\nQuando a resposta chega, o proxy pode cacheá-la de acordo com a semântica HTTP (`Cache-Control`, `ETag`), filtrar o conteúdo para malware ou palavras-chave bloqueadas, comprimi-la se o cliente suportar, e registrar a transação antes de encaminhar a resposta de volta ao cliente.\n\n### Cabeçalhos de Proxy e Privacidade\n\nProxies HTTP comumente adicionam cabeçalhos que revelam sua presença e o endereço IP real do cliente. O cabeçalho `Via` (RFC 9110) identifica o proxy na cadeia de requisição. O cabeçalho `X-Forwarded-For` contém o IP original do cliente, frequentemente formando uma cadeia se múltiplos proxies estão envolvidos. O cabeçalho `X-Forwarded-Proto` indica se a requisição original era HTTP ou HTTPS. Alguns proxies também adicionam `X-Real-IP` como alternativa mais simples ao `X-Forwarded-For`.\n\nTambém existe um cabeçalho padronizado `Forwarded` (RFC 7239) que combina toda essa informação em um único campo, por exemplo `Forwarded: for=192.168.1.100;proto=http;by=proxy.example.com`. Na prática, a maioria dos proxies ainda usa as variantes `X-Forwarded-*` já que têm suporte mais amplo.\n\nClientes legados e alguns navegadores mais antigos também podem enviar um cabeçalho `Proxy-Connection: keep-alive` em vez de `Connection: keep-alive` ao rotear através de um proxy. Este cabeçalho é um indicador bem conhecido de uso de proxy e um sinal clássico de detecção.\n\n!!! danger \"Detecção por Cabeçalho\"\n    Sistemas de detecção procuram a presença de cabeçalhos `Via`, `X-Forwarded-For` ou `Forwarded` para confirmar uso de proxy. Se `X-Real-IP` não corresponde ao IP de conexão, o proxy é confirmado. Proxies sofisticados podem remover esses cabeçalhos, mas muitos serviços de proxy comerciais os deixam por padrão. Sempre verifique o comportamento do seu proxy usando uma ferramenta como [browserleaks.com/ip](https://browserleaks.com/ip).\n\n### Capacidades e Limitações\n\nComo proxies HTTP analisam e entendem o protocolo HTTP, eles podem ler e modificar cada parte de uma requisição e resposta HTTP não criptografada: URLs, cabeçalhos, cookies e corpos. Isso permite que cacheiem respostas inteligentemente, filtrem conteúdo por URL ou palavra-chave, injetem ou removam cabeçalhos, autentiquem usuários e registrem todo o tráfego em detalhes.\n\nO tradeoff é que esse acoplamento profundo com HTTP significa que o proxy é limitado a tráfego HTTP. Ele não pode nativamente fazer proxy de FTP, SSH, SMTP ou protocolos personalizados (embora o método CONNECT, descrito abaixo, forneça uma solução de tunelamento para qualquer protocolo baseado em TCP). Não tem suporte para UDP, o que significa que tráfego WebRTC, consultas DNS e QUIC/HTTP/3 o ignoram completamente. E inspecionar conteúdo HTTPS requer terminação TLS, que quebra a criptografia de ponta a ponta.\n\n## O Método CONNECT: Tunelamento HTTPS\n\nO método CONNECT (RFC 9110, Seção 9.3.6) resolve um problema fundamental: como um proxy HTTP pode encaminhar tráfego criptografado que não pode ler? A resposta é tornar-se um túnel TCP cego.\n\nQuando um cliente quer acessar um site HTTPS através de um proxy, ele envia uma requisição `CONNECT` pedindo ao proxy para estabelecer uma conexão TCP bruta para o destino. Uma vez que o proxy confirma que o túnel está estabelecido, ele para de ser um proxy HTTP completamente e se torna um relay TCP transparente na Camada 4, encaminhando bytes em ambas as direções sem interpretá-los.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant Proxy\n    participant Server\n\n    Client->>Proxy: CONNECT example.com:443 HTTP/1.1<br/>Host: example.com:443<br/>Proxy-Authorization: Basic dXNlcjpwYXNz\n    Note over Client,Proxy: Requisição HTTP não criptografada\n\n    Proxy->>Server: TCP three-way handshake\n    Note over Proxy,Server: Conexão TCP estabelecida\n\n    Proxy->>Client: HTTP/1.1 200 Connection Established\n\n    Note right of Proxy: Proxy agora é um relay<br/>TCP transparente (Camada 4)\n\n    Client->>Server: TLS ClientHello\n    Note over Client,Server: Handshake TLS (proxy vê<br/>isso em texto plano)\n    Server->>Client: TLS ServerHello, Certificate\n\n    Client->>Server: Requisição HTTP/2 criptografada\n    Server->>Client: Resposta HTTP/2 criptografada\n\n    Note over Proxy: Proxy encaminha cegamente<br/>todos os dados criptografados\n```\n\n### A Requisição CONNECT\n\nA requisição CONNECT é mínima. O método é `CONNECT`, a URI de requisição é o `host:port` de destino (não um caminho), e inclui autenticação se o proxy a requer. Não há corpo de requisição. O proxy valida as credenciais, verifica suas regras de controle de acesso e abre uma conexão TCP para o host e porta especificados. Se tudo for bem-sucedido, ele envia de volta `HTTP/1.1 200 Connection Established` seguido por uma linha em branco. Após essa linha em branco, a conversação HTTP termina e o proxy se torna um relay transparente.\n\n### Visibilidade Após CONNECT\n\nUma vez que o túnel é estabelecido, a visibilidade do proxy é limitada. Ele sabe o hostname e porta de destino da requisição CONNECT. Ele pode observar o timing da conexão (quando foi estabelecida e por quanto tempo), o volume de dados transferidos em cada direção, e quando qualquer lado termina a conexão. Ele também pode observar o handshake TLS que se segue, o que é particularmente relevante.\n\nA mensagem TLS ClientHello, enviada imediatamente após o túnel ser estabelecido, é transmitida em texto plano. O proxy (e qualquer observador de rede) pode ler diretamente a versão TLS, a lista completa de cipher suites suportadas, as extensões e seus parâmetros, as curvas elípticas oferecidas, e a extensão SNI (Server Name Indication) que contém o hostname destino. Esta é exatamente a informação usada para TLS fingerprinting (JA3/JA4). Veja [Network Fingerprinting](../fingerprinting/network-fingerprinting.md) para detalhes.\n\nO que o proxy não pode ver é os dados de aplicação criptografados: métodos HTTP, URLs, cabeçalhos de requisição e resposta, cookies, tokens de sessão e conteúdo de resposta são todos criptografados dentro do túnel TLS.\n\n!!! note \"SNI e Encrypted Client Hello (ECH)\"\n    A extensão SNI no ClientHello revela o hostname destino em texto plano, que é redundante com a requisição CONNECT no cenário de proxy mas relevante para outros observadores de rede. Encrypted Client Hello (ECH), atualmente sendo implantado, visa criptografar o SNI para resolver esse vazamento. No entanto, a adoção do ECH ainda é limitada e requer suporte tanto do cliente quanto do servidor.\n\n### CONNECT para Protocolos Não-HTTPS\n\nEmbora CONNECT seja usado principalmente para HTTPS, ele pode tunelar qualquer protocolo baseado em TCP. Uma conexão IMAPS na porta 993, uma conexão SSH na porta 22 ou FTP-over-TLS na porta 990 todos funcionam através de um túnel CONNECT. O proxy não precisa entender esses protocolos porque após o túnel ser estabelecido, ele está simplesmente retransmitindo bytes.\n\nNa prática, muitos proxies corporativos restringem CONNECT à porta 443 (HTTPS) para prevenir abuso. Tentar `CONNECT example.com:22` para SSH frequentemente retornará `403 Forbidden`.\n\n### O Dilema do HTTPS\n\nProxies HTTP enfrentam uma escolha fundamental com tráfego criptografado. Com a abordagem de túnel CONNECT, a criptografia de ponta a ponta é preservada, o cliente verifica o certificado do servidor diretamente, e certificate pinning funciona normalmente. Mas o proxy não pode inspecionar, cachear ou filtrar o conteúdo criptografado.\n\nA alternativa é terminação TLS (MITM), onde o proxy descriptografa o tráfego HTTPS, inspeciona o conteúdo e re-criptografa antes de encaminhar. Isso requer instalar o certificado CA do proxy no cliente, quebra a criptografia de ponta a ponta e é detectável através de certificate pinning e logs de Certificate Transparency. A maioria dos proxies corporativos usa essa abordagem para filtragem de conteúdo e scanning de segurança, enquanto proxies focados em privacidade usam túneis CONNECT cegos.\n\nPara web scraping e automação, essa distinção importa para TLS fingerprinting. Se o proxy realiza terminação TLS, o fingerprint TLS que o servidor destino vê pertence ao proxy, não ao seu navegador. Se você está usando um túnel CONNECT, o fingerprint é preservado de ponta a ponta. Dependendo da sua estratégia de evasão, uma abordagem pode ser preferível à outra.\n\n| Aspecto | HTTP (sem CONNECT) | HTTPS (túnel CONNECT) |\n|---------|--------------------|-----------------------|\n| Visibilidade do proxy | Requisição/resposta HTTP completa | Apenas host:porta destino + TLS ClientHello |\n| Criptografia | Nenhuma (a menos que terminação TLS) | TLS de ponta a ponta |\n| Caching | Sim, baseado em semântica HTTP | Não (conteúdo criptografado) |\n| Filtragem de conteúdo | Sim | Não (apenas bloqueio baseado em hostname) |\n| Modificação de cabeçalhos | Sim | Não (cabeçalhos criptografados) |\n| Visibilidade de URL | URL completa | Apenas hostname (via CONNECT e SNI) |\n| Suporte a protocolo | Apenas HTTP | Qualquer protocolo sobre TCP |\n\n## Proxies HTTPS (TLS para o Proxy)\n\nUma distinção que vale esclarecer é a diferença entre fazer proxy de tráfego HTTPS e conectar ao próprio proxy via HTTPS. Quando você configura `--proxy-server=https://proxy:port` em vez de `http://proxy:port`, a conexão entre seu navegador e o proxy é criptografada com TLS. Isso protege suas credenciais de autenticação do proxy de serem interceptadas na rede local e esconde até o hostname CONNECT de observadores locais, já que está encapsulado dentro da conexão TLS para o proxy.\n\nO Chrome suporta isso via o esquema `https://` em `--proxy-server`. É particularmente importante ao usar um proxy em redes não confiáveis (Wi-Fi público, hospedagem compartilhada), onde a conexão entre você e o proxy é o elo mais fraco.\n\n## Autenticação\n\nA autenticação de proxy HTTP usa códigos de status e cabeçalhos HTTP padrão, seguindo a RFC 9110. Quando um proxy requer autenticação, ele responde com `407 Proxy Authentication Required` e um cabeçalho `Proxy-Authenticate` indicando quais esquemas de autenticação suporta. O cliente então retransmite a requisição com um cabeçalho `Proxy-Authorization` contendo as credenciais.\n\n### Esquemas de Autenticação\n\nExistem vários esquemas de autenticação, cada um com características de segurança diferentes.\n\n**Basic** (RFC 7617) é o mais simples. O cliente envia `Proxy-Authorization: Basic <base64(username:password)>`. Base64 é uma codificação, não criptografia, então as credenciais são trivialmente reversíveis. Qualquer um que intercepte o cabeçalho pode decodificá-lo instantaneamente e reusá-lo indefinidamente já que não há proteção contra replay. Auth Basic deve ser usado apenas sobre conexões criptografadas com TLS.\n\n**Digest** (RFC 7616) usa um mecanismo de challenge-response. O proxy envia um nonce aleatório, e o cliente computa um hash do username, password, nonce e URI da requisição. A senha nunca é transmitida, e o nonce fornece proteção contra replay. A versão original usa MD5, que é rápido o suficiente para brute-force eficiente, embora a RFC 7616 tenha adicionado suporte a SHA-256. Auth Digest é raramente implementado por serviços de proxy modernos.\n\n**NTLM** é o protocolo proprietário de challenge-response da Microsoft, comum em ambientes corporativos Windows. Usa uma negociação de três etapas (Tipo 1 negociação, Tipo 2 challenge, Tipo 3 autenticação) e integra com Active Directory para single sign-on. NTLMv1 usa DES (quebrado), e NTLMv2 usa HMAC-MD5 (considerado fraco pelos padrões modernos). A Microsoft recomenda Kerberos sobre NTLM para novas implantações. NTLM é vinculado à conexão, o que significa que quebra com multiplexação HTTP/2.\n\n**Negotiate** (RFC 4559) usa SPNEGO para selecionar entre Kerberos e NTLM, preferindo Kerberos. Kerberos oferece a segurança mais forte (criptografia AES, autenticação mútua, tickets com tempo limitado) mas requer infraestrutura Active Directory, máquinas no domínio e sincronização precisa de relógio. Em automação de navegador, Kerberos é difícil de configurar programaticamente.\n\n| Esquema | Segurança | Mecanismo | Notas Práticas |\n|---------|-----------|-----------|----------------|\n| Basic | Baixa | Credenciais codificadas em Base64 | Suporte universal. Usar apenas sobre TLS. |\n| Digest | Média | Challenge-response com MD5/SHA-256 | Proteção contra replay via nonce. Raramente implementado. |\n| NTLM | Média | Challenge-response (NT hash) | SSO Windows. Proprietário, vulnerabilidades conhecidas. |\n| Negotiate | Alta | Kerberos/SPNEGO | Mais forte. Requer Active Directory. |\n\n### Autenticação no Pydoll\n\nO Chrome não suporta credenciais de proxy inline na flag `--proxy-server`. Escrever `--proxy-server=http://user:pass@proxy:port` não funciona: o Chrome silenciosamente ignora a porção `user:pass` e conecta sem autenticação.\n\nO Pydoll resolve isso transparentemente através do seu `ProxyManager`. Quando você fornece uma URL de proxy com credenciais embutidas, o Pydoll extrai o username e password, remove-os da URL antes de passá-la ao Chrome, e usa o domínio CDP Fetch para interceptar respostas `407 Proxy Authentication Required` e automaticamente fornecer as credenciais via `Fetch.continueWithAuth`. Essa abordagem funciona para todos os esquemas de autenticação que o Chrome suporta (Basic, Digest, NTLM, Negotiate) sem o Pydoll precisar implementar a lógica específica do protocolo.\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\n# Pydoll extrai credenciais, limpa a URL e lida com 407 via CDP\noptions.add_argument('--proxy-server=http://user:pass@proxy.example.com:8080')\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n```\n\n!!! tip \"Melhores Práticas de Autenticação\"\n    Sempre use conexões de proxy criptografadas com TLS (proxy HTTPS ou túnel SSH) para proteger credenciais em trânsito. Prefira Bearer tokens para proxies de API já que são revogáveis e com tempo limitado. Nunca use auth Basic sobre uma conexão HTTP não criptografada para o proxy. Não codifique credenciais no código-fonte; use variáveis de ambiente.\n\n## Protocolos Modernos e Proxy\n\n### HTTP/2\n\nHTTP/2 introduziu multiplexação, enquadramento binário e compressão de cabeçalhos HPACK, que mudam fundamentalmente como proxies lidam com conexões. No HTTP/1.1, cada requisição ocupa uma conexão sequencialmente (pipelining existe mas é desabilitado na prática, então navegadores contornam isso abrindo seis conexões paralelas por host). No HTTP/2, uma única conexão TCP carrega múltiplos streams concorrentes, cada um com sua própria requisição e resposta.\n\nPara proxies, isso significa gerenciar IDs de stream, prioridades e janelas de controle de fluxo em ambos os lados da conexão. O proxy deve traduzir entre IDs de stream no lado do cliente e do servidor, manter árvores de prioridade e lidar com controle de fluxo por stream. Isso é significativamente mais complexo que o simples encaminhamento de requisição-resposta do HTTP/1.1.\n\nDa perspectiva de fingerprinting, metadados de stream HTTP/2 (tamanhos de janela, configurações de prioridade, ordenação de cabeçalhos dentro do HPACK) podem fazer fingerprint de clientes individuais mesmo quando múltiplos usuários compartilham o mesmo proxy.\n\n| Recurso | HTTP/1.1 | HTTP/2 |\n|---------|----------|--------|\n| Conexões | Sequencial por conexão (navegadores abrem 6 em paralelo) | Múltiplos streams concorrentes sobre uma conexão |\n| Multiplexação | Não (head-of-line blocking) | Sim (apenas em nível de stream) |\n| Compressão de Cabeçalhos | Nenhuma | HPACK |\n| Complexidade do Proxy | Encaminhamento simples de requisição/resposta | Mapeamento de ID de stream, gerenciamento de prioridade |\n\nNo HTTP/2, o método CONNECT foi estendido pela RFC 8441 para suportar um pseudo-cabeçalho `:protocol`, habilitando tunelamento WebSocket e outras atualizações de protocolo diretamente dentro de streams HTTP/2 sem requerer conexões separadas.\n\n### HTTP/3 e QUIC\n\nHTTP/3 roda sobre QUIC (RFC 9000), que é um protocolo de transporte baseado em UDP. Isso introduz desafios fundamentais para proxies HTTP. Proxies HTTP tradicionais operam sobre TCP e não podem lidar com o tráfego UDP do QUIC. Conexões QUIC podem sobreviver a mudanças de IP (migração de conexão), complicando o gerenciamento de sessão do proxy. E QUIC criptografa quase tudo, incluindo metadados de nível de transporte que eram anteriormente visíveis.\n\nFazer proxy de QUIC requer CONNECT-UDP (RFC 9298), um novo método para estabelecer túneis UDP através de proxies HTTP. A maioria dos proxies tradicionais, incluindo muitos serviços comerciais, ainda não suporta isso. Navegadores fazem fallback para HTTP/2 sobre TCP quando o proxy não suporta QUIC, o que significa que mais metadados podem vazar do que esperado se você estava contando com o transporte criptografado do HTTP/3.\n\nEm cenários de automação, considere desabilitar QUIC com a flag do Chrome `--disable-quic` para forçar HTTP/2 sobre TCP. Isso garante que todo tráfego passe pelo seu proxy e elimina o risco de vazamentos baseados em UDP do QUIC.\n\n| Aspecto | TCP + TLS (HTTP/1.1, HTTP/2) | QUIC/UDP (HTTP/3) |\n|---------|------------------------------|-------------------|\n| Transporte | TCP (orientado a conexão) | UDP (sem conexão) |\n| Handshake | TCP + TLS separados (2 RTT) | Combinado (0-1 RTT) |\n| Head-of-line blocking | Sim (nível TCP) | Não (apenas nível de stream) |\n| Migração de conexão | Não suportado | Suportado (sobrevive a mudanças de IP) |\n| Compatibilidade com proxy | Excelente | Limitada (requer suporte a relay UDP) |\n\n!!! warning \"Downgrade de Protocolo\"\n    Quando um proxy não suporta HTTP/3, o navegador silenciosamente faz fallback para HTTP/2 ou HTTP/1.1. Este downgrade pode expor metadados (cabeçalhos, padrões de timing) que HTTP/3 teria criptografado. Monitore seu tráfego para entender sua versão de protocolo real, e esteja ciente de que a adoção do HTTP/3 varia por região e CDN.\n\n## Resumo\n\nProxies HTTP fornecem funcionalidade rica ao custo de escopo limitado e preocupações de privacidade. Eles podem inspecionar, cachear e filtrar tráfego HTTP, mas não podem lidar com protocolos não-HTTP, tráfego UDP ou conteúdo HTTPS sem quebrar a criptografia. Sua presença é revelada através de cabeçalhos identificáveis a menos que explicitamente removidos.\n\nPara automação, o túnel CONNECT é o recurso mais relevante: ele preserva a criptografia TLS de ponta a ponta enquanto dá ao proxy apenas visibilidade em nível de hostname. O Pydoll lida com autenticação de proxy transparentemente através do domínio CDP Fetch, suportando todos os esquemas que o Chrome implementa.\n\n### Proxy HTTP vs SOCKS5\n\n| Necessidade | Proxy HTTP | SOCKS5 |\n|-------------|------------|--------|\n| Filtragem de conteúdo | Sim | Não |\n| Bloqueio baseado em URL | Sim | Não (apenas IP:porta) |\n| Caching | Sim | Não |\n| Suporte UDP | Não | Sim |\n| Flexibilidade de protocolo | Apenas HTTP (CONNECT para tunelamento TCP) | Qualquer TCP/UDP |\n| Privacidade | Baixa (analisa HTTP, adiciona cabeçalhos reveladores) | Média (não analisa ou modifica tráfego, mas conteúdo não criptografado ainda é visível ao operador) |\n| Resolução DNS | Proxy resolve (remoto) | Depende (SOCKS5: tipicamente cliente resolve, SOCKS5h: proxy resolve. Chrome sempre resolve remotamente para SOCKS5.) |\n\nPara ambientes corporativos que precisam de controle de conteúdo e caching, proxies HTTP são a escolha certa. Para automação focada em privacidade, SOCKS5 oferece melhor stealth e flexibilidade de protocolo. Para segurança máxima, use SOCKS5 sobre um túnel SSH ou VPN.\n\n**Próximos passos:**\n\n- [Proxies SOCKS](./socks-proxies.md): Proxy agnóstico a protocolo na camada de sessão\n- [Fundamentos de Rede](./network-fundamentals.md): TCP/IP, UDP, WebRTC\n- [Detecção de Proxy](./proxy-detection.md): Como proxies são detectados e como evitar\n- [Configuração de Proxy](../../features/configuration/proxy.md): Configuração prática de proxy no Pydoll\n- [Network Fingerprinting](../fingerprinting/network-fingerprinting.md): Fingerprinting TCP/IP e TLS\n\n## Referências\n\n- RFC 9110: HTTP Semantics (2022, substitui RFC 7230-7237) - https://www.rfc-editor.org/rfc/rfc9110.html\n- RFC 9112: HTTP/1.1 (2022) - https://www.rfc-editor.org/rfc/rfc9112.html\n- RFC 9113: HTTP/2 (2022, substitui RFC 7540) - https://www.rfc-editor.org/rfc/rfc9113.html\n- RFC 9114: HTTP/3 (2022) - https://www.rfc-editor.org/rfc/rfc9114.html\n- RFC 9000: QUIC Transport Protocol (2021) - https://www.rfc-editor.org/rfc/rfc9000.html\n- RFC 9298: Proxying UDP in HTTP (CONNECT-UDP, 2022) - https://www.rfc-editor.org/rfc/rfc9298.html\n- RFC 8441: Bootstrapping WebSockets with HTTP/2 (2018) - https://www.rfc-editor.org/rfc/rfc8441.html\n- RFC 7617: Basic Authentication (2015) - https://www.rfc-editor.org/rfc/rfc7617.html\n- RFC 7616: Digest Authentication (2015) - https://www.rfc-editor.org/rfc/rfc7616.html\n- RFC 7239: Forwarded HTTP Extension (2014) - https://www.rfc-editor.org/rfc/rfc7239.html\n- RFC 4559: Negotiate Authentication (2006) - https://www.rfc-editor.org/rfc/rfc4559.html\n- MDN Web Docs: Proxy servers and tunneling - https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling\n- Chrome DevTools Protocol: Fetch domain - https://chromedevtools.github.io/devtools-protocol/tot/Fetch/\n"
  },
  {
    "path": "docs/pt/deep-dive/network/index.md",
    "content": "# Análise Profunda de Rede e Segurança\n\n**Bem-vindo ao fundamento da comunicação moderna da internet, o campo de batalha do anonimato, detecção e evasão.**\n\nProtocolos de rede são a infraestrutura invisível que alimenta cada requisição web, conexão de navegador e script de automação. Entendê-los profundamente transforma você de um **usuário de ferramenta** em um **engenheiro de protocolo** capaz de navegar pelos mais sofisticados sistemas anti-bot.\n\n## Por que a Arquitetura de Rede Importa\n\nQuando você executa `tab.go_to('https://example.com')`, uma complexa sinfonia de protocolos entra em ação:\n\n1.  **Resolução DNS** traduz o domínio para um endereço IP (potencialmente vazando sua intenção)\n2.  **Handshake TCP** estabelece a conexão (revelando seu SO através de características de pacote)\n3.  **Negociação TLS** protege o canal (aplicando fingerprinting no seu navegador via suítes de cifras)\n4.  **Requisição HTTP/2** busca a página (expondo a versão do navegador através de frames SETTINGS)\n5.  **Descoberta WebRTC** pode sondar seu IP real (contornando completamente sua VPN)\n\n**Cada passo é uma oportunidade para detecção ou evasão.**\n\n!!! danger \"A Camada de Rede Não Pode Mentir\"\n    Diferente das características em nível de navegador (que o JavaScript pode modificar), os fingerprints de nível de rede estão **gravados no kernel do SO e na pilha TCP/IP**. Um desencontro aqui, como um navegador Chrome enviando opções TCP de Linux enquanto alega ser Windows, é instantaneamente fatal para a automação furtiva.\n\n## A Arquitetura da Privacidade na Internet\n\nEste módulo explora os **fundamentos técnicos** que tornam a privacidade possível (e quebrável) na internet moderna:\n\n### A Realidade do Modelo OSI\n\n```mermaid\ngraph TB\n    subgraph \"Camada de Aplicação 7\"\n        HTTP[Cabeçalhos HTTP/HTTPS]\n        DNS[Consultas DNS]\n    end\n    \n    subgraph \"Camada de Apresentação 6\"\n        TLS[Fingerprinting TLS/SSL]\n        Ciphers[Suítes de Cifras, Extensões]\n    end\n    \n    subgraph \"Camadas de Sessão/Transporte 5-4\"\n        SOCKS[Protocolo Proxy SOCKS]\n        TCP[Janela TCP, Opções, ISN]\n    end\n    \n    subgraph \"Camada de Rede 3\"\n        IP[IP TTL, Fragmentação]\n        Routing[Roteamento de Pacotes, Saltos]\n    end\n    \n    HTTP --> TLS\n    DNS --> TLS\n    TLS --> SOCKS\n    Ciphers --> TCP\n    SOCKS --> IP\n    TCP --> Routing\n```\n\n**Cada camada é tanto um escudo quanto uma vulnerabilidade:**\n\n- **Camada 7 (Aplicação)**: Proxies podem ler e modificar seu tráfego HTTP\n- **Camada 6 (Apresentação)**: Criptografia TLS protege o conteúdo, mas vaza metadados\n- **Camada 4 (Transporte)**: Características TCP traem seu sistema operacional\n- **Camada 3 (Rede)**: Endereços IP revelam sua localização física\n\n## O que Você Vai Dominar\n\nEste módulo está estruturado como uma **progressão técnica** de fundamentos até a exploração avançada:\n\n### 1. Fundamentos de Rede\n**[Fundamentos de Rede](./network-fundamentals.md)**\n\nConstrua a base: entenda os protocolos que movem a internet e como eles revelam, ou escondem, sua identidade.\n\n- **Camadas do Modelo OSI** e suas implicações para fingerprinting\n- **TCP vs UDP**: Por que seu proxy pode vazar tráfego UDP\n- **Vazamento de IP via WebRTC**: A ameaça oculta nos navegadores modernos\n- **Características da pilha de rede**: TTL, tamanho da janela, ordem das opções\n\n**Por que começar aqui**: Sem esta base, a configuração de proxy é **\"programação de culto à carga\"** (cargo cult programming), copiando comandos sem entender por que funcionam (ou não).\n\n### 2. Proxies HTTP/HTTPS\n**[Proxies HTTP/HTTPS](./http-proxies.md)**\n\nDomine o protocolo de proxy mais comum e entenda suas limitações fundamentais.\n\n- **Operação de proxy HTTP**: Encaminhamento de requisições, cache, injeção de cabeçalho\n- **Tunelamento CONNECT**: Como o HTTPS \"passa por um túnel\" através de proxies HTTP\n- **Complexidades do HTTP/2**: Multiplexação, prioridades de stream, fingerprinting de SETTINGS\n- **HTTP/3 e QUIC**: Desafios de proxying baseado em UDP\n- **Esquemas de autenticação**: Basic, Digest, NTLM, tokens Bearer\n\n**Visão crítica**: Proxies HTTP operam na Camada 7, eles podem **ler, modificar e registrar** seu tráfego não criptografado. Para privacidade verdadeira, você precisa de criptografia **antes** que o proxy veja seus dados.\n\n### 3. Proxies SOCKS\n**[Proxies SOCKS](./socks-proxies.md)**\n\nEntenda por que o SOCKS5 é o **padrão ouro** para automação consciente da privacidade.\n\n- **SOCKS4 vs SOCKS5**: Evolução do protocolo e capacidades\n- **Handshake SOCKS5**: Análise profunda do protocolo binário com estruturas de pacotes\n- **Suporte UDP**: Jogos, VoIP e WebRTC sobre SOCKS5\n- **Resolução DNS**: Por que o DNS no lado do proxy previne vazamentos\n- **Por que SOCKS5 > proxies HTTP**: Comparação em nível de protocolo\n\n**Vantagem chave**: SOCKS opera na Camada 5 (Sessão), **abaixo** da camada de aplicação. Ele não pode ler seu tráfego HTTP, apenas ver IPs de destino, reduzindo vastamente a superfície de confiança.\n\n### 4. Detecção de Proxy\n**[Detecção de Proxy e Anonimato](./proxy-detection.md)**\n\nAprenda como sites **detectam o uso de proxy** e como evadir a detecção.\n\n- **Níveis de anonimato**: Proxies transparentes, anônimos, elite\n- **Bancos de dados de reputação de IP**: Como seu IP de datacenter te trai\n- **Análise de cabeçalhos**: Cabeçalhos X-Forwarded-For, Via, Forwarded\n- **Checagens de consistência**: DNS reverso, desencontros de geolocalização\n- **Integração de fingerprinting de rede**: Combinando detecção de proxy com análise TCP/TLS\n\n**Dura realidade**: A maioria dos proxies \"anônimos\" é trivialmente detectável. Furtividade verdadeira requer **proxies residenciais de elite** + **fingerprinting de navegador consistente** + **comportamento semelhante ao humano**.\n\n### 5. Construindo Servidores Proxy\n**[Construindo Seu Próprio Proxy](./build-proxy.md)**\n\nImplemente proxies HTTP e SOCKS5 do zero em Python, a experiência de aprendizado definitiva.\n\n- **Servidor proxy HTTP**: Implementação assíncrona completa com autenticação\n- **Servidor proxy SOCKS5**: Manipulação de protocolo binário, tunelamento TCP\n- **Encadeamento de proxy (Proxy chaining)**: Anonimato em camadas (e trocas de latência)\n- **Pools de proxy rotativos**: Verificação de saúde, failover, balanceamento de carga\n- **Tópicos avançados**: Proxies transparentes, interceptação SSL MITM\n\n**Por que construir o seu**: Entender detalhes de implementação revela **vetores de ataque** e **oportunidades de otimização** invisíveis do exterior.\n\n### 6. Considerações Legais e Éticas\n**[Diretrizes Legais e Éticas](./proxy-legal.md)**\n\nNavegue pelo campo minado legal do uso de proxy e automação web.\n\n- **Conformidade regulatória**: GDPR, CFAA, leis internacionais\n- **Termos de Serviço**: O que constitui violação\n- **Diretrizes éticas**: robots.txt, limitação de taxa, transparência\n- **Estudos de caso**: Precedentes legais (hiQ vs LinkedIn, QVC vs Resultly)\n- **Quando evitar proxies**: Cenários de alto risco\n\n**Aviso**: Esta é **informação educacional**, não aconselhamento jurídico. A lei varia muito por jurisdição e caso de uso. Consulte um advogado qualificado.\n\n## O Paradoxo do Proxy\n\nAqui está a verdade desconfortável sobre proxies:\n\n!!! warning \"Proxies Não Te Tornam Anônimo. Eles Te Tornam **Diferente**\"\n    Um proxy muda seu endereço IP, mas ele também:\n    \n    - Adiciona **latência** (detectável via análise de tempo)\n    - Reseta valores de **TTL** (revelando saltos do proxy)\n    - Introduz desencontros de **fingerprint TCP** (SO do proxy ≠ seu SO)\n    - Pode injetar **cabeçalhos** (X-Forwarded-For, Via)\n    - Cria **inconsistências de geolocalização** (fuso horário do navegador ≠ localização do IP)\n    \n    Proxies são uma **ferramenta**, não uma solução. Furtividade verdadeira requer **consistência holística**.\n\n## Pré-requisitos\n\nEste é um **material avançado**. Você deve estar confortável com:\n\nConceitos básicos de redes (endereços IP, portas, protocolos)\nFundamentos de TCP/IP (three-way handshake, pacotes, roteamento)\nProgramação Python assíncrona (asyncio, async/await)\nBásico do Pydoll (veja [Conceitos Principais](../../features/core-concepts.md))\n\n**Se você é novo em redes**, recomendamos fortemente:\n\n1.  Ler um guia de fundamentos de TCP/IP primeiro\n2.  Experimentar com o Wireshark para visualizar o tráfego de rede\n3.  Tentar os exemplos de código com capturas de pacotes rodando\n4.  Construir os servidores proxy e testá-los localmente\n\n## Integração com Outros Módulos\n\nArquitetura de rede não existe isoladamente. Ela se integra profundamente com:\n\n- **[Fingerprinting](../fingerprinting/network-fingerprinting.md)**: Como características TCP/IP e TLS te identificam\n- **[Configuração do Navegador](../../features/configuration/browser-preferences.md)**: Alinhando comportamento do navegador com características do proxy\n- **[Camada de Conexão](../fundamentals/connection-layer.md)**: Como o Pydoll gerencia conexões WebSocket sobre proxies\n\n## A Trilha de Aprendizagem\n\nRecomendamos esta progressão:\n\n**Fase 1: Fundação**\n\n1.  Leia [Fundamentos de Rede](./network-fundamentals.md)\n2.  Entenda o modelo OSI e as camadas de protocolo\n3.  Aprenda sobre vazamentos WebRTC e tunelamento UDP\n\n**Fase 2: Análise Profunda de Protocolo**\n\n4.  Estude [Proxies HTTP/HTTPS](./http-proxies.md)\n5.  Domine [Proxies SOCKS](./socks-proxies.md)\n6.  Compare protocolos e entenda as trocas\n\n**Fase 3: Pensamento Adversário**\n\n7.  Explore [Detecção de Proxy](./proxy-detection.md)\n8.  Aprenda técnicas de detecção da perspectiva do defensor\n9.  Aplique estratégias de evasão\n\n**Fase 4: Implementação Prática**\n\n10. Construa servidores proxy de [Construindo Proxies](./build-proxy.md)\n11. Capture e analise tráfego com Wireshark\n12. Teste cadeias de proxy e estratégias de rotação\n\n**Fase 5: Segurança Operacional**\n\n13. Revise as diretrizes [Legais e Éticas](./proxy-legal.md)\n14. Entenda os requisitos de conformidade\n15. Desenvolva políticas de automação responsáveis\n\n\n## A Filosofia\n\nConhecimento de rede e segurança é **poder fundamental**. Diferente de habilidades específicas de frameworks (que se tornam obsoletas), o conhecimento de protocolos é **atemporal**:\n\n- TCP não mudou fundamentalmente desde a RFC 793 (1981)\n- TLS constrói sobre conceitos do SSL (1995)\n- HTTP/2 (2015) e HTTP/3 (2022) são evoluções, não revoluções\n\nDomine esses fundamentos uma vez, e você entenderá **todo sistema baseado em rede** que encontrar pelo resto de sua carreira.\n\n## Compromisso Ético\n\nAntes de prosseguir, reconheça:\n\nEu entendo que proxies podem ser usados tanto para fins legítimos quanto maliciosos\nEu respeitarei os termos de serviço dos sites e o robots.txt\nEu implementarei limitação de taxa e rastreamento respeitoso\nEu não usarei este conhecimento para fraude, abuso ou atividades ilegais\nEu consultarei aconselhamento jurídico quando incerto sobre conformidade\n\n**Com grandes poderes vêm grandes responsabilidades.** Use este conhecimento com sabedoria.\n\n---\n\n## Pronto para Começar?\n\nComece sua jornada com **[Fundamentos de Rede](./network-fundamentals.md)** para construir a base, então progrida através dos módulos em ordem. Cada documento constrói sobre o anterior, criando um entendimento abrangente de arquitetura de rede para automação.\n\n**É aqui que \"script kiddies\" se tornam engenheiros. Vamos começar.**\n\n---\n\n!!! info \"Status da Documentação\"\n    Este módulo sintetiza conhecimento de RFCs, especificações de protocolo, pesquisa em segurança e testes do mundo real. Todo exemplo de código está pronto para produção. Se encontrar imprecisões ou tiver melhorias, contribuições são bem-vindas.\n\n## Navegação Rápida\n\n**Protocolos Principais:**\n\n- [Fundamentos de Rede](./network-fundamentals.md) - TCP/IP, UDP, WebRTC\n- [Proxies HTTP/HTTPS](./http-proxies.md) - Proxying de camada de aplicação\n- [Proxies SOCKS](./socks-proxies.md) - Proxying de camada de sessão\n\n**Tópicos Avançados:**\n\n- [Detecção de Proxy](./proxy-detection.md) - Anonimato e evasão\n- [Construindo Proxies](./build-proxy.md) - Implementação do zero\n- [Legal e Ético](./proxy-legal.md) - Conformidade e responsabilidade\n\n**Módulos Relacionados:**\n\n- [Fingerprinting](../fingerprinting/index.md) - Técnicas de detecção\n- [Configuração do Navegador](../../features/configuration/browser-options.md) - Configuração prática"
  },
  {
    "path": "docs/pt/deep-dive/network/network-fundamentals.md",
    "content": "# Fundamentos de Rede\n\nEste documento cobre os protocolos de rede fundamentais que alimentam a internet e como eles podem expor ou proteger sua identidade em cenários de automação. Uma compreensão funcional de TCP, UDP, do modelo OSI e do WebRTC tornará a configuração de proxy muito menos misteriosa e muito mais eficaz.\n\n!!! info \"Navegação do Módulo\"\n    - [Visão Geral de Rede e Segurança](./index.md): Introdução ao módulo e trilha de aprendizado\n    - [Proxies HTTP/HTTPS](./http-proxies.md): Proxy de camada de aplicação\n    - [Proxies SOCKS](./socks-proxies.md): Proxy de camada de sessão\n\n    Para uso prático do Pydoll, veja [Configuração de Proxy](../../features/configuration/proxy.md) e [Opções do Navegador](../../features/configuration/browser-options.md).\n\n## A Pilha de Rede\n\nCada requisição HTTP que seu navegador faz viaja através de uma pilha de rede em camadas. Cada camada tem responsabilidades, protocolos e implicações de segurança específicas. Proxies operam em camadas diferentes, e a camada determina o que o proxy pode ver, modificar e ocultar. Características de rede em camadas inferiores podem aplicar fingerprinting no seu sistema real mesmo através de proxies, então entender a pilha ajuda você a ver onde ocorrem vazamentos de identidade e como preveni-los.\n\n### O Modelo OSI\n\nO modelo OSI (Open Systems Interconnection), desenvolvido pela ISO em 1984, fornece um framework conceitual para entender como os protocolos de rede interagem. As redes do mundo real usam o modelo TCP/IP (que antecede o OSI e tem apenas 4 camadas), mas a terminologia OSI permanece como a forma padrão de descrever onde os proxies operam e o que eles podem acessar.\n\n```mermaid\ngraph TD\n    L7[Layer 7: Application - HTTP, FTP, SMTP, DNS]\n    L6[Layer 6: Presentation - Encryption, Compression]\n    L5[Layer 5: Session - SOCKS]\n    L4[Layer 4: Transport - TCP, UDP]\n    L3[Layer 3: Network - IP, ICMP]\n    L2[Layer 2: Data Link - Ethernet, WiFi]\n    L1[Layer 1: Physical - Cables, Radio Waves]\n\n    L7 --> L6 --> L5 --> L4 --> L3 --> L2 --> L1\n```\n\nA Camada 7 (Aplicação) é onde vivem os protocolos voltados ao usuário: HTTP, HTTPS, FTP, SMTP e DNS operam aqui. Esta camada contém os dados reais com os quais sua aplicação se importa, como documentos HTML, respostas JSON e transferências de arquivos. Proxies HTTP operam nesta camada, o que lhes dá visibilidade total sobre o conteúdo de requisição e resposta.\n\nA Camada 6 (Apresentação) lida com tradução de formato de dados, criptografia e compressão. SSL/TLS é comumente associado a esta camada pelo seu papel de criptografia, embora na prática o TLS abranja as Camadas 4 a 6 e não se mapeie de forma limpa a nenhuma camada OSI específica. O que importa para automação é que a criptografia HTTPS acontece aqui, criptografando os dados da Camada 7 antes de descerem pela pilha.\n\nA Camada 5 (Sessão) gerencia conexões entre aplicações. Proxies SOCKS operam aqui, abaixo da camada de aplicação mas acima da de transporte. Esta posição torna o SOCKS agnóstico a protocolo: ele pode proxyar qualquer protocolo da Camada 7 (HTTP, FTP, SMTP, SSH) sem precisar entender suas especificidades.\n\nA Camada 4 (Transporte) fornece entrega de dados de ponta a ponta. TCP (orientado à conexão, confiável) e UDP (sem conexão, rápido) são os protocolos dominantes aqui. Esta camada lida com números de porta, controle de fluxo e correção de erros. Todos os proxies dependem, em última análise, da Camada 4 para a transmissão real de dados.\n\nA Camada 3 (Rede) lida com roteamento e endereçamento entre redes. O IP (Internet Protocol) opera aqui, gerenciando endereços IP e decisões de roteamento. É aqui que seu endereço IP real reside, e onde os proxies tentam substituí-lo.\n\nA Camada 2 (Enlace de Dados) gerencia a comunicação no mesmo segmento de rede físico. Ethernet, Wi-Fi e PPP operam aqui, lidando com endereços MAC e transmissão de frames. Endereços MAC são visíveis apenas no segmento de rede local e não são diretamente acessíveis por servidores remotos, embora possam ser expostos através de protocolos como IPv6 SLAAC (que incorpora o MAC no endereço).\n\nA Camada 1 (Física) é o hardware real: cabos, ondas de rádio e níveis de voltagem. Raramente relevante para automação de software.\n\n!!! tip \"OSI vs TCP/IP\"\n    O modelo TCP/IP (4 camadas: Enlace, Internet, Transporte, Aplicação) é o que as redes realmente usam. O OSI (7 camadas) é uma ferramenta de ensino e modelo de referência. Quando as pessoas dizem \"proxy de Camada 7\", elas estão usando a terminologia OSI, mas a implementação real roda sobre TCP/IP.\n\n### Como o Posicionamento da Camada Afeta os Proxies\n\nA camada onde um proxy opera determina o que ele pode e o que não pode fazer.\n\nProxies HTTP/HTTPS operam na Camada 7 (Aplicação). Como eles entendem HTTP, podem ler e modificar URLs, cabeçalhos, cookies e corpos de requisição. Podem fazer cache de respostas de forma inteligente com base na semântica HTTP, filtrar conteúdo por URL ou palavra-chave e injetar cabeçalhos de autenticação. A contrapartida é que eles só entendem HTTP. Não podem proxyar FTP, SMTP, SSH ou outros protocolos, e inspecionar conteúdo HTTPS requer terminação TLS, o que significa descriptografar e recriptografar o tráfego.\n\nProxies SOCKS operam na Camada 5 (Sessão). Como ficam abaixo da camada de aplicação, são agnósticos a protocolo e podem proxyar qualquer protocolo da Camada 7 sem modificação. O tráfego HTTPS passa criptografado de ponta a ponta, já que o proxy SOCKS nunca precisa descriptografá-lo. O SOCKS5 também suporta UDP, permitindo proxyar consultas DNS, VoIP e outros protocolos baseados em UDP. A contrapartida é que proxies SOCKS não têm visibilidade dos dados da camada de aplicação: não podem fazer cache, filtrar por URL ou inspecionar conteúdo. Podem apenas filtrar por IP e porta.\n\n!!! note \"A Contrapartida Fundamental\"\n    Camadas mais altas (Camada 7) oferecem mais controle mas menos flexibilidade. Camadas mais baixas (Camada 5) oferecem menos controle mas mais flexibilidade. Escolha proxies HTTP quando precisar de controle de conteúdo, e proxies SOCKS quando precisar de flexibilidade de protocolo ou criptografia de ponta a ponta.\n\n### O Problema de Vazamento de Camada\n\nMesmo com um proxy de Camada 7 perfeito, características de camadas inferiores podem expor sua identidade real. A pilha TCP do seu sistema operacional na Camada 4 tem um fingerprint único definido pelo tamanho da janela, ordem das opções e valores de TTL. Campos do cabeçalho IP na Camada 3, como TTL e comportamento de fragmentação, revelam seu SO e topologia de rede.\n\nPor exemplo, se você configurar um proxy para apresentar um User-Agent de \"Windows 10\", mas o fingerprint TCP real do seu sistema Linux contradizer isso na Camada 4, sistemas de detecção sofisticados podem marcar essa inconsistência como um forte indicador de bot. É por isso que o fingerprinting em nível de rede (abordado em [Network Fingerprinting](../fingerprinting/network-fingerprinting.md)) é tão perigoso: ele opera abaixo da camada do proxy, expondo seu sistema real mesmo quando o proxy da camada de aplicação é impecável.\n\n## TCP vs UDP\n\nNa Camada 4 (Transporte), dois protocolos fundamentalmente diferentes dominam a comunicação na internet. Eles representam filosofias de design opostas: confiabilidade versus velocidade.\n\nO TCP é orientado à conexão. Pense nele como uma ligação telefônica: você estabelece uma conexão, verifica se a outra parte está ouvindo, troca dados de forma confiável e, em seguida, desliga. Cada byte é confirmado, ordenado e tem entrega garantida. O UDP é sem conexão. Você envia seus dados e espera que cheguem. Sem handshake, sem confirmações, sem garantias. Apenas velocidade bruta com sobrecarga mínima.\n\n| Característica | TCP | UDP |\n|---------|-----|-----|\n| Conexão | Orientado à conexão (handshake necessário) | Sem conexão (sem handshake) |\n| Confiabilidade | Entrega garantida, pacotes ordenados | Entrega de melhor esforço, pacotes podem ser perdidos |\n| Velocidade | Mais lento (sobrecarga dos mecanismos de confiabilidade) | Mais rápido (sobrecarga mínima) |\n| Casos de Uso | Navegação web, transferência de arquivos, email | Streaming de vídeo, consultas DNS, jogos |\n| Tamanho do Cabeçalho | 20 bytes mínimo (até 60 com opções) | 8 bytes fixo |\n| Controle de Fluxo | Sim (janela deslizante, orientado pelo receptor) | Não (transmissor envia à vontade) |\n| Controle de Congestionamento | Sim (desacelera quando a rede está congestionada) | Não (responsabilidade da aplicação) |\n| Verificação de Erros | Extensiva (checksum + acknowledgments) | Básica (apenas checksum; opcional no IPv4, obrigatório no IPv6) |\n| Ordenação | Pacotes reordenados se recebidos fora de sequência | Sem ordenação, pacotes entregues como recebidos |\n| Retransmissão | Automática (pacotes perdidos são retransmitidos) | Nenhuma (aplicação deve tratar) |\n\n### TCP e Proxies\n\nTodos os protocolos de proxy (HTTP, HTTPS, SOCKS4, SOCKS5) usam TCP para seu canal de controle. Isso ocorre porque a autenticação de proxy e a troca de comandos exigem entrega garantida, protocolos de proxy têm sequências de comando estritas (handshake, depois auth, depois dados), e proxies precisam de conexões persistentes para rastrear o estado do cliente.\n\nNo entanto, o SOCKS5 também pode proxyar tráfego UDP, diferente do SOCKS4 ou de proxies HTTP. Isso torna o SOCKS5 essencial para proxyar consultas DNS, áudio/vídeo WebRTC, VoIP e protocolos de jogos.\n\n!!! danger \"UDP e Vazamento de IP\"\n    A maioria das conexões de navegador usa TCP (HTTP, WebSocket, etc.), mas o WebRTC usa UDP diretamente, contornando a configuração de proxy do navegador. Esta é a causa mais comum de vazamento de IP em automação de navegador com proxy: seu tráfego TCP passa pelo proxy enquanto seu tráfego UDP vaza seu IP real.\n\n### O Handshake de Três Vias do TCP\n\nAntes que quaisquer dados possam ser transmitidos, o TCP requer um handshake de três vias para estabelecer uma conexão. Esta negociação sincroniza números de sequência, acorda tamanhos de janela e estabelece o estado da conexão em ambas as pontas.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant Server\n\n    Client->>Server: SYN (Synchronize, seq=x)\n    Note over Client,Server: Client requests connection\n\n    Server->>Client: SYN-ACK (seq=y, ack=x+1)\n    Note over Client,Server: Server acknowledges and sends its own SYN\n\n    Client->>Server: ACK (ack=y+1)\n    Note over Client,Server: Connection established, data transfer begins\n```\n\nO processo começa quando o cliente envia um pacote SYN (Synchronize) contendo um Número de Sequência Inicial (ISN) aleatório, por exemplo `seq=1000`. Junto com o ISN, opções TCP são negociadas: tamanho da janela, Tamanho Máximo de Segmento (MSS), timestamps e suporte a SACK.\n\nO servidor responde com um SYN-ACK: ele escolhe seu próprio ISN aleatório (ex: `seq=5000`) e confirma o ISN do cliente definindo `ack=1001` (ISN do cliente + 1). Este único pacote tanto estabelece a direção servidor-para-cliente (SYN) quanto confirma a direção cliente-para-servidor (ACK). O servidor também retorna suas próprias opções TCP.\n\nO cliente então envia um ACK final, confirmando o ISN do servidor (`ack=5001`). Neste ponto, a conexão está totalmente estabelecida em ambas as direções e a transmissão de dados pode começar.\n\nO ISN é aleatorizado em vez de começar do zero para prevenir ataques de sequestro de TCP. Se ISNs fossem previsíveis, um atacante poderia injetar pacotes em uma conexão existente adivinhando os números de sequência. Sistemas modernos usam aleatoriedade criptográfica para seleção de ISN (RFC 6528).\n\n### Fingerprinting TCP\n\nO handshake TCP revela características que aplicam fingerprinting no seu sistema operacional. Diferentes SOs usam valores padrão diferentes para o tamanho inicial da janela, ordem das opções TCP, TTL (Time To Live), fator de escala da janela e comportamento de timestamp. Esses valores são definidos pelo kernel, não pelo navegador, então um proxy não pode alterá-los.\n\nAqui estão exemplos ilustrativos para sistemas operacionais modernos. Note que os valores reais variam entre versões de SO, configurações de kernel e ajustes de rede:\n\n```\nWindows 10/11 (modern builds):\n    Window Size: 65535\n    MSS: 1460\n    Options: MSS, NOP, WS, NOP, NOP, SACK_PERM\n    TTL: 128\n\nLinux (kernel 5.x+, Ubuntu 20.04+):\n    Window Size: 29200\n    MSS: 1460\n    Options: MSS, SACK_PERM, TS, NOP, WS\n    TTL: 64\n\nmacOS (Monterey+):\n    Window Size: 65535\n    TTL: 64\n```\n\nEssas diferenças estão gravadas no kernel. Um proxy não pode alterá-las porque são definidas pelo seu sistema operacional, não pelo seu navegador. É assim que sistemas de detecção sofisticados podem identificá-lo mesmo através de proxies.\n\n!!! warning \"Limitação do Proxy\"\n    Proxies HTTP e SOCKS operam acima da camada TCP. Eles não podem modificar características do handshake TCP. O fingerprint TCP do seu SO está sempre exposto ao servidor proxy e a quaisquer observadores de rede entre você e o proxy. Apenas soluções em nível de VPN ou configuração da pilha TCP em nível de SO podem resolver isso.\n\n!!! note \"Além do Fingerprinting TCP\"\n    O handshake TCP é apenas a primeira oportunidade de fingerprinting. Imediatamente após, o handshake TLS revela outro fingerprint único conhecido como JA3/JA4. Veja [Network Fingerprinting](../fingerprinting/network-fingerprinting.md) para detalhes sobre fingerprinting de TLS e HTTP/2.\n\n### UDP\n\nDiferente da abordagem confiável e orientada à conexão do TCP, o UDP é um protocolo de \"dispare-e-esqueça\". Ele troca confiabilidade por latência e sobrecarga mínimas, tornando-o ideal para aplicações em tempo real onde a velocidade importa mais que a entrega perfeita.\n\nUm datagrama UDP tem apenas um cabeçalho de 8 bytes (comparado com 20-60 bytes do TCP), contendo porta de origem, porta de destino, comprimento e um checksum. Não há estabelecimento de conexão, nenhuma garantia de confiabilidade, nenhum controle de fluxo e nenhum controle de congestionamento. Se um pacote for perdido, a aplicação deve decidir se e como tratar isso.\n\nO UDP é a escolha certa para comunicação em tempo real (chamadas de voz/vídeo via WebRTC e VoIP), jogos (atualizações de estado de baixa latência), streaming (onde perda ocasional de frames é aceitável) e consultas DNS (pares pequenos de requisição/resposta onde a aplicação cuida das retentativas). É uma escolha ruim para transferências de arquivos, navegação web, email ou bancos de dados, todos os quais precisam de entrega confiável e ordenada.\n\nO DNS é um exemplo particularmente importante no contexto de automação. O DNS usa UDP porque as consultas são tipicamente pequenas e se beneficiam da ausência de sobrecarga de handshake do UDP. Embora o EDNS0 (RFC 6891) tenha aumentado o payload máximo de DNS sobre UDP além do limite original de 512 bytes, a maioria das consultas permanece compacta. O cliente DNS cuida das retentativas em nível de aplicação se uma resposta não chegar dentro do timeout.\n\nPara automação de navegador, a preocupação principal com o UDP é que o WebRTC o utiliza para áudio e vídeo em tempo real, consultas DNS o utilizam para resolução de domínio, e a maioria dos proxies (HTTP, HTTPS, SOCKS4) só lida com TCP. A menos que você configure explicitamente o proxy de UDP, esse tráfego contorna seu proxy e vaza seu IP real.\n\n| Tipo de Proxy | Suporte UDP | Notas |\n|------------|-------------|-------|\n| Proxy HTTP | Não | Proxyia apenas HTTP/HTTPS baseado em TCP |\n| Proxy HTTPS (CONNECT) | Não | O método CONNECT só estabelece túneis TCP |\n| SOCKS4 | Não | Protocolo apenas TCP |\n| SOCKS5 | Sim | Suporta relay UDP via comando `UDP ASSOCIATE` |\n| VPN | Sim | Tunela todo o tráfego IP (TCP e UDP) |\n\nPara anonimato verdadeiro em automação de navegador, você precisa de: um proxy SOCKS5 com suporte UDP e WebRTC configurado para usá-lo, WebRTC desabilitado inteiramente (o que quebra videoconferência), uma VPN que tunele todo o tráfego, ou a flag do navegador `--force-webrtc-ip-handling-policy=disable_non_proxied_udp`.\n\n### QUIC e HTTP/3\n\nNavegadores modernos utilizam cada vez mais o QUIC (RFC 9000), um protocolo de transporte baseado em UDP que alimenta o HTTP/3. Como o QUIC roda sobre UDP, ele compartilha os mesmos problemas de bypass de proxy que o WebRTC e o DNS: a maioria dos proxies HTTP não consegue lidar com tráfego QUIC, e ele pode vazar para fora da sua configuração de proxy.\n\nEm cenários de automação, considere desabilitar o QUIC com a flag `--disable-quic` do Chrome para forçar HTTP/2 sobre TCP, garantindo que todo o tráfego web passe pelo seu proxy. O QUIC também tem suas próprias características de fingerprinting, similares ao JA3 para TLS, o que adiciona mais um vetor de detecção.\n\n## WebRTC e Vazamento de IP\n\nWebRTC (Web Real-Time Communication) é uma API de navegador padronizada pelo W3C que permite comunicação ponto-a-ponto de áudio, vídeo e dados diretamente entre navegadores, sem plugins ou servidores intermediários. Embora poderosa para aplicações em tempo real, o WebRTC é a maior fonte isolada de vazamento de IP em automação de navegador com proxy.\n\n### Como o WebRTC Vaza Seu IP\n\nO WebRTC foi projetado para conexões ponto-a-ponto diretas, otimizando para baixa latência em detrimento da privacidade. Para estabelecer conexões P2P, o WebRTC deve descobrir seu endereço IP público real e compartilhá-lo com o par remoto, mesmo se seu navegador estiver configurado para usar um proxy.\n\nO problema se desenrola assim: seu navegador usa um proxy para tráfego HTTP/HTTPS (que é TCP), mas o WebRTC usa servidores STUN para descobrir seu IP público real sobre UDP. Consultas STUN contornam o proxy porque a maioria dos proxies só lida com TCP. Seu IP real é descoberto e compartilhado com pares remotos como parte da negociação de conexão. JavaScript na página pode ler esses \"candidatos ICE\" e enviar seu IP real para o servidor do site.\n\n!!! danger \"Gravidade dos Vazamentos WebRTC\"\n    Mesmo com um proxy HTTP configurado corretamente, proxy HTTPS funcionando, consultas DNS proxyadas, User-Agent falsificado e fingerprinting de canvas mitigado, o WebRTC ainda pode vazar seu IP real em milissegundos. Isso porque o WebRTC opera abaixo da camada de proxy do navegador, interagindo diretamente com a pilha de rede do SO.\n\n### O Processo ICE\n\nO WebRTC usa ICE (Interactive Connectivity Establishment, RFC 8445) para descobrir caminhos de conexão possíveis e selecionar o melhor. Este processo inerentemente revela sua topologia de rede ao coletar três tipos de candidatos.\n\n```mermaid\nsequenceDiagram\n    participant Browser\n    participant STUN as STUN Server\n    participant TURN as TURN Relay\n    participant Peer as Remote Peer\n\n    Note over Browser: WebRTC connection initiated\n\n    Browser->>Browser: Gather local IP addresses<br/>(LAN interfaces)\n    Note over Browser: Local candidate:<br/>192.168.1.100:54321\n\n    Browser->>STUN: STUN Binding Request (over UDP)\n    Note over STUN: STUN server discovers public IP<br/>(bypasses proxy!)\n    STUN->>Browser: STUN Response with real public IP\n    Note over Browser: Server reflexive candidate:<br/>203.0.113.45:54321\n\n    Browser->>TURN: Allocate relay (if needed)\n    TURN->>Browser: Relay address assigned\n    Note over Browser: Relay candidate:<br/>198.51.100.10:61234\n\n    Browser->>Peer: Send all ICE candidates<br/>(local + public + relay)\n    Note over Peer: Now knows your:<br/>- LAN IP<br/>- Real public IP<br/>- Relay address\n\n    Peer->>Browser: Send ICE candidates\n\n    Note over Browser,Peer: ICE negotiation: try direct P2P first\n\n    alt Direct P2P succeeds\n        Browser<<->>Peer: Direct connection (bypasses proxy entirely!)\n    else Direct P2P fails (firewall/NAT)\n        Browser->>TURN: Use TURN relay\n        TURN<<->>Peer: Relayed connection\n        Note over Browser,Peer: Higher latency, but works\n    end\n```\n\n### Tipos de Candidatos ICE\n\nO ICE descobre três tipos de candidatos (possíveis endpoints de conexão), cada um revelando diferentes informações sobre sua rede.\n\n**Candidatos host** são seus endereços IP locais da LAN. O navegador enumera todas as interfaces de rede locais e cria candidatos para cada uma. Isso revela seus endereços IP locais em redes privadas, sua topologia de rede (presença de interfaces VPN, pontes de VM) e o número de interfaces de rede.\n\n```javascript\n// Example host candidates\ncandidate:1 1 UDP 2130706431 192.168.1.100 54321 typ host\ncandidate:2 1 UDP 2130706431 10.0.0.5 54322 typ host\n```\n\nNavegadores modernos (Chrome 75+, Firefox 78+, Safari) mitigam vazamentos de candidatos host substituindo endereços IP locais por nomes mDNS efêmeros (ex: `a1b2c3d4.local`) quando permissões de mídia (câmera/microfone) não foram concedidas. No entanto, candidatos reflexivos de servidor (seu IP público) permanecem expostos independentemente do mDNS.\n\n**Candidatos reflexivos de servidor** são seu IP público como visto por um servidor STUN. O navegador envia uma requisição STUN para um servidor público, que responde com seu endereço IP público. Este é o vazamento do qual todos falam: seu proxy mostra um IP, mas o WebRTC revela o seu real, junto com seu tipo de NAT, mapeamento de porta externa e informações do ISP.\n\n```javascript\n// Server reflexive candidate (your real public IP)\ncandidate:4 1 UDP 1694498815 203.0.113.45 54321 typ srflx raddr 192.168.1.100 rport 54321\n```\n\n**Candidatos de retransmissão** são endereços de servidores TURN usados como fallback quando o P2P direto falha. O candidato de retransmissão ainda pode conter seu IP real no campo `raddr` (endereço remoto), dependendo da implementação do servidor TURN.\n\n```javascript\n// Relay candidate (TURN server address)\ncandidate:5 1 UDP 16777215 198.51.100.10 61234 typ relay raddr 203.0.113.45 rport 54321\n```\n\n### O Protocolo STUN\n\nSTUN (Session Traversal Utilities for NAT, RFC 8489) é um protocolo simples de requisição-resposta sobre UDP. Sua função é direta: o cliente pergunta \"qual IP você vê de mim?\" e o servidor responde com o IP público e a porta do cliente.\n\nO cliente envia uma Binding Request contendo um magic cookie (`0x2112A442`, um valor fixo definido pela RFC) e um transaction ID aleatório de 12 bytes. O servidor responde com uma Binding Success Response que inclui um atributo `XOR-MAPPED-ADDRESS` contendo o IP público e a porta do cliente como vistos da perspectiva do servidor.\n\nO endereço IP na resposta é XOR'ado com o magic cookie e o transaction ID. Isso não é por segurança, mas por compatibilidade com NAT: alguns dispositivos NAT modificam incorretamente endereços IP em payloads de pacotes, e o XOR ofusca o endereço para prevenir essa interferência.\n\nServidores STUN públicos comumente usados por navegadores incluem `stun.l.google.com:19302` (Google), `stun1.l.google.com:19302` (Google), `stun.services.mozilla.com` (Mozilla) e `stun.stunprotocol.org:3478`.\n\n### Por que Proxies Não Conseguem Parar Vazamentos WebRTC\n\nVazamentos WebRTC acontecem por vários motivos que se reforçam. Primeiro, o WebRTC usa UDP, e a maioria dos proxies (HTTP, HTTPS CONNECT, SOCKS4) só lida com TCP. Apenas o SOCKS5 suporta UDP, e mesmo assim o navegador precisa ser explicitamente configurado para rotear o WebRTC através dele.\n\nSegundo, o WebRTC é uma API de navegador que opera abaixo da camada HTTP. Ele acessa diretamente a pilha de rede do SO, contornando configurações de proxy definidas para HTTP/HTTPS. Consultas STUN vão diretamente para a interface de rede, e a tabela de roteamento do SO determina seu caminho, não a configuração de proxy do navegador. Apenas roteamento em nível de VPN pode interceptá-las.\n\nTerceiro, o WebRTC enumera todas as interfaces de rede (ethernet física, Wi-Fi, adaptadores VPN, pontes de VM), incluindo interfaces não usadas para navegação regular. Isso vaza sua topologia de rede interna.\n\nFinalmente, páginas web podem ler candidatos ICE via JavaScript usando o evento `RTCPeerConnection.onicecandidate`, extrair endereços IP das strings de candidatos com um regex simples e enviar seu IP real para o servidor de rastreamento deles.\n\n### Prevenindo Vazamentos WebRTC no Pydoll\n\nO Pydoll fornece múltiplas estratégias para prevenir vazamentos de IP via WebRTC.\n\n**Método 1: Forçar o WebRTC a usar apenas rotas proxyadas (recomendado)**\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.webrtc_leak_protection = True  # Adds --force-webrtc-ip-handling-policy=disable_non_proxied_udp\n```\n\nO Pydoll fornece uma propriedade conveniente `webrtc_leak_protection` que gerencia a flag do Chrome subjacente para você. Isso desabilita UDP se nenhum proxy o suportar, força o WebRTC a usar apenas retransmissores TURN (sem P2P direto) e previne consultas STUN para servidores públicos. A contrapartida é maior latência para chamadas de vídeo, já que conexões P2P diretas são desabilitadas.\n\n**Método 2: Desabilitar o WebRTC inteiramente**\n\n```python\noptions.add_argument('--disable-features=WebRTC')\n```\n\nIsso desabilita completamente a API WebRTC, eliminando qualquer possibilidade de vazamento de IP por este vetor. A contrapartida é que todos os sites dependentes de WebRTC (videoconferência, chamadas de voz) deixarão de funcionar. Note que esta flag deve ser testada com sua versão específica do Chrome, pois nomes de feature flags podem variar entre versões.\n\n**Método 3: Restringir o WebRTC via preferências do navegador**\n\n```python\noptions.browser_preferences = {\n    'webrtc': {\n        'ip_handling_policy': 'disable_non_proxied_udp',\n        'multiple_routes_enabled': False,\n        'nonproxied_udp_enabled': False,\n        'allow_legacy_tls_protocols': False\n    }\n}\n```\n\nIsso alcança o mesmo efeito do Método 1, mas através de preferências em vez de flags de linha de comando. `multiple_routes_enabled` previne o uso de múltiplos caminhos de rede, e `nonproxied_udp_enabled` bloqueia UDP que não passa pelo proxy.\n\n**Método 4: Usar um proxy SOCKS5 com suporte UDP**\n\n```python\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\noptions.add_argument('--force-webrtc-ip-handling-policy=default_public_interface_only')\n```\n\nO SOCKS5 pode proxyar UDP via seu comando `UDP ASSOCIATE`, permitindo que as consultas STUN do WebRTC passem pelo proxy. Isso requer um proxy SOCKS5 que realmente suporte relay UDP, o que nem todos suportam.\n\n!!! warning \"Autenticação SOCKS5\"\n    O Chrome não suporta autenticação SOCKS5 inline (ex: `socks5://user:pass@host:port`) via a flag `--proxy-server`. O Pydoll fornece um `SOCKS5Forwarder` integrado que contorna essa limitação executando um proxy SOCKS5 local sem autenticação que encaminha o tráfego para o proxy remoto autenticado, cuidando do handshake de usuário/senha em nome do Chrome. Veja [Configuração de Proxy](../../features/configuration/proxy.md) para detalhes de uso.\n\n### Testando Vazamentos WebRTC\n\nVocê pode testar manualmente visitando [browserleaks.com/webrtc](https://browserleaks.com/webrtc) e verificando se seu IP real aparece na seção \"Public IP Address\". Se você vir seu IP real em vez do IP do proxy, está havendo vazamento.\n\nPara testes automatizados com o Pydoll:\n\n```python\nimport asyncio\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def test_webrtc_leak():\n    options = ChromiumOptions()\n    options.add_argument('--proxy-server=http://proxy.example.com:8080')\n    options.add_argument('--force-webrtc-ip-handling-policy=disable_non_proxied_udp')\n\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://browserleaks.com/webrtc')\n\n        await asyncio.sleep(3)\n\n        ips = await tab.execute_script('''\n            return Array.from(document.querySelectorAll('.ip-address'))\n                .map(el => el.textContent.trim());\n        ''')\n\n        print(\"Detected IPs:\", ips)\n        # Should only show proxy IP, not your real IP\n\nasyncio.run(test_webrtc_leak())\n```\n\n!!! danger \"Sempre Teste Vazamentos WebRTC\"\n    Nunca assuma que sua configuração de proxy previne vazamentos WebRTC. Sempre verifique com [browserleaks.com/webrtc](https://browserleaks.com/webrtc) ou [ipleak.net](https://ipleak.net). Mesmo um único vazamento WebRTC compromete instantaneamente toda a sua configuração de proxy, já que o site agora conhece sua localização real, ISP e topologia de rede.\n\n### Como Sites Exploram Vazamentos WebRTC\n\nSites podem intencionalmente acionar o WebRTC para extrair seu IP real usando poucas linhas de JavaScript:\n\n```javascript\nconst pc = new RTCPeerConnection({\n    iceServers: [{urls: 'stun:stun.l.google.com:19302'}]\n});\n\npc.createDataChannel('');\npc.createOffer().then(offer => pc.setLocalDescription(offer));\n\npc.onicecandidate = (event) => {\n    if (event.candidate) {\n        const ipRegex = /([0-9]{1,3}(\\.[0-9]{1,3}){3})/;\n        const ipMatch = event.candidate.candidate.match(ipRegex);\n\n        if (ipMatch) {\n            const realIP = ipMatch[1];\n            fetch(`/track?real_ip=${realIP}&proxy_ip=${window.clientIP}`);\n        }\n    }\n};\n```\n\nEste código cria um RTCPeerConnection, aciona a coleta de candidatos ICE (que contata servidores STUN), extrai endereços IP dos candidatos com um regex e envia seu IP real para um servidor de rastreamento. Desabilitar o WebRTC ou forçar rotas apenas proxyadas como descrito acima previne isso.\n\n## Resumo\n\nProxies operam em camadas específicas da pilha de rede: HTTP na Camada 7, SOCKS na Camada 5. A camada determina o que o proxy pode ver, modificar e ocultar. Fingerprints TCP (tamanho da janela, opções, TTL) vazam de camadas inferiores e revelam seu SO real mesmo através de um proxy. Tráfego UDP, incluindo WebRTC e DNS, frequentemente contorna proxies a menos que explicitamente configurado. O WebRTC é a fonte mais comum de vazamento de IP, e apenas SOCKS5 ou VPN podem proxyar tráfego UDP de forma eficaz. Navegadores modernos também usam QUIC (HTTP/3 sobre UDP), o que adiciona mais um vetor potencial de bypass.\n\n**Próximos passos:**\n\n- [Proxies HTTP/HTTPS](./http-proxies.md): Proxy de camada de aplicação\n- [Proxies SOCKS](./socks-proxies.md): Proxy de camada de sessão, agnóstico a protocolo\n- [Network Fingerprinting](../fingerprinting/network-fingerprinting.md): Técnicas de fingerprinting TCP/IP e TLS\n- [Configuração de Proxy](../../features/configuration/proxy.md): Configuração prática de proxy no Pydoll\n\n## Referências\n\n- RFC 793: Transmission Control Protocol (TCP) - https://tools.ietf.org/html/rfc793\n- RFC 768: User Datagram Protocol (UDP) - https://tools.ietf.org/html/rfc768\n- RFC 8489: Session Traversal Utilities for NAT (STUN) - https://tools.ietf.org/html/rfc8489\n- RFC 8445: Interactive Connectivity Establishment (ICE) - https://tools.ietf.org/html/rfc8445\n- RFC 8656: Traversal Using Relays around NAT (TURN) - https://tools.ietf.org/html/rfc8656\n- RFC 6528: Defending Against Sequence Number Attacks - https://tools.ietf.org/html/rfc6528\n- RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport - https://tools.ietf.org/html/rfc9000\n- W3C WebRTC 1.0: Real-Time Communication Between Browsers - https://www.w3.org/TR/webrtc/\n- BrowserLeaks: WebRTC Leak Test - https://browserleaks.com/webrtc\n- IPLeak: Comprehensive Leak Testing - https://ipleak.net\n"
  },
  {
    "path": "docs/pt/deep-dive/network/proxy-detection.md",
    "content": "# Detecção de Proxy\n\nA detecção de proxy é um processo probabilístico. Sites combinam dezenas de sinais para avaliar se uma conexão está sendo proxyada, desde simples consultas de reputação de IP até análise da pilha TCP/IP e perfil comportamental. Nenhum sinal isolado fornece prova definitiva, mas combinar sinais fracos suficientes produz decisões de alta confiança.\n\nEste documento cobre as principais técnicas de detecção, como elas funcionam em nível técnico e o que significam para automação de navegador com o Pydoll.\n\n!!! info \"Navegação do Módulo\"\n    - [Proxies SOCKS](./socks-proxies.md): Proxying na camada de sessão\n    - [Proxies HTTP/HTTPS](./http-proxies.md): Proxying na camada de aplicação\n    - [Fundamentos de Rede](./network-fundamentals.md): TCP/IP, UDP, WebRTC\n\n    Para detalhes sobre fingerprinting, veja [Network Fingerprinting](../fingerprinting/network-fingerprinting.md) e [Browser Fingerprinting](../fingerprinting/browser-fingerprinting.md).\n\n## Reputação de IP\n\nA análise de reputação de IP é a técnica de detecção de proxy mais amplamente implantada. Ela combina dados publicamente disponíveis (registros ASN, WHOIS, bancos de dados de geolocalização) com inteligência proprietária para classificar endereços IP em categorias de risco.\n\n### Classificação por ASN\n\nTodo endereço IP pertence a um Sistema Autônomo (AS), identificado por um ASN. O tipo de AS que possui um IP é o indicador individual mais forte de que ele é um proxy.\n\nIPs pertencentes a provedores de nuvem e hospedagem (AWS, DigitalOcean, OVH, Hetzner) são sinalizados como alto risco porque usuários reais não navegam na web a partir de servidores de datacenter. IPs de ISPs residenciais (Comcast, Deutsche Telekom, BT) são baixo risco porque parecem conexões domésticas normais. IPs de operadoras móveis (Verizon Wireless, AT&T Mobility) são o risco mais baixo porque são os mais difíceis de distinguir de usuários móveis reais.\n\nAlguns ASNs estão associados a infraestrutura conhecida de proxy, embora isso seja mais nuançado do que possa parecer. Grandes provedores de proxy residencial como BrightData ou Smartproxy não operam seus próprios ASNs; eles roteiam tráfego através de IPs residenciais reais pertencentes a ASNs de ISPs. Isso é precisamente o que torna proxies residenciais mais difíceis de detectar do que proxies de datacenter.\n\nSistemas de detecção consultam bancos de dados de ASN (Team Cymru, RIPE NCC, ARIN) e APIs comerciais de inteligência de IP para classificar cada IP conectando. IPs de datacenter são detectados com aproximadamente 95%+ de precisão porque a classificação por ASN é inequívoca. A detecção de proxy residencial é muito mais difícil (aproximadamente 40-70% de precisão) porque os IPs genuinamente pertencem a ISPs. A detecção de proxy móvel é a mais difícil (aproximadamente 20-40%) porque o NAT de operadoras móveis faz muitos usuários reais compartilharem IPs.\n\nEsse gradiente de precisão é o motivo pelo qual proxies residenciais e móveis custam de 10 a 100 vezes o preço dos proxies de datacenter.\n\n### Bancos de Dados de Proxies Conhecidos\n\nAlém da classificação por ASN, bancos de dados especializados rastreiam IPs que foram observados participando de redes de proxy. Serviços como IPQualityScore, proxycheck.io e Spur.us mantêm bancos de dados em tempo real de IPs conhecidos de proxy, VPN e nós de saída Tor. A lista de nós de saída Tor está disponível publicamente em [check.torproject.org](https://check.torproject.org/torbulkexitlist).\n\nEsses bancos de dados também rastreiam sinais comportamentais: IPs que rotacionam frequentemente (típico de pools de proxy), IPs com contagens anormalmente altas de sessões concorrentes (um IP residencial normalmente tem 1-5 conexões concorrentes, não 100+) e IPs previamente associados a atividade semelhante a bot.\n\n### Consistência de Geolocalização\n\nProxies frequentemente se revelam através de inconsistências geográficas. O endereço IP aponta para uma localização, mas os sinais reportados pelo navegador apontam para outra.\n\nOs desencontros mais comuns são entre a geolocalização do IP e o fuso horário do navegador (coletado via `Intl.DateTimeFormat().resolvedOptions().timeZone` do JavaScript), entre o país do IP e o cabeçalho `Accept-Language`, e entre a localização da sessão atual e a localização de uma sessão anterior. Um usuário aparecendo em Los Angeles com fuso horário do navegador `Europe/Berlin` é suspeito. Um usuário aparecendo em Tóquio 10 minutos após sua última sessão ter sido em Nova York é fisicamente impossível.\n\nSistemas de detecção também verificam se a geolocalização do IP corresponde à configuração de localidade do navegador. Um IP de datacenter dos EUA com `Accept-Language: zh-CN` e fuso horário `Asia/Shanghai` sugere fortemente um usuário chinês roteando através de um proxy nos EUA.\n\n!!! note \"Falsos Positivos\"\n    Cenários legítimos disparam alarmes de geolocalização: viajantes usando VPNs, expatriados com configurações de navegador do país de origem, usuários corporativos conectando através de VPNs da empresa e usuários multilíngues com preferências de idioma não padrão. Sistemas sofisticados usam pontuação de risco em vez de bloqueio binário para lidar com esses casos.\n\n## Análise de Cabeçalho HTTP\n\nCabeçalhos HTTP são o vetor de detecção mais simples. Proxies transparentes e anônimos adicionam cabeçalhos como `Via`, `X-Forwarded-For`, `X-Real-IP` e `Forwarded` (RFC 7239) que revelam diretamente o uso de proxy. Proxies elite removem esses cabeçalhos, mas sua ausência por si só não é prova de uma conexão direta.\n\nA detecção vai além de procurar cabeçalhos específicos de proxy. Cabeçalhos ausentes que navegadores reais sempre enviam (como `Accept-Language`, `Accept-Encoding` ou um `User-Agent` realista) são suspeitos. A ordem dos cabeçalhos também importa: navegadores enviam cabeçalhos em uma ordem consistente e específica da versão, e proxies ou ferramentas de automação que constroem cabeçalhos manualmente frequentemente erram a ordem.\n\nO cabeçalho legado `Proxy-Connection: keep-alive`, enviado por alguns clientes mais antigos ao rotear através de um proxy, é outro sinal clássico de detecção.\n\n### Níveis de Anonimato de Proxy\n\nProxies são tradicionalmente classificados em três níveis de anonimato com base em seu comportamento de cabeçalhos:\n\n| Nível | Comportamento | Detecção |\n|-------|---------------|----------|\n| Transparente | Encaminha seu IP real em `X-Forwarded-For`, adiciona cabeçalho `Via` | Trivial |\n| Anônimo | Esconde seu IP mas adiciona `Via` ou outros cabeçalhos de proxy | Fácil |\n| Elite | Remove todos os cabeçalhos identificadores de proxy | Requer análise mais profunda |\n\nEssa classificação data de uma era em que a análise de cabeçalhos era o método primário de detecção. Sistemas modernos de detecção usam reputação de IP, fingerprinting e análise comportamental, tornando a distinção transparente/anônimo/elite menos significativa. Um proxy elite com IP de datacenter é detectado instantaneamente através de consulta ASN. Um proxy transparente em um IP residencial ainda pode passar despercebido em sites menos sofisticados.\n\n## Network Fingerprinting\n\nO fingerprinting na camada de rede opera abaixo da camada de proxy, o que significa que pode detectar proxies mesmo quando o proxy em si está configurado perfeitamente.\n\n### Fingerprinting de TCP/IP\n\nCada sistema operacional tem uma implementação única da pilha TCP que se revela durante o handshake TCP. O tamanho inicial da janela, a ordem das opções TCP, o TTL (Time To Live) e o fator de escala de janela são todos definidos pelo kernel, não pelo navegador, e não podem ser alterados por um proxy.\n\nSistemas de detecção comparam essas características TCP com o cabeçalho `User-Agent`. Se o User-Agent alega Windows 10 mas o fingerprint TCP mostra características de Linux (TTL de 64, tamanho de janela de 29200), o desencontro é um forte indicador de proxy. O Windows usa um TTL padrão de 128 e versões modernas tipicamente mostram um tamanho de janela de 65535, enquanto o Linux usa TTL 64 e tamanhos de janela em torno de 29200.\n\nA análise de TTL adiciona outra camada. O TTL diminui em 1 a cada salto de rede. Se uma conexão Windows chega com TTL de 128, o cliente provavelmente está na mesma rede. Se chega com TTL de 115, cruzou aproximadamente 13 saltos. Se o valor do TTL não se alinha com a contagem esperada de saltos para a localização geográfica do IP, roteamento por proxy é provável.\n\nPara valores detalhados de fingerprint TCP e suas implicações, veja [Network Fingerprinting](../fingerprinting/network-fingerprinting.md).\n\n### Fingerprinting de TLS (JA3/JA4)\n\nA mensagem TLS ClientHello é transmitida em texto plano e contém parâmetros suficientes para identificar unicamente a aplicação cliente: versão TLS, conjuntos de cifras suportados, extensões, curvas elípticas e algoritmos de assinatura. O fingerprint JA3 é um hash MD5 desses parâmetros concatenados em uma ordem específica. JA4 é uma alternativa mais recente e mais granular.\n\nCada versão de navegador produz um fingerprint JA3/JA4 distinto. Sistemas de detecção mantêm bancos de dados de fingerprints conhecidos para Chrome, Firefox, Safari e outros navegadores. Se o fingerprint JA3 não corresponde a nenhum navegador conhecido, ou não corresponde ao navegador alegado no User-Agent, a conexão é sinalizada.\n\nUma nuance importante: proxies SOCKS5 e túneis HTTP CONNECT passam o TLS ClientHello sem modificação, então o servidor destino vê o fingerprint real do navegador. O proxy não altera os parâmetros TLS nessas configurações. Apenas proxies MITM (que terminam e reestabelecem TLS) mudam o fingerprint, e nesse caso o fingerprint pertence ao software do proxy, não a um navegador real, o que por si só é um sinal de detecção.\n\n### Fingerprinting de HTTP/2\n\nConexões HTTP/2 expõem sinais de fingerprinting que são distintos do TLS. O frame SETTINGS enviado no início de uma conexão HTTP/2 contém parâmetros como `HEADER_TABLE_SIZE`, `MAX_CONCURRENT_STREAMS`, `INITIAL_WINDOW_SIZE` e `MAX_HEADER_LIST_SIZE`. Cada navegador usa valores padrão diferentes para essas configurações.\n\nA ordem e prioridade dos pseudo-cabeçalhos (`:method`, `:authority`, `:scheme`, `:path`), o comportamento de compressão HPACK e os pesos de prioridade de stream também variam entre navegadores. Ferramentas como [browserleaks.com/http2](https://browserleaks.com/http2) mostram como é o seu fingerprint HTTP/2.\n\nFrameworks de automação e software de proxy que implementam suas próprias pilhas HTTP/2 frequentemente produzem fingerprints que não correspondem a nenhum navegador real, tornando isso um vetor eficaz de detecção.\n\n### Detecção Baseada em Latência\n\nA latência de rede entre um cliente e um servidor revela informações sobre o caminho físico da rede. Se o IP geolocaliza em Nova York mas o tempo de ida e volta sugere um caminho pela Ásia, a conexão provavelmente está sendo proxyada.\n\nSistemas de detecção medem o RTT (round-trip time) durante o handshake TCP e comparam com latências esperadas para a localização geográfica do IP. Eles também podem emitir desafios de temporização baseados em JavaScript que medem a latência da perspectiva do navegador, e então comparar com a latência observada pelo servidor. Uma discrepância significativa entre as duas sugere um intermediário (proxy) no caminho.\n\nA análise de desvio de relógio adiciona outra dimensão: ao medir o deslocamento do relógio do cliente via JavaScript (`Date.now()`) ou cabeçalhos HTTP `Date`, sistemas de detecção podem inferir o fuso horário real do cliente e compará-lo com o fuso horário esperado do IP.\n\n## Detecção Comportamental\n\nOs sistemas de detecção mais avançados vão além da análise de rede e protocolo para examinar o comportamento do usuário. Isso inclui temporização de requisições (as requisições estão espaçadas uniformemente, sugerindo automação?), padrões de movimento do mouse (analisados via listeners de eventos JavaScript), comportamento de rolagem, cadência de entrada do teclado e padrões gerais de navegação.\n\nModelos de machine learning treinados em milhões de sessões de usuários reais podem distinguir comportamento humano de automação com alta precisão. Esses modelos tipicamente combinam 50+ características incluindo padrões de navegação, distribuição de duração de sessão, posições de clique, temporização de interação com formulários e características de execução de JavaScript.\n\nAs interações humanizadas do Pydoll (movimento do mouse com curva de Bézier, temporização pela Lei de Fitts, digitação realista) são projetadas especificamente para passar na análise comportamental. Veja [Técnicas de Evasão](../fingerprinting/evasion-techniques.md) para a estratégia completa de evasão multicamada.\n\n## Pontuação de Risco Multi-Sinal\n\nSistemas modernos de detecção não dependem de nenhuma técnica isolada. Eles combinam todos os sinais disponíveis em uma pontuação de risco, tipicamente de 0 a 100, e aplicam limiares que variam por indústria e contexto.\n\nO peso de cada categoria de sinal varia, mas uma aproximação grosseira é que a reputação de IP representa a maior parcela (é o sinal mais barato e confiável), seguida por network fingerprinting (TCP/IP, TLS, HTTP/2), análise de cabeçalhos e protocolo, pontuação comportamental e verificações de consistência (geolocalização, fuso horário, idioma).\n\nOs limiares dependem do contexto de negócio. Sites bancários bloqueiam agressivamente (pontuação de risco acima de 50), sites de e-commerce apresentam CAPTCHAs em pontuações moderadas (acima de 70), e sites de conteúdo tendem a ser mais permissivos (bloqueando apenas acima de 80) pois dependem de impressões de anúncios.\n\nA implicação para automação é que passar em uma camada de detecção não é suficiente. Um IP residencial (boa reputação de IP) com um fingerprint TCP incompatível e comportamento robótico ainda será sinalizado. Evasão eficaz requer consistência em todas as camadas.\n\n## Detecção por Tipo de Proxy\n\n| Tipo de Proxy | Dificuldade de Detecção | Métodos Primários de Detecção |\n|---------------|-------------------------|-------------------------------|\n| HTTP Transparente | Trivial | Cabeçalhos HTTP (`Via`, `X-Forwarded-For`) |\n| HTTP Anônimo | Fácil | Cabeçalhos HTTP + Reputação de IP |\n| HTTP Elite (datacenter) | Médio | Reputação de IP (análise de ASN) |\n| SOCKS5 Datacenter | Médio | Reputação de IP (análise de ASN) |\n| Proxies residenciais | Difícil | Análise comportamental, padrões de conexão, latência |\n| Proxies móveis | Muito difícil | Principalmente comportamental, sinais de rede limitados |\n| Proxies rotativos | Difícil | Inconsistências de sessão, padrões de rotação de IP |\n\n## Princípios de Evasão\n\nEvasão eficaz é sobre consistência em todas as camadas de detecção, não sobre aperfeiçoar nenhuma camada individualmente.\n\nUse IPs residenciais ou móveis quando a furtividade importar. Eles são mais difíceis de detectar porque os IPs genuinamente pertencem a ISPs, e o custo premium reflete essa vantagem. Alinhe os sinais de geolocalização do navegador (fuso horário, idioma, localidade) com a localização do IP do proxy. Mantenha persistência de sessão não rotacionando IPs no meio da sessão, o que cria descontinuidades detectáveis. Garanta que seu fingerprint TCP/IP corresponda à alegação do seu User-Agent executando automação no mesmo SO que você está imitando. Use as interações humanizadas do Pydoll para passar na análise comportamental. E sempre teste por vazamentos (WebRTC, DNS, fuso horário) antes de executar automação em escala.\n\nO objetivo não é tornar a detecção impossível, mas sim torná-la cara e incerta. Force o sistema de detecção a usar múltiplos sinais correlacionados, misture-se com padrões de tráfego legítimo e crie negação plausível.\n\n!!! warning \"Nenhum Proxy é Indetectável\"\n    Com recursos suficientes, qualquer proxy pode ser detectado. Mesmo proxies residenciais de primeira linha alcançam aproximadamente 70-90% de taxa de sucesso contra sistemas anti-bot sofisticados como Akamai, Cloudflare Enterprise e DataDome. A questão prática é se a detecção é economicamente viável para o site alvo.\n\n**Próximos passos:**\n\n- [Network Fingerprinting](../fingerprinting/network-fingerprinting.md): Fingerprinting de TCP/IP e TLS em detalhes\n- [Browser Fingerprinting](../fingerprinting/browser-fingerprinting.md): Fingerprinting de Canvas, WebGL, HTTP/2\n- [Técnicas de Evasão](../fingerprinting/evasion-techniques.md): Estratégia de evasão multicamada\n- [Configuração de Proxy](../../features/configuration/proxy.md): Configuração prática de proxy no Pydoll\n\n## Referências\n\n- MaxMind GeoIP2: https://www.maxmind.com/en/geoip2-services-and-databases\n- IPQualityScore Proxy Detection: https://www.ipqualityscore.com/proxy-vpn-tor-detection-service\n- Spur.us (Anonymous IP Detection): https://spur.us/\n- Team Cymru IP to ASN Mapping: https://www.team-cymru.com/ip-asn-mapping\n- Salesforce Engineering: TLS Fingerprinting with JA3 and JA3S - https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/\n- Akamai: Passive Fingerprinting of HTTP/2 Clients (Black Hat EU 2017) - https://blackhat.com/docs/eu-17/materials/eu-17-Shuster-Passive-Fingerprinting-Of-HTTP2-Clients-wp.pdf\n- Incolumitas: TCP/IP Fingerprinting for VPN and Proxy Detection - https://incolumitas.com/2021/03/13/tcp-ip-fingerprinting-for-vpn-and-proxy-detection/\n- Incolumitas: Detecting Proxies and VPNs with Latencies - https://incolumitas.com/2021/06/07/detecting-proxies-and-vpn-with-latencies/\n- BrowserLeaks HTTP/2 Fingerprint: https://browserleaks.com/http2\n- BrowserLeaks IP: https://browserleaks.com/ip\n- RFC 7239: Forwarded HTTP Extension - https://www.rfc-editor.org/rfc/rfc7239.html\n- RFC 9110: HTTP Semantics - https://www.rfc-editor.org/rfc/rfc9110.html\n"
  },
  {
    "path": "docs/pt/deep-dive/network/proxy-legal.md",
    "content": "# Considerações Legais e Éticas\n\nEste documento fornece **informações gerais** sobre o cenário legal e ético do uso de proxies e automação web. As leis variam enormemente por jurisdição e caso de uso. Isto **não é aconselhamento jurídico**. Sempre consulte um advogado qualificado para sua situação específica.\n\n!!! info \"Navegação do Módulo\"\n    - **[← Construindo Proxies](./build-proxy.md)** - Implementação e tópicos avançados\n    - **[← Detecção de Proxy](./proxy-detection.md)** - Anonimato e evasão\n    - **[← Visão Geral de Rede e Segurança](./index.md)** - Introdução do módulo\n    \n    Para automação responsável, veja **[Contorno de Captcha Comportamental](../../features/advanced/behavioral-captcha-bypass.md)** e **[Interações Semelhantes a Humanas](../../features/automation/human-interactions.md)**.\n\n!!! danger \"Aviso Legal\"\n    Este documento fornece **apenas informações educacionais**. **Não é aconselhamento jurídico**. As leis relativas a web scraping, automação e uso de proxy variam por jurisdição e estão sujeitas a interpretação. Consulte um advogado qualificado antes de se engajar em atividades que possam ter implicações legais.\n\n## Considerações Legais e Éticas\n\nO uso de proxy situa-se na interseção da privacidade, segurança e conformidade. Entender o cenário legal é essencial para uma automação responsável.\n\n### Conformidade Regulatória\n\nDiferentes jurisdições têm regras variadas sobre o uso de proxy e coleta de dados:\n\n| Região | Regulação Chave | Implicações para Proxy |\n|---|---|---|\n| **União Europeia** | GDPR | Endereços IP são dados pessoais; nós de saída de proxy na UE devem cumprir |\n| **Estados Unidos** | CFAA, Leis Estaduais | Contornar controles de acesso pode violar leis de fraude computacional |\n| **China** | Lei de Cibersegurança | Uso de VPN/proxy fortemente regulamentado; apenas serviços aprovados permitidos |\n| **Rússia** | Lei de VPN | Provedores de VPN devem se registrar e registrar a atividade do usuário |\n| **Austrália** | Lei de Privacidade | Coleta de dados através de proxies sujeita a princípios de privacidade |\n\n**Considerações específicas do GDPR:**\n\n**Endereços IP como dados pessoais (Artigo 4):**\n\nAo raspar sites baseados na UE através de proxies:\n\n- O IP da UE do seu proxy é considerado dado pessoal\n- Sites devem manuseá-lo de acordo com os requisitos do GDPR\n- Você deve ter base legal para a coleta de dados\n- Princípio da minimização de dados se aplica\n\n**Bases legais para processamento (Artigo 6):**\n\n1.  **Consentimento** - Difícil de obter para scraping\n2.  **Contrato** - Legítimo se você for um cliente\n3.  **Obrigação legal** - Raro para casos de uso de scraping\n4.  **Interesses vitais** - Não aplicável a scraping\n5.  **Tarefa pública** - Não aplicável a scraping\n6.  **Interesses legítimos** - Mais aplicável para scraping (requer teste de balanceamento)\n\n### Termos de Serviço e Restrições de Acesso\n\nProxies não isentam você dos Termos de Serviço (ToS) do site:\n\n**Violações comuns de ToS:**\n\n1.  **Acesso Automatizado**: Muitos sites proíbem bots/scrapers independentemente do IP\n2.  **Contorno de Limitação de Taxa (Rate Limiting)**: Usar proxies rotativos para contornar limites de taxa\n3.  **Restrições Geográficas**: Contornar geo-bloqueios pode violar acordos de licenciamento de conteúdo\n4.  **Compartilhamento de Conta**: Usar proxies para mascarar múltiplos usuários como um só\n\n**Exemplos de precedentes legais:**\n\n```python\n# Casos notáveis (simplificado, não é aconselhamento jurídico)\ncases = {\n    'hiQ Labs v. LinkedIn (2022)': {\n        'issue': 'Raspar dados públicos após acesso revogado',\n        'outcome': 'Raspar dados publicamente disponíveis geralmente é permitido',\n        'caveat': 'Mas contornar barreiras tecnológicas pode violar o CFAA'\n    },\n    \n    'QVC v. Resultly (2020)': {\n        'issue': 'Scraping agressivo causando carga no servidor',\n        'outcome': 'Requisições excessivas constituem invasão de propriedade (trespass to chattels)',\n        'implication': 'Volume e impacto importam, não apenas o acesso técnico'\n    }\n}\n```\n\n### Diretrizes Éticas para Uso de Proxy\n\nAlém da conformidade legal, considere estes princípios éticos:\n\n**1. Respeite o robots.txt**\n```python\n# Mesmo com proxies, honre as diretrizes do site\nasync def ethical_scraping(url):\n    # Checar robots.txt independentemente da anonimidade do proxy\n    if not is_allowed_by_robots(url):\n        return None  # Respeite os desejos do site\n```\n\n**2. Limitação de Taxa (Rate Limiting)**\n```python\n# Não abuse da rotação de proxy para sobrecarregar servidores\nMINIMUM_DELAY = 1.0  # segundos entre requisições\nMAX_CONCURRENT = 5   # conexões concorrentes por site\n\n# Ruim: Rotacionar proxies para raspar a 1000 req/s\n# Bom: Raspagem respeitosa mesmo com rotação de proxy\n```\n\n**3. Transparência**\n```python\n# Identifique-se no User-Agent quando apropriado\nheaders = {\n    'User-Agent': 'MyBot/1.0 (contact@example.com)',  # Identificação honesta\n    # Não: 'Mozilla/5.0...'  # Enganoso quando não é um navegador\n}\n```\n\n**4. Minimização de Dados**\n```python\n# Colete apenas o que você precisa\n# Só porque você pode raspar tudo, não significa que deva\ndata_to_collect = {\n    'product_name': True,\n    'price': True,\n    'user_emails': False,      # PII - não colete a menos que necessário\n    'user_addresses': False,   # PII - preocupações com privacidade\n}\n```\n\n### Checklist de Conformidade\n\nAntes de implantar automação baseada em proxy:\n\n- [ ] **Revisão Legal**: Consulte aconselhamento jurídico para sua jurisdição\n- [ ] **Conformidade com ToS**: Revise os termos de serviço do site alvo\n- [ ] **Proteção de Dados**: Garanta conformidade com GDPR/CCPA se manusear dados pessoais\n- [ ] **Direitos de Acesso**: Verifique se você tem permissão para acessar os dados\n- [ ] **Limitação de Taxa**: Implemente taxas de requisição respeitosas\n- [ ] **Tratamento de Erros**: Lide apropriadamente com 429 (Too Many Requests)\n- [ ] **Logging**: Mantenha trilhas de auditoria para fins de conformidade\n- [ ] **Retenção de Dados**: Implemente políticas apropriadas de retenção/exclusão de dados\n- [ ] **Segurança**: Proteja os dados coletados com medidas apropriadas\n- [ ] **Transparência**: Seja honesto sobre suas atividades de scraping quando questionado\n\n!!! warning \"Isto Não é Aconselhamento Jurídico\"\n    Esta seção fornece apenas informações gerais. A legalidade do uso de proxy varia por jurisdição, contexto e circunstâncias específicas. Sempre consulte um advogado qualificado para sua situação específica.\n\n!!! tip \"Uso Responsável de Proxy\"\n    O uso de proxy mais defensável é:\n    \n    - **Transparente**: Você pode explicar por que está fazendo isso\n    - **Necessário**: Você tem uma razão legítima (pesquisa, monitoramento, etc.)\n    - **Proporcional**: Seus métodos correspondem às suas necessidades (não excessivos)\n    - **Documentado**: Você mantém registros de suas atividades\n    - **Conforme (Compliant)**: Você segue todas as leis e ToS aplicáveis\n\n### Quando Evitar Proxies\n\nAlguns cenários onde o uso de proxy é problemático:\n\n| Cenário | Risco | Alternativa |\n|---|---|---|\n| **Sites Bancários/Financeiros** | Detecção de fraude, suspensão de conta | Use apenas acesso legítimo |\n| **Portais Governamentais** | Penalidades legais, investigações de segurança | Acesso direto de locais autorizados |\n| **Dados de Saúde** | Violações HIPAA, penalidades severas | Use acesso API autorizado |\n| **Sistemas Corporativos Internos** | Violações de política, demissão | Siga as políticas de TI da empresa |\n| **Criação de Contas E-commerce** | Sinalizadores de fraude, banimentos permanentes | Use identidade única e verificada |\n\n## Conclusão\n\nEntender a arquitetura de proxy profundamente permite a você:\n\n**Tomar Decisões Informadas:**\n- Escolher o tipo de proxy certo para seu caso de uso\n- Entender implicações de segurança\n- Identificar quando proxies são necessários vs opcionais\n\n**Solucionar Problemas Efetivamente:**\n- Depurar problemas de conexão\n- Identificar vazamentos de DNS ou IP\n- Diagnosticar problemas de desempenho\n\n**Otimizar Desempenho:**\n- Configurar timeouts apropriados\n- Implementar pooling de conexão\n- Monitorar a saúde do proxy\n\n**Construir Automação Melhor:**\n- Combinar proxies com técnicas anti-detecção\n- Implementar tratamento robusto de erros\n- Escalar o uso de proxy eficientemente\n\nO cenário de proxies é complexo, mas com esta fundação, você está equipado para navegá-lo com sucesso.\n\n## Leitura Adicional\n\n- **[RFC 1928](https://tools.ietf.org/html/rfc1928)**: Especificação do Protocolo SOCKS5\n- **[RFC 1929](https://tools.ietf.org/html/rfc1929)**: Autenticação de Usuário/Senha SOCKS5\n- **[RFC 2616](https://tools.ietf.org/html/rfc2616)**: HTTP/1.1 (método CONNECT)\n- **[RFC 5389](https://tools.ietf.org/html/rfc5389)**: Protocolo STUN\n- **[RFC 9298](https://tools.ietf.org/html/rfc9298)**: CONNECT-UDP (proxying HTTP/3)\n- **[Guia de Configuração de Proxy](../features/configuration/proxy.md)**: Uso prático de proxy no Pydoll, autenticação, rotação e testes\n- **[Interceptação de Requisições](../features/network/interception.md)**: Como o Pydoll implementa autenticação de proxy internamente\n- **[Análise Profunda das Capacidades de Rede](./network-capabilities.md)**: Como o Pydoll lida com operações de rede\n\n!!! tip \"Experimentação\"\n    A melhor maneira de entender proxies verdadeiramente é:\n    \n    1. Configurar seu próprio servidor proxy (use o código acima)\n    2. Capturar tráfego com Wireshark para ver os pacotes brutos\n    3. Testar diferentes tipos de proxy com automação real\n    4. Criar vazamentos intencionalmente e aprender a detectá-los\n    \n    A experiência prática solidifica o conhecimento teórico!"
  },
  {
    "path": "docs/pt/deep-dive/network/socks-proxies.md",
    "content": "# Arquitetura do Protocolo SOCKS\n\nSOCKS (SOCKet Secure) é um protocolo de proxy que opera entre as camadas de transporte e aplicação da pilha de rede (comumente descrito como Camada 5 no modelo OSI). Diferente dos proxies HTTP, que analisam e compreendem o tráfego HTTP, os proxies SOCKS encaminham conexões TCP e UDP brutas sem inspecionar seu conteúdo. Esse design agnóstico a protocolo torna o SOCKS a escolha preferida para automação focada em privacidade: o proxy nunca precisa analisar suas requisições, injetar cabeçalhos ou terminar conexões TLS.\n\nEste documento cobre como o SOCKS funciona no nível do protocolo, as diferenças entre SOCKS4 e SOCKS5, o tratamento de autenticação no Chrome, o comportamento de resolução de DNS e a configuração prática no Pydoll.\n\n!!! info \"Navegação do Módulo\"\n    - [Proxies HTTP/HTTPS](./http-proxies.md): Proxy na camada de aplicação\n    - [Fundamentos de Rede](./network-fundamentals.md): TCP/IP, UDP, modelo OSI\n    - [Visão Geral de Rede e Segurança](./index.md): Introdução do módulo\n    - [Detecção de Proxy](./proxy-detection.md): Níveis de anonimato e evasão de detecção\n    - [Construindo Proxies](./build-proxy.md): Implementação do SOCKS5 do zero\n\n    Para configuração prática, veja [Configuração de Proxy](../../features/configuration/proxy.md).\n\n## Como o SOCKS Difere dos Proxies HTTP\n\nA diferença fundamental está no que cada proxy pode ver e fazer. Um proxy HTTP opera na camada de aplicação e compreende HTTP: ele pode ler URLs, cabeçalhos, cookies e corpos de requisição (para tráfego não criptografado), modificá-los em trânsito, armazenar respostas em cache e injetar seus próprios cabeçalhos como `Via` e `X-Forwarded-For`. Isso é poderoso para filtragem de conteúdo, mas significa que você precisa confiar no operador do proxy com os dados da sua aplicação.\n\nUm proxy SOCKS opera abaixo da camada de aplicação. Ele vê apenas o endereço de destino, a porta e o volume de dados sendo transferido. Ele não analisa, modifica ou sequer compreende qual protocolo está fluindo através dele. HTTP, HTTPS, FTP, SSH, WebSocket ou qualquer protocolo customizado parecem todos iguais para um proxy SOCKS: apenas bytes sendo retransmitidos entre dois endpoints.\n\nIsso tem uma implicação prática direta. Quando você envia uma requisição HTTPS através de um proxy SOCKS5, o proxy vê `example.com:443` e o fluxo TLS criptografado. Ele não consegue ler a URL, os cabeçalhos, os cookies ou o conteúdo da resposta. Ele não adiciona cabeçalhos identificadores. Ele não precisa terminar o TLS. O túnel criptografado funciona de ponta a ponta entre seu navegador e o servidor de destino.\n\nNo entanto, é importante entender o que o SOCKS não fornece. SOCKS é um protocolo de proxy, não um protocolo de criptografia. O nome \"SOCKet Secure\" refere-se à travessia segura de firewalls, não à segurança criptográfica. Se você enviar tráfego HTTP não criptografado através de um proxy SOCKS5, o operador do proxy pode ler os bytes passando através dele, mesmo que o proxy não tenha sido projetado para inspecioná-los. Para criptografia real, você precisa de TLS/HTTPS sobre SOCKS, ou de um túnel criptografado (SSH, VPN) envolvendo a conexão SOCKS.\n\n!!! note \"Modelo de Confiança\"\n    Com proxies HTTP, você confia no operador do proxy para não registrar seu histórico de navegação, roubar tokens, modificar respostas ou realizar ataques MITM. Com SOCKS5, você confia no proxy apenas para encaminhar pacotes corretamente e não registrar metadados de conexão. A superfície de ataque é menor, mas não é zero.\n\n## SOCKS4 vs SOCKS5\n\nO SOCKS possui duas versões de uso comum. O SOCKS4 foi desenvolvido pela NEC no início dos anos 1990 como um padrão informal sem RFC. O SOCKS5 foi padronizado como RFC 1928 em 1996 para resolver as limitações do SOCKS4.\n\n| Característica | SOCKS4 | SOCKS5 |\n|---------|--------|--------|\n| Padrão | Sem RFC oficial (de facto, 1992) | RFC 1928 (1996) |\n| Autenticação | Apenas identificação (campo USERID, sem senha) | Múltiplos métodos (nenhum, usuário/senha, GSSAPI) |\n| Versão IP | Apenas IPv4 | IPv4 e IPv6 |\n| Suporte UDP | Não | Sim (comando UDP ASSOCIATE) |\n| Resolução DNS | Lado do cliente (extensão SOCKS4A adiciona lado do servidor) | Lado do servidor ao usar nomes de domínio (ATYP=0x03) |\n| Suporte a protocolo | Apenas TCP | TCP e UDP |\n\nO SOCKS5 é superior em todos os aspectos práticos. Use SOCKS4 apenas se o proxy não suportar SOCKS5.\n\n## O Handshake SOCKS5\n\nO processo de conexão SOCKS5 segue a RFC 1928 e consiste em três fases: negociação de método, autenticação opcional e a requisição de conexão.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant SOCKS5 as SOCKS5 Proxy\n    participant Server as Target Server\n\n    Note over Client,SOCKS5: Phase 1: Method Negotiation\n    Client->>SOCKS5: Hello [VER=5, NMETHODS, METHODS]\n    SOCKS5->>Client: Method Selected [VER=5, METHOD]\n\n    Note over Client,SOCKS5: Phase 2: Authentication (if required)\n    Client->>SOCKS5: Auth Request [VER=1, ULEN, UNAME, PLEN, PASSWD]\n    SOCKS5->>Client: Auth Response [VER=1, STATUS]\n\n    Note over Client,SOCKS5: Phase 3: Connection Request\n    Client->>SOCKS5: Connect [VER=5, CMD=CONNECT, DST.ADDR, DST.PORT]\n    SOCKS5->>Server: Establish TCP connection\n    Server-->>SOCKS5: Connection established\n    SOCKS5->>Client: Reply [VER=5, REP=SUCCESS, BND.ADDR, BND.PORT]\n\n    Note over Client,Server: Data relay (proxied)\n    Client->>SOCKS5: Application data\n    SOCKS5->>Server: Forward data\n    Server->>SOCKS5: Response data\n    SOCKS5->>Client: Forward response\n```\n\n### Fase 1: Negociação de Método\n\nO cliente abre uma conexão TCP com o proxy e envia uma saudação contendo a versão do protocolo (sempre `0x05` para SOCKS5) e uma lista de métodos de autenticação que ele suporta.\n\n```python\n# Client Hello\n[\n    0x05,        # VER: Protocol version (5)\n    0x02,        # NMETHODS: Number of methods offered\n    0x00, 0x02   # METHODS: No auth (0x00) and Username/Password (0x02)\n]\n```\n\nO proxy responde com o método que ele seleciona. Se o proxy exigir autenticação e o cliente tiver oferecido `0x02` (usuário/senha), o proxy o seleciona. Se nenhum método aceitável foi oferecido, o proxy responde com `0xFF` e fecha a conexão.\n\n```python\n# Server response\n[\n    0x05,   # VER: Protocol version (5)\n    0x02    # METHOD: Username/Password selected\n]\n```\n\nCódigos de método definidos pela RFC 1928: `0x00` = sem autenticação, `0x01` = GSSAPI, `0x02` = usuário/senha (RFC 1929), `0x03-0x7F` = atribuídos pela IANA, `0x80-0xFE` = reservados para métodos privados, `0xFF` = nenhum método aceitável.\n\n### Fase 2: Autenticação\n\nSe o proxy selecionou o método `0x02`, o cliente envia as credenciais seguindo a RFC 1929. A subnegociação usa seu próprio número de versão (`0x01`, não `0x05`).\n\n```python\n# Client authentication\n[\n    0x01,              # VER: Subnegotiation version (1)\n    len(username),     # ULEN: Username length (max 255)\n    *username_bytes,   # UNAME: Username\n    len(password),     # PLEN: Password length (max 255)\n    *password_bytes    # PASSWD: Password\n]\n\n# Server response\n[\n    0x01,   # VER: Subnegotiation version (1)\n    0x00    # STATUS: 0 = success, non-zero = failure\n]\n```\n\nAs credenciais são transmitidas em texto claro durante este handshake. Isso é inerente ao protocolo SOCKS5 (RFC 1929). Para ambientes sensíveis, envolva a conexão SOCKS em um túnel SSH ou VPN.\n\n### Fase 3: Requisição de Conexão\n\nApós a autenticação ser bem-sucedida (ou se nenhuma autenticação foi necessária), o cliente envia uma requisição de conexão especificando o comando, o endereço de destino e a porta.\n\n```python\n[\n    0x05,          # VER: Protocol version (5)\n    0x01,          # CMD: 1=CONNECT, 2=BIND, 3=UDP ASSOCIATE\n    0x00,          # RSV: Reserved\n    0x03,          # ATYP: 1=IPv4 (4 bytes), 3=Domain (length+name), 4=IPv6 (16 bytes)\n    len(domain),   # Domain length (only for ATYP=0x03)\n    *domain_bytes, # Domain name\n    *port_bytes    # Port (2 bytes, big-endian)\n]\n```\n\nO tipo de endereço (ATYP) determina o formato: `0x01` significa que 4 bytes de endereço IPv4 seguem, `0x04` significa 16 bytes de IPv6, e `0x03` significa um byte de comprimento seguido pelo nome do domínio. Quando o cliente envia um nome de domínio (ATYP=0x03), o proxy resolve o DNS do seu lado, o que previne vazamentos de DNS para a rede local do cliente.\n\nO proxy conecta ao destino e responde com uma resposta:\n\n```python\n[\n    0x05,       # VER: Protocol version (5)\n    0x00,       # REP: 0x00=success, 0x01-0x08=various errors\n    0x00,       # RSV: Reserved\n    0x01,       # ATYP: Address type of bound address\n    *bind_addr, # BND.ADDR: Address the proxy bound to\n    *bind_port  # BND.PORT: Port the proxy bound to\n]\n```\n\nCódigos de resposta: `0x00` sucesso, `0x01` falha geral, `0x02` conexão não permitida, `0x03` rede inacessível, `0x04` host inacessível, `0x05` conexão recusada, `0x06` TTL expirado, `0x07` comando não suportado, `0x08` tipo de endereço não suportado.\n\nApós uma resposta bem-sucedida, o proxy começa a retransmitir dados bidirecionalmente. Todo o handshake SOCKS5 é um protocolo binário, tornando-o mais eficiente que o HTTP baseado em texto, mas mais difícil de depurar sem dumps hexadecimais.\n\n## Suporte UDP\n\nO SOCKS5 suporta proxy UDP através do comando `UDP ASSOCIATE` (CMD=0x03). Isso funciona de forma diferente do proxy TCP: o cliente envia uma requisição UDP ASSOCIATE pela conexão de controle TCP, e o proxy responde com um endereço e porta de retransmissão. O cliente então envia datagramas UDP para essa retransmissão, e o proxy os encaminha para seus destinos.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant SOCKS5\n    participant UDP_Server as UDP Server\n\n    Note over Client,SOCKS5: TCP control connection (handshake + auth)\n    Client->>SOCKS5: UDP ASSOCIATE request (CMD=0x03)\n    SOCKS5->>Client: Relay address and port\n\n    Note over Client,SOCKS5: UDP data transfer\n    Client->>SOCKS5: UDP datagram to relay\n    SOCKS5->>UDP_Server: Forward datagram\n    UDP_Server->>SOCKS5: Response datagram\n    SOCKS5->>Client: Forward response\n\n    Note over Client,SOCKS5: TCP control connection stays open\n```\n\nCada datagrama UDP enviado através da retransmissão inclui um pequeno cabeçalho com o endereço e a porta de destino:\n\n```python\n[\n    0x00, 0x00,    # RSV: Reserved\n    0x00,          # FRAG: Fragment number (0 = no fragmentation)\n    0x01,          # ATYP: Address type\n    *dst_addr,     # DST.ADDR: Destination address\n    *dst_port,     # DST.PORT: Destination port\n    *data          # DATA: Application data\n]\n```\n\nA conexão de controle TCP deve permanecer aberta durante toda a duração da associação UDP. Se ela for fechada, o proxy descarta a retransmissão UDP.\n\n!!! warning \"UDP no Chrome\"\n    O Chrome não utiliza UDP ASSOCIATE do SOCKS5 para nenhum tráfego. Mesmo quando configurado com um proxy SOCKS5, o Chrome apenas faz proxy de conexões TCP. WebRTC, DNS-sobre-UDP e outros tráfegos UDP não são roteados pelo proxy SOCKS5. Isso significa que vazamentos de IP via WebRTC ainda são possíveis com SOCKS5 no Chrome. Use `--force-webrtc-ip-handling-policy=disable_non_proxied_udp` ou `webrtc_leak_protection = True` do Pydoll para mitigar isso. Para mais detalhes, veja [Fundamentos de Rede: WebRTC e Vazamento de IP](./network-fundamentals.md#webrtc-and-ip-leakage).\n\n!!! tip \"Alternativas Modernas de Proxy UDP\"\n    Para cenários que exigem suporte UDP completo além do que a implementação SOCKS5 do Chrome oferece, considere Shadowsocks (protocolo criptografado semelhante ao SOCKS com UDP nativo), WireGuard (VPN com excelente desempenho) ou V2Ray/VMess (framework de proxy flexível com tratamento UDP abrangente).\n\n## Resolução de DNS\n\nUm equívoco comum é que proxies HTTP vazam consultas DNS enquanto proxies SOCKS5 não. A realidade no Chrome é mais nuançada.\n\nQuando o Chrome é configurado com qualquer proxy (HTTP, HTTPS ou SOCKS5), ele envia nomes de host para o proxy em vez de resolver DNS localmente. Para proxies HTTP, o nome do host aparece na requisição `CONNECT host:443`. Para SOCKS5, ele aparece na requisição de conexão com ATYP=0x03 (nome de domínio). Em ambos os casos, o proxy resolve o DNS do seu lado, e o Chrome não faz consultas DNS locais para tráfego direcionado ao proxy.\n\nA verdadeira diferença de privacidade de DNS entre os dois tipos de proxy não é quem resolve o DNS, mas o que o proxy vê na camada de aplicação. Um proxy HTTP vê a URL completa para requisições não criptografadas e o nome do host para requisições CONNECT. Um proxy SOCKS5 vê apenas o nome do host de destino e a porta como parâmetros opacos de conexão.\n\nNo entanto, existe uma ressalva importante: o prefetcher de DNS do Chrome pode fazer consultas DNS locais para nomes de host encontrados no conteúdo da página, mesmo quando um proxy está configurado. Isso pode vazar os domínios que você está navegando para o seu resolvedor DNS local. Para prevenir isso, desabilite o prefetching de DNS ou use a flag `--host-resolver-rules=\"MAP * ~NOTFOUND , EXCLUDE 127.0.0.1\"`.\n\n!!! note \"`socks5://` vs `socks5h://`\"\n    Muitas ferramentas fora do Chrome distinguem entre `socks5://` (cliente resolve DNS) e `socks5h://` (proxy resolve DNS, o \"h\" significa hostname). O Chrome sempre resolve DNS do lado do proxy para SOCKS5, comportando-se como `socks5h://` independentemente de qual esquema você use. Mas se você usar ferramentas como `curl`, Firefox ou bibliotecas Python junto com o Pydoll, a distinção importa: sempre use `socks5h://` para prevenir vazamentos de DNS.\n\n## SOCKS5 e Resistência a MITM\n\nO SOCKS5 é frequentemente descrito como \"resistente a MITM\". Isso é verdade em um sentido específico: como o SOCKS5 não compreende nem interage com TLS, ele não tem mecanismo para terminar uma conexão TLS e recriptografá-la. Um proxy SOCKS5 simplesmente retransmite bytes criptografados sem modificação.\n\nUm proxy HTTP, por outro lado, pode realizar terminação TLS (MITM) apresentando seu próprio certificado ao cliente, descriptografando o tráfego, inspecionando ou modificando-o, e recriptografando-o em direção ao servidor. Isso exige que o cliente confie no certificado CA do proxy, e é detectável através de certificate pinning e logs de Certificate Transparency. O comportamento normal de um proxy HTTP com HTTPS (usando CONNECT) é criar um túnel transparente sem terminação, mas a possibilidade arquitetônica de MITM existe.\n\nCom SOCKS5, a terminação TLS não é possível no nível do protocolo. O proxy não consegue se inserir no handshake TLS porque ele não analisa os dados da aplicação fluindo através dele. A criptografia de ponta a ponta entre cliente e servidor é preservada por design.\n\nVale notar que é o TLS que fornece a proteção criptográfica real, não o SOCKS5 em si. Se você enviar HTTP não criptografado através de um proxy SOCKS5, o operador do proxy pode ler tudo. A vantagem de segurança do SOCKS5 é arquitetônica (ele não exige nem permite terminação TLS), não criptográfica.\n\n## TLS e Browser Fingerprinting Através do SOCKS5\n\nUma limitação importante para entender: o SOCKS5 não altera o fingerprint do seu navegador. O handshake TLS (ClientHello) passa pelo proxy SOCKS5 byte por byte, o que significa que o servidor de destino vê o fingerprint JA3/JA4 exato do seu navegador. O mesmo se aplica aos frames HTTP/2 SETTINGS, à ordenação de cabeçalhos específica do navegador e a todos os outros sinais de fingerprinting na camada de aplicação.\n\nO SOCKS5 oculta seu endereço IP e impede que o proxy injete cabeçalhos identificadores. Ele não ajuda com nenhuma forma de browser fingerprinting ou fingerprinting comportamental. Para uma estratégia completa de evasão, você precisa abordar o fingerprinting em múltiplas camadas. Veja [Técnicas de Evasão](../fingerprinting/evasion-techniques.md) para detalhes.\n\n## Autenticação SOCKS5 no Chrome\n\nO Chrome não suporta autenticação por usuário/senha do SOCKS5. Esta é uma limitação de longa data rastreada como [Chromium Issue #40323993](https://issues.chromium.org/issues/40323993). Quando o Chrome realiza a negociação de método SOCKS5, ele oferece apenas o método `0x00` (sem autenticação). Se o proxy exigir autenticação, a conexão falha silenciosamente.\n\nIsso é fundamentalmente diferente da autenticação de proxy HTTP. Proxies HTTP autenticam via códigos de status HTTP (`407 Proxy Authentication Required`), que o Chrome trata através do domínio Fetch no CDP. O Pydoll intercepta esses eventos `Fetch.authRequired` e responde com as credenciais armazenadas automaticamente. A autenticação SOCKS5, por outro lado, acontece durante um handshake de protocolo binário na camada de sessão, antes que qualquer tráfego HTTP exista. Não há HTTP 407, nenhum evento `Fetch.authRequired` e nenhuma forma de ferramentas baseadas em CDP injetarem credenciais nesse processo.\n\nConfigurar `--proxy-server=socks5://user:pass@proxy:1080` não funciona. O Chrome ignora silenciosamente as credenciais embutidas.\n\n### SOCKS5Forwarder do Pydoll\n\nA solução padrão é um proxy forwarder local: um servidor SOCKS5 leve rodando no localhost que aceita conexões não autenticadas do Chrome e as encaminha para o proxy remoto com autenticação completa.\n\n```mermaid\nsequenceDiagram\n    participant Chrome\n    participant Forwarder as Local Forwarder<br/>(127.0.0.1:1081)\n    participant Remote as Remote SOCKS5 Proxy<br/>(proxy:1080)\n    participant Server as Destination Server\n\n    Note over Chrome,Forwarder: No authentication\n    Chrome->>Forwarder: SOCKS5 Hello [methods: 0x00]\n    Forwarder->>Chrome: Method selected [0x00]\n    Chrome->>Forwarder: CONNECT example.com:443\n\n    Note over Forwarder,Remote: With authentication\n    Forwarder->>Remote: SOCKS5 Hello [methods: 0x02]\n    Remote->>Forwarder: Method selected [0x02]\n    Forwarder->>Remote: Auth [username, password]\n    Remote->>Forwarder: Auth OK\n    Forwarder->>Remote: CONNECT example.com:443\n    Remote->>Server: TCP connection\n    Remote->>Forwarder: Connect OK\n\n    Forwarder->>Chrome: Connect OK\n\n    Note over Chrome,Server: Bidirectional data relay\n    Chrome->>Forwarder: TLS + application data\n    Forwarder->>Remote: Forward\n    Remote->>Server: Forward\n    Server->>Remote: Response\n    Remote->>Forwarder: Forward\n    Forwarder->>Chrome: Forward\n```\n\nO Pydoll fornece um `SOCKS5Forwarder` integrado no módulo `pydoll.utils`. É uma implementação async pura em Python, sem dependências externas, que lida com o handshake SOCKS5 completo com o proxy remoto, incluindo autenticação por usuário/senha (RFC 1929), tipos de endereço IPv4, IPv6 e domínio.\n\n```python\nimport asyncio\nfrom pydoll.utils import SOCKS5Forwarder\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def main():\n    forwarder = SOCKS5Forwarder(\n        remote_host='proxy.example.com',\n        remote_port=1080,\n        username='myuser',\n        password='mypass',\n        local_port=1081,  # Use 0 for auto-assigned port\n    )\n    async with forwarder:\n        options = ChromiumOptions()\n        options.add_argument(f'--proxy-server=socks5://127.0.0.1:{forwarder.local_port}')\n\n        async with Chrome(options=options) as browser:\n            tab = await browser.start()\n            await tab.go_to('https://httpbin.org/ip')\n\nasyncio.run(main())\n```\n\nO forwarder também pode ser executado como ferramenta CLI standalone para testes ou uso com outras aplicações:\n\n```bash\npython -m pydoll.utils.socks5_proxy_forwarder \\\n    --remote-host proxy.example.com \\\n    --remote-port 1080 \\\n    --username myuser \\\n    --password mypass \\\n    --local-port 1081\n```\n\nO forwarder se vincula a `127.0.0.1` por padrão, tornando-o acessível apenas da sua máquina. Nunca vincule a `0.0.0.0` em produção, pois isso exporia um proxy SOCKS5 sem autenticação para a rede. As credenciais nunca são registradas em texto claro nos logs. O forwarder adiciona latência sub-milissegundo, já que toda a comunicação acontece pela interface de loopback local.\n\n!!! tip \"Ambientes Restritos\"\n    Alguns ambientes (contêineres Docker, plataformas serverless, VMs endurecidas) podem restringir a vinculação a portas locais. Use `local_port=0` para deixar o SO atribuir uma porta disponível. Se a vinculação local estiver completamente bloqueada, considere usar um proxy HTTP CONNECT, que o Chrome suporta nativamente com autenticação via ProxyManager do Pydoll.\n\n## Configuração Prática\n\n**SOCKS5 básico (sem autenticação):**\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n```\n\n**SOCKS5 com autenticação (via SOCKS5Forwarder):**\n\nVeja a [seção do SOCKS5Forwarder](#socks5forwarder-do-pydoll) acima.\n\n**Prevenindo vazamentos:**\n\nPara uma configuração SOCKS5 completa, você também deve prevenir vazamentos de WebRTC e DNS prefetch:\n\n```python\noptions = ChromiumOptions()\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\noptions.webrtc_leak_protection = True  # Prevents WebRTC IP leaks\noptions.add_argument('--disable-quic')  # Forces HTTP/2 over TCP through proxy\n```\n\n**Testando sua configuração:**\n\nSempre verifique sua configuração de proxy com testes de vazamento. Visite [browserleaks.com/ip](https://browserleaks.com/ip) para confirmar seu IP, [browserleaks.com/webrtc](https://browserleaks.com/webrtc) para verificar vazamentos de WebRTC, e [dnsleaktest.com](https://dnsleaktest.com/) para confirmar que o DNS não está vazando.\n\n## Resumo\n\nO SOCKS5 fornece proxy agnóstico a protocolo com uma superfície de confiança menor que a dos proxies HTTP. Ele não analisa, modifica ou injeta nada no seu tráfego. A resolução de DNS acontece do lado do proxy no Chrome. A criptografia TLS é preservada de ponta a ponta. A principal limitação no Chrome é a falta de autenticação SOCKS5 nativa (resolvida pelo `SOCKS5Forwarder` do Pydoll) e a ausência de proxy UDP (mitigada desabilitando o WebRTC ou usando as flags apropriadas do navegador).\n\nO SOCKS5 não altera o fingerprint TLS do seu navegador, as configurações HTTP/2 ou quaisquer características da camada de aplicação. Para evasão completa, combine SOCKS5 com gerenciamento de browser fingerprint e simulação comportamental.\n\n**Próximos passos:**\n\n- [Detecção de Proxy](./proxy-detection.md): Como até mesmo proxies SOCKS5 podem ser detectados\n- [Construindo Proxies](./build-proxy.md): Implemente seu próprio servidor SOCKS5\n- [Configuração de Proxy](../../features/configuration/proxy.md): Configuração prática de proxy no Pydoll\n- [Técnicas de Evasão](../fingerprinting/evasion-techniques.md): Estratégia de evasão multicamada\n\n## Referências\n\n- RFC 1928: SOCKS Protocol Version 5 (1996) - https://datatracker.ietf.org/doc/html/rfc1928\n- RFC 1929: Username/Password Authentication for SOCKS V5 (1996) - https://datatracker.ietf.org/doc/html/rfc1929\n- RFC 1961: GSS-API Authentication Method for SOCKS V5 (1996) - https://datatracker.ietf.org/doc/html/rfc1961\n- RFC 3089: SOCKS-based IPv6/IPv4 Gateway Mechanism (2001) - https://datatracker.ietf.org/doc/html/rfc3089\n- Chromium Proxy Documentation - https://chromium.googlesource.com/chromium/src/+/689912289c/net/docs/proxy.md\n- Chromium Issue #40323993: SOCKS5 Authentication - https://issues.chromium.org/issues/40323993\n- BrowserLeaks: WebRTC Leak Test - https://browserleaks.com/webrtc\n- DNS Leak Test - https://dnsleaktest.com/\n- IPLeak: Comprehensive Leak Testing - https://ipleak.net\n"
  },
  {
    "path": "docs/pt/features/advanced/behavioral-captcha-bypass.md",
    "content": "# Interação com Cloudflare Turnstile\n\nO Pydoll oferece suporte nativo para interagir com captchas Cloudflare Turnstile realizando cliques realistas do navegador. Isso **não é um bypass ou evasão**. Ele simplesmente automatiza a mesma ação de clique que um humano realizaria na caixa de seleção do captcha.\n\n!!! warning \"O que esta Funcionalidade Realmente Faz\"\n    Esta funcionalidade **clica** na caixa de seleção do captcha Cloudflare Turnstile usando interações padrão do navegador. É isso. Não há:\n    \n    - **NÃO**: Bypass mágico ou evasão\n    - **NÃO**: Resolução de desafios (seleção de imagens, quebra-cabeças, etc.)\n    - **NÃO**: Manipulação de pontuação ou falsificação de fingerprint\n    - **SIM**: Apenas um clique realista no contêiner do captcha\n    \n    **O sucesso depende inteiramente do seu ambiente** (reputação do IP, fingerprint do navegador, padrões de comportamento). O Pydoll fornece o mecanismo para clicar; seu ambiente determina se o clique é aceito.\n\n!!! info \"O que é o Cloudflare Turnstile?\"\n    O Cloudflare Turnstile é um sistema de captcha moderno que analisa o ambiente do navegador e sinais comportamentais para determinar se você é humano. Ele geralmente aparece como uma caixa de seleção que os usuários devem clicar. O sistema analisa:\n    \n    - **Reputação do IP**: Seu endereço IP está sinalizado ou é suspeito?\n    - **Fingerprint do Navegador**: Seu navegador parece legítimo?\n    - **Padrões Comportamentais**: Você se comporta como um humano?\n    \n    Quando a pontuação de confiança é alta o suficiente, o clique na caixa de seleção é aceito. Quando está muito baixa, o Turnstile pode mostrar um desafio (que o Pydoll **não pode resolver**) ou bloqueá-lo totalmente. Para resolver desafios com imagens ou quebra-cabeças, considere usar o **[CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc)**.\n\n## Guia Rápido\n\n### Gerenciador de Contexto (Recomendado)\n\nO gerenciador de contexto espera o captcha aparecer, clica nele e espera pela resolução antes de continuar:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def turnstile_example():\n    options = ChromiumOptions()\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Gerenciador de contexto lida com o captcha automaticamente\n        async with tab.expect_and_bypass_cloudflare_captcha():\n            await tab.go_to('https://site-with-turnstile.com')\n        \n        # Este código só roda após o captcha ser clicado\n        print(\"Interação com o captcha Turnstile concluída!\")\n        \n        # Continue com sua automação\n        content = await tab.find(id='protected-content')\n        print(await content.text)\n\nasyncio.run(turnstile_example())\n```\n\n### Processamento em Segundo Plano\n\nHabilite o clique automático do captcha em segundo plano:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def background_turnstile():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Habilitar clique automático antes de navegar\n        await tab.enable_auto_solve_cloudflare_captcha()\n        \n        # Navegar para o site protegido\n        await tab.go_to('https://site-with-turnstile.com')\n        \n        # Esperar o captcha ser processado em segundo plano\n        await asyncio.sleep(5)\n        \n        print(\"Página carregada com manejo de captcha em segundo plano\")\n        \n        # Desabilitar quando não for mais necessário\n        await tab.disable_auto_solve_cloudflare_captcha()\n\nasyncio.run(background_turnstile())\n```\n\n## Personalizando a Interação com o Captcha\n\n### Como Funciona\n\nO Pydoll detecta automaticamente o Cloudflare Turnstile percorrendo o shadow DOM da página. Ele procura um shadow root contendo `challenges.cloudflare.com`, navega até seu iframe cross-origin, encontra o shadow root interno e clica no elemento checkbox real. Nenhuma configuração manual de seletor é necessária.\n\n### Configuração de Tempo (Timing)\n\nO shadow root do captcha nem sempre aparece imediatamente. Ajuste o timeout para corresponder ao comportamento do site:\n\n```python\nasync def timing_configuration_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n\n        async with tab.expect_and_bypass_cloudflare_captcha(\n            time_to_wait_captcha=10   # Esperar até 10 segundos pelo captcha aparecer (padrão: 5)\n        ):\n            await tab.go_to('https://site-with-slow-turnstile.com')\n\n        print(\"Interação com o captcha concluída com tempo personalizado!\")\n\nasyncio.run(timing_configuration_example())\n```\n\n**Referência de Parâmetros:**\n\n| Parâmetro | Tipo | Padrão | Descrição |\n|---|---|---|---|\n| `time_to_wait_captcha` | `float` | `5` | Segundos máximos para esperar o captcha aparecer |\n\n!!! info \"Por que o Tempo Importa\"\n    Alguns sites carregam o captcha assincronamente. Se o shadow root do Cloudflare não aparecer dentro de `time_to_wait_captcha`, a interação é pulada.\n\n## Outros Sistemas de Captcha\n\n### reCAPTCHA v3 (Invisível)\n\nO reCAPTCHA v3 é **completamente invisível** e **não requer interação**. Apenas navegue normalmente:\n\n```python\nasync def recaptcha_v3_example():\n    options = ChromiumOptions()\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Nenhum tratamento especial necessário - apenas navegue\n        await tab.go_to('https://site-with-recaptcha-v3.com')\n        \n        # reCAPTCHA v3 roda em segundo plano, analisando seu comportamento\n        await asyncio.sleep(3)\n        \n        # Continue com o envio do formulário\n        submit_button = await tab.find(id='submit-btn')\n        await submit_button.click()\n\nasyncio.run(recaptcha_v3_example())\n```\n\n!!! note \"Fatores de Sucesso do reCAPTCHA v3\"\n    Como o reCAPTCHA v3 é inteiramente passivo (sem interação), o sucesso depende de:\n    \n    - **Reputação do IP**: Use proxies residenciais com boa reputação\n    - **Fingerprint do Navegador**: Configure preferências de navegador realistas\n    - **Padrões Comportamentais**: Passe tempo na página, role naturalmente, digite realisticamente\n    \n    Se sua pontuação for muito baixa, alguns sites podem mostrar um desafio reCAPTCHA v2 (que o Pydoll **não pode resolver**).\n\n## O que Determina o Sucesso?\n\nO sucesso da interação com o captcha depende **inteiramente do seu ambiente**, não do Pydoll. O sistema de captcha analisa:\n\n### 1. Reputação do IP (Mais Crítico)\n\n| Tipo de IP | Nível de Confiança | Comportamento Esperado |\n|---|---|---|\n| **IP Residencial (limpo)** | Alto | Geralmente aceito sem desafios |\n| **IP Móvel** | Alto | Geralmente aceito sem desafios |\n| **IP de Datacenter** | Baixo | Frequentemente bloqueado ou desafiado |\n| **IP previamente bloqueado** | Muito Baixo | Quase sempre bloqueado ou desafiado |\n\n!!! danger \"Reputação do IP é Tudo\"\n    **Nenhuma ferramenta pode superar um endereço IP ruim.** Se seu IP estiver sinalizado, você será bloqueado ou desafiado, independentemente de quão realista seu navegador pareça.\n    \n    Use proxies residenciais com boa reputação para melhores resultados.\n\n### 2. Fingerprint do Navegador\n\nConfigure seu navegador para parecer legítimo:\n\n```python\nimport time\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def stealth_configuration():\n    options = ChromiumOptions()\n    \n    # Argumentos de furtividade\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    options.add_argument('--window-size=1920,1080')\n    \n    # Preferências de navegador realistas\n    current_time = int(time.time())\n    options.browser_preferences = {\n        'profile': {\n            'last_engagement_time': str(current_time - (3 * 60 * 60)),  # 3 horas atrás\n            'exited_cleanly': True,\n            'exit_type': 'Normal',\n        },\n        'safebrowsing': {'enabled': True},\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        async with tab.expect_and_bypass_cloudflare_captcha():\n            await tab.go_to('https://site-with-turnstile.com')\n\nasyncio.run(stealth_configuration())\n```\n\n### 3. Padrões Comportamentais\n\nSistemas de captcha analisam como você interage com a página:\n\n```python\nasync def realistic_behavior():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://site-with-turnstile.com')\n        \n        # Simular comportamento humano antes do captcha aparecer\n        await asyncio.sleep(2)  # Ler conteúdo da página\n        await tab.execute_script('window.scrollBy(0, 300)')  # Rolar\n        await asyncio.sleep(1)\n        \n        # Agora interagir com o captcha\n        async with tab.expect_and_bypass_cloudflare_captcha():\n            # A interação com o captcha acontece aqui\n            pass\n        \n        print(\"Captcha passado com comportamento realista!\")\n\nasyncio.run(realistic_behavior())\n```\n\n!!! tip \"Fingerprinting Comportamental\"\n    Para um entendimento aprofundado de como os padrões comportamentais afetam o sucesso do captcha, veja **[Fingerprinting Comportamental](../../deep-dive/fingerprinting/behavioral-fingerprinting.md)**. Este guia explica:\n    \n    - Padrões de movimento do mouse e detecção\n    - Análise de tempo de pressionamento de teclas\n    - Física do comportamento de rolagem\n    - Análise de sequência de eventos\n    \n    Entender esses conceitos pode ajudá-lo a construir uma automação mais realista que alcança taxas de sucesso mais altas.\n\n## Solução de Problemas\n\n### Captcha Não Está Sendo Clicado\n\n**Sintomas**: O captcha aparece, mas nunca é clicado, a página permanece no desafio.\n\n**Causas Possíveis:**\n\n1.  **Tempo muito curto**: O captcha ainda não carregou quando o Pydoll tenta clicar\n2.  **Shadow root não encontrado**: O shadow root do Cloudflare Turnstile ainda não apareceu no DOM\n\n**Soluções:**\n\n```python\nasync def troubleshooting_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n\n        # Aumentar tempos de espera\n        async with tab.expect_and_bypass_cloudflare_captcha(\n            time_before_click=5,     # Atraso maior antes de clicar\n            time_to_wait_captcha=15  # Mais tempo para encontrar o captcha\n        ):\n            await tab.go_to('https://problematic-site.com')\n\nasyncio.run(troubleshooting_example())\n```\n\n### Captcha Clicado, mas Mostra Desafio\n\n**Sintomas**: A caixa de seleção mostra a marca de verificação brevemente, depois apresenta um desafio de imagem/quebra-cabeça.\n\n**Causa Raiz**: A pontuação de confiança do seu ambiente está muito baixa.\n\n**Soluções:**\n\n- Use proxies residenciais com boa reputação\n- Configure um fingerprint de navegador realista\n- Adicione padrões comportamentais mais realistas (rolagem, movimento do mouse, atrasos)\n- **Nota**: O Pydoll não pode resolver o desafio em si. Se você precisa de resolução automática de captchas, considere integrar com o **[CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc)**\n\n### \"Acesso Negado\" ou Bloqueio Imediato\n\n**Sintomas**: O site mostra imediatamente \"Acesso Negado\" ou bloqueia você sem mostrar o captcha.\n\n**Causa Raiz**: **Seu endereço IP está sinalizado.**\n\n**Soluções:**\n\n- Use um proxy residencial diferente com boa reputação\n- Rotacione IPs entre as requisições\n- Teste seu IP em `https://www.cloudflare.com/cdn-cgi/trace`\n- **Nota**: Nenhuma configuração de navegador corrigirá um IP sinalizado\n\n### Funciona Localmente, mas Falha no Docker/CI\n\n**Sintomas**: A interação com o captcha funciona na sua máquina, mas falha em ambientes Docker/CI.\n\n**Causa Raiz**: IPs de datacenter são examinados de perto pelos sistemas de captcha.\n\n**Soluções:**\n\n1.  **Use o modo headless com exibição adequada** (para renderização completa):\n    ```dockerfile\n    FROM python:3.11-slim\n   \n    RUN apt-get update && apt-get install -y \\\n        chromium \\\n        chromium-driver \\\n        xvfb \\\n        && rm -rf /var/lib/apt/lists/*\n   \n    ENV DISPLAY=:99\n   \n    CMD Xvfb :99 -screen 0 1920x1080x24 & python your_script.py\n    ```\n\n2.  **Use proxy residencial** mesmo em CI/CD:\n    ```python\n    options = ChromiumOptions()\n    options.add_argument('--proxy-server=http://user:pass@residential-proxy.com:8080')\n    ```\n\n## Melhores Práticas\n\n1.  **Use proxies residenciais**: A reputação do IP é o fator mais crítico\n2.  **Configure opções de furtividade**: Remova indicadores de automação\n3.  **Adicione padrões comportamentais**: Role, espere, mova o mouse antes de clicar\n4.  **Ajuste o tempo**: Dê tempo ao captcha para carregar antes de tentar clicar\n5.  **Lide com falhas graciosamente**: Tenha lógica de fallback para quando o captcha não puder ser passado\n6.  **Teste seu ambiente**: Verifique a reputação do IP e o fingerprint do navegador antes da automação\n\n## Diretrizes Éticas\n\n!!! danger \"Termos de Serviço e Conformidade Legal\"\n    Interagir com captchas pode violar os Termos de Serviço de um site, mesmo que tecnicamente possível. **Sempre verifique e respeite os ToS** antes de automatizar qualquer site.\n    \n    Esta funcionalidade é fornecida **apenas para fins legítimos de automação**:\n    \n    **Casos de uso apropriados:**\n    - Teste automatizado de suas próprias aplicações\n    - Serviços de monitoramento que você tem permissão para monitorar\n    - Pesquisa e análise de segurança com autorização adequada\n    \n    **Casos de uso inapropriados:**\n    - Raspagem de conteúdo que você não tem permissão para acessar\n    - Contornar paywalls ou sistemas de assinatura\n    - Ataques de negação de serviço (Denial-of-Service) ou raspagem agressiva\n    - Qualquer atividade que viole os Termos de Serviço\n\n## Veja Também\n\n- **[Opções do Navegador](../configuration/browser-options.md)** - Configuração de furtividade\n- **[Preferências do Navegador](../configuration/browser-preferences.md)** - Fingerprinting avançado\n- **[Configuração de Proxy](../configuration/proxy.md)** - Configurando proxies\n- **[Fingerprinting Comportamental](../../deep-dive/fingerprinting/behavioral-fingerprinting.md)** - Entendendo a detecção comportamental\n- **[Interações Semelhantes a Humanas](../automation/human-interactions.md)** - Padrões de comportamento realistas\n\n---\n\n**Lembre-se**: O Pydoll fornece o mecanismo para clicar em captchas, mas seu ambiente (IP, fingerprint, comportamento) determina o sucesso. Esta não é uma solução mágica, é uma ferramenta que funciona quando usada no ambiente certo com a configuração adequada."
  },
  {
    "path": "docs/pt/features/advanced/decorators.md",
    "content": "# Decorator Retry\n\nWeb scraping é inerentemente imprevisível. Redes falham, páginas carregam lentamente, elementos aparecem e desaparecem, limites de taxa entram em ação e CAPTCHAs surgem inesperadamente. O decorator `@retry` fornece uma solução robusta e testada em produção para lidar com essas falhas inevitáveis de forma elegante.\n\n## Por Que Usar o Decorator Retry?\n\nNo scraping em produção, falhas não são exceções—são a norma. Em vez de deixar todo o seu trabalho de scraping travar por causa de uma falha temporária de rede ou um elemento ausente, o decorator retry permite que você:\n\n- **Recupere-se automaticamente** de falhas transitórias\n- **Implemente estratégias sofisticadas de retry** com backoff exponencial\n- **Execute lógica de recuperação** antes de tentar novamente (atualizar página, trocar proxy, reiniciar navegador)\n- **Mantenha sua lógica de negócio limpa** sem poluí-la com código de tratamento de erros\n\n## Início Rápido\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import WaitElementTimeout, NetworkError\n\n@retry(max_retries=3, exceptions=[WaitElementTimeout, NetworkError])\nasync def scrape_product_page(url: str):\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to(url)\n        \n        # Isso pode falhar devido a problemas de rede ou carregamento lento\n        product_title = await tab.find(class_name='product-title', timeout=5)\n        return await product_title.text\n\nasyncio.run(scrape_product_page('https://example.com/product/123'))\n```\n\nSe `scrape_product_page` falhar com `WaitElementTimeout` ou `NetworkError`, ela automaticamente tentará novamente até 3 vezes antes de desistir.\n\n## Boa Prática: Sempre Especifique Exceções\n\n!!! warning \"Boa Prática Crítica\"\n    **SEMPRE** especifique quais exceções devem acionar um retry. Usar o padrão `exceptions=Exception` vai capturar **tudo**, incluindo bugs no seu código que deveriam falhar imediatamente.\n\n**Ruim (captura tudo, incluindo bugs):**\n\n```python\n@retry(max_retries=3)  # NÃO FAÇA ISSO\nasync def scrape_data():\n    data = response['items'][0]  # Se 'items' não existir, retries não vão ajudar!\n    return data\n```\n\n**Bom (só tenta novamente em falhas esperadas):**\n\n```python\nfrom pydoll.exceptions import ElementNotFound, WaitElementTimeout, NetworkError\n\n@retry(\n    max_retries=3,\n    exceptions=[ElementNotFound, WaitElementTimeout, NetworkError]\n)\nasync def scrape_data():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        return await tab.find(id='data-container', timeout=10)\n```\n\nAo especificar exceções, você garante que:\n\n- **Erros de lógica falham rapidamente** (typos, seletores errados, bugs de código)\n- **Apenas erros recuperáveis são retentados** (problemas de rede, timeouts, elementos ausentes)\n- **Depuração é mais fácil** (você sabe exatamente o que deu errado)\n\n## Parâmetros\n\n### max_retries\n\nNúmero máximo de tentativas de retry antes de desistir.\n\n```python\nfrom pydoll.exceptions import WaitElementTimeout\n\n@retry(max_retries=5, exceptions=[WaitElementTimeout])\nasync def fetch_data():\n    # Tentará até 5 vezes no total\n    pass\n```\n\n### exceptions\n\nTipos de exceção que devem acionar um retry. Pode ser uma única exceção ou uma lista.\n\n```python\nfrom pydoll.exceptions import (\n    ElementNotFound,\n    WaitElementTimeout,\n    NetworkError,\n    ElementNotInteractable\n)\n\n# Exceção única\n@retry(exceptions=[WaitElementTimeout])\nasync def example1():\n    pass\n\n# Múltiplas exceções\n@retry(exceptions=[WaitElementTimeout, NetworkError, ElementNotFound, ElementNotInteractable])\nasync def example2():\n    pass\n```\n\n!!! tip \"Exceções Comuns de Scraping\"\n    Para web scraping com Pydoll, você normalmente vai querer retry em:\n\n    - `WaitElementTimeout` - Timeout esperando elemento aparecer\n    - `ElementNotFound` - Elemento não existe no DOM\n    - `ElementNotVisible` - Elemento existe mas não está visível\n    - `ElementNotInteractable` - Elemento não pode receber interação\n    - `NetworkError` - Problemas de conectividade de rede\n    - `ConnectionFailed` - Falha ao conectar ao navegador\n    - `PageLoadTimeout` - Timeout no carregamento de página\n    - `ClickIntercepted` - Click interceptado por outro elemento\n\n### delay\n\nTempo de espera entre tentativas de retry (em segundos).\n\n```python\nfrom pydoll.exceptions import WaitElementTimeout\n\n@retry(max_retries=3, exceptions=[WaitElementTimeout], delay=2.0)\nasync def scrape_with_delay():\n    # Espera 2 segundos entre cada retry\n    pass\n```\n\n### exponential_backoff\n\nQuando `True`, aumenta o delay exponencialmente com cada tentativa de retry.\n\n```python\nfrom pydoll.exceptions import NetworkError\n\n@retry(\n    max_retries=5,\n    exceptions=[NetworkError],\n    delay=1.0,\n    exponential_backoff=True\n)\nasync def scrape_with_backoff():\n    # Tentativa 1: falha → espera 1 segundo\n    # Tentativa 2: falha → espera 2 segundos\n    # Tentativa 3: falha → espera 4 segundos\n    # Tentativa 4: falha → espera 8 segundos\n    # Tentativa 5: falha → lança exceção\n    pass\n```\n\n**O que é Exponential Backoff?**\n\nExponential backoff é uma estratégia de retry onde o tempo de espera entre tentativas aumenta exponencialmente. Em vez de bombardear um servidor com requisições a cada segundo, você dá progressivamente mais tempo para ele se recuperar:\n\n- **Tentativa 1**: Espera `delay` segundos (ex: 1s)\n- **Tentativa 2**: Espera `delay * 2` segundos (ex: 2s)\n- **Tentativa 3**: Espera `delay * 4` segundos (ex: 4s)\n- **Tentativa 4**: Espera `delay * 8` segundos (ex: 8s)\n\nIsso é especialmente útil quando:\n\n- Lidando com **limites de taxa** (dê tempo ao servidor para resetar)\n- Lidando com **sobrecarga temporária do servidor** (não piore a situação)\n- Esperando **conteúdo dinâmico de carregamento lento**\n- Evitando **detecção como bot** (padrões de retry com aparência natural)\n\n### on_retry\n\nUma função callback executada após cada tentativa falhada, antes do próximo retry. Deve ser uma **função async**.\n\n```python\nfrom pydoll.exceptions import WaitElementTimeout\n\n@retry(\n    max_retries=3,\n    exceptions=[WaitElementTimeout],\n    on_retry=my_recovery_function\n)\nasync def scrape_data():\n    pass\n```\n\nO callback pode ser:\n\n- **Uma função async standalone**\n- **Um método de classe** (recebe `self` automaticamente)\n\n## O Callback on_retry: Seu Mecanismo de Recuperação\n\nO callback `on_retry` é onde a verdadeira mágica acontece. Esta é sua oportunidade de **restaurar o estado da aplicação** antes da próxima tentativa de retry.\n\n### Função Standalone\n\n```python\nimport asyncio\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import WaitElementTimeout\n\nasync def log_retry():\n    print(\"Tentativa de retry falhou, esperando antes da próxima tentativa...\")\n    await asyncio.sleep(1)\n\n@retry(max_retries=3, exceptions=[WaitElementTimeout], on_retry=log_retry)\nasync def scrape_page():\n    # Sua lógica de scraping\n    pass\n```\n\n### Método de Classe\n\nAo usar o decorator dentro de uma classe, o callback pode ser um método de classe. Ele receberá automaticamente `self` como primeiro argumento.\n\n```python\nimport asyncio\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import WaitElementTimeout\n\nclass DataCollector:\n    def __init__(self):\n        self.retry_count = 0\n    \n    # IMPORTANTE: Defina o callback ANTES do método decorado\n    async def log_retry(self):\n        self.retry_count += 1\n        print(f\"Tentativa {self.retry_count} falhou, tentando novamente...\")\n        await asyncio.sleep(1)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[WaitElementTimeout],\n        on_retry=log_retry  # Sem prefixo 'self.' necessário\n    )\n    async def fetch_data(self):\n        # Sua lógica de scraping aqui\n        pass\n```\n\n!!! warning \"Ordem de Definição de Métodos Importa\"\n    Ao usar `on_retry` com métodos de classe, **você deve definir o método callback ANTES do método decorado** na definição da sua classe. Python precisa saber sobre o callback quando o decorator é aplicado.\n\n    **Errado (vai falhar):**\n\n    ```python\n    class Scraper:\n        @retry(on_retry=handle_retry)  # handle_retry ainda não existe!\n        async def scrape(self):\n            pass\n        \n        async def handle_retry(self):  # Definido muito tarde\n            pass\n    ```\n\n    **Correto:**\n\n    ```python\n    class Scraper:\n        async def handle_retry(self):  # Definido primeiro\n            pass\n        \n        @retry(on_retry=handle_retry)  # Agora existe\n        async def scrape(self):\n            pass\n    ```\n\n## Casos de Uso do Mundo Real\n\n### 1. Atualização de Página e Recuperação de Estado\n\n**Este é o uso mais poderoso do `on_retry`**: recuperar de falhas atualizando a página e restaurando o estado da sua aplicação. Este exemplo demonstra por que o decorator retry é tão valioso para scraping em produção.\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound, WaitElementTimeout\nfrom pydoll.constants import Key\nimport asyncio\n\nclass DataScraper:\n    def __init__(self):\n        self.browser = None\n        self.tab = None\n        self.current_page = 1\n    \n    async def recover_from_failure(self):\n        \"\"\"Atualizar página e restaurar estado antes do retry\"\"\"\n        print(f\"Recuperando... atualizando página {self.current_page}\")\n        \n        if self.tab:\n            # Atualiza a página para recuperar de elementos obsoletos ou estado ruim\n            await self.tab.refresh()\n            await asyncio.sleep(2)  # Esperar a página carregar\n            \n            # Restaurar estado: navegar de volta para a página correta\n            if self.current_page > 1:\n                page_input = await self.tab.find(id='page-number')\n                await page_input.insert_text(str(self.current_page))\n                await self.tab.keyboard.press(Key.ENTER)\n                await asyncio.sleep(1)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound, WaitElementTimeout],\n        on_retry=recover_from_failure,\n        delay=1.0\n    )\n    async def scrape_page_data(self):\n        \"\"\"Fazer scraping dos dados da página atual\"\"\"\n        if not self.browser:\n            self.browser = Chrome()\n            self.tab = await self.browser.start()\n            await self.tab.go_to('https://example.com/data')\n        \n        # Navegar para página específica\n        page_input = await self.tab.find(id='page-number')\n        await page_input.insert_text(str(self.current_page))\n        await self.tab.keyboard.press(Key.ENTER)\n        await asyncio.sleep(1)\n        \n        # Fazer scraping dos dados (pode falhar se elementos ficarem obsoletos)\n        items = await self.tab.find(class_name='data-item', find_all=True)\n        return [await item.text for item in items]\n    \n    async def scrape_multiple_pages(self, start_page: int, end_page: int):\n        \"\"\"Fazer scraping de múltiplas páginas com retry automático em falhas\"\"\"\n        results = []\n        for page_num in range(start_page, end_page + 1):\n            self.current_page = page_num\n            data = await self.scrape_page_data()\n            results.extend(data)\n        return results\n\n# Uso\nasync def main():\n    scraper = DataScraper()\n    try:\n        # Fazer scraping das páginas 1-10 com recuperação automática em falhas\n        all_data = await scraper.scrape_multiple_pages(1, 10)\n        print(f\"Coletados {len(all_data)} itens\")\n    finally:\n        if scraper.browser:\n            await scraper.browser.stop()\n```\n\n**O que torna isso poderoso:**\n\n- `recover_from_failure()` realmente **restaura o estado** atualizando e navegando de volta\n- O método `scrape_page_data()` fica limpo, focado apenas na lógica de scraping\n- Se elementos ficarem obsoletos ou desaparecerem, o mecanismo de retry lida com a recuperação automaticamente\n- O navegador persiste entre as tentativas via `self.browser` e `self.tab`\n\n### 2. Recuperação de Modal de Diálogo\n\nÀs vezes um modal ou overlay aparece inesperadamente e bloqueia sua automação. Feche-o e tente novamente.\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound\n\nclass ModalAwareScraper:\n    def __init__(self):\n        self.tab = None\n    \n    async def close_modals(self):\n        \"\"\"Fechar quaisquer modals bloqueadores antes do retry\"\"\"\n        print(\"Verificando modals bloqueadores...\")\n        \n        # Tentar encontrar e fechar modals comuns\n        modal_close = await self.tab.find(\n            class_name='modal-close',\n            timeout=2,\n            raise_exc=False\n        )\n        if modal_close:\n            print(\"Modal encontrado, fechando...\")\n            await modal_close.click()\n            await asyncio.sleep(0.5)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound],\n        on_retry=close_modals,\n        delay=0.5\n    )\n    async def click_button(self, button_id: str):\n        button = await self.tab.find(id=button_id)\n        await button.click()\n```\n\n### 3. Reinício de Navegador e Rotação de Proxy\n\nPara trabalhos pesados de scraping, você pode precisar reiniciar completamente o navegador e trocar proxies após falhas.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import NetworkError, PageLoadTimeout\n\nclass RobustScraper:\n    def __init__(self):\n        self.browser = None\n        self.tab = None\n        self.proxy_list = [\n            'proxy1.example.com:8080',\n            'proxy2.example.com:8080',\n            'proxy3.example.com:8080',\n        ]\n        self.current_proxy_index = 0\n    \n    async def restart_with_new_proxy(self):\n        \"\"\"Reiniciar navegador com um proxy diferente\"\"\"\n        print(\"Reiniciando navegador com novo proxy...\")\n        \n        # Fechar navegador atual\n        if self.browser:\n            await self.browser.stop()\n            await asyncio.sleep(2)\n        \n        # Rotacionar para o próximo proxy\n        self.current_proxy_index = (self.current_proxy_index + 1) % len(self.proxy_list)\n        proxy = self.proxy_list[self.current_proxy_index]\n        \n        print(f\"Usando proxy: {proxy}\")\n        \n        # Iniciar novo navegador com novo proxy\n        options = ChromiumOptions()\n        options.add_argument(f'--proxy-server={proxy}')\n        \n        self.browser = Chrome(options=options)\n        self.tab = await self.browser.start()\n    \n    @retry(\n        max_retries=3,\n        exceptions=[NetworkError, PageLoadTimeout],\n        on_retry=restart_with_new_proxy,\n        delay=5.0,\n        exponential_backoff=True\n    )\n    async def scrape_protected_site(self, url: str):\n        if not self.browser:\n            await self.restart_with_new_proxy()\n        \n        await self.tab.go_to(url)\n        await asyncio.sleep(3)\n        \n        # Sua lógica de scraping aqui\n        content = await self.tab.find(id='content')\n        return await content.text\n```\n\n### 4. Detecção de Ociosidade da Rede com Retry\n\nEsperar que toda atividade de rede seja concluída, com lógica de retry se a página nunca estabilizar.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import TimeoutException\n\nclass NetworkAwareScraper:\n    def __init__(self):\n        self.tab = None\n    \n    async def reload_page(self):\n        \"\"\"Recarregar página se a rede nunca estabilizou\"\"\"\n        print(\"Página não estabilizou, recarregando...\")\n        if self.tab:\n            await self.tab.refresh()\n            await asyncio.sleep(2)\n    \n    @retry(\n        max_retries=2,\n        exceptions=[TimeoutException],\n        on_retry=reload_page,\n        delay=3.0\n    )\n    async def wait_for_page_ready(self):\n        \"\"\"Esperar todas as requisições de rede completarem\"\"\"\n        await self.tab.enable_network_events()\n        \n        # Esperar rede ociosa (sem requisições por 2 segundos)\n        idle_time = 0\n        max_wait = 10\n        \n        while idle_time < max_wait:\n            # Verificar se há requisições em andamento\n            # (Implementação depende do seu rastreamento de eventos)\n            await asyncio.sleep(0.5)\n            idle_time += 0.5\n        \n        if idle_time >= max_wait:\n            raise TimeoutException(\"Rede nunca estabilizou\")\n```\n\n### 5. Detecção e Recuperação de CAPTCHA\n\nDetectar quando um CAPTCHA aparece e tomar a ação apropriada.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound\n\nclass CaptchaScraper:\n    def __init__(self):\n        self.tab = None\n        self.captcha_count = 0\n    \n    async def handle_captcha(self):\n        \"\"\"Lidar com CAPTCHA esperando ou mudando estratégia\"\"\"\n        self.captcha_count += 1\n        print(f\"CAPTCHA detectado (contagem: {self.captcha_count})\")\n        \n        if self.captcha_count > 2:\n            print(\"Muitos CAPTCHAs, pode precisar mudar estratégia...\")\n            # Poderia mudar para uma abordagem diferente aqui\n        \n        # Esperar mais tempo entre tentativas\n        await asyncio.sleep(30)\n        \n        # Atualizar a página\n        await self.tab.refresh()\n        await asyncio.sleep(5)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound],\n        on_retry=handle_captcha,\n        delay=10.0,\n        exponential_backoff=True\n    )\n    async def scrape_protected_content(self, url: str):\n        if not self.tab:\n            browser = Chrome()\n            self.tab = await browser.start()\n        \n        await self.tab.go_to(url)\n        \n        # Verificar CAPTCHA\n        captcha = await self.tab.find(\n            class_name='g-recaptcha',\n            timeout=2,\n            raise_exc=False\n        )\n        \n        if captcha:\n            raise ElementNotFound(\"CAPTCHA detectado\")\n        \n        # Lógica de scraping normal\n        content = await self.tab.find(class_name='article-content')\n        return await content.text\n```\n\n## Padrões Avançados\n\n### Combinando Múltiplas Estratégias de Recuperação\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound, WaitElementTimeout, NetworkError\n\nclass AdvancedScraper:\n    def __init__(self):\n        self.tab = None\n        self.attempt = 0\n        self.strategies = [\n            self.strategy_refresh,\n            self.strategy_clear_cache,\n            self.strategy_restart_browser,\n        ]\n    \n    async def strategy_refresh(self):\n        \"\"\"Estratégia 1: Atualização simples\"\"\"\n        print(\"Estratégia 1: Atualizando página\")\n        await self.tab.refresh()\n        await asyncio.sleep(2)\n    \n    async def strategy_clear_cache(self):\n        \"\"\"Estratégia 2: Limpar cache e atualizar\"\"\"\n        print(\"Estratégia 2: Limpando cache\")\n        await self.tab.execute_command('Network.clearBrowserCache')\n        await self.tab.refresh()\n        await asyncio.sleep(3)\n    \n    async def strategy_restart_browser(self):\n        \"\"\"Estratégia 3: Reinício completo do navegador\"\"\"\n        print(\"Estratégia 3: Reiniciando navegador\")\n        if self.tab:\n            await self.tab._browser.stop()\n        \n        browser = Chrome()\n        self.tab = await browser.start()\n    \n    async def adaptive_recovery(self):\n        \"\"\"Tentar diferentes estratégias de recuperação baseado no número da tentativa\"\"\"\n        strategy_index = min(self.attempt, len(self.strategies) - 1)\n        strategy = self.strategies[strategy_index]\n        \n        print(f\"Tentativa {self.attempt + 1}: Usando {strategy.__name__}\")\n        await strategy()\n        \n        self.attempt += 1\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound, WaitElementTimeout, NetworkError],\n        on_retry=adaptive_recovery,\n        delay=2.0\n    )\n    async def scrape_with_adaptive_retry(self, url: str):\n        await self.tab.go_to(url)\n        return await self.tab.find(id='target-content')\n```\n\n### Exceção Customizada para Falha Específica\n\n```python\nimport asyncio\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import PydollException\n\nclass RateLimitError(PydollException):\n    \"\"\"Lançado quando limite de taxa é detectado\"\"\"\n    message = \"Limite de taxa da API excedido\"\n\nclass APIScraper:\n    async def wait_for_rate_limit_reset(self):\n        \"\"\"Esperar mais quando limitado por taxa\"\"\"\n        print(\"Limite de taxa detectado, esperando 60 segundos...\")\n        await asyncio.sleep(60)\n    \n    @retry(\n        max_retries=5,\n        exceptions=[RateLimitError],\n        on_retry=wait_for_rate_limit_reset,\n        delay=10.0,\n        exponential_backoff=True\n    )\n    async def fetch_api_data(self, endpoint: str):\n        response = await self.tab.request.get(endpoint)\n        \n        if response.status == 429:  # Too Many Requests\n            raise RateLimitError(\"Limite de taxa da API excedido\")\n        \n        return response.json()\n```\n\n## Resumo de Melhores Práticas\n\n1. **Sempre especifique exceções explicitamente** - Nunca use o padrão `exceptions=Exception`\n2. **Use exponential backoff para serviços externos** - Dê tempo aos servidores para se recuperarem\n3. **Mantenha contagens de retry razoáveis** - Geralmente 3-5 tentativas são suficientes\n4. **Registre tentativas de retry** - Use `on_retry` para registrar o que está acontecendo\n5. **Defina callbacks antes dos métodos decorados** - Ordem importa em definições de classe\n6. **Faça callbacks async** - O decorator requer callbacks async\n7. **Restaure estado nos callbacks** - Use `on_retry` para navegar de volta para onde você estava\n8. **Considere o custo dos retries** - Cada retry consome tempo e recursos\n9. **Combine com outros tratamentos de erro** - Retries não substituem blocos try/except\n10. **Teste sua lógica de retry** - Certifique-se de que callbacks de recuperação realmente funcionam\n\n## Saiba Mais\n\n- **[Tratamento de Exceções](../core-concepts.md#error-handling)** - Entendendo exceções do Pydoll\n- **[Eventos de Rede](../network/monitoring.md)** - Rastrear e lidar com falhas de rede\n- **[Opções do Navegador](../configuration/browser-options.md)** - Configurar proxies e outras configurações\n- **[Sistema de Eventos](event-system.md)** - Construir estratégias de retry reativas\n\nO decorator retry é uma ferramenta poderosa que transforma scripts de scraping frágeis em aplicações prontas para produção. Ao combiná-lo com estratégias de recuperação bem pensadas, você pode construir scrapers que lidam graciosamente com o caos da web real.\n\n"
  },
  {
    "path": "docs/pt/features/advanced/event-system.md",
    "content": "# Sistema de Eventos\n\nO sistema de eventos do Pydoll permite que você ouça e reaja às atividades do navegador em tempo real. Isso é essencial para construir automações dinâmicas, monitorar requisições de rede, detectar mudanças na página e criar fluxos de trabalho reativos.\n\n!!! info \"Análise Profunda Disponível\"\n    Este guia foca no uso prático. Para detalhes arquitetônicos e implementação interna, veja a [Análise Profunda da Arquitetura de Eventos](../../deep-dive/event-architecture.md).\n\n## Pré-requisitos\n\nAntes de trabalhar com eventos, você precisa habilitar o domínio CDP correspondente:\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    \n    # Habilite o domínio antes de ouvir os eventos\n    await tab.enable_page_events()     # Para eventos de ciclo de vida da página\n    await tab.enable_network_events()  # Para atividade de rede\n    await tab.enable_dom_events()      # Para mudanças no DOM\n```\n\n!!! warning \"Eventos Não Serão Disparados Sem Habilitar\"\n    Se você registrar um callback mas esquecer de habilitar o domínio, seu callback nunca será acionado. Sempre habilite o domínio primeiro!\n\n## Audição Básica de Eventos\n\nO método `on()` registra ouvintes de eventos:\n\n```python\nfrom pydoll.protocol.page.events import PageEvent, LoadEventFiredEvent\n\nasync def handle_page_load(event: LoadEventFiredEvent):\n    print(f\"Página carregada em {event['params']['timestamp']}\")\n\n# Registrar o callback\nawait tab.enable_page_events()\ncallback_id = await tab.on(PageEvent.LOAD_EVENT_FIRED, handle_page_load)\n```\n\n### Estrutura do Evento\n\nTodos os eventos seguem a mesma estrutura:\n\n```python\n{\n    'method': 'Page.loadEventFired',  # Nome do evento\n    'params': {                        # Dados específicos do evento\n        'timestamp': 123456.789\n    }\n}\n```\n\nAcesse os dados do evento através de `event['params']`:\n\n```python\nfrom pydoll.protocol.network.events import RequestWillBeSentEvent\n\nasync def handle_request(event: RequestWillBeSentEvent):\n    url = event['params']['request']['url']\n    method = event['params']['request']['method']\n    print(f\"{method} {url}\")\n```\n\n### Usando Dicas de Tipo (Type Hints) para Melhor Suporte da IDE\n\nUse dicas de tipo com os tipos de parâmetros de evento para obter autocompletar para as chaves do evento:\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\nfrom pydoll.protocol.page.events import PageEvent, LoadEventFiredEvent\n\n# Com dicas de tipo - a IDE conhece todas as chaves disponíveis!\nasync def handle_request(event: RequestWillBeSentEvent):\n    # A IDE irá autocompletar 'params', 'request', 'url', etc.\n    url = event['params']['request']['url']\n    method = event['params']['request']['method']\n    timestamp = event['params']['timestamp']\n    print(f\"{method} {url} em {timestamp}\")\n\nasync def handle_load(event: LoadEventFiredEvent):\n    # A IDE sabe que este evento tem 'timestamp' em params\n    timestamp = event['params']['timestamp']\n    print(f\"Página carregada em {timestamp}\")\n\nawait tab.enable_network_events()\nawait tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, handle_request)\n\nawait tab.enable_page_events()\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, handle_load)\n```\n\n!!! tip \"Dicas de Tipo para Parâmetros de Evento\"\n    Todos os tipos de evento são definidos em `pydoll.protocol.<domain>.events`. Usá-los oferece a você:\n    \n    - **Autocompletar**: A IDE sugere chaves disponíveis em `event['params']`\n    - **Segurança de tipo**: Pega erros de digitação antes de rodar o código\n    - **Documentação**: Veja quais dados cada evento fornece\n    \n    Os tipos de evento seguem o padrão: `<EventName>Event` (ex: `RequestWillBeSentEvent`, `ResponseReceivedEvent`)\n\n## Domínios de Eventos Comuns\n\n### Eventos de Página (Page)\n\nMonitore o ciclo de vida da página e diálogos:\n\n```python\nfrom pydoll.protocol.page.events import PageEvent, JavascriptDialogOpeningEvent\n\nawait tab.enable_page_events()\n\n# Página carregada\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, lambda e: print(\"Página carregada!\"))\n\n# DOM pronto\nawait tab.on(PageEvent.DOM_CONTENT_EVENT_FIRED, lambda e: print(\"DOM pronto!\"))\n\n# Diálogo JavaScript\nasync def handle_dialog(event: JavascriptDialogOpeningEvent):\n    message = event['params']['message']\n    dialog_type = event['params']['type']\n    print(f\"Diálogo ({dialog_type}): {message}\")\n    \n    # Lidar com isso automaticamente\n    if await tab.has_dialog():\n        await tab.handle_dialog(accept=True)\n\nawait tab.on(PageEvent.JAVASCRIPT_DIALOG_OPENING, handle_dialog)\n```\n\n### Eventos de Rede (Network)\n\nMonitore requisições e respostas:\n\n```python\nfrom pydoll.protocol.network.events import (\n    NetworkEvent,\n    RequestWillBeSentEvent,\n    ResponseReceivedEvent,\n    LoadingFailedEvent\n)\n\nawait tab.enable_network_events()\n\n# Rastrear requisições\nasync def log_request(event: RequestWillBeSentEvent):\n    request = event['params']['request']\n    print(f\"→ {request['method']} {request['url']}\")\n\nawait tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, log_request)\n\n# Rastrear respostas\nasync def log_response(event: ResponseReceivedEvent):\n    response = event['params']['response']\n    print(f\"← {response['status']} {response['url']}\")\n\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, log_response)\n\n# Rastrear falhas\nasync def log_failure(event: LoadingFailedEvent):\n    url = event['params']['type']\n    error = event['params']['errorText']\n    print(f\"[FALHOU] {url} - {error}\")\n\nawait tab.on(NetworkEvent.LOADING_FAILED, log_failure)\n```\n\n### Eventos DOM\n\nReaja a mudanças no DOM:\n\n```python\nfrom pydoll.protocol.dom.events import DomEvent, AttributeModifiedEvent\n\nawait tab.enable_dom_events()\n\n# Rastrear mudanças de atributo\nasync def on_attribute_change(event: AttributeModifiedEvent):\n    node_id = event['params']['nodeId']\n    attr_name = event['params']['name']\n    attr_value = event['params']['value']\n    print(f\"Nó {node_id}: {attr_name}={attr_value}\")\n\nawait tab.on(DomEvent.ATTRIBUTE_MODIFIED, on_attribute_change)\n\n# Rastrear atualizações do documento\nawait tab.on(DomEvent.DOCUMENT_UPDATED, lambda e: print(\"Documento atualizado!\"))\n```\n\n## Callbacks Temporários\n\nUse `temporary=True` para ouvintes de uma única vez:\n\n```python\nfrom pydoll.protocol.page.events import PageEvent\n\n# Isso disparará apenas uma vez e depois se auto-removerá\nawait tab.on(\n    PageEvent.LOAD_EVENT_FIRED,\n    lambda e: print(\"Primeiro carregamento!\"),\n    temporary=True\n)\n\nawait tab.go_to(\"https://example.com\")  # Dispara o callback\nawait tab.refresh()                      # Callback não disparará novamente\n```\n\n!!! tip \"Perfeito para Configuração Única\"\n    Callbacks temporários são ideais para tarefas de inicialização que devem acontecer apenas uma vez.\n\n## Acessando a Aba (Tab) nos Callbacks\n\nUse `functools.partial` para passar a aba para seus callbacks:\n\n```python\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def process_response(tab, event: ResponseReceivedEvent):\n    # Agora podemos usar o objeto tab!\n    request_id = event['params']['requestId']\n    \n    # Obter corpo da resposta\n    body = await tab.get_network_response_body(request_id)\n    print(f\"Corpo da resposta: {body[:100]}...\")\n\nawait tab.enable_network_events()\nawait tab.on(\n    NetworkEvent.RESPONSE_RECEIVED,\n    partial(process_response, tab)\n)\n```\n\n!!! info \"Por que Usar Partial?\"\n    O sistema de eventos passa apenas os dados do evento para os callbacks. `partial` permite que você vincule parâmetros adicionais, como a instância da aba.\n\n## Gerenciando Callbacks\n\n### Removendo Callbacks\n\n```python\nfrom pydoll.protocol.page.events import PageEvent\n\n# Salvar o ID do callback\ncallback_id = await tab.on(PageEvent.LOAD_EVENT_FIRED, my_callback)\n\n# Removê-lo mais tarde\nawait tab.remove_callback(callback_id)\n```\n\n### Limpando Todos os Callbacks\n\n```python\n# Remover todos os callbacks registrados para esta aba\nawait tab.clear_callbacks()\n```\n\n## Exemplos Práticos\n\n### Monitorar Chamadas de API\n\n```python\nimport asyncio\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def monitor_api_calls(tab):\n    collected_data = []\n    \n    # Dica de tipo ajuda a IDE a autocompletar chaves de evento\n    async def capture_api_response(tab, data_list, event: ResponseReceivedEvent):\n        url = event['params']['response']['url']\n        \n        # Filtrar apenas chamadas de API\n        if '/api/' not in url:\n            return\n        \n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        \n        data_list.append({\n            'url': url,\n            'body': body,\n            'status': event['params']['response']['status']\n        })\n        print(f\"Capturada chamada de API: {url}\")\n    \n    await tab.enable_network_events()\n    await tab.on(\n        NetworkEvent.RESPONSE_RECEIVED,\n        partial(capture_api_response, tab, collected_data)\n    )\n    \n    # Navegar e coletar\n    await tab.go_to(\"https://example.com\")\n    await asyncio.sleep(3)  # Esperar requisições completarem\n    \n    return collected_data\n```\n\n### Esperar por Evento Específico\n\n```python\nimport asyncio\nfrom pydoll.protocol.page.events import PageEvent, FrameNavigatedEvent\n\nasync def wait_for_navigation():\n    navigation_done = asyncio.Event()\n    \n    async def on_navigated(event: FrameNavigatedEvent):\n        navigation_done.set()\n    \n    await tab.enable_page_events()\n    await tab.on(PageEvent.FRAME_NAVIGATED, on_navigated, temporary=True)\n    \n    # Disparar navegação\n    button = await tab.find(id='next-page')\n    await button.click()\n    \n    # Esperar completar\n    await navigation_done.wait()\n    print(\"Navegação concluída!\")\n```\n\n### Detecção de Ociosidade da Rede (Network Idle)\n\n```python\nimport asyncio\nfrom pydoll.protocol.network.events import (\n    NetworkEvent,\n    RequestWillBeSentEvent,\n    LoadingFinishedEvent,\n    LoadingFailedEvent\n)\n\nasync def wait_for_network_idle(tab, timeout=5):\n    in_flight = 0\n    idle_event = asyncio.Event()\n    last_activity = asyncio.get_event_loop().time()\n    \n    async def on_request(event: RequestWillBeSentEvent):\n        nonlocal in_flight, last_activity\n        in_flight += 1\n        last_activity = asyncio.get_event_loop().time()\n    \n    async def on_finished(event: LoadingFinishedEvent | LoadingFailedEvent):\n        nonlocal in_flight, last_activity\n        in_flight -= 1\n        last_activity = asyncio.get_event_loop().time()\n        \n        if in_flight == 0:\n            idle_event.set()\n    \n    await tab.enable_network_events()\n    req_id = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, on_request)\n    fin_id = await tab.on(NetworkEvent.LOADING_FINISHED, on_finished)\n    fail_id = await tab.on(NetworkEvent.LOADING_FAILED, on_finished)\n    \n    try:\n        await asyncio.wait_for(idle_event.wait(), timeout=timeout)\n        print(\"Rede está ociosa!\")\n    except asyncio.TimeoutError:\n        print(f\"Rede ainda ativa após {timeout}s\")\n    finally:\n        # Limpeza\n        await tab.remove_callback(req_id)\n        await tab.remove_callback(fin_id)\n        await tab.remove_callback(fail_id)\n```\n\n### Raspagem de Conteúdo Dinâmico\n\n```python\nimport asyncio\nimport json\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def scrape_infinite_scroll(tab, max_items=100):\n    items = []\n    \n    async def capture_products(tab, items_list, event: ResponseReceivedEvent):\n        url = event['params']['response']['url']\n        \n        # Procurar por endpoint de API de produtos\n        if '/products' not in url:\n            return\n        \n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        \n        try:\n            data = json.loads(body)\n            if 'items' in data:\n                items_list.extend(data['items'])\n                print(f\"Coletados {len(data['items'])} itens (total: {len(items_list)})\")\n        except json.JSONDecodeError:\n            pass\n    \n    await tab.enable_network_events()\n    await tab.on(\n        NetworkEvent.RESPONSE_RECEIVED,\n        partial(capture_products, tab, items)\n    )\n    \n    await tab.go_to(\"https://example.com/products\")\n    \n    # Rolar para disparar carregamento infinito\n    while len(items) < max_items:\n        await tab.execute_script(\"window.scrollTo(0, document.body.scrollHeight)\")\n        await asyncio.sleep(1)\n    \n    return items[:max_items]\n```\n\n## Tabelas de Referência de Eventos\n\n### Domínios Disponíveis\n\n| Domínio | Método de Habilitação | Casos de Uso Comuns |\n|---|---|---|\n| Page | `enable_page_events()` | Ciclo de vida da página, navegação, diálogos |\n| Network | `enable_network_events()` | Monitoramento de requisição/resposta, rastreamento de API |\n| DOM | `enable_dom_events()` | Mudanças na estrutura DOM, modificações de atributos |\n| Fetch | `enable_fetch_events()` | Interceptação e modificação de requisições |\n| Runtime | `enable_runtime_events()` | Mensagens do console, exceções JavaScript |\n\n### Eventos Chave de Página (Page)\n\n| Evento | Quando Dispara | Caso de Uso |\n|---|---|---|\n| `LOAD_EVENT_FIRED` | Carregamento da página completo | Esperar pelo carregamento completo da página |\n| `DOM_CONTENT_EVENT_FIRED` | DOM pronto | Iniciar manipulação do DOM |\n| `JAVASCRIPT_DIALOG_OPENING` | Alert/confirm/prompt | Lidar automaticamente com diálogos |\n| `FRAME_NAVIGATED` | Navegação completa | Rastrear navegação de SPA |\n| `FILE_CHOOSER_OPENED` | Input de arquivo clicado | Uploads automáticos de arquivos |\n\n### Eventos Chave de Rede (Network)\n\n| Evento | Quando Dispara | Caso de Uso |\n|---|---|---|\n| `REQUEST_WILL_BE_SENT` | Antes da requisição ser enviada | Registrar/modificar requisições de saída |\n| `RESPONSE_RECEIVED` | Cabeçalhos da resposta recebidos | Capturar respostas de API |\n| `LOADING_FINISHED` | Corpo da resposta carregado | Obter dados completos da resposta |\n| `LOADING_FAILED` | Requisição falhou | Rastrear erros e retentativas |\n| `WEB_SOCKET_CREATED` | WebSocket aberto | Monitorar conexões em tempo real |\n\n### Eventos Chave do DOM\n\n| Evento | Quando Dispara | Caso de Uso |\n|---|---|---|\n| `DOCUMENT_UPDATED` | DOM reconstruído | Atualizar referências de elementos |\n| `ATTRIBUTE_MODIFIED` | Atributo do elemento mudou | Rastrear mudanças dinâmicas de atributos |\n| `CHILD_NODE_INSERTED` | Novo elemento adicionado | Detectar conteúdo adicionado dinamicamente |\n| `CHILD_NODE_REMOVED` | Elemento removido | Detectar conteúdo removido |\n\n### Referência de Tipo de Evento\n\nTodos os tipos de evento e suas estruturas de parâmetros são definidos nos módulos de protocolo:\n\n| Domínio | Caminho de Importação | Tipos de Exemplo |\n|---|---|---|\n| Page | `pydoll.protocol.page.events` | `LoadEventFiredEvent`, `FrameNavigatedEvent`, `JavascriptDialogOpeningEvent` |\n| Network | `pydoll.protocol.network.events` | `RequestWillBeSentEvent`, `ResponseReceivedEvent`, `LoadingFinishedEvent` |\n| DOM | `pydoll.protocol.dom.events` | `DocumentUpdatedEvent`, `AttributeModifiedEvent`, `ChildNodeInsertedEvent` |\n| Fetch | `pydoll.protocol.fetch.events` | `RequestPausedEvent`, `AuthRequiredEvent` |\n| Runtime | `pydoll.protocol.runtime.events` | `ConsoleAPICalledEvent`, `ExceptionThrownEvent` |\n\nCada tipo de evento é um `TypedDict` que define a estrutura exata do evento, incluindo todas as chaves disponíveis no dicionário `params`.\n\n## Melhores Práticas\n\n### 1. Sempre Habilite os Domínios Primeiro\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent\n\n# Bom\nawait tab.enable_network_events()\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, callback)\n\n# Ruim: callback nunca será disparado\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, callback)\nawait tab.enable_network_events()\n```\n\n### 2. Limpe Quando Terminar\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent\n\n# Habilitar para tarefa específica\nawait tab.enable_network_events()\ncallback_id = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, log_request)\n\n# Faça seu trabalho...\nawait tab.go_to(\"https://example.com\")\n\n# Limpar\nawait tab.remove_callback(callback_id)\nawait tab.disable_network_events()\n```\n\n### 3. Use Filtragem Precoce\n\n```python\nfrom pydoll.protocol.network.events import RequestWillBeSentEvent\n\n# Bom: filtrar cedo\nasync def handle_api_request(event: RequestWillBeSentEvent):\n    url = event['params']['request']['url']\n    if '/api/' not in url:\n        return  # Sair cedo\n    \n    # Processar apenas requisições de API\n    process_request(event)\n\n# Ruim: processa tudo\nasync def handle_all_requests(event: RequestWillBeSentEvent):\n    url = event['params']['request']['url']\n    process_request(event)\n    if '/api/' in url:\n        do_extra_work(event)\n```\n\n### 4. Lide com Erros Graciosamente\n\n```python\nfrom pydoll.protocol.network.events import ResponseReceivedEvent\n\nasync def safe_callback(event: ResponseReceivedEvent):\n    try:\n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        process_body(body)\n    except KeyError:\n        # Evento pode não ter requestId\n        pass\n    except Exception as e:\n        print(f\"Erro no callback: {e}\")\n        # Continuar sem quebrar o loop de eventos\n```\n\n## Considerações de Desempenho\n\n!!! warning \"Eventos de Alta Frequência\"\n    Eventos DOM podem disparar **muito frequentemente** em páginas dinâmicas. Use filtragem e debouncing para evitar problemas de desempenho.\n\n### Volume de Eventos por Domínio\n\n| Domínio | Frequência de Eventos | Impacto no Desempenho |\n|---|---|---|\n| Page | Baixa | Mínimo |\n| Network | Moderada-Alta | Moderado |\n| DOM | Muito Alta | Alto |\n| Fetch | Moderada | Moderado |\n\n### Dicas de Otimização\n\n1.  **Habilite apenas o que você precisa**: Não habilite todos os domínios de uma vez\n2.  **Use callbacks temporários**: Limpeza automática quando possível\n3.  **Filtre cedo**: Verifique condições antes de operações caras\n4.  **Desabilite quando terminar**: Libere recursos\n5.  **Evite processamento pesado**: Mantenha callbacks rápidos, descarregue o trabalho para tarefas separadas\n\n```python\nimport asyncio\nfrom pydoll.protocol.network.events import ResponseReceivedEvent\n\n# Bom: callback rápido, descarrega trabalho pesado\nasync def handle_response(event: ResponseReceivedEvent):\n    if should_process(event):\n        asyncio.create_task(heavy_processing(event))  # Não bloqueie\n\n# Ruim: bloqueia o loop de eventos\nasync def handle_response(event: ResponseReceivedEvent):\n    await heavy_processing(event)  # Bloqueia outros eventos\n```\n\n## Padrões Comuns\n\n### Gerenciador de Contexto para Eventos\n\n```python\nfrom contextlib import asynccontextmanager\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\n\n@asynccontextmanager\nasync def monitor_requests(tab):\n    \"\"\"Gerenciador de contexto para monitorar requisições durante um bloco.\"\"\"\n    requests = []\n    \n    async def capture(event: RequestWillBeSentEvent):\n        requests.append(event['params']['request'])\n    \n    await tab.enable_network_events()\n    cb_id = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, capture)\n    \n    try:\n        yield requests\n    finally:\n        await tab.remove_callback(cb_id)\n        await tab.disable_network_events()\n\n# Uso\nasync with monitor_requests(tab) as requests:\n    await tab.go_to(\"https://example.com\")\n    # Todas as requisições são capturadas\n\nprint(f\"Capturadas {len(requests)} requisições\")\n```\n\n### Registro Condicional de Eventos\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent\nfrom pydoll.protocol.dom.events import DomEvent\n\nasync def setup_monitoring(tab, track_network=False, track_dom=False):\n    \"\"\"Habilitar apenas o monitoramento especificado.\"\"\"\n    callbacks = []\n    \n    if track_network:\n        await tab.enable_network_events()\n        cb = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, log_request)\n        callbacks.append(('network', cb))\n    \n    if track_dom:\n        await tab.enable_dom_events()\n        cb = await tab.on(DomEvent.ATTRIBUTE_MODIFIED, log_dom_change)\n        callbacks.append(('dom', cb))\n    \n    return callbacks\n```\n\n## Leitura Adicional\n\n- **[Análise Profunda da Arquitetura de Eventos](../../deep-dive/event-architecture.md)** - Implementação interna e comunicação WebSocket\n- **[Monitoramento de Rede](../network/monitoring.md)** - Técnicas avançadas de análise de rede\n- **[Automação Reativa](reactive-automation.md)** - Construindo fluxos de trabalho orientados a eventos\n\n!!! tip \"Comece Simples\"\n    Comece com eventos de Página (Page) para entender o básico, depois passe para eventos de Rede (Network) e DOM conforme necessário. O sistema de eventos é poderoso, mas pode ser intimidador no início."
  },
  {
    "path": "docs/pt/features/advanced/remote-connections.md",
    "content": "# Conexões Remotas e Automação Híbrida\n\nO Pydoll permite que você se conecte a navegadores já em execução via WebSocket, habilitando cenários de controle remoto e automação híbrida. Isso é perfeito para pipelines de CI/CD, ambientes contêinerizados, sessões de depuração e integração do Pydoll com ferramentas CDP existentes.\n\n!!! info \"Nenhuma Configuração Necessária\"\n    Diferente da automação tradicional que inicia navegadores, conexões remotas permitem controlar navegadores que já estão rodando. Nenhum gerenciamento de processo é necessário!\n\n## Por que Conexões Remotas?\n\nConexões remotas desbloqueiam cenários poderosos de automação:\n\n| Caso de Uso | Benefício |\n|---|---|\n| **Pipelines de CI/CD** | Conecte-se a contêineres de navegador sem gerenciar processos |\n| **Ambientes Docker** | Controle navegadores rodando em contêineres separados |\n| **Depuração Remota** | Automatize navegadores em servidores remotos ou VMs |\n| **Ferramental Híbrido** | Integre o Pydoll com sua infraestrutura CDP existente |\n| **Desenvolvimento** | Anexe ao seu navegador local para testes rápidos |\n| **Automação Multi-Ferramenta** | Compartilhe sessões de navegador entre diferentes ferramentas |\n\n## Configurando um Servidor de Navegador Remoto\n\n!!! tip \"Já Tem um Serviço de Navegador Remoto?\"\n    Se você está usando um serviço de navegador na nuvem (BrowserStack, Selenium Grid, LambdaTest, etc.) ou já tem uma instância do Chrome rodando com uma URL WebSocket, você pode **pular esta seção inteira** e ir direto para [Métodos de Conexão](#métodos-de-conexão) para aprender como se conectar com o Pydoll.\n\nAntes de poder se conectar remotamente, você precisa iniciar o Chrome com a depuração habilitada e configurado corretamente para aceitar conexões externas.\n\n### Configuração Básica do Servidor (Linux)\n\nInicie o Chrome com depuração remota em um servidor:\n\n```bash\n# Configuração básica - acessível apenas do localhost\ngoogle-chrome \\\n  --remote-debugging-port=9222 \\\n  --headless=new \\\n  --no-sandbox \\\n  --disable-dev-shm-usage \\\n  --user-data-dir=/tmp/chrome-profile\n\n# Configuração do servidor - acessível de outras máquinas\ngoogle-chrome \\\n  --remote-debugging-port=9222 \\\n  --remote-debugging-address=0.0.0.0 \\\n  --headless=new \\\n  --no-sandbox \\\n  --disable-dev-shm-usage \\\n  --user-data-dir=/tmp/chrome-profile\n```\n\n!!! warning \"Criticidade de Segurança\"\n    Usar `--remote-debugging-address=0.0.0.0` torna a porta de depuração acessível de **qualquer interface de rede**. Isso é necessário para conexões remotas, mas cria um risco de segurança significativo se exposto à internet.\n\n### Configuração Recomendada do Servidor\n\n```bash\n# Configuração pronta para produção\ngoogle-chrome \\\n  --remote-debugging-port=9222 \\\n  --remote-debugging-address=0.0.0.0 \\\n  --headless=new \\\n  --no-sandbox \\\n  --disable-dev-shm-usage \\\n  --disable-gpu \\\n  --disable-software-rasterizer \\\n  --disable-extensions \\\n  --disable-background-networking \\\n  --disable-background-timer-throttling \\\n  --disable-client-side-phishing-detection \\\n  --disable-popup-blocking \\\n  --disable-prompt-on-repost \\\n  --disable-sync \\\n  --metrics-recording-only \\\n  --no-first-run \\\n  --safebrowsing-disable-auto-update \\\n  --user-data-dir=/tmp/chrome-remote-$(date +%s)\n```\n\n**Flags chave explicadas:**\n\n| Flag | Propósito |\n|---|---|\n| `--remote-debugging-port=9222` | Habilita o CDP na porta 9222 |\n| `--remote-debugging-address=0.0.0.0` | Permite conexões externas (risco de segurança!) |\n| `--headless=new` | Executa sem GUI (modo servidor) |\n| `--no-sandbox` | Necessário em Docker/contêineres (trade-off de segurança) |\n| `--disable-dev-shm-usage` | Previne problemas de memória /dev/shm em contêineres |\n| `--disable-gpu` | Sem aceleração por GPU (recomendado para headless) |\n| `--user-data-dir=/tmp/...` | Perfil isolado por instância |\n\n!!! warning \"Sobre a Flag --no-sandbox\"\n    A flag `--no-sandbox` desabilita o sandbox de segurança do Chrome, que isola o processo do navegador do sistema. Esta flag é **necessária** na maioria dos ambientes Docker/contêineres devido a restrições de capacidade do kernel, mas traz implicações de segurança:\n    \n    - **Risco**: Remove o isolamento entre o navegador e o sistema\n    - **Quando usar**: Contêineres Docker, ambientes restritos\n    - **Mitigação**: Garanta isolamento em nível de contêiner (namespaces, cgroups) e evite rodar como root\n    \n    Considere usar `--no-sandbox` apenas quando absolutamente necessário e implemente camadas adicionais de segurança no nível do contêiner.\n\n### Configuração do Docker\n\nCrie um servidor Chrome contêinerizado:\n\n!!! tip \"Usando Imagens Prontas\"\n    Para produção, considere usar imagens oficiais pré-construídas em vez de construir a sua própria:\n    \n    - **Imagens Selenium**: `selenium/standalone-chrome` (inclui WebDriver)\n    - **Zenika Alpine Chrome**: `zenika/alpine-chrome` (leve, ~200MB)\n    - **Browserless**: `browserless/chrome` (pronto para produção com monitoramento)\n    \n    Essas imagens são atualizadas regularmente, testadas em segurança e otimizadas para ambientes de contêiner.\n\n**Dockerfile (Build Personalizado):**\n```dockerfile\nFROM ubuntu:22.04\n\n# Instalar Chrome\nRUN apt-get update && apt-get install -y \\\n    wget \\\n    gnupg \\\n    ca-certificates \\\n    && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \\\n    && echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list \\\n    && apt-get update \\\n    && apt-get install -y google-chrome-stable \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Expor porta de depuração\nEXPOSE 9222\n\n# Iniciar Chrome com depuração remota\nCMD [\"google-chrome\", \\\n     \"--remote-debugging-port=9222\", \\\n     \"--remote-debugging-address=0.0.0.0\", \\\n     \"--headless=new\", \\\n     \"--no-sandbox\", \\\n     \"--disable-dev-shm-usage\", \\\n     \"--disable-gpu\", \\\n     \"--user-data-dir=/tmp/chrome-profile\"]\n```\n\n**docker-compose.yml:**\n```yaml\nservices:\n  chrome-server:\n    build: .\n    ports:\n      - \"127.0.0.1:9222:9222\"\n    \n    # Descomente a linha abaixo SOMENTE se precisar de acesso remoto\n    # E tiver protegido a porta com firewall ou proxy.\n    # - \"9222:9222\"\n\n    shm_size: '2gb'  # Crítico: Chrome usa /dev/shm para memória compartilhada\n                      # O shm_size padrão do Docker (64MB) é insuficiente\n    restart: unless-stopped\n    environment:\n      - DISPLAY=:99\n    networks:\n      - automation-network\n    # Opcional: Limites de recursos para produção\n    # deploy:\n    #   resources:\n    #     limits:\n    #       cpus: '2'\n    #       memory: 4G\n\n  automation-client:\n    image: python:3.11\n    depends_on:\n      - chrome-server\n    volumes:\n      - ./:/app\n    working_dir: /app\n    command: python automation_script.py\n    environment:\n      - CHROME_WS=ws://chrome-server:9222/devtools/browser\n    networks:\n      - automation-network\n\nnetworks:\n  automation-network:\n    driver: bridge\n```\n\n**Uso:**\n```bash\n# Iniciar a stack\ndocker-compose up -d\n\n# Verificar se o Chrome está rodando\ncurl http://localhost:9222/json/version\n\n# Conectar do cliente de automação (dentro da rede Docker)\n# ws://chrome-server:9222/devtools/browser/...\n```\n\n### Serviço Systemd (Servidor Linux)\n\nCrie um serviço Chrome persistente:\n\n**/etc/systemd/system/chrome-remote.service:**\n```ini\n[Unit]\nDescription=Chrome Remote Debugging Server\nAfter=network.target\n\n[Service]\nType=simple\nUser=chrome-user\nGroup=chrome-user\nEnvironment=\"DISPLAY=:99\"\nExecStart=/usr/bin/google-chrome \\\n    --remote-debugging-port=9222 \\\n    --remote-debugging-address=0.0.0.0 \\\n    --headless=new \\\n    --no-sandbox \\\n    --disable-dev-shm-usage \\\n    --disable-gpu \\\n    --user-data-dir=/var/lib/chrome-remote\nRestart=always\nRestartSec=10\n\n[Install]\nWantedBy=multi-user.target\n```\n\n**Configuração e gerenciamento:**\n```bash\n# Criar usuário dedicado\nsudo useradd -r -s /bin/false chrome-user\nsudo mkdir -p /var/lib/chrome-remote\nsudo chown chrome-user:chrome-user /var/lib/chrome-remote\n\n# Instalar e habilitar serviço\nsudo systemctl daemon-reload\nsudo systemctl enable chrome-remote\nsudo systemctl start chrome-remote\n\n# Verificar status\nsudo systemctl status chrome-remote\n\n# Ver logs\nsudo journalctl -u chrome-remote -f\n\n# Reiniciar serviço\nsudo systemctl restart chrome-remote\n```\n\n### Configuração de Segurança de Rede\n\n#### Regras de Firewall (iptables)\n\n```bash\n# Permitir que apenas IPs específicos acessem a porta 9222\nsudo iptables -A INPUT -p tcp --dport 9222 -s 192.168.1.100 -j ACCEPT\nsudo iptables -A INPUT -p tcp --dport 9222 -j DROP\n\n# Salvar regras\nsudo iptables-save > /etc/iptables/rules.v4\n```\n\n#### Regras de Firewall (ufw)\n\n```bash\n# Negar todo o acesso à porta 9222 por padrão\nsudo ufw deny 9222\n\n# Permitir IP específico\nsudo ufw allow from 192.168.1.100 to any port 9222\n\n# Permitir sub-rede específica\nsudo ufw allow from 192.168.1.0/24 to any port 9222\n\n# Habilitar firewall\nsudo ufw enable\n```\n\n#### Proxy Reverso Nginx (com Autenticação)\n\nProteja a depuração do Chrome com autenticação HTTP:\n\n**/etc/nginx/sites-available/chrome-remote:**\n```nginx\nserver {\n    listen 80;\n    server_name chrome.example.com;\n\n    # Autenticação básica\n    auth_basic \"Chrome Remote Debugging\";\n    auth_basic_user_file /etc/nginx/.htpasswd;\n\n    location / {\n        proxy_pass http://localhost:9222;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_read_timeout 86400;\n    }\n}\n```\n\n**Configuração:**\n```bash\n# Criar arquivo de senha\nsudo htpasswd -c /etc/nginx/.htpasswd admin\n\n# Habilitar site\nsudo ln -s /etc/nginx/sites-available/chrome-remote /etc/nginx/sites-enabled/\nsudo nginx -t\nsudo systemctl reload nginx\n\n# Conectar com autenticação\n# ws://admin:password@chrome.example.com/devtools/browser/...\n```\n\n### Conectando de Outro Computador\n\nUma vez que seu servidor esteja configurado, conecte-se da sua máquina cliente:\n\n```python\nimport asyncio\nimport aiohttp\nfrom pydoll.browser.chromium import Chrome\n\nasync def connect_to_remote_server():\n    \"\"\"Conectar ao Chrome rodando em um servidor remoto.\"\"\"\n    # IP e porta do servidor\n    server_ip = \"192.168.1.100\"\n    server_port = 9222\n\n    async with aiohttp.ClientSession() as session:\n        # Consultar o servidor por alvos disponíveis\n        url = f\"http://{server_ip}:{server_port}/json/version\"\n        \n        async with session.get(url) as response:\n            data = await response.json()\n            ws_url = data['webSocketDebuggerUrl']\n            \n            print(f\"Informações do servidor:\")\n            print(f\"  Navegador: {data.get('Browser')}\")\n            print(f\"  Protocolo: {data.get('Protocol-Version')}\")\n            print(f\"  WebSocket: {ws_url}\")\n    \n    # 2. Conectar ao navegador\n    chrome = Chrome()\n    tab = await chrome.connect(ws_url)\n    \n    print(f\"\\n[SUCESSO] Conectado ao servidor Chrome remoto!\")\n    \n    # 3. Usar normalmente\n    await tab.go_to('https://example.com')\n    title = await tab.execute_script('return document.title')\n    print(f\"Título da página: {title}\")\n    \n    # 4. Limpeza\n    await chrome.close()\n\nasyncio.run(connect_to_remote_server())\n```\n\n### Testando a Configuração do Seu Servidor\n\n```bash\n# 1. Verificar se o Chrome está rodando\nps aux | grep chrome\n\n# 2. Verificar se a porta está escutando\nnetstat -tulpn | grep 9222\n# Ou\nss -tulpn | grep 9222\n\n# 3. Testar acesso local\ncurl http://localhost:9222/json/version\n\n# 4. Testar acesso remoto (da máquina cliente)\ncurl http://SERVER_IP:9222/json/version\n\n# 5. Verificar URL do WebSocket\ncurl http://SERVER_IP:9222/json/version | jq -r '.webSocketDebuggerUrl'\n\n# 6. Listar todos os alvos disponíveis (abas/páginas)\ncurl http://SERVER_IP:9222/json/list\n```\n\n### Configuração de Múltiplas Instâncias\n\nExecute múltiplas instâncias do Chrome em portas diferentes:\n\n```bash\n#!/bin/bash\n# start-chrome-pool.sh\n\nfor port in 9222 9223 9224 9225; do\n    google-chrome \\\n        --remote-debugging-port=$port \\\n        --remote-debugging-address=0.0.0.0 \\\n        --headless=new \\\n        --no-sandbox \\\n        --disable-dev-shm-usage \\\n        --user-data-dir=/tmp/chrome-$port &\n    \n    echo \"Iniciado Chrome na porta $port\"\ndone\n\necho \"Pool de Chrome pronto. Portas: 9222-9225\"\n```\n\n**Cliente Python com pool:**\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nimport aiohttp\n\nasync def connect_to_pool(server_ip: str, ports: list[int]):\n    \"\"\"Conectar a múltiplas instâncias do Chrome.\"\"\"\n    tasks = []\n    \n    for port in ports:\n        task = connect_to_instance(server_ip, port)\n        tasks.append(task)\n    \n    results = await asyncio.gather(*tasks)\n    return results\n\nasync def connect_to_instance(server_ip: str, port: int):\n    \"\"\"Conectar a uma única instância do Chrome.\"\"\"\n    # Obter URL do WebSocket\n    async with aiohttp.ClientSession() as session:\n        url = f\"http://{server_ip}:{port}/json/version\"\n        async with session.get(url) as response:\n            data = await response.json()\n            ws_url = data['webSocketDebuggerUrl']\n    \n    # Conectar\n    chrome = Chrome()\n    tab = await chrome.connect(ws_url)\n    \n    # Rodar automação\n    await tab.go_to('https://example.com')\n    title = await tab.execute_script('return document.title')\n    \n    print(f\"Porta {port}: {title}\")\n    \n    await chrome.close()\n    return title\n\n# Uso\nasyncio.run(connect_to_pool('192.168.1.100', [9222, 9223, 9224, 9225]))\n```\n\n## Métodos de Conexão\n\nO Pydoll oferece duas abordagens para conexões remotas, cada uma adequada para cenários diferentes.\n\n### Método 1: Conexão no Nível do Navegador\n\nConecte-se a um navegador em execução usando seu endpoint WebSocket e tenha acesso a todas as abas abertas:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def connect_to_remote_browser():\n    chrome = Chrome()\n    \n    # Conectar ao navegador remoto via WebSocket\n    tab = await chrome.connect('ws://localhost:9222/devtools/browser/XXXX')\n    \n    # A aba retornada é a primeira aba disponível\n    print(f\"Conectado à aba: {await tab.execute_script('return document.title')}\")\n    \n    # Você pode obter todas as outras abas também\n    all_tabs = await chrome.get_opened_tabs()\n    print(f\"Total de abas disponíveis: {len(all_tabs)}\")\n    \n    # Use a aba normalmente\n    await tab.go_to('https://example.com')\n    element = await tab.find(id='main-content')\n    text = await element.text\n    print(f\"Conteúdo: {text}\")\n    \n    # Limpeza\n    await chrome.close()\n\nasyncio.run(connect_to_remote_browser())\n```\n\n!!! tip \"Obtendo a URL do WebSocket\"\n    Inicie o Chrome com a depuração habilitada:\n    ```bash\n    # Linux/Mac\n    google-chrome --remote-debugging-port=9222\n    \n    # Windows\n    \"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" --remote-debugging-port=9222\n    ```\n    \n    **Para conexões locais** (mesma máquina):\n    \n    - Visite `http://localhost:9222/json/version` no seu navegador para obter a URL do WebSocket no campo `webSocketDebuggerUrl`\n    - Ou consulte programaticamente como mostrado no exemplo acima usando `aiohttp`\n    - Para depuração rápida, você também pode verificar `browser._connection_port` após iniciar uma instância local do navegador\n    \n    **Para conexões remotas** (máquina diferente):\n    \n    - Consulte `http://SERVER_IP:9222/json/version` da sua máquina cliente\n    - Use a `webSocketDebuggerUrl` da resposta, substituindo `localhost` pelo IP real do servidor, se necessário\n\n### Método 2: Controle Direto de Elemento (Abordagem Híbrida)\n\nSe você já tem sua própria integração CDP ou ferramentas de baixo nível, pode envolver elementos existentes com a API de alto nível do Pydoll:\n\n```python\nimport asyncio\nimport json\nfrom pydoll.connection.connection_handler import ConnectionHandler\nfrom pydoll.elements.web_element import WebElement\n\nasync def custom_cdp_integration():\n    \"\"\"Use o Pydoll junto com sua implementação CDP personalizada.\"\"\"\n    # Sua configuração CDP existente encontrou um elemento\n    page_ws = 'ws://localhost:9222/devtools/page/ABC123'\n    \n    # Você usou Runtime.evaluate para encontrar um elemento\n    # e obteve seu objectId\n    element_object_id = '{\\\"injectedScriptId\\\":1,\\\"id\\\":1}'\n    \n    # Criar conexão Pydoll\n    connection = ConnectionHandler(ws_address=page_ws)\n    \n    # Envolver o elemento\n    button = WebElement(\n        object_id=element_object_id,\n        connection_handler=connection\n    )\n    \n    # Usar os métodos de alto nível do Pydoll\n    await button.wait_until(is_visible=True, timeout=5)\n    await button.wait_until(is_interactable=True)\n    \n    # Clicar com deslocamento realista\n    await button.click(offset_x=5, offset_y=5)\n    \n    # Obter propriedades computadas facilmente\n    is_enabled = await button.is_enabled()\n    bounds = await button.bounds\n    \n    print(f\"Botão clicado! Habilitado: {is_enabled}, Limites: {bounds}\")\n    \n    # Limpeza\n    await connection.close()\n\nasyncio.run(custom_cdp_integration())\n```\n\n!!! tip \"Formato do Object ID\"\n    O `objectId` é uma string retornada por comandos CDP como `Runtime.evaluate` ou `DOM.resolveNode`. Geralmente é uma string JSON com campos como `injectedScriptId` e `id`.\n\n\n!!! info \"O Melhor dos Dois Mundos\"\n    Esta abordagem híbrida permite que você aproveite sua infraestrutura CDP existente enquanto se beneficia da API ergonômica de elementos do Pydoll para interações, esperas e acesso a propriedades.\n\n## Considerações de Segurança\n\n!!! danger \"Ambientes de Produção\"\n    Portas de depuração remota expõem **controle total** sobre o navegador, incluindo:\n    \n    - Acesso a todas as páginas e dados\n    - Capacidade de executar JavaScript arbitrário\n    - Acesso a cookies e sessões\n    - Acesso ao sistema de arquivos via downloads\n    \n    **Nunca exponha portas de depuração à internet sem autenticação adequada e segurança de rede!**\n\n### Práticas de Segurança Recomendadas\n\n| Prática | Por quê | Como |\n|---|---|---|\n| **Túneis SSH** | Criptografa o tráfego e autentica | `ssh -L 9222:localhost:9222 user@host` |\n| **VPN** | Segurança em nível de rede | Conectar via VPN corporativa/privada |\n| **Regras de Firewall** | Restringir acesso | Permitir apenas IPs específicos |\n| **Redes Docker** | Isolamento de contêiner | Usar redes Docker privadas |\n| **Sem Exposição Pública** | Prevenir ataques | Nunca fazer bind para `0.0.0.0` em produção |\n\n## Leitura Adicional\n\n- **[Sistema de Eventos](event-system.md)** - Monitore eventos remotos do navegador\n- **[Monitoramento de Rede](../network/monitoring.md)** - Rastreie requisições em navegadores remotos\n- **[Opções do Navegador](../configuration/browser-options.md)** - Configure navegadores locais antes de iniciar\n\n!!! tip \"Comece Local, Escale Remotamente\"\n    Desenvolva sua automação localmente com `browser.start()` para iterações rápidas, depois implante com `browser.connect()` para pipelines de CI/CD de produção e ambientes contêinerizados."
  },
  {
    "path": "docs/pt/features/automation/file-operations.md",
    "content": "# Operações com Arquivos\n\nUploads de arquivos são um dos aspectos mais desafiadores da automação de navegadores. Ferramentas tradicionais frequentemente têm dificuldades com as caixas de diálogo de arquivo do nível do sistema operacional, exigindo soluções complexas ou bibliotecas externas. O Pydoll oferece duas abordagens diretas para lidar com uploads de arquivos, cada uma adequada para cenários diferentes.\n\n## Métodos de Upload\n\nO Pydoll suporta dois métodos principais para uploads de arquivos:\n\n1.  **Entrada direta de arquivo** (`set_input_files()`): Rápido e direto, funciona com elementos `<input type=\"file\">`\n2.  **Gerenciador de contexto de seletor de arquivo** (`expect_file_chooser()`): Intercepta a caixa de diálogo de arquivo, funciona com qualquer gatilho de upload\n\n## Entrada Direta de Arquivo\n\nA abordagem mais simples é usar `set_input_files()` diretamente em elementos de entrada de arquivo. Este método é rápido, confiável e ignora totalmente a caixa de diálogo de arquivo do sistema operacional.\n\n### Uso Básico\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def direct_file_upload():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/upload')\n        \n        # Encontrar o elemento de entrada de arquivo\n        file_input = await tab.find(tag_name='input', type='file')\n        \n        # Definir o arquivo diretamente\n        file_path = Path('path/to/document.pdf')\n        await file_input.set_input_files(file_path)\n        \n        # Enviar o formulário\n        submit_button = await tab.find(id='submit-button')\n        await submit_button.click()\n        \n        print(\"Arquivo enviado com sucesso!\")\n\nasyncio.run(direct_file_upload())\n```\n\n!!! tip \"Path vs String\"\n    Embora objetos `Path` do `pathlib` sejam recomendados como melhor prática para melhor manipulação de caminhos e compatibilidade entre plataformas, você também pode usar strings simples, se preferir:\n    ```python\n    await file_input.set_input_files('path/to/document.pdf')  # Também funciona!\n    ```\n\n### Múltiplos Arquivos\n\nPara entradas que aceitam múltiplos arquivos (`<input type=\"file\" multiple>`), passe uma lista de caminhos de arquivo:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def upload_multiple_files():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/multi-upload')\n        \n        file_input = await tab.find(tag_name='input', type='file')\n        \n        # Fazer upload de múltiplos arquivos de uma vez\n        files = [\n            Path('documents/report.pdf'),\n            Path('images/screenshot.png'),\n            Path('data/results.csv')\n        ]\n        await file_input.set_input_files(files)\n        \n        # Processar normalmente\n        upload_btn = await tab.find(id='upload-btn')\n        await upload_btn.click()\n\nasyncio.run(upload_multiple_files())\n```\n\n### Resolução Dinâmica de Caminho\n\nObjetos `Path` facilitam a construção dinâmica de caminhos e lidam com a compatibilidade entre plataformas:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def upload_with_dynamic_paths():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/upload')\n        \n        file_input = await tab.find(tag_name='input', type='file')\n        \n        # Construir caminhos dinamicamente\n        project_dir = Path(__file__).parent\n        file_path = project_dir / 'uploads' / 'data.json'\n\n        await file_input.set_input_files(file_path)\n        # Ou usar o diretório home\n        user_file = Path.home() / 'Documents' / 'report.pdf'\n        await file_input.set_input_files(user_file)\n\nasyncio.run(upload_with_dynamic_paths())\n```\n\n!!! tip \"Quando Usar a Entrada Direta de Arquivo\"\n    Use `set_input_files()` quando:\n    \n    - O input de arquivo está diretamente acessível no DOM\n    - Você quer velocidade e simplicidade máximas\n    - O upload não dispara uma caixa de diálogo de seletor de arquivo\n    - Você está trabalhando com elementos `<input type=\"file\">` padrão\n\n## Gerenciador de Contexto de Seletor de Arquivo\n\nAlguns sites escondem o input de arquivo e usam botões personalizados ou áreas de arrastar e soltar que disparam a caixa de diálogo de seletor de arquivo do sistema operacional. Para esses casos, use o gerenciador de contexto `expect_file_chooser()`.\n\n### Como Funciona\n\nO gerenciador de contexto `expect_file_chooser()`:\n\n1.  Habilita a interceptação do seletor de arquivo\n2.  Espera a caixa de diálogo do seletor de arquivo abrir\n3.  Define automaticamente os arquivos quando a caixa de diálogo aparece\n4.  Limpa os recursos após a conclusão da operação\n\n### Uso Básico\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def file_chooser_upload():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/custom-upload')\n        \n        # Preparar o caminho do arquivo\n        file_path = Path.cwd() / 'document.pdf'\n        \n        # Usar gerenciador de contexto para lidar com o seletor de arquivo\n        async with tab.expect_file_chooser(files=file_path):\n            # Clicar no botão de upload personalizado\n            upload_button = await tab.find(class_name='custom-upload-btn')\n            await upload_button.click()\n            # O arquivo é definido automaticamente quando a caixa de diálogo abre\n        \n        # Continuar com sua automação\n        print(\"Arquivo selecionado via seletor!\")\n\nasyncio.run(file_chooser_upload())\n```\n\n### Múltiplos Arquivos com Seletor de Arquivo\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def multiple_files_chooser():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/gallery-upload')\n        \n        # Preparar múltiplos arquivos\n        photos_dir = Path.home() / 'photos'\n        files = [\n            photos_dir / 'img1.jpg',\n            photos_dir / 'img2.jpg',\n            photos_dir / 'img3.jpg'\n        ]\n        \n        async with tab.expect_file_chooser(files=files):\n            # Disparar upload via botão personalizado\n            add_photos_btn = await tab.find(text='Add Photos')\n            await add_photos_btn.click()\n        \n        print(f\"{len(files)} arquivos selecionados!\")\n\nasyncio.run(multiple_files_chooser())\n```\n\n### Seleção Dinâmica de Arquivos\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def dynamic_file_selection():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/batch-upload')\n        \n        # Encontrar todos os arquivos CSV em um diretório usando Path.glob()\n        data_dir = Path('data')\n        csv_files = list(data_dir.glob('*.csv'))\n        \n        async with tab.expect_file_chooser(files=csv_files):\n            upload_area = await tab.find(class_name='drop-zone')\n            await upload_area.click()\n        \n        print(f\"Selecionados {len(csv_files)} arquivos CSV\")\n\nasyncio.run(dynamic_file_selection())\n```\n\n!!! tip \"Quando Usar o Seletor de Arquivo\"\n    Use `expect_file_chooser()` quando:\n    \n    - O input de arquivo está oculto ou não diretamente acessível\n    - Botões personalizados disparam a caixa de diálogo do seletor de arquivo\n    - Trabalhando com áreas de upload de arrastar e soltar\n    - O site usa JavaScript para abrir caixas de diálogo de arquivo\n\n## Comparação: Direto vs Seletor de Arquivo\n\n| Característica | `set_input_files()` | `expect_file_chooser()` |\n|---|---|---|\n| **Velocidade** | ⚡ Instantâneo | 🕐 Espera pela caixa de diálogo |\n| **Complexidade** | Simples | Requer gerenciador de contexto |\n| **Requisitos** | Input de arquivo visível | Qualquer gatilho de upload |\n| **Caso de Uso** | Formulários padrão | UIs de upload personalizadas |\n| **Manejo de Eventos** | Não necessário | Usa eventos de página |\n\n## Exemplo Completo\n\nAqui está um exemplo abrangente combinando ambas as abordagens:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def comprehensive_upload_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/upload-form')\n        \n        # Cenário 1: Entrada direta para foto de perfil (arquivo único)\n        avatar_input = await tab.find(id='avatar-upload')\n        avatar_path = Path.home() / 'Pictures' / 'profile.jpg'\n        await avatar_input.set_input_files(avatar_path)\n        \n        # Esperar um pouco para a pré-visualização carregar\n        await asyncio.sleep(1)\n        \n        # Cenário 2: Seletor de arquivo para upload de documento\n        document_path = Path.cwd() / 'documents' / 'resume.pdf'\n        async with tab.expect_file_chooser(files=document_path):\n            # Botão estilizado personalizado que dispara o seletor de arquivo\n            upload_btn = await tab.find(class_name='btn-upload-document')\n            await upload_btn.click()\n        \n        # Esperar pela confirmação do upload\n        await asyncio.sleep(2)\n        \n        # Cenário 3: Múltiplos arquivos via seletor de arquivo\n        certs_dir = Path('certs')\n        certificates = [\n            certs_dir / 'certificate1.pdf',\n            certs_dir / 'certificate2.pdf',\n            certs_dir / 'certificate3.pdf'\n        ]\n        async with tab.expect_file_chooser(files=certificates):\n            add_certs_btn = await tab.find(text='Add Certificates')\n            await add_certs_btn.click()\n        \n        # Enviar o formulário completo\n        submit_button = await tab.find(type='submit')\n        await submit_button.click()\n        \n        # Esperar pela mensagem de sucesso\n        success_msg = await tab.find(class_name='success-message', timeout=10)\n        message_text = await success_msg.text\n        print(f\"Resultado do upload: {message_text}\")\n\nasyncio.run(comprehensive_upload_example())\n```\n\n!!! info \"Resumo dos Métodos\"\n    Este exemplo demonstra a flexibilidade do sistema de upload de arquivos do Pydoll:\n    \n    - **Arquivos únicos**: Passe `Path` ou `str` diretamente (não precisa de lista)\n    - **Múltiplos arquivos**: Passe uma lista de objetos `Path` ou `str`\n    - **Entrada direta**: Rápido para elementos `<input>` visíveis\n    - **Seletor de arquivo**: Funciona com botões de upload personalizados e inputs ocultos\n\n## Aprenda Mais\n\nPara um entendimento mais profundo dos mecanismos de upload de arquivos:\n\n- **[Sistema de Eventos](../advanced/event-system.md)**: Aprenda sobre os eventos de página usados pelo `expect_file_chooser()`\n- **[Análise Profunda: Domínio da Aba](../../deep-dive/tab-domain.md#file-chooser-handling)**: Detalhes técnicos sobre a interceptação do seletor de arquivo\n- **[Análise Profunda: Sistema de Eventos](../../deep-dive/event-system.md#file-chooser-events)**: Como os eventos do seletor de arquivo funcionam internamente\n\nAs operações com arquivos no Pydoll eliminam um dos maiores pontos problemáticos na automação de navegadores, fornecendo métodos limpos e confiáveis tanto para cenários de upload simples quanto complexos."
  },
  {
    "path": "docs/pt/features/automation/human-interactions.md",
    "content": "# Interações Semelhantes a Humanas\n\nUm dos principais diferenciais entre uma automação bem-sucedida e bots facilmente detectados é o quão realistas são as interações. O Pydoll fornece ferramentas sofisticadas para tornar sua automação virtualmente indistinguível do comportamento humano.\n\n!!! info \"Status das Funcionalidades\"\n    **Já Implementado:**\n\n    - **Teclado Humanizado**: Velocidade de digitação variável, erros realistas com correção automática (passe `humanize=True`)\n    - **Scroll Humanizado**: Rolagem baseada em física com momentum, fricção, jitter e overshoot (passe `humanize=True`)\n    - **Mouse Humanizado**: Trajetórias com curvas de Bezier, temporização pela Lei de Fitts, velocidade minimum-jerk, tremor e overshoot (passe `humanize=True`)\n\n    **Em Breve:**\n\n    - **Deslocamentos de clique aleatórios automáticos**: Parâmetro opcional para randomizar automaticamente as posições de clique dentro dos elementos\n    - **Comportamento de hover**: Atrasos e movimentos realistas ao passar o mouse sobre elementos\n\n## Por que Interações Semelhantes a Humanas Importam\n\nSites modernos empregam técnicas sofisticadas de detecção de bots:\n\n- **Análise de tempo de eventos**: Detectando ações impossivelmente rápidas ou perfeitamente cronometradas\n- **Rastreamento de movimento do mouse**: Identificando movimentos em linha reta ou teletransporte instantâneo\n- **Padrões de teclado**: Percebendo inserção de texto instantânea sem pressionamentos de tecla individuais\n- **Posições de clique**: Detectando cliques sempre no centro exato dos elementos\n- **Sequências de ação**: Identificando padrões não humanos no comportamento do usuário\n\nO Pydoll ajuda você a evitar a detecção, fornecendo métodos de interação realistas que imitam o comportamento real do usuário.\n\n## Movimento Realista do Mouse\n\nA API de Mouse (`tab.mouse`) fornece controle humanizado do cursor com múltiplas camadas de realismo. Quando `humanize=True`, os movimentos do mouse seguem trajetórias naturais com curvas de Bezier, temporização pela Lei de Fitts, perfis de velocidade minimum-jerk, tremor fisiológico e correção de overshoot.\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n\n    # Mover com trajetória curva natural\n    await tab.mouse.move(500, 300, humanize=True)\n\n    # Clicar com movimento, deslocamento e temporização realistas\n    await tab.mouse.click(500, 300, humanize=True)\n\n    # Arrastar com movimento natural\n    await tab.mouse.drag(100, 200, 500, 400, humanize=True)\n```\n\nTécnicas aplicadas durante operações humanizadas do mouse:\n\n- **Trajetórias com curvas de Bezier**: Trajetórias curvas com pontos de controle assimétricos (mais curvatura no início do movimento)\n- **Temporização pela Lei de Fitts**: A duração do movimento escala com a distância: `MT = a + b × log₂(D/W + 1)`\n- **Velocidade minimum-jerk**: Perfil de velocidade em forma de sino, início lento, pico no meio, fim lento\n- **Tremor fisiológico**: Ruído gaussiano (σ ≈ 1px) escalado inversamente com a velocidade\n- **Overshoot e correção**: ~70% de chance de ultrapassar movimentos rápidos em 3–12%, depois corrigir\n!!! info \"Documentação Dedicada de Controle do Mouse\"\n    Para documentação completa sobre controle do mouse, incluindo todos os métodos, configuração personalizada de temporização, rastreamento de posição e modo debug, veja **[Controle do Mouse](mouse-control.md)**.\n\n## Cliques Realistas\n\n### Clique Básico com Eventos de Mouse Simulados\n\nO método `click()` simula eventos reais de pressionar e soltar o mouse, diferentemente de cliques baseados em JavaScript:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def realistic_clicking():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        button = await tab.find(id=\"submit-button\")\n        \n        # Clique realista básico\n        await button.click()\n        \n        # O clique inclui:\n        # - Movimento do mouse até o elemento\n        # - Evento de pressionar o mouse\n        # - Tempo de espera (hold) configurável\n        # - Evento de soltar o mouse\n\nasyncio.run(realistic_clicking())\n```\n\n### Clique com Deslocamento de Posição (Offset)\n\nUsuários reais raramente clicam no centro exato dos elementos. Use deslocamentos para variar as posições dos cliques:\n\n!!! info \"Estado Atual: Cálculo Manual de Deslocamento\"\n    Atualmente, você deve calcular manualmente e randomizar os deslocamentos de clique para cada interação. Versões futuras incluirão um parâmetro opcional para randomizar automaticamente as posições de clique dentro dos limites do elemento.\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\nasync def click_with_offset():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/form')\n        \n        submit_button = await tab.find(tag_name=\"button\", type=\"submit\")\n        \n        # Clicar ligeiramente fora do centro (mais natural)\n        await submit_button.click(\n            x_offset=5,   # 5 pixels à direita do centro\n            y_offset=-3   # 3 pixels acima do centro\n        )\n        \n        # Atualmente: Varie manualmente o deslocamento para cada clique para parecer mais humano\n        for item in await tab.find(class_name=\"clickable-item\", find_all=True):\n            offset_x = random.randint(-10, 10)\n            offset_y = random.randint(-10, 10)\n            await item.click(x_offset=offset_x, y_offset=offset_y)\n            await asyncio.sleep(random.uniform(0.5, 2.0))\n\nasyncio.run(click_with_offset())\n```\n\n### Tempo de Espera (Hold) do Clique Ajustável\n\nVarie a duração do pressionamento do botão do mouse para simular diferentes estilos de clique:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def variable_hold_time():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        button = await tab.find(class_name=\"action-button\")\n        \n        # Clique rápido (padrão é 0.1s)\n        await button.click(hold_time=0.05)\n        \n        # Clique normal\n        await button.click(hold_time=0.1)\n        \n        # Clique mais lento e deliberado\n        await button.click(hold_time=0.2)\n        \n        # Simular hesitação do usuário\n        await asyncio.sleep(0.8)\n        await button.click(hold_time=0.15)\n\nasyncio.run(variable_hold_time())\n```\n\n### Quando Usar click() vs click_using_js()\n\nEntender a diferença é crucial para evitar detecção:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def click_methods_comparison():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        button = await tab.find(id=\"interactive-button\")\n        \n        # Método 1: click() - Simula eventos reais do mouse\n        # Dispara todos os eventos do mouse (mousedown, mouseup, click)\n        # Respeita o posicionamento do elemento\n        # Mais realista e mais difícil de detectar\n        # Requer que o elemento esteja visível e dentro da viewport\n        await button.click()\n        \n        # Método 2: click_using_js() - Usa JavaScript click()\n        # Funciona em elementos ocultos\n        # Execução mais rápida\n        # Contorna sobreposições visuais\n        # Pode ser detectado como automação\n        # Não dispara a mesma sequência de eventos de um usuário real\n        await button.click_using_js()\n\nasyncio.run(click_methods_comparison())\n```\n\n!!! tip \"Melhor Prática: Prefira Eventos do Mouse\"\n    Use `click()` para interações voltadas ao usuário para manter o realismo. Reserve `click_using_js()` para operações de backend, elementos ocultos, ou quando a velocidade é crítica e a detecção não é uma preocupação.\n\n## Entrada de Texto Realista\n\nA API de teclado do Pydoll fornece dois modos de digitação para equilibrar velocidade e furtividade.\n\n!!! info \"Entendendo os Modos de Digitação\"\n    | Modo | Parâmetros | Comportamento | Caso de Uso |\n    |------|------------|---------------|-------------|\n    | **Padrão (Rápido)** | `humanize=False` | Intervalos fixos de 50ms, sem erros | Cenários de velocidade, baixo risco (padrão) |\n    | **Humanizado** | `humanize=True` | Timing variável, ~2% de taxa de erros com correção automática | **Evasão anti-bot** |\n\n    O parâmetro `interval` está obsoleto. Passe `humanize=True` para digitação realista.\n\n### Digitação Natural com Humanização\n\nQuando `humanize=True` é passado, `type_text()` usa modo humanizado, simulando digitação humana realista com velocidades variáveis e erros ocasionais que são corrigidos automaticamente:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def natural_typing():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/login')\n        \n        username_field = await tab.find(id=\"username\")\n        password_field = await tab.find(id=\"password\")\n\n        # Velocidade variável: 30-120ms entre teclas\n        # ~2% de taxa de erros com comportamento de correção realista\n        await username_field.type_text(\"john.doe@example.com\", humanize=True)\n        await password_field.type_text(\"MyC0mpl3xP@ssw0rd!\", humanize=True)\n\nasyncio.run(natural_typing())\n```\n\n### Entrada Rápida para Campos Não Visíveis\n\nPara campos que não exigem realismo (como campos ocultos ou operações de backend), use `insert_text()`:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def fast_vs_realistic_input():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/form')\n        \n        # Digitação realista para campos visíveis\n        username = await tab.find(id=\"username\")\n        await username.click()\n        await username.type_text(\"john_doe\", interval=0.12)\n        \n        # Inserção rápida para campos ocultos ou de backend\n        hidden_field = await tab.find(id=\"hidden-token\")\n        await hidden_field.insert_text(\"very-long-generated-token-12345678\")\n        \n        # Digitação realista para campos que importam\n        comment = await tab.find(id=\"comment-box\")\n        await comment.click()\n        await comment.type_text(\"This looks like human input!\", interval=0.15)\n\nasyncio.run(fast_vs_realistic_input())\n```\n\n!!! info \"Controle Avançado de Teclado\"\n    Para documentação abrangente sobre controle de teclado, incluindo teclas especiais, combinações de teclas, modificadores e tabelas de referência completas de teclas, veja **[Controle de Teclado](keyboard-control.md)**.\n\n## Rolagem Realista da Página\n\nO Pydoll fornece uma API dedicada de scroll que aguarda a conclusão da rolagem antes de prosseguir, tornando suas automações mais realistas e confiáveis.\n\n!!! info \"Entendendo os Modos de Scroll\"\n    A API de scroll do Pydoll oferece **três modos distintos**:\n\n    | Modo | Parâmetros | Comportamento | Caso de Uso |\n    |------|------------|---------------|-------------|\n    | **Suave (Padrão)** | `smooth=True` | Animação CSS, previsível | Simulação de navegação geral (padrão) |\n    | **Humanizado** | `humanize=True` | Motor de física com momentum, jitter, overshoot | **Evasão anti-bot** |\n    | **Instantâneo** | `smooth=False` | Teletransporta para a posição imediatamente | Operações focadas em velocidade |\n\n    Passe `humanize=True` para rolagem humanizada baseada em física para evasão anti-bot.\n\n### Rolagem Básica por Direção\n\nUse o método `scroll.by()` para rolar a página em qualquer direção com controle preciso:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import ScrollPosition\n\nasync def basic_scrolling():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/long-page')\n        \n        # Humanizado - motor de física com curvas de Bezier\n        # Inclui: momentum, fricção, jitter, micro-pausas, overshoot\n        await tab.scroll.by(ScrollPosition.DOWN, 500, humanize=True)\n        await tab.scroll.by(ScrollPosition.UP, 300, humanize=True)\n\n        # Animação CSS - visual agradável mas timing previsível\n        await tab.scroll.by(ScrollPosition.DOWN, 500, humanize=False, smooth=True)\n\n        # Teletransporta instantaneamente - mais rápido mas facilmente detectável\n        await tab.scroll.by(ScrollPosition.DOWN, 1000, humanize=False, smooth=False)\n\nasyncio.run(basic_scrolling())\n```\n\n### Rolagem para Posições Específicas\n\nNavegue para o topo ou o final da página com controle sobre o realismo:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scroll_to_positions():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/article')\n        \n        # Ler o início do artigo\n        await asyncio.sleep(2.0)\n        \n        # Scroll humanizado (motor de física, evasão anti-bot)\n        await tab.scroll.to_bottom(humanize=True)\n        await asyncio.sleep(1.5)\n        await tab.scroll.to_top(humanize=True)\n\n        # Scroll suave CSS (animação previsível)\n        await tab.scroll.to_bottom(humanize=False, smooth=True)\n        await asyncio.sleep(1.5)\n        await tab.scroll.to_top(humanize=False, smooth=True)\n\nasyncio.run(scroll_to_positions())\n```\n\n!!! tip \"Escolhendo o Modo Certo\"\n    - **`humanize=True`**: Melhor para evasão anti-bot\n    - **Padrão** (`smooth=True`): Bom para demos, screenshots e automação geral\n    - **`smooth=False`**: Velocidade máxima quando a furtividade não é uma preocupação\n\n### Padrões de Rolagem Semelhantes a Humanos\n\nO motor de scroll do Pydoll usa **Curvas de Bezier Cúbicas** para simular a física da rolagem humana. Isso inclui:\n\n- **Momentum**: Explosão inicial de velocidade seguida de desaceleração gradual.\n- **Fricção**: Desaceleração natural baseada em \"resistência física\".\n- **Micro-pausas**: Breves paradas durante scrolls longos, imitando leitura ou movimento dos olhos.\n- **Overshoot**: Rolagem ocasional além do alvo e correção de volta.\n\nEste comportamento é automaticamente habilitado quando você usa `humanize=True`.\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import ScrollPosition\n\nasync def human_like_scrolling():\n    \"\"\"Simular padrões de rolagem naturais ao ler um artigo.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/article')\n        \n        # Usuário começa a ler do topo\n        await asyncio.sleep(random.uniform(2.0, 4.0))\n        \n        # Rolar gradualmente enquanto lê\n        # O motor de scroll cuida da física (aceleração/desaceleração)\n        for _ in range(random.randint(5, 8)):\n            # Distâncias de rolagem variadas (simula velocidade de leitura)\n            scroll_distance = random.randint(300, 600)\n            await tab.scroll.by(\n                ScrollPosition.DOWN, \n                scroll_distance, \n                humanize=True  # Habilita física com curvas de Bezier\n            )\n            \n            # Pausar para \"ler\" o conteúdo\n            await asyncio.sleep(random.uniform(2.0, 5.0))\n        \n        # Scroll rápido para verificar o final\n        await tab.scroll.to_bottom(humanize=True)\n        await asyncio.sleep(random.uniform(1.0, 2.0))\n        \n        # Voltar ao topo para reler algo\n        await tab.scroll.to_top(humanize=True)\n\nasyncio.run(human_like_scrolling())\n```\n\n### Rolando Elementos para a Visão\n\nUse `scroll_into_view()` para garantir que elementos estejam visíveis antes de capturar screenshots da página:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scroll_for_screenshots():\n    \"\"\"Rolar elementos para a visão antes de capturar screenshots da página.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/product')\n        \n        # Rolar para seção de preços antes de tirar screenshot da página completa\n        pricing_section = await tab.find(id=\"pricing\")\n        await pricing_section.scroll_into_view()\n        await tab.take_screenshot(path=\"page_with_pricing.png\")\n        \n        # Rolar para seção de avaliações antes do screenshot\n        reviews = await tab.find(class_name=\"reviews\")\n        await reviews.scroll_into_view()\n        await tab.take_screenshot(path=\"page_with_reviews.png\")\n        \n        # Rolar para rodapé para capturar estado completo da página\n        footer = await tab.find(tag_name=\"footer\")\n        await footer.scroll_into_view()\n        await tab.take_screenshot(path=\"page_with_footer.png\")\n        \n        # Nota: click() já rola automaticamente, então não é necessário:\n        # await button.scroll_into_view()  # Desnecessário!\n        # await button.click()  # Isso já rola o botão para a visão\n\nasyncio.run(scroll_for_screenshots())\n```\n\n### Detectando Conteúdo de Scroll Infinito\n\nImplemente padrões de rolagem para carregar conteúdo lazy-loaded:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import ScrollPosition\n\nasync def infinite_scroll_loading():\n    \"\"\"Carregar conteúdo em páginas com scroll infinito.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/feed')\n        \n        items_loaded = 0\n        max_scrolls = 10\n        \n        for scroll_num in range(max_scrolls):\n            # Rolar até o final para acionar carregamento\n            await tab.scroll.to_bottom(smooth=True)\n            \n            # Aguardar o conteúdo carregar\n            await asyncio.sleep(random.uniform(2.0, 3.0))\n            \n            # Verificar se novos itens foram carregados\n            items = await tab.find(class_name=\"feed-item\", find_all=True)\n            new_count = len(items)\n            \n            if new_count == items_loaded:\n                print(\"Sem mais conteúdo para carregar\")\n                break\n            \n            items_loaded = new_count\n            print(f\"Rolagem {scroll_num + 1}: {items_loaded} itens carregados\")\n            \n            # Pequena rolagem para cima (comportamento humano)\n            if random.random() > 0.7:\n                await tab.scroll.by(ScrollPosition.UP, 200, smooth=True)\n                await asyncio.sleep(random.uniform(0.5, 1.0))\n\nasyncio.run(infinite_scroll_loading())\n```\n\n!!! success \"Aguarda Automático da Conclusão\"\n    Diferentemente de `execute_script(\"window.scrollBy(...)\")` que retorna imediatamente, a API `scroll` usa o parâmetro `awaitPromise` do CDP para aguardar o evento `scrollend` do navegador. Isso garante que suas ações subsequentes só executem após a rolagem terminar completamente.\n\n## Combinando Técnicas para Máximo Realismo\n\n### Exemplo Completo de Preenchimento de Formulário\n\nAqui está um exemplo abrangente combinando todas as técnicas de interação semelhantes a humanas. **Isso demonstra a abordagem manual atual** para alcançar o máximo realismo. Versões futuras automatizarão muito dessa aleatorização:\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import Key\n\nasync def human_like_form_filling():\n    \"\"\"Preencher um formulário com máximo realismo para evitar detecção.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/registration')\n        \n        # Esperar um pouco (usuário lendo a página)\n        await asyncio.sleep(random.uniform(1.5, 3.0))\n        \n        # Preencher o primeiro nome com velocidade de digitação variável\n        first_name = await tab.find(id=\"first-name\")\n        await first_name.click(\n            x_offset=random.randint(-5, 5),\n            y_offset=random.randint(-5, 5)\n        )\n        await asyncio.sleep(random.uniform(0.2, 0.5))\n        \n        # Digitação manual caractere por caractere com atrasos aleatórios\n        # (Isso será automatizado em versões futuras)\n        name_text = \"John\"\n        for char in name_text:\n            await first_name.type_text(char, interval=0)\n            await asyncio.sleep(random.uniform(0.08, 0.22))\n        \n        # Tab para o próximo campo\n        await asyncio.sleep(random.uniform(0.3, 0.8))\n        await first_name.press_keyboard_key(Key.TAB)\n        \n        # Preencher o sobrenome\n        await asyncio.sleep(random.uniform(0.2, 0.5))\n        last_name = await tab.find(id=\"last-name\")\n        await last_name.type_text(\"Doe\", interval=random.uniform(0.1, 0.18))\n        \n        # Tab para o email\n        await asyncio.sleep(random.uniform(0.4, 1.0))\n        await last_name.press_keyboard_key(Key.TAB)\n        \n        # Preencher email com pausas realistas\n        await asyncio.sleep(random.uniform(0.2, 0.5))\n        email = await tab.find(id=\"email\")\n        \n        email_text = \"john.doe@example.com\"\n        for i, char in enumerate(email_text):\n            await email.type_text(char, interval=0)\n            # Pausa mais longa nos símbolos @ e . (natural)\n            if char in ['@', '.']:\n                await asyncio.sleep(random.uniform(0.2, 0.4))\n            else:\n                await asyncio.sleep(random.uniform(0.08, 0.2))\n        \n        # Simular usuário revisando o que digitou\n        await asyncio.sleep(random.uniform(1.0, 2.5))\n        \n        # Aceitar checkbox de termos com deslocamento\n        terms_checkbox = await tab.find(id=\"accept-terms\")\n        await terms_checkbox.click(\n            x_offset=random.randint(-3, 3),\n            y_offset=random.randint(-3, 3),\n            hold_time=random.uniform(0.08, 0.15)\n        )\n        \n        # Pausar antes de enviar (usuário revisando formulário)\n        await asyncio.sleep(random.uniform(1.5, 3.0))\n        \n        # Clicar em enviar com parâmetros realistas\n        submit_button = await tab.find(tag_name=\"button\", type=\"submit\")\n        await submit_button.click(\n            x_offset=random.randint(-8, 8),\n            y_offset=random.randint(-5, 5),\n            hold_time=random.uniform(0.1, 0.2)\n        )\n        \n        print(\"Formulário enviado com comportamento semelhante ao humano\")\n\nasyncio.run(human_like_form_filling())\n```\n\n## Melhores Práticas para Evitar Detecção\n\n!!! tip \"Aleatorização Manual Atualmente Necessária\"\n    As seguintes melhores práticas representam o **estado atual do Pydoll**, onde você deve implementar a aleatorização manualmente. Embora isso exija mais código, oferece um controle refinado sobre o comportamento. Versões futuras automatizarão esses padrões, mantendo o mesmo nível de realismo.\n\n### 1. Sempre Adicione Atrasos Aleatórios\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\n# Ruim: Tempo previsível\nawait element1.click()\nawait element2.click()\nawait element3.click()\n\n# Bom: Tempo variável (atualmente necessário)\nawait element1.click()\nawait asyncio.sleep(random.uniform(0.5, 1.5))\nawait element2.click()\nawait asyncio.sleep(random.uniform(0.8, 2.0))\nawait element3.click()\n```\n\n### 2. Varie as Posições dos Cliques\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\n# Ruim: Sempre clica no centro\nfor button in buttons:\n    await button.click()\n\n# Bom: Posições variadas (atualmente manual)\nfor button in buttons:\n    await button.click(\n        x_offset=random.randint(-10, 10),\n        y_offset=random.randint(-10, 10)\n    )\n```\n\n### 3. Simule Comportamento Natural do Usuário\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\nasync def natural_user_simulation(tab):\n    # Usuário chega na página\n    await tab.go_to('https://example.com')\n    \n    # Usuário lê o conteúdo da página (1-3 segundos)\n    await asyncio.sleep(random.uniform(1.0, 3.0))\n    \n    # Usuário rola para baixo para ver mais\n    await tab.scroll.by(ScrollPosition.DOWN, 300, smooth=True)\n    await asyncio.sleep(random.uniform(0.5, 1.5))\n    \n    # Usuário encontra e clica no botão\n    button = await tab.find(class_name=\"cta-button\")\n    await button.click(\n        x_offset=random.randint(-5, 5),\n        y_offset=random.randint(-5, 5)\n    )\n    \n    # Usuário espera o conteúdo carregar\n    await asyncio.sleep(random.uniform(0.8, 1.5))\n```\n\n### 4. Combine Múltiplas Técnicas\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\nasync def advanced_stealth_automation():\n    \"\"\"Combinar múltiplas técnicas para máxima furtividade.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Usar espera de carregamento de página semelhante à humana\n        await tab.go_to('https://example.com/sensitive-page')\n        await asyncio.sleep(random.uniform(2.0, 4.0))\n        \n        # Rolar realisticamente com a API dedicada\n        for _ in range(random.randint(2, 4)):\n            scroll_amount = random.randint(200, 500)\n            await tab.scroll.by(ScrollPosition.DOWN, scroll_amount, smooth=True)\n            await asyncio.sleep(random.uniform(0.8, 2.0))\n        \n        # Encontrar elemento com timeout (simulando busca do usuário)\n        target = await tab.find(\n            class_name=\"target-element\",\n            timeout=random.randint(3, 7)\n        )\n        \n        # Clicar com todos os parâmetros realistas\n        await target.click(\n            x_offset=random.randint(-12, 12),\n            y_offset=random.randint(-8, 8),\n            hold_time=random.uniform(0.09, 0.18)\n        )\n        \n        # Tempo de reação humano\n        await asyncio.sleep(random.uniform(0.5, 1.2))\n\nasyncio.run(advanced_stealth_automation())\n```\n\n## Trocas entre Desempenho e Realismo\n\nÀs vezes, você precisa equilibrar velocidade com realismo:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def balanced_automation():\n    \"\"\"Escolher o nível de realismo apropriado com base no contexto.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/scraping-target')\n        \n        # Fase 1: Interação inicial (alto realismo)\n        # É quando os sistemas de detecção estão mais ativos\n        login_button = await tab.find(text=\"Login\")\n        await asyncio.sleep(random.uniform(1.0, 2.0))\n        await login_button.click(\n            x_offset=random.randint(-5, 5),\n            y_offset=random.randint(-5, 5)\n        )\n        \n        await asyncio.sleep(random.uniform(0.5, 1.0))\n        \n        username = await tab.find(id=\"username\")\n        await username.type_text(\"user@example.com\", interval=0.12)\n        \n        await asyncio.sleep(random.uniform(0.3, 0.7))\n        \n        password = await tab.find(id=\"password\")\n        await password.type_text(\"password123\", interval=0.10)\n        \n        submit = await tab.find(type=\"submit\")\n        await asyncio.sleep(random.uniform(0.8, 1.5))\n        await submit.click()\n        \n        # Fase 2: Extração de dados autenticada (menos realismo, mais velocidade)\n        # Menos escrutínio após autenticação bem-sucedida\n        await asyncio.sleep(2)\n        \n        # Navegação rápida pelas páginas\n        items = await tab.find(class_name=\"data-item\", find_all=True)\n        \n        for item in items:\n            # Clique rápido sem deslocamentos\n            await item.click_using_js()\n            await asyncio.sleep(0.3)  # Atraso mínimo\n            \n            # Extrair dados\n            title = await tab.find(class_name=\"title\")\n            data = await title.text\n            \n            # Navegação rápida\n            await tab.execute_script(\"window.history.back()\")\n            await asyncio.sleep(0.5)\n\nasyncio.run(balanced_automation())\n```\n\n## Monitorando e Ajustando\n\nTeste o realismo da sua automação:\n\n```python\nimport asyncio\nimport random\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def test_interaction_timing():\n    \"\"\"Registrar tempos para garantir padrões realistas.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/test-page')\n        \n        # Medir e registrar o tempo de interação\n        elements = await tab.find(class_name=\"clickable\", find_all=True)\n        \n        timings = []\n        last_time = time.time()\n        \n        for i, element in enumerate(elements):\n            await element.click(\n                x_offset=random.randint(-8, 8),\n                y_offset=random.randint(-8, 8)\n            )\n            \n            current_time = time.time()\n            elapsed = current_time - last_time\n            timings.append(elapsed)\n            \n            print(f\"Clique {i+1}: {elapsed:.3f}s desde a última ação\")\n            last_time = current_time\n            \n            await asyncio.sleep(random.uniform(0.5, 2.0))\n        \n        # Analisar distribuição de tempo\n        avg_time = sum(timings) / len(timings)\n        print(f\"\\nTempo médio entre ações: {avg_time:.3f}s\")\n        print(f\"Min: {min(timings):.3f}s, Max: {max(timings):.3f}s\")\n        \n        # Bom: Tempo variável com média realista (1-2 segundos)\n        # Ruim: Tempo constante ou irrealisticamente rápido (<0.1s)\n\nasyncio.run(test_interaction_timing())\n```\n\n## Aprenda Mais\n\nPara mais informações sobre métodos de interação com elementos:\n\n- **[Localização de Elementos](../element-finding.md)**: Localize elementos para interagir\n- **[Domínio WebElement](../../deep-dive/webelement-domain.md)**: Análise profunda das capacidades do WebElement\n- **[Operações com Arquivos](file-operations.md)**: Faça upload de arquivos e lide com downloads\n\nDomine as interações semelhantes a humanas, e sua automação será mais confiável, mais difícil de detectar e espelhará mais de perto o comportamento real do usuário."
  },
  {
    "path": "docs/pt/features/automation/iframes.md",
    "content": "# Trabalhando com IFrames\n\nPáginas modernas usam `<iframe>` para embutir outros documentos. Nas versões antigas do Pydoll era necessário transformar o iframe em uma `Tab` com `tab.get_frame()` e cuidar de alvos CDP manualmente. **Isso acabou.**  \nAgora um iframe se comporta como qualquer outro `WebElement`: você pode chamar `find()`, `query()`, `execute_script()`, `inner_html`, `text` e todos os utilitários diretamente — o Pydoll encaminha a operação para o contexto correto em qualquer domínio.\n\n!!! info \"Modelo mental simples\"\n    Pense no iframe como mais uma `div`. Localize o elemento, guarde a referência e continue a navegação a partir dele. O Pydoll se encarrega de criar o mundo isolado, configurar o contexto JavaScript e lidar com iframes aninhados automaticamente.\n\n## Guia rápido\n\n### Interagir com o primeiro iframe da página\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def interagir_iframe():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/page-with-iframe')\n\n        iframe = await tab.find(tag_name='iframe', id='content-frame')\n\n        # As chamadas abaixo já executam dentro do iframe\n        title = await iframe.find(tag_name='h1')\n        await title.click()\n\n        form = await iframe.find(id='login-form')\n        username = await form.find(name='username')\n        await username.type_text('john_doe')\n\nasyncio.run(interagir_iframe())\n```\n\n### Iframes aninhados\n\nBasta encadear as buscas:\n\n```python\nouter = await tab.find(id='outer-frame')\ninner = await outer.find(tag_name='iframe')  # procura dentro do primeiro iframe\n\nsubmit_button = await inner.find(id='submit')\nawait submit_button.click()\n```\n\nO fluxo é sempre o mesmo:\n\n1. Localize o iframe desejado.\n2. Use esse `WebElement` como escopo das próximas buscas.\n3. Repita para níveis mais profundos, se necessário.\n\n### Executar JavaScript dentro do iframe\n\n```python\niframe = await tab.find(tag_name='iframe')\nresult = await iframe.execute_script('return document.title', return_by_value=True)\nprint(result['result']['result']['value'])\n```\n\nO Pydoll garante que o script rode no contexto isolado do iframe, inclusive em frames cross-origin.\n\n## Por que ficou melhor?\n\n- **Intuitivo:** você programa exatamente o que vê na árvore DOM.\n- **Sem dor de cabeça com CDP:** mundos isolados e targets são configurados automaticamente.\n- **Suporte nativo a aninhamento:** cada busca é relativa ao elemento atual; hierarquias profundas continuam legíveis.\n- **Uma única API:** não é preciso alternar entre métodos de `Tab` e de `WebElement`.\n\n!!! tip \"Aviso de descontinuação\"\n    `Tab.get_frame()` agora emite `DeprecationWarning` e será removido em uma versão futura. Atualize seus scripts para usar o iframe diretamente, como mostrado acima.\n\n## Padrões comuns\n\n### Capturar imagem de conteúdo dentro do iframe\n\n```python\niframe = await tab.find(tag_name='iframe')\nchart = await iframe.find(id='sales-chart')\nawait chart.take_screenshot('chart.png')\n```\n\n### Iterar sobre vários iframes\n\n```python\niframes = await tab.find(tag_name='iframe', find_all=True)\nfor frame in iframes:\n    heading = await frame.find(tag_name='h2')\n    print(await heading.text)\n```\n\n### Aguardar até que um iframe esteja pronto\n\n```python\niframe = await tab.find(tag_name='iframe')\nawait iframe.wait_until(is_visible=True, timeout=10)\nbanner = await iframe.find(id='promo-banner')\n```\n\n## Seletores que Cruzam IFrames\n\nEm vez de localizar manualmente cada iframe e depois buscar dentro dele, você pode escrever um **único seletor** que cruza as fronteiras do iframe. O Pydoll detecta automaticamente os passos `iframe` no seu XPath ou CSS, divide em segmentos e percorre a cadeia de iframes por você.\n\n### Seletores CSS\n\nUse qualquer combinador padrão (`>`, espaço) após um composto `iframe`:\n\n```python\n# Cruzamento de um único iframe\nbutton = await tab.query('iframe > .submit-btn')\n\n# Com seletores de atributo no iframe\nbutton = await tab.query('iframe[src*=\"checkout\"] > #pay-button')\n\n# Iframes aninhados\nelement = await tab.query('iframe.outer > iframe.inner > div.content')\n\n# Múltiplos passos após o iframe\nlink = await tab.query('iframe > nav > a.home-link')\n\n# Iframe dentro de outro elemento (não na raiz)\nbutton = await tab.query('div > iframe > button.submit')\ncontent = await tab.query('.wrapper iframe > div.content')\n```\n\n### Expressões XPath\n\nUse `/` após um passo `iframe` — o Pydoll divide no nó do iframe:\n\n```python\n# Cruzamento de um único iframe\nbutton = await tab.query('//iframe/body/button[@id=\"submit\"]')\n\n# Iframe dentro de outro elemento (não na raiz)\ndiv = await tab.query('//div/iframe/div')\nitem = await tab.query('//div[@class=\"wrapper\"]/iframe/body/div')\n\n# Com predicados no iframe\nheading = await tab.query('//iframe[@src*=\"cloudflare\"]//h1')\n\n# Iframes aninhados\nelement = await tab.query('//iframe[@id=\"outer\"]//iframe[@id=\"inner\"]//div')\n```\n\n### Como funciona\n\nQuando o Pydoll encontra um seletor como `iframe[src*=\"checkout\"] > form > button`:\n\n1. **Analisa** o seletor em segmentos: `iframe[src*=\"checkout\"]` e `form > button`\n2. **Encontra** o elemento iframe usando o primeiro segmento\n3. **Busca dentro** do iframe usando o segundo segmento\n4. Para iframes aninhados, repete o processo em cada fronteira\n\nIsso equivale à abordagem manual, mas em uma única chamada:\n\n```python\n# Manual (continua funcionando)\niframe = await tab.find(tag_name='iframe', src='*checkout*')\nbutton = await iframe.query('form > button')\n\n# Automático (mesmo resultado, uma linha)\nbutton = await tab.query('iframe[src*=\"checkout\"] > form > button')\n```\n\n### Quando a divisão NÃO acontece\n\nSeletores só são divididos quando `iframe` aparece como **nome de tag**. Estes seletores passam inalterados:\n\n- `.iframe > body` — seletor de classe, não de tag\n- `#iframe > body` — seletor de ID\n- `div.iframe > body` — tag é `div`, não `iframe`\n- `[data-type=\"iframe\"] > body` — seletor de atributo\n- `iframe` ou `//iframe` — sem conteúdo após o iframe (nada para buscar dentro)\n\n### Suporte a find_all\n\nO último segmento respeita `find_all=True`, retornando todos os elementos correspondentes dentro do iframe final:\n\n```python\n# Obter todos os links dentro de um iframe\nlinks = await tab.query('iframe > a', find_all=True)\n```\n\n## Boas práticas\n\n- **Use o iframe como escopo:** prefira chamar `find`, `query` e derivados diretamente nele.\n- **Evite `tab.find` para elementos internos:** ele só enxerga o documento principal.\n- **Guarde referências úteis:** o contexto é cacheado pelo Pydoll.\n- **Continue aplicando os mesmos fluxos:** rolagem, screenshots, waits, scripts, atributos e texto funcionam igual a qualquer outro elemento.\n\n## Leituras recomendadas\n\n- **[Busca de Elementos](../element-finding.md)** – explica buscas encadeadas e escopos.\n- **[Capturas e PDFs](screenshots-and-pdfs.md)** – detalhes sobre captura de tela.\n- **[Event System](../advanced/event-system.md)** – monitore eventos de forma reativa (inclusive de iframes).\n\nCom o novo fluxo, iframes deixam de ser um caso especial: são apenas mais um nó na árvore DOM. Concentre-se na lógica da automação; o Pydoll cuida da parte difícil para você.\n"
  },
  {
    "path": "docs/pt/features/automation/keyboard-control.md",
    "content": "# Controle de Teclado\n\nA API de Teclado fornece controle completo sobre a entrada de teclado no nível da página, permitindo que você simule digitação realista, execute atalhos e controle sequências complexas de teclas. Diferente dos métodos de teclado em nível de elemento, a API de Teclado opera globalmente na página, dando a você a flexibilidade de interagir com qualquer elemento focado ou acionar ações de teclado em nível de página.\n\n!!! info \"Interface de Teclado Centralizada\"\n    Todas as operações de teclado são acessíveis via `tab.keyboard`, fornecendo uma API limpa e unificada para todas as interações de teclado.\n\n!!! warning \"Limitação Importante do CDP: Atalhos de UI do Navegador Não Funcionam\"\n    **Problema Conhecido**: Eventos injetados via Chrome DevTools Protocol são marcados como \"não confiáveis\" e **não** acionam ações da UI do navegador ou criam gestos de usuário.\n    \n    **O que NÃO funciona:**\n\n    - Atalhos do navegador (Ctrl+T, Ctrl+W, Ctrl+N)\n    - Atalhos de DevTools (F12, Ctrl+Shift+I)\n    - Navegação do navegador (Ctrl+Shift+T para reabrir abas)\n    - Qualquer atalho que modifica a UI ou janelas do navegador\n    \n    **O que funciona perfeitamente:**\n\n    - Atalhos em nível de página (Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+F)\n    - Seleção e manipulação de texto\n    - Navegação em formulários (Tab, Enter, teclas de seta)\n    - Interações com campos de entrada\n    - Atalhos personalizados de aplicações (em web apps)\n    \n    **Razão técnica**: Eventos CDP não criam \"gestos de usuário\" necessários pela segurança do navegador. Veja [chromium issue #615341](https://bugs.chromium.org/p/chromium/issues/detail?id=615341) e [documentação CDP](https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent).\n    \n    Para automação em nível de navegador, use comandos CDP do navegador diretamente (como `tab.close()`, `browser.new_tab()`) ao invés de atalhos de teclado.\n\n## Início Rápido\n\nA API de Teclado fornece três métodos principais:\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.constants import Key\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n    \n    # Pressionar e soltar uma tecla\n    await tab.keyboard.press(Key.ENTER)\n    \n    # Executar uma combinação de atalho\n    await tab.keyboard.hotkey(Key.CONTROL, Key.S)  # Ctrl+S\n    \n    # Controle manual\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.ARROWRIGHT)\n    await tab.keyboard.up(Key.SHIFT)\n```\n\n## Métodos Principais\n\n### Press: Ação Completa de Tecla\n\nO método `press()` executa um ciclo completo de pressionamento de tecla (pressionar → aguardar → soltar):\n\n```python\nfrom pydoll.constants import Key\n\n# Pressionamento básico de tecla\nawait tab.keyboard.press(Key.ENTER)\nawait tab.keyboard.press(Key.TAB)\nawait tab.keyboard.press(Key.ESCAPE)\n\n# Pressionar com modificadores\nawait tab.keyboard.press(Key.S, modifiers=2)  # Ctrl+S (modificador manual)\n\n# Duração personalizada de manutenção\nawait tab.keyboard.press(Key.SPACE, interval=0.5)  # Manter por 500ms\n```\n\n**Parâmetros:**\n\n- `key`: Tecla a ser pressionada (do enum `Key`)\n- `modifiers` (opcional): Flags de modificadores (Alt=1, Ctrl=2, Meta=4, Shift=8)\n- `interval` (opcional): Duração para manter a tecla em segundos (padrão: 0.1)\n\n### Down: Pressionar Tecla Sem Soltar\n\nO método `down()` pressiona uma tecla sem soltá-la, útil para manter modificadores ou criar sequências de teclas:\n\n```python\nfrom pydoll.constants import Key\n\n# Manter Shift enquanto pressiona outras teclas\nawait tab.keyboard.down(Key.SHIFT)\nawait tab.keyboard.press(Key.ARROWRIGHT)  # Selecionar texto\nawait tab.keyboard.press(Key.ARROWRIGHT)  # Continuar selecionando\nawait tab.keyboard.up(Key.SHIFT)\n\n# Pressionar com flags de modificador\nawait tab.keyboard.down(Key.A, modifiers=2)  # Ctrl+A (selecionar tudo)\n```\n\n**Parâmetros:**\n- `key`: Tecla a ser pressionada\n- `modifiers` (opcional): Flags de modificadores a aplicar\n\n### Up: Soltar uma Tecla\n\nO método `up()` solta uma tecla previamente pressionada:\n\n```python\nfrom pydoll.constants import Key\n\n# Sequência manual de teclas\nawait tab.keyboard.down(Key.CONTROL)\nawait tab.keyboard.down(Key.SHIFT)\nawait tab.keyboard.press(Key.T)  # Ctrl+Shift+T\nawait tab.keyboard.up(Key.SHIFT)\nawait tab.keyboard.up(Key.CONTROL)\n```\n\n**Parâmetros:**\n- `key`: Tecla a ser solta\n\n!!! tip \"Quando Usar Cada Método\"\n\n    - **`press()`**: Ações de tecla única (Enter, Tab, letras)\n    - **`hotkey()`**: Atalhos de teclado (Ctrl+C, Ctrl+Shift+T)\n    - **`down()`/`up()`**: Sequências complexas, manter modificadores, temporização personalizada\n\n## Hotkeys: Atalhos de Teclado Simplificados\n\nO método `hotkey()` detecta automaticamente teclas modificadoras e executa atalhos corretamente:\n\n### Hotkeys Básicos\n\n```python\nfrom pydoll.constants import Key\n\n# Atalhos comuns\nawait tab.keyboard.hotkey(Key.CONTROL, Key.C)  # Copiar\nawait tab.keyboard.hotkey(Key.CONTROL, Key.V)  # Colar\nawait tab.keyboard.hotkey(Key.CONTROL, Key.X)  # Recortar\nawait tab.keyboard.hotkey(Key.CONTROL, Key.Z)  # Desfazer\nawait tab.keyboard.hotkey(Key.CONTROL, Key.Y)  # Refazer\nawait tab.keyboard.hotkey(Key.CONTROL, Key.A)  # Selecionar tudo\nawait tab.keyboard.hotkey(Key.CONTROL, Key.S)  # Salvar\n\n```\n\n### Combinações de Três Teclas\n\n```python\nfrom pydoll.constants import Key\n\n# Atalhos de edição de texto (estes funcionam!)\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.ARROWLEFT)  # Selecionar palavra à esquerda\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.ARROWRIGHT)  # Selecionar palavra à direita\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.HOME)  # Selecionar até o início do documento\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.END)  # Selecionar até o fim do documento\n\n# Atalhos específicos de aplicação (se suportados pelo web app)\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.Z)  # Refazer em muitos apps\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.S)  # Salvar Como (se o app suportar)\n```\n\n### Atalhos Específicos de Plataforma\n\n```python\nimport sys\nfrom pydoll.constants import Key\n\n# Usar Meta (Command) no macOS, Control no Windows/Linux\nmodifier = Key.META if sys.platform == 'darwin' else Key.CONTROL\n\nawait tab.keyboard.hotkey(modifier, Key.C)  # Copiar (consciente da plataforma)\nawait tab.keyboard.hotkey(modifier, Key.V)  # Colar (consciente da plataforma)\n```\n\n### Como Funcionam os Hotkeys\n\nO método `hotkey()` lida inteligentemente com teclas modificadoras:\n\n1. **Detecta modificadores**: Identifica automaticamente Ctrl, Shift, Alt, Meta\n2. **Calcula flags**: Combina modificadores usando OR bit a bit (Ctrl=2, Shift=8 → 10)\n3. **Aplica corretamente**: Pressiona teclas não-modificadoras com flags de modificador aplicadas\n4. **Liberação limpa**: Solta teclas em ordem reversa\n\n```python\nfrom pydoll.constants import Key\n\n# Nos bastidores para hotkey(Key.CONTROL, Key.SHIFT, Key.T):\n# 1. Detecta: modifiers=[CONTROL, SHIFT], keys=[T]\n# 2. Calcula: modifier_value = 2 | 8 = 10\n# 3. Executa: pressiona T com modifiers=10\n# 4. Libera: solta T\n```\n\n!!! tip \"Valores de Modificador\"\n    Ao usar o parâmetro `modifiers` manualmente:\n\n    - Alt = 1\n    - Ctrl = 2\n    - Meta/Command = 4\n    - Shift = 8\n    \n    Combine-os: Ctrl+Shift = 2 + 8 = 10\n\n## Teclas Disponíveis\n\nO enum `Key` fornece cobertura abrangente do teclado:\n\n### Teclas de Letras (A-Z)\n\n```python\nfrom pydoll.constants import Key\n\n# Todas as letras A a Z\nawait tab.keyboard.press(Key.A)\nawait tab.keyboard.press(Key.Z)\n```\n\n### Teclas Numéricas\n\n```python\nfrom pydoll.constants import Key\n\n# Números da linha superior (0-9)\nawait tab.keyboard.press(Key.DIGIT0)\nawait tab.keyboard.press(Key.DIGIT9)\n\n# Números do teclado numérico\nawait tab.keyboard.press(Key.NUMPAD0)\nawait tab.keyboard.press(Key.NUMPAD9)\n```\n\n### Teclas de Função\n\n```python\nfrom pydoll.constants import Key\n\n# F1 até F12\nawait tab.keyboard.press(Key.F1)\nawait tab.keyboard.press(Key.F12)\n```\n\n### Teclas de Navegação\n\n```python\nfrom pydoll.constants import Key\n\nawait tab.keyboard.press(Key.ARROWUP)\nawait tab.keyboard.press(Key.ARROWDOWN)\nawait tab.keyboard.press(Key.ARROWLEFT)\nawait tab.keyboard.press(Key.ARROWRIGHT)\nawait tab.keyboard.press(Key.HOME)\nawait tab.keyboard.press(Key.END)\nawait tab.keyboard.press(Key.PAGEUP)\nawait tab.keyboard.press(Key.PAGEDOWN)\n```\n\n### Teclas Modificadoras\n\n```python\nfrom pydoll.constants import Key\n\nawait tab.keyboard.press(Key.CONTROL)\nawait tab.keyboard.press(Key.SHIFT)\nawait tab.keyboard.press(Key.ALT)\nawait tab.keyboard.press(Key.META)  # Command no macOS, tecla Windows no Windows\n```\n\n### Teclas Especiais\n\n```python\nfrom pydoll.constants import Key\n\nawait tab.keyboard.press(Key.ENTER)\nawait tab.keyboard.press(Key.TAB)\nawait tab.keyboard.press(Key.SPACE)\nawait tab.keyboard.press(Key.BACKSPACE)\nawait tab.keyboard.press(Key.DELETE)\nawait tab.keyboard.press(Key.ESCAPE)\nawait tab.keyboard.press(Key.INSERT)\n```\n\n## Exemplos Práticos\n\n### Navegação em Formulários\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.constants import Key\n\nasync def fill_form_with_keyboard():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/form')\n        \n        # Focar no primeiro campo e digitar\n        first_field = await tab.find(id='name')\n        await first_field.click()\n        await first_field.insert_text('João Silva')\n        \n        # Navegar para o próximo campo com Tab\n        await tab.keyboard.press(Key.TAB)\n        await tab.keyboard.press(Key.TAB)  # Pular um campo\n        \n        # Digitar no campo atualmente focado\n        second_field = await tab.find(id='email')\n        await second_field.insert_text('joao@example.com')\n        \n        # Enviar com Enter\n        await tab.keyboard.press(Key.ENTER)\n```\n\n### Seleção e Manipulação de Texto\n\n```python\nfrom pydoll.constants import Key\n\nasync def select_and_replace_text():\n    # Selecionar todo o texto\n    await tab.keyboard.hotkey(Key.CONTROL, Key.A)\n    \n    # Copiar seleção\n    await tab.keyboard.hotkey(Key.CONTROL, Key.C)\n    \n    # Mover para o fim\n    await tab.keyboard.press(Key.END)\n    \n    # Selecionar palavra por palavra\n    await tab.keyboard.down(Key.CONTROL)\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.ARROWLEFT)\n    await tab.keyboard.press(Key.ARROWLEFT)\n    await tab.keyboard.up(Key.SHIFT)\n    await tab.keyboard.up(Key.CONTROL)\n    \n    # Deletar seleção\n    await tab.keyboard.press(Key.DELETE)\n```\n\n### Navegação em Dropdown e Select\n\n```python\nfrom pydoll.constants import Key\n\nasync def navigate_dropdown():\n    # Abrir dropdown\n    select = await tab.find(tag_name='select')\n    await select.click()\n    \n    # Navegar opções com teclas de seta\n    await tab.keyboard.press(Key.ARROWDOWN)\n    await tab.keyboard.press(Key.ARROWDOWN)\n    \n    # Selecionar com Enter\n    await tab.keyboard.press(Key.ENTER)\n    \n    # Ou cancelar com Escape\n    await tab.keyboard.press(Key.ESCAPE)\n```\n\n### Sequências Complexas de Teclas\n\n```python\nfrom pydoll.constants import Key\nimport asyncio\n\nasync def complex_editing():\n    # Selecionar linha\n    await tab.keyboard.press(Key.HOME)  # Ir para o início\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.END)  # Selecionar até o fim\n    await tab.keyboard.up(Key.SHIFT)\n    \n    # Recortar\n    await tab.keyboard.hotkey(Key.CONTROL, Key.X)\n    \n    # Mover para baixo e colar\n    await tab.keyboard.press(Key.ARROWDOWN)\n    await tab.keyboard.hotkey(Key.CONTROL, Key.V)\n    \n    # Desfazer se necessário\n    await tab.keyboard.hotkey(Key.CONTROL, Key.Z)\n```\n\n## Melhores Práticas\n\n### 1. Adicione Atrasos para Confiabilidade\n\n```python\nfrom pydoll.constants import Key\nimport asyncio\n\n# Bom: Aguardar atualização da UI\nawait tab.keyboard.hotkey(Key.CONTROL, Key.F)  # Abrir busca\nawait asyncio.sleep(0.2)  # Aguardar diálogo\nawait tab.keyboard.press(Key.ESCAPE)  # Fechá-lo\n\n# Ruim: Sem atraso, pode não funcionar\nawait tab.keyboard.hotkey(Key.CONTROL, Key.F)\nawait tab.keyboard.press(Key.ESCAPE)  # Pode ser rápido demais\n```\n\n### 2. Focar Elementos Antes de Digitar\n\n```python\nfrom pydoll.constants import Key\n\n# Bom: Garantir que o elemento está focado\ninput_field = await tab.find(id='search')\nawait input_field.click()  # Focá-lo\nawait input_field.insert_text('consulta')\n\n# Ruim: Entrada de teclado vai para elemento errado\nawait tab.keyboard.press(Key.A)  # Para onde isso vai?\n```\n\n### 3. Use Atalhos Conscientes da Plataforma\n\n```python\nimport sys\nfrom pydoll.constants import Key\n\n# Bom: Consciente da plataforma\ncmd_key = Key.META if sys.platform == 'darwin' else Key.CONTROL\nawait tab.keyboard.hotkey(cmd_key, Key.C)\n\n# Ruim: Hardcoded (não funcionará no macOS)\nawait tab.keyboard.hotkey(Key.CONTROL, Key.C)\n```\n\n### 4. Limpe Sequências Longas\n\n```python\nfrom pydoll.constants import Key\n\n# Bom: Garantir que modificadores sejam liberados\ntry:\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.ARROWRIGHT)\n    # ... mais operações\nfinally:\n    await tab.keyboard.up(Key.SHIFT)  # Sempre liberar\n\n# Ruim: Modificador fica pressionado em erro\nawait tab.keyboard.down(Key.SHIFT)\nawait tab.keyboard.press(Key.ARROWRIGHT)\n# Erro aqui deixa Shift pressionado!\n```\n\n## Tabelas de Referência de Teclas\n\n### Atalhos Comuns em Nível de Página (Estes Funcionam!)\n\n| Ação | Windows/Linux | macOS | Notas |\n|------|--------------|-------|-------|\n| Copiar | Ctrl+C | Cmd+C | Funciona |\n| Colar | Ctrl+V | Cmd+V | Funciona |\n| Recortar | Ctrl+X | Cmd+X | Funciona |\n| Desfazer | Ctrl+Z | Cmd+Z | Funciona |\n| Refazer | Ctrl+Y | Cmd+Y | Funciona |\n| Selecionar Tudo | Ctrl+A | Cmd+A | Funciona |\n| Localizar | Ctrl+F | Cmd+F | Apenas se o web app implementar |\n| Salvar | Ctrl+S | Cmd+S | Apenas se o web app implementar |\n| Atualizar | F5 ou Ctrl+R | Cmd+R | Use `await tab.refresh()` |\n\n### Atalhos do Navegador (Estes NÃO Funcionam via CDP)\n\n| Ação | Atalho | Use Ao Invés |\n|------|--------|--------------|\n| Nova Aba | Ctrl+T | `await browser.new_tab()` |\n| Fechar Aba | Ctrl+W | `await tab.close()` |\n| Reabrir Aba | Ctrl+Shift+T | Rastreie abas manualmente |\n| DevTools | F12, Ctrl+Shift+I | Já disponível via CDP! |\n| Barra de Endereço | Ctrl+L | `await tab.go_to(url)` |\n\n### Todas as Teclas Disponíveis\n\n| Categoria | Teclas |\n|-----------|--------|\n| **Letras** | `Key.A` até `Key.Z` (26 teclas) |\n| **Números** | `Key.DIGIT0` até `Key.DIGIT9` (10 teclas) |\n| **Teclado Numérico** | `Key.NUMPAD0` até `Key.NUMPAD9`, `NUMPADMULTIPLY`, `NUMPADADD`, `NUMPADSUBTRACT`, `NUMPADDECIMAL`, `NUMPADDIVIDE` |\n| **Função** | `Key.F1` até `Key.F12` (12 teclas) |\n| **Navegação** | `ARROWUP`, `ARROWDOWN`, `ARROWLEFT`, `ARROWRIGHT`, `HOME`, `END`, `PAGEUP`, `PAGEDOWN` |\n| **Modificadores** | `CONTROL`, `SHIFT`, `ALT`, `META` |\n| **Especiais** | `ENTER`, `TAB`, `SPACE`, `BACKSPACE`, `DELETE`, `ESCAPE`, `INSERT` |\n| **Bloqueios** | `CAPSLOCK`, `NUMLOCK`, `SCROLLLOCK` |\n| **Símbolos** | `SEMICOLON`, `EQUALSIGN`, `COMMA`, `MINUS`, `PERIOD`, `SLASH`, `GRAVEACCENT`, `BRACKETLEFT`, `BACKSLASH`, `BRACKETRIGHT`, `QUOTE` |\n\n### Valores de Flag de Modificador\n\n| Modificador | Valor | Binário | Uso |\n|-------------|-------|---------|-----|\n| Alt | 1 | 0001 | `modifiers=1` |\n| Ctrl | 2 | 0010 | `modifiers=2` |\n| Meta | 4 | 0100 | `modifiers=4` |\n| Shift | 8 | 1000 | `modifiers=8` |\n| Ctrl+Shift | 10 | 1010 | `modifiers=10` |\n| Ctrl+Alt | 3 | 0011 | `modifiers=3` |\n| Ctrl+Shift+Alt | 11 | 1011 | `modifiers=11` |\n\n## Migração dos Métodos WebElement\n\nOs métodos de teclado anteriores em `WebElement` estão depreciados. Veja como migrar:\n\n### Antigo vs Novo\n\n```python\nfrom pydoll.constants import Key\n\n# Antigo (depreciado)\nelement = await tab.find(id='input')\nawait element.key_down(Key.A, modifiers=2)\nawait element.key_up(Key.A)\nawait element.press_keyboard_key(Key.ENTER)\n\n# Novo (recomendado)\nawait tab.keyboard.down(Key.A, modifiers=2)\nawait tab.keyboard.up(Key.A)\nawait tab.keyboard.press(Key.ENTER)\n```\n\n!!! warning \"Aviso de Depreciação\"\n    Os seguintes métodos de `WebElement` estão depreciados:\n\n    - `key_down()` → Use `tab.keyboard.down()`\n    - `key_up()` → Use `tab.keyboard.up()`\n    - `press_keyboard_key()` → Use `tab.keyboard.press()`\n    \n    Esses métodos ainda funcionam para compatibilidade retroativa, mas mostrarão avisos de depreciação.\n\n### Por Que Migrar?\n\n- **Centralizado**: Todas as operações de teclado em um só lugar\n- **API mais limpa**: Interface consistente para todas as ações de teclado\n- **Mais poderoso**: Suporte a hotkey, detecção inteligente de modificadores\n- **Melhor tipagem**: Suporte completo a autocompletar da IDE\n\n## Saiba Mais\n\nPara capacidades adicionais de automação:\n\n- **[Interações Humanas](human-interactions.md)**: Cliques, rolagem e movimento de mouse realistas\n- **[Manipulação de Formulários](form-handling.md)**: Fluxos completos de automação de formulários\n- **[Operações com Arquivos](file-operations.md)**: Automação de upload de arquivos\n\nA API de Teclado elimina a complexidade da automação de teclado, fornecendo métodos limpos e confiáveis para tudo, desde pressionamentos simples de teclas até atalhos complexos e sequências.\n"
  },
  {
    "path": "docs/pt/features/automation/mouse-control.md",
    "content": "# Controle do Mouse\n\nA API de Mouse fornece controle completo sobre a entrada do mouse no nível da página, permitindo simular movimentos realistas do cursor, cliques, cliques duplos e operações de arrastar. Quando `humanize=True` é passado, as operações do mouse usam simulação humanizada: as trajetórias seguem curvas de Bezier naturais com temporização pela Lei de Fitts, perfis de velocidade minimum-jerk, tremor fisiológico e correção de overshoot, tornando a automação virtualmente indistinguível do comportamento humano.\n\n!!! info \"Interface Centralizada de Mouse\"\n    Todas as operações do mouse são acessíveis via `tab.mouse`, fornecendo uma API limpa e unificada para todas as interações com o mouse.\n\n## Início Rápido\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.input.types import MouseButton\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n\n    # Mover cursor para posição\n    await tab.mouse.move(500, 300)\n\n    # Clicar na posição\n    await tab.mouse.click(500, 300)\n\n    # Clique direito\n    await tab.mouse.click(500, 300, button=MouseButton.RIGHT)\n\n    # Clique duplo\n    await tab.mouse.double_click(500, 300)\n\n    # Arrastar de uma posição para outra\n    await tab.mouse.drag(100, 200, 500, 400)\n```\n\n## Métodos Principais\n\n### move: Mover Cursor\n\nMove o cursor do mouse para uma posição específica na página:\n\n```python\n# Movimento padrão (único evento CDP, sem simulação)\nawait tab.mouse.move(500, 300)\n\n# Movimento humanizado (trajetória curva com temporização natural)\nawait tab.mouse.move(500, 300, humanize=True)\n```\n\n**Parâmetros:**\n\n- `x`: Coordenada X de destino (pixels CSS)\n- `y`: Coordenada Y de destino (pixels CSS)\n- `humanize` (keyword-only): Simular movimento curvo semelhante ao humano (padrão: `False`)\n\n### click: Clicar na Posição\n\nMove para a posição e realiza um clique do mouse:\n\n```python\nfrom pydoll.protocol.input.types import MouseButton\n\n# Clique esquerdo (padrão, instantâneo)\nawait tab.mouse.click(500, 300)\n\n# Clique direito\nawait tab.mouse.click(500, 300, button=MouseButton.RIGHT)\n\n# Clique duplo via click_count\nawait tab.mouse.click(500, 300, click_count=2)\n\n# Clique humanizado com movimento natural\nawait tab.mouse.click(500, 300, humanize=True)\n```\n\n**Parâmetros:**\n\n- `x`: Coordenada X de destino\n- `y`: Coordenada Y de destino\n- `button` (keyword-only): Botão do mouse, sendo `LEFT`, `RIGHT` ou `MIDDLE` (padrão: `LEFT`)\n- `click_count` (keyword-only): Número de cliques (padrão: `1`)\n- `humanize` (keyword-only): Simular comportamento semelhante ao humano (padrão: `False`)\n\n### double_click: Clique Duplo na Posição\n\nMétodo de conveniência equivalente a `click(x, y, click_count=2)`:\n\n```python\nawait tab.mouse.double_click(500, 300)\nawait tab.mouse.double_click(500, 300, humanize=False)\n```\n\n### down / up: Controle de Botão de Baixo Nível\n\nPressionar ou soltar botões do mouse independentemente:\n\n```python\n# Pressionar botão esquerdo na posição atual\nawait tab.mouse.down()\n\n# Soltar botão esquerdo\nawait tab.mouse.up()\n\n# Botão direito\nawait tab.mouse.down(button=MouseButton.RIGHT)\nawait tab.mouse.up(button=MouseButton.RIGHT)\n```\n\nEsses são primitivos que operam na posição atual do cursor e não possuem parâmetro `humanize`.\n\n### drag: Arrastar e Soltar\n\nMove do ponto inicial ao final mantendo o botão do mouse pressionado:\n\n```python\n# Arrastar padrão (instantâneo)\nawait tab.mouse.drag(100, 200, 500, 400)\n\n# Arrastar humanizado com movimento natural\nawait tab.mouse.drag(100, 200, 500, 400, humanize=True)\n```\n\n**Parâmetros:**\n\n- `start_x`, `start_y`: Coordenadas iniciais\n- `end_x`, `end_y`: Coordenadas finais\n- `humanize` (keyword-only): Simular arrasto semelhante ao humano (padrão: `False`)\n\n## Habilitando a Humanização\n\nTodos os métodos do mouse usam `humanize=False` por padrão. Para habilitar simulação humanizada com trajetórias naturais em curvas de Bezier e temporização realista, passe `humanize=True`:\n\n```python\n# Movimento humanizado, trajetória curva natural com temporização pela Lei de Fitts\nawait tab.mouse.move(500, 300, humanize=True)\n\n# Clique humanizado: movimento curvo + pausa pré-clique + press + release\nawait tab.mouse.click(500, 300, humanize=True)\n\n# Arrasto humanizado, curvas e pausas naturais\nawait tab.mouse.drag(100, 200, 500, 400, humanize=True)\n```\n\nIsso é recomendado quando a evasão de detecção é importante, por exemplo ao interagir com sites que empregam detecção de bots.\n\n## Modo Humanizado\n\nQuando `humanize=True` é passado, o módulo de mouse aplica múltiplas camadas de realismo:\n\n### Trajetórias com Curvas de Bezier\n\nO mouse segue uma trajetória curva natural em vez de uma linha reta. Os pontos de controle são deslocados aleatoriamente perpendiculares à linha início→fim, com posicionamento assimétrico (mais curvatura no início do movimento, como um alcance balístico real).\n\n### Temporização pela Lei de Fitts\n\nA duração do movimento segue a Lei de Fitts: `MT = a + b × log₂(D/W + 1)`. Distâncias maiores levam proporcionalmente mais tempo, correspondendo ao comportamento de controle motor humano.\n\n### Perfil de Velocidade Minimum-Jerk\n\nO cursor segue um perfil de velocidade em forma de sino, iniciando lento, acelerando até a velocidade máxima no meio e depois desacelerando no final. Isso corresponde à trajetória de movimento humano mais suave possível.\n\n### Tremor Fisiológico\n\nRuído gaussiano pequeno (σ ≈ 1px) é adicionado a cada quadro, simulando tremor da mão. A amplitude do tremor escala inversamente com a velocidade, com mais tremor quando o cursor está lento ou parado e menos durante movimentos balísticos rápidos.\n\n### Overshoot e Correção\n\nPara movimentos rápidos de longa distância (~70% de probabilidade), o cursor ultrapassa o alvo em 3–12% da distância, depois faz um pequeno sub-movimento corretivo de volta ao alvo. Isso corresponde a dados reais de controle motor humano.\n\n### Pausa Pré-Clique\n\nCliques humanizados incluem uma pausa pré-clique (50–200ms) que simula o tempo natural de estabilização antes de pressionar o botão.\n\n## Cliques Humanizados Automáticos em Elementos\n\nQuando você usa `element.click(humanize=True)`, a API do Mouse é utilizada para produzir um movimento realista com curva de Bezier da posição atual do cursor até o centro do elemento antes de clicar, tornando cliques em elementos indistinguíveis do comportamento humano.\n\n```python\n# Clique padrão: press/release CDP bruto\nbutton = await tab.find(id='submit')\nawait button.click()\n\n# Com deslocamento do centro\nawait button.click(x_offset=10, y_offset=5)\n\n# Clique humanizado: movimento com curva de Bezier + clique\nawait button.click(humanize=True)\n```\n\nO rastreamento de posição é mantido entre cliques em elementos. Clicar no elemento A, depois no elemento B, produz um caminho curvo natural de A até B.\n\n## Configuração Personalizada de Temporização\n\nTodos os parâmetros de humanização são configuráveis via `MouseTimingConfig`:\n\n```python\nfrom pydoll.interactions.mouse import MouseTimingConfig\n\nconfig = MouseTimingConfig(\n    fitts_a=0.070,              # Intercepto da Lei de Fitts (segundos)\n    fitts_b=0.150,              # Inclinação da Lei de Fitts (segundos/bit)\n    frame_interval=0.012,       # Intervalo base entre eventos mouseMoved\n    curvature_min=0.10,         # Curvatura mínima como fração da distância\n    curvature_max=0.30,         # Curvatura máxima\n    tremor_amplitude=1.0,       # Sigma do tremor em pixels\n    overshoot_probability=0.70, # Chance de overshoot em movimentos rápidos\n    min_duration=0.08,          # Duração mínima do movimento\n    max_duration=2.5,           # Duração máxima do movimento\n)\n\n# Aplicar à instância de mouse do tab\ntab.mouse.timing = config\n```\n\nVeja o dataclass `MouseTimingConfig` para todos os parâmetros disponíveis.\n\n## Rastreamento de Posição\n\nA API de Mouse rastreia a posição do cursor entre operações:\n\n```python\n# Posição inicial é (0, 0)\nawait tab.mouse.move(100, 200)\n# Posição agora é (100, 200)\n\nawait tab.mouse.click(300, 400)\n# Posição agora é (300, 400)\n\n# Métodos de baixo nível usam a posição rastreada\nawait tab.mouse.down()   # Pressiona em (300, 400)\nawait tab.mouse.up()     # Solta em (300, 400)\n```\n\n!!! note \"Estado da Posição\"\n    A posição do mouse é rastreada internamente. `WebElement.click()` utiliza automaticamente `tab.mouse` quando disponível, então o rastreamento de posição é mantido entre cliques em elementos.\n\n## Modo Debug\n\nAtive o modo debug para visualizar o movimento do mouse na página. Quando ativo, pontos coloridos são desenhados em um canvas de sobreposição transparente:\n\n- **Pontos azuis**: trajetória do cursor durante o movimento\n- **Pontos vermelhos**: posições de clique\n\n```python\n# Ativar em tempo de execução via propriedade\ntab.mouse.debug = True\n\n# Agora todos os movimentos desenham pontos coloridos\nawait tab.mouse.click(500, 300)\n\n# Desativar quando terminar\ntab.mouse.debug = False\n```\n\nIsso é útil para ajustar parâmetros de temporização e verificar que as trajetórias parecem naturais.\n\n## Exemplos Práticos\n\n### Clicar em um Botão com Movimento Realista\n\n```python\nasync def click_button_naturally(tab):\n    # element.click() usa automaticamente tab.mouse para movimento humanizado\n    button = await tab.find(id='submit')\n    await button.click()\n```\n\n### Arrastar um Slider\n\n```python\nasync def drag_slider(tab):\n    slider = await tab.find(css_selector='.slider-handle')\n    bounds = await slider.get_bounds_using_js()\n\n    start_x = bounds['x'] + bounds['width'] / 2\n    start_y = bounds['y'] + bounds['height'] / 2\n    end_x = start_x + 200  # Arrastar 200px para a direita\n\n    await tab.mouse.drag(start_x, start_y, end_x, start_y)\n```\n\n### Passar o Mouse Sobre Elementos\n\n```python\nasync def hover_menu(tab):\n    menu = await tab.find(css_selector='.dropdown-trigger')\n    bounds = await menu.get_bounds_using_js()\n\n    await tab.mouse.move(\n        bounds['x'] + bounds['width'] / 2,\n        bounds['y'] + bounds['height'] / 2,\n    )\n    # O menu agora deve estar visível via CSS :hover\n```\n\n## Aprenda Mais\n\n- **[Interações Humanas](human-interactions.md)**: Visão geral de todas as interações humanizadas\n- **[Controle de Teclado](keyboard-control.md)**: Simulação realista de teclado\n"
  },
  {
    "path": "docs/pt/features/automation/screenshots-and-pdfs.md",
    "content": "# Capturas de Tela (Screenshots) e PDFs\n\nO Pydoll oferece poderosas capacidades de captura de tela e geração de PDF através de comandos diretos do Chrome DevTools Protocol. Capture páginas inteiras, elementos específicos ou gere PDFs com controle refinado.\n\n## Capturas de Tela\n\n### Captura de Tela Básica da Página\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def take_page_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Salvar captura de tela em arquivo\n        await tab.take_screenshot('page.png', quality=100)\n\nasyncio.run(take_page_screenshot())\n```\n\n### Formatos Suportados\n\nO Pydoll suporta três formatos de imagem com base na extensão do arquivo:\n\n```python\n# Formato PNG (sem perdas, tamanho de arquivo maior)\nawait tab.take_screenshot('screenshot.png', quality=100)\n\n# Formato JPEG (com perdas, tamanho de arquivo menor)\nawait tab.take_screenshot('screenshot.jpeg', quality=85)\n\n# Formato WebP (moderno, eficiente)\nawait tab.take_screenshot('screenshot.webp', quality=90)\n```\n\n!!! info \"Detecção de Formato\"\n    O formato da imagem é determinado automaticamente pela extensão do arquivo. Usar uma extensão não suportada lança `InvalidFileExtension`.\n    \n    Tanto `.jpg` quanto `.jpeg` são suportados para o formato JPEG (`.jpg` é normalizado automaticamente para `.jpeg` internamente para corresponder aos requisitos do CDP).\n\n### Parâmetros de Captura de Tela\n\n| Parâmetro | Tipo | Padrão | Descrição |\n|---|---|---|---|\n| `path` | `Optional[str]` | `None` | Caminho do arquivo para salvar a captura de tela. Obrigatório se `as_base64=False`. |\n| `quality` | `int` | `100` | Qualidade da imagem (0-100). Valores mais altos significam melhor qualidade e arquivos maiores. |\n| `beyond_viewport` | `bool` | `False` | Captura a página inteira rolável, não apenas a área visível. |\n| `as_base64` | `bool` | `False` | Retorna a string codificada em base64 em vez de salvar em arquivo. |\n\n### Captura de Tela de Página Inteira\n\nCapture conteúdo além da área visível (viewport):\n\n```python\nasync def full_page_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/long-page')\n        \n        # Captura a página inteira, incluindo conteúdo abaixo da dobra\n        await tab.take_screenshot(\n            'full-page.png',\n            beyond_viewport=True,\n            quality=90\n        )\n```\n\n!!! warning \"Nota de Desempenho\"\n    Usar `beyond_viewport=True` em páginas muito longas pode consumir memória significativa e levar mais tempo para processar.\n\n### Captura de Tela em Base64\n\nObtenha a captura de tela como string base64 para incorporar ou enviar via API:\n\n```python\nasync def base64_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Obter captura de tela como string base64\n        screenshot_base64 = await tab.take_screenshot(\n            as_base64=True\n        )\n        \n        # Usar em tag img HTML\n        html = f'<img src=\"data:image/png;base64,{screenshot_base64}\" />'\n        \n        # Ou enviar via API\n        import aiohttp\n        async with aiohttp.ClientSession() as session:\n            await session.post(\n                'https://api.example.com/upload',\n                json={'image': screenshot_base64}\n            )\n```\n\n### Captura de Tela de Elemento\n\nCapture elementos específicos em vez da página inteira:\n\n```python\nasync def element_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Capturar um elemento específico (PNG)\n        header = await tab.find(tag_name='header')\n        await header.take_screenshot('header.png', quality=100)\n        \n        # Capturar um formulário (JPEG)\n        form = await tab.find(id='login-form')\n        await form.take_screenshot('login-form.jpeg', quality=85)\n        \n        # Capturar um gráfico (WebP)\n        chart = await tab.find(class_name='data-visualization')\n        await chart.take_screenshot('chart.webp', quality=90)\n```\n\n!!! info \"Detecção de Formato\"\n    O formato da imagem é detectado automaticamente a partir da extensão do arquivo (`.png`, `.jpeg`/`.jpg`, ou `.webp`). Usar uma extensão não suportada lança `InvalidFileExtension`.\n\n!!! tip \"Rolagem Automática\"\n    Ao capturar screenshots de elementos, o Pydoll rola automaticamente o elemento para a visão antes de tirar a foto.\n\n### Capturas de Tela de Elemento vs Página\n\n| Característica | `tab.take_screenshot()` | `element.take_screenshot()` |\n|---|---|---|\n| **Escopo** | Viewport inteira ou página | Apenas elemento específico |\n| **Suporte a Formato** | PNG, JPEG, WebP | PNG, JPEG, WebP |\n| **Além da Viewport** | Suportado | Não aplicável |\n| **Saída Base64** | Suportado | Suportado |\n| **Auto-Scroll** | Não aplicável | Sim |\n| **Caso de Uso** | Capturas de página inteira | Isolamento de componente, testes |\n\n\n## Geração de PDF\n\n### Exportação Básica de PDF\n\nConverta páginas para PDF com qualidade de impressão:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def generate_pdf():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/document')\n        \n        # Gerar PDF com Path\n        await tab.print_to_pdf(Path('document.pdf'))\n        \n        # Ou com string\n        await tab.print_to_pdf('document.pdf')\n\nasyncio.run(generate_pdf())\n```\n\n### Parâmetros de PDF\n\n| Parâmetro | Tipo | Padrão | Descrição |\n|---|---|---|---|\n| `path` | `Optional[str \\| Path]` | `None` | Caminho do arquivo para salvar o PDF. Obrigatório se `as_base64=False`. |\n| `landscape` | `bool` | `False` | Usar orientação paisagem (vs retrato). |\n| `display_header_footer` | `bool` | `False` | Incluir cabeçalho/rodapé gerado pelo navegador com título, URL, números de página. |\n| `print_background` | `bool` | `True` | Incluir gráficos e cores de fundo. |\n| `scale` | `float` | `1.0` | Fator de escala da página (0.1-2.0). Útil para efeitos de zoom/redução. |\n| `as_base64` | `bool` | `False` | Retorna string codificada em base64 em vez de salvar em arquivo. |\n\n!!! tip \"Path vs String\"\n    Embora objetos `Path` do `pathlib` sejam recomendados como melhor prática para melhor manipulação de caminhos e compatibilidade entre plataformas, você também pode usar strings simples, se preferir.\n\n### Opções Avançadas de PDF\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def advanced_pdf():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/report')\n        \n        # PDF paisagem com cabeçalhos/rodapés\n        await tab.print_to_pdf(\n            Path('report-landscape.pdf'),\n            landscape=True,\n            display_header_footer=True,\n            print_background=True,\n            scale=0.9\n        )\n        \n        # PDF retrato sem fundos (amigável à tinta)\n        await tab.print_to_pdf(\n            Path('report-ink-friendly.pdf'),\n            landscape=False,\n            print_background=False,\n            scale=1.0\n        )\n\nasyncio.run(advanced_pdf())\n```\n\n### Fator de Escala do PDF\n\nControle o nível de zoom da saída em PDF:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def scaled_pdfs():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/content')\n        \n        # Encolher conteúdo para caber mais em cada página\n        await tab.print_to_pdf(Path('compact.pdf'), scale=0.7)\n        \n        # Escala normal\n        await tab.print_to_pdf(Path('normal.pdf'), scale=1.0)\n        \n        # Ampliar conteúdo (menos páginas)\n        await tab.print_to_pdf(Path('large.pdf'), scale=1.5)\n\nasyncio.run(scaled_pdfs())\n```\n\n!!! warning \"Limites de Escala\"\n    O parâmetro `scale` aceita valores entre `0.1` e `2.0`. Valores fora dessa faixa podem produzir resultados inesperados.\n\n### PDF em Base64\n\nGere PDF como string base64 para transmissão via API:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def base64_pdf():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/invoice')\n        \n        # Obter PDF como base64 (não precisa de caminho)\n        pdf_base64 = await tab.print_to_pdf(as_base64=True)\n        \n        # Enviar via API\n        import aiohttp\n        async with aiohttp.ClientSession() as session:\n            await session.post(\n                'https://api.example.com/invoices',\n                json={'pdf': pdf_base64}\n            )\n\nasyncio.run(base64_pdf())\n```\n\n\n!!! info \"Referência do CDP\"\n    Para documentação completa do CDP sobre esses comandos, veja:\n    \n    - [Page.captureScreenshot](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot)\n    - [Page.printToPDF](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF)\n\n### Tratamento de Erros\n\n```python\nfrom pydoll.exceptions import (\n    InvalidFileExtension,\n    MissingScreenshotPath,\n    TopLevelTargetRequired\n)\n\nasync def safe_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        try:\n            # Caminho faltando e as_base64=False\n            await tab.take_screenshot()\n        except MissingScreenshotPath:\n            print(\"Erro: Deve fornecer o caminho ou definir as_base64=True\")\n        \n        try:\n            # Extensão inválida\n            await tab.take_screenshot('image.bmp')\n        except InvalidFileExtension as e:\n            print(f\"Erro: {e}\")\n        \n        # Limitação de screenshot de IFrame\n        iframe_element = await tab.find(tag_name='iframe')\n\n        # Isso ainda não funciona: screenshots de nível superior ignoram iframes\n        # await tab.take_screenshot('frame.png')\n\n        # Capture um elemento dentro do próprio iframe\n        content = await iframe_element.find(id='content')\n        await content.take_screenshot('iframe-content.png')\n```\n\n## Exportação de Bundle da Página\n\nSalve uma página inteira com todos os seus assets (CSS, JS, imagens, fontes) como um arquivo `.zip` para visualização offline.\n\n### Uso Básico\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def save_page():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n\n        # Salvar página com assets como arquivos separados\n        await tab.save_bundle('page.zip')\n\nasyncio.run(save_page())\n```\n\nO zip resultante contém um `index.html` com todas as URLs reescritas para referenciar arquivos locais no diretório `assets/`.\n\n### Modo Inline\n\nIncorpore tudo diretamente em um único `index.html` usando data URIs, `<style>` e `<script>`:\n\n```python\n# Um único arquivo HTML autocontido dentro do zip\nawait tab.save_bundle('page-inline.zip', inline_assets=True)\n```\n\n### Parâmetros\n\n| Parâmetro | Tipo | Padrão | Descrição |\n|-----------|------|--------|-----------|\n| `path` | `str \\| Path` | *(obrigatório)* | Caminho de destino. Deve terminar com `.zip`. |\n| `inline_assets` | `bool` | `False` | Incorporar todos os assets inline em vez de salvá-los como arquivos separados. |\n\n!!! info \"O Que é Incluído no Bundle\"\n    O bundle inclui recursos dos tipos: Document, Stylesheet, Script, Image, Font e Media. Recursos que falharam ao carregar, foram cancelados ou usam URIs `data:` são automaticamente ignorados.\n\n## Aprenda Mais\n\nPara contexto adicional sobre como screenshots e PDFs se integram com a arquitetura do Pydoll:\n\n- **[Análise Profunda: CDP](../../deep-dive/cdp.md)**: Entendendo os comandos do Chrome DevTools Protocol\n- **[Referência da API: Tab](../../api/browser/tab.md#take_screenshot)**: Assinaturas de método e parâmetros completos\n- **[Referência da API: WebElement](../../api/elements/web-element.md#take_screenshot)**: Capacidades de screenshot específicas de elementos\n\nScreenshots e PDFs são ferramentas essenciais para automação, testes e documentação. A integração direta do Pydoll com o CDP fornece saída de nível profissional com controle refinado."
  },
  {
    "path": "docs/pt/features/browser-management/contexts.md",
    "content": "# Contextos de Navegador (Browser Contexts)\n\nContextos de Navegador são a solução do Pydoll para criar ambientes de navegação completely isolados dentro de um único processo de navegador. Pense neles como \"janelas anônimas\" separadas, mas com controle programático total. Cada contexto mantém seus próprios cookies, armazenamento, cache e estado de autenticação.\n\n## Guia Rápido\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def basic_context_example():\n    async with Chrome() as browser:\n        # Inicia o navegador com a aba inicial no contexto padrão\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        \n        # Cria um contexto isolado\n        context_id = await browser.create_browser_context()\n        \n        # Nova aba no contexto isolado\n        isolated_tab = await browser.new_tab('https://example.com', browser_context_id=context_id)\n        \n        # Ambas as abas estão completamente isoladas - cookies, armazenamento, etc. diferentes\n        await initial_tab.execute_script(\"localStorage.setItem('user', 'Alice')\")\n        await isolated_tab.execute_script(\"localStorage.setItem('user', 'Bob')\")\n        \n        # Verifica o isolamento\n        user_default = await initial_tab.execute_script(\"return localStorage.getItem('user')\")\n        user_isolated = await isolated_tab.execute_script(\"return localStorage.getItem('user')\")\n        \n        print(f\"Contexto padrão: {user_default}\")  # Alice\n        print(f\"Contexto isolado: {user_isolated}\")  # Bob\n\nasyncio.run(basic_context_example())\n```\n\n## O que são Contextos de Navegador?\n\nUm contexto de navegador é um ambiente de navegação isolado dentro de um único processo de navegador. Cada contexto mantém separadamente:\n\n| Componente | Descrição | Nível de Isolamento |\n|---|---|---|\n| **Cookies** | Cookies HTTP e dados de sessão | ✓ Totalmente isolado |\n| **Local Storage** | `localStorage` e `sessionStorage` | ✓ Totalmente isolado |\n| **IndexedDB** | Banco de dados do lado do cliente | ✓ Totalmente isolado |\n| **Cache** | Cache HTTP e recursos | ✓ Totalmente isolado |\n| **Permissões** | Geolocalização, notificações, câmera, etc. | ✓ Totalmente isolado |\n| **Autenticação** | Sessões de login e tokens de autenticação | ✓ Totalmente isolado |\n| **Service Workers** | Scripts em segundo plano | ✓ Totalmente isolado |\n\n```mermaid\ngraph LR\n    Browser[Processo do Navegador] --> Default[Contexto Padrao]\n    Browser --> Context1[Contexto 1]\n    Browser --> Context2[Contexto 2]\n    \n    Default --> T1[Aba A]\n    Default --> T2[Aba B]\n    Context1 --> T3[Aba C]\n    Context2 --> T4[Aba D]\n```\n\n## Por que Usar Contextos de Navegador?\n\n### 1. Teste de Múltiplas Contas\n\nTeste diferentes contas de usuário simultaneamente sem interferência:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def perform_login(tab, email, password):\n    \"\"\"\n    Função auxiliar para navegar até a página de login\n    e enviar as credenciais da conta.\n    \"\"\"\n    print(f\"Tentando login com: {email}...\")\n    await tab.go_to('https://app.example.com/login')\n\n    # Encontrar elementos\n    email_field = await tab.find(id='email')\n    password_field = await tab.find(id='password')\n    login_btn = await tab.find(id='login-btn')\n\n    # Preencher credenciais e clicar\n    await email_field.type_text(email)\n    await password_field.type_text(password)\n    await login_btn.click()\n\n    # Esperar o login processar\n    await asyncio.sleep(2)\n    print(f\"Login bem-sucedido para {email}.\")\n\n\nasync def multi_account_test():\n    \"\"\"\n    Script principal para testar logins simultâneos\n    usando contextos de navegador isolados.\n    \"\"\"\n    accounts = [\n        {\"email\": \"user1@example.com\", \"password\": \"pass1\"},\n        {\"email\": \"user2@example.com\", \"password\": \"pass2\"},\n        {\"email\": \"admin@example.com\", \"password\": \"admin_pass\"}\n    ]\n\n    # Esta lista armazenará informações de cada sessão de usuário ativa.\n    user_sessions = []\n\n    async with Chrome() as browser:\n        first_account = accounts[0]\n        initial_tab = await browser.start()\n        await perform_login(initial_tab, first_account['email'], first_account['password'])\n        user_sessions.append({\n            \"email\": first_account['email'],\n            \"tab\": initial_tab,\n            \"context_id\": None  # 'None' representa o contexto padrão do navegador\n        })\n\n        # Iterar sobre o restante das contas\n        for account in accounts[1:]:\n            context_id = await browser.create_browser_context()\n            new_tab = await browser.new_tab(browser_context_id=context_id)\n            await perform_login(new_tab, account['email'], account['password'])\n\n            # Adicionar esta nova informação de sessão à lista\n            user_sessions.append({\n                \"email\": account['email'],\n                \"tab\": new_tab,\n                \"context_id\": context_id\n            })\n\n        print(\"\\n--- Verificando todas as sessões activas ---\")\n        for session in user_sessions:\n            tab = session[\"tab\"]\n            email = session[\"email\"]\n            await tab.go_to('https://app.example.com/dashboard')\n            username = await tab.find(class_name='username')\n            username_text = await username.text\n            print(f\"[Conta: {email}] -> Logado como: {username_text}\")\n            await asyncio.sleep(0.5)\n\n        print(\"\\n--- Limpando contextos ---\")\n        for session in user_sessions:\n            # Fechar apenas os contextos que criamos (diferentes de None)\n            if session[\"context_id\"] is not None:\n                print(f\"Fechando contexto para: {session['email']}\")\n                await session[\"tab\"].close()\n                await browser.delete_browser_context(session[\"context_id\"])\n        \n        # O contexto padrão (None) é fechado automaticamente\n        # pelo 'async with Chrome() as browser'\n\nasyncio.run(multi_account_test())\n```\n\n### 2. Teste de Geo-Localização com Proxies Específicos do Contexto\n\nCada contexto pode ter sua própria configuração de proxy:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def geo_location_testing():\n    async with Chrome() as browser:\n        # Inicia o navegador e usa a aba inicial para o primeiro teste (contexto padrão, sem proxy)\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://api.ipify.org')\n        await asyncio.sleep(2)\n        default_ip = await initial_tab.execute_script('return document.body.textContent')\n        print(f\"IP Padrão (sem proxy): {default_ip}\")\n        \n        # Contexto dos EUA com proxy dos EUA\n        us_context = await browser.create_browser_context(\n            proxy_server='http://us-proxy.example.com:8080'\n        )\n        us_tab = await browser.new_tab('https://api.ipify.org', browser_context_id=us_context)\n        await asyncio.sleep(2)\n        us_ip = await us_tab.execute_script('return document.body.textContent')\n        print(f\"IP dos EUA: {us_ip}\")\n        \n        # Contexto da UE com proxy da UE\n        eu_context = await browser.create_browser_context(\n            proxy_server='http://eu-proxy.example.com:8080'\n        )\n        eu_tab = await browser.new_tab('https://api.ipify.org', browser_context_id=eu_context)\n        await asyncio.sleep(2)\n        eu_ip = await eu_tab.execute_script('return document.body.textContent')\n        print(f\"IP da UE: {eu_ip}\")\n        \n        # Limpeza (pular aba inicial)\n        await us_tab.close()\n        await eu_tab.close()\n        await browser.delete_browser_context(us_context)\n        await browser.delete_browser_context(eu_context)\n\nasyncio.run(geo_location_testing())\n```\n\n!!! tip \"Autenticação de Proxy\"\n    O Pydoll lida automaticamente com a autenticação de proxy para contextos. Apenas inclua as credenciais na URL:\n    ```python\n    context_id = await browser.create_browser_context(\n        proxy_server='http://username:password@proxy.example.com:8080'\n    )\n    ```\n    As credenciais são higienizadas dos comandos CDP e usadas apenas quando o navegador solicita autenticação.\n\n### 3. Teste A/B\n\nCompare diferentes experiências de usuário em paralelo:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def ab_testing():\n    async with Chrome() as browser:\n        # Inicia o navegador com a aba inicial (Grupo de Controle no contexto padrão)\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        await initial_tab.execute_script(\"localStorage.setItem('experiment', 'control')\")\n        \n        # Grupo de Tratamento em contexto isolado\n        context_b = await browser.create_browser_context()\n        tab_b = await browser.new_tab('https://example.com', browser_context_id=context_b)\n        await tab_b.execute_script(\"localStorage.setItem('experiment', 'treatment')\")\n        \n        # Navega ambos para a página da funcionalidade\n        await initial_tab.go_to('https://example.com/feature')\n        await tab_b.go_to('https://example.com/feature')\n        \n        # Compara os resultados\n        result_a = await initial_tab.find(class_name='experiment-result')\n        result_b = await tab_b.find(class_name='experiment-result')\n        \n        print(f\"Resultado do grupo de controle: {await result_a.text}\")\n        print(f\"Resultado do grupo de tratamento: {await result_b.text}\")\n        \n        # Limpeza\n        await tab_b.close()\n        await browser.delete_browser_context(context_b)\n\nasyncio.run(ab_testing())\n```\n\n### 4. Raspagem Web Paralela\n\nRaspe múltiplos sites com diferentes configurações:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def parallel_scraping():\n    websites = [\n        {'url': 'https://news.ycombinator.com', 'selector': '.storylink'},\n        {'url': 'https://reddit.com/r/python', 'selector': '.title'},\n        {'url': 'https://github.com/trending', 'selector': '.h3'},\n    ]\n    \n    async with Chrome() as browser:\n        # Inicia o navegador e obtém a aba inicial\n        initial_tab = await browser.start()\n        \n        # Cria contextos para os sites restantes (o primeiro usa o contexto padrão)\n        contexts = [None] + [await browser.create_browser_context() for _ in websites[1:]]\n        \n        # Cria abas (reutilizando a aba inicial para o primeiro site)\n        tabs = [initial_tab] + [\n            await browser.new_tab(browser_context_id=ctx) for ctx in contexts[1:]\n        ]\n        \n        async def scrape_site(tab, site, context_id):\n            \"\"\"Raspa um único site dentro da aba e contexto fornecidos.\"\"\"\n            try:\n                await tab.go_to(site['url'])\n                await asyncio.sleep(3)\n                \n                # Extrai títulos usando seletor CSS\n                elements = await tab.query(site['selector'], find_all=True)\n                titles = [await elem.text for elem in elements[:5]]\n                \n                return {'url': site['url'], 'titles': titles}\n            finally:\n                # Limpa o contexto (pula o contexto padrão da aba inicial)\n                if context_id is not None:\n                    await tab.close()\n                    await browser.delete_browser_context(context_id)\n        \n        # Raspa todos os sites concorrentemente\n        results = await asyncio.gather(*[\n            scrape_site(tab, site, ctx) for tab, site, ctx in zip(tabs, websites, contexts)\n        ])\n        \n        # Exibe os resultados\n        for result in results:\n            print(f\"\\n{result['url']}:\")\n            for i, title in enumerate(result['titles'], 1):\n                print(f\"  {i}. {title}\")\n\nasyncio.run(parallel_scraping())\n```\n\n## Entendendo o Desempenho do Contexto\n\n### Contextos São Leves\n\n!!! info \"Características de Desempenho\"\n    Criar um contexto de navegador é **significativamente mais rápido e leve** do que lançar um novo processo de navegador:\n    \n    - **Criação de contexto**: ~50-100ms, sobrecarga mínima de memória\n    - **Novo processo de navegador**: ~2-5 segundos, 50-150 MB de memória base\n    \n    Para 10 ambientes isolados:\n\n    - **10 contextos em 1 navegador**: ~500ms de inicialização, ~500 MB no total\n    - **10 navegadores separados**: ~30 segundos de inicialização, ~1-1.5 GB no total\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def benchmark_contexts_vs_browsers():\n    # Benchmark de contextos\n    start = time.time()\n    async with Chrome() as browser:\n        # Inicia o navegador (aba inicial não usada neste exemplo)\n        await browser.start()\n        \n        contexts = []\n        for i in range(10):\n            context_id = await browser.create_browser_context()\n            contexts.append(context_id)\n        \n        print(f\"10 contextos criados em: {time.time() - start:.2f}s\")\n        \n        # Limpeza\n        for context_id in contexts:\n            await browser.delete_browser_context(context_id)\n\nasyncio.run(benchmark_contexts_vs_browsers())\n```\n\n### Headless vs Headed: O Comportamento da Janela\n\n!!! warning \"Importante: Janelas de Contexto no Modo Headed\"\n    Ao rodar em **modo headed** (com UI do navegador visível), há um comportamento importante a entender:\n    \n    **A primeira aba criada em um novo contexto abrirá uma nova janela do sistema operacional.**\n    \n    - Isso acontece porque o contexto precisa de uma \"janela hospedeira\" para renderizar sua primeira página\n    - Abas subsequentes nesse contexto podem abrir como abas dentro dessa janela\n    - Esta é uma limitação do CDP/Chromium, não uma escolha de design do Pydoll\n    \n    **No modo headless**, isso não importa—nenhuma janela é criada, tudo roda em segundo plano.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def demonstrate_window_behavior():\n    # Modo headed - verá janelas\n    options_headed = ChromiumOptions()\n    options_headed.headless = False\n    \n    async with Chrome(options=options_headed) as browser:\n        # Inicia o navegador com a aba inicial (abre a primeira janela no contexto padrão)\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        \n        # Cria novo contexto - a primeira aba abrirá uma NOVA janela\n        context = await browser.create_browser_context()\n        tab2 = await browser.new_tab('https://github.com', browser_context_id=context)\n        \n        # Segunda aba no mesmo contexto - abre como aba na janela existente\n        tab3 = await browser.new_tab('https://google.com', browser_context_id=context)\n        \n        await asyncio.sleep(10)  # Observe as janelas\n        \n        await tab2.close()\n        await tab3.close()\n        await browser.delete_browser_context(context)\n\n# Modo headless - sem janelas, contextos são invisíveis mas ainda isolados\nasync def headless_contexts():\n    options = ChromiumOptions()\n    options.headless = True  # Sem janelas visíveis\n    \n    async with Chrome(options=options) as browser:\n        # Inicia o navegador com a aba inicial no contexto padrão\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com/page0')\n        \n        # Cria mais 4 contextos - nenhuma janela aberta, tudo em segundo plano\n        contexts = []\n        for i in range(1, 5):\n            context_id = await browser.create_browser_context()\n            tab = await browser.new_tab(f'https://example.com/page{i}', browser_context_id=context_id)\n            contexts.append((context_id, tab))\n        \n        print(f\"Criados {len(contexts) + 1} contextos isolados (1 padrão + {len(contexts)} personalizados, invisíveis)\")\n        \n        # Limpeza\n        for context_id, tab in contexts:\n            await tab.close()\n            await browser.delete_browser_context(context_id)\n\nasyncio.run(headless_contexts())\n```\n\n!!! tip \"Melhor Prática: Use Headless para Contextos\"\n    Para máxima eficiência com múltiplos contextos:\n    \n    - **Desenvolvimento/Depuração**: Use o modo headed para ver o que está acontecendo\n    - **Produção/CI/CD**: Use o modo headless para execução mais rápida e leve\n    - **Múltiplos contextos**: Prefira fortemente o headless para evitar a complexidade do gerenciamento de janelas\n\n## Gerenciamento de Contexto\n\n### Criando Contextos\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def create_context_example():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Criar contexto básico\n        context_id = await browser.create_browser_context()\n        print(f\"Contexto criado: {context_id}\")\n        \n        # Criar contexto com proxy\n        proxied_context = await browser.create_browser_context(\n            proxy_server='http://proxy.example.com:8080',\n            proxy_bypass_list='localhost,127.0.0.1'\n        )\n        print(f\"Contexto com proxy criado: {proxied_context}\")\n        \n        # Criar contexto com proxy autenticado\n        auth_context = await browser.create_browser_context(\n            proxy_server='http://user:pass@proxy.example.com:8080'\n        )\n        print(f\"Contexto com autenticação criado: {auth_context}\")\n\nasyncio.run(create_context_example())\n```\n\n### Listando Contextos\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def list_contexts():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Obter todos os contextos (inclui o padrão)\n        contexts = await browser.get_browser_contexts()\n        print(f\"Contextos iniciais: {len(contexts)}\")  # Geralmente 1 (padrão)\n        \n        # Criar contextos adicionais\n        context1 = await browser.create_browser_context()\n        context2 = await browser.create_browser_context()\n        \n        # Listar novamente\n        contexts = await browser.get_browser_contexts()\n        print(f\"Após criar 2 novos contextos: {len(contexts)}\")  # 3 no total\n        \n        for i, context_id in enumerate(contexts):\n            print(f\"  Contexto {i+1}: {context_id}\")\n        \n        # Limpeza\n        await browser.delete_browser_context(context1)\n        await browser.delete_browser_context(context2)\n\nasyncio.run(list_contexts())\n```\n\n### Deletando Contextos\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def delete_context_example():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Criar contexto com abas\n        context_id = await browser.create_browser_context()\n        tab1 = await browser.new_tab('https://example.com', browser_context_id=context_id)\n        tab2 = await browser.new_tab('https://github.com', browser_context_id=context_id)\n        \n        print(f\"Contexto {context_id} criado com 2 abas\")\n        \n        # Deletar o contexto fecha todas as suas abas automaticamente\n        await browser.delete_browser_context(context_id)\n        print(\"Contexto deletado (todas as abas fechadas automaticamente)\")\n\nasyncio.run(delete_context_example())\n```\n\n!!! warning \"Deletar Contextos Fecha Todas as Abas\"\n    Quando você deleta um contexto de navegador, **todas as abas pertencentes a esse contexto são fechadas automaticamente**. Esta é uma maneira eficiente de limpar múltiplas abas de uma vez, mas certifique-se de ter salvo quaisquer dados importantes primeiro.\n\n## Contexto Padrão\n\nTodo navegador inicia com um **contexto padrão** que contém a aba inicial:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def default_context_example():\n    async with Chrome() as browser:\n        # Aba inicial está no contexto padrão\n        initial_tab = await browser.start()\n        \n        # Criar aba sem especificar contexto - usa o padrão\n        default_tab = await browser.new_tab('https://example.com')\n        \n        # Criar contexto personalizado\n        custom_context = await browser.create_browser_context()\n        custom_tab = await browser.new_tab('https://github.com', browser_context_id=custom_context)\n        \n        # Contextos padrão e personalizado são isolados\n        await default_tab.execute_script(\"localStorage.setItem('type', 'default')\")\n        await custom_tab.execute_script(\"localStorage.setItem('type', 'custom')\")\n        \n        # Verificar isolamento\n        default_type = await default_tab.execute_script(\"return localStorage.getItem('type')\")\n        custom_type = await custom_tab.execute_script(\"return localStorage.getItem('type')\")\n        \n        print(f\"Contexto padrão: {default_type}\")  # 'default'\n        print(f\"Contexto personalizado: {custom_type}\")    # 'custom'\n        \n        # Limpar contexto personalizado\n        await browser.delete_browser_context(custom_context)\n\nasyncio.run(default_context_example())\n```\n\n!!! info \"Você Não Pode Deletar o Contexto Padrão\"\n    O contexto padrão do navegador é permanente e não pode ser deletado. Ele existe por toda a sessão do navegador. Apenas contextos personalizados criados com `create_browser_context()` podem ser deletados.\n\n## Padrões Avançados\n\n### Pool de Contextos para Isolamento Reutilizável\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nclass ContextPool:\n    def __init__(self, browser, size=5):\n        self.browser = browser\n        self.size = size\n        self.contexts = []\n        self.in_use = set()\n    \n    async def initialize(self):\n        \"\"\"Criar pool de contextos\"\"\"\n        for _ in range(self.size):\n            context_id = await self.browser.create_browser_context()\n            self.contexts.append(context_id)\n        print(f\"Pool de contextos inicializado com {self.size} contextos\")\n    \n    async def acquire(self):\n        \"\"\"Obter contexto disponível do pool\"\"\"\n        for context_id in self.contexts:\n            if context_id not in self.in_use:\n                self.in_use.add(context_id)\n                return context_id\n        raise Exception(\"Nenhum contexto disponível no pool\")\n    \n    def release(self, context_id):\n        \"\"\"Devolver contexto ao pool\"\"\"\n        self.in_use.discard(context_id)\n    \n    async def cleanup(self):\n        \"\"\"Deletar todos os contextos no pool\"\"\"\n        for context_id in self.contexts:\n            await self.browser.delete_browser_context(context_id)\n\nasync def use_context_pool():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Criar pool\n        pool = ContextPool(browser, size=3)\n        await pool.initialize()\n        \n        # Usar contextos do pool\n        async def scrape_with_pool(url):\n            context_id = await pool.acquire()\n            try:\n                tab = await browser.new_tab(url, browser_context_id=context_id)\n                await asyncio.sleep(2)\n                title = await tab.execute_script('return document.title')\n                await tab.close()\n                return title\n            finally:\n                pool.release(context_id)\n        \n        # Raspar múltiplas URLs usando o pool\n        urls = [f'https://example.com/page{i}' for i in range(10)]\n        results = await asyncio.gather(*[scrape_with_pool(url) for url in urls])\n        \n        for i, title in enumerate(results):\n            print(f\"{urls[i]}: {title}\")\n        \n        # Limpeza\n        await pool.cleanup()\n\nasyncio.run(use_context_pool())\n```\n\n### Gerenciador de Configuração por Contexto\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def context_config_manager():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Definir configurações para diferentes cenários\n        configs = {\n            'us_user': {\n                'proxy': 'http://us-proxy.example.com:8080',\n                'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'\n            },\n            'eu_user': {\n                'proxy': 'http://eu-proxy.example.com:8080',\n                'user_agent': 'Mozilla/5.0 (X11; Linux x86_64)'\n            },\n            'mobile_user': {\n                'proxy': None,\n                'user_agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)'\n            }\n        }\n        \n        contexts = {}\n        \n        # Criar contexto para cada configuração\n        for name, config in configs.items():\n            if config['proxy']:\n                context_id = await browser.create_browser_context(\n                    proxy_server=config['proxy']\n                )\n            else:\n                context_id = await browser.create_browser_context()\n            \n            # Criar aba e definir user agent\n            tab = await browser.new_tab(browser_context_id=context_id)\n            # Nota: User agent seria definido via CDP ou opções, simplificado aqui\n            \n            contexts[name] = {'context_id': context_id, 'tab': tab}\n        \n        # Usar diferentes contextos para diferentes cenários\n        for name, data in contexts.items():\n            tab = data['tab']\n            await tab.go_to('https://httpbin.org/headers')\n            await asyncio.sleep(2)\n            print(f\"\\nConfiguração {name} ativa\")\n        \n        # Limpeza\n        for data in contexts.values():\n            await data['tab'].close()\n            await browser.delete_browser_context(data['context_id'])\n\nasyncio.run(context_config_manager())\n```\n\n## Melhores Práticas\n\n1.  **Use o modo headless para múltiplos contextos** para evitar a complexidade do gerenciamento de janelas\n2.  **Sempre delete os contextos quando terminar** para evitar vazamentos de memória\n3.  **Agrupe operações relacionadas no mesmo contexto** para melhor organização\n4.  **Prefira contextos a múltiplos processos de navegador** para melhor desempenho\n5.  **Use pools de contextos** para cenários que exigem muitos ambientes isolados de curta duração\n6.  **Feche as abas antes de deletar os contextos** para uma limpeza mais organizada (embora não seja estritamente necessário)\n\n## Veja Também\n\n- **[Gerenciamento de Múltiplas Abas](tabs.md)** - Gerenciando múltiplas abas dentro de contextos\n- **[Análise Profunda: Domínio do Navegador](../../deep-dive/browser-domain.md)** - Detalhes arquitetônicos sobre contextos\n- **[Rede: Requisições HTTP](../network/http-requests.md)** - Requisições no contexto do navegador herdam o estado do contexto\n- **[Conceitos Principais](../core-concepts.md)** - Entendendo a arquitetura do Pydoll\n\nContextos de Navegador são uma das funcionalidades mais poderosas do Pydoll para criar fluxos de trabalho de automação sofisticados. Ao entender como eles funcionam—especialmente o comportamento da janela no modo headed e sua natureza leve—você pode construir automação eficiente e escalável que lida com cenários complexos de múltiplos ambientes com facilidade."
  },
  {
    "path": "docs/pt/features/browser-management/cookies-sessions.md",
    "content": "# Cookies e Sessões\n\nGerenciar cookies e sessões de forma eficaz é crucial para uma automação de navegador realista. Os sites usam cookies para rastrear autenticação, preferências e comportamento do usuário, e esperam que os navegadores se comportem de acordo.\n\n## Por que os Cookies Importam para a Automação\n\nCookies são mais do que apenas dados armazenados: eles são uma impressão digital (fingerprint) da atividade do navegador:\n\n- **Autenticação**: Cookies de sessão mantêm o estado de login entre as requisições\n- **Prevenção de Rastreamento**: Sistemas anti-bot analisam padrões de cookies\n- **Comportamento Realista**: Um navegador sem cookies parece suspeito\n- **Persistência de Sessão**: Reutilizar cookies pode economizar tempo em logins repetidos\n\n!!! warning \"O Paradoxo dos Cookies\"\n    - **Muito limpo**: Um navegador sem cookies ou histórico parece ser um bot\n    - **Muito obsoleto**: Usar a mesma sessão por semanas aciona alertas de segurança\n    - **Ponto ideal**: Cookies novos com rotação ocasional e padrões de atividade realistas\n\n## Guia Rápido\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def basic_cookie_management():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Definir um cookie (usando um dict simples)\n        cookies = [\n            {\n                'name': 'session_id',\n                'value': 'abc123xyz',\n                'domain': 'example.com',\n                'path': '/',\n                'secure': True,\n                'httpOnly': True\n            }\n        ]\n        await tab.set_cookies(cookies)\n        \n        # Obter todos os cookies\n        all_cookies = await browser.get_cookies()\n        print(f\"Total de cookies: {len(all_cookies)}\")\n        \n        # Deletar todos os cookies\n        await tab.delete_all_cookies()\n\nasyncio.run(basic_cookie_management())\n```\n\n## Entendendo os Tipos de Cookie\n\n!!! info \"TypedDict: Use Dicionários Regulares na Prática\"\n    Ao longo desta documentação, você verá referências a `CookieParam` e `Cookie`. Estes são tipos **TypedDict**, eles são apenas dicionários Python regulares com dicas de tipo para autocompletar da IDE e verificação de tipo.\n    \n    **Na prática, você usa dicionários regulares:**\n    ```python\n    # Isso é o que você realmente escreve:\n    cookie = {'name': 'session', 'value': 'abc123', 'domain': 'example.com'}\n    \n    # A anotação de tipo é apenas para sua IDE:\n    from pydoll.protocol.network.types import CookieParam\n    cookie: CookieParam = {'name': 'session', 'value': 'abc123'}\n    ```\n    \n    Todos os exemplos abaixo usam dicionários simples por simplicidade.\n\n### Estrutura do Cookie\n\nO tipo `Cookie` (recuperado do navegador) contém informações completas do cookie:\n\n```python\n{\n    \"name\": str,           # Nome do cookie\n    \"value\": str,          # Valor do cookie\n    \"domain\": str,         # Domínio onde o cookie é válido\n    \"path\": str,           # Caminho onde o cookie é válido\n    \"expires\": float,      # Timestamp Unix (0 = cookie de sessão)\n    \"size\": int,           # Tamanho em bytes\n    \"httpOnly\": bool,      # Acessível apenas via HTTP (não JavaScript)\n    \"secure\": bool,        # Enviado apenas por HTTPS\n    \"session\": bool,       # True se expira quando o navegador fecha\n    \"sameSite\": str,       # \"Strict\", \"Lax\", ou \"None\"\n    \"priority\": str,       # \"Low\", \"Medium\", ou \"High\"\n    \"sourceScheme\": str,   # \"Unset\", \"NonSecure\", ou \"Secure\"\n    \"sourcePort\": int,     # Porta onde o cookie foi definido\n}\n```\n\n### Estrutura do CookieParam\n\nAo **definir** cookies, use um dict (apenas `name` e `value` são obrigatórios):\n\n```python\n# Cookie simples com apenas campos obrigatórios\ncookie = {\n    'name': 'user_token',\n    'value': 'token_value'\n}\n\n# Cookie completo com todos os campos opcionais\ncookie = {\n    'name': 'user_token',       # Obrigatório\n    'value': 'token_value',     # Obrigatório\n    'domain': 'example.com',    # Opcional: padrão é o domínio da página atual\n    'path': '/',                # Opcional: padrão é /\n    'secure': True,             # Opcional: Apenas HTTPS\n    'httpOnly': True,           # Opcional: sem acesso JS\n    'sameSite': 'Lax',          # Opcional: 'Strict', 'Lax', ou 'None'\n    'expires': 1735689600,      # Opcional: timestamp Unix\n    'priority': 'High',         # Opcional: 'Low', 'Medium', ou 'High'\n}\n```\n\n!!! info \"Comportamento Padrão de Campos Opcionais\"\n    Quando você omite campos opcionais:\n    \n    - `domain`: Usa o domínio da página atual\n    - `path`: Padrão é `/`\n    - `secure`: Padrão é `False`\n    - `httpOnly`: Padrão é `False`\n    - `sameSite`: Padrão do navegador (geralmente `Lax`)\n    - `expires`: Cookie de sessão (deletado quando o navegador fecha)\n\n## Operações de Gerenciamento de Cookies\n\n### Definindo Cookies\n\n#### Definir Múltiplos Cookies de Uma Vez\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def set_multiple_cookies():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        cookies = [\n            {\n                'name': 'session_id',\n                'value': 'xyz789',\n                'domain': 'example.com',\n                'secure': True,\n                'httpOnly': True,\n                'sameSite': 'Strict'\n            },\n            {\n                'name': 'preferences',\n                'value': 'dark_mode=true',\n                'domain': 'example.com',\n                'path': '/settings'\n            },\n            {\n                'name': 'analytics',\n                'value': 'tracking_id_12345',\n                'domain': 'example.com',\n                'expires': 1735689600  # Expira em data específica\n            }\n        ]\n        \n        await tab.set_cookies(cookies)\n        print(f\"Definidos {len(cookies)} cookies\")\n\nasyncio.run(set_multiple_cookies())\n```\n\n#### Definir Cookies em Contexto Específico\n\n```python\n# Definir cookies em um contexto de navegador específico\ncontext_id = await browser.create_browser_context()\nawait browser.set_cookies(cookies, browser_context_id=context_id)\n```\n\n!!! tip \"Métodos de Aba vs Navegador para Definir Cookies\"\n    - `tab.set_cookies(cookies)`: Define cookies no contexto de navegador da aba (atalho conveniente)\n    - `browser.set_cookies(cookies, browser_context_id=...)`: Define cookies com controle explícito de contexto\n    \n    Ambos os métodos adicionam cookies ao **contexto inteiro**, não apenas à página atual. Os cookies estarão disponíveis para todas as abas naquele contexto.\n\n### Recuperando Cookies\n\n#### Obter Todos os Cookies (Nível do Contexto)\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def get_cookies_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://github.com')\n        \n        # Esperar a página definir cookies\n        await asyncio.sleep(2)\n        \n        # Opção 1: Obter cookies via aba (atalho para o contexto atual)\n        cookies = await tab.get_cookies()\n        \n        # Opção 2: Obter cookies via navegador (controle explícito de contexto)\n        # cookies = await browser.get_cookies()  # Mesmo que tab.get_cookies() para o contexto padrão\n        \n        print(f\"Encontrados {len(cookies)} cookies:\")\n        for cookie in cookies:\n            print(f\"  - {cookie['name']}: {cookie['value'][:20]}...\")\n            print(f\"    Domínio: {cookie['domain']}, Secure: {cookie['secure']}\")\n\nasyncio.run(get_cookies_example())\n```\n\n!!! tip \"Métodos de Aba vs Navegador\"\n    - `tab.get_cookies()`: Retorna cookies do contexto de navegador da aba (atalho conveniente)\n    - `browser.get_cookies()`: Retorna cookies do contexto padrão (ou especifique `browser_context_id`)\n    \n    Ambos os métodos retornam **todos os cookies** do contexto, não apenas os cookies para o domínio da página atual.\n\n!!! warning \"Limitação do Modo Incógnito\"\n    `browser.get_cookies()` **não funciona** com o modo incógnito nativo (flag `--incognito`). Esta é uma limitação do Chrome DevTools Protocol onde `Storage.getCookies` não consegue acessar cookies no modo incógnito nativo.\n    \n    **Solução:** Use `tab.get_cookies()` em vez disso, que usa `Network.getCookies` e funciona corretamente no modo incógnito.\n\n#### Obter Cookies de Contexto Específico\n\n```python\n# Obter cookies de um contexto de navegador específico\ncontext_id = await browser.create_browser_context()\ncookies = await browser.get_cookies(browser_context_id=context_id)\n```\n\n### Deletando Cookies\n\n#### Deletar Todos os Cookies\n\n```python\n# Deletar todos os cookies do contexto da aba atual\nawait tab.delete_all_cookies()\n\n# Deletar todos os cookies de um contexto específico\nawait browser.delete_all_cookies(browser_context_id=context_id)\n```\n\n!!! warning \"Cookies São Deletados Imediatamente\"\n    Quando você deleta cookies, eles são removidos do navegador imediatamente. O site pode não detectar isso até a próxima requisição ou recarregamento da página.\n\n## Casos de Uso Práticos\n\n### 1. Sessões de Login Persistentes\n\nReutilize cookies de autenticação entre execuções do script:\n\n```python\nimport asyncio\nimport json\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nCOOKIE_FILE = Path('cookies.json')\n\nasync def save_cookies_after_login():\n    \"\"\"Fazer login e salvar cookies para uso futuro.\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/login')\n        \n        # Realizar login (simplificado)\n        email = await tab.find(id='email')\n        password = await tab.find(id='password')\n        await email.type_text('user@example.com')\n        await password.type_text('secret')\n        \n        login_btn = await tab.find(id='login')\n        await login_btn.click()\n        await asyncio.sleep(3)\n        \n        # Salvar cookies\n        cookies = await browser.get_cookies()\n        COOKIE_FILE.write_text(json.dumps(cookies, indent=2))\n        print(f\"Salvos {len(cookies)} cookies em {COOKIE_FILE}\")\n\nasync def reuse_saved_cookies():\n    \"\"\"Carregar cookies salvos para pular o login.\"\"\"\n    if not COOKIE_FILE.exists():\n        print(\"Nenhum cookie salvo encontrado. Execute save_cookies_after_login() primeiro.\")\n        return\n    \n    # Carregar cookies do arquivo\n    saved_cookies = json.loads(COOKIE_FILE.read_text())\n    \n    # Converter para formato simplificado (apenas campos obrigatórios)\n    # Nota: get_cookies() retorna objetos Cookie detalhados com campos somente leitura\n    # (size, session, sourceScheme, etc.). set_cookies() espera o formato CookieParam\n    # apenas com os campos configuráveis.\n    cookies_to_set = [\n        {\n            'name': c['name'],\n            'value': c['value'],\n            'domain': c['domain'],\n            'path': c.get('path', '/'),\n            'secure': c.get('secure', False),\n            'httpOnly': c.get('httpOnly', False)\n        }\n        for c in saved_cookies\n    ]\n    \n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Definir cookies antes de navegar\n        await tab.set_cookies(cookies_to_set)\n        print(f\"Carregados {len(cookies_to_set)} cookies do arquivo\")\n        \n        # Navegar - já deve estar logado\n        await tab.go_to('https://example.com/dashboard')\n        await asyncio.sleep(2)\n        \n        # Verificar login\n        try:\n            username = await tab.find(class_name='username')\n            print(f\"Logado como: {await username.text}\")\n        except Exception:\n            print(\"Login falhou - os cookies podem ter expirado\")\n\n# Primeira execução: fazer login e salvar cookies\n# asyncio.run(save_cookies_after_login())\n\n# Execuções subsequentes: reutilizar cookies\nasyncio.run(reuse_saved_cookies())\n```\n\n!!! note \"Reformatação de Cookies Necessária\"\n    `get_cookies()` retorna **objetos `Cookie` detalhados** com atributos somente leitura como `size`, `session`, `sourceScheme` e `sourcePort`. Ao usar `set_cookies()`, você deve fornecer o **formato `CookieParam`** contendo apenas os campos configuráveis (`name`, `value`, `domain`, `path`, `secure`, `httpOnly`, `sameSite`, `expires`, `priority`).\n    \n    A etapa de reformatação no exemplo acima é **essencial**. Passar objetos `Cookie` brutos para `set_cookies()` pode causar erros ou comportamento inesperado.\n\n!!! tip \"Expiração de Cookies\"\n    Sempre verifique se os cookies salvos expiraram. Cookies de sessão (`session=True`) expiram quando o navegador fecha, enquanto cookies persistentes têm um timestamp `expires` que você pode validar.\n\n### 2. Teste de Múltiplas Contas com Cookies Isolados\n\nCada contexto de navegador mantém cookies separados:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def test_multiple_accounts():\n    accounts = [\n        {'email': 'user1@example.com', 'cookie_value': 'session_user1'},\n        {'email': 'user2@example.com', 'cookie_value': 'session_user2'},\n    ]\n    \n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # Primeira conta no contexto padrão\n        cookies_user1 = [{\n            'name': 'session',\n            'value': accounts[0]['cookie_value'],\n            'domain': 'example.com',\n            'secure': True,\n            'httpOnly': True\n        }]\n        await initial_tab.set_cookies(cookies_user1)\n        await initial_tab.go_to('https://example.com/dashboard')\n        \n        # Segunda conta em contexto isolado\n        context2 = await browser.create_browser_context()\n        tab2 = await browser.new_tab(browser_context_id=context2)\n        \n        cookies_user2 = [{\n            'name': 'session',\n            'value': accounts[1]['cookie_value'],\n            'domain': 'example.com',\n            'secure': True,\n            'httpOnly': True\n        }]\n        await browser.set_cookies(cookies_user2, browser_context_id=context2)\n        await tab2.go_to('https://example.com/dashboard')\n        \n        # Ambos os usuários estão logados simultaneamente com sessões diferentes\n        print(\"Usuário 1 e Usuário 2 logados com cookies isolados\")\n        \n        await asyncio.sleep(5)\n        \n        # Limpeza\n        await tab2.close()\n        await browser.delete_browser_context(context2)\n\nasyncio.run(test_multiple_accounts())\n```\n\n### 3. Rotação de Cookies para Scripts de Longa Duração\n\nAtualize os cookies periodicamente para evitar detecção:\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_with_cookie_rotation():\n    urls = [f'https://example.com/page{i}' for i in range(100)]\n    \n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Fazer login inicialmente\n        await tab.go_to('https://example.com/login')\n        # ... realizar login ...\n        await asyncio.sleep(2)\n        \n        last_rotation = time.time()\n        rotation_interval = 600  # Rotacionar a cada 10 minutos\n        \n        for url in urls:\n            # Verificar se é hora de rotacionar os cookies\n            if time.time() - last_rotation > rotation_interval:\n                print(\"Rotacionando sessão...\")\n                \n                # Deletar cookies antigos\n                await tab.delete_all_cookies()\n                \n                # Fazer login novamente ou carregar cookies novos\n                await tab.go_to('https://example.com/login')\n                # ... realizar login novamente ...\n                \n                last_rotation = time.time()\n            \n            # Raspar página\n            await tab.go_to(url)\n            await asyncio.sleep(2)\n            # ... extrair dados ...\n\nasyncio.run(scrape_with_cookie_rotation())\n```\n\n!!! tip \"Frequência de Rotação\"\n    A frequência ideal de rotação depende do seu caso de uso:\n    \n    - **Sites de alta segurança**: Rotacione a cada 5-15 minutos\n    - **Sites normais**: Rotacione a cada 30-60 minutos\n    - **Raspagem de baixo risco**: Rotacione a cada poucas horas\n\n\n## Referência de Atributos de Cookie\n\n| Atributo | Tipo | Descrição | Padrão |\n|---|---|---|---|\n| `name` | `str` | Nome do cookie | *Obrigatório* |\n| `value` | `str` | Valor do cookie | *Obrigatório* |\n| `domain` | `str` | Domínio onde o cookie é válido | Domínio da página atual |\n| `path` | `str` | Caminho onde o cookie é válido | `/` |\n| `secure` | `bool` | Enviar apenas por HTTPS | `False` |\n| `httpOnly` | `bool` | Não acessível via JavaScript | `False` |\n| `sameSite` | `CookieSameSite` | Proteção CSRF: `Strict`, `Lax`, `None` | Padrão do navegador (`Lax`) |\n| `expires` | `float` | Timestamp Unix (0 = cookie de sessão) | `0` (sessão) |\n| `priority` | `CookiePriority` | Prioridade do cookie: `Low`, `Medium`, `High` | `Medium` |\n\n### Valores SameSite\n\n```python\n# Use valores string diretamente no seu dict de cookie:\n\n'sameSite': 'Strict'  # Cookie enviado apenas para requisições do mesmo site\n'sameSite': 'Lax'     # Cookie enviado para navegação de nível superior (padrão)\n'sameSite': 'None'    # Cookie enviado para todas as requisições (requer secure=True)\n\n# Ou use o enum para autocompletar da IDE:\nfrom pydoll.protocol.network.types import CookieSameSite\n\ncookie = {\n    'name': 'session',\n    'value': 'xyz',\n    'sameSite': CookieSameSite.STRICT  # IDE autocompletará: STRICT, LAX, NONE\n}\n```\n\n### Valores de Priority\n\n```python\n# Use valores string diretamente:\n\n'priority': 'Low'     # Baixa prioridade (deletado primeiro quando espaço é necessário)\n'priority': 'Medium'  # Média prioridade (padrão)\n'priority': 'High'    # Alta prioridade (deletado por último)\n\n# Ou use o enum:\nfrom pydoll.protocol.network.types import CookiePriority\n\ncookie = {\n    'name': 'session',\n    'value': 'xyz',\n    'priority': CookiePriority.HIGH  # IDE autocompletará: LOW, MEDIUM, HIGH\n}\n```\n\n## Padrões Comuns\n\n### Gerenciador de Contexto para Cookies Temporários\n\n```python\nfrom contextlib import asynccontextmanager\n\n@asynccontextmanager\nasync def temporary_cookies(browser, tab, cookies):\n    \"\"\"Define cookies temporários, executa código, depois restaura os cookies originais.\"\"\"\n    # Salvar cookies atuais\n    original_cookies = await browser.get_cookies()\n    \n    try:\n        # Definir cookies temporários\n        await tab.delete_all_cookies()\n        await tab.set_cookies(cookies)\n        yield tab\n    finally:\n        # Restaurar cookies originais\n        await tab.delete_all_cookies()\n        cookies_to_restore = [\n            {\n                'name': c['name'],\n                'value': c['value'],\n                'domain': c['domain'],\n                'path': c.get('path', '/')\n            }\n            for c in original_cookies\n        ]\n        await tab.set_cookies(cookies_to_restore)\n\n# Uso\nasync with temporary_cookies(browser, tab, test_cookies):\n    await tab.go_to('https://example.com')\n    # ... realizar ações com cookies temporários ...\n# Cookies originais restaurados automaticamente\n```\n\n!!! tip \"Usando APIs Públicas\"\n    Este gerenciador de contexto aceita tanto `browser` quanto `tab` como parâmetros para usar APIs públicas. Como `tab` não expõe seu `browser` pai como uma propriedade pública, passá-lo explicitamente é a abordagem recomendada para acessar métodos de nível de navegador.\n\n### Comparação de Fingerprint de Cookies\n\n```python\ndef cookie_fingerprint(cookies):\n    \"\"\"Gera um fingerprint simples do estado dos cookies.\"\"\"\n    return {\n        'count': len(cookies),\n        'domains': set(c['domain'] for c in cookies),\n        'names': sorted(c['name'] for c in cookies),\n        'secure_count': sum(1 for c in cookies if c.get('secure')),\n        'httponly_count': sum(1 for c in cookies if c.get('httpOnly')),\n    }\n\n# Comparar estados de cookies\nbefore = await browser.get_cookies()\nawait tab.go_to('https://example.com')\nafter = await browser.get_cookies()\n\nprint(f\"Antes: {cookie_fingerprint(before)}\")\nprint(f\"Depois: {cookie_fingerprint(after)}\")\n```\n\n## Considerações de Segurança\n\n!!! danger \"Nunca Codifique Cookies Sensíveis\"\n    Sempre carregue cookies de autenticação de armazenamento seguro (variáveis de ambiente, arquivos criptografados, gerenciadores de segredos).\n    \n    ```python\n    # Ruim - codificado no código\n    cookies = [{'name': 'session', 'value': 'abc123secret'}]\n    \n    # Bom - carregado do ambiente\n    import os\n    cookies = [{\n        'name': 'session',\n        'value': os.getenv('SESSION_COOKIE'),\n        'domain': os.getenv('COOKIE_DOMAIN')\n    }]\n    ```\n\n!!! warning \"Proteção Contra Roubo de Cookies\"\n    Ao salvar cookies em disco:\n    \n    - Use armazenamento criptografado (ex: biblioteca `cryptography`)\n    - Defina permissões restritivas de arquivo\n    - Nunca envie arquivos de cookies para o controle de versão\n    - Rotacione os cookies regularmente\n\n## Resumo das Melhores Práticas\n\n1.  **Comece com cookies realistas** - Não execute automação com um navegador completamente limpo\n2.  **Rotacione sessões periodicamente** - Evite usar os mesmos cookies por longos períodos\n3.  **Respeite os atributos de segurança dos cookies** - Use `secure`, `httpOnly`, `sameSite` apropriadamente\n4.  **Salve e reutilize cookies de autenticação** - Pule logins repetitivos quando apropriado\n5.  **Isole contextos para testes de múltiplas contas** - Cada contexto tem cookies independentes\n6.  **Monitore a evolução dos cookies** - A navegação real acumula cookies naturalmente\n7.  **Limpe cookies expirados** - Remova cookies inválidos antes de reutilizar\n8.  **Use armazenamento seguro** - Criptografe cookies salvos, nunca codifique segredos\n\n## Veja Também\n\n- **[Contextos de Navegador](contexts.md)** - Ambientes de cookies isolados\n- **[Requisições HTTP](../network/http-requests.md)** - Requisições no contexto do navegador herdam cookies automaticamente\n- **[Interações Semelhantes a Humanas](../automation/human-interactions.md)** - Combine cookies com comportamento realista\n- **[Referência da API: Comandos de Armazenamento](/api/commands/storage_commands/)** - Métodos completos de cookies do CDP\n\nO gerenciamento eficaz de cookies é a base para uma automação de navegador realista. Ao equilibrar o frescor com a persistência e respeitar os atributos de segurança, você pode construir uma automação que se comporta como um usuário real, mantendo-se eficiente e sustentável."
  },
  {
    "path": "docs/pt/features/browser-management/tabs.md",
    "content": "# Gerenciamento de Múltiplas Abas\n\nO Pydoll oferece capacidades sofisticadas de múltiplas abas que permitem fluxos de trabalho de automação complexos, abrangendo várias abas do navegador simultaneamente. Entender como as abas funcionam no Pydoll é essencial para construir uma automação robusta e escalável.\n\n## Entendendo as Abas no Pydoll\n\nNo Pydoll, uma instância de `Tab` representa uma única aba (ou janela) do navegador e fornece a interface principal para todas as operações de automação de página. Cada aba mantém seus próprios:\n\n- **Contexto de execução independente**: JavaScript, DOM e estado da página\n- **Manipuladores de eventos isolados**: Callbacks registrados em uma aba não afetam outras\n- **Monitoramento de rede separado**: Cada aba pode rastrear sua própria atividade de rede\n- **Conexão CDP única**: Comunicação WebSocket direta com o navegador\n\n```mermaid\ngraph LR\n    Browser[Instancia do Navegador] --> Tab1[Aba 1]\n    Browser --> Tab2[Aba 2]\n    Browser --> Tab3[...]\n    \n    Tab1 --> Features1[Contexto<br/>Independente]\n    Tab2 --> Features2[Contexto<br/>Independente]\n```\n\n| Componente da Aba | Descrição | Independência |\n|---|---|---|\n| **Contexto de Execução** | Runtime JavaScript, DOM, estado da página | ✓ Cada aba tem o seu |\n| **Manipuladores de Eventos** | Callbacks registrados para eventos CDP | ✓ Isolados por aba |\n| **Monitoramento de Rede** | Requisições HTTP, respostas, tempos | ✓ Rastreia separadamente |\n| **Conexão CDP** | Canal de comunicação WebSocket | ✓ Conexão direta |\n\n### O que é uma Aba de Navegador?\n\nUma aba de navegador é tecnicamente um **alvo (target) CDP** - um contexto de navegação isolado com seu próprio:\n\n- Document Object Model (DOM)\n- Ambiente de execução JavaScript\n- Pool de conexões de rede\n- Armazenamento de cookies (compartilhado com outras abas no mesmo contexto)\n- Loop de eventos e motor de renderização\n\nCada aba tem um `target_id` único atribuído pelo navegador, que o Pydoll usa para rotear comandos e eventos corretamente.\n\n## Gerenciamento de Instâncias de Aba\n\nA classe `Browser` do Pydoll mantém um registro de instâncias de `Tab` com base no `target_id` de cada aba. Isso garante que múltiplas referências à mesma aba do navegador sempre retornem o mesmo objeto Tab. O Browser armazena essas instâncias em um dicionário interno `_tabs_opened`.\n\n| Benefício | Descrição |\n|---|---|\n| **Eficiência de Recursos** | Uma instância de Tab por aba do navegador, sem duplicatas |\n| **Estado Consistente** | Todas as referências compartilham os mesmos manipuladores de eventos e estado |\n| **Segurança de Memória** | Evita múltiplas conexões WebSocket para o mesmo alvo |\n| **Comportamento Previsível** | Mudanças em uma referência afetam todas as referências |\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def tab_registry_demonstration():\n    async with Chrome() as browser:\n        # Inicia o navegador com a aba inicial\n        tab1 = await browser.start()\n\n        # Obtém a mesma aba através de métodos diferentes\n        # Nota: get_opened_tabs() retorna as abas em ordem inversa (mais nova primeiro)\n        # Então a aba inicial (mais antiga) está no final\n        opened_tabs = await browser.get_opened_tabs()\n        tab2 = opened_tabs[-1]  # A aba inicial é a mais antiga, então é a última\n\n        # Ambas as referências apontam para o mesmo objeto\n        # porque o Browser retorna a mesma instância de seu registro\n        print(f\"Mesma instância? {tab1 is tab2}\")  # True\n        print(f\"Mesmo target ID? {tab1._target_id == tab2._target_id}\")  # True\n\n        # Registrar evento em uma referência afeta a outra\n        await tab1.enable_network_events()\n        print(f\"Eventos de rede na aba 2? {tab2.network_events_enabled}\")  # True\n\n        # O Browser mantém o registro internamente\n        print(f\"Aba registrada no navegador? {tab1._target_id in browser._tabs_opened}\")  # True\n\nasyncio.run(tab_registry_demonstration())\n```\n\n!!! info \"Registro controlado pelo Browser\"\n    A classe Browser gerencia um dicionário `_tabs_opened` indexado por `target_id`. Quando você solicita uma aba (via `new_tab()` ou `get_opened_tabs()`), o Browser verifica este registro primeiro. Se uma instância de Tab já existe para aquele `target_id`, ele retorna a instância existente; caso contrário, cria uma nova e a armazena no registro. (Iframes não geram mais abas separadas — interaja com eles como elementos normais.)\n\n## Criando e Gerenciando Abas\n\n### Iniciando o Navegador\n\nQuando você inicia o navegador, o Pydoll automaticamente cria e retorna uma instância de Tab para a aba inicial do navegador:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def start_browser():\n    async with Chrome() as browser:\n        # Aba inicial é criada automaticamente\n        tab = await browser.start()\n        \n        print(f\"Aba criada com target ID: {tab._target_id}\")\n        await tab.go_to('https://example.com')\n        \n        title = await tab.execute_script('return document.title')\n        print(f\"Título da página: {title}\")\n\nasyncio.run(start_browser())\n```\n\n### Criando Abas Adicionais Programaticamente\n\nUse `browser.new_tab()` para criar abas adicionais com controle total:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def create_multiple_tabs():\n    async with Chrome() as browser:\n        # Começar com a aba inicial\n        main_tab = await browser.start()\n        \n        # Criar abas adicionais com URLs específicas\n        search_tab = await browser.new_tab('https://google.com')\n        docs_tab = await browser.new_tab('https://docs.python.org')\n        news_tab = await browser.new_tab('https://news.ycombinator.com')\n        \n        # Cada aba pode ser controlada independentemente\n        await search_tab.find(name='q')  # Caixa de busca do Google\n        await docs_tab.find(id='search-field')  # Busca da doc do Python\n        await news_tab.find(class_name='storylink', find_all=True)  # Matérias do HN\n        \n        # Obter todas as abas abertas\n        all_tabs = await browser.get_opened_tabs()\n        print(f\"Total de abas: {len(all_tabs)}\")  # 4 (inicial + 3 novas)\n        \n        # Fechar abas específicas quando terminar\n        await search_tab.close()\n        await docs_tab.close()\n        await news_tab.close()\n\nasyncio.run(create_multiple_tabs())\n```\n\n!!! tip \"Parâmetro de URL Opcional\"\n    Você pode criar abas sem especificar uma URL: `await browser.new_tab()`. A aba abrirá com uma página em branco (`about:blank`), pronta para navegação.\n\n### Lidando com Abas Abertas pelo Usuário\n\nQuando usuários clicam em links com `target=\"_blank\"` ou usam \"Abrir em nova aba\", o Pydoll pode detectar e gerenciar essas abas:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def handle_user_tabs():\n    async with Chrome() as browser:\n        main_tab = await browser.start()\n        await main_tab.go_to('https://example.com')\n        \n        # Registrar contagem inicial de abas\n        initial_tabs = await browser.get_opened_tabs()\n        print(f\"Abas iniciais: {len(initial_tabs)}\")\n        \n        # Clicar em um link que abre uma nova aba (target=\"_blank\")\n        external_link = await main_tab.find(text='Open in New Tab')\n        await external_link.click()\n        \n        # Esperar a nova aba abrir\n        await asyncio.sleep(2)\n        \n        # Detectar novas abas\n        current_tabs = await browser.get_opened_tabs()\n        print(f\"Abas atuais: {len(current_tabs)}\")\n        \n        # Encontrar a aba recém-aberta (última da lista)\n        if len(current_tabs) > len(initial_tabs):\n            new_tab = current_tabs[-1]\n            \n            # Trabalhar com a nova aba\n            url = await new_tab.current_url\n            print(f\"URL da nova aba: {url}\")\n            \n            await new_tab.go_to('https://different-site.com')\n            title = await new_tab.execute_script('return document.title')\n            print(f\"Título da nova aba: {title}\")\n            \n            # Fechá-la quando terminar\n            await new_tab.close()\n\nasyncio.run(handle_user_tabs())\n```\n\n### Listando Todas as Abas Abertas\n\nUse `browser.get_opened_tabs()` para recuperar todas as abas atualmente abertas:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def list_tabs():\n    async with Chrome() as browser:\n        # Usar a aba inicial retornada pelo start()\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        \n        # Abrir várias outras abas\n        await browser.new_tab('https://github.com')\n        await browser.new_tab('https://stackoverflow.com')\n        await browser.new_tab('https://reddit.com')\n        \n        # Obter todas as abas\n        all_tabs = await browser.get_opened_tabs()\n        \n        # Inspecionar cada aba\n        for i, tab in enumerate(all_tabs, 1):\n            url = await tab.current_url\n            title = await tab.execute_script('return document.title')\n            print(f\"Aba {i}: {title} - {url}\")\n\nasyncio.run(list_tabs())\n```\n\n## Operações Concorrentes de Abas\n\nA arquitetura assíncrona do Pydoll permite fluxos de trabalho concorrentes poderosos em múltiplas abas:\n\n### Coleta de Dados Paralela\n\nProcesse múltiplas páginas simultaneamente para máxima eficiência:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_page(tab, url):\n    \"\"\"Raspar uma única página dentro de uma aba específica.\"\"\"\n    await tab.go_to(url)\n    title = await tab.execute_script('return document.title')\n    articles = await tab.find(class_name='article', find_all=True)\n    content = [await article.text for article in articles[:5]]\n\n    return {\n        'url': url,\n        'title': title,\n        'articles_count': len(articles),\n        'sample_content': content\n    }\n\nasync def concurrent_scraping():\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n        'https://example.com/page4',\n    ]\n\n    async with Chrome() as browser:\n        # Iniciar o navegador e abrir a primeira aba\n        initial_tab = await browser.start()\n        # Criar uma aba por URL\n        tabs = [initial_tab] + [await browser.new_tab() for _ in urls[1:]]\n\n        # Executar todos os scrapers concorrentemente\n        results = await asyncio.gather(*[\n            scrape_page(tab, url) for tab, url in zip(tabs, urls)\n        ])\n\n        # Exibir resultados\n        for result in results:\n            print(f\"\\n{result['title']}\")\n            print(f\"  URL: {result['url']}\")\n            print(f\"  Artigos: {result['articles_count']}\")\n            if result['sample_content']:\n                print(f\"  Amostra: {result['sample_content'][0][:100]}...\")\n\nasyncio.run(concurrent_scraping())\n```\n\n!!! tip \"Ganho de Desempenho\"\n    A raspagem concorrente pode reduzir o tempo total de execução em 5 a 10 vezes em comparação com o processamento sequencial, especialmente para tarefas limitadas por I/O (entrada/saída) como carregamento de página.\n\n### Fluxos de Trabalho Coordenados de Múltiplas Abas\n\nOrquestre fluxos de trabalho complexos que exigem a interação de múltiplas abas:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\n\nasync def multi_tab_workflow():\n    async with Chrome() as browser:\n        # Usar a aba inicial para login\n        login_tab = await browser.start()\n        await login_tab.go_to('https://app.example.com/login')\n        await asyncio.sleep(2)\n        \n        username = await login_tab.find(id='username')\n        password = await login_tab.find(id='password')\n        \n        await username.type_text('admin@example.com')\n        await password.type_text('secure_password')\n        \n        login_btn = await login_tab.find(id='login')\n        await login_btn.click()\n        await asyncio.sleep(3)\n        \n        # Aba 2: Navegar para a página de exportação de dados\n        export_tab = await browser.new_tab('https://app.example.com/export')\n        await asyncio.sleep(2)\n        \n        export_btn = await export_tab.find(text='Export Data')\n        await export_btn.click()\n        \n        # Aba 3: Monitorar chamadas de API em um dashboard\n        monitor_tab = await browser.new_tab('https://app.example.com/dashboard')\n        await monitor_tab.enable_network_events()\n        \n        # Rastrear chamadas de API\n        api_calls = []\n        async def track_api(event: RequestWillBeSentEvent):\n            url = event['params']['request']['url']\n            if '/api/' in url:\n                api_calls.append(url)\n        \n        await monitor_tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, track_api)\n        await asyncio.sleep(5)\n        \n        print(f\"Rastreadas {len(api_calls)} chamadas de API:\")\n        for call in api_calls[:10]:\n            print(f\"  - {call}\")\n        \n        # Limpeza\n        await login_tab.close()\n        await export_tab.close()\n        await monitor_tab.close()\n\nasyncio.run(multi_tab_workflow())\n```\n\n## Ciclo de Vida e Limpeza da Aba\n\n### Fechamento Explícito de Aba\n\nSempre feche as abas quando terminar de usá-las para liberar recursos do navegador:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def explicit_cleanup():\n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # Criar abas para diferentes tarefas\n        tab1 = await browser.new_tab('https://example.com')\n        tab2 = await browser.new_tab('https://example.org')\n        \n        # Trabalhar com as abas\n        await tab1.go_to('https://different-site.com')\n        await tab2.take_screenshot('/tmp/screenshot.png')\n        \n        # Fechar abas explicitamente\n        await tab1.close()\n        await tab2.close()\n        \n        # Verificar se as abas estão fechadas\n        remaining = await browser.get_opened_tabs()\n        print(f\"Abas restantes: {len(remaining)}\")  # Deve ser 1 (inicial)\n\nasyncio.run(explicit_cleanup())\n```\n\n!!! warning \"Vazamentos de Memória\"\n    Deixar de fechar abas em automações de longa duração pode levar ao esgotamento da memória. Cada aba consome recursos do navegador (memória, handles de arquivo, conexões de rede).\n\n### Usando Gerenciadores de Contexto para Limpeza Automática\n\nEmbora o Pydoll não forneça um gerenciador de contexto de aba nativo, você pode criar o seu:\n\n```python\nimport asyncio\nfrom contextlib import asynccontextmanager\nfrom pydoll.browser.chromium import Chrome\n\n@asynccontextmanager\nasync def managed_tab(browser, url=None):\n    \"\"\"Gerenciador de contexto para limpeza automática de abas.\"\"\"\n    tab = await browser.new_tab(url)\n    try:\n        yield tab\n    finally:\n        await tab.close()\n\nasync def auto_cleanup_example():\n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # Aba fecha automaticamente ao sair do contexto\n        async with managed_tab(browser, 'https://example.com') as tab:\n            title = await tab.execute_script('return document.title')\n            print(f\"Título: {title}\")\n            \n            await tab.take_screenshot('/tmp/page.png')\n        # Aba é fechada automaticamente aqui\n        \n        tabs = await browser.get_opened_tabs()\n        print(f\"Abas após sair do contexto: {len(tabs)}\")  # 1 (apenas initial_tab)\n\nasyncio.run(auto_cleanup_example())\n```\n\n### Limpeza do Navegador\n\nQuando o navegador fecha, todas as abas são fechadas automaticamente:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def browser_cleanup():\n    # Usando gerenciador de contexto - limpeza automática\n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # Criar múltiplas abas\n        await browser.new_tab('https://example.com')\n        await browser.new_tab('https://github.com')\n        await browser.new_tab('https://stackoverflow.com')\n        \n        tabs = await browser.get_opened_tabs()\n        print(f\"Abas abertas: {len(tabs)}\")  # 4 (inicial + 3 novas)\n    \n    # Todas as abas são fechadas automaticamente quando o navegador sai\n    print(\"Navegador fechado, todas as abas limpas\")\n\nasyncio.run(browser_cleanup())\n```\n\n## Gerenciamento de Estado da Aba\n\n### Verificando o Estado da Aba\n\nConsulte vários aspectos do estado atual de uma aba:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def check_tab_state():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Verificar URL atual\n        url = await tab.current_url\n        print(f\"URL Atual: {url}\")\n        \n        # Verificar código-fonte da página\n        source = await tab.page_source\n        print(f\"Tamanho do código-fonte: {len(source)} caracteres\")\n        \n        # Verificar domínios de eventos habilitados\n        print(f\"Eventos de página habilitados: {tab.page_events_enabled}\")\n        print(f\"Eventos de rede habilitados: {tab.network_events_enabled}\")\n        print(f\"Eventos DOM habilitados: {tab.dom_events_enabled}\")\n        \n        # Habilitar eventos e verificar novamente\n        await tab.enable_network_events()\n        print(f\"Eventos de rede habilitados: {tab.network_events_enabled}\")  # True\n\nasyncio.run(check_tab_state())\n```\n\n### Identificação da Aba\n\nCada aba possui identificadores únicos:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def tab_identification():\n    async with Chrome() as browser:\n        tab1 = await browser.start()\n        tab2 = await browser.new_tab()\n        \n        # Target ID - identificador único atribuído pelo navegador\n        print(f\"Target ID da Aba 1: {tab1._target_id}\")\n        print(f\"Target ID da Aba 2: {tab2._target_id}\")\n        \n        # Detalhes da conexão\n        print(f\"Porta de conexão da Aba 1: {tab1._connection_port}\")\n        print(f\"Porta de conexão da Aba 2: {tab2._connection_port}\")\n        \n        # ID do contexto do navegador (geralmente None para o contexto padrão)\n        print(f\"ID do contexto da Aba 1: {tab1._browser_context_id}\")\n        print(f\"ID do contexto da Aba 2: {tab2._browser_context_id}\")\n\nasyncio.run(tab_identification())\n```\n\n## Funcionalidades Avançadas de Aba\n\n### Trazendo Abas para a Frente\n\nTorne uma aba específica visível (trazer para o primeiro plano):\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def bring_to_front():\n    async with Chrome() as browser:\n        tab1 = await browser.start()\n        tab2 = await browser.new_tab('https://github.com')\n        tab3 = await browser.new_tab('https://stackoverflow.com')\n        \n        # tab3 está atualmente na frente (última criada)\n        await asyncio.sleep(2)\n        \n        # Trazer tab1 para a frente\n        await tab1.bring_to_front()\n        print(\"Aba 1 trazida para a frente\")\n        \n        await asyncio.sleep(2)\n        \n        # Trazer tab2 para a frente\n        await tab2.bring_to_front()\n        print(\"Aba 2 trazida para a frente\")\n\nasyncio.run(bring_to_front())\n```\n\n### Monitoramento de Rede Específico da Aba\n\nCada aba pode monitorar independentemente sua própria atividade de rede:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def tab_network_monitoring():\n    async with Chrome() as browser:\n        # Usar aba inicial para navegação monitorada\n        tab1 = await browser.start()\n        await tab1.go_to('https://example.com')\n        \n        # Criar segunda aba sem monitoramento\n        tab2 = await browser.new_tab('https://github.com')\n        \n        # Habilitar monitoramento de rede apenas na aba 1\n        await tab1.enable_network_events()\n        \n        # Navegar em ambas as abas\n        await tab1.go_to('https://example.com/page1')\n        await tab2.go_to('https://github.com/explore')\n        \n        await asyncio.sleep(3)\n        \n        # Obter logs de rede apenas da aba 1\n        tab1_logs = await tab1.get_network_logs()\n        print(f\"Requisições de rede da Aba 1: {len(tab1_logs)}\")\n        \n        # tab2 não tem monitoramento de rede\n        print(f\"Eventos de rede da Aba 2 habilitados: {tab2.network_events_enabled}\")  # False\n\nasyncio.run(tab_network_monitoring())\n```\n\n### Manipuladores de Eventos Específicos da Aba\n\nRegistre diferentes manipuladores de eventos em diferentes abas:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.page.events import PageEvent\n\nasync def tab_specific_events():\n    async with Chrome() as browser:\n        # Usar aba inicial como primeira aba\n        tab1 = await browser.start()\n        tab2 = await browser.new_tab()\n        \n        # Habilitar eventos de página em ambas\n        await tab1.enable_page_events()\n        await tab2.enable_page_events()\n        \n        # Manipuladores diferentes para cada aba\n        async def tab1_handler(event):\n            print(\"Aba 1 carregada!\")\n        \n        async def tab2_handler(event):\n            print(\"Aba 2 carregada!\")\n        \n        await tab1.on(PageEvent.LOAD_EVENT_FIRED, tab1_handler)\n        await tab2.on(PageEvent.LOAD_EVENT_FIRED, tab2_handler)\n        \n        # Navegar em ambas as abas\n        await tab1.go_to('https://example.com')\n        await tab2.go_to('https://github.com')\n        \n        await asyncio.sleep(2)\n\nasyncio.run(tab_specific_events())\n```\n\n## Considerações de Desempenho\n\n| Cenário | Impacto nos Recursos | Recomendação |\n|---|---|---|\n| **1-5 abas** | Baixo | Gerenciamento direto, sem tratamento especial |\n| **5-20 abas** | Moderado | Usar semáforos para limitar concorrência |\n| **20-50 abas** | Alto | Processamento em lote, fechar abas agressivamente |\n| **50+ abas** | Muito Alto | Considerar processamento sequencial ou múltiplos navegadores |\n\n### Uso de Memória\n\nCada aba consome aproximadamente:\n\n- **Memória base**: 50-100 MB\n- **Com eventos de rede**: +10-20 MB\n- **Com eventos DOM**: +20-50 MB\n- **Página complexa (SPA)**: +100-300 MB\n\nPara 20 abas com monitoramento de rede: ~1.5-3 GB de memória.\n\n## Padrões Comuns\n\n### Processamento Sequencial com Aba Única\n\n```python\nasync def sequential_pattern():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        for url in urls:\n            await tab.go_to(url)\n            # Extrair dados\n            await tab.clear_callbacks()  # Limpar eventos\n\nasyncio.run(sequential_pattern())\n```\n\n### Processamento Paralelo com Múltiplas Abas\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def parallel_pattern():\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n        'https://example.com/page4',\n    ]\n\n    async with Chrome() as browser:\n        # Iniciar navegador e obter aba inicial\n        initial_tab = await browser.start()\n        # Criar uma aba por URL (reutilizando a inicial para a primeira)\n        tabs = [initial_tab] + [await browser.new_tab() for _ in urls[1:]]\n\n        async def process_page(tab, url):\n            \"\"\"Processar uma única página dentro da aba fornecida.\"\"\"\n            try:\n                await tab.go_to(url)\n                await asyncio.sleep(2)\n                title = await tab.evaluate('document.title')\n                print(f\"[{url}] {title}\")\n            finally:\n                if tab is not initial_tab:\n                    await tab.close()\n\n        # Executar todas as abas concorrentemente\n        await asyncio.gather(*[\n            process_page(tab, url) for tab, url in zip(tabs, urls)\n        ])\n\nasyncio.run(parallel_pattern())\n```\n\n### Padrão de Pool de Workers\n\n```python\nasync def worker_pool_pattern():\n    async with Chrome() as browser:\n        # Usar aba inicial como primeiro worker\n        initial_tab = await browser.start()\n        \n        # Criar abas worker adicionais (5 workers no total: 1 inicial + 4 novas)\n        workers = [initial_tab] + [await browser.new_tab() for _ in range(4)]\n        \n        # Distribuir trabalho entre todos os workers\n        for url in urls:\n            worker = workers[urls.index(url) % len(workers)]\n            await worker.go_to(url)\n            # Processar...\n        \n        # Limpar todos os workers (incluindo aba inicial)\n        for worker in workers:\n            await worker.close()\n\nasyncio.run(worker_pool_pattern())\n```\n\n!!! tip \"Reutilizando a Aba Inicial\"\n    Sempre use a aba retornada por `browser.start()` em vez de deixá-la ociosa. Isso economiza recursos do navegador e melhora o desempenho. Nos exemplos acima, a aba inicial é reutilizada como o primeiro worker ou para a primeira URL do lote.\n\n## Veja Também\n\n- **[Contextos de Navegador](contexts.md)** - Sessões de navegador isoladas\n- **[Cookies e Sessões](cookies-sessions.md)** - Gerenciando cookies entre abas\n- **[Sistema de Eventos](../advanced/event-system.md)** - Manipulação de eventos específica da aba\n- **[Raspagem Concorrente](../../features.md#concurrent-scraping)** - Exemplos do mundo real\n\nO gerenciamento de múltiplas abas no Pydoll fornece a base para construir automação de navegador escalável e eficiente. Ao entender o ciclo de vida da aba, o padrão singleton e as melhores práticas, você pode criar fluxos de trabalho de automação robustos que lidam com cenários complexos de múltiplas páginas com facilidade."
  },
  {
    "path": "docs/pt/features/configuration/browser-options.md",
    "content": "# Opções do Navegador (ChromiumOptions)\n\n`ChromiumOptions` é seu hub central de configuração para personalizar o comportamento do navegador. Ele controla tudo, desde argumentos de linha de comando e localização do binário até estados de carregamento de página e preferências de conteúdo.\n\n!!! info \"Documentação Relacionada\"\n    - **[Preferências do Navegador](browser-preferences.md)** - Análise profunda do sistema interno de preferências do Chromium\n    - **[Gerenciamento do Navegador](../browser-management/tabs.md)** - Trabalhando com instâncias e abas do navegador\n    - **[Contextos](../browser-management/contexts.md)** - Contextos de navegação isolados\n\n## Guia Rápido\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\nasync def main():\n    # Criar e configurar opções\n    options = ChromiumOptions()\n    \n    # Configuração básica\n    options.headless = True\n    options.start_timeout = 15\n    options.page_load_state = PageLoadState.INTERACTIVE\n    \n    # Adicionar argumentos de linha de comando\n    options.add_argument('--disable-gpu')\n    options.add_argument('--window-size=1920,1080')\n    \n    # Métodos auxiliares para configurações comuns\n    options.block_notifications = True\n    options.block_popups = True\n    options.set_default_download_directory('/tmp/downloads')\n    \n    # Usar as opções configuradas\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n\nasyncio.run(main())\n```\n\n## Propriedades Principais\n\n### Argumentos de Linha de Comando\n\nO Chromium suporta centenas de \"switches\" (opções) de linha de comando que controlam o comportamento do navegador no nível mais profundo. Use `add_argument()` para passar flags diretamente para o processo do navegador.\n\n```python\noptions = ChromiumOptions()\n\n# Adicionar argumento único\noptions.add_argument('--disable-blink-features=AutomationControlled')\n\n# Adicionar argumento com valor\noptions.add_argument('--window-size=1920,1080')\noptions.add_argument('--user-agent=Mozilla/5.0 ...')\n\n# Remover argumento se necessário\noptions.remove_argument('--window-size=1920,1080')\n\n# Obter todos os argumentos\nall_args = options.arguments\n```\n\n!!! tip \"Formato dos Argumentos\"\n    - Argumentos começando com `--` são flags: `--headless`, `--disable-gpu`\n    - Argumentos com `=` têm valores: `--window-size=1920,1080`\n    - Alguns aceitam múltiplos valores: `--disable-features=Feature1,Feature2`\n\n**Veja a [Referência de Argumentos de Linha de Comando](#referência-de-argumentos-de-linha-de-comando) abaixo para listas abrangentes.**\n\n### Localização do Binário\n\nEspecifique um executável de navegador personalizado em vez de usar o padrão do sistema:\n\n```python\noptions = ChromiumOptions()\n\n# Linux\noptions.binary_location = '/opt/google/chrome-beta/chrome'\n\n# macOS\noptions.binary_location = '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'\n\n# Windows\noptions.binary_location = r'C:\\Program Files\\Google\\Chrome Beta\\Application\\chrome.exe'\n```\n\n!!! info \"Quando Definir a Localização do Binário\"\n    - Testar diferentes versões do Chrome (Estável, Beta, Canary)\n    - Usar o Chromium em vez do Chrome\n    - Usar instalações portáteis do navegador\n    - Executar compilações específicas para depuração\n\n### Timeout de Inicialização\n\nControle quanto tempo o Pydoll espera para o navegador iniciar e responder:\n\n```python\noptions = ChromiumOptions()\noptions.start_timeout = 20  # segundos (padrão: 10)\n```\n\n!!! warning \"Considerações sobre Timeout\"\n    - **Muito baixo**: O navegador pode não inicializar completamente, causando falhas na inicialização\n    - **Muito alto**: Travamentos bloquearão sua automação por mais tempo\n    - **Recomendado**: 10-15s para a maioria dos casos, 20-30s para sistemas lentos ou perfis de navegador pesados\n\n### Modo Headless (Sem Interface Gráfica)\n\nExecute o navegador sem uma interface de usuário visível:\n\n```python\noptions = ChromiumOptions()\noptions.headless = True  # Adiciona automaticamente o argumento --headless\n\n# Ou manualmente\noptions.add_argument('--headless')\noptions.add_argument('--headless=new')  # Novo modo headless (Chrome 109+)\n```\n\n| Modo | Argumento | Descrição |\n|---|---|---|\n| **Headful** (Com UI) | (nenhum) | Janela do navegador visível (padrão) |\n| **Headless Clássico** | `--headless` | Modo headless legado |\n| **Novo Headless** | `--headless=new` | Modo headless moderno (Chrome 109+, melhor compatibilidade) |\n\n!!! tip \"Novo Modo Headless\"\n    O modo `--headless=new` (Chrome 109+) oferece melhor compatibilidade com recursos web modernos e é mais difícil de detectar. Use-o para automação em produção.\n\n### Estado de Carregamento da Página\n\nControle quando o `tab.go_to()` considera uma página \"carregada\":\n\n```python\nfrom pydoll.constants import PageLoadState\n\noptions = ChromiumOptions()\noptions.page_load_state = PageLoadState.INTERACTIVE  # ou PageLoadState.COMPLETE\n```\n\n| Estado | Quando a Navegação Completa | Caso de Uso |\n|---|---|---|\n| `COMPLETE` (padrão) | Evento `load` disparado, todos os recursos carregados | Esperar por imagens, fontes, scripts |\n| `INTERACTIVE` | `DOMContentLoaded` disparado, DOM pronto | Navegação mais rápida, interagir com o DOM imediatamente |\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\nasync def compare_load_states():\n    # Modo Complete - espera por tudo\n    options_complete = ChromiumOptions()\n    options_complete.page_load_state = PageLoadState.COMPLETE\n    \n    async with Chrome(options=options_complete) as browser:\n        tab = await browser.start()\n        \n        import time\n        start = time.time()\n        await tab.go_to('https://example.com')\n        complete_time = time.time() - start\n        print(f\"Modo COMPLETE: {complete_time:.2f}s\")\n    \n    # Modo Interactive - DOM pronto é suficiente\n    options_interactive = ChromiumOptions()\n    options_interactive.page_load_state = PageLoadState.INTERACTIVE\n    \n    async with Chrome(options=options_interactive) as browser:\n        tab = await browser.start()\n        \n        start = time.time()\n        await tab.go_to('https://example.com')\n        interactive_time = time.time() - start\n        print(f\"Modo INTERACTIVE: {interactive_time:.2f}s\")\n\nasyncio.run(compare_load_states())\n```\n\n!!! tip \"Quando Usar INTERACTIVE\"\n    Use `INTERACTIVE` quando:\n    \n    - Você só precisa de acesso ao DOM, não de imagens/fontes\n    - Raspagem de conteúdo de texto e estrutura\n    - A velocidade é crítica\n    - A página tem muitos recursos de carregamento lento\n    \n    Mantenha `COMPLETE` (padrão) quando:\n    \n    - Tirando screenshots (precisa de imagens carregadas)\n    - Esperando aplicações pesadas em JavaScript inicializarem completamente\n    - Testando o desempenho de carregamento da página\n\n## Referência de Argumentos de Linha de Comando\n\nO Chromium suporta centenas de \"switches\" de linha de comando. Abaixo estão os mais úteis para automação, organizados por categoria.\n\n!!! info \"Referência Completa\"\n    Lista completa de todos os switches do Chromium: [Switches de Linha de Comando do Chromium por Peter Beverloo](https://peter.sh/experiments/chromium-command-line-switches/)\n\n### Desempenho e Gerenciamento de Recursos\n\nOtimize o desempenho do navegador para uma automação mais rápida:\n\n```python\noptions = ChromiumOptions()\n\n# Desabilitar aceleração de GPU (headless, Docker, CI/CD)\noptions.add_argument('--disable-gpu')\noptions.add_argument('--disable-software-rasterizer')\n\n# Reduzir uso de memória\noptions.add_argument('--disable-dev-shm-usage')  # Docker: supera o limite de tamanho do /dev/shm\noptions.add_argument('--disable-extensions')\noptions.add_argument('--disable-background-networking')\n\n# Desabilitar recursos desnecessários\noptions.add_argument('--disable-sync')  # Sincronização de conta Google\noptions.add_argument('--disable-translate')\noptions.add_argument('--disable-background-timer-throttling')\noptions.add_argument('--disable-backgrounding-occluded-windows')\noptions.add_argument('--disable-renderer-backgrounding')\n\n# Otimizações de rede\noptions.add_argument('--disable-features=NetworkPrediction')\noptions.add_argument('--dns-prefetch-disable')\n\n# Janela e renderização\noptions.add_argument('--window-size=1920,1080')\noptions.add_argument('--window-position=0,0')\noptions.add_argument('--force-device-scale-factor=1')\n```\n\n| Argumento | Efeito | Quando Usar |\n|---|---|---|\n| `--disable-gpu` | Sem aceleração por GPU | Headless, Docker, servidores sem GPU |\n| `--disable-dev-shm-usage` | Usar `/tmp` em vez de `/dev/shm` | Contêineres Docker com memória compartilhada pequena |\n| `--disable-extensions` | Não carregar nenhuma extensão | Navegador limpo e rápido para automação |\n| `--window-size=W,H` | Definir dimensões iniciais da janela | Screenshots, viewport consistente |\n| `--force-device-scale-factor=1` | Desabilitar escalonamento high-DPI | Renderização consistente entre sistemas |\n\n### Furtividade (Stealth) e Fingerprinting\n\nTorne sua automação mais difícil de detectar com estes argumentos de linha de comando:\n\n| Argumento | Propósito | Exemplo |\n|---|---|---|\n| `--disable-blink-features=AutomationControlled` | Remove a flag `navigator.webdriver` | Essencial para furtividade |\n| `--user-agent=...` | Define um user agent realista e comum | Corresponder à região/dispositivo alvo |\n| `--use-gl=swiftshader` | Renderizador WebGL por software | Evitar fingerprints de GPU únicos |\n| `--force-webrtc-ip-handling-policy=...` | Prevenir vazamentos de IP via WebRTC | Usar `disable_non_proxied_udp` |\n| `--lang=en-US` | Definir idioma do navegador | Corresponder ao locale alvo |\n| `--accept-lang=en-US,en;q=0.9` | Cabeçalho Accept-Language | Preferências de idioma realistas |\n| `--tz=America/New_York` | Definir fuso horário | Corresponder à região alvo |\n| `--no-first-run` | Pular assistentes de primeira execução | Automação mais limpa |\n| `--no-default-browser-check` | Pular aviso de navegador padrão | Evitar interrupções na UI |\n| `--disable-reading-from-canvas` | Mitigação de fingerprinting de Canvas | Reduzir singularidade |\n| `--disable-features=AudioServiceOutOfProcess` | Mitigação de fingerprinting de Áudio | Reduzir singularidade |\n\n!!! warning \"Corrida Armamentista da Detecção\"\n    Nenhuma técnica isolada garante a indetectabilidade. Combine múltiplas estratégias:\n    \n    1.  **Argumentos de linha de comando** (esta tabela)\n    2.  **Preferências do navegador** - [Preferências do Navegador - Furtividade e Fingerprinting](browser-preferences.md#stealth-fingerprinting)\n    3.  **Interações semelhantes a humanas** - [Interações Semelhantes a Humanas](../automation/human-interactions.md)\n    4.  **Boa reputação de IP** - Use proxies residenciais com histórico limpo\n\n### Segurança e Privacidade\n\nControle recursos de segurança e configurações de privacidade:\n\n```python\noptions = ChromiumOptions()\n\n# Sandbox (desabilite apenas para Docker/CI)\noptions.add_argument('--no-sandbox')  # RISCO DE SEGURANÇA - use apenas em ambientes controlados\noptions.add_argument('--disable-setuid-sandbox')\n\n# HTTPS/SSL\noptions.add_argument('--ignore-certificate-errors')  # Ignorar erros SSL\noptions.add_argument('--ignore-ssl-errors')\noptions.add_argument('--allow-insecure-localhost')\n\n# Privacidade\noptions.add_argument('--disable-features=Translate')\noptions.add_argument('--disable-sync')\noptions.add_argument('--incognito')  # Abrir em modo anônimo\n\n# Concessão automática de permissões (para testes)\noptions.add_argument('--use-fake-ui-for-media-stream')  # Conceder automaticamente câmera/microfone\noptions.add_argument('--use-fake-device-for-media-stream')  # Usar dispositivos falsos\n```\n\n!!! danger \"Avisos sobre o Sandbox\"\n    **`--no-sandbox` é um risco de segurança!** Use-o apenas quando:\n    \n    - Rodando em contêineres Docker (sandbox conflita com isolamento do contêiner)\n    - Ambientes de CI/CD com permissões restritas\n    - Você confia totalmente no conteúdo sendo carregado\n    \n    **Nunca** use `--no-sandbox` quando:\n    \n    - Visitando sites não confiáveis\n    - Rodando código enviado por usuários\n    - Em ambientes de produção com entrada externa\n\n| Argumento | Efeito | Impacto na Segurança |\n|---|---|---|\n| `--no-sandbox` | Desabilita o sandbox do Chrome | **ALTO RISCO** - Permite execução de código |\n| `--ignore-certificate-errors` | Pula validação SSL | **RISCO MÉDIO** - Possibilita ataques MITM |\n| `--incognito` | Modo de navegação privada | Mais seguro - sem estado persistente |\n\n### Depuração e Desenvolvimento\n\nFerramentas para depurar automação e desenvolvimento:\n\n```python\noptions = ChromiumOptions()\n\n# DevTools\noptions.add_argument('--auto-open-devtools-for-tabs')\n\n# Logging\noptions.add_argument('--enable-logging')\noptions.add_argument('--v=1')  # Nível de verbosidade (0-3)\noptions.add_argument('--log-level=0')  # 0=INFO, 1=WARNING, 2=ERROR\n\n# Tratamento de falhas\noptions.add_argument('--disable-crash-reporter')\noptions.add_argument('--no-crash-upload')\n\n# Habilitar recursos experimentais\noptions.add_argument('--enable-features=NetworkService,NetworkServiceInProcess')\noptions.add_argument('--enable-experimental-web-platform-features')\n\n# Depuração de JavaScript\noptions.add_argument('--js-flags=--expose-gc')  # Expõe o coletor de lixo\n```\n\n!!! tip \"Depuração Remota\"\n    O Pydoll gerencia automaticamente a porta de depuração remota. Para acessar o Chrome DevTools:\n    \n    ```python\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Obter a porta de depuração\n        port = browser._connection_port\n        print(f\"DevTools disponível em: http://localhost:{port}\")\n        \n        # Abra esta URL no seu navegador para acessar o DevTools\n    ```\n    \n    **Não** use o argumento `--remote-debugging-port` - ele entrará em conflito com o gerenciamento interno do Pydoll!\n\n### Exibição e Renderização\n\nControle como o navegador renderiza o conteúdo:\n\n```python\noptions = ChromiumOptions()\n\n# Viewport e janela\noptions.add_argument('--window-size=1920,1080')\noptions.add_argument('--window-position=0,0')\noptions.add_argument('--start-maximized')\noptions.add_argument('--start-fullscreen')\n\n# Telas High DPI\noptions.add_argument('--force-device-scale-factor=1')\noptions.add_argument('--high-dpi-support=1')\n\n# Cor e renderização\noptions.add_argument('--force-color-profile=srgb')\noptions.add_argument('--disable-accelerated-2d-canvas')\noptions.add_argument('--disable-accelerated-video-decode')\n\n# Renderização de fontes\noptions.add_argument('--font-render-hinting=none')\noptions.add_argument('--disable-font-subpixel-positioning')\n\n# Animações\noptions.add_argument('--disable-animations')\noptions.add_argument('--wm-window-animations-disabled')\n```\n\n| Argumento | Efeito | Caso de Uso |\n|---|---|---|\n| `--window-size=W,H` | Define as dimensões da janela | Screenshots, viewport consistente |\n| `--start-maximized` | Abre a janela maximizada | Testes de UI, capturas de tela cheia |\n| `--force-device-scale-factor=1` | Desabilita escalonamento DPI | Renderização consistente entre sistemas |\n| `--disable-animations` | Sem animações CSS/UI | Testes mais rápidos, reduz instabilidade |\n\n### Configuração de Proxy\n\nConfigure proxies para todo o tráfego de rede:\n\n```python\noptions = ChromiumOptions()\n\n# Proxy HTTP/HTTPS\noptions.add_argument('--proxy-server=http://proxy.example.com:8080')\n\n# Proxy autenticado\noptions.add_argument('--proxy-server=http://user:pass@proxy.example.com:8080')\n\n# Proxy SOCKS\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\n\n# Ignorar proxy para hosts específicos\noptions.add_argument('--proxy-bypass-list=localhost,127.0.0.1,*.local')\n\n# Arquivo de auto-configuração de proxy (PAC)\noptions.add_argument('--proxy-pac-url=http://proxy.example.com/proxy.pac')\n```\n\n!!! info \"Autenticação de Proxy\"\n    Para proxies que exigem autenticação, o Pydoll lida automaticamente com os desafios de autenticação ao usar o argumento `--proxy-server` com credenciais.\n    \n    Veja **[Interceptação de Requisições](../network/interception.md)** para detalhes sobre a interação do domínio Fetch com proxies.\n\n## Métodos Auxiliares\n\n`ChromiumOptions` fornece métodos convenientes para tarefas comuns de configuração:\n\n### Gerenciamento de Downloads\n\n```python\noptions = ChromiumOptions()\n\n# Definir diretório de download\noptions.set_default_download_directory('/home/user/downloads')\n\n# Perguntar pelo local de download\noptions.prompt_for_download = True  # Perguntar ao usuário onde salvar\noptions.prompt_for_download = False  # Baixar silenciosamente (padrão)\n\n# Permitir múltiplos downloads automáticos\noptions.allow_automatic_downloads = True  # Permitir sem perguntar\noptions.allow_automatic_downloads = False  # Bloquear ou perguntar (padrão)\n```\n\n### Bloqueio de Conteúdo\n\n```python\noptions = ChromiumOptions()\n\n# Bloquear pop-ups\noptions.block_popups = True  # Bloquear (padrão na maioria dos casos)\noptions.block_popups = False  # Permitir\n\n# Bloquear notificações\noptions.block_notifications = True  # Bloquear pedidos\noptions.block_notifications = False  # Permitir que sites perguntem\n```\n\n### Controles de Privacidade\n\n```python\noptions = ChromiumOptions()\n\n# Gerenciador de senhas\noptions.password_manager_enabled = False  # Desabilitar avisos de salvar senha\noptions.password_manager_enabled = True  # Habilitar (padrão)\n\n# Proteção contra vazamento WebRTC (previne exposição do IP real via WebRTC)\noptions.webrtc_leak_protection = True  # Adiciona --force-webrtc-ip-handling-policy=disable_non_proxied_udp\noptions.webrtc_leak_protection = False  # Desabilitar (padrão)\n```\n\n!!! tip \"Proteção contra Vazamento WebRTC\"\n    O WebRTC pode vazar seu endereço IP real mesmo quando estiver usando um proxy. Habilite `webrtc_leak_protection` para bloquear conexões UDP não proxyadas, impedindo que requisições STUN contornem seu proxy. Isso é **essencial** ao usar proxies para anonimato. Veja **[Fundamentos de Rede - WebRTC](../../deep-dive/network/network-fundamentals.md#webrtc-e-vazamento-de-ip)** para detalhes.\n\n### Manuseio de Arquivos\n\n```python\noptions = ChromiumOptions()\n\n# Comportamento de PDF\noptions.open_pdf_externally = True  # Baixar PDFs em vez de visualizar\noptions.open_pdf_externally = False  # Visualizar no navegador (padrão)\n```\n\n### Internacionalização\n\n```python\noptions = ChromiumOptions()\n\n# Idiomas aceitos (afeta o cabeçalho Content-Language)\noptions.set_accept_languages('en-US,en;q=0.9,pt-BR;q=0.8')\n```\n\n## Exemplos de Configuração Completa\n\n### Configuração para Raspagem Rápida\n\nOtimizado para velocidade e eficiência de recursos:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\ndef create_fast_scraping_options() -> ChromiumOptions:\n    \"\"\"Configuração ultrarrápida para web scraping.\"\"\"\n    options = ChromiumOptions()\n    \n    # Headless para velocidade\n    options.headless = True\n    \n    # Carregamentos de página mais rápidos (DOM pronto é suficiente para scraping)\n    options.page_load_state = PageLoadState.INTERACTIVE\n    \n    # Desabilitar recursos desnecessários\n    options.add_argument('--disable-extensions')\n    options.add_argument('--disable-gpu')\n    options.add_argument('--disable-dev-shm-usage')\n    options.add_argument('--disable-background-networking')\n    options.add_argument('--disable-sync')\n    options.add_argument('--disable-translate')\n    \n    # Bloquear conteúdo que retarda o carregamento\n    options.block_notifications = True\n    options.block_popups = True\n    \n    # Desabilitar imagens para carregamento ainda mais rápido (se você não precisar delas)\n    options.add_argument('--blink-settings=imagesEnabled=false')\n    \n    # Otimizações de rede\n    options.add_argument('--disable-features=NetworkPrediction')\n    options.add_argument('--dns-prefetch-disable')\n    \n    return options\n\nasync def fast_scraping_example():\n    options = create_fast_scraping_options()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Navegação e raspagem super rápidas\n        urls = ['https://example.com', 'https://example.org', 'https://example.net']\n        \n        for url in urls:\n            await tab.go_to(url)\n            title = await tab.execute_script('return document.title')\n            print(f\"{url}: {title}\")\n\nasyncio.run(fast_scraping_example())\n```\n\n### Configuração Completa de Furtividade (Stealth)\n\nPara máxima indetectabilidade, combine argumentos de linha de comando com preferências do navegador:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\ndef create_full_stealth_options() -> ChromiumOptions:\n    \"\"\"Configuração completa de furtividade combinando argumentos e preferências.\"\"\"\n    options = ChromiumOptions()\n    \n    # ===== Argumentos de Linha de Comando =====\n    \n    # Furtividade principal\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    options.add_argument('--disable-features=IsolateOrigins,site-per-process')\n    \n    # User agent (use um recente e comum)\n    options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36')\n    \n    # Idioma e locale\n    options.add_argument('--lang=en-US')\n    options.add_argument('--accept-lang=en-US,en;q=0.9')\n    \n    # WebGL (renderizador por software para evitar assinaturas de GPU únicas)\n    options.add_argument('--use-gl=swiftshader')\n    options.add_argument('--disable-features=WebGLDraftExtensions')\n    \n    # Prevenção de vazamento de IP via WebRTC\n    options.webrtc_leak_protection = True\n\n    # Permissões e primeira execução\n    options.add_argument('--no-first-run')\n    options.add_argument('--no-default-browser-check')\n    \n    # Tamanho da janela (resolução comum)\n    options.add_argument('--window-size=1920,1080')\n    \n    # ===== Preferências do Navegador =====\n    # Para configuração abrangente de preferências do navegador, veja:\n    # https://pydoll.tech/docs/features/configuration/browser-preferences/#stealth-fingerprinting\n    \n    return options\n\nasync def stealth_automation_example():\n    options = create_full_stealth_options()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Testar em sites de detecção de bots\n        await tab.go_to('https://bot.sannysoft.com')\n        await asyncio.sleep(5)\n        \n        # Sua automação aqui...\n\nasyncio.run(stealth_automation_example())\n```\n\n!!! warning \"Consistência do User-Agent é Crítica\"\n    Definir `--user-agent` altera apenas o **cabeçalho HTTP**, mas os sistemas de detecção também verificam `navigator.userAgent`, `navigator.platform`, `navigator.vendor` e outras propriedades JavaScript. **Inconsistências entre esses valores são um forte indicador de bot.**\n    \n    Por exemplo, se o seu User-Agent HTTP diz \"Windows\" mas o `navigator.platform` diz \"Linux\", você será sinalizado imediatamente.\n    \n    **Solução**: Você deve também sobrescrever as propriedades JavaScript via CDP para manter a consistência. Veja **[Fingerprinting do Navegador - Consistência do User-Agent](../../deep-dive/fingerprinting/browser-fingerprinting.md#user-agent-consistency)** para explicação detalhada e implementação usando `Page.addScriptToEvaluateOnNewDocument`.\n    \n    É por isso que a furtividade abrangente requer tanto argumentos de linha de comando QUANTO configuração de preferências do navegador.\n\n!!! tip \"Estratégia Completa de Furtividade\"\n    Argumentos de linha de comando são apenas parte da solução. Para máxima furtividade:\n    \n    1.  **Use os argumentos acima** (navigator.webdriver, WebGL, WebRTC)\n    2.  **Configure as preferências do navegador** - Veja [Preferências do Navegador - Furtividade e Fingerprinting](browser-preferences.md#stealth-fingerprinting)\n    3.  **Interações semelhantes a humanas** - Veja [Interações Semelhantes a Humanas](../automation/human-interactions.md)\n    4.  **Boa reputação de IP/proxy** - Use proxies residenciais\n\n### Configuração para Docker/CI\n\nPara ambientes contêinerizados:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\ndef create_docker_options() -> ChromiumOptions:\n    \"\"\"Configuração para contêineres Docker e CI/CD.\"\"\"\n    options = ChromiumOptions()\n    \n    # Necessário para Docker\n    options.headless = True\n    options.add_argument('--no-sandbox')  # Sandbox conflita com isolamento do contêiner\n    options.add_argument('--disable-dev-shm-usage')  # Supera o limite de tamanho do /dev/shm\n    \n    # Estabilidade\n    options.add_argument('--disable-gpu')\n    options.add_argument('--disable-software-rasterizer')\n    \n    # Otimização de memória\n    options.add_argument('--disable-extensions')\n    options.add_argument('--disable-background-networking')\n    \n    # Carregamentos de página mais rápidos para CI\n    options.page_load_state = PageLoadState.INTERACTIVE\n    \n    # Aumentar timeout para runners de CI lentos\n    options.start_timeout = 20\n    \n    # Tratamento de falhas\n    options.add_argument('--disable-crash-reporter')\n    \n    return options\n\nasync def ci_testing_example():\n    options = create_docker_options()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Rode seus testes...\n        await tab.go_to('https://example.com')\n        assert await tab.execute_script('return document.title') == 'Example Domain'\n\nasyncio.run(ci_testing_example())\n```\n\n## Solução de Problemas\n\n### O Navegador Não Inicia\n\n```python\n# Aumente o timeout\noptions.start_timeout = 30\n\n# Verifique a localização do binário\noptions.binary_location = '/path/to/chrome'\n\n# Problemas com Docker/CI\noptions.add_argument('--no-sandbox')\noptions.add_argument('--disable-dev-shm-usage')\n```\n\n### Desempenho Lento\n\n```python\n# Desabilite a GPU se não for necessária\noptions.add_argument('--disable-gpu')\n\n# Desabilite imagens\noptions.add_argument('--blink-settings=imagesEnabled=false')\n\n# Use o estado de carregamento INTERACTIVE\noptions.page_load_state = PageLoadState.INTERACTIVE\n\n# Desabilite recursos desnecessários\noptions.add_argument('--disable-extensions')\noptions.add_argument('--disable-background-networking')\n```\n\n### Problemas de Memória no Docker\n\n```python\n# Essencial para Docker\noptions.add_argument('--disable-dev-shm-usage')\n\n# Reduzir consumo de memória\noptions.add_argument('--disable-extensions')\noptions.add_argument('--disable-gpu')\noptions.add_argument('--single-process')  # Último recurso (pode ser instável)\n```\n\n## Leitura Adicional\n\n- **[Preferências do Navegador](browser-preferences.md)** - Sistema interno de preferências do Chromium\n- **[Automação Furtiva](../automation/human-interactions.md)** - Interações semelhantes a humanas\n- **[Contextos](../browser-management/contexts.md)** - Contextos de navegação isolados\n- **[Interceptação de Rede](../network/interception.md)** - Manipulação de requisições/respostas\n\n!!! tip \"Experimentação é Chave\"\n    A configuração do navegador é altamente dependente do seu caso de uso específico. Comece com os exemplos aqui, depois ajuste com base em suas necessidades. Use `browser._connection_port` para acessar o DevTools e inspecionar o que está acontecendo dentro do navegador."
  },
  {
    "path": "docs/pt/features/configuration/browser-preferences.md",
    "content": "# Preferências Personalizadas do Navegador\n\nUma das funcionalidades mais poderosas do Pydoll é o acesso direto ao sistema interno de preferências do Chromium. Diferente das ferramentas tradicionais de automação de navegador que expõem apenas um conjunto limitado de opções, o Pydoll oferece o mesmo nível de controle que extensões e administradores corporativos têm, permitindo que você configure **qualquer** configuração de navegador disponível no código-fonte do Chromium.\n\n## Por que as Preferências do Navegador Importam\n\nAs preferências do navegador controlam cada aspecto de como o Chromium se comporta:\n\n- **Desempenho**: Desabilite recursos que você não precisa para carregamentos de página mais rápidos\n- **Privacidade**: Controle quais dados o navegador coleta e envia\n- **Automação**: Remova prompts e confirmações do usuário que quebram fluxos de trabalho\n- **Furtividade (Stealth)**: Crie fingerprints de navegador realistas para evitar detecção\n- **Corporativo**: Aplique políticas tipicamente disponíveis apenas através de Política de Grupo (Group Policy)\n\n!!! info \"O Poder do Acesso Direto\"\n    A maioria das ferramentas de automação expõe apenas 10-20 configurações comuns. O Pydoll lhe dá acesso a **centenas** de preferências, desde o comportamento de download até sugestões de busca, da predição de rede ao gerenciamento de plugins. Se o Chromium pode fazer, você pode configurar.\n\n## Guia Rápido\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def preferences_example():\n    options = ChromiumOptions()\n    \n    # Definir preferências usando um dict\n    options.browser_preferences = {\n        'download': {\n            'default_directory': '/tmp/downloads',\n            'prompt_for_download': False\n        },\n        'profile': {\n            'default_content_setting_values': {\n                'notifications': 2  # Bloquear notificações\n            }\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Downloads vão para /tmp/downloads automaticamente\n        # Nenhum prompt de notificação aparecerá\n\nasyncio.run(preferences_example())\n```\n\n## Entendendo as Preferências do Navegador\n\n### O que são Preferências?\n\nO Chromium armazena todas as configurações configuráveis pelo usuário em um arquivo JSON chamado `Preferences`, localizado no diretório de dados do usuário do navegador. Este arquivo contém **tudo**, desde a URL da sua página inicial até se as imagens carregam automaticamente.\n\n**Localização típica:**\n\n- **Linux**: `~/.config/google-chrome/Default/Preferences`\n- **macOS**: `~/Library/Application Support/Google/Chrome/Default/Preferences`\n- **Windows**: `%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Preferences`\n\n### Estrutura do Arquivo de Preferências\n\nO arquivo Preferences é um objeto JSON aninhado:\n\n```json\n{\n  \"download\": {\n    \"default_directory\": \"/home/user/Downloads\",\n    \"prompt_for_download\": true\n  },\n  \"profile\": {\n    \"default_content_setting_values\": {\n      \"notifications\": 1,\n      \"popups\": 0\n    },\n    \"password_manager_enabled\": true\n  },\n  \"search\": {\n    \"suggest_enabled\": true\n  },\n  \"net\": {\n    \"network_prediction_options\": 1\n  }\n}\n```\n\nCada nome de preferência separado por pontos no código-fonte do Chromium mapeia para um caminho JSON aninhado:\n\n- `download.default_directory` → `{'download': {'default_directory': ...}}`\n- `profile.password_manager_enabled` → `{'profile': {'password_manager_enabled': ...}}`\n\n### Como o Chromium Usa as Preferências\n\nQuando o Chromium inicia:\n\n1.  **Lê** o arquivo Preferences do disco\n2.  **Aplica** essas configurações para configurar o comportamento do navegador\n3.  **Atualiza** o arquivo quando os usuários alteram configurações via UI\n4.  **Recorre aos padrões** se as preferências estiverem ausentes\n\nO Pydoll intercepta o passo 1 pré-populando o arquivo Preferences antes do navegador iniciar, garantindo que suas configurações personalizadas sejam aplicadas desde o primeiro carregamento da página.\n\n## Como Funciona no Pydoll\n\n### Definindo Preferências\n\nUse a propriedade `browser_preferences` para definir qualquer preferência:\n\n```python\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\n\n# Atribuição direta - mescla com preferências existentes\noptions.browser_preferences = {\n    'download': {'default_directory': '/tmp'},\n    'intl': {'accept_languages': 'pt-BR,en-US'}\n}\n\n# Múltiplas atribuições são mescladas, não substituídas\noptions.browser_preferences = {\n    'profile': {'password_manager_enabled': False}\n}\n\n# Ambos os conjuntos de preferências estão agora ativos\n```\n\n!!! warning \"Preferências São Mescladas, Não Substituídas\"\n    Quando você define `browser_preferences` múltiplas vezes, as novas preferências são **mescladas** com as existentes. Apenas as chaves específicas que você define são atualizadas; todo o resto é preservado.\n    \n    ```python\n    options.browser_preferences = {'download': {'prompt': False}}\n    options.browser_preferences = {'profile': {'password_manager_enabled': False}}\n    \n    # Resultado: AMBAS as preferências são definidas\n    # {'download': {'prompt': False}, 'profile': {'password_manager_enabled': False}}\n    ```\n\n### Sintaxe de Caminho Aninhado\n\nAs preferências usam dicionários aninhados que espelham a notação de pontos do Chromium:\n\n```python\n# Constante do código-fonte do Chromium:\n# const char kDownloadDefaultDirectory[] = \"download.default_directory\";\n\n# Traduz para dict Python:\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/path/to/downloads'\n    }\n}\n```\n\nQuanto mais profundo o aninhamento, mais específica a preferência:\n\n```python\n# Nível superior: profile\n# Segundo nível: default_content_setting_values  \n# Terceiro nível: notifications\n\noptions.browser_preferences = {\n    'profile': {\n        'default_content_setting_values': {\n            'notifications': 2,  # Bloquear\n            'geolocation': 2,    # Bloquear\n            'media_stream': 2    # Bloquear\n        }\n    }\n}\n```\n\n## Casos de Uso Práticos\n\n### 1. Otimização de Desempenho\n\nDesabilite recursos que consomem muitos recursos para automação mais rápida:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def performance_optimized_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        # Desabilitar predição de rede e prefetching\n        'net': {\n            'network_prediction_options': 2  # 2 = Nunca prever\n        },\n        # Desabilitar carregamento de imagens\n        'profile': {\n            'default_content_setting_values': {\n                'images': 2  # 2 = Bloquear, 1 = Permitir\n            }\n        },\n        # Desabilitar plugins\n        'webkit': {\n            'webprefs': {\n                'plugins_enabled': False\n            }\n        },\n        # Desabilitar verificação ortográfica\n        'browser': {\n            'enable_spellchecking': False\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Páginas carregam 3-5x mais rápido sem imagens e recursos desnecessários\n        await tab.go_to('https://example.com')\n        print(\"Carregamento rápido completo!\")\n\nasyncio.run(performance_optimized_browser())\n```\n\n!!! tip \"Impacto no Desempenho\"\n    Apenas desabilitar imagens pode reduzir o tempo de carregamento da página em 50-70% para sites pesados em imagens. Combine com a desabilitação de prefetch, verificação ortográfica e plugins para velocidade máxima.\n\n### 2. Privacidade e Anti-Rastreamento\n\nCrie uma configuração de navegador focada em privacidade:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def privacy_focused_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        # Habilitar Do Not Track\n        'enable_do_not_track': True,\n        \n        # Desabilitar referrers\n        'enable_referrers': False,\n        \n        # Desabilitar Safe Browsing (envia URLs para o Google)\n        'safebrowsing': {\n            'enabled': False\n        },\n        \n        # Desabilitar gerenciador de senhas\n        'profile': {\n            'password_manager_enabled': False\n        },\n        \n        # Desabilitar preenchimento automático\n        'autofill': {\n            'enabled': False,\n            'profile_enabled': False\n        },\n        \n        # Desabilitar sugestões de busca (envia consultas para o motor de busca)\n        'search': {\n            'suggest_enabled': False\n        },\n        \n        # Desabilitar telemetria e métricas\n        'user_experience_metrics': {\n            'reporting_enabled': False\n        },\n        \n        # Bloquear cookies de terceiros\n        'profile': {\n            'block_third_party_cookies': True,\n            'cookie_controls_mode': 1\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        print(\"Navegador focado em privacidade pronto!\")\n\nasyncio.run(privacy_focused_browser())\n```\n\n### 3. Downloads Silenciosos\n\nAutomatize downloads de arquivos sem interação do usuário:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def silent_download_automation():\n    download_dir = Path.home() / 'automation_downloads'\n    download_dir.mkdir(exist_ok=True)\n    \n    options = ChromiumOptions()\n    options.browser_preferences = {\n        'download': {\n            'default_directory': str(download_dir),\n            'prompt_for_download': False,\n            'directory_upgrade': True\n        },\n        'profile': {\n            'default_content_setting_values': {\n                'automatic_downloads': 1  # 1 = Permitir, 2 = Bloquear\n            }\n        },\n        # Sempre baixar PDFs em vez de abrir no visualizador\n        'plugins': {\n            'always_open_pdf_externally': True\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/downloads')\n        \n        # Clicar em links de download - arquivos salvam automaticamente\n        download_link = await tab.find(text='Download Report')\n        await download_link.click()\n        \n        await asyncio.sleep(3)\n        print(f\"Arquivo baixado para: {download_dir}\")\n\nasyncio.run(silent_download_automation())\n```\n\n### 4. Bloquear Elementos de UI Intrusivos\n\nRemova popups, notificações e prompts que quebram a automação:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def clean_ui_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        'profile': {\n            'default_content_setting_values': {\n                'notifications': 2,      # Bloquear notificações\n                'popups': 0,             # Bloquear popups\n                'geolocation': 2,        # Bloquear requisições de localização\n                'media_stream': 2,       # Bloquear acesso à câmera/microfone\n                'media_stream_mic': 2,   # Bloquear microfone\n                'media_stream_camera': 2 # Bloquear câmera\n            }\n        },\n        # Desabilitar prompts de tradução\n        'translate': {\n            'enabled': False\n        },\n        # Desabilitar prompt de salvar senha\n        'credentials_enable_service': False,\n        \n        # Desabilitar infobar \"O Chrome está sendo controlado por automação\"\n        'devtools': {\n            'preferences': {\n                'currentDockState': '\"undocked\"'\n            }\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        # Sem popups, sem prompts, automação limpa!\n\nasyncio.run(clean_ui_browser())\n```\n\n### 5. Internacionalização e Localização\n\nConfigure preferências de idioma e localidade:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def localized_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        # Idiomas aceitos (ordem de prioridade)\n        'intl': {\n            'accept_languages': 'pt-BR,pt,en-US,en'\n        },\n        \n        # Idiomas da verificação ortográfica\n        'spellcheck': {\n            'dictionaries': ['pt-BR', 'en-US']\n        },\n        \n        # Configurações de tradução\n        'translate': {\n            'enabled': True\n        },\n        'translate_blocked_languages': ['en'],  # Não oferecer para traduzir Inglês\n        \n        # Codificação de caracteres padrão\n        'default_charset': 'UTF-8'\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        # Navegador configurado para Português do Brasil\n\nasyncio.run(localized_browser())\n```\n\n## Métodos Auxiliares\n\nPara cenários comuns, o Pydoll fornece métodos de conveniência:\n\n```python\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\n\n# Gerenciamento de download\noptions.set_default_download_directory('/tmp/downloads')\noptions.prompt_for_download = False\noptions.allow_automatic_downloads = True\noptions.open_pdf_externally = True\n\n# Bloqueio de conteúdo\noptions.block_notifications = True\noptions.block_popups = True\n\n# Privacidade\noptions.password_manager_enabled = False\n\n# Internacionalização\noptions.set_accept_languages('pt-BR,en-US,en')\n```\n\nEsses métodos são atalhos que definem as preferências aninhadas corretas para você:\n\n```python\n# Este auxiliar:\noptions.set_default_download_directory('/tmp')\n\n# É equivalente a:\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/tmp'\n    }\n}\n```\n\n!!! tip \"Combine Auxiliares com Preferências Diretas\"\n    Use auxiliares para configurações comuns e `browser_preferences` para configurações avançadas:\n    \n    ```python\n    # Comece com auxiliares\n    options.block_notifications = True\n    options.prompt_for_download = False\n    \n    # Adicione preferências avançadas\n    options.browser_preferences = {\n        'net': {'network_prediction_options': 2},\n        'webkit': {'webprefs': {'plugins_enabled': False}}\n    }\n    ```\n\n## Encontrando Preferências no Código-Fonte do Chromium\n\n### Referência do Código-Fonte\n\nO Chromium define todas as constantes de preferência em `pref_names.cc`:\n\n**Fonte oficial**: [chromium/src/+/main/chrome/common/pref_names.cc](https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.cc)\n\n### Lendo o Código-Fonte\n\nAs constantes de preferência usam notação de pontos que mapeia diretamente para dicts aninhados:\n\n```cpp\n// Do código-fonte do Chromium (pref_names.cc):\nconst char kDownloadDefaultDirectory[] = \"download.default_directory\";\nconst char kPromptForDownload[] = \"download.prompt_for_download\";\nconst char kSafeBrowsingEnabled[] = \"safebrowsing.enabled\";\nconst char kBlockThirdPartyCookies[] = \"profile.block_third_party_cookies\";\n```\n\n**Converte para Python:**\n\n```python\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/path/to/dir',\n        'prompt_for_download': False\n    },\n    'safebrowsing': {\n        'enabled': False\n    },\n    'profile': {\n        'block_third_party_cookies': True\n    }\n}\n```\n\n### Processo de Descoberta\n\n1.  **Pesquise no código-fonte**: Vá para [pref_names.cc](https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.cc)\n2.  **Encontre sua preferência**: Pesquise por palavras-chave (ex: \"download\", \"password\", \"notification\")\n3.  **Anote o nome da constante**: ex: `kDownloadDefaultDirectory[] = \"download.default_directory\"`\n4.  **Converta para dict**: Divida pelos pontos e crie a estrutura aninhada\n\n**Exemplo - Encontrando preferências de notificação:**\n\n```cpp\n// Pesquise por \"notification\" em pref_names.cc:\nconst char kPushMessagingAppIdentifierMap[] = \n    \"gcm.push_messaging_application_id_map\";\nconst char kDefaultNotificationsSetting[] = \n    \"profile.default_content_setting_values.notifications\";\n```\n\n```python\n# Torna-se:\noptions.browser_preferences = {\n    'profile': {\n        'default_content_setting_values': {\n            'notifications': 2  # 2 = bloquear, 1 = permitir, 0 = perguntar\n        }\n    }\n}\n```\n\n### Padrões Comuns de Preferência\n\n| Categoria | Exemplo de Constante | Caminho do Dict Python |\n|---|---|---|\n| Downloads | `download.default_directory` | `{'download': {'default_directory': ...}}` |\n| Config. de Conteúdo | `profile.default_content_setting_values.X` | `{'profile': {'default_content_setting_values': {'X': ...}}}` |\n| Rede | `net.network_prediction_options` | `{'net': {'network_prediction_options': ...}}` |\n| Privacidade | `safebrowsing.enabled` | `{'safebrowsing': {'enabled': ...}}` |\n| Sessão | `session.restore_on_startup` | `{'session': {'restore_on_startup': ...}}` |\n\n!!! warning \"Preferências Não Documentadas\"\n    Nem todas as preferências estão documentadas. Algumas são:\n    \n    - **Experimentais**: Podem mudar ou ser removidas em futuras versões do Chromium\n    - **Internas**: Usadas pelos sistemas internos do Chromium\n    - **Específicas da plataforma**: Funcionam apenas em certos sistemas operacionais\n    \n    Teste exaustivamente antes de confiar em preferências não documentadas.\n\n## Referência de Preferências Úteis\n\nAqui está uma lista selecionada de preferências interessantes e úteis do `pref_names.cc` do Chromium:\n\n### Configurações de Conteúdo e Mídia\n\n```python\noptions.browser_preferences = {\n    'profile': {\n        'default_content_setting_values': {\n            # Controle de conteúdo (0=perguntar, 1=permitir, 2=bloquear)\n            'cookies': 1,                    # Permitir cookies\n            'images': 1,                     # Permitir imagens (2 para bloquear)\n            'javascript': 1,                 # Permitir JavaScript (2 para bloquear)\n            'plugins': 2,                    # Bloquear plugins (Flash, etc.)\n            'popups': 0,                     # Bloquear popups\n            'geolocation': 2,                # Bloquear requisições de localização\n            'notifications': 2,              # Bloquear notificações\n            'media_stream': 2,               # Bloquear câmera/microfone\n            'media_stream_mic': 2,           # Bloquear apenas microfone\n            'media_stream_camera': 2,        # Bloquear apenas câmera\n            'automatic_downloads': 1,        # Permitir downloads automáticos\n            'midi_sysex': 2,                 # Bloquear acesso MIDI\n            'clipboard': 1,                  # Permitir acesso à área de transferência\n            'sensors': 2,                    # Bloquear sensores de movimento\n            'usb_guard': 2,                  # Bloquear acesso a dispositivos USB\n            'serial_guard': 2,               # Bloquear acesso à porta serial\n            'bluetooth_guard': 2,            # Bloquear Bluetooth\n            'file_system_write_guard': 2,    # Bloquear escrita no sistema de arquivos\n        }\n    }\n}\n```\n\n### Rede e Desempenho\n\n```python\noptions.browser_preferences = {\n    'net': {\n        # Predição de rede: 0=sempre, 1=apenas wifi, 2=nunca\n        'network_prediction_options': 2,\n        \n        # Verificação rápida de alcançabilidade do servidor\n        'quick_check_enabled': False\n    },\n    \n    # Prefetching de DNS\n    'dns_prefetching': {\n        'enabled': False  # Desabilitar para reduzir tráfego de rede\n    },\n    \n    # Pré-conectar a resultados de busca\n    'search': {\n        'suggest_enabled': False,           # Desabilitar sugestões de busca\n        'instant_enabled': False            # Desabilitar resultados instantâneos\n    },\n    \n    # Páginas de erro alternativas\n    'alternate_error_pages': {\n        'enabled': False  # Não sugerir alternativas para 404s\n    }\n}\n```\n\n### Preferências de Download\n\n```python\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/path/to/downloads',\n        'prompt_for_download': False,\n        'directory_upgrade': True,\n        'extensions_to_open': '',           # Tipos de arquivo para abrir automaticamente\n        'open_pdf_externally': True,        # Não usar o visualizador de PDF interno\n    },\n    \n    'download_bubble': {\n        'partial_view_enabled': True        # Mostrar balão de progresso do download\n    },\n    \n    'safebrowsing': {\n        'enabled': False  # Desabilitar avisos de download do Safe Browsing\n    }\n}\n```\n\n### Privacidade e Segurança\n\n```python\noptions.browser_preferences = {\n    # Do Not Track\n    'enable_do_not_track': True,\n    \n    # Referrers\n    'enable_referrers': False,\n    \n    # Safe Browsing\n    'safebrowsing': {\n        'enabled': False,                   # Desabilitar Safe Browsing\n        'enhanced': False                   # Desabilitar proteção avançada\n    },\n    \n    # Privacy Sandbox (substituto de cookies do Google)\n    'privacy_sandbox': {\n        'apis_enabled': False,\n        'topics_enabled': False,\n        'fledge_enabled': False\n    },\n    \n    # Cookies de terceiros\n    'profile': {\n        'block_third_party_cookies': True,\n        'cookie_controls_mode': 1,          # Bloquear terceiros no modo anônimo\n        \n        # Configurações de conteúdo\n        'default_content_setting_values': {\n            'cookies': 1,\n            'third_party_cookie_blocking_enabled': True\n        }\n    },\n    \n    # WebRTC (pode vazar IP real)\n    'webrtc': {\n        'ip_handling_policy': 'default_public_interface_only',\n        'multiple_routes_enabled': False,\n        'nonproxied_udp_enabled': False\n    }\n}\n```\n\n### Preenchimento Automático e Senhas\n\n```python\noptions.browser_preferences = {\n    'autofill': {\n        'enabled': False,                   # Desabilitar preenchimento automático de formulários\n        'profile_enabled': False,           # Desabilitar preenchimento automático de endereço\n        'credit_card_enabled': False,       # Desabilitar preenchimento automático de cartão de crédito\n        'credit_card_fido_auth_enabled': False\n    },\n    \n    'profile': {\n        'password_manager_enabled': False,\n        'password_manager_leak_detection': False\n    },\n    \n    'credentials_enable_service': False,\n    'credentials_enable_autosignin': False\n}\n```\n\n### Comportamento do Navegador e UI\n\n```python\nimport time\n\noptions.browser_preferences = {\n    # Página inicial e inicialização\n    'homepage': 'https://www.google.com',\n    'homepage_is_newtabpage': False,\n    'newtab_page_location_override': 'https://www.google.com',\n    \n    'session': {\n        'restore_on_startup': 1,            # 0=nova aba, 1=restaurar, 4=URLs específicas, 5=página nova aba\n        'startup_urls': ['https://www.google.com'],\n        'session_data_status': 3            # Status dos dados da sessão (interno)\n    },\n    \n    # Página de boas-vindas e janela\n    'browser': {\n        'has_seen_welcome_page': True,      # Pular tela de boas-vindas\n        'window_placement': {\n            'bottom': 1032,                 # Posição inferior da janela\n            'left': 2247,                   # Posição esquerda da janela\n            'right': 3192,                  # Posição direita da janela\n            'top': 31,                      # Posição superior da janela\n            'maximized': False,             # Janela está maximizada\n            'work_area_bottom': 1080,       # Área de trabalho inferior da tela\n            'work_area_left': 1920,         # Área de trabalho esquerda da tela\n            'work_area_right': 3840,        # Área de trabalho direita da tela\n            'work_area_top': 0              # Área de trabalho superior da tela\n        }\n    },\n    \n    # Extensões\n    'extensions': {\n        'ui': {\n            'developer_mode': False\n        },\n        'alerts': {\n            'initialized': True\n        },\n        'theme': {\n            'system_theme': 2               # 0=padrão, 1=claro, 2=escuro\n        },\n        'last_chrome_version': '130.0.6723.91'  # Deve corresponder à sua versão\n    },\n    \n    # Tradução\n    'translate': {\n        'enabled': False                    # Desabilitar prompts de tradução\n    },\n    'translate_blocked_languages': ['en'],  # Nunca oferecer para traduzir Inglês\n    'translate_site_blacklist': [],         # Legado (use blocklist_with_time)\n    \n    # Barra de favoritos\n    'bookmark_bar': {\n        'show_on_all_tabs': False\n    },\n    \n    # Abas\n    'tabs': {\n        'new_tab_position': 0               # 0=à direita, 1=após atual\n    },\n    'pinned_tabs': [],                      # Lista de URLs de abas fixadas\n    \n    # Página Nova Aba (timestamps em formato Chrome)\n    'NewTabPage': {\n        'PrevNavigationTime': str(int(time.time() * 1000000) + 11644473600000000)  # Timestamp do Chrome\n    },\n    'ntp': {\n        'num_personal_suggestions': 6       # Número de sugestões (0-10)\n    },\n    \n    # Personalização da barra de ferramentas\n    'toolbar': {\n        'pinned_chrome_labs_migration_complete': True\n    }\n}\n```\n\n!!! info \"Formato de Timestamp do Chrome\"\n    O Chrome usa o formato Windows FILETIME: microssegundos desde 1º de janeiro de 1601 UTC.\n    \n    Converter timestamp do Python:\n    ```python\n    import time\n    chrome_time = int(time.time() * 1000000) + 11644473600000000\n    ```\n\n### Ortografia e Idioma\n\n```python\noptions.browser_preferences = {\n    'browser': {\n        'enable_spellchecking': False       # Desabilitar verificação ortográfica\n    },\n    \n    'spellcheck': {\n        'dictionaries': ['en-US', 'pt-BR'], # Idiomas da verificação ortográfica\n        'dictionary': '',                   # Preferência legada (manter vazio)\n        'use_spelling_service': False       # Não enviar ao Google\n    },\n    \n    'intl': {\n        'accept_languages': 'pt-BR,pt,en-US,en',\n        'selected_languages': 'pt-BR,pt,en-US,en'  # Selecionados explicitamente\n    },\n    \n    # Comportamento e histórico de tradução\n    'translate': {\n        'enabled': True\n    },\n    'translate_accepted_count': {\n        'pt-BR': 0,\n        'es': 5                             # Aceitou 5 traduções de espanhol\n    },\n    'translate_denied_count_for_language': {\n        'en': 10                            # Nunca traduzir inglês\n    },\n    'translate_ignored_count_for_language': {\n        'en': 1\n    },\n    'translate_site_blocklist_with_time': {},  # Sites para nunca traduzir\n    \n    # Idioma das legendas de acessibilidade\n    'accessibility': {\n        'captions': {\n            'live_caption_language': 'pt-BR'\n        }\n    },\n    \n    # Contadores do modelo de idioma (estatísticas de uso)\n    'language_model_counters': {\n        'en': 2,                            # Contagem de palavras em inglês\n        'pt': 10                            # Contagem de palavras em português\n    }\n}\n```\n\n!!! info \"Contadores do Modelo de Idioma\"\n    Esses contadores rastreiam estatísticas de uso de idioma para os modelos de aprendizado de máquina do Chrome:\n    \n    - Usados para prever as preferências de idioma do usuário\n    - Afeta sugestões de busca e autocompletar\n    - Contagens mais altas indicam uso mais frequente\n    - Valores realistas: 0-1000 para uso ocasional, 1000+ para uso intenso\n\n### Acessibilidade\n\n```python\noptions.browser_preferences = {\n    'accessibility': {\n        'image_labels_enabled': False       # Não obter legendas de imagem do Google\n    },\n    \n    # Configurações de fonte\n    'webkit': {\n        'webprefs': {\n            'default_font_size': 16,\n            'default_fixed_font_size': 13,\n            'minimum_font_size': 0,\n            'minimum_logical_font_size': 6,\n            'fonts': {\n                'standard': {\n                    'Zyyy': 'Arial'\n                },\n                'serif': {\n                    'Zyyy': 'Times New Roman'\n                }\n            }\n        }\n    }\n}\n```\n\n### Mídia e Áudio\n\n```python\noptions.browser_preferences = {\n    # Áudio\n    'audio': {\n        'mute_enabled': False               # Iniciar com áudio ligado/desligado\n    },\n    \n    # Autoplay\n    'media': {\n        'autoplay_policy': 0,               # 0=permitir, 1=gesto do usuário, 2=ativação do usuário no documento\n        'video_fullscreen_orientation_lock': False\n    },\n    \n    # WebGL\n    'webkit': {\n        'webprefs': {\n            'webgl_enabled': True,          # Habilitar/desabilitar WebGL\n            'webgl2_enabled': True\n        }\n    }\n}\n```\n\n### Impressão\n\n```python\noptions.browser_preferences = {\n    'printing': {\n        'print_preview_sticky_settings': {\n            'appState': '{\\\"version\\\":2,\\\"recentDestinations\\\":[{\\\"id\\\":\\\"Save as PDF\\\",\\\"origin\\\":\\\"local\\\"}],\\\"marginsType\\\":3,\\\"customMargins\\\":{\\\"marginTop\\\":63,\\\"marginRight\\\":192,\\\"marginBottom\\\":240,\\\"marginLeft\\\":260}}'\n        }\n    },\n    \n    'savefile': {\n        'default_directory': '/tmp'         # Local padrão para salvar PDFs\n    }\n}\n```\n\n!!! tip \"Formato appState da Impressão\"\n    O `appState` é uma string codificada em JSON. Para manipulação mais fácil:\n    \n    ```python\n    import json\n    \n    app_state = {\n        'version': 2,\n        'recentDestinations': [{\n            'id': 'Save as PDF',\n            'origin': 'local'\n        }],\n        'marginsType': 3,                   # 0=padrão, 1=sem margens, 2=mínimo, 3=personalizado\n        'customMargins': {\n            'marginTop': 63,\n            'marginRight': 192,\n            'marginBottom': 240,\n            'marginLeft': 260\n        },\n        'isHeaderFooterEnabled': False,\n        'scaling': '100',\n        'scalingType': 3,                   # 0=padrão, 1=ajustar à página, 2=ajustar ao papel, 3=personalizado\n        'isColorEnabled': True,\n        'isDuplexEnabled': False,\n        'isCssBackgroundEnabled': True,\n        'dpi': {\n            'horizontal_dpi': 300,\n            'vertical_dpi': 300,\n            'is_default': True\n        },\n        'mediaSize': {\n            'name': 'ISO_A4',\n            'width_microns': 210000,\n            'height_microns': 297000,\n            'custom_display_name': 'A4',\n            'is_default': True\n        }\n    }\n    \n    # Converter para string para o appState\n    options.browser_preferences = {\n        'printing': {\n            'print_preview_sticky_settings': {\n                'appState': json.dumps(app_state)\n            }\n        }\n    }\n    ```\n\n### WebRTC e Peer-to-Peer\n\n```python\noptions.browser_preferences = {\n    'webrtc': {\n        # Política de manuseio de IP\n        'ip_handling_policy': 'default_public_interface_only',\n        \n        # Opções de transporte UDP\n        'udp_port_range': '10000-10100',    # Restringir intervalo de portas UDP\n        \n        # Desabilitar peer-to-peer\n        'multiple_routes_enabled': False,\n        'nonproxied_udp_enabled': False,\n        \n        # Coleta de log de texto\n        'text_log_collection_allowed': False\n    }\n}\n```\n\n### Isolamento de Site e Segurança\n\n```python\noptions.browser_preferences = {\n    # Isolamento de site\n    'site_isolation': {\n        'isolate_origins': '',              # Origens separadas por vírgula para isolar\n        'site_per_process': True            # Isolamento total de site\n    },\n    \n    # Conteúdo misto\n    'mixed_content': {\n        'auto_upgrade_enabled': True        # Atualizar HTTP para HTTPS\n    },\n    \n    # SSL/TLS\n    'ssl': {\n        'rev_checking': {\n            'enabled': True                 # Verificar revogação de certificado\n        }\n    }\n}\n```\n\n### Metadados de Instalação e País\n\n```python\nimport uuid\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    # ID do país na instalação (afeta config. padrão e localidade)\n    'countryid_at_install': 16978,          # Varia por país (ex: 16978 para Brasil)\n    \n    # Estado de instalação de aplicativos padrão\n    'default_apps_install_state': 3,        # 0=não inst., 1=inst., 3=migrado\n    \n    # GUID do perfil corporativo (para navegadores gerenciados)\n    'enterprise_profile_guid': str(uuid.uuid4()),\n    \n    # Provedor de busca padrão\n    'default_search_provider': {\n        'guid': ''                          # Vazio para padrão (Google)\n    }\n}\n```\n\n!!! info \"Valores de ID de País\"\n    `countryid_at_install` é um código numérico que representa o país onde o Chrome foi instalado pela primeira vez:\n    \n    - **16978**: Brasil (BR)\n    - **16965**: Estados Unidos (US)\n    - **16967**: Grã-Bretanha (GB)\n    - **16966**: Alemanha (DE)\n    - **16972**: Japão (JP)\n    - E muitos outros...\n    \n    Isso afeta o idioma padrão, moeda e configurações regionais. Para um fingerprinting realista, combine isso com sua região alvo.\n\n### Recursos Experimentais\n\n```python\noptions.browser_preferences = {\n    # Experimentos do Chrome Labs\n    'browser': {\n        'labs': {\n            'enabled': False\n        }\n    },\n    \n    # Pré-carregamento\n    'preload': {\n        'enabled': False                    # Desabilitar pré-carregamento de página\n    },\n    \n    # Rolagem suave\n    'smooth_scrolling': {\n        'enabled': True\n    },\n    \n    # Aceleração de hardware\n    'hardware_acceleration_mode': {\n        'enabled': True                     # Desabilitar para desempenho headless\n    }\n}\n```\n\n### DevTools e Opções de Desenvolvedor\n\n```python\noptions.browser_preferences = {\n    'devtools': {\n        'preferences': {\n            # Aparência do DevTools\n            'currentDockState': '\"right\"',              # \"bottom\", \"right\", \"undocked\"\n            'uiTheme': '\"dark\"',                        # \"dark\", \"light\", \"system\"\n            \n            # Configurações do Console\n            'consoleTimestampsEnabled': 'true',\n            'preserveConsoleLog': 'true',\n            \n            # Painel de Rede\n            'network.disableCache': 'false',\n            'network.color-code-resource-types': 'true',\n            'network-panel-split-view-state': '{\"vertical\":{\"size\":0}}',\n            \n            # Mapas de origem\n            'cssSourceMapsEnabled': 'true',\n            'jsSourceMapsEnabled': 'true',\n            \n            # Painel de Elementos\n            'elements.styles.sidebar.width': '{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}',\n            \n            # Versionamento do Inspetor\n            'inspectorVersion': '37',\n            \n            # Painel selecionado\n            'panel-selected-tab': '\"network\"',          # Último painel aberto\n            \n            # Categorias expandidas de info de requisição\n            'request-info-general-category-expanded': 'true',\n            'request-info-request-headers-category-expanded': 'true',\n            'request-info-response-headers-category-expanded': 'true'\n        },\n        'synced_preferences_sync_disabled': {\n            'adorner-settings': '[{\"adorner\":\"grid\",\"isEnabled\":true},{\"adorner\":\"flex\",\"isEnabled\":true}]',\n            'syncedInspectorVersion': '37'\n        }\n    },\n    \n    # GCM (Google Cloud Messaging)\n    'gcm': {\n        'product_category_for_subtypes': 'com.chrome.linux'  # com.chrome.windows, com.chrome.macos\n    }\n}\n```\n\n!!! tip \"Formato das Preferências do DevTools\"\n    As preferências do DevTools usam um formato único onde valores booleanos e strings são armazenados como **strings codificadas em JSON** (ex: `'true'` em vez de `True`, `'\"dark\"'` em vez de `'dark'`). Isso ocorre porque as configurações do DevTools são serializadas diretamente para JSON.\n    \n    Para objetos complexos, codifique duas vezes:\n    ```python\n    import json\n    \n    # Crie o objeto\n    split_view = {'vertical': {'size': 0}}\n    \n    # Codifique duas vezes para o DevTools\n    devtools_value = json.dumps(json.dumps(split_view))\n    # Resultado: '\"{\\\\\"vertical\\\\\":{\\\\\"size\\\\\":0}}\"'\n    ```\n\n### Controle de Sincronização e Login\n\n```python\nimport time\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    'signin': {\n        'allowed': True,                        # Permitir login no Google\n        'cookie_clear_on_exit_migration_notice_complete': True\n    },\n    \n    'sync': {\n        'data_type_status_for_sync_to_signin': {\n            'bookmarks': False,\n            'history': False,\n            'passwords': False,\n            'preferences': False\n        },\n        'encryption_bootstrap_token_per_account_migration_done': True,\n        'passwords_per_account_pref_migration_done': True,\n        'feature_status_for_sync_to_signin': 5\n    },\n    \n    # Serviços do Google\n    'google': {\n        'services': {\n            'signin_scoped_device_id': '<your-device-id>'  # Gere um ID único\n        }\n    },\n    \n    # GAIA (Google Accounts Infrastructure)\n    'gaia_cookie': {\n        'changed_time': str(int(time.time())),\n        'hash': '',\n        'last_list_accounts_data': '[]'\n    }\n}\n```\n\n### Otimização e Rastreamento de Desempenho\n\n```python\nimport time\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    # Guia de otimização (dicas de desempenho do Google)\n    'optimization_guide': {\n        'hintsfetcher': {\n            'hosts_successfully_fetched': {}\n        },\n        'predictionmodelfetcher': {\n            'last_fetch_attempt': str(int(time.time())),\n            'last_fetch_success': str(int(time.time()))\n        },\n        'previously_registered_optimization_types': {}\n    },\n    \n    # Clusters de histórico (agrupando navegação relacionada)\n    'history_clusters': {\n        'all_cache': {\n            'all_keywords': {},\n            'all_timestamp': str(int(time.time()))\n        },\n        'last_selected_tab': 0,\n        'short_cache': {\n            'short_keywords': {},\n            'short_timestamp': '0'\n        }\n    },\n    \n    # Métricas de diversidade de domínio\n    'domain_diversity': {\n        'last_reporting_timestamp': str(int(time.time()))\n    },\n    \n    # Plataforma de segmentação (análise de comportamento do usuário)\n    'segmentation_platform': {\n        'device_switcher_util': {\n            'result': {\n                'labels': ['NotSynced']\n            }\n        },\n        'last_db_compaction_time': str(int(time.time()))\n    },\n    \n    # Zero suggest (previsões da omnibox)\n    'zerosuggest': {\n        'cachedresults': '',\n        'cachedresults_with_url': {}\n    }\n}\n```\n\n!!! info \"Preferências de Rastreamento de Desempenho\"\n    Essas preferências são tipicamente usadas pelo Chrome para rastrear e otimizar o desempenho. Para automação, você pode deixá-las vazias ou definir valores realistas para parecer mais com um navegador normal.\n\n### Eventos de Sessão e Tratamento de Falhas\n\nO Chrome rastreia o histórico da sessão para recuperação e telemetria:\n\n```python\nimport time\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    'sessions': {\n        'event_log': [\n            {\n                'crashed': False,\n                'time': str(int(time.time() * 1000000) + 11644473600000000),\n                'type': 0                   # 0=início da sessão\n            },\n            {\n                'crashed': False,\n                'did_schedule_command': True,\n                'first_session_service': True,\n                'tab_count': 1,\n                'time': str(int(time.time() * 1000000) + 11644473600000000),\n                'type': 2,                  # 2=dados da sessão salvos\n                'window_count': 1\n            }\n        ],\n        'session_data_status': 3            # 0=desconhecido, 1=sem dados, 2=alguns dados, 3=dados completos\n    },\n    \n    # Tipo de saída do perfil (importante para fingerprinting)\n    'profile': {\n        'exit_type': 'Crashed'              # 'Normal', 'Crashed', 'SessionEnded'\n    }\n}\n```\n\n!!! warning \"Crashed vs Normal\"\n    A maioria dos navegadores reais **falha ocasionalmente**. Mostrar sempre a saída `'Normal'` é suspeito.\n    \n    **Estratégia realista**: Defina `'Crashed'` para ~10-20% dos perfis para simular a experiência normal do usuário. Ironicamente, ter falhas ocasionais faz sua automação parecer mais humana.\n\n!!! tip \"Tipos de Eventos de Sessão\"\n    - **Tipo 0**: Início da sessão\n    - **Tipo 1**: Sessão terminada normalmente\n    - **Tipo 2**: Dados da sessão salvos (abas, janelas)\n    - **Tipo 3**: Sessão restaurada\n    \n    O `event_log` constrói um histórico de sessões do navegador ao longo do tempo.\n\n## Furtividade (Stealth) e Fingerprinting\n\nCriar um fingerprint de navegador realista é crucial para evitar sistemas de detecção de bots. Esta seção cobre técnicas básicas e avançadas.\n\n### Configuração Rápida de Furtividade\n\nPara a maioria dos casos de uso, esta configuração simples fornece boa anti-detecção:\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def quick_stealth():\n    options = ChromiumOptions()\n    \n    # Simular um navegador com 60 dias de uso\n    fake_timestamp = int(time.time()) - (60 * 24 * 60 * 60)\n    \n    options.browser_preferences = {\n        # Histórico de uso falso\n        'profile': {\n            'last_engagement_time': fake_timestamp,\n            'exited_cleanly': True,\n            'exit_type': 'Normal'\n        },\n        \n        # Página inicial realista\n        'homepage': 'https://www.google.com',\n        'session': {\n            'restore_on_startup': 1,\n            'startup_urls': ['https://www.google.com']\n        },\n        \n        # Habilitar recursos que usuários reais têm\n        'enable_do_not_track': False,  # A maioria dos usuários não habilita isso\n        'safebrowsing': {'enabled': True},\n        'autofill': {'enabled': True},\n        'search': {'suggest_enabled': True},\n        'dns_prefetching': {'enabled': True}\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://bot-detection-site.com')\n        print(\"Modo furtivo ativado!\")\n\nasyncio.run(quick_stealth())\n```\n\n!!! tip \"Princípios Chave da Furtividade\"\n    **Habilite, não desabilite**: Usuários reais têm Safe Browsing, preenchimento automático e sugestões de busca habilitados. Desabilitar tudo parece suspeito.\n    \n    **Envelheça seu perfil**: Instalações novas são um sinal de alerta. Simule um navegador que foi usado por semanas ou meses.\n    \n    **Combine com a maioria**: Use configurações padrão que 90% dos usuários têm, não configurações focadas em privacidade.\n\n### Fingerprinting Avançado\n\nPara máximo realismo, simule um histórico detalhado de uso do navegador:\n\n```python\nimport time\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\ndef create_realistic_browser() -> ChromiumOptions:\n    \"\"\"Cria um navegador com resistência abrangente a fingerprinting.\"\"\"\n    options = ChromiumOptions()\n    \n    # Timestamps\n    current_time = int(time.time())\n    install_time = current_time - (90 * 24 * 60 * 60)  # 90 dias atrás\n    last_use = current_time - (3 * 60 * 60)            # 3 horas atrás\n    \n    options.browser_preferences = {\n        # Metadados do perfil (crítico para fingerprinting)\n        'profile': {\n            'created_by_version': '130.0.6723.91',      # Deve corresponder à sua versão do Chrome\n            'creation_time': str(install_time),\n            'last_engagement_time': str(last_use),\n            'exit_type': 'Crashed',                     # 'Normal', 'Crashed', 'SessionEnded'\n            'name': 'Pessoa 1',                         # Nome de perfil realista\n            'avatar_index': 26,                         # 0-26 avatares disponíveis\n            \n            # Configurações de conteúdo realistas\n            'default_content_setting_values': {\n                'cookies': 1,\n                'images': 1,\n                'javascript': 1,\n                'popups': 0,\n                'notifications': 2,\n                'geolocation': 0,           # Perguntar (não bloquear)\n                'media_stream': 0           # Perguntar (realista)\n            },\n            \n            'password_manager_enabled': False,\n            'cookie_controls_mode': 0,\n            'content_settings': {\n                'pref_version': 1,\n                'enable_quiet_permission_ui': {\n                    'notifications': False\n                },\n                'enable_quiet_permission_ui_enabling_method': {\n                    'notifications': 1\n                }\n            },\n            \n            # Metadados de segurança\n            'family_member_role': 'not_in_family',\n            'managed_user_id': '',\n            'were_old_google_logins_removed': True\n        },\n        \n        # Metadados de uso do navegador\n        'browser': {\n            'has_seen_welcome_page': True,\n            'window_placement': {\n                'work_area_bottom': 1080,\n                'work_area_left': 0,\n                'work_area_right': 1920,\n                'work_area_top': 0\n            }\n        },\n        \n        # Metadados de instalação\n        'countryid_at_install': 16978,              # Varia por país\n        'default_apps_install_state': 3,\n        \n        # Metadados de extensões\n        'extensions': {\n            'last_chrome_version': '130.0.6723.91',  # Deve corresponder à sua versão\n            'alerts': {'initialized': True},\n            'theme': {'system_theme': 2}\n        },\n        \n        # Atividade da sessão (mostra uso regular)\n        'in_product_help': {\n            'session_start_time': str(current_time),\n            'session_last_active_time': str(current_time),\n            'recent_session_start_times': [\n                str(current_time - (24 * 60 * 60)),\n                str(current_time - (48 * 60 * 60)),\n                str(current_time - (72 * 60 * 60))\n            ]\n        },\n        \n        # Restauração de sessão\n        'session': {\n            'restore_on_startup': 1,\n            'startup_urls': ['https://www.google.com']\n        },\n        \n        # Página inicial\n        'homepage': 'https://www.google.com',\n        'homepage_is_newtabpage': False,\n        \n        # Histórico de tradução (mostra uso multilíngue)\n        'translate': {'enabled': True},\n        'translate_accepted_count': {'es': 2, 'fr': 1},\n        'translate_denied_count_for_language': {'en': 1},\n        \n        # Verificação ortográfica\n        'spellcheck': {\n            'dictionaries': ['en-US', 'pt-BR'],\n            'dictionary': ''\n        },\n        \n        # Idiomas\n        'intl': {\n            'selected_languages': 'en-US,en,pt-BR'\n        },\n        \n        # Metadados de login\n        'signin': {\n            'allowed': True,\n            'cookie_clear_on_exit_migration_notice_complete': True\n        },\n        \n        # Safe Browsing (a maioria dos usuários tem isso)\n        'safebrowsing': {\n            'enabled': True,\n            'enhanced': False\n        },\n        \n        # Preenchimento automático (comum para usuários reais)\n        'autofill': {\n            'enabled': True,\n            'profile_enabled': True\n        },\n        \n        # Sugestões de busca\n        'search': {'suggest_enabled': True},\n        \n        # DNS prefetch\n        'dns_prefetching': {'enabled': True},\n        \n        # Do NOT Track (geralmente desligado)\n        'enable_do_not_track': False,\n        \n        # WebRTC (configurações padrão)\n        'webrtc': {\n            'ip_handling_policy': 'default',\n            'multiple_routes_enabled': True\n        },\n        \n        # Privacy Sandbox (novo sistema de rastreamento do Google - usuários realistas têm isso)\n        'privacy_sandbox': {\n            'first_party_sets_data_access_allowed_initialized': True,\n            'm1': {\n                'ad_measurement_enabled': True,\n                'fledge_enabled': True,\n                'row_notice_acknowledged': True,\n                'topics_enabled': True\n            }\n        },\n        \n        # Engajamento de mídia\n        'media': {\n            'engagement': {'schema_version': 5}\n        },\n        \n        # Web apps\n        'web_apps': {\n            'did_migrate_default_chrome_apps': ['app-id'],\n            'last_preinstall_synchronize_version': '130'\n        }\n    }\n    \n    return options\n\n# Uso\nasync def advanced_stealth():\n    options = create_realistic_browser()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://advanced-bot-detection.com')\n        # O navegador aparece como uma instalação genuína de 90 dias\n\n```\n\n!!! warning \"Consistência de Versão é Crítica\"\n    **Sempre combine as versões do Chrome**: Garanta que `profile.created_by_version` e `extensions.last_chrome_version` correspondam à sua versão real do Chrome. Versões incompatíveis são um sinal de alerta instantâneo.\n    \n    ```python\n    # Obtenha sua versão do Chrome programaticamente:\n    async with Chrome() as browser:\n        tab = await browser.start()\n        version = await browser.get_version()\n        chrome_version = version['product'].split('/')[1]  # ex: '130.0.6723.91'\n        print(f\"Use esta versão: {chrome_version}\")\n    ```\n\n!!! info \"O que as Preferências de Fingerprinting Fazem\"\n    **Idade do perfil**: `creation_time` e `last_engagement_time` provam que o navegador não é uma instalação nova.\n    \n    **Histórico de uso**: `recent_session_start_times` mostra padrões regulares de navegação.\n    \n    **Histórico de tradução**: `translate_accepted_count` indica uma pessoa real usando múltiplos idiomas.\n    \n    **Posicionamento da janela**: Dimensões de tela realistas que correspondem a resoluções de monitor reais.\n    \n    **Privacy Sandbox**: Novo sistema de rastreamento do Google. Desabilitá-lo é incomum e suspeito.\n\n## Impacto no Desempenho\n\nEntender as implicações de desempenho das preferências do navegador ajuda a otimizar para seu caso de uso específico:\n\n| Categoria de Preferência | Impacto Esperado | Caso de Uso |\n|---|---|---|\n| Desabilitar imagens | 50-70% carregamentos mais rápidos | Raspagem de conteúdo de texto |\n| Desabilitar prefetch | 10-20% carregamentos mais rápidos | Reduzir uso de banda |\n| Desabilitar plugins | 5-10% carregamentos mais rápidos | Segurança e desempenho |\n| Bloquear notificações | Elimina popups | Automação limpa |\n| Downloads silenciosos | Elimina prompts | Downloads automatizados de arquivos |\n\n!!! tip \"Troca entre Velocidade e Furtividade\"\n    **Para velocidade**: Desabilite imagens, prefetch, plugins e verificação ortográfica.\n    \n    **Para furtividade**: Habilite Safe Browsing, preenchimento automático, sugestões de busca e DNS prefetch (mesmo que eles tornem as coisas mais lentas).\n    \n    **Abordagem equilibrada**: Habilite recursos de furtividade, mas desabilite imagens e plugins. Isso dá 40-50% de ganho de velocidade enquanto mantém um fingerprint realista.\n\n## Veja Também\n\n- **[Análise Profunda: Preferências do Navegador](../../deep-dive/browser-preferences.md)** - Detalhes arquitetônicos e internos\n- **[Estado de Carregamento da Página](page-load-state.md)** - Controle quando as páginas são consideradas carregadas\n- **[Configuração de Proxy](proxy.md)** - Configure proxies de rede\n- **[Cookies e Sessões](../browser-management/cookies-sessions.md)** - Gerencie o estado do navegador\n- **[Código-Fonte do Chromium: pref_names.cc](https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.cc)** - Constantes oficiais de preferência\n- **[Código-Fonte do Chromium: pref_names.h](https://github.com/chromium/chromium/blob/main/chrome/common/pref_names.h)** - Arquivo de cabeçalho com definições\n\nAs preferências personalizadas do navegador oferecem um controle sem precedentes sobre o comportamento do navegador, permitindo automação sofisticada, otimização de desempenho e configuração de privacidade que simplesmente não são possíveis com ferramentas de automação tradicionais. Este nível de acesso transforma o Pydoll de uma simples biblioteca de automação em um sistema completo de controle de navegador."
  },
  {
    "path": "docs/pt/features/configuration/proxy.md",
    "content": "# Configuração de Proxy\n\nProxies são essenciais para a automação web profissional, permitindo contornar limites de requisições (rate limits), acessar conteúdo geo-restrito e manter o anonimato. O Pydoll oferece suporte nativo a proxies com tratamento automático de autenticação.\n\n!!! info \"Documentação Relacionada\"\n    - **[Opções do Navegador](browser-options.md)** - Argumentos de proxy via linha de comando\n    - **[Interceptação de Requisições](../network/interception.md)** - Como a autenticação de proxy funciona internamente\n    - **[Automação Furtiva](../automation/human-interactions.md)** - Combine proxies com anti-detecção\n    - **[Análise Profunda da Arquitetura de Proxy](../../deep-dive/proxy-architecture.md)** - Fundamentos de rede, protocolos, segurança e construção do seu próprio proxy\n\n## Por que Usar Proxies?\n\nProxies oferecem capacidades críticas para automação:\n\n| Benefício | Descrição | Caso de Uso |\n|---|---|---|\n| **Rotação de IP** | Distribui requisições por múltiplos IPs | Evitar limites de requisição, raspar em escala |\n| **Acesso Geográfico** | Acessa conteúdo bloqueado por região | Testar recursos geo-direcionados, contornar restrições |\n| **Anonimato** | Esconde seu endereço IP real | Automação focada em privacidade, análise de concorrentes |\n| **Distribuição de Carga** | Espalha o tráfego por múltiplos endpoints | Raspagem de alto volume, testes de estresse |\n| **Evitar Banimento** | Previne banimentos permanentes de IP | Automação de longa duração, raspagem agressiva |\n\n!!! tip \"Quando Usar Proxies\"\n    **Sempre use proxies para:**\n    \n    - Raspagem web em produção (>100 requisições/hora)\n    - Acessar conteúdo geo-restrito\n    - Contornar limites de requisição ou bloqueios baseados em IP\n    - Testar de diferentes regiões\n    - Manter o anonimato\n    \n    **Você pode pular os proxies para:**\n    \n    - Desenvolvimento e testes locais\n    - Automação interna/corporativa\n    - Automação de baixo volume (<50 requisições/dia)\n    - Quando raspando sua própria infraestrutura\n\n## Tipos de Proxy\n\nDiferentes protocolos de proxy servem a propósitos distintos:\n\n| Tipo | Porta | Autenticação | Velocidade | Segurança | Caso de Uso |\n|---|---|---|---|---|---|\n| **HTTP** | 80, 8080 | Opcional | Rápido | Baixa | Raspagem web básica, dados não sensíveis |\n| **HTTPS** | 443, 8443 | Opcional | Rápido | Média | Raspagem web segura, tráfego criptografado |\n| **SOCKS5** | 1080, 1081 | Opcional | Média | Alta | Suporte total TCP/UDP, casos de uso avançados |\n\n### Proxies HTTP/HTTPS\n\nProxies web padrão, ideais para a maioria das tarefas de automação:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def http_proxy_example():\n    options = ChromiumOptions()\n    \n    # Proxy HTTP (não criptografado)\n    options.add_argument('--proxy-server=http://proxy.example.com:8080')\n    \n    # Ou proxy HTTPS (criptografado)\n    # options.add_argument('--proxy-server=https://proxy.example.com:8443')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Todo o tráfego passa pelo proxy\n        await tab.go_to('https://httpbin.org/ip')\n        \n        # Verificar IP do proxy\n        ip = await tab.execute_script('return document.body.textContent')\n        print(f\"IP Atual: {ip}\")\n\nasyncio.run(http_proxy_example())\n```\n\n**Prós:**\n\n- Rápido e eficiente\n- Amplo suporte em todos os serviços\n- Fácil de configurar\n\n**Contras:**\n\n- HTTP: Sem criptografia (tráfego visível para o proxy)\n- Pode ser detectado mais facilmente que o SOCKS5\n\n### Proxies SOCKS5\n\nProxies avançados com suporte total a TCP/UDP:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def socks5_proxy_example():\n    options = ChromiumOptions()\n    \n    # Proxy SOCKS5\n    options.add_argument('--proxy-server=socks5://proxy.example.com:1080')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://httpbin.org/ip')\n\nasyncio.run(socks5_proxy_example())\n```\n\n**Prós:**\n\n- Agnóstico a protocolo (funciona com qualquer tráfego TCP/UDP)\n- Melhor para casos de uso avançados (WebSockets, WebRTC)\n- Mais furtivo (mais difícil de detectar)\n\n**Contras:**\n\n- Ligeiramente mais lento que HTTP/HTTPS\n- Menos comum em serviços de proxy gratuitos/baratos\n\n!!! info \"SOCKS4 vs SOCKS5\"\n    **SOCKS5** é recomendado em vez do SOCKS4 porque:\n    \n    - Suporta autenticação (usuário/senha)\n    - Lida com tráfego UDP (para WebRTC, DNS, etc.)\n    - Fornece melhor tratamento de erros\n    \n    Use `socks5://` a menos que você precise especificamente de SOCKS4 (`socks4://`).\n\n## Proxies Autenticados\n\nO Pydoll lida automaticamente com a autenticação de proxy sem intervenção manual.\n\n### Como a Autenticação Funciona\n\nQuando você fornece credenciais na URL do proxy, o Pydoll:\n\n1.  **Intercepta o desafio de autenticação** usando o domínio Fetch\n2.  **Responde automaticamente** com as credenciais\n3.  **Continua a navegação** sem interrupções\n\nIsso acontece de forma transparente, você não precisa lidar com a autenticação manualmente!\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def authenticated_proxy_example():\n    options = ChromiumOptions()\n    \n    # Proxy com autenticação (usuario:senha)\n    options.add_argument('--proxy-server=http://user:pass@proxy.example.com:8080')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Autenticação tratada automaticamente!\n        await tab.go_to('https://example.com')\n        print(\"Conectado através de proxy autenticado\")\n\nasyncio.run(authenticated_proxy_example())\n```\n\n!!! tip \"Formato das Credenciais\"\n    Inclua as credenciais diretamente na URL do proxy:\n\n    - HTTP: `http://username:password@host:port`\n    - HTTPS: `https://username:password@host:port`\n    - SOCKS5: `socks5://username:password@host:port`\n\n    O Pydoll extrai e usa automaticamente essas credenciais.\n\n!!! warning \"Limitação da Autenticação SOCKS5\"\n    **O Chrome não suporta autenticação SOCKS5 nativamente** ([Chromium Issue #40323993](https://issues.chromium.org/issues/40323993)). Credenciais incorporadas em `socks5://user:pass@host:port` são silenciosamente ignoradas — o Chrome envia apenas uma saudação \"sem autenticação\" para o proxy SOCKS5.\n\n    Isso significa que a autenticação automática de proxy do Pydoll (via `Fetch.authRequired`) **não funciona para SOCKS5**, pois o Chrome nunca emite um desafio HTTP 407 para conexões SOCKS5.\n\n    **Solução — Proxy forwarder local:**\n\n    Execute um proxy SOCKS5 local (sem autenticação) que encaminha para o proxy autenticado remoto. O Pydoll fornece um script pronto para uso:\n\n    ```python\n    import asyncio\n    from pydoll.utils import SOCKS5Forwarder\n    from pydoll.browser.chromium import Chrome\n    from pydoll.browser.options import ChromiumOptions\n\n    async def main():\n        forwarder = SOCKS5Forwarder(\n            remote_host='proxy.example.com',\n            remote_port=1080,\n            username='myuser',\n            password='mypass',\n            local_port=1081,\n        )\n        async with forwarder:\n            options = ChromiumOptions()\n            options.add_argument('--proxy-server=socks5://127.0.0.1:1081')\n\n            async with Chrome(options=options) as browser:\n                tab = await browser.start()\n                await tab.go_to('https://httpbin.org/ip')\n\n    asyncio.run(main())\n    ```\n\n    O forwarder realiza o handshake de usuário/senha com o proxy remoto enquanto o Chrome se conecta ao localhost sem autenticação.\n\n    Para a explicação técnica completa de por que isso acontece, veja **[Análise Profunda da Autenticação SOCKS5](../../deep-dive/network/socks-proxies.md#autenticacao-socks5-e-chrome)**.\n\n### Detalhes da Implementação da Autenticação\n\nO Pydoll usa o **domínio Fetch** do Chrome no nível do navegador para interceptar e lidar com desafios de autenticação:\n\n```python\n# Isso é tratado internamente pelo Pydoll\n# Você não precisa escrever este código!\n\nasync def _handle_proxy_auth(event):\n    \"\"\"Manipulador interno de autenticação de proxy do Pydoll.\"\"\"\n    if event['params']['authChallenge']['source'] == 'Proxy':\n        await browser.continue_request_with_auth(\n            request_id=event['params']['requestId'],\n            username='user',\n            password='pass'\n        )\n```\n\n!!! info \"Nos Bastidores\"\n    Para detalhes técnicos sobre como o Pydoll intercepta e lida com a autenticação de proxy, veja:\n    \n    - **[Interceptação de Requisições](../network/interception.md)** - Domínio Fetch e manipulação de requisições\n    - **[Sistema de Eventos](../advanced/event-system.md)** - Autenticação orientada a eventos\n\n!!! warning \"Conflitos do Domínio Fetch\"\n    Ao usar **proxies autenticados** + **interceptação de requisições no nível da aba**, esteja ciente:\n    \n    - O Pydoll habilita o Fetch no **Nível do Navegador** para autenticação de proxy\n    - Se você habilitar o Fetch no **Nível da Aba**, eles compartilham o mesmo domínio\n    - **Solução**: Chame `tab.go_to()` uma vez antes de habilitar a interceptação no nível da aba\n    \n    ```python\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # 1. Primeira navegação dispara autenticação do proxy (Fetch Nível Navegador)\n        await tab.go_to('https://example.com')\n        \n        # 2. Então habilite a interceptação no nível da aba com segurança\n        await tab.enable_fetch_events()\n        await tab.on('Fetch.requestPaused', my_interceptor)\n        \n        # 3. Continue com sua automação\n        await tab.go_to('https://example.com/page2')\n    ```\n    \n    Veja [Interceptação de Requisição - Proxy + Interceptação](../network/interception.md#private-proxy-request-interception-fetch) para detalhes.\n\n## Lista de Bypass de Proxy\n\nExclua domínios específicos de usar o proxy:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def proxy_bypass_example():\n    options = ChromiumOptions()\n    \n    # Usar proxy para a maior parte do tráfego\n    options.add_argument('--proxy-server=http://proxy.example.com:8080')\n    \n    # Mas ignorar o proxy para estes domínios\n    options.add_argument('--proxy-bypass-list=localhost,127.0.0.1,*.local,internal.company.com')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Usa proxy\n        await tab.go_to('https://external-site.com')\n        \n        # Ignora o proxy (conexão direta)\n        await tab.go_to('http://localhost:8000')\n        await tab.go_to('http://internal.company.com')\n\nasyncio.run(proxy_bypass_example())\n```\n\n**Padrões da lista de bypass:**\n\n| Padrão | Corresponde a | Exemplo |\n|---|---|---|\n| `localhost` | Apenas Localhost | `http://localhost` |\n| `127.0.0.1` | IP de Loopback | `http://127.0.0.1` |\n| `*.local` | Todos os domínios `.local` | `http://server.local` |\n| `internal.company.com` | Domínio específico | `http://internal.company.com` |\n| `192.168.1.*` | Faixa de IP | `http://192.168.1.100` |\n\n!!! tip \"Quando Usar a Lista de Bypass\"\n    Ignore o proxy para:\n    \n    - **Servidores de desenvolvimento local** (`localhost`, `127.0.0.1`)\n    - **Recursos internos da empresa** (VPN, intranet)\n    - **Ambientes de teste** (domínios `.local`, `.test`)\n    - **Recursos de alta largura de banda** (quando o proxy é lento)\n\n## PAC (Proxy Auto-Config)\n\nUse um arquivo PAC para regras complexas de roteamento de proxy:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def pac_proxy_example():\n    options = ChromiumOptions()\n    \n    # Carregar arquivo PAC de uma URL\n    options.add_argument('--proxy-pac-url=http://proxy.example.com/proxy.pac')\n    \n    # Ou usar arquivo PAC local\n    # options.add_argument('--proxy-pac-url=file:///path/to/proxy.pac')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n\nasyncio.run(pac_proxy_example())\n```\n\n**Exemplo de arquivo PAC:**\n\n```javascript\nfunction FindProxyForURL(url, host) {\n    // Conexão direta para endereços locais\n    if (isInNet(host, \"192.168.0.0\", \"255.255.0.0\") ||\n        isInNet(host, \"127.0.0.0\", \"255.0.0.0\")) {\n        return \"DIRECT\";\n    }\n    \n    // Usar proxy específico para certos domínios\n    if (dnsDomainIs(host, \".example.com\")) {\n        return \"PROXY proxy1.example.com:8080\";\n    }\n    \n    // Proxy padrão para todo o resto\n    return \"PROXY proxy2.example.com:8080\";\n}\n```\n\n!!! info \"Casos de Uso de Arquivo PAC\"\n    Arquivos PAC são úteis para:\n    \n    - **Regras de roteamento complexas** (baseadas em domínio, IP)\n    - **Failover de proxy** (tentar múltiplos proxies)\n    - **Balanceamento de carga** (distribuir entre pool de proxies)\n    - **Ambientes corporativos** (gerenciamento centralizado de proxy)\n\n## Rotação de Proxies\n\nRotacione entre múltiplos proxies para melhor distribuição:\n\n```python\nimport asyncio\nfrom itertools import cycle\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def rotating_proxy_example():\n    # Lista de proxies\n    proxies = [\n        'http://user:pass@proxy1.example.com:8080',\n        'http://user:pass@proxy2.example.com:8080',\n        'http://user:pass@proxy3.example.com:8080',\n    ]\n    \n    # Alternar entre os proxies\n    proxy_pool = cycle(proxies)\n    \n    # Raspar múltiplas URLs com diferentes proxies\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n    ]\n    \n    for url in urls:\n        # Obter próximo proxy\n        proxy = next(proxy_pool)\n        \n        # Configurar opções com este proxy\n        options = ChromiumOptions()\n        options.add_argument(f'--proxy-server={proxy}')\n        \n        # Usar proxy para esta instância do navegador\n        async with Chrome(options=options) as browser:\n            tab = await browser.start()\n            await tab.go_to(url)\n            \n            title = await tab.execute_script('return document.title')\n            print(f\"[{proxy.split('@')[1]}] {url}: {title}\")\n\nasyncio.run(rotating_proxy_example())\n```\n\n!!! tip \"Estratégias de Rotação de Proxy\"\n    **Rotação por navegador** (acima):\n\n    - Cada instância do navegador usa um proxy diferente\n    - Melhor para isolamento e evitar conflitos de sessão\n    \n    **Rotação por requisição**:\n\n    - Mais complexo, requer interceptação de requisições\n    - Veja [Interceptação de Requisições](../network/interception.md) para implementação\n\n## Proxies Residenciais vs Datacenter\n\nEntender os tipos de proxy ajuda a escolher o serviço certo:\n\n| Característica | Residenciais | Datacenter |\n|---|---|---|\n| **Fonte do IP** | ISPs residenciais reais | Data centers |\n| **Legitimidade** | Alta (usuários reais) | Baixa (faixas conhecidas) |\n| **Risco de Detecção** | Muito baixo | Alto |\n| **Velocidade** | Média (150-500ms) | Muito rápida (<50ms) |\n| **Custo** | Caro ($5-15/GB) | Barato ($0.10-1/GB) |\n| **Melhor Para** | Sites anti-bot, e-commerce | APIs, ferramentas internas |\n\n### Proxies Residenciais\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def residential_proxy_example():\n    \"\"\"Usar proxy residencial para sites anti-bot.\"\"\"\n    options = ChromiumOptions()\n    \n    # Proxy residencial com alta pontuação de confiança\n    options.add_argument('--proxy-server=http://user:pass@residential.proxy.com:8080')\n    \n    # Combinar com opções de furtividade\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Acessar site protegido\n        await tab.go_to('https://protected-site.com')\n        print(\"Acessado com sucesso através de proxy residencial\")\n\nasyncio.run(residential_proxy_example())\n```\n\n**Quando usar Residenciais:**\n\n- Sites com forte proteção anti-bot (Cloudflare, DataDome)\n- Raspagem de e-commerce (Amazon, eBay, etc.)\n- Automação de mídias sociais\n- Serviços financeiros\n- Qualquer site que bloqueia ativamente IPs de datacenter\n\n### Proxies Datacenter\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def datacenter_proxy_example():\n    \"\"\"Usar proxy datacenter rápido para APIs e sites não protegidos.\"\"\"\n    options = ChromiumOptions()\n    \n    # Proxy datacenter rápido\n    options.add_argument('--proxy-server=http://user:pass@datacenter.proxy.com:8080')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Raspagem rápida de API\n        await tab.go_to('https://api.example.com/data')\n\nasyncio.run(datacenter_proxy_example())\n```\n\n**Quando usar Datacenter:**\n\n- APIs públicas sem limites de requisição\n- Automação interna/corporativa\n- Sites sem medidas anti-bot\n- Raspagem de alto volume e crítica em velocidade\n- Desenvolvimento e testes\n\n!!! warning \"A Qualidade do Proxy Importa\"\n    **Proxies ruins** causam mais problemas do que resolvem:\n    \n    - Tempos de resposta lentos (timeouts)\n    - Falhas de conexão (taxas de erro)\n    - IPs em lista negra (banimentos imediatos)\n    - Vazamento do IP real (violação de privacidade)\n    \n    **Invista em proxies de qualidade** de provedores respeitáveis. Proxies gratuitos quase nunca valem a pena.\n\n## Testando Seu Proxy\n\nVerifique a configuração do proxy antes de rodar a automação em produção:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def test_proxy():\n    \"\"\"Testar conexão e configuração do proxy.\"\"\"\n    proxy_url = 'http://user:pass@proxy.example.com:8080'\n    \n    options = ChromiumOptions()\n    options.add_argument(f'--proxy-server={proxy_url}')\n    \n    try:\n        async with Chrome(options=options) as browser:\n            tab = await browser.start()\n            \n            # Teste 1: Conexão\n            print(\"Testando conexão do proxy...\")\n            await tab.go_to('https://httpbin.org/ip', timeout=10)\n            \n            # Teste 2: Verificação de IP\n            print(\"Verificando IP do proxy...\")\n            ip_response = await tab.execute_script('return document.body.textContent')\n            print(f\"[OK] IP do Proxy: {ip_response}\")\n            \n            # Teste 3: Localização geográfica (se disponível)\n            await tab.go_to('https://ipapi.co/json/')\n            geo_data = await tab.execute_script('return document.body.textContent')\n            print(f\"[OK] Dados geográficos: {geo_data}\")\n            \n            # Teste 4: Teste de velocidade\n            import time\n            start = time.time()\n            await tab.go_to('https://example.com')\n            load_time = time.time() - start\n            print(f\"[OK] Tempo de carregamento: {load_time:.2f}s\")\n            \n            if load_time > 5:\n                print(\"[AVISO] Tempo de resposta do proxy lento\")\n            \n            print(\"\\n[SUCESSO] Todos os testes de proxy passaram!\")\n            \n    except asyncio.TimeoutError:\n        print(\"[ERRO] Timeout na conexão do proxy\")\n    except Exception as e:\n        print(f\"[ERRO] Teste de proxy falhou: {e}\")\n\nasyncio.run(test_proxy())\n```\n\n## Leitura Adicional\n\n- **[Análise Profunda da Arquitetura de Proxy](../../deep-dive/proxy-architecture.md)** - Fundamentos de rede, TCP/UDP, HTTP/2/3, internos do SOCKS5, análise de segurança e construção do seu próprio servidor proxy\n- **[Opções do Navegador](browser-options.md)** - Argumentos de linha de comando e configuração\n- **[Interceptação de Requisições](../network/interception.md)** - Como a autenticação de proxy funciona\n- **[Preferências do Navegador](browser-preferences.md)** - Furtividade e fingerprinting\n- **[Contextos](../browser-management/contexts.md)** - Usando diferentes proxies por contexto\n\n!!! tip \"Comece Simples\"\n    Comece com uma configuração de proxy simples, teste exaustivamente, depois adicione complexidade (rotação, lógica de retentativa, monitoramento) conforme necessário. Proxies de qualidade são mais importantes do que estratégias complexas de rotação.\n    \n    Para aqueles interessados em entender proxies em um nível mais profundo, a **[Análise Profunda da Arquitetura de Proxy](../../deep-dive/proxy-architecture.md)** fornece cobertura abrangente de protocolos de rede, considerações de segurança e até o guia na construção do seu próprio servidor proxy."
  },
  {
    "path": "docs/pt/features/core-concepts.md",
    "content": "# Conceitos Principais\n\nEntender o que torna o Pydoll diferente começa com suas decisões fundamentais de design. Estas não são apenas escolhas técnicas; elas impactam diretamente como você escreve scripts de automação, quais problemas você pode resolver e quão confiáveis serão suas soluções.\n\n## Zero WebDrivers\n\nUma das vantagens mais significativas do Pydoll é a eliminação completa das dependências do WebDriver. Se você já lutou com erros do tipo \"a versão do chromedriver não corresponde à versão do Chrome\" ou lidou com falhas misteriosas do driver, você apreciará esta abordagem.\n\n### Como Funciona\n\nFerramentas tradicionais de automação de navegador, como o Selenium, dependem de executáveis WebDriver que atuam como intermediários entre seu código e o navegador. O Pydoll segue um caminho diferente, conectando-se diretamente aos navegadores através do Chrome DevTools Protocol (CDP).\n\n```mermaid\ngraph LR\n    %% Fluxo Pydoll\n    subgraph P[\"Fluxo Pydoll\"]\n        direction LR\n        P1[\"💻 Seu Codigo\"] --> P2[\"🪄 Pydoll\"]\n        P2 --> P3[\"🌐 Navegador (via CDP)\"]\n    end\n\n    %% Fluxo Tradicional Selenium\n    subgraph S[\"Fluxo Tradicional Selenium\"]\n        direction LR\n        S1[\"💻 Seu Codigo\"] --> S2[\"🔌 Cliente WebDriver\"]\n        S2 --> S3[\"⚙️ Executavel WebDriver\"]\n        S3 --> S4[\"🌐 Navegador\"]\n    end\n\n```\n\nQuando você inicia um navegador com o Pydoll, é isto que acontece nos bastidores:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    # Isso cria uma instância do Navegador\n    browser = Chrome()\n    \n    # start() inicia o Chrome com --remote-debugging-port\n    # e estabelece uma conexão WebSocket com o endpoint CDP\n    tab = await browser.start()\n    \n    # Agora você pode controlar o navegador através de comandos CDP\n    await tab.go_to('https://example.com')\n    \n    await browser.stop()\n\nasyncio.run(main())\n```\n\nNos bastidores, `browser.start()` faz o seguinte:\n\n1.  **Inicia o processo do navegador** com a flag `--remote-debugging-port=<porta>`\n2.  **Aguarda o servidor CDP** ficar disponível nessa porta\n3.  **Estabelece uma conexão WebSocket** com `ws://localhost:<porta>/devtools/...`\n4.  **Retorna uma instância de Tab** pronta para automação\n\n!!! info \"Quer Saber Mais?\"\n    Para detalhes técnicos sobre como o processo do navegador é gerenciado internamente, veja a [Análise Profunda do Domínio do Navegador](../../deep-dive/browser-domain.md#browser-process-manager).\n\n### Benefícios que Você Notará\n\n**Sem Dores de Cabeça com Gerenciamento de Versão**\n```python\n# Com Selenium, você pode ver:\n# SessionNotCreatedException: Esta versão do ChromeDriver suporta apenas a versão 120 do Chrome\n\n# Com Pydoll, você só precisa ter o Chrome instalado:\nasync with Chrome() as browser:\n    tab = await browser.start()  # Funciona com qualquer versão do Chrome\n```\n\n**Configuração Mais Simples**\n```bash\n# Configuração Selenium:\n$ pip install selenium\n$ brew install chromedriver  # ou baixe, chmod +x, adicione ao PATH...\n$ chromedriver --version     # corresponde ao seu Chrome?\n\n# Configuração Pydoll:\n$ pip install pydoll-python  # É isso!\n```\n\n**Mais Confiável**\n\nSem o WebDriver como camada intermediária, há menos pontos de falha. Seu código se comunica diretamente com o navegador através de um protocolo bem definido que os próprios desenvolvedores do Chromium usam e mantêm.\n\n### CDP: O Protocolo Por Trás da Mágica\n\nO Chrome DevTools Protocol não é apenas para o Pydoll; é o mesmo protocolo que alimenta o Chrome DevTools quando você abre o inspetor. Isso significa:\n\n- **Confiabilidade testada em batalha**: Usado por milhões de desenvolvedores diariamente\n- **Capacidades ricas**: Tudo o que o DevTools pode fazer, o Pydoll pode fazer\n- **Desenvolvimento ativo**: O Google mantém e evolui o CDP continuamente\n\n!!! tip \"Análise Profunda: Entendendo o CDP\"\n    Para uma compreensão abrangente de como o CDP funciona e por que ele é superior ao WebDriver, veja nossa [Análise Profunda do Chrome DevTools Protocol](../../deep-dive/cdp.md).\n\n## Arquitetura Async-First (Prioritariamente Assíncrona)\n\nO Pydoll não é apenas compatível com async; ele foi projetado desde o início para alavancar o framework `asyncio` do Python. Isso não é uma funcionalidade superficial; é fundamental para como o Pydoll alcança alto desempenho.\n\n!!! info \"Novo na Programação Assíncrona?\"\n    Se você não está familiarizado com a sintaxe `async`/`await` do Python ou conceitos do asyncio, recomendamos fortemente ler nosso guia [Entendendo Async/Await](../../deep-dive/connection-layer.md#understanding-asyncawait) primeiro. Ele explica os fundamentos com exemplos práticos que o ajudarão a entender como a arquitetura assíncrona do Pydoll funciona e por que ela é tão poderosa para automação de navegador.\n\n### Por que Async é Importante para Automação de Navegador\n\nA automação de navegador envolve muita espera: páginas carregando, elementos aparecendo, requisições de rede completando. Ferramentas síncronas tradicionais desperdiçam tempo de CPU durante essas esperas. A arquitetura assíncrona permite que você faça trabalho útil enquanto espera.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_page(browser, url):\n    \"\"\"Raspar uma única página.\"\"\"\n    tab = await browser.new_tab()\n    await tab.go_to(url)\n    title = await tab.execute_script('return document.title')\n    await tab.close()\n    return title\n\nasync def main():\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n    ]\n    \n    async with Chrome() as browser:\n        await browser.start()\n        \n        # Processar todas as URLs concorrentemente!\n        titles = await asyncio.gather(\n            *(scrape_page(browser, url) for url in urls)\n        )\n        \n        print(titles)\n\nasyncio.run(main())\n```\n\nNeste exemplo, em vez de raspar as páginas uma após a outra (o que poderia levar 3 × 2 segundos = 6 segundos), todas as três páginas são raspadas concorrentemente, levando aproximadamente 2 segundos no total.\n\n### Concorrência Verdadeira vs Threading\n\nDiferente de abordagens baseadas em threading, a arquitetura assíncrona do Pydoll fornece execução concorrente verdadeira sem a complexidade do gerenciamento de threads:\n\n```mermaid\nsequenceDiagram\n    participant Main as Tarefa Principal\n    participant Tab1 as Aba 1\n    participant Tab2 as Aba 2\n    participant Tab3 as Aba 3\n    \n    Main->>Tab1: go_to(url1)\n    Main->>Tab2: go_to(url2)\n    Main->>Tab3: go_to(url3)\n    \n    Note over Tab1,Tab3: Todas as abas navegam concorrentemente\n    \n    Tab1-->>Main: Pagina 1 carregada\n    Tab2-->>Main: Pagina 2 carregada\n    Tab3-->>Main: Pagina 3 carregada\n    \n    Main->>Main: Processar resultados\n```\n\n### Padrões Modernos do Python\n\nO Pydoll abraça idiomas modernos do Python em toda a sua estrutura:\n\n**Gerenciadores de Contexto**\n```python\n# Limpeza automática de recursos\nasync with Chrome() as browser:\n    tab = await browser.start()\n    # ... fazer trabalho ...\n# O navegador é automaticamente parado ao sair do contexto\n```\n\n**Iteradores Assíncronos**\n```python\n# Receber eventos de rede à medida que ocorrem\nawait tab.enable_network_events()\n\nasync for event in tab.network_event_stream():\n    if 'api' in event['params']['request']['url']:\n        print(f\"Chamada de API detectada: {event['params']['request']['url']}\")\n```\n\n**Gerenciadores de Contexto Assíncronos para Operações**\n```python\n# Esperar e lidar com downloads\nasync with tab.expect_download(keep_file_at='/downloads') as dl:\n    await (await tab.find(text='Download PDF')).click()\n    pdf_data = await dl.read_bytes()\n```\n\n!!! tip \"Análise Profunda\"\n    Quer entender como as operações assíncronas funcionam internamente? Confira a [Análise Profunda da Camada de Conexão](../../deep-dive/connection-layer.md) para detalhes de implementação.\n\n### Implicações de Desempenho\n\nO design \"async-first\" oferece melhorias mensuráveis de desempenho:\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def benchmark_concurrent():\n    \"\"\"Raspar 10 páginas concorrentemente.\"\"\"\n    async with Chrome() as browser:\n        await browser.start()\n        \n        start = time.time()\n        tasks = [\n            browser.new_tab(f'https://example.com/page{i}')\n            for i in range(10)\n        ]\n        await asyncio.gather(*tasks)\n        elapsed = time.time() - start\n        \n        print(f\"10 páginas carregadas em {elapsed:.2f}s\")\n        # Resultado típico: ~2-3 segundos vs 20+ segundos sequencialmente\n\nasyncio.run(benchmark_concurrent())\n```\n\n## Suporte a Múltiplos Navegadores\n\nO Pydoll fornece uma API unificada em todos os navegadores baseados em Chromium. Escreva sua automação uma vez, execute-a em qualquer lugar.\n\n### Navegadores Suportados\n\n**Google Chrome**: Alvo principal com suporte completo a funcionalidades.\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n```\n\n**Microsoft Edge**: Suporte completo, incluindo funcionalidades específicas do Edge.\n```python\nfrom pydoll.browser.chromium import Edge\n\nasync with Edge() as browser:\n    tab = await browser.start()\n```\n\n**Outros Navegadores Chromium**: Brave, Vivaldi, Opera, etc.\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.binary_location = '/path/to/brave-browser'  # ou qualquer navegador Chromium\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n```\n\nO principal benefício: todos os navegadores baseados em Chromium compartilham a mesma API. Escreva sua automação uma vez, e ela funciona no Chrome, Edge, Brave ou qualquer outro navegador Chromium sem alterações de código.\n\n### Testes Cross-Browser\n\nTeste sua automação em múltiplos navegadores sem alterar o código:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome, Edge\n\nasync def test_login(browser_class, browser_name):\n    \"\"\"Testar fluxo de login em um navegador específico.\"\"\"\n    async with browser_class() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://app.example.com/login')\n        \n        await (await tab.find(id='username')).type_text('user@example.com')\n        await (await tab.find(id='password')).type_text('password123')\n        await (await tab.find(id='login-btn')).click()\n        \n        # Verificar sucesso do login\n        success = await tab.find(id='dashboard', raise_exc=False)\n        print(f\"{browser_name} login: {'✓' if success else '✗'}\")\n\nasync def main():\n    # Testar tanto no Chrome quanto no Edge\n    await test_login(Chrome, \"Chrome\")\n    await test_login(Edge, \"Edge\")\n\nasyncio.run(main())\n```\n\n## Comportamento Semelhante ao Humano\n\nNavegadores automatizados são frequentemente detectáveis porque se comportam de forma robótica. O Pydoll inclui funcionalidades nativas para fazer as interações parecerem mais humanas.\n\n### Digitação Natural\n\nUsuários reais não digitam em velocidades perfeitamente consistentes. O método `type_text()` do Pydoll inclui atrasos aleatórios entre as teclas:\n\n```python\n# Digitar com tempo semelhante ao humano\nusername_field = await tab.find(id='username')\nawait username_field.type_text(\n    'user@example.com',\n    interval=0.1  # Média de 100ms entre teclas, com aleatoriedade\n)\n\n# Digitação mais rápida (ainda semelhante à humana)\nawait username_field.type_text(\n    'user@example.com',\n    interval=0.05  # Mais rápido, mas ainda varia\n)\n\n# Instantâneo (robótico; use apenas quando a velocidade importa mais que a furtividade)\nawait username_field.type_text(\n    'user@example.com',\n    interval=0\n)\n```\n\nO parâmetro `interval` define o atraso médio, mas o Pydoll adiciona variação aleatória para tornar o tempo mais natural.\n\n### Cliques Realistas\n\nCliques não são apenas \"disparar e esquecer\". O Pydoll automaticamente dispara todos os eventos de mouse que um usuário real dispararia:\n\n```python\nbutton = await tab.find(id='submit-button')\n\n# Comportamento padrão: clica no centro do elemento\n# Dispara automaticamente: mouseover, mouseenter, mousemove, mousedown, mouseup, click\nawait button.click()\n\n# Clique com deslocamento (útil para evitar detecção em elementos maiores)\nawait button.click(offset_x=10, offset_y=5)\n```\n\n!!! info \"Eventos do Mouse\"\n    O Pydoll dispara a sequência completa de eventos do mouse na ordem correta, simulando como navegadores reais lidam com cliques de usuários. Isso torna os cliques mais realistas em comparação com simples chamadas JavaScript `.click()`.\n\n!!! warning \"Considerações sobre Detecção\"\n    Embora o comportamento semelhante ao humano ajude a evitar a detecção básica de bots, sistemas anti-automação sofisticados usam muitos sinais. Combine essas funcionalidades com:\n    \n    - Fingerprints de navegador realistas (via preferências do navegador)\n    - Configuração adequada de proxy\n    - Atrasos razoáveis entre ações\n    - Padrões de navegação variados\n\n## Design Orientado a Eventos\n\nDiferente da automação tradicional baseada em polling (verificação periódica), o Pydoll permite que você reaja a eventos do navegador assim que eles acontecem. Isso é mais eficiente e possibilita padrões de interação sofisticados.\n\n### Monitoramento de Eventos em Tempo Real\n\nInscreva-se em eventos do navegador e execute callbacks quando eles dispararem:\n\n```python\nimport asyncio\nfrom functools import partial\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.page.events import PageEvent\nfrom pydoll.protocol.network.events import NetworkEvent\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Reagir a eventos de carregamento de página\n        async def on_page_load(event):\n            print(f\"Página carregada: {await tab.current_url}\")\n        \n        await tab.enable_page_events()\n        await tab.on(PageEvent.LOAD_EVENT_FIRED, on_page_load)\n        \n        # Monitorar requisições de rede\n        async def on_request(tab, event):\n            url = event['params']['request']['url']\n            if '/api/' in url:\n                print(f\"Chamada de API: {url}\")\n        \n        await tab.enable_network_events()\n        await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, partial(on_request, tab))\n        \n        # Navegar e observar os eventos dispararem\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)  # Deixar os eventos processarem\n\nasyncio.run(main())\n```\n\n### Categorias de Eventos\n\nO Pydoll expõe vários domínios de eventos CDP nos quais você pode se inscrever:\n\n| Domínio | Eventos de Exemplo |\n|---|---|\n| **Eventos de Página** | Carregamento concluído, navegação, diálogos JavaScript |\n| **Eventos de Rede** | Requisição enviada, resposta recebida, atividade WebSocket |\n| **Eventos DOM** | Mudanças no DOM, modificações de atributos |\n| **Eventos Fetch** | Requisição pausada, autenticação necessária |\n| **Eventos de Runtime** | Mensagens do console, exceções |\n\n### Padrões Práticos Orientados a Eventos\n\n**Capturar Respostas de API**\n```python\nimport json\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent\n\napi_data = []\n\nasync def capture_api(tab, event):\n    url = event['params']['response']['url']\n    if '/api/data' in url:\n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        api_data.append(json.loads(body))\n\nawait tab.enable_network_events()\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, partial(capture_api, tab))\n\n# Navegar e capturar automaticamente as respostas da API\nawait tab.go_to('https://app.example.com')\nawait asyncio.sleep(2)\n\nprint(f\"Capturadas {len(api_data)} respostas de API\")\n```\n\n**Esperar por Condições Específicas**\n```python\nimport asyncio\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent\n\nasync def wait_for_api_call(tab, endpoint):\n    \"\"\"Esperar por uma chamada de endpoint de API específica.\"\"\"\n    event_occurred = asyncio.Event()\n    \n    async def check_endpoint(tab, event):\n        url = event['params']['request']['url']\n        if endpoint in url:\n            event_occurred.set()\n    \n    await tab.enable_network_events()\n    callback_id = await tab.on(\n        NetworkEvent.REQUEST_WILL_BE_SENT,\n        partial(check_endpoint, tab),\n        temporary=True  # Remover automaticamente após o primeiro disparo\n    )\n\n    await event_occurred.wait()\n    print(f\"Endpoint de API {endpoint} foi chamado!\")\n\n# Uso\nawait wait_for_api_call(tab, '/api/users')\n```\n\n!!! info \"Análise Profunda: Detalhes do Sistema de Eventos\"\n    Para um guia completo sobre manejo de eventos, padrões de callback e considerações de desempenho, veja a [Análise Profunda do Sistema de Eventos](../../deep-dive/event-system.md).\n\n### Desempenho de Eventos\n\nEventos são poderosos, mas vêm com uma sobrecarga. Melhores práticas:\n\n```python\n# ✓ Bom: Habilitar apenas o que você precisa\nawait tab.enable_network_events()\n\n# ✗ Evite: Habilitar todos os eventos desnecessariamente\nawait tab.enable_page_events()\nawait tab.enable_network_events()\nawait tab.enable_dom_events()\nawait tab.enable_fetch_events()\nawait tab.enable_runtime_events()\n\n# ✓ Bom: Filtrar cedo nos callbacks\nasync def handle_request(event):\n    url = event['params']['request']['url']\n    if '/api/' not in url:\n        return  # Pular requisições não-API cedo\n    # Processar requisição de API...\n\n# ✓ Bom: Desabilitar quando terminar\nawait tab.disable_network_events()\n```\n\n## Juntando Tudo\n\nEsses conceitos principais trabalham juntos para criar um framework de automação poderoso:\n\n```python\nimport asyncio\nimport json\nfrom functools import partial\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import NetworkEvent\nfrom pydoll.constants import Keys\n\nasync def advanced_scraping():\n    \"\"\"Demonstra múltiplos conceitos principais trabalhando juntos.\"\"\"\n    async with Chrome() as browser:  # Gerenciador de contexto assíncrono\n        tab = await browser.start()\n        \n        # Orientado a eventos: Capturar dados de API\n        api_responses = []\n        \n        async def capture_data(tab, event):\n            url = event['params']['response']['url']\n            if '/api/products' in url:\n                request_id = event['params']['requestId']\n                body = await tab.get_network_response_body(request_id)\n                api_responses.append(json.loads(body))\n        \n        await tab.enable_network_events()\n        await tab.on(NetworkEvent.RESPONSE_RECEIVED, partial(capture_data, tab))\n        \n        # Navegar com simplicidade zero-webdriver\n        await tab.go_to('https://example.com/products')\n        \n        # Interação semelhante à humana\n        search = await tab.find(id='search')\n        await search.type_text('laptop', interval=0.1)  # Digitação natural\n        await search.press_keyboard_key(Keys.ENTER)\n        \n        # Esperar por respostas da API (eficiência assíncrona)\n        await asyncio.sleep(2)\n        \n        print(f\"Capturados {len(api_responses)} produtos da API\")\n        return api_responses\n\n# Suporte a múltiplos navegadores: funciona com Chrome, Edge, etc.\nasyncio.run(advanced_scraping())\n```\n\nEsses conceitos fundamentais informam todo o resto no Pydoll. À medida que você explora funcionalidades específicas, verá esses princípios em ação, trabalhando juntos para criar automação de navegador confiável, eficiente e sustentável.\n\n---\n\n## O Que Vem a Seguir?\n\nAgora que você entende o design principal do Pydoll, está pronto para explorar funcionalidades específicas:\n\n- **[Localização de Elementos](element-finding.md)** - Aprenda as APIs intuitivas de localização de elementos do Pydoll\n- **[Funcionalidades de Rede](../network/monitoring.md)** - Aproveite o sistema de eventos para análise de rede\n- **[Gerenciamento do Navegador](../browser-management/tabs.md)** - Use padrões assíncronos para operações concorrentes\n\nPara um entendimento técnico mais profundo, explore a seção [Análise Profunda](../../deep-dive/index.md)."
  },
  {
    "path": "docs/pt/features/element-finding.md",
    "content": "# Localização de Elementos\n\nEncontrar elementos em uma página web é a base da automação de navegadores. O Pydoll introduz uma abordagem revolucionária e intuitiva que torna a localização de elementos mais poderosa e fácil de usar do que os métodos tradicionais baseados em seletores.\n\n## Por que a Abordagem do Pydoll é Diferente\n\nFerramentas tradicionais de automação de navegador forçam você a pensar em termos de seletores CSS e expressões XPath desde o início. O Pydoll inverte isso: você descreve o que está procurando usando atributos HTML naturais, e o Pydoll descobre a estratégia de seletor ideal.\n\n```python\n# Abordagem tradicional (outras ferramentas)\nelement = driver.find_element(By.XPATH, \"//input[@type='email' and @name='username']\")\n\n# Abordagem do Pydoll\nelement = await tab.find(tag_name=\"input\", type=\"email\", name=\"username\")\n```\n\nAmbos encontram o mesmo elemento, mas a sintaxe do Pydoll é mais clara, mais fácil de manter e menos propensa a erros.\n\n### Visão Geral dos Métodos de Localização de Elementos\n\nO Pydoll oferece três abordagens principais para encontrar elementos:\n\n| Método | Usar Quando | Exemplo |\n|---|---|---|\n| **`find()`** | Você sabe os atributos HTML | `await tab.find(id=\"username\")` |\n| **`query()`** | Você tem um seletor CSS/XPath | `await tab.query(\"div.content\")` |\n| **Travessia** | Você quer explorar a partir de um elemento conhecido | `await element.get_children_elements()` |\n\n```mermaid\nflowchart LR\n    A[\"Precisa de Elemento?\"] --> B{\"O que voce tem?\"};\n    B -->|\"Atributos HTML\"| C[\"Metodo find()\"];\n    B -->|\"CSS/XPath\"| D[\"Metodo query()\"];\n    B -->|\"Elemento Pai\"| E[\"Travessia\"];\n    \n    C --> F[\"WebElement\"];\n    D --> F;\n    E --> G[\"Lista de WebElements\"];\n```\n\n!!! info \"Análise profunda: Como Funciona\"\n    Curioso sobre como o Pydoll implementa a localização de elementos internamente? Confira a documentação [FindElements Mixin](../deep-dive/find-elements-mixin.md) para aprender sobre a arquitetura, otimizações de desempenho e estratégias internas de seletores.\n\n## O Método find(): Seleção Natural de Elementos\n\nO método `find()` é sua principal ferramenta para localizar elementos. Ele aceita atributos HTML comuns como parâmetros e constrói automaticamente o seletor mais eficiente.\n\n### Uso Básico\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def basic_finding():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Encontrar por ID (mais comum e mais rápido)\n        username = await tab.find(id=\"username\")\n        \n        # Encontrar por nome de classe\n        submit_button = await tab.find(class_name=\"btn-primary\")\n        \n        # Encontrar por nome de tag\n        first_paragraph = await tab.find(tag_name=\"p\")\n        \n        # Encontrar por atributo name\n        email_field = await tab.find(name=\"email\")\n        \n        # Encontrar por conteúdo de texto\n        login_link = await tab.find(text=\"Login\")\n\nasyncio.run(basic_finding())\n```\n\n### Combinando Atributos para Precisão\n\nO verdadeiro poder do `find()` vem da combinação de múltiplos atributos para criar seletores precisos:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def precise_finding():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/form')\n        \n        # Combinar nome de tag com tipo\n        password_input = await tab.find(tag_name=\"input\", type=\"password\")\n        \n        # Combinar tag, classe e atributos personalizados\n        submit_button = await tab.find(\n            tag_name=\"button\",\n            class_name=\"btn\",\n            type=\"submit\"\n        )\n        \n        # Usar atributos data\n        product_card = await tab.find(\n            tag_name=\"div\",\n            data_testid=\"product-card\",\n            data_category=\"electronics\"\n        )\n        \n        # Combinar múltiplas condições\n        specific_link = await tab.find(\n            tag_name=\"a\",\n            class_name=\"nav-link\",\n            href=\"/dashboard\"\n        )\n\nasyncio.run(precise_finding())\n```\n\n!!! info \"Lógica de Combinação: E (AND)\"\n    Combinar atributos no `find()` funciona como uma operação E (AND). O elemento deve corresponder a **todos** os atributos fornecidos.\n    \n    Para cenários mais complexos que exigem lógica OU (OR) — como encontrar um elemento que pode ter um `id` ou um `name` diferente — a abordagem correta é encadear múltiplas chamadas `find()`, como demonstrado na seção \"Exemplo Completo\".\n\n!!! tip \"Convenção de Nomenclatura de Atributos\"\n    Use underscores para nomes de atributos com hífens. Por exemplo, `data-testid` se torna `data_testid`, e `aria-label` se torna `aria_label`. O Pydoll os converte automaticamente para o formato correto.\n\n### Como o find() Seleciona a Estratégia Ideal\n\nO Pydoll escolhe automaticamente o seletor mais eficiente com base nos atributos que você fornece:\n\n| Atributos Fornecidos | Estratégia Usada | Desempenho |\n|---|---|---|\n| Único: `id` | `By.ID` | ⚡ Mais Rápido |\n| Único: `class_name` | `By.CLASS_NAME` | ⚡ Rápido |\n| Único: `name` | `By.NAME` | ⚡ Rápido |\n| Único: `tag_name` | `By.TAG_NAME` | ⚡ Rápido |\n| Único: `text` | `By.XPATH` | ⚡ Rápido |\n| Múltiplos atributos | Expressão XPath | ✓ Eficiente |\n\n```mermaid\nflowchart LR\n    A[\"Atributos do find()\"] --> B{\"Unico ou Multiplo?\"};\n    B -->|\"Unico\"| C[\"Seletor Direto\"];\n    B -->|\"Multiplo\"| D[\"Construir XPath\"];\n    C --> E[\"Execucao Rapida\"];\n    D --> E;\n```\n\n### Encontrando Múltiplos Elementos\n\nUse `find_all=True` para obter uma lista de todos os elementos correspondentes:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def find_multiple():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/products')\n        \n        # Encontrar todos os cards de produto\n        products = await tab.find(class_name=\"product-card\", find_all=True)\n        print(f\"Encontrados {len(products)} produtos\")\n        \n        # Encontrar todos os links na navegação\n        nav_links = await tab.find(\n            tag_name=\"a\",\n            class_name=\"nav-link\",\n            find_all=True\n        )\n        \n        # Processar cada elemento\n        for link in nav_links:\n            text = await link.text\n            href = await link.get_attribute(\"href\")\n            print(f\"Link: {text} → {href}\")\n\nasyncio.run(find_multiple())\n```\n\n### Esperando por Elementos Dinâmicos\n\nAplicações web modernas carregam conteúdo dinamicamente. Use `timeout` para esperar que os elementos apareçam:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def wait_for_elements():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/dashboard')\n        \n        # Esperar até 10 segundos pelo elemento aparecer\n        dynamic_content = await tab.find(\n            class_name=\"dynamic-content\",\n            timeout=10\n        )\n        \n        # Esperar por dados carregados via AJAX\n        user_profile = await tab.find(\n            id=\"user-profile\",\n            timeout=15\n        )\n        \n        # Lidar com elementos que podem não aparecer\n        optional_banner = await tab.find(\n            class_name=\"promo-banner\",\n            timeout=3,\n            raise_exc=False  # Retorna None se não encontrado\n        )\n        \n        if optional_banner:\n            await optional_banner.click()\n        else:\n            print(\"Nenhum banner promocional presente\")\n\nasyncio.run(wait_for_elements())\n```\n\n!!! warning \"Melhores Práticas de Timeout\"\n    Use valores de timeout razoáveis. Muito curtos e você perderá elementos de carregamento lento; muito longos e você desperdiçará tempo esperando por elementos que não existem. Comece com 5-10 segundos para a maioria dos conteúdos dinâmicos.\n\n## O Método query(): Acesso Direto a Seletores\n\nPara desenvolvedores que preferem seletores tradicionais ou precisam de lógicas de seleção mais complexas, o método `query()` fornece acesso direto a seletores CSS e expressões XPath.\n\n### Seletores CSS\n\nSeletores CSS são rápidos, amplamente compreendidos e perfeitos para a maioria dos casos de uso:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def css_selector_examples():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Seletores simples\n        main_nav = await tab.query(\"nav.main-menu\")\n        first_article = await tab.query(\"article:first-child\")\n        \n        # Seletores de atributo\n        submit_button = await tab.query(\"button[type='submit']\")\n        required_inputs = await tab.query(\"input[required]\", find_all=True)\n        \n        # Seletores complexos\n        nested = await tab.query(\"div.container > .content .item:nth-child(2)\")\n        \n        # Pseudo-classes\n        first_enabled_button = await tab.query(\"button:not([disabled])\")\n\nasyncio.run(css_selector_examples())\n```\n\n### Expressões XPath\n\nXPath se destaca em relações complexas e correspondência de texto:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def xpath_examples():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/table')\n        \n        # Correspondência de texto\n        button = await tab.query(\"//button[contains(text(), 'Submit')]\")\n        \n        # Navegar para o pai\n        input_parent = await tab.query(\"//input[@name='email']/parent::div\")\n        \n        # Encontrar elementos irmãos\n        label_input = await tab.query(\n            \"//label[text()='Email:']/following-sibling::input\"\n        )\n        \n        # Consultas complexas de tabela\n        edit_button = await tab.query(\n            \"//tr[td[text()='John Doe']]//button[@class='btn-edit']\"\n        )\n\nasyncio.run(xpath_examples())\n```\n\n!!! info \"CSS vs XPath: Qual Usar?\"\n    Para um guia completo sobre como escolher entre seletores CSS e XPath, incluindo referências de sintaxe e exemplos do mundo real, veja o [Guia de Seletores](../deep-dive/selectors-guide.md).\n\n## Travessia do DOM: Filhos e Irmãos\n\nÀs vezes, você precisa explorar a árvore DOM a partir de um ponto de partida conhecido. O Pydoll fornece métodos dedicados para atravessar relações entre elementos.\n\n### Estrutura da Árvore DOM\n\nEntender a estrutura da árvore DOM ajuda a escolher o método de travessia correto:\n\n```mermaid\ngraph TB\n    Root[Raiz do Documento]\n    Root --> Container[div id='container']\n    \n    Container --> Child1[div class='card']\n    Container --> Child2[div class='card']\n    Container --> Child3[div class='card']\n    \n    Child1 --> GrandChild1[h2 title]\n    Child1 --> GrandChild2[p description]\n    Child1 --> GrandChild3[button action]\n    \n    Child2 --> GrandChild4[h2 title]\n    Child2 --> GrandChild5[p description]\n    \n    Child3 --> GrandChild6[h2 title]\n```\n\n### Obtendo Elementos Filhos\n\nO método `get_children_elements()` recupera descendentes de um elemento:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def traverse_children():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/cards')\n        \n        # Obter contêiner\n        container = await tab.find(id=\"cards-container\")\n        \n        # Obter apenas filhos diretos (max_depth=1)\n        direct_children = await container.get_children_elements(max_depth=1)\n        print(f\"Contêiner tem {len(direct_children)} filhos diretos\")\n        \n        # Incluir netos (max_depth=2)\n        descendants = await container.get_children_elements(max_depth=2)\n        print(f\"Encontrados {len(descendants)} elementos até 2 níveis de profundidade\")\n        \n        # Filtrar por nome de tag\n        links = await container.get_children_elements(\n            max_depth=3,\n            tag_filter=[\"a\"]\n        )\n        print(f\"Encontrados {len(links)} links no contêiner\")\n        \n        # Combinar filtros para elementos específicos\n        nav_links = await container.get_children_elements(\n            max_depth=2,\n            tag_filter=[\"a\", \"button\"]\n        )\n\nasyncio.run(traverse_children())\n```\n\n### Obtendo Elementos Irmãos\n\nO método `get_siblings_elements()` encontra elementos no mesmo nível:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def traverse_siblings():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/list')\n        \n        # Encontrar item ativo\n        active_item = await tab.find(class_name=\"item-active\")\n        \n        # Obter todos os irmãos (excluindo o próprio active_item)\n        all_siblings = await active_item.get_siblings_elements()\n        print(f\"Item ativo tem {len(all_siblings)} irmãos\")\n        \n        # Filtrar irmãos por tag\n        link_siblings = await active_item.get_siblings_elements(\n            tag_filter=[\"a\"]\n        )\n        \n        # Processar elementos irmãos\n        for sibling in all_siblings:\n            text = await sibling.text\n            print(f\"Irmão: {text}\")\n\nasyncio.run(traverse_siblings())\n```\n\n!!! tip \"Considerações de Desempenho\"\n    A travessia do DOM pode ser cara para árvores grandes. Prefira valores `max_depth` rasos e parâmetros `tag_filter` específicos para minimizar o número de nós processados. Para estruturas profundamente aninhadas, considere múltiplas chamadas `find()` direcionadas em vez de uma única travessia profunda.\n\n## Encontrando Elementos Dentro de Elementos\n\nUma vez que você tem um elemento, pode pesquisar dentro de seu escopo usando os mesmos métodos `find()` e `query()`.\n\n!!! warning \"Importante: Comportamento de Profundidade de Busca\"\n    Quando você chama `element.find()` ou `element.query()`, o Pydoll busca em **TODOS os descendentes** (filhos, netos, bisnetos, etc.), não apenas nos filhos diretos. Este é o comportamento padrão do `querySelector()` e corresponde ao que a maioria dos desenvolvedores espera.\n\n### Entendendo o Escopo de Busca\n\n```mermaid\ngraph TB\n    Container[div id='container']\n    \n    Container --> Child1[div class='card' ✓]\n    Container --> Child2[div class='card' ✓]\n    Container --> Child3[div class='other']\n    \n    Child1 --> GrandChild1[div class='card' ✓]\n    Child1 --> GrandChild2[p class='text']\n    \n    Child3 --> GrandChild3[div class='card' ✓]\n    Child3 --> GrandChild4[div class='card' ✓]\n```\n\n```python\n# Isso encontra TODOS os 5 elementos com class='card' na árvore\n# (2 filhos diretos + 3 descendentes aninhados)\ncards = await container.find(class_name=\"card\", find_all=True)\nprint(len(cards))  # Saída: 5\n```\n\n### Busca Básica com Escopo\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scoped_search():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/products')\n        \n        # Encontrar um contêiner de produto\n        product_card = await tab.find(class_name=\"product-card\")\n        \n        # Pesquisar dentro do card de produto (busca em TODOS os descendentes, retorna apenas a primeira correspondência)\n        product_title = await product_card.find(class_name=\"title\")\n        product_price = await product_card.find(class_name=\"price\")\n        add_button = await product_card.find(tag_name=\"button\", text=\"Add to Cart\")\n        \n        # Fazer query dentro do escopo\n        product_image = await product_card.query(\"img.product-image\")\n        \n        # Encontrar todos os itens dentro de um contêiner (TODOS os descendentes)\n        nav_menu = await tab.find(class_name=\"nav-menu\")\n        menu_items = await nav_menu.find(tag_name=\"li\", find_all=True)\n        \n        print(f\"Menu tem {len(menu_items)} itens\")\n\nasyncio.run(scoped_search())\n```\n\n### Encontrando Apenas Filhos Diretos\n\nSe você precisa encontrar **apenas filhos diretos** (profundidade 1), use o combinador filho `>` do CSS ou XPath:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def direct_children_only():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/cards')\n        \n        container = await tab.find(id=\"cards-container\")\n        \n        # Método 1: Combinador filho CSS (>)\n        # Encontra APENAS filhos diretos com class='card'\n        direct_cards = await container.query(\"> .card\", find_all=True)\n        print(f\"Filhos diretos: {len(direct_cards)}\")\n        \n        # Método 2: XPath filho direto\n        direct_divs = await container.query(\"./div[@class='card']\", find_all=True)\n        \n        # Método 3: Usar get_children_elements() com max_depth=1\n        # (mas isso filtra apenas por tag, não por outros atributos)\n        direct_children = await container.get_children_elements(\n            max_depth=1,\n            tag_filter=[\"div\"]\n        )\n        \n        # Então filtre manualmente por classe\n        cards_only = [\n            child for child in direct_children\n            if 'card' in (await child.get_attribute('class') or '')\n        ]\n\nasyncio.run(direct_children_only())\n```\n\n### Comparação: find() vs get_children_elements()\n\n| Funcionalidade | `find()` / `query()` | `get_children_elements()` |\n|---|---|---|\n| **Profundidade de Busca** | TODOS os descendentes | Configurável com `max_depth` |\n| **Filtrar Por** | Qualquer atributo HTML | Apenas nome da tag |\n| **Caso de Uso** | Encontrar elementos específicos em qualquer lugar na subárvore | Explorar estrutura DOM, obter filhos diretos |\n| **Desempenho** | Otimizado para atributo único | Bom para exploração ampla |\n| **Parâmetro** | `tag_name=\"a\"` (string) | `tag_filter=[\"a\"]` (lista) |\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def comparison_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        container = await tab.find(id=\"container\")\n        \n        # Cenário 1: Eu quero TODOS os links em qualquer lugar no contêiner\n        # Use find() - busca em todos os descendentes\n        all_links = await container.find(tag_name=\"a\", find_all=True)\n        \n        # Cenário 2: Eu quero APENAS links filhos diretos\n        # Use combinador filho CSS\n        direct_links = await container.query(\"> a\", find_all=True)\n        \n        # Cenário 3: Eu quero filhos diretos com classe específica\n        # Use combinador filho CSS\n        direct_cards = await container.query(\"> .card\", find_all=True)\n        \n        # Cenário 4: Eu quero explorar a estrutura DOM\n        # Use get_children_elements()\n        direct_children = await container.get_children_elements(max_depth=1)\n        \n        # Cenário 5: Eu quero todos os descendentes até a profundidade 2, filtrados por tag\n        # Use get_children_elements()\n        shallow_links = await container.get_children_elements(\n            max_depth=2,\n            tag_filter=[\"a\"]\n        )\n\nasyncio.run(comparison_example())\n```\n\n!!! tip \"Quando Usar Cada Método\"\n    - **Use `find()`**: Quando você sabe os atributos (classe, id, etc.) e quer pesquisar toda a subárvore\n    - **Use `query(\"> .class\")`**: Quando você precisa apenas de filhos diretos com atributos específicos\n    - **Use `get_children_elements()`**: Ao explorar a estrutura DOM ou filtrar apenas por tag\n\n### Casos de Uso Comuns\n\nEssa busca com escopo é incrivelmente útil para trabalhar com padrões repetitivos como:\n\n- Cards de produtos em sites de e-commerce\n- Linhas de tabela com múltiplas células\n- Seções de formulário com múltiplos campos\n- Menus de navegação com itens aninhados\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def practical_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/products')\n        \n        # Encontrar todos os cards de produto na página\n        product_cards = await tab.find(class_name=\"product-card\", find_all=True)\n        \n        for card in product_cards:\n            # Dentro de cada card, encontrar TODOS os descendentes com essas classes\n            title = await card.find(class_name=\"product-title\")\n            price = await card.find(class_name=\"product-price\")\n            \n            # Obter o botão que está em qualquer lugar dentro deste card\n            buy_button = await card.find(tag_name=\"button\", text=\"Buy Now\")\n            \n            title_text = await title.text\n            price_text = await price.text\n            \n            print(f\"Produto: {title_text}, Preço: {price_text}\")\n            \n            # Clicar no botão de compra\n            await buy_button.click()\n\nasyncio.run(practical_example())\n```\n\n\n## Suporte a Shadow DOM\n\nMuitas aplicações web modernas utilizam [Shadow DOM](https://developer.mozilla.org/pt-BR/docs/Web/API/Web_components/Using_shadow_DOM) para encapsular os internos de componentes. O Pydoll fornece acesso transparente a elementos dentro de árvores shadow através da classe `ShadowRoot`.\n\n### Como o Shadow DOM Funciona\n\n```mermaid\ngraph TB\n    Host[\"div#my-component (shadow host)\"]\n    SR[\"ShadowRoot (open)\"]\n    Internal1[\"button.internal-btn\"]\n    Internal2[\"input.internal-input\"]\n\n    Host --> SR\n    SR --> Internal1\n    SR --> Internal2\n```\n\nElementos dentro de um shadow root são ocultos de consultas DOM regulares. Você precisa primeiro acessar o shadow root e então buscar dentro dele.\n\n### Acessando Shadow Roots\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def shadow_dom_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/web-components')\n\n        # Encontrar o elemento shadow host\n        shadow_host = await tab.find(id='my-component')\n\n        # Acessar seu shadow root\n        shadow_root = await shadow_host.get_shadow_root()\n\n        # Encontrar elementos dentro do shadow root usando query() com seletores CSS\n        button = await shadow_root.query('.internal-btn')\n        await button.click()\n\n        input_field = await shadow_root.query('input[type=\"email\"]')\n        await input_field.type_text('user@example.com')\n\nasyncio.run(shadow_dom_example())\n```\n\n### query() com Seletores CSS\n\n`ShadowRoot` herda de `FindElementsMixin` com a restrição `_css_only`, o que significa que apenas `query()` com seletores CSS é suportado. O método `find()` e `query()` com XPath lançam `NotImplementedError`:\n\n```python\n# query() com seletores CSS — abordagem recomendada\nelement = await shadow_root.query('#inner-id')\nelement = await shadow_root.query('button.primary')\nelement = await shadow_root.query('div.container > .content')\n\n# find_all para múltiplos elementos\nitems = await shadow_root.query('.item', find_all=True)\n\n# Espera com timeout\nelement = await shadow_root.query('#dynamic', timeout=5)\n```\n\n!!! warning \"find() e XPath não são suportados em ShadowRoot\"\n    Chamar `shadow_root.find()` ou `shadow_root.query('//xpath')` lançará `NotImplementedError`. Sempre use `query()` com seletores CSS ao trabalhar com shadow roots.\n\n### Shadow Roots Aninhados\n\nWeb components podem conter outros web components com seus próprios shadow roots:\n\n```python\nasync def nested_shadow():\n    outer_host = await tab.find(tag_name='outer-component')\n    outer_shadow = await outer_host.get_shadow_root()\n\n    inner_host = await outer_shadow.query('inner-component')\n    inner_shadow = await inner_host.get_shadow_root()\n\n    deep_button = await inner_shadow.query('.deep-btn')\n    await deep_button.click()\n```\n\n### Buscando Shadow Roots: find_shadow_roots()\n\nQuando você precisa explorar quais shadow roots existem na página (útil para depuração ou páginas dinâmicas como desafios Cloudflare), use `find_shadow_roots()`:\n\n```python\n# Buscar todos os shadow roots na página\nshadow_roots = await tab.find_shadow_roots()\n\nfor sr in shadow_roots:\n    print(f'Modo: {sr.mode}, Host: {sr.host_element}')\n    # Buscar dentro de cada shadow root\n    btn = await sr.query('button', raise_exc=False)\n    if btn:\n        await btn.click()\n```\n\n#### Esperando Shadow Roots: `timeout`\n\nShadow hosts sao frequentemente injetados de forma assincrona (ex: Cloudflare Turnstile carregando dentro de um OOPIF). Use `timeout` para fazer polling ate que os shadow roots aparecam:\n\n```python\n# Esperar ate 10 segundos pelos shadow roots\nshadow_roots = await tab.find_shadow_roots(timeout=10)\n```\n\nO metodo `get_shadow_root()` em elementos tambem suporta `timeout`:\n\n```python\n# Esperar pelo shadow root de um elemento\nhost = await tab.find(id='my-component', timeout=5)\nshadow = await host.get_shadow_root(timeout=5)\n```\n\n#### Travessia Profunda: IFrames Cross-Origin (OOPIFs)\n\nPor padrão, `find_shadow_roots()` percorre apenas a árvore DOM do documento principal (que inclui iframes same-origin via `contentDocument`, mas **não** iframes cross-origin). Passe `deep=True` para também descobrir shadow roots dentro de iframes cross-origin (OOPIFs):\n\n```python\n# Incluir shadow roots de iframes cross-origin (ex: Cloudflare Turnstile)\nshadow_roots = await tab.find_shadow_roots(deep=True, timeout=10)\n\nfor sr in shadow_roots:\n    print(f'Modo: {sr.mode}, Host: {sr.host_element}')\n    # Elementos encontrados dentro desses shadow roots roteiam\n    # automaticamente comandos CDP pela sessão OOPIF correta\n    btn = await sr.query('input[type=\"checkbox\"]', raise_exc=False)\n    if btn:\n        await btn.click()\n```\n\n!!! tip \"Quando usar `deep=True`\"\n    Use `deep=True` ao automatizar páginas com widgets embutidos cross-origin, como captchas Cloudflare Turnstile, formulários de pagamento de terceiros ou botões de login social. Esses widgets tipicamente usam iframes cross-origin com shadow roots fechados dentro deles.\n\n### Propriedades do Shadow Root\n\n```python\nshadow_root = await element.get_shadow_root()\n\n# Verificar o modo do shadow root (open, closed ou user-agent)\nprint(shadow_root.mode)  # ShadowRootType.OPEN\n\n# Acessar o elemento host\nhost = shadow_root.host_element\n\n# Obter o HTML interno do shadow root\nhtml = await shadow_root.inner_html\n```\n\n!!! note \"Shadow Roots Fechados\"\n    Shadow roots fechados (`mode='closed'`) são acessíveis via CDP pois o protocolo ignora as restrições do JavaScript. Porém, alguns shadow roots internos do navegador (user-agent) podem ter acessibilidade limitada.\n\n## Trabalhando com iFrames\n\n!!! info \"Guia Completo de IFrame Disponível\"\n    Esta seção cobre a interação básica com iframe para localização de elementos. Para um guia completo incluindo iframes aninhados, manejo de CAPTCHA, análise técnica profunda e solução de problemas, veja **[Trabalhando com IFrames](automation/iframes.md)**.\n\niFrames apresentam um desafio especial na automação de navegador porque eles têm contextos DOM separados. O Pydoll torna a interação com iframe transparente:\n\n```mermaid\nflowchart TB\n    Principal[tab]\n    IFrame[\"WebElement do iframe\"]\n    Conteudo[\"elementos dentro do iframe\"]\n\n    Principal -->|\"find('iframe')\"| IFrame\n    IFrame -->|\"find('button#submit')\"| Conteudo\n```\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def iframe_interacao():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/pagina-com-iframe')\n\n        iframe = await tab.query(\"iframe.conteudo\", timeout=10)\n\n        # Os utilitários de WebElement já executam dentro do iframe\n        iframe_button = await iframe.find(tag_name=\"button\", class_name=\"submit\")\n        await iframe_button.click()\n\n        iframe_input = await iframe.find(id=\"captcha-input\")\n        await iframe_input.type_text(\"codigo-de-verificacao\")\n\n        # Iframe aninhado? Continue encadeando\n        inner_iframe = await iframe.find(tag_name=\"iframe\")\n        download_link = await inner_iframe.find(text=\"Baixar PDF\")\n        await download_link.click()\n\nasyncio.run(iframe_interacao())\n```\n!!! note \"Screenshots em iframes\"\n    `tab.take_screenshot()` funciona apenas no alvo principal. Para capturar o conteúdo de um iframe, selecione um elemento dentro dele e chame `element.take_screenshot()`.\n\n## Estratégias de Tratamento de Erros\n\nAutomação robusta requer o tratamento de casos onde elementos não existem ou demoram mais para aparecer do que o esperado.\n\n### Fluxo de Localização de Elemento com Tratamento de Erros\n\n```mermaid\nflowchart TB\n    Start[Iniciar Localizacao de Elemento] --> Immediate[Tentar Localizacao Imediata]\n    \n    Immediate --> Found1{Elemento Encontrado?}\n    Found1 -->|Sim| Return1[Retornar WebElement]\n    Found1 -->|Nao & timeout=0| Check1{raise_exc=True?}\n    Found1 -->|Nao & timeout>0| Wait[Iniciar Loop de Espera]\n    \n    Check1 -->|Sim| Error1[Lancar ElementNotFound]\n    Check1 -->|Nao| ReturnNone[Retornar None]\n    \n    Wait --> Sleep[Esperar 0.5 segundos]\n    Sleep --> TryAgain[Tentar Localizar Novamente]\n    TryAgain --> Found2{Elemento Encontrado?}\n    \n    Found2 -->|Sim| Return2[Retornar WebElement]\n    Found2 -->|Nao| TimeCheck{Timeout Excedido?}\n    \n    TimeCheck -->|Nao| Sleep\n    TimeCheck -->|Sim| Check2{raise_exc=True?}\n    \n    Check2 -->|Sim| Error2[Lancar WaitElementTimeout]\n    Check2 -->|Nao| ReturnNone2[Retornar None]\n```\n\n### Usando o Parâmetro raise_exc\n\nControle se uma exceção deve ser lançada quando elementos não são encontrados:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.exceptions import ElementNotFound\n\nasync def error_handling():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # Lançar exceção se não encontrado (comportamento padrão)\n        try:\n            critical_element = await tab.find(id=\"must-exist\")\n        except ElementNotFound:\n            print(\"Elemento crítico ausente! Não é possível continuar.\")\n            return\n        \n        # Retornar None se não encontrado (elementos opcionais)\n        optional_banner = await tab.find(\n            class_name=\"promo-banner\",\n            raise_exc=False\n        )\n        \n        if optional_banner:\n            print(\"Banner encontrado, fechando-o\")\n            close_button = await optional_banner.find(class_name=\"close-btn\")\n            await close_button.click()\n        else:\n            print(\"Nenhum banner presente, continuando\")\n\nasyncio.run(error_handling())\n```\n\n## Melhores Práticas\n\n### 1. Prefira Seletores Estáveis\n\nUse atributos que têm baixa probabilidade de mudar:\n\n```python\n# Bom: Atributos semânticos\nawait tab.find(id=\"user-profile\")  # IDs geralmente são estáveis\nawait tab.find(data_testid=\"submit-button\")  # IDs de teste são feitos para automação\nawait tab.find(name=\"username\")  # Nomes de formulário são estáveis\n\n# Evite: Dependências estruturais\nawait tab.query(\"div > div > div:nth-child(3) > input\")  # Frágil, quebra facilmente\nawait tab.query(\"body > div:nth-child(2) > form > div:first-child\")\n```\n\n### 2. Use o Seletor Mais Simples que Funciona\n\nComece simples e adicione complexidade apenas quando necessário:\n\n```python\n# Bom: Simples e claro\nawait tab.find(id=\"login-form\")\n\n# Desnecessário: Complicado demais\nawait tab.query(\"//div[@id='content']/descendant::form[@id='login-form']\")\n```\n\n### 3. Escolha o Método Certo\n\n- Use `find()` para buscas simples baseadas em atributos\n- Use `query()` para padrões CSS ou XPath complexos\n- Use métodos de travessia para explorar a partir de âncoras conhecidas\n\n```python\n# Use find() para casos diretos\nusername = await tab.find(id=\"username\")\n\n# Use query() para padrões complexos\nactive_nav_link = await tab.query(\"nav.menu a.active\")\n\n# Use travessia para buscas baseadas em relacionamento\ncontainer = await tab.find(id=\"cards\")\nchild_links = await container.get_children_elements(tag_filter=[\"a\"])\n```\n\n### 4. Adicione Timeouts Significativos\n\nNão use timeouts zero para conteúdo dinâmico, e não espere para sempre por elementos opcionais:\n\n```python\n# Bom: Timeouts razoáveis\ncritical_data = await tab.find(id=\"data\", timeout=10)\noptional_popup = await tab.find(class_name=\"popup\", timeout=2, raise_exc=False)\n\n# Ruim: Sem timeout para conteúdo dinâmico\ndynamic_element = await tab.find(class_name=\"ajax-loaded\")  # Falhará imediatamente\n\n# Ruim: Timeout muito longo para elemento opcional\nbanner = await tab.find(class_name=\"ad-banner\", timeout=60)  # Desperdício de tempo\n```\n\n### 5. Trate Erros Graciosamente\n\nPlaneje para elementos que podem não existir:\n\n```python\n# Elementos críticos: deixe as exceções subirem\nsubmit_button = await tab.find(id=\"submit-btn\")\n\n# Elementos opcionais: trate explicitamente\ncookie_notice = await tab.find(class_name=\"cookie-notice\", raise_exc=False)\nif cookie_notice:\n    accept_button = await cookie_notice.find(text=\"Accept\")\n    await accept_button.click()\n```\n\n## Exemplo Completo: Automação de Formulário\n\nAqui está um exemplo completo combinando múltiplas técnicas de localização de elementos:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.exceptions import ElementNotFound\n\nasync def automate_registration_form():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        try:\n            # Navegar para a página de registro\n            await tab.go_to('https://example.com/register', timeout=10)\n            \n            # Lidar com banner de cookie opcional\n            cookie_banner = await tab.find(\n                class_name=\"cookie-banner\",\n                timeout=2,\n                raise_exc=False\n            )\n            if cookie_banner:\n                accept = await cookie_banner.find(text=\"Accept\")\n                await accept.click()\n                await asyncio.sleep(1)\n            \n            # Preencher o formulário de registro\n            # Encontrar campos do formulário\n            username_field = await tab.find(name=\"username\", timeout=5)\n            email_field = await tab.find(name=\"email\")\n            password_field = await tab.find(type=\"password\", name=\"password\")\n            confirm_password = await tab.find(type=\"password\", name=\"confirm_password\")\n            \n            # Inserir informações\n            await username_field.type_text(\"john_doe_2024\", interval=0.1)\n            await email_field.type_text(\"john@example.com\", interval=0.1)\n            await password_field.type_text(\"SecurePass123!\", interval=0.1)\n            await confirm_password.type_text(\"SecurePass123!\", interval=0.1)\n            \n            # Encontrar e marcar checkbox de termos\n            # Tentar múltiplas estratégias\n            terms_checkbox = await tab.find(id=\"terms\", raise_exc=False)\n            if not terms_checkbox:\n                terms_checkbox = await tab.find(name=\"accept_terms\", raise_exc=False)\n            if not terms_checkbox:\n                terms_checkbox = await tab.query(\"input[type='checkbox']\")\n            \n            await terms_checkbox.click()\n            \n            # Encontrar e clicar no botão de envio\n            submit_button = await tab.find(\n                tag_name=\"button\",\n                type=\"submit\",\n                timeout=2\n            )\n            await submit_button.click()\n            \n            # Esperar por mensagem de sucesso com timeout maior (processamento do formulário)\n            success_message = await tab.find(\n                class_name=\"success-message\",\n                timeout=15\n            )\n            \n            message_text = await success_message.text\n            print(f\"Registro bem-sucedido: {message_text}\")\n            \n            # Verificar redirecionamento para o dashboard\n            await asyncio.sleep(2)\n            current_url = await tab.current_url\n            \n            if \"dashboard\" in current_url:\n                print(\"Redirecionado com sucesso para o dashboard\")\n                \n                # Encontrar mensagem de boas-vindas\n                welcome = await tab.find(class_name=\"welcome-message\", timeout=5)\n                welcome_text = await welcome.text\n                print(f\"Mensagem de boas-vindas: {welcome_text}\")\n            else:\n                print(f\"URL inesperada após registro: {current_url}\")\n                \n        except ElementNotFound as e:\n            print(f\"Elemento não encontrado: {e}\")\n            # Tirar screenshot para depuração\n            await tab.take_screenshot(\"error_screenshot.png\")\n        except Exception as e:\n            print(f\"Erro inesperado: {e}\")\n            await tab.take_screenshot(\"unexpected_error.png\")\n\nasyncio.run(automate_registration_form())\n```\n\n## Aprenda Mais\n\nQuer mergulhar mais fundo na localização de elementos?\n\n- **[Análise aprofundada em FindElements Mixin](../deep-dive/find-elements-mixin.md)**: Aprenda sobre a arquitetura, estratégias internas de seletores e otimizações de desempenho\n- **[Guia de Seletores](../deep-dive/selectors-guide.md)**: Guia completo de seletores CSS e XPath com referências de sintaxe e exemplos do mundo real\n- **[Domínio WebElement](../deep-dive/webelement-domain.md)**: Entenda o que você pode fazer com os elementos depois de encontrá-los\n\nA localização de elementos é a base para uma automação de navegador bem-sucedida. Domine essas técnicas, e você será capaz de localizar confiavelmente qualquer elemento em qualquer página web, não importa quão complexa seja a estrutura."
  },
  {
    "path": "docs/pt/features/index.md",
    "content": "# Guia de Funcionalidades\n\nBem-vindo à documentação abrangente de funcionalidades do Pydoll! Aqui você descobrirá tudo o que torna o Pydoll uma ferramenta de automação de navegador poderosa e flexível. Esteja você apenas começando ou procurando aproveitar capacidades avançadas, você encontrará guias detalhados, exemplos práticos e melhores práticas para cada funcionalidade.\n\n## O Que Você Encontrará Aqui\n\nEste guia está organizado em seções lógicas que refletem sua jornada na automação: de conceitos básicos a técnicas avançadas. Cada página é projetada para ser autocontida, para que você possa pular diretamente para o que lhe interessa ou seguir sequencialmente.\n\n## Conceitos Principais\n\nAntes de mergulhar em funcionalidades específicas, vale a pena entender o que diferencia o Pydoll. Esses conceitos fundamentais informam como toda a biblioteca funciona.\n\n**[Conceitos Principais](core-concepts.md)**: Descubra as decisões arquitetônicas que tornam o Pydoll diferente: a abordagem \"zero-webdriver\" que elimina dores de cabeça de compatibilidade, o design \"async-first\" que permite operações concorrentes verdadeiras, e o suporte nativo para múltiplos navegadores baseados em Chromium.\n\n## Localização e Interação com Elementos\n\nEncontrar e interagir com elementos da página é o pão com manteiga da automação. O Pydoll torna isso surpreendentemente intuitivo com APIs modernas que simplesmente fazem sentido.\n\n**[Localização de Elementos](element-finding.md)**: Domine as estratégias de localização de elementos do Pydoll, desde o intuitivo método `find()` que usa atributos HTML naturais, até o poderoso método `query()` para seletores CSS e XPath. Você também aprenderá sobre auxiliares de travessia do DOM que permitem navegar pela estrutura da página eficientemente.\n\n## Capacidades de Automação\n\nEstas são as funcionalidades que dão vida à sua automação: simular interações do usuário, controle de teclado, lidar com operações de arquivo, trabalhar com iframes e capturar conteúdo visual.\n\n**[Interações Semelhantes a Humanas](automation/human-interactions.md)**: Aprenda como criar interações que parecem genuinamente humanas: digitar com variações naturais de tempo, clicar com movimentos realistas do mouse e usar atalhos de teclado exatamente como um usuário real faria. Isso é crucial para evitar detecção em sites sensíveis à automação.\n\n**[Controle de Teclado](automation/keyboard-control.md)**: Domine as interações de teclado com suporte abrangente para combinações de teclas, modificadores e teclas especiais. Essencial para formulários, atalhos e testes de acessibilidade.\n\n**[Operações com Arquivos](automation/file-operations.md)**: O manuseio de arquivos pode ser complicado na automação de navegador. O Pydoll fornece soluções robustas tanto para uploads quanto para downloads, com o gerenciador de contexto `expect_download` oferecendo um manuseio elegante da conclusão assíncrona de downloads.\n\n**[Interação com IFrames](automation/iframes.md)**: Trate iframes como qualquer elemento—encontre o iframe e continue pesquisando a partir dele. Sem targets extras, sem abas adicionais.\n\n**[Capturas de Tela e PDF](automation/screenshots-and-pdfs.md)**: Capture conteúdo visual de suas sessões de automação. Se você precisa de capturas de tela de página inteira para testes de regressão visual, capturas de elementos específicos para depuração, ou exportações de PDF para arquivamento, o Pydoll tem o que você precisa.\n\n## Funcionalidades de Rede\n\nAs capacidades de rede do Pydoll são onde ele realmente brilha, dando a você visibilidade e controle sem precedentes sobre o tráfego HTTP.\n\n**[Monitoramento de Rede](network/monitoring.md)**: Observe e analise toda a atividade de rede em sua sessão de navegador. Extraia respostas de API, rastreie o tempo de requisição, identifique requisições falhas e entenda exatamente quais dados estão sendo trocados. Essencial para depuração, testes e extração de dados.\n\n**[Interceptação de Requisições](network/interception.md)**: Vá além da observação para modificar ativamente o comportamento da rede. Bloqueie recursos indesejados, injete cabeçalhos personalizados, modifique payloads de requisição, ou até mesmo atenda requisições com dados mockados. Isso é poderoso para testes, otimização e controle de privacidade.\n\n**[Requisições HTTP no Contexto do Navegador](network/http-requests.md)**: Faça requisições HTTP que executam dentro do contexto JavaScript do navegador, herdando automaticamente estado de sessão, cookies e autenticação. Esta abordagem híbrida combina a familiaridade da biblioteca `requests` do Python com os benefícios da execução no contexto do navegador.\n\n## Gerenciamento do Navegador\n\nO gerenciamento eficaz do navegador e das abas é essencial para cenários complexos de automação, processamento paralelo e testes multiusuário.\n\n**[Gerenciamento de Múltiplas Abas](browser-management/tabs.md)**: Trabalhe com múltiplas abas do navegador simultaneamente, garantindo o uso eficiente de recursos enquanto lhe dá controle total sobre o ciclo de vida das abas, detecção de abas abertas pelo usuário e operações de scraping concorrentes.\n\n**[Contextos do Navegador](browser-management/contexts.md)**: Crie ambientes de navegação completamente isolados dentro de um único processo de navegador. Cada contexto mantém cookies, armazenamento, cache e permissões separados: perfeito para testes de múltiplas contas, testes A/B, ou scraping paralelo com diferentes configurações.\n\n\n**[Cookies e Sessões](browser-management/cookies-sessions.md)**: Gerencie o estado da sessão tanto no nível do navegador quanto no da aba. Defina cookies programaticamente, extraia dados de sessão e mantenha diferentes sessões entre contextos de navegador para cenários de testes sofisticados.\n\n\n## Configuração\n\nPersonalize cada aspecto do comportamento do navegador para corresponder às suas necessidades de automação, desde preferências de baixo nível do Chromium até argumentos de linha de comando e estratégias de carregamento de página.\n\n**[Opções do Navegador](configuration/browser-options.md)**: Configure os parâmetros de inicialização do Chromium, argumentos de linha de comando e controle do estado de carregamento da página. Ajuste fino do comportamento do navegador, ative recursos experimentais e otimize o desempenho para suas necessidades de automação.\n\n**[Preferências do Navegador](configuration/browser-preferences.md)**: O acesso direto ao sistema interno de preferências do Chromium lhe dá controle sobre centenas de configurações. Configure downloads, desative funcionalidades, otimize o desempenho ou crie fingerprints de navegador realistas para automação furtiva.\n\n**[Configuração de Proxy](configuration/proxy.md)**: Suporte nativo a proxy com capacidades completas de autenticação. Essencial para projetos de web scraping que exigem rotação de IP, testes geo-direcionados ou automação focada em privacidade.\n\n\n## Funcionalidades Avançadas\n\nEstas capacidades sofisticadas abordam desafios complexos de automação e casos de uso especializados.\n\n**[Contorno de Captcha Comportamental](advanced/behavioral-captcha-bypass.md)**: O manejo nativo de captcha comportamental do Pydoll é uma de suas funcionalidades mais solicitadas. Aprenda como interagir com Cloudflare Turnstile, reCAPTCHA v3 e desafios invisíveis hCaptcha usando duas abordagens - gerenciador de contexto síncrono para conclusão garantida, e processamento em segundo plano para operação não bloqueante.\n\n**[Sistema de Eventos](advanced/event-system.md)**: Construa automação reativa que responde a eventos do navegador em tempo real. Monitore carregamentos de página, atividade de rede, mudanças no DOM e execução de JavaScript para criar scripts de automação inteligentes e adaptativos.\n\n**[Conexões Remotas](advanced/remote-connections.md)**: Conecte-se a navegadores já em execução via WebSocket para cenários de automação híbrida. Perfeito para pipelines de CI/CD, ambientes contêinerizados, ou integração do Pydoll em ferramentas CDP existentes.\n\n\n## Como Usar Este Guia\n\nCada página de funcionalidade segue uma estrutura consistente:\n\n1.  **Visão Geral** - O que a funcionalidade faz e por que ela é importante\n2.  **Uso Básico** - Comece rapidamente com exemplos simples\n3.  **Padrões Avançados** - Aproveite todo o potencial da funcionalidade\n4.  **Melhores Práticas** - Dicas para uso eficaz e eficiente\n5.  **Armadilhas Comuns** - Aprenda com os erros comuns\n\nSinta-se à vontade para explorar as funcionalidades em qualquer ordem com base em suas necessidades. Os exemplos de código são completos e estão prontos para rodar - apenas copie, cole e adapte ao seu caso de uso.\n\nPronto para mergulhar fundo nas capacidades do Pydoll? Escolha uma funcionalidade que lhe interessa e comece a explorar! 🚀"
  },
  {
    "path": "docs/pt/features/network/http-requests.md",
    "content": "# Requisições HTTP no Contexto do Navegador\n\nFaça requisições HTTP que herdam automaticamente o estado de sessão, cookies e autenticação do seu navegador. Perfeito para automação híbrida, combinando navegação de UI com a eficiência de APIs.\n\n!!! tip \"Uma Revolução para Automação Híbrida\"\n    Já desejou poder fazer requisições HTTP que automaticamente obtêm todos os cookies e autenticação do seu navegador? Agora você pode! A propriedade `tab.request` oferece uma bela interface semelhante ao `requests` que executa chamadas HTTP **diretamente no contexto JavaScript do navegador**.\n\n## Por que Usar Requisições no Contexto do Navegador?\n\nA automação tradicional frequentemente exige que você extraia cookies e cabeçalhos manualmente para fazer chamadas de API. As requisições no contexto do navegador eliminam esse incômodo:\n\n| Abordagem Tradicional | Requisições no Contexto do Navegador |\n|---|---|\n| Extrair cookies manualmente | Cookies herdados automaticamente |\n| Gerenciar tokens de sessão | Estado da sessão preservado |\n| Lidar com CORS separadamente | Políticas CORS respeitadas |\n| Lidar com dois clientes HTTP | Uma interface unificada |\n| Sincronizar estado de autenticação | Sempre autenticado |\n\n**Perfeito para:**\n\n- Raspar APIs autenticadas após login via UI\n- Fluxos de trabalho híbridos misturando interação de navegador e chamadas de API\n- Testar endpoints autenticados sem gerenciamento de token\n- Contornar fluxos complexos de autenticação\n- Trabalhar com aplicações de página única (SPAs)\n\n## Guia Rápido\n\nO exemplo mais simples: fazer login via UI e, em seguida, fazer chamadas de API autenticadas:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def hybrid_automation():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 1. Faça login normalmente através da UI\n        await tab.go_to('https://example.com/login')\n        await (await tab.find(id='username')).type_text('user@example.com')\n        await (await tab.find(id='password')).type_text('password123')\n        await (await tab.find(id='login-btn')).click()\n        \n        # Aguarde o redirecionamento após o login\n        await asyncio.sleep(2)\n        \n        # 2. Agora faça chamadas de API com a sessão autenticada!\n        response = await tab.request.get('https://example.com/api/user/profile')\n        user_data = response.json()\n        \n        print(f\"Logado como: {user_data['name']}\")\n        print(f\"Email: {user_data['email']}\")\n\nasyncio.run(hybrid_automation())\n```\n\n!!! success \"Nenhum Gerenciamento de Cookie Necessário\"\n    Percebeu como não extraímos ou passamos nenhum cookie? A requisição herdou automaticamente a sessão autenticada do navegador!\n\n## Casos de Uso Comuns\n\n### 1. Raspagem de APIs Autenticadas\n\nUse a UI para fazer login e, em seguida, dispare requisições às APIs para extração de dados:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_user_data():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Login via UI (lida com fluxos de autenticação complexos)\n        await tab.go_to('https://app.example.com/login')\n        await (await tab.find(id='email')).type_text('user@example.com')\n        await (await tab.find(id='password')).type_text('password')\n        await (await tab.find(type='submit')).click()\n        await asyncio.sleep(2)\n        \n        # Agora extraia dados via API (muito mais rápido que raspar UI)\n        all_users = []\n        for page in range(1, 6):\n            response = await tab.request.get(\n                f'https://app.example.com/api/users',\n                params={'page': str(page), 'limit': '100'}\n            )\n            users = response.json()['users']\n            all_users.extend(users)\n            print(f\"Página {page}: buscou {len(users)} usuários\")\n        \n        print(f\"Total de usuários raspados: {len(all_users)}\")\n\nasyncio.run(scrape_user_data())\n```\n\n### 2. Testando Endpoints Protegidos\n\nTeste endpoints de API sem gerenciar tokens de autenticação:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def test_api_endpoints():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Autentique uma vez\n        await tab.go_to('https://api.example.com/login')\n        # ... realize o login ...\n        await asyncio.sleep(2)\n        \n        # Teste múltiplos endpoints\n        endpoints = [\n            '/api/users/me',\n            '/api/settings',\n            '/api/notifications',\n            '/api/dashboard/stats'\n        ]\n        \n        for endpoint in endpoints:\n            response = await tab.request.get(f'https://api.example.com{endpoint}')\n            \n            if response.ok:\n                print(f\"Sucesso {endpoint}: {response.status_code}\")\n            else:\n                print(f\"Falha {endpoint}: {response.status_code}\")\n                print(f\"   Erro: {response.text[:100]}\")\n\nasyncio.run(test_api_endpoints())\n```\n\n### 3. Enviando Formulários via API\n\nPreencha formulários mais rapidamente postando diretamente para a API:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def bulk_form_submission():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Faça login primeiro\n        await tab.go_to('https://crm.example.com/login')\n        # ... lógica de login ...\n        await asyncio.sleep(2)\n        \n        # Envie múltiplas entradas via API (muito mais rápido que preencher formulários)\n        contacts = [\n            {'name': 'John Doe', 'email': 'john@example.com', 'company': 'Acme Inc'},\n            {'name': 'Jane Smith', 'email': 'jane@example.com', 'company': 'Tech Corp'},\n            {'name': 'Bob Wilson', 'email': 'bob@example.com', 'company': 'StartupXYZ'},\n        ]\n        \n        for contact in contacts:\n            response = await tab.request.post(\n                'https://crm.example.com/api/contacts',\n                json=contact\n            )\n            \n            if response.ok:\n                print(f\"Adicionado: {contact['name']}\")\n            else:\n                print(f\"Falha: {contact['name']} - {response.status_code}\")\n\nasyncio.run(bulk_form_submission())\n```\n\n### 4. Baixando Arquivos com Sessão\n\nBaixe arquivos que exigem autenticação:\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def download_authenticated_file():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Autentique\n        await tab.go_to('https://portal.example.com/login')\n        # ... lógica de login ...\n        await asyncio.sleep(2)\n        \n        # Baixe o arquivo que requer autenticação\n        response = await tab.request.get(\n            'https://portal.example.com/api/reports/monthly.pdf'\n        )\n        \n        if response.ok:\n            # Salve o arquivo\n            output_path = Path('/tmp/monthly_report.pdf')\n            output_path.write_bytes(response.content)\n            print(f\"Baixado: {output_path} ({len(response.content)} bytes)\")\n        else:\n            print(f\"Download falhou: {response.status_code}\")\n\nasyncio.run(download_authenticated_file())\n```\n\n### 5. Trabalhando com Cabeçalhos Personalizados\n\nAdicione cabeçalhos personalizados às suas requisições:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def custom_headers_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Faça login primeiro\n        await tab.go_to('https://api.example.com/login')\n        # ... lógica de login ...\n        \n        # Faça requisição com cabeçalhos personalizados\n        headers: list[HeaderEntry] = [\n            {'name': 'X-API-Version', 'value': '2.0'},\n            {'name': 'X-Request-ID', 'value': 'unique-id-123'},\n            {'name': 'Accept-Language', 'value': 'pt-BR,pt;q=0.9'},\n        ]\n        \n        response = await tab.request.get(\n            'https://api.example.com/data',\n            headers=headers\n        )\n        \n        print(f\"Status: {response.status_code}\")\n        print(f\"Data: {response.json()}\")\n\nasyncio.run(custom_headers_example())\n```\n\n### 6. Lidando com Diferentes Tipos de Resposta\n\nAcesse dados de resposta em múltiplos formatos:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def response_formats():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://api.example.com')\n        \n        # Resposta JSON\n        json_response = await tab.request.get('/api/users/1')\n        user = json_response.json()\n        print(f\"JSON: {user}\")\n        \n        # Resposta de texto\n        text_response = await tab.request.get('/api/status')\n        status_text = text_response.text\n        print(f\"Texto: {status_text}\")\n        \n        # Resposta binária (ex: imagem)\n        image_response = await tab.request.get('/api/avatar/1')\n        image_bytes = image_response.content\n        print(f\"Binário: {len(image_bytes)} bytes\")\n        \n        # Verificar status da resposta\n        if json_response.ok:\n            print(\"Requisição bem-sucedida!\")\n        \n        # Acessar URL da resposta (útil após redirecionamentos)\n        print(f\"URL Final: {json_response.url}\")\n\nasyncio.run(response_formats())\n```\n\n## Métodos HTTP\n\nTodos os métodos HTTP padrão são suportados:\n\n### GET - Recuperar Dados\n\n```python\n# GET simples\nresponse = await tab.request.get('https://api.example.com/users')\n\n# GET com parâmetros de consulta\nresponse = await tab.request.get(\n    'https://api.example.com/search',\n    params={'q': 'python', 'limit': '10'}\n)\n```\n\n### POST - Criar Recursos\n\n```python\n# POST com dados JSON\nresponse = await tab.request.post(\n    'https://api.example.com/users',\n    json={'name': 'John Doe', 'email': 'john@example.com'}\n)\n\n# POST com dados de formulário\nresponse = await tab.request.post(\n    'https://api.example.com/login',\n    data={'username': 'john', 'password': 'secret'}\n)\n```\n\n### PUT - Atualizar Recursos\n\n```python\n# Atualizar recurso inteiro\nresponse = await tab.request.put(\n    'https://api.example.com/users/123',\n    json={'name': 'Jane Doe', 'email': 'jane@example.com', 'role': 'admin'}\n)\n```\n\n### PATCH - Atualizações Parciais\n\n```python\n# Atualizar campos específicos\nresponse = await tab.request.patch(\n    'https://api.example.com/users/123',\n    json={'email': 'newemail@example.com'}\n)\n```\n\n### DELETE - Remover Recursos\n\n```python\n# Deletar um recurso\nresponse = await tab.request.delete('https://api.example.com/users/123')\n```\n\n### HEAD - Obter Apenas Cabeçalhos\n\n```python\n# Verificar se o recurso existe sem baixá-lo\nresponse = await tab.request.head('https://example.com/large-file.zip')\nprint(f\"Content-Length: {response.headers}\")\n```\n\n### OPTIONS - Verificar Capacidades\n\n```python\n# Verificar métodos permitidos\nresponse = await tab.request.options('https://api.example.com/users')\nprint(f\"Métodos permitidos: {response.headers}\")\n```\n\n!!! info \"Como Isso Funciona?\"\n    Requisições no contexto do navegador executam chamadas HTTP diretamente no contexto JavaScript do navegador usando a API Fetch, enquanto monitoram eventos de rede CDP para capturar metadados abrangentes (cabeçalhos, cookies, tempo).\n    \n    Para uma explicação detalhada da arquitetura interna, monitoramento de eventos e detalhes de implementação, veja a [Arquitetura de Requisições do Navegador](../../deep-dive/browser-requests-architecture.md).\n\n## Objeto Response\n\nO objeto `Response` fornece uma interface familiar semelhante ao `requests.Response`:\n\n```python\nresponse = await tab.request.get('https://api.example.com/users')\n\n# Código de status\nprint(response.status_code)  # 200, 404, 500, etc.\n\n# Verificar se foi bem-sucedido (2xx ou 3xx)\nif response.ok:\n    print(\"Sucesso!\")\n\n# Corpo da resposta\ntext_data = response.text      # Como string\nbyte_data = response.content   # Como bytes\njson_data = response.json()    # JSON parseado\n\n# Cabeçalhos\nfor header in response.headers:\n    print(f\"{header['name']}: {header['value']}\")\n\n# Cabeçalhos da requisição (o que foi realmente enviado)\nfor header in response.request_headers:\n    print(f\"{header['name']}: {header['value']}\")\n\n# Cookies definidos pela resposta\nfor cookie in response.cookies:\n    print(f\"{cookie['name']} = {cookie['value']}\")\n\n# URL final (após redirecionamentos)\nprint(response.url)\n\n# Lançar exceção para códigos de status de erro\nresponse.raise_for_status()  # Lança HTTPError se for 4xx ou 5xx\n```\n\n!!! note \"Redirecionamentos e Rastreamento de URL\"\n    A propriedade `response.url` contém apenas a **URL final** após todos os redirecionamentos. Se você precisar rastrear a cadeia completa de redirecionamento (URLs intermediárias, códigos de status, tempo), use o [Monitoramento de Rede](monitoring.md) para observar todas as requisições em detalhes.\n\n## Cabeçalhos e Cookies\n\n### Trabalhando com Cabeçalhos\n\nCabeçalhos são representados como objetos `HeaderEntry`:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def header_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Usando o tipo HeaderEntry para autocompletar da IDE e verificação de tipo\n        headers: list[HeaderEntry] = [\n            {'name': 'Authorization', 'value': 'Bearer token-123'},\n            {'name': 'X-Custom-Header', 'value': 'custom-value'},\n        ]\n        \n        response = await tab.request.get(\n            'https://api.example.com/protected',\n            headers=headers\n        )\n        \n        # Inspecionar cabeçalhos de resposta (também são dicts tipados HeaderEntry)\n        for header in response.headers:\n            if header['name'] == 'Content-Type':\n                print(f\"Content-Type: {header['value']}\")\n\nasyncio.run(header_example())\n```\n\n!!! tip \"Dicas de Tipo para Cabeçalhos\"\n    `HeaderEntry` é um `TypedDict` de `pydoll.protocol.fetch.types`. Usá-lo como uma dica de tipo oferece a você:\n    \n    - **Autocompletar**: IDE sugere chaves `name` e `value`\n    - **Segurança de tipo**: Pega erros de digitação e chaves faltantes antes de rodar\n    - **Documentação**: Estrutura clara para cabeçalhos\n    \n    Embora você possa passar dicionários simples, usar a dica de tipo melhora a qualidade do código e o suporte da IDE.\n\n!!! tip \"Comportamento de Cabeçalhos Personalizados\"\n    Cabeçalhos personalizados são enviados **juntamente com** os cabeçalhos automáticos do navegador (como `User-Agent`, `Accept`, `Referer`, etc.). \n    \n    Se você tentar definir um cabeçalho padrão do navegador (ex: `User-Agent`), o comportamento depende do cabeçalho específico; alguns podem ser sobrescritos, outros ignorados, e alguns podem causar conflitos. Para a maioria dos casos de uso, atenha-se a cabeçalhos personalizados (ex: `X-API-Key`, `Authorization`) para evitar comportamentos inesperados.\n\n### Entendendo Cookies\n\nCookies são gerenciados automaticamente pelo navegador:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def cookie_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Primeira requisição define cookies\n        login_response = await tab.request.post(\n            'https://api.example.com/login',\n            json={'username': 'user', 'password': 'pass'}\n        )\n        \n        # Verificar cookies definidos pelo servidor\n        print(\"Cookies definidos pelo servidor:\")\n        for cookie in login_response.cookies:\n            print(f\"  {cookie['name']} = {cookie['value']}\")\n        \n        # Requisições subsequentes incluem cookies automaticamente\n        profile_response = await tab.request.get(\n            'https://api.example.com/profile'\n        )\n        # Não é preciso passar cookies - o navegador cuida disso!\n        \n        print(f\"Dados do perfil: {profile_response.json()}\")\n\nasyncio.run(cookie_example())\n```\n\n## Comparação com Requisições Tradicionais\n\n| Funcionalidade | Biblioteca `requests` | Requisições no Contexto do Navegador |\n|---|---|---|\n| **Gerenciamento de Sessão** | Manual (cookies) | Automático via navegador |\n| **Autenticação** | Extrair e passar tokens | Herdada do navegador |\n| **CORS** | Não aplicável | Navegador impõe políticas |\n| **JavaScript** | Não pode executar | Acesso total ao contexto do navegador |\n| **Armazenamento de Cookies** | Instância separada | Armazenamento nativo de cookies do navegador |\n| **Cabeçalhos** | Definidos manualmente | Navegador adiciona cabeçalhos padrão |\n| **Caso de Uso** | Scripts do lado do servidor | Automação de navegador |\n| **Configuração** | Biblioteca externa | Embutido no Pydoll |\n\n## Veja Também\n\n- **[Arquitetura de Requisições do Navegador](../../deep-dive/browser-requests-architecture.md)** - Implementação interna e arquitetura\n- **[Monitoramento de Rede](monitoring.md)** - Observe todo o tráfego de rede\n- **[Interceptação de Requisições](interception.md)** - Modifique requisições antes de serem enviadas\n- **[Sistema de Eventos](../advanced/event-system.md)** - Reaja a eventos do navegador\n- **[Análise Profunda: Capacidades de Rede](../../deep-dive/network-capabilities.md)** - Detalhes técnicos\n\nRequisições no contexto do navegador são uma virada de jogo para automação híbrida. Combine o poder da automação de UI com a velocidade de chamadas diretas de API, tudo isso mantendo a continuidade perfeita da sessão!"
  },
  {
    "path": "docs/pt/features/network/interception.md",
    "content": "# Interceptação de Requisições\n\nA interceptação de requisições permite que você intercepte, modifique, bloqueie ou simule (mock) requisições e respostas HTTP em tempo real. Isso é essencial para testes, otimização de desempenho, filtragem de conteúdo e simulação de várias condições de rede.\n\n!!! info \"Domínio Network vs Fetch\"\n    O **domínio Network** é para monitoramento passivo (observar o tráfego). O **domínio Fetch** é para interceptação ativa (modificar/bloquear requisições). Este guia foca na interceptação. Para monitoramento passivo, veja [Monitoramento de Rede](monitoring.md).\n\n## Entendendo a Interceptação de Requisições\n\nQuando você habilita a interceptação de requisições, o Pydoll pausa as requisições correspondentes antes que elas sejam enviadas ao servidor (ou após receber a resposta). Você então tem três opções:\n\n1.  **Continuar**: Deixar a requisição prosseguir (opcionalmente com modificações)\n2.  **Bloquear**: Falhar a requisição com um erro\n3.  **Simular (Mock)**: Atender à requisição com uma resposta personalizada\n\n```mermaid\nsequenceDiagram\n    participant Browser\n    participant Pydoll\n    participant Server\n    \n    Browser->>Pydoll: Requisição iniciada\n    Note over Pydoll: Requisição Pausada\n    Pydoll->>Pydoll: Callback executado\n    \n    alt Continuar\n        Pydoll->>Server: Encaminhar requisição\n        Server-->>Browser: Resposta\n    else Bloquear\n        Pydoll-->>Browser: Resposta de erro\n    else Simular (Mock)\n        Pydoll-->>Browser: Resposta personalizada\n    end\n```\n\n!!! warning \"Impacto no Desempenho\"\n    A interceptação de requisições adiciona latência a cada requisição correspondente. Intercepte apenas o que você precisa e desabilite quando terminar para evitar lentidão no carregamento das páginas.\n\n## Habilitando a Interceptação de Requisições\n\nAntes de interceptar requisições, você deve habilitar o domínio Fetch:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Habilitar eventos fetch (intercepta todas as requisições por padrão)\n        await tab.enable_fetch_events()\n        \n        await tab.go_to('https://example.com')\n        \n        # Desabilitar quando terminar\n        await tab.disable_fetch_events()\n\nasyncio.run(main())\n```\n\n### Interceptação Seletiva\n\nVocê pode filtrar quais requisições interceptar por tipo de recurso:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def selective_interception():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Interceptar apenas imagens e folhas de estilo\n        await tab.enable_fetch_events(\n            resource_type='Image'  # Ou 'Stylesheet', 'Script', etc.\n        )\n        \n        await tab.go_to('https://example.com')\n        await tab.disable_fetch_events()\n\nasyncio.run(selective_interception())\n```\n\n!!! tip \"Tipos de Recurso\"\n    Veja a seção [Referência de Tipos de Recurso](#referência-de-tipos-de-recurso) para uma lista completa de tipos de recursos interceptáveis.\n\n## Interceptando Requisições\n\nUse o evento `RequestPaused` para interceptar requisições:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\n\nasync def basic_interception():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Callback com dica de tipo para suporte da IDE\n        async def handle_request(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            print(f\"Interceptado: {url}\")\n            \n            # Continuar a requisição sem modificações\n            await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, handle_request)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(basic_interception())\n```\n\n!!! info \"Dicas de Tipo para Melhor Suporte da IDE\"\n    Use dicas de tipo como `RequestPausedEvent` para obter autocompletar para as chaves do evento. Todos os tipos de evento estão em `pydoll.protocol.fetch.events`.\n\n!!! note \"Espera Pronta para Produção\"\n    Os exemplos neste guia usam `asyncio.sleep()` por simplicidade. Em código de produção, considere usar estratégias de espera mais explícitas, como esperar por elementos específicos ou implementar detecção de ociosidade da rede. Veja o guia [Monitoramento de Rede](monitoring.md) para técnicas avançadas.\n\n## Casos de Uso Comuns\n\n### 1. Bloqueando Recursos para Economizar Banda\n\nBloqueie imagens, folhas de estilo ou outros recursos para acelerar o carregamento da página:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.network.types import ErrorReason\n\nasync def block_images():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        blocked_count = 0\n        \n        async def block_resource(event: RequestPausedEvent):\n            nonlocal blocked_count\n            request_id = event['params']['requestId']\n            resource_type = event['params']['resourceType']\n            url = event['params']['request']['url']\n            \n            # Bloquear imagens e folhas de estilo\n            if resource_type in ['Image', 'Stylesheet']:\n                blocked_count += 1\n                print(f\"🚫 Bloqueado {resource_type}: {url[:60]}\")\n                await tab.fail_request(request_id, ErrorReason.BLOCKED_BY_CLIENT)\n            else:\n                # Continuar outras requisições\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, block_resource)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        print(f\"\\n📊 Total bloqueado: {blocked_count} recursos\")\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(block_images())\n```\n\n### 2. Modificando Cabeçalhos de Requisição\n\nAdicione, modifique ou remova cabeçalhos antes que as requisições sejam enviadas:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def modify_headers():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def add_custom_headers(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            # Modificar apenas requisições de API\n            if '/api/' in url:\n                # Construir cabeçalhos personalizados (usando dica de tipo HeaderEntry para suporte da IDE)\n                headers: list[HeaderEntry] = [\n                    {'name': 'X-Custom-Header', 'value': 'MyValue'},\n                    {'name': 'Authorization', 'value': 'Bearer my-token-123'},\n                ]\n                \n                print(f\"✨ Cabeçalhos modificados para: {url}\")\n                await tab.continue_request(request_id, headers=headers)\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, add_custom_headers)\n        \n        await tab.go_to('https://your-app.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(modify_headers())\n```\n\n!!! tip \"Dicas de Tipo para Cabeçalhos\"\n    `HeaderEntry` é um `TypedDict` de `pydoll.protocol.fetch.types`. Usá-lo como uma dica de tipo oferece autocompletar da IDE para as chaves `name` e `value`. Você também pode usar dicionários simples sem a dica de tipo.\n\n!!! tip \"Gerenciamento de Cabeçalhos\"\n    Quando você fornece cabeçalhos personalizados, eles **substituem** todos os cabeçalhos existentes. Certifique-se de incluir os cabeçalhos necessários, como `User-Agent`, `Accept`, etc., se necessário.\n\n### 3. Simulando (Mocking) Respostas de API\n\nSubstitua respostas reais de API por dados simulados personalizados:\n\n```python\nimport asyncio\nimport json\nimport base64\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def mock_api_responses():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def mock_response(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            # Simular endpoint de API específico\n            if '/api/users' in url:\n                # Criar dados de resposta simulada\n                mock_data = {\n                    'users': [\n                        {'id': 1, 'name': 'Mock User 1'},\n                        {'id': 2, 'name': 'Mock User 2'},\n                    ],\n                    'total': 2\n                }\n                \n                # Converter para JSON e codificar em base64\n                body_json = json.dumps(mock_data)\n                body_base64 = base64.b64encode(body_json.encode()).decode()\n                \n                # Cabeçalhos da resposta\n                headers: list[HeaderEntry] = [\n                    {'name': 'Content-Type', 'value': 'application/json'},\n                    {'name': 'Access-Control-Allow-Origin', 'value': '*'},\n                ]\n                \n                print(f\"🎭 Resposta simulada para: {url}\")\n                await tab.fulfill_request(\n                    request_id=request_id,\n                    response_code=200,\n                    response_headers=headers,\n                    body=body_base64,\n                    response_phrase='OK'\n                )\n            else:\n                # Continuar outras requisições normalmente\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, mock_response)\n        \n        await tab.go_to('https://your-app.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(mock_api_responses())\n```\n\n!!! warning \"Codificação Base64 Obrigatória\"\n    O parâmetro `body` em `fulfill_request()` deve ser codificado em base64. Use o módulo `base64` do Python para codificar seus dados de resposta.\n\n### 4. Modificando URLs de Requisição\n\nRedirecione requisições para URLs diferentes:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\n\nasync def redirect_requests():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def redirect_url(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            original_url = event['params']['request']['url']\n            \n            # Redirecionar requisições de CDN para servidor local\n            if 'cdn.example.com' in original_url:\n                new_url = original_url.replace(\n                    'cdn.example.com',\n                    'localhost:8080'\n                )\n                print(f\"🔀 Redirecionado: {original_url} → {new_url}\")\n                await tab.continue_request(request_id, url=new_url)\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, redirect_url)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(redirect_requests())\n```\n\n### 5. Modificando Corpo da Requisição\n\nModifique dados POST antes de enviar:\n\n```python\nimport asyncio\nimport base64\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\n\nasync def modify_post_data():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def modify_body(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            method = event['params']['request']['method']\n            url = event['params']['request']['url']\n            \n            # Modificar requisições POST\n            if method == 'POST' and '/api/submit' in url:\n                # Criar novos dados POST\n                new_data = '{\"modified\": true, \"timestamp\": 123456789}'\n                post_data_base64 = base64.b64encode(new_data.encode()).decode()\n                \n                print(f\"✏️  Dados POST modificados para: {url}\")\n                await tab.continue_request(\n                    request_id,\n                    post_data=post_data_base64\n                )\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, modify_body)\n        \n        await tab.go_to('https://your-app.com/form')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(modify_post_data())\n```\n\n### 6. Lidando com Desafios de Autenticação\n\nResponda manualmente a desafios de autenticação HTTP (Basic Auth, Digest Auth, etc.):\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, AuthRequiredEvent\nfrom pydoll.protocol.fetch.types import AuthChallengeResponseType\n\nasync def handle_auth():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def respond_to_auth(event: AuthRequiredEvent):\n            request_id = event['params']['requestId']\n            auth_challenge = event['params']['authChallenge']\n            \n            print(f\"🔐 Desafio de autenticação de: {auth_challenge['origin']}\")\n            print(f\"   Esquema: {auth_challenge['scheme']}\")\n            print(f\"   Realm: {auth_challenge.get('realm', 'N/A')}\")\n            \n            # Fornecer credenciais para o desafio de autenticação\n            await tab.continue_with_auth(\n                request_id=request_id,\n                auth_challenge_response=AuthChallengeResponseType.PROVIDE_CREDENTIALS,\n                proxy_username='myuser',\n                proxy_password='mypassword'\n            )\n        \n        # Habilitar com tratamento de autenticação\n        await tab.enable_fetch_events(handle_auth=True)\n        await tab.on(FetchEvent.AUTH_REQUIRED, respond_to_auth)\n        \n        await tab.go_to('https://httpbin.org/basic-auth/myuser/mypassword')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(handle_auth())\n```\n\n!!! note \"Autenticação Automática de Proxy\"\n    **O Pydoll lida automaticamente com a autenticação de proxy** (407 Proxy Authentication Required) quando você configura credenciais de proxy através das opções do navegador. Este exemplo demonstra o **tratamento manual** de desafios de autenticação, que é útil para:\n    \n    - Autenticação HTTP Basic/Digest de servidores (401 Unauthorized)\n    - Fluxos de autenticação personalizados\n    - Seleção dinâmica de credenciais com base no desafio\n    - Testar cenários de falha de autenticação\n    \n    Para uso padrão de proxy, simplesmente configure suas credenciais de proxy nas opções do navegador - não é necessário tratamento manual!\n\n### 7. Simulando Erros de Rede\n\nTeste como sua aplicação lida com falhas de rede:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.network.types import ErrorReason\n\nasync def simulate_errors():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        request_count = 0\n        \n        async def fail_some_requests(event: RequestPausedEvent):\n            nonlocal request_count\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            request_count += 1\n            \n            # Falhar a cada 3ª requisição\n            if request_count % 3 == 0:\n                print(f\"❌ Simulando timeout para: {url[:60]}\")\n                await tab.fail_request(request_id, ErrorReason.TIMED_OUT)\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, fail_some_requests)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(simulate_errors())\n```\n\n## Estágios da Requisição\n\nVocê pode interceptar requisições em diferentes estágios:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.types import RequestStage\n\nasync def intercept_responses():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Interceptar respostas em vez de requisições\n        await tab.enable_fetch_events(request_stage=RequestStage.RESPONSE)\n        \n        # Agora você pode modificar respostas antes que elas cheguem à página\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(intercept_responses())\n```\n\n| Estágio | Quando Interceptado | Casos de Uso |\n|---|---|---|\n| `Request` (padrão) | Antes da requisição ser enviada | Modificar cabeçalhos, bloquear requisições, mudar URL |\n| `Response` | Após a resposta ser recebida | Modificar corpo da resposta, mudar códigos de status |\n\n!!! tip \"Interceptação de Resposta\"\n    Ao interceptar respostas, você pode usar `intercept_response=True` em `continue_request()` para também interceptar a resposta para aquela requisição específica.\n\n## Referência de Tipos de Recurso\n\n| Tipo de Recurso | Descrição | Extensões de Arquivo Comuns |\n|---|---|---|\n| `Document` | Documentos HTML | `.html` |\n| `Stylesheet` | Arquivos CSS | `.css` |\n| `Image` | Recursos de imagem | `.jpg`, `.png`, `.gif`, `.webp`, `.svg` |\n| `Media` | Áudio/vídeo | `.mp4`, `.webm`, `.mp3`, `.ogg` |\n| `Font` | Fontes web | `.woff`, `.woff2`, `.ttf`, `.otf` |\n| `Script` | JavaScript | `.js` |\n| `TextTrack` | Legendas | `.vtt`, `.srt` |\n| `XHR` | XMLHttpRequest | Requisições AJAX |\n| `Fetch` | API Fetch | Chamadas de API modernas |\n| `EventSource` | Server-Sent Events | Streams em tempo real |\n| `WebSocket` | WebSocket | Comunicação bidirecional |\n| `Manifest` | Manifesto de aplicativo web | Configuração de PWA |\n| `Other` | Outros tipos | Diversos |\n\n## Referência de Razões de Erro\n\nUse estes com `fail_request()` para simular diferentes falhas de rede:\n\n| Razão do Erro | Descrição | Caso de Uso |\n|---|---|---|\n| `FAILED` | Falha genérica | Erro geral |\n| `ABORTED` | Requisição abortada | Usuário cancelou |\n| `TIMED_OUT` | Timeout da requisição | Timeout de rede |\n| `ACCESS_DENIED` | Acesso negado | Erro de permissão |\n| `CONNECTION_CLOSED` | Conexão fechada | Servidor desconectou |\n| `CONNECTION_RESET` | Conexão resetada | Reset de rede |\n| `CONNECTION_REFUSED` | Conexão recusada | Servidor inacessível |\n| `NAME_NOT_RESOLVED` | Falha no DNS | Hostname inválido |\n| `INTERNET_DISCONNECTED` | Sem internet | Modo offline |\n| `BLOCKED_BY_CLIENT` | Bloqueado pelo cliente | Simulação de ad blocker |\n| `BLOCKED_BY_RESPONSE` | Resposta bloqueada | Violação de CORS/CSP |\n\n## Melhores Práticas\n\n### 1. Sempre Continue ou Falhe as Requisições\n\n```python\n# Bom: Toda requisição pausada é tratada\nasync def handle_request(event: RequestPausedEvent):\n    request_id = event['params']['requestId']\n    try:\n        # Sua lógica aqui\n        await tab.continue_request(request_id)\n    except Exception as e:\n        # Falhar em caso de erro para evitar travamento\n        await tab.fail_request(request_id, ErrorReason.FAILED)\n\n# Ruim: Requisição pode travar se o callback lançar exceção\nasync def handle_request(event: RequestPausedEvent):\n    request_id = event['params']['requestId']\n    # Se isso lançar exceção, a requisição trava para sempre\n    await tab.continue_request(request_id)\n```\n\n### 2. Use Interceptação Seletiva\n\n```python\n# Bom: Intercepte apenas o que você precisa\nawait tab.enable_fetch_events(resource_type='Image')\n\n# Ruim: Intercepta tudo, torna todas as requisições mais lentas\nawait tab.enable_fetch_events()\n```\n\n### 3. Desabilite Quando Terminar\n\n```python\n# Bom: Limpe depois de usar\nawait tab.enable_fetch_events()\n# ... faça o trabalho ...\nawait tab.disable_fetch_events()\n\n# Ruim: Deixa a interceptação habilitada\nawait tab.enable_fetch_events()\n# ... faça o trabalho ...\n# (nunca desabilitado)\n```\n\n### 4. Trate Erros Graciosamente\n\n```python\n# Bom: Envolva em try/except\nasync def safe_handler(event: RequestPausedEvent):\n    request_id = event['params']['requestId']\n    try:\n        # Lógica complexa que pode falhar\n        modified_url = transform_url(event['params']['request']['url'])\n        await tab.continue_request(request_id, url=modified_url)\n    except Exception as e:\n        print(f\"Erro ao tratar requisição: {e}\")\n        # Continue sem modificações em caso de erro\n        await tab.continue_request(request_id)\n```\n\n## Exemplo Completo: Controle Avançado de Requisições\n\nAqui está um exemplo completo combinando múltiplas técnicas de interceptação:\n\n```python\nimport asyncio\nimport base64\nimport json\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.fetch.types import HeaderEntry\nfrom pydoll.protocol.network.types import ErrorReason\n\nasync def advanced_interception():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        stats = {\n            'blocked': 0,\n            'mocked': 0,\n            'modified': 0,\n            'continued': 0\n        }\n        \n        async def intelligent_handler(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            resource_type = event['params']['resourceType']\n            method = event['params']['request']['method']\n            \n            try:\n                # Bloquear anúncios e rastreadores\n                if any(tracker in url for tracker in ['analytics', 'ads', 'tracking']):\n                    stats['blocked'] += 1\n                    print(f\"🚫 Bloqueado rastreador: {url[:50]}\")\n                    await tab.fail_request(request_id, ErrorReason.BLOCKED_BY_CLIENT)\n                \n                # Simular (mock) respostas de API\n                elif '/api/config' in url:\n                    stats['mocked'] += 1\n                    mock_config = {'feature_x': True, 'debug_mode': False}\n                    body = base64.b64encode(json.dumps(mock_config).encode()).decode()\n                    headers: list[HeaderEntry] = [\n                        {'name': 'Content-Type', 'value': 'application/json'},\n                    ]\n                    print(f\"🎭 API de configuração simulada\")\n                    await tab.fulfill_request(\n                        request_id, 200, headers, body, 'OK'\n                    )\n                \n                # Adicionar cabeçalhos de autenticação a requisições de API\n                elif '/api/' in url and method == 'GET':\n                    stats['modified'] += 1\n                    headers: list[HeaderEntry] = [\n                        {'name': 'Authorization', 'value': 'Bearer token-123'},\n                    ]\n                    print(f\"✨ Adicionado auth para: {url[:50]}\")\n                    await tab.continue_request(request_id, headers=headers)\n                \n                # Continuar todo o resto normalmente\n                else:\n                    stats['continued'] += 1\n                    await tab.continue_request(request_id)\n                    \n            except Exception as e:\n                print(f\"⚠️  Erro ao tratar requisição: {e}\")\n                # Sempre continuar em caso de erro para evitar travamento\n                await tab.continue_request(request_id)\n        \n        # Habilitar interceptação\n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, intelligent_handler)\n        \n        # Navegar\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(5)\n        \n        # Imprimir estatísticas\n        print(f\"\\n📊 Estatísticas de Interceptação:\")\n        print(f\"   Bloqueados: {stats['blocked']}\")\n        print(f\"   Simulados: {stats['mocked']}\")\n        print(f\"   Modificados: {stats['modified']}\")\n        print(f\"   Continuados: {stats['continued']}\")\n        print(f\"   Total: {sum(stats.values())}\")\n        \n        # Limpeza\n        await tab.disable_fetch_events()\n\nasyncio.run(advanced_interception())\n```\n\n## Veja Também\n\n- **[Monitoramento de Rede](monitoring.md)** - Observação passiva de tráfego de rede\n- **[Domínio Fetch do CDP](../../deep-dive/network-capabilities.md#fetch-domain)** - Análise profunda sobre o domínio Fetch\n- **[Sistema de Eventos](../advanced/event-system.md)** - Entendendo a arquitetura de eventos do Pydoll\n\nA interceptação de requisições é uma ferramenta poderosa para testes, otimização e simulação (mocking). Domine essas técnicas para construir scripts de automação de navegador robustos e eficientes."
  },
  {
    "path": "docs/pt/features/network/monitoring.md",
    "content": "# Monitoramento de Rede\n\nO monitoramento de rede no Pydoll permite observar e analisar requisições HTTP, respostas e outras atividades de rede durante a automação do navegador. Isso é essencial para depuração, análise de desempenho, testes de API e para entender como as aplicações web se comunicam com os servidores.\n\n!!! info \"Domínio Network vs Fetch\"\n    O **domínio Network** é para monitoramento passivo (observar o tráfego). O **domínio Fetch** é para interceptação ativa (modificar requisições/respostas). Este guia foca no monitoramento. Para interceptação de requisições, veja a documentação avançada.\n\n## Habilitando Eventos de Rede\n\nAntes de poder monitorar a atividade de rede, você deve habilitar o domínio Network:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Habilitar monitoramento de rede\n        await tab.enable_network_events()\n        \n        # Agora navegue\n        await tab.go_to('https://api.github.com')\n        \n        # Não se esqueça de desabilitar quando terminar (opcional, mas recomendado)\n        await tab.disable_network_events()\n\nasyncio.run(main())\n```\n\n!!! warning \"Habilite Antes de Navegar\"\n    Sempre habilite os eventos de rede **antes** de navegar para capturar todas as requisições. Requisições feitas antes da habilitação não serão capturadas.\n\n## Obtendo Logs de Rede\n\nO Pydoll armazena automaticamente os logs de rede quando os eventos de rede estão habilitados. Você pode recuperá-los usando `get_network_logs()`:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def analyze_requests():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # Navegar para uma página\n        await tab.go_to('https://httpbin.org/json')\n        \n        # Esperar a página carregar completamente\n        await asyncio.sleep(2)\n        \n        # Obter todos os logs de rede\n        logs = await tab.get_network_logs()\n        \n        print(f\"Total de requisições capturadas: {len(logs)}\")\n        \n        for log in logs:\n            request = log['params']['request']\n            print(f\"→ {request['method']} {request['url']}\")\n\nasyncio.run(analyze_requests())\n```\n\n!!! note \"Espera Pronta para Produção\"\n    Os exemplos acima usam `asyncio.sleep(2)` por simplicidade. Em código de produção, considere usar estratégias de espera mais explícitas:\n    \n    - Esperar por elementos específicos aparecerem\n    - Usar o [Sistema de Eventos](../advanced/event-system.md) para detectar quando todos os recursos foram carregados\n    - Implementar detecção de ociosidade da rede (veja a seção Monitoramento de Rede em Tempo Real)\n    \n    Isso garante que sua automação espere exatamente o tempo necessário, nem mais, nem menos.\n\n### Filtrando Logs de Rede\n\nVocê pode filtrar logs por padrão de URL:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def filter_logs_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        \n        # Obter todos os logs\n        all_logs = await tab.get_network_logs()\n        \n        # Obter logs para um domínio específico\n        api_logs = await tab.get_network_logs(filter='api.example.com')\n        \n        # Obter logs para um endpoint específico\n        user_logs = await tab.get_network_logs(filter='/api/users')\n\nasyncio.run(filter_logs_example())\n```\n\n## Entendendo a Estrutura de Eventos de Rede\n\nOs logs de rede contêm informações detalhadas sobre cada requisição. Aqui está a estrutura:\n\n### Evento RequestWillBeSent\n\nEste evento é disparado quando uma requisição está prestes a ser enviada:\n\n```python\n{\n    'method': 'Network.requestWillBeSent',\n    'params': {\n        'requestId': 'unique-request-id',\n        'loaderId': 'loader-id',\n        'documentURL': 'https://example.com',\n        'request': {\n            'url': 'https://api.example.com/data',\n            'method': 'GET',  # ou 'POST', 'PUT', 'DELETE', etc.\n            'headers': {\n                'User-Agent': 'Chrome/...',\n                'Accept': 'application/json',\n                ...\n            },\n            'postData': '...',  # Presente apenas para requisições POST/PUT\n            'initialPriority': 'High',\n            'referrerPolicy': 'strict-origin-when-cross-origin'\n        },\n        'timestamp': 1234567890.123,\n        'wallTime': 1234567890.123,\n        'initiator': {\n            'type': 'script',  # ou 'parser', 'other'\n            'stack': {...}  # Call stack se iniciado por script\n        },\n        'type': 'XHR',  # Tipo de recurso: Document, Script, Image, XHR, etc.\n        'frameId': 'frame-id',\n        'hasUserGesture': False\n    }\n}\n```\n\n### Referência de Campos Chave\n\n| Campo | Localização | Tipo | Descrição |\n|---|---|---|---|\n| `requestId` | `params.requestId` | `str` | Identificador único para esta requisição |\n| `url` | `params.request.url` | `str` | URL completa da requisição |\n| `method` | `params.request.method` | `str` | Método HTTP (GET, POST, etc.) |\n| `headers` | `params.request.headers` | `dict` | Cabeçalhos da requisição |\n| `postData` | `params.request.postData` | `str` | Corpo da requisição (POST/PUT) |\n| `timestamp` | `params.timestamp` | `float` | Tempo monotônico quando a requisição iniciou |\n| `type` | `params.type` | `str` | Tipo de recurso (Document, XHR, Image, etc.) |\n| `initiator` | `params.initiator` | `dict` | O que disparou esta requisição |\n\n## Obtendo Corpos de Resposta\n\nPara obter o conteúdo real da resposta, use `get_network_response_body()`:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def fetch_api_response():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # Navegar para o endpoint da API\n        await tab.go_to('https://httpbin.org/json')\n        await asyncio.sleep(2)\n        \n        # Obter todas as requisições\n        logs = await tab.get_network_logs()\n        \n        for log in logs:\n            request_id = log['params']['requestId']\n            url = log['params']['request']['url']\n            \n            # Obter resposta apenas para o endpoint JSON\n            if 'httpbin.org/json' in url:\n                try:\n                    # Obter corpo da resposta\n                    response_body = await tab.get_network_response_body(request_id)\n                    print(f\"Resposta de {url}:\")\n                    print(response_body)\n                except Exception as e:\n                    print(f\"Não foi possível obter o corpo da resposta: {e}\")\n\nasyncio.run(fetch_api_response())\n```\n\n!!! warning \"Disponibilidade do Corpo da Resposta\"\n    Corpos de resposta estão disponíveis apenas para requisições que foram concluídas. Além disso, alguns tipos de resposta (como imagens ou redirecionamentos) podem não ter corpos acessíveis.\n\n## Casos de Uso Práticos\n\n### 1. Teste e Validação de API\n\nMonitore chamadas de API para verificar se as requisições corretas estão sendo feitas:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def validate_api_calls():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # Navegar para sua aplicação\n        await tab.go_to('https://your-app.com')\n        \n        # Disparar alguma ação que faça chamadas de API\n        button = await tab.find(id='load-data-button')\n        await button.click()\n        await asyncio.sleep(2)\n        \n        # Obter logs da API\n        api_logs = await tab.get_network_logs(filter='/api/')\n        \n        print(f\"\\n📊 Resumo das Chamadas de API:\")\n        print(f\"Total de chamadas de API: {len(api_logs)}\")\n        \n        for log in api_logs:\n            request = log['params']['request']\n            method = request['method']\n            url = request['url']\n            \n            # Verificar se o cabeçalho de autenticação correto está presente\n            headers = request.get('headers', {})\n            has_auth = 'Authorization' in headers or 'authorization' in headers\n            \n            print(f\"\\n{method} {url}\")\n            print(f\"  ✓ Possui Autorização: {has_auth}\")\n            \n            # Validar dados POST se aplicável\n            if method == 'POST' and 'postData' in request:\n                print(f\"  📤 Corpo: {request['postData'][:100]}...\")\n\nasyncio.run(validate_api_calls())\n```\n\n### 2. Análise de Desempenho\n\nAnalise o tempo das requisições e identifique recursos lentos:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def analyze_performance():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(5)\n        \n        logs = await tab.get_network_logs()\n        \n        # Armazenar dados de tempo\n        timings = []\n        \n        for log in logs:\n            params = log['params']\n            request_id = params['requestId']\n            url = params['request']['url']\n            resource_type = params.get('type', 'Other')\n            \n            timings.append({\n                'url': url,\n                'type': resource_type,\n                'timestamp': params['timestamp']\n            })\n        \n        # Ordenar por timestamp\n        timings.sort(key=lambda x: x['timestamp'])\n        \n        print(\"\\n⏱️  Linha do Tempo das Requisições:\")\n        start_time = timings[0]['timestamp'] if timings else 0\n        \n        for timing in timings[:20]:  # Mostrar as primeiras 20\n            elapsed = (timing['timestamp'] - start_time) * 1000  # Converter para ms\n            print(f\"{elapsed:7.0f}ms | {timing['type']:12} | {timing['url'][:80]}\")\n\nasyncio.run(analyze_performance())\n```\n\n### 3. Detectando Recursos Externos\n\nEncontre todos os domínios externos aos quais sua página se conecta:\n\n```python\nimport asyncio\nfrom urllib.parse import urlparse\nfrom collections import Counter\nfrom pydoll.browser.chromium import Chrome\n\nasync def analyze_domains():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        await tab.go_to('https://news.ycombinator.com')\n        await asyncio.sleep(5)\n        \n        logs = await tab.get_network_logs()\n        \n        # Contar requisições por domínio\n        domains = Counter()\n        \n        for log in logs:\n            url = log['params']['request']['url']\n            try:\n                domain = urlparse(url).netloc\n                if domain:\n                    domains[domain] += 1\n            except:\n                pass\n        \n        print(\"\\n🌐 Domínios Externos:\")\n        for domain, count in domains.most_common(10):\n            print(f\"  {count:3} requisições | {domain}\")\n\nasyncio.run(analyze_domains())\n```\n\n### 4. Monitorando Tipos Específicos de Recursos\n\nRastreie tipos específicos de recursos, como imagens ou scripts:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def track_resource_types():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        logs = await tab.get_network_logs()\n        \n        # Agrupar por tipo de recurso\n        by_type = {}\n        \n        for log in logs:\n            params = log['params']\n            resource_type = params.get('type', 'Other')\n            url = params['request']['url']\n            \n            if resource_type not in by_type:\n                by_type[resource_type] = []\n            \n            by_type[resource_type].append(url)\n        \n        print(\"\\n📦 Recursos por Tipo:\")\n        for rtype in sorted(by_type.keys()):\n            urls = by_type[rtype]\n            print(f\"\\n{rtype}: {len(urls)} recurso(s)\")\n            for url in urls[:3]:  # Mostrar os 3 primeiros\n                print(f\"  • {url}\")\n            if len(urls) > 3:\n                print(f\"  ... e mais {len(urls) - 3}\")\n\nasyncio.run(track_resource_types())\n```\n\n## Monitoramento de Rede em Tempo Real\n\nPara monitoramento em tempo real, use callbacks de eventos em vez de consultar `get_network_logs()`:\n\n!!! info \"Entendendo Eventos\"\n    O monitoramento em tempo real usa o sistema de eventos do Pydoll para reagir à atividade de rede assim que ela acontece. Para uma análise profunda de como os eventos funcionam, veja **[Sistema de Eventos](../advanced/event-system.md)**.\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import (\n    NetworkEvent,\n    RequestWillBeSentEvent,\n    ResponseReceivedEvent,\n    LoadingFailedEvent\n)\n\nasync def real_time_monitoring():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Estatísticas\n        stats = {\n            'requests': 0,\n            'responses': 0,\n            'failed': 0\n        }\n        \n        # Callback de requisição\n        async def on_request(event: RequestWillBeSentEvent):\n            stats['requests'] += 1\n            url = event['params']['request']['url']\n            method = event['params']['request']['method']\n            print(f\"→ {method:6} | {url}\")\n        \n        # Callback de resposta\n        async def on_response(event: ResponseReceivedEvent):\n            stats['responses'] += 1\n            response = event['params']['response']\n            status = response['status']\n            url = response['url']\n            \n            # Código de cor por status\n            if 200 <= status < 300:\n                color = '\\033[92m'  # Verde\n            elif 300 <= status < 400:\n                color = '\\033[93m'  # Amarelo\n            else:\n                color = '\\033[91m'  # Vermelho\n            reset = '\\033[0m'\n            \n            print(f\"← {color}{status}{reset} | {url}\")\n        \n        # Callback de falha\n        async def on_failed(event: LoadingFailedEvent):\n            stats['failed'] += 1\n            error = event['params']['errorText']\n            print(f\"✗ FALHOU: {error}\")\n        \n        # Habilitar e registrar callbacks\n        await tab.enable_network_events()\n        await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, on_request)\n        await tab.on(NetworkEvent.RESPONSE_RECEIVED, on_response)\n        await tab.on(NetworkEvent.LOADING_FAILED, on_failed)\n        \n        # Navegar\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(5)\n        \n        print(f\"\\n📊 Resumo:\")\n        print(f\"  Requisições: {stats['requests']}\")\n        print(f\"  Respostas: {stats['responses']}\")\n        print(f\"  Falhas: {stats['failed']}\")\n\nasyncio.run(real_time_monitoring())\n```\n\n## Referência de Tipos de Recurso\n\nO Pydoll captura os seguintes tipos de recurso:\n\n| Tipo | Descrição | Exemplos |\n|---|---|---|\n| `Document` | Documentos HTML principais | Carregamentos de página, fontes de iframe |\n| `Stylesheet` | Arquivos CSS | .css externo, estilos inline |\n| `Image` | Recursos de imagem | .jpg, .png, .gif, .webp, .svg |\n| `Media` | Arquivos de áudio/vídeo | .mp4, .webm, .mp3, .ogg |\n| `Font` | Fontes web | .woff, .woff2, .ttf, .otf |\n| `Script` | Arquivos JavaScript | Arquivos .js, scripts inline |\n| `TextTrack` | Arquivos de legenda | .vtt, .srt |\n| `XHR` | XMLHttpRequest | Requisições AJAX, chamadas de API legadas |\n| `Fetch` | Requisições da API Fetch | Chamadas de API modernas |\n| `EventSource` | Server-Sent Events | Streams em tempo real |\n| `WebSocket` | Conexões WebSocket | Comunicação bidirecional |\n| `Manifest` | Manifestos de aplicativos web | Configuração de PWA |\n| `Other` | Outros tipos de recurso | Diversos |\n\n## Avançado: Extraindo Tempos de Resposta\n\nEventos de rede incluem informações detalhadas de tempo:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def analyze_timing():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # Callback personalizado para capturar tempos\n        timing_data = []\n        \n        async def on_response(event: ResponseReceivedEvent):\n            response = event['params']['response']\n            timing = response.get('timing')\n            \n            if timing:\n                # Calcular diferentes fases\n                dns_time = timing.get('dnsEnd', 0) - timing.get('dnsStart', 0)\n                connect_time = timing.get('connectEnd', 0) - timing.get('connectStart', 0)\n                ssl_time = timing.get('sslEnd', 0) - timing.get('sslStart', 0)\n                send_time = timing.get('sendEnd', 0) - timing.get('sendStart', 0)\n                wait_time = timing.get('receiveHeadersStart', 0) - timing.get('sendEnd', 0)\n                receive_time = timing.get('receiveHeadersEnd', 0) - timing.get('receiveHeadersStart', 0)\n                \n                timing_data.append({\n                    'url': response['url'][:50],\n                    'dns': dns_time if dns_time > 0 else 0,\n                    'connect': connect_time if connect_time > 0 else 0,\n                    'ssl': ssl_time if ssl_time > 0 else 0,\n                    'send': send_time,\n                    'wait': wait_time,\n                    'receive': receive_time,\n                    'total': receive_time + wait_time + send_time\n                })\n        \n        await tab.on(NetworkEvent.RESPONSE_RECEIVED, on_response)\n        await tab.go_to('https://github.com')\n        await asyncio.sleep(5)\n        \n        # Imprimir detalhamento de tempo\n        print(\"\\n⏱️  Detalhamento de Tempo da Requisição (ms):\")\n        print(f\"{'URL':<50} | {'DNS':>6} | {'Connect':>8} | {'SSL':>6} | {'Send':>6} | {'Wait':>6} | {'Receive':>8} | {'Total':>7}\")\n        print(\"-\" * 120)\n        \n        for data in sorted(timing_data, key=lambda x: x['total'], reverse=True)[:10]:\n            print(f\"{data['url']:<50} | {data['dns']:6.1f} | {data['connect']:8.1f} | {data['ssl']:6.1f} | \"\n                  f\"{data['send']:6.1f} | {data['wait']:6.1f} | {data['receive']:8.1f} | {data['total']:7.1f}\")\n\nasyncio.run(analyze_timing())\n```\n\n## Explicação dos Campos de Tempo\n\n| Fase | Campos | Descrição |\n|---|---|---|\n| **DNS** | `dnsStart` → `dnsEnd` | Tempo de lookup DNS |\n| **Connect** | `connectStart` → `connectEnd` | Estabelecimento da conexão TCP |\n| **SSL** | `sslStart` → `sslEnd` | Handshake SSL/TLS |\n| **Send** | `sendStart` → `sendEnd` | Tempo para enviar a requisição |\n| **Wait** | `sendEnd` → `receiveHeadersStart` | Esperando pela resposta do servidor (TTFB) |\n| **Receive** | `receiveHeadersStart` → `receiveHeadersEnd` | Tempo para receber os cabeçalhos da resposta |\n\n!!! tip \"Time to First Byte (TTFB)\"\n    TTFB é a fase \"Wait\" - o tempo entre enviar a requisição e receber o primeiro byte da resposta. Isso é crucial para análise de desempenho.\n\n## Melhores Práticas\n\n### 1. Habilite Eventos de Rede Apenas Quando Necessário\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def best_practice_enable():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # Bom: Habilitar antes da navegação, desabilitar depois\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        logs = await tab.get_network_logs()\n        await tab.disable_network_events()\n        \n        # Ruim: Deixar habilitado durante toda a sessão\n        # await tab.enable_network_events()\n        # ... longa sessão de automação ...\n```\n\n### 2. Filtre Logs para Reduzir o Uso de Memória\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def best_practice_filter():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        \n        # Bom: Filtrar por requisições específicas\n        api_logs = await tab.get_network_logs(filter='/api/')\n        \n        # Ruim: Obter todos os logs quando você só precisa de específicos\n        all_logs = await tab.get_network_logs()\n        filtered = [log for log in all_logs if '/api/' in log['params']['request']['url']]\n```\n\n### 3. Acesse Campos Faltantes com Segurança\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def best_practice_safe_access():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        \n        logs = await tab.get_network_logs()\n        \n        # Bom: Acesso seguro com .get()\n        for log in logs:\n            params = log.get('params', {})\n            request = params.get('request', {})\n            url = request.get('url', 'Unknown')\n            post_data = request.get('postData')  # Pode ser None\n            \n            if post_data:\n                print(f\"Dados POST: {post_data}\")\n        \n        # Ruim: Acesso direto pode levantar KeyError\n        # url = log['params']['request']['url']\n        # post_data = log['params']['request']['postData']  # Pode não existir!\n```\n\n### 4. Use Callbacks de Evento para Necessidades em Tempo Real\n\n```python\nimport asyncio\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\n\n# Bom: Monitoramento em tempo real com callbacks\nasync def on_request(event: RequestWillBeSentEvent):\n    print(f\"Nova requisição: {event['params']['request']['url']}\")\n\nawait tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, on_request)\n\n# Ruim: Consultar logs repetidamente (ineficiente)\nwhile True:\n    logs = await tab.get_network_logs()\n    # Processar logs...\n    await asyncio.sleep(0.5)  # Desperdício!\n```\n\n## Veja Também\n\n- **[Domínio de Rede CDP](../../deep-dive/network-capabilities.md)** - Análise profunda sobre as capacidades de rede\n- **[Sistema de Eventos](../advanced/event-system.md)** - Entendendo a arquitetura de eventos do Pydoll\n- **[Interceptação de Requisições](interception.md)** - Modificando requisições e respostas"
  },
  {
    "path": "docs/pt/features/network/network-recording.md",
    "content": "# Gravacao de Rede HAR\n\nCapture toda a atividade de rede durante uma sessao do navegador e exporte como um arquivo HAR (HTTP Archive) 1.2 padrao. Perfeito para depuracao, analise de desempenho e fixtures de teste.\n\n!!! tip \"Depure Como um Profissional\"\n    Arquivos HAR sao o padrao da industria para gravar trafego de rede. Voce pode importa-los diretamente no Chrome DevTools, Charles Proxy ou qualquer visualizador HAR para analise detalhada.\n\n## Por que Usar Gravacao HAR?\n\n| Caso de Uso | Beneficio |\n|-------------|-----------|\n| Depurar requisicoes com falha | Veja headers exatos, timing e corpos de resposta |\n| Analise de desempenho | Identifique requisicoes lentas e gargalos |\n| Documentacao de API | Capture pares reais de requisicao/resposta |\n| Fixtures de teste | Grave trafego real para mocking em testes |\n\n## Inicio Rapido\n\nGrave todo o trafego de rede durante uma navegacao:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def gravar_trafego():\n    async with Chrome() as browser:\n        tab = await browser.start()\n\n        async with tab.request.record() as capture:\n            await tab.go_to('https://example.com')\n\n        # Salve a captura como arquivo HAR\n        capture.save('flow.har')\n        print(f'Capturadas {len(capture.entries)} requisicoes')\n\nasyncio.run(gravar_trafego())\n```\n\n## API de Gravacao\n\n### `tab.request.record(resource_types=None)`\n\nGerenciador de contexto que captura o trafego de rede na aba.\n\n| Parametro | Tipo | Descricao |\n|-----------|------|-----------|\n| `resource_types` | `list[ResourceType] \\| None` | Lista opcional de tipos de recurso a capturar. Quando `None` (padrao), todos os tipos sao capturados. |\n\n```python\nasync with tab.request.record() as capture:\n    # Toda atividade de rede dentro deste bloco e capturada\n    await tab.go_to('https://example.com')\n    await (await tab.find(id='search')).type_text('pydoll')\n    await (await tab.find(type='submit')).click()\n```\n\nO objeto `capture` (`HarCapture`) fornece:\n\n| Propriedade/Metodo | Descricao |\n|-------------------|-----------|\n| `capture.entries` | Lista de entradas HAR capturadas |\n| `capture.to_dict()` | Dict HAR 1.2 completo (para processamento customizado) |\n| `capture.save(path)` | Salvar como arquivo JSON HAR |\n\n### Filtrando por Tipo de Recurso\n\nGrave apenas tipos de recurso especificos ao inves de todo o trafego:\n\n```python\nfrom pydoll.protocol.network.types import ResourceType\n\n# Gravar apenas requisicoes fetch/XHR (ignorar documentos, imagens, etc.)\nasync with tab.request.record(\n    resource_types=[ResourceType.FETCH, ResourceType.XHR]\n) as capture:\n    await tab.go_to('https://example.com')\n\n# Gravar apenas documentos e folhas de estilo\nasync with tab.request.record(\n    resource_types=[ResourceType.DOCUMENT, ResourceType.STYLESHEET]\n) as capture:\n    await tab.go_to('https://example.com')\n```\n\nValores disponiveis de `ResourceType`:\n\n| Valor | Descricao |\n|-------|-----------|\n| `ResourceType.DOCUMENT` | Documentos HTML |\n| `ResourceType.STYLESHEET` | Folhas de estilo CSS |\n| `ResourceType.SCRIPT` | Arquivos JavaScript |\n| `ResourceType.IMAGE` | Imagens |\n| `ResourceType.FONT` | Fontes web |\n| `ResourceType.MEDIA` | Audio/video |\n| `ResourceType.FETCH` | Requisicoes Fetch API |\n| `ResourceType.XHR` | Chamadas XMLHttpRequest |\n| `ResourceType.WEB_SOCKET` | Conexoes WebSocket |\n| `ResourceType.OTHER` | Outros tipos de recurso |\n\n### Salvando Capturas\n\n```python\n# Salvar como arquivo HAR (pode ser aberto no Chrome DevTools)\ncapture.save('flow.har')\n\n# Salvar em diretorio aninhado (criado automaticamente)\ncapture.save('recordings/session1/flow.har')\n\n# Acessar o dict HAR bruto para processamento customizado\nhar_dict = capture.to_dict()\nprint(har_dict['log']['version'])  # \"1.2\"\n```\n\n### Inspecionando Entradas\n\n```python\nasync with tab.request.record() as capture:\n    await tab.go_to('https://example.com')\n\nfor entry in capture.entries:\n    req = entry['request']\n    resp = entry['response']\n    print(f\"{req['method']} {req['url']} -> {resp['status']}\")\n```\n\n## Uso Avancado\n\n### Filtrando Entradas Capturadas\n\n```python\nasync with tab.request.record() as capture:\n    await tab.go_to('https://example.com')\n\n# Filtrar apenas chamadas de API\napi_entries = [\n    e for e in capture.entries\n    if '/api/' in e['request']['url']\n]\n\n# Filtrar apenas requisicoes com falha\nfalhas = [\n    e for e in capture.entries\n    if e['response']['status'] >= 400\n]\n```\n\n### Processamento HAR Customizado\n\n```python\nhar = capture.to_dict()\n\n# Contar requisicoes por tipo\nfrom collections import Counter\ntipos = Counter(\n    e.get('_resourceType', 'Other')\n    for e in har['log']['entries']\n)\nprint(tipos)  # Counter({'Document': 1, 'Script': 5, 'Stylesheet': 3, ...})\n```\n\n## Formato de Arquivo HAR\n\nO HAR exportado segue a [especificacao HAR 1.2](http://www.softwareishard.com/blog/har-12-spec/). Cada entrada contem:\n\n- **Request**: metodo, URL, headers, parametros de query, dados POST\n- **Response**: status, headers, corpo da resposta (texto ou codificado em base64)\n- **Timings**: DNS, conexao, SSL, envio, espera (TTFB), recebimento\n- **Metadata**: IP do servidor, ID de conexao, tipo de recurso\n\n!!! note \"Corpos de Resposta\"\n    Os corpos de resposta sao capturados automaticamente apos cada requisicao ser concluida. Conteudo binario (imagens, fontes, etc.) e armazenado como strings codificadas em base64.\n"
  },
  {
    "path": "docs/pt/index.md",
    "content": "<p align=\"center\">\n    <img src=\"../resources/images/logo.png\" alt=\"Pydoll Logo\" /> <br><br>\n</p>\n\n<p align=\"center\">\n    <a href=\"https://codecov.io/gh/autoscrape-labs/pydoll\">\n        <img src=\"https://codecov.io/gh/autoscrape-labs/pydoll/graph/badge.svg?token=40I938OGM9\"/>\n    </a>\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/tests.yml/badge.svg\" alt=\"Testes\">\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/ruff-ci.yml/badge.svg\" alt=\"Ruff CI\">\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/release.yml/badge.svg\" alt=\"Release\">\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/mypy.yml/badge.svg\" alt=\"MyPy CI\">\n</p>\n\n\n# Bem-vindo ao Pydoll\n\nOlá! Obrigado por conferir o Pydoll, a próxima geração de automação de navegadores para Python. Se você está cansado de lidar com webdrivers e procura uma maneira mais suave e confiável de automatizar navegadores, você está no lugar certo.\n\n## O que é o Pydoll?\n\nO Pydoll está revolucionando a automação de navegadores, **eliminando completamente a necessidade de webdrivers**! Ao contrário de outras soluções que dependem de dependências externas, o Pydoll se conecta diretamente aos navegadores usando o Chrome DevTools Protocol, proporcionando uma experiência de automação perfeita e confiável com desempenho assíncrono nativo.\n\nSeja para extrair dados, [testar aplicativos web](https://www.lambdatest.com/web-testing) ou automatizar tarefas repetitivas, o Pydoll torna tudo surpreendentemente fácil com sua API intuitiva e recursos poderosos. \n\n## Instalação\n\nCrie e ative um [ambiente virtual](https://docs.python.org/3/tutorial/venv.html) primeiro e, em seguida, instale o Pydoll:\n\n<div class=\"termy\">\n```bash\n$ pip install pydoll-python\n\n---> 100%\n```\n</div>\n\nPara a versão de desenvolvimento mais recente, você pode instalar diretamente do GitHub:\n\n```bash\n$ pip install git+https://github.com/autoscrape-labs/pydoll.git\n```\n\n## Por que escolher o Pydoll?\n\n- **Simplicidade Genuína**: Não queremos que você perca tempo configurando drivers ou lidando com problemas de compatibilidade. Com o Pydoll, você instala e está pronto para automatizar.\n- **Interações Verdadeiramente Humanas**: Nossos algoritmos simulam padrões de comportamento humano reais, desde o tempo entre os cliques até a forma como o mouse se move pela tela.\n- **Desempenho Assíncrono Nativo**: Construído do zero com `asyncio`, o Pydoll não apenas suporta operações assíncronas, mas foi projetado para elas.\n- **Inteligência Integrada**: Bypass automático de captchas Cloudflare Turnstile e reCAPTCHA v3, sem serviços externos ou configurações complexas.\n- **Monitoramento de Rede Poderoso**: Intercepte, modifique e analise todo o tráfego de rede com facilidade, dando a você controle total sobre as requisições.\n- **Arquitetura Orientada a Eventos**: Reaja a eventos da página, requisições de rede e interações do usuário em tempo real.\n- **Localização de Elementos Intuitiva**: Métodos modernos `find()` e `query()` que fazem sentido e funcionam como você esperaria.\n- **Segurança de Tipos Robusta**: Sistema de tipos abrangente para melhor suporte da IDE e prevenção de erros.\n\n\nPronto para começar? As páginas a seguir guiarão você pela instalação, uso básico e recursos avançados para ajudá-lo a aproveitar ao máximo o Pydoll.\n\nVamos começar a automatizar a web, da maneira certa! 🚀\n\n## Guia de Início Rápido: Um exemplo simples\n\nVamos começar com um exemplo prático. O script a seguir abrirá o repositório Pydoll no GitHub e o marcará como favorito:\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://github.com/autoscrape-labs/pydoll')\n\n        star_button = await tab.find(\n            tag_name='button',\n            timeout=5,\n            raise_exc=False\n        )\n        if not star_button:\n            print(\"Ops! O botão não foi encontrado.\")\n            return\n\n        await star_button.click()\n        await asyncio.sleep(3)\n\nasyncio.run(main())\n```\n\nEste exemplo demonstra como navegar até um site, esperar que um elemento apareça e interagir com ele. Você pode adaptar esse padrão para automatizar diversas tarefas web.\n\n??? note \"Ou use sem o gerenciador de contexto...\"\n    Se preferir não usar o padrão de gerenciador de contexto, você pode gerenciar a instância do navegador manualmente:\n    ```python\n    import asyncio\n    from pydoll.browser.chromium import Chrome\n\n    async def main():\n        browser = Chrome()\n        tab = await browser.start()\n        await tab.go_to('https://github.com/autoscrape-labs/pydoll')\n\n        star_button = await tab.find(\n            tag_name='button',\n            timeout=5,\n            raise_exc=False\n        )\n        if not star_button:\n            print(\"Ops! O botão não foi encontrado.\")\n            return\n\n        await star_button.click()\n        await asyncio.sleep(3)\n        await browser.stop()\n\n    asyncio.run(main())\n    ```\n    Observe que, ao não usar o gerenciador de contexto, você precisará chamar explicitamente `browser.stop()` para liberar os recursos.\n\n\n## Exemplo Estendido: Configuração personalizada do navegador\n\nPara cenários de uso mais avançados, o Pydoll permite personalizar a configuração do seu navegador usando a classe `ChromiumOptions`. Isso é útil quando você precisa:\n\n- Executar em modo headless (sem janela do navegador visível)\n- Especificar um caminho personalizado para o executável do navegador\n- Configurar proxies, user agents ou outras configurações do navegador\n- Definir as dimensões da janela ou argumentos de inicialização\n\nAqui está um exemplo mostrando como usar opções personalizadas para o Chrome:\n\n```python hl_lines=\"8-12 30-32 34-38\"\nimport asyncio\nimport os\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def main():\n    options = ChromiumOptions()\n    options.binary_location = '/usr/bin/google-chrome-stable'\n    options.add_argument('--headless=new')\n    options.add_argument('--start-maximized')\n    options.add_argument('--disable-notifications')\n\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://github.com/autoscrape-labs/pydoll')\n\n        star_button = await tab.find(\n            tag_name='button',\n            timeout=5,\n            raise_exc=False\n        )\n        if not star_button:\n            print(\"Ops! O botão não foi encontrado.\")\n            return\n\n        await star_button.click()\n        await asyncio.sleep(3)\n\n        screenshot_path = os.path.join(os.getcwd(), 'pydoll_repo.png')\n        await tab.take_screenshot(path=screenshot_path)\n        print(f\"Captura de tela salva em: {screenshot_path}\")\n\n        base64_screenshot = await tab.take_screenshot(as_base64=True)\n\n        repo_description_element = await tab.find(\n            class_name='f4.my-3'\n        )\n        repo_description = await repo_description_element.text\n        print(f\"Descrição do repositório: {repo_description}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nEste exemplo estendido demonstra:\n\n1. Criação e configuração de opções do navegador\n2. Definição de um caminho personalizado para o binário do Chrome\n3. Habilitação do modo headless para operação invisível\n4. Definição de sinalizadores adicionais do navegador\n5. Captura de tela (especialmente útil em modo headless) modo)\n\n??? info \"Sobre as Opções do Chromium\"\n    O método `options.add_argument()` permite que você passe qualquer argumento de linha de comando do Chromium para personalizar o comportamento do navegador. Existem centenas de opções disponíveis para controlar tudo, desde rede até comportamento de renderização. \n\n    Opções comuns do Chrome\n\n    ```python\n    # Opções de Desempenho e Comportamento\n    options.add_argument('--headless=new')         # Executar o Chrome em modo headless\n    options.add_argument('--disable-gpu')          # Desabilitar a aceleração de hardware da GPU\n    options.add_argument('--no-sandbox')           # Desabilitar o sandbox (use com cuidado)\n    options.add_argument('--disable-dev-shm-usage') # Superar problemas de recursos limitados\n\n    # Opções de Aparência\n    options.add_argument('--start-maximized')      # Iniciar com a janela maximizada\n    options.add_argument('--window-size=1920,1080') # Definir tamanho específico da janela\n    options.add_argument('--hide-scrollbars')      # Ocultar barras de rolagem\n\n    # Opções de Rede\n    options.add_argument('--proxy-server=socks5://127.0.0.1:9050') # Usar proxy\n    options.add_argument('--disable-extensions')   # Desabilitar extensões\n    options.add_argument('--disable-notifications') # Desabilitar notificações\n\n    # Privacidade e Segurança\n    options.add_argument('--incognito')            # Executar em modo anônimo\n    options.add_argument('--disable-infobars')     # Desabilitar barras de informações\n    ```\n\n    Guias de Referência Completos\n\n    Para obter uma lista completa de todos os argumentos de linha de comando do Chrome disponíveis, consulte estes recursos:\n\n    - [Opções de Linha de Comando do Chromium](https://peter.sh/experiments/chromium-command-line-switches/) - Lista de referência completa\n    - [Flags do Chrome](chrome://flags) - Digite isso na barra de endereço do seu navegador Chrome para ver os recursos experimentais\n    - [Flags do Código-Fonte do Chromium](https://source.chromium.org/chromium/chromium/src/+/main:chrome/common/chrome_switches.cc) - Referência direta ao código-fonte\n\n    Lembre-se de que algumas opções podem se comportar de maneira diferente em diferentes versões do Chrome, portanto, é uma boa prática testar sua configuração ao atualizar o Chrome. \n\nCom essas configurações, você pode executar o Pydoll em diversos ambientes, incluindo pipelines de CI/CD, servidores sem interface gráfica ou contêineres Docker.\n\nContinue lendo a documentação para explorar os recursos poderosos do Pydoll para lidar com captchas, trabalhar com várias abas, interagir com elementos e muito mais.\n\n## Dependências Mínimas\n\nUma das vantagens do Pydoll é sua leveza. Ao contrário de outras ferramentas de automação de navegador que exigem inúmeras dependências, o Pydoll foi projetado intencionalmente para ser minimalista, mantendo recursos poderosos.\n\n### Dependências Principais\n\nO Pydoll depende de apenas alguns pacotes cuidadosamente selecionados:\n\n```\npython = \"^3.10\"\nwebsockets = \"^13.1\"\naiohttp = \"^3.9.5\"\naiofiles = \"^23.2.1\"\nbs4 = \"^0.0.2\"\n```\n\nÉ só isso! Essa dependência mínima do Pydoll significa:\n\n- **Instalação mais rápida** - Sem árvore de dependências complexa para resolver\n- **Menos conflitos** - Menor chance de conflitos de versão com outros pacotes\n- **Menor consumo de recursos** - Menor uso de espaço em disco\n- **Melhor segurança** - Menor superfície de ataque e vulnerabilidades relacionadas a dependências\n- **Atualizações mais fáceis** - Manutenção mais simples e menos alterações que quebram a compatibilidade\n\nO pequeno número de dependências também contribui para a confiabilidade e o desempenho do Pydoll, pois há menos fatores externos que podem impactar seu funcionamento.\n\n## Top Sponsors\n\n<a href=\"https://substack.thewebscraping.club/p/pydoll-webdriver-scraping?utm_source=github&utm_medium=repo&utm_campaign=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n  <img src=\"../resources/images/banner-the-webscraping-club.png\" alt=\"The Web Scraping Club\" />\n</a>\n\n<sub>Leia uma review completa do Pydoll no <b><a href=\"https://substack.thewebscraping.club/p/pydoll-webdriver-scraping?utm_source=github&utm_medium=repo&utm_campaign=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">The Web Scraping Club</a></b>, a newsletter #1 dedicada a web scraping.</sub>\n\n## Patrocinadores\n\nO apoio dos patrocinadores é essencial para manter o projeto vivo, em constante evolução e acessível a toda a comunidade. Cada parceria ajuda a cobrir custos, impulsionar novos recursos e garantir o desenvolvimento contínuo. Somos muito gratos a todos que acreditam e apoiam o projeto!\n\n<div class=\"sponsors-grid\">\n  <a href=\"https://www.thordata.com/?ls=github&lk=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n    <img src=\"../resources/images/Thordata-logo.png\" alt=\"Thordata\" />\n  </a>\n  <a href=\"https://www.testmuai.com/?utm_medium=sponsor&utm_source=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n    <img src=\"../resources/images/logo-lamda-test.svg\" alt=\"LambdaTest\" />\n  </a>\n  <a href=\"https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n    <img src=\"../resources/images/capsolver-logo.png\" alt=\"CapSolver\" />\n  </a>\n</div>\n\n<p>\n  <a href=\"https://github.com/sponsors/thalissonvs\" target=\"_blank\" rel=\"noopener\">Seja um patrocinador</a>\n</p>\n\n\n## Licença\n\nO Pydoll é lançado sob a Licença MIT, que lhe dá a liberdade de usar, modificar e distribuir o código com restrições mínimas. Esta licença permissiva torna o Pydoll adequado para projetos pessoais e comerciais.\n\n??? info \"Ver o texto completo da Licença MIT\"\n    ```\n    MIT License\n    \n    Copyright (c) 2023 Pydoll Contributors\n    \n    Permission is hereby granted, free of charge, to any person obtaining a copy\n    of this software and associated documentation files (the \"Software\"), to deal\n    in the Software without restriction, including without limitation the rights\n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n    copies of the Software, and to permit persons to whom the Software is\n    furnished to do so, subject to the following conditions:\n    \n    The above copyright notice and this permission notice shall be included in all\n    copies or substantial portions of the Software.\n    \n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n    SOFTWARE.\n    ```"
  },
  {
    "path": "docs/resources/scripts/extra.js",
    "content": "function setupTermynal() {\n    document.querySelectorAll(\".use-termynal\").forEach(node => {\n        node.style.display = \"block\";\n        new Termynal(node, {\n            lineDelay: 500\n        });\n    });\n    const progressLiteralStart = \"---> 100%\";\n    const promptLiteralStart = \"$ \";\n    const customPromptLiteralStart = \"# \";\n    const termynalActivateClass = \"termy\";\n    let termynals = [];\n\n    function createTermynals() {\n        document\n            .querySelectorAll(`.${termynalActivateClass} .highlight code`)\n            .forEach(node => {\n                const text = node.textContent;\n                const lines = text.split(\"\\n\");\n                const useLines = [];\n                let buffer = [];\n                function saveBuffer() {\n                    if (buffer.length) {\n                        let isBlankSpace = true;\n                        buffer.forEach(line => {\n                            if (line) {\n                                isBlankSpace = false;\n                            }\n                        });\n                        dataValue = {};\n                        if (isBlankSpace) {\n                            dataValue[\"delay\"] = 0;\n                        }\n                        if (buffer[buffer.length - 1] === \"\") {\n                            // A last single <br> won't have effect\n                            // so put an additional one\n                            buffer.push(\"\");\n                        }\n                        const bufferValue = buffer.join(\"<br>\");\n                        dataValue[\"value\"] = bufferValue;\n                        useLines.push(dataValue);\n                        buffer = [];\n                    }\n                }\n                for (let line of lines) {\n                    if (line === progressLiteralStart) {\n                        saveBuffer();\n                        useLines.push({\n                            type: \"progress\"\n                        });\n                    } else if (line.startsWith(promptLiteralStart)) {\n                        saveBuffer();\n                        const value = line.replace(promptLiteralStart, \"\").trimEnd();\n                        useLines.push({\n                            type: \"input\",\n                            value: value\n                        });\n                    } else if (line.startsWith(\"// \")) {\n                        saveBuffer();\n                        const value = \"💬 \" + line.replace(\"// \", \"\").trimEnd();\n                        useLines.push({\n                            value: value,\n                            class: \"termynal-comment\",\n                            delay: 0\n                        });\n                    } else if (line.startsWith(customPromptLiteralStart)) {\n                        saveBuffer();\n                        const promptStart = line.indexOf(promptLiteralStart);\n                        if (promptStart === -1) {\n                            console.error(\"Custom prompt found but no end delimiter\", line)\n                        }\n                        const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, \"\")\n                        let value = line.slice(promptStart + promptLiteralStart.length);\n                        useLines.push({\n                            type: \"input\",\n                            value: value,\n                            prompt: prompt\n                        });\n                    } else {\n                        buffer.push(line);\n                    }\n                }\n                saveBuffer();\n                const div = document.createElement(\"div\");\n                node.replaceWith(div);\n                const termynal = new Termynal(div, {\n                    lineData: useLines,\n                    noInit: true,\n                    lineDelay: 500\n                });\n                termynals.push(termynal);\n            });\n    }\n\n    function loadVisibleTermynals() {\n        termynals = termynals.filter(termynal => {\n            if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) {\n                termynal.init();\n                return false;\n            }\n            return true;\n        });\n    }\n    window.addEventListener(\"scroll\", loadVisibleTermynals);\n    createTermynals();\n    loadVisibleTermynals();\n}\n\nfunction shuffle(array) {\n    var currentIndex = array.length, temporaryValue, randomIndex;\n    while (0 !== currentIndex) {\n        randomIndex = Math.floor(Math.random() * currentIndex);\n        currentIndex -= 1;\n        temporaryValue = array[currentIndex];\n        array[currentIndex] = array[randomIndex];\n        array[randomIndex] = temporaryValue;\n    }\n    return array;\n}\n\nasync function showRandomAnnouncement(groupId, timeInterval) {\n    const announceFastAPI = document.getElementById(groupId);\n    if (announceFastAPI) {\n        let children = [].slice.call(announceFastAPI.children);\n        children = shuffle(children)\n        let index = 0\n        const announceRandom = () => {\n            children.forEach((el, i) => { el.style.display = \"none\" });\n            children[index].style.display = \"block\"\n            index = (index + 1) % children.length\n        }\n        announceRandom()\n        setInterval(announceRandom, timeInterval\n        )\n    }\n}\n\nasync function main() {\n    setupTermynal();\n    showRandomAnnouncement('announce-left', 5000)\n    showRandomAnnouncement('announce-right', 10000)\n}\ndocument$.subscribe(() => {\n    main()\n})"
  },
  {
    "path": "docs/resources/scripts/termynal.js",
    "content": "/**\n * termynal.js\n * A lightweight, modern and extensible animated terminal window, using\n * async/await.\n *\n * @author Ines Montani <ines@ines.io>\n * @version 0.0.1\n * @license MIT\n */\n\n'use strict';\n\n/** Generate a terminal widget. */\nclass Termynal {\n    /**\n     * Construct the widget's settings.\n     * @param {(string|Node)=} container - Query selector or container element.\n     * @param {Object=} options - Custom settings.\n     * @param {string} options.prefix - Prefix to use for data attributes.\n     * @param {number} options.startDelay - Delay before animation, in ms.\n     * @param {number} options.typeDelay - Delay between each typed character, in ms.\n     * @param {number} options.lineDelay - Delay between each line, in ms.\n     * @param {number} options.progressLength - Number of characters displayed as progress bar.\n     * @param {string} options.progressChar – Character to use for progress bar, defaults to █.\n\t * @param {number} options.progressPercent - Max percent of progress.\n     * @param {string} options.cursor – Character to use for cursor, defaults to ▋.\n     * @param {Object[]} lineData - Dynamically loaded line data objects.\n     * @param {boolean} options.noInit - Don't initialise the animation.\n     */\n    constructor(container = '#termynal', options = {}) {\n        this.container = (typeof container === 'string') ? document.querySelector(container) : container;\n        this.pfx = `data-${options.prefix || 'ty'}`;\n        this.originalStartDelay = this.startDelay = options.startDelay\n            || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600;\n        this.originalTypeDelay = this.typeDelay = options.typeDelay\n            || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90;\n        this.originalLineDelay = this.lineDelay = options.lineDelay\n            || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500;\n        this.progressLength = options.progressLength\n            || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40;\n        this.progressChar = options.progressChar\n            || this.container.getAttribute(`${this.pfx}-progressChar`) || '█';\n\t\tthis.progressPercent = options.progressPercent\n            || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100;\n        this.cursor = options.cursor\n            || this.container.getAttribute(`${this.pfx}-cursor`) || '▋';\n        this.lineData = this.lineDataToElements(options.lineData || []);\n        this.loadLines()\n        if (!options.noInit) this.init()\n    }\n\n    loadLines() {\n        // Load all the lines and create the container so that the size is fixed\n        // Otherwise it would be changing and the user viewport would be constantly\n        // moving as she/he scrolls\n        const finish = this.generateFinish()\n        finish.style.visibility = 'hidden'\n        this.container.appendChild(finish)\n        // Appends dynamically loaded lines to existing line elements.\n        this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);\n        for (let line of this.lines) {\n            line.style.visibility = 'hidden'\n            this.container.appendChild(line)\n        }\n        const restart = this.generateRestart()\n        restart.style.visibility = 'hidden'\n        this.container.appendChild(restart)\n        this.container.setAttribute('data-termynal', '');\n    }\n\n    /**\n     * Initialise the widget, get lines, clear container and start animation.\n     */\n    init() {\n        /**\n         * Calculates width and height of Termynal container.\n         * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.\n         */\n        const containerStyle = getComputedStyle(this.container);\n        this.container.style.width = containerStyle.width !== '0px' ?\n            containerStyle.width : undefined;\n        this.container.style.minHeight = containerStyle.height !== '0px' ?\n            containerStyle.height : undefined;\n\n        this.container.setAttribute('data-termynal', '');\n        this.container.innerHTML = '';\n        for (let line of this.lines) {\n            line.style.visibility = 'visible'\n        }\n        this.start();\n    }\n\n    /**\n     * Start the animation and rener the lines depending on their data attributes.\n     */\n    async start() {\n        this.addFinish()\n        await this._wait(this.startDelay);\n\n        for (let line of this.lines) {\n            const type = line.getAttribute(this.pfx);\n            const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay;\n\n            if (type == 'input') {\n                line.setAttribute(`${this.pfx}-cursor`, this.cursor);\n                await this.type(line);\n                await this._wait(delay);\n            }\n\n            else if (type == 'progress') {\n                await this.progress(line);\n                await this._wait(delay);\n            }\n\n            else {\n                this.container.appendChild(line);\n                await this._wait(delay);\n            }\n\n            line.removeAttribute(`${this.pfx}-cursor`);\n        }\n        this.addRestart()\n        this.finishElement.style.visibility = 'hidden'\n        this.lineDelay = this.originalLineDelay\n        this.typeDelay = this.originalTypeDelay\n        this.startDelay = this.originalStartDelay\n    }\n\n    generateRestart() {\n        const restart = document.createElement('a')\n        restart.onclick = (e) => {\n            e.preventDefault()\n            this.container.innerHTML = ''\n            this.init()\n        }\n        restart.href = '#'\n        restart.setAttribute('data-terminal-control', '')\n        restart.innerHTML = \"restart ↻\"\n        return restart\n    }\n\n    generateFinish() {\n        const finish = document.createElement('a')\n        finish.onclick = (e) => {\n            e.preventDefault()\n            this.lineDelay = 0\n            this.typeDelay = 0\n            this.startDelay = 0\n        }\n        finish.href = '#'\n        finish.setAttribute('data-terminal-control', '')\n        finish.innerHTML = \"fast →\"\n        this.finishElement = finish\n        return finish\n    }\n\n    addRestart() {\n        const restart = this.generateRestart()\n        this.container.appendChild(restart)\n    }\n\n    addFinish() {\n        const finish = this.generateFinish()\n        this.container.appendChild(finish)\n    }\n\n    /**\n     * Animate a typed line.\n     * @param {Node} line - The line element to render.\n     */\n    async type(line) {\n        const chars = [...line.textContent];\n        line.textContent = '';\n        this.container.appendChild(line);\n\n        for (let char of chars) {\n            const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay;\n            await this._wait(delay);\n            line.textContent += char;\n        }\n    }\n\n    /**\n     * Animate a progress bar.\n     * @param {Node} line - The line element to render.\n     */\n    async progress(line) {\n        const progressLength = line.getAttribute(`${this.pfx}-progressLength`)\n            || this.progressLength;\n        const progressChar = line.getAttribute(`${this.pfx}-progressChar`)\n            || this.progressChar;\n        const chars = progressChar.repeat(progressLength);\n\t\tconst progressPercent = line.getAttribute(`${this.pfx}-progressPercent`)\n\t\t\t|| this.progressPercent;\n        line.textContent = '';\n        this.container.appendChild(line);\n\n        for (let i = 1; i < chars.length + 1; i++) {\n            await this._wait(this.typeDelay);\n            const percent = Math.round(i / chars.length * 100);\n            line.textContent = `${chars.slice(0, i)} ${percent}%`;\n\t\t\tif (percent>progressPercent) {\n\t\t\t\tbreak;\n\t\t\t}\n        }\n    }\n\n    /**\n     * Helper function for animation delays, called with `await`.\n     * @param {number} time - Timeout, in ms.\n     */\n    _wait(time) {\n        return new Promise(resolve => setTimeout(resolve, time));\n    }\n\n    /**\n     * Converts line data objects into line elements.\n     *\n     * @param {Object[]} lineData - Dynamically loaded lines.\n     * @param {Object} line - Line data object.\n     * @returns {Element[]} - Array of line elements.\n     */\n    lineDataToElements(lineData) {\n        return lineData.map(line => {\n            let div = document.createElement('div');\n            div.innerHTML = `<span ${this._attributes(line)}>${line.value || ''}</span>`;\n\n            return div.firstElementChild;\n        });\n    }\n\n    /**\n     * Helper function for generating attributes string.\n     *\n     * @param {Object} line - Line data object.\n     * @returns {string} - String of attributes.\n     */\n    _attributes(line) {\n        let attrs = '';\n        for (let prop in line) {\n            // Custom add class\n            if (prop === 'class') {\n                attrs += ` class=${line[prop]} `\n                continue\n            }\n            if (prop === 'type') {\n                attrs += `${this.pfx}=\"${line[prop]}\" `\n            } else if (prop !== 'value') {\n                attrs += `${this.pfx}-${prop}=\"${line[prop]}\" `\n            }\n        }\n\n        return attrs;\n    }\n}\n\n/**\n* HTML API: If current script has container(s) specified, initialise Termynal.\n*/\nif (document.currentScript.hasAttribute('data-termynal-container')) {\n    const containers = document.currentScript.getAttribute('data-termynal-container');\n    containers.split('|')\n        .forEach(container => new Termynal(container))\n}"
  },
  {
    "path": "docs/resources/stylesheets/extra.css",
    "content": ".termynal-comment {\n  color: #4a968f;\n  font-style: italic;\n  display: block;\n}\n\n.termy {\n  /* For right to left languages */\n  direction: ltr;\n}\n\n.termy [data-termynal] {\n  white-space: pre-wrap;\n}\n\n.termy .linenos {\n  display: none;\n}\n\n.label-class {\n  background-color: #1e88e5;\n  color: white;\n  padding: 2px 6px;\n  font-size: 0.75em;\n  border-radius: 4px;\n  font-family: monospace;\n}\n\n.label-attr {\n  background-color: #fb8c00;\n  color: white;\n  padding: 2px 6px;\n  font-size: 0.75em;\n  border-radius: 4px;\n  font-family: monospace;\n}\n\n.label-meth {\n  background-color: #43a047;\n  color: white;\n  padding: 2px 6px;\n  font-size: 0.75em;\n  border-radius: 4px;\n  font-family: monospace;\n}\n\n\n[data-md-color-scheme=\"default\"] {\n  --md-primary-fg-color:        #0D141C;\n  --md-primary-fg-color--light: #3a7e9d;\n  --md-primary-fg-color--dark:  #004059;\n  \n  --md-accent-fg-color: #0091d0;\n  --md-accent-bg-color: rgba(0, 145, 208, 0.1);\n  \n  /* Background color personalizado */\n  --md-default-bg-color: #E2ECED;\n}\n\n[data-md-color-scheme=\"slate\"] {\n  --md-primary-fg-color:        #2b1d43;\n  --md-primary-fg-color--light: #b4b7bc;\n  --md-primary-fg-color--dark:  #2b1d43;\n\n  --md-accent-fg-color: #8caabf;\n  --md-accent-bg-color: rgba(140, 170, 191, 0.1);\n  \n  --md-default-bg-color: #0D141C;\n  --md-default-fg-color: #ffffff;\n}\n\n\n[data-md-color-scheme=\"slate\"] .md-content h3 a,\n[data-md-color-scheme=\"slate\"] .md-content h2 a,\n[data-md-color-scheme=\"slate\"] .md-content h1 a {\n  color: inherit !important;\n  text-decoration: none;\n}\n\n[data-md-color-scheme=\"slate\"] .md-content h3 a:hover,\n[data-md-color-scheme=\"slate\"] .md-content h2 a:hover,\n[data-md-color-scheme=\"slate\"] .md-content h1 a:hover {\n  text-decoration: underline;\n  opacity: 0.8;\n}\n\n/* Corrigir links dentro de cabeçalhos no modo claro */\n[data-md-color-scheme=\"default\"] .md-content h3 a,\n[data-md-color-scheme=\"default\"] .md-content h2 a,\n[data-md-color-scheme=\"default\"] .md-content h1 a {\n  color: inherit !important; /* Herdar a cor do cabeçalho pai */\n  text-decoration: none;\n}\n\n[data-md-color-scheme=\"default\"] .md-content h3 a:hover,\n[data-md-color-scheme=\"default\"] .md-content h2 a:hover,\n[data-md-color-scheme=\"default\"] .md-content h1 a:hover {\n  text-decoration: underline;\n  opacity: 0.8;\n}\n\n/* Estilo básico para links ativos - modo claro */\n.md-nav__link--active {\n  font-weight: bold;\n  color: var(--md-accent-fg-color);\n}\n\n/* Sobrescrever cor apenas para o modo escuro */\n[data-md-color-scheme=\"slate\"] .md-nav__link--active {\n  color: #b4c0dd; /* Cor clara para contraste no modo escuro */\n}\n\n/* Logo personalizado */\n.md-header__button.md-logo img,\n.md-header__button.md-logo svg {\n  display: none;\n}\n\n.md-header__button.md-logo {\n  background-image: url('../images/logo.png');\n  background-size: contain;\n  background-repeat: no-repeat;\n  background-position: center;\n  width: 100px;\n  height: 50px;\n}\n\n.md-header__button.md-logo:before {\n  content: '';\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n\n/* Ocultar o nome do site no cabeçalho */\n.md-header__topic {\n  display: none;\n}\n\n/* Logo automático baseado no tema para a página index */\n/* Ocultar todas as imagens de logo por padrão */\n.md-content img[alt=\"Pydoll Logo\"] {\n  display: none;\n}\n\n/* Modo claro - mostrar logo roxo */\n[data-md-color-scheme=\"default\"] .md-content img[alt=\"Pydoll Logo\"] {\n  display: block;\n  content: url('../images/logo-black.png');\n}\n\n/* Modo escuro - mostrar logo cinza */\n[data-md-color-scheme=\"slate\"] .md-content img[alt=\"Pydoll Logo\"] {\n  display: block;\n  content: url('../images/logo.png');\n}\n\n/* ===== SPONSORS GRID ===== */\n\n.md-typeset .sponsors-grid {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  gap: 24px;\n  margin: 16px 0;\n}\n\n.md-typeset .sponsors-grid img {\n  max-width: none !important;\n  height: auto !important;\n}\n\n.md-typeset .sponsors-grid img[alt=\"Thordata\"] {\n  height: 40px !important;\n}\n\n.md-typeset .sponsors-grid img[alt=\"LambdaTest\"] {\n  height: 40px !important;\n  width: 160px !important;\n}\n\n.md-typeset .sponsors-grid img[alt=\"CapSolver\"] {\n  height: 60px !important;\n}\n\n/* ===== MELHORIAS DE LINKS PARA MODO ESCURO ===== */\n\n/* Links gerais no conteúdo - modo escuro */\n[data-md-color-scheme=\"slate\"] .md-content a {\n  color: #64b5f6 !important; /* Azul claro para boa visibilidade */\n  text-decoration: none;\n}\n\n[data-md-color-scheme=\"slate\"] .md-content a:hover {\n  color: #90caf9 !important; /* Azul mais claro no hover */\n  text-decoration: underline;\n}\n\n/* Links na navegação lateral - modo escuro */\n[data-md-color-scheme=\"slate\"] .md-nav__link {\n  color: #e0e0e0 !important; /* Cinza claro para links normais */\n}\n\n[data-md-color-scheme=\"slate\"] .md-nav__link:hover {\n  color: #ffffff !important; /* Branco no hover */\n}\n\n[data-md-color-scheme=\"slate\"] .md-nav__link--active {\n  color: #90caf9 !important; /* Verde claro para link ativo */\n  font-weight: bold;\n}\n\n/* Links em tabelas - modo escuro */\n[data-md-color-scheme=\"slate\"] .md-typeset table a {\n  color: #64b5f6 !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset table a:hover {\n  color: #90caf9 !important;\n}\n\n/* Links em listas - modo escuro */\n[data-md-color-scheme=\"slate\"] .md-typeset ul a,\n[data-md-color-scheme=\"slate\"] .md-typeset ol a {\n  color: #64b5f6 !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset ul a:hover,\n[data-md-color-scheme=\"slate\"] .md-typeset ol a:hover {\n  color: #90caf9 !important;\n}\n\n/* Links em admonitions (caixas de aviso) - modo escuro */\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition a {\n  color: #64b5f6 !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition a:hover {\n  color: #90caf9 !important;\n}\n\n/* ===== MELHORIAS DE LINKS PARA MODO CLARO ===== */\n\n/* Links gerais no conteúdo - modo claro */\n[data-md-color-scheme=\"default\"] .md-content a {\n  color: #1976d2 !important; /* Azul escuro para boa visibilidade */\n  text-decoration: none;\n}\n\n[data-md-color-scheme=\"default\"] .md-content a:hover {\n  color: #1565c0 !important; /* Azul mais escuro no hover */\n  text-decoration: underline;\n}\n\n/* Links na navegação lateral - modo claro */\n[data-md-color-scheme=\"default\"] .md-nav__link {\n  color: #424242 !important; /* Cinza escuro para links normais */\n}\n\n[data-md-color-scheme=\"default\"] .md-nav__link:hover {\n  color: #1976d2 !important; /* Azul no hover */\n}\n\n[data-md-color-scheme=\"default\"] .md-nav__link--active {\n  color: #2e7d32 !important; /* Verde escuro para link ativo */\n  font-weight: bold;\n}\n\n/* Links em tabelas - modo claro */\n[data-md-color-scheme=\"default\"] .md-typeset table a {\n  color: #1976d2 !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-typeset table a:hover {\n  color: #1565c0 !important;\n}\n\n"
  },
  {
    "path": "docs/resources/stylesheets/termynal.css",
    "content": "/**\n * termynal.js\n *\n * @author Ines Montani <ines@ines.io>\n * @version 0.0.1\n * @license MIT\n */\n\n :root {\n    --color-bg: #252a33;\n    --color-text: #eee;\n    --color-text-subtle: #a2a2a2;\n}\n\n[data-termynal] {\n    width: 750px;\n    max-width: 100%;\n    background: var(--color-bg);\n    color: var(--color-text);\n    /* font-size: 18px; */\n    font-size: 15px;\n    /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */\n    font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;\n    border-radius: 4px;\n    padding: 75px 45px 35px;\n    position: relative;\n    -webkit-box-sizing: border-box;\n            box-sizing: border-box;\n    /* Custom line-height */\n    line-height: 1.2;\n}\n\n[data-termynal]:before {\n    content: '';\n    position: absolute;\n    top: 15px;\n    left: 15px;\n    display: inline-block;\n    width: 15px;\n    height: 15px;\n    border-radius: 50%;\n    /* A little hack to display the window buttons in one pseudo element. */\n    background: #d9515d;\n    -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;\n            box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;\n}\n\n[data-termynal]:after {\n    content: 'bash';\n    position: absolute;\n    color: var(--color-text-subtle);\n    top: 5px;\n    left: 0;\n    width: 100%;\n    text-align: center;\n}\n\na[data-terminal-control] {\n    text-align: right;\n    display: block;\n    color: #aebbff;\n}\n\n[data-ty] {\n    display: block;\n    line-height: 2;\n}\n\n[data-ty]:before {\n    /* Set up defaults and ensure empty lines are displayed. */\n    content: '';\n    display: inline-block;\n    vertical-align: middle;\n}\n\n[data-ty=\"input\"]:before,\n[data-ty-prompt]:before {\n    margin-right: 0.75em;\n    color: var(--color-text-subtle);\n}\n\n[data-ty=\"input\"]:before {\n    content: '$';\n}\n\n[data-ty][data-ty-prompt]:before {\n    content: attr(data-ty-prompt);\n}\n\n[data-ty-cursor]:after {\n    content: attr(data-ty-cursor);\n    font-family: monospace;\n    margin-left: 0.5em;\n    -webkit-animation: blink 1s infinite;\n            animation: blink 1s infinite;\n}\n\n\n/* Cursor animation */\n\n@-webkit-keyframes blink {\n    50% {\n        opacity: 0;\n    }\n}\n\n@keyframes blink {\n    50% {\n        opacity: 0;\n    }\n}"
  },
  {
    "path": "docs/zh/api/browser/chrome.md",
    "content": "# Chrome Browser\n \n::: pydoll.browser.chromium.Chrome\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2 "
  },
  {
    "path": "docs/zh/api/browser/edge.md",
    "content": "# Edge Browser\n \n::: pydoll.browser.chromium.Edge\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2 "
  },
  {
    "path": "docs/zh/api/browser/managers.md",
    "content": "# 浏览器管理器\n\n管理器模块提供专门的类来管理浏览器生命周期和配置。\n\n## 总览\n\nBrowser managers handle specific responsibilities in browser automation:\n\n::: pydoll.browser.managers\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## 管理器类\n\n### 浏览器进程管理器\n管理浏览器进程的生命周期，包括启动、停止和监控浏览器进程。\n\n::: pydoll.browser.managers.browser_process_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n### 浏览器选项管理器\n处理浏览器配置选项和命令行参数。\n\n::: pydoll.browser.managers.browser_options_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n### 代理管理器\n管理浏览器实例的代理配置和身份验证。\n\n::: pydoll.browser.managers.proxy_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n### 临时目录管理器\n处理浏览器实例使用的临时目录的创建和清理。\n\n::: pydoll.browser.managers.temp_dir_manager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## 用法\n管理器通常由 Chrome 和 Edge 等浏览器类内部使用。它们提供可组合的模块化功能：\n\n```python\nfrom pydoll.browser.managers.proxy_manager import ProxyManager\nfrom pydoll.browser.managers.temp_dir_manager import TempDirManager\n\n# Managers are used internally by browser classes\n# Direct usage is for advanced scenarios only\nproxy_manager = ProxyManager()\ntemp_manager = TempDirManager()\n```\n\n!!! note \"Internal Usage\"\n    These managers are primarily used internally by the browser classes. Direct usage is recommended only for advanced scenarios or when extending the library. "
  },
  {
    "path": "docs/zh/api/browser/options.md",
    "content": "# Browser Options\n\n## ChromiumOptions\n\n::: pydoll.browser.options.ChromiumOptions\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## Options Interface\n\n::: pydoll.browser.interfaces.Options\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## BrowserOptionsManager Interface\n\n::: pydoll.browser.interfaces.BrowserOptionsManager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3 "
  },
  {
    "path": "docs/zh/api/browser/requests.md",
    "content": "# 浏览器请求\n\n请求模块在浏览器上下文中提供 HTTP 请求功能，支持继承浏览器会话状态、cookies 和身份验证的无缝 API 调用。\n\n## 概述\n\n浏览器请求模块为在浏览器 JavaScript 上下文中直接进行 HTTP 调用提供了类似 `requests` 的接口。这种方法相比传统 HTTP 库提供了几个优势：\n\n- **会话继承**: 自动处理 cookie、身份验证和 CORS\n- **浏览器上下文**: 请求在与页面相同的安全上下文中执行\n- **无需会话管理**: 消除在自动化和 API 调用之间传输 cookies 和令牌的需要\n- **SPA 兼容性**: 完美适配具有复杂身份验证流程的单页应用\n\n## Request 类\n\n在浏览器上下文中进行 HTTP 请求的主要接口。\n\n::: pydoll.browser.requests.request.Request\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n      group_by_category: true\n      members_order: source\n      filters:\n        - \"!^__\"\n\n## Response 类\n\n表示 HTTP 请求的响应，提供类似于 `requests` 库的熟悉接口。\n\n::: pydoll.browser.requests.response.Response\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n      group_by_category: true\n      members_order: source\n      filters:\n        - \"!^__\"\n\n## 使用示例\n\n### 基本 HTTP 方法\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to(\"https://api.example.com\")\n    \n    # GET 请求\n    response = await tab.request.get(\"/users/123\")\n    user_data = await response.json()\n    \n    # POST 请求\n    response = await tab.request.post(\"/users\", json={\n        \"name\": \"John Doe\",\n        \"email\": \"john@example.com\"\n    })\n    \n    # 带 headers 的 PUT 请求\n    response = await tab.request.put(\"/users/123\", \n        json={\"name\": \"Jane Doe\"},\n        headers={\"Authorization\": \"Bearer token123\"}\n    )\n```\n\n### 响应处理\n\n```python\n# 检查响应状态\nif response.ok:\n    print(f\"成功: {response.status_code}\")\nelse:\n    print(f\"错误: {response.status_code}\")\n    response.raise_for_status()  # 对 4xx/5xx 抛出 HTTPError\n\n# 访问响应数据\ntext_data = response.text\njson_data = await response.json()\nraw_bytes = response.content\n\n# 检查 headers 和 cookies\nprint(\"响应 headers:\", response.headers)\nprint(\"请求 headers:\", response.request_headers)\nfor cookie in response.cookies:\n    print(f\"Cookie: {cookie.name}={cookie.value}\")\n```\n\n### 高级功能\n\n```python\n# 带自定义 headers 和参数的请求\nresponse = await tab.request.get(\"/search\", \n    params={\"q\": \"python\", \"limit\": 10},\n    headers={\n        \"User-Agent\": \"Custom Bot 1.0\",\n        \"Accept\": \"application/json\"\n    }\n)\n\n# 文件上传模拟\nresponse = await tab.request.post(\"/upload\",\n    data={\"description\": \"Test file\"},\n    files={\"file\": (\"test.txt\", \"file content\", \"text/plain\")}\n)\n\n# 表单数据提交\nresponse = await tab.request.post(\"/login\",\n    data={\"username\": \"user\", \"password\": \"pass\"}\n)\n```\n\n## 与 Tab 的集成\n\n请求功能通过 `tab.request` 属性访问，该属性为每个 tab 提供一个单例 `Request` 实例：\n\n```python\n# 每个 tab 都有自己的 request 实例\ntab1 = await browser.get_tab(0)\ntab2 = await browser.new_tab()\n\n# 这些是独立的 Request 实例\nrequest1 = tab1.request  # 绑定到 tab1 的 Request\nrequest2 = tab2.request  # 绑定到 tab2 的 Request\n\n# 请求继承 tab 的上下文\nawait tab1.go_to(\"https://site1.com\")\nawait tab2.go_to(\"https://site2.com\")\n\n# 这些请求将具有不同的 cookie/会话上下文\nresponse1 = await tab1.request.get(\"/api/data\")  # 使用 site1.com 的 cookies\nresponse2 = await tab2.request.get(\"/api/data\")  # 使用 site2.com 的 cookies\n```\n\n!!! tip \"混合自动化\"\n    该模块对于需要结合 UI 交互和 API 调用的混合自动化场景特别强大。例如，通过 UI 登录，然后使用已认证的会话进行 API 调用，无需手动处理 cookies 或令牌。"
  },
  {
    "path": "docs/zh/api/browser/tab.md",
    "content": "# Tab\n\n::: pydoll.browser.tab.Tab\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/zh/api/commands/browser.md",
    "content": "# 浏览器命令\n\n浏览器命令提供对浏览器实例及其配置的底层控制。\n\n## 概述\n\n浏览器命令模块处理浏览器级别的操作，例如版本信息、目标管理和浏览器范围的设置。\n\n::: pydoll.commands.browser_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## 用法\n\n浏览器命令通常由浏览器类在内部使用，用于管理浏览器实例：\n\n```python\nfrom pydoll.commands.browser_commands import get_version\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Get browser version information\nconnection = ConnectionHandler()\nversion_info = await get_version(connection)\n```\n\n## 可用命令\n\n浏览器命令模块提供以下功能：\n\n- 获取浏览器版本和用户代理信息\n- 管理浏览器目标（标签页、窗口）\n- 控制浏览器范围的设置和权限\n- 处理浏览器生命周期事件\n\n!!! note \"Internal Usage\"\n    These commands are primarily used internally by the `Chrome` and `Edge` browser classes. Direct usage is recommended only for advanced scenarios. "
  },
  {
    "path": "docs/zh/api/commands/dom.md",
    "content": "# DOM命令\n\nDOM 命令模块提供了与网页文档对象模型交互的全面功能。\n\n## 概述\n\nDOM 命令模块是 Pydoll 中最重要的模块之一，它提供了查找、交互和操作网页上的 HTML 元素所需的所有功能。\n\n::: pydoll.commands.dom_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## Usage\n\nDOM commands are used extensively by the `WebElement` class and element finding methods:\n\n## 用法\n\n`WebElement` 类和元素查找方法广泛使用DOM 命令：\n\n```python\nfrom pydoll.commands.dom_commands import query_selector, get_attributes\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Find element and get its attributes\nconnection = ConnectionHandler()\nnode_id = await query_selector(connection, selector=\"#username\")\nattributes = await get_attributes(connection, node_id=node_id)\n```\n\n## 主要功能\n\nDOM 命令模块提供以下功能：\n\n### 元素定位\n- `query_selector()` - 通过CSS选择器进行元素定位\n- `query_selector_all()` - 通过CSS选择器进行元素定位（查找多个元素）\n- `get_document()` - 获取document的根节点\n\n### 元素交互\n- `click_element()` - 点击元素\n- `focus_element()` - 焦点置于元素\n- `set_attribute_value()` - 设置元素属性\n- `get_attributes()` - 获取元素属性\n\n### 元素信息\n- `get_box_model()` - 获取元素位置和尺寸\n- `describe_node()` - 获取元素详细信息\n- `get_outer_html()` - 获取元素的HTML内容\n\n### DOM 操作\n- `remove_node()` - 从DOM节点中删除元素\n- `set_node_value()` - 设置元素值\n- `request_child_nodes()` - 获取子元素\n\n!!! tip \"High-Level APIs\"\n    While these commands provide powerful low-level access, most users should use the higher-level `WebElement` class methods like `click()`, `type_text()`, and `get_attribute()` which use these commands internally. "
  },
  {
    "path": "docs/zh/api/commands/fetch.md",
    "content": "# Fetch 命令\n\nFetch 命令使用 Fetch API 域提供高级网络请求处理和拦截功能。\n\n## 概述\n\nFetch 命令模块支持复杂的网络请求管理，包括请求修改、响应拦截和身份验证处理。\n\n::: pydoll.commands.fetch_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## 用法\n\nFetch 命令用于高级网络拦截和请求处理：\n\n```python\nfrom pydoll.commands.fetch_commands import enable, request_paused, continue_request\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Enable fetch domain\nconnection = ConnectionHandler()\nawait enable(connection, patterns=[{\n    \"urlPattern\": \"*\",\n    \"requestStage\": \"Request\"\n}])\n\n# Handle paused requests\nasync def handle_paused_request(request_id, request):\n    # Modify request or continue as-is\n    await continue_request(connection, request_id=request_id)\n```\n\n## 关键功能\n\nfetch 命令模块提供以下功能：\n\n### 请求拦截\n- `enable()` - 激活fetch模式\n- `disable()` - 关闭fetch模式\n- `continue_request()` - 继续请求（放行）\n- `fail_request()` - 返回特定错误请求\n\n### 修改请求\n- 修改请求headers\n- 更改请求 URL\n- 更改请求方法（GET、POST 等）\n- 修改请求body\n\n### 响应处理\n- `fulfill_request()` - 提供自定义响应\n- `get_response_body()` - 获取响应内容\n- 修改响应头\n- 响应状态码控制\n\n### 身份验证\n- `continue_with_auth()` - 处理身份验证挑战\n- 基本身份验证支持\n- 自定义身份验证流程\n\n## 高级功能\n\n### 基于模式的拦截\n\n```python\n# Intercept specific URL patterns\npatterns = [\n    {\"urlPattern\": \"*/api/*\", \"requestStage\": \"Request\"},\n    {\"urlPattern\": \"*.js\", \"requestStage\": \"Response\"},\n    {\"urlPattern\": \"https://example.com/*\", \"requestStage\": \"Request\"}\n]\n\nawait enable(connection, patterns=patterns)\n```\n\n### 请求修改\n```python\n# Modify intercepted requests\nasync def modify_request(request_id, request):\n    # Add authentication header\n    headers = request.headers.copy()\n    headers[\"Authorization\"] = \"Bearer token123\"\n    \n    # Continue with modified headers\n    await continue_request(\n        connection,\n        request_id=request_id,\n        headers=headers\n    )\n```\n\n### 响应模拟\n```python\n# Mock API responses\nawait fulfill_request(\n    connection,\n    request_id=request_id,\n    response_code=200,\n    response_headers=[\n        {\"name\": \"Content-Type\", \"value\": \"application/json\"},\n        {\"name\": \"Access-Control-Allow-Origin\", \"value\": \"*\"}\n    ],\n    body='{\"status\": \"success\", \"data\": {\"mocked\": true}}'\n)\n```\n\n### 身份验证处理\n```python\n# Handle authentication challenges\nawait continue_with_auth(\n    connection,\n    request_id=request_id,\n    auth_challenge_response={\n        \"response\": \"ProvideCredentials\",\n        \"username\": \"user\",\n        \"password\": \"pass\"\n    }\n)\n```\n\n## 请求阶段\n\nFetch 命令可以在不同阶段拦截请求：\n\n| 阶段 | 描述 | 用例 |\n|-------|-------------|-----------|\n| 请求 | 请求发送前 | 修改标头、URL 和方法 |\n| 响应 | 收到响应后 | 模拟响应，修改内容 |\n\n## 错误处理\n\n```python\n# Fail requests with specific errors\nawait fail_request(\n    connection,\n    request_id=request_id,\n    error_reason=\"ConnectionRefused\"  # or \"AccessDenied\", \"TimedOut\", etc.\n)\n```\n\n## 与网络命令集成\n\nFetch 命令与网络命令协同工作，但提供更精细的控制：\n\n- **网络命令**：更广泛的网络监控和控制\n- **Fetch 命令**：特定的请求/响应拦截和修改\n\n!!! tip \"Performance Considerations\"\n    Fetch interception can impact page loading performance. Use specific URL patterns and disable when not needed to minimize overhead. "
  },
  {
    "path": "docs/zh/api/commands/index.md",
    "content": "# 命令概述\n\n命令模块提供了与Chrome DevTools协议(CDP)域交互的高级接口。每个命令模块对应一个特定的CDP域，并提供执行各种浏览器操作的方法。\n\n## 可用命令模块\n\n### 浏览器命令\n- **模块**: `browser_commands.py`\n- **用途**: 浏览器级别操作和窗口管理\n- **文档**: [浏览器命令](browser.md)\n\n### DOM命令\n- **模块**: `dom_commands.py`\n- **用途**: DOM树操作和元素操作\n- **文档**: [DOM命令](dom.md)\n\n### 输入命令\n- **模块**: `input_commands.py`\n- **用途**: 输入事件模拟(键盘、鼠标、触摸)\n- **文档**: [输入命令](input.md)\n\n### 网络命令\n- **模块**: `network_commands.py`\n- **用途**: 网络监控和请求拦截\n- **文档**: [网络命令](network.md)\n\n### 页面命令\n- **模块**: `page_commands.py`\n- **用途**: 页面生命周期管理和导航\n- **文档**: [页面命令](page.md)\n\n### 运行时命令\n- **模块**: `runtime_commands.py`\n- **用途**: JavaScript执行和运行时管理\n- **文档**: [运行时命令](runtime.md)\n\n### 存储命令\n- **模块**: `storage_commands.py`\n- **用途**: 浏览器存储访问(cookies、本地存储等)\n- **文档**: [存储命令](storage.md)\n\n### 目标命令\n- **模块**: `target_commands.py`\n- **用途**: 目标管理和标签页操作\n- **文档**: [目标命令](target.md)\n\n### 获取命令\n- **模块**: `fetch_commands.py`\n- **用途**: 网络请求拦截和修改\n- **文档**: [获取命令](fetch.md)\n\n## 使用模式\n\n命令通常通过浏览器或标签页实例访问：\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\n# 初始化浏览器\nbrowser = Chrome()\nawait browser.start()\n\n# 获取活动标签页\ntab = await browser.get_active_tab()\n\n# 通过标签页使用命令\nawait tab.navigate(\"https://example.com\")\nelement = await tab.find(id=\"button\")\nawait element.click()\n```\n\n## 命令结构\n\n每个命令模块遵循一致的模式：\n- **静态方法**: 用于直接命令执行\n- **类型提示**: 使用协议类型的完整类型安全\n- **错误处理**: 对CDP错误的正确异常处理\n- **文档**: 包含示例的全面文档字符串 "
  },
  {
    "path": "docs/zh/api/commands/input.md",
    "content": "# 输入命令\n\n输入命令处理鼠标和键盘交互，提供真人仿真的输入模拟。\n\n## 概述\n\n输入命令模块提供模拟用户输入的功能，包括鼠标移动、点击、键盘输入和按键操作。\n\n::: pydoll.commands.input_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## 用法\n\n输入命令由元素交互方法使用，可直接用于高级输入场景：\n\n```python\nfrom pydoll.commands.input_commands import dispatch_mouse_event, dispatch_key_event\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Simulate mouse click\nconnection = ConnectionHandler()\nawait dispatch_mouse_event(\n    connection, \n    type=\"mousePressed\", \n    x=100, \n    y=200, \n    button=\"left\"\n)\n\n# Simulate keyboard typing\nawait dispatch_key_event(\n    connection,\n    type=\"keyDown\",\n    key=\"Enter\"\n)\n```\n\n## 主要功能\n\n输入命令模块提供以下函数：\n\n### 鼠标事件\n- `dispatch_mouse_event()` - 鼠标点击、移动和滚轮事件\n- 鼠标按键状态（左键、右键、中键）\n- 基于坐标的定位\n- 拖放操作\n\n\n### 键盘事件\n- `dispatch_key_event()` - 键盘按下和释放事件\n- `insert_text()` - 直接插入文本\n- 特殊键处理（Enter、Tab、箭头键等）\n- 修饰键（Ctrl、Alt、Shift）\n\n\n### 触摸事件\n- 触摸屏模拟\n- 多点触控手势\n- 触摸坐标和压力控制\n\n## 仿真行为\n\n输入命令支持仿真行为模式：\n\n- 平滑的鼠标移动曲线\n- 真实的打字速度和模式\n- 操作之间随机的微延迟\n- 压力感应触摸事件\n\n!!! tip \"Element Methods\"\n    For most use cases, use the higher-level element methods like `element.click()` and `element.type_text()` which provide a more convenient API and handle common scenarios automatically. "
  },
  {
    "path": "docs/zh/api/commands/network.md",
    "content": "# 网络命令\n\n网络命令提供对网络请求、响应和浏览器网络行为的全面控制。\n\n## 概述\n\n网络命令模块支持请求拦截、响应修改、Cookie 管理和网络监控功能。\n\n::: pydoll.commands.network_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## 用法\n\n网络命令用于请求拦截和网络监控等高级场景：\n\n```python\nfrom pydoll.commands.network_commands import enable, set_request_interception\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Enable network monitoring\nconnection = ConnectionHandler()\nawait enable(connection)\n\n# Enable request interception\nawait set_request_interception(connection, patterns=[{\"urlPattern\": \"*\"}])\n```\n\n## 主要功能\n\n网络命令模块提供以下功能：\n\n\n### 请求管理\n- `enable()` / `disable()` - 启用/禁用网络监控\n- `set_request_interception()` - 拦截并修改请求\n- `continue_intercepted_request()` - 继续或修改拦截的请求\n- `get_request_post_data()` - 获取请求体数据\n\n\n### 响应处理\n- `get_response_body()` - 获取响应内容\n- `fulfill_request()` - 提供自定义响应\n- `fail_request()` - 模拟网络异常\n\n### Cookie 管理\n- `get_cookies()` - 获取浏览器 Cookie\n- `set_cookies()` - 设置浏览器 Cookie\n- `delete_cookies()` - 删除指定 Cookie\n- `clear_browser_cookies()` - 清除所有 Cookie\n\n### 缓存控制\n- `clear_browser_cache()` - 清除浏览器缓存\n- `set_cache_disabled()` - 禁用浏览器缓存\n- `get_response_body_for_interception()` - 获取缓存的响应\n\n### 安全和标头\n- `set_user_agent_override()` - 覆盖用户代理\n- `set_extra_http_headers()` - 添加自定义标头\n- `emulate_network_conditions()` - 模拟网络连接状况\n\n## 高级用例\n\n### 请求拦截\n\n```python\n# 拦截修改请求\nawait set_request_interception(connection, patterns=[\n    {\"urlPattern\": \"*/api/*\", \"requestStage\": \"Request\"}\n])\n\n# 拦截请求处理\nasync def handle_request(request):\n    if \"api/login\" in request.url:\n        # 修改请求头\n        headers = request.headers.copy()\n        headers[\"Authorization\"] = \"Bearer token\"\n        await continue_intercepted_request(\n            connection, \n            request_id=request.request_id,\n            headers=headers\n        )\n```\n\n### 响应模拟\n```python\n# 模拟 API 响应\nawait fulfill_request(\n    connection,\n    request_id=request_id,\n    response_code=200,\n    response_headers={\"Content-Type\": \"application/json\"},\n    body='{\"status\": \"success\"}'\n)\n```\n\n!!! warning \"Performance Impact\"\n    Network interception can impact page loading performance. Use selectively and disable when not needed. "
  },
  {
    "path": "docs/zh/api/commands/page.md",
    "content": "# 页面命令\n\n页面命令处理页面导航、生命周期事件和页面操作。\n\n## 概述\n\n页面命令模块提供页面间导航、管理页面生命周期、处理 JavaScript 执行以及控制页面行为的功能。\n\n::: pydoll.commands.page_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## 用法\n\n“Tab”类广泛使用页面命令进行导航和页面管理：\n\n```python\nfrom pydoll.commands.page_commands import navigate, reload, enable\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Navigate to a URL\nconnection = ConnectionHandler()\nawait enable(connection)  # Enable page events\nawait navigate(connection, url=\"https://example.com\")\n\n# Reload the page\nawait reload(connection)\n```\n\n## 关键功能\n\n页面命令模块提供以下函数：\n\n### 导航\n- `navigate()` - 访问URL\n- `reload()` - 重新加载当前页面\n- `go_back()` - 后退一步\n- `go_forward()` - 前进一步\n- `stop_loading()` - 停止页面加载\n\n### 页面生命周期\n- `enable()` / `disable()` - 启用/禁用页面事件\n- `get_frame_tree()` - 获取页面框架结构\n- `get_navigation_history()` - 获取导航历史记录\n\n### 内容管理\n- `get_resource_content()` - 获取页面资源内容\n- `search_in_resource()` - 在页面资源内搜索\n- `set_document_content()` - 设置页面 HTML 内容\n\n### 截图和 PDF\n- `capture_screenshot()` - 页面截图\n- `print_to_pdf()` - 将页面保存为PDF\n- `capture_snapshot()` - 页面快照\n\n### JavaScript 执行\n- `add_script_to_evaluate_on_new_document()` - 添加启动脚本(在网页加载前注入js)\n- `remove_script_to_evaluate_on_new_document()` - 移除启动脚本\n\n### 页面设置\n- `set_lifecycle_events_enabled()` - 控制生命周期事件\n- `set_ad_blocking_enabled()` - 启用/禁用广告拦截\n- `set_bypass_csp()` - 绕过内容安全策略\n\n## 高级功能\n### 框架管理\n\n```python\n# Get all frames in the page\nframe_tree = await get_frame_tree(connection)\nfor frame in frame_tree.child_frames:\n    print(f\"Frame: {frame.frame.url}\")\n```\n\n### 资源拦截\n```python\n# Get resource content\ncontent = await get_resource_content(\n    connection, \n    frame_id=frame_id, \n    url=\"https://example.com/script.js\"\n)\n```\n\n### 页面事件\n页面命令可与各种页面事件配合使用：\n- `Page.loadEventFired` - 页面加载完成\n- `Page.domContentEventFired` - DOM 内容已加载\n- `Page.frameNavigated` - 框架访问结束\n- `Page.frameStartedLoading` - 框架加载开始\n\n\n!!! 小提示“Tab 类集成”\n大多数页面操作都可以通过 `Tab` 类方法实现，例如 `tab.go_to()`、`tab.reload()` 和 `tab.screenshot()`，这些方法提供了更便捷的 API。"
  },
  {
    "path": "docs/zh/api/commands/runtime.md",
    "content": "# 运行时命令\n\n运行时命令提供 JavaScript 执行功能和运行时环境管理。\n\n## 概述\n\n运行时命令模块支持在浏览器上下文中执行 JavaScript 代码、检查对象以及控制运行时环境。\n\n::: pydoll.commands.runtime_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## 用法\n\n运行时命令用于 JavaScript 执行和运行时管理：\n\n```python\nfrom pydoll.commands.runtime_commands import evaluate, enable\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Enable runtime events\nconnection = ConnectionHandler()\nawait enable(connection)\n\n# Execute JavaScript\nresult = await evaluate(\n    connection, \n    expression=\"document.title\",\n    return_by_value=True\n)\nprint(result.value)  # Page title\n```\n\n## 主要功能\n\n运行时命令模块提供以下功能：\n\n### JavaScript 执行\n- `evaluate()` - 执行 JavaScript 表达式\n- `call_function_on()` - 调用对象上的函数\n- `compile_script()` - 编译 JavaScript 以供复用\n- `run_script()` - 运行已编译的脚本\n\n### 对象管理\n- `get_properties()` - 获取对象属性\n- `release_object()` - 释放对象引用\n- `release_object_group()` - 释放对象组\n\n### 运行时控制\n- `enable()` / `disable()` - 启用/禁用运行时事件\n- `discard_console_entries()` - 清除控制台记录\n- `set_custom_object_formatter_enabled()` - 启用自定义格式化程序\n\n### 异常处理\n- `set_async_call_stack_depth()` - 设置调用堆栈深度\n- 异常捕获和报告\n- 错误对象检查\n\n## 高级用法\n\n### 复杂的 JavaScript 执行\n\n```python\n# 执行带有错误处理的复杂 JavaScript\nscript = \"\"\"\ntry {\n    const elements = document.querySelectorAll('.item');\n    return Array.from(elements).map(el => ({\n        text: el.textContent,\n        href: el.href\n    }));\n} catch (error) {\n    return { error: error.message };\n}\n\"\"\"\n\nresult = await evaluate(\n    connection,\n    expression=script,\n    return_by_value=True,\n    await_promise=True\n)\n```\n\n### 对象检查\n```python\n# Get detailed object properties\nproperties = await get_properties(\n    connection,\n    object_id=object_id,\n    own_properties=True,\n    accessor_properties_only=False\n)\n\nfor prop in properties:\n    print(f\"{prop.name}: {prop.value}\")\n```\n\n### 控制台集成\n运行时命令与浏览器控制台集成：\n- 控制台消息和错误\n- 控制台 API 方法调用\n- 自定义控制台格式化程序\n\n!!! note \"Performance Considerations\"\n    JavaScript execution through runtime commands can be slower than native browser execution. Use judiciously for complex operations. "
  },
  {
    "path": "docs/zh/api/commands/storage.md",
    "content": "# 存储命令\n\n存储命令提供全面的浏览器存储管理，包括 Cookie、localStorage、sessionStorage 和 IndexedDB。\n\n## 概述\n\n存储命令模块支持管理所有浏览器存储机制，提供数据持久化和检索功能。\n\n::: pydoll.commands.storage_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## 用法\n\n存储命令用于跨不同机制管理浏览器存储：\n\n```python\nfrom pydoll.commands.storage_commands import get_cookies, set_cookies, clear_data_for_origin\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Get cookies for a domain\nconnection = ConnectionHandler()\ncookies = await get_cookies(connection, urls=[\"https://example.com\"])\n\n# Set a new cookie\nawait set_cookies(connection, cookies=[{\n    \"name\": \"session_id\",\n    \"value\": \"abc123\",\n    \"domain\": \"example.com\",\n    \"path\": \"/\",\n    \"httpOnly\": True,\n    \"secure\": True\n}])\n\n# Clear all storage for an origin\nawait clear_data_for_origin(\n    connection,\n    origin=\"https://example.com\",\n    storage_types=\"all\"\n)\n```\n\n## 关键功能\n\n存储命令模块提供以下函数：\n\n### Cookie 管理\n- `get_cookies()` - 通过 URL 或域名获取 Cookie\n- `set_cookies()` - 设置新 Cookie\n- `delete_cookies()` - 删除特定 Cookie\n- `clear_cookies()` - 清除所有 Cookie\n\n\n### 本地存储\n- `get_dom_storage_items()` - 获取localStorage\n- `set_dom_storage_item()` - 设置localStorage\n- `remove_dom_storage_item()` - 移除localStorage\n- `clear_dom_storage()` - 清除localStorage\n\n### 会话存储\n- 会话存储操作（类似于本地存储）\n- 特定会话的数据管理\n- 选项卡隔离存储\n\n### IndexedDB\n- `get_database_names()` - 获取 IndexedDB 数据库\n- `request_database()` - 访问数据库结构\n- `request_data()` - 查询数据库数据\n- `clear_object_store()` - 清除对象存储\n\n### 缓存存储\n- `request_cache_names()` - 获取缓存名称\n- `request_cached_response()` - 获取缓存响应\n- `delete_cache()` - 删除缓存条目\n\n### 应用程序缓存（已弃用）\n- 支持旧版应用程序缓存\n- 基于清单的缓存\n\n## 高级功能\n\n### 批量操作\n```python\n# Clear all storage types for multiple origins\norigins = [\"https://example.com\", \"https://api.example.com\"]\nfor origin in origins:\n    await clear_data_for_origin(\n        connection,\n        origin=origin,\n        storage_types=\"cookies,local_storage,session_storage,indexeddb\"\n    )\n```\n\n### 存储配额\n```python\n# Get storage quota information\nquota_info = await get_usage_and_quota(connection, origin=\"https://example.com\")\nprint(f\"Used: {quota_info.usage} bytes\")\nprint(f\"Quota: {quota_info.quota} bytes\")\n```\n\n### Cross-Origin 存储\n```python\n# Manage storage across different origins\nawait set_cookies(connection, cookies=[{\n    \"name\": \"cross_site_token\",\n    \"value\": \"token123\",\n    \"domain\": \".example.com\",  # Applies to all subdomains\n    \"sameSite\": \"None\",\n    \"secure\": True\n}])\n```\n\n## 存储类型\n\n该模块支持多种存储机制：\n\n| 存储类型 | 持久性 | 范围 | 容量 |\n|-----------|----------|----------|----------|\n| Cookies | 持久性 | 域/路径 | 每个 cookie 约 4KB |\n| localStorage | 持久性 | 来源 | 约 5-10MB |\n| sessionStorage | 会话 | Tab | 约 5-10MB |\n| IndexedDB | 持久性 | 来源 | 大容量 (GB+) |\n| Cache API | 持久性 | 来源 | 大容量 |\n\n!!! warning \"Privacy Considerations\"\n    Storage operations can affect user privacy. Always handle storage data responsibly and in compliance with privacy regulations. "
  },
  {
    "path": "docs/zh/api/commands/target.md",
    "content": "# Target命令\n\nTarget命令管理浏览器目标，包括标签页、窗口和其他浏览上下文。\n\n## 概述\n\nTarget命令模块提供创建、管理和控制浏览器目标（例如标签页、弹出窗口和服务工作线程）的功能。\n\n::: pydoll.commands.target_commands\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## 用法\n\nTarget命令由浏览器类内部使用，用于管理标签页和窗口：\n\n```python\nfrom pydoll.commands.target_commands import get_targets, create_target, close_target\nfrom pydoll.connection.connection_handler import ConnectionHandler\n\n# Get all browser targets\nconnection = ConnectionHandler()\ntargets = await get_targets(connection)\n\n# Create a new tab\nnew_target = await create_target(connection, url=\"https://example.com\")\n\n# Close a target\nawait close_target(connection, target_id=new_target.target_id)\n```\n\n## 主要功能\n\nTarget命令模块提供以下功能：\n\n\n### Target管理\n- `get_targets()` - 列出所有浏览器Target\n- `create_target()` - 创建新的标签页或窗口\n- `close_target()` - 关闭特定Target\n- `activate_target()` - 将Target置于前台\n\n### Target 信息\n- `get_target_info()` - 获取详细的Target信息\n- Target类型：页面、background_page、service_worker、浏览器\n- Target状态：已连接、已分离、崩溃\n\n### Session 管理\n- `attach_to_target()` - 附加到Target进行控制\n- `detach_from_target()` - 分离Target\n- `send_message_to_target()` - 向Target发送命令\n\n### 浏览器上下文\n- `create_browser_context()` - 创建独立的浏览器上下文\n- `dispose_browser_context()` - 移除浏览器上下文\n- `get_browser_contexts()` - 列出浏览器上下文\n\n## 目标类型\n\n可以管理不同类型的目标：\n\n### 页面 Targets\n```python\n# Create a new tab\npage_target = await create_target(\n    connection,\n    url=\"https://example.com\",\n    width=1920,\n    height=1080,\n    browser_context_id=None  # Default context\n)\n```\n\n### 弹窗\n```python\n# Create a popup window\npopup_target = await create_target(\n    connection,\n    url=\"https://popup.example.com\",\n    width=800,\n    height=600,\n    new_window=True\n)\n```\n\n### 无痕上下文\n```python\n# Create incognito browser context\nincognito_context = await create_browser_context(connection)\n\n# Create tab in incognito context\nincognito_tab = await create_target(\n    connection,\n    url=\"https://private.example.com\",\n    browser_context_id=incognito_context.browser_context_id\n)\n```\n\n!!! info \"Headless 与 Headed：上下文如何呈现\"\n    浏览器上下文是逻辑上的隔离环境。在 Headed 模式下，在新的上下文中创建的第一个页面通常会打开一个新的系统窗口。 在 Headless 模式下不会显示窗口——隔离依然存在于后台（cookies、storage、缓存与认证状态仍按上下文分离）。在 CI/Headless 环境中优先使用上下文以获得更高性能与更干净的隔离。\n\n## 高级特性\n\n### 目标事件\nTarget命令可与各种Target事件配合使用：\n- `Target.targetCreated` - 新Target创建\n- `Target.targetDestroyed` - Target关闭\n- `Target.targetInfoChanged` - Target信息更新\n- `Target.targetCrashed` - Target崩溃\n\n### 多Target协调\n\n```python\n# Manage multiple tabs\ntargets = await get_targets(connection)\npage_targets = [t for t in targets if t.type == \"page\"]\n\nfor target in page_targets:\n    # Perform operations on each tab\n    await activate_target(connection, target_id=target.target_id)\n    # ... do work in this tab\n```\n\n### Target 隔离\n```python\n# Create isolated browser context for testing\ntest_context = await create_browser_context(connection)\n\n# All targets in this context are isolated\ntest_tab1 = await create_target(\n    connection, \n    url=\"https://test1.com\",\n    browser_context_id=test_context.browser_context_id\n)\n\ntest_tab2 = await create_target(\n    connection,\n    url=\"https://test2.com\", \n    browser_context_id=test_context.browser_context_id\n)\n```\n\n!!! note \"Browser Integration\"\n    Target commands are primarily used internally by the `Chrome` and `Edge` browser classes. The high-level browser APIs provide more convenient methods for tab management. "
  },
  {
    "path": "docs/zh/api/connection/connection.md",
    "content": "# 连接处理器\n\n::: pydoll.connection.connection_handler.ConnectionHandler\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/zh/api/connection/managers.md",
    "content": "# 连接管理器\n\n## 命令管理器\n\n::: pydoll.connection.managers.commands_manager.CommandsManager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n\n## 事件管理器\n\n::: pydoll.connection.managers.events_manager.EventsManager\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3 "
  },
  {
    "path": "docs/zh/api/core/constants.md",
    "content": "# 常量\n\n本节记录了 Pydoll 中使用的所有常量、枚举和配置值。\n\n::: pydoll.constants\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      group_by_category: true\n      members_order: source "
  },
  {
    "path": "docs/zh/api/core/exceptions.md",
    "content": "# 异常\n\n本节记录了 Pydoll 操作可能引发的所有自定义异常。\n\n::: pydoll.exceptions\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      group_by_category: true\n      members_order: source "
  },
  {
    "path": "docs/zh/api/core/utils.md",
    "content": "# 实用功能\n\n本节记录了 Pydoll 中使用的实用程序函数和辅助类。\n\n::: pydoll.utils\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      group_by_category: true\n      members_order: source "
  },
  {
    "path": "docs/zh/api/elements/mixins.md",
    "content": "# 元素mixins\n\nmixins 模块提供可复用的功能，可以将其混合到元素类中以扩展其功能。\n\n## 元素定位mixins\n\n`FindElementsMixin` 为包含它的类提供元素查找功能。\n\n::: pydoll.elements.mixins.find_elements_mixin\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      filters:\n        - \"!^_\"\n        - \"!^__\"\n\n## 用法\n\nMixin 通常由库内部使用，用于组合功能。`Tab` 和 `WebElement` 等类使用 `FindElementsMixin` 来提供元素定位方法：\n\n```python\n# 这些方法来自 FindElementsMixin\nelement = await tab.find(id=\"username\")\nelements = await tab.find(class_name=\"item\", find_all=True)\nelement = await tab.query(\"#submit-button\")\n```\n\n\n## 可用方法\n\n`FindElementsMixin` 提供了多种元素定位的方法：\n\n- `find()` - 使用关键字参数的现代元素查找方法\n- `query()` - CSS 选择器和 XPath 查询\n- `find_element()` - 旧版元素定位方法\n- `find_elements()` - 查找多个元素的旧版方法\n\n!!! 提示“现代 vs 传统”\n`find()` 方法是最新的、推荐的查找元素的方法。`find_element()` 和 `find_elements()` 方法保留下来，以实现向后兼容。"
  },
  {
    "path": "docs/zh/api/elements/shadow_root.md",
    "content": "# ShadowRoot\n\n::: pydoll.elements.shadow_root.ShadowRoot\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      members_order: source\n      group_by_category: true\n"
  },
  {
    "path": "docs/zh/api/elements/web_element.md",
    "content": "# 网页元素\n\n::: pydoll.elements.web_element.WebElement\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n      members_order: source\n      group_by_category: true "
  },
  {
    "path": "docs/zh/api/index.md",
    "content": "# API 参考\n\n这里是Pydoll API 参考！本节提供 Pydoll 库中所有类、方法和函数的详尽文档。\n\n## 概述\n\nPydoll 几个关键模块组成，每个模块在浏览器自动化中都有特定的用途：\n\n### 浏览器模块\n浏览器模块可以管理浏览器实例和生命周期。\n\n- **[Chrome](browser/chrome.md)** - Chrome 浏览器自动化\n- **[Edge](browser/edge.md)** - Microsoft Edge 浏览器自动化  \n- **[Options](browser/options.md)** - 浏览器配置选项  \n- **[Tab](browser/tab.md)** - 页面标签和交互  \n- **[Requests](browser/requests.md)** - 浏览器上下文中的 HTTP 请求\n- **[Managers](browser/managers.md)** - 浏览器生命周期管理器  \n\n### 元素模块\n元素模块提供与网页元素交互的功能。\n\n- **[WebElement](elements/web_element.md)** - 网页元素交互\n- **[Mixins](elements/mixins.md)** - 可复用的元素交互功能\n\n### 连接模块\n连接模块通过 Chrome DevTools 协议处理与浏览器的通信。\n\n- **[Connection Handler](connection/connection.md)** - WebSocket连接管理器\n- **[Managers](connection/managers.md)** - 连接生命周期管理器\n\n### 命令模块\n命令模块提供低级 Chrome DevTools 协议命令实现。\n\n- **[Commands Overview](commands/index.md)** - CDP command implementations by domain\n\n### 协议模块\n协议模块实现了 Chrome DevTools 协议命令和事件。\n\n- **[Base Types](protocol/base.md)** - Base types for Chrome DevTools Protocol\n- **[Browser](protocol/browser.md)** - Browser domain commands and events\n- **[DOM](protocol/dom.md)** - DOM domain commands and events\n- **[Fetch](protocol/fetch.md)** - Fetch domain commands and events\n- **[Input](protocol/input.md)** - Input domain commands and events\n- **[Network](protocol/network.md)** - Network domain commands and events\n- **[Page](protocol/page.md)** - Page domain commands and events\n- **[Runtime](protocol/runtime.md)** - Runtime domain commands and events\n- **[Storage](protocol/storage.md)** - Storage domain commands and events\n- **[Target](protocol/target.md)** - Target domain commands and events\n\n### 核心模块\n核心模块包含基础程序、常量和异常。\n\n- **[Constants](core/constants.md)** - 库常量和枚举\n- **[Exceptions](core/exceptions.md)** - 自定义异常类\n- **[Utils](core/utils.md)** - 实用功能\n\n## 快捷导航\n\n### 常用类\n\n| 类                 | 功能           | 模块                            |\n|-------------------|--------------|-------------------------------|\n| `Chrome`          | Chrome浏览器自动化 | `pydoll.browser.chromium`     |\n| `Edge`            | Edge浏览器自动化   | `pydoll.browser.chromium`     |\n| `Tab`             | 标签页交互和控制     | `pydoll.browser.tab`          |\n| `WebElement`      | 元素交互         | `pydoll.elements.web_element` |\n| `ChromiumOptions` | 浏览器配置        | `pydoll.browser.options`      |\n\n### 关键枚举和常量\n\n| 名称               | 功能 | 模块 |\n|------------------|---------|--------|\n| `By`             | 元素选择器策略 | `pydoll.constants` |\n| `Key`            | 键盘按键常量 | `pydoll.constants` |\n| `PermissionType` | 浏览器权限类型 | `pydoll.constants` |\n\n### 常见异常类型\n\n| 异常                   | 原因        | 模块                  |\n|----------------------|-----------|---------------------|\n| `ElementNotFound`    | 元素在DOM未找到 | `pydoll.exceptions` |\n| `WaitElementTimeout` | 元素等待超时    | `pydoll.exceptions` |\n| `BrowserNotStarted`  | 浏览器未开启    | `pydoll.exceptions` |\n\n## 使用模式\n\n### 基本浏览器自动化\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to(\"https://example.com\")\n    element = await tab.find(id=\"my-element\")\n    await element.click()\n```\n\n### 元素定位\n\n```python\n# Using the modern find() method\nelement = await tab.find(id=\"username\")\nelement = await tab.find(tag_name=\"button\", class_name=\"submit\")\n\n# Using CSS selectors or XPath\nelement = await tab.query(\"#username\")\nelement = await tab.query(\"//button[@class='submit']\")\n```\n\n### 事件处理\n\n```python\nawait tab.enable_page_events()\nawait tab.on('Page.loadEventFired', handle_page_load)\n```\n\n## 类型提示\n\nPydoll 具有完整的类型支持，并提供全面的类型提示，以提供更好的 IDE 支持和代码安全性。所有公共 API 均包含正确的类型注释。\n\n```python\nfrom typing import Optional, List\nfrom pydoll.elements.web_element import WebElement\n\n# Methods return properly typed objects\nelement: Optional[WebElement] = await tab.find(id=\"test\", raise_exc=False)\nelements: List[WebElement] = await tab.find(class_name=\"item\", find_all=True)\n```\n\n## Async/Await 支持\n\n所有 Pydoll 操作都是异步的，必须与 `async`/`await` 一起使用：\n\n```python\nimport asyncio\n\nasync def main():\n    # All Pydoll operations are async\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to(\"https://example.com\")\n        \nasyncio.run(main())\n```\n\n浏览以下部分以了解每个模块的完整 API 文档。"
  },
  {
    "path": "docs/zh/api/protocol/base.md",
    "content": "# 协议基础类型\n\nChrome DevTools 协议命令、响应和事件的基础类型和结构。\n\n## 基础类型\n\n::: pydoll.protocol.base\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 3\n      group_by_category: true\n      members_order: source\n      filters:\n        - \"!^__\""
  },
  {
    "path": "docs/zh/api/protocol/browser.md",
    "content": "# 浏览器协议\n\nChrome DevTools 协议的浏览器域命令、事件和类型。\n\n## 方法\n\n::: pydoll.protocol.browser.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 事件\n\n::: pydoll.protocol.browser.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 类型\n\n::: pydoll.protocol.browser.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/zh/api/protocol/dom.md",
    "content": "# DOM 协议\n\nChrome DevTools 协议的 DOM 域命令、事件和类型。\n\n## 方法\n\n::: pydoll.protocol.dom.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 事件\n\n::: pydoll.protocol.dom.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 类型\n\n::: pydoll.protocol.dom.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/zh/api/protocol/fetch.md",
    "content": "# 获取协议\n\nChrome DevTools 协议的获取域命令、事件和类型。\n\n## 方法\n\n::: pydoll.protocol.fetch.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 事件\n\n::: pydoll.protocol.fetch.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 类型\n\n::: pydoll.protocol.fetch.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/zh/api/protocol/input.md",
    "content": "# 输入协议\n\nChrome DevTools 协议的输入域命令、事件和类型。\n\n## 方法\n\n::: pydoll.protocol.input.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 事件\n\n::: pydoll.protocol.input.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 类型\n\n::: pydoll.protocol.input.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/zh/api/protocol/network.md",
    "content": "# 网络协议\n\nChrome DevTools 协议的网络域命令、事件和类型。\n\n## 方法\n\n::: pydoll.protocol.network.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 事件\n\n::: pydoll.protocol.network.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 类型\n\n::: pydoll.protocol.network.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/zh/api/protocol/page.md",
    "content": "# 页面协议\n\nChrome DevTools 协议的页面域命令、事件和类型。\n\n## 方法\n\n::: pydoll.protocol.page.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 事件\n\n::: pydoll.protocol.page.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 类型\n\n::: pydoll.protocol.page.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/zh/api/protocol/runtime.md",
    "content": "# 运行时协议\n\nChrome DevTools 协议的运行时域命令、事件和类型。\n\n## 方法\n\n::: pydoll.protocol.runtime.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 事件\n\n::: pydoll.protocol.runtime.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 类型\n\n::: pydoll.protocol.runtime.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/zh/api/protocol/storage.md",
    "content": "# 存储协议\n\nChrome DevTools 协议的存储域命令、事件和类型。\n\n## 方法\n\n::: pydoll.protocol.storage.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n## 事件\n\n::: pydoll.protocol.storage.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n## 类型\n\n::: pydoll.protocol.storage.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/zh/api/protocol/target.md",
    "content": "# 目标协议\n\nChrome DevTools 协议的目标域命令、事件和类型。\n\n## 方法\n\n::: pydoll.protocol.target.methods\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n\n## 事件\n\n::: pydoll.protocol.target.events\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2\n## 类型\n\n::: pydoll.protocol.target.types\n    options:\n      show_root_heading: true\n      show_source: false\n      heading_level: 2"
  },
  {
    "path": "docs/zh/deep-dive/architecture/browser-domain.md",
    "content": "# 浏览器域架构\n\n浏览器域代表 Pydoll 自动化层次结构的最高级别，管理浏览器进程生命周期、CDP 连接、上下文隔离和全局浏览器操作。本文档探讨了浏览器级控制的内部架构、设计决策和技术实现。\n\n!!! info \"实用指南\"\n    有关实际示例和使用模式，请参阅[浏览器管理](../features/browser-management/tabs.md)和[浏览器上下文](../features/browser-management/contexts.md)指南。\n\n## 架构概述\n\n浏览器域位于进程管理、协议通信和资源协调的交叉点。它协调多个专门的组件，为浏览器自动化提供统一的接口：\n\n```mermaid\ngraph TB\n    subgraph \"Browser Domain\"\n        Browser[Browser Instance]\n        Browser --> ConnectionHandler[Connection Handler]\n        Browser --> ProcessManager[Process Manager]\n        Browser --> ProxyManager[Proxy Manager]\n        Browser --> TempDirManager[Temp Directory Manager]\n        Browser --> TabRegistry[Tab Registry]\n        Browser --> ContextAuth[Context Proxy Auth]\n    end\n    \n    ConnectionHandler <--> |WebSocket| CDP[Chrome DevTools Protocol]\n    ProcessManager --> |Manages| BrowserProcess[Browser Process]\n    TabRegistry --> Tab1[Tab Instance 1]\n    TabRegistry --> Tab2[Tab Instance 2]\n    TabRegistry --> Tab3[Tab Instance N]\n    \n    CDP <--> BrowserProcess\n```\n\n### 层次结构与抽象\n\n浏览器域被实现为一个**抽象基类**，定义了所有浏览器实现的契约：\n\n```python\nclass Browser(ABC):\n    \"\"\"Abstract base class for browser automation via CDP.\"\"\"\n    \n    @abstractmethod\n    def _get_default_binary_location(self) -> str:\n        \"\"\"子类必须提供特定于浏览器的可执行文件路径。\"\"\"\n        pass\n    \n    async def start(self, headless: bool = False) -> Tab:\n        \"\"\"所有浏览器共享的具体实现。\"\"\"\n        # 1. 解析二进制位置\n        # 2. 设置用户数据目录\n        # 3. 启动浏览器进程\n        # 4. 验证 CDP 连接\n        # 5. 配置代理（如果需要）\n        # 6. 返回初始标签页\n```\n\n这种设计实现了**多态性** - Chrome、Edge 和其他基于 Chromium 的浏览器共享 99% 的代码，仅在可执行文件路径和次要标志变化上有所不同。\n\n## 组件架构\n\nBrowser 类协调多个专门的管理器，每个管理器负责浏览器自动化的特定方面。理解这些组件是理解 Pydoll 设计的关键。\n\n### 连接处理器\n\nConnectionHandler 是 Pydoll 和浏览器进程之间的**通信桥梁**。它管理：\n\n- **WebSocket 生命周期**：连接建立、保持活动、重新连接\n- **命令执行**：发送 CDP 命令并等待响应\n- **事件分发**：将 CDP 事件路由到已注册的回调\n- **回调注册表**：维护每个连接的事件监听器\n\n```python\nclass Browser:\n    def __init__(self, ...):\n        # ConnectionHandler 使用端口或 WebSocket 地址初始化\n        self._connection_handler = ConnectionHandler(self._connection_port)\n    \n    async def _execute_command(self, command, timeout=10):\n        \"\"\"所有 CDP 命令都通过连接处理器流动。\"\"\"\n        return await self._connection_handler.execute_command(command, timeout)\n```\n\n!!! info \"连接层深入探讨\"\n    有关 WebSocket 通信、命令/响应流程和异步模式的详细信息，请参阅[连接层架构](./connection-layer.md)。\n\n### 进程管理器\n\nBrowserProcessManager 处理**操作系统进程生命周期**：\n\n```python\nclass BrowserProcessManager:\n    def start_browser_process(self, binary, port, arguments):\n        \"\"\"\n        1. 使用二进制路径 + 参数构造命令行\n        2. 使用适当的 stdio 处理生成子进程\n        3. 监控进程启动\n        4. 存储进程句柄以供后续终止\n        \"\"\"\n        \n    def stop_process(self):\n        \"\"\"\n        1. 尝试优雅终止（SIGTERM）\n        2. 等待进程退出\n        3. 如果超时则强制终止（SIGKILL）\n        4. 清理进程资源\n        \"\"\"\n```\n\n**为什么要分离进程管理？**\n\n- **可测试性**：进程管理器可以在单元测试中被模拟\n- **跨平台**：封装特定于操作系统的进程处理\n- **可靠性**：处理僵尸进程、孤立子进程等边缘情况\n\n### 标签页注册表\n\nBrowser 维护一个 **Tab 实例注册表**以确保每个目标的单例行为：\n\n```python\nclass Browser:\n    def __init__(self, ...):\n        self._tabs_opened: dict[str, Tab] = {}\n    \n    async def new_tab(self, url='', browser_context_id=None) -> Tab:\n        # 通过 CDP 创建目标\n        response = await self._execute_command(\n            TargetCommands.create_target(browser_context_id=browser_context_id)\n        )\n        target_id = response['result']['targetId']\n        \n        # 检查标签页是否已存在于注册表中\n        if target_id in self._tabs_opened:\n            return self._tabs_opened[target_id]\n        \n        # 创建新的 Tab 实例并注册它\n        tab = Tab(self, target_id=target_id, ...)\n        self._tabs_opened[target_id] = tab\n        return tab\n```\n\n**为什么使用单例 Tab 实例？**\n\n- **状态一致性**：对同一标签页的多个引用共享状态（已启用的域、回调）\n- **内存效率**：防止同一目标的重复 Tab 实例\n- **事件路由**：确保事件路由到正确的 Tab 实例\n\n### 代理身份验证架构\n\nPydoll 通过 Fetch 域实现**自动代理身份验证**，以避免在 CDP 命令中暴露凭据。根据代理范围，实现使用**两种不同的机制**：\n\n#### 机制 1：浏览器级代理身份验证（全局代理）\n\n当通过 `ChromiumOptions` 配置代理时（适用于默认上下文中的所有标签页）：\n\n```python\n# 在 Browser.start() -> _configure_proxy() 中\nasync def _configure_proxy(self, private_proxy, proxy_credentials):\n    # 在浏览器级别启用 Fetch\n    await self.enable_fetch_events(handle_auth_requests=True)\n    \n    # 在浏览器级别注册回调（影响所有标签页）\n    await self.on(FetchEvent.REQUEST_PAUSED, self._continue_request_callback, temporary=True)\n    await self.on(FetchEvent.AUTH_REQUIRED, \n                  partial(self._continue_request_with_auth_callback,\n                          proxy_username=credentials[0],\n                          proxy_password=credentials[1]),\n                  temporary=True)\n```\n\n**作用域：**浏览器级 WebSocket 连接 → 影响**默认上下文中的所有标签页**\n\n#### 机制 2：标签页级代理身份验证（按上下文代理）\n\n当通过 `create_browser_context(proxy_server=...)` 为每个上下文配置代理时：\n\n```python\n# 按上下文存储凭据\nasync def create_browser_context(self, proxy_server, ...):\n    sanitized_proxy, extracted_auth = self._sanitize_proxy_and_extract_auth(proxy_server)\n    \n    response = await self._execute_command(\n        TargetCommands.create_browser_context(proxy_server=sanitized_proxy)\n    )\n    context_id = response['result']['browserContextId']\n    \n    if extracted_auth:\n        self._context_proxy_auth[context_id] = extracted_auth  # 按上下文存储\n    \n    return context_id\n\n# 为该上下文中的每个标签页设置身份验证\nasync def _setup_context_proxy_auth_for_tab(self, tab, browser_context_id):\n    creds = self._context_proxy_auth.get(browser_context_id)\n    if not creds:\n        return\n    \n    # 在标签页上启用 Fetch（标签页级 WebSocket）\n    await tab.enable_fetch_events(handle_auth=True)\n    \n    # 在标签页上注册回调（仅影响此标签页）\n    await tab.on(FetchEvent.REQUEST_PAUSED, \n                 partial(self._tab_continue_request_callback, tab=tab), \n                 temporary=True)\n    await tab.on(FetchEvent.AUTH_REQUIRED,\n                 partial(self._tab_continue_request_with_auth_callback,\n                         tab=tab,\n                         proxy_username=creds[0],\n                         proxy_password=creds[1]),\n                 temporary=True)\n```\n\n**作用域：**标签页级 WebSocket 连接 → 仅影响**该特定标签页**\n\n#### 为什么使用两种机制？\n\n| 方面 | 浏览器级 | 标签页级 |\n|--------|---------------|-----------|\n| **触发器** | `ChromiumOptions` 中的代理 | `create_browser_context()` 中的代理 |\n| **WebSocket** | 浏览器级连接 | 标签页级连接 |\n| **作用域** | 默认上下文中的所有标签页 | 仅该上下文中的标签页 |\n| **效率** | 所有标签页一个监听器 | 每个标签页一个监听器 |\n| **隔离** | 无上下文分离 | 每个上下文具有不同的凭据 |\n\n**标签页级身份验证的设计理由：**\n\n- **上下文隔离**：每个上下文可以有**不同的代理**和**不同的凭据**\n- **CDP 限制**：Fetch 域不能在浏览器级别限定到特定上下文\n- **权衡**：效率稍低（每个标签页一个监听器），但对于按上下文代理支持是必需的\n\n这种架构确保**凭据永远不会出现在 CDP 日志中**，身份验证以透明方式处理。\n\n!!! warning \"Fetch 域副作用\"\n    - **浏览器级 Fetch**：暂时暂停默认上下文中**所有标签页的所有请求**，直到身份验证完成\n    - **标签页级 Fetch**：暂时暂停**该特定标签页的所有请求**，直到身份验证完成\n    \n    这是 CDP 限制 - Fetch 启用请求拦截。身份验证完成后，Fetch 被禁用以最小化开销。\n\n## 初始化和生命周期\n\n### 构造函数设计\n\nBrowser 构造函数初始化所有内部组件，但**不启动浏览器进程**。这种分离允许在启动之前进行配置：\n\n```python\nclass Browser(ABC):\n    def __init__(\n        self,\n        options_manager: BrowserOptionsManager,\n        connection_port: Optional[int] = None,\n    ):\n        # 1. 验证参数\n        self._validate_connection_port(connection_port)\n        \n        # 2. 通过管理器初始化选项\n        self.options = options_manager.initialize_options()\n        \n        # 3. 确定 CDP 端口（如果未指定则随机）\n        self._connection_port = connection_port or randint(9223, 9322)\n        \n        # 4. 初始化专门的管理器\n        self._proxy_manager = ProxyManager(self.options)\n        self._browser_process_manager = BrowserProcessManager()\n        self._temp_directory_manager = TempDirectoryManager()\n        self._connection_handler = ConnectionHandler(self._connection_port)\n        \n        # 5. 初始化状态跟踪\n        self._tabs_opened: dict[str, Tab] = {}\n        self._context_proxy_auth: dict[str, tuple[str, str]] = {}\n        self._ws_address: Optional[str] = None\n```\n\n**关键设计决策：**\n\n- **延迟进程启动**：构造函数是同步的；`start()` 是异步的\n- **端口灵活性**：随机端口防止并行自动化中的冲突\n- **选项管理器模式**：用于浏览器特定配置的策略模式\n- **组件组合**：专门的管理器而不是单体类\n\n### 启动序列\n\n`start()` 方法协调浏览器启动和连接：\n\n```python\nasync def start(self, headless: bool = False) -> Tab:\n    # 1. 解析二进制位置\n    binary_location = self.options.binary_location or self._get_default_binary_location()\n    \n    # 2. 设置用户数据目录（临时或持久）\n    self._setup_user_dir()\n    \n    # 3. 提取代理凭据（如果是私有代理）\n    proxy_config = self._proxy_manager.get_proxy_credentials()\n    \n    # 4. 使用参数启动浏览器进程\n    self._browser_process_manager.start_browser_process(\n        binary_location, self._connection_port, self.options.arguments\n    )\n    \n    # 5. 验证 CDP 端点是否响应\n    await self._verify_browser_running()\n    \n    # 6. 配置代理身份验证（通过 Fetch 域）\n    await self._configure_proxy(proxy_config[0], proxy_config[1])\n    \n    # 7. 获取第一个有效目标并创建 Tab\n    valid_tab_id = await self._get_valid_tab_id(await self.get_targets())\n    tab = Tab(self, target_id=valid_tab_id, connection_port=self._connection_port)\n    self._tabs_opened[valid_tab_id] = tab\n    \n    return tab\n```\n\n!!! tip \"为什么 start() 返回一个 Tab\"\n    这是为了人体工程学的**设计妥协**。理想情况下，`start()` 只会启动浏览器，用户会单独调用 `new_tab()`。但是，返回初始标签页减少了 90% 用例（单标签页自动化）的样板代码。权衡：即使在多标签页场景中也无法避免初始标签页。\n\n### 上下文管理器协议\n\nBrowser 实现了 `__aenter__` 和 `__aexit__` 以自动清理：\n\n```python\nasync def __aexit__(self, exc_type, exc_val, exc_tb):\n    # 1. 恢复备份首选项（如果已修改）\n    if self._backup_preferences_dir:\n        shutil.copy2(self._backup_preferences_dir, ...)\n    \n    # 2. 检查浏览器是否仍在运行\n    if await self._is_browser_running(timeout=2):\n        await self.stop()\n    \n    # 3. 关闭 WebSocket 连接\n    await self._connection_handler.close()\n```\n\n这确保即使在自动化期间发生异常也能正确清理。\n\n## 浏览器上下文架构\n\n浏览器上下文是 Pydoll 最复杂的隔离机制，在单个浏览器进程内提供**完整的浏览环境分离**。理解它们的架构对于高级自动化至关重要。\n\n### CDP 层次结构：浏览器、上下文、目标\n\nCDP 将浏览器结构组织为三个级别：\n\n```mermaid\ngraph TB\n    Browser[Browser Process]\n    Browser --> DefaultContext[Default BrowserContext]\n    Browser --> Context1[BrowserContext ID: abc-123]\n    Browser --> Context2[BrowserContext ID: def-456]\n    \n    DefaultContext --> Target1[Target/Page ID: page-1]\n    DefaultContext --> Target2[Target/Page ID: page-2]\n    \n    Context1 --> Target3[Target/Page ID: page-3]\n    \n    Context2 --> Target4[Target/Page ID: page-4]\n    Context2 --> Target5[Target/Page ID: page-5]\n```\n\n**关键概念：**\n\n1. **浏览器进程**：具有一个 CDP 端点的单个 Chromium 实例\n2. **BrowserContext**：隔离的存储/缓存/权限边界（类似于无痕模式）\n3. **目标**：单个页面、弹出窗口、worker 或后台目标\n\n### 上下文隔离边界\n\n每个浏览器上下文维护以下内容的**严格隔离**：\n\n| 资源 | 隔离级别 | 实现 |\n|----------|----------------|----------------|\n| Cookies | 完全 | 每个上下文单独的 cookie jar |\n| localStorage | 完全 | 每个上下文每个源单独的存储 |\n| IndexedDB | 完全 | 每个上下文每个源单独的数据库 |\n| 缓存 | 完全 | 每个上下文独立的 HTTP 缓存 |\n| 权限 | 完全 | 上下文特定的权限授予 |\n| 网络代理 | 完全 | 按上下文的代理配置 |\n| 身份验证 | 完全 | 每个上下文独立的身份验证状态 |\n\n!!! info \"为什么上下文是轻量级的\"\n    与启动多个浏览器进程不同，上下文共享**渲染引擎、GPU 进程和网络栈**。只有存储和状态被隔离。这使得创建上下文比新浏览器实例快 10-100 倍。\n\n### 上下文创建和目标绑定\n\n创建上下文和目标涉及两个 CDP 命令：\n\n```python\n# 步骤 1：创建隔离的浏览上下文\nresponse = await self._execute_command(\n    TargetCommands.create_browser_context(\n        proxy_server='http://proxy.example.com:8080',\n        proxy_bypass_list='localhost,127.0.0.1'\n    )\n)\ncontext_id = response['result']['browserContextId']\n\n# 步骤 2：在该上下文中创建目标（页面）\nresponse = await self._execute_command(\n    TargetCommands.create_target(\n        browser_context_id=context_id  # 将目标绑定到上下文\n    )\n)\ntarget_id = response['result']['targetId']\n```\n\n**关键细节：**`browser_context_id` 参数**将目标绑定到上下文的隔离边界**。没有它，目标将在默认上下文中创建。\n\n### 有头模式下的窗口实体化\n\n在**有头模式**（可见 UI）中，浏览器上下文有一个重要的物理约束：\n\n- 上下文最初仅存在于**内存中**（无窗口）\n- 在上下文中创建的**第一个目标****必须**打开一个顶级窗口\n- **后续目标**可以作为该窗口内的标签页打开\n\n这是一个 **CDP/Chromium 限制**，而不是 Pydoll 的设计选择：\n\n```python\n# 上下文中的第一个目标：必须创建窗口\ntab1 = await browser.new_tab(browser_context_id=context_id)  # 打开新窗口\n\n# 后续目标：可以作为现有窗口中的标签页打开\ntab2 = await browser.new_tab(browser_context_id=context_id)  # 作为标签页打开\n```\n\n**为什么这很重要？**\n\n- 在**无头模式**中：完全无关（不渲染窗口）\n- 在**有头模式**中：每个上下文的第一个目标将打开一个可见窗口\n- 在**测试环境**中：多个上下文 → 多个窗口（可能会令人困惑）\n\n!!! tip \"无头上下文更干净\"\n    对于 CI/CD、抓取或批量自动化，请使用无头模式。上下文隔离的工作方式相同，但没有窗口实体化开销。\n\n### 上下文删除和清理\n\n删除上下文会**立即关闭其中的所有目标**：\n\n```python\nawait browser.delete_browser_context(context_id)\n# 此上下文中的所有标签页现已关闭\n# 此上下文的所有存储已清除\n# 上下文不能重用（ID 无效）\n```\n\n**清理序列：**\n\n1. CDP 发送 `Target.disposeBrowserContext` 命令\n2. 浏览器关闭该上下文中的所有目标\n3. 浏览器清除该上下文的所有存储\n4. 浏览器使上下文 ID 无效\n5. Pydoll 从内部注册表中删除上下文\n\n## 浏览器级别的事件系统\n\n浏览器域支持跨所有标签页和上下文操作的**浏览器级事件监听器**。这与标签页级事件不同。\n\n### 浏览器与标签页事件作用域\n\n```python\n# 浏览器级事件：适用于所有标签页\nawait browser.on('Target.targetCreated', handle_new_target)\n\n# 标签页级事件：适用于一个标签页\nawait tab.on('Page.loadEventFired', handle_page_load)\n```\n\n**架构差异：**\n\n- **浏览器事件**使用**浏览器级 WebSocket 连接**（基于端口或 `ws://host/devtools/browser/...`）\n- **标签页事件**使用**标签页级 WebSocket 连接**（`ws://host/devtools/page/<target_id>`）\n\n### Fetch 域：全局请求拦截\n\nFetch 域可以在**浏览器和标签页**两个级别启用，具有不同的作用域：\n\n```python\n# 浏览器级 Fetch：拦截所有标签页的请求\nawait browser.enable_fetch_events(handle_auth_requests=True)\nawait browser.on('Fetch.requestPaused', handle_request)\n\n# 标签页级 Fetch：拦截一个标签页的请求\nawait tab.enable_fetch_events(handle_auth_requests=True)\nawait tab.on('Fetch.requestPaused', handle_request)\n```\n\n**何时使用每种方式：**\n\n| 用例 | 级别 | 原因 |\n|----------|-------|--------|\n| 代理身份验证 | 浏览器 | 全局应用于所有上下文 |\n| 广告拦截 | 浏览器 | 在所有标签页中拦截广告 |\n| API 模拟 | 标签页 | 为特定测试模拟特定 API |\n| 请求日志 | 标签页 | 仅记录相关标签页的请求 |\n\n!!! warning \"Fetch 性能影响\"\n    在浏览器级别启用 Fetch 会**暂停所有标签页的所有请求**，直到回调执行。这会为每个请求增加延迟。尽可能使用标签页级 Fetch 以最小化影响。\n\n### 命令路由\n\n所有 CDP 命令都通过浏览器的连接处理器流动：\n\n```python\nasync def _execute_command(self, command, timeout=10):\n    \"\"\"\n    将命令路由到适当的连接：\n    - 浏览器级命令 → 浏览器 WebSocket\n    - 标签页级命令 → 委托给 Tab 实例\n    \"\"\"\n    return await self._connection_handler.execute_command(command, timeout)\n```\n\n这种集中式路由实现：\n\n- **请求/响应关联**：通过 ID 匹配响应与请求\n- **超时管理**：取消超过超时的命令\n- **错误处理**：将 CDP 错误转换为 Python 异常\n\n## 资源管理\n\n### Cookie 和存储操作\n\n浏览器域公开**浏览器级**和**上下文特定**的存储操作：\n\n```python\n# 浏览器级操作（所有上下文）\nawait browser.set_cookies(cookies)\nawait browser.get_cookies()\nawait browser.delete_all_cookies()\n\n# 上下文特定操作\nawait browser.set_cookies(cookies, browser_context_id=context_id)\nawait browser.get_cookies(browser_context_id=context_id)\nawait browser.delete_all_cookies(browser_context_id=context_id)\n```\n\n这些操作在底层使用 **Storage 域**：\n\n- `Storage.getCookies`：检索上下文或所有上下文的 cookie\n- `Storage.setCookies`：使用域/路径/过期时间设置 cookie\n- `Storage.clearCookies`：清除上下文或所有上下文的 cookie\n\n!!! info \"浏览器与标签页存储作用域\"\n    - **浏览器级**：对整个浏览器或特定上下文操作\n    - **标签页级**：限定于标签页的当前源\n    \n    使用浏览器级进行全局 cookie 管理（例如，为所有域设置会话 cookie）。使用标签页级进行特定于源的操作（例如，注销后清除 cookie）。\n\n### 权限授予\n\n浏览器域提供**编程式权限控制**，绕过浏览器提示：\n\n```python\nawait browser.grant_permissions(\n    [PermissionType.GEOLOCATION, PermissionType.NOTIFICATIONS],\n    origin='https://example.com',\n    browser_context_id=context_id\n)\n```\n\n**架构：**\n\n- 通过 `Browser.grantPermissions` CDP 命令授予权限\n- 权限是**上下文特定的**（每个上下文隔离）\n- 授予会覆盖默认提示行为\n- `reset_permissions()` 恢复到默认行为\n\n### 下载管理\n\n下载行为通过 `Browser.setDownloadBehavior` 命令配置：\n\n```python\nawait browser.set_download_behavior(\n    behavior=DownloadBehavior.ALLOW,\n    download_path='/path/to/downloads',\n    events_enabled=True,  # 发出下载进度事件\n    browser_context_id=context_id\n)\n```\n\n**选项：**\n\n- `ALLOW`：保存到指定路径\n- `DENY`：取消所有下载\n- `DEFAULT`：显示浏览器的默认下载 UI\n\n### 窗口管理\n\n窗口操作应用于目标的**物理操作系统窗口**：\n\n```python\nwindow_id = await browser.get_window_id_for_target(target_id)\nawait browser.set_window_bounds({\n    'left': 100, 'top': 100,\n    'width': 1920, 'height': 1080,\n    'windowState': 'normal'  # 或 'minimized'、'maximized'、'fullscreen'\n})\n```\n\n**实现细节：**\n\n- 使用 `Browser.getWindowForTarget` 从目标 ID 解析窗口 ID\n- `Browser.setWindowBounds` 修改窗口几何形状\n- **无头模式**：窗口操作是无操作的（不存在物理窗口）\n\n## 架构洞察和设计权衡\n\n### 单例标签页注册表：为什么？\n\n标签页注册表模式（`_tabs_opened: dict[str, Tab]`）确保：\n\n1. **事件路由正确工作**：CDP 事件包含 `targetId` 但没有 Tab 引用。注册表映射 `targetId` → `Tab` 以实现正确的回调分发。\n2. **状态一致性**：引用同一目标的多个代码路径获得**相同的 Tab 实例**，防止状态分歧。\n3. **内存效率**：没有注册表，`get_opened_tabs()` 会在每次调用时创建重复的 Tab 实例。\n\n**权衡：**内存使用随标签页数量增长，但对于有状态的 Tab 实例这是不可避免的。\n\n### 为什么 start() 返回一个 Tab\n\n这个设计决策牺牲纯粹性以获得**人体工程学**：\n\n- **缺点**：即使在多标签页自动化中也无法避免初始标签页\n- **优点**：90% 的用户（单标签页脚本）不需要样板代码：\n\n```python\n# start() 返回 Tab\ntab = await browser.start()\n\n# 不返回（纯粹设计）\nawait browser.start()\ntab = await browser.new_tab()\n```\n\n**探索的替代方案：**在 `new_tab()` 中自动关闭初始标签页。因为这是令人惊讶的行为（隐式副作用）而被拒绝。\n\n### 代理身份验证：两级架构权衡\n\nPydoll 的代理身份验证使用两种不同的 Fetch 域策略：\n\n**浏览器级（全局代理）：**\n- **安全优势**：凭据永远不会记录在 CDP 跟踪中\n- **性能成本**：Fetch 暂停**所有标签页的所有请求**，直到身份验证完成\n- **效率**：默认上下文中所有标签页的单个监听器\n- **缓解**：第一次身份验证后禁用 Fetch，最小化开销\n\n**标签页级（按上下文代理）：**\n- **安全优势**：凭据永远不会记录在 CDP 跟踪中\n- **性能成本**：Fetch 暂停**该标签页的所有请求**，直到身份验证完成\n- **效率**：每个标签页单独的监听器（效率较低，但对于隔离是必需的）\n- **隔离优势**：每个上下文可以有不同的代理凭据\n- **缓解**：每个标签页在第一次身份验证后禁用 Fetch\n\n**为什么不使用 Browser.setProxyAuth？**这个 CDP 命令不存在。Fetch 是编程式身份验证的唯一机制。\n\n**为什么对上下文使用标签页级？**CDP 的 Fetch 域不能限定到特定的 BrowserContext。由于每个上下文可以有不同的代理和不同的凭据，Pydoll 必须在标签页级别处理身份验证以尊重上下文边界。\n\n### 端口随机化策略\n\n随机 CDP 端口（9223-9322）防止并行运行浏览器实例时的冲突：\n\n```python\nself._connection_port = connection_port or randint(9223, 9322)\n```\n\n**为什么不从 9222 递增？**\n\n- 多进程环境中的竞态条件（例如 pytest-xdist）\n- 与用户的手动端口选择冲突\n\n**权衡：**随机端口更难调试（无法硬编码）。解决方案：`browser._connection_port` 暴露所选端口。\n\n### 组件分离：为什么使用管理器？\n\nBrowser 类委托给专门的管理器（ProcessManager、ProxyManager、TempDirManager、ConnectionHandler）以实现：\n\n1. **可测试性**：管理器可以独立模拟\n2. **可重用性**：ProxyManager 逻辑在 Browser 实现之间共享\n3. **可维护性**：每个管理器都有单一职责\n4. **跨平台**：特定于操作系统的逻辑在 ProcessManager 中隔离\n\n**权衡：**更多的间接层次，但在规模上代码组织显著更好。\n\n## 关键要点\n\n1. **Browser 是一个协调器**，而不是单体。它协调管理器并处理 CDP 通信。\n2. **标签页注册表确保单例实例**每个目标，对于事件路由和状态一致性至关重要。\n3. **浏览器上下文是轻量级隔离**，共享浏览器进程但分离存储/缓存/身份验证。\n4. **通过 Fetch 的代理身份验证**是一种安全权衡 - 隐藏凭据但增加延迟。\n5. **事件系统有两个级别**：浏览器级和标签页特定，具有不同的 WebSocket 连接。\n6. **组件分离**（管理器）改善了可测试性和跨平台支持。\n\n## 相关文档\n\n要深入了解相关架构组件：\n\n- **[连接层](./connection-layer.md)**：WebSocket 通信、命令/响应流程、异步模式\n- **[事件架构](./event-architecture.md)**：事件分发、回调管理、域启用\n- **[标签页域](./tab-domain.md)**：标签页级操作、页面导航、元素查找\n- **[CDP 深入探讨](./cdp.md)**：Chrome DevTools Protocol 基础\n- **[代理架构](./proxy-architecture.md)**：网络级代理概念和实现\n\n实际使用模式：\n\n- **[标签页管理](../features/browser-management/tabs.md)**：多标签页自动化模式\n- **[浏览器上下文](../features/browser-management/contexts.md)**：上下文隔离实践\n- **[代理配置](../features/configuration/proxy.md)**：设置代理和身份验证\n"
  },
  {
    "path": "docs/zh/deep-dive/architecture/browser-requests-architecture.md",
    "content": "# 浏览器上下文请求架构\n\n本文档探讨了 Pydoll 浏览器上下文 HTTP 请求系统的架构设计，该系统能够发起无缝继承浏览器会话状态、cookie 和身份验证的 HTTP 请求。\n\n!!! info \"提供实用指南\"\n    这是架构深入探讨。有关实际示例和用例，请参阅 [HTTP 请求指南](../features/network/http-requests.md)。\n\n## 架构概述\n\n浏览器上下文请求解决了混合自动化中的一个基本问题：在 UI 交互和 API 调用之间保持会话连续性。传统方法需要手动提取 cookie 和标头，在浏览器和 HTTP 客户端之间创建脆弱的耦合。\n\nPydoll 的架构通过在浏览器的 JavaScript 上下文**内部**执行 HTTP 请求来消除这种复杂性，同时利用 CDP 网络事件捕获 JavaScript 单独无法提供的全面元数据。\n\n### 为什么选择这种架构？\n\n| 传统方法 | Pydoll 架构 |\n|---------------------|---------------------|\n| 独立的 HTTP 客户端（requests、aiohttp） | 统一的基于浏览器的执行 |\n| 手动 cookie 提取和同步 | 自动 cookie 继承 |\n| 两个独立的会话状态 | 单一会话状态 |\n| 有限的 CORS 处理 | 浏览器原生 CORS 强制执行 |\n| 复杂的身份验证流程 | 透明的身份验证保留 |\n\n\n## 组件架构\n\n浏览器上下文请求系统由两个主要类组成，它们与 Pydoll 的事件系统协同工作：\n\n```mermaid\nclassDiagram\n    class Tab {\n        +request: Request\n        +enable_network_events()\n        +disable_network_events()\n        +get_network_response_body()\n        +on(event_name, callback)\n        +clear_callbacks()\n    }\n    \n    class Request {\n        -tab: Tab\n        -_network_events_enabled: bool\n        -_requests_sent: list\n        -_requests_received: list\n        +get(url, params, kwargs)\n        +post(url, data, json, kwargs)\n        +put(url, data, json, kwargs)\n        +patch(url, data, json, kwargs)\n        +delete(url, kwargs)\n        +head(url, kwargs)\n        +options(url, kwargs)\n        -_execute_fetch_request()\n        -_register_callbacks()\n        -_extract_headers()\n        -_extract_cookies()\n    }\n    \n    class Response {\n        -_status_code: int\n        -_content: bytes\n        -_text: str\n        -_json: dict\n        -_response_headers: list\n        -_request_headers: list\n        -_cookies: list\n        -_url: str\n        +ok: bool\n        +status_code: int\n        +text: str\n        +content: bytes\n        +url: str\n        +headers: list\n        +request_headers: list\n        +cookies: list\n        +json()\n        +raise_for_status()\n    }\n    \n    Tab *-- Request\n    Request ..> Response : creates\n    Request ..> Tab : uses events\n```\n\n### Request 类\n\n`Request` 类作为接口层，提供类似 `requests` 的熟悉 API，同时协调 JavaScript 执行和网络事件监控之间的复杂交互。\n\n**主要职责：**\n\n- 将 Python 方法调用转换为 Fetch API JavaScript\n- 管理临时网络事件监听器\n- 在请求执行期间累积网络事件\n- 从 CDP 事件中提取元数据\n- 使用完整信息构造 Response 对象\n\n### Response 类\n\n`Response` 类提供与 `requests.Response` 兼容的接口，使从传统 HTTP 客户端迁移变得无缝。\n\n**主要特性：**\n\n- 多种内容访问器（文本、字节、JSON）\n- 带缓存的延迟 JSON 解析\n- 全面的标头信息（已发送和已接收）\n- 从 Set-Cookie 标头提取 cookie\n- 重定向后的最终 URL\n\n## 执行流程\n\n请求执行遵循六阶段管道：\n\n```mermaid\nflowchart TD\n    Start([tab.request.get#40;url#41;]) --> Phase1[<b>1. 准备</b><br/>构建 URL + 选项]\n    \n    Phase1 --> Phase2[<b>2. 事件注册</b><br/>启用网络事件<br/>注册回调]\n    \n    Phase2 --> Phase3[<b>3. JavaScript 执行</b><br/>Runtime.evaluate&#40;fetch&#41;]\n    \n    Phase3 --> Phase4{<b>4. 网络活动</b>}\n    Phase4 -->|请求已发送| Event1[REQUEST_WILL_BE_SENT]\n    Phase4 -->|响应已接收| Event2[RESPONSE_RECEIVED]\n    Phase4 -->|额外信息| Event3[*_EXTRA_INFO events]\n    \n    Event1 --> Collect[收集元数据]\n    Event2 --> Collect\n    Event3 --> Collect\n    \n    Collect --> Phase5[<b>5. 构造</b><br/>提取标头/cookie<br/>构建 Response 对象]\n    \n    Phase5 --> Phase6[<b>6. 清理</b><br/>清除回调<br/>禁用事件]\n    \n    Phase6 --> End([返回 Response])\n```\n\n### 阶段详情\n\n| 阶段 | 层 | 关键操作 | 异步 |\n|-------|-------|----------------|--------------|\n| **1. 准备** | Request | URL 构建、选项格式化 | 否 |\n| **2. 事件注册** | Tab | 启用事件、注册回调 | 是 |\n| **3. JavaScript 执行** | CDP/Browser | 在浏览器上下文中执行 fetch() | 是 |\n| **4. 网络活动** | Browser/CDP | HTTP 请求、发出 CDP 事件 | 是（并行） |\n| **5. 构造** | Request | 解析事件、构建 Response | 否 |\n| **6. 清理** | Tab | 删除回调、禁用事件 | 是 |\n\n## 事件系统集成\n\n浏览器上下文请求与 Pydoll 的事件系统架构紧密集成。理解这种关系至关重要。\n\n### 临时事件生命周期\n\n```mermaid\nstateDiagram-v2\n    [*] --> NoEvents: Request starts\n    NoEvents --> EventsEnabled: Enable network events\n    EventsEnabled --> CallbacksRegistered: Register callbacks\n    CallbacksRegistered --> ExecutingRequest: Execute fetch\n    ExecutingRequest --> CapturingEvents: Events fire\n    CapturingEvents --> ExecutingRequest: More events\n    ExecutingRequest --> CleaningUp: Fetch completes\n    CleaningUp --> CallbacksRemoved: Clear callbacks\n    CallbacksRemoved --> EventsDisabled: Disable if needed\n    EventsDisabled --> [*]: Request complete\n```\n\n### 为什么同时使用 JavaScript 和事件？\n\n一个常见问题：如果 JavaScript 可以执行请求，为什么要使用网络事件？\n\n| 信息来源 | JavaScript（Fetch API） | 网络事件（CDP） |\n|-------------------|------------------------|----------------------|\n| 响应状态 | 可用 | 可用 |\n| 响应正文 | 可用 | 不可用 |\n| 响应标头 | 部分（CORS 受限） | 完整 |\n| 请求标头 | 不可访问 | 完整 |\n| Set-Cookie 标头 | 浏览器隐藏 | 可用 |\n| 时序信息 | 有限 | 全面 |\n| 重定向链 | 仅最终 URL | 完整链 |\n\n**解决方案：** 结合两个来源以获取完整信息。\n\n!!! tip \"互补技术\"\n    JavaScript 提供响应正文并在浏览器上下文中触发请求（带有 cookie、身份验证）。网络事件提供 JavaScript 安全策略隐藏的元数据。\n\n### CDP 网络事件类型\n\n该架构使用四种 CDP 事件类型来捕获完整的元数据：\n\n| 事件 | 目的 | 关键信息 |\n|-------|---------|----------------|\n| `REQUEST_WILL_BE_SENT` | 主要传出请求 | URL、方法、标准标头 |\n| `REQUEST_WILL_BE_SENT_EXTRA_INFO` | 额外请求元数据 | 关联的 cookie、原始标头 |\n| `RESPONSE_RECEIVED` | 主要响应已接收 | 状态、标头、MIME 类型、时序 |\n| `RESPONSE_RECEIVED_EXTRA_INFO` | 额外响应元数据 | Set-Cookie 标头、安全信息 |\n\n!!! info \"事件多重性\"\n    单个 HTTP 请求生成多个 CDP 事件。Request 类累积所有相关事件，并在构造阶段提取非重复信息。\n\n## 标头和 Cookie 架构\n\n### 标头提取策略\n\n标头存在于多个 CDP 事件中，可能存在重复。该架构使用去重策略：\n\n```mermaid\nflowchart TD\n    A[Network Events] --> B{Event Type}\n    B -->|REQUEST events| C[Extract Sent Headers]\n    B -->|RESPONSE events| D[Extract Received Headers]\n    \n    C --> E[Deduplicate by name+value]\n    D --> F[Deduplicate by name+value]\n    \n    E --> G[Request Headers List]\n    F --> H[Response Headers List]\n    \n    G --> I[Response Object]\n    H --> I\n```\n\n**去重逻辑：**\n\n1. 按顺序处理事件\n2. 每个标头由 `(name, value)` 元组标识\n3. 仅保留每个元组的第一次出现\n4. 结果：唯一、非冗余的标头列表\n\n### Cookie 解析架构\n\nCookie 需要特殊处理，因为它们来自 `RESPONSE_RECEIVED_EXTRA_INFO` 事件中的 `Set-Cookie` 标头：\n\n```mermaid\nflowchart TD\n    A[RESPONSE_RECEIVED_EXTRA_INFO] --> B[Extract Set-Cookie headers]\n    B --> C{Multi-line header?}\n    C -->|Yes| D[Split by newline]\n    C -->|No| E[Parse single cookie]\n    D --> F[Parse each line]\n    F --> G[Extract name=value]\n    E --> G\n    G --> H{Valid name?}\n    H -->|Yes| I[Create CookieParam]\n    H -->|No| J[Discard]\n    I --> K[Add to cookie list]\n    K --> L[Deduplicate]\n    L --> M[Response Object]\n```\n\n**Cookie 提取原则：**\n\n- 只有 `EXTRA_INFO` 事件包含 `Set-Cookie` 标头\n- 忽略 Cookie 属性（Path、Domain、Secure、HttpOnly）\n- 浏览器在内部管理 cookie 属性\n- 仅提取名称-值对以供参考\n\n!!! warning \"Cookie 范围\"\n    `Response.cookies` 属性仅包含来自此特定响应的**新的或更新的** cookie。现有浏览器 cookie 会自动管理，不会通过此接口公开。\n\n## JavaScript 执行上下文\n\nFetch API 执行发生在浏览器的 JavaScript 上下文中，这是该架构强大功能的关键：\n\n### Fetch API 集成\n\n请求被转换为 JavaScript：\n\n```javascript\n// 简化表示\n(async () => {\n    const response = await fetch(url, {\n        method: 'GET',\n        headers: {'X-Custom': 'value'},\n        // 浏览器自动添加：\n        // - Cookie 标头\n        // - 如果设置了 Authorization\n        // - 标准标头（User-Agent、Accept 等）\n    });\n    \n    return {\n        status: response.status,\n        url: response.url,  // 重定向后的最终 URL\n        text: await response.text(),\n        content: new Uint8Array(await response.arrayBuffer()),\n        json: response.headers.get('Content-Type')?.includes('application/json')\n            ? await response.clone().json()\n            : null\n    };\n})()\n```\n\n### 浏览器上下文优势\n\n在浏览器上下文中执行提供：\n\n| 优势 | 描述 |\n|---------|-------------|\n| **自动 Cookie 包含** | 浏览器自动发送所有适用的 cookie |\n| **身份验证状态保留** | 从浏览器会话维护身份验证标头 |\n| **CORS 强制执行** | 浏览器应用与用户交互相同的 CORS 策略 |\n| **TLS/SSL 处理** | 应用浏览器的证书验证和安全策略 |\n| **压缩** | 自动处理 gzip、br、deflate |\n| **重定向** | 浏览器透明地跟随重定向 |\n| **相同安全上下文** | 请求与用户发起的请求完全相同 |\n\n!!! info \"反机器人检测\"\n    在浏览器上下文中执行的请求与用户发起的请求无法区分，使其对分析请求模式的反机器人系统有效。\n\n## 性能考虑\n\n### 事件开销\n\n网络事件为请求执行增加了开销：\n\n| 场景 | 开销 | 建议 |\n|----------|----------|----------------|\n| 单个请求 | 低 | 可接受 |\n| 多个顺序请求 | 中等 | 启用一次事件 |\n| 批量请求（100+） | 高 | 考虑在标签页级别启用事件 |\n| 长时间运行的自动化 | 内存问题 | 完成后禁用 |\n\n### 优化模式\n\n```python\n# 低效 - 事件反复启用/禁用\nfor url in urls:\n    response = await tab.request.get(url)\n\n# 高效 - 事件启用一次\nawait tab.enable_network_events()\nfor url in urls:\n    response = await tab.request.get(url)\nawait tab.disable_network_events()\n```\n\n!!! tip \"自动优化\"\n    Request 类检查网络事件是否已启用，并自动跳过冗余的启用/禁用操作。\n\n### JSON 解析策略\n\nResponse JSON 解析使用带缓存的延迟评估：\n\n1. 首次调用 `response.json()`：解析并缓存\n2. 后续调用：返回缓存结果\n3. 如果在构造期间预解析了 JSON：使用它\n\n这可以防止冗余的解析开销。\n\n## 安全架构\n\n### CORS 策略强制执行\n\n浏览器上下文请求遵守 CORS 策略：\n\n```mermaid\nflowchart TD\n    A[tab.request.get&#40;url&#41;] --> B{Same Origin?}\n    B -->|Yes| C[Request Allowed]\n    B -->|No| D{CORS Headers Present?}\n    D -->|Yes| E[Request Allowed]\n    D -->|No| F[Request Blocked]\n    \n    C --> G[Response Returned]\n    E --> G\n    F --> H[CORS Error]\n```\n\n**CORS 行为：**\n\n- 对同源的请求：始终允许\n- 跨源请求：需要服务器的 CORS 标头\n- 不透明响应：可能被浏览器阻止\n\n**CORS 问题的解决方法：**\n\n首先导航到域以建立同源上下文：\n\n```python\nawait tab.go_to('https://different-domain.com')\nresponse = await tab.request.get('https://different-domain.com/api')\n```\n\n### Cookie 安全\n\n浏览器处理带有安全标志（`HttpOnly`、`Secure`、`SameSite`）的 Cookie：\n\n- **HttpOnly cookie**：自动发送但不暴露给 JavaScript 或 CDP\n- **Secure cookie**：仅通过 HTTPS 发送\n- **SameSite cookie**：浏览器强制执行 SameSite 策略\n\n由于这些安全限制，`Response.cookies` 属性可能不会显示所有 cookie。\n\n### TLS/SSL 验证\n\n浏览器验证 SSL 证书。自签名或无效证书会导致请求失败，除非：\n\n```python\noptions = ChromiumOptions()\noptions.add_argument('--ignore-certificate-errors')\nbrowser = Chrome(options=options)\n```\n\n!!! warning \"安全权衡\"\n    禁用证书验证会降低安全性。仅在受控环境中使用。\n\n## 限制和设计决策\n\n### 请求正文大小\n\n非常大的请求正文（文件、大型数据集）具有 JavaScript 内存约束。对于文件上传，请改用 `WebElement.set_input_files()` 或文件选择器拦截器。\n\n### 二进制响应处理\n\n二进制响应通过 JavaScript 的 `ArrayBuffer` 和 `Uint8Array` 转换，这会为非常大的响应（>100MB）增加一些开销。\n\n### 重定向透明度\n\nFetch API 自动跟随重定向。仅捕获最终 URL。如果您需要重定向链，请单独使用网络监控。\n\n### 事件时序\n\n事件必须在执行 fetch **之前**注册。架构通过注册阶段确保这一点，但手动事件处理需要仔细的时序。\n\n## 架构原则\n\n浏览器上下文请求架构遵循以下原则：\n\n1. **会话连续性**：永远不要破坏浏览器的会话状态\n2. **零手动同步**：不需要 cookie/标头提取\n3. **完整信息**：结合 JavaScript + 事件以获取完整元数据\n4. **自动清理**：每个请求后释放资源\n5. **熟悉的接口**：与 `requests` 兼容的 API，易于采用\n6. **性能意识**：针对常见用例进行优化\n7. **安全意识**：遵守浏览器安全策略\n\n## 与其他系统的集成\n\n### 事件系统依赖\n\n浏览器上下文请求依赖于事件系统架构：\n\n- 利用 `Tab.on()` 进行回调注册\n- 使用 `Tab.clear_callbacks()` 进行清理\n- 尊重现有的网络事件启用\n- 与事件生命周期管理集成\n\n详见[事件系统架构](event-architecture.md)。\n\n### 类型系统集成\n\n该架构广泛使用 Python 的类型系统：\n\n- `HeaderEntry` TypedDict 用于标头\n- `CookieParam` TypedDict 用于 cookie\n- 来自 `pydoll.protocol.network.events` 的事件类型定义\n- 提供 IDE 自动完成和类型安全\n\n详见[类型系统](typing-system.md)。\n\n## 进一步阅读\n\n- **[HTTP 请求指南](../features/network/http-requests.md)** - 实际示例和用例\n- **[事件系统架构](event-architecture.md)** - 事件系统内部设计\n- **[网络监控](../features/network/monitoring.md)** - 被动网络观察\n- **[请求拦截](../features/network/interception.md)** - 主动请求修改\n- **[类型系统](typing-system.md)** - 类型系统集成\n\n## 总结\n\nPydoll 的浏览器上下文请求架构通过结合 JavaScript Fetch API 执行和 CDP 网络事件监控来实现无缝 HTTP 通信。这种混合方法提供：\n\n- 来自 JavaScript 和 CDP 事件的**完整元数据**\n- 通过浏览器上下文执行实现**自动会话连续性**\n- 与 requests 库兼容的**熟悉接口**\n- 通过事件重用实现**性能优化**\n- 符合浏览器策略的**安全合规性**\n\n该架构展示了结合互补技术（JavaScript + CDP 事件）如何优雅地解决复杂问题，在不影响完整性或安全性的情况下提供强大功能和便利性。\n"
  },
  {
    "path": "docs/zh/deep-dive/architecture/event-architecture.md",
    "content": "# 事件系统架构\n\n本文档探讨 Pydoll 事件系统的内部架构，涵盖 WebSocket 通信、事件流、回调管理和性能考虑。\n\n!!! info \"实用指南\"\n    有关实际示例和使用模式，请参阅 [事件系统指南](../features/advanced/event-system.md)。\n\n## WebSocket 通信和 CDP\n\nPydoll 事件系统的核心是 Chrome DevTools Protocol（CDP），它提供了一种结构化的方式来通过 WebSocket 连接与浏览器活动进行交互和监控。这个双向通信通道允许你的代码向浏览器发送命令并接收事件。\n\n```mermaid\nsequenceDiagram\n    participant Client as Pydoll Code\n    participant Connection as ConnectionHandler\n    participant WebSocket\n    participant Browser\n    \n    Client->>Connection: Register callback for event\n    Connection->>Connection: Store callback in registry\n    \n    Client->>Connection: Enable event domain\n    Connection->>WebSocket: Send CDP command to enable domain\n    WebSocket->>Browser: Forward command\n    Browser-->>WebSocket: Acknowledge domain enabled\n    WebSocket-->>Connection: Forward response\n    Connection-->>Client: Domain enabled\n    \n    Browser->>WebSocket: Event occurs, sends CDP event message\n    WebSocket->>Connection: Forward event message\n    Connection->>Connection: Look up callbacks for this event\n    Connection->>Client: Execute registered callback\n```\n\n### WebSocket 通信模型\n\nPydoll 和浏览器之间的 WebSocket 连接遵循以下模式：\n\n1. **连接建立**：浏览器启动时，会创建一个 WebSocket 服务器，Pydoll 建立与其的连接\n2. **双向消息传递**：Pydoll 和浏览器都可以随时发送消息\n3. **消息类型**：\n   - **命令**：从 Pydoll 发送到浏览器（例如导航、DOM 操作）\n   - **命令响应**：浏览器响应命令发送给 Pydoll\n   - **事件**：当浏览器发生某些事情时发送给 Pydoll（例如页面加载、网络活动）\n\n### Chrome DevTools Protocol 结构\n\nCDP 将其功能组织成域，每个域负责浏览器功能的特定区域：\n\n| 域 | 职责 | 典型事件 |\n|----|------|---------|\n| Page | 页面生命周期 | 加载事件、导航、对话框 |\n| Network | 网络活动 | 请求/响应监控、WebSockets |\n| DOM | 文档结构 | DOM 变更、属性修改 |\n| Fetch | 请求拦截 | 请求暂停、需要身份验证 |\n| Runtime | JavaScript 执行 | 控制台消息、异常 |\n| Browser | 浏览器管理 | 窗口创建、标签页、上下文 |\n\n每个域必须在发出事件之前显式启用，这有助于通过仅处理实际需要的事件来管理性能。\n\n## 域架构\n\n### 启用/禁用模式\n\n显式启用/禁用模式服务于几个重要的架构目的：\n\n1. **性能优化**：通过仅启用你感兴趣的域，减少事件处理的开销\n2. **资源管理**：某些事件域（如 Network 或 DOM 监控）可能产生大量消耗内存的事件\n3. **协议合规**：CDP 要求在发出事件之前显式启用域\n4. **受控清理**：显式禁用域确保在不再需要事件时进行适当的清理\n\n```mermaid\nstateDiagram-v2\n    [*] --> Disabled: Initial State\n    Disabled --> Enabled: enable_xxx_events()\n    Enabled --> Disabled: disable_xxx_events()\n    Enabled --> [*]: Tab Closed\n    Disabled --> [*]: Tab Closed\n```\n\n!!! warning \"事件泄漏防护\"\n    如果不再需要时未禁用事件域，可能导致内存泄漏和性能下降，特别是在长时间运行的自动化中。完成后始终禁用事件域，尤其是对于高容量事件（如网络监控）。\n\n### 域特定的启用方法\n\n不同的域通过适当对象上的特定方法启用：\n\n| 域 | 启用方法 | 禁用方法 | 可用对象 |\n|----|---------|---------|---------|\n| Page | `enable_page_events()` | `disable_page_events()` | Tab |\n| Network | `enable_network_events()` | `disable_network_events()` | Tab |\n| DOM | `enable_dom_events()` | `disable_dom_events()` | Tab |\n| Fetch | `enable_fetch_events()` | `disable_fetch_events()` | Tab, Browser |\n| File Chooser | `enable_intercept_file_chooser_dialog()` | `disable_intercept_file_chooser_dialog()` | Tab |\n\n!!! info \"域所有权\"\n    事件根据其功能属于特定域。某些域仅在某些级别可用 - 例如，Page 事件在 Tab 实例上可用，但在 Browser 级别不直接可用。\n\n## 事件注册系统\n\n### `on()` 方法\n\n订阅事件的核心方法是 `on()` 方法，在 Tab 和 Browser 实例上都可用：\n\n```python\nasync def on(\n    self, event_name: str, callback: callable, temporary: bool = False\n) -> int:\n    \"\"\"\n    注册事件监听器。\n\n    Args:\n        event_name (str): 要监听的事件名称。\n        callback (callable): 事件触发时要执行的回调函数。\n        temporary (bool): 如果为 True，回调将在触发一次后被删除。\n            默认为 False。\n\n    Returns:\n        int: 已注册回调的 ID。\n    \"\"\"\n```\n\n此方法返回一个回调 ID，如果需要，可以稍后用于删除回调。\n\n### 回调注册表\n\n在内部，`ConnectionHandler` 维护一个回调注册表：\n\n```python\n{\n    'Page.loadEventFired': [\n        (callback_id_1, callback_function_1, temporary=False),\n        (callback_id_2, callback_function_2, temporary=True),\n    ],\n    'Network.requestWillBeSent': [\n        (callback_id_3, callback_function_3, temporary=False),\n    ]\n}\n```\n\n当事件通过 WebSocket 到达时：\n\n1. 从消息中提取事件名称\n2. 查询注册表以获取匹配的回调\n3. 使用事件数据执行每个回调\n4. 执行后删除临时回调\n\n### 异步回调处理\n\n回调可以是同步的或异步的。事件系统处理两者：\n\n```python\nasync def _trigger_callbacks(self, event_name: str, event_data: dict):\n    for cb_id, cb_data in self._event_callbacks.items():\n        if cb_data['event'] == event_name:\n            if asyncio.iscoroutinefunction(cb_data['callback']):\n                await cb_data['callback'](event_data)\n            else:\n                cb_data['callback'](event_data)\n```\n\n异步回调按顺序等待。这意味着每个回调在下一个执行之前完成，这对以下方面很重要：\n\n- **可预测的执行顺序**：回调按注册顺序执行\n- **错误处理**：一个回调中的异常不会阻止其他回调执行\n- **状态一致性**：回调可以依赖于顺序的状态更改\n\n!!! info \"顺序执行 vs 并发执行\"\n    回调在同一事件内顺序执行。但是，不同的事件可以并发处理，因为事件循环同时处理多个连接。\n\n## 事件流和生命周期\n\n事件生命周期遵循以下步骤：\n\n```mermaid\nflowchart TD\n    A[Browser Activity] -->|Generates| B[CDP Event]\n    B -->|Sent via WebSocket| C[ConnectionHandler]\n    C -->|Filters by Event Name| D{Registered Callbacks?}\n    D -->|Yes| E[Process Event]\n    D -->|No| F[Discard Event]\n    E -->|For Each Callback| G[Execute Callback]\n    G -->|If Temporary| H[Remove Callback]\n    G -->|If Permanent| I[Retain for Future Events]\n```\n\n### 详细流程\n\n1. **浏览器活动**：浏览器中发生某些事情（页面加载、发送请求、DOM 变更）\n2. **CDP 事件生成**：浏览器生成 CDP 事件消息\n3. **WebSocket 传输**：消息通过 WebSocket 发送到 Pydoll\n4. **事件接收**：ConnectionHandler 接收事件\n5. **回调查找**：ConnectionHandler 在其注册表中检查与事件名称匹配的回调\n6. **回调执行**：如果存在回调，则使用事件数据执行每个回调\n7. **临时删除**：如果回调注册为临时回调，则在执行后将其删除\n\n## 浏览器级别 vs 标签页级别事件\n\nPydoll 的事件系统在浏览器和标签页级别运行，具有重要的区别：\n\n```mermaid\ngraph TD\n    Browser[Browser Instance] -->|\"Global Events (e.g., Target events)\"| BrowserCallbacks[Browser-Level Callbacks]\n    Browser -->|\"Creates\"| Tab1[Tab Instance 1]\n    Browser -->|\"Creates\"| Tab2[Tab Instance 2]\n    Tab1 -->|\"Tab-Specific Events\"| Tab1Callbacks[Tab 1 Callbacks]\n    Tab2 -->|\"Tab-Specific Events\"| Tab2Callbacks[Tab 2 Callbacks]\n```\n\n### 浏览器级别事件\n\n浏览器级别事件在所有标签页中全局操作。这些事件仅限于特定域，如：\n\n- **Target 事件**：标签页创建、销毁、崩溃\n- **Browser 事件**：窗口管理、下载协调\n\n```python\n# 浏览器级别事件注册\nawait browser.on('Target.targetCreated', handle_new_target)\n```\n\n浏览器级别的事件域是有限的，尝试使用标签页特定的事件将引发异常。\n\n### 标签页级别事件\n\n标签页级别事件特定于单个标签页：\n\n```python\n# 每个标签页都有自己的事件上下文\ntab1 = await browser.start()\ntab2 = await browser.new_tab()\n\nawait tab1.enable_page_events()\nawait tab1.on(PageEvent.LOAD_EVENT_FIRED, handle_tab1_load)\n\nawait tab2.enable_page_events()\nawait tab2.on(PageEvent.LOAD_EVENT_FIRED, handle_tab2_load)\n```\n\n此架构允许：\n\n- **隔离的事件处理**：一个标签页中的事件不会影响其他标签页\n- **每个标签页的配置**：不同的标签页可以监控不同的事件类型\n- **资源效率**：仅在需要的标签页上启用事件\n\n!!! info \"域特定范围\"\n    并非所有事件域在两个级别都可用：\n    \n    - **Fetch 事件**：在浏览器和标签页级别都可用\n    - **Page 事件**：仅在标签页级别可用\n    - **Target 事件**：仅在浏览器级别可用\n\n## 性能架构\n\n### 事件系统开销\n\n事件系统为浏览器自动化增加了开销，特别是对于高频事件：\n\n| 事件域 | 典型事件量 | 性能影响 |\n|--------|----------|---------|\n| Page | 低 | 最小 |\n| Network | 高 | 中等到高 |\n| DOM | 非常高 | 高 |\n| Fetch | 中等 | 中等（拦截时更高） |\n\n### 性能优化策略\n\n1. **选择性域启用**：仅启用你正在积极使用的事件域\n2. **战略范围**：仅对真正的浏览器范围的问题使用浏览器级别事件\n3. **及时禁用**：完成后始终禁用事件域\n4. **早期过滤**：在回调中，尽早过滤掉无关的事件\n5. **临时回调**：对一次性事件使用 `temporary=True` 标志\n\n### 内存管理\n\n事件系统通过几种机制管理内存：\n\n1. **回调注册表清理**：删除回调释放其引用\n2. **临时自动删除**：临时回调会自动清理\n3. **域禁用**：禁用域会停止事件生成\n4. **标签页关闭**：标签页关闭时，其所有回调会自动删除\n\n!!! warning \"内存泄漏防护\"\n    在长时间运行的自动化中，完成后始终清理回调并禁用域。高频事件（尤其是 DOM）如果保持启用状态，可能会累积大量内存。\n\n## Connection Handler 架构\n\n`ConnectionHandler` 是管理 WebSocket 通信和事件分发的核心组件。\n\n### 关键职责\n\n1. **WebSocket 管理**：建立和维护 WebSocket 连接\n2. **消息路由**：区分命令响应和事件\n3. **回调注册表**：维护事件名称到回调的映射\n4. **事件分发**：事件到达时执行注册的回调\n5. **清理**：删除回调并关闭连接\n\n### 内部结构\n\n```python\nclass ConnectionHandler:\n    def __init__(self, ...):\n        self._events_handler = EventsManager()\n        self._websocket = None\n        # ... other attributes\n    \n    async def register_callback(self, event_name, callback, temporary):\n        return self._events_handler.register_callback(event_name, callback, temporary)\n\nclass EventsManager:\n    def __init__(self):\n        self._event_callbacks = {}  # Callback ID -> callback data\n        self._callback_id = 0\n    \n    def register_callback(self, event_name, callback, temporary):\n        self._callback_id += 1\n        self._event_callbacks[self._callback_id] = {\n            'event': event_name,\n            'callback': callback,\n            'temporary': temporary\n        }\n        return self._callback_id\n    \n    async def _trigger_callbacks(self, event_name, event_data):\n        callbacks_to_remove = []\n        \n        for cb_id, cb_data in self._event_callbacks.items():\n            if cb_data['event'] == event_name:\n                # Execute callback (await if async, call directly if sync)\n                if asyncio.iscoroutinefunction(cb_data['callback']):\n                    await cb_data['callback'](event_data)\n                else:\n                    cb_data['callback'](event_data)\n                \n                # Mark temporary callbacks for removal\n                if cb_data['temporary']:\n                    callbacks_to_remove.append(cb_id)\n        \n        # Remove temporary callbacks after all callbacks executed\n        for cb_id in callbacks_to_remove:\n            self.remove_callback(cb_id)\n```\n\n此架构确保：\n\n- **高效查找**：事件名称直接映射到回调列表\n- **最小开销**：仅处理已注册的事件\n- **自动清理**：临时回调在执行后被删除\n- **线程安全**：操作是异步安全的\n\n## 事件消息格式\n\nCDP 事件遵循标准化的消息格式：\n\n```json\n{\n    \"method\": \"Network.requestWillBeSent\",\n    \"params\": {\n        \"requestId\": \"1234.56\",\n        \"loaderId\": \"7890.12\",\n        \"documentURL\": \"https://example.com\",\n        \"request\": {\n            \"url\": \"https://api.example.com/data\",\n            \"method\": \"GET\",\n            \"headers\": {...}\n        },\n        \"timestamp\": 123456.789,\n        \"wallTime\": 1234567890.123,\n        \"initiator\": {...},\n        \"type\": \"XHR\"\n    }\n}\n```\n\n关键组件：\n\n- **`method`**：`Domain.eventName` 格式的事件名称\n- **`params`**：事件特定数据，因事件类型而异\n- **无 `id` 字段**：与命令不同，事件没有请求 ID\n\n事件系统提取 `method` 字段以路由到适当的回调，将整个消息传递给每个回调。\n\n## 多标签页事件协调\n\nPydoll 的架构支持复杂的多标签页事件协调：\n\n### 独立标签页上下文\n\n每个标签页维护自己的：\n\n- 事件域启用状态\n- 回调注册表\n- 事件通信通道\n- 网络日志（如果启用了网络事件）\n\n!!! info \"通信架构\"\n    每个标签页都有自己与浏览器的事件通信通道。有关 WebSocket 连接和目标 ID 在协议级别如何工作的技术细节，请参阅 [浏览器域架构](./browser-domain.md)。\n\n### 共享浏览器上下文\n\n多个标签页可以共享：\n\n- 浏览器级别事件监听器\n- Cookie 存储\n- 缓存\n- 浏览器进程\n\n此架构允许：\n\n1. **并行事件处理**：多个标签页可以同时处理事件\n2. **隔离的故障**：一个标签页中的问题不会影响其他标签页\n3. **资源共享**：高效共享常见的浏览器功能\n4. **协调操作**：浏览器级别事件可以协调跨标签页活动\n\n## 结论\n\nPydoll 的事件系统架构旨在：\n\n- **性能**：通过选择性域启用和高效回调分发实现最小开销\n- **灵活性**：支持浏览器级别和标签页级别事件\n- **可扩展性**：使用独立的事件上下文处理多个标签页\n- **可靠性**：自动清理和内存管理\n\n理解此架构可以帮助你：\n\n- **优化性能**：了解哪些域具有高开销\n- **调试问题**：当事情不按预期工作时理解事件流\n- **设计更好的自动化**：利用架构实现高效的事件驱动工作流\n- **避免陷阱**：防止内存泄漏和性能下降\n\n有关实际使用模式和示例，请参阅 [事件系统指南](../features/advanced/event-system.md)。\n\n"
  },
  {
    "path": "docs/zh/deep-dive/architecture/find-elements-mixin.md",
    "content": "# FindElements Mixin 架构\n\nFindElementsMixin 代表了 Pydoll 中的一个关键架构决策：使用**组合优于继承**在 `Tab` 和 `WebElement` 之间共享元素查找能力，而不通过公共基类耦合它们。本文档探讨 mixin 模式、其实现以及元素定位的内部机制。\n\n!!! info \"实用使用指南\"\n    有关实际示例和使用模式，请参阅[元素查找指南](../features/automation/element-finding.md)和[选择器指南](./selectors-guide.md)。\n\n## Mixin 模式：设计理念\n\n### 什么是 Mixin？\n\nMixin 是一个旨在**向其他类提供方法**的类，而不是传统继承层次结构中的基类。与标准继承（建模\"is-a\"关系）不同，mixin 建模**\"can-do\"能力**。\n\n```python\n# 传统继承：\"is-a\"\nclass Animal:\n    def breathe(self): ...\n\nclass Dog(Animal):  # Dog IS-A Animal（狗是一种动物）\n    def bark(self): ...\n\n# Mixin 模式：\"can-do\"\nclass FlyableMixin:\n    def fly(self): ...\n\nclass Bird(Animal, FlyableMixin):  # Bird IS-A Animal, CAN fly（鸟是动物，能飞）\n    pass\n```\n\n### 为什么使用 Mixin 而不是继承？\n\nPydoll 面临特定的架构挑战：\n\n- **`Tab`** 需要在**文档上下文**中查找元素\n- **`WebElement`** 需要**相对于自身**查找元素（子元素）\n- 两者都需要**相同的选择器逻辑**（CSS、XPath、属性构建）\n\n**选项 1：共享基类**\n\n```python\nclass ElementLocator:\n    def find(...): ...\n\nclass Tab(ElementLocator):\n    pass\n\nclass WebElement(ElementLocator):\n    pass\n```\n\n**问题：**\n- 紧耦合：`Tab` 和 `WebElement` 现在共享继承层次结构\n- 违反单一职责：`Tab` 不应该从与 `WebElement` 相同的类继承\n- 难以扩展：添加新功能需要修改基类\n\n**选项 2：Mixin 模式（选定方法）**\n\n```python\nclass FindElementsMixin:\n    def find(...): ...\n    def query(...): ...\n\nclass Tab(FindElementsMixin):\n    # Tab 特定逻辑\n    pass\n\nclass WebElement(FindElementsMixin):\n    # WebElement 特定逻辑\n    pass\n```\n\n**优点：**\n\n- **解耦**：`Tab` 和 `WebElement` 保持独立\n- **可重用性**：两个类中使用相同的元素查找逻辑\n- **可组合性**：可以添加其他 mixin 而不会冲突\n- **可测试性**：Mixin 可以单独测试\n\n!!! tip \"Mixin 特性\"\n    1. **无状态**：Mixin 不维护自己的状态（没有 `__init__`）\n    2. **依赖注入**：假定使用类提供依赖项（例如 `_connection_handler`）\n    3. **单一目的**：每个 mixin 提供一个内聚的能力\n    4. **不可实例化**：永远不要直接创建 `FindElementsMixin()`\n\n## Pydoll 中的 Mixin 实现\n\n### 类结构\n\nFindElementsMixin 使用**依赖注入**与提供 `_connection_handler` 的任何类一起工作：\n\n```python\nclass FindElementsMixin:\n    \"\"\"\n    提供元素查找能力的 Mixin。\n    \n    假定使用类具有：\n    - _connection_handler: 用于 CDP 命令的 ConnectionHandler 实例\n    - _object_id: 用于上下文相对搜索的 Optional[str]（仅 WebElement）\n    \"\"\"\n    \n    if TYPE_CHECKING:\n        _connection_handler: ConnectionHandler  # 类型提示，不是实际属性\n    \n    async def find(self, ...):\n        # 实现使用 self._connection_handler\n        # 检查 self._object_id 以确定上下文\n```\n\n**关键见解：** Mixin 不定义 `_connection_handler` 或 `_object_id`。它通过鸭子类型**假定**它们存在。\n\n### Tab 和 WebElement 如何使用 Mixin\n\n```python\n# Tab：文档级搜索\nclass Tab(FindElementsMixin):\n    def __init__(self, browser, target_id, connection_port):\n        self._connection_handler = ConnectionHandler(connection_port)\n        # 没有 _object_id → 从文档根开始搜索\n\n# WebElement：元素相对搜索\nclass WebElement(FindElementsMixin):\n    def __init__(self, object_id, connection_handler, ...):\n        self._object_id = object_id  # CDP 对象 ID\n        self._connection_handler = connection_handler\n        # 有 _object_id → 相对于此元素搜索\n```\n\n**关键区别：**\n\n- **Tab**：`hasattr(self, '_object_id')` → `False` → 使用 `RuntimeCommands.evaluate()`（文档上下文）\n- **WebElement**：`hasattr(self, '_object_id')` → `True` → 使用 `RuntimeCommands.call_function_on()`（元素上下文）\n\n### 上下文检测\n\nMixin 动态检测搜索上下文：\n\n```python\nasync def _find_element(self, by, value, raise_exc=True):\n    if hasattr(self, '_object_id'):\n        # 相对搜索：在此元素上调用 JavaScript 函数\n        command = self._get_find_element_command(by, value, self._object_id)\n    else:\n        # 文档搜索：在全局上下文中评估 JavaScript\n        command = self._get_find_element_command(by, value)\n    \n    response = await self._execute_command(command)\n    # ...\n```\n\n这个单一实现处理两者：\n\n- `tab.find(id='submit')` → 搜索整个文档\n- `form_element.find(id='submit')` → 在 `form_element` 内搜索\n\n!!! warning \"Mixin 依赖耦合\"\n    Mixin **紧密耦合**到 CDP 的对象模型。它假定：\n    \n    - 元素由 `objectId` 字符串表示\n    - 文档搜索使用 `Runtime.evaluate()`\n    - 元素相对搜索使用 `Runtime.callFunctionOn()`\n    \n    这是可以接受的，因为 Pydoll 是 **CDP 特定的**。更通用的设计需要抽象层。\n\n## 公共 API 设计\n\nMixin 暴露两个具有不同设计理念的高级方法：\n\n### find()：基于属性的选择\n\n```python\n@overload\nasync def find(self, find_all: Literal[False], ...) -> WebElement: ...\n\n@overload\nasync def find(self, find_all: Literal[True], ...) -> list[WebElement]: ...\n\nasync def find(\n    self,\n    id: Optional[str] = None,\n    class_name: Optional[str] = None,\n    name: Optional[str] = None,\n    tag_name: Optional[str] = None,\n    text: Optional[str] = None,\n    timeout: int = 0,\n    find_all: bool = False,\n    raise_exc: bool = True,\n    **attributes,\n) -> Union[WebElement, list[WebElement], None]:\n```\n\n**设计决策：**\n\n1. **Kwargs 优于位置 By 枚举**：\n   ```python\n   # Pydoll（直观）\n   await tab.find(id='submit', class_name='primary')\n   \n   # Selenium（冗长）\n   driver.find_element(By.ID, 'submit')  # 不容易组合属性\n   ```\n\n2. **自动解析为最佳选择器**：\n   - 单个属性 → 使用 `By.ID`、`By.CLASS_NAME` 等（最快）\n   - 多个属性 → 构建 XPath（灵活但较慢）\n\n3. **`**attributes` 用于扩展性**：\n   ```python\n   await tab.find(data_testid='submit-btn', aria_label='Submit form')\n   # 构建：//\\*[@data-testid='submit-btn' and @aria-label='Submit form']\n   ```\n\n### query()：基于表达式的选择\n\n```python\n@overload\nasync def query(self, expression, find_all: Literal[False], ...) -> WebElement: ...\n\n@overload\nasync def query(self, expression, find_all: Literal[True], ...) -> list[WebElement]: ...\n\nasync def query(\n    self, \n    expression: str, \n    timeout: int = 0, \n    find_all: bool = False, \n    raise_exc: bool = True\n) -> Union[WebElement, list[WebElement], None]:\n```\n\n**设计决策：**\n\n1. **自动检测 CSS vs XPath**：\n   ```python\n   # XPath 检测（以 / 或 ./ 开头）\n   await tab.query(\"//div[@id='content']\")\n   \n   # CSS 检测（默认）\n   await tab.query(\"div#content > p.intro\")\n   ```\n\n2. **单个表达式参数**（与 `find()` 不同）：\n   - 假定用户知道选择器语法\n   - 没有抽象开销\n\n3. **直接传递到浏览器**：\n   - CSS 使用 `querySelector()` / `querySelectorAll()`\n   - XPath 使用 `document.evaluate()`\n\n### 类型安全的重载模式\n\n两种方法都使用 `@overload` 提供**精确的返回类型**：\n\n```python\n# IDE 知道返回类型是 WebElement\nelement = await tab.find(id='submit')\n\n# IDE 知道返回类型是 list[WebElement]\nelements = await tab.find(class_name='item', find_all=True)\n\n# IDE 知道返回类型是 Optional[WebElement]\nmaybe_element = await tab.find(id='optional', raise_exc=False)\n```\n\n这对于 IDE 自动完成和类型检查至关重要。有关详细信息，请参阅[类型系统深入了解](./typing-system.md)。\n\n## 选择器解析架构\n\nMixin 通过解析管道将用户输入转换为 CDP 命令：\n\n| 阶段 | 输入 | 输出 | 关键决策 |\n|-------|-------|--------|-------------|\n| **1. 方法选择** | `find()` kwargs 或 `query()` 表达式 | 选择器策略 | 基于属性 vs 基于表达式 |\n| **2. 策略解析** | 属性或表达式 | `By` 枚举 + 值 | 单个属性 → 原生方法，多个 → XPath |\n| **3. 上下文检测** | `By` + 值 + `hasattr(_object_id)` | CDP 命令类型 | 文档 vs 元素相对搜索 |\n| **4. 命令生成** | CDP 命令类型 + 选择器 | JavaScript + CDP 方法 | `evaluate()` vs `callFunctionOn()` |\n| **5. 执行** | CDP 命令 | `objectId` 或 `objectId` 数组 | 通过 ConnectionHandler |\n| **6. WebElement 创建** | `objectId` + 属性 | `WebElement` 实例 | 工厂函数避免循环导入 |\n\n### 关键架构决策\n\n**1. 单个 vs 多个属性**\n\n```python\n# 单个属性 → 直接选择器（快速）\nawait tab.find(id='username')  # 使用 By.ID → getElementById()\n\n# 多个属性 → XPath（灵活）\nawait tab.find(tag_name='input', type='password', name='pwd')\n# → //input[@type='password' and @name='pwd']\n```\n\n**为什么这很重要：**\n- 原生方法（`getElementById`、`getElementsByClassName`）比 XPath 快 10-50%\n- 组合属性时 XPath 开销可接受（无替代方案）\n\n**2. 选择器类型的自动检测**\n\n```python\nawait tab.query(\"//div\")       # 以 / 开头 → XPath\nawait tab.query(\"#login\")      # 默认 → CSS\n```\n\n**实现：**\n```python\nif expression.startswith(('./', '/', '(/')):\n    return By.XPATH\nreturn By.CSS_SELECTOR\n```\n\n启发式是**明确的** - CSS 选择器不能以 `/` 开头。\n\n**3. XPath 相对路径调整**\n\n对于元素相对搜索，绝对 XPath 必须转换：\n\n```python\n# 用户提供：//div\n# 对于 WebElement：.//div（相对于元素，而不是文档）\n\ndef _ensure_relative_xpath(xpath):\n    return f'.{xpath}' if not xpath.startswith('.') else xpath\n```\n\n没有这个，`element.find()` 将从文档根开始搜索。\n\n## CDP 命令生成\n\nMixin 根据搜索上下文路由到不同的 CDP 方法：\n\n| 上下文 | 选择器类型 | CDP 方法 | JavaScript 等价 |\n|---------|--------------|------------|---------------------|\n| 文档 | CSS | `Runtime.evaluate` | `document.querySelector()` |\n| 文档 | XPath | `Runtime.evaluate` | `document.evaluate()` |\n| 元素 | CSS | `Runtime.callFunctionOn` | `this.querySelector()` |\n| 元素 | XPath | `Runtime.callFunctionOn` | `document.evaluate(..., this)` |\n\n**关键见解：** `Runtime.callFunctionOn` 需要一个 `objectId`（要调用的元素），而 `Runtime.evaluate` 在全局范围内执行。\n\n### JavaScript 模板\n\nPydoll 使用预定义的模板以保持一致性和性能：\n\n```python\n# CSS 选择器\nScripts.QUERY_SELECTOR = 'document.querySelector(\"{selector}\")'\nScripts.RELATIVE_QUERY_SELECTOR = 'this.querySelector(\"{selector}\")'\n\n# XPath 表达式\nScripts.FIND_XPATH_ELEMENT = '''\n    document.evaluate(\"{escaped_value}\", document, null,\n                      XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue\n'''\n```\n\n模板避免运行时字符串连接并集中 JavaScript 代码。\n\n## 对象 ID 解析和 WebElement 创建\n\nCDP 将 DOM 节点表示为 **`objectId` 字符串**。Mixin 抽象了这一点：\n\n**单个元素流程：**\n1. 执行 CDP 命令 → 从响应中提取 `objectId`\n2. 调用 `DOM.describeNode(objectId)` → 获取属性、标签名\n3. 创建 `WebElement(objectId, connection_handler, attributes)`\n\n**多个元素流程：**\n1. 执行 CDP 命令 → 返回**作为单个远程对象的数组**\n2. 调用 `Runtime.getProperties(array_objectId)` → 枚举数组索引\n3. 为每个元素提取单独的 `objectId`\n4. 描述并为每个创建 `WebElement`\n\n**为什么使用 `Runtime.getProperties`？** CDP 不直接返回数组 - 它返回对数组对象的**引用**。我们必须枚举其属性以提取单个元素。\n\n## 架构见解和设计权衡\n\n### 为什么使用 Kwargs 而不是 By 枚举？\n\n**Pydoll 的选择：**\n```python\nawait tab.find(id='submit', class_name='primary')\n```\n\n**Selenium 的方法：**\n```python\ndriver.find_element(By.ID, 'submit')  # 不能组合属性\n```\n\n**理由：**\n\n- **可发现性**：IDE 自动完成显示所有可用参数\n- **可组合性**：可以在一次调用中组合多个属性\n- **可读性**：`id='submit'` 比 `(By.ID, 'submit')` 更直观\n\n**权衡：** Kwargs 对选择器策略不够明确。通过文档和日志记录解决。\n\n### 为什么自动检测 CSS vs XPath？\n\n`_get_expression_type()` 启发式消除了用户负担：\n\n```python\nawait tab.query(\"//div\")       # 自动：XPath\nawait tab.query(\"#login\")      # 自动：CSS\nawait tab.query(\"div > p\")     # 自动：CSS\n```\n\n**优点：**\n\n- **人体工程学**：用户不需要指定选择器类型\n- **正确性**：不可能误用（使用 CSS 方法的 XPath，反之亦然）\n\n**限制：** 无法强制对模糊选择器进行 CSS 解释（罕见的边缘情况）。\n\n### 防止循环导入：create_web_element()\n\nMixin 使用**工厂函数**来避免循环导入：\n\n```python\ndef create_web_element(*args, **kwargs):\n    \"\"\"在运行时动态导入 WebElement。\"\"\"\n    from pydoll.elements.web_element import WebElement  # 延迟导入\n    return WebElement(*args, **kwargs)\n```\n\n**为什么需要？**\n\n- `FindElementsMixin` → 需要创建 `WebElement`\n- `WebElement` → 从 `FindElementsMixin` 继承\n- 循环依赖！\n\n**解决方案：** 工厂函数内的延迟导入。导入仅在调用函数时执行，打破循环。\n\n### hasattr() 进行上下文检测：优雅还是 Hacky？\n\nMixin 使用 `hasattr(self, '_object_id')` 检测 Tab vs WebElement：\n\n```python\nif hasattr(self, '_object_id'):\n    # WebElement：元素相对搜索\nelse:\n    # Tab：文档级搜索\n```\n\n**这是\"hacky\"吗？**\n\n- **不**：这是**鸭子类型**（Pythonic 习语）\n- Mixin 不需要知道类层次结构\n- Tab 和 WebElement 都提供 `_connection_handler`\n- WebElement 另外提供 `_object_id`\n\n**替代方法：**\n\n1. **类型检查**：`if isinstance(self, WebElement)` → 将 mixin 耦合到 WebElement\n2. **抽象方法**：要求 Tab/WebElement 实现 `get_search_context()` → 更多样板代码\n3. **依赖注入**：将上下文作为参数传递 → 破坏 API 人体工程学\n\n**结论：** `hasattr()` 是此用例的最佳解决方案。\n\n## 关键要点\n\n1. **Mixin 实现代码共享**，而不通过继承耦合 `Tab` 和 `WebElement`\n2. **通过鸭子类型进行上下文检测**（`hasattr`）使 mixin 与类层次结构解耦\n3. **自动解析优化性能**，通过对单个属性使用原生方法\n4. **XPath 构建提供可组合性**用于多属性查询\n5. **基于轮询的等待很简单**，但以 CPU 周期换取实现简单性\n6. **CDP 对象模型复杂性**隐藏在 WebElement 抽象后面\n7. **通过重载实现类型安全**为 IDE 支持提供精确的返回类型\n\n## 相关文档\n\n要更深入地了解相关架构组件：\n\n- **[类型系统](./typing-system.md)**：重载模式、TypedDict、泛型类型\n- **[WebElement 域](./webelement-domain.md)**：WebElement 架构和交互方法\n- **[选择器指南](./selectors-guide.md)**：CSS vs XPath 语法和最佳实践\n- **[Tab 域](./tab-domain.md)**：Tab 级操作和上下文管理\n\n有关实际使用模式：\n\n- **[元素查找指南](../features/automation/element-finding.md)**：实际示例和模式\n- **[类人交互](../features/automation/human-interactions.md)**：真实的元素交互\n"
  },
  {
    "path": "docs/zh/deep-dive/architecture/index.md",
    "content": "# 内部架构\n\n**理解设计，然后有意识地打破规则。**\n\n大多数文档向你展示框架**做什么**。本节揭示 Pydoll **如何**以及**为什么**以这种方式构建：塑造每一行代码的设计模式、架构决策和权衡。\n\n## 为什么架构很重要\n\n你可以在不理解其内部架构的情况下有效地使用 Pydoll。但当你需要：\n\n- **调试**跨多个组件的复杂问题\n- **优化**大规模自动化中的性能瓶颈\n- **扩展** Pydoll 的自定义功能\n- **贡献**对代码库的改进\n- **构建**针对不同用例的类似工具\n\n...架构知识变得**不可或缺**。\n\n!!! quote \"架构即语言\"\n    **\"建筑是凝固的音乐。\"** - 约翰·沃尔夫冈·冯·歌德\n    \n    良好的架构不仅仅是让代码工作，更是让代码**可理解**、**可维护**和**可扩展**。理解 Pydoll 的架构将教会你适用于每个项目的模式。\n\n## 六大架构域\n\nPydoll 的架构组织为**六个内聚域**，每个域都有明确的职责和接口：\n\n### 1. 浏览器域\n**[→ 探索浏览器架构](./browser-domain.md)**\n\n**协调者：管理进程、上下文和全局状态。**\n\n浏览器域位于层次结构的顶部，协调：\n\n- **进程管理**：启动/终止浏览器可执行文件\n- **浏览器上下文**：隔离环境（如隐私窗口）\n- **标签页注册表**：Tab 实例的单例模式\n- **代理认证**：通过 Fetch 域自动认证\n- **全局操作**：下载、权限、窗口管理\n\n**关键架构模式**：\n\n- **抽象基类**，适用于 Chrome/Edge/其他 Chromium 浏览器\n- **管理器模式**（ProcessManager、ProxyManager、TempDirManager）\n- **单例注册表**用于 Tab 实例（防止重复）\n- **上下文管理器协议**用于自动清理\n\n**关键洞察**：浏览器不直接操作页面，它**协调**低级组件。这种关注点分离使多浏览器支持和并发标签操作成为可能。\n\n---\n\n### 2. 标签页域\n**[→ 探索标签页架构](./tab-domain.md)**\n\n**主力军：执行命令、管理状态、协调自动化。**\n\n标签页域是 Pydoll 的主要接口，处理：\n\n- **导航**：具有可配置等待状态的页面加载\n- **元素查找**：委托给 FindElementsMixin\n- **JavaScript 执行**：页面和元素上下文\n- **事件协调**：特定于标签页的事件监听器\n- **网络监控**：请求/响应捕获和分析\n- **IFrame 处理**：嵌套上下文管理\n\n**关键架构模式**：\n\n- **外观模式**：简化的 CDP 复杂操作接口\n- **混入组合**：FindElementsMixin 用于元素定位\n- **每标签页 WebSocket**：并行的独立连接\n- **状态标志**：跟踪启用的域（network_events_enabled 等）\n- **延迟初始化**：首次访问时创建请求对象\n\n**关键洞察**：每个 Tab 拥有自己的 **ConnectionHandler**，实现跨标签的真正并行操作，无需竞争或状态泄漏。\n\n---\n\n### 3. WebElement 域\n**[→ 探索 WebElement 架构](./webelement-domain.md)**\n\n**交互器：连接 Python 代码和 DOM 元素。**\n\nWebElement 域表示**单个 DOM 元素**，提供：\n\n- **交互方法**：点击、输入、滚动、选择\n- **属性访问**：文本、HTML、边界、属性\n- **状态查询**：可见性、启用状态、值\n- **截图**：特定于元素的图像捕获\n- **子元素查找**：相对元素定位（也通过 FindElementsMixin）\n\n**关键架构模式**：\n\n- **代理模式**：表示远程浏览器元素的 Python 对象\n- **对象 ID 抽象**：CDP 的 objectId 隐藏在 Python API 后面\n- **混合属性**：同步（属性）vs 异步（动态状态）\n- **命令模式**：交互方法包装 CDP 命令\n- **回退策略**：多种方法提高鲁棒性\n\n**关键洞察**：WebElement 维护**缓存的属性**（从创建时）和**动态状态**（按需获取），平衡性能与新鲜度。\n\n---\n\n### 4. FindElements 混入\n**[→ 探索 FindElements 架构](./find-elements-mixin.md)**\n\n**定位器：将选择器转换为 DOM 查询。**\n\nFindElementsMixin 通过**组合**而非继承为 Tab 和 WebElement 提供元素查找功能：\n\n- **基于属性的查找**：`find(id='submit', class_name='btn')`\n- **基于表达式的查询**：`query('div.container > p')`\n- **策略解析**：针对单个或多个属性的最优选择器\n- **等待机制**：具有可配置超时的轮询\n- **上下文检测**：文档 vs 元素相对搜索\n\n**关键架构模式**：\n- **混入模式**：无需继承层次结构的共享功能\n- **策略模式**：基于输入的不同选择器策略\n- **模板方法**：通用流程，特定于策略的实现\n- **工厂函数**：延迟导入以避免循环依赖\n- **重载模式**：类型安全的返回类型（WebElement vs list）\n\n**关键洞察**：混入使用**鸭子类型**（`hasattr(self, '_object_id')`）来检测 Tab vs WebElement，实现代码重用而不紧密耦合。\n\n---\n\n### 5. 事件架构\n**[→ 探索事件架构](./event-architecture.md)**\n\n**调度器：将浏览器事件路由到 Python 回调。**\n\n事件架构通过以下方式实现响应式自动化：\n\n- **事件注册**：`on()` 方法订阅 CDP 事件\n- **回调调度**：异步执行不阻塞\n- **域管理**：显式启用/禁用以提高性能\n- **临时回调**：首次调用后自动删除\n- **多级作用域**：浏览器范围 vs 标签页特定事件\n\n**关键架构模式**：\n\n- **观察者模式**：订阅/通知事件驱动代码\n- **注册表模式**：事件名称 → 回调列表映射\n- **包装器模式**：自动包装同步回调以进行异步执行\n- **清理协议**：标签页关闭时自动删除回调\n- **作用域隔离**：每个标签页独立的事件上下文\n\n**关键洞察**：事件是**推送式**的（浏览器通知 Python），而非轮询式，实现低延迟响应式自动化，无需忙等待。\n\n---\n\n### 6. 浏览器请求架构\n**[→ 探索请求架构](./browser-requests-architecture.md)**\n\n**混合体：具有浏览器会话状态的 HTTP 请求。**\n\n浏览器请求系统连接 HTTP 和浏览器自动化：\n\n- **会话连续性**：自动包含 Cookie 和认证\n- **双重数据源**：JavaScript Fetch API + CDP 网络事件\n- **完整元数据**：标头、Cookie、时间（并非所有通过 JavaScript 可用）\n- **类 `requests` API**：具有浏览器能力的熟悉接口\n\n**关键架构模式**：\n\n- **混合执行**：JavaScript 获取主体，CDP 获取元数据\n- **临时事件注册**：启用/捕获/禁用模式\n- **延迟属性初始化**：首次使用时创建请求对象\n- **适配器模式**：与浏览器 fetch 兼容的 Requests 接口\n\n**关键洞察**：浏览器请求结合**两个信息源**（JavaScript 和 CDP 事件）。JavaScript 提供响应主体，CDP 提供 JavaScript 安全策略隐藏的标头和 Cookie。\n\n---\n\n## 架构原则\n\n这六个域遵循一致的原则：\n\n### 1. 关注点分离\n每个域都有一个**单一、明确定义的职责**：\n\n- Browser → 进程/上下文管理\n- Tab → 命令执行和状态\n- WebElement → 元素交互\n- FindElements → 元素定位\n- Events → 响应式调度\n- Requests → 浏览器上下文中的 HTTP\n\n**优势**：一个域的更改很少需要更改其他域。\n\n### 2. 组合优于继承\nPydoll 使用以下方式而非深层继承层次结构：\n\n- **混入**（Tab 和 WebElement 共享 FindElementsMixin）\n- **管理器**（ProcessManager、ProxyManager、TempDirManager）\n- **依赖注入**（ConnectionHandler 传递给组件）\n\n**优势**：灵活的组件重用而不紧密耦合。\n\n### 3. 默认异步\n所有 I/O 操作都是 `async def` 并且必须 `await`：\n\n- WebSocket 通信\n- CDP 命令执行\n- 事件回调调度\n- 网络请求\n\n**优势**：实现多个标签页的真正并发、并行操作和非阻塞 I/O。\n\n### 4. 类型安全\n每个公共 API 都有类型注解：\n\n- 函数参数和返回类型\n- 作为 `TypedDict` 的 CDP 响应\n- 回调参数的事件类型\n- 多态方法的重载\n\n**优势**：IDE 自动完成、静态类型检查、自文档化代码。\n\n### 5. 资源管理\n上下文管理器确保清理：\n\n- `async with Browser()` → 退出时关闭浏览器\n- `async with tab.expect_file_chooser()` → 禁用拦截器\n- `async with tab.expect_download()` → 清理临时文件\n\n**优势**：自动资源清理，即使在异常情况下也能防止泄漏。\n\n## 组件交互\n\n理解域如何交互是关键：\n\n```mermaid\ngraph TB\n    User[你的 Python 代码]\n    \n    User --> Browser[浏览器域]\n    User --> Tab[标签页域]\n    User --> Element[WebElement 域]\n    \n    Browser --> ProcessMgr[进程管理器]\n    Browser --> ContextMgr[上下文管理器]\n    Browser --> TabRegistry[标签页注册表]\n    \n    Tab --> ConnHandler[连接处理器]\n    Tab --> FindMixin[FindElements 混入]\n    Tab --> EventSystem[事件系统]\n    Tab --> RequestSystem[请求系统]\n    \n    Element --> ConnHandler2[连接处理器]\n    Element --> FindMixin2[FindElements 混入]\n    \n    ConnHandler --> WebSocket[WebSocket 到 CDP]\n    ConnHandler2 --> WebSocket\n    EventSystem --> ConnHandler\n    RequestSystem --> ConnHandler\n    RequestSystem --> EventSystem\n    \n    WebSocket --> Chrome[Chrome 浏览器]\n```\n\n**关键交互**：\n\n1. **Browser 创建 Tabs** → Tabs 存储在注册表中\n2. **Tab 和 WebElement 都使用 FindElementsMixin** → 共享元素定位\n3. **每个 Tab 拥有一个 ConnectionHandler** → 独立的 WebSocket 连接\n4. **请求系统使用事件系统** → 网络事件捕获元数据\n5. **所有组件都使用 ConnectionHandler** → 集中式 CDP 通信\n\n## 先决条件\n\n要充分受益于本节：\n\n- **[核心基础](../fundamentals/cdp.md)** - 理解 CDP、异步和类型\n- **Python 设计模式** - 熟悉常见模式\n- **OOP 概念** - 类、继承、组合、接口\n- **异步 Python** - 熟悉 `async def` 和 `await`  \n\n**如果你还没有阅读基础知识**，请先从那里开始。架构建立在这些概念之上。\n\n## 超越架构\n\n掌握内部架构后，你将准备好：\n\n- **贡献代码**：了解新功能的适配位置\n- **性能优化**：识别瓶颈和低效率\n- **自定义扩展**：基于 Pydoll 的模式构建\n- **类似工具**：将这些模式应用于其他项目\n\n## 设计哲学\n\n良好的架构是**不可见的**，它不应该妨碍你。Pydoll 的架构优先考虑：\n\n1. **简单性**：每个组件做好一件事\n2. **一致性**：类似的操作有类似的模式\n3. **明确性**：没有魔法，没有隐藏行为\n4. **类型安全**：在设计时而非运行时捕获错误\n5. **性能**：默认异步，无锁并行\n\n这些不是任意选择，它们是几十年软件工程**经过实战检验的原则**。\n\n---\n\n## 准备好理解设计了吗？\n\n从**[浏览器域](./browser-domain.md)**开始，了解进程管理和上下文隔离如何工作，然后按顺序浏览各个域。\n\n**这是从使用到精通的转变。**\n\n---\n\n!!! success \"完成架构学习后\"\n    一旦你理解了这些模式，你会在软件工程的各个地方看到它们，而不仅仅是 Pydoll。这些是应用于浏览器自动化的**通用模式**：\n    \n    - 外观模式（Tab 简化 CDP 复杂性）\n    - 观察者模式（用于响应式代码的事件系统）\n    - 混入模式（FindElementsMixin 用于代码重用）\n    - 注册表模式（Browser 跟踪 Tab 实例）\n    - 策略模式（FindElements 解析最优选择器）\n    \n    良好的架构是**永恒的知识**。\n"
  },
  {
    "path": "docs/zh/deep-dive/architecture/shadow-dom.md",
    "content": "# Shadow DOM 架构\n\nShadow DOM 是现代 Web 自动化中最具挑战性的方面之一。Shadow 树内的元素对常规 DOM 查询不可见，这打破了传统的自动化方法。本文档解释了 Shadow DOM 在浏览器层面的工作原理，为什么传统工具无法处理封闭的 shadow root，以及 Pydoll 如何通过直接的 CDP 访问绕过这些限制。\n\n!!! info \"实用指南\"\n    有关使用示例和快速入门模式，请参阅 [元素查找指南 — Shadow DOM 部分](../../features/element-finding.md#shadow-dom-支持)。\n\n## 什么是 Shadow DOM？\n\nShadow DOM 是一项实现 **DOM 封装** 的 Web 标准。它允许组件拥有自己的隔离 DOM 树（\"shadow 树\"），附加到常规 DOM 元素（\"shadow 宿主\"）上。Shadow 树内的元素对主文档的查询是隐藏的。\n\n```mermaid\ngraph TB\n    subgraph \"主 DOM（Light DOM）\"\n        Document[\"document\"]\n        Host[\"div#my-component\\n(shadow 宿主)\"]\n        Other[\"p.normal-content\"]\n    end\n\n    subgraph \"Shadow 树（封装的）\"\n        SR[\"#shadow-root (open)\"]\n        Style[\"style\"]\n        Button[\"button.internal\"]\n        Input[\"input.private\"]\n    end\n\n    Document --> Host\n    Document --> Other\n    Host -.->|\"attachShadow()\"| SR\n    SR --> Style\n    SR --> Button\n    SR --> Input\n```\n\n### Shadow Root 模式\n\n当组件通过 `attachShadow()` 创建 shadow root 时，它指定一个 **模式**：\n\n| 模式 | JavaScript 访问 | CDP 访问 | 常见用途 |\n|------|-----------------|----------|----------|\n| `open` | `element.shadowRoot` 返回根节点 | 通过 `backendNodeId` 完全访问 | 自定义 Web 组件（Lit、Stencil） |\n| `closed` | `element.shadowRoot` 返回 `null` | 通过 `backendNodeId` 完全访问 | 安全敏感组件、支付表单 |\n| `user-agent` | 无法通过 JS 访问 | 有限访问 | 浏览器内部 UI（输入占位符、视频控件） |\n\n这个区别至关重要：**JavaScript 级别的访问受模式限制，但 CDP 级别的访问不受限制。**\n\n### 为什么传统自动化会失败\n\n传统自动化工具依赖于在页面上下文中执行 JavaScript：\n\n```javascript\n// WebDriver / Selenium 方法\ndocument.querySelector('#my-component')        // ✓ 找到宿主\ndocument.querySelector('#my-component button') // ✗ 无法穿越 shadow 边界\nelement.shadowRoot                             // ✗ 对封闭根返回 null\n```\n\nShadow 边界由浏览器的 JavaScript 引擎强制执行。任何通过执行 JavaScript 来查找元素的自动化工具都会遇到这道墙。这包括 Selenium、Playwright 的 `page.evaluate()`，以及任何在文档级别使用 `Runtime.evaluate()` 配合 `document.querySelector()` 的工具。\n\n## Pydoll 如何绕过 Shadow 边界\n\nPydoll 的方法在 **JavaScript 之下** 的层级工作：Chrome DevTools Protocol。CDP 可以直接访问浏览器的内部 DOM 表示，完全忽略 shadow 模式限制。\n\n### CDP 优势\n\n```mermaid\nsequenceDiagram\n    participant User as 用户代码\n    participant SR as ShadowRoot\n    participant CH as ConnectionHandler\n    participant CDP as Chrome CDP\n    participant DOM as 浏览器 DOM\n\n    User->>SR: shadow_root.query('.btn')\n    SR->>SR: _get_find_element_command(object_id)\n    SR->>CH: execute_command(Runtime.callFunctionOn)\n    CH->>CDP: WebSocket 发送\n    CDP->>DOM: 在 shadow root 对象上执行 querySelector\n    DOM-->>CDP: 元素结果\n    CDP-->>CH: 包含 objectId 的响应\n    CH-->>SR: 元素数据\n    SR-->>User: WebElement 实例\n```\n\n关键洞察在于 **shadow root 对象如何获取** 以及 **查询如何对其执行**：\n\n1. **发现**：`DOM.describeNode` 配合 `pierce=true` 返回 shadow root 节点及其 `backendNodeId`，无论模式如何\n2. **解析**：`DOM.resolveNode` 将 `backendNodeId` 转换为直接引用 shadow root 的 JavaScript `objectId`\n3. **查询**：`Runtime.callFunctionOn` 在 shadow root 的 `objectId` 上执行 `this.querySelector()`；这之所以有效，是因为调用是在 **shadow root 对象本身** 上进行的，而不是从文档上下文\n\n### 逐步解析：Shadow Root 访问\n\n```mermaid\nflowchart TD\n    A[\"WebElement\\n(shadow 宿主)\"]\n    B[\"shadowRoots[]\\n包含 backendNodeId\"]\n    C[\"JavaScript objectId\\n用于 shadow root\"]\n    D[\"ShadowRoot 实例\"]\n    E[\"WebElement\\n(shadow 内部)\"]\n\n    A -->|\"DOM.describeNode\\ndepth=1, pierce=true\"| B\n    B -->|\"DOM.resolveNode\\nbackendNodeId\"| C\n    C -->|\"创建 ShadowRoot\\n使用 objectId\"| D\n    D -->|\"find() / query()\\n通过 callFunctionOn\"| E\n```\n\n#### 步骤 1：描述宿主节点\n\n```python\n# Pydoll 发送此 CDP 命令：\n{\n    \"method\": \"DOM.describeNode\",\n    \"params\": {\n        \"objectId\": \"<host-element-object-id>\",\n        \"depth\": 1,\n        \"pierce\": true  # ← 这是关键标志\n    }\n}\n```\n\n`pierce` 参数告诉 CDP 在描述节点时穿越 shadow 边界。响应包含 shadow root 信息，无论 shadow root 的模式如何：\n\n```json\n{\n    \"result\": {\n        \"node\": {\n            \"nodeName\": \"DIV\",\n            \"shadowRoots\": [\n                {\n                    \"nodeId\": 0,\n                    \"backendNodeId\": 5,\n                    \"shadowRootType\": \"closed\",\n                    \"childNodeCount\": 4\n                }\n            ]\n        }\n    }\n}\n```\n\n!!! warning \"nodeId 与 backendNodeId\"\n    当 DOM 域未显式启用时（这是 Pydoll 的默认设置以最小化开销），`nodeId` 始终为 `0`。`backendNodeId` 是稳定的、始终可用的标识符。Pydoll 专门使用 `backendNodeId` 进行 shadow root 解析，这就是为什么它不需要 `DOM.enable()` 就能工作。\n\n#### 步骤 2：解析为 JavaScript 对象\n\n```python\n# 将 backendNodeId 转换为可用的 objectId：\n{\n    \"method\": \"DOM.resolveNode\",\n    \"params\": {\n        \"backendNodeId\": 5\n    }\n}\n```\n\n响应提供一个 `objectId`，即 JavaScript 对象空间中 shadow root 的句柄：\n\n```json\n{\n    \"result\": {\n        \"object\": {\n            \"objectId\": \"-2296764575741119861.1.3\"\n        }\n    }\n}\n```\n\n#### 步骤 3：在 Shadow Root 内查询\n\n有了 shadow root 的 `objectId`，Pydoll 利用 `FindElementsMixin` 现有的相对搜索机制：\n\n```python\n# 当调用 ShadowRoot.query('.btn') 时：\n{\n    \"method\": \"Runtime.callFunctionOn\",\n    \"params\": {\n        \"functionDeclaration\": \"function() { return this.querySelector(\\\".btn\\\"); }\",\n        \"objectId\": \"-2296764575741119861.1.3\"\n    }\n}\n```\n\n函数以 `this` 绑定到 shadow root 对象运行。由于 shadow root 原生实现了 `querySelector()` 和 `querySelectorAll()` 接口，CSS 选择器在 shadow 边界内自然工作。\n\n## ShadowRoot 架构\n\n### 设计决策：复用 FindElementsMixin\n\n最关键的架构决策是让 `ShadowRoot` 继承 `FindElementsMixin`：\n\n```python\nclass ShadowRoot(FindElementsMixin):\n    def __init__(self, object_id, connection_handler, mode, host_element):\n        self._object_id = object_id               # Shadow root CDP 引用\n        self._connection_handler = connection_handler  # 用于 CDP 通信\n        self._mode = mode                          # ShadowRootType 枚举\n        self._host_element = host_element          # 返回宿主的引用\n```\n\n**为什么这能工作**：`FindElementsMixin._find_element()` 检查 `hasattr(self, '_object_id')`。当存在时，它使用 `RELATIVE_QUERY_SELECTOR`，即在引用的对象上调用 `this.querySelector()`。由于 shadow root 原生支持 `querySelector()`，使用 CSS 选择器的 `query()` 自动工作。`ShadowRoot` 上的 `_css_only = True` 标志阻止 `find()` 和使用 XPath 的 `query()`，会抛出 `NotImplementedError`。\n\n```python\n# FindElementsMixin 中的这一行启用了 shadow root 搜索：\nelif hasattr(self, '_object_id'):\n    command = self._get_find_element_command(by, value, self._object_id)\n```\n\n这意味着 `ShadowRoot` 继承了 `query()` 和 `find_or_wait_element()`。但是，`_css_only = True` 标志将使用限制为仅使用 CSS 选择器的 `query()`；`find()` 和 XPath 会抛出 `NotImplementedError`。\n\n!!! tip \"架构一致性\"\n    这与 `WebElement.find()` 在元素子节点内搜索的机制相同：`_object_id` 属性表示\"相对于我搜索\"而不是\"搜索整个文档\"。`ShadowRoot`、`WebElement` 和 `Tab` 通过 `FindElementsMixin` 共享完全相同的元素查找行为。\n\n### 类关系\n\n| 类 | 有 `_object_id` | 有 `_connection_handler` | 查找范围 |\n|----|:-:|:-:|---|\n| `Tab` | 否 | 是 | 整个文档 |\n| `WebElement` | 是 | 是 | 元素子树内 |\n| `ShadowRoot` | 是 | 是 | Shadow 树内 |\n\n三者都继承自 `FindElementsMixin`。`_object_id` 的存在与否决定搜索是文档全局的还是限定在特定节点。\n\n### 解析 Shadow Root：backendNodeId 策略\n\nPydoll 故意使用 `backendNodeId` 而不是 `nodeId` 进行 shadow root 解析：\n\n| 属性 | `nodeId` | `backendNodeId` |\n|------|----------|-----------------|\n| 需要 `DOM.enable()` | 是 | 否 |\n| 跨 describe 调用稳定 | 否（DOM 未启用时为 0） | 是 |\n| 适用于 shadow root 解析 | 仅在 DOM 启用时 | 始终 |\n| 性能开销 | 较高（DOM 域跟踪） | 无 |\n\n通过依赖 `backendNodeId`，Pydoll 避免了启用 DOM 域的开销，同时保持可靠的 shadow root 访问。这是一个务实的选择：大多数自动化场景不需要 DOM 域的事件流，启用它会增加内存和处理开销来跟踪每次 DOM 变更。\n\n## 封闭的 Shadow Root：为什么 CDP 访问有效\n\n这是最常被问到的问题：**如果 `element.shadowRoot` 在 JavaScript 中对封闭的 shadow root 返回 `null`，CDP 怎么能访问它们？**\n\n答案在于理解浏览器的架构：\n\n```mermaid\ngraph TB\n    subgraph \"JavaScript 运行时\"\n        JS[\"JavaScript 代码\"]\n        API[\"Web APIs\\n(shadowRoot 属性)\"]\n    end\n\n    subgraph \"浏览器内部\"\n        CDP_Layer[\"CDP 协议层\"]\n        DOM_Internal[\"内部 DOM 树\"]\n    end\n\n    JS -->|\"element.shadowRoot\"| API\n    API -->|\"mode == 'closed'\\n→ 返回 null\"| JS\n    CDP_Layer -->|\"DOM.describeNode\\npierce=true\"| DOM_Internal\n    DOM_Internal -->|\"始终返回\\n完整 shadow 树\"| CDP_Layer\n```\n\n**JavaScript 访问** 经过 Web API 层，该层强制执行 shadow 模式限制。当 `mode='closed'` 时，API 返回 `null`；这是对网页代码的有意访问控制边界。\n\n**CDP 访问** 在 Web API 层之下运行。它直接与浏览器的内部 DOM 表示通信。`closed` 模式限制是 **JavaScript 级别的策略**，不是 **DOM 级别的限制**。Shadow 树仍然存在于 DOM 中；它只是对 JavaScript 的视图隐藏了。\n\n!!! info \"安全影响\"\n    这是 DevTools Protocol 的设计意图。CDP 面向需要完全 DOM 访问的调试和自动化工具。`closed` 模式保护 shadow 内容免受同一页面上其他脚本（如第三方脚本）的访问，而不是来自浏览器调试接口的访问。这与浏览器 DevTools 能够在 Elements 面板中检查封闭 shadow root 的原因相同。\n\n### 实际验证\n\n你可以自己验证这个行为：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.dom.types import ShadowRootType\n\nasync def verify_closed_access():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('about:blank')\n\n        # 通过 JavaScript 创建封闭的 shadow root\n        await tab.execute_script(\"\"\"\n            const host = document.createElement('div');\n            host.id = 'test-host';\n            document.body.appendChild(host);\n            const shadow = host.attachShadow({ mode: 'closed' });\n            shadow.innerHTML = '<p class=\"secret\">隐藏内容</p>';\n        \"\"\")\n\n        # JavaScript 无法访问：\n        result = await tab.execute_script(\n            \"return document.getElementById('test-host').shadowRoot\",\n            return_by_value=True,\n        )\n        js_value = result['result']['result'].get('value')\n        print(f\"JS shadowRoot: {js_value}\")  # None\n\n        # 但 Pydoll 可以：\n        host = await tab.find(id='test-host')\n        shadow = await host.get_shadow_root()\n        print(f\"Shadow 模式: {shadow.mode}\")  # ShadowRootType.CLOSED\n\n        secret = await shadow.query('.secret')\n        text = await secret.text\n        print(f\"内容: {text}\")  # \"隐藏内容\"\n\nasyncio.run(verify_closed_access())\n```\n\n## 嵌套 Shadow Root\n\nWeb 组件经常组合其他 Web 组件，创建多级 shadow 树：\n\n```mermaid\ngraph TB\n    subgraph \"Light DOM\"\n        Host1[\"outer-component\\n(shadow 宿主)\"]\n    end\n\n    subgraph \"外部 Shadow 树\"\n        SR1[\"#shadow-root (open)\"]\n        Host2[\"inner-component\\n(shadow 宿主)\"]\n        P1[\"p.outer-text\"]\n    end\n\n    subgraph \"内部 Shadow 树\"\n        SR2[\"#shadow-root (closed)\"]\n        Button[\"button.deep-btn\"]\n        P2[\"p.inner-text\"]\n    end\n\n    Host1 -.-> SR1\n    SR1 --> P1\n    SR1 --> Host2\n    Host2 -.-> SR2\n    SR2 --> P2\n    SR2 --> Button\n```\n\nPydoll 通过链式 `get_shadow_root()` 调用自然处理这种情况。每个 `ShadowRoot` 产生的 `WebElement` 实例本身也可以有 shadow root：\n\n```python\nouter_host = await tab.find(tag_name='outer-component')\nouter_shadow = await outer_host.get_shadow_root()        # open\n\ninner_host = await outer_shadow.query('inner-component')\ninner_shadow = await inner_host.get_shadow_root()        # closed，仍然有效\n\ndeep_button = await inner_shadow.query('.deep-btn')\nawait deep_button.click()\n```\n\n每个层级遵循相同的 CDP 解析流程：`describeNode` 然后 `resolveNode` 然后带有 `_object_id` 的 `ShadowRoot` 然后通过 `callFunctionOn` 执行 `querySelector`。\n\n## IFrame 内的 Shadow Root\n\n一个常见的实际场景涉及跨域 iframe 内的 shadow root——例如 Cloudflare Turnstile 验证码。这结合了两种隔离机制：iframe 边界和 shadow 边界。\n\n```mermaid\ngraph TB\n    subgraph \"主页面\"\n        Host[\"div.widget\\n(shadow 宿主)\"]\n    end\n\n    subgraph \"Shadow 树\"\n        SR1[\"#shadow-root\"]\n        IFrame[\"iframe\\n(跨域)\"]\n    end\n\n    subgraph \"IFrame (OOPIF)\"\n        Body[\"body\"]\n    end\n\n    subgraph \"IFrame Shadow 树\"\n        SR2[\"#shadow-root\"]\n        Button[\"label.checkbox\"]\n    end\n\n    Host -.-> SR1\n    SR1 --> IFrame\n    IFrame -.->|\"独立进程\"| Body\n    Body -.-> SR2\n    SR2 --> Button\n```\n\nPydoll 通过 **iframe 上下文传播** 透明地处理这种情况。当创建 `ShadowRoot` 时，它从宿主元素继承 iframe 路由上下文：\n\n```python\n# 完整链：主页面 → shadow root → iframe → shadow root → 元素\nshadow_host = await tab.find(id='widget-container')\nfirst_shadow = await shadow_host.get_shadow_root()\n\niframe = await first_shadow.query('iframe')\nbody = await iframe.find(tag_name='body')\nsecond_shadow = await body.get_shadow_root()\n\n# click() 正确工作——鼠标事件通过 OOPIF 会话路由\nbutton = await second_shadow.query('label.checkbox')\nawait button.click()\n```\n\n### 上下文传播如何工作\n\n跨域 iframe 在浏览器的独立进程中运行（Out-of-Process IFrame，即 OOPIF）。这些 iframe 的 CDP 命令必须通过专用的 `sessionId` 路由。Pydoll 自动在整个链中传播此路由上下文：\n\n1. **IFrame 解析其上下文**：`iframe.find()` 建立包含 `session_id` 和 `session_handler` 的 `IFrameContext`\n2. **子元素继承上下文**：在 iframe 内找到的元素接收 `IFrameContext`\n3. **Shadow root 从宿主继承**：`ShadowRoot` 复制其宿主元素的 `_iframe_context`\n4. **Shadow 内的元素从 shadow root 继承**：通过 `shadow.query()` 找到的元素接收传播的上下文\n5. **命令正确路由**：`_execute_command()` 检测继承的上下文，并通过 OOPIF 会话路由 CDP 命令（包括 `click()` 的 `Input.dispatchMouseEvent`）\n\n这意味着来自 `DOM.getBoxModel` 的坐标（相对于 iframe 视口）与发送到同一 OOPIF 会话的鼠标事件正确配对。\n\n## 查找 Shadow Root：find_shadow_roots()\n\n`Tab.find_shadow_roots()` 遍历整个 DOM 树以收集页面上找到的所有 shadow root。\n\n### 工作原理\n\n```\nTab.find_shadow_roots()\n  ├─ DOM.getDocument(depth=-1, pierce=true)\n  │   └─ 返回包含 shadowRoots 数组的完整 DOM 树\n  ├─ 递归树遍历：_collect_shadow_roots_from_tree()\n  │   ├─ 收集包含宿主 backendNodeId 的 shadowRoots 条目\n  │   ├─ 递归遍历子节点\n  │   └─ 遍历 contentDocument（同源 iframe）\n  ├─ 对于每个 shadow root 条目：\n  │   ├─ DOM.resolveNode(backendNodeId) → objectId\n  │   └─ 解析宿主元素（尽力而为）\n  └─ 返回 list[ShadowRoot] 包含宿主引用\n```\n\n### 超时：等待 Shadow Root\n\nShadow 宿主通常是异步注入的。`Tab.find_shadow_roots()` 接受 `timeout` 参数，每 0.5 秒轮询一次，直到找到至少一个 shadow root 或超时到期（抛出 `WaitElementTimeout`）。同样，`WebElement.get_shadow_root()` 也支持 `timeout` 来等待特定元素的 shadow root：\n\n```python\n# 等待最多 10 秒让 shadow root 出现\nshadow_roots = await tab.find_shadow_roots(timeout=10)\n\n# 等待特定元素的 shadow root\nshadow = await element.get_shadow_root(timeout=5)\n```\n\n### 关键细节\n\n- `DOM.getDocument` 中的 **`pierce=True`** 使浏览器在节点描述中包含 `shadowRoots` 数组，允许发现所有 shadow root 而无需逐个导航到每个宿主。\n- **同源 iframe 内容** 通过 `contentDocument` 节点包含在树中。遍历会处理这些。\n- 每个返回的 `ShadowRoot` 都有对其 `host_element` 的引用（通过 `DOM.resolveNode` 尽力解析）。\n\n### 深度遍历：跨域 IFrame（OOPIF）\n\n默认情况下，跨域 iframe（OOPIF）**不**包含在 DOM 树中——其内容存在于浏览器的独立进程中。传入 `deep=True` 以同时发现 OOPIF 内的 shadow root：\n\n```python\nshadow_roots = await tab.find_shadow_roots(deep=True, timeout=10)\n```\n\n当设置 `deep=True` 时，该方法执行额外步骤：\n\n```\nTab.find_shadow_roots(deep=True)\n  ├─ ...（如上所述的主文档遍历）...\n  └─ _collect_oopif_shadow_roots()\n      ├─ 浏览器级别的 ConnectionHandler（无 page_id → 浏览器端点）\n      ├─ Target.getTargets() → 过滤 type='iframe'\n      └─ 对于每个 iframe 目标：\n          ├─ Target.attachToTarget(targetId, flatten=True) → sessionId\n          ├─ DOM.getDocument(depth=-1, pierce=True) 带 sessionId\n          ├─ _collect_shadow_roots_from_tree() 在 OOPIF DOM 上执行\n          └─ 对于找到的每个 shadow root：\n              ├─ DOM.resolveNode(backendNodeId) 带 sessionId\n              ├─ 解析宿主元素（尽力而为）带 sessionId\n              ├─ 创建 IFrameContext(frame_id, session_handler, session_id)\n              └─ 在宿主元素上设置 IFrameContext（或直接在 ShadowRoot 上设置）\n```\n\n返回的 `ShadowRoot` 对象携带 OOPIF 路由上下文（`IFrameContext`），因此通过 `shadow_root.query()` 找到的元素会自动通过正确的 OOPIF 会话路由 CDP 命令。这对于 Cloudflare Turnstile 验证码等场景至关重要，其中复选框位于跨域 iframe 内的封闭 shadow root 中。\n\n## 限制和边界情况\n\n### Shadow Root 内的选择器策略\n\n!!! warning \"在 Shadow Root 内仅使用 query() 配合 CSS\"\n    `ShadowRoot` 设置了 `_css_only = True`，这意味着仅支持使用 CSS 选择器的 `query()`。`find()` 和使用 XPath 的 `query()` 会抛出 `NotImplementedError`。\n\nShadow root 原生实现了 `querySelector()` 和 `querySelectorAll()`，使 CSS 选择器成为自然且可靠的选择：\n\n| 方法 | Shadow Root 内 | 说明 |\n|------|:--:|---|\n| `query('css选择器')` | 完全支持 | 推荐方法 |\n| `query('css选择器', find_all=True)` | 完全支持 | 返回元素列表 |\n| `find()` | 不支持 | 抛出 `NotImplementedError` |\n| `query('//xpath')` | 不支持 | 抛出 `NotImplementedError` |\n\n```python\nshadow = await host.get_shadow_root()\n\n# ✓ 推荐：query() 配合 CSS 选择器\nbutton = await shadow.query('button.submit')\nemail = await shadow.query('#email-input')\nitems = await shadow.query('.item', find_all=True)\n\n# ✗ 不支持：find() 和 XPath 抛出 NotImplementedError\n# shadow.find(id='email-input')        # NotImplementedError\n# shadow.query('//button')             # NotImplementedError\n```\n\n### XPath 无法穿越 Shadow 边界\n\n从文档根开始的 XPath 表达式无法穿越 shadow 边界。这是 XPath 的根本限制，因为它在 Shadow DOM 出现之前就被设计了：\n\n```python\n# 无法找到 shadow 内容：文档级 XPath 无法穿越边界\nelement = await tab.find(xpath='//div[@id=\"host\"]//button')\n```\n\n### User-Agent Shadow Root\n\n浏览器内部的 shadow root（如 `<input>` 占位符样式、`<video>` 控件）类型为 `user-agent`。它们可以通过 CDP 访问，但其内部结构因浏览器版本而异，不属于任何 Web 标准。\n\n```python\ninput_element = await tab.find(tag_name='input')\ntry:\n    ua_shadow = await input_element.get_shadow_root()\n    # ua_shadow.mode == ShadowRootType.USER_AGENT\n    # 内部结构是浏览器特定的\nexcept ShadowRootNotFound:\n    pass  # 并非所有 input 都有 user-agent shadow root\n```\n\n!!! warning \"User-Agent Shadow Root 稳定性\"\n    不要构建依赖 user-agent shadow root 内部结构的自动化逻辑。它们的 DOM 结构是实现细节，可能在浏览器版本之间无通知地更改。\n\n### 过期的 Shadow Root 引用\n\n如果宿主元素从 DOM 中移除后重新添加（在单页应用中很常见），shadow root 的 `objectId` 将变为过期。解决方案是重新获取 shadow root：\n\n```python\n# 页面导航或 DOM 重建后：\nhost = await tab.find(id='my-component', timeout=5)  # 重新查找宿主\nshadow = await host.get_shadow_root()                 # 新的 shadow root\n```\n\n## 关键要点\n\n- **Shadow DOM 封装** 隐藏元素不被文档级 `querySelector()` 发现，破坏传统自动化\n- **CDP 在 JavaScript API 层之下运行**，完全绕过 shadow 模式限制\n- **`backendNodeId`** 是用于 shadow root 解析的稳定标识符，避免了启用 DOM 域的需要\n- **`ShadowRoot` 继承 `FindElementsMixin`**，带有 `_css_only = True`，仅支持使用 CSS 选择器的 `query()`；`find()` 和 XPath 抛出 `NotImplementedError`\n- **封闭的 shadow root** 完全可访问，因为 `closed` 模式是 JavaScript 级别的策略，不是 DOM 级别的限制\n- **嵌套 shadow root** 通过在每个层级链式调用 `get_shadow_root()` 自然工作\n- **IFrame 内的 shadow root** 通过自动 iframe 上下文传播透明地工作\n- **使用 CSS 选择器**（`query()`）在 shadow root 内查找元素；`find()` 和 XPath 不受支持\n- **`find_shadow_roots()`** 发现页面上的所有 shadow root；支持 `timeout` 进行轮询和 `deep=True` 用于跨域 iframe（OOPIF）\n- **`get_shadow_root(timeout)`** 等待特定元素的 shadow root 出现\n\n## 相关文档\n\n- **[元素查找指南](../../features/element-finding.md)**：`find()`、`query()` 和 shadow root 访问的实际用法\n- **[IFrame 与上下文](../fundamentals/iframes-and-contexts.md)**：Pydoll 如何解析和路由命令到 iframe，包括 OOPIF 处理\n- **[FindElements Mixin 架构](./find-elements-mixin.md)**：`_object_id` 机制如何实现作用域搜索\n- **[WebElement 域](./webelement-domain.md)**：元素如何与 CDP 交互\n- **[连接层](../fundamentals/connection-layer.md)**：与浏览器的 WebSocket 通信\n"
  },
  {
    "path": "docs/zh/deep-dive/architecture/tab-domain.md",
    "content": "# Tab 域架构\n\nTab 域是 Pydoll 浏览器自动化的主要接口，充当编排层，将多个 CDP 域集成到一个内聚的 API 中。本文档探讨其内部架构、设计模式以及塑造其行为的工程决策。\n\n!!! info \"实际用法\"\n    有关使用示例和实际模式，请参阅 [Tab 管理指南](../features/automation/tabs.md)。\n\n## 架构概述\n\n`Tab` 类充当 Chrome DevTools Protocol 的**外观**，将多域协调的复杂性抽象为统一的接口。\n\n### 组件结构\n\n| 组件 | 关系 | 目的 |\n|-----------|-------------|---------|\n| **Tab** | 核心类 | 主要自动化接口 |\n| ↳ **ConnectionHandler** | 组合（拥有） | 与 CDP 的 WebSocket 通信 |\n| ↳ **Browser** | 引用（父级） | 访问浏览器级别的状态和配置 |\n| ↳ **FindElementsMixin** | 继承 | 元素定位能力 |\n| ↳ **WebElement** | 工厂（创建） | 单个 DOM 元素表示 |\n\n### CDP 域集成\n\n`ConnectionHandler` 将 Tab 操作路由到多个 CDP 域：\n\n```\nTab 方法                CDP 域            目的\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ngo_to(), refresh()     →   Page            →  导航和生命周期\nexecute_script()       →   Runtime         →  JavaScript 执行\nfind(), query()        →   Runtime/DOM     →  元素定位\nget_cookies()          →   Storage         →  会话状态\nenable_network_events()→   Network         →  流量监控\nenable_fetch_events()  →   Fetch           →  请求拦截\n```\n\n### 核心职责\n\n1. **CDP 命令路由**：将高级操作转换为特定域的 CDP 命令\n2. **状态管理**：跟踪启用的域、活动回调和会话状态\n3. **事件协调**：将 CDP 事件桥接到用户定义的回调\n4. **元素工厂**：从 CDP `objectId` 字符串创建 `WebElement` 实例\n5. **生命周期管理**：处理清理和资源释放\n\n## 组合与继承：FindElementsMixin\n\nTab 域中的一个关键架构决策是**从 `FindElementsMixin` 继承**而不是使用组合：\n\n```python\nclass Tab(FindElementsMixin):\n    def __init__(self, ...):\n        self._connection_handler = ConnectionHandler(...)\n        # Mixin 方法现在在 Tab 上可用\n```\n\n**为什么这里使用继承？**\n\n| 方法 | 优点 | 缺点 | Pydoll 的选择 |\n|----------|------|------|-----------------|\n| **继承** | 简洁的 API（`tab.find()`）、类型兼容性 | 紧密耦合 | 使用 |\n| 组合 | 松散耦合、灵活 | 冗长（`tab.finder.find()`）、包装器开销 | 未使用 |\n\n**理由：** mixin 模式是合理的，因为：\n\n- 元素查找是 **Tab 身份的核心**（每个标签页都可以查找元素）\n- mixin 是**无状态的** - 它只需要 `_connection_handler`（通过鸭子类型的依赖注入）\n- API 人体工程学很重要 - `tab.find()` 比 `tab.elements.find()` 更直观\n\n详见 [FindElements Mixin 深入探讨](./find-elements-mixin.md) 的架构细节。\n\n## 状态管理架构\n\nTab 类管理**多层状态**：\n\n###  1. 域启用标志\n\n```python\nclass Tab:\n    def __init__(self, ...):\n        self._page_events_enabled = False\n        self._network_events_enabled = False\n        self._fetch_events_enabled = False\n        self._dom_events_enabled = False\n        self._runtime_events_enabled = False\n        self._intercept_file_chooser_dialog_enabled = False\n```\n\n**为什么使用显式标志？**\n\n- **幂等性**：两次调用 `enable_page_events()` 不会重复注册\n- **状态检查**：`tab.page_events_enabled` 等属性公开当前状态\n- **清理跟踪**：知道在标签页关闭时需要禁用哪些域\n\n**替代方案（未使用）：** 每次检查时查询 CDP 以获取启用的域 → 太慢，增加延迟。\n\n### 2. 目标标识\n\n```python\nself._target_id: str              # 唯一的 CDP 标识符\nself._browser_context_id: Optional[str]  # 隔离上下文\nself._connection_port: int        # WebSocket 端口\n```\n\n**设计决策：** `target_id` 是**主要标识符**，而不是 Tab 实例本身。这使得：\n\n- **浏览器级别的 Tab 注册表**：`Browser._tabs_opened[target_id] = tab`\n- **单例模式**：相同的 `target_id` 始终返回相同的 `Tab` 实例\n- **连接重用**：同一标签页上的多个操作共享 WebSocket\n\n### 3. 特定功能状态\n\n```python\nself._cloudflare_captcha_callback_id: Optional[int] = None  # 用于清理\nself._request: Optional[Request] = None  # 延迟初始化\n```\n\n**延迟初始化模式：** `Request` 仅在访问 `tab.request` 时创建：\n\n```python\n@property\ndef request(self) -> Request:\n    if self._request is None:\n        self._request = Request(self)\n    return self._request\n```\n\n**为什么延迟？** 大多数自动化不使用浏览器上下文 HTTP 请求。节省内存和初始化时间。\n\n\n## JavaScript 执行：双上下文架构\n\n`execute_script()` 方法实现**上下文多态性** - 相同的接口，不同的 CDP 命令：\n\n| 上下文 | CDP 方法 | 用例 |\n|---------|-----------|----------|\n| 全局（无元素） | `Runtime.evaluate` | `document.title`、全局脚本 |\n| 元素绑定 | `Runtime.callFunctionOn` | 元素特定操作 |\n\n**关键架构决策：** 基于 `element` 参数的存在自动检测执行模式，消除了单独的 API（`evaluate()` 与 `call_function_on()`）。\n\n**脚本转换管道：**\n\n1. 替换 `argument` → `this`（Selenium 兼容性）\n2. 检测脚本是否已包装在 `function() { }` 中\n3. 如果需要则包装：`script` → `function() { script }`\n4. 路由到适当的 CDP 命令\n\n**为什么使用 `argument` 关键字？** 为 Selenium 用户提供迁移路径，API 熟悉度。\n\n!!! info \"实际用法\"\n    有关真实世界的脚本执行模式，请参阅[类人交互](../features/automation/human-interactions.md)。\n\n## 事件系统集成\n\nTab 充当 ConnectionHandler 事件系统的**薄包装器**，但添加了重要的一层：**非阻塞回调执行**。\n\n```python\nasync def on(self, event_name: str, callback: Callable, temporary: bool = False) -> int:\n    # 包装异步回调以在后台执行\n    async def callback_wrapper(event):\n        asyncio.create_task(callback(event))\n    \n    if asyncio.iscoroutinefunction(callback):\n        function_to_register = callback_wrapper  # 非阻塞包装器\n    else:\n        function_to_register = callback  # 同步回调直接执行\n    \n    # 将注册委托给 ConnectionHandler\n    return await self._connection_handler.register_callback(\n        event_name, function_to_register, temporary\n    )\n```\n\n**架构角色：** Tab 提供具有非阻塞执行语义的标签页作用域事件注册，而 ConnectionHandler 处理 WebSocket 管道和顺序回调调用。\n\n**关键特性：**\n\n- 通过 `asyncio.create_task()` 为异步回调提供**后台执行**（即发即忘）\n- **同步/异步回调自动检测**\n- **临时回调**用于一次性处理程序\n- **回调 ID** 用于显式删除\n\n**执行模型：**\n\n| 层 | 行为 | 目的 |\n|-------|----------|---------|\n| **用户回调** | 在后台任务中运行 | 永远不会阻塞其他回调或 CDP 命令 |\n| **Tab 包装器** | `create_task(callback())` | 启动后台任务，立即返回 |\n| **EventsManager** | `await wrapper()` | 按顺序调用同一事件的包装器 |\n\n**为什么需要包装器？** 没有它，一个慢速异步回调会阻塞同一事件的其他回调。`create_task` 包装器确保所有回调\"同时\"启动（在单独的任务中），防止一个慢速回调延迟其他回调。\n\n!!! info \"详细架构\"\n    有关内部事件路由机制和 EventsManager 的顺序调用模式，请参阅[事件架构深入探讨](./event-architecture.md)。\n    \n    **实际用法：** [事件系统指南](../features/advanced/event-system.md)\n\n## 会话状态：Cookie 管理\n\n**架构分离：** Cookie 路由到 **Storage 域**（操作），而不是 Network 域（观察）。\n\n```python\nasync def set_cookies(self, cookies: list[CookieParam]):\n    return await self._execute_command(\n        StorageCommands.set_cookies(cookies, self._browser_context_id)\n    )\n```\n\n**上下文感知设计：** `browser_context_id` 参数确保 cookie 隔离，实现多账户自动化。\n\n!!! info \"实际 Cookie 管理\"\n    有关使用模式和反检测策略，请参阅 [Cookie 与会话指南](../features/browser-management/cookies-sessions.md)。\n\n## 内容捕获：CDP 目标限制\n\n**关键限制：** `Page.captureScreenshot` 仅适用于**顶级目标**。Iframe 标签页静默失败（响应中没有 `data` 字段）。\n\n```python\ntry:\n    screenshot_data = response['result']['data']\nexcept KeyError:\n    raise TopLevelTargetRequired(...)  # 引导用户使用 WebElement.take_screenshot()\n```\n\n**设计影响：** 旧版本会为 iframe 创建独立的 Tab。现在 iframe 直接作为 `WebElement` 处理，因此需要在框内元素上执行操作，例如 `await iframe_element.find(...).take_screenshot()`。\n\n**PDF 生成：** `Page.printToPDF` 返回 base64 编码的数据。Pydoll 抽象文件 I/O，但底层数据始终是 base64（CDP 规范）。\n\n!!! info \"实际用法\"\n    有关参数、格式和真实世界示例，请参阅[屏幕截图和 PDF 指南](../features/automation/screenshots-and-pdfs.md)。\n\n## 网络监控：有状态设计\n\n**架构原则：** 网络方法需要**启用状态** - 运行时检查防止访问不存在的数据。\n\n**存储分离：**\n\n- **日志**：在 `ConnectionHandler` 中缓冲（接收所有 CDP 事件）\n- **Tab**：查询处理程序，无重复存储\n- **响应正文**：通过 `Network.getResponseBody(requestId)` 按需检索\n\n**关键时序约束：** 响应正文必须在响应后**约 30 秒内**获取（浏览器垃圾回收）。\n\n!!! info \"实际网络监控\"\n    有关全面的事件跟踪和分析模式，请参阅[网络监控指南](../features/network/monitoring.md)。\n    \n    **请求拦截：** [请求拦截指南](../features/network/interception.md)\n\n## 对话框管理：事件捕获模式\n\n**关键 CDP 行为：** JavaScript 对话框**阻塞所有 CDP 命令**直到处理。\n\n**架构解决方案：** `ConnectionHandler` 立即捕获 `Page.javascriptDialogOpening` 事件，防止自动化挂起。\n\n```python\n# 处理程序在用户代码运行之前存储对话框事件\nself._connection_handler.dialog  # 由处理程序捕获\n# Tab 查询存储的事件\nasync def has_dialog(self) -> bool:\n    return bool(self._connection_handler.dialog)\n```\n\n**为什么选择这种设计？** 事件在用户回调执行之前触发。没有立即捕获，自动化将死锁等待被阻塞的 CDP 响应。\n\n## IFrame 架构：Tab 重用模式\n\n**关键洞察：** IFrame 是 **CDP 的一等目标** → 表示为 `Tab` 实例。\n\n**目标解析算法：**\n\n1. 从 iframe 元素提取 `src` 属性\n2. 通过 `Target.getTargets()` 查询所有 CDP 目标\n3. 将 iframe URL 匹配到目标 `targetId`\n4. 检查单例注册表（`Browser._tabs_opened`）\n5. 返回现有实例或创建 + 注册新 Tab\n\n**设计权衡：** IFrame 标签页继承所有 Tab 方法，但有些会失败（例如 `take_screenshot()`）。替代方案（专用的 `IFrame` 类）将为最小的好处复制 90% 的 API。\n\n!!! info \"使用 IFrame\"\n    有关实际模式、嵌套框架和常见陷阱，请参阅 [IFrame 交互指南](../features/automation/iframes.md)。\n\n## 上下文管理器：自动资源清理\n\n**架构模式：** 状态恢复 + 乐观资源获取。\n\n### 关键上下文管理器\n\n| 管理器 | 模式 | 关键特性 |\n|---------|---------|-------------|\n| `expect_file_chooser()` | 状态恢复 | 退出后恢复域启用 |\n| `expect_download()` | 临时资源 | 自动清理临时目录 |\n\n**文件选择器设计：**\n\n- 启用所需的域（`Page`、文件选择器拦截）\n- 注册**临时回调**（首次触发后自动删除）\n- 退出时恢复原始状态（如果之前禁用了域，则再次禁用）\n\n**下载处理设计：**\n\n- 创建临时目录（或使用提供的路径）\n- 使用 `asyncio.Future` 进行协调（`will_begin_future`、`done_future`）\n- 浏览器级别配置（下载是每个上下文的，而不是每个标签页的）\n- 通过 `finally` 块保证清理\n\n!!! info \"实际文件操作\"\n    有关上传模式、文件选择器使用和下载处理，请参阅[文件操作指南](../features/automation/file-operations.md)。\n\n## 生命周期：Tab 关闭和失效\n\n**Tab 关闭级联：**\n\n1. CDP 关闭浏览器标签页（`Page.close`）\n2. Tab 从 `Browser._tabs_opened` 注销\n3. WebSocket 自动关闭（CDP 目标已销毁）\n4. 事件回调被垃圾回收\n\n**关闭后行为：** Tab 实例变为**无效** - 进一步的操作失败（WebSocket 已关闭）。\n\n**设计决策：** 没有显式的 `_closed` 标志。用户管理生命周期。替代方案（状态跟踪）为边际安全好处增加了开销。\n\n## 关键架构决策\n\n### 每个 Tab 的 WebSocket 策略\n\n**选择的设计：** 每个 Tab 创建自己的 ConnectionHandler，具有到 `ws://localhost:port/devtools/page/{targetId}` 的专用 WebSocket 连接。\n\n**理由：**\n\nCDP 支持**两种连接模型**：\n\n1. **浏览器级别**：到 `ws://localhost:port/devtools/browser/...` 的单个连接（由 Browser 实例使用）\n2. **Tab 级别**：到 `ws://localhost:port/devtools/page/{targetId}` 的每个标签页连接（由 Tab 实例使用）\n\nPydoll 使用**两者**：\n\n- **Browser** 有自己的 ConnectionHandler，用于浏览器范围的操作（上下文、下载、浏览器级别事件）\n- **每个 Tab** 有自己的 ConnectionHandler，用于标签页特定的操作（导航、元素查找、标签页事件）\n\n**每个标签页 WebSocket 的好处：**\n\n- **真正的并行性**：多个标签页可以同时执行 CDP 命令而无需等待\n- **独立的事件流**：每个标签页仅接收自己的事件（无需过滤）\n- **隔离的故障**：一个标签页中的连接问题不会影响其他标签页\n- **简化路由**：无需按 targetId 解复用消息\n\n**权衡：** 更多打开的连接（每个标签页一个），但 CDP 和浏览器可以有效地处理这一点。对于 10 个标签页，这总共是 11 个连接（1 个浏览器 + 10 个标签页），与标签页本身创建的 HTTP 连接相比可以忽略不计。\n\n!!! info \"浏览器与 Tab 通信\"\n    有关浏览器级别 ConnectionHandler 以及 Browser/Tab 协调如何工作的详细信息，请参阅 [Browser 域架构](./browser-domain.md)。\n\n### 浏览器引用的必要性\n\n**为什么 Tab 存储 `_browser` 引用：**\n- 上下文查询（cookie 的 `browser_context_id`）\n- 浏览器级别操作（下载行为、iframe 注册表）\n- 配置访问（`browser.options.page_load_state`）\n\n### API 设计选择\n\n| 选择 | 理由 |\n|--------|-----------|\n| **异步属性**（`current_url`、`page_source`） | 信号实时数据 + CDP 成本 |\n| **单独的 `enable`/`disable` 方法** | 显式优于隐式，匹配 CDP 命名 |\n| **无 `_closed` 标志** | 用户管理生命周期，减少开销 |\n| **脚本中的 `argument` 关键字** | Selenium 兼容性，迁移路径 |\n\n## 与其他域的关系\n\nTab 域位于 Pydoll 架构的**中心**：\n\n```mermaid\ngraph TD\n    Browser[Browser Domain<br/>Lifecycle & Process] -->|creates| Tab[Tab Domain<br/>Automation Interface]\n    Tab -->|uses| ConnectionHandler[ConnectionHandler<br/>CDP Communication]\n    Tab -->|creates| WebElement[WebElement Domain<br/>Element Interaction]\n    Tab -->|inherits| FindMixin[FindElementsMixin<br/>Locator Strategies]\n    Tab -->|uses| Commands[CDP Commands<br/>Typed Protocol]\n    \n    ConnectionHandler -->|dispatches| Events[Event System]\n    Tab -.->|references| Browser\n    WebElement -.->|references| ConnectionHandler\n```\n\n**关键关系：**\n\n1. **Browser → Tab**：父子关系。Browser 管理 Tab 生命周期和共享状态。\n2. **Tab → ConnectionHandler**：组合。Tab 委托 CDP 通信。\n3. **Tab → WebElement**：工厂。Tab 从 `objectId` 字符串创建元素。\n4. **Tab ← FindElementsMixin**：继承。Tab 获得元素定位方法。\n5. **Tab ↔ Browser**：双向引用。Tab 查询浏览器以获取上下文信息。\n\n## 总结：设计理念\n\nTab 域优先考虑 **API 人体工程学**和**正确性**而不是微优化：\n\n- **外观模式**抽象 CDP 复杂性\n- 通过显式标志进行**状态管理**，防止双重启用\n- 通过上下文管理器进行**资源管理**\n- 具有后台执行（非阻塞）的**事件协调**\n\n**核心权衡：**\n\n| 决策 | 好处 | 成本 | 判定 |\n|----------|---------|------|---------|\n| 每个标签页的 WebSocket | 真正的并行性 | 更多连接 | 合理 |\n| 继承 FindElementsMixin | 简洁的 API | 紧密耦合 | 合理 |\n| 延迟 Request 初始化 | 内存效率 | 属性开销 | 合理 |\n\n## 进一步阅读\n\n**实用指南：**\n\n- [Tab 管理](../features/automation/tabs.md) - 多标签页模式、生命周期、并发\n- [元素查找](../features/element-finding.md) - 选择器和 DOM 遍历\n- [事件系统](../features/advanced/event-system.md) - 实时浏览器监控\n\n**架构深入探讨：**\n\n- [事件架构](./event-architecture.md) - WebSocket 管道和事件路由\n- [FindElements Mixin](./find-elements-mixin.md) - 选择器解析算法\n- [Browser 域](./browser-domain.md) - 进程管理和上下文\n"
  },
  {
    "path": "docs/zh/deep-dive/architecture/webelement-domain.md",
    "content": "# WebElement 域架构\n\nWebElement 域通过 Chrome DevTools Protocol 在高级自动化代码和低级 DOM 交互之间架起桥梁。本文档探讨其内部架构、设计模式和工程决策。\n\n!!! info \"实用使用\"\n    有关使用示例和交互模式，请参阅：\n    \n    - [元素查找指南](../features/element-finding.md)\n    - [类人交互](../features/automation/human-interactions.md)\n    - [文件操作](../features/automation/file-operations.md)\n\n## 架构概述\n\nWebElement 通过 CDP 的 `objectId` 机制表示对 DOM 元素的**远程对象引用**：\n\n```\n用户代码 → WebElement → ConnectionHandler → CDP Runtime → 浏览器 DOM\n```\n\n**关键特性：**\n\n- **异步设计**：所有操作都遵循 Python 的 async/await 模式\n- **远程引用**：维护 CDP `objectId` 以引用浏览器端元素\n- **Mixin 继承**：继承 `FindElementsMixin` 以进行子元素搜索\n- **混合状态**：结合缓存属性和实时 DOM 查询\n\n### 核心状态\n\n```python\nclass WebElement(FindElementsMixin):\n    def __init__(self, object_id: str, connection_handler: ConnectionHandler, ...):\n        self._object_id = object_id              # CDP 远程对象引用\n        self._connection_handler = connection_handler  # WebSocket 通信\n        self._attributes: dict[str, str] = {}    # 缓存的 HTML 属性\n        self._search_method = method             # 元素如何被找到（调试）\n        self._selector = selector                # 原始选择器（调试）\n```\n\n**为什么缓存属性？** 初始元素定位返回 HTML 属性。缓存提供对常见属性（`id`、`class`、`tag_name`）的快速同步访问，无需额外的 CDP 调用。\n\n## 设计模式\n\n### 1. 命令模式\n\n所有元素交互都转换为 CDP 命令：\n\n| 用户操作 | CDP 域 | 命令 |\n|----------------|-----------|---------|\n| `element.click()` | Input | `Input.dispatchMouseEvent` |\n| `element.text` | Runtime | `Runtime.callFunctionOn` |\n| `element.bounds` | DOM | `DOM.getBoxModel` |\n| `element.take_screenshot()` | Page | `Page.captureScreenshot` |\n\n### 2. 桥接模式\n\nWebElement 抽象 CDP 协议复杂性：\n\n```python\nasync def click(self, x_offset=0, y_offset=0, hold_time=0.1):\n    # 高级 API\n    \n    # → 转换为低级 CDP 命令：\n    # 1. DOM.getBoxModel（获取位置）\n    # 2. Input.dispatchMouseEvent（按下）\n    # 3. Input.dispatchMouseEvent（释放）\n```\n\n### 3. 用于子搜索的 Mixin 继承\n\n**为什么继承 FindElementsMixin？** 启用元素相对搜索：\n\n```python\nform = await tab.find(id='login-form')\nusername = await form.find(name='username')  # 在表单内搜索\n```\n\n**设计决策：** 组合（`form.finder.find()`）会更灵活但不太符合人体工程学。为了 API 简单性选择继承。\n\n## 混合属性系统\n\n**架构创新：** WebElement 结合同步和异步属性访问。\n\n### 同步属性（缓存属性）\n\n```python\n@property\ndef id(self) -> str:\n    return self._attributes.get('id')  # 来自缓存的 HTML 属性\n\n@property  \ndef class_name(self) -> str:\n    return self._attributes.get('class_name')  # 'class' → 'class_name'（Python 关键字）\n```\n\n**来源：** 来自 CDP 元素定位响应的扁平列表，在 `__init__` 期间解析。\n\n### 异步属性（实时 DOM 状态）\n\n```python\n@property\nasync def text(self) -> str:\n    outer_html = await self.inner_html  # CDP 调用\n    soup = BeautifulSoup(outer_html, 'html.parser')\n    return soup.get_text(strip=True)\n\n@property\nasync def bounds(self) -> dict:\n    response = await self._execute_command(DomCommands.get_box_model(self._object_id))\n    # 解析并返回边界\n```\n\n**理由：** 文本和边界是**动态的** - 它们随着页面更新而变化。属性是**静态的** - 在定位时捕获。\n\n| 属性类型 | 访问 | 来源 | 用例 |\n|--------------|--------|--------|----------|\n| 同步 | `element.id` | 缓存属性 | 快速访问、静态数据 |\n| 异步 | `await element.text` | 实时 CDP 查询 | 当前状态、动态数据 |\n\n## 点击实现：多阶段管道\n\n点击操作遵循复杂的管道以确保可靠性：\n\n### 1. 特殊元素检测\n\n```python\nasync def click(self, x_offset=0, y_offset=0, hold_time=0.1):\n    # 阶段 1：处理特殊元素\n    if self._is_option_tag():\n        return await self.click_option_tag()  # <option> 需要 JavaScript 选择\n```\n\n**为什么特殊处理？** `<select>` 内的 `<option>` 元素不响应鼠标事件。需要 JavaScript `selected = true`。\n\n### 2. 可见性检查\n\n```python\n    # 阶段 2：验证元素是否可见\n    if not await self.is_visible():\n        raise ElementNotVisible()\n```\n\n**为什么检查？** CDP 鼠标事件目标坐标。隐藏的元素会在错误位置接收点击或静默失败。\n\n### 3. 位置计算\n\n```python\n    # 阶段 3：滚动到视图并获取位置\n    await self.scroll_into_view()\n    bounds = await self.bounds\n    \n    # 阶段 4：计算点击坐标\n    position_to_click = (\n        bounds['x'] + bounds['width'] / 2 + x_offset,\n        bounds['y'] + bounds['height'] / 2 + y_offset,\n    )\n```\n\n**偏移支持：** 启用各种点击位置以实现类人行为（反检测）。\n\n### 4. 鼠标事件分发\n\n```python\n    # 阶段 5：发送 CDP 鼠标事件\n    await self._execute_command(InputCommands.mouse_press(*position_to_click))\n    await asyncio.sleep(hold_time)  # 可配置的保持时间（默认 0.1 秒）\n    await self._execute_command(InputCommands.mouse_release(*position_to_click))\n```\n\n**为什么两个命令？** 模拟真实的鼠标行为（按下 → 保持 → 释放）。一些网站检测即时点击为机器人。\n\n### 点击回退：JavaScript 替代方案\n\n```python\nasync def click_using_js(self):\n    \"\"\"无法通过鼠标事件点击的元素的回退。\"\"\"\n    await self.execute_script('this.click()')\n```\n\n**何时使用：**\n- 隐藏元素（例如，使用 CSS 样式的文件输入）\n- 叠加层后面的元素\n- 性能关键场景（跳过可见性/位置检查）\n\n!!! info \"鼠标 vs JavaScript 点击\"\n    请参阅[类人交互](../features/automation/human-interactions.md)了解何时使用每种方法及检测影响。\n\n## 截图架构：裁剪区域\n\n**关键机制：** 带有 `clip` 参数的 `Page.captureScreenshot`。\n\n```python\nasync def take_screenshot(self, path: str, quality: int = 100):\n    # 1. 获取元素边界（位置 + 尺寸）\n    bounds = await self.get_bounds_using_js()\n    \n    # 2. 创建裁剪区域\n    clip = Viewport(x=bounds['x'], y=bounds['y'], \n                    width=bounds['width'], height=bounds['height'], scale=1)\n    \n    # 3. 仅捕获裁剪区域\n    screenshot = await self._execute_command(\n        PageCommands.capture_screenshot(format=ScreenshotFormat.JPEG, clip=clip, quality=quality)\n    )\n```\n\n**为什么使用 JavaScript 边界？** `DOM.getBoxModel` 可能对某些元素失败。JavaScript `getBoundingClientRect()` 是更可靠的回退。\n\n**格式限制：** 元素截图始终使用 JPEG（带裁剪区域的 CDP 限制）。\n\n!!! info \"截图功能\"\n    请参阅[截图和 PDF](../features/automation/screenshots-and-pdfs.md)了解整页与元素截图的比较。\n\n## JavaScript 执行上下文\n\n**关键 CDP 功能：** `Runtime.callFunctionOn(objectId, ...)` 在元素上下文中执行 JavaScript（`this` = 元素）。\n\n```python\nasync def execute_script(self, script: str, return_by_value=False):\n    return await self._execute_command(\n        RuntimeCommands.call_function_on(self._object_id, script, return_by_value)\n    )\n```\n\n**用例：**\n\n- 可见性检查：`await element.is_visible()` → JavaScript 检查计算样式\n- 样式操作：`await element.execute_script(\"this.style.border = '2px solid red'\")`\n- 属性访问：某些属性需要 JavaScript（例如，输入的 `value`）\n\n**替代方案（未使用）：** 使用元素选择器执行全局脚本 → 较慢，有陈旧引用风险。\n\n## 状态验证管道\n\n**可靠性策略：** 在交互之前预先检查元素状态以防止失败。\n\n| 检查 | 目的 | 实现 |\n|-------|---------|----------------|\n| `is_visible()` | 元素在视口中，未隐藏 | JavaScript：`offsetWidth > 0 && offsetHeight > 0` |\n| `is_on_top()` | 没有叠加层阻挡元素 | JavaScript：`document.elementFromPoint(x, y) === this` |\n| `is_interactable()` | 可见 + 在顶部 | 结合两项检查 |\n\n**为什么使用 JavaScript 检查可见性？** CSS `display: none`、`visibility: hidden`、`opacity: 0` 都以不同方式影响可见性。JavaScript 提供统一检查。\n\n## 性能策略\n\n### 1. 特定于操作的优化\n\n**原则：** 为每种操作类型选择最快的方法。\n\n| 操作 | 主要方法 | 理由 |\n|-----------|-----------------|-----------|\n| 文本提取 | BeautifulSoup 解析 | 比 JavaScript `innerText` 更准确 |\n| 可见性检查 | JavaScript | 单个 CDP 调用 vs 多个 DOM 查询 |\n| 点击 | CDP 鼠标事件 | 最真实，反检测所需 |\n| 边界 | `DOM.getBoxModel` | 比 JavaScript 快，有 JS 回退 |\n\n### 2. 本地计算\n\n**最小化 CDP 往返**，尽可能在本地计算：\n\n```python\n# 好：单次边界查询，本地计算\nbounds = await element.bounds\nclick_x = bounds['x'] + bounds['width'] / 2 + offset_x\nclick_y = bounds['y'] + bounds['height'] / 2 + offset_y\n\n# 不好：为简单数学进行多次 CDP 调用\nclick_x = await element.execute_script('return this.offsetLeft + this.offsetWidth / 2')\nclick_y = await element.execute_script('return this.offsetTop + this.offsetHeight / 2')\n```\n\n### 3. 缓存属性\n\n**设计决策：** 在创建时缓存静态属性：\n\n```python\n# 快速同步访问（无 CDP 调用）\nelement_id = element.id\nelement_class = element.class_name\n```\n\n**权衡：** 属性不会反映运行时更改。对于动态属性，使用异步：`await element.text`。\n\n## 关键架构决策\n\n| 决策 | 理由 |\n|----------|-----------|\n| **继承 FindElementsMixin** | 启用子搜索，维护 API 一致性 |\n| **混合同步/异步属性** | 平衡性能（同步）与新鲜度（异步）|\n| **JavaScript 回退** | 关键操作的可靠性优于性能 |\n| **特殊元素检测** | `<option>`、`<input type=\"file\">` 需要独特处理 |\n| **点击前可见性检查** | 清晰错误的快速失败 vs 静默失败 |\n\n## 总结\n\nWebElement 域通过以下方式在 Python 自动化代码和浏览器 DOM 之间架起桥梁：\n\n- **远程对象引用**通过 CDP `objectId`\n- **混合属性系统**平衡同步属性和异步状态\n- **多阶段交互管道**确保可靠性\n- **专门处理**元素类型变化\n\n**核心权衡：**\n\n| 决策 | 收益 | 成本 | 结论 |\n|----------|---------|------|---------|\n| Mixin 继承 | 干净的 API | 紧耦合 | 合理 |\n| 缓存属性 | 快速同步访问 | 陈旧数据风险 | 合理 |\n| JavaScript 回退 | 可靠性 | 性能损失 | 合理 |\n| 可见性预检查 | 清晰错误 | 额外的 CDP 调用 | 合理 |\n\n## 进一步阅读\n\n**实用指南：**\n\n- [元素查找](../features/element-finding.md) - 定位元素、选择器\n- [类人交互](../features/automation/human-interactions.md) - 点击、输入、真实性\n- [文件操作](../features/automation/file-operations.md) - 文件上传和下载\n\n**架构深入了解：**\n\n- [FindElements Mixin](./find-elements-mixin.md) - 选择器解析管道\n- [Tab 域](./tab-domain.md) - Tab 作为元素工厂\n- [连接层](./connection-layer.md) - WebSocket 通信\n"
  },
  {
    "path": "docs/zh/deep-dive/fingerprinting/behavioral-fingerprinting.md",
    "content": "# 行为 Fingerprinting\n\n行为 fingerprinting 分析的是用户与 Web 应用的交互方式，而非他们使用的工具。虽然网络和浏览器指纹可以通过设置正确的值来伪造，但人类行为遵循生物力学模式，难以令人信服地复制。检测系统收集鼠标移动、击键时间、滚动行为和交互序列，然后使用统计模型来区分人类和自动化。\n\n本文档涵盖检测技术、背后的科学原理，以及 Pydoll 的人性化功能如何应对各个检测向量。\n\n!!! info \"模块导航\"\n    - [网络 Fingerprinting](./network-fingerprinting.md)：TCP/IP、TLS、HTTP/2 协议 fingerprinting\n    - [浏览器 Fingerprinting](./browser-fingerprinting.md)：Canvas、WebGL、navigator 属性\n    - [规避技术](./evasion-techniques.md)：实用对策\n\n## 鼠标移动分析\n\n鼠标移动是最强大的行为指标之一，因为人类的运动控制遵循生物力学定律，简单的自动化无法复制。检测系统收集 `mousemove` 事件（每个事件包含 x、y 坐标和时间戳），并分析轨迹的属性，以区分有机移动和程序化光标瞬移。\n\n### Fitts's Law\n\nFitts's Law 描述将指针移动到目标所需的时间。Shannon 公式（MacKenzie, 1992）是使用最广泛的版本，表述如下：\n\n```\nT = a + b * log2(D/W + 1)\n```\n\n其中 `T` 是移动时间，`a` 是代表反应/启动时间的常数，`b` 是代表输入设备固有速度的常数，`D` 是到目标的距离，`W` 是目标的宽度（大小）。对数关系意味着距离加倍会增加固定的时间量，而目标大小减半也会增加相同的固定时间量。\n\n这对机器人检测具有重要意义。人类到达小而远的目标需要更长时间，而到达大而近的目标则很快。他们在移动开始时加速，大约在路径中点达到峰值速度，并在接近目标时减速。如果机器人无论距离和目标大小如何都以恒定时间移动光标，就违反了 Fitts's Law，很容易被检测到。\n\n检测系统测量每次点击事件的移动时间，根据距离和目标大小计算预期时间，并标记那些明显快于 Fitts's Law 预测或在距离/大小与移动时间之间没有相关性的移动。\n\n### 轨迹形状\n\n两点之间的人类手部移动不是直线。Abend、Bizzi 和 Morasso（1982）的研究表明，由于手臂关节和肌肉的生物力学约束，手部路径通常是弯曲的。Flash 和 Hogan（1985）证明，人类到达运动遵循最小 jerk 轨迹，即轨迹在移动持续时间内最小化 jerk（加速度的导数）的积分。由此产生的速度曲线呈钟形，用五次（5 阶）多项式描述：\n\n```\nx(t) = x0 + (xf - x0) * (10t^3 - 15t^4 + 6t^5)\n```\n\n其中 `t` 是从 0 到 1 的归一化时间，`x0`/`xf` 是起始和终止位置。这会产生从静止开始的平滑加速、大约在路径中点的峰值速度，以及回到静止的平滑减速。\n\n检测系统分析轨迹曲率、速度曲线和加速度模式。它们寻找的具体信号包括：\n\n**直线检测。** 两点之间完全笔直的路径（每个采样点曲率为零）是最明显的机器人信号。由于手臂旋转关节的存在，人类路径总是有一定的曲率。\n\n**恒定速度。** 人类表现出钟形速度曲线（加速、达到峰值、减速）。整个移动过程中恒定的速度表明是线性插值，这是大多数自动化工具的默认行为。\n\n**缺少子移动。** 长距离移动由多个重叠的子移动组成（Meyer 等，1988），每个子移动都有自己的速度峰值。覆盖 500+ 像素但只有一个平滑速度峰值的移动是可疑的；该距离的真实移动通常会显示 2-4 个速度峰值。\n\n**无 overshoot。** 人类经常会略微超过目标（5-15 像素），然后做一个小的校正回来。每次都精确命中目标的完美移动在统计上是不可能的。\n\n### 移动熵\n\n在这个语境中，熵衡量鼠标路径的不可预测性。检测系统将轨迹分成段，测量每个点的方向变化，并计算方向变化分布上的 Shannon 熵。直线的熵为零（每段指向相同方向）。随机游走的熵最大。人类移动具有中等到高的熵，反映了有意方向和非自主变异性的结合。\n\n在一个会话中多次鼠标移动都表现出低熵是一个强烈的机器人信号，即使个别移动具有合理的曲率。\n\n### Pydoll 的鼠标 humanize 功能\n\nPydoll 通过点击操作上的 `humanize=True` 参数实现了全面的鼠标人性化。启用后，鼠标模块会生成针对上述每个检测向量的移动：\n\n路径遵循带有随机控制点的三次 Bezier 曲线，产生自然曲率而非直线。沿路径的速度遵循最小 jerk 曲线（`10t^3 - 15t^4 + 6t^5`），产生 Fitts's Law 预测的钟形速度曲线。移动持续时间使用 Fitts's Law 和可配置常数（默认 `a=0.070`，`b=0.150`）计算。\n\n通过向光标位置添加高斯噪声来模拟生理震颤，振幅与速度成反比（当手移动缓慢时震颤更明显，这与真实生理学一致）。overshoot 以 70% 的概率发生，超过目标总距离的 3-12%，然后进行校正移动。微停顿（15-40ms）以 3% 的概率在移动过程中发生，模拟短暂的犹豫。\n\n```python\n# 基本的 humanize 点击\nawait element.click(humanize=True)\n\n# 也可以直接使用 Mouse 类获得更多控制\nfrom pydoll.interactions.mouse import Mouse\n\nmouse = Mouse(connection_handler)\nawait mouse.click(500, 300, humanize=True)\n```\n\n!!! note \"Pydoll 目前未实现的功能\"\n    Pydoll 的鼠标人性化目前不会对非常长的距离建模子移动（路径是单个 Bezier 段）。对于大多数 Web 交互（距离在 500 像素以内），这已经足够了。极长的移动（全屏对角线穿越）可能会受益于未来的多段支持。\n\n## 击键动态\n\n击键动态分析键盘输入的时间模式。该技术可以追溯到 1850 年代的电报操作员，他们可以通过各自的莫尔斯电码\"拳头\"（特征性时间模式）来识别彼此。现代系统通过 `keydown` 和 `keyup` 事件以毫秒精度测量时间。\n\n### 时间特征\n\n两个基本测量是停留时间（单个按键 `keydown` 和 `keyup` 之间的持续时间，人类通常为 50-200ms）和飞行时间（释放一个键到按下下一个键之间的持续时间，通常为 80-400ms）。连续按键对的停留时间和飞行时间的组合称为二元组（digraph）延迟。\n\n二元组延迟并不均匀。它们取决于键入的特定按键对（bigram），因为打字是一种运动技能，常见序列存储为程序记忆。关键的生物力学因素包括：\n\n**双手交替。** 用双手交替输入的 bigram（如 \"th\"，在 QWERTY 键盘上 \"t\" 是左手，\"h\" 是右手）通常比同一只手输入的 bigram（如 \"de\"，两个键都在左手）更快。交替手可以在第一只手完成击键时就开始移动。\n\n**手指距离。** 主行到主行的过渡最快。到达顶行或底行会增加与手指必须移动的物理距离成比例的时间。\n\n**手指独立性。** 同一只手上的无名指和小指组合比食指和中指组合更慢，因为无名指和小指共享肌腱，独立运动控制能力较弱。\n\n**频率效应。** 经常输入的 bigram（如英语中的 \"th\"、\"er\"、\"in\"）由于运动记忆而执行更快，与其物理布局无关。\n\n### 检测信号\n\n检测系统寻找几种将人类打字与自动化区分开的信号：\n\n**零或恒定停留时间。** 许多自动化工具在 `keydown` 和 `keyup` 事件之间以零或接近零的延迟（低于 5ms）发送。真实的按键具有可测量的停留时间。所有按键的停留时间恒定同样可疑。\n\n**均匀飞行时间。** 设置固定的击键间隔（如 `type_text(\"hello\", interval=0.1)`）会产生完全规律的时间，非常容易被检测。人类的飞行时间因 bigram、疲劳和认知负荷而变化。\n\n**无打字错误。** 在较长的文本输入（50+ 个字符）中，完全没有退格键或删除键的按下是不寻常的。人类的错误率大约为 1-5%，取决于打字熟练度和文本复杂度。\n\n**超人速度。** 持续超过 150 WPM 的打字速度超出了除精英竞技打字员以外所有人的能力。比这更快发送字符的自动化工具会立即被标记。\n\n### Pydoll 的键盘 humanize 功能\n\nPydoll 的 `type_text(humanize=True)` 通过可配置参数应对每个检测向量：\n\n击键延迟从均匀分布中抽取（默认 30-120ms），而不是固定间隔。标点字符（`.!?;:,`）接收额外延迟（80-180ms），模拟打字者考虑句子结构时的停顿。思考停顿（300-700ms）以 2% 的概率发生，模拟短暂的思考时刻。分心停顿（500-1200ms）以 0.5% 的概率发生，模拟打字者看向别处或被短暂打断。\n\n逼真的打字错误以大约每字符 2% 的概率发生，包含五种按其真实世界频率加权的不同错误类型：相邻键错误（55%，按下 QWERTY 键盘上的相邻键）、换位（20%，交换两个连续字符）、重复按键（12%，连续按两次键）、跳过字符（8%，在正确输入前犹豫）和遗漏空格（5%，忘记单词之间的空格）。每种错误类型包含逼真的恢复序列（停顿、退格、校正）和适当的时间。\n\n```python\n# Humanize 打字\nawait element.type_text(\"Hello, world!\", humanize=True)\n\n# 使用自定义时间配置\nfrom pydoll.interactions.keyboard import Keyboard, TimingConfig, TypoConfig\n\nconfig = TimingConfig(\n    keystroke_min=0.04,\n    keystroke_max=0.15,\n    thinking_probability=0.03,\n)\nkeyboard = Keyboard(connection_handler, timing_config=config)\nawait keyboard.type_text(\"Custom timing example\", humanize=True)\n```\n\n!!! note \"Pydoll 目前未实现的功能\"\n    Pydoll 的键盘人性化使用均匀随机延迟，而非基于 bigram 的时间。它不会建模每个按键的停留时间变化或双手交替速度差异。对于大多数自动化场景（表单填写、搜索查询），均匀变化足以通过行为检测。需要认证级别击键生物识别规避的应用需要自定义时间模型。\n\n## 滚动行为分析\n\n滚动 fingerprinting 分析用户如何在页面内容中进行垂直（和水平）导航。人类滚动和自动滚动之间的区别非常明显：程序化的 `window.scrollTo()` 调用产生即时的离散跳跃，而通过鼠标滚轮、触控板或触摸屏进行的人类滚动则产生一连串带有惯性和减速效果的小增量事件。\n\n### 物理滚动特征\n\n鼠标滚轮滚动产生带有一致 delta 值的离散 `wheel` 事件（通常每个凹槽 100 或 120 像素，取决于操作系统和浏览器）。事件以不规则的间隔到达，反映用户转动滚轮的速度。触控板滚动产生许多小事件，delta 值递减，模拟物理惯性。触摸滚动类似于触控板，但初始 delta 更大，减速尾部更长。\n\n检测系统分析 delta 分布、事件间时间和减速曲线。`scrollTo(0, 5000)` 调用产生单次跳跃且没有中间事件，这与人类滚动产生的数百个增量事件根本不同。\n\n### 检测信号\n\n**即时滚动。** 使用 `window.scrollTo()` 或 `window.scrollBy()` 配合大值会产生零中间滚动事件。监听 `scroll` 事件的检测系统会看到滚动位置在单帧内发生变化。\n\n**均匀 delta。** 程序化滚动模拟以恒定 delta 值发送 wheel 事件（例如始终 100 像素），缺少人类滚动中的自然变化，人类滚动的 delta 值由于手指压力不一致会波动 10-30%。\n\n**无减速。** 人类滚动，尤其是在触控板上，有一个惯性阶段，在用户抬起手指后滚动继续，速度呈指数递减。突然停止的自动滚动缺少这个减速尾部。\n\n**缺少方向变化。** 人类经常过度滚动然后略微回滚，或在页面中途暂停阅读内容。以恒定速度沿一个方向移动而没有暂停或反转的自动滚动是可疑的。\n\n### Pydoll 的滚动 humanize 功能\n\nPydoll 的滚动模块通过 `scroll.by(position, distance, humanize=True)` 实现人性化滚动：\n\n滚动遵循三次 Bezier 缓动曲线（默认控制点 `0.645, 0.045, 0.355, 1.0`），产生自然的加速和减速。每帧 ±3 像素的 jitter 为 delta 值添加变化。微停顿（20-50ms）以 5% 的概率发生，模拟短暂的阅读停顿。overshoot 以 15% 的概率发生，滚过目标 2-8% 然后校正回来。对于长距离，滚动被分解为多个\"轻弹\"手势（每次 100-1200 像素），模拟真实用户通过重复滑动而非单次连续动作来滚动长页面的方式。\n\n```python\nfrom pydoll.interactions.scroll import Scroll, ScrollPosition\n\nscroll = Scroll(connection_handler)\n\n# Humanize 向下滚动 800 像素\nawait scroll.by(ScrollPosition.Y, 800, humanize=True)\n\n# 滚动到顶部/底部使用多次类人轻弹\nawait scroll.to_bottom(humanize=True)\n```\n\n## 其他检测向量\n\n除了鼠标、键盘和滚动分析之外，复杂的检测系统还监控其他几种行为信号。\n\n### 焦点和可见性\n\nPage Visibility API（`document.visibilityState`）和焦点事件（`window.onfocus`、`window.onblur`）揭示用户是否在主动查看页面。真实用户的会话包括标签页切换、窗口最小化和不活动期。一个连续保持焦点数小时而没有一个 blur 事件的自动化脚本在行为上是异常的。同样，`document.hasFocus()` 在较长时间内持续返回 `true` 也是不寻常的。\n\n### 空闲模式\n\n真实用户有自然的空闲期：阅读内容、在行动前思考、被分心。检测系统测量交互之间空闲时间的分布。如果一个会话中每个动作都在前一个动作的 100-500ms 内发生，没有更长的停顿，这种模式在统计上与人类浏览有明显区别——人类浏览中动作之间 2-30 秒的空闲期是正常的。\n\n### 事件序列完整性\n\n浏览器为用户交互生成特定的事件序列。一次鼠标点击产生 `pointerdown`、`mousedown`、`pointerup`、`mouseup`、`click`，按此顺序，之前还有 `pointermove`/`mousemove` 事件显示光标正在接近点击目标。发送裸 `click` 事件而没有前置移动和指针事件的自动化工具可以通过事件序列分析被检测到。\n\nPydoll 基于 CDP 的事件发送生成完整的事件序列，因为它使用 Chrome 的输入模拟，产生与真实用户输入相同的事件链。\n\n## 机器学习检测\n\n现代反机器人系统（DataDome、Akamai Bot Manager、Cloudflare Bot Management、PerimeterX/HUMAN Security）不使用简单的阈值规则。它们在数百万真实用户会话和数百万已知机器人会话上训练机器学习模型，学习基于 50+ 个特征同时区分人类和自动化。\n\n这些模型捕获难以列举为单独规则的统计属性：移动速度和曲率的联合分布、打字速度和错误率之间的相关性、滚动深度和阅读时间之间的关系，以及浏览会话的整体\"节奏\"。一个通过每项单独检查但在特征之间存在微妙错误相关性的系统，仍然可以被训练良好的模型标记。\n\n实际意义在于，行为规避必须在所有交互类型之间保持一致，而不仅仅是单独看起来合理。Pydoll 的 `humanize=True` 参数提供了跨鼠标、键盘和滚动交互的连贯人性化层，但开发者仍然需要负责更高层面的行为合理性：在页面加载之间添加阅读延迟、变化多页工作流的节奏，以及包含自然的空闲期。\n\n## 参考文献\n\n- Fitts, P. M. (1954). The Information Capacity of the Human Motor System in Controlling the Amplitude of Movement. Journal of Experimental Psychology.\n- MacKenzie, I. S. (1992). Fitts' Law as a Research and Design Tool in Human-Computer Interaction. Human-Computer Interaction.\n- Flash, T., & Hogan, N. (1985). The Coordination of Arm Movements: An Experimentally Confirmed Mathematical Model. Journal of Neuroscience.\n- Abend, W., Bizzi, E., & Morasso, P. (1982). Human Arm Trajectory Formation. Brain.\n- Meyer, D. E., Abrams, R. A., Kornblum, S., Wright, C. E., & Smith, J. E. K. (1988). Optimality in Human Motor Performance. Psychological Review.\n- Ahmed, A. A. E., & Traore, I. (2007). A New Biometric Technology Based on Mouse Dynamics. IEEE TDSC.\n"
  },
  {
    "path": "docs/zh/deep-dive/fingerprinting/browser-fingerprinting.md",
    "content": "# 浏览器 Fingerprinting\n\n浏览器 fingerprinting 通过分析 JavaScript API、HTTP 标头和渲染引擎暴露的属性来识别客户端。与网络 fingerprinting 检查操作系统内核和 TLS 库的协议级信号不同，浏览器 fingerprinting 针对的是应用层：具体的浏览器、版本、配置以及运行它的硬件。这些信号可以通过标准 Web API 被任何网站访问，而足够多属性的组合往往能在数百万访客中创建出唯一的指纹。\n\n!!! info \"模块导航\"\n    - [网络 Fingerprinting](./network-fingerprinting.md): TCP/IP、TLS、HTTP/2 协议 fingerprinting\n    - [行为 Fingerprinting](./behavioral-fingerprinting.md): 鼠标、键盘、滚动分析\n    - [规避技术](./evasion-techniques.md): 实用对策\n\n## JavaScript Navigator 属性\n\n`navigator` 对象是浏览器 fingerprinting 数据最丰富的单一来源。它暴露了数十个属性，揭示了浏览器、其功能以及运行它的系统。检测系统会收集这些属性，将它们相互交叉比对并与 HTTP 标头进行对照，标记出不一致之处。\n\n以下 JavaScript 收集了 fingerprinting 系统通常检查的核心属性集：\n\n```javascript\nconst fingerprint = {\n    // Identity\n    userAgent: navigator.userAgent,\n    platform: navigator.platform,\n    vendor: navigator.vendor,\n\n    // Language and locale\n    language: navigator.language,\n    languages: navigator.languages,\n\n    // Hardware\n    hardwareConcurrency: navigator.hardwareConcurrency,\n    deviceMemory: navigator.deviceMemory,\n    maxTouchPoints: navigator.maxTouchPoints,\n\n    // Features\n    cookieEnabled: navigator.cookieEnabled,\n    doNotTrack: navigator.doNotTrack,\n    webdriver: navigator.webdriver,\n\n    // Screen\n    screenWidth: screen.width,\n    screenHeight: screen.height,\n    colorDepth: screen.colorDepth,\n    devicePixelRatio: window.devicePixelRatio,\n\n    // Window chrome (toolbar, scrollbar dimensions)\n    chromeHeight: window.outerHeight - window.innerHeight,\n    chromeWidth: window.outerWidth - window.innerWidth,\n\n    // Timezone\n    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n    timezoneOffset: new Date().getTimezoneOffset(),\n};\n```\n\n其中一些属性值得单独关注，因为它们在 fingerprinting 中权重更高，或者更容易被自动化工具错误配置。\n\n### 平台与 User-Agent 一致性\n\n`navigator.platform` 属性返回一个字符串，如 `Win32`、`MacIntel` 或 `Linux x86_64`。检测系统会将其与 User-Agent 标头进行比较。如果 HTTP User-Agent 声称是 `Windows NT 10.0`，但 `navigator.platform` 返回 `Linux x86_64`，这种不匹配就是一个强烈的信号。这是自动化中最常见的错误之一：通过 `--user-agent=` 设置了自定义 User-Agent，却没有同时覆盖 platform。\n\n### 硬件属性\n\n`navigator.hardwareConcurrency` 返回逻辑 CPU 核心数。值为 1 或 2 通常意味着是最小化的虚拟机或容器，而非真实用户的机器。`navigator.deviceMemory` 以 GB 为单位报告大致的 RAM 容量（0.25、0.5、1、2、4、8）。此属性仅在 Chromium 浏览器中可用；Firefox 和 Safari 返回 `undefined`。这两个值应与声称的设备一致：User-Agent 声称是现代桌面设备，却报告 1 个核心和 0.5 GB RAM，这是可疑的。\n\n### WebDriver 属性\n\n当浏览器被基于 WebDriver 的自动化工具（Selenium、以 WebDriver 模式运行的 Playwright）控制时，`navigator.webdriver` 属性为 `true`。这是最明显的自动化指标。Pydoll 直接使用 CDP（Chrome DevTools Protocol），不会设置此标志。在 Pydoll 控制的浏览器中，`navigator.webdriver` 为 `undefined`，与正常用户会话的行为一致。\n\n### 插件\n\n`navigator.plugins` 属性在历史上是一个强力的 fingerprinting 向量，因为不同的浏览器和操作系统配置会暴露不同的插件列表。现代 Chromium 浏览器（Chrome 90+）无论实际插件状态如何，都返回固定的五个 PDF 相关插件：\n\n```javascript\n// Modern Chrome always returns these 5 plugins:\n// 1. PDF Viewer\n// 2. Chrome PDF Viewer\n// 3. Chromium PDF Viewer\n// 4. Microsoft Edge PDF Viewer\n// 5. WebKit built-in PDF\nconsole.log(navigator.plugins.length); // 5\n```\n\n一个常见的误解认为现代浏览器会为 `navigator.plugins` 返回空数组。这是不正确的。返回空数组本身就是一个检测信号，表明可能是 headless 模式或非浏览器 HTTP 客户端。\n\n### 屏幕和窗口尺寸\n\n`window.outerWidth`/`outerHeight` 与 `window.innerWidth`/`innerHeight` 之间的差异代表浏览器 chrome（工具栏、滚动条、窗口边框）。Headless 浏览器通常报告零差异，因为它们没有可见的 UI。检测系统会将 `outerWidth` 等于 `innerWidth` 的客户端标记为可能的 headless。同样，`screen.width` 与 `innerWidth` 完全匹配表明这是一个最大化的 headless 窗口，而非正常的桌面会话。\n\n`devicePixelRatio` 因显示器而异：标准显示器报告 `1.0`，MacBook Retina 显示屏报告 `2.0`，智能手机报告 `2.0` 到 `3.0`。此值应与 User-Agent 中声称的设备一致。\n\n## User-Agent Client Hints\n\n现代 Chromium 浏览器（Chrome、Edge、Opera）通过 Client Hints 标头补充传统的 User-Agent 字符串：`Sec-CH-UA`、`Sec-CH-UA-Platform`、`Sec-CH-UA-Mobile`，以及（按需提供的）更高熵值如 `Sec-CH-UA-Full-Version-List`、`Sec-CH-UA-Arch` 和 `Sec-CH-UA-Bitness`。\n\n```http\nSec-CH-UA: \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\", \"Not:A-Brand\";v=\"99\"\nSec-CH-UA-Mobile: ?0\nSec-CH-UA-Platform: \"Windows\"\n```\n\nClient Hints 提供结构化的、机器可读的数据，更难被不一致地伪造。服务器可以将 `Sec-CH-UA-Platform` 标头与 `navigator.platform`、User-Agent 字符串以及 TCP/IP 指纹进行比较。这些层之间的任何不一致都是检测信号。\n\nJavaScript 端的等价物是 `navigator.userAgentData`，它将 `brands`、`mobile` 和 `platform` 作为低熵值暴露，并通过 `getHighEntropyValues()` 提供详细的版本、架构和位宽信息：\n\n```javascript\n// Low-entropy (always available, no permission needed)\nconsole.log(navigator.userAgentData.brands);\n// [{brand: \"Chromium\", version: \"120\"}, {brand: \"Google Chrome\", version: \"120\"}, ...]\nconsole.log(navigator.userAgentData.platform); // \"Windows\"\nconsole.log(navigator.userAgentData.mobile);   // false\n\n// High-entropy (requires promise, may require permission)\nconst highEntropy = await navigator.userAgentData.getHighEntropyValues([\n    'architecture', 'bitness', 'platformVersion', 'uaFullVersion'\n]);\n// {architecture: \"x86\", bitness: \"64\", platformVersion: \"15.0.0\", ...}\n```\n\n!!! warning \"浏览器支持\"\n    Client Hints 是 Chromium 独有的功能。Firefox 和 Safari 不会发送 `Sec-CH-UA` 标头，也不会暴露 `navigator.userAgentData`。如果 User-Agent 声称是 Firefox，但服务器收到了 Client Hints 标头，那么该客户端不是 Firefox。\n\n## Canvas Fingerprinting\n\nCanvas fingerprinting 利用了 HTML5 Canvas API 在不同 GPU、图形驱动、操作系统和浏览器组合下产生微妙不同像素输出的特性。这种差异来自字体光栅化（亚像素渲染、字体微调、抗锯齿）、GPU 特定的着色器执行、图形管线中的浮点精度，以及操作系统级别的文本渲染库（Windows 上的 DirectWrite、macOS 上的 Core Text、Linux 上的 FreeType）。\n\n该技术在隐藏的 canvas 上绘制文本、形状和渐变，提取像素数据，然后进行哈希处理：\n\n```javascript\nfunction generateCanvasFingerprint() {\n    const canvas = document.createElement('canvas');\n    canvas.width = 220;\n    canvas.height = 30;\n    const ctx = canvas.getContext('2d');\n\n    // Colored rectangle (exposes blending differences)\n    ctx.fillStyle = '#f60';\n    ctx.fillRect(125, 1, 62, 20);\n\n    // Text with emoji (maximizes rendering variation)\n    ctx.font = '14px Arial';\n    ctx.textBaseline = 'alphabetic';\n    ctx.fillStyle = '#069';\n    ctx.fillText('Cwm fjordbank glyphs vext quiz, 😃', 2, 15);\n\n    // Semi-transparent overlay (exposes alpha compositing differences)\n    ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';\n    ctx.fillText('Cwm fjordbank glyphs vext quiz, 😃', 4, 17);\n\n    return canvas.toDataURL();\n}\n```\n\n全字母句 \"Cwm fjordbank glyphs vext quiz\" 之所以被选用，是因为它使用了不常见的字符组合，能够充分测试字体渲染。表情符号增加了另一个维度，因为表情符号渲染在不同操作系统之间差异显著。半透明叠加测试了 alpha 合成，这在不同 GPU 实现之间也有所不同。\n\nCanvas fingerprinting 能有效区分大类设备，但其唯一性有时被夸大了。Laperdrix 等人（2016）的研究发现，仅靠 canvas 指纹只能提供中等程度的区分能力，其真正价值在于与其他信号（WebGL、navigator 属性、时区）结合使用以实现高唯一性。\n\n!!! note \"Canvas 噪声注入\"\n    一些隐私工具会向 canvas 输出注入随机噪声以干扰 fingerprinting。检测系统通过在同一会话中多次请求 canvas 指纹来应对。如果哈希值在请求之间发生变化，则说明存在噪声注入，而这本身就是一个检测信号。因此，随机化 canvas 输出适得其反：它既不能防止识别，又暴露了反 fingerprinting 工具的使用。\n\n由于 Pydoll 控制的是一个具有真实 GPU 渲染的 Chrome 实例，canvas 指纹是真实的，并且在多次读取之间保持一致。无需注入或伪造。\n\n## WebGL Fingerprinting\n\nWebGL fingerprinting 将 canvas fingerprinting 扩展到 3D 渲染管线。它更为强大，因为它直接暴露了难以伪造的硬件标识符。\n\n最具辨识度的数据来自 `WEBGL_debug_renderer_info` 扩展，它揭示了 GPU 供应商和型号：\n\n```javascript\nfunction getWebGLFingerprint() {\n    const canvas = document.createElement('canvas');\n    const gl = canvas.getContext('webgl');\n    if (!gl) return null;\n\n    // GPU identification (most distinctive)\n    const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');\n    const vendor = debugInfo\n        ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL)\n        : gl.getParameter(gl.VENDOR);\n    const renderer = debugInfo\n        ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)\n        : gl.getParameter(gl.RENDERER);\n\n    return {\n        vendor,    // e.g. \"Google Inc. (NVIDIA)\"\n        renderer,  // e.g. \"ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0)\"\n        version: gl.getParameter(gl.VERSION),\n        shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),\n        maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),\n        extensions: gl.getSupportedExtensions(),\n    };\n}\n```\n\nrenderer 字符串直接标明了 GPU 硬件。声称是移动设备的客户端却报告了桌面 GPU，这显然是不一致的。虚拟机通常报告软件渲染器如 \"SwiftShader\" 或 \"llvmpipe\"，这在真实用户中几乎不会出现。\n\n除了元数据之外，WebGL 还可以渲染一个 3D 场景（例如一个渐变三角形）并对像素输出进行哈希，产生类似于 canvas fingerprinting 的渲染指纹，但针对的是 3D 管线。GPU 标识符、支持的扩展、参数限制（`MAX_TEXTURE_SIZE`、`MAX_VIEWPORT_DIMS`）和着色器精度格式的组合，创建了图形栈的详细指纹。\n\n## AudioContext Fingerprinting\n\nWeb Audio API 通过处理音频并测量输出来生成指纹。标准技术是创建一个 `OscillatorNode`，将其路由通过一个 `DynamicsCompressorNode`，然后从 `AnalyserNode` 或 `OfflineAudioContext` 读取生成的音频采样。不同浏览器和操作系统音频栈在音频处理实现上的差异会产生不同的输出。\n\n```javascript\nfunction getAudioFingerprint() {\n    const ctx = new OfflineAudioContext(1, 44100, 44100);\n    const oscillator = ctx.createOscillator();\n    oscillator.type = 'triangle';\n    oscillator.frequency.setValueAtTime(10000, ctx.currentTime);\n\n    const compressor = ctx.createDynamicsCompressor();\n    compressor.threshold.setValueAtTime(-50, ctx.currentTime);\n    compressor.knee.setValueAtTime(40, ctx.currentTime);\n    compressor.ratio.setValueAtTime(12, ctx.currentTime);\n    compressor.attack.setValueAtTime(0, ctx.currentTime);\n    compressor.release.setValueAtTime(0.25, ctx.currentTime);\n\n    oscillator.connect(compressor);\n    compressor.connect(ctx.destination);\n    oscillator.start(0);\n\n    return ctx.startRendering().then(buffer => {\n        const data = buffer.getChannelData(0);\n        // Hash a subset of the audio samples\n        let hash = 0;\n        for (let i = 4500; i < 5000; i++) {\n            hash += Math.abs(data[i]);\n        }\n        return hash;\n    });\n}\n```\n\nAudioContext fingerprinting 的部署范围不如 canvas 或 WebGL fingerprinting 广泛，但它为整体指纹增加了另一个维度。该信号对于区分同一操作系统上的不同浏览器特别有用，因为音频处理在不同浏览器引擎之间的差异比在不同操作系统版本之间更大。\n\n## Battery Status API\n\nBattery Status API（`navigator.getBattery()`）暴露了设备的电池电量、充电状态以及预估的充电/放电时间。这些值在会话持续期间创建了一个短暂但唯一的指纹。\n\n此 API 仅在 Chromium 浏览器中可用。Firefox 在版本 52（2017 年）中出于隐私考虑将其移除，Safari 从未实现过。如果检测系统从声称是 Firefox 或 Safari 的客户端看到 Battery API 结果，就知道该客户端伪造了身份。\n\n## HTTP 标头 Fingerprinting\n\n除了 JavaScript API 之外，HTTP 标头提供了服务器在任何 JavaScript 执行之前就可见的 fingerprinting 信号。\n\n### 标头顺序\n\n浏览器以一致的、特定于版本的顺序发送 HTTP 标头。Chrome 将 `Sec-CH-UA` 标头放在较前的位置，位于 `User-Agent` 之前。Firefox 以 `User-Agent` 开头，随后是 `Accept` 和 `Accept-Language`。自动化 HTTP 库如 Python 的 `requests` 或 `httpx` 以另一种顺序发送标头，通常以 `Host` 和 `Connection` 开头。\n\n检测系统记录前 10-15 个标头的顺序，并与已知的浏览器签名进行比较。即使所有单独的标头值都正确，以错误的顺序发送也会暴露该请求不是由所声称的浏览器生成的。由于 Pydoll 控制的是真实的 Chrome 实例，标头顺序是真实的。\n\n### Accept-Encoding\n\n现代浏览器除了 `gzip` 和 `deflate` 之外还支持 Brotli 压缩（`br`）。Chrome 还支持 `zstd`。现代 Chrome 的 `Accept-Encoding` 类似于 `gzip, deflate, br, zstd`。声称是 Chrome 但缺少 Brotli 的客户端要么是过时的，要么是自动化的。\n\n### Accept-Language 一致性\n\n`Accept-Language` 标头应与 `navigator.language`、`navigator.languages`、时区以及 IP 地理位置保持一致。来自东京 IP、时区为 `Asia/Tokyo` 的请求带有 `Accept-Language: en-US`，对于旅行者来说是合理的，但与其他信号结合时就显得可疑。来自中国数据中心 IP、带有 `Accept-Language: zh-CN` 和时区 `America/New_York` 的请求则是强烈的代理指标。\n\n## 对 Pydoll 的影响\n\n由于 Pydoll 通过 CDP 驱动真实的 Chromium 浏览器，所有浏览器级别的指纹默认都是真实的。Canvas、WebGL 和 AudioContext 指纹来自实际的 GPU 和音频硬件。Navigator 属性、插件和屏幕尺寸反映了真实的浏览器状态。HTTP 标头（包括其顺序）由 Chrome 的网络栈生成。\n\n自动化中的主要风险是各层之间的不一致。设置自定义 User-Agent 而不同步相关属性会创建容易被检测到的不匹配。Pydoll 会自动处理这个问题：当它检测到浏览器参数中的 `--user-agent=` 时，会使用 `Emulation.setUserAgentOverride` 在所有层同步 User-Agent 字符串、平台和完整的 Client Hints 元数据。它还通过 `Page.addScriptToEvaluateOnNewDocument` 注入 `navigator.vendor` 和 `navigator.appVersion` 覆盖，以确保新打开的标签页中的一致性。\n\n对于时区和地理位置一致性（以匹配代理 IP 的位置），JavaScript 覆盖可以设置 `Intl.DateTimeFormat().resolvedOptions().timeZone` 和 `Date.prototype.getTimezoneOffset`。`--lang` 标志和 `set_accept_languages()` 配置语言标头。`webrtc_leak_protection` 选项可防止 WebRTC 暴露代理背后的真实 IP。\n\n总体原则是，Pydoll 提供真实的浏览器指纹作为基线，开发者只需确保可配置的层（User-Agent、时区、语言、地理位置）彼此一致，并与代理的特征相匹配。\n\n## 参考文献\n\n- Laperdrix, P., Rudametkin, W., & Baudry, B. (2016). Beauty and the Beast: Diverting Modern Web Browsers to Build Unique Browser Fingerprints. IEEE S&P.\n- Mowery, K., & Shacham, H. (2012). Pixel Perfect: Fingerprinting Canvas in HTML5. USENIX Security.\n- Eckersley, P. (2010). How Unique Is Your Web Browser? Privacy Enhancing Technologies Symposium.\n- W3C Client Hints Infrastructure: https://wicg.github.io/client-hints-infrastructure/\n- BrowserLeaks: https://browserleaks.com/\n- CreepJS: https://abrahamjuliot.github.io/creepjs/\n"
  },
  {
    "path": "docs/zh/deep-dive/fingerprinting/evasion-techniques.md",
    "content": "# 规避技术\n\n本文档介绍使用 Pydoll 规避 fingerprinting 检测的实用技术。前面几节分别描述了各层检测的工作原理：[网络 fingerprinting](./network-fingerprinting.md)（TCP/IP、TLS、HTTP/2）、[浏览器 fingerprinting](./browser-fingerprinting.md)（Canvas、WebGL、navigator 属性）以及[行为 fingerprinting](./behavioral-fingerprinting.md)（鼠标、键盘、滚动）。本节聚焦于反制措施。\n\n核心原则是各层之间的一致性。通过了某一检测层却在另一层失败，仍然会被标记。住宅 IP 搭配不匹配的 TCP 指纹，或完美的浏览器指纹搭配机器人式的鼠标移动，都会被任何关联信号的系统捕获。\n\n!!! info \"模块导航\"\n    - [网络 Fingerprinting](./network-fingerprinting.md)：协议级识别\n    - [浏览器 Fingerprinting](./browser-fingerprinting.md)：应用层检测\n    - [行为 Fingerprinting](./behavioral-fingerprinting.md)：人类行为分析\n\n## Pydoll 默认提供的能力\n\n在配置任何东西之前，了解 Pydoll 通过 CDP 使用真实 Chrome 实例默认提供了什么非常有帮助。\n\n**真实的网络指纹。** Chrome 的 TCP/IP 协议栈、TLS 实现（BoringSSL）和 HTTP/2 协议栈会产生真实的指纹。TLS ClientHello、HTTP/2 SETTINGS 帧、伪标头顺序和流优先级都与真实 Chrome 浏览器一致。以编程方式构造 HTTP 请求的工具（requests、httpx、curl）在这些层会产生非浏览器指纹。使用 Pydoll，这些默认就是真实的。\n\n**真实的浏览器指纹。** Canvas、WebGL 和 AudioContext 指纹来自真实的 GPU 和音频硬件。Navigator 属性、插件（标准的 5 个 PDF 插件）和 MIME 类型反映真实的浏览器状态。这里无需任何配置。\n\n**没有 `navigator.webdriver`。** Selenium、Playwright 和 Puppeteer 会将 `navigator.webdriver` 设置为 `true`。Pydoll 直接使用 CDP，不会设置此标志。该属性为 `undefined`，与正常用户会话一致。\n\n**完整的事件序列。** 当 Pydoll 通过 CDP 的 Input 域分发输入事件时，Chrome 会生成完整的事件链（pointermove、pointerdown、mousedown、pointerup、mouseup、click），与真实用户输入完全一致。\n\n## User-Agent 一致性\n\n自动化中最常见的 fingerprinting 不一致是 HTTP `User-Agent` 标头、JavaScript 中的 `navigator.userAgent`、`navigator.platform` 以及 Client Hints 标头（`Sec-CH-UA`、`Sec-CH-UA-Platform`）之间的不匹配。仅设置 `--user-agent=` 作为 Chrome 标志只会更改 HTTP 标头，而 JavaScript 属性和 Client Hints 保持不变。\n\nPydoll 自动解决此问题。当它在浏览器参数中检测到 `--user-agent=` 时，会：\n\n1. 解析 UA 字符串以提取浏览器名称、版本和操作系统。\n2. 通过 CDP 调用 `Emulation.setUserAgentOverride`，包含完整的 `userAgent`、正确的 `platform` 值（例如 Windows 对应 `Win32`）以及完整的 `userAgentMetadata`（Client Hints 数据，包括 `Sec-CH-UA`、`Sec-CH-UA-Platform`、`Sec-CH-UA-Full-Version-List`）。\n3. 通过 `Page.addScriptToEvaluateOnNewDocument` 注入 `navigator.vendor` 和 `navigator.appVersion` 覆盖，确保在新打开的标签页中也保持一致。\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.add_argument(\n    '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) '\n    'AppleWebKit/537.36 (KHTML, like Gecko) '\n    'Chrome/120.0.6099.109 Safari/537.36'\n)\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n    # 现在所有层都保持一致：\n    # - HTTP User-Agent 标头\n    # - navigator.userAgent / navigator.platform / navigator.appVersion\n    # - Sec-CH-UA / Sec-CH-UA-Platform / Sec-CH-UA-Full-Version-List\n    # - navigator.userAgentData.brands / .platform\n    await tab.go_to('https://example.com')\n```\n\n此覆盖会自动应用于初始标签页、通过 `browser.new_tab()` 创建的新标签页，以及通过 `browser.get_opened_tabs()` 发现的所有标签页。\n\n!!! note \"支持的平台\"\n    UA 解析器支持 Chrome、Edge、Windows（NT 6.1 到 10.0）、macOS、Linux、Android、iOS 和 Chrome OS。它按照 Chromium 规范生成正确的 GREASE 品牌值。\n\n## Timezone 和 Locale 一致性\n\n使用 proxy 时，浏览器的 timezone 和语言应与 proxy IP 的地理位置匹配。一个定位到东京的 IP 配合 `America/New_York` 的浏览器 timezone 和 `Accept-Language: en-US` 是可被检测的不一致。\n\n### 语言配置\n\n语言通过 Chrome 标志和 Pydoll 的选项 API 配置：\n\n```python\noptions = ChromiumOptions()\noptions.add_argument('--lang=ja-JP')\noptions.set_accept_languages('ja-JP,ja;q=0.9,en;q=0.8')\n```\n\n这会同时设置 `Accept-Language` HTTP 标头以及 `navigator.language` / `navigator.languages`。\n\n### Timezone 覆盖\n\nPydoll 目前没有封装 CDP 的 `Emulation.setTimezoneOverride` 命令，因此 timezone 覆盖需要 JavaScript 注入。需要覆盖的关键 API 是 `Intl.DateTimeFormat().resolvedOptions().timeZone` 和 `Date.prototype.getTimezoneOffset()`：\n\n```python\nasync def set_timezone(tab, timezone_id: str, offset_minutes: int):\n    \"\"\"\n    通过 JavaScript 覆盖 timezone。\n\n    Args:\n        timezone_id: IANA timezone 名称（例如 'Asia/Tokyo'）\n        offset_minutes: UTC 偏移量，以分钟为单位（例如 JST 为 -540）\n    \"\"\"\n    script = f'''\n        const _origDTF = Intl.DateTimeFormat;\n        Intl.DateTimeFormat = function(...args) {{\n            const opts = args[1] || {{}};\n            opts.timeZone = '{timezone_id}';\n            return new _origDTF(args[0], opts);\n        }};\n        Object.defineProperty(Intl.DateTimeFormat, 'prototype', {{\n            value: _origDTF.prototype\n        }});\n        Date.prototype.getTimezoneOffset = function() {{ return {offset_minutes}; }};\n    '''\n    await tab.execute_script(script)\n```\n\n!!! warning \"`execute_script` 与 `addScriptToEvaluateOnNewDocument`\"\n    `tab.execute_script()` 在当前页面上下文中运行 JavaScript。如果页面导航，覆盖就会丢失。对于需要在导航间持久保持的覆盖，请使用 CDP 的 `Page.addScriptToEvaluateOnNewDocument`，它会在每次新文档加载时、在任何页面 JavaScript 运行之前注入脚本。Pydoll 内部对 User-Agent 覆盖就使用了此方法。对于 timezone，你可以直接发送 CDP 命令：\n\n    ```python\n    await tab._connection_handler.execute_command(\n        'Page.addScriptToEvaluateOnNewDocument',\n        {'source': script}\n    )\n    ```\n\n### Geolocation 覆盖\n\n对于请求地理位置权限的网站，可以通过 JavaScript 覆盖 Geolocation API：\n\n```python\nasync def set_geolocation(tab, latitude: float, longitude: float):\n    script = f'''\n        navigator.geolocation.getCurrentPosition = function(success) {{\n            success({{\n                coords: {{\n                    latitude: {latitude}, longitude: {longitude},\n                    accuracy: 1, altitude: null, altitudeAccuracy: null,\n                    heading: null, speed: null\n                }},\n                timestamp: Date.now()\n            }});\n        }};\n        navigator.geolocation.watchPosition = function(success) {{\n            return navigator.geolocation.getCurrentPosition(success);\n        }};\n    '''\n    await tab.execute_script(script)\n```\n\n## WebRTC 泄露防护\n\nWebRTC 可以通过绕过 proxy 隧道的 STUN/TURN 服务器请求，暴露客户端的真实 IP 地址，即使使用了 proxy。Pydoll 提供了内置选项来防止这种情况：\n\n```python\noptions = ChromiumOptions()\noptions.webrtc_leak_protection = True\n# 添加：--force-webrtc-ip-handling-policy=disable_non_proxied_udp\n```\n\n这会强制 Chrome 将所有 WebRTC 流量通过 proxy 路由，防止 IP 泄露。在使用 proxy 进行隐蔽自动化时应始终启用此选项。\n\n## 行为 humanize\n\nPydoll 通过 `humanize=True` 参数为鼠标、键盘和滚动实现了 humanize 交互。这些不是未来功能或手动变通方案，而是框架内置的功能。\n\n### 鼠标\n\n```python\n# humanize 点击：贝塞尔曲线路径、Fitts 定律计时、\n# 最小加加速度速度曲线、颤动、过冲 + 修正\nawait element.click(humanize=True)\n```\n\n当 `humanize=True` 传递给 WebElement 的 `click()` 时，Pydoll 会生成一条从当前光标位置到元素的完整鼠标移动路径，使用带有随机控制点的三次贝塞尔曲线。速度遵循最小加加速度曲线。会添加生理性颤动、过冲（70% 概率）和微暂停。移动持续时间根据 Fitts 定律基于距离和目标大小计算。详细参数描述请参见[行为 Fingerprinting](./behavioral-fingerprinting.md#pydolls-mouse-humanization)。\n\n### 键盘\n\n```python\n# humanize 打字：可变延迟、逼真的错别字（约 2%）、\n# 标点停顿、思考停顿、分心停顿\nawait element.type_text(\"Hello, world!\", humanize=True)\n```\n\nhumanize 打字使用可变的按键间延迟（30-120ms 均匀分布）、标点停顿、思考停顿（2% 概率）、分心停顿（0.5% 概率），以及具有五种不同错误类型和自然修正序列的逼真错别字。完整参数说明请参见[行为 Fingerprinting](./behavioral-fingerprinting.md#pydolls-keyboard-humanization)。\n\n### 滚动\n\n```python\nfrom pydoll.interactions.scroll import Scroll, ScrollPosition\n\nscroll = Scroll(connection_handler)\n# humanize 滚动：贝塞尔缓动、抖动、微暂停、过冲\nawait scroll.by(ScrollPosition.Y, 800, humanize=True)\n```\n\nhumanize 滚动使用贝塞尔缓动曲线、逐帧抖动（±3px）、微暂停（5% 概率）和过冲修正（15% 概率）。大距离会被拆分为多个\"轻弹\"手势。详情请参见[行为 Fingerprinting](./behavioral-fingerprinting.md#pydolls-scroll-humanization)。\n\n## 请求拦截\n\nPydoll 通过 CDP 的 Fetch 域支持请求拦截，允许你在请求到达服务器之前修改标头、阻止请求或提供自定义响应：\n\n```python\nfrom pydoll.protocol.fetch.events import FetchEvent\n\nasync def handle_request(event):\n    request_id = event['params']['requestId']\n    request = event['params']['request']\n    headers = request.get('headers', {})\n\n    # 示例：确保声明了 Brotli 支持\n    if 'Accept-Encoding' in headers and 'br' not in headers['Accept-Encoding']:\n        headers['Accept-Encoding'] = 'gzip, deflate, br, zstd'\n\n    header_list = [{'name': k, 'value': v} for k, v in headers.items()]\n    await tab.continue_request(request_id=request_id, headers=header_list)\n\nawait tab.enable_fetch_events()\nawait tab.on(FetchEvent.REQUEST_PAUSED, handle_request)\n```\n\n实际上，使用 Pydoll 很少需要修改标头，因为 Chrome 本身就会生成正确的标头。请求拦截更适用于阻止追踪脚本、修改响应内容或调试。\n\n## 浏览器偏好设置增强真实性\n\nChrome 存储的用户偏好设置可以被 fingerprinting 系统检查。一个全新的浏览器配置文件——没有历史记录、没有保存的偏好设置、一切都是默认值——看起来与已使用数周的配置文件不同。Pydoll 的 `browser_preferences` 选项允许你预填充这些设置：\n\n```python\nimport time\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    'profile': {\n        'created_by_version': '120.0.6099.130',\n        'creation_time': str(time.time() - 90 * 86400),  # 90 天前\n        'exit_type': 'Normal',\n    },\n    'profile.default_content_setting_values': {\n        'cookies': 1,\n        'images': 1,\n        'javascript': 1,\n        'notifications': 2,  # \"询问\"（真实的默认值）\n    },\n}\n```\n\n## 常见错误\n\n### 随机化一切\n\n从头生成随机指纹（随机 hardwareConcurrency、随机 deviceMemory、随机屏幕尺寸）会产生不可能的组合。真实设备有受约束的配置：4 核、8 GB RAM、1920x1080 屏幕、Windows 10 是一个合理的配置。17 核、0.5 GB RAM、3840x2160 屏幕、`navigator.platform: Linux armv7l` 则不是。请使用从真实浏览器捕获的配置文件，而不是随机生成。\n\n### Canvas 噪声注入\n\n向 Canvas 输出添加随机噪声来防止 fingerprinting 会适得其反。检测系统会多次请求指纹。如果哈希值在请求之间发生变化，噪声注入就会被检测到，这本身就是一个强烈的自动化信号。使用 Pydoll，Canvas 指纹是真实且一致的。不要去动它。\n\n### 过时的 User-Agent\n\n使用 6 个月以上的浏览器版本的 User-Agent 是可被检测的，因为该版本缺少当前发行版应有的功能和 Client Hints 值。User-Agent 字符串应保持在最近 2-3 个 Chrome 主要版本之内。\n\n### 忽略会话级行为\n\n即使有完美的指纹和 humanize 的交互，会话级行为仍然很重要。在 60 秒内加载 100 个页面、从不滚动、只点击按钮（从不点击链接）、以及在没有任何标签页切换或空闲期的情况下保持数小时的持续焦点，这些都是行为异常。在导航之间添加阅读延迟，变化多页面工作流的节奏，并包含自然的空闲期。\n\n## 验证\n\n在大规模部署自动化之前，使用以下工具验证你的指纹：\n\n| 工具 | URL | 测试内容 |\n|------|-----|----------|\n| BrowserLeaks | https://browserleaks.com/ | Canvas、WebGL、字体、IP、WebRTC、HTTP/2 |\n| CreepJS | https://abrahamjuliot.github.io/creepjs/ | 欺骗检测、一致性检查 |\n| Fingerprint.com | https://fingerprint.com/demo/ | 商业级识别 |\n| PixelScan | https://pixelscan.net/ | 机器人检测分析 |\n| IPLeak | https://ipleak.net/ | WebRTC、DNS、IP 泄露 |\n\n使用 Pydoll 的基本验证脚本：\n\n```python\nasync def verify_fingerprint(tab):\n    result = await tab.execute_script('''\n        return {\n            userAgent: navigator.userAgent,\n            platform: navigator.platform,\n            webdriver: navigator.webdriver,\n            languages: navigator.languages,\n            plugins: navigator.plugins.length,\n            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n            colorDepth: screen.colorDepth,\n            deviceMemory: navigator.deviceMemory,\n            hardwareConcurrency: navigator.hardwareConcurrency,\n        };\n    ''')\n    fp = result['result']['result']['value']\n\n    # 检查明显问题\n    assert fp['webdriver'] is None, 'navigator.webdriver should be undefined'\n    assert fp['plugins'] == 5, f'Expected 5 plugins, got {fp[\"plugins\"]}'\n    assert 'HeadlessChrome' not in fp['userAgent'], 'Headless detected in UA'\n```\n\n## 参考资料\n\n- Chrome DevTools Protocol, Emulation Domain: https://chromedevtools.github.io/devtools-protocol/tot/Emulation/\n- Chrome DevTools Protocol, Fetch Domain: https://chromedevtools.github.io/devtools-protocol/tot/Fetch/\n- Chromium Source, Inspector Emulation Agent: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc\n"
  },
  {
    "path": "docs/zh/deep-dive/fingerprinting/index.md",
    "content": "# 浏览器与网络指纹\n\n本模块涵盖浏览器和网络指纹，这是现代 Web 自动化和检测系统的关键方面。\n\n指纹技术处于网络协议、密码学、浏览器内部原理和行为分析的交叉点。它包含了用于在会话间识别和跟踪设备、浏览器和用户，而不依赖于 Cookie 或 IP 地址等传统标识符的技术。\n\n## 为何如此重要\n\n浏览器与网站的每一次连接都会暴露多种特征，从网络数据包中 TCP 选项的精确顺序，到特定 GPU 的 canvas 渲染，再到 JavaScript 执行计时模式。单独来看，这些特征可能显得无害。但结合起来，它们会创建一个能够唯一识别设备或浏览器实例的指纹。\n\n对于自动化工程师、机器人开发者和注重隐私的用户来说，理解指纹对于构建有效的检测规避系统和在技术层面理解跟踪机制的运作至关重要。\n\n!!! danger \"多层检测系统\"\n    现代反机器人系统跨越多个层面进行综合分析：\n    \n    - **网络层面**：TCP/IP 协议栈行为、TLS 握手模式、HTTP/2 设置\n    - **浏览器层面**：Canvas 渲染、WebGL 供应商字符串、JavaScript 属性枚举\n    - **行为层面**：鼠标移动熵、按键计时、滚动模式\n    \n    一个单一的不一致（例如 Chrome User-Agent 却带有 Firefox 的 TLS 指纹）就可能触发立即阻止。\n\n## 模块范围与方法论\n\n指纹技术的文档分散在多个来源中，其可访问性和可靠性各不相同：\n\n- 学术论文（通常有付费墙且偏于理论）\n- 浏览器源代码（数百万行代码需要分析）\n- 安全研究人员的博客（技术性强但零散）\n- 反机器人供应商的白皮书（以营销为中心，省略细节）\n- 地下论坛（实用但不可靠）\n\n本模块将这些知识集中、验证并组织成一个有凝聚力的技术指南。这里描述的每一种技术都经过了：\n\n- **验证**：对照浏览器源代码和 RFCs\n- **测试**：在真实的自动化场景中\n- **引用**：附有权威参考资料\n- **解释**：从基本原理到实现\n\n## 模块结构\n\n本模块分为三个渐进的层次，从网络基础到实用的规避技术：\n\n### 1. 网络级指纹\n**[网络指纹](./network-fingerprinting.md)**\n\n涵盖在浏览器渲染开始之前，通过传输层和会话层的网络行为进行设备识别。\n\n- **TCP/IP 指纹**：TTL、窗口大小、选项顺序\n- **TLS 指纹**：JA3/JA4、密码套件、ALPN 协商\n- **HTTP/2 指纹**：SETTINGS 帧、优先级模式\n- **工具与技术**：p0f、Nmap、Scapy、tshark 分析\n\n**技术意义**：网络指纹是最难伪造的，因为它们需要操作系统级别的修改。在 JavaScript 执行开始之前，这一层的不一致就会被检测到。\n\n### 2. 浏览器级指纹\n**[浏览器指纹](./browser-fingerprinting.md)**\n\n在应用层检查通过 JavaScript API、渲染引擎和插件生态系统进行的浏览器识别。\n\n- **Canvas & WebGL 指纹**：特定 GPU 的渲染伪影\n- **音频指纹**：音频 API 输出的细微差异\n- **字体枚举**：已安装字体揭示操作系统和区域设置\n- **JavaScript 属性**：Navigator 对象、屏幕尺寸、时区\n- **标头分析**：Accept-Language、User-Agent 一致性\n\n**技术意义**：这一层占了大多数检测事件。即使网络级指纹正确，暴露的自动化属性（例如 `navigator.webdriver`）也会触发阻止。\n\n### 3. 行为指纹\n**[行为指纹](./behavioral-fingerprinting.md)**\n\n分析用户交互模式，以区分人类行为和自动化系统。\n\n- **鼠标移动分析**：轨迹曲率、速度分布、菲茨定律合规性\n- **按键动力学**：打字节奏、停留时间、飞行时间、二元组模式\n- **滚动模式**：动量、惯性、减速曲线\n- **事件序列**：自然的交互顺序 (mousemove → click)、计时分析\n- **机器学习**：在数十亿行为信号上训练的 ML 模型\n\n**技术意义**：即使网络和浏览器指纹被正确伪造，行为分析也能检测到自动化。这一层尤其具有挑战性，因为它需要复制生物力学的人类行为模式。\n\n### 4. 规避技术\n**[规避技术](./evasion-techniques.md)**\n\n使用 Pydoll 的 CDP 集成、JavaScript 覆盖和架构特性，实际实现指纹规避。\n\n- **基于 CDP 的伪造**：时区、地理位置、设备指标\n- **JavaScript 属性覆盖**：重新定义 navigator 对象、canvas 投毒\n- **请求拦截**：强制标头一致性\n- **行为模拟**：类人计时、熵注入\n- **检测测试**：用于验证您的规避设置的工具\n\n**技术意义**：本节演示了将指纹概念实际应用于真实自动化场景，整合了前面所有层次的技术。\n\n## 谁应该阅读本文\n\n### **如果您符合以下情况，您必须阅读本文：**\n- 正在构建与受反机器人保护的网站进行交互的自动化\n- 正在大规模开发抓取基础设施\n- 正在实施保护隐私的浏览器自动化\n- 正在出于攻击或防御目的研究机器人检测\n\n### **如果您符合以下情况，这是高级材料：**\n- 刚接触网络协议（从 [网络基础](../network/network-fundamentals.md) 开始）\n- 不熟悉 CDP（请先阅读 [Chrome 开发者工具协议](../fundamentals/cdp.md)）\n- 刚开始学习 Python 类型（请参阅 [类型系统](../fundamentals/typing-system.md)）\n\n### **本文不是：**\n- “银弹” 般的反检测解决方案（不存在这种东西）\n- 关于网络抓取的法律建议（请咨询 [法律与道德](../network/proxy-legal.md)）\n- 替代遵守 robots.txt 和速率限制的方案\n\n## 技术理念\n\n指纹防御 **不是要变得隐形**——而是要变得 **与合法流量无法区分**。这意味着：\n\n1.  **一致性优于完美性**：一个配置完美的 Firefox 指纹胜过一个“完美”但不一致的 Chrome 指纹\n2.  **整体方法**：您必须统一网络、浏览器和行为层面\n3.  **持续适应**：指纹技术每月都在演变；这是一份动态文档\n\n!!! tip \"黄金法则\"\n    **每一层都必须讲述同一个故事。** 如果您的 TLS 指纹显示“Chrome 120”，您的 HTTP/2 设置必须匹配 Chrome 120，您的 User-Agent 必须显示 Chrome 120，并且您的 canvas 渲染必须产生 Chrome 120 的伪影。一个不匹配 = 被检测。\n\n## 伦理考量\n\n指纹知识是 **双重用途技术**：\n\n- **防御性**：保护您的隐私免受侵入性跟踪\n- **攻击性**：规避检测系统以实现自动化\n\n我们相信您会 **负责任地、合乎道德地** 使用这些知识：\n\n**推荐实践：**\n- 尊重网站的服务条款\n- 实施速率限制和友好的爬行模式\n- 评估自动化是否必要\n- 在适当的时候保持透明\n\n**禁止用途：**\n- 欺诈、账户滥用或非法活动\n- 以侵略性的抓取压垮服务器\n- 在不了解后果的情况下将这些知识武器化\n\n## 准备好深入探索了吗？\n\n指纹是一个复杂且技术性强的领域，需要系统性学习。在有检测系统的环境中，理解这些技术对于有效的 Web 自动化至关重要。\n\n从 **[网络指纹](./network-fingerprinting.md)** 开始建立基础知识，继续学习 **[浏览器指纹](./browser-fingerprinting.md)** 以理解应用层，最后以 **[规避技术](./evasion-techniques.md)** 结束以进行实际部署。\n\n---\n\n!!! info \"文档状态\"\n    本模块代表了结合学术论文、浏览器源代码、真实世界测试和社区知识的 **广泛研究**。每一项声明都经过引用和验证。如果您发现不准确之处或有更新，欢迎贡献。\n\n## 进一步阅读\n\n在深入之前，请考虑以下补充主题：\n\n- **[代理架构](../network/http-proxies.md)**：网络级匿名基础\n- **[浏览器偏好设置](../../features/configuration/browser-preferences.md)**：实用的指纹配置\n- **[行为验证码绕过](../../features/advanced/behavioral-captcha-bypass.md)**：行为分析与规避"
  },
  {
    "path": "docs/zh/deep-dive/fingerprinting/network-fingerprinting.md",
    "content": "# Network Fingerprinting\n\nNetwork fingerprinting 通过分析 TCP/IP 协议栈、TLS 握手和 HTTP/2 连接的特征来识别客户端。这些信号由操作系统内核和 TLS 库设定，而非浏览器的 JavaScript 环境，因此比浏览器层面的指纹更难伪造。代理或 VPN 可以更改你的 IP 地址，但无法改变你的 TCP 窗口大小、TLS cipher suite 列表或 HTTP/2 SETTINGS 帧。检测系统正是利用了这一差异。\n\n!!! info \"模块导航\"\n    - [Browser Fingerprinting](./browser-fingerprinting.md)：Canvas、WebGL、AudioContext\n    - [Evasion Techniques](./evasion-techniques.md)：多层对抗措施\n\n    有关协议基础知识，请参阅 [Network Fundamentals](../network/network-fundamentals.md)。有关代理检测的背景知识，请参阅 [Proxy Detection](../network/proxy-detection.md)。\n\n## TCP/IP Fingerprinting\n\n每个操作系统对 TCP/IP 协议栈的实现方式各不相同。发起 TCP 连接的 SYN 数据包携带了足够的信息来高置信度地识别操作系统：初始 TTL、TCP 窗口大小、最大报文段长度（MSS）以及 TCP 选项的顺序和选择。这些值均不受浏览器控制，它们全部来自内核。\n\n### TTL (Time To Live)\n\n初始 TTL 是最简单的操作系统标识符之一。Linux 和 macOS 将其设为 64，Windows 设为 128，网络设备（路由器、防火墙）通常使用 255。每经过一个路由器跳点，TTL 递减 1，因此一个到达时 TTL 为 118 的数据包很可能起始于 128（Windows），经过了 10 个跳点。\n\nTTL 的 fingerprinting 价值在于将其与 User-Agent 进行交叉验证。如果浏览器声称是 Windows 上的 Chrome，但数据包到达时的 TTL 接近 64，那么该连接要么是通过 Linux 服务器代理的，要么 User-Agent 被伪造了。检测系统会将观察到的 TTL 向上取整到最近的已知初始值（64、128、255），然后与声称的操作系统进行比对。\n\n当流量经过代理时，TTL 会被重置，因为代理的内核会生成一个新的 TCP 连接到目标。目标看到的是代理的 TTL，而不是你的。这就是为什么 TTL 不匹配是代理检测信号：User-Agent 声称是 Windows（TTL 128），但 TCP 指纹显示的是 Linux（TTL 64）。\n\n### TCP 窗口大小和缩放\n\nSYN 数据包中的初始 TCP 窗口大小因操作系统和内核版本而异。现代 Linux 内核（3.x 及更高版本）通常发送 29200 字节的初始窗口，即 `20 * MSS`（标准以太网的 MSS 为 1460）。某些较新的内核（5.x、6.x）根据配置和 `initcwnd` 设置可能使用 64240。Windows 10 和 11 通常发送 65535 并启用窗口缩放，但确切值取决于自动调优配置和补丁级别。macOS 也默认为 65535。\n\n窗口缩放因子（一个 TCP 选项）将 16 位窗口大小字段进行乘法运算以支持更大的接收窗口。Linux 通常使用缩放因子 7（允许最大 8MB 的窗口），而 Windows 通常使用 8。结合基础窗口大小，缩放因子创建出比单独任何一个值都更精细的指纹。\n\n### TCP 选项顺序\n\nSYN 数据包中 TCP 选项的选择和排列顺序具有高度辨识度。每个操作系统按固定的、版本特定的顺序排列选项，且内核不将其作为可配置参数暴露。Linux 发送 `MSS, SACK_PERM, TIMESTAMP, NOP, WSCALE`。Windows 发送 `MSS, NOP, WSCALE, NOP, NOP, SACK_PERM`，并且在默认配置中明显省略了 TIMESTAMP 选项。macOS 发送 `MSS, NOP, WSCALE, NOP, NOP, TIMESTAMP, SACK_PERM`。\n\n特定选项的有无与顺序同样重要。Windows 历史上省略了 TCP 时间戳，而 Linux 和 macOS 默认包含它。所有现代系统都支持 SACK（选择性确认），但某些旧版或嵌入式系统可能不会通告它。哪些选项出现以及它们的顺序组合起来形成一个签名，p0f 等工具会将其与已知操作系统指纹数据库进行匹配。\n\n### p0f\n\n[p0f](https://lcamtuf.coredump.cx/p0f3/) 是被动 TCP/IP fingerprinting 的标准工具。它在不生成任何数据包的情况下观察流量，分析 SYN 和 SYN+ACK 数据包并与签名数据库进行比对。其签名格式编码了关键的 fingerprinting 字段：\n\n```\nversion:ittl:olen:mss:wsize,scale:olayout:quirks:pclass\n```\n\n`ittl` 是推断的初始 TTL，`mss` 是最大报文段长度，`wsize,scale` 是窗口大小（可以是绝对值，也可以是相对于 MSS 的值，如 `mss*20`），`olayout` 是使用简写名称（`mss`、`nop`、`ws`、`sok`、`sack`、`ts`、`eol+N`）表示的 TCP 选项布局。`quirks` 字段捕获异常行为，如 Don't Fragment 标志（`df`）或 DF 数据包上的非零 IP ID（`id+`）。\n\n典型的 Linux 4.x+ 签名在 p0f 中看起来像 `4:64:0:*:mss*20,7:mss,sok,ts,nop,ws:df,id+:0`。Windows 10 的签名可能看起来像 `4:128:0:*:65535,8:mss,nop,ws,nop,nop,sok:df,id+:0`。反机器人服务在内部维护类似的数据库，将传入连接与已知操作系统配置文件进行匹配，并标记与声明的 User-Agent 不一致的情况。\n\n## TLS Fingerprinting\n\nTLS ClientHello 消息在加密建立之前传输，因此对网络路径上的任何观察者都是可见的。它包含 TLS 版本、支持的 cipher suites、TLS 扩展、支持的椭圆曲线（命名组）和 EC 点格式。每个浏览器和 TLS 库都会产生这些字段的特征组合。\n\n### JA3\n\nJA3 由 Salesforce 的 John Althouse、Jeff Atkinson 和 Josh Atkins 开发，是第一个被广泛采用的 TLS fingerprinting 方法。它将 ClientHello 中的五个字段（TLS 版本、cipher suites、扩展、椭圆曲线、EC 点格式）连接起来，每个字段内的值用连字符连接，五个字段之间用逗号分隔，然后对结果字符串取 MD5 哈希。\n\n```\nJA3 string: 771,4865-4866-4867-49195-49199-49196-49200-52393-52392,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0\nJA3 hash:   cd08e31494b9531f560d64c695473da9\n```\n\n有一个细微之处：JA3 中的\"TLS 版本\"字段使用的是 `ClientHello.legacy_version`，而不是 `supported_versions` 扩展。由于 TLS 1.3（RFC 8446）要求客户端为了向后兼容将 `legacy_version` 设为 `0x0303`（TLS 1.2），因此对于现代客户端，JA3 的版本字段几乎总是 `771`，即使它们支持 TLS 1.3。实际的 TLS 1.3 协商通过扩展 43（`supported_versions`）进行，但 JA3 使用的是头部字段。\n\nJA3 在哈希之前必须过滤 GREASE 值。GREASE（RFC 8701）是一种机制，浏览器会将随机选择的保留值插入 cipher suites、扩展和其他字段中，以防止协议僵化。有效的 GREASE 值为 `0x0a0a`、`0x1a1a`、`0x2a2a`，以此类推直到 `0xfafa`。每个值都有两个相同的字节，其中每个字节的低半字节为 `0x0a`。正确的 GREASE 过滤器需要检查两个条件：\n\n```python\ndef is_grease(value: int) -> bool:\n    return (value & 0x0f0f) == 0x0a0a and (value >> 8) == (value & 0xff)\n```\n\n!!! warning \"JA3 在现代浏览器中的局限性\"\n    自 Chrome 110（2023 年 1 月）和 Firefox 114 起，浏览器会在每次连接时随机化 TLS 扩展的顺序。这意味着同一个浏览器在每次连接时会产生不同的 JA3 哈希，使得 JA3 对于识别现代浏览器实际上已经失效。JA3 对于 fingerprinting 非浏览器客户端（Python `requests`、`curl`、自定义机器人）仍然有用，因为这些客户端不实现扩展随机化。\n\n### JA4\n\nJA4 是 JA3 的继任者，由同一位主要作者（John Althouse）在 FoxIO 开发。它专门设计用于应对 TLS 扩展随机化，通过在哈希之前对扩展和 cipher suites 进行排序来实现。其格式由三个部分组成，以下划线分隔：`a_b_c`。\n\n`a` 部分是人类可读的元数据字符串：协议（`t` 代表 TCP，`q` 代表 QUIC），TLS 版本（`12` 或 `13`），是否存在 SNI（`d` 代表域名，`i` 代表 IP），cipher suites 的数量（两位数），扩展的数量（两位数），以及第一个和最后一个 ALPN 值（`h2` 代表 HTTP/2，`00` 代表无）。例如，`t13d1516h2` 表示 TCP TLS 1.3 带 SNI，15 个 cipher suites，16 个扩展，以及 HTTP/2 ALPN。\n\n`b` 部分是排序后的 cipher suites 的截断 SHA-256 哈希。`c` 部分是排序后的扩展与签名算法连接后的截断 SHA-256 哈希。因为两个列表在哈希之前都经过排序，所以扩展随机化不会影响输出。\n\nCloudflare、AWS 和其他主要平台已采用 JA4。完整的 JA4+ 套件还包括 JA4S（服务器 fingerprinting）、JA4H（HTTP 客户端 fingerprinting）、JA4X（X.509 证书 fingerprinting）和 JA4SSH（SSH fingerprinting）。规范和工具可在 [github.com/FoxIO-LLC/ja4](https://github.com/FoxIO-LLC/ja4) 获取。\n\n### JA3S（服务器 Fingerprinting）\n\nJA3S 将相同的概念应用于 ServerHello 消息，但格式更简单，因为服务器选择的是单个 cipher suite 而非提供一个列表。JA3S 字符串为 `version,cipher,extensions`，其 MD5 哈希标识了服务器的 TLS 实现。将 JA3（或 JA4）与 JA3S 配对可以创建双向指纹：特定客户端与特定服务器通信会产生可预测的 JA3+JA3S 对，这比单独任何一个指纹都更具辨识度。\n\n### 代理如何影响 TLS 指纹\n\n代理的类型决定了 TLS 指纹是否被保留。SOCKS5 代理和 HTTP CONNECT 隧道在不终止 TLS 的情况下中继 TCP 流，因此目标服务器看到的是原始客户端的 TLS 指纹，不会发生任何变化。这是这些代理类型在保持指纹一致性方面的主要优势。\n\nMITM 代理（终止 TLS 并与目标重新建立新连接）会用自身的 TLS 指纹替换客户端的指纹。目标看到的是代理软件的 cipher suites 和扩展，而不是浏览器的。如果代理使用标准 TLS 库（如 OpenSSL 或 BoringSSL）的默认设置，指纹将不匹配任何已知浏览器，这本身就是一个检测信号。\n\n这就是为什么 Pydoll 使用 `--proxy-server`（创建 CONNECT 隧道，保留浏览器的 TLS 指纹）的方式优于外部 MITM 代理设置来进行隐蔽自动化。\n\n## HTTP/2 Fingerprinting\n\nHTTP/2 连接暴露了一组与 TLS 不同的独立 fingerprinting 信号。客户端发送的第一个帧是 SETTINGS 帧，包含 `HEADER_TABLE_SIZE`、`ENABLE_PUSH`、`MAX_CONCURRENT_STREAMS`、`INITIAL_WINDOW_SIZE`、`MAX_FRAME_SIZE` 和 `MAX_HEADER_LIST_SIZE` 等参数。每个浏览器使用不同的默认值，并包含这些参数的不同子集。\n\n除了 SETTINGS 之外，WINDOW_UPDATE 帧大小、初始流的优先级/权重以及 HTTP/2 伪头部（`:method`、`:authority`、`:scheme`、`:path`）的顺序在不同实现之间也各不相同。Chrome、Firefox 和 Safari 各自产生独特的这些值的组合。\n\nAkamai 在 2017 年欧洲 Black Hat 大会上发表了关于 HTTP/2 fingerprinting 的基础性研究。他们的指纹格式连接了 SETTINGS 值、WINDOW_UPDATE 大小、PRIORITY 帧和伪头部顺序。JA4+ 套件中的 `JA4H` 用于 HTTP 层 fingerprinting，涵盖了头部顺序和值。\n\nHTTP/2 fingerprinting 对自动化工具特别有效，因为许多机器人框架和 HTTP 库实现了自己的 HTTP/2 协议栈，其默认参数与任何真实浏览器都不匹配。即使工具正确伪造了 TLS 指纹（使用 curl-impersonate 或类似工具），其 HTTP/2 SETTINGS 帧也可能暴露它。\n\n你可以在 [browserleaks.com/http2](https://browserleaks.com/http2) 检查你的 HTTP/2 指纹。因为 Pydoll 通过 CDP 控制真实的 Chrome 实例，HTTP/2 指纹始终是真实的——这是相对于以编程方式构建 HTTP 请求的工具的固有优势。\n\n## 对浏览器自动化的影响\n\n对于使用 Pydoll 进行自动化的实际要点是：network fingerprinting 是控制真实浏览器能提供显著优势的领域。Chrome 的 TCP/IP 协议栈、TLS 实现（BoringSSL）和 HTTP/2 协议栈默认产生真实的指纹。主要风险在于环境不匹配：在 Linux 服务器上运行 Chrome 而 User-Agent 声称是 Windows，会导致 TCP/IP 指纹不一致（TTL 为 64 而非 128，Linux TCP 选项顺序而非 Windows 的）。\n\n对于基于代理的设置，指纹流程是：你的机器的 TCP/IP 协议栈生成到代理的连接（代理的运营商可以看到但目标无法看到），代理的 TCP/IP 协议栈生成到目标的连接。目标看到的是代理服务器的 TTL 和 TCP 选项。如果代理运行 Linux（大多数都是），无论 User-Agent 如何，TCP 指纹都将显示 Linux。这是一个众所周知的检测信号，住宅代理可以部分缓解（代理端点是真实用户的机器，因此其 TCP 指纹是合理的），但数据中心代理无法做到。\n\n另一方面，TLS 和 HTTP/2 指纹通过 SOCKS5 和 CONNECT 隧道不做修改地传递。这些是浏览器的指纹，不是代理的。因此，通过 CONNECT 隧道使用 Pydoll 时，目标看到的是真实的 Chrome TLS 和 HTTP/2 指纹，配合代理的 TCP/IP 指纹。这种组合与真实用户通过 VPN 或企业代理浏览是一致的，这是一种常见且合理的模式。\n\n## 参考资料\n\n- Salesforce Engineering: TLS Fingerprinting with JA3 and JA3S - https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/\n- FoxIO JA4+ Network Fingerprinting - https://github.com/FoxIO-LLC/ja4\n- Cloudflare: JA4 Signals - https://blog.cloudflare.com/ja4-signals/\n- Akamai: Passive Fingerprinting of HTTP/2 Clients (Black Hat EU 2017) - https://blackhat.com/docs/eu-17/materials/eu-17-Shuster-Passive-Fingerprinting-Of-HTTP2-Clients-wp.pdf\n- p0f v3: Passive OS Fingerprinting - https://lcamtuf.coredump.cx/p0f3/\n- RFC 8446: TLS 1.3 - https://datatracker.ietf.org/doc/html/rfc8446\n- RFC 8701: GREASE for TLS - https://datatracker.ietf.org/doc/html/rfc8701\n- RFC 6528: Defending against Sequence Number Attacks - https://datatracker.ietf.org/doc/html/rfc6528\n- BrowserLeaks HTTP/2 Fingerprint - https://browserleaks.com/http2\n- Stamus Networks: JA3 Fingerprints Fade as Browsers Embrace Extension Randomization - https://www.stamus-networks.com/blog/ja3-fingerprints-fade-browsers-embrace-tls-extension-randomization\n"
  },
  {
    "path": "docs/zh/deep-dive/fundamentals/cdp.md",
    "content": "# Chrome 开发者工具协议 (CDP)\n\nChrome 开发者工具协议 (CDP) 是 Pydoll 能够在没有传统 webdriver 的情况下控制浏览器的基础。理解 CDP 的工作原理有助于深入了解 Pydoll 的功能和内部架构。\n\n\n## 什么是 CDP？\n\nChrome 开发者工具协议是 Chromium 团队开发的一个强大接口，允许通过编程方式与基于 Chromium 的浏览器进行交互。它与您检查网页时 Chrome 开发者工具所使用的协议相同，但它作为可编程 API 暴露出来，可供自动化工具利用。\n\nCDP 的核心是提供了一套全面的方法和事件，用于与浏览器内部进行交互。这使得我们可以精细控制浏览器的各个方面，从页面导航到操作 DOM、拦截网络请求和监控性能指标。\n\n!!! info \"CDP 的演进\"\n    自推出以来，Chrome 开发者工具协议一直在不断发展。Google 随着每个 Chrome 版本的发布都会维护和更新该协议，定期添加新功能并改进现有特性。\n    \n    虽然该协议最初是为 Chrome 的开发者工具设计的，但其全面的功能已使其成为下一代浏览器自动化工具（如 Puppeteer、Playwright，当然还有 Pydoll）的基础。\n\n## WebSocket 通信\n\nCDP 在架构上的一个关键决策是使用 WebSocket 进行通信。当基于 Chromium 的浏览器以启用远程调试标志启动时，它会在指定端口上打开一个 WebSocket 服务器：\n\n```\nchrome --remote-debugging-port=9222\n```\n\nPydoll 连接到此 WebSocket 端点，以与浏览器建立双向通信通道。该连接：\n\n1.  在整个自动化会话期间 **保持持久性**\n2.  使浏览器能够将 **实时事件** 推送给客户端\n3.  允许向浏览器 **发送命令**\n4.  **支持二进制数据**，以高效传输屏幕截图、PDF 和其他资产\n\nWebSocket 协议特别适用于浏览器自动化，因为它提供：\n\n- **低延迟通信** - 响应式自动化所必需\n- **双向消息传递** - 事件驱动架构的基础\n- **持久连接** - 消除了每个操作的连接设置开销\n\n以下是 Pydoll 与浏览器通信方式的简化视图：\n\n```mermaid\nsequenceDiagram\n    participant App as Pydoll 应用程序\n    participant WS as WebSocket 连接\n    participant Browser as Chrome 浏览器\n\n    App ->> WS: 命令：导航到 URL\n    WS ->> Browser: 执行导航\n\n    Browser -->> WS: 发送页面加载事件\n    WS -->> App: 接收页面加载事件\n```\n\n!!! info \"WebSocket vs HTTP\"\n    早期的浏览器自动化协议通常依赖 HTTP 端点进行通信。CDP 转向 WebSocket 代表了一项重大的架构改进，可实现响应更灵敏的自动化和实时事件监控。\n    \n    基于 HTTP 的协议需要持续轮询以检测变化，这会产生开销和延迟。WebSocket 允许浏览器在事件发生时立即将通知推送给您的自动化脚本，延迟极小。\n\n## 关键 CDP 域\n\nCDP 被组织成逻辑域，每个域负责浏览器功能的特定方面。一些最重要的域包括：\n\n\n| 域 (Domain) | 职责 | 示例用例 |\n|---|---|---|\n| **Browser** | 控制浏览器应用程序本身 | 窗口管理、浏览器上下文创建 |\n| **Page** | 与页面生命周期交互 | 导航、JavaScript 执行、框架管理 |\n| **DOM** | 访问页面结构 | 查询选择器、属性修改、事件监听器 |\n| **Network** | 网络流量监控和控制 | 请求拦截、响应检查、缓存 |\n| **Runtime** | JavaScript 执行环境 | 评估表达式、调用函数、处理异常 |\n| **Input** | 模拟用户交互 | 鼠标移动、键盘输入、触摸事件 |\n| **Target** | 管理浏览器上下文和目标 | 创建标签页、访问 iframe、处理弹出窗口 |\n| **Fetch** | 底层网络拦截 | 修改请求、模拟响应、身份验证 |\n\nPydoll 将这些 CDP 域映射到更直观的 API 结构中，同时保留了底层协议的全部功能。\n\n## 事件驱动架构\n\nCDP 最强大的功能之一是其事件系统。该协议允许客户端订阅浏览器在正常操作期间发出的各种事件。这些事件几乎涵盖了浏览器行为的各个方面：\n\n- **生命周期事件**：页面加载、框架导航、目标创建\n- **DOM 事件**：元素变化、属性修改\n- **网络事件**：请求/响应周期、WebSocket 消息\n- **执行事件**：JavaScript 异常、控制台消息\n- **性能事件**：渲染、脚本和更多指标\n\n\n当您在 Pydoll 中启用事件监控时（例如，使用 `page.enable_network_events()`），库会与浏览器设置必要的订阅，并为您的代码提供钩子以对这些事件做出反应。\n\n```python\nfrom pydoll.events.network import NetworkEvents\nfrom functools import partial\n\nasync def on_request(page, event):\n    url = event['params']['request']['url']\n    print(f\"Request to: {url}\")\n\n# 订阅网络请求事件\nawait page.enable_network_events()\nawait page.on(NetworkEvents.REQUEST_WILL_BE_SENT, partial(on_request, page))\n```\n\n这种事件驱动的方法允许自动化脚本立即对浏览器状态变化做出反应，而无需依赖低效的轮询或任意延迟。\n\n## 直接 CDP 集成的性能优势\n\n像 Pydoll 那样直接使用 CDP，与传统的基于 webdriver 的自动化相比，具有多种性能优势：\n\n### 1. 消除协议转换层\n\n传统的基于 webdriver 的工具（如 Selenium）使用多层方法：\n\n```mermaid\ngraph LR\n    AS[自动化脚本] --> WC[WebDriver 客户端]\n    WC --> WS[WebDriver 服务器]\n    WS --> B[浏览器]\n```\n\n每一层都会增加开销，尤其是 WebDriver 服务器，它充当 WebDriver 协议和浏览器本机 API 之间的转换层。\n\nPydoll 的方法将其简化为：\n\n```mermaid\ngraph LR\n    AS[自动化脚本] --> P[Pydoll]\n    P --> B[通过 CDP 连接浏览器]\n```\n\n这种直接通信消除了中间服务器的计算和网络开销，从而加快了操作速度。\n\n### 2. 高效的命令批处理\n\nCDP 允许在单个消息中批处理多个命令，减少了复杂操作所需的往返次数。这对于需要多个步骤的操作（例如查找元素然后与其交互）特别有价值。\n\n### 3. 异步操作\n\nCDP 基于 WebSocket、事件驱动的架构与 Python 的 asyncio 框架完美契合，可实现真正的异步操作。这使得 Pydoll 能够：\n\n- 并发执行多个操作\n- 在事件发生时处理它们\n- 在 I/O 操作期间避免阻塞主线程\n\n```mermaid\ngraph TD\n    subgraph \"Pydoll 异步架构\"\n        EL[事件循环]\n        \n        subgraph \"并发任务\"\n            T1[任务 1: 导航]\n            T2[任务 2: 等待元素]\n            T3[任务 3: 处理网络事件]\n        end\n        \n        EL --> T1\n        EL --> T2\n        EL --> T3\n        \n        T1 --> WS[WebSocket 连接]\n        T2 --> WS\n        T3 --> WS\n        \n        WS --> B[浏览器]\n    end\n```\n\n!!! info \"异步性能提升\"\n    asyncio 和 CDP 的结合对性能产生了倍增效应。在基准测试中，Pydoll 的异步方法可以以接近线性的扩展性并行处理多个页面，而传统的同步工具在并发性增加时收益递减。\n    \n    例如，使用同步工具抓取 10 个各需 2 秒加载的页面可能需要超过 20 秒，但使用 Pydoll 的异步架构（加上一些最小的开销）仅需 2 秒多一点。\n\n### 4. 精细的控制\n\nCDP 提供了比 WebDriver 协议更精细的浏览器行为控制。这使得 Pydoll 能够为常见操作实施优化策略：\n\n- 更精确的等待条件（而非任意超时）\n- 直接访问浏览器缓存和存储\n- 在特定上下文中定向执行 JavaScript\n- 详细的网络控制以优化请求\n\n\n## 结论\n\nChrome 开发者工具协议构成了 Pydoll 零 webdriver 浏览器自动化方法的基础。通过利用 CDP 的 WebSocket 通信、全面的域覆盖、事件驱动架构和直接的浏览器集成，Pydoll 实现了优于传统自动化工具的性能和可靠性。\n\n在接下来的部分中，我们将更深入地探讨 Pydoll 如何实现特定的 CDP 域，并将低级协议转换为直观、对开发人员友好的 API。\n"
  },
  {
    "path": "docs/zh/deep-dive/fundamentals/connection-layer.md",
    "content": "# Connection Handler (连接处理器)\n\nConnection Handler 是 Pydoll 架构的基础层，充当 Python 代码与浏览器 Chrome DevTools Protocol (CDP) 之间的桥梁。该组件管理与浏览器的 WebSocket 连接，处理命令执行，并以非阻塞、异步的方式处理事件。\n\n```mermaid\ngraph TD\n    A[Python 代码] --> B[Connection Handler]\n    B <--> C[WebSocket]\n    C <--> D[浏览器 CDP 端点]\n\n    subgraph \"Connection Handler\"\n        E[命令管理器]\n        F[事件处理器]\n        G[WebSocket 客户端]\n    end\n\n    B --> E\n    B --> F\n    B --> G\n```\n\n## 异步编程模型\n\nPydoll 构建于 Python 的 `asyncio` 框架之上，该框架支持非阻塞 I/O 操作。这种设计选择对于高性能的浏览器自动化至关重要，因为它允许多个操作并发执行，而无需等待每个操作完成。\n\n### 理解 Async/Await\n\n\n为了理解 async/await 在实践中如何工作，让我们看一个包含两个并发操作的更详细的示例：\n\n```python\nimport asyncio\nfrom pydoll.browser.chrome import Chrome\n\nasync def fetch_page_data(url):\n    print(f\"开始抓取 {url}\")\n    browser = Chrome()\n    await browser.start()\n    page = await browser.get_page()\n    \n    # 导航需要时间 - 这是我们让出控制权的地方\n    await page.go_to(url)\n    \n    # 获取页面标题\n    title = await page.execute_script(\"return document.title\")\n    \n    # 提取一些数据\n    description = await page.execute_script(\n        \"return document.querySelector('meta[name=\\\"description\\\"]')?.content || ''\"\n    )\n    \n    await browser.stop()\n    print(f\"完成抓取 {url}\")\n    return {\"url\": url, \"title\": title, \"description\": description}\n\nasync def main():\n    # 并发启动两个页面操作\n    task1 = asyncio.create_task(fetch_page_data(\"https://example.com\"))\n    task2 = asyncio.create_task(fetch_page_data(\"https://github.com\"))\n    \n    # 等待两者完成并获取结果\n    result1 = await task1\n    result2 = await task2\n    \n    return [result1, result2]\n\n# 运行异步函数\nresults = asyncio.run(main())\n```\n\n此示例演示了我们如何并发地从两个不同的网站获取数据，与顺序执行相比，这可能将总执行时间缩短近一半。\n\n#### 异步执行流程图\n\n以下是执行上述代码时事件循环中发生的情况：\n\n```mermaid\nsequenceDiagram\n    participant A as 主代码\n    participant B as 任务 1<br/> (example.com)\n    participant C as 任务 2<br/> (github.com)\n    participant D as 事件循环\n    \n    A->>B: 创建任务 1\n    B->>D: 在循环中注册\n    A->>C: 创建任务 2\n    C->>D: 在循环中注册\n    D->>B: 执行直到 browser.start()\n    D->>C: 执行直到 browser.start()\n    D-->>B: WebSocket 连接后恢复\n    D-->>C: WebSocket 连接后恢复\n    D->>B: 执行直到 page.go_to()\n    D->>C: 执行直到 page.go_to()\n    D-->>B: 页面加载后恢复\n    D-->>C: 页面加载后恢复\n    B-->>A: 返回结果\n    C-->>A: 返回结果\n```\n\n此序列图说明了 Python 的 asyncio 如何管理我们示例代码中的两个并发任务：\n\n1.  主函数创建两个任务，用于从不同网站获取数据\n2.  两个任务都在事件循环中注册\n3.  事件循环执行每个任务，直到遇到 `await` 语句（如 `browser.start()`）\n4.  当异步操作完成时（如 WebSocket 连接建立），任务恢复执行\n5.  循环在每个 `await` 点继续在任务之间切换\n6.  当每个任务完成时，它将其结果返回给主函数\n\n在 `fetch_page_data` 示例中，这允许两个浏览器实例并发工作 - 当一个实例等待页面加载时，另一个实例可以取得进展。这比顺序处理每个网站要高效得多，因为 I/O 等待时间不会阻塞其他任务的执行。\n\n!!! info \"协作式多任务\"\n    Asyncio 使用协作式多任务，其中任务在 `await` 点自愿让出控制权。这不同于抢占式多任务（线程），后者中任务可能在任何时候被中断。协作式多任务可以为 I/O 密集型操作提供更好的性能，但需要仔细编码以避免阻塞事件循环。\n\n## Connection Handler 实现\n\n`ConnectionHandler` 类旨在管理命令执行和事件处理，为 CDP WebSocket 连接提供了一个健壮的接口。\n\n### 类初始化\n\n```python\ndef __init__(\n    self,\n    connection_port: int,\n    page_id: str = 'browser',\n    ws_address_resolver: Callable[[int], str] = get_browser_ws_address,\n    ws_connector: Callable = websockets.connect,\n):\n    # 初始化组件...\n```\n\nConnectionHandler 接受几个参数：\n\n| 参数 | 类型 | 描述 |\n|---|---|---|\n| `connection_port` | `int` | 浏览器 CDP 端点正在监听的端口号 |\n| `page_id` | `str` | 特定页面/目标的标识符（用于浏览器级别的连接时使用 'browser'） |\n| `ws_address_resolver` | `Callable` | 从端口号解析 WebSocket URL 的函数 |\n| `ws_connector` | `Callable` | 建立 WebSocket 连接的函数 |\n\n### 内部组件\n\nConnectionHandler 协调三个主要组件：\n\n1.  **WebSocket 连接**：管理与浏览器的实际 WebSocket 通信\n2.  **命令管理器**：处理发送命令和接收响应\n3.  **事件处理器**：处理来自浏览器的事件并触发适当的回调\n\n```mermaid\nclassDiagram\n    class ConnectionHandler {\n        -_connection_port: int\n        -_page_id: str\n        -_ws_connection\n        -_command_manager: CommandManager\n        -_events_handler: EventsHandler\n        +execute_command(command, timeout) async\n        +register_callback(event_name, callback) async\n        +remove_callback(callback_id) async\n        +ping() async\n        +close() async\n        -_receive_events() async\n    }\n\n    class CommandManager {\n        -_pending_commands: dict\n        +create_command_future(command)\n        +resolve_command(id, response)\n        +remove_pending_command(id)\n    }\n\n    class EventsHandler {\n        -_callbacks: dict\n        -_network_logs: list\n        -_dialog: dict\n        +register_callback(event_name, callback, temporary)\n        +remove_callback(callback_id)\n        +clear_callbacks()\n        +process_event(event) async\n    }\n\n    ConnectionHandler *-- CommandManager\n    ConnectionHandler *-- EventsHandler\n```\n\n## 命令执行流程\n\n通过 CDP 执行命令时，ConnectionHandler 遵循特定模式：\n\n1.  确保存在活动的 WebSocket 连接\n2.  创建一个 Future 对象来表示挂起的响应\n3.  通过 WebSocket 发送命令\n4.  等待 Future 被响应解析\n5.  将响应返回给调用者\n\n```python\nasync def execute_command(self, command: dict, timeout: int = 10) -> dict:\n    # 验证命令\n    if not isinstance(command, dict):\n        logger.error('Command must be a dictionary.')\n        raise exceptions.InvalidCommand('Command must be a dictionary')\n\n    # 确保连接处于活动状态\n    await self._ensure_active_connection()\n    \n    # 为此命令创建 future\n    future = self._command_manager.create_command_future(command)\n    command_str = json.dumps(command)\n\n    # 发送命令并等待响应\n    try:\n        await self._ws_connection.send(command_str)\n        response: str = await asyncio.wait_for(future, timeout)\n        return json.loads(response)\n    except asyncio.TimeoutError as exc:\n        self._command_manager.remove_pending_command(command['id'])\n        raise exc\n    except websockets.ConnectionClosed as exc:\n        await self._handle_connection_loss()\n        raise exc\n```\n\n!!! warning \"命令超时\"\n    未在指定超时期限内收到响应的命令将引发 `TimeoutError`。这可以防止自动化脚本因缺少响应而无限期挂起。默认超时为 10 秒，但可以根据复杂操作的预期响应时间进行调整。\n\n## 事件处理系统\n\n事件系统是启用 Pydoll 中反应式编程模式的关键架构组件。它允许您为特定浏览器事件注册回调，并在这些事件发生时自动执行它们。\n\n### 事件流\n\n事件处理流程遵循以下步骤：\n\n1.  `_receive_events` 方法作为后台任务运行，持续从 WebSocket 接收消息\n2.  每条消息被解析并分类为命令响应或事件\n3.  事件被传递给 EventsHandler 进行处理\n4.  EventsHandler 识别该事件已注册的回调并调用它们\n\n```mermaid\nflowchart TD\n    A[WebSocket 消息] --> B{是命令响应吗？}\n    B -->|是| C[解析命令 Future]\n    B -->|否| D[作为事件处理]\n    D --> E[查找匹配的回调]\n    E --> F[执行回调]\n    F --> G{是临时的吗？}\n    G -->|是| H[移除回调]\n    G -->|否| I[保留回调]\n```\n\n### 回调注册\n\nConnectionHandler 提供了注册、移除和管理事件回调的方法：\n\n```python\n# 为特定事件注册回调\ncallback_id = await connection.register_callback(\n    'Page.loadEventFired', \n    handle_page_load\n)\n\n# 移除特定回调\nawait connection.remove_callback(callback_id)\n\n# 移除所有回调\nawait connection.clear_callbacks()\n```\n\n!!! tip \"临时回调\"\n    您可以将回调注册为临时的，这意味着它在触发一次后将自动移除。这对于一次性事件（如处理对话框）很有用：\n    \n    ```python\n    await connection.register_callback(\n        'Page.javascriptDialogOpening',\n        handle_dialog,\n        temporary=True\n    )\n    ```\n\n### 异步回调执行\n\n回调可以是同步函数或异步协程。EventsHandler（由 ConnectionHandler 管理）可以正确处理这两种类型：\n\n```python\n# 同步回调\ndef synchronous_callback(event):\n    print(f\"Event received: {event['method']}\")\n\n# 异步回调\nasync def asynchronous_callback(event):\n    await asyncio.sleep(0.1)  # 执行一些异步操作\n    print(f\"Event processed asynchronously: {event['method']}\")\n\n# 两者都可以用相同的方式注册\nawait connection.register_callback('Network.requestWillBeSent', synchronous_callback)\nawait connection.register_callback('Network.responseReceived', asynchronous_callback)\n```\n\n**顺序执行模型：**\n\n异步回调由 EventsManager **顺序等待 (await)**。这确保了对于单个事件，回调按照它们注册的顺序执行，防止了多个回调修改共享状态时出现竞争条件。\n\n```python\n# 在 EventsManager.process_event() 内部\nfor callback_data in callbacks:\n    if asyncio.iscoroutinefunction(callback_data['callback']):\n        await callback_data['callback'](event_data)  # 顺序 await\n    else:\n        callback_data['callback'](event_data)  # 同步执行\n```\n\n**非阻塞执行**（用于不应阻塞其他操作的 UI 回调）是在 **更高层** 实现的，例如在 `Tab.on()` 方法中，它在注册用户回调之前将其包装在 `asyncio.create_task()` 中。这种架构提供了：\n\n- **底层** (ConnectionHandler/EventsManager)：保证顺序执行和可预测的顺序\n- **高层** (Tab.on())：在需要时提供非阻塞语义\n\n!!! info \"事件架构详情\"\n    有关多层事件系统和顺序回调执行原理的完整详细信息，请参阅 [事件架构深入探讨](../architecture/event-architecture.md)。\n\n## 连接管理\n\nConnectionHandler 实现了多种策略以确保连接的健壮性：\n\n### 延迟连接建立\n\n仅在需要时才建立连接，通常是在执行第一个命令时或明确请求时。这种延迟初始化方法可以节省资源，并允许更灵活的连接管理。\n\n### 自动重新连接\n\n如果 WebSocket 连接意外丢失或关闭，ConnectionHandler 将在执行下一个命令时尝试自动重新建立连接。这提供了对瞬态网络问题的弹性。\n\n```python\nasync def _ensure_active_connection(self):\n    \"\"\"\n    保证在继续之前存在活动连接。\n    \"\"\"\n    if self._ws_connection is None or self._ws_connection.closed:\n        await self._establish_new_connection()\n```\n\n### 资源清理\n\nConnectionHandler 实现了显式清理方法和 Python 的异步上下文管理器协议（`__aenter__` 和 `__aexit__`），确保在不再需要资源时正确释放它们：\n\n```python\nasync def close(self):\n    \"\"\"\n    关闭 WebSocket 连接并清除所有回调。\n    \"\"\"\n    await self.clear_callbacks()\n    if self._ws_connection is not None:\n        try:\n            await self._ws_connection.close()\n        except websockets.ConnectionClosed as e:\n            logger.info(f'WebSocket connection has closed: {e}')\n        logger.info('WebSocket connection closed.')\n```\n\n!!! info \"上下文管理器用法\"\n    将 ConnectionHandler 用作上下文管理器是确保正确清理资源的的推荐模式：\n    \n    ```python\n    async with ConnectionHandler(9222, 'browser') as connection:\n        # 使用连接...\n        await connection.execute_command(...)\n    # 退出上下文时自动关闭连接\n    ```\n\n## 消息处理管道\n\nConnectionHandler 实现了一个复杂的消息处理管道，用于处理来自 WebSocket 连接的连续消息流：\n\n```mermaid\nsequenceDiagram\n    participant WS as WebSocket\n    participant RCV as _receive_events\n    participant MSG as _process_single_message\n    participant PARSE as _parse_message\n    participant CMD as _handle_command_message\n    participant EVT as _handle_event_message\n    \n    loop 当连接时\n        WS->>RCV: 消息\n        RCV->>MSG: 原始消息\n        MSG->>PARSE: 原始消息\n        PARSE-->>MSG: 解析后的 JSON 或 None\n        \n        alt 是命令响应\n            MSG->>CMD: 消息\n            CMD->>CMD: 解析命令 future\n        else 是事件通知\n            MSG->>EVT: 消息\n            EVT->>EVT: 处理事件并触发回调\n        end\n    end\n```\n\n该管道确保了命令响应和异步事件的高效处理，使 Pydoll 即使在大量消息的情况下也能保持响应灵敏的操作。\n\n## 高级用法\n\nConnectionHandler 通常通过 Browser 和 Page 类间接使用，但也可以直接用于高级场景：\n\n### 直接事件监控\n\n对于特殊用例，您可能希望绕过更高级别的 API，直接监控特定的 CDP 事件：\n\n```python\nfrom pydoll.connection.connection import ConnectionHandler\n\nasync def monitor_network():\n    connection = ConnectionHandler(9222)\n    \n    async def log_request(event):\n        url = event['params']['request']['url']\n        print(f\"Request: {url}\")\n    \n    await connection.register_callback(\n        'Network.requestWillBeSent', \n        log_request\n    )\n    \n    # 通过 CDP 命令启用网络事件\n    await connection.execute_command({\n        \"id\": 1,\n        \"method\": \"Network.enable\"\n    })\n    \n    # 持续运行直到被中断\n    try:\n        while True:\n            await asyncio.sleep(1)\n    finally:\n        await connection.close()\n```\n\n### 自定义命令执行\n\n您可以直接执行任意 CDP 命令：\n\n```python\nasync def custom_cdp_command(connection, method, params=None):\n    command = {\n        \"id\": random.randint(1, 10000),\n        \"method\": method,\n        \"params\": params or {}\n    }\n    return await connection.execute_command(command)\n\n# 示例：不使用 Page 类获取文档 HTML\nasync def get_html(connection):\n    result = await custom_cdp_command(\n        connection,\n        \"Runtime.evaluate\",\n        {\"expression\": \"document.documentElement.outerHTML\"}\n    )\n    return result['result']['result']['value']\n```\n\n!!! warning \"高级接口\"\n    直接使用 ConnectionHandler 需要深入了解 Chrome DevTools 协议。对于大多数用例，更高级别的 Browser 和 Page API 提供了更直观、更安全的接口。\n\n\n## 高级并发模式\n\nConnectionHandler 的异步设计支持复杂的并发模式：\n\n### 并行命令执行\n\n并发执行多个命令并等待所有结果：\n\n```python\nasync def get_page_metrics(connection):\n    commands = [\n        {\"id\": 1, \"method\": \"Performance.getMetrics\"},\n        {\"id\": 2, \"method\": \"Network.getResponseBody\", \"params\": {\"requestId\": \"...\"}},\n        {\"id\": 3, \"method\": \"DOM.getDocument\"}\n    ]\n    \n    results = await asyncio.gather(\n        *(connection.execute_command(cmd) for cmd in commands)\n    )\n    \n    return results\n```\n\n## 结论\n\nConnectionHandler 是 Pydoll 架构的基础，为 Chrome DevTools 协议提供了健壮、高效的接口。通过利用 Python 的 asyncio 框架和 WebSocket 通信，它支持高性能的浏览器自动化，并具有优雅的、事件驱动的编程模式。\n\n理解 ConnectionHandler 的设计和操作，有助于深入了解 Pydoll 的内部工作原理，并为在特殊场景下进行高级定制和优化提供了机会。\n\n对于大多数用例，您将通过更高级别的 Browser 和 Page API 间接与 ConnectionHandler 交互，这些 API 提供了更直观的接口，同时利用了 ConnectionHandler 的强大功能。"
  },
  {
    "path": "docs/zh/deep-dive/fundamentals/iframes-and-contexts.md",
    "content": "# Iframes、OOPIF 和执行上下文（深度解析）\n\n理解浏览器自动化如何处理 iframe 对于构建健壮的自动化工具至关重要。本综合指南探讨了 Pydoll 中 iframe 处理的技术基础，涵盖了文档对象模型 (DOM)、Chrome DevTools 协议 (CDP) 机制、执行上下文、隔离世界 (isolated worlds) 以及使 iframe 交互变得无缝的复杂解析管道。\n\n!!! info \"实用用法优先\"\n    如果您只需要在自动化脚本中使用 iframe，请从功能指南开始：**功能 → 自动化 → IFrames**。\n    本深度解析解释了架构决策、协议的细微差别以及内部实现细节。\n\n---\n\n## 目录\n\n1. [基础：文档对象模型 (DOM)](#基础文档对象模型-dom)\n2. [什么是 Iframes 及其重要性](#什么是-iframes-及其重要性)\n3. [挑战：跨进程 Iframes (OOPIFs)](#挑战跨进程-iframes-oopifs)\n4. [Chrome DevTools 协议和 Frame 管理](#chrome-devtools-协议和-frame-管理)\n5. [执行上下文和隔离世界](#执行上下文和隔离世界)\n6. [CDP 标识符参考](#cdp-标识符参考)\n7. [Pydoll 的解析管道](#pydoll-的解析管道)\n8. [会话路由和扁平化模式](#会话路由和扁平化模式)\n9. [实现深度解析](#实现深度解析)\n10. [性能考量](#性能考量)\n11. [失败模式和调试](#失败模式和调试)\n\n---\n\n## 基础：文档对象模型 (DOM)\n\n在深入研究 iframe 之前，我们必须了解 DOM——在内存中表示 HTML 文档的树形结构。\n\n### 什么是 DOM？\n\n**文档对象模型** (Document Object Model) 是 HTML 和 XML 文档的编程接口。它将页面结构表示为一个节点树，其中每个节点对应文档的一部分：\n\n- **元素节点**：HTML 标签，如 `<div>`、`<iframe>`、`<button>`\n- **文本节点**：实际的文本内容\n- **属性节点**：元素属性，如 `id`、`class`、`src`\n- **文档节点**：树的根节点\n\n```mermaid\ngraph TD\n    Document[文档] --> HTML[html 元素]\n    HTML --> Head[head 元素]\n    HTML --> Body[body 元素]\n    Body --> Div1[div 元素]\n    Body --> Div2[div 元素]\n    Div1 --> Text1[文本节点: 'Hello']\n    Div2 --> Iframe[iframe 元素]\n    Iframe --> IframeDoc[iframe 的文档]\n    IframeDoc --> IframeBody[iframe body]\n    IframeBody --> IframeContent[iframe 内容...]\n```\n\n### DOM 树属性\n\n1. **层级结构**：每个节点都有一个父节点（Document 除外），并且可以有子节点\n2. **节点标识**：节点可以通过以下方式标识：\n   - `nodeId`：文档上下文（DOM 域）内的内部标识符\n   - `backendNodeId`：可以跨不同文档引用节点的稳定标识符\n3. **实时表示**：对 DOM 的更改会立即反映在树中\n\n### 为什么这对 Iframes 很重要\n\n每个 `<iframe>` 元素都会创建一个**新的、独立的 DOM 树**。 iframe 元素本身存在于父级的 DOM 中，但加载到 iframe 中的内容拥有自己完整的 Document 节点和树结构。这种分离是所有 iframe 复杂性的基础。\n\n---\n\n## 什么是 Iframes 及其重要性\n\n### 定义\n\n**iframe**（内联框架）是一个 HTML 元素（`<iframe>`），它在当前页面中嵌入另一个 HTML 文档。被嵌入的文档保持其自己的上下文，包括：\n\n- 独立的 HTML 结构和 DOM 树\n- 独立的 JavaScript 执行环境\n- 自己的 CSS 样式（除非明确共享）\n- 不同的导航历史\n\n```html\n<body>\n  <h1>父页面</h1>\n  <iframe src=\"https://example.com/embedded.html\" id=\"content-frame\"></iframe>\n  <p>更多父页面内容</p>\n</body>\n```\n\n### 常见用例\n\n| 用例 | 描述 | 示例 |\n|----------|-------------|---------|\n| **第三方小部件** | 安全地嵌入外部内容 | 支付表单、社交媒体 feeds、聊天窗口 |\n| **内容隔离** | 沙盒化不受信任的内容 | 用户生成的 HTML、广告 |\n| **模块化架构** | 可重用组件 | 仪表盘小部件、插件系统 |\n| **跨域内容** | 从不同域加载资源 | 地图、视频播放器、分析仪表盘 |\n\n### 安全模型：同源策略 (Same-Origin Policy)\n\n浏览器对 iframes 强制执行**同源策略**：\n\n- **同源 iframes**：父级可以通过 JavaScript 访问 iframe 的 DOM (`iframe.contentDocument`)\n- **跨域 iframes**：父级不能直接访问 iframe 的 DOM（安全限制）\n\n这个安全边界就是为什么自动化工具需要特殊机制（如 CDP）来与 iframe 内容交互。\n\n!!! warning \"对自动化很重要\"\n    由于浏览器安全限制，传统的基于 JavaScript 的自动化（如 Selenium 的早期方法）无法直接访问跨域 iframe 的内容。CDP 在更底层运行，出于调试目的绕过了这个限制。\n\n---\n\n## 挑战：跨进程 Iframes (OOPIFs)\n\n### 什么是 OOPIFs？\n\n现代 Chromium 使用**站点隔离 (site isolation)** 来提高安全性和稳定性。这意味着不同的源 (origin) 可能会在单独的操作系统进程中渲染。来自不同源的 iframe 会成为**跨进程 Iframe (OOPIF)**。\n\n```mermaid\ngraph LR\n    subgraph \"进程 1: example.com\"\n        MainPage[主页面 DOM]\n    end\n    \n    subgraph \"进程 2: widget.com\"\n        IframeDOM[Iframe DOM]\n    end\n    \n    MainPage -.进程边界.-> IframeDOM\n```\n\n### 为什么 OOPIFs 使自动化复杂化\n\n| 方面 | 进程内 Iframe | 跨进程 Iframe (OOPIF) |\n|--------|-------------------|-------------------------------|\n| **DOM 访问** | 内存中共享的文档树 | 拥有自己文档的独立目标 (target) |\n| **命令路由** | 单个连接 | 需要目标附加 (target attachment) 和会话路由 (session routing) |\n| **Frame 树** | 所有 frames 在一棵树中 | 根 frame + OOPIFs 的独立目标 |\n| **JavaScript 上下文** | 相同的执行上下文 | 每个进程有不同的执行上下文 |\n| **CDP 通信** | 直接命令 | 命令必须包含 `sessionId` |\n\n### 传统方法（手动切换上下文）\n\n没有复杂的处理，自动化 OOPIFs 需要：\n\n```python\n# 其他工具的传统（手动）方法\nmain_page = browser.get_page()\niframe_element = main_page.find_element_by_id(\"iframe-id\")\n\n# 必须手动切换上下文\ndriver.switch_to.frame(iframe_element)\n\n# 现在命令指向 iframe\nbutton = driver.find_element_by_id(\"button-in-iframe\")\nbutton.click()\n\n# 必须手动切换回来\ndriver.switch_to.default_content()\n```\n\n**这种方法的问题：**\n\n1. **开发者负担**：每个 iframe 都需要显式的上下文管理\n2. **嵌套 iframes**：每一层都需要再次切换\n3. **OOPIF 检测**：很难知道何时需要手动附加\n4. **容易出错**：忘记切换回来 → 后续命令失败\n5. **不可组合**：辅助函数必须知道它们所处的 iframe 上下文\n\n### Pydoll 的解决方案：透明的上下文解析\n\nPydoll 通过自动解析 iframe 上下文来消除手动上下文切换：\n\n```python\n# Pydoll 方法（无手动切换）\niframe = await tab.find(id=\"iframe-id\")\nbutton = await iframe.find(id=\"button-in-iframe\")\nawait button.click()\n\n# 嵌套 iframes？同样的模式\nouter = await tab.find(id=\"outer-iframe\")\ninner = await outer.find(tag_name=\"iframe\")\nbutton = await inner.find(text=\"Submit\")\nawait button.click()\n```\n\n复杂性在内部处理。让我们来探究一下是如何做到的。\n\n---\n\n## Chrome DevTools 协议和 Frame 管理\n\n正如在 [深度解析 → 基础 → Chrome DevTools 协议](./cdp.md) 中讨论的，CDP 通过 WebSocket 通信提供全面的浏览器控制。Frame 管理分散在多个 CDP 域中。\n\n### 相关的 CDP 域\n\n#### 1. **Page 域**\n\n管理页面生命周期、frames 和导航。\n\n**关键方法：**\n\n- `Page.getFrameTree()`：返回页面中所有 frames 的层级结构\n  ```json\n  {\n    \"frameTree\": {\n      \"frame\": {\n        \"id\": \"main-frame-id\",\n        \"url\": \"https://example.com\",\n        \"securityOrigin\": \"https://example.com\",\n        \"mimeType\": \"text/html\"\n      },\n      \"childFrames\": [\n        {\n          \"frame\": {\n            \"id\": \"child-frame-id\",\n            \"parentId\": \"main-frame-id\",\n            \"url\": \"https://widget.com/embed\"\n          }\n        }\n      ]\n    }\n  }\n  ```\n\n- `Page.createIsolatedWorld(frameId, worldName)`：在特定 frame 中创建一个新的 JavaScript 执行上下文\n  ```json\n  {\n    \"executionContextId\": 42\n  }\n  ```\n\n**Pydoll 用法：**\n\n```python\n# 源自 pydoll/elements/web_element.py\n@staticmethod\nasync def _get_frame_tree_for(\n    handler: ConnectionHandler, session_id: Optional[str]\n) -> FrameTree:\n    \"\"\"获取给定连接/目标的 Page frame 树。\"\"\"\n    command = PageCommands.get_frame_tree()\n    if session_id:\n        command['sessionId'] = session_id\n    response: GetFrameTreeResponse = await handler.execute_command(command)\n    return response['result']['frameTree']\n```\n\n#### 2. **DOM 域**\n\n提供对 DOM 结构的访问。\n\n**关键方法：**\n\n- `DOM.describeNode(objectId)`：返回有关 DOM 节点的详细信息\n  ```json\n  {\n    \"node\": {\n      \"nodeId\": 123,\n      \"backendNodeId\": 456,\n      \"nodeName\": \"IFRAME\",\n      \"frameId\": \"parent-frame-id\",\n      \"contentDocument\": {\n        \"frameId\": \"iframe-frame-id\",\n        \"documentURL\": \"https://embedded.com/page.html\"\n      }\n    }\n  }\n  ```\n\n- `DOM.getFrameOwner(frameId)`：返回拥有某个 frame 的 `<iframe>` 元素的 `backendNodeId`\n  ```json\n  {\n    \"backendNodeId\": 456\n  }\n  ```\n\n**Pydoll 用法：**\n\n```python\n# 源自 pydoll/elements/web_element.py\n@staticmethod\nasync def _owner_backend_for(\n    handler: ConnectionHandler, session_id: Optional[str], frame_id: str\n) -> Optional[int]:\n    \"\"\"获取拥有给定 frame 的 DOM 元素的 backendNodeId。\"\"\"\n    command = DomCommands.get_frame_owner(frame_id=frame_id)\n    if session_id:\n        command['sessionId'] = session_id\n    response: GetFrameOwnerResponse = await handler.execute_command(command)\n    return response.get('result', {}).get('backendNodeId')\n```\n\n#### 3. **Target 域**\n\n管理浏览器目标（页面、iframes、workers 等）。\n\n**关键方法：**\n\n- `Target.getTargets()`：列出所有可用的目标\n  ```json\n  {\n    \"targetInfos\": [\n      {\n        \"targetId\": \"page-target-id\",\n        \"type\": \"page\",\n        \"title\": \"Main Page\",\n        \"url\": \"https://example.com\"\n      },\n      {\n        \"targetId\": \"iframe-target-id\",\n        \"type\": \"iframe\",\n        \"title\": \"\",\n        \"url\": \"https://widget.com/embed\",\n        \"parentFrameId\": \"main-frame-id\"\n      }\n    ]\n  }\n  ```\n\n- `Target.attachToTarget(targetId, flatten)`：附加到一个目标以进行调试\n  - 当 `flatten=true` 时：返回一个 `sessionId` 用于在扁平化模式下路由命令\n  - 所有通信都通过同一个 WebSocket 进行，通过 `sessionId` 区分\n\n**Pydoll 用法：**\n\n```python\n# 源自 pydoll/interactions/iframe.py (简化版)\nasync def _resolve_oopif_by_parent(self, content_frame_id: str, ...):\n    \"\"\"使用 content frame id 解析 OOPIF。\"\"\"\n    browser_handler = ConnectionHandler(...)\n    targets_response: GetTargetsResponse = await browser_handler.execute_command(\n        TargetCommands.get_targets()\n    )\n    target_infos = targets_response.get('result', {}).get('targetInfos', [])\n\n    # 查找 parentFrameId 匹配的目标\n    direct_children = [\n        target_info for target_info in target_infos\n        if target_info.get('parentFrameId') == content_frame_id\n    ]\n    \n    if direct_children:\n        attach_response: AttachToTargetResponse = await browser_handler.execute_command(\n            TargetCommands.attach_to_target(\n                target_id=direct_children[0]['targetId'], \n                flatten=True\n            )\n        )\n        attached_session_id = attach_response.get('result', {}).get('sessionId')\n        # ... 对后续命令使用 session_id\n```\n\n#### 4. **Runtime 域**\n\n执行 JavaScript 并管理执行上下文。\n\n**关键方法：**\n\n- `Runtime.evaluate(expression, contextId)`：在特定的执行上下文中评估 JavaScript\n- `Runtime.callFunctionOn(functionDeclaration, objectId)`：在一个特定对象上调用函数（作为 `this`）\n\n**Pydoll 用于 iframe 文档访问的用法：**\n\n```python\n# 源自 pydoll/elements/web_element.py\nasync def _set_iframe_document_object_id(self, execution_context_id: int):\n    \"\"\"在 iframe 上下文中评估 document.documentElement 并缓存其 object id。\"\"\"\n    evaluate_command = RuntimeCommands.evaluate(\n        expression='document.documentElement',\n        context_id=execution_context_id,\n    )\n    if self._iframe_context and self._iframe_context.session_id:\n        evaluate_command['sessionId'] = self._iframe_context.session_id\n    \n    evaluate_response: EvaluateResponse = await (\n        (self._iframe_context.session_handler if self._iframe_context else None)\n        or self._connection_handler\n    ).execute_command(evaluate_command)\n    \n    document_object_id = evaluate_response.get('result', {}).get('result', {}).get('objectId')\n    if self._iframe_context:\n        self._iframe_context.document_object_id = document_object_id\n```\n\n---\n\n## 执行上下文和隔离世界\n\n### 什么是执行上下文？\n\n**执行上下文** (execution context) 是执行 JavaScript 代码的环境。浏览器中的每个 frame 至少有一个执行上下文。该上下文包括：\n\n- **全局对象**（在浏览器中是 `window`）\n- **作用域链**：如何解析变量\n- **This 绑定**：`this` 指向什么\n- **变量环境**：所有声明的变量和函数\n\n### 每个 Frame 的多个上下文\n\n单个 frame 可以有多个执行上下文：\n\n1. **主世界 (main world)（默认上下文）**：页面自己的 JavaScript 运行的地方\n2. **隔离世界 (isolated worlds)**：共享相同 DOM 但具有不同 JavaScript 全局作用域的独立上下文\n\n```mermaid\ngraph TB\n    Frame[Frame: example.com/page]\n    Frame --> MainWorld[主世界<br/>页面的 JavaScript]\n    Frame --> IsolatedWorld1[隔离世界 1<br/>扩展的内容脚本]\n    Frame --> IsolatedWorld2[隔离世界 2<br/>Pydoll 自动化]\n    \n    DOM[共享的 DOM 树]\n    MainWorld -.可以访问.-> DOM\n    IsolatedWorld1 -.可以访问.-> DOM\n    IsolatedWorld2 -.可以访问.-> DOM\n    \n    MainWorld -.无法访问.-> IsolatedWorld1\n    MainWorld -.无法访问.-> IsolatedWorld2\n```\n\n### 什么是隔离世界？\n\n**隔离世界** (isolated world) 是一个独立的 JavaScript 执行上下文，它：\n\n- **共享相同的 DOM**：可以读取/修改 DOM 元素\n- **拥有独立的全局对象**：变量/函数不会在世界之间泄漏\n- **防止干扰**：页面脚本无法检测或干扰隔离世界中的脚本\n\n**起源**：隔离世界是为浏览器扩展创建的。内容脚本 (Content scripts) 运行在隔离世界中，因此它们可以与页面 DOM 交互，而不会：\n\n- 被页面脚本覆盖其变量\n- 被防篡改代码检测到\n- 与页面的 JavaScript 冲突\n\n### 为什么 Pydoll 对 Iframes 使用隔离世界\n\n当 Pydoll 与 iframe 内容交互时，它会在该 iframe 的上下文中创建一个隔离世界。这提供了：\n\n1. **干净的 JavaScript 环境**：与 iframe 自己的脚本没有冲突\n2. **一致的行为**：无论 iframe 运行什么 JavaScript，自动化脚本都能工作\n3. **反检测**：iframe 的 JavaScript 无法轻易检测到 Pydoll 的存在\n4. **安全的评估**：自动化代码不会意外触发页面逻辑\n\n**实现：**\n\n```python\n# 源自 pydoll/elements/web_element.py\n@staticmethod\nasync def _create_isolated_world_for_frame(\n    frame_id: str,\n    handler: ConnectionHandler,\n    session_id: Optional[str],\n) -> int:\n    \"\"\"为给定的 frame 创建一个隔离世界 (Page.createIsolatedWorld)。\"\"\"\n    create_command = PageCommands.create_isolated_world(\n        frame_id=frame_id,\n        world_name=f'pydoll::iframe::{frame_id}',\n        grant_universal_access=True,\n    )\n    if session_id:\n        create_command['sessionId'] = session_id\n    \n    create_response: CreateIsolatedWorldResponse = await handler.execute_command(\n        create_command\n    )\n    execution_context_id = create_response.get('result', {}).get('executionContextId')\n    if not execution_context_id:\n        raise InvalidIFrame('无法为 iframe 创建隔离世界')\n    return execution_context_id\n```\n\n`grant_universal_access=True` 参数允许隔离世界：\n\n- 访问跨域 frames（通常被同源策略阻止）\n- 执行自动化所需的特权操作\n\n!!! tip \"实践中的隔离世界\"\n    每当您使用 `await iframe.find(...)` 时，Pydoll 都会在专门为该 iframe 创建的隔离世界中评估选择器查询。这确保您的自动化逻辑永远不会与 iframe 自己的 JavaScript 冲突，并且 iframe 无法检测或阻止您的自动化。\n\n---\n\n## CDP 标识符参考\n\n理解 CDP 标识符对于处理 iframe 至关重要。这是一个全面的参考：\n\n| 标识符 | 域 | 范围 | 目的 | 在 Pydoll 中的用例 |\n|------------|--------|-------|---------|----------------------|\n| **`nodeId`** | DOM | 文档局部 | 在特定文档上下文中标识一个 DOM 节点 | 内部 CDP 操作；在导航中不稳定 |\n| **`backendNodeId`** | DOM | 跨文档稳定 | DOM 节点的稳定标识符；可以将 frames 映射到所有者元素 | 用于通过 `DOM.getFrameOwner` 将 iframe 元素与 frame IDs 匹配 |\n| **`frameId`** | Page | Frame | 标识页面 frame 树中的一个 frame | 用于为 `Page.createIsolatedWorld` 和 frame 树遍历指定哪个 frame |\n| **`targetId`** | Target | 全局 | 标识一个调试目标（页面、iframe、worker 等） | 用于 `Target.attachToTarget` 以连接到 OOPIFs |\n| **`sessionId`** | Target | 目标特定 | 在扁平化模式下将命令路由到特定目标 | 注入到命令中，将它们路由到正确的 OOPIF |\n| **`executionContextId`** | Runtime | Frame + 世界 | 标识一个 JavaScript 执行上下文（包括隔离世界） | 由 `Page.createIsolatedWorld` 返回；用于 `Runtime.evaluate` |\n| **`objectId`** | Runtime | 执行上下文 | 远程对象引用（例如 DOM 元素、函数、对象） | 对 iframe 的 `document.documentElement` 的引用，用于相对查询 |\n\n### 标识符关系\n\n以下是 iframe 解析期间标识符之间的关系：\n\n```\n┌─────────────────────────────────────────────────────────────────────────┐\n│                         解析流程                                        │\n└─────────────────────────────────────────────────────────────────────────┘\n\n1. 开始: <iframe> 元素\n   └─ backendNodeId: 789\n   \n2. 查找 Frame ──────────────[DOM.getFrameOwner]──────────────┐\n   └─ frameId: abc-123                                       │\n                                                             │\n3. OOPIF? 检查源 ─────[检测到不同源]──────┤\n   └─ targetId: xyz-456                                      │\n                                                             │\n4. 附加到目标 ────────[Target.attachToTarget]──────────┤\n   └─ sessionId: session-789                                 │\n                                                             │\n5. 创建隔离世界 ───[Page.createIsolatedWorld]───────┤\n   └─ executionContextId: 42                                 │\n                                                             │\n6. 获取文档 ────────────[Runtime.evaluate]───────────────┘\n   └─ objectId: obj-999\n```\n\n**关键转换点：**\n\n| 从 | 方法 | 到 | 目的 |\n|------|--------|-----|---------|\n| `backendNodeId` | `DOM.getFrameOwner` | `frameId` | 查找哪个 frame 拥有该 iframe 元素 |\n| `targetId` | `Target.attachToTarget(flatten=true)` | `sessionId` | 连接到 OOPIF 以进行命令路由 |\n| `frameId` | `Page.createIsolatedWorld` | `executionContextId` | 创建安全的 JavaScript 环境 |\n| `executionContextId` | `Runtime.evaluate('document.documentElement')` | `objectId` | 获取对 iframe 文档的引用 |\n\n### Pydoll 中的代码表示\n\n```python\n# 源自 pydoll/elements/web_element.py\n@dataclass\nclass _IFrameContext:\n    \"\"\"封装 iframe 的所有标识符和路由信息。\"\"\"\n    frame_id: str                                   # frameId: 标识 frame\n    document_url: Optional[str] = None              # frame 加载的 URL\n    execution_context_id: Optional[int] = None      # executionContextId: 隔离世界\n    document_object_id: Optional[str] = None        # objectId: document.documentElement\n    session_handler: Optional[ConnectionHandler] = None  # 用于 OOPIF 目标\n    session_id: Optional[str] = None                # sessionId: 将命令路由到 OOPIF\n```\n\n这个 dataclass 被缓存在代表 iframe 的每个 `WebElement` 上，实现了所有后续操作的自动路由。\n\n---\n\n## Pydoll 的解析管道\n\n当您在 Pydoll 中访问 iframe（例如，`await iframe.find(...)`）时，一个精密的解析管道会在幕后执行。本节分解了每一步。\n\n### 高级流程\n\n```mermaid\nsequenceDiagram\n    participant User as 用户\n    participant WebElement\n    participant Pipeline as 解析管道\n    participant CDP\n    \n    用户->>WebElement: iframe.find(id='button')\n    WebElement->>WebElement: 检查 iframe 上下文是否已缓存\n    alt 上下文未缓存\n        WebElement->>Pipeline: _ensure_iframe_context()\n        Pipeline->>CDP: DOM.describeNode(iframe)\n        CDP-->>Pipeline: 节点信息 (frameId?, backendNodeId, 等)\n        \n        alt 节点信息中没有 frameId\n            Pipeline->>Pipeline: _resolve_frame_by_owner()\n            Pipeline->>CDP: Page.getFrameTree()\n            CDP-->>Pipeline: Frame 树\n            Pipeline->>CDP: DOM.getFrameOwner(每个 frame)\n            CDP-->>Pipeline: backendNodeId\n            Pipeline->>Pipeline: 匹配 backendNodeId 以查找 frameId\n        end\n        \n        alt frameId 仍然缺失 (OOPIF)\n            Pipeline->>Pipeline: _resolve_oopif_by_parent()\n            Pipeline->>CDP: Target.getTargets()\n            CDP-->>Pipeline: 目标列表\n            Pipeline->>CDP: Target.attachToTarget(targetId, flatten=true)\n            CDP-->>Pipeline: sessionId\n            Pipeline->>CDP: Page.getFrameTree(sessionId)\n            CDP-->>Pipeline: OOPIF frame 树\n        end\n        \n        Pipeline->>CDP: Page.createIsolatedWorld(frameId)\n        CDP-->>Pipeline: executionContextId\n        \n        Pipeline->>CDP: Runtime.evaluate('document.documentElement', contextId)\n        CDP-->>Pipeline: objectId (文档引用)\n        \n        Pipeline->>WebElement: 缓存 _IFrameContext\n    end\n    \n    WebElement->>WebElement: 使用缓存的上下文进行 find()\n    WebElement-->>用户: 按钮元素 (带上下文)\n```\n\n### 步骤深度解析\n\n#### **步骤 1：描述 Iframe 元素**\n\n**目标**：从 `<iframe>` DOM 元素中提取元数据。\n\n**方法**：`DOM.describeNode(objectId=iframe_object_id)`\n\n**我们得到什么**：\n\n- `backendNodeId`：iframe 元素的稳定标识符\n- `frameId`（来自 `contentDocument`）：如果 iframe 的内容已加载并在进程内\n- `documentURL`：iframe 中加载的 URL\n- `parentFrameId`（来自节点上的 `frameId` 字段）：包含此 iframe 元素的 frame\n\n**代码**：\n\n```python\n# 源自 pydoll/interactions/iframe.py\nasync def resolve(self) -> IFrameContext:\n    \"\"\"解析并返回 iframe 上下文。\"\"\"\n    base_handler, base_session_id = self._get_base_session()\n    node_info = await self._describe_element_node(base_handler, base_session_id)\n    frame_id, document_url, content_frame_id, backend_node_id = self._extract_frame_metadata(\n        node_info\n    )\n    # ... 继续解析\n```\n\n**辅助方法**：\n\n```python\n@staticmethod\ndef _extract_frame_metadata(\n    node_info: Node,\n) -> tuple[Optional[str], Optional[str], Optional[str], Optional[int]]:\n    \"\"\"从 DOM.describeNode 节点中提取 iframe 相关的元数据。\"\"\"\n    content_document = node_info.get('contentDocument') or {}\n    content_frame_id = node_info.get('frameId')\n    backend_node_id = node_info.get('backendNodeId')\n    frame_id = content_document.get('frameId')\n    document_url = (\n        content_document.get('documentURL')\n        or content_document.get('baseURL')\n        or node_info.get('documentURL')\n        or node_info.get('baseURL')\n    )\n    return frame_id, document_url, content_frame_id, backend_node_id\n```\n\n**结果**：\n\n- **如果 `frame_id` 存在**：很好！iframe 在进程内；进入步骤 4。\n- **如果 `frame_id` 缺失**：iframe 可能是 OOPIF 或未完全加载；进入步骤 2。\n\n---\n\n#### **步骤 2：通过所有者解析 Frame（backendNodeId 匹配）**\n\n**目标**：通过将 iframe 元素的 `backendNodeId` 与 frame 树中的 frame 所有者匹配，来找到 `frameId`。\n\n**策略**：\n\n1. 获取页面的 frame 树 (`Page.getFrameTree`)\n2. 对树中的每个 frame，调用 `DOM.getFrameOwner(frameId)` 来获取所属 iframe 元素的 `backendNodeId`\n3. 与我们的 iframe 的 `backendNodeId` 进行比较\n4. 当它们匹配时，我们就找到了正确的 `frameId`\n\n**代码**：\n\n```python\n# 源自 pydoll/elements/web_element.py\nasync def _resolve_frame_by_owner(\n    self,\n    base_handler: ConnectionHandler,\n    base_session_id: Optional[str],\n    backend_node_id: int,\n    current_document_url: Optional[str],\n) -> tuple[Optional[str], Optional[str]]:\n    \"\"\"通过匹配所有者的 backend_node_id 来解析 frame id 和 URL。\"\"\"\n    owner_frame_id, owner_url = await self._find_frame_by_owner(\n        base_handler, base_session_id, backend_node_id\n    )\n    if not owner_frame_id:\n        return None, current_document_url\n    return owner_frame_id, owner_url or current_document_url\n\nasync def _find_frame_by_owner(\n    self, handler: ConnectionHandler, session_id: Optional[str], backend_node_id: int\n) -> tuple[Optional[str], Optional[str]]:\n    \"\"\"通过匹配 <iframe> 元素的所有者 backend_node_id 来查找 frame。\"\"\"\n    frame_tree = await self._get_frame_tree_for(handler, session_id)\n    for frame_node in WebElement._walk_frames(frame_tree):\n        candidate_frame_id = frame_node.get('id', '')\n        if not candidate_frame_id:\n            continue\n        owner_backend_id = await self._owner_backend_for(\n            handler, session_id, candidate_frame_id\n        )\n        if owner_backend_id == backend_node_id:\n            return candidate_frame_id, frame_node.get('url')\n    return None, None\n```\n\n**为什么这是必要的**：\n\n- 对于跨域或延迟加载的 iframes，`DOM.describeNode` 有时不包含 `contentDocument.frameId`\n- frame 树总是包含所有 frames（甚至是 OOPIFs），所以我们可以间接找到它\n\n**结果**：\n\n- **如果找到 `frameId`**：进入步骤 4。\n- **如果仍然找不到**：iframe 很可能是一个在独立目标中的 OOPIF；进入步骤 3。\n\n---\n\n#### **步骤 3：通过父 Frame 解析 OOPIF**\n\n**目标**：对于跨进程 Iframes，找到正确的目标，附加到它，并从该目标的 frame 树中获取 `frameId`（以及必要时用于路由的 `sessionId`）。\n\n**何时会进入此步骤**：\n\n- 已经有 `frameId` 且**没有** `backendNodeId` 的同源 / 进程内 iframe 会跳过此步骤（直接使用 `frameId`）。\n- 具有 `backendNodeId` 的跨域 / OOPIF iframe，或在步骤 2 中仍无法解析 `frameId` 的 iframe，会进入此步骤。\n\n**策略**：\n\n**3a. 直接子目标查找（快速路径）**：\n\n1. 调用 `Target.getTargets()` 列出所有调试目标。\n2. 筛选 `type` 为 `\"iframe\"` 或 `\"page\"` 且 `parentFrameId` 与我们的父 frame 匹配的目标。\n3. 如果只有**一个**匹配的子目标且**没有 `backendNodeId`**，则直接使用 `Target.attachToTarget(targetId, flatten=true)` 附加到该目标。\n4. 为该目标获取 `Page.getFrameTree(sessionId)`；此树的根 frame 就是我们 iframe 的 frame。\n\n当存在**多个**直接子目标或我们有 `backendNodeId`（典型 OOPIF 情况）时，Pydoll 会对每个子目标执行以下流程：\n\n1. 使用 `Target.attachToTarget(flatten=true)` 附加。\n2. 获取 `Page.getFrameTree(sessionId)` 并读取根 `frame.id`。\n3. 在**主连接**上调用 `DOM.getFrameOwner(frameId=root_id)`。\n4. 将返回的 `backendNodeId` 与 iframe 元素自身的 `backendNodeId` 比较。\n5. 根所有者匹配的那个子目标被选为正确的 OOPIF 目标。\n\n**3b. 备用方案：扫描所有目标（根所有者 + 子节点查找）**：\n\n如果没有找到合适的直接子目标（或 `parentFrameId` 信息不完整），Pydoll 会退回到扫描**所有** iframe/page 目标：\n\n1. 遍历所有 iframe/page 目标。\n2. 附加到每个目标并获取其 frame 树。\n3. 先尝试通过 `DOM.getFrameOwner(root_frame_id)` 将**根 frame 的所有者**与 iframe 的 `backendNodeId` 进行匹配。\n4. 如果仍不匹配，则查找 `parentId` 等于我们的 `content_frame_id` 的**子 frame**（覆盖 OOPIF 由中间 frame 间接承载的情况）。\n\n**代码**：\n\n```python\n# 源自 pydoll/interactions/iframe.py\nasync def _resolve_oopif_by_parent(\n    self,\n    content_frame_id: str,\n    backend_node_id: Optional[int],\n    base_handler: Optional[ConnectionHandler] = None,\n    base_session_id: Optional[str] = None,\n) -> tuple[Optional[ConnectionHandler], Optional[str], Optional[str], Optional[str]]:\n    \"\"\"使用 content frame id 解析 OOPIF。\"\"\"\n    browser_handler = ConnectionHandler(\n        connection_port=self._element._connection_handler._connection_port\n    )\n    targets_response: GetTargetsResponse = await browser_handler.execute_command(\n        TargetCommands.get_targets()\n    )\n    target_infos = targets_response.get('result', {}).get('targetInfos', [])\n\n    # 可以解析 DOM.getFrameOwner 的处理程序。\n    # 当 <iframe> 位于嵌套 OOPIF 内部时，Tab 级处理程序没有可见性；\n    # 我们必须通过最初发现该元素的会话路由。\n    owner_handler = base_handler or self._element._connection_handler\n    owner_session_id = base_session_id\n\n    # 策略 3a：直接子目标（快速路径）\n    direct_children = [\n        target_info\n        for target_info in target_infos\n        if target_info.get('type') in {'iframe', 'page'}\n        and target_info.get('parentFrameId') == content_frame_id\n    ]\n\n    is_single_child = len(direct_children) == 1\n    for child_target in direct_children:\n        attach_response: AttachToTargetResponse = await browser_handler.execute_command(\n            TargetCommands.attach_to_target(\n                target_id=child_target['targetId'], flatten=True\n            )\n        )\n        attached_session_id = attach_response.get('result', {}).get('sessionId')\n        if not attached_session_id:\n            continue\n\n        frame_tree = await self._get_frame_tree_for(browser_handler, attached_session_id)\n        root_frame = (frame_tree or {}).get('frame', {})\n        root_frame_id = root_frame.get('id', '')\n\n        # 简单 / 同源场景：只有一个子目标且没有 backend_node_id\n        if is_single_child and root_frame_id and backend_node_id is None:\n            return (\n                browser_handler,\n                attached_session_id,\n                root_frame_id,\n                root_frame.get('url'),\n            )\n\n        # OOPIF 场景：通过 DOM.getFrameOwner 确认所有权\n        if root_frame_id and backend_node_id is not None:\n            owner_backend_id = await self._owner_backend_for(\n                owner_handler, owner_session_id, root_frame_id\n            )\n            if owner_backend_id == backend_node_id:\n                return (\n                    browser_handler,\n                    attached_session_id,\n                    root_frame_id,\n                    root_frame.get('url'),\n                )\n\n    # 策略 3b：扫描所有目标（根所有者 + 子节点查找）\n    for target_info in target_infos:\n        if target_info.get('type') not in {'iframe', 'page'}:\n            continue\n        attach_response = await browser_handler.execute_command(\n            TargetCommands.attach_to_target(\n                target_id=target_info.get('targetId', ''), flatten=True\n            )\n        )\n        attached_session_id = attach_response.get('result', {}).get('sessionId')\n        if not attached_session_id:\n            continue\n\n        frame_tree = await self._get_frame_tree_for(browser_handler, attached_session_id)\n        root_frame = (frame_tree or {}).get('frame', {})\n        root_frame_id = root_frame.get('id', '')\n\n        # 直接匹配：content_frame_id 等于该目标的根 frame ID\n        if root_frame_id and root_frame_id == content_frame_id:\n            return (\n                browser_handler,\n                attached_session_id,\n                root_frame_id,\n                root_frame.get('url'),\n            )\n\n        # 优先尝试根据 backend_node_id 匹配根 frame 的所有者\n        if root_frame_id and backend_node_id is not None:\n            owner_backend_id = await self._owner_backend_for(\n                owner_handler, owner_session_id, root_frame_id\n            )\n            if owner_backend_id == backend_node_id:\n                return (\n                    browser_handler,\n                    attached_session_id,\n                    root_frame_id,\n                    root_frame.get('url'),\n                )\n\n        # 备用：查找 parentId 等于 content_frame_id 的子 frame\n        child_frame_id = IFrameContextResolver._find_child_by_parent(\n            frame_tree, content_frame_id\n        )\n        if child_frame_id:\n            return browser_handler, attached_session_id, child_frame_id, None\n\n    return None, None, None, None\n```\n\n**结果**：\n\n- **如果 OOPIF 已解析**：我们现在有了 `sessionId`、`session_handler` 和 `frameId`；进入步骤 4。\n- **如果解析失败**：抛出 `InvalidIFrame` 异常（在 `_ensure_iframe_context` 中处理）。\n\n---\n\n#### **步骤 4：创建隔离世界**\n\n**目标**：在已解析的 frame 中创建一个独立的 JavaScript 执行上下文。\n\n**方法**：`Page.createIsolatedWorld(frameId, worldName='pydoll::iframe::<frameId>', grantUniversalAccess=true)`\n\n**参数**：\n- `frameId`：在其中创建隔离世界的 frame\n- `worldName`：世界的标识符（用于调试）\n- `grantUniversalAccess`：允许跨域访问（自动化需要）\n\n**响应**：`{ executionContextId: 42 }`\n\n**代码**：\n\n```python\n# 源自 pydoll/elements/web_element.py\n@staticmethod\nasync def _create_isolated_world_for_frame(\n    frame_id: str,\n    handler: ConnectionHandler,\n    session_id: Optional[str],\n) -> int:\n    \"\"\"为给定的 frame 创建一个隔离世界。\"\"\"\n    create_command = PageCommands.create_isolated_world(\n        frame_id=frame_id,\n        world_name=f'pydoll::iframe::{frame_id}',\n        grant_universal_access=True,\n    )\n    if session_id:\n        create_command['sessionId'] = session_id\n    create_response: CreateIsolatedWorldResponse = await handler.execute_command(create_command)\n    execution_context_id = create_response.get('result', {}).get('executionContextId')\n    if not execution_context_id:\n        raise InvalidIFrame('无法为 iframe 创建隔离世界')\n    return execution_context_id\n```\n\n**为什么需要隔离世界**：\n\n- **隔离**：我们的自动化 JavaScript 不会干扰 iframe 的 JavaScript\n- **反检测**：iframe 无法轻易检测到我们的存在\n- **一致性**：无论 iframe 的脚本环境如何，行为都是可预测的\n\n**结果**：我们有了一个 `executionContextId` 用于在 iframe 中运行 JavaScript。\n\n---\n\n#### **步骤 5：将 Iframe 文档固定为运行时对象**\n\n**目标**：获取对 iframe 的 `document.documentElement`（iframe 的 `<html>` 元素）的 `objectId` 引用。\n\n**方法**：`Runtime.evaluate(expression='document.documentElement', contextId=executionContextId)`\n\n**为什么我们需要这个**：\n\n- 以便在 iframe 内部执行**相对查询**（如 `element.querySelector()`）\n- `objectId` 允许使用 `Runtime.callFunctionOn(objectId, ...)`，并将 `this` 绑定到 iframe 的文档\n\n**代码**：\n\n```python\n# 源自 pydoll/elements/web_element.py\nasync def _set_iframe_document_object_id(self, execution_context_id: int) -> None:\n    \"\"\"在 iframe 上下文中评估 document.documentElement 并缓存其 object id。\"\"\"\n    evaluate_command = RuntimeCommands.evaluate(\n        expression='document.documentElement',\n        context_id=execution_context_id,\n    )\n    if self._iframe_context and self._iframe_context.session_id:\n        evaluate_command['sessionId'] = self._iframe_context.session_id\n    evaluate_response: EvaluateResponse = await (\n        (self._iframe_context.session_handler if self._iframe_context else None)\n        or self._connection_handler\n    ).execute_command(evaluate_command)\n    result_object = evaluate_response.get('result', {}).get('result', {})\n    document_object_id = result_object.get('objectId')\n    if not document_object_id:\n        raise InvalidIFrame('无法获取 iframe 的文档引用')\n    if self._iframe_context:\n        self._iframe_context.document_object_id = document_object_id\n```\n\n**结果**：`_IFrameContext` 现在已完全填充并缓存在 `WebElement` 上。\n\n---\n\n#### **步骤 6：缓存和传播上下文**\n\n**目标**：将解析的上下文存储在 iframe 元素上，并将其传播到在 iframe 中找到的所有子元素。\n\n**缓存**：\n\n```python\n# 源自 pydoll/elements/web_element.py\ndef _init_iframe_context(\n    self,\n    frame_id: str,\n    document_url: Optional[str],\n    session_handler: Optional[ConnectionHandler],\n    session_id: Optional[str],\n) -> None:\n    \"\"\"在此元素上初始化并缓存 iframe 上下文。\"\"\"\n    self._iframe_context = _IFrameContext(frame_id=frame_id, document_url=document_url)\n    # 清理路由属性（这些是用于嵌套 iframe 的）\n    if hasattr(self, '_routing_session_handler'):\n        delattr(self, '_routing_session_handler')\n    if hasattr(self, '_routing_session_id'):\n        delattr(self, '_routing_session_id')\n    # 如果需要，存储 OOPIF 路由\n    if session_handler and session_id:\n        self._iframe_context.session_handler = session_handler\n        self._iframe_context.session_id = session_id\n```\n\n**传播**（在 iframe 内部查找元素时）：\n\n```python\n# 源自 pydoll/elements/mixins/find_elements_mixin.py\ndef _apply_iframe_context_to_element(\n    self, element: WebElement, iframe_context: _IFrameContext | None\n) -> None:\n    \"\"\"将 iframe 上下文传播到新创建的元素。\"\"\"\n    if not iframe_context:\n        return\n    \n    # 如果子元素也是一个 iframe，设置路由\n    if getattr(element, 'is_iframe', False):\n        element._routing_session_handler = (\n            iframe_context.session_handler or self._connection_handler\n        )\n        element._routing_session_id = iframe_context.session_id\n        element._routing_parent_frame_id = iframe_context.frame_id\n        return\n    \n    # 否则，注入父 iframe 的上下文\n    element._iframe_context = iframe_context\n```\n\n**为什么传播很重要**：\n\n- 在 iframe 内部找到的元素会继承 iframe 的上下文\n- 这确保了后续操作（点击、键入、查找嵌套元素）自动使用正确的路由\n- 嵌套的 iframes 接收路由信息，以便它们可以相对于父 iframe 解析自己的上下文\n\n---\n\n## 会话路由和扁平化模式\n\n### 扁平化会话模型\n\n正如在 [深度解析 → 基础 → CDP](./cdp.md) 中讨论的，传统的 CDP 对每个目标使用单独的 WebSocket 连接。**扁平化模式 (Flattened mode)** 是一种优化，所有目标共享一个 WebSocket 连接，命令使用 `sessionId` 进行路由。\n\n```mermaid\ngraph TB\n    subgraph \"传统模式\"\n        WS1[WebSocket 1] --> MainPage[主页面目标]\n        WS2[WebSocket 2] --> Iframe1[OOPIF 目标 1]\n        WS3[WebSocket 3] --> Iframe2[OOPIF 目标 2]\n    end\n    \n    subgraph \"扁平化模式\"\n        WS[单个 WebSocket] --> Router{CDP 路由器}\n        Router -->|sessionId: null| MainPage2[主页面目标]\n        Router -->|sessionId: session-1| Iframe3[OOPIF 目标 1]\n        Router -->|sessionId: session-2| Iframe4[OOPIF 目标 2]\n    end\n```\n\n### 会话路由如何工作\n\n**附加到 OOPIF 时**：\n\n```python\nresponse = await handler.execute_command(\n    TargetCommands.attach_to_target(targetId=\"iframe-target-id\", flatten=True)\n)\nsession_id = response['result']['sessionId']  # 例如 \"8E6C...-1234\"\n```\n\n**向该 OOPIF 发送命令时**：\n\n```python\ncommand = PageCommands.get_frame_tree()\ncommand['sessionId'] = 'session-1'  # 路由到 OOPIF\nresponse = await handler.execute_command(command)\n```\n\n浏览器的 CDP 实现会根据 `sessionId` 将命令路由到正确的目标。\n\n### Pydoll 的命令路由\n\nPydoll 元素发送的每个命令都会自动路由到正确的目标：\n\n```python\n# 源自 pydoll/elements/mixins/find_elements_mixin.py\ndef _resolve_routing(self) -> tuple[ConnectionHandler, Optional[str]]:\n    \"\"\"为当前上下文解析 handler 和 sessionId。\"\"\"\n    # 检查元素是否具有带 OOPIF 路由的 iframe 上下文\n    iframe_context = getattr(self, '_iframe_context', None)\n    if iframe_context and getattr(iframe_context, 'session_handler', None):\n        return iframe_context.session_handler, getattr(iframe_context, 'session_id', None)\n    \n    # 检查元素是否从父 iframe 继承了路由\n    routing_handler = getattr(self, '_routing_session_handler', None)\n    if routing_handler is not None:\n        return routing_handler, getattr(self, '_routing_session_id', None)\n    \n    # 默认：使用标签页的主连接\n    return self._connection_handler, None\n\nasync def _execute_command(\n    self, command: Command[T_CommandParams, T_CommandResponse]\n) -> T_CommandResponse:\n    \"\"\"通过解析的 handler 执行 CDP 命令（60 秒超时）。\"\"\"\n    handler, session_id = self._resolve_routing()\n    if session_id:\n        command['sessionId'] = session_id\n    return await handler.execute_command(command, timeout=60)\n```\n\n**路由逻辑**：\n\n1. **OOPIF iframe 内的元素**：使用 `iframe_context.session_id` 和 `iframe_context.session_handler`\n2. **嵌套 iframe（OOPIF 的子节点）**：使用继承的 `_routing_session_id` 和 `_routing_session_handler`\n3. **常规元素或进程内 iframe**：使用主连接 (`_connection_handler`)，无 `sessionId`\n\n### 扩展的命令类型\n\n为了使 `sessionId` 类型安全，Pydoll 扩展了 `Command` TypedDict：\n\n```python\n# 源自 pydoll/protocol/base.py\nclass Command(TypedDict, Generic[T_CommandParams, T_CommandResponse]):\n    \"\"\"所有命令的基础结构。\"\"\"\n    id: NotRequired[int]\n    method: str\n    params: NotRequired[T_CommandParams]\n    sessionId: NotRequired[str]  # 为扁平化会话路由添加\n```\n\n这允许类型检查器将 `command['sessionId'] = '...'` 识别为有效，而无需抑制类型警告。\n\n---\n\n## 性能考量\n\n### 缓存策略\n\n**首次访问是昂贵的**：\n\n- `DOM.describeNode`：1 次往返\n- Frame 树检索：1+ 次往返（主目标 + OOPIF 目标）\n- 每个 frame 的 `DOM.getFrameOwner`：N 次往返（最坏情况下）\n- `Target.getTargets` + 附加：1 + M 次往返（M = OOPIF 目标数量）\n- `Page.createIsolatedWorld`：1 次往返\n- `Runtime.evaluate` (文档)：1 次往返\n\n**总计**：根据页面结构，可能需要 5-20+ 次往返。\n\n**后续访问是 O(1)**：\n\n- `iframe_context` 缓存在 `WebElement` 实例上\n- 多次访问 `await iframe.iframe_context` 会立即返回缓存的值\n- 在 iframe 中找到的所有元素都会继承上下文（无需重新解析）\n\n### 优化：直接子目标查找\n\n在 `_resolve_oopif_by_parent` 中，Pydoll 首先按 `parentFrameId` 检查直接子节点：\n\n```python\ndirect_children = [\n    target_info\n    for target_info in target_infos\n    if target_info.get('type') in {'iframe', 'page'}\n    and target_info.get('parentFrameId') == content_frame_id\n]\nif direct_children:\n    # 立即附加，跳过扫描所有目标\n```\n\n**为什么这有帮助**：\n\n- 大多数 OOPIFs 都正确设置了 `parentFrameId`\n- 避免了推测性地附加到每个目标\n- 在常见情况下，将往返次数从 O(目标数量) 减少到 O(1)\n\n### 异步并行解析（未来增强）\n\n目前，frame 所有者匹配是顺序的（逐个检查每个 frame）。未来的优化可以并行化：\n\n```python\n# 当前（顺序）\nfor frame_node in frames:\n    owner = await self._owner_backend_for(...)\n    if owner == backend_node_id:\n        return frame_node['id']\n\n# 潜在（并行）\nresults = await asyncio.gather(*(\n    self._owner_backend_for(..., frame['id'])\n    for frame in frames\n))\nfor i, owner in enumerate(results):\n    if owner == backend_node_id:\n        return frames[i]['id']\n```\n\n这将把延迟从 `N * RTT` 减少到 `RTT`（其中 RTT = 往返时间）。\n\n---\n\n## 失败模式和调试\n\n### 常见失败场景\n\n#### 1. **InvalidIFrame: 无法解析 frameId**\n\n**原因**：\n\n- iframe 是动态创建的，尚未完全初始化\n- iframe 被具有限制性策略的沙盒化\n- 网络问题延迟了 iframe 加载\n\n**解决方案**：\n\n- **等待 iframe**：使用带超时的 `await tab.find(id='iframe', timeout=10)`\n- **检查 sandbox 属性**：限制性沙盒 (`<iframe sandbox>`) 可能会阻止某些 CDP 操作\n- **重试策略**：实现带指数退避的重试逻辑\n\n**调试**：\n\n```python\ntry:\n    iframe = await tab.find(id='problem-iframe')\n    context = await iframe.iframe_context\nexcept InvalidIFrame as e:\n    # 检查我们拥有的信息\n    node_info = await iframe._describe_node(object_id=iframe._object_id)\n    print(f\"节点信息: {node_info}\")\n    \n    # 手动检查 frame 树\n    frame_tree = await WebElement._get_frame_tree_for(tab._connection_handler, None)\n    print(f\"Frame 树: {frame_tree}\")\n```\n\n#### 2. **InvalidIFrame: 无法创建隔离世界**\n\n**原因**：\n\n- 在解析步骤之间，Frame 已被销毁/导航离开\n- Chrome 错误（罕见）\n\n**解决方案**：\n\n- **重新解析上下文**：清除缓存的上下文并重新访问\n- **检查导航**：确保 iframe 在解析期间没有导航\n\n**调试**：\n\n```python\n# 清除缓存并重试\niframe._iframe_context = None\ncontext = await iframe.iframe_context\n```\n\n#### 3. **InvalidIFrame: 无法获取文档引用**\n\n**原因**：\n\n- 隔离世界已创建，但文档尚未准备好\n- Frame 即将导航\n\n**解决方案**：\n\n- 等待 frame 加载：使用 Page 事件检测 `Page.frameNavigated` 或 `Page.loadEventFired`\n- 稍作延迟后重试\n\n#### 4. **会话路由失败（命令超时或返回错误）**\n\n**原因**：\n\n- OOPIF 目标已分离（页面导航，iframe 被移除）\n- `sessionId` 已过时\n\n**解决方案**：\n\n- **重新附加到目标**：创建一个新的 `ConnectionHandler` 并重新解析 OOPIF\n- **验证目标**：调用 `Target.getTargets()` 检查目标是否仍然存在\n\n**调试**：\n\n```python\n# 检查会话是否仍然有效\ntargets = await handler.execute_command(TargetCommands.get_targets())\nactive_sessions = [t['targetId'] for t in targets['result']['targetInfos']]\nprint(f\"活动目标: {active_sessions}\")\n\nif iframe._iframe_context and iframe._iframe_context.session_id:\n    print(f\"我们的会话: {iframe._iframe_context.session_id}\")\n```\n\n### 诊断工具\n\n#### 启用 CDP 日志记录\n\n```python\nimport logging\nlogging.basicConfig(level=logging.DEBUG)\nlogger = logging.getLogger('pydoll')\nlogger.setLevel(logging.DEBUG)\n```\n\n这将记录所有 CDP 命令和响应，有助于追踪 iframe 解析步骤。\n\n#### 检查 iframe 上下文\n\n```python\niframe = await tab.find(id='my-iframe')\nctx = await iframe.iframe_context\n\nprint(f\"Frame ID: {ctx.frame_id}\")\nprint(f\"文档 URL: {ctx.document_url}\")\nprint(f\"执行上下文 ID: {ctx.execution_context_id}\")\nprint(f\"文档对象 ID: {ctx.document_object_id}\")\nprint(f\"会话 ID (OOPIF): {ctx.session_id}\")\nprint(f\"会话 Handler: {ctx.session_handler}\")\n```\n\n---\n\n## 结论\n\nPydoll 的 iframe 处理代表了对 CDP frame 管理能力的复杂实现。通过理解：\n\n- **DOM**：树结构和节点标识\n- **Iframes**：独立的文档上下文和安全边界\n- **OOPIFs**：站点隔离和基于目标的架构\n- **CDP 域**：Page、DOM、Target、Runtime 的协调\n- **执行上下文**：用于纯净自动化的隔离世界\n- **标识符**：backendNodeId、frameId、targetId、sessionId、executionContextId、objectId 之间的关系\n- **解析管道**：用于查找 frames 的多阶段回退策略\n- **会话路由**：扁平化模式和自动命令路由\n\n您就能理解为什么 Pydoll 消除了手动上下文切换。这种复杂性是真实存在的，但 Pydoll 将其抽象在一个简单、直观的 API 背后：\n\n```python\niframe = await tab.find(id='login-frame')\nusername = await iframe.find(name='username')\nawait username.type_text('user@example.com')\n```\n\n三行代码。没有上下文切换。没有目标附加。没有会话管理。它就是能用。\n\n---\n\n## 进一步阅读\n\n- **CDP 规范**：[Chrome DevTools 协议 - Page 域](https://chromedevtools.github.io/devtools-protocol/tot/Page/)\n- **CDP 规范**：[Chrome DevTools 协议 - DOM 域](https://chromedevtools.github.io/devtools-protocol/tot/DOM/)\n- **CDP 规范**：[Chrome DevTools 协议 - Target 域](https://chromedevtools.github.io/devtools-protocol/tot/Target/)\n- **CDP 规范**：[Chrome DevTools 协议 - Runtime 域](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/)\n- **Chromium 站点隔离**：[站点隔离 - Chromium 项目](https://www.chromium.org/Home/chromium-security/site-isolation/)\n- **内容脚本和隔离世界**：[Chrome 扩展 - 内容脚本](https://developer.chrome.com/docs/extensions/mv3/content_scripts/)\n- **Pydoll 文档**：[深度解析 → 基础 → Chrome DevTools 协议](./cdp.md)\n- **Pydoll 文档**：[功能 → 自动化 → IFrames](../../features/automation/iframes.md)\n\n---\n\n!!! tip \"设计理念\"\n    Pydoll iframe 处理的目标是**符合人体工程学的自动化**：编写代码时就好像 iframes 不存在一样，让库来处理复杂性。这次深度解析展示了幕后发生的事情——但在您的自动化脚本中，您永远不必考虑它。"
  },
  {
    "path": "docs/zh/deep-dive/fundamentals/index.md",
    "content": "# 核心基础\n\n**掌握了基础，其他一切都会变得更容易。**\n\n本节涵盖了 Pydoll 赖以运行的 **基石技术**：Chrome 开发者工具协议 (CDP)、基于 WebSocket 的异步通信以及 Python 的类型系统集成。这些不仅仅是实现细节，它们是使 Pydoll 快速、强大且类型安全的 **基本设计决策**。\n\n## 为什么基础知识很重要\n\n大多数自动化框架都将其通信层抽象化，留给您一个“黑匣子”，它能正常工作直到出现问题。当出现问题时，如果不了解底层机制，调试和优化将变得困难。\n\n**Pydoll 采用了不同的方法**：我们揭示并解释基础知识，使您能够同时作为 **框架用户** 和 **协议工程师** 工作。\n\n!!! quote \"第一性原理的力量\"\n    **“通晓大道者，万物皆在其中。”** - 宫本武藏\n    \n    理解 CDP、异步通信和类型系统不仅仅是为了 Pydoll，它是为了从 **核心层面理解现代浏览器自动化是如何工作的**。这些知识可以转移到任何基于 CDP 的工具和任何异步 Python 项目中。\n\n## 三大支柱\n\n### 1. Chrome 开发者工具协议 (CDP)\n**[→ 阅读 CDP 深度解析](./cdp.md)**\n\n**驱动现代浏览器自动化的协议。**\n\nCDP 是 Chrome 的原生调试协议，与 Chrome 开发者工具 (F12) 使用的协议相同。通过直接与 CDP 通信，Pydoll 能够：\n\n- **消除 WebDriver**（没有 Selenium 开销，没有 geckodriver/chromedriver 中间件）\n- **获得深度控制**（修改请求、拦截事件、执行特权操作）\n- **实现原生速度**（直接 WebSocket 通信，无 HTTP 轮询）\n- **变得无法检测**（没有 `navigator.webdriver`，没有 WebDriver 指纹）\n\n**您将学到什么：**\n\n- CDP 如何将功能组织到各个域中（Page, Network, DOM, Fetch 等）\n- 驱动反应式自动化的命令/事件架构\n- 为什么基于 CDP 的工具 **从根本上比 Selenium 更强大**\n- 如何阅读 CDP 文档并扩展 Pydoll\n\n**为什么这很重要**：CDP 不仅仅是 Pydoll 的实现细节，它是现代浏览器自动化的基础。Puppeteer、Playwright 和类似的工具都使用 CDP。一次理解，知识可通用于多种工具。\n\n---\n\n### 2. 连接层\n**[→ 阅读连接层架构](./connection-layer.md)**\n\n**正确实现的异步通信。**\n\nCDP 定义了您 **能做什么**，而连接层则定义了 Pydoll **如何** 与浏览器通信。在这里，协议消息变成了 Python 对象，async/await 模式实现了并发，WebSocket 提供了实时的双向通信。\n\n**您将学到什么：**\n\n- WebSocket 架构：持久连接、消息分帧、心跳维持\n- async/await 模式：为什么 `async def` 和 `await` 能实现并发自动化\n- 命令/响应关联：Pydoll 如何将响应与请求匹配\n- 事件分发：浏览器事件如何触发 Python 回调\n- 错误处理：超时管理、连接失败、优雅降级\n\n**为什么这很重要**：连接层是 Pydoll 的通信骨干。理解它有助于：\n- **有效调试**：检查 Python 和 Chrome 之间流动的消息\n- **性能优化**：识别延迟来源并使操作并行化\n- **扩展能力**：添加自定义 CDP 命令或修改现有行为\n\n---\n\n### 3. Python 类型系统集成\n**[→ 阅读类型系统深度解析](./typing-system.md)**\n\n**类型同时提供安全性和生产力。**\n\nPython 的类型系统（自 3.5 版引入，此后每个版本都有增强）显著改善了开发体验。Pydoll 利用 `TypedDict`, `Literal`, `overload` 和泛型来提供：\n\n- **IDE 自动补全** CDP 响应字段\n- **类型检查** 以在运行时之前捕获错误 (`mypy`, `pyright`)\n- **自文档化代码**（函数签名揭示了结构）\n- **重构安全**（重命名字段，IDE 会更新所有用法）\n\n**您将学到什么：**\n\n- `TypedDict` 如何为 CDP 事件/响应结构建模\n- 为什么 `overload` 为 `find()` / `query()` 提供了精确的返回类型\n- 泛型（`TypeVar`, `Generic[T]`）如何实现灵活的命令构建\n- 实用模式：注解回调、为异步函数添加类型、使用 `Literal`\n- 工具集成：配置 mypy、利用 IDE 类型推断\n\n**为什么这很重要**：类型提示在现代 Python 中变得越来越重要。Pydoll 全面的类型覆盖意味着：\n- **更快的开发**：自动补全揭示了可用的字段和方法\n- **更少的错误**：类型检查器在错误进入生产环境前捕获它们\n- **更好的重构**：借助 IDE 支持，自信地更改签名\n\n---\n\n## 这些基础知识如何相互连接\n\n理解 CDP、异步通信和类型系统如何 **协同工作** 是关键：\n\n```mermaid\ngraph TB\n    Python[Python 代码:<br/>await tab.go_to#40;url#41;]\n    \n    Python --> TypeSystem[类型系统:<br/>函数签名揭示了<br/>参数和返回类型]\n    \n    TypeSystem --> ConnectionLayer[连接层:<br/>将命令序列化为 JSON,<br/>通过 WebSocket 发送]\n    \n    ConnectionLayer --> CDP[CDP:<br/>浏览器接收<br/>Page.navigate 命令]\n    \n    CDP --> Browser[Chrome:<br/>执行导航,<br/>发出事件]\n    \n    Browser --> CDPEvents[CDP 事件:<br/>Page.loadEventFired,<br/>Network.requestWillBeSent]\n    \n    CDPEvents --> ConnectionLayer2[连接层:<br/>反序列化事件,<br/>分派给回调]\n    \n    ConnectionLayer2 --> TypedDicts[TypedDict:<br/>事件数据作为<br/>类型化字典]\n    \n    TypedDicts --> PythonCallback[Python 回调:<br/>IDE 通过类型推断<br/>显示可用字段]\n```\n\n**流程**：\n1.  您编写带有 **类型注解** 的 Python 代码（类型系统）\n2.  代码序列化为 JSON 并通过 **WebSocket** 发送（连接层）\n3.  浏览器接收并执行 **CDP 命令**（CDP）\n4.  浏览器将 **CDP 事件** 发回（CDP）\n5.  事件反序列化为 **TypedDict 实例**（类型系统）\n6.  您的回调接收到 **类型安全的事件对象**（类型系统）\n\n每一层都 **放大** 了其他层的作用：\n- 类型使 CDP 响应易于发现\n- CDP 的事件模型支持了异步模式\n- 异步通信使类型变得至关重要（这个响应上有哪些字段？）\n\n## 学习路径\n\n我们推荐以下进阶路径：\n\n### 步骤 1: CDP (1-2 小时)\n**[从这里开始: Chrome 开发者工具协议](./cdp.md)**\n\n理解驱动一切的协议。学习域、命令、事件以及如何阅读 CDP 文档。\n\n**成果**：您将知道如何查找和使用任何 CDP 功能，而不仅仅是 Pydoll 暴露的功能。\n\n### 步骤 2: 连接层 (2-3 小时)\n**[继续: 连接层架构](./connection-layer.md)**\n\n深入了解 WebSocket 通信、异步模式和事件分发。\n\n**成果**：您将确切理解消息如何在 Python 和 Chrome 之间流动，从而实现调试和优化。\n\n### 步骤 3: 类型系统 (1-2 小时)\n**[完成: Python 类型系统](./typing-system.md)**\n\n学习 Pydoll 如何使用现代 Python 类型来实现安全性和生产力。\n\n**成果**：您将编写出具有完整 IDE 支持的类型安全的自动化代码，在运行前捕获错误。\n\n**总时间**：4-7 小时\n**回报**：对基于 CDP 的自动化基础的 **永久理解**\n\n## 先决条件\n\n要从本节中获得最大收益：\n\n- **Python 基础知识** - 函数、类、装饰器\n- **基本的 async/await** - 理解 `async def` 和 `await` 关键字\n- **熟悉 JSON** - 知道对象/数组如何序列化\n- **浏览器开发者工具** - 使用过 Chrome 检查器 (F12)\n\n**如果您是 Python 异步编程的新手**，请先阅读：[Real Python: Async IO in Python](https://realpython.com/async-io-python/)\n\n## 超越基础\n\n掌握了这些基础知识后，您就可以开始学习：\n\n- **[内部架构](../architecture/browser-domain.md)** - Pydoll 的组件是如何组合在一起的\n- **[网络与安全](../network/index.md)** - 理解代理所需的协议级知识\n- **[指纹识别](../fingerprinting/index.md)** - 需要 CDP 知识的检测技术\n\n## 常见问题\n\n### “我需要理解这些才能使用 Pydoll 吗？”\n\n**不需要**，但理解这些基础知识将使您更有效率。基本用法在没有这些知识的情况下也能正常工作。然而，当您需要：\n- 调试为什么某些功能不工作\n- 优化缓慢的自动化\n- 使用自定义 CDP 命令扩展 Pydoll\n- 理解错误消息\n- 为项目做贡献\n\n这些基础知识就会变得非常有用。\n\n### “这是不是太底层了？”\n\n这种详细程度是故意的。大多数框架隐藏了这些基础知识，但抽象是有代价的：\n\n- 理解有助于更好的调试\n- 可见性有助于优化\n- 知识有助于扩展\n\n通过教授基础知识，我们使您能够超越 Pydoll 开箱即用的功能。\n\n### “我需要记住多少内容？”\n\n**一点也不用。** 目标是建立心智模型，而不是记忆。阅读完这些部分后，您将培养出一种直觉：\n\n- “这需要 CDP，我去查查协议文档”\n- “这很慢是因为顺序 await，让我来并行化”\n- “这个类型错误意味着我用错了字段名”\n\n具体细节会淡忘，但理解会长存。\n\n## 理念\n\n这些基础知识代表了持久的知识：\n\n- **CDP** 是 Chrome 的原生协议，并持续演进\n- **Async/await** 是 Python 的并发标准\n- **类型系统** 在 Python 中变得越来越重要（PEP 484 以后）\n\n学习这些概念将为您的整个开发生涯提供价值。\n\n---\n\n## 准备好奠定您的基础了吗？\n\n从 **[Chrome 开发者工具协议](./cdp.md)** 开始，理解驱动一切的协议。然后逐步学习连接层和类型系统，以完善您的基础理解。\n\n**这就是自动化成为工程学的地方。**\n\n---\n\n!!! tip \"完成基础知识之后\"\n    一旦您掌握了这些概念，您会在 Pydoll 架构中 **无处不在** 地看到它们：\n    \n    - Browser/Tab/WebElement 都使用 **连接层**\n    - 网络事件都遵循 **CDP 的事件模型**\n    - 所有响应都使用 **TypedDict** 来确保类型安全\n    \n    基础知识与 Pydoll 并不分离，它们 **就是** Pydoll 的基石。"
  },
  {
    "path": "docs/zh/deep-dive/fundamentals/typing-system.md",
    "content": "# Python 的类型系统与 Pydoll\n\nPydoll 广泛利用 Python 的类型系统来提供出色的 IDE 支持、及早发现错误并使 API 自我记录。本指南将解释类型提示的基础知识，以及 Pydoll 如何使用它们来增强您的开发体验。\n\n## 类型提示基础\n\n类型提示是可选的注解，用于指定变量、参数或返回值应该是什么类型的值。它们不影响运行时行为，但能启用强大的工具。\n\n### 简单类型提示\n\n```python\n# 基本类型\nname: str = \"Pydoll\"\nport: int = 9222\nis_headless: bool = False\nquality: float = 0.85\n\n# 函数注解\ndef navigate(url: str, timeout: int = 30) -> bool:\n    # ... 实现\n    return True\n```\n\n### 容器类型\n\n```python\nfrom typing import List, Dict, Optional\n\n# 列表和字典\nurls: List[str] = ['https://example.com', 'https://google.com']\nheaders: Dict[str, str] = {'User-Agent': 'MyBot/1.0'}\n\n# 可选值 (可以是 None)\ntarget_id: Optional[str] = None\n\n# 现代语法 (Python 3.9+)\nurls: list[str] = ['https://example.com']\nheaders: dict[str, str] = {'User-Agent': 'MyBot/1.0'}\n```\n\n!!! tip \"Python 3.9+ 语法\"\n    Pydoll 的代码库使用较旧的 `List[]`、`Dict[]` 语法以实现向后兼容，但如果您使用的是 Python 3.9+，您可以在代码中使用小写的 `list[]`、`dict[]`。\n\n## TypedDict：结构化字典\n\nTypedDict 允许您定义具有特定键和值类型的字典结构。这在 Pydoll 的 CDP 协议定义中被 **大量使用**。\n\n### 基本 TypedDict\n\n```python\nfrom typing import TypedDict\n\nclass UserInfo(TypedDict):\n    name: str\n    age: int\n    email: str\n\n# IDE 完全知道存在哪些键\nuser: UserInfo = {\n    'name': 'Alice',\n    'age': 30,\n    'email': 'alice@example.com'\n}\n\n# 自动补全功能可用！\nprint(user['name'])  # IDE 建议: name, age, email\n```\n\n### Pydoll 如何使用 TypedDict\n\nPydoll 将 **每个 CDP 命令、响应和事件** 定义为 TypedDict。这意味着您的 IDE 完全知道哪些属性可用：\n\n```python\n# 来自 pydoll/protocol/page/methods.py\nclass CaptureScreenshotParams(TypedDict, total=False):\n    \"\"\"captureScreenshot 的参数。\"\"\"\n    format: ScreenshotFormat\n    quality: int\n    clip: Viewport\n    fromSurface: bool\n    captureBeyondViewport: bool\n    optimizeForSpeed: bool\n\nclass CaptureScreenshotResult(TypedDict):\n    \"\"\"captureScreenshot 命令的结果。\"\"\"\n    data: str\n```\n\n当您调用返回 CDP 响应的方法时，您的 IDE 会自动补全响应键：\n\n```python\nasync def example():\n    response = await tab.take_screenshot(as_base64=True)\n    \n    # IDE 知道这是 CaptureScreenshotResponse\n    # 并建议 'result' -> 'data'\n    screenshot_data = response['result']['data']  # 完整的自动补全！\n```\n\n### 可选字段与必选字段\n\nTypedDict 使用 `NotRequired[]` 支持可选字段：\n\n```python\nfrom typing import TypedDict, NotRequired\n\n# 来自 pydoll/protocol/network/methods.py\nclass GetCookiesParams(TypedDict):\n    \"\"\"用于检索浏览器 cookie 的参数。\"\"\"\n    urls: NotRequired[list[str]]  # 此字段是可选的\n```\n\n`total=False` 标志使 **所有** 字段都可选：\n\n```python\nclass CaptureScreenshotParams(TypedDict, total=False):\n    format: ScreenshotFormat  # 所有字段都可选\n    quality: int\n    clip: Viewport\n```\n\n!!! info \"自动补全的魔力\"\n    当您键入 `response['` 时，您的 IDE 会显示所有可用的键及其类型。这就是 TypedDict 的超能力在起作用！\n\n## Enums (枚举)：类型安全的常量\n\n枚举提供了类型安全的常量，您的 IDE 可以自动补全。Pydoll 广泛使用它们来表示 CDP 的值。\n\n### 基本枚举\n\n```python\nfrom enum import Enum\n\nclass ScreenshotFormat(str, Enum):\n    JPEG = 'jpeg'\n    PNG = 'png'\n    WEBP = 'webp'\n\n# IDE 自动补全可用的格式\nformat = ScreenshotFormat.PNG  # 类型是 ScreenshotFormat\nprint(format.value)  # 'png'\n```\n\n### Pydoll 的枚举用法\n\n```python\nfrom pydoll.constants import Key\nfrom pydoll.protocol.page.types import ScreenshotFormat\nfrom pydoll.protocol.input.types import KeyModifier\n\n# 查找元素 - 使用 kwargs，而非枚举\nelement = await tab.find(id='submit-btn')\nelement = await tab.find(class_name='btn-primary')\nelement = await tab.find(tag_name='button')\n\n# 键盘输入 - IDE 建议所有键\nawait element.press_keyboard_key(Key.ENTER)\nawait element.press_keyboard_key(Key.TAB)\nawait element.press_keyboard_key(Key.ESCAPE)\n\n# 修饰键是整数枚举 (用于特殊键)\nawait element.press_keyboard_key(Key.TAB, modifiers=KeyModifier.SHIFT)\n\n# 截图格式枚举\nawait tab.take_screenshot('file.webp', format=ScreenshotFormat.WEBP)\n```\n\n!!! tip \"枚举自动补全\"\n    键入 `Key.` 或 `ScreenshotFormat.`，您的 IDE 就会显示所有可用选项。再也不用记忆字符串了！\n\n## 函数重载 (Function Overloads)\n\n重载允许一个函数根据其参数返回不同的类型。Pydoll 使用它来提供精确的类型信息。\n\n### 基本重载示例\n\n```python\nfrom typing import overload\n\n# 重载签名 (不执行)\n@overload\ndef process(data: str) -> str: ...\n\n@overload\ndef process(data: int) -> int: ...\n\n# 实际实现\ndef process(data):\n    return data * 2\n\n# IDE 知道返回类型\nresult1 = process(\"hello\")  # 类型: str\nresult2 = process(42)       # 类型: int\n```\n\n### Pydoll 的重载用法\n\n`find()` 和 `query()` 方法根据 `find_all` 参数返回不同的类型：\n\n```python\n# 来自 pydoll/elements/mixins/find_elements_mixin.py\nclass FindElementsMixin:\n    @overload\n    async def find(\n        self, find_all: Literal[False] = False, **kwargs\n    ) -> WebElement: ...\n    \n    @overload\n    async def find(\n        self, find_all: Literal[True], **kwargs\n    ) -> list[WebElement]: ...\n    \n    async def find(\n        self, find_all: bool = False, **kwargs\n    ) -> Union[WebElement, list[WebElement]]:\n        # 实现...\n```\n\n在您的代码中：\n\n```python\n# find_all=False (默认) - IDE 知道返回类型是 WebElement\nbutton = await tab.find(id='submit-btn')\nawait button.click()  # 单个元素的方法可用！\n\n# find_all=True - IDE 知道返回类型是 list[WebElement]\nbuttons = await tab.find(class_name='btn', find_all=True)\nfor btn in buttons:  # IDE 知道这是一个列表！\n    await btn.click()\n\n# query() 也是如此\nelement = await tab.query('#submit-btn')  # 类型: WebElement\nelements = await tab.query('.btn', find_all=True)  # 类型: list[WebElement]\n```\n\n!!! tip \"智能类型推断\"\n    您的 IDE 会根据 `find_all` 参数自动知道您获取的是单个元素还是列表。无需类型转换或类型断言！\n\n## 泛型 (Generic Types)\n\n泛型就像“类型容器”，可以与不同类型一起工作，同时保留类型信息。可以把它们想象成能适应您放入任何东西的模板。\n\n### 理解泛型：一个简单的类比\n\n想象一个可以装任何东西的 `Box`。没有泛型：\n\n```python\n# 没有泛型 - IDE 不知道里面是什么\nclass Box:\n    def __init__(self, content):\n        self.content = content\n    \n    def get(self):\n        return self.content\n\nmy_box = Box(\"hello\")\nitem = my_box.get()  # 类型: Unknown - 可能是任何东西！\n```\n\n使用泛型：\n\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar('T')  # T 是一个 \"类型占位符\"\n\nclass Box(Generic[T]):\n    def __init__(self, content: T):\n        self.content = content\n    \n    def get(self) -> T:\n        return self.content\n\n# 现在 IDE 完全知道每个盒子里装的是什么\nstring_box: Box[str] = Box(\"hello\")\nitem1 = string_box.get()  # 类型: str\n\nnumber_box: Box[int] = Box(42)\nitem2 = number_box.get()  # 类型: int\n\n# List 是一个内置的泛型\nnumbers: list[int] = [1, 2, 3]  # 包含 int 的列表\nnames: list[str] = [\"Alice\", \"Bob\"]  # 包含 str 的列表\n```\n\n!!! tip \"泛型简化了类型提示\"\n    泛型让您只需编写一个可重用的 `list[T]`，它能适应您放入的任何东西，而无需为每种可能的列表类型编写 `Union[List[str], List[int], List[float], ...]`。\n\n### 现实世界中的泛型示例\n\n```python\nfrom typing import TypeVar, Generic\n\nT = TypeVar('T')\n\nclass Response(Generic[T]):\n    \"\"\"一个通用的 API 响应包装器。\"\"\"\n    def __init__(self, data: T, status: int):\n        self.data = data\n        self.status = status\n    \n    def get_data(self) -> T:\n        return self.data\n\n# 每个响应都保留了其数据类型\nuser_response: Response[dict] = Response({\"name\": \"Alice\"}, 200)\nuser_data = user_response.get_data()  # 类型: dict\n\ncount_response: Response[int] = Response(42, 200)\ncount = count_response.get_data()  # 类型: int\n```\n\n### Pydoll 如何使用泛型\n\nPydoll 的 CDP 命令系统使用泛型来确保响应类型与命令匹配：\n\n```python\n# 来自 pydoll/protocol/base.py\nfrom typing import Generic, TypeVar\n\nT_CommandParams = TypeVar('T_CommandParams')\nT_CommandResponse = TypeVar('T_CommandResponse')\n\nclass Command(TypedDict, Generic[T_CommandParams, T_CommandResponse]):\n    \"\"\"所有命令的基础结构。\"\"\"\n    id: NotRequired[int]\n    method: str\n    params: NotRequired[T_CommandParams]\n\nclass Response(TypedDict, Generic[T_CommandResponse]):\n    \"\"\"所有响应的基础结构。\"\"\"\n    id: int\n    result: T_CommandResponse\n```\n\n这意味着当您执行一个命令时，响应类型会被自动推断：\n\n```python\n# PageCommands.navigate 返回 Command[NavigateParams, NavigateResult]\ncommand = PageCommands.navigate('https://example.com')\n\n# ConnectionHandler.execute_command 保留了泛型类型\nresponse = await connection_handler.execute_command(command)\n\n# IDE 知道 response['result'] 是 NavigateResult (不仅仅是 \"any dict\")\nframe_id = response['result']['frameId']  # 自动补全可用！\nloader_id = response['result']['loaderId']  # 所有字段都已知！\n```\n\n!!! info \"为什么泛型在 Pydoll 中很重要\"\n    没有泛型，每个 CDP 响应的类型都只是 `dict[str, Any]`，您将失去所有的自动补全功能。有了泛型，IDE 能根据您发送的命令知道每个响应的确切结构。\n\n## 联合类型 (Union Types)\n\n联合 (Union) 表示值可能是多种类型之一：\n\n```python\nfrom typing import Union\n\n# 可以是字符串或整数\nidentifier: Union[str, int] = \"user-123\"\nidentifier = 456  # 也有效\n\n# 现代语法 (Python 3.10+)\nidentifier: str | int = \"user-123\"\n```\n\n### Pydoll 的联合类型用法\n\n```python\n# 文件路径可以是字符串或 Path 对象\nfrom pathlib import Path\n\nasync def upload_file(files: Union[str, Path, list[Union[str, Path]]]):\n    # 处理多种输入类型\n    pass\n\n# 所有这些都有效：\nawait tab.expect_file_chooser('/path/to/file.txt')\nawait tab.expect_file_chooser(Path('/path/to/file.txt'))\nawait tab.expect_file_chooser(['/file1.txt', Path('/file2.txt')])\n```\n\n## Pydoll 中的实际好处\n\n### 1. 智能自动补全\n\n您的 IDE 会建议可用的键、方法和值：\n\n```python\nfrom pydoll.protocol.page.events import PageEvent\nfrom pydoll.protocol.network.types import ResourceType\nfrom pydoll.protocol.input.types import KeyModifier\nfrom pydoll.constants import Key\n\n# 自动补全事件名称\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, callback)\nawait tab.on(PageEvent.JAVASCRIPT_DIALOG_OPENING, callback)\n\n# 自动补全资源类型\nawait tab.enable_fetch_events(resource_type=ResourceType.XHR)\nawait tab.enable_fetch_events(resource_type=ResourceType.DOCUMENT)\n\n# 自动补全按键\nawait element.press_keyboard_key(Key.ENTER)\nawait element.press_keyboard_key(Key.TAB, modifiers=KeyModifier.SHIFT)\n\n# 自动补全 find() 中的 kwargs\nelement = await tab.find(id='submit-btn')  # IDE 建议: id, class_name, tag_name, 等.\n```\n\n### 2. 及早发现错误\n\n像 mypy 或 Pylance 这样的类型检查器会在运行时之前捕获错误：\n\n```python\n# 类型检查器会捕获这个\nawait tab.take_screenshot('file.png', quality='high')  # 错误: quality 必须是 int\n\n# 类型检查器会捕获这个\nevent = await tab.find(id='button')\nawait tab.on(event, callback)  # 错误: event 是 WebElement, 不是 str\n\n# 正确的\nawait tab.take_screenshot('file.png', quality=90)\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, callback)\n```\n\n### 3. 自我记录的代码\n\n类型可作为内联文档：\n\n```python\n# 您立即知道每个参数期望什么\nasync def take_screenshot(\n    self,\n    path: Optional[str] = None,\n    quality: int = 100,\n    beyond_viewport: bool = False,\n    as_base64: bool = False,\n) -> Optional[str]:\n    pass\n```\n\n### 4. CDP 响应导航\n\n自信地浏览复杂的 CDP 响应：\n\n```python\n# 来自 pydoll/protocol/browser/methods.py\nclass GetVersionResult(TypedDict):\n    protocolVersion: str\n    product: str\n    revision: str\n    userAgent: str\n    jsVersion: str\n\n# 在您的代码中\nversion_info = await browser.get_version()\n\n# IDE 建议所有可用的键\nprint(version_info['product'])         # 自动补全！\nprint(version_info['userAgent'])       # 自动补全！\nprint(version_info['protocolVersion']) # 自动补全！\n```\n\n## 类型检查您的代码\n\n### 使用 Pylance (VS Code)\n\nPylance 在 VS Code 中提供实时类型检查：\n\n1.  安装 Pylance 扩展\n2.  在设置中设置类型检查模式：\n\n```json\n{\n    \"python.analysis.typeCheckingMode\": \"basic\"  // 或 \"strict\"\n}\n```\n\n现在您可以获得即时反馈：\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 当您键入时，Pylance 会显示参数类型\n        await tab.go_to('https://example.com', timeout=30)\n        \n        # Pylance 会对错误的类型发出警告\n        await tab.take_screenshot(quality='high')  # 警告！\n```\n\n### 使用 mypy\n\n运行 mypy 来检查您的整个项目：\n\n```bash\npip install mypy\nmypy your_script.py\n```\n\n示例输出：\n\n```\nyour_script.py:10: error: Argument \"quality\" to \"take_screenshot\" has incompatible type \"str\"; expected \"int\"\nFound 1 error in 1 file (checked 1 source file)\n```\n\n## Pydoll 的协议类型系统\n\nPydoll 的 `protocol/` 目录包含整个 Chrome DevTools 协议的全面类型定义：\n\n```\npydoll/protocol/\n├── base.py              # 泛型 Command, Response, CDPEvent 类型\n├── browser/\n│   ├── events.py        # BrowserEvent 枚举, 事件参数 TypedDicts\n│   ├── methods.py       # Browser 方法枚举, 参数/结果 TypedDicts\n│   └── types.py         # Browser 域类型 (Bounds, PermissionType, 等.)\n├── dom/\n│   ├── events.py        # DOM 事件定义\n│   ├── methods.py       # DOM 命令定义\n│   └── types.py         # DOM 类型 (Node, BackendNode, 等.)\n├── page/\n│   ├── events.py        # Page 事件 (LOAD_EVENT_FIRED, 等.)\n│   ├── methods.py       # Page 方法 (navigate, captureScreenshot, 等.)\n│   └── types.py         # Page 类型 (Frame, ScreenshotFormat, 等.)\n├── network/\n│   └── ...              # Network 域类型\n└── ...                  # 其他 CDP 域\n```\n\n### 示例：完整的类型流\n\n让我们追踪一个从命令到响应的完整类型流：\n\n```python\n# 1. 方法枚举 (protocol/page/methods.py)\nclass PageMethod(str, Enum):\n    CAPTURE_SCREENSHOT = 'Page.captureScreenshot'\n\n# 2. 参数 TypedDict (protocol/page/methods.py)\nclass CaptureScreenshotParams(TypedDict, total=False):\n    format: ScreenshotFormat\n    quality: int\n    clip: Viewport\n\n# 3. 结果 TypedDict (protocol/page/methods.py)\nclass CaptureScreenshotResult(TypedDict):\n    data: str\n\n# 4. 命令创建 (commands/page_commands.py)\nclass PageCommands:\n    @staticmethod\n    def capture_screenshot(\n        format: Optional[ScreenshotFormat] = None,\n        quality: Optional[int] = None,\n        ...\n    ) -> Command[CaptureScreenshotParams, CaptureScreenshotResult]:\n        return {\n            'method': PageMethod.CAPTURE_SCREENSHOT,\n            'params': {...}\n        }\n\n# 5. 在 Tab 中使用 (browser/tab.py)\nclass Tab:\n    async def take_screenshot(...) -> Optional[str]:\n        response: CaptureScreenshotResponse = await self._execute_command(\n            PageCommands.capture_screenshot(...)\n        )\n        screenshot_data = response['result']['data']  # 完全类型化！\n        return screenshot_data\n```\n\n每一步都保留了类型信息，让您在整个过程中都能获得自动补全和类型检查！\n\n## 最佳实践\n\n### 1. 让 Pydoll 的类型引导您\n\n不要抗拒类型，它们是来帮助您的：\n\n```python\n# 好的：使用 kwargs (IDE 自动补全参数名称)\nelement = await tab.find(id='submit-btn')\nbutton = await tab.find(class_name='btn-primary')\n\n# 好的：在适用的地方使用枚举\nfrom pydoll.constants import Key\nawait element.press_keyboard_key(Key.ENTER)\n\n# 避免：魔法字符串\nawait element.press_keyboard_key('Enter')  # 没有自动补全，容易出错\n```\n\n### 2. 在您的 IDE 中探索类型\n\n将鼠标悬停在变量上以查看其类型：\n\n```python\n# 悬停在 'response' 上查看: Response[CaptureScreenshotResult]\nresponse = await tab._execute_command(PageCommands.capture_screenshot(...))\n\n# 悬停在 'data' 上查看: str\ndata = response['result']['data']\n```\n\n\n### 3. 不要过度注解\n\nPython 的类型推断很智能，不要注解所有东西：\n\n```python\n# 过多\nname: str = \"Alice\"\ncount: int = 5\nis_active: bool = True\n\n# 让 Python 推断简单的字面量\nname = \"Alice\"\ncount = 5\nis_active = True\n\n# 当类型不明显时进行注解\nfrom typing import Optional\n\nresult: Optional[WebElement] = await tab.find(id='missing', raise_exc=False)\n```\n\n## 了解更多\n\n要更深入地了解 Python 的类型系统和 CDP 协议：\n\n- **[Python typing 文档](https://docs.python.org/3/library/typing.html)**：官方 Python 类型参考\n- **[PEP 484](https://peps.python.org/pep-0484/)**：原始的类型提示提案\n- **[Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)**：CDP 文档\n- **[深入探讨：CDP](./cdp.md)**：Pydoll 如何实现 CDP\n- **[API 参考：Protocol](../api/protocol/base.md)**：Pydoll 的协议类型定义\n\n类型系统将 Pydoll 从一个简单的自动化库转变为一个 **类型安全、自我记录、IDE 友好** 的框架。它能在错误发生之前捕获它们，并使探索 API 变得轻而易举！"
  },
  {
    "path": "docs/zh/deep-dive/guides/index.md",
    "content": "# 实用指南\n\n**理论与实践相结合，为应对真实自动化挑战提供可行的模式。**\n\n“深度探讨”的其他部分探讨了 **基础知识** 和 **架构**，而本节则为常见的自动化场景提供了 **经过实战检验的实用指南**。这些不是学术练习，而是通过生产使用提炼出来的模式。\n\n## 指南的目的\n\n您已经学过：\n- **[基础知识](../fundamentals/cdp.md)** - CDP、异步、类型\n- **[架构](../architecture/browser-domain.md)** - 内部设计模式\n- **[网络](../network/index.md)** - 协议和代理\n- **[指纹识别](../fingerprinting/index.md)** - 检测与规避\n\n那么现在呢？**您如何将这些知识应用于实际问题？**\n\n这就是指南的作用：**连接理论与实践**。\n\n!!! quote \"实践智慧\"\n    **“理论上，理论和实践是一样的。实践中，它们并非如此。”** - 瑜伽·贝拉 (Yogi Berra)\n    \n    指南将复杂的技术知识提炼为您可以立即使用的 **可操作模式**。它们向您展示了在生产中 **哪些方法有效**，而不仅仅是理论上可能的东西。\n\n## 当前指南\n\n### CSS 选择器 vs XPath\n**[→ 阅读选择器指南](./selectors-guide.md)**\n\n**用数据和最佳实践解决永恒的辩论。**\n\n在 CSS 选择器和 XPath 之间做出选择，无关偏好。关键在于理解 **权衡**、**性能特征** 和 **可维护性**。\n\n**您将学到什么**：\n\n- **语法比较** - 常见模式的并排示例\n- **性能基准** - 真实的测量数据，而非神话\n- **强大功能 vs 简洁性** - 当 CSS 不够用时（文本匹配、轴）\n- **浏览器支持** - 兼容性和边缘情况\n- **最佳实践** - 何时使用哪种，应避免的反模式\n- **复杂示例** - 解决真实世界的选择器挑战\n\n**为什么这很重要**：元素定位是自动化的 **基础**。选错工具，您将永远与选择器作斗争。明智地选择，自动化将变得简单明了。\n\n---\n\n## 即将推出\n\n### Asyncio 与并发自动化\n**将在未来版本中推出**\n\n**深入探讨 Python 的 asyncio：事件循环内部原理、实用的并发模式以及真实世界的示例。**\n\n理解 asyncio 对 Pydoll 至关重要。本指南将全面分析 Python 的事件循环、并发原语，以及如何将它们应用于浏览器自动化而避免陷阱。\n\n**将涵盖**：\n\n- **事件循环内部原理**：`asyncio.run()` 是如何工作的、任务调度和执行流程\n- **Async/Await 深入探讨**：协程、future 和异步状态机\n- **并发原语**：`gather()`、`create_task()`、`TaskGroup` 以及何时使用它们\n- **速率限制**：信号量、队列和节流策略\n- **真实世界示例**：多标签页抓取、并行表单填充、协调的浏览器实例\n- **常见陷阱**：阻塞事件循环、任务取消、异常传播\n- **性能分析**：分析异步代码、识别瓶颈、优化 I/O\n\n**为什么这很重要**：Asyncio 是 Pydoll 架构的动力源泉。掌握它，您就能在没有竞争条件或状态损坏的情况下实现真正的并发自动化。\n\n---\n\n### 架构模式与健壮的选择器\n**将在未来版本中推出**\n\n**PageObject 模式、可维护的选择器以及用于可扩展自动化的架构方法。**\n\n从临时脚本转向结构化、可维护的自动化架构。学习可从简单脚本扩展到生产系统的模式。\n\n**将涵盖**：\n\n- **PageObject 模式**：封装页面结构、减少重复、提高可维护性\n- **健壮的选择器策略**：构建能在页面变更后幸存的选择器，避免脆弱的定位器\n- **组件抽象**：用于常见 UI 模式（模态框、下拉菜单、表格）的可重用组件\n- **等待策略**：超越简单超时的智能等待模式\n- **状态管理**：跨页面和流程管理自动化状态\n- **测试模式**：如何构建易于测试的自动化代码\n- **真实世界架构**：可用于生产的项目结构和组织\n\n**为什么这很重要**：临时脚本和可维护自动化系统之间的区别在于架构。学习使您的代码能够适应变化的模式。\n\n---\n\n## 指南的理念\n\n指南遵循一致的原则：\n\n### 1. 可用于生产的代码\n所有示例都是 **完整且经过测试的**，而不是伪代码或简化的演示。您可以复制粘贴并根据需要进行调整。\n\n### 2. 真实世界的场景\n指南解决的是在生产自动化中遇到的 **实际问题**，而不是虚构的例子。\n\n### 3. 权衡分析\n当存在多种方法时，指南会客观地 **比较** 它们，并提供优缺点，而不仅仅是“这是一种方法”。\n\n### 4. 渐进的复杂性\n从简单开始，逐步增加复杂性。首先是基本模式，然后是边缘情况和高级变体。\n\n### 5. 突出显示反模式\n明确展示 **不该做什么**，以及通过代码审查或生产调试发现的常见错误。\n\n## 如何使用指南\n\n指南是 **参考材料**，而不是按顺序学习的教程：\n\n- **浏览** 与您当前问题相关的模式\n- **收藏** 您需要重复使用的指南\n- **调整** 示例以适应您的特定情境\n- **组合** 来自多个指南的模式\n\n不要按顺序从头到尾阅读。\n不要在不理解权衡的情况下盲目复制。\n不要使用过时的模式（请检查发布日期）。\n\n## 贡献指南\n\n有值得分享的模式吗？指南是 **由社区驱动的**：\n\n**怎样才是一篇好的指南**：\n\n- 解决了在生产中遇到的 **实际问题**\n- 提供了 **可工作的代码**，而不仅仅是概念\n- 比较了 **多种方法** 并进行了权衡\n- 明确指出了 **常见错误**\n- 解释了 **为什么**，而不仅仅是 **怎么做**\n\n有关提交指南，请参阅 [贡献](../../CONTRIBUTING.md)。\n\n## 指南 vs 功能文档\n\n**对两者的区别感到困惑吗？**\n\n|| 功能文档 | 深度探讨指南 |\n|---|---|---|\n| **目的** | 教授 Pydoll 能做什么 | 展示如何解决问题 |\n| **范围** | 单个方法/功能 | 多个功能组合 |\n| **深度** | API 参考 + 示例 | 模式 + 权衡 + 最佳实践 |\n| **顺序** | 按组件构建 | 按问题构建 |\n| **示例** | 简单、独立 | 复杂、可用于生产 |\n\n**使用功能文档**：学习 Pydoll 的 API\n**使用指南**：解决真实的自动化挑战\n\n## 超越指南\n\n掌握了实用模式之后：\n\n- **[架构](../architecture/browser-domain.md)** - 理解模式为何有效\n- **[网络](../network/index.md)** - 网络层面的优化\n- **[指纹识别](../fingerprinting/evasion-techniques.md)** - 反检测技术\n\n指南提供 **直接价值**。架构提供 **深刻理解**。两者都能让您变得高效。\n\n---\n\n## 准备好学习实用模式了吗？\n\n从 **[CSS 选择器 vs XPath](./selectors-guide.md)** 开始，掌握元素定位——这是所有自动化的基础。\n\n**更多指南即将推出。请给本仓库加星以保持更新！**\n\n---\n\n!!! tip \"请求一篇指南\"\n    您有什么希望被记录下来的自动化模式吗？请提交一个标题为“指南请求：[主题]”的 issue，描述：\n    \n    - 您试图解决的问题\n    - 您到目前为止尝试了什么\n    - 为什么现有文档没有涵盖它\n    \n    我们将根据社区需求优先安排指南。\n\n## 快速参考\n\n**现已推出：**\n- [CSS 选择器 vs XPath](./selectors-guide.md)\n\n**即将推出：**\n- Asyncio 与并发自动化\n- 架构模式与健壮的选择器\n\n**时间表**：根据社区反馈和生产经验添加新指南。"
  },
  {
    "path": "docs/zh/deep-dive/guides/selectors-guide.md",
    "content": "# CSS 选择器 vs XPath：完整指南\n\n使用 `query()` 方法时，您有两种强大的选择器语言可供选择：CSS 选择器和 XPath。了解何时以及如何使用每种语言对于有效的元素定位至关重要。\n\n## 根本差异\n\n| 方面 | CSS 选择器 | XPath |\n|---|---|---|\n| **语法** | 简单，类似 CSS | XML 路径语言 |\n| **性能** | 更快 (浏览器原生支持) | 稍慢 |\n| **方向** | 只能向下和横向遍历 | 可以向任何方向遍历 |\n| **文本匹配** | 有限 (伪选择器) | 强大的文本函数 |\n| **复杂性** | 最适合简单到中等的情况 | 擅长处理复杂关系 |\n| **可读性** | Web 开发人员更直观 | 学习曲线更陡峭 |\n\n## 何时使用 CSS 选择器\n\nCSS 选择器是以下情况的理想选择：\n\n- 通过 ID、类或标签进行简单的元素选择\n- 直接的父子关系\n- 具有简单模式的属性匹配\n- 对性能要求严格的场景\n- 在 DOM 中向下遍历时\n\n```python\n# 简洁高效的 CSS 示例\nawait tab.query(\"#login-form\")\nawait tab.query(\".submit-button\")\nawait tab.query(\"div.container > p.intro\")\nawait tab.query(\"input[type='email'][required]\")\nawait tab.query(\"ul.menu li:first-child\")\n```\n\n## 何时使用 XPath\n\nXPath 是以下情况的理想选择：\n\n- 复杂的文本匹配和部分文本搜索\n- 向上遍历到父元素\n- 查找相对于兄弟元素的元素\n- 选择器中的条件逻辑\n- 复杂的 DOM 关系\n\n```python\n# 强大的 XPath 示例\nawait tab.query(\"//button[contains(text(), 'Submit')]\")\nawait tab.query(\"//input[@name='email']/parent::div\")\nawait tab.query(\"//td[text()='John']/following-sibling::td[2]\")\nawait tab.query(\"//div[contains(@class, 'product') and @data-price > 100]\")\n```\n\n## CSS 选择器语法参考\n\n### 基本选择器\n\n```python\n# 元素选择器\nawait tab.query(\"div\")              # 第一个 <div> 元素\nawait tab.query(\"div\", find_all=True)  # 所有 <div> 元素\nawait tab.query(\"button\")           # 第一个 <button> 元素\n\n# ID 选择器\nawait tab.query(\"#username\")        # id=\"username\" 的元素\n\n# 类选择器\nawait tab.query(\".submit-btn\")      # 第一个 class=\"submit-btn\" 的元素\nawait tab.query(\".submit-btn\", find_all=True)  # 所有带该类的元素\nawait tab.query(\".btn.primary\")     # 第一个同时具有这两个类的元素\n\n# 通用选择器\nawait tab.query(\"*\", find_all=True) # 所有元素\n```\n\n### 组合器\n\n```python\n# 后代组合器 (空格)\nawait tab.query(\"div p\")            # <div> 内的第一个 <p>\nawait tab.query(\"div p\", find_all=True)  # <div> 内的所有 <p> (任何深度)\n\n# 子组合器 (>)\nawait tab.query(\"div > p\")          # <div> 直接子元素中的第一个 <p>\nawait tab.query(\"div > p\", find_all=True)  # 所有作为直接子元素的 <p>\n\n# 相邻兄弟组合器 (+)\nawait tab.query(\"h1 + p\")           # 紧跟 <h1> 后的 <p>\n\n# 通用兄弟组合器 (~)\nawait tab.query(\"h1 ~ p\")           # <h1> 后的第一个 <p> 兄弟元素\nawait tab.query(\"h1 ~ p\", find_all=True)  # <h1> 后的所有 <p> 兄弟元素\n```\n\n### 属性选择器\n\n```python\n# 属性存在\nawait tab.query(\"input[required]\")                # 第一个带 'required' 的 input\nawait tab.query(\"input[required]\", find_all=True) # 所有带 'required' 的 input\n\n# 属性等于\nawait tab.query(\"input[type='email']\")            # 第一个 email input\nawait tab.query(\"input[type='email']\", find_all=True)  # 所有 email input\n\n# 属性包含单词\nawait tab.query(\"div[class~='active']\")           # 第一个 class 中包含 'active' 的 div\n\n# 属性以...开头\nawait tab.query(\"a[href^='https://']\")            # 第一个 HTTPS 链接\nawait tab.query(\"a[href^='https://']\", find_all=True)  # 所有 HTTPS 链接\n\n# 属性以...结尾\nawait tab.query(\"img[src$='.png']\")               # 第一个 PNG 图像\nawait tab.query(\"img[src$='.png']\", find_all=True)     # 所有 PNG 图像\n\n# 属性包含子字符串\nawait tab.query(\"a[href*='example']\")             # 第一个 href 中包含 'example' 的链接\nawait tab.query(\"a[href*='example']\", find_all=True)   # 所有 href 中包含 'example' 的链接\n\n# 不区分大小写匹配\nawait tab.query(\"input[type='text' i]\")           # 不区分大小写匹配\n```\n\n### 伪类\n\n```python\n# 结构伪类\nawait tab.query(\"li:first-child\")                 # 作为第一个子元素的第一个 <li>\nawait tab.query(\"li:last-child\")                  # 作为最后一个子元素的第一个 <li>\nawait tab.query(\"li:nth-child(2)\")                # 作为第二个子元素的第一个 <li>\nawait tab.query(\"li:nth-child(odd)\", find_all=True)  # 所有奇数位置的 <li>\nawait tab.query(\"li:nth-child(even)\", find_all=True)  # 所有偶数位置的 <li>\nawait tab.query(\"li:nth-child(3n)\", find_all=True)    # 每第 3 个 <li>\n\n# 类型伪类\nawait tab.query(\"p:first-of-type\")                # 兄弟元素中的第一个 <p>\nawait tab.query(\"p:last-of-type\")                 # 兄弟元素中的最后一个 <p>\nawait tab.query(\"p:nth-of-type(2)\")               # 兄弟元素中的第二个 <p>\n\n# 状态伪类\nawait tab.query(\"input:enabled\")                  # 第一个启用的 input\nawait tab.query(\"input:enabled\", find_all=True)   # 所有启用的 input\nawait tab.query(\"input:disabled\")                 # 第一个禁用的 input\nawait tab.query(\"input:checked\")                  # 第一个选中的 checkbox/radio\nawait tab.query(\"input:focus\")                    # 当前获得焦点的 input\n\n# 其他有用的伪类\nawait tab.query(\"div:empty\")                      # 第一个空元素\nawait tab.query(\"div:empty\", find_all=True)       # 所有空元素\nawait tab.query(\"div:not(.exclude)\")              # 第一个没有 'exclude' 类的 div\nawait tab.query(\"div:not(.exclude)\", find_all=True)  # 所有没有 'exclude' 类的 div\n```\n\n## XPath 语法参考\n\n### 基本路径表达式\n\n```python\n# 绝对路径 (从根开始)\nawait tab.query(\"/html/body/div\")                 # 处于该精确路径的第一个 div\n\n# 相对路径 (从任何地方开始)\nawait tab.query(\"//div\")                          # 第一个 <div> 元素\nawait tab.query(\"//div\", find_all=True)           # 所有 <div> 元素\nawait tab.query(\"//div/p\")                        # 任何 <div> 内的第一个 <p>\nawait tab.query(\"//div/p\", find_all=True)         # 任何 <div> 内的所有 <p>\n\n# 当前节点\nawait tab.query(\"./div\")                          # 相对于当前的第一个 <div>\n\n# 父节点\nawait tab.query(\"..\")                             # 当前节点的父节点\n```\n\n### 属性选择\n\n```python\n# 基本属性匹配\nawait tab.query(\"//input[@type='email']\")         # 第一个 email input\nawait tab.query(\"//input[@type='email']\", find_all=True)  # 所有 email input\nawait tab.query(\"//div[@id='content']\")           # id='content' 的 div\n\n# 多个属性\nawait tab.query(\"//input[@type='text' and @required]\")  # 第一个匹配项\nawait tab.query(\"//input[@type='text' and @required]\", find_all=True)  # 所有匹配项\nawait tab.query(\"//div[@class='card' or @class='panel']\")  # 第一个 card 或 panel\n\n# 属性存在\nawait tab.query(\"//button[@disabled]\")            # 第一个 disabled button\nawait tab.query(\"//button[@disabled]\", find_all=True)  # 所有 disabled button\n```\n\n## XPath 轴 (方向导航)\n\nXPath 的真正威力来自于它能够在 DOM 树中向任何方向导航。\n\n### 轴参考表\n\n| 轴 | 方向 | 描述 | 示例 |\n|---|---|---|---|\n| `child::` | 向下 | 仅直接子元素 | `//div/child::p` |\n| `descendant::` | 向下 | 所有后代 (任何深度) | `//div/descendant::a` |\n| `parent::` | 向上 | 直接父元素 | `//input/parent::div` |\n| `ancestor::` | 向上 | 所有祖先 (任何深度) | `//span/ancestor::div` |\n| `following-sibling::` | 横向 | 当前元素之后的所有兄弟元素 | `//h1/following-sibling::p` |\n| `preceding-sibling::` | 横向 | 当前元素之前的所有兄弟元素 | `//p/preceding-sibling::h1` |\n| `following::` | 向前 | 当前节点之后的所有节点 | `//h1/following::*` |\n| `preceding::` | 向后 | 当前节点之前的所有节点 | `//h1/preceding::*` |\n| `ancestor-or-self::` | 向上 | 祖先 + 当前节点 | `//div/ancestor-or-self::*` |\n| `descendant-or-self::` | 向下 | 后代 + 当前节点 | `//div/descendant-or-self::*` |\n| `self::` | 当前 | 仅当前节点 | `//div/self::div` |\n| `attribute::` | 属性 | 当前节点的属性 | `//div/attribute::class` |\n\n!!! info \"简写语法\"\n    - `//div` 是 `//descendant-or-self::div` 的简写\n    - `//div/p` 是 `//div/child::p` 的简写\n    - `@id` 是 `attribute::id` 的简写\n    - `..` 是 `parent::node()` 的简写\n\n### 实用轴示例\n\n```python\n# 导航到父元素\nawait tab.query(\"//input[@name='email']/parent::div\")\nawait tab.query(\"//span[@class='error']/..\")       # 简写\n\n# 查找祖先元素\nawait tab.query(\"//input/ancestor::form\")          # 第一个祖先 <form>\nawait tab.query(\"//button/ancestor::div[@class='modal']\")\n\n# 兄弟元素导航\nawait tab.query(\"//label[text()='Email:']/following-sibling::input\")\nawait tab.query(\"//h2/following-sibling::p[1]\")    # <h2> 后的第一个 <p>\nawait tab.query(\"//h2/following-sibling::p\", find_all=True)  # <h2> 后的所有 <p>\nawait tab.query(\"//button/preceding-sibling::input[last()]\")\n\n# 复杂关系\nawait tab.query(\"//tr/td[1]/following-sibling::td[2]\")  # 第一行中的第 3 个单元格\nawait tab.query(\"//tr/td[1]/following-sibling::td[2]\", find_all=True)  # 所有行中的第 3 个单元格\n```\n\n## XPath 函数\n\n### 文本函数\n\n```python\n# 精确文本匹配\nawait tab.query(\"//button[text()='Submit']\")\n\n# 包含文本\nawait tab.query(\"//p[contains(text(), 'welcome')]\")\n\n# 以...开头\nawait tab.query(\"//a[starts-with(@href, 'https://')]\")\n\n# 文本规范化 (移除多余的空白)\nawait tab.query(\"//button[normalize-space(text())='Submit']\")\n\n# 字符串长度\nawait tab.query(\"//input[string-length(@value) > 5]\")\n\n# 字符串连接\nawait tab.query(\"//div[concat(@data-first, @data-last)='JohnDoe']\")\n```\n\n### 数字函数\n\n```python\n# 位置匹配\nawait tab.query(\"//li[position()=1]\")              # 第一个 <li>\nawait tab.query(\"//li[position() > 3]\", find_all=True)  # 第 3 个之后的所有 <li>\nawait tab.query(\"//li[last()]\")                    # 最后一个 <li>\nawait tab.query(\"//li[last()-1]\")                  # 倒数第二个\n\n# 计数\nawait tab.query(\"//ul[count(li) > 5]\")             # 第一个包含超过 5 个 li 的 <ul>\nawait tab.query(\"//ul[count(li) > 5]\", find_all=True)  # 所有包含超过 5 个 li 的 <ul>\n\n# 数值运算\nawait tab.query(\"//div[@data-price > 100]\")        # 第一个 price > 100 的 div\nawait tab.query(\"//div[@data-price > 100]\", find_all=True)  # 所有\nawait tab.query(\"//div[number(@data-stock) = 0]\")  # 第一个 stock = 0 的\n```\n\n### 布尔函数\n\n```python\n# 布尔逻辑\nawait tab.query(\"//div[@visible='true' and @enabled='true']\")  # 第一个匹配项\nawait tab.query(\"//input[@type='text' or @type='email']\")  # 第一个 text 或 email\nawait tab.query(\"//input[@type='text' or @type='email']\", find_all=True)  # 所有\nawait tab.query(\"//button[not(@disabled)]\")        # 第一个启用的 button\nawait tab.query(\"//button[not(@disabled)]\", find_all=True)  # 所有启用的 button\n\n# 存在性检查\nawait tab.query(\"//div[child::p]\")                 # 第一个有 <p> 子元素的 div\nawait tab.query(\"//div[child::p]\", find_all=True)  # 所有有 <p> 子元素的 div\nawait tab.query(\"//div[not(child::*)]\")            # 第一个空 div\nawait tab.query(\"//div[not(child::*)]\", find_all=True)  # 所有空 div\n```\n\n## XPath 谓词 (Predicates)\n\n谓词使用方括号 `[]` 中的条件来过滤节点集。\n\n```python\n# 位置谓词\nawait tab.query(\"(//div)[1]\")                      # 文档中的第一个 <div>\nawait tab.query(\"(//div)[last()]\")                 # 文档中的最后一个 <div>\nawait tab.query(\"//ul/li[3]\")                      # <ul> 中的第一个第 3 个 <li>\nawait tab.query(\"//ul/li[3]\", find_all=True)       # 每个 <ul> 中的所有第 3 个 <li>\n\n# 多个谓词 (AND 逻辑)\nawait tab.query(\"//input[@type='text'][@required]\")  # 第一个匹配项\nawait tab.query(\"//div[@class='product'][position() < 4]\", find_all=True)  # 前 3 个\n\n# 属性谓词\nawait tab.query(\"//div[@data-id='123']\")\nawait tab.query(\"//a[contains(@class, 'button')]\")  # 第一个匹配的链接\nawait tab.query(\"//input[starts-with(@name, 'user')]\")  # 第一个匹配的 input\n```\n\n## 真实世界示例：复杂元素查找\n\n让我们使用一个真实的 HTML 结构来演示高级选择器。\n\n### 示例 HTML 结构\n\n```html\n<div class=\"dashboard\">\n    <header>\n        <h1>User Dashboard</h1>\n        <nav class=\"menu\">\n            <a href=\"/home\" class=\"active\">Home</a>\n            <a href=\"/profile\">Profile</a>\n            <a href=\"/settings\">Settings</a>\n        </nav>\n    </header>\n    \n    <main>\n        <section class=\"products\">\n            <h2>Available Products</h2>\n            <table id=\"products-table\">\n                <thead>\n                    <tr>\n                        <th>Product Name</th>\n                        <th>Price</th>\n                        <th>Stock</th>\n                        <th>Actions</th>\n                    </tr>\n                </thead>\n                <tbody>\n                    <tr data-product-id=\"101\">\n                        <td>Laptop</td>\n                        <td class=\"price\">$999</td>\n                        <td class=\"stock\">15</td>\n                        <td>\n                            <button class=\"btn-edit\">Edit</button>\n                            <button class=\"btn-delete\">Delete</button>\n                        </td>\n                    </tr>\n                    <tr data-product-id=\"102\">\n                        <td>Mouse</td>\n                        <td class=\"price\">$25</td>\n                        <td class=\"stock\">0</td>\n                        <td>\n                            <button class=\"btn-edit\">Edit</button>\n                            <button class=\"btn-delete\" disabled>Delete</button>\n                        </td>\n                    </tr>\n                    <tr data-product-id=\"103\">\n                        <td>Keyboard</td>\n                        <td class=\"price\">$75</td>\n                        <td class=\"stock\">8</td>\n                        <td>\n                            <button class=\"btn-edit\">Edit</button>\n                            <button class=\"btn-delete\">Delete</button>\n                        </td>\n                    </tr>\n                </tbody>\n            </table>\n        </section>\n        \n        <section class=\"user-form\">\n            <h2>User Information</h2>\n            <form id=\"user-form\">\n                <div class=\"form-group\">\n                    <label for=\"username\">Username:</label>\n                    <input type=\"text\" id=\"username\" name=\"username\" required>\n                    <span class=\"error-message\" style=\"display:none;\">Invalid username</span>\n                </div>\n                <div class=\"form-group\">\n                    <label for=\"email\">Email:</label>\n                    <input type=\"email\" id=\"email\" name=\"email\" required>\n                    <span class=\"error-message\" style=\"display:none;\">Invalid email</span>\n                </div>\n                <div class=\"form-group\">\n                    <input type=\"checkbox\" id=\"newsletter\" name=\"newsletter\">\n                    <label for=\"newsletter\">Subscribe to newsletter</label>\n                </div>\n                <button type=\"submit\" class=\"btn-primary\">Save Changes</button>\n                <button type=\"button\" class=\"btn-secondary\">Cancel</button>\n            </form>\n        </section>\n    </main>\n</div>\n```\n\n### 挑战 1：查找活动的导航链接\n\n**目标**：找到当前活动的导航链接。\n\n```python\n# CSS 选择器\nactive_link = await tab.query(\"nav.menu a.active\")\n\n# XPath\nactive_link = await tab.query(\"//nav[@class='menu']//a[@class='active']\")\n\n# 获取其文本\ntext = await active_link.text\nprint(text)  # \"Home\"\n```\n\n### 挑战 2：查找特定产品的编辑按钮\n\n**目标**：找到产品 \"Mouse\" 的编辑按钮 (不知道其行位置)。\n\n```python\n# XPath (推荐用于此情况)\nedit_button = await tab.query(\n    \"//tr[td[text()='Mouse']]//button[contains(@class, 'btn-edit')]\"\n)\n\n# 备选方案：使用 following-sibling\nedit_button = await tab.query(\n    \"//td[text()='Mouse']/following-sibling::td//button[@class='btn-edit']\"\n)\n```\n\n!!! tip \"为什么这里使用 XPath？\"\n    CSS 选择器无法向上遍历找到行，然后再向下找到按钮。XPath 在 DOM 中自由移动的能力使这变得微不足道。\n\n### 挑战 3：查找所有价格超过 $50 的产品\n\n**目标**：获取价格大于 $50 的所有表格行。\n\n```python\n# 带有数值比较的 XPath\nexpensive_products = await tab.query(\n    \"//tr[number(translate(td[@class='price'], '$,', '')) > 50]\",\n    find_all=True\n)\n\n# 更易读的版本：对于更简单的情况使用 contains\n# 这会查找价格包含特定金额的产品\nproducts = await tab.query(\"//tr[contains(td[@class='price'], '$75')]\", find_all=True)\n```\n\n!!! note \"文本到数字的转换\"\n    `translate()` 函数移除了 `$` 和 `,` 字符，然后 `number()` 将其转换为数值进行比较。\n\n### 挑战 4：查找所有缺货产品\n\n**目标**：找到所有库存为 0 的产品。\n\n```python\n# XPath\nout_of_stock = await tab.query(\n    \"//tr[td[@class='stock' and text()='0']]\",\n    find_all=True\n)\n\n# 备选方案：查找所有行并检查库存\nrows = await tab.query(\"//tbody/tr[td[@class='stock']/text()='0']\", find_all=True)\n```\n\n### 挑战 5：通过标签查找输入字段\n\n**目标**：首先定位其标签，然后找到 email 输入字段。\n\n```python\n# XPath 使用 label 的 'for' 属性\nemail_input = await tab.query(\"//label[text()='Email:']/following-sibling::input\")\n\n# 备选方案：使用 for 属性\nemail_input = await tab.query(\"//input[@id=(//label[text()='Email:']/@for)]\")\n\n# 更通用的：按标签文本查找\nusername_input = await tab.query(\n    \"//label[contains(text(), 'Username')]/following-sibling::input\"\n)\n```\n\n### 挑战 6：查找 Email 字段旁的错误消息\n\n**目标**：获取出现在 email 输入字段旁边的错误消息 span。\n\n```python\n# XPath - 查找 email input 的错误兄弟元素\nerror_span = await tab.query(\n    \"//input[@id='email']/following-sibling::span[@class='error-message']\"\n)\n\n# 备选方案：从父 div 导航\nerror_span = await tab.query(\n    \"//input[@id='email']/parent::div//span[@class='error-message']\"\n)\n\n# 检查可见性\nis_visible = await error_span.is_visible()\n```\n\n### 挑战 7：查找提交按钮 (而不是取消按钮)\n\n**目标**：找到提交按钮，排除取消按钮。\n\n```python\n# CSS 选择器 (简单)\nsubmit_button = await tab.query(\"button[type='submit']\")\nsubmit_button = await tab.query(\"button.btn-primary\")\n\n# 带文本的 XPath\nsubmit_button = await tab.query(\"//button[text()='Save Changes']\")\n\n# 排除其他的 XPath\nsubmit_button = await tab.query(\n    \"//button[@type='submit' and not(@class='btn-secondary')]\"\n)\n```\n\n### 挑战 8：查找所有必填的表单字段\n\n**目标**：获取表单中所有必填的 input 字段。\n\n```python\n# CSS 选择器 (简洁)\nrequired_fields = await tab.query(\n    \"#user-form input[required]\",\n    find_all=True\n)\n\n# XPath\nrequired_fields = await tab.query(\n    \"//form[@id='user-form']//input[@required]\",\n    find_all=True\n)\n\n# 验证\nfor field in required_fields:\n    field_name = await field.get_attribute(\"name\")\n    print(f\"Required: {field_name}\")\n```\n\n### 挑战 9：查找第一个未禁用的删除按钮\n\n**目标**：找到第一个未被禁用的删除按钮。\n\n```python\n# CSS 选择器\nfirst_enabled_delete = await tab.query(\"button.btn-delete:not([disabled])\")\n\n# XPath\nfirst_enabled_delete = await tab.query(\n    \"//button[contains(@class, 'btn-delete') and not(@disabled)]\"\n)\n\n# 获取所有启用的删除按钮\nall_enabled = await tab.query(\n    \"//button[@class='btn-delete' and not(@disabled)]\",\n    find_all=True\n)\n```\n\n### 挑战 10：按多个条件查找表格行\n\n**目标**：查找库存 > 0 且价格 < $100 的产品。\n\n```python\n# 具有复杂逻辑的 XPath\navailable_affordable = await tab.query(\n    \"\"\"\n    //tr[\n        number(td[@class='stock']) > 0 \n        and \n        number(translate(td[@class='price'], '$', '')) < 100\n    ]\n    \"\"\",\n    find_all=True\n)\n\n# 对于每个匹配的产品\nfor row in available_affordable:\n    cells = await row.query(\"td\", find_all=True)\n    product_name = await cells[0].text\n    print(f\"Available: {product_name}\")\n```\n\n### 挑战 11：导航复杂关系\n\n**目标**：从删除按钮获取同一行中的产品名称。\n\n```python\n# 从删除按钮开始\ndelete_button = await tab.query(\"//tr[@data-product-id='101']//button[@class='btn-delete']\")\n\n# 导航到父行，然后到第一个单元格\nproduct_name_cell = await delete_button.query(\"./ancestor::tr/td[1]\")\nproduct_name = await product_name_cell.text\nprint(product_name)  # \"Laptop\"\n\n# 备选方案：首先获取整行\nrow = await delete_button.query(\"./ancestor::tr\")\nproduct_id = await row.get_attribute(\"data-product-id\")\nprint(product_id)  # \"101\"\n```\n\n### 挑战 12：同时查找复选框及其标签\n\n**目标**：找到 newsletter 复选框并验证其标签。\n\n```python\n# 查找复选框\ncheckbox = await tab.query(\"#newsletter\")\n\n# 使用 'for' 属性获取关联的标签\nlabel = await tab.query(\"//label[@for='newsletter']\")\nlabel_text = await label.text\nprint(label_text)  # \"Subscribe to newsletter\"\n\n# 备选方案：从复选框导航到标签\nlabel = await checkbox.query(\"//following::label[@for='newsletter']\")\n\n# 检查是否选中\nis_checked = await checkbox.is_checked()\n```\n\n## 高级模式：动态构建选择器\n\n处理动态内容时，您可能需要以编程方式构建选择器：\n\n```python\nasync def find_product_by_name(tab, product_name: str):\n    \"\"\"通过名称动态查找产品行。\"\"\"\n    # 转义产品名称中的引号以防止 XPath 注入\n    safe_name = product_name.replace(\"'\", \"\\\\'\")\n    \n    xpath = f\"//tr[td[text()='{safe_name}']]\"\n    return await tab.query(xpath)\n\nasync def find_table_cell(tab, row_text: str, column_index: int):\n    \"\"\"通过行内容和列位置查找特定单元格。\"\"\"\n    xpath = f\"//tr[td[contains(text(), '{row_text}')]]/td[{column_index}]\"\n    return await tab.query(xpath)\n\n# 用法\nproduct_row = await find_product_by_name(tab, \"Laptop\")\nprice_cell = await find_table_cell(tab, \"Laptop\", 2)\nprice = await price_cell.text\nprint(price)  # \"$999\"\n```\n\n## 性能比较\n\n```python\nimport asyncio\nimport time\n\nasync def benchmark_selectors(tab):\n    \"\"\"比较 CSS 与 XPath 的性能。\"\"\"\n    \n    # 预热\n    await tab.query(\"#products-table\")\n    \n    # 基准测试 CSS\n    start = time.time()\n    for _ in range(100):\n        await tab.query(\"#products-table tbody tr\", find_all=True)\n    css_time = time.time() - start\n    \n    # 基准测试 XPath\n    start = time.time()\n    for _ in range(100):\n        await tab.query(\"//table[@id='products-table']//tbody//tr\", find_all=True)\n    xpath_time = time.time() - start\n    \n    print(f\"CSS: {css_time:.3f}s\")\n    print(f\"XPath: {xpath_time:.3f}s\")\n    print(f\"CSS is {xpath_time/css_time:.2f}x faster\")\n\n# 典型结果：对于简单选择器，CSS 快 1.2-1.5 倍\n```\n\n!!! warning \"性能 vs 可读性\"\n    虽然 CSS 选择器通常更快，但对于单个查询，差异通常可以忽略不计（毫秒级）。请选择使您的代码更具可读性和可维护性的选择器，特别是对于 XPath 擅长的复杂关系。\n\n## 选择器最佳实践\n\n### 1. 优先使用稳定的选择器\n\n```python\n# 好的：使用语义属性\nawait tab.query(\"#user-email\")\nawait tab.query(\"[data-testid='submit-button']\")\nawait tab.query(\"input[name='username']\")\n\n# 避免：基于结构的脆弱选择器\nawait tab.query(\"div > div > div:nth-child(3) > input\")\nawait tab.query(\"body > div:nth-child(2) > form > div:first-child\")\n```\n\n### 2. 使用能工作的最简单的选择器\n\n```python\n# 好的：简单高效\nawait tab.query(\"#login-form\")\nawait tab.query(\".submit-button\")\n\n# 避免：在不必要时过度复杂化\nawait tab.query(\"//div[@id='content']/descendant::form[@id='login-form']\")\n```\n\n### 3. 适当组合 find() 和 query()\n\n```python\n# 使用 find() 进行简单的属性匹配\nusername = await tab.find(id=\"username\")\nsubmit = await tab.find(tag_name=\"button\", type=\"submit\")\n\n# 使用 query() 处理复杂模式\nactive_link = await tab.query(\"nav.menu a.active\")\nerror_msg = await tab.query(\"//input[@name='email']/following-sibling::span[@class='error']\")\n```\n\n### 4. 为复杂的选择器添加注释\n\n```python\n# 查找包含产品 \"Laptop\" 的行中的 \"Edit\" 按钮\n# XPath: 导航到带有 \"Laptop\" 文本的行, 然后查找编辑按钮\nedit_button = await tab.query(\n    \"//tr[td[text()='Laptop']]//button[@class='btn-edit']\"\n)\n```\n\n## 结论\n\n通过理解 CSS 选择器和 XPath，以及它们各自的优势和用例，您可以创建出健壮且可维护的浏览器自动化，以处理现代 Web 应用程序的复杂性。请记住：\n\n- **使用 CSS 选择器** 进行简单的、对性能要求严格的选择\n- **使用 XPath** 处理复杂关系、文本匹配和向上导航\n- 编写选择器时，**选择稳定性** 而非简洁性\n- **注释复杂的查询** 以保持代码的可读性\n\n有关 Pydoll 内部如何使用这些选择器的更多信息，请参阅 [FindElements Mixin](find-elements-mixin.md) 文档。"
  },
  {
    "path": "docs/zh/deep-dive/index.md",
    "content": "# 深度探讨：技术基础\n\n**欢迎来到 Pydoll 的技术核心，在这里我们将探索驱动浏览器自动化的系统和协议。**\n\n本节提供了关于网络抓取、浏览器自动化、网络协议和反检测技术的全面技术教育。我们不只关注使用模式，而是探讨底层机制，从第一个 TCP 数据包到最终渲染的像素。\n\n## 是什么让这里与众不同\n\n大多数自动化文档教您 **如何使用工具**。本节教您 **互联网实际上是如何工作的**，以及如何在每一层对其进行操控：\n\n- **网络协议** (TCP/IP, TLS, HTTP/2) - 每个请求背后的无形基础\n- **浏览器内部原理** (CDP, 渲染引擎, JavaScript 上下文) - Chrome 内部发生了什么\n- **检测系统** (指纹识别, 行为分析, 代理检测) - 网站如何识别机器人\n- **规避技术** (CDP 覆盖, 一致性强制, 人类模拟) - 如何变得无法检测\n\n!!! quote \"理念\"\n    **“任何足够先进的技术都与魔法无异。”**\n    \n    本节旨在通过解释底层系统来揭开浏览器自动化的神秘面纱。理解这些基础知识将使您在自动化工作中获得更好的控制力和可预测性。\n\n## 知识的架构\n\n本节分为 **五个渐进的层次**，每个层次都建立在上一层的基础上：\n\n### 核心基础\n**[→ 探索基础知识](./fundamentals/cdp.md)**\n\n从基础开始：理解驱动 Pydoll 的协议和系统。\n\n- **[Chrome 开发者工具协议](./fundamentals/cdp.md)** - Pydoll 如何绕过 WebDriver 与浏览器对话\n- **[连接层](./fundamentals/connection-layer.md)** - WebSocket 架构、异步模式、实时 CDP\n- **[Python 类型系统](./fundamentals/typing-system.md)** - 类型安全、用于 CDP 的 TypedDict、IDE 集成\n\n**为什么从这里开始**：理解 CDP 和异步通信为理解浏览器自动化的所有其他方面奠定了基础。\n\n---\n\n### 内部架构\n**[→ 探索架构](./architecture/browser-domain.md)**\n\n更上一层楼：了解 Pydoll 的内部组件如何协同工作。\n\n- **[浏览器域](./architecture/browser-domain.md)** - 进程管理、上下文、多配置文件自动化\n- **[标签页域](./architecture/tab-domain.md)** - 标签页生命周期、并发操作、iframe 处理\n- **[WebElement 域](./architecture/webelement-domain.md)** - 元素交互、Shadow DOM、属性处理\n- **[FindElements Mixin](./architecture/find-elements-mixin.md)** - 选择器策略、DOM 遍历、优化\n- **[事件架构](./architecture/event-architecture.md)** - 反应式事件系统、回调、异步分发\n- **[浏览器请求架构](./architecture/browser-requests-architecture.md)** - 浏览器上下文中的 HTTP\n\n**为什么这很重要**：了解内部架构可以揭示从表面使用中看不出来的优化机会和设计模式。\n\n---\n\n### 网络与安全\n**[→ 探索网络与安全](./network/index.md)**\n\n深入协议层：了解数据如何在互联网上传输。\n\n- **[网络基础](./network/network-fundamentals.md)** - OSI 模型、TCP/UDP、WebRTC 泄露\n- **[HTTP/HTTPS 代理](./network/http-proxies.md)** - 应用层代理、CONNECT 隧道\n- **[SOCKS 代理](./network/socks-proxies.md)** - 会话层代理、UDP 支持、安全\n- **[代理检测](./network/proxy-detection.md)** - 匿名级别、检测技术、规避\n- **[构建代理服务器](./network/build-proxy.md)** - 完整的 HTTP 和 SOCKS5 实现\n- **[法律与道德](./network/proxy-legal.md)** - GDPR、CFAA、合规性、负责任的使用\n\n**关键见解**：网络特征是在操作系统级别确定的。声称的浏览器身份与网络级指纹之间的不匹配可以被复杂的反机器人系统检测到。\n\n---\n\n### 指纹识别\n**[→ 探索指纹识别](./fingerprinting/index.md)**\n\n了解浏览器自动化的检测系统和规避技术。\n\n- **[网络指纹](./fingerprinting/network-fingerprinting.md)** - TCP/IP, TLS/JA3, p0f, Nmap, Scapy\n- **[浏览器指纹](./fingerprinting/browser-fingerprinting.md)** - HTTP/2, Canvas, WebGL, JavaScript API\n- **[规避技术](./fingerprinting/evasion-techniques.md)** - CDP 覆盖、一致性、实用代码\n\n**关键见解**：每次连接都会揭示众多特征（canvas 渲染、TCP 窗口大小、TLS 密码顺序）。有效的隐蔽需要在所有检测层保持一致性。\n\n---\n\n### 实用指南\n**[→ 探索指南](./guides/selectors-guide.md)**\n\n应用您的知识：应对常见自动化挑战的实用指南。\n\n- **[CSS 选择器 vs XPath](./guides/selectors-guide.md)** - 选择器语法、性能、最佳实践\n\n**即将推出**：更多实用指南，将技术知识融合成可操作的模式。\n\n---\n\n## 学习路径\n\n不同的目标需要不同的知识。选择您的路径：\n\n### 路径 1：隐蔽自动化\n**目标：构建无法检测的抓取工具**\n\n1.  **[指纹识别概述](./fingerprinting/index.md)** - 了解检测环境\n2.  **[网络指纹](./fingerprinting/network-fingerprinting.md)** - TCP/IP, TLS 签名\n3.  **[浏览器指纹](./fingerprinting/browser-fingerprinting.md)** - Canvas, WebGL, HTTP/2\n4.  **[规避技术](./fingerprinting/evasion-techniques.md)** - 基于 CDP 的对策\n5.  **[网络与安全](./network/index.md)** - 代理选择和配置\n6.  **[浏览器域](./architecture/browser-domain.md)** - 上下文隔离、进程管理\n\n**时间投入**：12-16 小时的深度技术学习\n**回报**：能够绕过复杂的反机器人系统\n\n---\n\n### 路径 2：架构精通\n**目标：为 Pydoll 做贡献或构建类似的工具**\n\n1.  **[CDP 深度探讨](./fundamentals/cdp.md)** - 协议基础\n2.  **[连接层](./fundamentals/connection-layer.md)** - WebSocket 异步模式\n3.  **[事件架构](./architecture/event-architecture.md)** - 事件驱动设计\n4.  **[浏览器域](./architecture/browser-domain.md)** - 浏览器管理\n5.  **[标签页域](./architecture/tab-domain.md)** - 标签页生命周期\n6.  **[WebElement 域](./architecture/webelement-domain.md)** - 元素交互\n7.  **[Python 类型系统](./fundamentals/typing-system.md)** - 类型安全集成\n\n**时间投入**：16-20 小时的架构学习\n**回报**：深入理解浏览器自动化的内部原理\n\n---\n\n### 路径 3：网络工程\n**目标：掌握代理、指纹和网络级隐蔽技术**\n\n1.  **[网络基础](./network/network-fundamentals.md)** - OSI 模型, TCP/UDP, WebRTC\n2.  **[网络指纹](./fingerprinting/network-fingerprinting.md)** - TCP/IP 签名, TLS/JA3\n3.  **[HTTP/HTTPS 代理](./network/http-proxies.md)** - 应用层代理\n4.  **[SOCKS 代理](./network/socks-proxies.md)** - 会话层代理\n5.  **[代理检测](./network/proxy-detection.md)** - 匿名与规避\n6.  **[构建代理服务器](./network/build-proxy.md)** - 从头开始实现\n\n**时间投入**：14-18 小时的网络协议学习\n**回报**：完全理解网络级的匿名与检测\n\n---\n\n## 先决条件\n\n这是高级技术材料。推荐的先决条件包括：\n\n- **Python 基础** - 类、async/await、上下文管理器、装饰器\n- **基本网络知识** - IP 地址、端口、HTTP 协议\n- **Pydoll 基础** - 参见 [功能特性](../features/core-concepts.md) 和 [快速入门](../index.md)\n- **浏览器开发者工具** - Chrome 检查器、网络选项卡、控制台\n\n**如果您对这些不熟悉**，我们建议：\n\n1.  首先完成 [功能特性](../features/index.md) 部分\n2.  使用 Pydoll 练习基本的自动化\n3.  当您需要更深入的理解时再回到这里\n\n## 精通的理念\n\nWeb 自动化涉及多个专业领域：\n\n- **协议工程** - 理解 TCP/IP, TLS, HTTP/2\n- **系统编程** - 管理进程, 异步 I/O, WebSocket\n- **安全研究** - 指纹, 检测, 规避\n- **浏览器内部原理** - 渲染, JavaScript 上下文, CDP\n- **操作安全** - 法律合规, 道德准则\n\n大多数开发者是随着时间的推移独立学习这些知识的。本节通过以下方式整合了这些知识：\n\n1.  **集中知识** - 不再需要分散的博客文章和学术论文\n2.  **提供背景** - 从第一性原理出发解释每种技术\n3.  **提供可用代码** - 所有示例都可用于生产\n4.  **引用来源** - 每个声明都有 RFC、文档或研究支持\n5.  **渐进的复杂性** - 每个部分都建立在先前的知识之上\n\n## 文档标准\n\n本文档代表了广泛的研究、测试和验证：\n\n- 每个协议细节都根据 RFC 进行了验证\n- 每种指纹技术都在生产环境中进行了测试\n- 每个代码示例都无需修改即可运行\n- 每个声明都引用了权威来源\n- 每个图表都根据真实系统行为生成\n\n在整个文档中，技术准确性和实际适用性是优先考虑的。\n\n## 合乎道德的使用\n\n拥有这些知识的同时也伴随着责任：\n\n!!! danger \"负责任地使用\"\n    此处描述的技术既可用于合法的自动化，也可用作恶意目的。负责任的使用包括：\n    \n    - 尊重网站的服务条款和 robots.txt\n    - 实现速率限制和友好的爬行\n    - 考虑自动化是否真的必要\n    - 在不确定时咨询法律顾问\n    - 在适当的时候对您的自动化保持透明\n    \n    避免将此知识用于：\n    - 欺诈、账户滥用或非法活动\n    - 以侵略性的抓取压垮服务器\n    - 在不了解后果的情况下进行有害活动\n\n有关详细指导，请参阅 **[法律与道德考量](./network/proxy-legal.md)**。\n\n## 贡献\n\n发现错误？有建议？看到过时的东西？\n\n本文档是一个 **动态的项目**。指纹技术在发展，协议在更新，新的规避方法在出现。我们欢迎以下贡献：\n\n- 纠正技术上的不准确之处\n- 添加新的指纹技术\n- 更新协议信息\n- 改进代码示例\n- 扩展对检测系统的覆盖\n\n有关指南，请参阅 [贡献](../CONTRIBUTING.md)。\n\n---\n\n## 开始入门\n\n根据您的目标选择一条路径：\n\n**刚接触深度技术内容？**\n→ 从 **[Chrome 开发者工具协议](./fundamentals/cdp.md)** 开始，了解 Pydoll 的基础\n\n**需要隐蔽自动化？**\n→ 跳转到 **[指纹识别](./fingerprinting/index.md)** 了解检测和规避技术\n\n**想要网络级的控制？**\n→ 探索 **[网络与安全](./network/index.md)** 了解代理架构和协议\n\n**正在构建自动化基础设施？**\n→ 学习 **[内部架构](./architecture/browser-domain.md)** 了解设计模式\n\n**只是想浏览一下？**\n→ 从侧边栏任选一个主题，每篇文章都是自成体系的\n\n---\n\n!!! success \"技术深度探讨\"\n    本节提供了浏览器自动化的全面技术知识，从基础协议到高级规避技术。\n    \n    请按您自己的节奏探索。"
  },
  {
    "path": "docs/zh/deep-dive/network/build-proxy.md",
    "content": "# 构建代理服务器\n\n本文档使用 Python asyncio 从零实现 HTTP 和 SOCKS5 代理服务器。目标不是生产就绪，而是协议理解：观察每个字节如何被解析、安全边界在哪里，以及为什么真实的代理软件中存在某些设计决策。\n\n!!! info \"模块导航\"\n    - [网络基础](./network-fundamentals.md)：TCP/IP、UDP、WebRTC\n    - [HTTP/HTTPS 代理](./http-proxies.md)：应用层代理\n    - [SOCKS 代理](./socks-proxies.md)：会话层代理\n    - [代理检测](./proxy-detection.md)：检测技术与规避\n\n    有关在 Pydoll 中实际使用代理的方法，请参阅[代理配置](../../features/configuration/proxy.md)。\n\n!!! warning \"教育用途代码\"\n    这些实现以清晰度为优先，而非健壮性。它们缺少连接限制、访问控制列表以及生产代理所需的许多错误恢复路径。请勿将它们暴露于不受信任的网络中。\n\n## HTTP 代理\n\nHTTP 代理以两种模式运行。对于明文 HTTP，它接收完整的请求（带有绝对形式的 URL，例如 `GET http://example.com/path HTTP/1.1`），将请求目标重写为原始形式（`GET /path HTTP/1.1`），连接到目标服务器，转发请求，然后将响应传回。对于 HTTPS，客户端发送 `CONNECT host:port` 请求，代理打开到目标的 TCP 连接，以 `200 Connection Established` 响应，然后在两个方向之间盲目中继字节，不检查加密内容。\n\n下面的实现处理了这两种模式。阅读代码时需要注意几点。`_pipe_data` 方法在一端关闭时调用 `write_eof()`，这会向另一端发送 TCP FIN。如果不这样做，隧道会无限挂起，因为另一端的 `read()` 永远不会返回空字节。HTTP 转发路径使用相同的管道方法而不是单次 `read()` 调用，因为 HTTP 响应可以任意大，固定大小的读取会静默截断它们。请求目标重写保留了查询字符串，仅使用 `urlparse().path` 会丢失它们。\n\n```python\nimport asyncio\nimport base64\nimport contextlib\nimport logging\nfrom urllib.parse import urlparse\n\nlogger = logging.getLogger(__name__)\n\n\nclass HTTPProxy:\n    \"\"\"带有可选 Basic 认证的异步 HTTP/HTTPS 代理。\"\"\"\n\n    def __init__(self, host='0.0.0.0', port=8080, username=None, password=None):\n        self.host = host\n        self.port = port\n        self.username = username\n        self.password = password\n\n    async def start(self):\n        server = await asyncio.start_server(\n            self._handle_client, self.host, self.port\n        )\n        logger.info(f'HTTP proxy listening on {self.host}:{self.port}')\n        async with server:\n            await server.serve_forever()\n\n    async def _handle_client(self, reader, writer):\n        try:\n            request_line = await asyncio.wait_for(\n                reader.readline(), timeout=30\n            )\n            if not request_line:\n                return\n\n            parts = request_line.decode('latin-1').split()\n            if len(parts) != 3:\n                writer.write(b'HTTP/1.1 400 Bad Request\\r\\n\\r\\n')\n                await writer.drain()\n                return\n\n            method, url, _ = parts\n            headers = await self._read_headers(reader)\n\n            if not self._check_auth(headers):\n                writer.write(\n                    b'HTTP/1.1 407 Proxy Authentication Required\\r\\n'\n                    b'Proxy-Authenticate: Basic realm=\"Proxy\"\\r\\n'\n                    b'Content-Length: 0\\r\\n\\r\\n'\n                )\n                await writer.drain()\n                return\n\n            if method == 'CONNECT':\n                await self._handle_connect(url, reader, writer)\n            else:\n                await self._handle_http(method, url, headers, reader, writer)\n        except Exception as e:\n            logger.error(f'Client handler error: {e}')\n        finally:\n            writer.close()\n            await writer.wait_closed()\n\n    async def _read_headers(self, reader):\n        headers = {}\n        while True:\n            line = await reader.readline()\n            if line in (b'\\r\\n', b'\\n', b''):\n                break\n            if b':' in line:\n                key, value = line.decode('latin-1').split(':', 1)\n                headers[key.strip().lower()] = value.strip()\n        return headers\n\n    def _check_auth(self, headers):\n        if not self.username:\n            return True\n        auth = headers.get('proxy-authorization', '')\n        if not auth.startswith('Basic '):\n            return False\n        try:\n            decoded = base64.b64decode(auth[6:]).decode('utf-8')\n            if ':' not in decoded:\n                return False\n            user, pwd = decoded.split(':', 1)\n            return user == self.username and pwd == self.password\n        except Exception:\n            return False\n\n    async def _handle_connect(self, target, client_reader, client_writer):\n        \"\"\"为 HTTPS 建立盲 TCP 隧道。\"\"\"\n        # 解析 host:port，处理 IPv6 字面量如 [::1]:443\n        if target.startswith('['):\n            bracket_end = target.index(']')\n            host = target[1:bracket_end]\n            port = int(target[bracket_end + 2:])\n        elif ':' in target:\n            host, port_str = target.rsplit(':', 1)\n            port = int(port_str)\n        else:\n            client_writer.write(b'HTTP/1.1 400 Bad Request\\r\\n\\r\\n')\n            await client_writer.drain()\n            return\n\n        try:\n            server_reader, server_writer = await asyncio.open_connection(\n                host, port\n            )\n        except OSError as e:\n            logger.error(f'CONNECT failed to {host}:{port}: {e}')\n            client_writer.write(b'HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n')\n            await client_writer.drain()\n            return\n\n        client_writer.write(b'HTTP/1.1 200 Connection Established\\r\\n\\r\\n')\n        await client_writer.drain()\n\n        await asyncio.gather(\n            self._pipe(client_reader, server_writer),\n            self._pipe(server_reader, client_writer),\n        )\n\n    async def _handle_http(self, method, url, headers, client_reader, client_writer):\n        \"\"\"转发明文 HTTP 请求。\"\"\"\n        parsed = urlparse(url)\n        host = parsed.hostname\n        port = parsed.port or 80\n\n        # 在请求目标中保留查询字符串\n        path = parsed.path or '/'\n        if parsed.query:\n            path += f'?{parsed.query}'\n\n        try:\n            server_reader, server_writer = await asyncio.open_connection(\n                host, port\n            )\n        except OSError as e:\n            logger.error(f'HTTP forward failed to {host}:{port}: {e}')\n            client_writer.write(b'HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n')\n            await client_writer.drain()\n            return\n\n        # 将请求目标从绝对形式重写为原始形式\n        request = f'{method} {path} HTTP/1.1\\r\\n'\n\n        # 如果端口不是标准端口，Host 头必须包含端口号\n        if port != 80:\n            request += f'Host: {host}:{port}\\r\\n'\n        else:\n            request += f'Host: {host}\\r\\n'\n\n        # 移除不应转发的 hop-by-hop 头\n        hop_by_hop = {\n            'proxy-authorization', 'proxy-connection',\n            'connection', 'keep-alive', 'te', 'trailer', 'upgrade',\n        }\n        for key, value in headers.items():\n            if key not in hop_by_hop:\n                request += f'{key}: {value}\\r\\n'\n\n        # 强制 Connection: close，使服务器不保持连接，\n        # 否则响应流不会结束\n        request += 'Connection: close\\r\\n\\r\\n'\n\n        server_writer.write(request.encode('latin-1'))\n\n        # 如果存在请求体则转发\n        content_length = int(headers.get('content-length', 0))\n        if content_length > 0:\n            body = await client_reader.readexactly(content_length)\n            server_writer.write(body)\n\n        await server_writer.drain()\n\n        # 将整个响应传回（而不是单次固定大小读取）\n        while True:\n            chunk = await server_reader.read(65536)\n            if not chunk:\n                break\n            client_writer.write(chunk)\n            await client_writer.drain()\n\n        server_writer.close()\n        await server_writer.wait_closed()\n\n    async def _pipe(self, reader, writer):\n        \"\"\"带有正确半关闭处理的双向数据中继。\"\"\"\n        try:\n            while True:\n                data = await reader.read(8192)\n                if not data:\n                    break\n                writer.write(data)\n                await writer.drain()\n        except (ConnectionResetError, BrokenPipeError, OSError):\n            pass\n        finally:\n            with contextlib.suppress(Exception):\n                if writer.can_write_eof():\n                    writer.write_eof()\n```\n\n有几个值得理解的协议细节。HTTP 头使用 ISO-8859-1（Latin-1）编码，而非 UTF-8。Latin-1 将每个字节值 0-255 映射到一个字符，因此 `decode('latin-1')` 永远不会抛出 `UnicodeDecodeError`，而 `decode('utf-8')` 在某些头部值上会崩溃。`Proxy-Authorization` 头使用 Base64 编码，但 Base64 不是加密：凭据以明文（或者更准确地说，可轻易还原的编码）传输，除非客户端与代理之间的连接本身受到 TLS 保护。hop-by-hop 头（`Connection`、`Keep-Alive`、`TE`、`Trailer`、`Upgrade`、`Proxy-Connection`）是用于两个节点之间直接连接的，不应端到端转发。RFC 9110 第 7.6.1 节要求代理在转发前将其剥离。\n\n!!! warning \"SSRF 风险\"\n    此实现不验证目标地址。客户端可以请求 `CONNECT 127.0.0.1:6379` 来访问本地 Redis 实例，或请求 `CONNECT 169.254.169.254:80` 来访问云实例元数据（AWS、GCP、Azure）。任何暴露给不受信任客户端的代理都必须针对私有和链路本地地址范围（`127.0.0.0/8`、`10.0.0.0/8`、`172.16.0.0/12`、`192.168.0.0/16`、`169.254.0.0/16`、`::1`、`fc00::/7`）建立拒绝列表来验证目标。\n\n## SOCKS5 代理\n\nSOCKS5 代理在比 HTTP 更低的层级运行。它使用 RFC 1928 中定义的二进制协议，包含三个阶段：方法协商、可选的认证和连接请求。代理完全不解析 HTTP。一旦隧道建立，它只是中继原始字节，不理解流经其中的是什么协议。\n\nSOCKS5 的二进制特性意味着每次读取都必须精确接收预期数量的字节。TCP 是流协议，不保证 `read(4)` 返回 4 个字节：根据网络条件，它可能返回 1、2 或 3 个字节。下面的实现使用 asyncio 的 `readexactly()`，它在内部进行缓冲，直到请求数量的字节到达或连接关闭（抛出 `IncompleteReadError`）。\n\n```python\nimport asyncio\nimport contextlib\nimport struct\nimport logging\n\nlogger = logging.getLogger(__name__)\n\n\nclass SOCKS5Proxy:\n    \"\"\"支持 CONNECT 和可选认证的异步 SOCKS5 代理（RFC 1928）。\"\"\"\n\n    VERSION = 0x05\n\n    def __init__(self, host='0.0.0.0', port=1080, username=None, password=None):\n        self.host = host\n        self.port = port\n        self.username = username\n        self.password = password\n\n    async def start(self):\n        server = await asyncio.start_server(\n            self._handle_client, self.host, self.port\n        )\n        logger.info(f'SOCKS5 proxy listening on {self.host}:{self.port}')\n        async with server:\n            await server.serve_forever()\n\n    async def _handle_client(self, reader, writer):\n        try:\n            if not await self._negotiate_method(reader, writer):\n                return\n            if self.username and not await self._authenticate(reader, writer):\n                return\n            await self._handle_request(reader, writer)\n        except (asyncio.IncompleteReadError, ConnectionResetError):\n            pass\n        except Exception as e:\n            logger.error(f'SOCKS5 error: {e}')\n        finally:\n            writer.close()\n            await writer.wait_closed()\n\n    async def _negotiate_method(self, reader, writer):\n        \"\"\"第一阶段：客户端提供认证方法，服务器选择一个。\"\"\"\n        version = (await reader.readexactly(1))[0]\n        if version != self.VERSION:\n            return False\n\n        nmethods = (await reader.readexactly(1))[0]\n        methods = await reader.readexactly(nmethods)\n\n        if self.username:\n            if 0x02 not in methods:\n                writer.write(bytes([self.VERSION, 0xFF]))\n                await writer.drain()\n                return False\n            selected = 0x02\n        else:\n            selected = 0x00\n\n        writer.write(bytes([self.VERSION, selected]))\n        await writer.drain()\n        return True\n\n    async def _authenticate(self, reader, writer):\n        \"\"\"第二阶段：用户名/密码子协商（RFC 1929）。\"\"\"\n        auth_ver = (await reader.readexactly(1))[0]\n        if auth_ver != 0x01:\n            return False\n\n        ulen = (await reader.readexactly(1))[0]\n        username = (await reader.readexactly(ulen)).decode('utf-8')\n        plen = (await reader.readexactly(1))[0]\n        password = (await reader.readexactly(plen)).decode('utf-8')\n\n        ok = username == self.username and password == self.password\n        writer.write(bytes([0x01, 0x00 if ok else 0x01]))\n        await writer.drain()\n        return ok\n\n    async def _handle_request(self, reader, writer):\n        \"\"\"第三阶段：解析 CONNECT 请求并建立隧道。\"\"\"\n        header = await reader.readexactly(4)\n        version, command, _, atyp = header\n\n        # 根据地址类型解析目标地址\n        if atyp == 0x01:  # IPv4\n            raw = await reader.readexactly(4)\n            address = '.'.join(str(b) for b in raw)\n        elif atyp == 0x03:  # Domain name\n            length = (await reader.readexactly(1))[0]\n            address = (await reader.readexactly(length)).decode('ascii')\n        elif atyp == 0x04:  # IPv6\n            raw = await reader.readexactly(16)\n            groups = [f'{raw[i]:02x}{raw[i+1]:02x}' for i in range(0, 16, 2)]\n            address = ':'.join(groups)\n        else:\n            await self._reply(writer, 0x08)\n            return\n\n        port = struct.unpack('!H', await reader.readexactly(2))[0]\n        logger.info(f'SOCKS5 CONNECT {address}:{port}')\n\n        if command != 0x01:  # Only CONNECT is implemented\n            await self._reply(writer, 0x07)\n            return\n\n        try:\n            server_reader, server_writer = await asyncio.open_connection(\n                address, port\n            )\n        except ConnectionRefusedError:\n            await self._reply(writer, 0x05)\n            return\n        except OSError:\n            await self._reply(writer, 0x04)\n            return\n\n        # BND.ADDR 和 BND.PORT 应反映连接成功后的本地套接字地址。\n        # 大多数客户端对 CONNECT 命令忽略这些字段，但正确填充\n        # 满足 RFC 1928 的要求。\n        local = server_writer.get_extra_info('sockname')\n        await self._reply(writer, 0x00, local[0], local[1])\n\n        await asyncio.gather(\n            self._pipe(reader, server_writer),\n            self._pipe(server_reader, writer),\n        )\n\n    async def _reply(self, writer, status, bind_addr='0.0.0.0', bind_port=0):\n        \"\"\"发送带有指定状态和绑定地址的 SOCKS5 回复。\"\"\"\n        import socket\n        try:\n            packed_ip = socket.inet_aton(bind_addr)\n            atyp = 0x01\n        except OSError:\n            packed_ip = socket.inet_aton('0.0.0.0')\n            atyp = 0x01\n\n        writer.write(bytes([\n            self.VERSION, status, 0x00, atyp,\n            *packed_ip,\n            (bind_port >> 8) & 0xFF, bind_port & 0xFF,\n        ]))\n        await writer.drain()\n\n    async def _pipe(self, reader, writer):\n        try:\n            while True:\n                data = await reader.read(8192)\n                if not data:\n                    break\n                writer.write(data)\n                await writer.drain()\n        except (ConnectionResetError, BrokenPipeError, OSError):\n            pass\n        finally:\n            with contextlib.suppress(Exception):\n                if writer.can_write_eof():\n                    writer.write_eof()\n```\n\n当地址类型为 `0x03`（域名）时，代理通过 `asyncio.open_connection()` 自行解析 DNS。这是 SOCKS5 代理的核心隐私特性：客户端发送域名而不是在本地解析，从而防止 DNS 查询泄露到客户端的本地网络。这与 Chrome 配置 `--proxy-server=socks5://...` 时的行为相同，如[SOCKS 代理](./socks-proxies.md)中所述。\n\n`_reply` 方法在成功连接后用实际的本地套接字地址填充 `BND.ADDR` 和 `BND.PORT`，这是 RFC 1928 的要求。许多 SOCKS5 实现在这里返回 `0.0.0.0:0`，因为大多数客户端对 CONNECT 命令忽略这些字段，但正确填充它们没有任何代价，还能避免协议违规。\n\n## 同时运行两个代理\n\n```python\nasync def main():\n    http_proxy = HTTPProxy(\n        port=8080, username='user', password='pass'\n    )\n    socks5_proxy = SOCKS5Proxy(\n        port=1080, username='user', password='pass'\n    )\n    await asyncio.gather(http_proxy.start(), socks5_proxy.start())\n\n# asyncio.run(main())\n```\n\n可以使用 curl 进行测试：\n\n```bash\n# HTTP proxy\ncurl -x http://user:pass@localhost:8080 http://httpbin.org/ip\n\n# HTTPS through HTTP proxy (CONNECT tunnel)\ncurl -x http://user:pass@localhost:8080 https://httpbin.org/ip\n\n# SOCKS5 proxy\ncurl --socks5 localhost:1080 --proxy-user user:pass https://httpbin.org/ip\n```\n\n## 代码未处理的内容\n\n这些实现省略了生产代理需要处理的若干事项。理解缺少什么与理解已有什么同样具有教育意义。\n\n没有连接限制。`asyncio.start_server` 无限制地接受连接，因此单个客户端打开数千个连接会耗尽文件描述符。生产代理使用信号量或连接池来限制并发数。\n\n没有目标验证。两个代理都会连接到客户端请求的任何地址，包括 `127.0.0.1`、`169.254.169.254`（云元数据）和内部网络范围。这是一个服务端请求伪造（SSRF）向量。生产代理维护私有和链路本地地址范围的拒绝列表。\n\n没有流量日志或指标。生产代理跟踪请求数量、传输字节数、错误率和延迟百分位数，通常导出到 Prometheus 或类似系统。\n\nHTTP 代理没有添加 `Via` 头。RFC 9110 第 7.6.3 节要求中间节点在转发消息时附加 `Via` 字段。为了简洁起见这里省略了，但符合标准的代理必须包含它。\n\n两个代理都没有实现优雅关闭。当服务器停止时，活跃的隧道会被突然终止，而不是被排空。生产代理跟踪活跃连接并等待它们完成（有截止时间），然后才关闭。\n\n## 代理链\n\n代理链是指将流量依次通过多个代理路由：客户端到代理 A，代理 A 到代理 B，代理 B 到目标服务器。链中的每个代理只知道其直接邻居，而非完整路径。\n\n主要用例是分散信任。如果你不完全信任任何单一代理提供商，将两个提供商链接在一起意味着没有一个能同时看到你的真实 IP 和你的目标地址。代价是延迟：每一跳都会增加自己的连接建立时间和转发延迟。单个代理通常增加 50 到 100ms 的开销。两个代理大约翻倍，三个代理可以使总开销超过 300ms。\n\n超过两跳后，边际隐私收益递减，而延迟和故障概率增加。大多数实际部署使用一到两个代理。Tor 使用三个中继节点（守卫节点、中间节点、出口节点），因为其威胁模型假设某些中继节点已被入侵，但 Tor 将延迟惩罚视为明确的设计权衡。\n\n```\nClient --> Proxy A (SOCKS5) --> Proxy B (SOCKS5) --> Target\n           sees: client IP          sees: Proxy A IP\n           sees: Proxy B addr       sees: target addr\n```\n\n通过另一个 SOCKS5 代理链接 SOCKS5 代理的工作方式是让代理 A 将代理 B 视为目标。客户端连接到代理 A 并发送指向代理 B 地址的 CONNECT 请求。一旦该隧道建立，客户端通过隧道发送第二次 SOCKS5 握手，这次请求真正的目标。代理 A 看到流向代理 B 的流量，但如果内部连接已加密，则无法读取其内容。\n\n## 参考资料\n\n- RFC 1928: SOCKS Protocol Version 5 - https://datatracker.ietf.org/doc/html/rfc1928\n- RFC 1929: Username/Password Authentication for SOCKS V5 - https://datatracker.ietf.org/doc/html/rfc1929\n- RFC 9110: HTTP Semantics - https://www.rfc-editor.org/rfc/rfc9110.html\n- RFC 9112: HTTP/1.1 - https://www.rfc-editor.org/rfc/rfc9112.html\n- OWASP SSRF Prevention Cheat Sheet - https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html\n- mitmproxy (Python HTTPS intercepting proxy) - https://mitmproxy.org/\n"
  },
  {
    "path": "docs/zh/deep-dive/network/http-proxies.md",
    "content": "# HTTP/HTTPS Proxy 架构\n\nHTTP proxy 是互联网上最常见的代理协议。几乎每个企业网络都在使用它们，大多数商业代理服务也将其作为默认选项。它们在 OSI 模型的第 7 层（应用层）运行，这意味着它们能够理解 HTTP，并可以解析、修改、缓存和过滤流量。然而，这种与协议的深度集成也是它们最大的局限：它们只能处理 HTTP 流量，会通过可识别的标头暴露代理的使用，并且无法代理 UDP，导致 WebRTC 和 DNS 容易泄露。\n\n本文档涵盖 HTTP proxy 在协议层面的工作原理、用于 HTTPS 隧道的 CONNECT 方法、身份验证机制，以及 HTTP/2 和 HTTP/3 等现代协议的影响。\n\n!!! info \"模块导航\"\n    - [网络基础](./network-fundamentals.md)：TCP/IP、UDP、OSI 模型\n    - [网络与安全概述](./index.md)：模块介绍\n    - [SOCKS Proxy](./socks-proxies.md)：协议无关的替代方案\n    - [Proxy 检测](./proxy-detection.md)：如何避免被检测\n\n    有关实际配置，请参阅 [Proxy 配置](../../features/configuration/proxy.md)。\n\n## HTTP Proxy 的工作原理\n\nHTTP proxy 位于客户端和目标服务器之间，维护两个独立的 TCP 连接：一个从客户端到 proxy，另一个从 proxy 到目标服务器。由于 proxy 理解 HTTP，它可以对经过的流量做出智能决策。\n\n### 请求流程\n\n当客户端配置为使用 HTTP proxy 时，它会将完整的 HTTP 请求发送到 proxy，而不是直接发送到目标服务器。与直接请求的关键区别在于，请求行包含绝对 URI，而不仅仅是路径。例如，客户端发送的不是 `GET /page HTTP/1.1`，而是 `GET http://example.com/page HTTP/1.1`。这告诉 proxy 应将请求转发到哪里。\n\n```mermaid\nsequenceDiagram\n    participant Client as Client Browser\n    participant Proxy as HTTP Proxy\n    participant Server as Target Server\n\n    Client->>Proxy: GET http://example.com/page HTTP/1.1<br/>Host: example.com<br/>User-Agent: Mozilla/5.0\n    Note over Client,Proxy: TCP connection #1\n\n    Note over Proxy: Parse request, check auth,<br/>check cache, apply rules\n\n    Proxy->>Server: GET /page HTTP/1.1<br/>Host: example.com<br/>Via: 1.1 proxy.example.com<br/>X-Forwarded-For: 192.168.1.100\n    Note over Proxy,Server: TCP connection #2\n\n    Server->>Proxy: HTTP/1.1 200 OK<br/>[response body]\n\n    Note over Proxy: Cache response if allowed,<br/>filter content, log transaction\n\n    Proxy->>Client: HTTP/1.1 200 OK<br/>Via: 1.1 proxy.example.com<br/>[possibly modified body]\n```\n\nProxy 接收到完整的 HTTP 请求后，会解析方法、URL 和标头，然后决定如何处理。它可能会检查身份验证凭据、根据访问控制列表验证 URL、查找资源的缓存副本，并在转发前修改标头。然后它会打开一个到目标服务器的独立 TCP 连接并发送请求，可能带有修改过的标头。\n\n当响应到达时，proxy 可以根据 HTTP 语义（`Cache-Control`、`ETag`）缓存响应，过滤恶意软件或被拦截的关键词，在客户端支持时进行压缩，并在将响应转发回客户端之前记录事务。\n\n### Proxy 标头与隐私\n\nHTTP proxy 通常会添加标头来暴露它们的存在以及客户端的真实 IP 地址。`Via` 标头（RFC 9110）标识请求链中的 proxy。`X-Forwarded-For` 标头包含原始客户端 IP，如果涉及多个 proxy 则会形成链。`X-Forwarded-Proto` 标头指示原始请求是 HTTP 还是 HTTPS。一些 proxy 还会添加 `X-Real-IP` 作为 `X-Forwarded-For` 的简化替代。\n\n还有一个标准化的 `Forwarded` 标头（RFC 7239），将所有这些信息整合到一个字段中，例如 `Forwarded: for=192.168.1.100;proto=http;by=proxy.example.com`。实际上，大多数 proxy 仍然使用 `X-Forwarded-*` 变体，因为它们有更广泛的支持。\n\n旧版客户端和一些老旧浏览器在通过 proxy 路由时，可能还会发送 `Proxy-Connection: keep-alive` 标头而非 `Connection: keep-alive`。这个标头是一个众所周知的 proxy 使用指标，也是经典的检测信号。\n\n!!! danger \"标头检测\"\n    检测系统会查找 `Via`、`X-Forwarded-For` 或 `Forwarded` 标头的存在来确认 proxy 的使用。如果 `X-Real-IP` 与连接 IP 不匹配，则可以确认使用了 proxy。高级 proxy 可以剥离这些标头，但许多商业 proxy 服务默认会保留它们。请务必使用 [browserleaks.com/ip](https://browserleaks.com/ip) 等工具验证你的 proxy 行为。\n\n### 能力与限制\n\n由于 HTTP proxy 能够解析和理解 HTTP 协议，它们可以读取和修改未加密 HTTP 请求和响应的每个部分：URL、标头、Cookie 和正文。这使它们能够智能缓存响应、按 URL 或关键词过滤内容、注入或剥离标头、验证用户身份，以及详细记录所有流量。\n\n代价是与 HTTP 的深度耦合意味着 proxy 仅限于 HTTP 流量。它无法原生代理 FTP、SSH、SMTP 或自定义协议（尽管下面描述的 CONNECT 方法为任何基于 TCP 的协议提供了隧道解决方案）。它不支持 UDP，这意味着 WebRTC、DNS 查询和 QUIC/HTTP/3 流量会完全绕过它。而检查 HTTPS 内容需要 TLS 终止，这会破坏端到端加密。\n\n## CONNECT 方法：HTTPS 隧道\n\nCONNECT 方法（RFC 9110，第 9.3.6 节）解决了一个根本问题：HTTP proxy 如何转发它无法读取的加密流量？答案是成为一个盲目的 TCP 隧道。\n\n当客户端想要通过 proxy 访问 HTTPS 站点时，它会发送一个 `CONNECT` 请求，要求 proxy 建立到目标的原始 TCP 连接。一旦 proxy 确认隧道已建立，它就不再是 HTTP proxy，而是变成第 4 层的透明 TCP 中继，在两个方向上转发字节而不解释它们。\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant Proxy\n    participant Server\n\n    Client->>Proxy: CONNECT example.com:443 HTTP/1.1<br/>Host: example.com:443<br/>Proxy-Authorization: Basic dXNlcjpwYXNz\n    Note over Client,Proxy: Unencrypted HTTP request\n\n    Proxy->>Server: TCP three-way handshake\n    Note over Proxy,Server: TCP connection established\n\n    Proxy->>Client: HTTP/1.1 200 Connection Established\n\n    Note right of Proxy: Proxy is now a transparent<br/>TCP relay (Layer 4)\n\n    Client->>Server: TLS ClientHello\n    Note over Client,Server: TLS handshake (proxy sees<br/>this in plaintext)\n    Server->>Client: TLS ServerHello, Certificate\n\n    Client->>Server: Encrypted HTTP/2 request\n    Server->>Client: Encrypted HTTP/2 response\n\n    Note over Proxy: Proxy blindly forwards<br/>all encrypted data\n```\n\n### CONNECT 请求\n\nCONNECT 请求非常简洁。方法是 `CONNECT`，请求 URI 是目标的 `host:port`（而不是路径），如果 proxy 需要则包含身份验证。没有请求体。Proxy 验证凭据，检查访问控制规则，然后打开到指定主机和端口的 TCP 连接。如果一切成功，它会返回 `HTTP/1.1 200 Connection Established`，后跟一个空行。在该空行之后，HTTP 对话结束，proxy 变成透明中继。\n\n### CONNECT 之后的可见性\n\n一旦隧道建立，proxy 的可见性就很有限了。它知道来自 CONNECT 请求的目标主机名和端口。它可以观察连接时间（何时建立以及持续多久）、每个方向传输的数据量，以及任一方终止连接的时刻。它还可以观察随后的 TLS 握手，这一点特别值得关注。\n\nTLS ClientHello 消息在隧道建立后立即发送，且以明文传输。Proxy（以及任何网络观察者）可以直接读取 TLS 版本、完整的支持密码套件列表、扩展及其参数、提供的椭圆曲线，以及包含目标主机名的 SNI（Server Name Indication）扩展。这正是用于 TLS 指纹识别（JA3/JA4）的信息。详情请参阅[网络指纹](../fingerprinting/network-fingerprinting.md)。\n\nProxy 无法看到的是加密的应用数据：HTTP 方法、URL、请求和响应标头、Cookie、会话令牌和响应内容都在 TLS 隧道内加密。\n\n!!! note \"SNI 与 Encrypted Client Hello (ECH)\"\n    ClientHello 中的 SNI 扩展以明文暴露目标主机名，这在 proxy 场景中与 CONNECT 请求是冗余的，但对其他网络观察者来说很有意义。Encrypted Client Hello (ECH) 目前正在部署中，旨在加密 SNI 来解决这一泄露问题。不过，ECH 的采用仍然有限，需要客户端和服务器双方的支持。\n\n### CONNECT 用于非 HTTPS 协议\n\n虽然 CONNECT 主要用于 HTTPS，但它可以隧道传输任何基于 TCP 的协议。到端口 993 的 IMAPS 连接、到端口 22 的 SSH 连接，或到端口 990 的 FTP-over-TLS 都可以通过 CONNECT 隧道工作。Proxy 不需要理解这些协议，因为隧道建立后它只是简单地中继字节。\n\n实际上，许多企业 proxy 会将 CONNECT 限制在端口 443（HTTPS），以防止滥用。尝试 `CONNECT example.com:22` 进行 SSH 连接通常会返回 `403 Forbidden`。\n\n### HTTPS 困境\n\nHTTP proxy 在处理加密流量时面临一个根本性的选择。使用 CONNECT 隧道方式，端到端加密得以保留，客户端直接验证服务器证书，证书固定正常工作。但 proxy 无法检查、缓存或过滤加密内容。\n\n另一种方式是 TLS 终止（MITM），proxy 解密 HTTPS 流量、检查内容，然后重新加密后转发。这需要在客户端上安装 proxy 的 CA 证书，会破坏端到端加密，并且可以通过证书固定和证书透明度日志检测到。大多数企业 proxy 使用这种方式进行内容过滤和安全扫描，而注重隐私的 proxy 则使用盲目 CONNECT 隧道。\n\n对于网页抓取和自动化来说，这一区别对 TLS 指纹识别很重要。如果 proxy 执行 TLS 终止，目标服务器看到的 TLS 指纹属于 proxy，而非你的浏览器。如果你使用的是 CONNECT 隧道，指纹则是端到端保留的。根据你的规避策略，其中一种方式可能比另一种更合适。\n\n| 方面 | HTTP（无 CONNECT） | HTTPS（CONNECT 隧道） |\n|--------|-------------------|------------------------|\n| Proxy 可见性 | 完整的 HTTP 请求/响应 | 仅目标 host:port + TLS ClientHello |\n| 加密 | 无（除非 TLS 终止） | 端到端 TLS |\n| 缓存 | 是，基于 HTTP 语义 | 否（加密内容） |\n| 内容过滤 | 是 | 否（仅基于主机名拦截） |\n| 标头修改 | 是 | 否（加密标头） |\n| URL 可见性 | 完整 URL | 仅主机名（通过 CONNECT 和 SNI） |\n| 协议支持 | 仅 HTTP | 任何基于 TCP 的协议 |\n\n## HTTPS Proxy（到 Proxy 的 TLS）\n\n有一个值得澄清的区别：代理 HTTPS 流量与通过 HTTPS 连接到 proxy 本身是不同的。当你配置 `--proxy-server=https://proxy:port` 而非 `http://proxy:port` 时，你的浏览器与 proxy 之间的连接是通过 TLS 加密的。这可以保护你的 proxy 身份验证凭据不被本地网络嗅探，并且对本地观察者隐藏 CONNECT 主机名，因为它被封装在到 proxy 的 TLS 连接内。\n\nChrome 通过 `--proxy-server` 中的 `https://` 方案支持此功能。当在不受信任的网络（公共 Wi-Fi、共享主机）上使用 proxy 时，这一点尤其重要，因为你与 proxy 之间的连接是最薄弱的环节。\n\n## 身份验证\n\nHTTP proxy 身份验证使用标准 HTTP 状态码和标头，遵循 RFC 9110。当 proxy 需要身份验证时，它会返回 `407 Proxy Authentication Required` 以及一个 `Proxy-Authenticate` 标头，指示它支持哪些身份验证方案。然后客户端使用包含凭据的 `Proxy-Authorization` 标头重新传输请求。\n\n### 身份验证方案\n\n有多种身份验证方案，每种都有不同的安全特性。\n\n**Basic**（RFC 7617）是最简单的。客户端发送 `Proxy-Authorization: Basic <base64(username:password)>`。Base64 是一种编码而非加密，因此凭据可以被轻易还原。任何拦截到该标头的人都可以立即解码并无限期地重用，因为没有重放保护。Basic 身份验证应仅通过 TLS 加密的连接使用。\n\n**Digest**（RFC 7616）使用挑战-响应机制。Proxy 发送一个随机 nonce，客户端计算用户名、密码、nonce 和请求 URI 的哈希值。密码永不传输，nonce 提供重放保护。原始版本使用 MD5，其速度足以被高效暴力破解，不过 RFC 7616 增加了 SHA-256 支持。Digest 身份验证很少被现代 proxy 服务实现。\n\n**NTLM** 是微软专有的挑战-响应协议，常见于 Windows 企业环境。它使用三步协商（Type 1 协商、Type 2 挑战、Type 3 认证），并与 Active Directory 集成实现单点登录。NTLMv1 使用 DES（已被攻破），NTLMv2 使用 HMAC-MD5（按现代标准被认为较弱）。微软建议在新部署中使用 Kerberos 替代 NTLM。NTLM 是绑定连接的，这意味着它在 HTTP/2 多路复用下会出问题。\n\n**Negotiate**（RFC 4559）使用 SPNEGO 在 Kerberos 和 NTLM 之间选择，优先使用 Kerberos。Kerberos 提供最强的安全性（AES 加密、相互身份验证、有时间限制的票据），但需要 Active Directory 基础设施、加入域的机器和精确的时钟同步。在浏览器自动化中，Kerberos 难以通过编程方式配置。\n\n| 方案 | 安全性 | 机制 | 实用说明 |\n|--------|----------|-----------|-----------------|\n| Basic | 低 | Base64 编码的凭据 | 通用支持。仅通过 TLS 使用。 |\n| Digest | 中 | 使用 MD5/SHA-256 的挑战-响应 | 通过 nonce 提供重放保护。很少被实现。 |\n| NTLM | 中 | 挑战-响应（NT 哈希） | Windows SSO。专有，存在已知漏洞。 |\n| Negotiate | 高 | Kerberos/SPNEGO | 最强。需要 Active Directory。 |\n\n### Pydoll 中的身份验证\n\nChrome 不支持在 `--proxy-server` 标志中内联 proxy 凭据。写 `--proxy-server=http://user:pass@proxy:port` 不会生效：Chrome 会静默忽略 `user:pass` 部分并在不进行身份验证的情况下连接。\n\nPydoll 通过其 `ProxyManager` 透明地解决了这个问题。当你提供带有内嵌凭据的 proxy URL 时，Pydoll 会提取用户名和密码，在传递给 Chrome 之前从 URL 中剥离它们，然后使用 CDP Fetch 域拦截 `407 Proxy Authentication Required` 响应，并通过 `Fetch.continueWithAuth` 自动提供凭据。这种方式适用于 Chrome 支持的所有身份验证方案（Basic、Digest、NTLM、Negotiate），而无需 Pydoll 实现特定于协议的逻辑。\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\n# Pydoll extracts credentials, cleans the URL, and handles 407 via CDP\noptions.add_argument('--proxy-server=http://user:pass@proxy.example.com:8080')\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n```\n\n!!! tip \"身份验证最佳实践\"\n    始终使用 TLS 加密的 proxy 连接（HTTPS proxy 或 SSH 隧道）来保护传输中的凭据。对于 API proxy，优先使用 Bearer 令牌，因为它们可撤销且有时间限制。切勿通过未加密的 HTTP 连接使用 Basic 身份验证。不要在源代码中硬编码凭据，请使用环境变量。\n\n## 现代协议与代理\n\n### HTTP/2\n\nHTTP/2 引入了多路复用、二进制分帧和 HPACK 标头压缩，从根本上改变了 proxy 处理连接的方式。在 HTTP/1.1 中，每个请求按顺序占用一个连接（虽然存在流水线但实际上已被禁用，因此浏览器通过为每个主机打开六个并行连接来解决这个问题）。在 HTTP/2 中，单个 TCP 连接承载多个并发流，每个流都有自己的请求和响应。\n\n对于 proxy 来说，这意味着需要在连接的两端管理流 ID、优先级和流控窗口。Proxy 必须在客户端侧和服务器侧之间转换流 ID、维护优先级树，并对每个流进行流量控制。这比 HTTP/1.1 简单的请求-响应转发要复杂得多。\n\n从指纹识别的角度来看，HTTP/2 流元数据（窗口大小、优先级设置、HPACK 内的标头排序）可以对单个客户端进行指纹识别，即使多个用户共享同一个 proxy。\n\n| 特性 | HTTP/1.1 | HTTP/2 |\n|---------|----------|--------|\n| 连接 | 每个连接按顺序处理（浏览器并行打开 6 个） | 单个连接上的多个并发流 |\n| 多路复用 | 否（队头阻塞） | 是（仅流级别） |\n| 标头压缩 | 无 | HPACK |\n| Proxy 复杂度 | 简单的请求/响应转发 | 流 ID 映射、优先级管理 |\n\n在 HTTP/2 中，CONNECT 方法通过 RFC 8441 进行了扩展，支持 `:protocol` 伪标头，使 WebSocket 隧道和其他协议升级可以直接在 HTTP/2 流中进行，而无需单独的连接。\n\n### HTTP/3 与 QUIC\n\nHTTP/3 运行在 QUIC（RFC 9000）之上，这是一种基于 UDP 的传输协议。这给 HTTP proxy 带来了根本性的挑战。传统 HTTP proxy 运行在 TCP 之上，无法处理 QUIC 的 UDP 流量。QUIC 连接可以在 IP 变化后继续存活（连接迁移），使 proxy 会话管理变得复杂。而且 QUIC 几乎加密了所有内容，包括之前可见的传输层元数据。\n\n代理 QUIC 需要 CONNECT-UDP（RFC 9298），这是一种通过 HTTP proxy 建立 UDP 隧道的新方法。大多数传统 proxy，包括许多商业服务，尚不支持此功能。当 proxy 不支持 QUIC 时，浏览器会回退到基于 TCP 的 HTTP/2，这意味着如果你依赖 HTTP/3 的加密传输，实际泄露的元数据可能比预期更多。\n\n在自动化场景中，考虑使用 `--disable-quic` Chrome 标志禁用 QUIC，以强制使用基于 TCP 的 HTTP/2。这可以确保所有流量都通过你的 proxy，并消除 QUIC 导致的 UDP 泄露风险。\n\n| 方面 | TCP + TLS（HTTP/1.1、HTTP/2） | QUIC/UDP（HTTP/3） |\n|--------|------------------------------|-------------------|\n| 传输 | TCP（面向连接） | UDP（无连接） |\n| 握手 | 分离的 TCP + TLS（2 RTT） | 合并（0-1 RTT） |\n| 队头阻塞 | 是（TCP 级别） | 否（仅流级别） |\n| 连接迁移 | 不支持 | 支持（可在 IP 变化后存活） |\n| Proxy 兼容性 | 极好 | 有限（需要 UDP 中继支持） |\n\n!!! warning \"协议降级\"\n    当 proxy 不支持 HTTP/3 时，浏览器会静默回退到 HTTP/2 或 HTTP/1.1。这种降级可能暴露 HTTP/3 本会加密的元数据（标头、时序模式）。请监控你的流量以了解实际的协议版本，并注意 HTTP/3 的采用率因地区和 CDN 而异。\n\n## 总结\n\nHTTP proxy 提供了丰富的功能，但代价是范围有限和隐私问题。它们可以检查、缓存和过滤 HTTP 流量，但无法处理非 HTTP 协议、UDP 流量或 HTTPS 内容（除非破坏加密）。除非明确剥离，否则它们的存在会通过可识别的标头暴露。\n\n对于自动化来说，CONNECT 隧道是最相关的功能：它在保留端到端 TLS 加密的同时，仅让 proxy 获得主机名级别的可见性。Pydoll 通过 CDP Fetch 域透明地处理 proxy 身份验证，支持 Chrome 实现的所有方案。\n\n### HTTP Proxy 与 SOCKS5 对比\n\n| 需求 | HTTP Proxy | SOCKS5 |\n|------|------------|--------|\n| 内容过滤 | 是 | 否 |\n| 基于 URL 拦截 | 是 | 否（仅 IP:port） |\n| 缓存 | 是 | 否 |\n| UDP 支持 | 否 | 是 |\n| 协议灵活性 | 仅 HTTP（CONNECT 可用于 TCP 隧道） | 任何 TCP/UDP |\n| 隐私 | 低（解析 HTTP，添加暴露性标头） | 中（不解析或修改流量，但未加密内容对运营商仍然可见） |\n| DNS 解析 | Proxy 解析（远程） | 取决于配置（SOCKS5：通常客户端解析，SOCKS5h：proxy 解析。Chrome 对 SOCKS5 始终使用远程解析。） |\n\n对于需要内容控制和缓存的企业环境，HTTP proxy 是正确的选择。对于注重隐私的自动化，SOCKS5 提供更好的隐蔽性和协议灵活性。要获得最高安全性，请使用 SOCKS5 over SSH 隧道或 VPN。\n\n**后续步骤：**\n\n- [SOCKS Proxy](./socks-proxies.md)：协议无关的会话层代理\n- [网络基础](./network-fundamentals.md)：TCP/IP、UDP、WebRTC\n- [Proxy 检测](./proxy-detection.md)：如何检测 proxy 以及如何避免\n- [Proxy 配置](../../features/configuration/proxy.md)：Pydoll 实际 proxy 设置\n- [网络指纹](../fingerprinting/network-fingerprinting.md)：TCP/IP 和 TLS 指纹识别\n\n## 参考文献\n\n- RFC 9110: HTTP Semantics (2022, replaces RFC 7230-7237) - https://www.rfc-editor.org/rfc/rfc9110.html\n- RFC 9112: HTTP/1.1 (2022) - https://www.rfc-editor.org/rfc/rfc9112.html\n- RFC 9113: HTTP/2 (2022, replaces RFC 7540) - https://www.rfc-editor.org/rfc/rfc9113.html\n- RFC 9114: HTTP/3 (2022) - https://www.rfc-editor.org/rfc/rfc9114.html\n- RFC 9000: QUIC Transport Protocol (2021) - https://www.rfc-editor.org/rfc/rfc9000.html\n- RFC 9298: Proxying UDP in HTTP (CONNECT-UDP, 2022) - https://www.rfc-editor.org/rfc/rfc9298.html\n- RFC 8441: Bootstrapping WebSockets with HTTP/2 (2018) - https://www.rfc-editor.org/rfc/rfc8441.html\n- RFC 7617: Basic Authentication (2015) - https://www.rfc-editor.org/rfc/rfc7617.html\n- RFC 7616: Digest Authentication (2015) - https://www.rfc-editor.org/rfc/rfc7616.html\n- RFC 7239: Forwarded HTTP Extension (2014) - https://www.rfc-editor.org/rfc/rfc7239.html\n- RFC 4559: Negotiate Authentication (2006) - https://www.rfc-editor.org/rfc/rfc4559.html\n- MDN Web Docs: Proxy servers and tunneling - https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling\n- Chrome DevTools Protocol: Fetch domain - https://chromedevtools.github.io/devtools-protocol/tot/Fetch/\n"
  },
  {
    "path": "docs/zh/deep-dive/network/index.md",
    "content": "# 网络与安全深度探讨\n\n**欢迎来到现代互联网通信的基础——这里是匿名、检测与规避的战场。**\n\n网络协议是驱动每一次 Web 请求、浏览器连接和自动化脚本的无形基础设施。深入理解它们，您将从一个 **工具使用者** 转变为一个 **协议工程师**，能够应对最复杂的反机器人系统。\n\n## 为什么网络架构很重要\n\n当您运行 `tab.go_to('https://example.com')` 时，一场复杂的协议交响乐便拉开了序幕：\n\n1.  **DNS 解析** 将域名转换为 IP 地址（可能会泄露您的意图）\n2.  **TCP 握手** 建立连接（通过数据包特征暴露您的操作系统）\n3.  **TLS 协商** 保护通道安全（通过密码套件对您的浏览器进行指纹识别）\n4.  **HTTP/2 请求** 获取页面（通过 SETTINGS 帧暴露浏览器版本）\n5.  **WebRTC 发现** 可能会探测您的真实 IP（完全绕过您的 VPN）\n\n**每一步都是检测或规避的机会。**\n\n!!! danger \"网络层无法说谎\"\n    与浏览器级特征（JavaScript 可以修改）不同，网络级指纹被 **烙印在操作系统内核和 TCP/IP 协议栈中**。像 Chrome 浏览器声称自己是 Windows，却发送 Linux 的 TCP 选项，这种不匹配对于隐形自动化来说是立竿见影的致命伤。\n\n## 互联网隐私的架构\n\n本模块探讨了在现代互联网上使隐私成为可能（或被破坏）的 **技术基础**：\n\n### OSI 模型的现实\n\n```mermaid\ngraph TB\n    subgraph \"应用层 (第 7 层)\"\n        HTTP[HTTP/HTTPS 标头]\n        DNS[DNS 查询]\n    end\n    \n    subgraph \"表示层 (第 6 层)\"\n        TLS[TLS/SSL 指纹]\n        Ciphers[密码套件, 扩展]\n    end\n    \n    subgraph \"会话/传输层 (第 5-4 层)\"\n        SOCKS[SOCKS 代理协议]\n        TCP[TCP 窗口, 选项, ISN]\n    end\n    \n    subgraph \"网络层 (第 3 层)\"\n        IP[IP TTL, 分片]\n        Routing[数据包路由, 跳数]\n    end\n    \n    HTTP --> TLS\n    DNS --> TLS\n    TLS --> SOCKS\n    Ciphers --> TCP\n    SOCKS --> IP\n    TCP --> Routing\n```\n\n**每一层既是盾牌，也是弱点：**\n\n- **第 7 层 (应用层)**：代理可以读取和修改您的 HTTP 流量\n- **第 6 层 (表示层)**：TLS 加密保护内容，但泄露元数据\n- **第 4 层 (传输层)**：TCP 特征暴露您的操作系统\n- **第 3 层 (网络层)**：IP 地址揭示您的物理位置\n\n## 您将掌握什么\n\n本模块按照从基础到高级利用的 **技术进阶** 构建：\n\n### 1. 网络基础\n**[网络基础](./network-fundamentals.md)**\n\n构建基础：了解驱动互联网的协议，以及它们如何揭示或隐藏您的身份。\n\n- **OSI 模型分层** 及其指纹识别含义\n- **TCP vs UDP**：为什么您的代理可能会泄露 UDP 流量\n- **WebRTC IP 泄露**：现代浏览器中的隐藏威胁\n- **网络堆栈特征**：TTL、窗口大小、选项顺序\n\n**为什么从这里开始**：没有这个基础，代理配置就是 **“货物崇拜编程”**，只是复制命令而不理解它们为什么有效（或无效）。\n\n### 2. HTTP/HTTPS 代理\n**[HTTP/HTTPS 代理](./http-proxies.md)**\n\n掌握最常见的代理协议，并理解其根本局限性。\n\n- **HTTP 代理操作**：请求转发、缓存、标头注入\n- **CONNECT 隧道**：HTTPS 如何“隧道”通过 HTTP 代理\n- **HTTP/2 的复杂性**：多路复用、流优先级、SETTINGS 指纹\n- **HTTP/3 和 QUIC**：基于 UDP 的代理挑战\n- **身份验证方案**：Basic, Digest, NTLM, Bearer 令牌\n\n**关键见解**：HTTP 代理在第 7 层运行，它们可以 **读取、修改和记录** 您未加密的流量。要获得真正的隐私，您需要在代理看到您的数据 **之前** 进行加密。\n\n### 3. SOCKS 代理\n**[SOCKS 代理](./socks-proxies.md)**\n\n理解为什么 SOCKS5 是注重隐私的自动化的 **黄金标准**。\n\n- **SOCKS4 vs SOCKS5**：协议演进和功能\n- **SOCKS5 握手**：二进制协议深度解析与数据包结构\n- **UDP 支持**：通过 SOCKS5 运行游戏、VoIP 和 WebRTC\n- **DNS 解析**：为什么代理端 DNS 能防止泄露\n- **为什么 SOCKS5 > HTTP 代理**：协议级比较\n\n**关键优势**：SOCKS 在第 5 层（会话层）运行，**低于** 应用层。它无法读取您的 HTTP 流量，只能看到目标 IP，从而极大地减少了信任面。\n\n### 4. 代理检测\n**[代理检测与匿名](./proxy-detection.md)**\n\n了解网站如何 **检测代理使用** 以及如何规避检测。\n\n- **匿名级别**：透明代理、匿名代理、精英代理\n- **IP 信誉数据库**：您数据中心的 IP 如何暴露您\n- **标头分析**：X-Forwarded-For, Via, Forwarded 标头\n- **一致性检查**：DNS 反向查找、地理位置不匹配\n- **网络指纹集成**：将代理检测与 TCP/TLS 分析相结合\n\n**残酷的现实**：大多数“匿名”代理都很容易被检测到。真正的隐蔽需要 **精英住宅代理** + **一致的浏览器指纹** + **类人行为**。\n\n### 5. 构建代理服务器\n**[构建您自己的代理](./build-proxy.md)**\n\n在 Python 中从头开始实现 HTTP 和 SOCKS5 代理，这是终极的学习体验。\n\n- **HTTP 代理服务器**：带身份验证的完整异步实现\n- **SOCKS5 代理服务器**：二进制协议处理、TCP 隧道\n- **代理链**：分层匿名（以及延迟权衡）\n- **旋转代理池**：健康检查、故障转移、负载均衡\n- **高级主题**：透明代理、MITM SSL 拦截\n\n**为什么要构建自己的**：了解实现细节可以揭示从外部看不到的 **攻击向量** 和 **优化机会**。\n\n### 6. 法律与道德考量\n**[法律与道德准则](./proxy-legal.md)**\n\n驾驭代理使用和网络自动化的法律雷区。\n\n- **法规遵从**：GDPR, CFAA, 国际法律\n- **服务条款**：什么构成违规\n- **道德准则**：robots.txt, 速率限制, 透明度\n- **案例研究**：法律先例 (hiQ vs LinkedIn, QVC vs Resultly)\n- **何时避免使用代理**：高风险场景\n\n**免责声明**：这是 **教育信息**，不是法律建议。法律因司法管辖区和用例而异。请咨询合格的法律顾问。\n\n## 代理悖论\n\n关于代理，有一个令人不安的事实：\n\n!!! warning \"代理不会让你匿名。它们让你 **与众不同**\"\n    代理会更改您的 IP 地址，但它也会：\n    \n    - 增加 **延迟**（可通过计时分析检测）\n    - 重置 **TTL** 值（暴露代理跳数）\n    - 引入 **TCP 指纹** 不匹配（代理操作系统 ≠ 您的操作系统）\n    - 可能注入 **标头** (X-Forwarded-For, Via)\n    - 造成 **地理位置** 不一致（浏览器时区 ≠ IP 位置）\n    \n    代理是一种 **工具**，而不是一个解决方案。真正的隐蔽需要 **全面的一致性**。\n\n## 先决条件\n\n这是 **高级材料**。您应该熟悉：\n\n- 基本网络概念（IP 地址、端口、协议）\n- TCP/IP 基础（三次握手、数据包、路由）\n- 异步 Python 编程 (asyncio, async/await)\n- Pydoll 基础知识 (参见 [核心概念](../../features/core-concepts.md))\n\n**如果您对网络不熟悉**，我们强烈建议您：\n\n1.  首先阅读 TCP/IP 基础指南\n2.  尝试使用 Wireshark 来可视化网络流量\n3.  在运行数据包捕获的同时尝试代码示例\n4.  构建代理服务器并在本地测试它们\n\n## 与其他模块的集成\n\n网络架构并非孤立存在。它与以下内容深度集成：\n\n- **[指纹识别](../fingerprinting/network-fingerprinting.md)**：TCP/IP 和 TLS 特征如何识别您\n- **[浏览器配置](../../features/configuration/browser-preferences.md)**：使浏览器行为与代理特征保持一致\n- **[连接层](../fundamentals/connection-layer.md)**：Pydoll 如何通过代管理 WebSocket 连接\n\n## 学习路径\n\n我们推荐以下进阶路径：\n\n**阶段 1：基础**\n1.  阅读 [网络基础](./network-fundamentals.md)\n2.  理解 OSI 模型和协议分层\n3.  了解 WebRTC 泄露和 UDP 隧道\n\n**阶段 2：协议深度探讨**\n4.  学习 [HTTP/HTTPS 代理](./http-proxies.md)\n5.  掌握 [SOCKS 代理](./socks-proxies.md)\n6.  比较协议并理解权衡\n\n**阶段 3：对抗性思维**\n7.  探索 [代理检测](./proxy-detection.md)\n8.  从防御者的角度学习检测技术\n9.  应用规避策略\n\n**阶段 4：动手实践**\n10. 从 [构建代理](./build-proxy.md) 构建代理服务器\n11. 使用 Wireshark 捕获和分析流量\n12. 测试代理链和轮换策略\n\n**阶段 5：操作安全**\n13. 查看 [法律与道德](./proxy-legal.md) 准则\n14. 理解合规性要求\n15. 制定负责任的自动化策略\n\n\n## 理念\n\n网络和安全知识是 **基础性的力量**。与特定框架的技能（会过时）不同，协议知识是 **永恒的**：\n\n- TCP 自 RFC 793 (1981) 以来没有根本改变\n- TLS 建立在 SSL (1995) 的概念之上\n- HTTP/2 (2015) 和 HTTP/3 (2022) 是演进，而不是革命\n\n一次掌握这些概念，您将在职业生涯的剩余时间里理解您遇到的 **每一个基于网络的系统**。\n\n## 道德承诺\n\n在继续之前，请确认：\n\n我理解代理可用于合法和恶意目的\n我将尊重网站的服务条款和 robots.txt\n我将实现速率限制和友好的爬行\n我不会将此知识用于欺诈、滥用或非法活动\n当不确定合规性时，我将咨询法律顾问\n\n**能力越大，责任越大。** 请明智地使用这些知识。\n\n---\n\n## 准备好开始了吗？\n\n从 **[网络基础](./network-fundamentals.md)** 开始您的旅程，以构建基础，然后按顺序浏览各个模块。每个文档都建立在“前一个”文档的基础之上，从而全面了解用于自动化的网络架构。\n\n**这是脚本小子成为工程师的地方。让我们开始吧。**\n\n---\n\n!!! info \"文档状态\"\n    本模块综合了来自 RFC、协议规范、安全研究和真实世界测试的知识。每个代码示例都是可用于生产的。如果您发现不准确之处或有改进意见，欢迎贡献。\n\n## 快速导航\n\n**核心协议：**\n- [网络基础](./network-fundamentals.md) - TCP/IP, UDP, WebRTC\n- [HTTP/HTTPS 代理](./http-proxies.md) - 应用层代理\n- [SOCKS 代理](./socks-proxies.md) - 会话层代理\n\n**高级主题：**\n- [代理检测](./proxy-detection.md) - 匿名与规避\n- [构建代理](./build-proxy.md) - 从头开始实现\n- [法律与道德](./proxy-legal.md) - 合规与责任\n\n**相关模块：**\n- [指纹识别](../fingerprinting/index.md) - 检测技术\n- [浏览器配置](../../features/configuration/browser-options.md) - 实际设置"
  },
  {
    "path": "docs/zh/deep-dive/network/network-fundamentals.md",
    "content": "# 网络基础\n\n本文档涵盖了驱动互联网的基础网络协议，以及它们如何在自动化场景中暴露或保护您的身份。充分理解 TCP、UDP、OSI 模型和 WebRTC，将使代理配置不再神秘，并且更加有效。\n\n!!! info \"模块导航\"\n    - [网络与安全概述](./index.md)：模块介绍和学习路径\n    - [HTTP/HTTPS 代理](./http-proxies.md)：应用层代理\n    - [SOCKS 代理](./socks-proxies.md)：会话层代理\n\n    有关 Pydoll 的实际用法，请参阅[代理配置](../../features/configuration/proxy.md)和[浏览器选项](../../features/configuration/browser-options.md)。\n\n## 网络堆栈\n\n浏览器发出的每一个 HTTP 请求都会经过一个分层的网络堆栈。每一层都有特定的职责、协议和安全影响。代理在不同的层运行，运行的层决定了代理能看到、修改和隐藏什么。低层的网络特征即使通过代理也能对您的真实系统进行 fingerprinting，因此理解协议栈有助于了解身份泄露发生在哪里，以及如何防止它们。\n\n### OSI 模型\n\nOSI（开放系统互连）模型由 ISO 于 1984 年制定，提供了一个概念框架来理解网络协议是如何交互的。现实世界的网络使用 TCP/IP 模型（早于 OSI，只有 4 层），但 OSI 术语仍然是描述代理运行位置及其可访问内容的标准方式。\n\n```mermaid\ngraph TD\n    L7[Layer 7: Application - HTTP, FTP, SMTP, DNS]\n    L6[Layer 6: Presentation - Encryption, Compression]\n    L5[Layer 5: Session - SOCKS]\n    L4[Layer 4: Transport - TCP, UDP]\n    L3[Layer 3: Network - IP, ICMP]\n    L2[Layer 2: Data Link - Ethernet, WiFi]\n    L1[Layer 1: Physical - Cables, Radio Waves]\n\n    L7 --> L6 --> L5 --> L4 --> L3 --> L2 --> L1\n```\n\n第 7 层（应用层）是面向用户的协议所在层：HTTP、HTTPS、FTP、SMTP 和 DNS 都在这里运行。这一层包含应用程序关心的实际数据，如 HTML 文档、JSON 响应和文件传输。HTTP 代理在这一层运行，因此对请求和响应内容具有完全的可见性。\n\n第 6 层（表示层）处理数据格式转换、加密和压缩。SSL/TLS 通常与这一层关联，因为它承担加密职责，但实际上 TLS 横跨第 4 层到第 6 层，无法干净地映射到单个 OSI 层。对自动化来说重要的是，HTTPS 加密发生在这里，在数据传递到下层之前对第 7 层数据进行加密。\n\n第 5 层（会话层）管理应用程序之间的连接。SOCKS 代理在这一层运行，低于应用层但高于传输层。这个位置使得 SOCKS 与协议无关：它可以代理任何第 7 层协议（HTTP、FTP、SMTP、SSH），而无需理解它们的具体内容。\n\n第 4 层（传输层）提供端到端的数据传输。TCP（面向连接、可靠）和 UDP（无连接、快速）是这一层的主要协议。这一层处理端口号、流量控制和错误纠正。所有代理最终都依赖第 4 层进行实际的数据传输。\n\n第 3 层（网络层）处理网络之间的路由和寻址。IP（互联网协议）在这一层运行，管理 IP 地址和路由决策。您的真实 IP 地址就在这一层，也是代理试图替换它的地方。\n\n第 2 层（数据链路层）管理同一物理网段上的通信。以太网、Wi-Fi 和 PPP 在这里运行，处理 MAC 地址和帧传输。MAC 地址仅在本地网段可见，远程服务器无法直接访问，但它们可能通过 IPv6 SLAAC（将 MAC 嵌入地址中）等协议暴露。\n\n第 1 层（物理层）是实际的硬件：电缆、无线电波和电压水平。与软件自动化几乎无关。\n\n!!! tip \"OSI vs TCP/IP\"\n    TCP/IP 模型（4 层：链路层、互联网层、传输层、应用层）是网络实际使用的模型。OSI（7 层）是教学工具和参考模型。当人们说\"第 7 层代理\"时，他们使用的是 OSI 术语，但实际实现运行在 TCP/IP 上。\n\n### 层级位置如何影响代理\n\n代理运行的层级决定了它能做什么和不能做什么。\n\nHTTP/HTTPS 代理运行在第 7 层（应用层）。因为它们理解 HTTP，所以可以读取和修改 URL、标头、Cookie 和请求正文。它们可以根据 HTTP 语义智能缓存响应，按 URL 或关键字过滤内容，以及注入身份验证标头。代价是它们只理解 HTTP。它们无法代理 FTP、SMTP、SSH 或其他协议，而且检查 HTTPS 内容需要 TLS 终止，即解密然后重新加密流量。\n\nSOCKS 代理运行在第 5 层（会话层）。因为它们位于应用层之下，所以与协议无关，可以在不修改的情况下代理任何第 7 层协议。HTTPS 流量以端到端加密方式通过，因为 SOCKS 代理无需对其解密。SOCKS5 还支持 UDP，使其能够代理 DNS 查询、VoIP 和其他基于 UDP 的协议。代价是 SOCKS 代理对应用层数据没有可见性：它们无法缓存、按 URL 过滤或检查内容。它们只能按 IP 和端口进行过滤。\n\n!!! note \"根本性的权衡\"\n    更高层（第 7 层）给您更多控制，但灵活性更低。更低层（第 5 层）给您更少控制，但灵活性更高。需要内容控制时选择 HTTP 代理，需要协议灵活性或端到端加密时选择 SOCKS 代理。\n\n### 层泄露问题\n\n即使有完美的第 7 层代理，低层的特征也能暴露您的真实身份。您操作系统的 TCP 堆栈在第 4 层有独特的 fingerprint，由窗口大小、选项顺序和 TTL 值定义。第 3 层的 IP 标头字段（如 TTL 和分片行为）会揭示您的操作系统和网络拓扑。\n\n例如，如果您配置代理来显示 \"Windows 10\" 的 User-Agent，但您实际 Linux 系统的 TCP fingerprint 在第 4 层与此相矛盾，复杂的检测系统就能将这种不一致标记为强烈的机器人指标。这就是网络级 fingerprinting（在[网络指纹](../fingerprinting/network-fingerprinting.md)中介绍）如此危险的原因：它运行在代理层之下，即使应用层代理完美无缺，也会暴露您的真实系统。\n\n## TCP vs UDP\n\n在第 4 层（传输层），两种根本不同的协议主导着互联网通信。它们代表了相反的设计理念：可靠性与速度。\n\nTCP 是面向连接的。可以把它想象成打电话：您建立连接，确认对方正在收听，可靠地交换数据，然后挂断。每个字节都会被确认、排序，并保证到达。UDP 是无连接的。您发送数据，希望它能到达。没有 handshake，没有确认，没有保证。只有原始的速度和最小的开销。\n\n| 特性 | TCP | UDP |\n|---------|-----|-----|\n| 连接 | 面向连接（需要 handshake） | 无连接（无需 handshake） |\n| 可靠性 | 保证交付，有序数据包 | 尽力而为交付，数据包可能丢失 |\n| 速度 | 较慢（可靠性机制带来开销） | 较快（最小开销） |\n| 用例 | 网页浏览、文件传输、电子邮件 | 视频流、DNS 查询、游戏 |\n| 标头大小 | 最小 20 字节（带选项时可达 60） | 固定 8 字节 |\n| 流量控制 | 有（滑动窗口，接收方驱动） | 无（发送方随意传输） |\n| 拥塞控制 | 有（网络拥塞时减速） | 无（应用程序的责任） |\n| 错误检查 | 广泛（校验和 + 确认） | 基本（仅校验和；在 IPv4 中可选，在 IPv6 中强制） |\n| 排序 | 乱序接收时重新排序数据包 | 无排序，按接收顺序交付 |\n| 重传 | 自动（丢失的数据包会重传） | 无（应用程序必须处理） |\n\n### TCP 与代理\n\n所有代理协议（HTTP、HTTPS、SOCKS4、SOCKS5）都使用 TCP 作为其控制通道。这是因为代理身份验证和命令交换需要保证交付，代理协议有严格的命令序列（handshake，然后认证，然后数据），代理需要持久连接来跟踪客户端状态。\n\n然而，SOCKS5 还可以代理 UDP 流量，这与 SOCKS4 或 HTTP 代理不同。这使得 SOCKS5 对于代理 DNS 查询、WebRTC 音频/视频、VoIP 和游戏协议至关重要。\n\n!!! danger \"UDP 与 IP 泄露\"\n    大多数浏览器连接使用 TCP（HTTP、WebSocket 等），但 WebRTC 直接使用 UDP，绕过了浏览器的代理配置。这是代理浏览器自动化中 IP 泄露的最常见原因：您的 TCP 流量通过代理传输，而 UDP 流量却泄露了您的真实 IP。\n\n### TCP 三次握手\n\n在传输任何数据之前，TCP 需要三次 handshake 来建立连接。这个协商过程同步序列号，商定窗口大小，并在两端建立连接状态。\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant Server\n\n    Client->>Server: SYN (Synchronize, seq=x)\n    Note over Client,Server: Client requests connection\n\n    Server->>Client: SYN-ACK (seq=y, ack=x+1)\n    Note over Client,Server: Server acknowledges and sends its own SYN\n\n    Client->>Server: ACK (ack=y+1)\n    Note over Client,Server: Connection established, data transfer begins\n```\n\n该过程从客户端发送 SYN（同步）包开始，其中包含一个随机的初始序列号（ISN），例如 `seq=1000`。除了 ISN 之外，还会协商 TCP 选项：窗口大小、最大分段大小（MSS）、时间戳和 SACK 支持。\n\n服务器以 SYN-ACK 响应：它选择自己的随机 ISN（例如 `seq=5000`），并通过设置 `ack=1001`（客户端的 ISN + 1）来确认客户端的 ISN。这个单一的包既建立了服务器到客户端的方向（SYN），又确认了客户端到服务器的方向（ACK）。服务器还会返回自己的 TCP 选项。\n\n然后客户端发送最终的 ACK，确认服务器的 ISN（`ack=5001`）。此时连接在两个方向上都已完全建立，数据传输可以开始。\n\nISN 是随机化的而非从零开始，以防止 TCP 劫持攻击。如果 ISN 是可预测的，攻击者可以通过猜测序列号将数据包注入到现有连接中。现代系统使用加密随机性来选择 ISN（RFC 6528）。\n\n### TCP Fingerprinting\n\nTCP handshake 揭示了能够 fingerprint 您操作系统的特征。不同操作系统对初始窗口大小、TCP 选项顺序、TTL（生存时间）、窗口缩放因子和时间戳行为使用不同的默认值。这些值由内核设置，而不是浏览器，因此代理无法更改它们。\n\n以下是现代操作系统的示例值。请注意，实际值因操作系统版本、内核配置和网络调优而异：\n\n```\nWindows 10/11 (modern builds):\n    Window Size: 65535\n    MSS: 1460\n    Options: MSS, NOP, WS, NOP, NOP, SACK_PERM\n    TTL: 128\n\nLinux (kernel 5.x+, Ubuntu 20.04+):\n    Window Size: 29200\n    MSS: 1460\n    Options: MSS, SACK_PERM, TS, NOP, WS\n    TTL: 64\n\nmacOS (Monterey+):\n    Window Size: 65535\n    TTL: 64\n```\n\n这些差异烙印在内核中。代理无法更改它们，因为它们是由您的操作系统而不是浏览器设置的。这就是复杂的检测系统即使通过代理也能识别您的原因。\n\n!!! warning \"代理的局限性\"\n    HTTP 和 SOCKS 代理运行在 TCP 层之上。它们无法修改 TCP handshake 特征。您操作系统的 TCP fingerprint 始终暴露给代理服务器以及您和代理之间的任何网络观察者。只有 VPN 级别的解决方案或操作系统级的 TCP 堆栈配置才能解决这个问题。\n\n!!! note \"TCP Fingerprinting 之外\"\n    TCP handshake 只是第一个 fingerprinting 机会。紧接着，TLS handshake 会揭示另一个独特的 fingerprint，即 JA3/JA4。详情请参阅[网络指纹](../fingerprinting/network-fingerprinting.md)。\n\n### UDP\n\n与 TCP 可靠、面向连接的方法不同，UDP 是一种\"发射后不管\"的协议。它以可靠性换取最小的延迟和开销，使其成为实时应用的理想选择，因为在这些应用中速度比完美交付更重要。\n\nUDP 数据报只有 8 字节标头（相比 TCP 的 20-60 字节），包含源端口、目标端口、长度和校验和。没有连接建立，没有可靠性保证，没有流量控制，没有拥塞控制。如果数据包丢失，应用程序必须自行决定是否以及如何处理。\n\nUDP 适用于实时通信（通过 WebRTC 和 VoIP 进行的语音/视频通话）、游戏（低延迟的状态更新）、流媒体（偶尔的帧丢失可以接受）和 DNS 查询（小型请求/响应对，应用程序处理重试）。它不适合文件传输、网页浏览、电子邮件或数据库，这些都需要可靠、有序的交付。\n\nDNS 在自动化上下文中是一个特别重要的例子。DNS 使用 UDP 是因为查询通常很小，并且受益于 UDP 零 handshake 的开销优势。虽然 EDNS0（RFC 6891）将最大 UDP DNS 负载增加到了原始 512 字节限制之上，但大多数查询仍然很紧凑。如果响应未在超时时间内到达，DNS 客户端会在应用层处理重试。\n\n对于浏览器自动化，UDP 的关键问题是 WebRTC 使用它进行实时音频和视频，DNS 查询使用它进行域名解析，而大多数代理（HTTP、HTTPS、SOCKS4）只处理 TCP。除非您显式配置 UDP 代理，否则这些流量会绕过您的代理并泄露您的真实 IP。\n\n| 代理类型 | UDP 支持 | 说明 |\n|------------|-------------|-------|\n| HTTP 代理 | 否 | 只代理基于 TCP 的 HTTP/HTTPS |\n| HTTPS 代理 (CONNECT) | 否 | CONNECT 方法只建立 TCP 隧道 |\n| SOCKS4 | 否 | 仅 TCP 协议 |\n| SOCKS5 | 是 | 通过 `UDP ASSOCIATE` 命令支持 UDP 中继 |\n| VPN | 是 | 隧道传输所有 IP 流量（TCP 和 UDP） |\n\n为了在浏览器自动化中实现真正的匿名，您需要：支持 UDP 的 SOCKS5 代理并将 WebRTC 配置为使用它、完全禁用 WebRTC（这会破坏视频会议）、隧道传输所有流量的 VPN，或者浏览器标志 `--force-webrtc-ip-handling-policy=disable_non_proxied_udp`。\n\n### QUIC 与 HTTP/3\n\n现代浏览器越来越多地使用 QUIC（RFC 9000），这是一种基于 UDP 的传输协议，为 HTTP/3 提供支持。由于 QUIC 运行在 UDP 上，它与 WebRTC 和 DNS 存在相同的代理绕过问题：大多数 HTTP 代理无法处理 QUIC 流量，它可能会泄露到您的代理配置之外。\n\n在自动化场景中，考虑使用 `--disable-quic` Chrome 标志禁用 QUIC，以强制使用基于 TCP 的 HTTP/2，确保所有网页流量通过您的代理。QUIC 还有自己的 fingerprinting 特征，类似于 TLS 的 JA3，增加了另一个检测向量。\n\n## WebRTC 与 IP 泄露\n\nWebRTC（Web 实时通信）是 W3C 标准化的浏览器 API，支持浏览器之间直接进行点对点的音频、视频和数据通信，无需插件或中介服务器。虽然 WebRTC 对实时应用功能强大，但它是代理浏览器自动化中最大的 IP 泄露源。\n\n### WebRTC 如何泄露您的 IP\n\nWebRTC 专为直接的点对点连接设计，优先考虑低延迟而非隐私。为了建立 P2P 连接，WebRTC 必须发现您的真实公共 IP 地址并与远程对等方共享，即使您的浏览器配置为使用代理。\n\n问题是这样展开的：您的浏览器使用代理处理 HTTP/HTTPS 流量（即 TCP），但 WebRTC 使用 STUN 服务器通过 UDP 发现您的真实公共 IP。STUN 查询绕过代理，因为大多数代理只处理 TCP。您的真实 IP 被发现并作为连接协商的一部分与远程对等方共享。页面上的 JavaScript 可以读取这些\"ICE 候选者\"并将您的真实 IP 发送到网站的服务器。\n\n!!! danger \"WebRTC 泄露的严重性\"\n    即使正确配置了 HTTP 代理、HTTPS 代理工作正常、DNS 查询被代理、User-Agent 被伪造、canvas fingerprinting 被缓解，WebRTC 仍然可以在毫秒内泄露您的真实 IP。这是因为 WebRTC 运行在浏览器的代理层之下，直接与操作系统的网络堆栈交互。\n\n### ICE 过程\n\nWebRTC 使用 ICE（交互式连接建立，RFC 8445）来发现可能的连接路径并选择最佳路径。这个过程本身就会揭示您的网络拓扑，它收集三种类型的候选者。\n\n```mermaid\nsequenceDiagram\n    participant Browser\n    participant STUN as STUN Server\n    participant TURN as TURN Relay\n    participant Peer as Remote Peer\n\n    Note over Browser: WebRTC connection initiated\n\n    Browser->>Browser: Gather local IP addresses<br/>(LAN interfaces)\n    Note over Browser: Local candidate:<br/>192.168.1.100:54321\n\n    Browser->>STUN: STUN Binding Request (over UDP)\n    Note over STUN: STUN server discovers public IP<br/>(bypasses proxy!)\n    STUN->>Browser: STUN Response with real public IP\n    Note over Browser: Server reflexive candidate:<br/>203.0.113.45:54321\n\n    Browser->>TURN: Allocate relay (if needed)\n    TURN->>Browser: Relay address assigned\n    Note over Browser: Relay candidate:<br/>198.51.100.10:61234\n\n    Browser->>Peer: Send all ICE candidates<br/>(local + public + relay)\n    Note over Peer: Now knows your:<br/>- LAN IP<br/>- Real public IP<br/>- Relay address\n\n    Peer->>Browser: Send ICE candidates\n\n    Note over Browser,Peer: ICE negotiation: try direct P2P first\n\n    alt Direct P2P succeeds\n        Browser<<->>Peer: Direct connection (bypasses proxy entirely!)\n    else Direct P2P fails (firewall/NAT)\n        Browser->>TURN: Use TURN relay\n        TURN<<->>Peer: Relayed connection\n        Note over Browser,Peer: Higher latency, but works\n    end\n```\n\n### ICE 候选者类型\n\nICE 发现三种类型的候选者（可能的连接端点），每种类型揭示关于您网络的不同信息。\n\n**主机候选者**是您的本地局域网 IP 地址。浏览器枚举所有本地网络接口，并为每个接口创建候选者。这会揭示您在专用网络上的本地 IP 地址、网络拓扑（是否存在 VPN 接口、虚拟机网桥）以及网络接口的数量。\n\n```javascript\n// Example host candidates\ncandidate:1 1 UDP 2130706431 192.168.1.100 54321 typ host\ncandidate:2 1 UDP 2130706431 10.0.0.5 54322 typ host\n```\n\n现代浏览器（Chrome 75+、Firefox 78+、Safari）通过在未授予媒体权限（摄像头/麦克风）时将本地 IP 地址替换为临时的 mDNS 名称（例如 `a1b2c3d4.local`）来缓解主机候选者泄露。然而，无论 mDNS 如何，服务器自反候选者（您的公共 IP）仍然会暴露。\n\n**服务器自反候选者**是 STUN 服务器看到的您的公共 IP。浏览器向公共 STUN 服务器发送请求，后者回复您的公共 IP 地址。这就是人们常说的泄露：您的代理显示一个 IP，但 WebRTC 揭示了您的真实 IP，连同您的 NAT 类型、外部端口映射和 ISP 信息。\n\n```javascript\n// Server reflexive candidate (your real public IP)\ncandidate:4 1 UDP 1694498815 203.0.113.45 54321 typ srflx raddr 192.168.1.100 rport 54321\n```\n\n**中继候选者**是在直接 P2P 失败时用作后备的 TURN 服务器地址。根据 TURN 服务器实现的不同，中继候选者的 `raddr`（远程地址）字段中可能仍然包含您的真实 IP。\n\n```javascript\n// Relay candidate (TURN server address)\ncandidate:5 1 UDP 16777215 198.51.100.10 61234 typ relay raddr 203.0.113.45 rport 54321\n```\n\n### STUN 协议\n\nSTUN（NAT 会话穿透实用工具，RFC 8489）是一个简单的基于 UDP 的请求-响应协议。它的工作很直接：客户端询问\"您看到的我是什么 IP？\"，服务器回复客户端的公共 IP 和端口。\n\n客户端发送一个绑定请求，其中包含一个魔法 Cookie（`0x2112A442`，RFC 定义的固定值）和一个随机的 12 字节事务 ID。服务器响应一个绑定成功响应，其中包含一个 `XOR-MAPPED-ADDRESS` 属性，包含从服务器角度看到的客户端公共 IP 和端口。\n\n响应中的 IP 地址与魔法 Cookie 和事务 ID 进行了异或运算。这不是为了安全，而是为了 NAT 兼容性：一些 NAT 设备会错误地修改数据包负载中的 IP 地址，异或运算混淆了地址以防止这种干扰。\n\n浏览器常用的公共 STUN 服务器包括 `stun.l.google.com:19302`（Google）、`stun1.l.google.com:19302`（Google）、`stun.services.mozilla.com`（Mozilla）和 `stun.stunprotocol.org:3478`。\n\n### 为什么代理无法阻止 WebRTC 泄露\n\nWebRTC 泄露发生有几个相互强化的原因。首先，WebRTC 使用 UDP，而大多数代理（HTTP、HTTPS CONNECT、SOCKS4）只处理 TCP。只有 SOCKS5 支持 UDP，即便如此，浏览器也必须显式配置为通过它路由 WebRTC。\n\n其次，WebRTC 是一个运行在 HTTP 层之下的浏览器 API。它直接访问操作系统网络堆栈，绕过为 HTTP/HTTPS 配置的代理设置。STUN 查询直接进入网络接口，操作系统路由表决定它们的路径，而不是浏览器的代理配置。只有 VPN 级别的路由才能拦截它们。\n\n第三，WebRTC 枚举所有网络接口（物理以太网、Wi-Fi、VPN 适配器、虚拟机网桥），包括未用于常规浏览的接口。这会泄露您的内部网络拓扑。\n\n最后，网页可以通过 JavaScript 使用 `RTCPeerConnection.onicecandidate` 事件读取 ICE 候选者，用简单的正则表达式从候选者字符串中提取 IP 地址，并将您的真实 IP 发送到他们的跟踪服务器。\n\n### 在 Pydoll 中防止 WebRTC 泄露\n\nPydoll 提供了多种策略来防止 WebRTC IP 泄露。\n\n**方法 1：强制 WebRTC 仅使用代理路由（推荐）**\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.webrtc_leak_protection = True  # Adds --force-webrtc-ip-handling-policy=disable_non_proxied_udp\n```\n\nPydoll 提供了一个便捷的 `webrtc_leak_protection` 属性来管理底层的 Chrome 标志。这会在没有代理支持 UDP 时禁用 UDP，强制 WebRTC 仅使用 TURN 中继（不使用直接 P2P），并阻止对公共服务器的 STUN 查询。代价是视频通话的延迟更高，因为直接 P2P 连接被禁用。\n\n**方法 2：完全禁用 WebRTC**\n\n```python\noptions.add_argument('--disable-features=WebRTC')\n```\n\n这会完全禁用 WebRTC API，消除通过此向量发生 IP 泄露的任何可能性。代价是所有依赖 WebRTC 的网站（视频会议、语音通话）将无法工作。请注意，此标志应在您的特定 Chrome 版本上测试，因为功能标志名称可能因版本而异。\n\n**方法 3：通过浏览器首选项限制 WebRTC**\n\n```python\noptions.browser_preferences = {\n    'webrtc': {\n        'ip_handling_policy': 'disable_non_proxied_udp',\n        'multiple_routes_enabled': False,\n        'nonproxied_udp_enabled': False,\n        'allow_legacy_tls_protocols': False\n    }\n}\n```\n\n这与方法 1 效果相同，但通过首选项而非命令行标志实现。`multiple_routes_enabled` 防止使用多个网络路径，`nonproxied_udp_enabled` 阻止不通过代理的 UDP。\n\n**方法 4：使用支持 UDP 的 SOCKS5 代理**\n\n```python\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\noptions.add_argument('--force-webrtc-ip-handling-policy=default_public_interface_only')\n```\n\nSOCKS5 可以通过其 `UDP ASSOCIATE` 命令代理 UDP，允许 WebRTC 的 STUN 查询通过代理。这需要实际支持 UDP 中继的 SOCKS5 代理，而并非所有代理都支持。\n\n!!! warning \"SOCKS5 身份验证\"\n    Chrome 不支持通过 `--proxy-server` 标志内联 SOCKS5 身份验证（例如 `socks5://user:pass@host:port`）。Pydoll 提供了内置的 `SOCKS5Forwarder` 来解决此限制，它运行一个本地无需身份验证的 SOCKS5 代理，将流量转发到远程经过身份验证的代理，代替 Chrome 处理用户名/密码 handshake。详情请参阅[代理配置](../../features/configuration/proxy.md)。\n\n### 测试 WebRTC 泄露\n\n您可以通过访问 [browserleaks.com/webrtc](https://browserleaks.com/webrtc) 并检查\"Public IP Address\"部分来手动测试。如果您看到的是您的真实 IP 而不是代理 IP，则说明存在泄露。\n\n使用 Pydoll 进行自动化测试：\n\n```python\nimport asyncio\nfrom pydoll.browser import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def test_webrtc_leak():\n    options = ChromiumOptions()\n    options.add_argument('--proxy-server=http://proxy.example.com:8080')\n    options.add_argument('--force-webrtc-ip-handling-policy=disable_non_proxied_udp')\n\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://browserleaks.com/webrtc')\n\n        await asyncio.sleep(3)\n\n        ips = await tab.execute_script('''\n            return Array.from(document.querySelectorAll('.ip-address'))\n                .map(el => el.textContent.trim());\n        ''')\n\n        print(\"Detected IPs:\", ips)\n        # Should only show proxy IP, not your real IP\n\nasyncio.run(test_webrtc_leak())\n```\n\n!!! danger \"务必测试 WebRTC 泄露\"\n    切勿假设您的代理配置能阻止 WebRTC 泄露。始终使用 [browserleaks.com/webrtc](https://browserleaks.com/webrtc) 或 [ipleak.net](https://ipleak.net) 进行验证。即使是单个 WebRTC 泄露也会立即危及您的整个代理设置，因为网站现在知道了您的真实位置、ISP 和网络拓扑。\n\n### 网站如何利用 WebRTC 泄露\n\n网站可以用几行 JavaScript 有意触发 WebRTC 来提取您的真实 IP：\n\n```javascript\nconst pc = new RTCPeerConnection({\n    iceServers: [{urls: 'stun:stun.l.google.com:19302'}]\n});\n\npc.createDataChannel('');\npc.createOffer().then(offer => pc.setLocalDescription(offer));\n\npc.onicecandidate = (event) => {\n    if (event.candidate) {\n        const ipRegex = /([0-9]{1,3}(\\.[0-9]{1,3}){3})/;\n        const ipMatch = event.candidate.candidate.match(ipRegex);\n\n        if (ipMatch) {\n            const realIP = ipMatch[1];\n            fetch(`/track?real_ip=${realIP}&proxy_ip=${window.clientIP}`);\n        }\n    }\n};\n```\n\n这段代码创建一个 RTCPeerConnection，触发 ICE 候选者收集（联系 STUN 服务器），用正则表达式从候选者中提取 IP 地址，并将您的真实 IP 发送到跟踪服务器。按照上述方法禁用 WebRTC 或强制仅使用代理路由可以防止这种情况。\n\n## 总结\n\n代理运行在网络堆栈的特定层：HTTP 在第 7 层，SOCKS 在第 5 层。层级决定了代理能看到、修改和隐藏什么。TCP fingerprint（窗口大小、选项、TTL）从低层泄露，即使通过代理也会揭示您的真实操作系统。UDP 流量（包括 WebRTC 和 DNS）除非显式配置，否则通常会绕过代理。WebRTC 是 IP 泄露最常见的来源，只有 SOCKS5 或 VPN 才能有效代理 UDP 流量。现代浏览器还使用 QUIC（基于 UDP 的 HTTP/3），增加了另一个潜在的绕过向量。\n\n**后续步骤：**\n\n- [HTTP/HTTPS 代理](./http-proxies.md)：应用层代理\n- [SOCKS 代理](./socks-proxies.md)：会话层、协议无关的代理\n- [网络指纹](../fingerprinting/network-fingerprinting.md)：TCP/IP 和 TLS fingerprinting 技术\n- [代理配置](../../features/configuration/proxy.md)：实用 Pydoll 代理设置\n\n## 参考资料\n\n- RFC 793: Transmission Control Protocol (TCP) - https://tools.ietf.org/html/rfc793\n- RFC 768: User Datagram Protocol (UDP) - https://tools.ietf.org/html/rfc768\n- RFC 8489: Session Traversal Utilities for NAT (STUN) - https://tools.ietf.org/html/rfc8489\n- RFC 8445: Interactive Connectivity Establishment (ICE) - https://tools.ietf.org/html/rfc8445\n- RFC 8656: Traversal Using Relays around NAT (TURN) - https://tools.ietf.org/html/rfc8656\n- RFC 6528: Defending Against Sequence Number Attacks - https://tools.ietf.org/html/rfc6528\n- RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport - https://tools.ietf.org/html/rfc9000\n- W3C WebRTC 1.0: Real-Time Communication Between Browsers - https://www.w3.org/TR/webrtc/\n- BrowserLeaks: WebRTC Leak Test - https://browserleaks.com/webrtc\n- IPLeak: Comprehensive Leak Testing - https://ipleak.net\n"
  },
  {
    "path": "docs/zh/deep-dive/network/proxy-detection.md",
    "content": "# Proxy 检测\n\nProxy 检测是一个概率性过程。网站结合数十种信号来评估连接是否经过 proxy，从简单的 IP 信誉查询到 TCP/IP 协议栈分析和行为画像，不一而足。任何单一信号都无法提供确定性证据，但将足够多的弱信号组合在一起，就能产生高置信度的判断。\n\n本文档涵盖主要的检测技术、其技术层面的工作原理，以及它们对使用 Pydoll 进行浏览器自动化意味着什么。\n\n!!! info \"模块导航\"\n    - [SOCKS Proxy](./socks-proxies.md)：会话层代理\n    - [HTTP/HTTPS Proxy](./http-proxies.md)：应用层代理\n    - [网络基础](./network-fundamentals.md)：TCP/IP、UDP、WebRTC\n\n    有关 fingerprinting 的详细信息，请参阅[网络 Fingerprinting](../fingerprinting/network-fingerprinting.md) 和[浏览器 Fingerprinting](../fingerprinting/browser-fingerprinting.md)。\n\n## IP 信誉\n\nIP 信誉分析是部署最广泛的 proxy 检测技术。它结合公开可用的数据（ASN 记录、WHOIS、地理定位数据库）和专有情报，将 IP 地址分类为不同的风险等级。\n\n### ASN 分类\n\n每个 IP 地址都属于一个自治系统（AS），由 ASN 标识。拥有该 IP 的 AS 类型是判断其是否为 proxy 的最强单一指标。\n\n属于云服务和托管提供商（AWS、DigitalOcean、OVH、Hetzner）的 IP 被标记为高风险，因为真实用户不会从数据中心服务器浏览网页。来自住宅 ISP（Comcast、Deutsche Telekom、BT）的 IP 风险较低，因为它们看起来像普通的家庭连接。移动运营商 IP（Verizon Wireless、AT&T Mobility）风险最低，因为它们最难与真实的移动用户区分开来。\n\n一些 ASN 与已知的 proxy 基础设施相关联，但这比表面看起来更复杂。像 BrightData 或 Smartproxy 这样的大型住宅 proxy 提供商并不运营自己的 ASN；它们通过属于 ISP ASN 的真实住宅 IP 路由流量。这正是住宅 proxy 比数据中心 proxy 更难检测的原因。\n\n检测系统查询 ASN 数据库（Team Cymru、RIPE NCC、ARIN）和商业 IP 情报 API 来分类每个连接 IP。数据中心 IP 的检测准确率大约在 95% 以上，因为 ASN 分类是明确的。住宅 proxy 的检测要困难得多（准确率大约 40-70%），因为这些 IP 确实属于 ISP。移动 proxy 的检测最为困难（大约 20-40%），因为移动运营商 NAT 使许多真实用户共享 IP。\n\n这种准确率梯度正是住宅和移动 proxy 价格比数据中心 proxy 高出 10-100 倍的原因。\n\n### 已知 Proxy 数据库\n\n除了 ASN 分类之外，专门的数据库会追踪已被观察到参与 proxy 网络的 IP。IPQualityScore、proxycheck.io 和 Spur.us 等服务维护着已知 proxy、VPN 和 Tor 出口节点 IP 的实时数据库。Tor 出口节点列表可在 [check.torproject.org](https://check.torproject.org/torbulkexitlist) 公开获取。\n\n这些数据库还会追踪行为信号：频繁轮换的 IP（proxy 池的典型特征）、并发会话数异常高的 IP（住宅 IP 通常只有 1-5 个并发连接，而不是 100+），以及之前与机器人行为关联的 IP。\n\n### 地理位置一致性\n\nProxy 经常通过地理不一致性暴露自己。IP 地址指向一个位置，但浏览器报告的信号指向另一个位置。\n\n最常见的不匹配包括：IP 地理位置与浏览器时区之间的不匹配（通过 JavaScript 的 `Intl.DateTimeFormat().resolvedOptions().timeZone` 收集）、IP 所在国家与 `Accept-Language` 标头之间的不匹配，以及当前会话位置与上一个会话位置之间的不匹配。一个出现在洛杉矶但浏览器时区为 `Europe/Berlin` 的用户是可疑的。一个在上一次会话位于纽约后 10 分钟出现在东京的用户在物理上是不可能的。\n\n检测系统还会检查 IP 的地理位置是否与浏览器的区域配置匹配。一个美国数据中心 IP 配上 `Accept-Language: zh-CN` 和时区 `Asia/Shanghai`，强烈暗示这是一个通过美国 proxy 路由的中国用户。\n\n!!! note \"误报\"\n    合法场景也会触发地理位置警报：使用 VPN 的旅行者、浏览器设置保留母国配置的外籍人士、通过公司 VPN 连接的企业用户，以及使用非默认语言偏好的多语言用户。成熟的系统使用风险评分而非二元阻断来处理这些情况。\n\n## HTTP 标头分析\n\nHTTP 标头是最简单的检测途径。透明和匿名 proxy 会添加 `Via`、`X-Forwarded-For`、`X-Real-IP` 和 `Forwarded`（RFC 7239）等标头，直接暴露 proxy 的使用。精英 proxy 会剥离这些标头，但仅凭其缺失并不能证明是直连。\n\n检测不仅限于寻找 proxy 专有标头。缺少真实浏览器总会发送的标头（如 `Accept-Language`、`Accept-Encoding` 或真实的 `User-Agent`）也很可疑。标头顺序同样重要：浏览器以一致的、版本特定的顺序发送标头，手动构造标头的 proxy 或自动化工具往往会搞错顺序。\n\n旧版 `Proxy-Connection: keep-alive` 标头是另一个经典的检测信号，某些老旧客户端在通过 proxy 路由时会发送此标头。\n\n### Proxy 匿名级别\n\nProxy 传统上根据其标头行为被分为三个匿名级别：\n\n| 级别 | 行为 | 检测难度 |\n|------|------|----------|\n| 透明 | 在 `X-Forwarded-For` 中转发你的真实 IP，添加 `Via` 标头 | 极易检测 |\n| 匿名 | 隐藏你的 IP 但添加 `Via` 或其他 proxy 标头 | 容易检测 |\n| 精英 | 剥离所有标识 proxy 的标头 | 需要更深入的分析 |\n\n这种分类来自 HTTP 标头分析作为主要检测方法的时代。现代检测系统使用 IP 信誉、fingerprinting 和行为分析，使得透明/匿名/精英的区分不再那么有意义。一个使用数据中心 IP 的精英 proxy 通过 ASN 查询就能立即被检测到。而一个使用住宅 IP 的透明 proxy 在不太成熟的网站上可能仍然不会被发现。\n\n## 网络 Fingerprinting\n\n网络层 fingerprinting 在 proxy 层之下运作，这意味着即使 proxy 本身配置完美，它也能检测到 proxy 的使用。\n\n### TCP/IP Fingerprinting\n\n每个操作系统都有独特的 TCP 协议栈实现，在 TCP 握手过程中会暴露出来。初始窗口大小、TCP 选项顺序、TTL（生存时间）和窗口缩放因子都由内核设置，而非浏览器，且无法被 proxy 更改。\n\n检测系统将这些 TCP 特征与 `User-Agent` 标头进行比较。如果 User-Agent 声称是 Windows 10，但 TCP fingerprint 显示 Linux 特征（TTL 为 64，窗口大小为 29200），这种不匹配就是一个强 proxy 指标。Windows 使用默认 TTL 128，现代版本通常显示窗口大小 65535，而 Linux 使用 TTL 64，窗口大小约 29200。\n\nTTL 分析增加了另一个层面。TTL 在每个网络跳点递减 1。如果一个 Windows 连接到达时 TTL 为 128，客户端很可能在同一网络上。如果到达时 TTL 为 115，则它经过了大约 13 个跳点。如果 TTL 值与 IP 地理位置的预期跳数不一致，则很可能存在 proxy 路由。\n\n有关 TCP fingerprint 值及其含义的详细信息，请参阅[网络 Fingerprinting](../fingerprinting/network-fingerprinting.md)。\n\n### TLS Fingerprinting（JA3/JA4）\n\nTLS ClientHello 消息以明文传输，包含足够的参数来唯一标识客户端应用程序：TLS 版本、支持的密码套件、扩展、椭圆曲线和签名算法。JA3 fingerprint 是将这些参数按特定顺序拼接后的 MD5 哈希值。JA4 是一种更新、更细粒度的替代方案。\n\n每个浏览器版本都会产生独特的 JA3/JA4 fingerprint。检测系统维护着 Chrome、Firefox、Safari 和其他浏览器的已知 fingerprint 数据库。如果 JA3 fingerprint 与任何已知浏览器不匹配，或者与 User-Agent 中声称的浏览器不匹配，该连接就会被标记。\n\n一个重要的细节：SOCKS5 proxy 和 HTTP CONNECT 隧道会原样传递 TLS ClientHello，因此目标服务器看到的是真实浏览器的 fingerprint。在这些配置中，proxy 不会改变 TLS 参数。只有 MITM proxy（终止并重新建立 TLS 连接的 proxy）才会改变 fingerprint，在这种情况下 fingerprint 属于 proxy 软件而非真实浏览器，这本身就是一个检测信号。\n\n### HTTP/2 Fingerprinting\n\nHTTP/2 连接暴露出与 TLS 不同的 fingerprinting 信号。HTTP/2 连接开始时发送的 SETTINGS 帧包含 `HEADER_TABLE_SIZE`、`MAX_CONCURRENT_STREAMS`、`INITIAL_WINDOW_SIZE` 和 `MAX_HEADER_LIST_SIZE` 等参数。每个浏览器对这些设置使用不同的默认值。\n\n伪标头（`:method`、`:authority`、`:scheme`、`:path`）的顺序和优先级、HPACK 压缩行为以及流优先级权重在不同浏览器之间也有所不同。[browserleaks.com/http2](https://browserleaks.com/http2) 等工具可以展示你的 HTTP/2 fingerprint 是什么样的。\n\n实现了自己 HTTP/2 协议栈的自动化框架和 proxy 软件通常会产生与任何真实浏览器都不匹配的 fingerprint，使其成为一种有效的检测途径。\n\n### 基于延迟的检测\n\n客户端与服务器之间的网络延迟揭示了物理网络路径的信息。如果 IP 地理定位在纽约，但往返时间暗示路径经过了亚洲，则该连接很可能经过了 proxy。\n\n检测系统在 TCP 握手期间测量 RTT（往返时间），并将其与 IP 地理位置的预期延迟进行比较。它们还可能发起基于 JavaScript 的计时挑战，从浏览器角度测量延迟，然后将其与服务器观察到的延迟进行比较。两者之间的显著差异暗示路径中存在中间节点（proxy）。\n\n时钟偏移分析增加了另一个维度：通过 JavaScript（`Date.now()`）或 HTTP `Date` 标头测量客户端的时钟偏移量，检测系统可以推断客户端的实际时区，并将其与 IP 预期时区进行比较。\n\n## 行为检测\n\n最先进的检测系统超越了网络和协议分析，转而检查用户行为。这包括请求时序（请求是否均匀间隔，暗示自动化？）、鼠标移动模式（通过 JavaScript 事件监听器分析）、滚动行为、键盘输入节奏以及整体浏览模式。\n\n基于数百万真实用户会话训练的机器学习模型能够以高准确率区分人类行为和自动化行为。这些模型通常结合 50 多个特征，包括导航模式、会话持续时间分布、点击位置、表单交互时序和 JavaScript 执行特征。\n\nPydoll 的人性化交互（贝塞尔曲线鼠标移动、Fitts 定律时序、真实的打字节奏）专为通过行为分析而设计。请参阅[规避技术](../fingerprinting/evasion-techniques.md)了解完整的多层规避策略。\n\n## 多信号风险评分\n\n现代检测系统不依赖任何单一技术。它们将所有可用信号组合成一个风险评分（通常为 0-100），并应用因行业和场景而异的阈值。\n\n每类信号的权重各不相同，但粗略来说，IP 信誉占最大份额（它是最廉价且最可靠的信号），其次是网络 fingerprinting（TCP/IP、TLS、HTTP/2）、标头和协议分析、行为评分，以及一致性检查（地理位置、时区、语言）。\n\n阈值取决于业务场景。银行网站阻断策略激进（风险评分超过 50 即阻断），电商网站在中等评分时展示 CAPTCHA（超过 70），内容网站则更为宽松（仅在超过 80 时阻断），因为它们依赖广告展示量。\n\n这对自动化的启示是，仅通过一层检测是不够的。一个住宅 IP（良好的 IP 信誉）如果配上不匹配的 TCP fingerprint 和机器人行为，仍然会被标记。有效的规避需要所有层面的一致性。\n\n## 按 Proxy 类型划分的检测难度\n\n| Proxy 类型 | 检测难度 | 主要检测方法 |\n|------------|----------|-------------|\n| 透明 HTTP | 极易 | HTTP 标头（`Via`、`X-Forwarded-For`） |\n| 匿名 HTTP | 容易 | HTTP 标头 + IP 信誉 |\n| 精英 HTTP（数据中心） | 中等 | IP 信誉（ASN 分析） |\n| 数据中心 SOCKS5 | 中等 | IP 信誉（ASN 分析） |\n| 住宅 proxy | 困难 | 行为分析、连接模式、延迟 |\n| 移动 proxy | 非常困难 | 主要靠行为分析，网络信号有限 |\n| 轮换 proxy | 困难 | 会话不一致、IP 轮换模式 |\n\n## 规避原则\n\n有效的规避在于所有检测层面的一致性，而不是完善任何单一层面。\n\n当隐蔽性重要时，使用住宅或移动 IP。它们更难被检测，因为这些 IP 确实属于 ISP，价格溢价反映了这一优势。将浏览器的地理位置信号（时区、语言、区域设置）与 proxy IP 的位置匹配。通过不在会话中途轮换 IP 来保持会话持久性，因为这会产生可检测的不连续性。确保你的 TCP/IP fingerprint 与 User-Agent 声明匹配，方法是在你所模拟的相同操作系统上运行自动化。使用 Pydoll 的人性化交互来通过行为分析。并且在大规模运行自动化之前，始终测试是否存在泄露（WebRTC、DNS、时区）。\n\n目标不是使检测变得不可能，而是使其变得昂贵和不确定。迫使检测系统使用多个关联信号，融入合法流量模式，并创造合理的否认空间。\n\n!!! warning \"没有 Proxy 是不可检测的\"\n    拥有足够资源的情况下，任何 proxy 都可以被检测到。即使是顶级住宅 proxy 在面对 Akamai、Cloudflare Enterprise 和 DataDome 等成熟的反机器人系统时，成功率也大约只有 70-90%。实际问题在于，对于目标网站来说，检测是否在经济上值得。\n\n**后续阅读：**\n\n- [网络 Fingerprinting](../fingerprinting/network-fingerprinting.md)：TCP/IP 和 TLS fingerprinting 详解\n- [浏览器 Fingerprinting](../fingerprinting/browser-fingerprinting.md)：Canvas、WebGL、HTTP/2 fingerprinting\n- [规避技术](../fingerprinting/evasion-techniques.md)：多层规避策略\n- [Proxy 配置](../../features/configuration/proxy.md)：Pydoll proxy 实用配置指南\n\n## 参考资料\n\n- MaxMind GeoIP2: https://www.maxmind.com/en/geoip2-services-and-databases\n- IPQualityScore Proxy Detection: https://www.ipqualityscore.com/proxy-vpn-tor-detection-service\n- Spur.us (Anonymous IP Detection): https://spur.us/\n- Team Cymru IP to ASN Mapping: https://www.team-cymru.com/ip-asn-mapping\n- Salesforce Engineering: TLS Fingerprinting with JA3 and JA3S - https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/\n- Akamai: Passive Fingerprinting of HTTP/2 Clients (Black Hat EU 2017) - https://blackhat.com/docs/eu-17/materials/eu-17-Shuster-Passive-Fingerprinting-Of-HTTP2-Clients-wp.pdf\n- Incolumitas: TCP/IP Fingerprinting for VPN and Proxy Detection - https://incolumitas.com/2021/03/13/tcp-ip-fingerprinting-for-vpn-and-proxy-detection/\n- Incolumitas: Detecting Proxies and VPNs with Latencies - https://incolumitas.com/2021/06/07/detecting-proxies-and-vpn-with-latencies/\n- BrowserLeaks HTTP/2 Fingerprint: https://browserleaks.com/http2\n- BrowserLeaks IP: https://browserleaks.com/ip\n- RFC 7239: Forwarded HTTP Extension - https://www.rfc-editor.org/rfc/rfc7239.html\n- RFC 9110: HTTP Semantics - https://www.rfc-editor.org/rfc/rfc9110.html\n"
  },
  {
    "path": "docs/zh/deep-dive/network/proxy-legal.md",
    "content": "# 法律和道德考量\n\n本文档提供了有关代理使用和网络自动化法律及道德环境的 **一般信息**。法律因司法管辖区和用例而异。本文 **不构成法律建议**。请务务必就您的具体情况咨询合格的法律顾问。\n\n!!! info \"模块导航\"\n    - **[← 构建代理](./build-proxy.md)** - 实现与高级主题\n    - **[← 代理检测](./proxy-detection.md)** - 匿名与规避\n    - **[← 网络与安全概述](./index.md)** - 模块介绍\n    \n    有关负责任的自动化，请参阅 **[行为验证码绕过](../../features/advanced/behavioral-captcha-bypass.md)** 和 **[类人交互](../../features/automation/human-interactions.md)**。\n\n!!! danger \"法律免责声明\"\n    本文档仅提供 **教育信息**。它 **不是法律建议**。有关网络抓取、自动化和代理使用的法律因司法管辖区而异，并可能受到解释的影响。在从事可能具有法律影响的活动之前，请咨询合格的法律顾问。\n\n## 法律和道德考量\n\n代理的使用处于隐私、安全和合规的交叉点。了解法律环境对于负责任的自动化至关重要。\n\n### 法规遵从\n\n不同的司法管辖区对代理使用和数据收集有不同的规定：\n\n| 地区 | 关键法规 | 对代理的影响 |\n|---|---|---|\n| **欧盟** | GDPR | IP 地址是个人数据；欧盟的代理出口节点必须合规 |\n| **美国** | CFAA, 州法律 | 规避访问控制可能违反计算机欺诈法 |\n| **中国** | 网络安全法 | VPN/代理使用受到严格监管；只允许经批准的服务 |\n| **俄罗斯** | VPN 法 | VPN 提供商必须注册并记录用户活动 |\n| **澳大利亚** | 隐私法 | 通过代理收集数据受隐私原则约束 |\n\n**GDPR 特定考量：**\n\n**作为个人数据的 IP 地址 (第 4 条)：**\n\n通过代理抓取位于欧盟的网站时：\n\n- 您的代理在欧盟的 IP 被视为个人数据\n- 网站必须按照 GDPR 要求处理它\n- 您必须有合法的数据收集依据\n- 适用数据最小化原则\n\n**处理的合法依据 (第 6 条)：**\n\n1.  **同意** - 难以通过抓取获得\n2.  **合同** - 如果您是客户，则是合法的\n3.  **法律义务** - 罕见于抓取用例\n4.  **重大利益** - 不适用于抓取\n5.  **公共任务** - 不适用于抓取\n6.  **合法利益** - 最适用于抓取 (需要进行平衡测试)\n\n### 服务条款和访问限制\n\n代理不能使您免于遵守网站的服务条款 (ToS)：\n\n**常见的 ToS 违规行为：**\n\n1.  **自动访问**：许多网站禁止机器人/抓取工具，无论 IP 如何\n2.  **规避速率限制**：使用旋转代理绕过速率限制\n3.  **地理限制**：绕过地理封锁可能违反内容许可协议\n4.  **账户共享**：使用代理将多个用户伪装成一个\n\n**法律先例示例：**\n\n```python\n# 著名案例 (简化，非法律建议)\ncases = {\n    'hiQ Labs v. LinkedIn (2022)': {\n        'issue': '在访问被撤销后抓取公共数据',\n        'outcome': '抓取公开可用的数据通常是允许的',\n        'caveat': '但规避技术壁垒可能违反 CFAA'\n    },\n    \n    'QVC v. Resultly (2020)': {\n        'issue': '侵略性抓取导致服务器负载',\n        'outcome': '过度请求构成对动产的侵犯',\n        'implication': '重要的是数量和影响，而不仅仅是技术访问'\n    }\n}\n```\n\n### 代理使用的道德准则\n\n除了法律合规性，还应考虑以下道德原则：\n\n**1. 尊重 robots.txt**\n```python\n# 即使使用代理，也要遵守网站准则\nasync def ethical_scraping(url):\n    # 无论代理匿名性如何，都要检查 robots.txt\n    if not is_allowed_by_robots(url):\n        return None  # 尊重网站的意愿\n```\n\n**2. 速率限制**\n```python\n# 不要滥用代理轮换来压垮服务器\nMINIMUM_DELAY = 1.0  # 请求之间的最小延迟（秒）\nMAX_CONCURRENT = 5   # 每个站点的最大并发连接数\n\n# 错误：轮换代理以 1000 请求/秒的速度抓取\n# 正确：即使使用代理轮换，也要进行友好的抓取\n```\n\n**3. 透明度**\n```python\n# 在适当的时候在 User-Agent 中标识自己\nheaders = {\n    'User-Agent': 'MyBot/1.0 (contact@example.com)',  # 诚实的标识\n    # 而不是: 'Mozilla/5.0...'  # 在不是浏览器时具有欺骗性\n}\n```\n\n**4. 数据最小化**\n```python\n# 只收集您需要的数据\n# 仅仅因为您可以抓取所有内容，并不意味着您应该这样做\ndata_to_collect = {\n    'product_name': True,\n    'price': True,\n    'user_emails': False,      # PII - 除非必要，否则不要收集\n    'user_addresses': False,   # PII - 隐私问题\n}\n```\n\n### 合规性清单\n\n在部署基于代理的自动化之前：\n\n- [ ] **法律审查**：咨询您所在司法管辖区的法律顾问\n- [ ] **ToS 合规性**：审查目标网站的服务条款\n- [ ] **数据保护**：如果处理个人数据，确保符合 GDPR/CCPA\n- [ ] **访问权限**：验证您有权访问数据\n- [ ] **速率限制**：实施友好的请求速率\n- [ ] **错误处理**：适当处理 429 (请求过多)\n- [ ] **日志记录**：保留审计跟踪以用于合规性目的\n- [ ] **数据保留**：实施适当的数据保留/删除策略\n- [ ] **安全**：采取适当措施保护收集的数据\n- [ ] **透明度**：在被问及时，诚实说明您的抓取活动\n\n!!! warning \"这不是法律建议\"\n    本节仅提供一般信息。代理使用的合法性因司法管辖区、上下文和具体情况而异。请务必就您的具体情况咨询合格的法律顾问。\n\n!!! tip \"负责任的代理使用\"\n    最站得住脚的代理用法是：\n    \n    - **透明**：您可以解释为什么这么做\n    - **必要**：您有合法的理由 (研究、监控等)\n    - **相称**：您的方法与您的需求相匹配 (不过度)\n    - **有记录**：您保留了您的活动记录\n    - **合规**：您遵守所有适用的法律和 ToS\n\n### 何时避免使用代理\n\n在某些情况下，使用代理是有问题的：\n\n| 场景 | 风险 | 替代方案 |\n|---|---|---|\n| **银行/金融网站** | 欺诈检测, 账户暂停 | 仅使用合法访问 |\n| **政府门户网站** | 法律处罚, 安全调查 | 从授权位置直接访问 |\n| **医疗保健数据** | 违反 HIPAA, 严厉处罚 | 使用授权的 API 访问 |\n| **内部企业系统** | 违反政策, 终止雇佣 | 遵守公司 IT 政策 |\n| **电子商务账户创建** | 欺诈标记, 永久封禁 | 使用单一、已验证的身份 |\n\n## 结论\n\n深入了解代理架构使您能够：\n\n**做出明智的决策：**\n- 为您的用例选择正确的代理类型\n- 了解安全隐患\n- 确定何时代理是必要的 vs 可选的\n\n**有效地进行故障排除：**\n- 调试连接问题\n- 识别 DNS 泄露或 IP 泄露\n- 诊断性能问题\n\n**优化性能：**\n- 配置适当的超时\n- 实现连接池\n- 监控代理健康状况\n\n**构建更好的自动化：**\n- 将代理与反检测技术相结合\n- 实现健壮的错误处理\n- 高效地扩展代理使用\n\n代理领域是复杂的，但有了这个基础，您就具备了成功驾驭它的能力。\n\n## 进一步阅读\n\n- **[RFC 1928](https://tools.ietf.org/html/rfc1928)**: SOCKS5 协议规范\n- **[RFC 1929](https://tools.ietf.org/html/rfc1929)**: SOCKS5 用户名/密码身份验证\n- **[RFC 2616](https://tools.ietf.org/html/rfc2616)**: HTTP/1.1 (CONNECT 方法)\n- **[RFC 5389](https://tools.ietf.org/html/rfc5389)**: STUN 协议\n- **[RFC 9298](https://tools.ietf.org/html/rfc9298)**: CONNECT-UDP (HTTP/3 代理)\n- **[代理配置指南](../features/configuration/proxy.md)**: 实用的 Pydoll 代理用法、身份验证、轮换和测试\n- **[请求拦截](../features/network/interception.md)**: Pydoll 内部如何实现代理身份验证\n- **[网络能力深度探讨](./network-capabilities.md)**: Pydoll 如何处理网络操作\n\n!!! tip \"实验\"\n    真正理解代理的最好方法是：\n    \n    1. 建立您自己的代理服务器 (使用上面的代码)\n    2. 使用 Wireshark 捕获流量以查看原始数据包\n    3. 使用真实的自动化测试不同类型的代理\n    4. 故意制造泄露并学会检测它们\n    \n    亲身实践能巩固理论知识！"
  },
  {
    "path": "docs/zh/deep-dive/network/socks-proxies.md",
    "content": "# SOCKS 协议架构\n\nSOCKS（SOCKet Secure）是一种运行在网络栈传输层和应用层之间的代理协议（通常被描述为 OSI 模型的第 5 层）。与解析和理解 HTTP 流量的 HTTP 代理不同，SOCKS 代理在不检查内容的情况下转发原始 TCP 和 UDP 连接。这种协议无关的设计使 SOCKS 成为注重隐私的自动化的首选：代理无需解析您的请求、注入标头或终止 TLS 连接。\n\n本文档涵盖了 SOCKS 在协议层面的工作原理、SOCKS4 与 SOCKS5 的区别、Chrome 中的身份验证处理、DNS 解析行为，以及在 Pydoll 中的实际配置。\n\n!!! info \"模块导航\"\n    - [HTTP/HTTPS 代理](./http-proxies.md)：应用层代理\n    - [网络基础](./network-fundamentals.md)：TCP/IP、UDP、OSI 模型\n    - [网络与安全概述](./index.md)：模块介绍\n    - [代理检测](./proxy-detection.md)：匿名级别和检测规避\n    - [构建代理](./build-proxy.md)：从零开始实现 SOCKS5\n\n    有关实际配置，请参阅[代理配置](../../features/configuration/proxy.md)。\n\n## SOCKS 与 HTTP 代理的区别\n\n根本区别在于每种代理能看到和做到什么。HTTP 代理在应用层运行，理解 HTTP：它可以读取 URL、标头、Cookie 和请求体（针对未加密流量），在传输过程中修改它们，缓存响应，并注入自己的标头，如 `Via` 和 `X-Forwarded-For`。这对内容过滤很有用，但意味着您必须信任代理运营商处理您的应用数据。\n\nSOCKS 代理在应用层之下运行。它只能看到目标地址、端口和正在传输的数据量。它不会解析、修改甚至理解通过它流动的是什么协议。HTTP、HTTPS、FTP、SSH、WebSocket 或任何自定义协议对于 SOCKS 代理来说都是一样的：只是在两个端点之间中继的字节流。\n\n这有一个直接的实际影响。当您通过 SOCKS5 代理发送 HTTPS 请求时，代理看到的是 `example.com:443` 和加密的 TLS 流。它无法读取 URL、标头、Cookie 或响应内容。它不会添加识别性标头。它不需要终止 TLS。加密隧道在您的浏览器和目标服务器之间是端到端的。\n\n然而，理解 SOCKS 不提供什么同样重要。SOCKS 是一种代理协议，而不是加密协议。\"SOCKet Secure\"这个名称指的是安全的防火墙穿越，而非密码学安全。如果您通过 SOCKS5 代理发送未加密的 HTTP 流量，即使代理并非设计用来检查流量，代理运营商也能读取通过的字节。要实现真正的加密，您需要在 SOCKS 之上使用 TLS/HTTPS，或者用加密隧道（SSH、VPN）包裹 SOCKS 连接。\n\n!!! note \"信任模型\"\n    使用 HTTP 代理时，您信任代理运营商不会记录您的浏览历史、窃取令牌、修改响应或执行 MITM 攻击。使用 SOCKS5 时，您只需信任代理能正确转发数据包且不记录连接元数据。攻击面更小，但并非为零。\n\n## SOCKS4 与 SOCKS5\n\nSOCKS 有两个常用版本。SOCKS4 由 NEC 在 20 世纪 90 年代初开发，是一个没有 RFC 的非正式标准。SOCKS5 于 1996 年被标准化为 RFC 1928，以解决 SOCKS4 的局限性。\n\n| 特性 | SOCKS4 | SOCKS5 |\n|---------|--------|--------|\n| 标准 | 无官方 RFC（1992 年的事实标准） | RFC 1928（1996） |\n| 身份验证 | 仅标识（USERID 字段，无密码） | 多种方法（无认证、用户名/密码、GSSAPI） |\n| IP 版本 | 仅 IPv4 | IPv4 和 IPv6 |\n| UDP 支持 | 否 | 是（UDP ASSOCIATE 命令） |\n| DNS 解析 | 客户端（SOCKS4A 扩展添加了服务器端） | 使用域名时由服务器端解析（ATYP=0x03） |\n| 协议支持 | 仅 TCP | TCP 和 UDP |\n\nSOCKS5 在各方面都更优越。仅在代理不支持 SOCKS5 时才使用 SOCKS4。\n\n## SOCKS5 握手\n\nSOCKS5 连接过程遵循 RFC 1928，由三个阶段组成：方法协商、可选的身份验证和连接请求。\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant SOCKS5 as SOCKS5 Proxy\n    participant Server as Target Server\n\n    Note over Client,SOCKS5: Phase 1: Method Negotiation\n    Client->>SOCKS5: Hello [VER=5, NMETHODS, METHODS]\n    SOCKS5->>Client: Method Selected [VER=5, METHOD]\n\n    Note over Client,SOCKS5: Phase 2: Authentication (if required)\n    Client->>SOCKS5: Auth Request [VER=1, ULEN, UNAME, PLEN, PASSWD]\n    SOCKS5->>Client: Auth Response [VER=1, STATUS]\n\n    Note over Client,SOCKS5: Phase 3: Connection Request\n    Client->>SOCKS5: Connect [VER=5, CMD=CONNECT, DST.ADDR, DST.PORT]\n    SOCKS5->>Server: Establish TCP connection\n    Server-->>SOCKS5: Connection established\n    SOCKS5->>Client: Reply [VER=5, REP=SUCCESS, BND.ADDR, BND.PORT]\n\n    Note over Client,Server: Data relay (proxied)\n    Client->>SOCKS5: Application data\n    SOCKS5->>Server: Forward data\n    Server->>SOCKS5: Response data\n    SOCKS5->>Client: Forward response\n```\n\n### 阶段 1：方法协商\n\n客户端打开一个到代理的 TCP 连接，发送一个包含协议版本（SOCKS5 始终为 `0x05`）和支持的身份验证方法列表的问候消息。\n\n```python\n# Client Hello\n[\n    0x05,        # VER: Protocol version (5)\n    0x02,        # NMETHODS: Number of methods offered\n    0x00, 0x02   # METHODS: No auth (0x00) and Username/Password (0x02)\n]\n```\n\n代理回复它选择的方法。如果代理需要身份验证且客户端提供了 `0x02`（用户名/密码），代理就选择它。如果没有提供可接受的方法，代理回复 `0xFF` 并关闭连接。\n\n```python\n# Server response\n[\n    0x05,   # VER: Protocol version (5)\n    0x02    # METHOD: Username/Password selected\n]\n```\n\nRFC 1928 定义的方法代码：`0x00` = 无身份验证，`0x01` = GSSAPI，`0x02` = 用户名/密码（RFC 1929），`0x03-0x7F` = IANA 分配，`0x80-0xFE` = 保留给私有方法，`0xFF` = 无可接受的方法。\n\n### 阶段 2：身份验证\n\n如果代理选择了方法 `0x02`，客户端按照 RFC 1929 发送凭据。子协商使用自己的版本号（`0x01`，而非 `0x05`）。\n\n```python\n# Client authentication\n[\n    0x01,              # VER: Subnegotiation version (1)\n    len(username),     # ULEN: Username length (max 255)\n    *username_bytes,   # UNAME: Username\n    len(password),     # PLEN: Password length (max 255)\n    *password_bytes    # PASSWD: Password\n]\n\n# Server response\n[\n    0x01,   # VER: Subnegotiation version (1)\n    0x00    # STATUS: 0 = success, non-zero = failure\n]\n```\n\n在此握手过程中，凭据以明文传输。这是 SOCKS5 协议（RFC 1929）固有的特性。对于敏感环境，请将 SOCKS 连接包裹在 SSH 隧道或 VPN 中。\n\n### 阶段 3：连接请求\n\n身份验证成功后（或者不需要身份验证时），客户端发送一个连接请求，指定命令、目标地址和端口。\n\n```python\n[\n    0x05,          # VER: Protocol version (5)\n    0x01,          # CMD: 1=CONNECT, 2=BIND, 3=UDP ASSOCIATE\n    0x00,          # RSV: Reserved\n    0x03,          # ATYP: 1=IPv4 (4 bytes), 3=Domain (length+name), 4=IPv6 (16 bytes)\n    len(domain),   # Domain length (only for ATYP=0x03)\n    *domain_bytes, # Domain name\n    *port_bytes    # Port (2 bytes, big-endian)\n]\n```\n\n地址类型（ATYP）决定了格式：`0x01` 表示后面跟 4 字节的 IPv4 地址，`0x04` 表示 16 字节的 IPv6 地址，`0x03` 表示一个长度字节后跟域名。当客户端发送域名（ATYP=0x03）时，代理在其侧解析 DNS，这可以防止 DNS 泄露到客户端的本地网络。\n\n代理连接到目标并回复：\n\n```python\n[\n    0x05,       # VER: Protocol version (5)\n    0x00,       # REP: 0x00=success, 0x01-0x08=various errors\n    0x00,       # RSV: Reserved\n    0x01,       # ATYP: Address type of bound address\n    *bind_addr, # BND.ADDR: Address the proxy bound to\n    *bind_port  # BND.PORT: Port the proxy bound to\n]\n```\n\n回复代码：`0x00` 成功，`0x01` 一般性故障，`0x02` 不允许连接，`0x03` 网络不可达，`0x04` 主机不可达，`0x05` 连接被拒绝，`0x06` TTL 过期，`0x07` 不支持的命令，`0x08` 不支持的地址类型。\n\n成功回复后，代理开始双向中继数据。整个 SOCKS5 握手是二进制协议，比基于文本的 HTTP 更高效，但没有十六进制转储就更难调试。\n\n## UDP 支持\n\nSOCKS5 通过 `UDP ASSOCIATE` 命令（CMD=0x03）支持 UDP 代理。其工作方式与 TCP 代理不同：客户端通过 TCP 控制连接发送 UDP ASSOCIATE 请求，代理回复中继地址和端口。然后客户端将 UDP 数据报发送到该中继，代理将其转发到目标。\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant SOCKS5\n    participant UDP_Server as UDP Server\n\n    Note over Client,SOCKS5: TCP control connection (handshake + auth)\n    Client->>SOCKS5: UDP ASSOCIATE request (CMD=0x03)\n    SOCKS5->>Client: Relay address and port\n\n    Note over Client,SOCKS5: UDP data transfer\n    Client->>SOCKS5: UDP datagram to relay\n    SOCKS5->>UDP_Server: Forward datagram\n    UDP_Server->>SOCKS5: Response datagram\n    SOCKS5->>Client: Forward response\n\n    Note over Client,SOCKS5: TCP control connection stays open\n```\n\n通过中继发送的每个 UDP 数据报都包含一个带有目标地址和端口的小标头：\n\n```python\n[\n    0x00, 0x00,    # RSV: Reserved\n    0x00,          # FRAG: Fragment number (0 = no fragmentation)\n    0x01,          # ATYP: Address type\n    *dst_addr,     # DST.ADDR: Destination address\n    *dst_port,     # DST.PORT: Destination port\n    *data          # DATA: Application data\n]\n```\n\nTCP 控制连接在 UDP 关联期间必须保持打开。如果它关闭，代理会丢弃 UDP 中继。\n\n!!! warning \"Chrome 中的 UDP\"\n    Chrome 不会为任何流量使用 SOCKS5 UDP ASSOCIATE。即使配置了 SOCKS5 代理，Chrome 也只代理 TCP 连接。WebRTC、DNS-over-UDP 和其他 UDP 流量不会通过 SOCKS5 代理路由。这意味着在 Chrome 中使用 SOCKS5 时仍可能存在 WebRTC IP 泄露。使用 `--force-webrtc-ip-handling-policy=disable_non_proxied_udp` 或 Pydoll 的 `webrtc_leak_protection = True` 来缓解此问题。更多详情请参阅 [网络基础：WebRTC 和 IP 泄露](./network-fundamentals.md#webrtc-and-ip-leakage)。\n\n!!! tip \"现代 UDP 代理替代方案\"\n    对于需要超出 Chrome SOCKS5 实现所提供的完整 UDP 支持的场景，可以考虑 Shadowsocks（带有原生 UDP 的加密类 SOCKS 协议）、WireGuard（性能出色的 VPN）或 V2Ray/VMess（具有全面 UDP 处理能力的灵活代理框架）。\n\n## DNS 解析\n\n一个常见的误解是 HTTP 代理会泄露 DNS 查询，而 SOCKS5 代理不会。Chrome 中的实际情况更加微妙。\n\n当 Chrome 配置了任何代理（HTTP、HTTPS 或 SOCKS5）时，它会将主机名发送给代理，而不是在本地解析 DNS。对于 HTTP 代理，主机名出现在 `CONNECT host:443` 请求中。对于 SOCKS5，它出现在带有 ATYP=0x03（域名）的连接请求中。在这两种情况下，代理在其侧解析 DNS，Chrome 不会对代理流量进行本地 DNS 查询。\n\n两种代理类型之间真正的 DNS 隐私差异不在于谁解析 DNS，而在于代理在应用层能看到什么。HTTP 代理能看到未加密请求的完整 URL 和 CONNECT 请求的主机名。SOCKS5 代理只能看到目标主机名和端口作为不透明的连接参数。\n\n但是，有一个重要的注意事项：即使配置了代理，Chrome 的 DNS 预取器也可能会对页面内容中发现的主机名进行本地 DNS 查询。这可能会将您正在浏览的域名泄露给本地 DNS 解析器。要防止这种情况，请禁用 DNS 预取或使用标志 `--host-resolver-rules=\"MAP * ~NOTFOUND , EXCLUDE 127.0.0.1\"`。\n\n!!! note \"`socks5://` 与 `socks5h://`\"\n    Chrome 之外的许多工具区分 `socks5://`（客户端解析 DNS）和 `socks5h://`（代理解析 DNS，\"h\"代表 hostname）。Chrome 对 SOCKS5 始终在代理侧解析 DNS，无论您使用哪种方案，行为都类似于 `socks5h://`。但如果您在 Pydoll 之外使用 `curl`、Firefox 或 Python 库等工具，这个区别就很重要：请始终使用 `socks5h://` 以防止 DNS 泄露。\n\n## SOCKS5 与 MITM 抵抗\n\nSOCKS5 经常被描述为\"抗 MITM\"。在特定意义上这是正确的：因为 SOCKS5 不理解或与 TLS 交互，它没有机制来终止 TLS 连接并重新加密。SOCKS5 代理只是原样中继加密的字节。\n\n相比之下，HTTP 代理可以通过向客户端提供自己的证书来执行 TLS 终止（MITM），解密流量、检查或修改内容，然后重新加密发送给服务器。这需要客户端信任代理的 CA 证书，并且可以通过证书固定和证书透明度日志检测到。HTTP 代理处理 HTTPS 的正常行为（使用 CONNECT）是创建透明隧道而不终止 TLS，但 MITM 的架构可能性是存在的。\n\n使用 SOCKS5 时，TLS 终止在协议层面是不可能的。代理无法将自己注入 TLS 握手，因为它不解析流经的应用数据。客户端和服务器之间的端到端加密在设计上得到了保护。\n\n值得注意的是，提供实际密码学保护的是 TLS，而不是 SOCKS5 本身。如果您通过 SOCKS5 代理发送未加密的 HTTP，代理运营商可以读取所有内容。SOCKS5 的安全优势是架构性的（它不需要也不启用 TLS 终止），而非密码学意义上的。\n\n## TLS 和通过 SOCKS5 的浏览器 fingerprinting\n\n一个需要理解的重要局限：SOCKS5 不会改变浏览器的 fingerprint。TLS 握手（ClientHello）逐字节通过 SOCKS5 代理传递，这意味着目标服务器能看到浏览器的确切 JA3/JA4 fingerprint。HTTP/2 SETTINGS 帧、浏览器特有的标头排序以及所有其他应用层 fingerprinting 信号同样如此。\n\nSOCKS5 隐藏了您的 IP 地址并防止代理注入识别性标头。但它对任何形式的浏览器或行为 fingerprinting 都没有帮助。要实现完整的规避策略，您需要在多个层面应对 fingerprinting。详情请参阅[规避技术](../fingerprinting/evasion-techniques.md)。\n\n## Chrome 中的 SOCKS5 身份验证\n\nChrome 不支持 SOCKS5 用户名/密码身份验证。这是一个长期存在的限制，跟踪为 [Chromium Issue #40323993](https://issues.chromium.org/issues/40323993)。当 Chrome 执行 SOCKS5 方法协商时，它只提供方法 `0x00`（无身份验证）。如果代理需要身份验证，连接会静默失败。\n\n这与 HTTP 代理身份验证有本质区别。HTTP 代理通过 HTTP 状态码（`407 Proxy Authentication Required`）进行身份验证，Chrome 通过 CDP 中的 Fetch 域来处理。Pydoll 拦截这些 `Fetch.authRequired` 事件并自动使用存储的凭据响应。而 SOCKS5 身份验证发生在会话层的二进制协议握手期间，在任何 HTTP 流量存在之前。没有 HTTP 407，没有 `Fetch.authRequired` 事件，基于 CDP 的工具也无法将凭据注入此过程。\n\n配置 `--proxy-server=socks5://user:pass@proxy:1080` 不会生效。Chrome 会静默忽略嵌入的凭据。\n\n### Pydoll 的 SOCKS5Forwarder\n\n标准解决方案是本地代理转发器：一个运行在 localhost 上的轻量级 SOCKS5 服务器，接受来自 Chrome 的未认证连接，并将其转发到带有完整身份验证的远程代理。\n\n```mermaid\nsequenceDiagram\n    participant Chrome\n    participant Forwarder as Local Forwarder<br/>(127.0.0.1:1081)\n    participant Remote as Remote SOCKS5 Proxy<br/>(proxy:1080)\n    participant Server as Destination Server\n\n    Note over Chrome,Forwarder: No authentication\n    Chrome->>Forwarder: SOCKS5 Hello [methods: 0x00]\n    Forwarder->>Chrome: Method selected [0x00]\n    Chrome->>Forwarder: CONNECT example.com:443\n\n    Note over Forwarder,Remote: With authentication\n    Forwarder->>Remote: SOCKS5 Hello [methods: 0x02]\n    Remote->>Forwarder: Method selected [0x02]\n    Forwarder->>Remote: Auth [username, password]\n    Remote->>Forwarder: Auth OK\n    Forwarder->>Remote: CONNECT example.com:443\n    Remote->>Server: TCP connection\n    Remote->>Forwarder: Connect OK\n\n    Forwarder->>Chrome: Connect OK\n\n    Note over Chrome,Server: Bidirectional data relay\n    Chrome->>Forwarder: TLS + application data\n    Forwarder->>Remote: Forward\n    Remote->>Server: Forward\n    Server->>Remote: Response\n    Remote->>Forwarder: Forward\n    Forwarder->>Chrome: Forward\n```\n\nPydoll 在 `pydoll.utils` 模块中提供了内置的 `SOCKS5Forwarder`。这是一个纯 Python、零依赖的异步实现，处理与远程代理的完整 SOCKS5 握手，包括用户名/密码身份验证（RFC 1929）、IPv4、IPv6 和域名地址类型。\n\n```python\nimport asyncio\nfrom pydoll.utils import SOCKS5Forwarder\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def main():\n    forwarder = SOCKS5Forwarder(\n        remote_host='proxy.example.com',\n        remote_port=1080,\n        username='myuser',\n        password='mypass',\n        local_port=1081,  # Use 0 for auto-assigned port\n    )\n    async with forwarder:\n        options = ChromiumOptions()\n        options.add_argument(f'--proxy-server=socks5://127.0.0.1:{forwarder.local_port}')\n\n        async with Chrome(options=options) as browser:\n            tab = await browser.start()\n            await tab.go_to('https://httpbin.org/ip')\n\nasyncio.run(main())\n```\n\n转发器也可以作为独立的 CLI 工具运行，用于测试或与其他应用配合使用：\n\n```bash\npython -m pydoll.utils.socks5_proxy_forwarder \\\n    --remote-host proxy.example.com \\\n    --remote-port 1080 \\\n    --username myuser \\\n    --password mypass \\\n    --local-port 1081\n```\n\n转发器默认绑定到 `127.0.0.1`，使其只能从本机访问。切勿在生产环境中绑定到 `0.0.0.0`，因为这会向网络暴露一个未认证的 SOCKS5 代理。凭据永远不会以明文记录到日志中。由于所有通信都通过本地回环接口进行，转发器增加的延迟不到一毫秒。\n\n!!! tip \"受限环境\"\n    某些环境（Docker 容器、无服务器平台、加固的虚拟机）可能会限制绑定到本地端口。使用 `local_port=0` 让操作系统分配一个可用端口。如果本地绑定完全被阻止，请考虑使用 HTTP CONNECT 代理，Chrome 通过 Pydoll 的 ProxyManager 原生支持其身份验证。\n\n## 实际配置\n\n**基本 SOCKS5（无身份验证）：**\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n```\n\n**带身份验证的 SOCKS5（通过 SOCKS5Forwarder）：**\n\n请参阅上面的 [SOCKS5Forwarder 章节](#pydolls-socks5forwarder)。\n\n**防止泄露：**\n\n要建立完整的 SOCKS5 配置，您还应该防止 WebRTC 和 DNS 预取泄露：\n\n```python\noptions = ChromiumOptions()\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\noptions.webrtc_leak_protection = True  # Prevents WebRTC IP leaks\noptions.add_argument('--disable-quic')  # Forces HTTP/2 over TCP through proxy\n```\n\n**测试您的配置：**\n\n始终通过泄露测试验证您的代理配置。访问 [browserleaks.com/ip](https://browserleaks.com/ip) 确认您的 IP，访问 [browserleaks.com/webrtc](https://browserleaks.com/webrtc) 检查 WebRTC 泄露，访问 [dnsleaktest.com](https://dnsleaktest.com/) 验证 DNS 是否泄露。\n\n## 总结\n\nSOCKS5 提供协议无关的代理，与 HTTP 代理相比具有更小的信任面。它不会解析、修改或向您的流量注入任何内容。在 Chrome 中，DNS 解析在代理侧进行。TLS 加密端到端保持不变。Chrome 中的主要限制是缺乏原生 SOCKS5 身份验证（通过 Pydoll 的 `SOCKS5Forwarder` 解决）以及不支持 UDP 代理（通过禁用 WebRTC 或使用适当的浏览器标志来缓解）。\n\nSOCKS5 不会改变浏览器的 TLS fingerprint、HTTP/2 设置或任何应用层特征。要实现完整的规避，请将 SOCKS5 与浏览器 fingerprint 管理和行为模拟相结合。\n\n**后续步骤：**\n\n- [代理检测](./proxy-detection.md)：即使 SOCKS5 代理也可能被检测到\n- [构建代理](./build-proxy.md)：实现您自己的 SOCKS5 服务器\n- [代理配置](../../features/configuration/proxy.md)：Pydoll 代理的实际设置\n- [规避技术](../fingerprinting/evasion-techniques.md)：多层规避策略\n\n## 参考资料\n\n- RFC 1928: SOCKS Protocol Version 5 (1996) - https://datatracker.ietf.org/doc/html/rfc1928\n- RFC 1929: Username/Password Authentication for SOCKS V5 (1996) - https://datatracker.ietf.org/doc/html/rfc1929\n- RFC 1961: GSS-API Authentication Method for SOCKS V5 (1996) - https://datatracker.ietf.org/doc/html/rfc1961\n- RFC 3089: SOCKS-based IPv6/IPv4 Gateway Mechanism (2001) - https://datatracker.ietf.org/doc/html/rfc3089\n- Chromium Proxy Documentation - https://chromium.googlesource.com/chromium/src/+/689912289c/net/docs/proxy.md\n- Chromium Issue #40323993: SOCKS5 Authentication - https://issues.chromium.org/issues/40323993\n- BrowserLeaks: WebRTC Leak Test - https://browserleaks.com/webrtc\n- DNS Leak Test - https://dnsleaktest.com/\n- IPLeak: Comprehensive Leak Testing - https://ipleak.net\n"
  },
  {
    "path": "docs/zh/features/advanced/behavioral-captcha-bypass.md",
    "content": "# Cloudflare Turnstile 交互\n\nPydoll 通过执行真实的浏览器点击，为与 Cloudflare Turnstile 验证码交互提供原生支持。这**不是绕过或规避**。它只是自动化人类在验证码复选框上执行的相同点击操作。\n\n!!! warning \"此功能实际做什么\"\n    此功能使用标准浏览器交互**点击** Cloudflare Turnstile 验证码复选框。就这样。没有：\n    \n    - **没有**：魔法绕过或规避\n    - **没有**：挑战解决（图像选择、拼图等）\n    - **没有**：分数操纵或指纹欺骗\n    - **有**：只是对验证码容器的真实点击\n    \n    **成功完全取决于您的环境**（IP 声誉、浏览器指纹、行为模式）。Pydoll 提供点击机制；您的环境决定点击是否被接受。\n\n!!! info \"什么是 Cloudflare Turnstile？\"\n    Cloudflare Turnstile 是一个现代验证码系统，它分析浏览器环境和行为信号来判断您是否是人类。它通常显示为用户必须点击的复选框。系统分析：\n    \n    - **IP 声誉**：您的 IP 地址是否被标记或可疑？\n    - **浏览器指纹**：您的浏览器看起来合法吗？\n    - **行为模式**：您的行为像人类吗？\n    \n    当信任分数足够高时，复选框点击被接受。当分数太低时，Turnstile 可能会显示挑战（Pydoll **无法解决**）或完全阻止您。对于图像或拼图挑战，可以考虑使用 **[CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc)**。\n\n## 快速开始\n\n### 上下文管理器（推荐）\n\n上下文管理器等待验证码出现，点击它，并在继续之前等待解决：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def turnstile_example():\n    options = ChromiumOptions()\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # 上下文管理器自动处理验证码\n        async with tab.expect_and_bypass_cloudflare_captcha():\n            await tab.go_to('https://site-with-turnstile.com')\n        \n        # 此代码仅在验证码被点击后运行\n        print(\"Turnstile 验证码交互完成！\")\n        \n        # 继续您的自动化\n        content = await tab.find(id='protected-content')\n        print(await content.text)\n\nasyncio.run(turnstile_example())\n```\n\n### 后台处理\n\n在后台启用自动验证码点击：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def background_turnstile():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 在导航前启用自动点击\n        await tab.enable_auto_solve_cloudflare_captcha()\n        \n        # 导航到受保护的站点\n        await tab.go_to('https://site-with-turnstile.com')\n        \n        # 等待验证码在后台处理\n        await asyncio.sleep(5)\n        \n        print(\"页面加载完成，后台处理验证码\")\n        \n        # 不再需要时禁用\n        await tab.disable_auto_solve_cloudflare_captcha()\n\nasyncio.run(background_turnstile())\n```\n\n## 自定义验证码交互\n\n### 工作原理\n\nPydoll 通过遍历页面的 shadow DOM 自动检测 Cloudflare Turnstile。它查找包含 `challenges.cloudflare.com` 的 shadow root，导航到其跨域 iframe，找到内部 shadow root，并点击实际的复选框元素。无需手动配置选择器。\n\n### 时间配置\n\n验证码的 shadow root 并不总是立即出现。调整超时以匹配站点的行为：\n\n```python\nasync def timing_configuration_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n\n        async with tab.expect_and_bypass_cloudflare_captcha(\n            time_to_wait_captcha=10   # 等待最多 10 秒让验证码出现（默认：5）\n        ):\n            await tab.go_to('https://site-with-slow-turnstile.com')\n\n        print(\"使用自定义时间完成验证码交互！\")\n\nasyncio.run(timing_configuration_example())\n```\n\n**参数参考：**\n\n| 参数 | 类型 | 默认值 | 描述 |\n|-----------|------|---------|-------------|\n| `time_to_wait_captcha` | `float` | `5` | 等待验证码出现的最大秒数 |\n\n!!! info \"为什么时间很重要\"\n    某些站点异步加载验证码。如果 Cloudflare 的 shadow root 在 `time_to_wait_captcha` 时间内没有出现，交互将被跳过。\n\n## 其他验证码系统\n\n### reCAPTCHA v3（隐形）\n\nreCAPTCHA v3 是**完全隐形的**，**不需要交互**。只需正常导航：\n\n```python\nasync def recaptcha_v3_example():\n    options = ChromiumOptions()\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # 不需要特殊处理 - 只需导航\n        await tab.go_to('https://site-with-recaptcha-v3.com')\n        \n        # reCAPTCHA v3 在后台运行，分析您的行为\n        await asyncio.sleep(3)\n        \n        # 继续提交表单\n        submit_button = await tab.find(id='submit-btn')\n        await submit_button.click()\n\nasyncio.run(recaptcha_v3_example())\n```\n\n!!! note \"reCAPTCHA v3 成功因素\"\n    由于 reCAPTCHA v3 完全是被动的（无需交互），成功取决于：\n    \n    - **IP 声誉**：使用声誉良好的住宅代理\n    - **浏览器指纹**：配置真实的浏览器首选项\n    - **行为模式**：在页面上花费时间，自然滚动，真实打字\n    \n    如果您的分数太低，某些站点可能会显示 reCAPTCHA v2 挑战（Pydoll **无法解决**）。\n\n## 什么决定成功？\n\n验证码交互的成功**完全取决于您的环境**，而不是 Pydoll。验证码系统分析：\n\n### 1. IP 声誉（最关键）\n\n| IP 类型 | 信任级别 | 预期行为 |\n|---------|-------------|-------------------|\n| **住宅 IP（干净）** | 高 | 通常无需挑战即被接受 |\n| **移动 IP** | 高 | 通常无需挑战即被接受 |\n| **数据中心 IP** | 低 | 经常被阻止或挑战 |\n| **先前被阻止的 IP** | 非常低 | 几乎总是被阻止或挑战 |\n\n!!! danger \"IP 声誉就是一切\"\n    **没有工具可以克服糟糕的 IP 地址。** 如果您的 IP 被标记，无论您的浏览器看起来多么真实，您都会被阻止或挑战。\n    \n    使用声誉良好的住宅代理以获得最佳结果。\n\n### 2. 浏览器指纹\n\n配置您的浏览器使其看起来合法：\n\n```python\nimport time\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def stealth_configuration():\n    options = ChromiumOptions()\n    \n    # 隐蔽参数\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    options.add_argument('--window-size=1920,1080')\n    \n    # 真实的浏览器首选项\n    current_time = int(time.time())\n    options.browser_preferences = {\n        'profile': {\n            'last_engagement_time': str(current_time - (3 * 60 * 60)),  # 3 小时前\n            'exited_cleanly': True,\n            'exit_type': 'Normal',\n        },\n        'safebrowsing': {'enabled': True},\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        async with tab.expect_and_bypass_cloudflare_captcha():\n            await tab.go_to('https://site-with-turnstile.com')\n\nasyncio.run(stealth_configuration())\n```\n\n### 3. 行为模式\n\n验证码系统分析您如何与页面交互：\n\n```python\nasync def realistic_behavior():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://site-with-turnstile.com')\n        \n        # 在验证码出现之前模拟人类行为\n        await asyncio.sleep(2)  # 阅读页面内容\n        await tab.execute_script('window.scrollBy(0, 300)')  # 滚动\n        await asyncio.sleep(1)\n        \n        # 现在与验证码交互\n        async with tab.expect_and_bypass_cloudflare_captcha():\n            # 验证码交互在这里发生\n            pass\n        \n        print(\"使用真实行为通过验证码！\")\n\nasyncio.run(realistic_behavior())\n```\n\n!!! tip \"行为指纹识别\"\n    要深入了解行为模式如何影响验证码成功，请参阅**[行为指纹识别](../../deep-dive/fingerprinting/behavioral-fingerprinting.md)**。本指南解释：\n    \n    - 鼠标移动模式和检测\n    - 击键时间分析\n    - 滚动行为物理学\n    - 事件序列分析\n    \n    理解这些概念可以帮助您构建更真实的自动化，实现更高的成功率。\n\n## 故障排除\n\n### 验证码未被点击\n\n**症状**：验证码出现但从未被点击，页面停留在挑战上。\n\n**可能的原因：**\n\n1. **时间太短**：Pydoll 尝试点击时验证码尚未加载\n2. **Shadow root 未找到**：Cloudflare Turnstile 的 shadow root 尚未出现在 DOM 中\n\n**解决方案：**\n\n```python\nasync def troubleshooting_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n\n        # 增加等待时间\n        async with tab.expect_and_bypass_cloudflare_captcha(\n            time_before_click=5,     # 点击前更长的延迟\n            time_to_wait_captcha=15  # 更多时间查找验证码\n        ):\n            await tab.go_to('https://problematic-site.com')\n\nasyncio.run(troubleshooting_example())\n```\n\n### 验证码被点击但显示挑战\n\n**症状**：复选框短暂显示勾号，然后呈现图像/拼图挑战。\n\n**根本原因**：您的环境的信任分数太低。\n\n**解决方案：**\n\n- 使用声誉良好的住宅代理\n- 配置真实的浏览器指纹\n- 添加更真实的行为模式（滚动、鼠标移动、延迟）\n- **注意**：Pydoll 无法自行解决挑战 — 如果您需要自动验证码解决，请考虑集成 **[CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc)**\n\n### \"访问被拒绝\"或立即阻止\n\n**症状**：站点立即显示\"访问被拒绝\"或阻止您而不显示验证码。\n\n**根本原因**：**您的 IP 地址被标记。**\n\n**解决方案：**\n\n- 使用声誉良好的不同住宅代理\n- 在请求之间轮换 IP\n- 在 `https://www.cloudflare.com/cdn-cgi/trace` 测试您的 IP\n- **注意**：再多的浏览器配置都无法修复被标记的 IP\n\n### 在本地工作但在 Docker/CI 中失败\n\n**症状**：验证码交互在您的机器上工作，但在 Docker/CI 环境中失败。\n\n**根本原因**：验证码系统严格审查数据中心 IP。\n\n**解决方案：**\n\n1. **使用带有适当显示的无头模式**（用于完全渲染）：\n   ```dockerfile\n   FROM python:3.11-slim\n   \n   RUN apt-get update && apt-get install -y \\\n       chromium \\\n       chromium-driver \\\n       xvfb \\\n       && rm -rf /var/lib/apt/lists/*\n   \n   ENV DISPLAY=:99\n   \n   CMD Xvfb :99 -screen 0 1920x1080x24 & python your_script.py\n   ```\n\n2. **即使在 CI/CD 中也使用住宅代理**：\n   ```python\n   options = ChromiumOptions()\n   options.add_argument('--proxy-server=http://user:pass@residential-proxy.com:8080')\n   ```\n\n## 最佳实践\n\n1. **使用住宅代理**：IP 声誉是最关键的因素\n2. **配置隐蔽选项**：移除自动化指示器\n3. **添加行为模式**：点击前滚动、等待、移动鼠标\n4. **调整时间**：在尝试点击之前给验证码加载时间\n5. **优雅地处理失败**：当无法通过验证码时有备用逻辑\n6. **测试您的环境**：在自动化前验证 IP 声誉和浏览器指纹\n\n## 道德准则\n\n!!! danger \"服务条款和法律合规\"\n    即使技术上可行，与验证码交互也可能违反网站的服务条款。在自动化任何网站之前**始终检查并尊重服务条款**。\n    \n    此功能仅用于**合法的自动化目的**：\n    \n    **适当的用例：**\n    - 对您自己的应用程序进行自动化测试\n    - 监控您有权监控的服务\n    - 具有适当授权的研究和安全分析\n    \n    **不适当的用例：**\n    - 抓取您无权访问的内容\n    - 规避付费墙或订阅系统\n    - 拒绝服务攻击或激进抓取\n    - 任何违反服务条款的活动\n\n## 另请参阅\n\n- **[浏览器选项](../configuration/browser-options.md)** - 隐蔽配置\n- **[浏览器首选项](../configuration/browser-preferences.md)** - 高级指纹识别\n- **[代理配置](../configuration/proxy.md)** - 设置代理\n- **[行为指纹识别](../../deep-dive/fingerprinting/behavioral-fingerprinting.md)** - 理解行为检测\n- **[类人交互](../automation/human-interactions.md)** - 真实的行为模式\n\n---\n\n**记住**：Pydoll 提供点击验证码的机制，但您的环境（IP、指纹、行为）决定成功。这不是魔法解决方案，它是在正确的环境和适当配置下使用的工具。对于需要图像识别或拼图解决的挑战，可以考虑使用 **[CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc)** — 使用代码 **PYDOLL** 获得额外 6% 余额奖励。"
  },
  {
    "path": "docs/zh/features/advanced/decorators.md",
    "content": "# Retry 装饰器\n\n网页爬虫本质上是不可预测的。网络故障、页面加载缓慢、元素出现和消失、触发速率限制以及意外出现的验证码。`@retry` 装饰器提供了一个经过实战测试的强大解决方案，能够优雅地处理这些不可避免的故障。\n\n## 为什么使用 Retry 装饰器？\n\n在生产环境的爬虫中，故障不是例外——而是常态。与其让整个爬虫任务因为临时的网络故障或缺失的元素而崩溃，retry 装饰器允许您：\n\n- **自动恢复** 临时性故障\n- **实施复杂的重试策略** 使用指数退避\n- **在重试前执行恢复逻辑** （刷新页面、切换代理、重启浏览器）\n- **保持业务逻辑清晰** 不会被错误处理代码污染\n\n## 快速开始\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import WaitElementTimeout, NetworkError\n\n@retry(max_retries=3, exceptions=[WaitElementTimeout, NetworkError])\nasync def scrape_product_page(url: str):\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to(url)\n        \n        # 这可能因网络问题或加载缓慢而失败\n        product_title = await tab.find(class_name='product-title', timeout=5)\n        return await product_title.text\n\nasyncio.run(scrape_product_page('https://example.com/product/123'))\n```\n\n如果 `scrape_product_page` 因 `WaitElementTimeout` 或 `NetworkError` 失败，它将自动重试最多 3 次才会放弃。\n\n## 最佳实践：始终指定异常\n\n!!! warning \"关键最佳实践\"\n    **始终** 指定应该触发重试的异常。使用默认的 `exceptions=Exception` 会捕获 **所有** 异常，包括应该立即失败的代码错误。\n\n**错误（捕获所有内容，包括错误）：**\n\n```python\n@retry(max_retries=3)  # 不要这样做\nasync def scrape_data():\n    data = response['items'][0]  # 如果 'items' 不存在，重试无济于事！\n    return data\n```\n\n**正确（仅对预期的失败重试）：**\n\n```python\nfrom pydoll.exceptions import ElementNotFound, WaitElementTimeout, NetworkError\n\n@retry(\n    max_retries=3,\n    exceptions=[ElementNotFound, WaitElementTimeout, NetworkError]\n)\nasync def scrape_data():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        return await tab.find(id='data-container', timeout=10)\n```\n\n通过指定异常，您可以确保：\n\n- **逻辑错误快速失败** （拼写错误、错误的选择器、代码错误）\n- **仅重试可恢复的错误** （网络问题、超时、缺失元素）\n- **调试更容易** （您确切知道出了什么问题）\n\n## 参数\n\n### max_retries\n\n放弃前的最大重试次数。\n\n```python\nfrom pydoll.exceptions import WaitElementTimeout\n\n@retry(max_retries=5, exceptions=[WaitElementTimeout])\nasync def fetch_data():\n    # 总共将尝试 5 次\n    pass\n```\n\n### exceptions\n\n应该触发重试的异常类型。可以是单个异常或列表。\n\n```python\nfrom pydoll.exceptions import (\n    ElementNotFound,\n    WaitElementTimeout,\n    NetworkError,\n    ElementNotInteractable\n)\n\n# 单个异常\n@retry(exceptions=[WaitElementTimeout])\nasync def example1():\n    pass\n\n# 多个异常\n@retry(exceptions=[WaitElementTimeout, NetworkError, ElementNotFound, ElementNotInteractable])\nasync def example2():\n    pass\n```\n\n!!! tip \"常见爬虫异常\"\n    对于使用 Pydoll 进行网页爬虫，您通常会希望重试：\n\n    - `WaitElementTimeout` - 等待元素出现超时\n    - `ElementNotFound` - DOM 中不存在元素\n    - `ElementNotVisible` - 元素存在但不可见\n    - `ElementNotInteractable` - 元素无法接收交互\n    - `NetworkError` - 网络连接问题\n    - `ConnectionFailed` - 连接浏览器失败\n    - `PageLoadTimeout` - 页面加载超时\n    - `ClickIntercepted` - 点击被另一个元素拦截\n\n### delay\n\n重试尝试之间的等待时间（以秒为单位）。\n\n```python\nfrom pydoll.exceptions import WaitElementTimeout\n\n@retry(max_retries=3, exceptions=[WaitElementTimeout], delay=2.0)\nasync def scrape_with_delay():\n    # 每次重试之间等待 2 秒\n    pass\n```\n\n### exponential_backoff\n\n当设置为 `True` 时，随着每次重试尝试，延迟会指数级增加。\n\n```python\nfrom pydoll.exceptions import NetworkError\n\n@retry(\n    max_retries=5,\n    exceptions=[NetworkError],\n    delay=1.0,\n    exponential_backoff=True\n)\nasync def scrape_with_backoff():\n    # 尝试 1: 失败 → 等待 1 秒\n    # 尝试 2: 失败 → 等待 2 秒\n    # 尝试 3: 失败 → 等待 4 秒\n    # 尝试 4: 失败 → 等待 8 秒\n    # 尝试 5: 失败 → 抛出异常\n    pass\n```\n\n**什么是指数退避？**\n\n指数退避是一种重试策略，尝试之间的等待时间呈指数级增长。与其每秒对服务器发起请求，不如逐渐给服务器更多恢复时间：\n\n- **尝试 1**：等待 `delay` 秒（例如 1 秒）\n- **尝试 2**：等待 `delay * 2` 秒（例如 2 秒）\n- **尝试 3**：等待 `delay * 4` 秒（例如 4 秒）\n- **尝试 4**：等待 `delay * 8` 秒（例如 8 秒）\n\n这在以下情况下特别有用：\n\n- 处理 **速率限制** （给服务器时间重置）\n- 处理 **临时服务器过载** （不要让情况变得更糟）\n- 等待 **加载缓慢的动态内容**\n- 避免 **被检测为机器人** （看起来自然的重试模式）\n\n### on_retry\n\n在每次失败尝试后、下次重试前执行的回调函数。必须是 **async 函数**。\n\n```python\nfrom pydoll.exceptions import WaitElementTimeout\n\n@retry(\n    max_retries=3,\n    exceptions=[WaitElementTimeout],\n    on_retry=my_recovery_function\n)\nasync def scrape_data():\n    pass\n```\n\n回调可以是：\n\n- **独立的 async 函数**\n- **类方法** （自动接收 `self`）\n\n## on_retry 回调：您的恢复机制\n\n`on_retry` 回调是真正神奇的地方。这是您在下次重试尝试之前 **恢复应用程序状态** 的机会。\n\n### 独立函数\n\n```python\nimport asyncio\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import WaitElementTimeout\n\nasync def log_retry():\n    print(\"重试尝试失败，下次尝试前等待...\")\n    await asyncio.sleep(1)\n\n@retry(max_retries=3, exceptions=[WaitElementTimeout], on_retry=log_retry)\nasync def scrape_page():\n    # 您的爬虫逻辑\n    pass\n```\n\n### 类方法\n\n在类内部使用装饰器时，回调可以是类方法。它将自动接收 `self` 作为第一个参数。\n\n```python\nimport asyncio\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import WaitElementTimeout\n\nclass DataCollector:\n    def __init__(self):\n        self.retry_count = 0\n    \n    # 重要：在装饰方法之前定义回调\n    async def log_retry(self):\n        self.retry_count += 1\n        print(f\"尝试 {self.retry_count} 失败，正在重试...\")\n        await asyncio.sleep(1)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[WaitElementTimeout],\n        on_retry=log_retry  # 不需要 'self.' 前缀\n    )\n    async def fetch_data(self):\n        # 您的爬取逻辑\n        pass\n```\n\n!!! warning \"方法定义顺序很重要\"\n    使用类方法的 `on_retry` 时，**必须在类定义中的装饰方法之前定义回调方法**。Python 在应用装饰器时需要知道回调。\n\n    **错误（会失败）：**\n\n    ```python\n    class Scraper:\n        @retry(on_retry=handle_retry)  # handle_retry 还不存在！\n        async def scrape(self):\n            pass\n        \n        async def handle_retry(self):  # 定义太晚\n            pass\n    ```\n\n    **正确：**\n\n    ```python\n    class Scraper:\n        async def handle_retry(self):  # 首先定义\n            pass\n        \n        @retry(on_retry=handle_retry)  # 现在存在\n        async def scrape(self):\n            pass\n    ```\n\n## 实际应用案例\n\n### 1. 页面刷新和状态恢复\n\n**这是 `on_retry` 最强大的用法**：通过刷新页面并恢复应用程序状态来从故障中恢复。此示例演示了为什么 retry 装饰器对生产爬虫如此有价值。\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound, WaitElementTimeout\nfrom pydoll.constants import Key\nimport asyncio\n\nclass DataScraper:\n    def __init__(self):\n        self.browser = None\n        self.tab = None\n        self.current_page = 1\n    \n    async def recover_from_failure(self):\n        \"\"\"刷新页面并在重试前恢复状态\"\"\"\n        print(f\"恢复中... 刷新第 {self.current_page} 页\")\n        \n        if self.tab:\n            # 刷新页面以从陈旧元素或错误状态中恢复\n            await self.tab.refresh()\n            await asyncio.sleep(2)  # 等待页面加载\n            \n            # 恢复状态：导航回正确页面\n            if self.current_page > 1:\n                page_input = await self.tab.find(id='page-number')\n                await page_input.insert_text(str(self.current_page))\n                await self.tab.keyboard.press(Key.ENTER)\n                await asyncio.sleep(1)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound, WaitElementTimeout],\n        on_retry=recover_from_failure,\n        delay=1.0\n    )\n    async def scrape_page_data(self):\n        \"\"\"从当前页面抓取数据\"\"\"\n        if not self.browser:\n            self.browser = Chrome()\n            self.tab = await self.browser.start()\n            await self.tab.go_to('https://example.com/data')\n        \n        # 导航到特定页面\n        page_input = await self.tab.find(id='page-number')\n        await page_input.insert_text(str(self.current_page))\n        await self.tab.keyboard.press(Key.ENTER)\n        await asyncio.sleep(1)\n        \n        # 抓取数据（如果元素变陈旧可能会失败）\n        items = await self.tab.find(class_name='data-item', find_all=True)\n        return [await item.text for item in items]\n    \n    async def scrape_multiple_pages(self, start_page: int, end_page: int):\n        \"\"\"抓取多个页面，失败时自动重试\"\"\"\n        results = []\n        for page_num in range(start_page, end_page + 1):\n            self.current_page = page_num\n            data = await self.scrape_page_data()\n            results.extend(data)\n        return results\n\n# 用法\nasync def main():\n    scraper = DataScraper()\n    try:\n        # 抓取第 1-10 页，失败时自动恢复\n        all_data = await scraper.scrape_multiple_pages(1, 10)\n        print(f\"已抓取 {len(all_data)} 个项目\")\n    finally:\n        if scraper.browser:\n            await scraper.browser.stop()\n```\n\n**这为什么强大：**\n\n- `recover_from_failure()` 真正**恢复状态**：刷新并导航回来\n- `scrape_page_data()` 方法保持简洁，只专注于爬取逻辑\n- 如果元素变陈旧或消失，重试机制会自动处理恢复\n- 浏览器通过 `self.browser` 和 `self.tab` 在重试之间保持\n\n### 2. 模态对话框恢复\n\n有时模态框或遮罩层会意外出现并阻止自动化。关闭它并重试。\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound\n\nclass ModalAwareScraper:\n    def __init__(self):\n        self.tab = None\n    \n    async def close_modals(self):\n        \"\"\"在重试前关闭任何阻挡的模态框\"\"\"\n        print(\"检查阻挡的模态框...\")\n        \n        # 尝试查找并关闭常见模态框\n        modal_close = await self.tab.find(\n            class_name='modal-close',\n            timeout=2,\n            raise_exc=False\n        )\n        if modal_close:\n            print(\"找到模态框，关闭中...\")\n            await modal_close.click()\n            await asyncio.sleep(0.5)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound],\n        on_retry=close_modals,\n        delay=0.5\n    )\n    async def click_button(self, button_id: str):\n        button = await self.tab.find(id=button_id)\n        await button.click()\n```\n\n### 3. 浏览器重启和代理轮换\n\n对于大型爬虫任务，失败后可能需要完全重启浏览器并切换代理。\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import NetworkError, PageLoadTimeout\n\nclass RobustScraper:\n    def __init__(self):\n        self.browser = None\n        self.tab = None\n        self.proxy_list = [\n            'proxy1.example.com:8080',\n            'proxy2.example.com:8080',\n            'proxy3.example.com:8080',\n        ]\n        self.current_proxy_index = 0\n    \n    async def restart_with_new_proxy(self):\n        \"\"\"使用不同代理重启浏览器\"\"\"\n        print(\"使用新代理重启浏览器...\")\n        \n        # 关闭当前浏览器\n        if self.browser:\n            await self.browser.stop()\n            await asyncio.sleep(2)\n        \n        # 轮换到下一个代理\n        self.current_proxy_index = (self.current_proxy_index + 1) % len(self.proxy_list)\n        proxy = self.proxy_list[self.current_proxy_index]\n        \n        print(f\"使用代理: {proxy}\")\n        \n        # 使用新代理启动新浏览器\n        options = ChromiumOptions()\n        options.add_argument(f'--proxy-server={proxy}')\n        \n        self.browser = Chrome(options=options)\n        self.tab = await self.browser.start()\n    \n    @retry(\n        max_retries=3,\n        exceptions=[NetworkError, PageLoadTimeout],\n        on_retry=restart_with_new_proxy,\n        delay=5.0,\n        exponential_backoff=True\n    )\n    async def scrape_protected_site(self, url: str):\n        if not self.browser:\n            await self.restart_with_new_proxy()\n        \n        await self.tab.go_to(url)\n        await asyncio.sleep(3)\n        \n        # 您的爬虫逻辑\n        content = await self.tab.find(id='content')\n        return await content.text\n```\n\n### 4. 网络空闲检测与重试\n\n等待所有网络活动完成，如果页面从未稳定则使用重试逻辑。\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import TimeoutException\n\nclass NetworkAwareScraper:\n    def __init__(self):\n        self.tab = None\n    \n    async def reload_page(self):\n        \"\"\"如果网络从未稳定则重新加载页面\"\"\"\n        print(\"页面未稳定，重新加载...\")\n        if self.tab:\n            await self.tab.refresh()\n            await asyncio.sleep(2)\n    \n    @retry(\n        max_retries=2,\n        exceptions=[TimeoutException],\n        on_retry=reload_page,\n        delay=3.0\n    )\n    async def wait_for_page_ready(self):\n        \"\"\"等待所有网络请求完成\"\"\"\n        await self.tab.enable_network_events()\n        \n        # 等待网络空闲（2 秒内无请求）\n        idle_time = 0\n        max_wait = 10\n        \n        while idle_time < max_wait:\n            # 检查是否有正在进行的请求\n            # （实现取决于您的事件跟踪）\n            await asyncio.sleep(0.5)\n            idle_time += 0.5\n        \n        if idle_time >= max_wait:\n            raise TimeoutException(\"网络从未稳定\")\n```\n\n### 5. 验证码检测和恢复\n\n检测验证码何时出现并采取适当行动。\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound\n\nclass CaptchaScraper:\n    def __init__(self):\n        self.tab = None\n        self.captcha_count = 0\n    \n    async def handle_captcha(self):\n        \"\"\"通过等待或切换策略处理验证码\"\"\"\n        self.captcha_count += 1\n        print(f\"检测到验证码（计数：{self.captcha_count}）\")\n        \n        if self.captcha_count > 2:\n            print(\"验证码过多，可能需要更改策略...\")\n            # 可以在这里切换到不同的方法\n        \n        # 尝试之间等待更长时间\n        await asyncio.sleep(30)\n        \n        # 刷新页面\n        await self.tab.refresh()\n        await asyncio.sleep(5)\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound],\n        on_retry=handle_captcha,\n        delay=10.0,\n        exponential_backoff=True\n    )\n    async def scrape_protected_content(self, url: str):\n        if not self.tab:\n            browser = Chrome()\n            self.tab = await browser.start()\n        \n        await self.tab.go_to(url)\n        \n        # 检查验证码\n        captcha = await self.tab.find(\n            class_name='g-recaptcha',\n            timeout=2,\n            raise_exc=False\n        )\n        \n        if captcha:\n            raise ElementNotFound(\"检测到验证码\")\n        \n        # 正常爬虫逻辑\n        content = await self.tab.find(class_name='article-content')\n        return await content.text\n```\n\n## 高级模式\n\n### 组合多种恢复策略\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import ElementNotFound, WaitElementTimeout, NetworkError\n\nclass AdvancedScraper:\n    def __init__(self):\n        self.tab = None\n        self.attempt = 0\n        self.strategies = [\n            self.strategy_refresh,\n            self.strategy_clear_cache,\n            self.strategy_restart_browser,\n        ]\n    \n    async def strategy_refresh(self):\n        \"\"\"策略 1：简单刷新\"\"\"\n        print(\"策略 1：刷新页面\")\n        await self.tab.refresh()\n        await asyncio.sleep(2)\n    \n    async def strategy_clear_cache(self):\n        \"\"\"策略 2：清除缓存并刷新\"\"\"\n        print(\"策略 2：清除缓存\")\n        await self.tab.execute_command('Network.clearBrowserCache')\n        await self.tab.refresh()\n        await asyncio.sleep(3)\n    \n    async def strategy_restart_browser(self):\n        \"\"\"策略 3：完全重启浏览器\"\"\"\n        print(\"策略 3：重启浏览器\")\n        if self.tab:\n            await self.tab._browser.stop()\n        \n        browser = Chrome()\n        self.tab = await browser.start()\n    \n    async def adaptive_recovery(self):\n        \"\"\"根据尝试次数尝试不同的恢复策略\"\"\"\n        strategy_index = min(self.attempt, len(self.strategies) - 1)\n        strategy = self.strategies[strategy_index]\n        \n        print(f\"尝试 {self.attempt + 1}：使用 {strategy.__name__}\")\n        await strategy()\n        \n        self.attempt += 1\n    \n    @retry(\n        max_retries=3,\n        exceptions=[ElementNotFound, WaitElementTimeout, NetworkError],\n        on_retry=adaptive_recovery,\n        delay=2.0\n    )\n    async def scrape_with_adaptive_retry(self, url: str):\n        await self.tab.go_to(url)\n        return await self.tab.find(id='target-content')\n```\n\n### 特定失败的自定义异常\n\n```python\nimport asyncio\nfrom pydoll.decorators import retry\nfrom pydoll.exceptions import PydollException\n\nclass RateLimitError(PydollException):\n    \"\"\"检测到速率限制时引发\"\"\"\n    message = \"API 速率限制已超出\"\n\nclass APIScraper:\n    async def wait_for_rate_limit_reset(self):\n        \"\"\"被速率限制时等待更长时间\"\"\"\n        print(\"检测到速率限制，等待 60 秒...\")\n        await asyncio.sleep(60)\n    \n    @retry(\n        max_retries=5,\n        exceptions=[RateLimitError],\n        on_retry=wait_for_rate_limit_reset,\n        delay=10.0,\n        exponential_backoff=True\n    )\n    async def fetch_api_data(self, endpoint: str):\n        response = await self.tab.request.get(endpoint)\n        \n        if response.status == 429:  # 请求过多\n            raise RateLimitError(\"API 速率限制已超出\")\n        \n        return response.json()\n```\n\n## 最佳实践总结\n\n1. **始终明确指定异常** - 永不使用默认的 `exceptions=Exception`\n2. **对外部服务使用指数退避** - 给服务器恢复时间\n3. **保持合理的重试次数** - 通常 3-5 次尝试就足够了\n4. **记录重试尝试** - 使用 `on_retry` 记录发生的事情\n5. **在装饰方法之前定义回调** - 类定义中的顺序很重要\n6. **使回调异步** - 装饰器需要异步回调\n7. **在回调中恢复状态** - 使用 `on_retry` 导航回原位置\n8. **考虑重试的成本** - 每次重试都会消耗时间和资源\n9. **与其他错误处理结合** - 重试不能替代 try/except 块\n10. **测试您的重试逻辑** - 确保恢复回调实际有效\n\n## 了解更多\n\n- **[异常处理](../core-concepts.md#error-handling)** - 理解 Pydoll 异常\n- **[网络事件](../network/monitoring.md)** - 跟踪和处理网络故障\n- **[浏览器选项](../configuration/browser-options.md)** - 配置代理和其他设置\n- **[事件系统](event-system.md)** - 构建响应式重试策略\n\nretry 装饰器是一个强大的工具，可以将脆弱的爬虫脚本转变为生产就绪的应用程序。通过将其与周到的恢复策略相结合，您可以构建能够优雅地处理真实网络混乱情况的爬虫。\n\n"
  },
  {
    "path": "docs/zh/features/advanced/event-system.md",
    "content": "# 事件系统\n\nPydoll 的事件系统允许您实时监听和响应浏览器活动。这对于构建动态自动化、监控网络请求、检测页面更改和创建响应式工作流至关重要。\n\n!!! info \"提供深入探讨\"\n    本指南专注于实际使用。有关架构细节和内部实现，请参阅[事件架构深入探讨](../../deep-dive/event-architecture.md)。\n\n## 前提条件\n\n在使用事件之前，您需要启用相应的 CDP 域：\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    \n    # 在监听事件之前启用域\n    await tab.enable_page_events()     # 用于页面生命周期事件\n    await tab.enable_network_events()  # 用于网络活动\n    await tab.enable_dom_events()      # 用于 DOM 更改\n```\n\n!!! warning \"不启用事件将不会触发\"\n    如果您注册了回调但忘记启用域，您的回调将永远不会被触发。始终先启用域！\n\n## 基本事件监听\n\n`on()` 方法注册事件监听器：\n\n```python\nfrom pydoll.protocol.page.events import PageEvent, LoadEventFiredEvent\n\nasync def handle_page_load(event: LoadEventFiredEvent):\n    print(f\"页面在 {event['params']['timestamp']} 加载完成\")\n\n# 注册回调\nawait tab.enable_page_events()\ncallback_id = await tab.on(PageEvent.LOAD_EVENT_FIRED, handle_page_load)\n```\n\n### 事件结构\n\n所有事件遵循相同的结构：\n\n```python\n{\n    'method': 'Page.loadEventFired',  # 事件名称\n    'params': {                        # 事件特定数据\n        'timestamp': 123456.789\n    }\n}\n```\n\n通过 `event['params']` 访问事件数据：\n\n```python\nfrom pydoll.protocol.network.events import RequestWillBeSentEvent\n\nasync def handle_request(event: RequestWillBeSentEvent):\n    url = event['params']['request']['url']\n    method = event['params']['request']['method']\n    print(f\"{method} {url}\")\n```\n\n### 使用类型提示以获得更好的 IDE 支持\n\n使用事件参数类型的类型提示来获取事件键的自动完成：\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\nfrom pydoll.protocol.page.events import PageEvent, LoadEventFiredEvent\n\n# 使用类型提示 - IDE 知道所有可用的键！\nasync def handle_request(event: RequestWillBeSentEvent):\n    # IDE 将自动完成 'params'、'request'、'url' 等\n    url = event['params']['request']['url']\n    method = event['params']['request']['method']\n    timestamp = event['params']['timestamp']\n    print(f\"{method} {url} 在 {timestamp}\")\n\nasync def handle_load(event: LoadEventFiredEvent):\n    # IDE 知道此事件在 params 中有 'timestamp'\n    timestamp = event['params']['timestamp']\n    print(f\"页面在 {timestamp} 加载完成\")\n\nawait tab.enable_network_events()\nawait tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, handle_request)\n\nawait tab.enable_page_events()\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, handle_load)\n```\n\n!!! tip \"事件参数的类型提示\"\n    所有事件类型都定义在 `pydoll.protocol.<domain>.events` 中。使用它们可以获得：\n    \n    - **自动完成**：IDE 建议 `event['params']` 中的可用键\n    - **类型安全**：在运行代码之前捕获拼写错误\n    - **文档**：查看每个事件提供的数据\n    \n    事件类型遵循模式：`<EventName>Event`（例如，`RequestWillBeSentEvent`、`ResponseReceivedEvent`）\n\n## 常见事件域\n\n### 页面事件\n\n监控页面生命周期和对话框：\n\n```python\nfrom pydoll.protocol.page.events import PageEvent, JavascriptDialogOpeningEvent\n\nawait tab.enable_page_events()\n\n# 页面已加载\nawait tab.on(PageEvent.LOAD_EVENT_FIRED, lambda e: print(\"页面已加载！\"))\n\n# DOM 就绪\nawait tab.on(PageEvent.DOM_CONTENT_EVENT_FIRED, lambda e: print(\"DOM 就绪！\"))\n\n# JavaScript 对话框\nasync def handle_dialog(event: JavascriptDialogOpeningEvent):\n    message = event['params']['message']\n    dialog_type = event['params']['type']\n    print(f\"对话框 ({dialog_type}): {message}\")\n    \n    # 自动处理\n    if await tab.has_dialog():\n        await tab.handle_dialog(accept=True)\n\nawait tab.on(PageEvent.JAVASCRIPT_DIALOG_OPENING, handle_dialog)\n```\n\n### 网络事件\n\n监控请求和响应：\n\n```python\nfrom pydoll.protocol.network.events import (\n    NetworkEvent,\n    RequestWillBeSentEvent,\n    ResponseReceivedEvent,\n    LoadingFailedEvent\n)\n\nawait tab.enable_network_events()\n\n# 跟踪请求\nasync def log_request(event: RequestWillBeSentEvent):\n    request = event['params']['request']\n    print(f\"→ {request['method']} {request['url']}\")\n\nawait tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, log_request)\n\n# 跟踪响应\nasync def log_response(event: ResponseReceivedEvent):\n    response = event['params']['response']\n    print(f\"← {response['status']} {response['url']}\")\n\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, log_response)\n\n# 跟踪失败\nasync def log_failure(event: LoadingFailedEvent):\n    url = event['params']['type']\n    error = event['params']['errorText']\n    print(f\"[失败] {url} - {error}\")\n\nawait tab.on(NetworkEvent.LOADING_FAILED, log_failure)\n```\n\n### DOM 事件\n\n响应 DOM 更改：\n\n```python\nfrom pydoll.protocol.dom.events import DomEvent, AttributeModifiedEvent\n\nawait tab.enable_dom_events()\n\n# 跟踪属性更改\nasync def on_attribute_change(event: AttributeModifiedEvent):\n    node_id = event['params']['nodeId']\n    attr_name = event['params']['name']\n    attr_value = event['params']['value']\n    print(f\"节点 {node_id}: {attr_name}={attr_value}\")\n\nawait tab.on(DomEvent.ATTRIBUTE_MODIFIED, on_attribute_change)\n\n# 跟踪文档更新\nawait tab.on(DomEvent.DOCUMENT_UPDATED, lambda e: print(\"文档已更新！\"))\n```\n\n## 临时回调\n\n使用 `temporary=True` 进行一次性监听器：\n\n```python\nfrom pydoll.protocol.page.events import PageEvent\n\n# 这只会触发一次，然后自动删除\nawait tab.on(\n    PageEvent.LOAD_EVENT_FIRED,\n    lambda e: print(\"首次加载！\"),\n    temporary=True\n)\n\nawait tab.go_to(\"https://example.com\")  # 触发回调\nawait tab.refresh()                      # 回调不会再次触发\n```\n\n!!! tip \"非常适合一次性设置\"\n    临时回调非常适合只应发生一次的初始化任务。\n\n## 在回调中访问 Tab\n\n使用 `functools.partial` 将 tab 传递给您的回调：\n\n```python\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def process_response(tab, event: ResponseReceivedEvent):\n    # 现在我们可以使用 tab 对象！\n    request_id = event['params']['requestId']\n    \n    # 获取响应体\n    body = await tab.get_network_response_body(request_id)\n    print(f\"响应体: {body[:100]}...\")\n\nawait tab.enable_network_events()\nawait tab.on(\n    NetworkEvent.RESPONSE_RECEIVED,\n    partial(process_response, tab)\n)\n```\n\n!!! info \"为什么使用 Partial？\"\n    事件系统只将事件数据传递给回调。`partial` 允许您绑定其他参数，如 tab 实例。\n\n## 管理回调\n\n### 删除回调\n\n```python\nfrom pydoll.protocol.page.events import PageEvent\n\n# 保存回调 ID\ncallback_id = await tab.on(PageEvent.LOAD_EVENT_FIRED, my_callback)\n\n# 稍后删除它\nawait tab.remove_callback(callback_id)\n```\n\n### 清除所有回调\n\n```python\n# 删除此 tab 的所有已注册回调\nawait tab.clear_callbacks()\n```\n\n## 实用示例\n\n### 监控 API 调用\n\n```python\nimport asyncio\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def monitor_api_calls(tab):\n    collected_data = []\n    \n    # 类型提示帮助 IDE 自动完成事件键\n    async def capture_api_response(tab, data_list, event: ResponseReceivedEvent):\n        url = event['params']['response']['url']\n        \n        # 仅过滤 API 调用\n        if '/api/' not in url:\n            return\n        \n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        \n        data_list.append({\n            'url': url,\n            'body': body,\n            'status': event['params']['response']['status']\n        })\n        print(f\"捕获 API 调用: {url}\")\n    \n    await tab.enable_network_events()\n    await tab.on(\n        NetworkEvent.RESPONSE_RECEIVED,\n        partial(capture_api_response, tab, collected_data)\n    )\n    \n    # 导航并收集\n    await tab.go_to(\"https://example.com\")\n    await asyncio.sleep(3)  # 等待请求完成\n    \n    return collected_data\n```\n\n### 等待特定事件\n\n```python\nimport asyncio\nfrom pydoll.protocol.page.events import PageEvent, FrameNavigatedEvent\n\nasync def wait_for_navigation():\n    navigation_done = asyncio.Event()\n    \n    async def on_navigated(event: FrameNavigatedEvent):\n        navigation_done.set()\n    \n    await tab.enable_page_events()\n    await tab.on(PageEvent.FRAME_NAVIGATED, on_navigated, temporary=True)\n    \n    # 触发导航\n    button = await tab.find(id='next-page')\n    await button.click()\n    \n    # 等待它完成\n    await navigation_done.wait()\n    print(\"导航完成！\")\n```\n\n### 网络空闲检测\n\n```python\nimport asyncio\nfrom pydoll.protocol.network.events import (\n    NetworkEvent,\n    RequestWillBeSentEvent,\n    LoadingFinishedEvent,\n    LoadingFailedEvent\n)\n\nasync def wait_for_network_idle(tab, timeout=5):\n    in_flight = 0\n    idle_event = asyncio.Event()\n    last_activity = asyncio.get_event_loop().time()\n    \n    async def on_request(event: RequestWillBeSentEvent):\n        nonlocal in_flight, last_activity\n        in_flight += 1\n        last_activity = asyncio.get_event_loop().time()\n    \n    async def on_finished(event: LoadingFinishedEvent | LoadingFailedEvent):\n        nonlocal in_flight, last_activity\n        in_flight -= 1\n        last_activity = asyncio.get_event_loop().time()\n        \n        if in_flight == 0:\n            idle_event.set()\n    \n    await tab.enable_network_events()\n    req_id = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, on_request)\n    fin_id = await tab.on(NetworkEvent.LOADING_FINISHED, on_finished)\n    fail_id = await tab.on(NetworkEvent.LOADING_FAILED, on_finished)\n    \n    try:\n        await asyncio.wait_for(idle_event.wait(), timeout=timeout)\n        print(\"网络空闲！\")\n    except asyncio.TimeoutError:\n        print(f\"{timeout}秒后网络仍然活跃\")\n    finally:\n        # 清理\n        await tab.remove_callback(req_id)\n        await tab.remove_callback(fin_id)\n        await tab.remove_callback(fail_id)\n```\n\n### 动态内容抓取\n\n```python\nimport asyncio\nimport json\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def scrape_infinite_scroll(tab, max_items=100):\n    items = []\n    \n    async def capture_products(tab, items_list, event: ResponseReceivedEvent):\n        url = event['params']['response']['url']\n        \n        # 查找产品 API 端点\n        if '/products' not in url:\n            return\n        \n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        \n        try:\n            data = json.loads(body)\n            if 'items' in data:\n                items_list.extend(data['items'])\n                print(f\"收集了 {len(data['items'])} 个项目（总计: {len(items_list)}）\")\n        except json.JSONDecodeError:\n            pass\n    \n    await tab.enable_network_events()\n    await tab.on(\n        NetworkEvent.RESPONSE_RECEIVED,\n        partial(capture_products, tab, items)\n    )\n    \n    await tab.go_to(\"https://example.com/products\")\n    \n    # 滚动以触发无限加载\n    while len(items) < max_items:\n        await tab.execute_script(\"window.scrollTo(0, document.body.scrollHeight)\")\n        await asyncio.sleep(1)\n    \n    return items[:max_items]\n```\n\n## 事件参考表\n\n### 可用域\n\n| 域 | 启用方法 | 常见用例 |\n|--------|--------------|------------------|\n| Page | `enable_page_events()` | 页面生命周期、导航、对话框 |\n| Network | `enable_network_events()` | 请求/响应监控、API 跟踪 |\n| DOM | `enable_dom_events()` | DOM 结构更改、属性修改 |\n| Fetch | `enable_fetch_events()` | 请求拦截和修改 |\n| Runtime | `enable_runtime_events()` | 控制台消息、JavaScript 异常 |\n\n### 关键页面事件\n\n| 事件 | 何时触发 | 用例 |\n|-------|---------------|----------|\n| `LOAD_EVENT_FIRED` | 页面加载完成 | 等待完整页面加载 |\n| `DOM_CONTENT_EVENT_FIRED` | DOM 就绪 | 开始 DOM 操作 |\n| `JAVASCRIPT_DIALOG_OPENING` | Alert/confirm/prompt | 自动处理对话框 |\n| `FRAME_NAVIGATED` | 导航完成 | 跟踪 SPA 导航 |\n| `FILE_CHOOSER_OPENED` | 文件输入被点击 | 自动化文件上传 |\n\n### 关键网络事件\n\n| 事件 | 何时触发 | 用例 |\n|-------|---------------|----------|\n| `REQUEST_WILL_BE_SENT` | 请求发送前 | 记录/修改传出请求 |\n| `RESPONSE_RECEIVED` | 接收响应头 | 捕获 API 响应 |\n| `LOADING_FINISHED` | 响应体加载完成 | 获取完整响应数据 |\n| `LOADING_FAILED` | 请求失败 | 跟踪错误和重试 |\n| `WEB_SOCKET_CREATED` | WebSocket 打开 | 监控实时连接 |\n\n### 关键 DOM 事件\n\n| 事件 | 何时触发 | 用例 |\n|-------|---------------|----------|\n| `DOCUMENT_UPDATED` | DOM 重建 | 刷新元素引用 |\n| `ATTRIBUTE_MODIFIED` | 元素属性更改 | 跟踪动态属性更改 |\n| `CHILD_NODE_INSERTED` | 添加新元素 | 检测动态添加的内容 |\n| `CHILD_NODE_REMOVED` | 删除元素 | 检测删除的内容 |\n\n### 事件类型参考\n\n所有事件类型及其参数结构都定义在协议模块中：\n\n| 域 | 导入路径 | 示例类型 |\n|--------|-------------|---------------|\n| Page | `pydoll.protocol.page.events` | `LoadEventFiredEvent`、`FrameNavigatedEvent`、`JavascriptDialogOpeningEvent` |\n| Network | `pydoll.protocol.network.events` | `RequestWillBeSentEvent`、`ResponseReceivedEvent`、`LoadingFinishedEvent` |\n| DOM | `pydoll.protocol.dom.events` | `DocumentUpdatedEvent`、`AttributeModifiedEvent`、`ChildNodeInsertedEvent` |\n| Fetch | `pydoll.protocol.fetch.events` | `RequestPausedEvent`、`AuthRequiredEvent` |\n| Runtime | `pydoll.protocol.runtime.events` | `ConsoleAPICalledEvent`、`ExceptionThrownEvent` |\n\n每个事件类型都是一个 `TypedDict`，定义了事件的确切结构，包括 `params` 字典中的所有可用键。\n\n## 最佳实践\n\n### 1. 始终先启用域\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent\n\n# 好\nawait tab.enable_network_events()\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, callback)\n\n# 坏：回调永远不会触发\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, callback)\nawait tab.enable_network_events()\n```\n\n### 2. 完成后清理\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent\n\n# 为特定任务启用\nawait tab.enable_network_events()\ncallback_id = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, log_request)\n\n# 执行您的工作...\nawait tab.go_to(\"https://example.com\")\n\n# 清理\nawait tab.remove_callback(callback_id)\nawait tab.disable_network_events()\n```\n\n### 3. 使用早期过滤\n\n```python\nfrom pydoll.protocol.network.events import RequestWillBeSentEvent\n\n# 好：早期过滤\nasync def handle_api_request(event: RequestWillBeSentEvent):\n    url = event['params']['request']['url']\n    if '/api/' not in url:\n        return  # 提前退出\n    \n    # 仅处理 API 请求\n    process_request(event)\n\n# 坏：处理所有内容\nasync def handle_all_requests(event: RequestWillBeSentEvent):\n    url = event['params']['request']['url']\n    process_request(event)\n    if '/api/' in url:\n        do_extra_work(event)\n```\n\n### 4. 优雅地处理错误\n\n```python\nfrom pydoll.protocol.network.events import ResponseReceivedEvent\n\nasync def safe_callback(event: ResponseReceivedEvent):\n    try:\n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        process_body(body)\n    except KeyError:\n        # 事件可能没有 requestId\n        pass\n    except Exception as e:\n        print(f\"回调中的错误: {e}\")\n        # 继续而不中断事件循环\n```\n\n## 性能注意事项\n\n!!! warning \"高频事件\"\n    DOM 事件在动态页面上可能**非常频繁地**触发。使用过滤和防抖动以避免性能问题。\n\n### 按域划分的事件量\n\n| 域 | 事件频率 | 性能影响 |\n|--------|----------------|-------------------|\n| Page | 低 | 最小 |\n| Network | 中-高 | 中等 |\n| DOM | 非常高 | 高 |\n| Fetch | 中等 | 中等 |\n\n### 优化技巧\n\n1. **仅启用您需要的**：不要一次启用所有域\n2. **使用临时回调**：尽可能自动清理\n3. **早期过滤**：在昂贵的操作之前检查条件\n4. **完成后禁用**：释放资源\n5. **避免繁重的处理**：保持回调快速，将工作卸载到单独的任务\n\n```python\nimport asyncio\nfrom pydoll.protocol.network.events import ResponseReceivedEvent\n\n# 好：快速回调，卸载繁重的工作\nasync def handle_response(event: ResponseReceivedEvent):\n    if should_process(event):\n        asyncio.create_task(heavy_processing(event))  # 不阻塞\n\n# 坏：阻塞事件循环\nasync def handle_response(event: ResponseReceivedEvent):\n    await heavy_processing(event)  # 阻塞其他事件\n```\n\n## 常见模式\n\n### 事件的上下文管理器\n\n```python\nfrom contextlib import asynccontextmanager\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\n\n@asynccontextmanager\nasync def monitor_requests(tab):\n    \"\"\"在块期间监控请求的上下文管理器。\"\"\"\n    requests = []\n    \n    async def capture(event: RequestWillBeSentEvent):\n        requests.append(event['params']['request'])\n    \n    await tab.enable_network_events()\n    cb_id = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, capture)\n    \n    try:\n        yield requests\n    finally:\n        await tab.remove_callback(cb_id)\n        await tab.disable_network_events()\n\n# 用法\nasync with monitor_requests(tab) as requests:\n    await tab.go_to(\"https://example.com\")\n    # 捕获所有请求\n\nprint(f\"捕获了 {len(requests)} 个请求\")\n```\n\n### 条件事件注册\n\n```python\nfrom pydoll.protocol.network.events import NetworkEvent\nfrom pydoll.protocol.dom.events import DomEvent\n\nasync def setup_monitoring(tab, track_network=False, track_dom=False):\n    \"\"\"仅启用指定的监控。\"\"\"\n    callbacks = []\n    \n    if track_network:\n        await tab.enable_network_events()\n        cb = await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, log_request)\n        callbacks.append(('network', cb))\n    \n    if track_dom:\n        await tab.enable_dom_events()\n        cb = await tab.on(DomEvent.ATTRIBUTE_MODIFIED, log_dom_change)\n        callbacks.append(('dom', cb))\n    \n    return callbacks\n```\n\n## 进一步阅读\n\n- **[事件架构深入探讨](../../deep-dive/event-architecture.md)** - 内部实现和 WebSocket 通信\n- **[网络监控](../network/monitoring.md)** - 高级网络分析技术\n- **[响应式自动化](reactive-automation.md)** - 构建事件驱动的工作流\n\n!!! tip \"从简单开始\"\n    从 Page 事件开始了解基础知识，然后根据需要转向 Network 和 DOM 事件。事件系统很强大，但一开始可能会让人不知所措。"
  },
  {
    "path": "docs/zh/features/advanced/remote-connections.md",
    "content": "# 远程连接和混合自动化\n\nPydoll 允许您通过 WebSocket 连接到已运行的浏览器，实现远程控制和混合自动化场景。这非常适合 CI/CD 管道、容器化环境、调试会话以及将 Pydoll 与现有 CDP 工具集成。\n\n!!! info \"无需设置\"\n    与启动浏览器的传统自动化不同，远程连接让您控制已经运行的浏览器。不需要进程管理！\n\n## 为什么使用远程连接？\n\n远程连接解锁了强大的自动化场景：\n\n| 用例 | 好处 |\n|----------|---------|\n| **CI/CD 管道** | 连接到浏览器容器而无需管理进程 |\n| **Docker 环境** | 控制在单独容器中运行的浏览器 |\n| **远程调试** | 自动化远程服务器或虚拟机上的浏览器 |\n| **混合工具** | 将 Pydoll 与现有 CDP 基础设施集成 |\n| **开发** | 连接到本地浏览器进行快速测试 |\n| **多工具自动化** | 在不同工具之间共享浏览器会话 |\n\n## 设置远程浏览器服务器\n\n!!! tip \"已经有远程浏览器服务？\"\n    如果您正在使用云浏览器服务（BrowserStack、Selenium Grid、LambdaTest 等）或已经有一个运行中的 Chrome 实例并带有 WebSocket URL，您可以**跳过整个部分**，直接跳转到[连接方法](#connection-methods)了解如何使用 Pydoll 连接。\n\n在远程连接之前，您需要启动启用了调试并正确配置以接受外部连接的 Chrome。\n\n### 基本服务器设置（Linux）\n\n在服务器上启动带有远程调试的 Chrome：\n\n```bash\n# 基本设置 - 仅从 localhost 可访问\ngoogle-chrome \\\n  --remote-debugging-port=9222 \\\n  --headless=new \\\n  --no-sandbox \\\n  --disable-dev-shm-usage \\\n  --user-data-dir=/tmp/chrome-profile\n\n# 服务器设置 - 从其他机器可访问\ngoogle-chrome \\\n  --remote-debugging-port=9222 \\\n  --remote-debugging-address=0.0.0.0 \\\n  --headless=new \\\n  --no-sandbox \\\n  --disable-dev-shm-usage \\\n  --user-data-dir=/tmp/chrome-profile\n```\n\n!!! warning \"安全关键\"\n    使用 `--remote-debugging-address=0.0.0.0` 使调试端口可从**任何网络接口**访问。这对于远程连接是必要的，但如果暴露到互联网，会造成重大安全风险。\n\n### 推荐的服务器配置\n\n```bash\n# 生产就绪配置\ngoogle-chrome \\\n  --remote-debugging-port=9222 \\\n  --remote-debugging-address=0.0.0.0 \\\n  --headless=new \\\n  --no-sandbox \\\n  --disable-dev-shm-usage \\\n  --disable-gpu \\\n  --disable-software-rasterizer \\\n  --disable-extensions \\\n  --disable-background-networking \\\n  --disable-background-timer-throttling \\\n  --disable-client-side-phishing-detection \\\n  --disable-popup-blocking \\\n  --disable-prompt-on-repost \\\n  --disable-sync \\\n  --metrics-recording-only \\\n  --no-first-run \\\n  --safebrowsing-disable-auto-update \\\n  --user-data-dir=/tmp/chrome-remote-$(date +%s)\n```\n\n**关键标志说明：**\n\n| 标志 | 目的 |\n|------|---------|\n| `--remote-debugging-port=9222` | 在端口 9222 上启用 CDP |\n| `--remote-debugging-address=0.0.0.0` | 允许外部连接（安全风险！） |\n| `--headless=new` | 无 GUI 运行（服务器模式） |\n| `--no-sandbox` | 在 Docker/容器中必需（安全权衡） |\n| `--disable-dev-shm-usage` | 防止容器中的 /dev/shm 内存问题 |\n| `--disable-gpu` | 无 GPU 加速（建议用于无头模式） |\n| `--user-data-dir=/tmp/...` | 每个实例的隔离配置文件 |\n\n!!! warning \"关于 --no-sandbox 标志\"\n    `--no-sandbox` 标志禁用 Chrome 的安全沙箱，该沙箱将浏览器进程与系统隔离。由于内核能力限制，此标志在大多数 Docker/容器环境中是**必需的**，但它带来了安全影响：\n    \n    - **风险**：移除浏览器和系统之间的隔离\n    - **何时使用**：Docker 容器、受限环境\n    - **缓解措施**：确保容器级隔离（命名空间、cgroups）并避免以 root 身份运行\n    \n    仅在绝对必要时考虑使用 `--no-sandbox`，并在容器级别实施额外的安全层。\n\n### Docker 设置\n\n创建容器化的 Chrome 服务器：\n\n!!! tip \"使用预构建镜像\"\n    对于生产环境，考虑使用官方预构建镜像而不是自己构建：\n    \n    - **Selenium 镜像**：`selenium/standalone-chrome`（包含 WebDriver）\n    - **Zenika Alpine Chrome**：`zenika/alpine-chrome`（轻量级，约 200MB）\n    - **Browserless**：`browserless/chrome`（生产就绪，带监控）\n    \n    这些镜像定期更新、经过安全测试，并针对容器环境进行了优化。\n\n**Dockerfile（自定义构建）：**\n```dockerfile\nFROM ubuntu:22.04\n\n# 安装 Chrome\nRUN apt-get update && apt-get install -y \\\n    wget \\\n    gnupg \\\n    ca-certificates \\\n    && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \\\n    && echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list \\\n    && apt-get update \\\n    && apt-get install -y google-chrome-stable \\\n    && rm -rf /var/lib/apt/lists/*\n\n# 暴露调试端口\nEXPOSE 9222\n\n# 使用远程调试启动 Chrome\nCMD [\"google-chrome\", \\\n     \"--remote-debugging-port=9222\", \\\n     \"--remote-debugging-address=0.0.0.0\", \\\n     \"--headless=new\", \\\n     \"--no-sandbox\", \\\n     \"--disable-dev-shm-usage\", \\\n     \"--disable-gpu\", \\\n     \"--user-data-dir=/tmp/chrome-profile\"]\n```\n\n**docker-compose.yml：**\n```yaml\nservices:\n  chrome-server:\n    build: .\n    ports:\n      - \"127.0.0.1:9222:9222\"\n    \n    # 仅当您需要远程访问且已使用防火墙或代理保护端口时，取消注释下面的行。\n    # - \"9222:9222\"\n\n    shm_size: '2gb'  # 关键：Chrome 使用 /dev/shm 进行共享内存\n                      # 默认的 Docker shm_size（64MB）不足\n    restart: unless-stopped\n    environment:\n      - DISPLAY=:99\n    networks:\n      - automation-network\n    # 可选：生产环境的资源限制\n    # deploy:\n    #   resources:\n    #     limits:\n    #       cpus: '2'\n    #       memory: 4G\n\n  automation-client:\n    image: python:3.11\n    depends_on:\n      - chrome-server\n    volumes:\n      - ./:/app\n    working_dir: /app\n    command: python automation_script.py\n    environment:\n      - CHROME_WS=ws://chrome-server:9222/devtools/browser\n    networks:\n      - automation-network\n\nnetworks:\n  automation-network:\n    driver: bridge\n```\n\n**用法：**\n```bash\n# 启动堆栈\ndocker-compose up -d\n\n# 检查 Chrome 是否运行\ncurl http://localhost:9222/json/version\n\n# 从自动化客户端连接（在 Docker 网络内）\n# ws://chrome-server:9222/devtools/browser/...\n```\n\n### Systemd 服务（Linux 服务器）\n\n创建持久的 Chrome 服务：\n\n**/etc/systemd/system/chrome-remote.service：**\n```ini\n[Unit]\nDescription=Chrome Remote Debugging Server\nAfter=network.target\n\n[Service]\nType=simple\nUser=chrome-user\nGroup=chrome-user\nEnvironment=\"DISPLAY=:99\"\nExecStart=/usr/bin/google-chrome \\\n    --remote-debugging-port=9222 \\\n    --remote-debugging-address=0.0.0.0 \\\n    --headless=new \\\n    --no-sandbox \\\n    --disable-dev-shm-usage \\\n    --disable-gpu \\\n    --user-data-dir=/var/lib/chrome-remote\nRestart=always\nRestartSec=10\n\n[Install]\nWantedBy=multi-user.target\n```\n\n**设置和管理：**\n```bash\n# 创建专用用户\nsudo useradd -r -s /bin/false chrome-user\nsudo mkdir -p /var/lib/chrome-remote\nsudo chown chrome-user:chrome-user /var/lib/chrome-remote\n\n# 安装并启用服务\nsudo systemctl daemon-reload\nsudo systemctl enable chrome-remote\nsudo systemctl start chrome-remote\n\n# 检查状态\nsudo systemctl status chrome-remote\n\n# 查看日志\nsudo journalctl -u chrome-remote -f\n\n# 重启服务\nsudo systemctl restart chrome-remote\n```\n\n### 网络安全配置\n\n#### 防火墙规则（iptables）\n\n```bash\n# 仅允许特定 IP 访问端口 9222\nsudo iptables -A INPUT -p tcp --dport 9222 -s 192.168.1.100 -j ACCEPT\nsudo iptables -A INPUT -p tcp --dport 9222 -j DROP\n\n# 保存规则\nsudo iptables-save > /etc/iptables/rules.v4\n```\n\n#### 防火墙规则（ufw）\n\n```bash\n# 默认拒绝对端口 9222 的所有访问\nsudo ufw deny 9222\n\n# 允许特定 IP\nsudo ufw allow from 192.168.1.100 to any port 9222\n\n# 允许特定子网\nsudo ufw allow from 192.168.1.0/24 to any port 9222\n\n# 启用防火墙\nsudo ufw enable\n```\n\n#### Nginx 反向代理（带身份验证）\n\n使用 HTTP 身份验证保护 Chrome 调试：\n\n**/etc/nginx/sites-available/chrome-remote：**\n```nginx\nserver {\n    listen 80;\n    server_name chrome.example.com;\n\n    # 基本身份验证\n    auth_basic \"Chrome Remote Debugging\";\n    auth_basic_user_file /etc/nginx/.htpasswd;\n\n    location / {\n        proxy_pass http://localhost:9222;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_read_timeout 86400;\n    }\n}\n```\n\n**设置：**\n```bash\n# 创建密码文件\nsudo htpasswd -c /etc/nginx/.htpasswd admin\n\n# 启用站点\nsudo ln -s /etc/nginx/sites-available/chrome-remote /etc/nginx/sites-enabled/\nsudo nginx -t\nsudo systemctl reload nginx\n\n# 使用身份验证连接\n# ws://admin:password@chrome.example.com/devtools/browser/...\n```\n\n### 从另一台计算机连接\n\n配置服务器后，从客户端机器连接：\n\n```python\nimport asyncio\nimport aiohttp\nfrom pydoll.browser.chromium import Chrome\n\nasync def connect_to_remote_server():\n    \"\"\"连接到远程服务器上运行的 Chrome。\"\"\"\n    # 服务器 IP 和端口\n    server_ip = \"192.168.1.100\"\n    server_port = 9222\n\n    async with aiohttp.ClientSession() as session:\n        # 查询服务器的可用目标\n        url = f\"http://{server_ip}:{server_port}/json/version\"\n        \n        async with session.get(url) as response:\n            data = await response.json()\n            ws_url = data['webSocketDebuggerUrl']\n            \n            print(f\"服务器信息:\")\n            print(f\"  浏览器: {data.get('Browser')}\")\n            print(f\"  协议: {data.get('Protocol-Version')}\")\n            print(f\"  WebSocket: {ws_url}\")\n    \n    # 2. 连接到浏览器\n    chrome = Chrome()\n    tab = await chrome.connect(ws_url)\n    \n    print(f\"\\n[成功] 已连接到远程 Chrome 服务器！\")\n    \n    # 3. 正常使用\n    await tab.go_to('https://example.com')\n    title = await tab.execute_script('return document.title')\n    print(f\"页面标题: {title}\")\n    \n    # 4. 清理\n    await chrome.close()\n\nasyncio.run(connect_to_remote_server())\n```\n\n### 测试服务器设置\n\n```bash\n# 1. 检查 Chrome 是否运行\nps aux | grep chrome\n\n# 2. 检查端口是否在监听\nnetstat -tulpn | grep 9222\n# 或\nss -tulpn | grep 9222\n\n# 3. 测试本地访问\ncurl http://localhost:9222/json/version\n\n# 4. 测试远程访问（从客户端机器）\ncurl http://SERVER_IP:9222/json/version\n\n# 5. 检查 WebSocket URL\ncurl http://SERVER_IP:9222/json/version | jq -r '.webSocketDebuggerUrl'\n\n# 6. 列出所有可用目标（标签页/页面）\ncurl http://SERVER_IP:9222/json/list\n```\n\n### 多实例设置\n\n在不同端口上运行多个 Chrome 实例：\n\n```bash\n#!/bin/bash\n# start-chrome-pool.sh\n\nfor port in 9222 9223 9224 9225; do\n    google-chrome \\\n        --remote-debugging-port=$port \\\n        --remote-debugging-address=0.0.0.0 \\\n        --headless=new \\\n        --no-sandbox \\\n        --disable-dev-shm-usage \\\n        --user-data-dir=/tmp/chrome-$port &\n    \n    echo \"在端口 $port 上启动了 Chrome\"\ndone\n\necho \"Chrome 池已就绪。端口: 9222-9225\"\n```\n\n**使用池的 Python 客户端：**\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nimport aiohttp\n\nasync def connect_to_pool(server_ip: str, ports: list[int]):\n    \"\"\"连接到多个 Chrome 实例。\"\"\"\n    tasks = []\n    \n    for port in ports:\n        task = connect_to_instance(server_ip, port)\n        tasks.append(task)\n    \n    results = await asyncio.gather(*tasks)\n    return results\n\nasync def connect_to_instance(server_ip: str, port: int):\n    \"\"\"连接到单个 Chrome 实例。\"\"\"\n    # 获取 WebSocket URL\n    async with aiohttp.ClientSession() as session:\n        url = f\"http://{server_ip}:{port}/json/version\"\n        async with session.get(url) as response:\n            data = await response.json()\n            ws_url = data['webSocketDebuggerUrl']\n    \n    # 连接\n    chrome = Chrome()\n    tab = await chrome.connect(ws_url)\n    \n    # 运行自动化\n    await tab.go_to('https://example.com')\n    title = await tab.execute_script('return document.title')\n    \n    print(f\"端口 {port}: {title}\")\n    \n    await chrome.close()\n    return title\n\n# 用法\nasyncio.run(connect_to_pool('192.168.1.100', [9222, 9223, 9224, 9225]))\n```\n\n## 连接方法\n\nPydoll 提供两种远程连接方法，每种都适合不同的场景。\n\n### 方法 1：浏览器级连接\n\n使用 WebSocket 端点连接到运行中的浏览器并访问所有打开的标签页：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def connect_to_remote_browser():\n    chrome = Chrome()\n    \n    # 通过 WebSocket 连接到远程浏览器\n    tab = await chrome.connect('ws://localhost:9222/devtools/browser/XXXX')\n    \n    # 返回的标签页是第一个可用的标签页\n    print(f\"已连接到标签页: {await tab.execute_script('return document.title')}\")\n    \n    # 您也可以获取所有其他标签页\n    all_tabs = await chrome.get_opened_tabs()\n    print(f\"可用的标签页总数: {len(all_tabs)}\")\n    \n    # 正常使用标签页\n    await tab.go_to('https://example.com')\n    element = await tab.find(id='main-content')\n    text = await element.text\n    print(f\"内容: {text}\")\n    \n    # 清理\n    await chrome.close()\n\nasyncio.run(connect_to_remote_browser())\n```\n\n!!! tip \"获取 WebSocket URL\"\n    启动启用了调试的 Chrome：\n    ```bash\n    # Linux/Mac\n    google-chrome --remote-debugging-port=9222\n    \n    # Windows\n    \"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" --remote-debugging-port=9222\n    ```\n    \n    **对于本地连接**（同一台机器）：\n    \n    - 在浏览器中访问 `http://localhost:9222/json/version` 以在 `webSocketDebuggerUrl` 字段中获取 WebSocket URL\n    - 或使用 `aiohttp` 以编程方式查询它，如上面的示例所示\n    - 对于快速调试，您还可以在启动本地浏览器实例后检查 `browser._connection_port`\n    \n    **对于远程连接**（不同机器）：\n    \n    - 从客户端机器查询 `http://SERVER_IP:9222/json/version`\n    - 使用响应中的 `webSocketDebuggerUrl`，如果需要，将 `localhost` 替换为实际服务器 IP\n\n### 方法 2：直接元素控制（混合方法）\n\n如果您已经有自己的 CDP 集成或低级工具，可以用 Pydoll 的高级 API 包装现有元素：\n\n```python\nimport asyncio\nimport json\nfrom pydoll.connection.connection_handler import ConnectionHandler\nfrom pydoll.elements.web_element import WebElement\n\nasync def custom_cdp_integration():\n    \"\"\"将 Pydoll 与您的自定义 CDP 实现一起使用。\"\"\"\n    # 您现有的 CDP 设置已找到一个元素\n    page_ws = 'ws://localhost:9222/devtools/page/ABC123'\n    \n    # 您已使用 Runtime.evaluate 查找元素\n    # 并获得了其 objectId\n    element_object_id = '{\\\"injectedScriptId\\\":1,\\\"id\\\":1}'\n    \n    # 创建 Pydoll 连接\n    connection = ConnectionHandler(ws_address=page_ws)\n    \n    # 包装元素\n    button = WebElement(\n        object_id=element_object_id,\n        connection_handler=connection\n    )\n    \n    # 使用 Pydoll 的高级方法\n    await button.wait_until(is_visible=True, timeout=5)\n    await button.wait_until(is_interactable=True)\n    \n    # 使用真实偏移点击\n    await button.click(offset_x=5, offset_y=5)\n    \n    # 轻松获取计算的属性\n    is_enabled = await button.is_enabled()\n    bounds = await button.bounds\n    \n    print(f\"按钮已点击！启用: {is_enabled}, 边界: {bounds}\")\n    \n    # 清理\n    await connection.close()\n\nasyncio.run(custom_cdp_integration())\n```\n\n!!! tip \"对象 ID 格式\"\n    `objectId` 是由 CDP 命令（如 `Runtime.evaluate` 或 `DOM.resolveNode`）返回的字符串。它通常是一个带有 `injectedScriptId` 和 `id` 等字段的 JSON 字符串。\n\n!!! info \"两全其美\"\n    这种混合方法让您利用现有的 CDP 基础设施，同时受益于 Pydoll 的人性化元素 API 来进行交互、等待和属性访问。\n\n## 安全注意事项\n\n!!! danger \"生产环境\"\n    远程调试端口暴露了对浏览器的**完全控制**，包括：\n    \n    - 访问所有页面和数据\n    - 执行任意 JavaScript 的能力\n    - Cookie 和会话访问\n    - 通过下载访问文件系统\n    \n    **未经适当的身份验证和网络安全，切勿将调试端口暴露到互联网！**\n\n### 推荐的安全实践\n\n| 实践 | 原因 | 如何做 |\n|----------|-----|-----|\n| **SSH 隧道** | 加密流量并进行身份验证 | `ssh -L 9222:localhost:9222 user@host` |\n| **VPN** | 网络级安全 | 通过企业/私有 VPN 连接 |\n| **防火墙规则** | 限制访问 | 仅允许特定 IP |\n| **Docker 网络** | 容器隔离 | 使用私有 Docker 网络 |\n| **不公开暴露** | 防止攻击 | 在生产环境中切勿绑定到 `0.0.0.0` |\n\n## 进一步阅读\n\n- **[事件系统](event-system.md)** - 监控远程浏览器事件\n- **[网络监控](../network/monitoring.md)** - 跟踪远程浏览器中的请求\n- **[浏览器选项](../configuration/browser-options.md)** - 在启动前配置本地浏览器\n\n!!! tip \"从本地开始，远程扩展\"\n    使用 `browser.start()` 在本地开发自动化以进行快速迭代，然后使用 `browser.connect()` 部署到生产 CI/CD 管道和容器化环境。"
  },
  {
    "path": "docs/zh/features/automation/file-operations.md",
    "content": "# 文件操作\n\n文件上传是浏览器自动化中最具挑战性的方面之一。传统工具经常难以处理操作系统级别的文件对话框，需要复杂的解决方法或外部库。Pydoll 提供两种直接的文件上传方法，每种都适合不同的场景。\n\n## 上传方法\n\nPydoll 支持两种主要的文件上传方法：\n\n1. **直接文件输入**（`set_input_files()`）：快速直接，适用于 `<input type=\"file\">` 元素\n2. **文件选择器上下文管理器**（`expect_file_chooser()`）：拦截文件对话框，适用于任何上传触发器\n\n## 直接文件输入\n\n最简单的方法是直接在文件输入元素上使用 `set_input_files()`。这种方法快速、可靠，并完全绕过操作系统文件对话框。\n\n### 基本用法\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def direct_file_upload():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/upload')\n        \n        # 查找文件输入元素\n        file_input = await tab.find(tag_name='input', type='file')\n        \n        # 直接设置文件\n        file_path = Path('path/to/document.pdf')\n        await file_input.set_input_files(file_path)\n        \n        # 提交表单\n        submit_button = await tab.find(id='submit-button')\n        await submit_button.click()\n        \n        print(\"文件上传成功！\")\n\nasyncio.run(direct_file_upload())\n```\n\n!!! tip \"Path 与字符串\"\n    虽然推荐使用 `pathlib` 中的 `Path` 对象作为最佳实践，以获得更好的路径处理和跨平台兼容性，但如果您喜欢，也可以使用纯字符串：\n    ```python\n    await file_input.set_input_files('path/to/document.pdf')  # 也可以！\n    ```\n\n### 多个文件\n\n对于接受多个文件的输入（`<input type=\"file\" multiple>`），传递文件路径列表：\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def upload_multiple_files():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/multi-upload')\n        \n        file_input = await tab.find(tag_name='input', type='file')\n        \n        # 一次上传多个文件\n        files = [\n            Path('documents/report.pdf'),\n            Path('images/screenshot.png'),\n            Path('data/results.csv')\n        ]\n        await file_input.set_input_files(files)\n        \n        # 正常处理\n        upload_btn = await tab.find(id='upload-btn')\n        await upload_btn.click()\n\nasyncio.run(upload_multiple_files())\n```\n\n### 动态路径解析\n\n`Path` 对象使动态构建路径和处理跨平台兼容性变得容易：\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def upload_with_dynamic_paths():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/upload')\n        \n        file_input = await tab.find(tag_name='input', type='file')\n        \n        # 动态构建路径\n        project_dir = Path(__file__).parent\n        file_path = project_dir / 'uploads' / 'data.json'\n\n        await file_input.set_input_files(file_path)\n        # 或使用主目录\n        user_file = Path.home() / 'Documents' / 'report.pdf'\n        await file_input.set_input_files(user_file)\n\nasyncio.run(upload_with_dynamic_paths())\n```\n\n!!! tip \"何时使用直接文件输入\"\n    在以下情况下使用 `set_input_files()`：\n    \n    - 文件输入在 DOM 中可直接访问\n    - 您想要最大的速度和简单性\n    - 上传不会触发文件选择器对话框\n    - 您正在使用标准的 `<input type=\"file\">` 元素\n\n## 文件选择器上下文管理器\n\n某些网站隐藏文件输入并使用自定义按钮或拖放区域来触发操作系统文件选择器对话框。对于这些情况，使用 `expect_file_chooser()` 上下文管理器。\n\n### 工作原理\n\n`expect_file_chooser()` 上下文管理器：\n\n1. 启用文件选择器拦截\n2. 等待文件选择器对话框打开\n3. 在对话框出现时自动设置文件\n4. 在操作完成后清理\n\n### 基本用法\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def file_chooser_upload():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/custom-upload')\n        \n        # 准备文件路径\n        file_path = Path.cwd() / 'document.pdf'\n        \n        # 使用上下文管理器处理文件选择器\n        async with tab.expect_file_chooser(files=file_path):\n            # 点击自定义上传按钮\n            upload_button = await tab.find(class_name='custom-upload-btn')\n            await upload_button.click()\n            # 对话框打开时文件自动设置\n        \n        # 继续您的自动化\n        print(\"通过选择器选择的文件！\")\n\nasyncio.run(file_chooser_upload())\n```\n\n### 使用文件选择器的多个文件\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def multiple_files_chooser():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/gallery-upload')\n        \n        # 准备多个文件\n        photos_dir = Path.home() / 'photos'\n        files = [\n            photos_dir / 'img1.jpg',\n            photos_dir / 'img2.jpg',\n            photos_dir / 'img3.jpg'\n        ]\n        \n        async with tab.expect_file_chooser(files=files):\n            # 通过自定义按钮触发上传\n            add_photos_btn = await tab.find(text='Add Photos')\n            await add_photos_btn.click()\n        \n        print(f\"已选择 {len(files)} 个文件！\")\n\nasyncio.run(multiple_files_chooser())\n```\n\n### 动态文件选择\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def dynamic_file_selection():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/batch-upload')\n        \n        # 使用 Path.glob() 查找目录中的所有 CSV 文件\n        data_dir = Path('data')\n        csv_files = list(data_dir.glob('*.csv'))\n        \n        async with tab.expect_file_chooser(files=csv_files):\n            upload_area = await tab.find(class_name='drop-zone')\n            await upload_area.click()\n        \n        print(f\"已选择 {len(csv_files)} 个 CSV 文件\")\n\nasyncio.run(dynamic_file_selection())\n```\n\n!!! tip \"何时使用文件选择器\"\n    在以下情况下使用 `expect_file_chooser()`：\n    \n    - 文件输入被隐藏或不可直接访问\n    - 自定义按钮触发文件选择器对话框\n    - 使用拖放上传区域\n    - 站点使用 JavaScript 打开文件对话框\n\n## 比较：直接与文件选择器\n\n| 特性 | `set_input_files()` | `expect_file_chooser()` |\n|---------|---------------------|-------------------------|\n| **速度** | ⚡ 即时 | 🕐 等待对话框 |\n| **复杂性** | 简单 | 需要上下文管理器 |\n| **要求** | 可见的文件输入 | 任何上传触发器 |\n| **用例** | 标准表单 | 自定义上传 UI |\n| **事件处理** | 不需要 | 使用页面事件 |\n\n## 完整示例\n\n这是一个结合两种方法的综合示例：\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def comprehensive_upload_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/upload-form')\n        \n        # 场景 1：个人资料图片的直接输入（单个文件）\n        avatar_input = await tab.find(id='avatar-upload')\n        avatar_path = Path.home() / 'Pictures' / 'profile.jpg'\n        await avatar_input.set_input_files(avatar_path)\n        \n        # 等待预览加载\n        await asyncio.sleep(1)\n        \n        # 场景 2：文档上传的文件选择器\n        document_path = Path.cwd() / 'documents' / 'resume.pdf'\n        async with tab.expect_file_chooser(files=document_path):\n            # 触发文件选择器的自定义样式按钮\n            upload_btn = await tab.find(class_name='btn-upload-document')\n            await upload_btn.click()\n        \n        # 等待上传确认\n        await asyncio.sleep(2)\n        \n        # 场景 3：通过文件选择器的多个文件\n        certs_dir = Path('certs')\n        certificates = [\n            certs_dir / 'certificate1.pdf',\n            certs_dir / 'certificate2.pdf',\n            certs_dir / 'certificate3.pdf'\n        ]\n        async with tab.expect_file_chooser(files=certificates):\n            add_certs_btn = await tab.find(text='Add Certificates')\n            await add_certs_btn.click()\n        \n        # 提交完整表单\n        submit_button = await tab.find(type='submit')\n        await submit_button.click()\n        \n        # 等待成功消息\n        success_msg = await tab.find(class_name='success-message', timeout=10)\n        message_text = await success_msg.text\n        print(f\"上传结果: {message_text}\")\n\nasyncio.run(comprehensive_upload_example())\n```\n\n!!! info \"方法摘要\"\n    此示例演示了 Pydoll 文件上传系统的灵活性：\n    \n    - **单个文件**：直接传递 `Path` 或 `str`（不需要列表）\n    - **多个文件**：传递 `Path` 或 `str` 对象的列表\n    - **直接输入**：对可见的 `<input>` 元素快速\n    - **文件选择器**：适用于自定义上传按钮和隐藏输入\n\n## 了解更多\n\n要更深入地了解文件上传机制：\n\n- **[事件系统](../advanced/event-system.md)**：了解 `expect_file_chooser()` 使用的页面事件\n- **[深入探讨：Tab 域](../../deep-dive/tab-domain.md#file-chooser-handling)**：文件选择器拦截的技术细节\n- **[深入探讨：事件系统](../../deep-dive/event-system.md#file-chooser-events)**：文件选择器事件如何在底层工作\n\nPydoll 中的文件操作消除了浏览器自动化中最大的痛点之一，为简单和复杂的上传场景提供了干净、可靠的方法。"
  },
  {
    "path": "docs/zh/features/automation/human-interactions.md",
    "content": "# 类人交互\n\n成功自动化与易被识破的机器人之间的关键区别之一在于交互的逼真程度。Pydoll提供精密工具，使您的自动化操作几乎与人类行为无异。\n\n!!! info \"功能状态\"\n    **已实现:**\n\n    - **人性化键盘**: 可变输入速度，真实错误与自动纠正（传入 `humanize=True`）\n    - **人性化滚动**: 基于物理的滚动，包含动量、摩擦、抖动和过冲（传入 `humanize=True`）\n    - **人性化鼠标**: 贝塞尔曲线路径、菲茨定律时序、最小急动速度、手抖和过冲（传入 `humanize=True`）\n\n    **即将推出:**\n\n    - **自动随机点击偏移**: 可选参数自动随机化元素内点击位置\n    - **悬停行为**: 悬停时的真实延迟与移动效果\n\n## 拟人化交互为何重要\n\n现代网站采用精密的机器人检测技术：\n\n- **事件时间分析**：识别超高速或精准定时操作\n- **鼠标轨迹追踪**：识别直线移动或瞬移行为\n- **键盘输入模式**：识别无单个按键操作的即时文本插入\n- **点击位置**：检测始终精准落在元素中心的点击\n- **操作序列**：识别用户行为中的非人类模式\n\nPydoll通过提供模拟真实用户行为的交互方法，助您规避检测。\n\n## 逼真鼠标移动\n\n鼠标API（`tab.mouse`）提供多层逼真效果的人性化光标控制。启用`humanize=True`时，鼠标移动遵循自然贝塞尔曲线路径，配合菲茨定律时序、最小急动速度曲线、生理性手抖和过冲修正。\n\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n\n    # 以自然曲线路径移动\n    await tab.mouse.move(500, 300, humanize=True)\n\n    # 以逼真的移动、偏移和时序点击\n    await tab.mouse.click(500, 300, humanize=True)\n\n    # 以自然移动拖拽\n    await tab.mouse.drag(100, 200, 500, 400, humanize=True)\n```\n\n人性化鼠标操作中应用的关键技术：\n\n- **贝塞尔曲线路径**：具有非对称控制点的曲线轨迹（移动初期曲率更大）\n- **菲茨定律时序**：移动持续时间随距离缩放：`MT = a + b × log₂(D/W + 1)`\n- **最小急动速度**：钟形速度曲线，起始缓慢、中间达到峰值、结尾缓慢\n- **生理性手抖**：高斯噪声（σ ≈ 1像素）与速度成反比\n- **过冲与修正**：快速移动约70%概率过冲3–12%，然后修正回来\n!!! info \"鼠标控制专用文档\"\n    有关鼠标控制的完整文档，包括所有方法、自定义时序配置、位置追踪和调试模式，请参阅**[鼠标控制](mouse-control.md)**。\n\n## 真实点击模拟\n\n### 基础点击：模拟鼠标事件\n\n`click()`方法模拟真实的鼠标按下与释放事件，区别于基于JavaScript的点击方式：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def realistic_clicking():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to(‘https://example.com’)\n        \n        button = await tab.find(id=“submit-button”)\n        \n        # 基础真实点击\n        await button.click()\n        \n        # 点击包含：\n        # - 鼠标移动至元素\n        # - 鼠标按下事件\n        # - 可配置按压时长\n        # - 鼠标释放事件\n\nasyncio.run(realistic_clicking())\n```\n\n### 带位置偏移的点击\n\n真实用户很少精确点击元素中心。使用偏移量改变点击位置：\n\n!!! info “当前状态：手动偏移计算”\n    目前每次交互需手动计算并随机化点击偏移量。未来版本将提供可选参数，支持在元素边界内自动随机化点击位置。\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\nasync def click_with_offset():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to(‘https://example.com/form’)\n        \n        submit_button = await tab.find(tag_name=“button”, type=\"submit\")\n        \n        # 点击位置略偏离中心（更自然）\n        await submit_button.click(\n            x_offset=5,   # 中心右偏5像素\n            y_offset=-3   # 中心上偏3像素\n        )\n        \n        # 当前方案：每次点击手动调整偏移量以模拟人类行为\n        for item in await tab.find(class_name=“clickable-item”, find_all=True):\n            offset_x = random.randint(-10, 10)\n            offset_y = random.randint(-10, 10)\n            await item.click(x_offset=offset_x, y_offset=offset_y)\n            await asyncio.sleep(random.uniform(0.5, 2.0))\n\nasyncio.run(click_with_offset())\n```\n\n可调点击按压时长\n\n通过调整鼠标按键按压时长模拟不同点击方式：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def variable_hold_time():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to(‘https://example.com’)\n        \n        button = await tab.find(class_name=“action-button”)\n        \n        # 快速点击（默认0.1秒）\n        await button.click(hold_time=0.05)\n        \n        # 正常点击\n        await button.click(hold_time=0.1)\n        \n        # 更慢、更刻意点击\n        await button.click(hold_time=0.2)\n        \n        # 模拟用户犹豫\n        await asyncio.sleep(0.8)\n        await button.click(hold_time=0.15)\n\nasyncio.run(variable_hold_time())\n```\n\n### 何时使用click()与click_using_js()\n\n理解两者差异对规避检测至关重要：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def click_methods_comparison():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to(‘https://example.com’)\n        \n        button = await tab.find(id=“interactive-button”)\n        \n        # 方法1：click() - 模拟真实鼠标事件\n        # ✅ 触发全部鼠标事件（按下、松开、点击）\n        # ✅ 保持元素定位\n        # ✅ 更逼真且更难被检测\n        # ❌ 需元素可见且在视口内\n        await button.click()\n        \n        # 方法二：click_using_js() - 使用 JavaScript 的 click()\n        # ✅ 可作用于隐藏元素\n        # ✅ 执行速度更快\n        # ✅ 绕过视觉覆盖层\n        # ❌ 可能被识别为自动化操作\n        # ❌ 无法触发与真实用户相同的事件序列\n        await button.click_using_js()\n\nasyncio.run(click_methods_comparison())\n```\n\n!!! 提示 “最佳实践：优先使用鼠标事件”\n    用户交互场景请使用`click()`以保持真实感。仅在后端操作、隐藏元素或追求速度且无需规避检测时使用`click_using_js()`。\n\n## 逼真文本输入\n\nPydoll的键盘API提供两种输入模式，平衡速度与隐蔽性。\n\n!!! info \"了解输入模式\"\n    | 模式 | 参数 | 行为 | 使用场景 |\n    |------|------|------|----------|\n    | **默认（快速）** | `humanize=False` | 固定50毫秒间隔，无错误 | 速度优先、低风险场景（默认） |\n    | **人性化** | `humanize=True` | 可变时序，约2%错误率并自动纠正 | **反机器人规避** |\n\n    `interval`参数已弃用。传入`humanize=True`进行真实输入。\n\n### 人性化自然输入\n\n当传入`humanize=True`时，`type_text()`使用人性化模式，模拟真实人类输入，包含可变速度和自动纠正的偶发错误：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def natural_typing():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/login')\n        \n        username_field = await tab.find(id=\"username\")\n        password_field = await tab.find(id=\"password\")\n\n        # 可变速度：按键间隔30-120毫秒\n        # 约2%错误率，带真实纠正行为\n        await username_field.type_text(\"john.doe@example.com\", humanize=True)\n        await password_field.type_text(\"MyC0mpl3xP@ssw0rd!\", humanize=True)\n\nasyncio.run(natural_typing())\n```\n\n### 不可见字段的快速输入\n\n对于无需真实模拟的字段（如隐藏字段或后端操作），使用`insert_text()`：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def fast_vs_realistic_input():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to(‘https://example.com/form’)\n        \n        # 可见字段的真实输入\n        username = await tab.find(id=“username”)\n        await username.click()\n        await username.type_text(“john_doe”, interval=0.12)\n        \n        # 隐藏/后台字段的快速插入\n        hidden_field = await tab.find(id=“hidden-token”)\n        await hidden_field.insert_text(“very-long-generated-token-12345678”)\n        \n        # 关键字段采用真实输入模拟\n        comment = await tab.find(id=“comment-box”)\n        await comment.click()\n        await comment.type_text(“This looks like human input!”, interval=0.15)\n\nasyncio.run(fast_vs_realistic_input())\n```\n\n!!! info “高级键盘控制”\n    有关全面的键盘控制文档（包括特殊键、组合键、修饰键及完整键位参考表），请参阅**[键盘控制](keyboard-control.md)**。\n\n## 逼真页面滚动\n\nPydoll提供专用滚动API，在继续执行前等待滚动完成，使您的自动化更加真实可靠。\n\n!!! info \"了解滚动模式\"\n    Pydoll的滚动API提供**三种不同模式**：\n\n    | 模式 | 参数 | 行为 | 使用场景 |\n    |------|------|------|----------|\n    | **平滑（默认）** | `smooth=True` | CSS动画，可预测 | 一般浏览模拟（默认） |\n    | **人性化** | `humanize=True` | 物理引擎：动量、抖动、过冲 | **反机器人规避** |\n    | **即时** | `smooth=False` | 立即传送到目标位置 | 速度优先场景 |\n\n    传入`humanize=True`以启用基于物理的人性化滚动来规避机器人检测。\n\n### 基础方向滚动\n\n使用`scroll.by()`方法精确控制页面任意方向的滚动：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import ScrollPosition\n\nasync def basic_scrolling():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/long-page')\n        \n        # 人性化 - 贝塞尔曲线物理引擎\n        # 包含：动量、摩擦、抖动、微停顿、过冲\n        await tab.scroll.by(ScrollPosition.DOWN, 500, humanize=True)\n        await tab.scroll.by(ScrollPosition.UP, 300, humanize=True)\n\n        # CSS动画 - 外观平滑但时序可预测\n        await tab.scroll.by(ScrollPosition.DOWN, 500, humanize=False, smooth=True)\n\n        # 即时传送 - 最快但易被检测\n        await tab.scroll.by(ScrollPosition.DOWN, 1000, humanize=False, smooth=False)\n\nasyncio.run(basic_scrolling())\n```\n\n### 滚动至特定位置\n\n导航至页面顶部或底部，可控制逼真程度：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scroll_to_positions():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/article')\n        \n        # 阅读文章开头\n        await asyncio.sleep(2.0)\n        \n        # 人性化滚动（物理引擎，反机器人）\n        await tab.scroll.to_bottom(humanize=True)\n        await asyncio.sleep(1.5)\n        await tab.scroll.to_top(humanize=True)\n\n        # CSS平滑滚动（可预测动画）\n        await tab.scroll.to_bottom(humanize=False, smooth=True)\n        await asyncio.sleep(1.5)\n        await tab.scroll.to_top(humanize=False, smooth=True)\n\nasyncio.run(scroll_to_positions())\n```\n\n!!! tip \"选择正确的模式\"\n    - **`humanize=True`**：反机器人规避的最佳选择\n    - **默认** (`smooth=True`)：适用于演示、截图和一般自动化\n    - **`smooth=False`**：隐蔽性不重要时追求最大速度\n\n### 类人滚动模式\n\nPydoll的滚动引擎使用**三次贝塞尔曲线**模拟人类滚动的物理特性，包括：\n\n- **动量**：初始速度爆发后逐渐减速\n- **摩擦**：基于\"物理阻力\"的自然减速\n- **微停顿**：长距离滚动时的短暂停顿，模拟阅读或眼球移动\n- **过冲**：偶尔滚动超过目标后回调\n\n使用`humanize=True`时自动启用此行为。\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import ScrollPosition\n\nasync def human_like_scrolling():\n    \"\"\"模拟阅读文章时的自然滚动模式。\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/article')\n        \n        # 用户从顶部开始阅读\n        await asyncio.sleep(random.uniform(2.0, 4.0))\n        \n        # 阅读时逐步滚动\n        # 滚动引擎处理物理效果（加速/减速）\n        for _ in range(random.randint(5, 8)):\n            # 变化滚动距离（模拟阅读速度）\n            scroll_distance = random.randint(300, 600)\n            await tab.scroll.by(\n                ScrollPosition.DOWN, \n                scroll_distance, \n                humanize=True  # 启用贝塞尔曲线物理\n            )\n            \n            # 停顿\"阅读\"内容\n            await asyncio.sleep(random.uniform(2.0, 5.0))\n        \n        # 快速滚动查看末尾\n        await tab.scroll.to_bottom(humanize=True)\n        await asyncio.sleep(random.uniform(1.0, 2.0))\n        \n        # 滚回顶部重读某处\n        await tab.scroll.to_top(humanize=True)\n\nasyncio.run(human_like_scrolling())\n```\n\n### 将元素滚动至可见区\n\n使用`scroll_into_view()`确保元素在截取页面屏幕截图前可见：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scroll_for_screenshots():\n    \"\"\"截取页面屏幕截图前将元素滚动至可见区。\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/product')\n        \n        # 截取完整页面屏幕截图前滚动至价格部分\n        pricing_section = await tab.find(id=\"pricing\")\n        await pricing_section.scroll_into_view()\n        await tab.take_screenshot(path=\"page_with_pricing.png\")\n        \n        # 截图前滚动至评论区\n        reviews = await tab.find(class_name=\"reviews\")\n        await reviews.scroll_into_view()\n        await tab.take_screenshot(path=\"page_with_reviews.png\")\n        \n        # 滚动至页脚以捕获完整页面状态\n        footer = await tab.find(tag_name=\"footer\")\n        await footer.scroll_into_view()\n        await tab.take_screenshot(path=\"page_with_footer.png\")\n        \n        # 注意：click()已自动滚动，因此无需：\n        # await button.scroll_into_view()  # 多余！\n        # await button.click()  # 此操作已将按钮滚动至可见区\n\nasyncio.run(scroll_for_screenshots())\n```\n\n### 处理无限滚动内容\n\n实现滚动模式加载延迟加载的内容：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import ScrollPosition\n\nasync def infinite_scroll_loading():\n    \"\"\"在无限滚动页面上加载内容。\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/feed')\n        \n        items_loaded = 0\n        max_scrolls = 10\n        \n        for scroll_num in range(max_scrolls):\n            # 滚动至底部触发加载\n            await tab.scroll.to_bottom(smooth=True)\n            \n            # 等待内容加载\n            await asyncio.sleep(random.uniform(2.0, 3.0))\n            \n            # 检查是否加载新项目\n            items = await tab.find(class_name=\"feed-item\", find_all=True)\n            new_count = len(items)\n            \n            if new_count == items_loaded:\n                print(\"无更多内容可加载\")\n                break\n            \n            items_loaded = new_count\n            print(f\"滚动 {scroll_num + 1}：已加载 {items_loaded} 项\")\n            \n            # 小幅向上滚动（人类行为）\n            if random.random() > 0.7:\n                await tab.scroll.by(ScrollPosition.UP, 200, smooth=True)\n                await asyncio.sleep(random.uniform(0.5, 1.0))\n\nasyncio.run(infinite_scroll_loading())\n```\n\n!!! success \"自动等待完成\"\n    不同于立即返回的`execute_script(\"window.scrollBy(...)\")`，`scroll` API使用CDP的`awaitPromise`参数等待浏览器的`scrollend`事件。这确保后续操作仅在滚动完全完成后执行。\n\n## 组合技术实现最高逼真度\n\n### 完整表单填写示例\n\n以下综合示例融合了所有类人交互技术。**这展示了当前手动实现最高逼真度的方案**。未来版本将自动化处理大部分随机化操作：\n\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.constants import Key\n\nasync def human_like_form_filling():\n    “”‘以最大真实感填写表单以规避检测’“”\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await 标签页.go_to(‘https://example.com/registration’)\n        \n        # 等待片刻（模拟用户阅读页面）\n        await asyncio.sleep(random.uniform(1.5, 3.0))\n        \n        # 以随机打字速度填写名字\n        名字 = await 标签页.find(id=“first-name”)\n        await first_name.click(\n            x_offset=random.randint(-5, 5),\n            y_offset=random.randint(-5, 5)\n        )\n        await asyncio.sleep(random.uniform(0.2, 0.5))\n        \n        # 手动逐字符输入并随机延迟\n        # （未来版本将实现自动化）\n        name_text = “John”\n        for char in name_text:\n            await first_name.type_text(char, interval=0)\n            await asyncio.sleep(random.uniform(0.08, 0.22))\n        \n        # 跳转至下一个字段\n        await asyncio.sleep(random.uniform(0.3, 0.8))\n        await first_name.press_keyboard_key(Key.TAB)\n        \n        # 填写姓氏\n        await asyncio.sleep(random.uniform(0.2, 0.5))\n        last_name = await tab.find(id=“last-name”)\n        await last_name.type_text(“Doe”, interval=random.uniform(0.1, 0.18))\n        \n        # 跳转至邮箱字段\n        await asyncio.sleep(random.uniform(0.4, 1.0))\n        await last_name.press_keyboard_key(Key.TAB)\n        \n        # 填充邮箱时加入真实停顿\n        await asyncio.sleep(random.uniform(0.2, 0.5))\n        email = await tab.find(id=“email”)\n        \n        email_text = “john.doe@example.com”\n        for i, char in enumerate(email_text):\n            await email.type_text(char, interval=0)\n            # 在@和.符号处延长停顿（自然）\n            if char in [‘@’, ‘.’]:\n                await asyncio.sleep(random.uniform(0.2, 0.4))\n            else:\n                await asyncio.sleep(random.uniform(0.08, 0.2))\n        \n        # 模拟用户检查输入内容\n        await asyncio.sleep(random.uniform(1.0, 2.5))\n        \n        # 带偏移量的条款同意复选框\n        terms_checkbox = await tab.find(id=“accept-terms”)\n        await terms_checkbox.click(\n            x_offset=random.randint(-3, 3),\n            y_offset=random.randint(-3, 3),\n            hold_time=random.uniform(0.08, 0.15)\n        )\n        \n        # 提交前暂停（用户审核表单）\n        await asyncio.sleep(random.uniform(1.5, 3.0))\n        \n        # 模拟真实参数点击提交按钮\n        submit_button = await tab.find(tag_name=“button”, type=\"submit\")\n        await submit_button.click(\n            x_offset=random.randint(-8, 8),\n            y_offset=random.randint(-5, 5),\n            hold_time=random.uniform(0.1, 0.2)\n        )\n        \n        print(“表单已按人类行为提交”)\n\nasyncio.run(human_like_form_filling())\n```\n\n## 规避检测的最佳实践\n\n!!! 提示 “当前需手动随机化”\n    以下最佳实践代表**Pydoll的当前状态**，您必须手动实现随机化。虽然这需要更多代码，但能让您精细控制行为。未来版本将自动实现这些模式，同时保持同等逼真度。\n\n### 1. 始终添加随机延迟\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\n# 错误示例：可预测的操作时序\nawait element1.click()\nawait element2.click()\nawait element3.click()\n\n# 良好：可变时序（当前必需）\nawait element1.click()\nawait asyncio.sleep(random.uniform(0.5, 1.5))\nawait element2.click()\nawait asyncio.sleep(random.uniform(0.8, 2.0))\nawait element3.click()\n```\n\n### 2. 变化点击位置\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\n# 错误示例：始终点击中心位置\nfor button in buttons:\n    await button.click()\n\n# 正确示例：随机变化点击位置（当前需手动设置）\nfor button in buttons:\n    await button.click(\n        x_offset=random.randint(-10, 10),\n        y_offset=random.randint(-10, 10)\n    )\n```\n\n### 3. 模拟自然用户行为\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\nasync def natural_user_simulation(tab):\n    # 用户到达页面\n    await tab.go_to(‘https://example.com’)\n    \n    # 用户阅读页面内容（1-3秒）\n    await asyncio.sleep(random.uniform(1.0, 3.0))\n    \n    # 用户向下滚动查看更多内容\n    await tab.scroll.by(ScrollPosition.DOWN, 300, smooth=True)\n    await asyncio.sleep(random.uniform(0.5, 1.5))\n    \n    # 用户找到并点击按钮\n    button = await tab.find(class_name=“cta-button”)\n    await button.click(\n        x_offset=random.randint(-5, 5),\n        y_offset=random.randint(-5, 5)\n    )\n    \n    # 用户等待内容加载\n    await asyncio.sleep(random.uniform(0.8, 1.5))\n```\n\n### 4. 组合多种技术\n\n```python\nimport asyncio\nimport random\nfrom pydoll.browser.chromium import Chrome\n\nasync def advanced_stealth_automation():\n    “”‘组合多种技术实现最大隐蔽性’“”\n    async with Chrome() as browser:\n    tab = await browser.start()\n        \n        # 模拟人类等待页面加载\n        await tab.go_to(‘https://example.com/sensitive-page’)\n        await asyncio.sleep(random.uniform(2.0, 4.0))\n        \n        # 模拟真实滚动（当前手动实现）\n        # 未来版本将提供带惯性效果的专用滚动方法\n        for _ in range(random.randint(2, 4)):\n            scroll_amount = random.randint(200, 500)\n            await tab.execute_script(f“window.scrollBy(0, {scroll_amount})”)\n            await asyncio.sleep(random.uniform(0.8, 2.0))\n        \n        # 超时查找元素（模拟用户搜索）\n        target = await tab.find(\n            class_name=\"target-element\",\n            timeout=random.randint(3, 7)\n        )\n        \n        # 模拟真实点击参数\n        await target.click(\n            x_offset=random.randint(-12, 12),\n            y_offset=random.randint(-8, 8),\n            hold_time=random.uniform(0.09, 0.18)\n        )\n        \n        # 人类反应时间\n        await asyncio.sleep(random.uniform(0.5, 1.2))\n\nasyncio.run(advanced_stealth_automation())\n```\n\n## 性能与逼真度权衡\n\n有时需要在速度与逼真度之间找到平衡：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def balanced_automation():\n    \"\"\"根据场景选择适当的逼真度级别\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/scraping-target')\n        \n        # 阶段1：初始交互（高逼真度）\n        # 此时检测系统最为活跃\n        login_button = await tab.find(text=\"Login\")\n        await asyncio.sleep(random.uniform(1.0, 2.0))\n        await login_button.click(\n            x_offset=random.randint(-5, 5),\n            y_offset=random.randint(-5, 5)\n        )\n        \n        await asyncio.sleep(random.uniform(0.5, 1.0))\n        \n        username = await tab.find(id=\"username\")\n        await username.type_text(\"user@example.com\", interval=0.12)\n        \n        await asyncio.sleep(random.uniform(0.3, 0.7))\n        \n        password = await tab.find(id=\"password\")\n        await password.type_text(\"password123\", interval=0.10)\n        \n        submit = await tab.find(type=\"submit\")\n        await asyncio.sleep(random.uniform(0.8, 1.5))\n        await submit.click()\n        \n        # 阶段2：已认证数据提取（低逼真度，高速度）\n        # 成功认证后受审查较少\n        await asyncio.sleep(2)\n        \n        # 快速浏览页面\n        items = await tab.find(class_name=\"data-item\", find_all=True)\n        \n        for item in items:\n            # 无偏移量快速点击\n            await item.click_using_js()\n            await asyncio.sleep(0.3)  # 最小延迟\n            \n            # 提取数据\n            title = await tab.find(class_name=\"title\")\n            data = await title.text\n            \n            # 快速导航\n            await tab.execute_script(\"window.history.back()\")\n            await asyncio.sleep(0.5)\n\nasyncio.run(balanced_automation())\n```\n\n## 监控与调整\n\n测试自动化操作的逼真度：\n\n```python\nimport asyncio\nimport random\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def test_interaction_timing():\n    \"\"\"记录时序以确保逼真的行为模式\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/test-page')\n        \n        # 测量并记录交互时序\n        elements = await tab.find(class_name=\"clickable\", find_all=True)\n        \n        timings = []\n        last_time = time.time()\n        \n        for i, element in enumerate(elements):\n            await element.click(\n                x_offset=random.randint(-8, 8),\n                y_offset=random.randint(-8, 8)\n            )\n            \n            current_time = time.time()\n            elapsed = current_time - last_time\n            timings.append(elapsed)\n            \n            print(f\"点击 {i+1}: 距上次操作 {elapsed:.3f}秒\")\n            last_time = current_time\n            \n            await asyncio.sleep(random.uniform(0.5, 2.0))\n        \n        # 分析时序分布\n        avg_time = sum(timings) / len(timings)\n        print(f\"\\n操作间平均时间: {avg_time:.3f}秒\")\n        print(f\"最小值: {min(timings):.3f}秒, 最大值: {max(timings):.3f}秒\")\n        \n        # 良好：可变时序且平均时间逼真（1-2秒）\n        # 不佳：恒定时序或速度不真实（<0.1秒）\n\nasyncio.run(test_interaction_timing())\n```\n\n## 了解更多\n\n有关元素交互方法的更多信息：\n\n- **[元素查找](../element-finding.md)**：定位需要交互的元素\n- **[WebElement域](../../deep-dive/architecture/webelement-domain.md)**：深入了解WebElement功能\n- **[文件操作](file-operations.md)**：上传文件和处理下载\n\n掌握类人交互技术，您的自动化将更可靠、更难检测，并更贴近真实用户行为。\n"
  },
  {
    "path": "docs/zh/features/automation/iframes.md",
    "content": "# 处理 IFrame\n\n现代网页经常使用 `<iframe>` 嵌入其他文档。旧版本的 Pydoll 需要手动调用 `tab.get_frame()` 把 iframe 转成 `Tab` 并管理 CDP target。**现在不再需要这样做。**  \niframe 现在和其他 `WebElement` 一样：可以直接调用 `find()`、`query()`、`execute_script()`、`inner_html`、`text` 等方法，Pydoll 会自动在正确的浏览上下文中执行（无论是否跨域）。\n\n!!! info \"更轻松的心智模型\"\n    把 iframe 当成页面上的普通 `div`。找到它后，就以它为起点继续查找内部元素。Pydoll 会自动创建隔离执行环境，缓存上下文，并处理多层嵌套。\n\n## 快速入门\n\n### 与页面上的第一个 iframe 交互\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def interact_with_iframe():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/page-with-iframe')\n\n        iframe = await tab.find(tag_name='iframe', id='content-frame')\n\n        # 以下调用都会在 iframe 内部执行\n        title = await iframe.find(tag_name='h1')\n        await title.click()\n\n        form = await iframe.find(id='login-form')\n        username = await form.find(name='username')\n        await username.type_text('john_doe')\n\nasyncio.run(interact_with_iframe())\n```\n\n### 多层 iframe\n\n逐层查找即可：\n\n```python\nouter = await tab.find(id='outer-frame')\ninner = await outer.find(tag_name='iframe')  # 在外层 iframe 内继续查找\n\nsubmit_button = await inner.find(id='submit')\nawait submit_button.click()\n```\n\n流程始终相同：\n\n1. 找到需要的 iframe 元素。\n2. 使用该 `WebElement` 作为新的查找范围。\n3. 如果还有内层 iframe，重复以上步骤。\n\n### 在 iframe 中执行 JavaScript\n\n```python\niframe = await tab.find(tag_name='iframe')\nresult = await iframe.execute_script('return document.title', return_by_value=True)\nprint(result['result']['result']['value'])\n```\n\nPydoll 会自动在 iframe 的隔离上下文中执行脚本，同样适用于跨域 iframe。\n\n## 为什么这样更好？\n\n- **直观：** DOM 树是什么样子，就怎么编写脚本。\n- **无需了解 CDP 细节：** 隔离世界、执行上下文、target 缓存全部由 Pydoll 处理。\n- **天然支持嵌套：** 每次查找都以当前元素为范围，多层结构依然清晰。\n- **统一 API：** 不再需要在 `Tab` 与 `WebElement` 方法之间切换。\n\n!!! tip \"`Tab.get_frame()` 将被移除\"\n    现在调用 `Tab.get_frame()` 会抛出 `DeprecationWarning`，并将在未来版本删除。请尽快改为直接使用 iframe 元素。\n\n## 常见模式\n\n### 截取 iframe 内部元素的截图\n\n```python\niframe = await tab.find(tag_name='iframe')\nchart = await iframe.find(id='sales-chart')\nawait chart.take_screenshot('chart.png')\n```\n\n### 遍历多个 iframe\n\n```python\niframes = await tab.find(tag_name='iframe', find_all=True)\nfor frame in iframes:\n    heading = await frame.find(tag_name='h2')\n    print(await heading.text)\n```\n\n### 等待 iframe 内容加载\n\n```python\niframe = await tab.find(tag_name='iframe')\nawait iframe.wait_until(is_visible=True, timeout=10)\nbanner = await iframe.find(id='promo-banner')\n```\n\n## 跨 iframe 选择器\n\n无需手动逐个查找 iframe 再在其中搜索，您可以编写一个**单一选择器**来跨越 iframe 边界。Pydoll 会自动检测 XPath 或 CSS 选择器中的 `iframe` 步骤，将其拆分为片段，并依次遍历 iframe 链。\n\n### CSS 选择器\n\n在 `iframe` 复合选择器后使用任意标准组合器（`>`、空格）：\n\n```python\n# 单个 iframe 穿越\nbutton = await tab.query('iframe > .submit-btn')\n\n# iframe 上带属性选择器\nbutton = await tab.query('iframe[src*=\"checkout\"] > #pay-button')\n\n# 嵌套 iframe\nelement = await tab.query('iframe.outer > iframe.inner > div.content')\n\n# iframe 后的多步查找\nlink = await tab.query('iframe > nav > a.home-link')\n\n# iframe 在其他元素内部（不在根位置）\nbutton = await tab.query('div > iframe > button.submit')\ncontent = await tab.query('.wrapper iframe > div.content')\n```\n\n### XPath 表达式\n\n在 `iframe` 步骤后使用 `/` —— Pydoll 会在 iframe 节点处拆分：\n\n```python\n# 单个 iframe 穿越\nbutton = await tab.query('//iframe/body/button[@id=\"submit\"]')\n\n# iframe 在其他元素内部（不在根位置）\ndiv = await tab.query('//div/iframe/div')\nitem = await tab.query('//div[@class=\"wrapper\"]/iframe/body/div')\n\n# iframe 上带谓词\nheading = await tab.query('//iframe[@src*=\"cloudflare\"]//h1')\n\n# 嵌套 iframe\nelement = await tab.query('//iframe[@id=\"outer\"]//iframe[@id=\"inner\"]//div')\n```\n\n### 工作原理\n\n当 Pydoll 遇到类似 `iframe[src*=\"checkout\"] > form > button` 的选择器时：\n\n1. **解析**选择器为片段：`iframe[src*=\"checkout\"]` 和 `form > button`\n2. 使用第一个片段**查找** iframe 元素\n3. 使用第二个片段**在 iframe 内部搜索**\n4. 对于嵌套 iframe，在每个边界重复此过程\n\n这等同于手动方式，但只需一次调用：\n\n```python\n# 手动方式（仍然有效）\niframe = await tab.find(tag_name='iframe', src='*checkout*')\nbutton = await iframe.query('form > button')\n\n# 自动方式（相同结果，一行代码）\nbutton = await tab.query('iframe[src*=\"checkout\"] > form > button')\n```\n\n### 不进行拆分的情况\n\n只有当 `iframe` 作为**标签名**出现时才会进行拆分。以下选择器保持不变：\n\n- `.iframe > body` —— 类选择器，不是标签\n- `#iframe > body` —— ID 选择器\n- `div.iframe > body` —— 标签是 `div`，不是 `iframe`\n- `[data-type=\"iframe\"] > body` —— 属性选择器\n- `iframe` 或 `//iframe` —— iframe 后无内容（没有需要搜索的内容）\n\n### find_all 支持\n\n最后一个片段支持 `find_all=True`，返回最终 iframe 内所有匹配的元素：\n\n```python\n# 获取 iframe 内的所有链接\nlinks = await tab.query('iframe > a', find_all=True)\n```\n\n## 最佳实践\n\n- **把 iframe 作为作用域：** 在 iframe `WebElement` 上调用 `find`、`query`、`execute_script` 等方法。\n- **避免 `tab.find` 查找内部元素：** 它只能访问顶级文档。\n- **复用引用：** Pydoll 会缓存 iframe 的上下文，可重复使用。\n- **现有工作流保持一致：** 滚动、截图、等待、脚本执行、读取属性等操作与普通元素完全一致。\n\n## 延伸阅读\n\n- **[元素查找](../element-finding.md)** —— 介绍查找范围与链式查询。\n- **[截图与 PDF](screenshots-and-pdfs.md)** —— 讲解如何获取视觉输出。\n- **[事件系统](../advanced/event-system.md)** —— 以事件驱动方式监听页面变化（包括 iframe）。\n\n在新模型下，iframe 不再是“特殊情况”。把它视为普通 DOM 节点，专注于自动化逻辑，其余复杂度交给 Pydoll 处理。"
  },
  {
    "path": "docs/zh/features/automation/keyboard-control.md",
    "content": "# 键盘控制\n\n键盘 API 提供了在页面级别对键盘输入的完全控制，使您能够模拟真实的输入、执行快捷键和控制复杂的按键序列。与元素级键盘方法不同，键盘 API 在页面上全局操作，为您提供与任何焦点元素交互或触发页面级键盘操作的灵活性。\n\n!!! info \"集中式键盘接口\"\n    所有键盘操作均可通过 `tab.keyboard` 访问，为所有键盘交互提供统一的 API。\n\n!!! warning \"重要的 CDP 限制：浏览器 UI 快捷键无法使用\"\n    **已知问题**：通过 Chrome DevTools Protocol 注入的事件被标记为\"不可信\"，**不会**触发浏览器 UI 操作或创建用户手势。\n    \n    **不起作用的功能：**\n\n    - 浏览器快捷键（Ctrl+T、Ctrl+W、Ctrl+N）\n    - 开发者工具快捷键（F12、Ctrl+Shift+I）\n    - 浏览器导航（Ctrl+Shift+T 重新打开标签）\n    - 任何修改浏览器 UI 或窗口的快捷键\n    \n    **完美工作的功能：**\n\n    - 页面级快捷键（Ctrl+A、Ctrl+C、Ctrl+V、Ctrl+F）\n    - 文本选择和操作\n    - 表单导航（Tab、Enter、方向键）\n    - 输入字段交互\n    - 自定义应用快捷键（在 Web 应用中）\n    \n    **技术原因**：CDP 事件不会创建浏览器安全所需的\"用户手势\"。参见 [chromium issue #615341](https://bugs.chromium.org/p/chromium/issues/detail?id=615341) 和 [CDP 文档](https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent)。\n    \n    对于浏览器级自动化，请直接使用 CDP 浏览器命令（如 `tab.close()`、`browser.new_tab()`），而不是键盘快捷键。\n\n## 快速开始\n\n键盘 API 提供三种主要方法:\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.constants import Key\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n    \n    # 按下并释放一个键\n    await tab.keyboard.press(Key.ENTER)\n    \n    # 执行快捷键组合\n    await tab.keyboard.hotkey(Key.CONTROL, Key.S)  # Ctrl+S\n    \n    # 手动控制\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.ARROWRIGHT)\n    await tab.keyboard.up(Key.SHIFT)\n```\n\n## 核心方法\n\n### Press: 完整的按键操作\n\n`press()` 方法执行完整的按键周期（按下 → 等待 → 释放）:\n\n```python\nfrom pydoll.constants import Key\n\n# 基本按键\nawait tab.keyboard.press(Key.ENTER)\nawait tab.keyboard.press(Key.TAB)\nawait tab.keyboard.press(Key.ESCAPE)\n\n# 带修饰键的按键\nawait tab.keyboard.press(Key.S, modifiers=2)  # Ctrl+S（手动修饰符）\n\n# 自定义按住时长\nawait tab.keyboard.press(Key.SPACE, interval=0.5)  # 按住 500 毫秒\n```\n\n**参数:**\n\n- `key`: 要按下的键（来自 `Key` 枚举）\n- `modifiers` (可选): 修饰符标志（Alt=1, Ctrl=2, Meta=4, Shift=8）\n- `interval` (可选): 按住键的时长（秒）（默认: 0.1）\n\n### Down: 按下键而不释放\n\n`down()` 方法按下键但不释放它，对于按住修饰键或创建按键序列很有用:\n\n```python\nfrom pydoll.constants import Key\n\n# 按住 Shift 键的同时按其他键\nawait tab.keyboard.down(Key.SHIFT)\nawait tab.keyboard.press(Key.ARROWRIGHT)  # 选择文本\nawait tab.keyboard.press(Key.ARROWRIGHT)  # 继续选择\nawait tab.keyboard.up(Key.SHIFT)\n\n# 使用修饰符标志按下\nawait tab.keyboard.down(Key.A, modifiers=2)  # Ctrl+A（全选）\n```\n\n**参数:**\n\n- `key`: 要按下的键\n- `modifiers` (可选): 要应用的修饰符标志\n\n### Up: 释放按键\n\n`up()` 方法释放先前按下的键:\n\n```python\nfrom pydoll.constants import Key\n\n# 手动按键序列\nawait tab.keyboard.down(Key.CONTROL)\nawait tab.keyboard.down(Key.SHIFT)\nawait tab.keyboard.press(Key.T)  # Ctrl+Shift+T\nawait tab.keyboard.up(Key.SHIFT)\nawait tab.keyboard.up(Key.CONTROL)\n```\n\n**参数:**\n\n- `key`: 要释放的键\n\n!!! tip \"何时使用每种方法\"\n\n    - **`press()`**: 单个按键操作（Enter、Tab、字母）\n    - **`hotkey()`**: 键盘快捷键（Ctrl+C、Ctrl+Shift+T）\n    - **`down()`/`up()`**: 复杂序列、按住修饰键、自定义时序\n\n## 快捷键：轻松实现键盘快捷方式\n\n`hotkey()` 方法自动检测修饰键并正确执行快捷键:\n\n### 基本快捷键\n\n```python\nfrom pydoll.constants import Key\n\n# 常用快捷键\nawait tab.keyboard.hotkey(Key.CONTROL, Key.C)  # 复制\nawait tab.keyboard.hotkey(Key.CONTROL, Key.V)  # 粘贴\nawait tab.keyboard.hotkey(Key.CONTROL, Key.X)  # 剪切\nawait tab.keyboard.hotkey(Key.CONTROL, Key.Z)  # 撤销\nawait tab.keyboard.hotkey(Key.CONTROL, Key.Y)  # 重做\nawait tab.keyboard.hotkey(Key.CONTROL, Key.A)  # 全选\nawait tab.keyboard.hotkey(Key.CONTROL, Key.S)  # 保存\n\n```\n\n### 三键组合\n\n```python\nfrom pydoll.constants import Key\n\n# 文本编辑快捷键（这些有效！）\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.ARROWLEFT)  # 向左选择单词\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.ARROWRIGHT)  # 向右选择单词\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.HOME)  # 选择到文档开头\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.END)  # 选择到文档末尾\n\n# 应用特定快捷键（如果 Web 应用支持）\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.Z)  # 在许多应用中重做\nawait tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.S)  # 另存为（如果应用支持）\n```\n\n### 平台特定快捷键\n\n```python\nimport sys\nfrom pydoll.constants import Key\n\n# 在 macOS 上使用 Meta（Command），在 Windows/Linux 上使用 Control\nmodifier = Key.META if sys.platform == 'darwin' else Key.CONTROL\n\nawait tab.keyboard.hotkey(modifier, Key.C)  # 复制（平台感知）\nawait tab.keyboard.hotkey(modifier, Key.V)  # 粘贴（平台感知）\n```\n\n### 快捷键工作原理\n\n`hotkey()` 方法智能处理修饰键:\n\n1. **检测修饰键**: 自动识别 Ctrl、Shift、Alt、Meta\n2. **计算标志**: 使用按位或组合修饰键（Ctrl=2, Shift=8 → 10）\n3. **正确应用**: 按下非修饰键时应用修饰符标志\n4. **干净释放**: 按相反顺序释放键\n\n```python\nfrom pydoll.constants import Key\n\n# hotkey(Key.CONTROL, Key.SHIFT, Key.T) 的幕后:\n# 1. 检测: modifiers=[CONTROL, SHIFT], keys=[T]\n# 2. 计算: modifier_value = 2 | 8 = 10\n# 3. 执行: 按下 T，modifiers=10\n# 4. 释放: 释放 T\n```\n\n!!! tip \"修饰符值\"\n    手动使用 `modifiers` 参数时:\n\n    - Alt = 1\n    - Ctrl = 2\n    - Meta/Command = 4\n    - Shift = 8\n    \n    组合它们: Ctrl+Shift = 2 + 8 = 10\n\n## 可用按键\n\n`Key` 枚举提供全面的键盘覆盖:\n\n### 字母键 (A-Z)\n\n```python\nfrom pydoll.constants import Key\n\n# 所有字母 A 到 Z\nawait tab.keyboard.press(Key.A)\nawait tab.keyboard.press(Key.Z)\n```\n\n### 数字键\n\n```python\nfrom pydoll.constants import Key\n\n# 顶部行数字 (0-9)\nawait tab.keyboard.press(Key.DIGIT0)\nawait tab.keyboard.press(Key.DIGIT9)\n\n# 数字键盘数字\nawait tab.keyboard.press(Key.NUMPAD0)\nawait tab.keyboard.press(Key.NUMPAD9)\n```\n\n### 功能键\n\n```python\nfrom pydoll.constants import Key\n\n# F1 到 F12\nawait tab.keyboard.press(Key.F1)\nawait tab.keyboard.press(Key.F12)\n```\n\n### 导航键\n\n```python\nfrom pydoll.constants import Key\n\nawait tab.keyboard.press(Key.ARROWUP)\nawait tab.keyboard.press(Key.ARROWDOWN)\nawait tab.keyboard.press(Key.ARROWLEFT)\nawait tab.keyboard.press(Key.ARROWRIGHT)\nawait tab.keyboard.press(Key.HOME)\nawait tab.keyboard.press(Key.END)\nawait tab.keyboard.press(Key.PAGEUP)\nawait tab.keyboard.press(Key.PAGEDOWN)\n```\n\n### 修饰键\n\n```python\nfrom pydoll.constants import Key\n\nawait tab.keyboard.press(Key.CONTROL)\nawait tab.keyboard.press(Key.SHIFT)\nawait tab.keyboard.press(Key.ALT)\nawait tab.keyboard.press(Key.META)  # macOS 上的 Command，Windows 上的 Windows 键\n```\n\n### 特殊键\n\n```python\nfrom pydoll.constants import Key\n\nawait tab.keyboard.press(Key.ENTER)\nawait tab.keyboard.press(Key.TAB)\nawait tab.keyboard.press(Key.SPACE)\nawait tab.keyboard.press(Key.BACKSPACE)\nawait tab.keyboard.press(Key.DELETE)\nawait tab.keyboard.press(Key.ESCAPE)\nawait tab.keyboard.press(Key.INSERT)\n```\n\n## 实用示例\n\n### 表单导航\n\n```python\nfrom pydoll.browser import Chrome\nfrom pydoll.constants import Key\n\nasync def fill_form_with_keyboard():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/form')\n        \n        # 聚焦第一个字段并输入\n        first_field = await tab.find(id='name')\n        await first_field.click()\n        await first_field.insert_text('张三')\n        \n        # 使用 Tab 键导航到下一个字段\n        await tab.keyboard.press(Key.TAB)\n        await tab.keyboard.press(Key.TAB)  # 跳过一个字段\n        \n        # 在当前焦点字段中输入\n        second_field = await tab.find(id='email')\n        await second_field.insert_text('zhangsan@example.com')\n        \n        # 使用 Enter 提交\n        await tab.keyboard.press(Key.ENTER)\n```\n\n### 文本选择和操作\n\n```python\nfrom pydoll.constants import Key\n\nasync def select_and_replace_text():\n    # 全选文本\n    await tab.keyboard.hotkey(Key.CONTROL, Key.A)\n    \n    # 复制选中内容\n    await tab.keyboard.hotkey(Key.CONTROL, Key.C)\n    \n    # 移动到末尾\n    await tab.keyboard.press(Key.END)\n    \n    # 逐字选择\n    await tab.keyboard.down(Key.CONTROL)\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.ARROWLEFT)\n    await tab.keyboard.press(Key.ARROWLEFT)\n    await tab.keyboard.up(Key.SHIFT)\n    await tab.keyboard.up(Key.CONTROL)\n    \n    # 删除选中内容\n    await tab.keyboard.press(Key.DELETE)\n```\n\n### 下拉菜单和选择导航\n\n```python\nfrom pydoll.constants import Key\n\nasync def navigate_dropdown():\n    # 打开下拉菜单\n    select = await tab.find(tag_name='select')\n    await select.click()\n    \n    # 使用箭头键导航选项\n    await tab.keyboard.press(Key.ARROWDOWN)\n    await tab.keyboard.press(Key.ARROWDOWN)\n    \n    # 使用 Enter 选择\n    await tab.keyboard.press(Key.ENTER)\n    \n    # 或使用 Escape 取消\n    await tab.keyboard.press(Key.ESCAPE)\n```\n\n### 复杂按键序列\n\n```python\nfrom pydoll.constants import Key\nimport asyncio\n\nasync def complex_editing():\n    # 选择行\n    await tab.keyboard.press(Key.HOME)  # 移动到开头\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.END)  # 选择到末尾\n    await tab.keyboard.up(Key.SHIFT)\n    \n    # 剪切\n    await tab.keyboard.hotkey(Key.CONTROL, Key.X)\n    \n    # 向下移动并粘贴\n    await tab.keyboard.press(Key.ARROWDOWN)\n    await tab.keyboard.hotkey(Key.CONTROL, Key.V)\n    \n    # 如果需要，撤销\n    await tab.keyboard.hotkey(Key.CONTROL, Key.Z)\n```\n\n## 最佳实践\n\n### 1. 添加延迟以提高可靠性\n\n```python\nfrom pydoll.constants import Key\nimport asyncio\n\n# 好: 等待 UI 更新\nawait tab.keyboard.hotkey(Key.CONTROL, Key.F)  # 打开查找\nawait asyncio.sleep(0.2)  # 等待对话框\nawait tab.keyboard.press(Key.ESCAPE)  # 关闭它\n\n# 差: 没有延迟，可能不起作用\nawait tab.keyboard.hotkey(Key.CONTROL, Key.F)\nawait tab.keyboard.press(Key.ESCAPE)  # 可能太快了\n```\n\n### 2. 输入前聚焦元素\n\n```python\nfrom pydoll.constants import Key\n\n# 好: 确保元素已聚焦\ninput_field = await tab.find(id='search')\nawait input_field.click()  # 聚焦它\nawait input_field.insert_text('query')\n\n# 差: 键盘输入进入错误元素\nawait tab.keyboard.press(Key.A)  # 这会去哪里？\n```\n\n### 3. 使用平台感知快捷键\n\n```python\nimport sys\nfrom pydoll.constants import Key\n\n# 好: 平台感知\ncmd_key = Key.META if sys.platform == 'darwin' else Key.CONTROL\nawait tab.keyboard.hotkey(cmd_key, Key.C)\n\n# 差: 硬编码（在 macOS 上不起作用）\nawait tab.keyboard.hotkey(Key.CONTROL, Key.C)\n```\n\n### 4. 清理长序列\n\n```python\nfrom pydoll.constants import Key\n\n# 好: 确保修饰键被释放\ntry:\n    await tab.keyboard.down(Key.SHIFT)\n    await tab.keyboard.press(Key.ARROWRIGHT)\n    # ... 更多操作\nfinally:\n    await tab.keyboard.up(Key.SHIFT)  # 始终释放\n\n# 差: 错误时修饰键保持按下\nawait tab.keyboard.down(Key.SHIFT)\nawait tab.keyboard.press(Key.ARROWRIGHT)\n# 这里出错会让 Shift 保持按下！\n```\n\n## 按键参考表\n\n### 常用页面级快捷键（这些有效！）\n\n| 操作 | Windows/Linux | macOS | 备注 |\n|------|--------------|-------|------|\n| 复制 | Ctrl+C | Cmd+C | 有效 |\n| 粘贴 | Ctrl+V | Cmd+V | 有效 |\n| 剪切 | Ctrl+X | Cmd+X | 有效 |\n| 撤销 | Ctrl+Z | Cmd+Z | 有效 |\n| 重做 | Ctrl+Y | Cmd+Y | 有效 |\n| 全选 | Ctrl+A | Cmd+A | 有效 |\n| 查找 | Ctrl+F | Cmd+F | 仅当 Web 应用实现时 |\n| 保存 | Ctrl+S | Cmd+S | 仅当 Web 应用实现时 |\n| 刷新 | F5 或 Ctrl+R | Cmd+R | 改用 `await tab.refresh()` |\n\n### 浏览器快捷键（通过 CDP 无法使用）\n\n| 操作 | 快捷键 | 改用 |\n|------|--------|------|\n| 新标签 | Ctrl+T | `await browser.new_tab()` |\n| 关闭标签 | Ctrl+W | `await tab.close()` |\n| 重新打开标签 | Ctrl+Shift+T | 手动跟踪标签 |\n| 开发者工具 | F12, Ctrl+Shift+I | 已经可以通过 CDP 访问！ |\n| 地址栏 | Ctrl+L | `await tab.go_to(url)` |\n\n### 所有可用按键\n\n| 类别 | 按键 |\n|------|------|\n| **字母** | `Key.A` 到 `Key.Z` (26 个键) |\n| **数字** | `Key.DIGIT0` 到 `Key.DIGIT9` (10 个键) |\n| **数字键盘** | `Key.NUMPAD0` 到 `Key.NUMPAD9`, `NUMPADMULTIPLY`, `NUMPADADD`, `NUMPADSUBTRACT`, `NUMPADDECIMAL`, `NUMPADDIVIDE` |\n| **功能键** | `Key.F1` 到 `Key.F12` (12 个键) |\n| **导航** | `ARROWUP`, `ARROWDOWN`, `ARROWLEFT`, `ARROWRIGHT`, `HOME`, `END`, `PAGEUP`, `PAGEDOWN` |\n| **修饰键** | `CONTROL`, `SHIFT`, `ALT`, `META` |\n| **特殊键** | `ENTER`, `TAB`, `SPACE`, `BACKSPACE`, `DELETE`, `ESCAPE`, `INSERT` |\n| **锁定键** | `CAPSLOCK`, `NUMLOCK`, `SCROLLLOCK` |\n| **符号** | `SEMICOLON`, `EQUALSIGN`, `COMMA`, `MINUS`, `PERIOD`, `SLASH`, `GRAVEACCENT`, `BRACKETLEFT`, `BACKSLASH`, `BRACKETRIGHT`, `QUOTE` |\n\n### 修饰符标志值\n\n| 修饰符 | 值 | 二进制 | 用法 |\n|--------|---|--------|------|\n| Alt | 1 | 0001 | `modifiers=1` |\n| Ctrl | 2 | 0010 | `modifiers=2` |\n| Meta | 4 | 0100 | `modifiers=4` |\n| Shift | 8 | 1000 | `modifiers=8` |\n| Ctrl+Shift | 10 | 1010 | `modifiers=10` |\n| Ctrl+Alt | 3 | 0011 | `modifiers=3` |\n| Ctrl+Shift+Alt | 11 | 1011 | `modifiers=11` |\n\n## 从 WebElement 方法迁移\n\n先前在 `WebElement` 上的键盘方法已弃用。以下是如何迁移:\n\n### 旧 vs 新\n\n```python\nfrom pydoll.constants import Key\n\n# 旧（已弃用）\nelement = await tab.find(id='input')\nawait element.key_down(Key.A, modifiers=2)\nawait element.key_up(Key.A)\nawait element.press_keyboard_key(Key.ENTER)\n\n# 新（推荐）\nawait tab.keyboard.down(Key.A, modifiers=2)\nawait tab.keyboard.up(Key.A)\nawait tab.keyboard.press(Key.ENTER)\n```\n\n!!! warning \"弃用通知\"\n    以下 `WebElement` 方法已弃用:\n\n    - `key_down()` → 使用 `tab.keyboard.down()`\n    - `key_up()` → 使用 `tab.keyboard.up()`\n    - `press_keyboard_key()` → 使用 `tab.keyboard.press()`\n    \n    这些方法仍然可以工作以保持向后兼容性，但会显示弃用警告。\n\n### 为什么要迁移？\n\n- **集中化**: 所有键盘操作在一个地方\n- **更清晰的 API**: 所有键盘操作的一致接口\n- **更强大**: 快捷键支持，智能修饰符检测\n- **更好的类型支持**: 完整的 IDE 自动完成支持\n\n## 了解更多\n\n有关其他自动化功能:\n\n- **[人性化交互](human-interactions.md)**: 真实的点击、滚动和鼠标移动\n- **[表单处理](form-handling.md)**: 完整的表单自动化工作流程\n- **[文件操作](file-operations.md)**: 文件上传自动化\n\n键盘 API 消除了键盘自动化的复杂性，为从简单按键到复杂快捷键和序列的所有内容提供了干净、可靠的方法。\n"
  },
  {
    "path": "docs/zh/features/automation/mouse-control.md",
    "content": "# 鼠标控制\n\n鼠标API提供页面级别的完整鼠标输入控制，支持模拟逼真的光标移动、点击、双击和拖拽操作。当传入`humanize=True`时，鼠标操作使用人性化模拟：路径遵循自然贝塞尔曲线，配合菲茨定律时序、最小急动速度曲线、生理性手抖和过冲修正，使自动化操作几乎无法与人类行为区分。\n\n!!! info \"集中式鼠标接口\"\n    所有鼠标操作均通过`tab.mouse`访问，为所有鼠标交互提供简洁统一的API。\n\n## 快速开始\n\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.input.types import MouseButton\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n    await tab.go_to('https://example.com')\n\n    # 移动光标到指定位置\n    await tab.mouse.move(500, 300)\n\n    # 在指定位置点击\n    await tab.mouse.click(500, 300)\n\n    # 右键点击\n    await tab.mouse.click(500, 300, button=MouseButton.RIGHT)\n\n    # 双击\n    await tab.mouse.double_click(500, 300)\n\n    # 从一个位置拖拽到另一个位置\n    await tab.mouse.drag(100, 200, 500, 400)\n```\n\n## 核心方法\n\n### move: 移动光标\n\n将鼠标光标移动到页面上的指定位置：\n\n```python\n# 默认移动（单个CDP事件，无模拟）\nawait tab.mouse.move(500, 300)\n\n# 人性化移动（自然时序的曲线路径）\nawait tab.mouse.move(500, 300, humanize=True)\n```\n\n**参数：**\n\n- `x`：目标X坐标（CSS像素）\n- `y`：目标Y坐标（CSS像素）\n- `humanize`（仅关键字）：模拟人类般的曲线移动（默认：`False`）\n\n### click: 在指定位置点击\n\n移动到指定位置并执行鼠标点击：\n\n```python\nfrom pydoll.protocol.input.types import MouseButton\n\n# 左键点击（默认，瞬时）\nawait tab.mouse.click(500, 300)\n\n# 右键点击\nawait tab.mouse.click(500, 300, button=MouseButton.RIGHT)\n\n# 通过click_count实现双击\nawait tab.mouse.click(500, 300, click_count=2)\n\n# 人性化点击，自然移动\nawait tab.mouse.click(500, 300, humanize=True)\n```\n\n**参数：**\n\n- `x`：目标X坐标\n- `y`：目标Y坐标\n- `button`（仅关键字）：鼠标按钮，可选 `LEFT`、`RIGHT`、`MIDDLE`（默认：`LEFT`）\n- `click_count`（仅关键字）：点击次数（默认：`1`）\n- `humanize`（仅关键字）：模拟人类般的行为（默认：`False`）\n\n### double_click: 在指定位置双击\n\n等价于`click(x, y, click_count=2)`的便捷方法：\n\n```python\nawait tab.mouse.double_click(500, 300)\nawait tab.mouse.double_click(500, 300, humanize=False)\n```\n\n### down / up: 底层按钮控制\n\n独立按下或释放鼠标按钮：\n\n```python\n# 在当前位置按下左键\nawait tab.mouse.down()\n\n# 释放左键\nawait tab.mouse.up()\n\n# 右键\nawait tab.mouse.down(button=MouseButton.RIGHT)\nawait tab.mouse.up(button=MouseButton.RIGHT)\n```\n\n这些是底层原语，在当前光标位置操作，没有`humanize`参数。\n\n### drag: 拖放\n\n按住鼠标按钮从起点移动到终点：\n\n```python\n# 默认拖拽（瞬时）\nawait tab.mouse.drag(100, 200, 500, 400)\n\n# 人性化拖拽，自然移动\nawait tab.mouse.drag(100, 200, 500, 400, humanize=True)\n```\n\n**参数：**\n\n- `start_x`、`start_y`：起始坐标\n- `end_x`、`end_y`：结束坐标\n- `humanize`（仅关键字）：模拟人类般的拖拽（默认：`False`）\n\n## 启用人性化\n\n所有鼠标方法默认使用`humanize=False`。要启用带有自然贝塞尔曲线路径和真实时序的人性化模拟，传入`humanize=True`：\n\n```python\n# 人性化移动，菲茨定律时序的自然曲线路径\nawait tab.mouse.move(500, 300, humanize=True)\n\n# 人性化点击：曲线移动+点击前停顿+按下+释放\nawait tab.mouse.click(500, 300, humanize=True)\n\n# 人性化拖拽，自然曲线和停顿\nawait tab.mouse.drag(100, 200, 500, 400, humanize=True)\n```\n\n当规避检测很重要时推荐使用，例如与采用机器人检测的网站交互时。\n\n## 人性化模式\n\n当传入`humanize=True`时，鼠标模块应用多层逼真效果：\n\n### 贝塞尔曲线路径\n\n鼠标沿自然曲线轨迹移动，而非直线。控制点在起点→终点连线的垂直方向上随机偏移，采用非对称放置（移动初期曲率更大，模拟真实的弹道伸展）。\n\n### 菲茨定律时序\n\n移动持续时间遵循菲茨定律：`MT = a + b × log₂(D/W + 1)`。距离越远所需时间成比例增加，符合人类运动控制行为。\n\n### 最小急动速度曲线\n\n光标遵循钟形速度曲线，起始缓慢，中间加速到峰值速度，然后在末尾减速。这符合最平滑的人类运动轨迹。\n\n### 生理性手抖\n\n每帧添加小幅高斯噪声（σ ≈ 1像素），模拟手部震颤。颤抖幅度与速度成反比，光标缓慢或悬停时颤抖更多，快速弹道运动时颤抖减少。\n\n### 过冲与修正\n\n对于快速长距离移动（约70%概率），光标会超过目标3–12%的距离，然后做一个小的修正子运动回到目标。这符合真实人类运动控制数据。\n\n### 点击前停顿\n\n人性化点击包含点击前停顿（50–200毫秒），模拟按下按钮前的自然稳定时间。\n\n## 自动人性化元素点击\n\n当您使用`element.click(humanize=True)`时，鼠标API会从当前光标位置到元素中心产生逼真的贝塞尔曲线运动后再点击，使元素点击与人类行为无法区分。\n\n```python\n# 默认点击：原始CDP按下/释放\nbutton = await tab.find(id='submit')\nawait button.click()\n\n# 带中心偏移\nawait button.click(x_offset=10, y_offset=5)\n\n# 人性化点击：贝塞尔曲线运动+点击\nawait button.click(humanize=True)\n```\n\n位置追踪在元素点击之间保持。点击元素A，然后点击元素B，会产生从A到B的自然曲线路径。\n\n## 自定义时序配置\n\n所有人性化参数均可通过`MouseTimingConfig`配置：\n\n```python\nfrom pydoll.interactions.mouse import MouseTimingConfig\n\nconfig = MouseTimingConfig(\n    fitts_a=0.070,              # 菲茨定律截距（秒）\n    fitts_b=0.150,              # 菲茨定律斜率（秒/比特）\n    frame_interval=0.012,       # mouseMoved事件间的基础间隔\n    curvature_min=0.10,         # 最小路径曲率（距离的分数）\n    curvature_max=0.30,         # 最大路径曲率\n    tremor_amplitude=1.0,       # 颤抖sigma值（像素）\n    overshoot_probability=0.70, # 快速移动时过冲的概率\n    min_duration=0.08,          # 最小移动持续时间\n    max_duration=2.5,           # 最大移动持续时间\n)\n\n# 应用到tab的鼠标实例\ntab.mouse.timing = config\n```\n\n查看`MouseTimingConfig`数据类了解所有可用参数。\n\n## 位置追踪\n\n鼠标API在操作之间追踪光标位置：\n\n```python\n# 初始位置为(0, 0)\nawait tab.mouse.move(100, 200)\n# 位置现在是(100, 200)\n\nawait tab.mouse.click(300, 400)\n# 位置现在是(300, 400)\n\n# 底层方法使用追踪的位置\nawait tab.mouse.down()   # 在(300, 400)按下\nawait tab.mouse.up()     # 在(300, 400)释放\n```\n\n!!! note \"位置状态\"\n    鼠标位置在内部追踪。`WebElement.click()`在可用时自动使用`tab.mouse`，因此位置追踪在元素点击之间保持一致。\n\n## 调试模式\n\n启用调试模式以在页面上可视化鼠标移动。激活后，彩色点将绘制在透明覆盖画布上：\n\n- **蓝色点**：移动过程中的光标路径\n- **红色点**：点击位置\n\n```python\n# 通过属性在运行时启用\ntab.mouse.debug = True\n\n# 现在所有移动都会绘制彩色点\nawait tab.mouse.click(500, 300)\n\n# 完成后禁用\ntab.mouse.debug = False\n```\n\n这对于调整时序参数和验证路径是否自然很有用。\n\n## 实用示例\n\n### 以逼真移动点击按钮\n\n```python\nasync def click_button_naturally(tab):\n    # element.click() 自动使用 tab.mouse 进行人性化移动\n    button = await tab.find(id='submit')\n    await button.click()\n```\n\n### 拖动滑块\n\n```python\nasync def drag_slider(tab):\n    slider = await tab.find(css_selector='.slider-handle')\n    bounds = await slider.get_bounds_using_js()\n\n    start_x = bounds['x'] + bounds['width'] / 2\n    start_y = bounds['y'] + bounds['height'] / 2\n    end_x = start_x + 200  # 向右拖拽200像素\n\n    await tab.mouse.drag(start_x, start_y, end_x, start_y)\n```\n\n### 悬停在元素上\n\n```python\nasync def hover_menu(tab):\n    menu = await tab.find(css_selector='.dropdown-trigger')\n    bounds = await menu.get_bounds_using_js()\n\n    await tab.mouse.move(\n        bounds['x'] + bounds['width'] / 2,\n        bounds['y'] + bounds['height'] / 2,\n    )\n    # 菜单现在应通过CSS :hover可见\n```\n\n## 了解更多\n\n- **[类人交互](human-interactions.md)**：所有人性化交互的概述\n- **[键盘控制](keyboard-control.md)**：逼真的键盘模拟\n"
  },
  {
    "path": "docs/zh/features/automation/screenshots-and-pdfs.md",
    "content": "# 截图与PDF\n\nPydoll通过直接使用Chrome DevTools Protocol命令提供强大的截图和PDF生成功能。可以捕获完整页面、特定元素或生成具有精细控制的PDF。\n\n## 截图\n\n### 基础页面截图\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def take_page_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # 保存截图到文件\n        await tab.take_screenshot('page.png', quality=100)\n\nasyncio.run(take_page_screenshot())\n```\n\n### 支持的格式\n\nPydoll基于文件扩展名支持三种图像格式：\n\n```python\n# PNG格式（无损，文件较大）\nawait tab.take_screenshot('screenshot.png', quality=100)\n\n# JPEG格式（有损，文件较小）\nawait tab.take_screenshot('screenshot.jpeg', quality=85)\n\n# WebP格式（现代、高效）\nawait tab.take_screenshot('screenshot.webp', quality=90)\n```\n\n!!! info \"格式检测\"\n    图像格式由文件扩展名自动确定。使用不支持的扩展名会引发`InvalidFileExtension`异常。\n    \n    JPEG格式同时支持`.jpg`和`.jpeg`（`.jpg`会自动在内部标准化为`.jpeg`以匹配CDP要求）。\n\n### 截图参数\n\n| 参数 | 类型 | 默认值 | 描述 |\n|-----------|------|---------|-------------|\n| `path` | `Optional[str]` | `None` | 保存截图的文件路径。如果`as_base64=False`则为必需。 |\n| `quality` | `int` | `100` | 图像质量（0-100）。值越高质量越好，文件越大。 |\n| `beyond_viewport` | `bool` | `False` | 捕获整个可滚动页面，而不仅仅是可见区域。 |\n| `as_base64` | `bool` | `False` | 返回base64编码的字符串而不是保存到文件。 |\n\n### 完整页面截图\n\n捕获超出可见视口的内容：\n\n```python\nasync def full_page_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/long-page')\n        \n        # 捕获整个页面，包括折叠下方的内容\n        await tab.take_screenshot(\n            'full-page.png',\n            beyond_viewport=True,\n            quality=90\n        )\n```\n\n!!! warning \"性能注意\"\n    在非常长的页面上使用`beyond_viewport=True`可能会消耗大量内存并需要更长的处理时间。\n\n### Base64截图\n\n获取截图的base64字符串用于嵌入或通过API发送：\n\n```python\nasync def base64_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # 获取截图的base64字符串\n        screenshot_base64 = await tab.take_screenshot(\n            as_base64=True\n        )\n        \n        # 在HTML img标签中使用\n        html = f'<img src=\"data:image/png;base64,{screenshot_base64}\" />'\n        \n        # 或通过API发送\n        import aiohttp\n        async with aiohttp.ClientSession() as session:\n            await session.post(\n                'https://api.example.com/upload',\n                json={'image': screenshot_base64}\n            )\n```\n\n### 元素截图\n\n捕获特定元素而非整个页面：\n\n```python\nasync def element_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # 截取特定元素（PNG）\n        header = await tab.find(tag_name='header')\n        await header.take_screenshot('header.png', quality=100)\n        \n        # 截取表单（JPEG）\n        form = await tab.find(id='login-form')\n        await form.take_screenshot('login-form.jpeg', quality=85)\n        \n        # 截取图表或图形（WebP）\n        chart = await tab.find(class_name='data-visualization')\n        await chart.take_screenshot('chart.webp', quality=90)\n```\n\n!!! info \"格式检测\"\n    图像格式自动从文件扩展名（`.png`、`.jpeg`/`.jpg`或`.webp`）检测。使用不支持的扩展名会引发`InvalidFileExtension`异常。\n\n!!! tip \"自动滚动\"\n    捕获元素截图时，Pydoll会在截图前自动将元素滚动到视图中。\n\n### 元素截图 vs 页面截图\n\n| 功能 | `tab.take_screenshot()` | `element.take_screenshot()` |\n|---------|------------------------|----------------------------|\n| **范围** | 整个视口或页面 | 仅特定元素 |\n| **格式支持** | PNG, JPEG, WebP | PNG, JPEG, WebP |\n| **超出视口** | ✅ 支持 | ❌ 不适用 |\n| **Base64输出** | ✅ 支持 | ✅ 支持 |\n| **自动滚动** | ❌ 不适用 | ✅ 是 |\n| **使用场景** | 完整页面捕获 | 组件隔离、测试 |\n\n\n## PDF生成\n\n### 基础PDF导出\n\n将页面转换为打印质量的PDF输出：\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def generate_pdf():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/document')\n        \n        # 使用Path生成PDF\n        await tab.print_to_pdf(Path('document.pdf'))\n        \n        # 或使用字符串\n        await tab.print_to_pdf('document.pdf')\n\nasyncio.run(generate_pdf())\n```\n\n### PDF参数\n\n| 参数 | 类型 | 默认值 | 描述 |\n|-----------|------|---------|-------------|\n| `path` | `Optional[str \\| Path]` | `None` | 保存PDF的文件路径。如果`as_base64=False`则为必需。 |\n| `landscape` | `bool` | `False` | 使用横向方向（相对于纵向）。 |\n| `display_header_footer` | `bool` | `False` | 包含浏览器生成的页眉/页脚，带有标题、URL、页码。 |\n| `print_background` | `bool` | `True` | 包含背景图形和颜色。 |\n| `scale` | `float` | `1.0` | 页面缩放因子（0.1-2.0）。用于放大/缩小效果。 |\n| `as_base64` | `bool` | `False` | 返回base64编码的字符串而不是保存到文件。 |\n\n!!! tip \"Path vs 字符串\"\n    虽然推荐使用`pathlib`的`Path`对象作为最佳实践以获得更好的路径处理和跨平台兼容性，但如果您愿意，也可以使用普通字符串。\n\n### 高级PDF选项\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def advanced_pdf():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/report')\n        \n        # 带页眉/页脚的横向PDF\n        await tab.print_to_pdf(\n            Path('report-landscape.pdf'),\n            landscape=True,\n            display_header_footer=True,\n            print_background=True,\n            scale=0.9\n        )\n        \n        # 无背景的纵向PDF（节省墨水）\n        await tab.print_to_pdf(\n            Path('report-ink-friendly.pdf'),\n            landscape=False,\n            print_background=False,\n            scale=1.0\n        )\n\nasyncio.run(advanced_pdf())\n```\n\n### PDF缩放因子\n\n控制PDF输出的缩放级别：\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def scaled_pdfs():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/content')\n        \n        # 缩小内容以在每页上容纳更多\n        await tab.print_to_pdf(Path('compact.pdf'), scale=0.7)\n        \n        # 正常缩放\n        await tab.print_to_pdf(Path('normal.pdf'), scale=1.0)\n        \n        # 放大内容（页数更少）\n        await tab.print_to_pdf(Path('large.pdf'), scale=1.5)\n\nasyncio.run(scaled_pdfs())\n```\n\n!!! warning \"缩放限制\"\n    `scale`参数接受`0.1`到`2.0`之间的值。超出此范围的值可能产生意外结果。\n\n### Base64 PDF\n\n生成PDF的base64字符串用于API传输：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def base64_pdf():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/invoice')\n        \n        # 获取PDF的base64（无需路径）\n        pdf_base64 = await tab.print_to_pdf(as_base64=True)\n        \n        # 通过API发送\n        import aiohttp\n        async with aiohttp.ClientSession() as session:\n            await session.post(\n                'https://api.example.com/invoices',\n                json={'pdf': pdf_base64}\n            )\n\nasyncio.run(base64_pdf())\n```\n\n\n!!! info \"CDP参考\"\n    有关这些命令的完整CDP文档，请参阅：\n    \n    - [Page.captureScreenshot](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot)\n    - [Page.printToPDF](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF)\n\n### 错误处理\n\n```python\nfrom pydoll.exceptions import (\n    InvalidFileExtension,\n    MissingScreenshotPath,\n    TopLevelTargetRequired\n)\n\nasync def safe_screenshot():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        try:\n            # 缺少路径且as_base64=False\n            await tab.take_screenshot()\n        except MissingScreenshotPath:\n            print(\"错误：必须提供路径或设置as_base64=True\")\n        \n        try:\n            # 无效的扩展名\n            await tab.take_screenshot('image.bmp')\n        except InvalidFileExtension as e:\n            print(f\"错误：{e}\")\n        \n        # IFrame截图限制\n        iframe_element = await tab.find(tag_name='iframe')\n\n        # 仍然无效：顶层截图不会包含 iframe 内容\n        # await tab.take_screenshot('frame.png')\n\n        # 选择 iframe 内部的元素进行截图\n        content = await iframe_element.find(id='content')\n        await content.take_screenshot('iframe-content.png')\n```\n\n## 页面打包导出\n\n将整个页面及其所有资源（CSS、JS、图片、字体）保存为 `.zip` 压缩包，支持离线查看。\n\n### 基本用法\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def save_page():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n\n        # 将页面和资源保存为独立文件\n        await tab.save_bundle('page.zip')\n\nasyncio.run(save_page())\n```\n\n生成的 zip 包含一个 `index.html`，所有 URL 已被重写为引用 `assets/` 目录下的本地文件。\n\n### 内联模式\n\n使用 data URI、`<style>` 和 `<script>` 标签将所有内容直接嵌入到单个 `index.html` 中：\n\n```python\n# zip 中只包含一个自包含的 HTML 文件\nawait tab.save_bundle('page-inline.zip', inline_assets=True)\n```\n\n### 参数\n\n| 参数 | 类型 | 默认值 | 描述 |\n|------|------|--------|------|\n| `path` | `str \\| Path` | *（必填）* | 目标路径，必须以 `.zip` 结尾。 |\n| `inline_assets` | `bool` | `False` | 将所有资源内联嵌入，而非保存为独立文件。 |\n\n!!! info \"打包包含的内容\"\n    打包包括以下类型的资源：Document、Stylesheet、Script、Image、Font 和 Media。加载失败、已取消或使用 `data:` URI 的资源会被自动跳过。\n\n## 了解更多\n\n有关截图和PDF如何与Pydoll架构集成的更多信息：\n\n- **[深入探讨：CDP](../../deep-dive/fundamentals/cdp.md)**：理解Chrome DevTools Protocol命令\n- **[API参考：Tab](../../api/browser/tab.md#take_screenshot)**：完整的方法签名和参数\n- **[API参考：WebElement](../../api/elements/web-element.md#take_screenshot)**：元素特定的截图能力\n\n截图和PDF是自动化、测试和文档编制的必备工具。Pydoll的直接CDP集成提供专业级输出和精细控制。\n\n"
  },
  {
    "path": "docs/zh/features/browser-management/contexts.md",
    "content": "# 浏览器上下文\n\n浏览器上下文是Pydoll在单个浏览器进程内创建完全隔离的浏览环境的解决方案。可以将它们视为独立的\"隐私窗口\"，但具有完全的编程控制，每个上下文维护自己的Cookie、存储、缓存和认证状态。\n\n## 快速入门\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def basic_context_example():\n    async with Chrome() as browser:\n        # 在默认上下文中启动浏览器并创建初始标签页\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        \n        # 创建隔离的上下文\n        context_id = await browser.create_browser_context()\n        \n        # 在隔离上下文中创建新标签页\n        isolated_tab = await browser.new_tab('https://example.com', browser_context_id=context_id)\n        \n        # 两个标签页完全隔离 - 不同的Cookie、存储等\n        await initial_tab.execute_script(\"localStorage.setItem('user', 'Alice')\")\n        await isolated_tab.execute_script(\"localStorage.setItem('user', 'Bob')\")\n        \n        # 验证隔离\n        user_default = await initial_tab.execute_script(\"return localStorage.getItem('user')\")\n        user_isolated = await isolated_tab.execute_script(\"return localStorage.getItem('user')\")\n        \n        print(f\"默认上下文: {user_default}\")  # Alice\n        print(f\"隔离上下文: {user_isolated}\")  # Bob\n\nasyncio.run(basic_context_example())\n```\n\n## 什么是浏览器上下文？\n\n浏览器上下文是单个浏览器进程内的隔离浏览环境。每个上下文维护完全独立的：\n\n| 组件 | 描述 | 隔离级别 |\n|-----------|-------------|-----------------|\n| **Cookie** | HTTP Cookie和会话数据 | ✓ 完全隔离 |\n| **本地存储** | `localStorage`和`sessionStorage` | ✓ 完全隔离 |\n| **IndexedDB** | 客户端数据库 | ✓ 完全隔离 |\n| **缓存** | HTTP缓存和资源 | ✓ 完全隔离 |\n| **权限** | 地理位置、通知、摄像头等 | ✓ 完全隔离 |\n| **认证** | 登录会话和认证令牌 | ✓ 完全隔离 |\n| **Service Workers** | 后台脚本 | ✓ 完全隔离 |\n\n```mermaid\ngraph LR\n    Browser[浏览器进程] --> Default[默认上下文]\n    Browser --> Context1[上下文1]\n    Browser --> Context2[上下文2]\n    \n    Default --> T1[标签A]\n    Default --> T2[标签B]\n    Context1 --> T3[标签C]\n    Context2 --> T4[标签D]\n```\n\n## 为什么使用浏览器上下文？\n\n### 1. 多账户测试\n\n同时测试不同的用户账户而不产生干扰：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def perform_login(tab, email, password):\n    \"\"\"\n    辅助函数：导航到登录页面\n    并提交账户凭据。\n    \"\"\"\n    print(f\"尝试使用以下账户登录：{email}...\")\n    await tab.go_to('https://app.example.com/login')\n\n    # 查找元素\n    email_field = await tab.find(id='email')\n    password_field = await tab.find(id='password')\n    login_btn = await tab.find(id='login-btn')\n\n    # 填写凭据并点击\n    await email_field.type_text(email)\n    await password_field.type_text(password)\n    await login_btn.click()\n\n    # 等待登录处理\n    await asyncio.sleep(2)\n    print(f\"{email} 登录成功。\")\n\n\nasync def multi_account_test():\n    \"\"\"\n    使用隔离的浏览器上下文\n    测试同时登录的主脚本。\n    \"\"\"\n    accounts = [\n        {\"email\": \"user1@example.com\", \"password\": \"pass1\"},\n        {\"email\": \"user2@example.com\", \"password\": \"pass2\"},\n        {\"email\": \"admin@example.com\", \"password\": \"admin_pass\"}\n    ]\n\n    # 此列表将存储每个活动用户会话的信息\n    user_sessions = []\n\n    async with Chrome() as browser:\n        first_account = accounts[0]\n        initial_tab = await browser.start()\n        await perform_login(initial_tab, first_account['email'], first_account['password'])\n        user_sessions.append({\n            \"email\": first_account['email'],\n            \"tab\": initial_tab,\n            \"context_id\": None  # 'None' 表示默认浏览器上下文\n        })\n\n        # 遍历其余账户\n        for account in accounts[1:]:\n            context_id = await browser.create_browser_context()\n            new_tab = await browser.new_tab(browser_context_id=context_id)\n            await perform_login(new_tab, account['email'], account['password'])\n\n            # 将新会话信息添加到列表\n            user_sessions.append({\n                \"email\": account['email'],\n                \"tab\": new_tab,\n                \"context_id\": context_id\n            })\n\n        print(\"\\n--- 验证所有活动会话 ---\")\n        for session in user_sessions:\n            tab = session[\"tab\"]\n            email = session[\"email\"]\n            await tab.go_to('https://app.example.com/dashboard')\n            username = await tab.find(class_name='username')\n            username_text = await username.text\n            print(f\"[账户：{email}] -> 登录为：{username_text}\")\n            await asyncio.sleep(0.5)\n\n        print(\"\\n--- 清理上下文 ---\")\n        for session in user_sessions:\n            # 仅关闭我们创建的上下文（非None）\n            if session[\"context_id\"] is not None:\n                print(f\"关闭上下文：{session['email']}\")\n                await session[\"tab\"].close()\n                await browser.delete_browser_context(session[\"context_id\"])\n        \n        # 默认上下文（None）由\n        # 'async with Chrome() as browser' 自动关闭\n\nasyncio.run(multi_account_test())\n```\n\n### 2. 使用上下文特定代理的地理位置测试\n\n每个上下文可以拥有自己的代理配置：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def geo_location_testing():\n    async with Chrome() as browser:\n        # 启动浏览器并使用初始标签页进行第一个测试（默认上下文，无代理）\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://api.ipify.org')\n        await asyncio.sleep(2)\n        default_ip = await initial_tab.execute_script('return document.body.textContent')\n        print(f\"默认IP（无代理）：{default_ip}\")\n        \n        # 带美国代理的美国上下文\n        us_context = await browser.create_browser_context(\n            proxy_server='http://us-proxy.example.com:8080'\n        )\n        us_tab = await browser.new_tab('https://api.ipify.org', browser_context_id=us_context)\n        await asyncio.sleep(2)\n        us_ip = await us_tab.execute_script('return document.body.textContent')\n        print(f\"美国IP：{us_ip}\")\n        \n        # 带欧盟代理的欧盟上下文\n        eu_context = await browser.create_browser_context(\n            proxy_server='http://eu-proxy.example.com:8080'\n        )\n        eu_tab = await browser.new_tab('https://api.ipify.org', browser_context_id=eu_context)\n        await asyncio.sleep(2)\n        eu_ip = await eu_tab.execute_script('return document.body.textContent')\n        print(f\"欧盟IP：{eu_ip}\")\n        \n        # 清理（跳过初始标签页）\n        await us_tab.close()\n        await eu_tab.close()\n        await browser.delete_browser_context(us_context)\n        await browser.delete_browser_context(eu_context)\n\nasyncio.run(geo_location_testing())\n```\n\n!!! tip \"代理认证\"\n    Pydoll自动处理上下文的代理认证。只需在URL中包含凭据：\n    ```python\n    context_id = await browser.create_browser_context(\n        proxy_server='http://username:password@proxy.example.com:8080'\n    )\n    ```\n    凭据从CDP命令中清理，仅在浏览器要求认证时使用。\n\n### 3. A/B测试\n\n并行比较不同的用户体验：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def ab_testing():\n    async with Chrome() as browser:\n        # 启动浏览器并创建初始标签页（默认上下文中的对照组）\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        await initial_tab.execute_script(\"localStorage.setItem('experiment', 'control')\")\n        \n        # 隔离上下文中的实验组\n        context_b = await browser.create_browser_context()\n        tab_b = await browser.new_tab('https://example.com', browser_context_id=context_b)\n        await tab_b.execute_script(\"localStorage.setItem('experiment', 'treatment')\")\n        \n        # 将两者导航到功能页面\n        await initial_tab.go_to('https://example.com/feature')\n        await tab_b.go_to('https://example.com/feature')\n        \n        # 比较结果\n        result_a = await initial_tab.find(class_name='experiment-result')\n        result_b = await tab_b.find(class_name='experiment-result')\n        \n        print(f\"对照组结果：{await result_a.text}\")\n        print(f\"实验组结果：{await result_b.text}\")\n        \n        # 清理\n        await tab_b.close()\n        await browser.delete_browser_context(context_b)\n\nasyncio.run(ab_testing())\n```\n\n### 4. 并行网页抓取\n\n使用不同配置抓取多个网站：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def parallel_scraping():\n    websites = [\n        {'url': 'https://news.ycombinator.com', 'selector': '.storylink'},\n        {'url': 'https://reddit.com/r/python', 'selector': '.title'},\n        {'url': 'https://github.com/trending', 'selector': '.h3'},\n    ]\n    \n    async with Chrome() as browser:\n        # 启动浏览器并获取初始标签页\n        initial_tab = await browser.start()\n        \n        # 为其余网站创建上下文（第一个使用默认上下文）\n        contexts = [None] + [await browser.create_browser_context() for _ in websites[1:]]\n        \n        # 创建标签页（为第一个网站重用初始标签页）\n        tabs = [initial_tab] + [\n            await browser.new_tab(browser_context_id=ctx) for ctx in contexts[1:]\n        ]\n        \n        async def scrape_site(tab, site, context_id):\n            \"\"\"在给定的标签页和上下文内抓取单个网站\"\"\"\n            try:\n                await tab.go_to(site['url'])\n                await asyncio.sleep(3)\n                \n                # 使用CSS选择器提取标题\n                elements = await tab.query(site['selector'], find_all=True)\n                titles = [await elem.text for elem in elements[:5]]\n                \n                return {'url': site['url'], 'titles': titles}\n            finally:\n                # 清理上下文（跳过初始标签页的默认上下文）\n                if context_id is not None:\n                    await tab.close()\n                    await browser.delete_browser_context(context_id)\n        \n        # 并发抓取所有网站\n        results = await asyncio.gather(*[\n            scrape_site(tab, site, ctx) for tab, site, ctx in zip(tabs, websites, contexts)\n        ])\n        \n        # 显示结果\n        for result in results:\n            print(f\"\\n{result['url']}:\")\n            for i, title in enumerate(result['titles'], 1):\n                print(f\"  {i}. {title}\")\n\nasyncio.run(parallel_scraping())\n```\n\n## 理解上下文性能\n\n### 上下文是轻量级的\n\n!!! info \"性能特征\"\n    创建浏览器上下文**显著快于且更轻量**于启动新的浏览器进程：\n    \n    - **上下文创建**：约50-100毫秒，内存开销最小\n    - **新浏览器进程**：约2-5秒，基础内存50-150 MB\n    \n    对于10个隔离环境：\n\n    - **1个浏览器中的10个上下文**：约500毫秒启动，总计约500 MB\n    - **10个独立浏览器**：约30秒启动，总计约1-1.5 GB\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def benchmark_contexts_vs_browsers():\n    # Benchmark contexts\n    start = time.time()\n    async with Chrome() as browser:\n        # 启动浏览器（此示例中未使用初始标签页）\n        await browser.start()\n        \n        contexts = []\n        for i in range(10):\n            context_id = await browser.create_browser_context()\n            contexts.append(context_id)\n        \n        print(f\"创建10个上下文耗时：{time.time() - start:.2f}秒\")\n        \n        # 清理\n        for context_id in contexts:\n            await browser.delete_browser_context(context_id)\n\nasyncio.run(benchmark_contexts_vs_browsers())\n```\n\n### 无头模式 vs 有头模式：窗口行为\n\n!!! warning \"重要：有头模式中的上下文窗口\"\n    在**有头模式**（可见的浏览器UI）下运行时，有一个重要的行为需要理解：\n    \n    **在新上下文中创建的第一个标签页将打开一个新的操作系统窗口。**\n    \n    - 这是因为上下文需要一个\"宿主窗口\"来渲染其第一个页面\n    - 该上下文中的后续标签页可以作为该窗口内的标签页打开\n    - 这是CDP/Chromium的限制，而非Pydoll的设计选择\n    \n    **在无头模式下**，这不重要——不会创建窗口，一切都在后台运行。\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def demonstrate_window_behavior():\n    # 有头模式 - 将看到窗口\n    options_headed = ChromiumOptions()\n    options_headed.headless = False\n    \n    async with Chrome(options=options_headed) as browser:\n        # 启动浏览器并创建初始标签页（在默认上下文中打开第一个窗口）\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        \n        # 创建新上下文 - 第一个标签页将打开一个新窗口\n        context = await browser.create_browser_context()\n        tab2 = await browser.new_tab('https://github.com', browser_context_id=context)\n        \n        # 同一上下文中的第二个标签页 - 在现有窗口中作为标签页打开\n        tab3 = await browser.new_tab('https://google.com', browser_context_id=context)\n        \n        await asyncio.sleep(10)  # 观察窗口\n        \n        await tab2.close()\n        await tab3.close()\n        await browser.delete_browser_context(context)\n\n# 无头模式 - 无窗口，上下文不可见但仍然隔离\nasync def headless_contexts():\n    options = ChromiumOptions()\n    options.headless = True  # 无可见窗口\n    \n    async with Chrome(options=options) as browser:\n        # 在默认上下文中启动浏览器并创建初始标签页\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com/page0')\n        \n        # 再创建4个上下文 - 未打开窗口，全部在后台\n        contexts = []\n        for i in range(1, 5):\n            context_id = await browser.create_browser_context()\n            tab = await browser.new_tab(f'https://example.com/page{i}', browser_context_id=context_id)\n            contexts.append((context_id, tab))\n        \n        print(f\"创建了{len(contexts) + 1}个隔离上下文（1个默认 + {len(contexts)}个自定义，不可见）\")\n        \n        # 清理\n        for context_id, tab in contexts:\n            await tab.close()\n            await browser.delete_browser_context(context_id)\n\nasyncio.run(headless_contexts())\n```\n\n!!! tip \"最佳实践：对上下文使用无头模式\"\n    为了在多个上下文中实现最大效率：\n    \n    - **开发/调试**：使用有头模式查看发生的情况\n    - **生产/CI/CD**：使用无头模式以获得更快、更轻量的执行\n    - **多个上下文**：强烈建议使用无头模式以避免窗口管理的复杂性\n\n## 上下文管理\n\n### 创建上下文\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def create_context_example():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # 创建基本上下文\n        context_id = await browser.create_browser_context()\n        print(f\"已创建上下文：{context_id}\")\n        \n        # 创建带代理的上下文\n        proxied_context = await browser.create_browser_context(\n            proxy_server='http://proxy.example.com:8080',\n            proxy_bypass_list='localhost,127.0.0.1'\n        )\n        print(f\"已创建代理上下文：{proxied_context}\")\n        \n        # 创建带认证代理的上下文\n        auth_context = await browser.create_browser_context(\n            proxy_server='http://user:pass@proxy.example.com:8080'\n        )\n        print(f\"已创建认证上下文：{auth_context}\")\n\nasyncio.run(create_context_example())\n```\n\n### 列出上下文\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def list_contexts():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # 获取所有上下文（包括默认上下文）\n        contexts = await browser.get_browser_contexts()\n        print(f\"初始上下文：{len(contexts)}\")  # 通常为1（默认）\n        \n        # 创建额外的上下文\n        context1 = await browser.create_browser_context()\n        context2 = await browser.create_browser_context()\n        \n        # 再次列出\n        contexts = await browser.get_browser_contexts()\n        print(f\"创建2个新上下文后：{len(contexts)}\")  # 总计3个\n        \n        for i, context_id in enumerate(contexts):\n            print(f\"  上下文 {i+1}：{context_id}\")\n        \n        # 清理\n        await browser.delete_browser_context(context1)\n        await browser.delete_browser_context(context2)\n\nasyncio.run(list_contexts())\n```\n\n### 删除上下文\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def delete_context_example():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # 创建带标签页的上下文\n        context_id = await browser.create_browser_context()\n        tab1 = await browser.new_tab('https://example.com', browser_context_id=context_id)\n        tab2 = await browser.new_tab('https://github.com', browser_context_id=context_id)\n        \n        print(f\"已创建上下文 {context_id}，包含2个标签页\")\n        \n        # 删除上下文会自动关闭其所有标签页\n        await browser.delete_browser_context(context_id)\n        print(\"上下文已删除（所有标签页自动关闭）\")\n\nasyncio.run(delete_context_example())\n```\n\n!!! warning \"删除上下文会关闭所有标签页\"\n    删除浏览器上下文时，**属于该上下文的所有标签页会自动关闭**。这是一次清理多个标签页的高效方法，但请确保您已保存了任何重要数据。\n\n## 默认上下文\n\n每个浏览器都以包含初始标签页的**默认上下文**开始：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def default_context_example():\n    async with Chrome() as browser:\n        # 初始标签页位于默认上下文中\n        initial_tab = await browser.start()\n        \n        # 创建标签页而不指定上下文 - 使用默认上下文\n        default_tab = await browser.new_tab('https://example.com')\n        \n        # 创建自定义上下文\n        custom_context = await browser.create_browser_context()\n        custom_tab = await browser.new_tab('https://github.com', browser_context_id=custom_context)\n        \n        # 默认上下文和自定义上下文是隔离的\n        await default_tab.execute_script(\"localStorage.setItem('type', 'default')\")\n        await custom_tab.execute_script(\"localStorage.setItem('type', 'custom')\")\n        \n        # 验证隔离\n        default_type = await default_tab.execute_script(\"return localStorage.getItem('type')\")\n        custom_type = await custom_tab.execute_script(\"return localStorage.getItem('type')\")\n        \n        print(f\"默认上下文：{default_type}\")  # 'default'\n        print(f\"自定义上下文：{custom_type}\")    # 'custom'\n        \n        # 清理自定义上下文\n        await browser.delete_browser_context(custom_context)\n\nasyncio.run(default_context_example())\n```\n\n!!! info \"您无法删除默认上下文\"\n    默认浏览器上下文是永久性的，无法删除。它在整个浏览器会话期间存在。只有使用`create_browser_context()`创建的自定义上下文可以被删除。\n\n## 高级模式\n\n### 用于可重用隔离的上下文池\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nclass ContextPool:\n    def __init__(self, browser, size=5):\n        self.browser = browser\n        self.size = size\n        self.contexts = []\n        self.in_use = set()\n    \n    async def initialize(self):\n        \"\"\"创建上下文池\"\"\"\n        for _ in range(self.size):\n            context_id = await self.browser.create_browser_context()\n            self.contexts.append(context_id)\n        print(f\"上下文池已初始化，包含 {self.size} 个上下文\")\n    \n    async def acquire(self):\n        \"\"\"从池中获取可用上下文\"\"\"\n        for context_id in self.contexts:\n            if context_id not in self.in_use:\n                self.in_use.add(context_id)\n                return context_id\n        raise Exception(\"池中没有可用的上下文\")\n    \n    def release(self, context_id):\n        \"\"\"将上下文返回到池\"\"\"\n        self.in_use.discard(context_id)\n    \n    async def cleanup(self):\n        \"\"\"删除池中的所有上下文\"\"\"\n        for context_id in self.contexts:\n            await self.browser.delete_browser_context(context_id)\n\nasync def use_context_pool():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # 创建池\n        pool = ContextPool(browser, size=3)\n        await pool.initialize()\n        \n        # 从池中使用上下文\n        async def scrape_with_pool(url):\n            context_id = await pool.acquire()\n            try:\n                tab = await browser.new_tab(url, browser_context_id=context_id)\n                await asyncio.sleep(2)\n                title = await tab.execute_script('return document.title')\n                await tab.close()\n                return title\n            finally:\n                pool.release(context_id)\n        \n        # 使用池抓取多个URL\n        urls = [f'https://example.com/page{i}' for i in range(10)]\n        results = await asyncio.gather(*[scrape_with_pool(url) for url in urls])\n        \n        for i, title in enumerate(results):\n            print(f\"{urls[i]}: {title}\")\n        \n        # 清理\n        await pool.cleanup()\n\nasyncio.run(use_context_pool())\n```\n\n### 每个上下文的配置管理器\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def context_config_manager():\n    async with Chrome() as browser:\n        await browser.start()\n        \n        # 为不同场景定义配置\n        configs = {\n            'us_user': {\n                'proxy': 'http://us-proxy.example.com:8080',\n                'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'\n            },\n            'eu_user': {\n                'proxy': 'http://eu-proxy.example.com:8080',\n                'user_agent': 'Mozilla/5.0 (X11; Linux x86_64)'\n            },\n            'mobile_user': {\n                'proxy': None,\n                'user_agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)'\n            }\n        }\n        \n        contexts = {}\n        \n        # 为每个配置创建上下文\n        for name, config in configs.items():\n            if config['proxy']:\n                context_id = await browser.create_browser_context(\n                    proxy_server=config['proxy']\n                )\n            else:\n                context_id = await browser.create_browser_context()\n            \n            # 创建标签页并设置用户代理\n            tab = await browser.new_tab(browser_context_id=context_id)\n            # 注意：用户代理将通过CDP或选项设置，此处简化\n            \n            contexts[name] = {'context_id': context_id, 'tab': tab}\n        \n        # 为不同场景使用不同上下文\n        for name, data in contexts.items():\n            tab = data['tab']\n            await tab.go_to('https://httpbin.org/headers')\n            await asyncio.sleep(2)\n            print(f\"\\n{name} 配置已激活\")\n        \n        # 清理\n        for data in contexts.values():\n            await data['tab'].close()\n            await browser.delete_browser_context(data['context_id'])\n\nasyncio.run(context_config_manager())\n```\n\n## 最佳实践\n\n1. **对多个上下文使用无头模式**以避免窗口管理的复杂性\n2. **使用完毕后始终删除上下文**以防止内存泄漏\n3. **将相关操作分组在同一上下文中**以获得更好的组织\n4. **优先使用上下文而非多个浏览器进程**以获得更好的性能\n5. **使用上下文池**用于需要许多短期隔离环境的场景\n6. **在删除上下文前关闭标签页**以获得更干净的清理（虽然不是严格要求）\n\n## 另请参阅\n\n- **[多标签管理](tabs.md)** - 管理上下文中的多个标签页\n- **[深入探讨：Browser域](../../deep-dive/architecture/browser-domain.md)** - 上下文的架构细节\n- **[网络：HTTP请求](../network/http-requests.md)** - 浏览器上下文请求继承上下文状态\n- **[核心概念](../core-concepts.md)** - 理解Pydoll的架构\n\n浏览器上下文是Pydoll创建复杂自动化工作流的最强大功能之一。通过理解它们的工作方式——特别是有头模式下的窗口行为及其轻量级特性——您可以构建高效、可扩展的自动化，轻松处理复杂的多环境场景。\n\n"
  },
  {
    "path": "docs/zh/features/browser-management/cookies-sessions.md",
    "content": "# Cookie 与会话管理\n\n有效管理 Cookie 和会话对于真实的浏览器自动化至关重要。网站使用 Cookie 来跟踪身份验证、偏好设置和用户行为，并期望浏览器能相应地表现。\n\n## 为什么 Cookie 对自动化很重要\n\nCookie 不仅仅是存储的数据：它们是浏览器活动的指纹：\n\n- **身份验证**：会话 Cookie 在请求之间维护登录状态\n- **跟踪防护**：反机器人系统分析 Cookie 模式\n- **真实行为**：没有 Cookie 的浏览器看起来很可疑\n- **会话持久性**：重用 Cookie 可以节省重复登录的时间\n\n!!! warning \"Cookie 悖论\"\n    - **太干净**：没有 Cookie 或历史记录的浏览器看起来像机器人\n    - **太陈旧**：使用相同的会话数周会触发安全警报\n    - **最佳点**：新鲜的 Cookie 配合偶尔的轮换和真实的活动模式\n\n## 快速入门\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def basic_cookie_management():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # 设置 Cookie（使用简单的字典）\n        cookies = [\n            {\n                'name': 'session_id',\n                'value': 'abc123xyz',\n                'domain': 'example.com',\n                'path': '/',\n                'secure': True,\n                'httpOnly': True\n            }\n        ]\n        await tab.set_cookies(cookies)\n        \n        # 获取所有 Cookie\n        all_cookies = await browser.get_cookies()\n        print(f\"总 Cookie 数: {len(all_cookies)}\")\n        \n        # 删除所有 Cookie\n        await tab.delete_all_cookies()\n\nasyncio.run(basic_cookie_management())\n```\n\n## 理解 Cookie 类型\n\n!!! info \"TypedDict：实践中使用常规字典\"\n    在本文档中，您会看到对 `CookieParam` 和 `Cookie` 的引用。这些是 **TypedDict** 类型，它们只是带有类型提示的常规 Python 字典，用于 IDE 自动完成和类型检查。\n    \n    **实际上，您使用常规字典：**\n    ```python\n    # 这是您实际编写的：\n    cookie = {'name': 'session', 'value': 'abc123', 'domain': 'example.com'}\n    \n    # 类型注释只是为了您的 IDE：\n    from pydoll.protocol.network.types import CookieParam\n    cookie: CookieParam = {'name': 'session', 'value': 'abc123'}\n    ```\n    \n    下面的所有示例为简单起见都使用普通字典。\n\n### Cookie 结构\n\n`Cookie` 类型（从浏览器检索）包含完整的 Cookie 信息：\n\n```python\n{\n    \"name\": str,           # Cookie 名称\n    \"value\": str,          # Cookie 值\n    \"domain\": str,         # Cookie 有效的域\n    \"path\": str,           # Cookie 有效的路径\n    \"expires\": float,      # Unix 时间戳（0 = 会话 Cookie）\n    \"size\": int,           # 大小（字节）\n    \"httpOnly\": bool,      # 仅通过 HTTP 访问（不是 JavaScript）\n    \"secure\": bool,        # 仅通过 HTTPS 发送\n    \"session\": bool,       # 如果浏览器关闭时过期则为 True\n    \"sameSite\": str,       # \"Strict\"、\"Lax\" 或 \"None\"\n    \"priority\": str,       # \"Low\"、\"Medium\" 或 \"High\"\n    \"sourceScheme\": str,   # \"Unset\"、\"NonSecure\" 或 \"Secure\"\n    \"sourcePort\": int,     # 设置 Cookie 的端口\n}\n```\n\n### CookieParam 结构\n\n当**设置** Cookie 时，使用字典（只有 `name` 和 `value` 是必需的）：\n\n```python\n# 仅包含必需字段的简单 Cookie\ncookie = {\n    'name': 'user_token',\n    'value': 'token_value'\n}\n\n# 包含所有可选字段的完整 Cookie\ncookie = {\n    'name': 'user_token',       # 必需\n    'value': 'token_value',     # 必需\n    'domain': 'example.com',    # 可选：默认为当前页面域\n    'path': '/',                # 可选：默认为 /\n    'secure': True,             # 可选：仅 HTTPS\n    'httpOnly': True,           # 可选：无 JS 访问\n    'sameSite': 'Lax',          # 可选：'Strict'、'Lax' 或 'None'\n    'expires': 1735689600,      # 可选：Unix 时间戳\n    'priority': 'High',         # 可选：'Low'、'Medium' 或 'High'\n}\n```\n\n!!! info \"可选字段默认行为\"\n    当您省略可选字段时：\n    \n    - `domain`：使用当前页面的域\n    - `path`：默认为 `/`\n    - `secure`：默认为 `False`\n    - `httpOnly`：默认为 `False`\n    - `sameSite`：浏览器的默认值（通常为 `Lax`）\n    - `expires`：会话 Cookie（浏览器关闭时删除）\n\n## Cookie 管理操作\n\n### 设置 Cookie\n\n#### 一次设置多个 Cookie\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def set_multiple_cookies():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        cookies = [\n            {\n                'name': 'session_id',\n                'value': 'xyz789',\n                'domain': 'example.com',\n                'secure': True,\n                'httpOnly': True,\n                'sameSite': 'Strict'\n            },\n            {\n                'name': 'preferences',\n                'value': 'dark_mode=true',\n                'domain': 'example.com',\n                'path': '/settings'\n            },\n            {\n                'name': 'analytics',\n                'value': 'tracking_id_12345',\n                'domain': 'example.com',\n                'expires': 1735689600  # 在特定日期过期\n            }\n        ]\n        \n        await tab.set_cookies(cookies)\n        print(f\"设置了 {len(cookies)} 个 Cookie\")\n\nasyncio.run(set_multiple_cookies())\n```\n\n#### 在特定上下文中设置 Cookie\n\n```python\n# 在特定浏览器上下文中设置 Cookie\ncontext_id = await browser.create_browser_context()\nawait browser.set_cookies(cookies, browser_context_id=context_id)\n```\n\n!!! tip \"标签页与浏览器方法设置 Cookie\"\n    - `tab.set_cookies(cookies)`：在标签页的浏览器上下文中设置 Cookie（便捷快捷方式）\n    - `browser.set_cookies(cookies, browser_context_id=...)`：使用显式上下文控制设置 Cookie\n    \n    两种方法都将 Cookie 添加到**整个上下文**，而不仅仅是当前页面。Cookie 将可用于该上下文中的所有标签页。\n\n### 检索 Cookie\n\n#### 获取所有 Cookie（上下文范围）\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def get_cookies_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://github.com')\n        \n        # 等待页面设置 Cookie\n        await asyncio.sleep(2)\n        \n        # 选项 1：通过标签页获取 Cookie（当前上下文的快捷方式）\n        cookies = await tab.get_cookies()\n        \n        # 选项 2：通过浏览器获取 Cookie（显式上下文控制）\n        # cookies = await browser.get_cookies()  # 对于默认上下文与 tab.get_cookies() 相同\n        \n        print(f\"找到 {len(cookies)} 个 Cookie：\")\n        for cookie in cookies:\n            print(f\"  - {cookie['name']}: {cookie['value'][:20]}...\")\n            print(f\"    域: {cookie['domain']}, 安全: {cookie['secure']}\")\n\nasyncio.run(get_cookies_example())\n```\n\n!!! tip \"标签页与浏览器方法\"\n    - `tab.get_cookies()`：从标签页的浏览器上下文返回 Cookie（便捷快捷方式）\n    - `browser.get_cookies()`：从默认上下文返回 Cookie（或指定 `browser_context_id`）\n    \n    两种方法都返回上下文中的**所有 Cookie**，而不仅仅是当前页面域的 Cookie。\n\n!!! warning \"隐身模式限制\"\n    `browser.get_cookies()` 在原生隐身模式（`--incognito` 标志）下**不起作用**。这是 Chrome DevTools Protocol 的限制，`Storage.getCookies` 无法在原生隐身模式下访问 Cookie。\n    \n    **解决方法：** 改用 `tab.get_cookies()`，它使用 `Network.getCookies` 并在隐身模式下正常工作。\n\n#### 从特定上下文获取 Cookie\n\n```python\n# 从特定浏览器上下文获取 Cookie\ncontext_id = await browser.create_browser_context()\ncookies = await browser.get_cookies(browser_context_id=context_id)\n```\n\n### 删除 Cookie\n\n#### 删除所有 Cookie\n\n```python\n# 从当前标签页的上下文删除所有 Cookie\nawait tab.delete_all_cookies()\n\n# 从特定上下文删除所有 Cookie\nawait browser.delete_all_cookies(browser_context_id=context_id)\n```\n\n!!! warning \"Cookie 立即删除\"\n    当您删除 Cookie 时，它们会立即从浏览器中移除。网站可能直到下一次请求或页面重新加载才会检测到这一点。\n\n## 实际用例\n\n### 1. 持久登录会话\n\n跨脚本运行重用身份验证 Cookie：\n\n```python\nimport asyncio\nimport json\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nCOOKIE_FILE = Path('cookies.json')\n\nasync def save_cookies_after_login():\n    \"\"\"登录并保存 Cookie 供将来使用。\"\"\"\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/login')\n        \n        # 执行登录（简化）\n        email = await tab.find(id='email')\n        password = await tab.find(id='password')\n        await email.type_text('user@example.com')\n        await password.type_text('secret')\n        \n        login_btn = await tab.find(id='login')\n        await login_btn.click()\n        await asyncio.sleep(3)\n        \n        # 保存 Cookie\n        cookies = await browser.get_cookies()\n        COOKIE_FILE.write_text(json.dumps(cookies, indent=2))\n        print(f\"已将 {len(cookies)} 个 Cookie 保存到 {COOKIE_FILE}\")\n\nasync def reuse_saved_cookies():\n    \"\"\"加载保存的 Cookie 以跳过登录。\"\"\"\n    if not COOKIE_FILE.exists():\n        print(\"未找到保存的 Cookie。请先运行 save_cookies_after_login()。\")\n        return\n    \n    # 从文件加载 Cookie\n    saved_cookies = json.loads(COOKIE_FILE.read_text())\n    \n    # 转换为简化格式（仅必需字段）\n    # 注意：get_cookies() 返回详细的 Cookie 对象，带有只读字段\n    # （size、session、sourceScheme 等）。set_cookies() 期望 CookieParam\n    # 格式，仅包含可设置的字段。\n    cookies_to_set = [\n        {\n            'name': c['name'],\n            'value': c['value'],\n            'domain': c['domain'],\n            'path': c.get('path', '/'),\n            'secure': c.get('secure', False),\n            'httpOnly': c.get('httpOnly', False)\n        }\n        for c in saved_cookies\n    ]\n    \n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 在导航之前设置 Cookie\n        await tab.set_cookies(cookies_to_set)\n        print(f\"从文件加载了 {len(cookies_to_set)} 个 Cookie\")\n        \n        # 导航 - 应该已经登录\n        await tab.go_to('https://example.com/dashboard')\n        await asyncio.sleep(2)\n        \n        # 验证登录\n        try:\n            username = await tab.find(class_name='username')\n            print(f\"登录为: {await username.text}\")\n        except Exception:\n            print(\"登录失败 - Cookie 可能已过期\")\n\n# 首次运行：登录并保存 Cookie\n# asyncio.run(save_cookies_after_login())\n\n# 后续运行：重用 Cookie\nasyncio.run(reuse_saved_cookies())\n```\n\n!!! note \"需要重新格式化 Cookie\"\n    `get_cookies()` 返回**详细的 `Cookie` 对象**，带有只读属性如 `size`、`session`、`sourceScheme` 和 `sourcePort`。使用 `set_cookies()` 时，您必须提供 **`CookieParam` 格式**，仅包含可设置的字段（`name`、`value`、`domain`、`path`、`secure`、`httpOnly`、`sameSite`、`expires`、`priority`）。\n    \n    上面示例中的重新格式化步骤是**必不可少的**。将原始 `Cookie` 对象传递给 `set_cookies()` 可能会导致错误或意外行为。\n\n!!! tip \"Cookie 过期\"\n    始终检查保存的 Cookie 是否已过期。会话 Cookie（`session=True`）在浏览器关闭时过期，而持久性 Cookie 有一个您可以验证的 `expires` 时间戳。\n\n### 2. 使用隔离 Cookie 进行多账户测试\n\n每个浏览器上下文维护单独的 Cookie：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def test_multiple_accounts():\n    accounts = [\n        {'email': 'user1@example.com', 'cookie_value': 'session_user1'},\n        {'email': 'user2@example.com', 'cookie_value': 'session_user2'},\n    ]\n    \n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # 默认上下文中的第一个账户\n        cookies_user1 = [{\n            'name': 'session',\n            'value': accounts[0]['cookie_value'],\n            'domain': 'example.com',\n            'secure': True,\n            'httpOnly': True\n        }]\n        await initial_tab.set_cookies(cookies_user1)\n        await initial_tab.go_to('https://example.com/dashboard')\n        \n        # 隔离上下文中的第二个账户\n        context2 = await browser.create_browser_context()\n        tab2 = await browser.new_tab(browser_context_id=context2)\n        \n        cookies_user2 = [{\n            'name': 'session',\n            'value': accounts[1]['cookie_value'],\n            'domain': 'example.com',\n            'secure': True,\n            'httpOnly': True\n        }]\n        await browser.set_cookies(cookies_user2, browser_context_id=context2)\n        await tab2.go_to('https://example.com/dashboard')\n        \n        # 两个用户使用不同的会话同时登录\n        print(\"用户 1 和用户 2 使用隔离的 Cookie 登录\")\n        \n        await asyncio.sleep(5)\n        \n        # 清理\n        await tab2.close()\n        await browser.delete_browser_context(context2)\n\nasyncio.run(test_multiple_accounts())\n```\n\n### 3. 长时间运行脚本的 Cookie 轮换\n\n定期刷新 Cookie 以避免检测：\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_with_cookie_rotation():\n    urls = [f'https://example.com/page{i}' for i in range(100)]\n    \n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 最初登录\n        await tab.go_to('https://example.com/login')\n        # ... 执行登录 ...\n        await asyncio.sleep(2)\n        \n        last_rotation = time.time()\n        rotation_interval = 600  # 每 10 分钟轮换一次\n        \n        for url in urls:\n            # 检查是否是轮换 Cookie 的时候\n            if time.time() - last_rotation > rotation_interval:\n                print(\"轮换会话...\")\n                \n                # 删除旧 Cookie\n                await tab.delete_all_cookies()\n                \n                # 重新登录或加载新 Cookie\n                await tab.go_to('https://example.com/login')\n                # ... 再次执行登录 ...\n                \n                last_rotation = time.time()\n            \n            # 抓取页面\n            await tab.go_to(url)\n            await asyncio.sleep(2)\n            # ... 提取数据 ...\n\nasyncio.run(scrape_with_cookie_rotation())\n```\n\n!!! tip \"轮换频率\"\n    理想的轮换频率取决于您的用例：\n    \n    - **高安全性网站**：每 5-15 分钟轮换一次\n    - **普通网站**：每 30-60 分钟轮换一次\n    - **低风险抓取**：每几小时轮换一次\n\n\n## Cookie 属性参考\n\n| 属性 | 类型 | 描述 | 默认值 |\n|-----------|------|-------------|---------|\n| `name` | `str` | Cookie 名称 | *必需* |\n| `value` | `str` | Cookie 值 | *必需* |\n| `domain` | `str` | Cookie 有效的域 | 当前页面域 |\n| `path` | `str` | Cookie 有效的路径 | `/` |\n| `secure` | `bool` | 仅通过 HTTPS 发送 | `False` |\n| `httpOnly` | `bool` | 无法通过 JavaScript 访问 | `False` |\n| `sameSite` | `CookieSameSite` | CSRF 保护：`Strict`、`Lax`、`None` | 浏览器默认（`Lax`）|\n| `expires` | `float` | Unix 时间戳（0 = 会话 Cookie）| `0`（会话）|\n| `priority` | `CookiePriority` | Cookie 优先级：`Low`、`Medium`、`High` | `Medium` |\n\n### SameSite 值\n\n```python\n# 在您的 Cookie 字典中直接使用字符串值：\n\n'sameSite': 'Strict'  # 仅为同站点请求发送 Cookie\n'sameSite': 'Lax'     # 为顶级导航发送 Cookie（默认）\n'sameSite': 'None'    # 为所有请求发送 Cookie（需要 secure=True）\n\n# 或使用枚举获得 IDE 自动完成：\nfrom pydoll.protocol.network.types import CookieSameSite\n\ncookie = {\n    'name': 'session',\n    'value': 'xyz',\n    'sameSite': CookieSameSite.STRICT  # IDE 将自动完成：STRICT、LAX、NONE\n}\n```\n\n### Priority 值\n\n```python\n# 直接使用字符串值：\n\n'priority': 'Low'     # 低优先级（需要空间时首先删除）\n'priority': 'Medium'  # 中优先级（默认）\n'priority': 'High'    # 高优先级（最后删除）\n\n# 或使用枚举：\nfrom pydoll.protocol.network.types import CookiePriority\n\ncookie = {\n    'name': 'session',\n    'value': 'xyz',\n    'priority': CookiePriority.HIGH  # IDE 将自动完成：LOW、MEDIUM、HIGH\n}\n```\n\n## 常见模式\n\n### 临时 Cookie 的上下文管理器\n\n```python\nfrom contextlib import asynccontextmanager\n\n@asynccontextmanager\nasync def temporary_cookies(browser, tab, cookies):\n    \"\"\"设置 Cookie，执行代码，然后恢复原始 Cookie。\"\"\"\n    # 保存当前 Cookie\n    original_cookies = await browser.get_cookies()\n    \n    try:\n        # 设置临时 Cookie\n        await tab.delete_all_cookies()\n        await tab.set_cookies(cookies)\n        yield tab\n    finally:\n        # 恢复原始 Cookie\n        await tab.delete_all_cookies()\n        cookies_to_restore = [\n            {\n                'name': c['name'],\n                'value': c['value'],\n                'domain': c['domain'],\n                'path': c.get('path', '/')\n            }\n            for c in original_cookies\n        ]\n        await tab.set_cookies(cookies_to_restore)\n\n# 使用\nasync with temporary_cookies(browser, tab, test_cookies):\n    await tab.go_to('https://example.com')\n    # ... 使用临时 Cookie 执行操作 ...\n# 原始 Cookie 自动恢复\n```\n\n!!! tip \"使用公共 API\"\n    此上下文管理器接受 `browser` 和 `tab` 作为参数以使用公共 API。由于 `tab` 不将其父 `browser` 作为公共属性公开，因此显式传递它是访问浏览器级方法的推荐方法。\n\n### Cookie 指纹比较\n\n```python\ndef cookie_fingerprint(cookies):\n    \"\"\"生成 Cookie 状态的简单指纹。\"\"\"\n    return {\n        'count': len(cookies),\n        'domains': set(c['domain'] for c in cookies),\n        'names': sorted(c['name'] for c in cookies),\n        'secure_count': sum(1 for c in cookies if c.get('secure')),\n        'httponly_count': sum(1 for c in cookies if c.get('httpOnly')),\n    }\n\n# 比较 Cookie 状态\nbefore = await browser.get_cookies()\nawait tab.go_to('https://example.com')\nafter = await browser.get_cookies()\n\nprint(f\"之前: {cookie_fingerprint(before)}\")\nprint(f\"之后: {cookie_fingerprint(after)}\")\n```\n\n## 安全注意事项\n\n!!! danger \"切勿硬编码敏感 Cookie\"\n    始终从安全存储（环境变量、加密文件、密钥管理器）加载身份验证 Cookie。\n    \n    ```python\n    # ❌ 不好 - 在代码中硬编码\n    cookies = [{'name': 'session', 'value': 'abc123secret'}]\n    \n    # ✅ 好 - 从环境加载\n    import os\n    cookies = [{\n        'name': 'session',\n        'value': os.getenv('SESSION_COOKIE'),\n        'domain': os.getenv('COOKIE_DOMAIN')\n    }]\n    ```\n\n!!! warning \"Cookie 盗窃保护\"\n    将 Cookie 保存到磁盘时：\n    \n    - 使用加密存储（例如，`cryptography` 库）\n    - 设置限制性文件权限\n    - 切勿将 Cookie 文件提交到版本控制\n    - 定期轮换 Cookie\n\n## 最佳实践总结\n\n1. **从真实 Cookie 开始** - 不要在完全干净的浏览器中运行自动化\n2. **定期轮换会话** - 避免长时间使用相同的 Cookie\n3. **尊重 Cookie 安全属性** - 适当使用 `secure`、`httpOnly`、`sameSite`\n4. **保存和重用身份验证 Cookie** - 适当时跳过重复登录\n5. **隔离多账户测试的上下文** - 每个上下文都有独立的 Cookie\n6. **监控 Cookie 演变** - 真实浏览自然会积累 Cookie\n7. **清理过期的 Cookie** - 重用前删除无效 Cookie\n8. **使用安全存储** - 加密保存的 Cookie，切勿硬编码密钥\n\n## 另请参阅\n\n- **[浏览器上下文](contexts.md)** - 隔离的 Cookie 环境\n- **[HTTP 请求](../network/http-requests.md)** - 浏览器上下文请求自动继承 Cookie\n- **[类人交互](../automation/human-interactions.md)** - 将 Cookie 与真实行为结合\n- **[API 参考：存储命令](/api/commands/storage_commands/)** - 完整的 CDP Cookie 方法\n\n有效的 Cookie 管理是真实浏览器自动化的基础。通过平衡新鲜度与持久性并尊重安全属性，您可以构建表现得像真实用户一样的自动化，同时保持高效和可维护性。\n"
  },
  {
    "path": "docs/zh/features/browser-management/tabs.md",
    "content": "# 多标签页管理\n\nPydoll 提供了强大的多标签页功能，可以实现跨多个浏览器标签页同时进行复杂的自动化工作流。理解 Pydoll 中标签页的工作原理对于构建健壮、可扩展的自动化至关重要。\n\n## 理解 Pydoll 中的标签页\n\n在 Pydoll 中，`Tab` 实例代表单个浏览器标签页（或窗口），并提供所有页面自动化操作的主要接口。每个标签页维护自己的：\n\n- **独立执行上下文**：JavaScript、DOM 和页面状态\n- **隔离的事件处理器**：在一个标签页上注册的回调不会影响其他标签页\n- **独立的网络监控**：每个标签页可以跟踪自己的网络活动\n- **唯一的 CDP 连接**：与浏览器的直接 WebSocket 通信\n\n```mermaid\ngraph LR\n    Browser[浏览器实例] --> Tab1[标签页 1]\n    Browser --> Tab2[标签页 2]\n    Browser --> Tab3[...]\n    \n    Tab1 --> Features1[独立<br/>上下文]\n    Tab2 --> Features2[独立<br/>上下文]\n```\n\n| 标签页组件 | 描述 | 独立性 |\n|-----------|------|--------|\n| **执行上下文** | JavaScript 运行时、DOM、页面状态 | ✓ 每个标签页都有自己的 |\n| **事件处理器** | CDP 事件的注册回调 | ✓ 每个标签页隔离 |\n| **网络监控** | HTTP 请求、响应、时序 | ✓ 单独跟踪 |\n| **CDP 连接** | WebSocket 通信通道 | ✓ 直接连接 |\n\n### 什么是浏览器标签页？\n\n浏览器标签页在技术上是一个 **CDP 目标** - 一个具有自己的隔离浏览上下文：\n\n- 文档对象模型（DOM）\n- JavaScript 执行环境\n- 网络连接池\n- Cookie 存储（与同一上下文中的其他标签页共享）\n- 事件循环和渲染引擎\n\n每个标签页都有浏览器分配的唯一 `target_id`，Pydoll 使用它来正确路由命令和事件。\n\n## 标签页实例管理\n\nPydoll 的 `Browser` 类根据每个标签页的 `target_id` 维护一个 Tab 实例注册表。这确保对同一浏览器标签页的多个引用始终返回相同的 Tab 对象。Browser 将这些实例存储在内部的 `_tabs_opened` 字典中。\n\n| 优势 | 描述 |\n|------|------|\n| **资源效率** | 每个浏览器标签页一个 Tab 实例，无重复 |\n| **状态一致** | 所有引用共享相同的事件处理器和状态 |\n| **内存安全** | 防止与同一目标建立多个 WebSocket 连接 |\n| **可预测的行为** | 一个引用的变化会影响所有引用 |\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def tab_registry_demonstration():\n    async with Chrome() as browser:\n        # 启动浏览器并获取初始标签页\n        tab1 = await browser.start()\n\n        # 通过不同方法获取同一标签页\n        # 注意：get_opened_tabs() 以相反顺序返回标签页（最新的在前）\n        # 所以初始标签页（最旧的）在最后\n        opened_tabs = await browser.get_opened_tabs()\n        tab2 = opened_tabs[-1]  # 初始标签页是最旧的，所以在最后\n\n        # 两个引用指向同一对象\n        # 因为 Browser 从其注册表返回相同的实例\n        print(f\"Same instance? {tab1 is tab2}\")  # True\n        print(f\"Same target ID? {tab1._target_id == tab2._target_id}\")  # True\n\n        # 在一个引用上注册事件会影响另一个\n        await tab1.enable_network_events()\n        print(f\"Network events on tab2? {tab2.network_events_enabled}\")  # True\n\n        # Browser 在内部维护注册表\n        print(f\"Tab registered in browser? {tab1._target_id in browser._tabs_opened}\")  # True\n\nasyncio.run(tab_registry_demonstration())\n```\n\n!!! info \"Browser 管理的注册表\"\n    Browser 类管理一个以 `target_id` 为键的 `_tabs_opened` 字典。当你请求一个标签页（通过 `new_tab()` 或 `get_opened_tabs()`）时，Browser 会先检查该注册表。如果已经存在对应的 Tab，就复用该实例；否则创建新的并缓存。（IFrame 现在作为普通元素处理，不再注册为独立的 Tab。）\n\n## 创建和管理标签页\n\n### 启动浏览器\n\n启动浏览器时，Pydoll 自动为初始浏览器标签页创建并返回一个 Tab 实例：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def start_browser():\n    async with Chrome() as browser:\n        # 自动创建初始标签页\n        tab = await browser.start()\n        \n        print(f\"Tab created with target ID: {tab._target_id}\")\n        await tab.go_to('https://example.com')\n        \n        title = await tab.execute_script('return document.title')\n        print(f\"Page title: {title}\")\n\nasyncio.run(start_browser())\n```\n\n### 以编程方式创建额外的标签页\n\n使用 `browser.new_tab()` 完全控制创建额外的标签页：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def create_multiple_tabs():\n    async with Chrome() as browser:\n        # 从初始标签页开始\n        main_tab = await browser.start()\n        \n        # 创建带有特定 URL 的额外标签页\n        search_tab = await browser.new_tab('https://google.com')\n        docs_tab = await browser.new_tab('https://docs.python.org')\n        news_tab = await browser.new_tab('https://news.ycombinator.com')\n        \n        # 每个标签页可以独立控制\n        await search_tab.find(name='q')  # Google 搜索框\n        await docs_tab.find(id='search-field')  # Python 文档搜索\n        await news_tab.find(class_name='storylink', find_all=True)  # HN 故事\n        \n        # 获取所有打开的标签页\n        all_tabs = await browser.get_opened_tabs()\n        print(f\"Total tabs: {len(all_tabs)}\")  # 4（初始 + 3 个新的）\n        \n        # 完成后关闭特定标签页\n        await search_tab.close()\n        await docs_tab.close()\n        await news_tab.close()\n\nasyncio.run(create_multiple_tabs())\n```\n\n!!! tip \"URL 参数可选\"\n    你可以创建不指定 URL 的标签页：`await browser.new_tab()`。标签页将打开空白页面（`about:blank`），准备导航。\n\n### 处理用户打开的标签页\n\n当用户点击带有 `target=\"_blank\"` 的链接或使用\"在新标签页中打开\"时，Pydoll 可以检测并管理这些标签页：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def handle_user_tabs():\n    async with Chrome() as browser:\n        main_tab = await browser.start()\n        await main_tab.go_to('https://example.com')\n        \n        # 记录初始标签页数量\n        initial_tabs = await browser.get_opened_tabs()\n        print(f\"Initial tabs: {len(initial_tabs)}\")\n        \n        # 点击在新标签页中打开的链接（target=\"_blank\"）\n        external_link = await main_tab.find(text='Open in New Tab')\n        await external_link.click()\n        \n        # 等待新标签页打开\n        await asyncio.sleep(2)\n        \n        # 检测新标签页\n        current_tabs = await browser.get_opened_tabs()\n        print(f\"Current tabs: {len(current_tabs)}\")\n        \n        # 找到新打开的标签页（列表中的最后一个）\n        if len(current_tabs) > len(initial_tabs):\n            new_tab = current_tabs[-1]\n            \n            # 使用新标签页\n            url = await new_tab.current_url\n            print(f\"New tab URL: {url}\")\n            \n            await new_tab.go_to('https://different-site.com')\n            title = await new_tab.execute_script('return document.title')\n            print(f\"New tab title: {title}\")\n            \n            # 完成后关闭\n            await new_tab.close()\n\nasyncio.run(handle_user_tabs())\n```\n\n### 列出所有打开的标签页\n\n使用 `browser.get_opened_tabs()` 检索所有当前打开的标签页：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def list_tabs():\n    async with Chrome() as browser:\n        # 使用 start() 返回的初始标签页\n        initial_tab = await browser.start()\n        await initial_tab.go_to('https://example.com')\n        \n        # 再打开几个标签页\n        await browser.new_tab('https://github.com')\n        await browser.new_tab('https://stackoverflow.com')\n        await browser.new_tab('https://reddit.com')\n        \n        # 获取所有标签页\n        all_tabs = await browser.get_opened_tabs()\n        \n        # 检查每个标签页\n        for i, tab in enumerate(all_tabs, 1):\n            url = await tab.current_url\n            title = await tab.execute_script('return document.title')\n            print(f\"Tab {i}: {title} - {url}\")\n\nasyncio.run(list_tabs())\n```\n\n## 并发标签页操作\n\nPydoll 的异步架构支持跨多个标签页的强大并发工作流：\n\n### 并行数据收集\n\n同时处理多个页面以获得最大效率：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_page(tab, url):\n    \"\"\"在给定标签页中抓取单个页面。\"\"\"\n    await tab.go_to(url)\n    title = await tab.execute_script('return document.title')\n    articles = await tab.find(class_name='article', find_all=True)\n    content = [await article.text for article in articles[:5]]\n\n    return {\n        'url': url,\n        'title': title,\n        'articles_count': len(articles),\n        'sample_content': content\n    }\n\nasync def concurrent_scraping():\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n        'https://example.com/page4',\n    ]\n\n    async with Chrome() as browser:\n        # 启动浏览器并打开第一个标签页\n        initial_tab = await browser.start()\n        # 为每个 URL 创建一个标签页\n        tabs = [initial_tab] + [await browser.new_tab() for _ in urls[1:]]\n\n        # 并发运行所有爬虫\n        results = await asyncio.gather(*[\n            scrape_page(tab, url) for tab, url in zip(tabs, urls)\n        ])\n\n        # 显示结果\n        for result in results:\n            print(f\"\\n{result['title']}\")\n            print(f\"  URL: {result['url']}\")\n            print(f\"  Articles: {result['articles_count']}\")\n            if result['sample_content']:\n                print(f\"  Sample: {result['sample_content'][0][:100]}...\")\n\nasyncio.run(concurrent_scraping())\n```\n\n!!! tip \"性能提升\"\n    与顺序处理相比，并发抓取可以将总执行时间减少 5-10 倍，特别是对于 I/O 密集型任务如页面加载。\n\n### 协调多标签页工作流\n\n编排需要多个标签页交互的复杂工作流：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\n\nasync def multi_tab_workflow():\n    async with Chrome() as browser:\n        # 使用初始标签页进行登录\n        login_tab = await browser.start()\n        await login_tab.go_to('https://app.example.com/login')\n        await asyncio.sleep(2)\n        \n        username = await login_tab.find(id='username')\n        password = await login_tab.find(id='password')\n        \n        await username.type_text('admin@example.com')\n        await password.type_text('secure_password')\n        \n        login_btn = await login_tab.find(id='login')\n        await login_btn.click()\n        await asyncio.sleep(3)\n        \n        # 标签页 2：导航到数据导出页面\n        export_tab = await browser.new_tab('https://app.example.com/export')\n        await asyncio.sleep(2)\n        \n        export_btn = await export_tab.find(text='Export Data')\n        await export_btn.click()\n        \n        # 标签页 3：在仪表板中监控 API 调用\n        monitor_tab = await browser.new_tab('https://app.example.com/dashboard')\n        await monitor_tab.enable_network_events()\n        \n        # 跟踪 API 调用\n        api_calls = []\n        async def track_api(event: RequestWillBeSentEvent):\n            url = event['params']['request']['url']\n            if '/api/' in url:\n                api_calls.append(url)\n        \n        await monitor_tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, track_api)\n        await asyncio.sleep(5)\n        \n        print(f\"Tracked {len(api_calls)} API calls:\")\n        for call in api_calls[:10]:\n            print(f\"  - {call}\")\n        \n        # 清理\n        await login_tab.close()\n        await export_tab.close()\n        await monitor_tab.close()\n\nasyncio.run(multi_tab_workflow())\n```\n\n## 标签页生命周期和清理\n\n### 显式标签页关闭\n\n完成后始终关闭标签页以释放浏览器资源：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def explicit_cleanup():\n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # 为不同任务创建标签页\n        tab1 = await browser.new_tab('https://example.com')\n        tab2 = await browser.new_tab('https://example.org')\n        \n        # 使用标签页进行工作\n        await tab1.go_to('https://different-site.com')\n        await tab2.take_screenshot('/tmp/screenshot.png')\n        \n        # 显式关闭标签页\n        await tab1.close()\n        await tab2.close()\n        \n        # 验证标签页已关闭\n        remaining = await browser.get_opened_tabs()\n        print(f\"Remaining tabs: {len(remaining)}\")  # 应该是 1（初始）\n\nasyncio.run(explicit_cleanup())\n```\n\n!!! warning \"内存泄漏\"\n    在长时间运行的自动化中不关闭标签页可能导致内存耗尽。每个标签页消耗浏览器资源（内存、文件句柄、网络连接）。\n\n### 使用上下文管理器自动清理\n\n虽然 Pydoll 没有提供内置的标签页上下文管理器，但你可以创建自己的：\n\n```python\nimport asyncio\nfrom contextlib import asynccontextmanager\nfrom pydoll.browser.chromium import Chrome\n\n@asynccontextmanager\nasync def managed_tab(browser, url=None):\n    \"\"\"用于自动清理标签页的上下文管理器。\"\"\"\n    tab = await browser.new_tab(url)\n    try:\n        yield tab\n    finally:\n        await tab.close()\n\nasync def auto_cleanup_example():\n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # 退出上下文时标签页自动关闭\n        async with managed_tab(browser, 'https://example.com') as tab:\n            title = await tab.execute_script('return document.title')\n            print(f\"Title: {title}\")\n            \n            await tab.take_screenshot('/tmp/page.png')\n        # 标签页在这里自动关闭\n        \n        tabs = await browser.get_opened_tabs()\n        print(f\"Tabs after context exit: {len(tabs)}\")  # 1（仅 initial_tab）\n\nasyncio.run(auto_cleanup_example())\n```\n\n### 浏览器清理\n\n浏览器关闭时，所有标签页会自动关闭：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def browser_cleanup():\n    # 使用上下文管理器 - 自动清理\n    async with Chrome() as browser:\n        initial_tab = await browser.start()\n        \n        # 创建多个标签页\n        await browser.new_tab('https://example.com')\n        await browser.new_tab('https://github.com')\n        await browser.new_tab('https://stackoverflow.com')\n        \n        tabs = await browser.get_opened_tabs()\n        print(f\"Tabs open: {len(tabs)}\")  # 4（初始 + 3 个新的）\n    \n    # 浏览器退出时所有标签页自动关闭\n    print(\"Browser closed, all tabs cleaned up\")\n\nasyncio.run(browser_cleanup())\n```\n\n## 标签页状态管理\n\n### 检查标签页状态\n\n查询标签页当前状态的各个方面：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def check_tab_state():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # 检查当前 URL\n        url = await tab.current_url\n        print(f\"Current URL: {url}\")\n        \n        # 检查页面源代码\n        source = await tab.page_source\n        print(f\"Page source length: {len(source)} characters\")\n        \n        # 检查已启用的事件域\n        print(f\"Page events enabled: {tab.page_events_enabled}\")\n        print(f\"Network events enabled: {tab.network_events_enabled}\")\n        print(f\"DOM events enabled: {tab.dom_events_enabled}\")\n        \n        # 启用事件并再次检查\n        await tab.enable_network_events()\n        print(f\"Network events enabled: {tab.network_events_enabled}\")  # True\n\nasyncio.run(check_tab_state())\n```\n\n### 标签页标识\n\n每个标签页都有唯一标识符：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def tab_identification():\n    async with Chrome() as browser:\n        tab1 = await browser.start()\n        tab2 = await browser.new_tab()\n        \n        # Target ID - 浏览器分配的唯一标识符\n        print(f\"Tab 1 target ID: {tab1._target_id}\")\n        print(f\"Tab 2 target ID: {tab2._target_id}\")\n        \n        # 连接详情\n        print(f\"Tab 1 connection port: {tab1._connection_port}\")\n        print(f\"Tab 2 connection port: {tab2._connection_port}\")\n        \n        # 浏览器上下文 ID（默认上下文通常为 None）\n        print(f\"Tab 1 context ID: {tab1._browser_context_id}\")\n        print(f\"Tab 2 context ID: {tab2._browser_context_id}\")\n\nasyncio.run(tab_identification())\n```\n\n## 高级标签页功能\n\n### 将标签页置于前台\n\n使特定标签页可见（置于前台）：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def bring_to_front():\n    async with Chrome() as browser:\n        tab1 = await browser.start()\n        tab2 = await browser.new_tab('https://github.com')\n        tab3 = await browser.new_tab('https://stackoverflow.com')\n        \n        # tab3 当前在前台（最后创建的）\n        await asyncio.sleep(2)\n        \n        # 将 tab1 置于前台\n        await tab1.bring_to_front()\n        print(\"Tab 1 brought to front\")\n        \n        await asyncio.sleep(2)\n        \n        # 将 tab2 置于前台\n        await tab2.bring_to_front()\n        print(\"Tab 2 brought to front\")\n\nasyncio.run(bring_to_front())\n```\n\n### 标签页特定的网络监控\n\n每个标签页可以独立监控自己的网络活动：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def tab_network_monitoring():\n    async with Chrome() as browser:\n        # 使用初始标签页进行监控导航\n        tab1 = await browser.start()\n        await tab1.go_to('https://example.com')\n        \n        # 创建第二个标签页不进行监控\n        tab2 = await browser.new_tab('https://github.com')\n        \n        # 仅在 tab1 上启用网络监控\n        await tab1.enable_network_events()\n        \n        # 导航两个标签页\n        await tab1.go_to('https://example.com/page1')\n        await tab2.go_to('https://github.com/explore')\n        \n        await asyncio.sleep(3)\n        \n        # 仅从 tab1 获取网络日志\n        tab1_logs = await tab1.get_network_logs()\n        print(f\"Tab 1 network requests: {len(tab1_logs)}\")\n        \n        # tab2 没有网络监控\n        print(f\"Tab 2 network events enabled: {tab2.network_events_enabled}\")  # False\n\nasyncio.run(tab_network_monitoring())\n```\n\n### 标签页特定的事件处理器\n\n在不同标签页上注册不同的事件处理器：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.page.events import PageEvent\n\nasync def tab_specific_events():\n    async with Chrome() as browser:\n        # 使用初始标签页作为第一个标签页\n        tab1 = await browser.start()\n        tab2 = await browser.new_tab()\n        \n        # 在两个标签页上启用页面事件\n        await tab1.enable_page_events()\n        await tab2.enable_page_events()\n        \n        # 为每个标签页使用不同的处理器\n        async def tab1_handler(event):\n            print(\"Tab 1 loaded!\")\n        \n        async def tab2_handler(event):\n            print(\"Tab 2 loaded!\")\n        \n        await tab1.on(PageEvent.LOAD_EVENT_FIRED, tab1_handler)\n        await tab2.on(PageEvent.LOAD_EVENT_FIRED, tab2_handler)\n        \n        # 导航两个标签页\n        await tab1.go_to('https://example.com')\n        await tab2.go_to('https://github.com')\n        \n        await asyncio.sleep(2)\n\nasyncio.run(tab_specific_events())\n```\n\n## 性能考虑\n\n| 场景 | 资源影响 | 建议 |\n|------|---------|------|\n| **1-5 个标签页** | 低 | 直接管理，无需特殊处理 |\n| **5-20 个标签页** | 中等 | 使用信号量限制并发 |\n| **20-50 个标签页** | 高 | 批处理，积极关闭标签页 |\n| **50+ 个标签页** | 非常高 | 考虑顺序处理或多个浏览器 |\n\n### 内存使用\n\n每个标签页大约消耗：\n\n- **基础内存**：50-100 MB\n- **启用网络事件**：+10-20 MB\n- **启用 DOM 事件**：+20-50 MB\n- **复杂页面（SPA）**：+100-300 MB\n\n20 个启用网络监控的标签页：约 1.5-3 GB 内存。\n\n## 常见模式\n\n### 使用单个标签页的顺序处理\n\n```python\nasync def sequential_pattern():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        for url in urls:\n            await tab.go_to(url)\n            # 提取数据\n            await tab.clear_callbacks()  # 清理事件\n\nasyncio.run(sequential_pattern())\n```\n\n### 使用多个标签页的并行处理\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def parallel_pattern():\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n        'https://example.com/page4',\n    ]\n\n    async with Chrome() as browser:\n        # 启动浏览器并获取初始标签页\n        initial_tab = await browser.start()\n        # 为每个 URL 创建一个标签页（为第一个重用初始标签页）\n        tabs = [initial_tab] + [await browser.new_tab() for _ in urls[1:]]\n\n        async def process_page(tab, url):\n            \"\"\"在给定标签页中处理单个页面。\"\"\"\n            try:\n                await tab.go_to(url)\n                await asyncio.sleep(2)\n                title = await tab.evaluate('document.title')\n                print(f\"[{url}] {title}\")\n            finally:\n                if tab is not initial_tab:\n                    await tab.close()\n\n        # 并发运行所有标签页\n        await asyncio.gather(*[\n            process_page(tab, url) for tab, url in zip(tabs, urls)\n        ])\n\nasyncio.run(parallel_pattern())\n```\n\n### 工作池模式\n\n```python\nasync def worker_pool_pattern():\n    async with Chrome() as browser:\n        # 使用初始标签页作为第一个工作者\n        initial_tab = await browser.start()\n        \n        # 创建额外的工作者标签页（总共 5 个工作者：1 个初始 + 4 个新的）\n        workers = [initial_tab] + [await browser.new_tab() for _ in range(4)]\n        \n        # 在所有工作者之间分配工作\n        for url in urls:\n            worker = workers[urls.index(url) % len(workers)]\n            await worker.go_to(url)\n            # 处理...\n        \n        # 清理所有工作者（包括初始标签页）\n        for worker in workers:\n            await worker.close()\n\nasyncio.run(worker_pool_pattern())\n```\n\n!!! tip \"重用初始标签页\"\n    始终使用 `browser.start()` 返回的标签页，而不是让它空闲。这可以节省浏览器资源并提高性能。在上面的示例中，初始标签页被重用作为第一个工作者或批处理中的第一个 URL。\n\n## 另请参阅\n\n- **[浏览器上下文](contexts.md)** - 隔离的浏览器会话\n- **[Cookie 和会话](cookies-sessions.md)** - 跨标签页管理 cookie\n- **[事件系统](../advanced/event-system.md)** - 标签页特定的事件处理\n- **[并发抓取](../../features.md#concurrent-scraping)** - 实际示例\n\nPydoll 中的多标签页管理为构建可扩展、高效的浏览器自动化提供了基础。通过理解标签页生命周期、单例模式和最佳实践，你可以创建健壮的自动化工作流，轻松处理复杂的多页面场景。\n"
  },
  {
    "path": "docs/zh/features/configuration/browser-options.md",
    "content": "# 浏览器选项 (ChromiumOptions)\n\n`ChromiumOptions` 是自定义浏览器行为的中央配置中心。它控制从命令行参数和二进制文件位置到页面加载状态和内容偏好的所有内容。\n\n!!! info \"相关文档\"\n    - **[浏览器偏好设置](browser-preferences.md)** - 深入了解 Chromium 的内部偏好系统\n    - **[浏览器管理](../browser-management/tabs.md)** - 使用浏览器实例和标签页\n    - **[上下文](../browser-management/contexts.md)** - 隔离的浏览上下文\n\n## 快速入门\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\nasync def main():\n    # 创建并配置选项\n    options = ChromiumOptions()\n    \n    # 基本配置\n    options.headless = True\n    options.start_timeout = 15\n    options.page_load_state = PageLoadState.INTERACTIVE\n    \n    # 添加命令行参数\n    options.add_argument('--disable-gpu')\n    options.add_argument('--window-size=1920,1080')\n    \n    # 常见设置的辅助方法\n    options.block_notifications = True\n    options.block_popups = True\n    options.set_default_download_directory('/tmp/downloads')\n    \n    # 使用配置的选项\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n\nasyncio.run(main())\n```\n\n## 核心属性\n\n### 命令行参数\n\nChromium 支持数百个控制浏览器行为的命令行开关。使用 `add_argument()` 直接将标志传递给浏览器进程。\n\n```python\noptions = ChromiumOptions()\n\n# 添加单个参数\noptions.add_argument('--disable-blink-features=AutomationControlled')\n\n# 添加带值的参数\noptions.add_argument('--window-size=1920,1080')\noptions.add_argument('--user-agent=Mozilla/5.0 ...')\n\n# 如需要可删除参数\noptions.remove_argument('--window-size=1920,1080')\n\n# 获取所有参数\nall_args = options.arguments\n```\n\n!!! tip \"参数格式\"\n    - 以 `--` 开头的参数是标志：`--headless`、`--disable-gpu`\n    - 带 `=` 的参数有值：`--window-size=1920,1080`\n    - 有些接受多个值：`--disable-features=Feature1,Feature2`\n\n**请参阅下面的[命令行参数参考](#命令行参数参考)获取完整列表。**\n\n### 二进制文件位置\n\n指定自定义浏览器可执行文件而不是使用系统默认值：\n\n```python\noptions = ChromiumOptions()\n\n# Linux\noptions.binary_location = '/opt/google/chrome-beta/chrome'\n\n# macOS\noptions.binary_location = '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'\n\n# Windows\noptions.binary_location = r'C:\\Program Files\\Google\\Chrome Beta\\Application\\chrome.exe'\n```\n\n!!! info \"何时设置二进制位置\"\n    - 测试不同的 Chrome 版本（Stable、Beta、Canary）\n    - 使用 Chromium 而不是 Chrome\n    - 使用便携式浏览器安装\n    - 运行特定构建进行调试\n\n### 启动超时\n\n控制 Pydoll 等待浏览器启动和响应的时间：\n\n```python\noptions = ChromiumOptions()\noptions.start_timeout = 20  # 秒（默认：10）\n```\n\n!!! warning \"超时注意事项\"\n    - **太低**：浏览器可能无法完全初始化，导致启动失败\n    - **太高**：挂起会阻塞您的自动化更长时间\n    - **推荐**：大多数情况下 10-15 秒，慢速系统或大型浏览器配置文件 20-30 秒\n\n### 无头模式\n\n在没有可见 UI 的情况下运行浏览器：\n\n```python\noptions = ChromiumOptions()\noptions.headless = True  # 自动添加 --headless 参数\n\n# 或手动\noptions.add_argument('--headless')\noptions.add_argument('--headless=new')  # 新的无头模式（Chrome 109+）\n```\n\n| 模式 | 参数 | 描述 |\n|------|----------|-------------|\n| **有头** | (无) | 可见的浏览器窗口（默认）|\n| **经典无头** | `--headless` | 旧版无头模式 |\n| **新无头** | `--headless=new` | 现代无头（Chrome 109+，更好的兼容性）|\n\n!!! tip \"新无头模式\"\n    `--headless=new` 模式（Chrome 109+）提供更好的现代 Web 功能兼容性，更难检测。在生产自动化中使用它。\n\n### 页面加载状态\n\n控制 `tab.go_to()` 何时认为页面\"已加载\"：\n\n```python\nfrom pydoll.constants import PageLoadState\n\noptions = ChromiumOptions()\noptions.page_load_state = PageLoadState.INTERACTIVE  # 或 PageLoadState.COMPLETE\n```\n\n| 状态 | 导航完成时 | 用例 |\n|-------|---------------------------|----------|\n| `COMPLETE`（默认）| 触发 `load` 事件，所有资源已加载 | 等待图像、字体、脚本 |\n| `INTERACTIVE` | 触发 `DOMContentLoaded`，DOM 就绪 | 更快的导航，立即与 DOM 交互 |\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\nasync def compare_load_states():\n    # 完整模式 - 等待所有内容\n    options_complete = ChromiumOptions()\n    options_complete.page_load_state = PageLoadState.COMPLETE\n    \n    async with Chrome(options=options_complete) as browser:\n        tab = await browser.start()\n        \n        import time\n        start = time.time()\n        await tab.go_to('https://example.com')\n        complete_time = time.time() - start\n        print(f\"COMPLETE 模式: {complete_time:.2f}s\")\n    \n    # 交互模式 - DOM 就绪就足够了\n    options_interactive = ChromiumOptions()\n    options_interactive.page_load_state = PageLoadState.INTERACTIVE\n    \n    async with Chrome(options=options_interactive) as browser:\n        tab = await browser.start()\n        \n        start = time.time()\n        await tab.go_to('https://example.com')\n        interactive_time = time.time() - start\n        print(f\"INTERACTIVE 模式: {interactive_time:.2f}s\")\n\nasyncio.run(compare_load_states())\n```\n\n!!! tip \"何时使用 INTERACTIVE\"\n    在以下情况使用 `INTERACTIVE`：\n    \n    - 只需要访问 DOM，不需要图像/字体\n    - 抓取文本内容和结构\n    - 速度至关重要\n    - 页面有许多加载缓慢的资源\n    \n    在以下情况坚持使用 `COMPLETE`（默认）：\n    \n    - 截图（需要加载图像）\n    - 等待 JavaScript 重型应用完全初始化\n    - 测试页面加载性能\n\n## 命令行参数参考\n\nChromium 支持数百个命令行开关。以下是自动化最有用的参数，按类别组织。\n\n!!! info \"完整参考\"\n    所有 Chromium 开关的完整列表：[Peter Beverloo 的 Chromium 命令行开关](https://peter.sh/experiments/chromium-command-line-switches/)\n\n### 性能和资源管理\n\n优化浏览器性能以加快自动化：\n\n```python\noptions = ChromiumOptions()\n\n# 禁用 GPU 加速（无头、Docker、CI/CD）\noptions.add_argument('--disable-gpu')\noptions.add_argument('--disable-software-rasterizer')\n\n# 减少内存使用\noptions.add_argument('--disable-dev-shm-usage')  # Docker：克服 /dev/shm 大小限制\noptions.add_argument('--disable-extensions')\noptions.add_argument('--disable-background-networking')\n\n# 禁用不必要的功能\noptions.add_argument('--disable-sync')  # Google 账户同步\noptions.add_argument('--disable-translate')\noptions.add_argument('--disable-background-timer-throttling')\noptions.add_argument('--disable-backgrounding-occluded-windows')\noptions.add_argument('--disable-renderer-backgrounding')\n\n# 网络优化\noptions.add_argument('--disable-features=NetworkPrediction')\noptions.add_argument('--dns-prefetch-disable')\n\n# 窗口和渲染\noptions.add_argument('--window-size=1920,1080')\noptions.add_argument('--window-position=0,0')\noptions.add_argument('--force-device-scale-factor=1')\n```\n\n| 参数 | 效果 | 何时使用 |\n|----------|--------|-------------|\n| `--disable-gpu` | 无 GPU 加速 | 无头、Docker、没有 GPU 的服务器 |\n| `--disable-dev-shm-usage` | 使用 `/tmp` 而不是 `/dev/shm` | 共享内存小的 Docker 容器 |\n| `--disable-extensions` | 不加载任何扩展 | 用于自动化的干净、快速的浏览器 |\n| `--window-size=W,H` | 设置初始窗口尺寸 | 截图、一致的视口 |\n| `--force-device-scale-factor=1` | 禁用高 DPI 缩放 | 跨系统一致渲染 |\n\n### 隐蔽和指纹识别\n\n使用这些命令行参数使您的自动化更难被检测：\n\n| 参数 | 目的 | 示例 |\n|----------|---------|---------|\n| `--disable-blink-features=AutomationControlled` | 删除 `navigator.webdriver` 标志 | 隐蔽性必不可少 |\n| `--user-agent=...` | 设置真实、常见的用户代理 | 匹配目标区域/设备 |\n| `--use-gl=swiftshader` | 软件 WebGL 渲染器 | 避免独特的 GPU 指纹 |\n| `--force-webrtc-ip-handling-policy=...` | 防止 WebRTC IP 泄露 | 使用 `disable_non_proxied_udp` |\n| `--lang=en-US` | 设置浏览器语言 | 匹配目标区域 |\n| `--accept-lang=en-US,en;q=0.9` | Accept-Language 标头 | 真实的语言偏好 |\n| `--tz=America/New_York` | 设置时区 | 匹配目标区域 |\n| `--no-first-run` | 跳过首次运行向导 | 更干净的自动化 |\n| `--no-default-browser-check` | 跳过默认浏览器提示 | 避免 UI 中断 |\n| `--disable-reading-from-canvas` | Canvas 指纹识别缓解 | 减少独特性 |\n| `--disable-features=AudioServiceOutOfProcess` | 音频指纹识别缓解 | 减少独特性 |\n\n!!! warning \"检测军备竞赛\"\n    没有单一技术能保证不可检测性。结合多种策略：\n    \n    1. **命令行参数**（此表）\n    2. **浏览器偏好设置** - [浏览器偏好设置 - 隐蔽和指纹识别](browser-preferences.md#stealth-fingerprinting)\n    3. **类人交互** - [类人交互](../automation/human-interactions.md)\n    4. **良好的 IP 声誉** - 使用历史干净的住宅代理\n\n### 安全和隐私\n\n控制安全功能和隐私设置：\n\n```python\noptions = ChromiumOptions()\n\n# 沙箱（仅在 Docker/CI 中禁用）\noptions.add_argument('--no-sandbox')  # 安全风险 - 仅在受控环境中使用\noptions.add_argument('--disable-setuid-sandbox')\n\n# HTTPS/SSL\noptions.add_argument('--ignore-certificate-errors')  # 忽略 SSL 错误\noptions.add_argument('--ignore-ssl-errors')\noptions.add_argument('--allow-insecure-localhost')\n\n# 隐私\noptions.add_argument('--disable-features=Translate')\noptions.add_argument('--disable-sync')\noptions.add_argument('--incognito')  # 在隐身模式下打开\n\n# 权限自动授予（用于测试）\noptions.add_argument('--use-fake-ui-for-media-stream')  # 自动授予摄像头/麦克风\noptions.add_argument('--use-fake-device-for-media-stream')  # 使用假设备\n```\n\n!!! danger \"沙箱警告\"\n    **`--no-sandbox` 是安全风险！** 仅在以下情况使用：\n    \n    - 在 Docker 容器中运行（沙箱与容器隔离冲突）\n    - 具有受限权限的 CI/CD 环境\n    - 您完全信任正在加载的内容\n    \n    **永远不要**在以下情况使用 `--no-sandbox`：\n    \n    - 访问不受信任的网站\n    - 运行用户提交的代码\n    - 在具有外部输入的生产环境中\n\n| 参数 | 效果 | 安全影响 |\n|----------|--------|-----------------|\n| `--no-sandbox` | 禁用 Chrome 沙箱 | **高风险** - 允许代码执行 |\n| `--ignore-certificate-errors` | 跳过 SSL 验证 | **中等风险** - 可能发生 MITM 攻击 |\n| `--incognito` | 隐私浏览模式 | 更安全 - 没有持久状态 |\n\n### 调试和开发\n\n用于调试自动化和开发的工具：\n\n```python\noptions = ChromiumOptions()\n\n# DevTools\noptions.add_argument('--auto-open-devtools-for-tabs')\n\n# 日志记录\noptions.add_argument('--enable-logging')\noptions.add_argument('--v=1')  # 详细级别（0-3）\noptions.add_argument('--log-level=0')  # 0=INFO, 1=WARNING, 2=ERROR\n\n# 崩溃处理\noptions.add_argument('--disable-crash-reporter')\noptions.add_argument('--no-crash-upload')\n\n# 启用实验性功能\noptions.add_argument('--enable-features=NetworkService,NetworkServiceInProcess')\noptions.add_argument('--enable-experimental-web-platform-features')\n\n# JavaScript 调试\noptions.add_argument('--js-flags=--expose-gc')  # 公开垃圾收集器\n```\n\n!!! tip \"远程调试\"\n    Pydoll 自动管理远程调试端口。要访问 Chrome DevTools：\n    \n    ```python\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 获取调试端口\n        port = browser._connection_port\n        print(f\"DevTools 可用于: http://localhost:{port}\")\n        \n        # 在浏览器中打开此 URL 以访问 DevTools\n    ```\n    \n    **不要**使用 `--remote-debugging-port` 参数 - 它会与 Pydoll 的内部管理冲突！\n\n### 显示和渲染\n\n控制浏览器如何渲染内容：\n\n```python\noptions = ChromiumOptions()\n\n# 视口和窗口\noptions.add_argument('--window-size=1920,1080')\noptions.add_argument('--window-position=0,0')\noptions.add_argument('--start-maximized')\noptions.add_argument('--start-fullscreen')\n\n# 高 DPI 显示器\noptions.add_argument('--force-device-scale-factor=1')\noptions.add_argument('--high-dpi-support=1')\n\n# 颜色和渲染\noptions.add_argument('--force-color-profile=srgb')\noptions.add_argument('--disable-accelerated-2d-canvas')\noptions.add_argument('--disable-accelerated-video-decode')\n\n# 字体渲染\noptions.add_argument('--font-render-hinting=none')\noptions.add_argument('--disable-font-subpixel-positioning')\n\n# 动画\noptions.add_argument('--disable-animations')\noptions.add_argument('--wm-window-animations-disabled')\n```\n\n| 参数 | 效果 | 用例 |\n|----------|--------|----------|\n| `--window-size=W,H` | 设置窗口尺寸 | 截图、一致的视口 |\n| `--start-maximized` | 打开最大化窗口 | UI 测试、全屏捕获 |\n| `--force-device-scale-factor=1` | 禁用 DPI 缩放 | 跨系统一致渲染 |\n| `--disable-animations` | 无 CSS/UI 动画 | 更快的测试、减少不稳定 |\n\n### 代理配置\n\n为所有网络流量配置代理：\n\n```python\noptions = ChromiumOptions()\n\n# HTTP/HTTPS 代理\noptions.add_argument('--proxy-server=http://proxy.example.com:8080')\n\n# 认证代理\noptions.add_argument('--proxy-server=http://user:pass@proxy.example.com:8080')\n\n# SOCKS 代理\noptions.add_argument('--proxy-server=socks5://proxy.example.com:1080')\n\n# 为特定主机绕过代理\noptions.add_argument('--proxy-bypass-list=localhost,127.0.0.1,*.local')\n\n# 代理自动配置（PAC）文件\noptions.add_argument('--proxy-pac-url=http://proxy.example.com/proxy.pac')\n```\n\n!!! info \"代理身份验证\"\n    对于需要身份验证的代理，当使用带凭据的 `--proxy-server` 参数时，Pydoll 会自动处理身份验证挑战。\n    \n    请参阅 **[请求拦截](../network/interception.md)** 了解 Fetch 域与代理的交互详情。\n\n## 辅助方法\n\n`ChromiumOptions` 为常见配置任务提供便捷方法：\n\n### 下载管理\n\n```python\noptions = ChromiumOptions()\n\n# 设置下载目录\noptions.set_default_download_directory('/home/user/downloads')\n\n# 提示下载位置\noptions.prompt_for_download = True  # 询问用户保存位置\noptions.prompt_for_download = False  # 静默下载（默认）\n\n# 允许多个自动下载\noptions.allow_automatic_downloads = True  # 无需提示即允许\noptions.allow_automatic_downloads = False  # 阻止或询问（默认）\n```\n\n### 内容阻止\n\n```python\noptions = ChromiumOptions()\n\n# 阻止弹出窗口\noptions.block_popups = True  # 阻止（在大多数情况下为默认）\noptions.block_popups = False  # 允许\n\n# 阻止通知\noptions.block_notifications = True  # 阻止请求\noptions.block_notifications = False  # 允许网站询问\n```\n\n### 隐私控制\n\n```python\noptions = ChromiumOptions()\n\n# 密码管理器\noptions.password_manager_enabled = False  # 禁用保存密码提示\noptions.password_manager_enabled = True  # 启用（默认）\n\n# WebRTC 泄露保护（防止通过 WebRTC 暴露真实 IP）\noptions.webrtc_leak_protection = True  # 添加 --force-webrtc-ip-handling-policy=disable_non_proxied_udp\noptions.webrtc_leak_protection = False  # 禁用（默认）\n```\n\n!!! tip \"WebRTC 泄露保护\"\n    即使使用代理，WebRTC 也可能泄露您的真实 IP 地址。启用 `webrtc_leak_protection` 以阻止非代理的 UDP 连接，防止 STUN 请求绕过您的代理。在使用代理进行匿名时，这是**必不可少的**。详见 **[网络基础 - WebRTC](../../deep-dive/network/network-fundamentals.md#webrtc-和-ip-泄露)** 了解详情。\n\n### 文件处理\n\n```python\noptions = ChromiumOptions()\n\n# PDF 行为\noptions.open_pdf_externally = True  # 下载 PDF 而不是查看\noptions.open_pdf_externally = False  # 在浏览器中查看（默认）\n```\n\n### 国际化\n\n```python\noptions = ChromiumOptions()\n\n# 接受语言（影响 Content-Language 标头）\noptions.set_accept_languages('en-US,en;q=0.9,pt-BR;q=0.8')\n```\n\n## 完整配置示例\n\n### 快速抓取配置\n\n针对速度和资源效率进行优化：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\ndef create_fast_scraping_options() -> ChromiumOptions:\n    \"\"\"用于 Web 抓取的超快配置。\"\"\"\n    options = ChromiumOptions()\n    \n    # 无头模式以提高速度\n    options.headless = True\n    \n    # 更快的页面加载（DOM 就绪足以进行抓取）\n    options.page_load_state = PageLoadState.INTERACTIVE\n    \n    # 禁用不必要的功能\n    options.add_argument('--disable-extensions')\n    options.add_argument('--disable-gpu')\n    options.add_argument('--disable-dev-shm-usage')\n    options.add_argument('--disable-background-networking')\n    options.add_argument('--disable-sync')\n    options.add_argument('--disable-translate')\n    \n    # 阻止减慢加载速度的内容\n    options.block_notifications = True\n    options.block_popups = True\n    \n    # 禁用图像以实现更快的加载（如果不需要）\n    options.add_argument('--blink-settings=imagesEnabled=false')\n    \n    # 网络优化\n    options.add_argument('--disable-features=NetworkPrediction')\n    options.add_argument('--dns-prefetch-disable')\n    \n    return options\n\nasync def fast_scraping_example():\n    options = create_fast_scraping_options()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # 极快的导航和抓取\n        urls = ['https://example.com', 'https://example.org', 'https://example.net']\n        \n        for url in urls:\n            await tab.go_to(url)\n            title = await tab.execute_script('return document.title')\n            print(f\"{url}: {title}\")\n\nasyncio.run(fast_scraping_example())\n```\n\n### 完整隐蔽配置\n\n为了最大的不可检测性，将命令行参数与浏览器偏好设置相结合：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\ndef create_full_stealth_options() -> ChromiumOptions:\n    \"\"\"结合参数和偏好设置的完整隐蔽配置。\"\"\"\n    options = ChromiumOptions()\n    \n    # ===== 命令行参数 =====\n    \n    # 核心隐蔽\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    options.add_argument('--disable-features=IsolateOrigins,site-per-process')\n    \n    # 用户代理（使用最新的、常见的）\n    options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36')\n    \n    # 语言和区域\n    options.add_argument('--lang=en-US')\n    options.add_argument('--accept-lang=en-US,en;q=0.9')\n    \n    # WebGL（软件渲染器以避免独特的 GPU 签名）\n    options.add_argument('--use-gl=swiftshader')\n    options.add_argument('--disable-features=WebGLDraftExtensions')\n    \n    # WebRTC IP 泄露防护\n    options.webrtc_leak_protection = True\n\n    # 权限和首次运行\n    options.add_argument('--no-first-run')\n    options.add_argument('--no-default-browser-check')\n    \n    # 窗口大小（常见分辨率）\n    options.add_argument('--window-size=1920,1080')\n    \n    # ===== 浏览器偏好设置 =====\n    # 有关全面的浏览器偏好设置配置，请参阅：\n    # https://pydoll.tech/docs/features/configuration/browser-preferences/#stealth-fingerprinting\n    \n    return options\n\nasync def stealth_automation_example():\n    options = create_full_stealth_options()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # 在机器人检测网站上测试\n        await tab.go_to('https://bot.sannysoft.com')\n        await asyncio.sleep(5)\n        \n        # 您的自动化在这里...\n\nasyncio.run(stealth_automation_example())\n```\n\n!!! warning \"用户代理一致性至关重要\"\n    设置 `--user-agent` 只会更改 **HTTP 标头**，但检测系统还会检查 `navigator.userAgent`、`navigator.platform`、`navigator.vendor` 和其他 JavaScript 属性。**这些值之间的不一致是强烈的机器人指标。**\n    \n    例如，如果您的 HTTP User-Agent 说\"Windows\"但 `navigator.platform` 说\"Linux\"，您将立即被标记。\n    \n    **解决方案**：您还必须通过 CDP 覆盖 JavaScript 属性以保持一致性。请参阅 **[浏览器指纹识别 - 用户代理一致性](../../deep-dive/fingerprinting/browser-fingerprinting.md#user-agent-consistency)** 获取详细说明和使用 `Page.addScriptToEvaluateOnNewDocument` 的实现。\n    \n    这就是为什么全面的隐蔽需要命令行参数**和**浏览器偏好设置配置。\n\n!!! tip \"完整隐蔽策略\"\n    命令行参数只是解决方案的一部分。为了最大的隐蔽性：\n    \n    1. **使用上述参数**（navigator.webdriver、WebGL、WebRTC）\n    2. **配置浏览器偏好设置** - 请参阅[浏览器偏好设置 - 隐蔽和指纹识别](browser-preferences.md#stealth-fingerprinting)\n    3. **类人交互** - 请参阅[类人交互](../automation/human-interactions.md)\n    4. **良好的代理/IP 声誉** - 使用住宅代理\n\n### Docker/CI 配置\n\n用于容器化环境：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.constants import PageLoadState\n\ndef create_docker_options() -> ChromiumOptions:\n    \"\"\"用于 Docker 容器和 CI/CD 的配置。\"\"\"\n    options = ChromiumOptions()\n    \n    # Docker 所需\n    options.headless = True\n    options.add_argument('--no-sandbox')  # 沙箱与容器隔离冲突\n    options.add_argument('--disable-dev-shm-usage')  # 克服 /dev/shm 大小限制\n    \n    # 稳定性\n    options.add_argument('--disable-gpu')\n    options.add_argument('--disable-software-rasterizer')\n    \n    # 内存优化\n    options.add_argument('--disable-extensions')\n    options.add_argument('--disable-background-networking')\n    \n    # 为 CI 更快的页面加载\n    options.page_load_state = PageLoadState.INTERACTIVE\n    \n    # 为慢速 CI 运行器增加超时\n    options.start_timeout = 20\n    \n    # 崩溃处理\n    options.add_argument('--disable-crash-reporter')\n    \n    return options\n\nasync def ci_testing_example():\n    options = create_docker_options()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # 运行您的测试...\n        await tab.go_to('https://example.com')\n        assert await tab.execute_script('return document.title') == 'Example Domain'\n\nasyncio.run(ci_testing_example())\n```\n\n## 故障排除\n\n### 浏览器无法启动\n\n```python\n# 增加超时\noptions.start_timeout = 30\n\n# 检查二进制位置\noptions.binary_location = '/path/to/chrome'\n\n# Docker/CI 问题\noptions.add_argument('--no-sandbox')\noptions.add_argument('--disable-dev-shm-usage')\n```\n\n### 性能慢\n\n```python\n# 如果不需要则禁用 GPU\noptions.add_argument('--disable-gpu')\n\n# 禁用图像\noptions.add_argument('--blink-settings=imagesEnabled=false')\n\n# 使用 INTERACTIVE 加载状态\noptions.page_load_state = PageLoadState.INTERACTIVE\n\n# 禁用不必要的功能\noptions.add_argument('--disable-extensions')\noptions.add_argument('--disable-background-networking')\n```\n\n### Docker 中的内存问题\n\n```python\n# Docker 必需\noptions.add_argument('--disable-dev-shm-usage')\n\n# 减少内存占用\noptions.add_argument('--disable-extensions')\noptions.add_argument('--disable-gpu')\noptions.add_argument('--single-process')  # 最后手段（可能不稳定）\n```\n\n## 进一步阅读\n\n- **[浏览器偏好设置](browser-preferences.md)** - Chromium 的内部偏好系统\n- **[隐蔽自动化](../automation/human-interactions.md)** - 类人交互\n- **[上下文](../browser-management/contexts.md)** - 隔离的浏览上下文\n- **[网络拦截](../network/interception.md)** - 请求/响应操作\n\n!!! tip \"实验是关键\"\n    浏览器配置高度依赖于您的具体用例。从这里的示例开始，然后根据您的需求进行调整。使用 `browser._connection_port` 访问 DevTools 并检查浏览器内部发生的情况。\n"
  },
  {
    "path": "docs/zh/features/configuration/browser-preferences.md",
    "content": "# 自定义浏览器首选项\n\nPydoll 最强大的功能之一是直接访问 Chromium 的内部首选项系统。与传统的浏览器自动化工具只公开有限的选项不同，Pydoll 为您提供与扩展程序和企业管理员相同级别的控制权，允许您配置 Chromium 源代码中提供的**任何**浏览器设置。\n\n## 为什么浏览器首选项很重要\n\n浏览器首选项控制 Chromium 行为的方方面面：\n\n- **性能**：禁用不需要的功能以加快页面加载速度\n- **隐私**：控制浏览器收集和发送的数据\n- **自动化**：删除破坏工作流程的用户提示和确认\n- **隐身**：创建逼真的浏览器指纹以避免检测\n- **企业**：应用通常只能通过组策略获得的策略\n\n!!! info \"直接访问的力量\"\n    大多数自动化工具只公开 10-20 个常见设置。Pydoll 为您提供**数百个**首选项的访问权限，从下载行为到搜索建议，从网络预测到插件管理。如果 Chromium 可以做到，您就可以配置它。\n\n## 快速开始\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def preferences_example():\n    options = ChromiumOptions()\n    \n    # 使用字典设置首选项\n    options.browser_preferences = {\n        'download': {\n            'default_directory': '/tmp/downloads',\n            'prompt_for_download': False\n        },\n        'profile': {\n            'default_content_setting_values': {\n                'notifications': 2  # 阻止通知\n            }\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # 下载自动保存到 /tmp/downloads\n        # 不会出现通知提示\n\nasyncio.run(preferences_example())\n```\n\n## 了解浏览器首选项\n\n### 什么是首选项？\n\nChromium 将所有用户可配置的设置存储在一个名为 `Preferences` 的 JSON 文件中，该文件位于浏览器的用户数据目录中。此文件包含**所有内容**，从您的主页 URL 到图像是否自动加载。\n\n**典型位置：**\n\n- **Linux**: `~/.config/google-chrome/Default/Preferences`\n- **macOS**: `~/Library/Application Support/Google/Chrome/Default/Preferences`\n- **Windows**: `%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Preferences`\n\n### 首选项文件结构\n\n首选项文件是一个嵌套的 JSON 对象：\n\n```json\n{\n  \"download\": {\n    \"default_directory\": \"/home/user/Downloads\",\n    \"prompt_for_download\": true\n  },\n  \"profile\": {\n    \"default_content_setting_values\": {\n      \"notifications\": 1,\n      \"popups\": 0\n    },\n    \"password_manager_enabled\": true\n  },\n  \"search\": {\n    \"suggest_enabled\": true\n  },\n  \"net\": {\n    \"network_prediction_options\": 1\n  }\n}\n```\n\nChromium 源代码中的每个点分隔的首选项名称都映射到嵌套的 JSON 路径：\n\n- `download.default_directory` → `{'download': {'default_directory': ...}}`\n- `profile.password_manager_enabled` → `{'profile': {'password_manager_enabled': ...}}`\n\n### Chromium 如何使用首选项\n\n当 Chromium 启动时：\n\n1. **读取**磁盘上的首选项文件\n2. **应用**这些设置来配置浏览器行为\n3. **更新**用户通过 UI 更改设置时的文件\n4. **回退**到默认值（如果缺少首选项）\n\nPydoll 通过在浏览器启动前预填充首选项文件来拦截步骤 1，确保您的自定义设置从第一次页面加载开始就被应用。\n\n## 在 Pydoll 中的工作原理\n\n### 设置首选项\n\n使用 `browser_preferences` 属性设置任何首选项：\n\n```python\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\n\n# 直接赋值 - 与现有首选项合并\noptions.browser_preferences = {\n    'download': {'default_directory': '/tmp'},\n    'intl': {'accept_languages': 'pt-BR,en-US'}\n}\n\n# 多次赋值会合并，而不是替换\noptions.browser_preferences = {\n    'profile': {'password_manager_enabled': False}\n}\n\n# 现在两组首选项都处于活动状态\n```\n\n!!! warning \"首选项是合并的，而不是替换的\"\n    当您多次设置 `browser_preferences` 时，新首选项会与现有首选项**合并**。只有您设置的特定键会被更新；其他所有内容都会保留。\n    \n    ```python\n    options.browser_preferences = {'download': {'prompt': False}}\n    options.browser_preferences = {'profile': {'password_manager_enabled': False}}\n    \n    # 结果：两个首选项都已设置\n    # {'download': {'prompt': False}, 'profile': {'password_manager_enabled': False}}\n    ```\n\n### 嵌套路径语法\n\n首选项使用嵌套字典，镜像 Chromium 的点表示法：\n\n```python\n# Chromium 源代码常量：\n# const char kDownloadDefaultDirectory[] = \"download.default_directory\";\n\n# 转换为 Python 字典：\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/path/to/downloads'\n    }\n}\n```\n\n嵌套越深，首选项越具体：\n\n```python\n# 顶层：profile\n# 第二层：default_content_setting_values  \n# 第三层：notifications\n\noptions.browser_preferences = {\n    'profile': {\n        'default_content_setting_values': {\n            'notifications': 2,  # 阻止\n            'geolocation': 2,    # 阻止\n            'media_stream': 2    # 阻止\n        }\n    }\n}\n```\n\n## 实际用例\n\n### 1. 性能优化\n\n禁用资源密集型功能以实现更快的自动化：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def performance_optimized_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        # 禁用网络预测和预取\n        'net': {\n            'network_prediction_options': 2  # 2 = 从不预测\n        },\n        # 禁用图像加载\n        'profile': {\n            'default_content_setting_values': {\n                'images': 2  # 2 = 阻止，1 = 允许\n            }\n        },\n        # 禁用插件\n        'webkit': {\n            'webprefs': {\n                'plugins_enabled': False\n            }\n        },\n        # 禁用拼写检查\n        'browser': {\n            'enable_spellchecking': False\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # 在没有图像和不必要功能的情况下，页面加载速度提高 3-5 倍\n        await tab.go_to('https://example.com')\n        print(\"快速加载完成！\")\n\nasyncio.run(performance_optimized_browser())\n```\n\n!!! tip \"性能影响\"\n    仅禁用图像就可以将图像密集型网站的页面加载时间减少 50-70%。结合禁用预取、拼写检查和插件，可实现最大速度。\n\n### 2. 隐私与反跟踪\n\n创建注重隐私的浏览器配置：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def privacy_focused_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        # 启用请勿跟踪\n        'enable_do_not_track': True,\n        \n        # 禁用引荐来源\n        'enable_referrers': False,\n        \n        # 禁用安全浏览（将 URL 发送到 Google）\n        'safebrowsing': {\n            'enabled': False\n        },\n        \n        # 禁用密码管理器\n        'profile': {\n            'password_manager_enabled': False\n        },\n        \n        # 禁用自动填充\n        'autofill': {\n            'enabled': False,\n            'profile_enabled': False\n        },\n        \n        # 禁用搜索建议（将查询发送到搜索引擎）\n        'search': {\n            'suggest_enabled': False\n        },\n        \n        # 禁用遥测和指标\n        'user_experience_metrics': {\n            'reporting_enabled': False\n        },\n        \n        # 阻止第三方 cookie\n        'profile': {\n            'block_third_party_cookies': True,\n            'cookie_controls_mode': 1\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        print(\"注重隐私的浏览器已准备就绪！\")\n\nasyncio.run(privacy_focused_browser())\n```\n\n### 3. 静默下载\n\n自动化文件下载，无需用户交互：\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def silent_download_automation():\n    download_dir = Path.home() / 'automation_downloads'\n    download_dir.mkdir(exist_ok=True)\n    \n    options = ChromiumOptions()\n    options.browser_preferences = {\n        'download': {\n            'default_directory': str(download_dir),\n            'prompt_for_download': False,\n            'directory_upgrade': True\n        },\n        'profile': {\n            'default_content_setting_values': {\n                'automatic_downloads': 1  # 1 = 允许，2 = 阻止\n            }\n        },\n        # 始终下载 PDF 而不是在查看器中打开\n        'plugins': {\n            'always_open_pdf_externally': True\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/downloads')\n        \n        # 点击下载链接 - 文件自动保存\n        download_link = await tab.find(text='Download Report')\n        await download_link.click()\n        \n        await asyncio.sleep(3)\n        print(f\"文件已下载到：{download_dir}\")\n\nasyncio.run(silent_download_automation())\n```\n\n### 4. 阻止侵入性 UI 元素\n\n删除破坏自动化的弹出窗口、通知和提示：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def clean_ui_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        'profile': {\n            'default_content_setting_values': {\n                'notifications': 2,      # 阻止通知\n                'popups': 0,             # 阻止弹出窗口\n                'geolocation': 2,        # 阻止位置请求\n                'media_stream': 2,       # 阻止摄像头/麦克风访问\n                'media_stream_mic': 2,   # 阻止麦克风\n                'media_stream_camera': 2 # 阻止摄像头\n            }\n        },\n        # 禁用翻译提示\n        'translate': {\n            'enabled': False\n        },\n        # 禁用保存密码提示\n        'credentials_enable_service': False,\n        \n        # 禁用\"Chrome 正在被自动化软件控制\"信息栏\n        'devtools': {\n            'preferences': {\n                'currentDockState': '\"undocked\"'\n            }\n        }\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        # 没有弹出窗口，没有提示，干净的自动化！\n\nasyncio.run(clean_ui_browser())\n```\n\n### 5. 国际化与本地化\n\n配置语言和区域设置首选项：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def localized_browser():\n    options = ChromiumOptions()\n    options.browser_preferences = {\n        # 接受语言（优先顺序）\n        'intl': {\n            'accept_languages': 'pt-BR,pt,en-US,en'\n        },\n        \n        # 拼写检查语言\n        'spellcheck': {\n            'dictionaries': ['pt-BR', 'en-US']\n        },\n        \n        # 翻译设置\n        'translate': {\n            'enabled': True\n        },\n        'translate_blocked_languages': ['en'],  # 不提供翻译英语\n        \n        # 默认字符编码\n        'default_charset': 'UTF-8'\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        # 为巴西葡萄牙语配置的浏览器\n\nasyncio.run(localized_browser())\n```\n\n## 辅助方法\n\n对于常见场景，Pydoll 提供便利方法：\n\n```python\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\n\n# 下载管理\noptions.set_default_download_directory('/tmp/downloads')\noptions.prompt_for_download = False\noptions.allow_automatic_downloads = True\noptions.open_pdf_externally = True\n\n# 内容阻止\noptions.block_notifications = True\noptions.block_popups = True\n\n# 隐私\noptions.password_manager_enabled = False\n\n# 国际化\noptions.set_accept_languages('pt-BR,en-US,en')\n```\n\n这些方法是为您设置正确嵌套首选项的快捷方式：\n\n```python\n# 这个辅助方法：\noptions.set_default_download_directory('/tmp')\n\n# 等同于：\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/tmp'\n    }\n}\n```\n\n!!! tip \"将辅助方法与直接首选项结合使用\"\n    使用辅助方法处理常见设置，使用 `browser_preferences` 处理高级配置：\n    \n    ```python\n    # 从辅助方法开始\n    options.block_notifications = True\n    options.prompt_for_download = False\n    \n    # 添加高级首选项\n    options.browser_preferences = {\n        'net': {'network_prediction_options': 2},\n        'webkit': {'webprefs': {'plugins_enabled': False}}\n    }\n    ```\n\n## 在 Chromium 源代码中查找首选项\n\n### 源代码参考\n\nChromium 在 `pref_names.cc` 中定义所有首选项常量：\n\n**官方源代码**：[chromium/src/+/main/chrome/common/pref_names.cc](https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.cc)\n\n### 阅读源代码\n\n首选项常量使用点表示法，直接映射到嵌套字典：\n\n```cpp\n// 来自 Chromium 源代码 (pref_names.cc)：\nconst char kDownloadDefaultDirectory[] = \"download.default_directory\";\nconst char kPromptForDownload[] = \"download.prompt_for_download\";\nconst char kSafeBrowsingEnabled[] = \"safebrowsing.enabled\";\nconst char kBlockThirdPartyCookies[] = \"profile.block_third_party_cookies\";\n```\n\n**转换为 Python：**\n\n```python\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/path/to/dir',\n        'prompt_for_download': False\n    },\n    'safebrowsing': {\n        'enabled': False\n    },\n    'profile': {\n        'block_third_party_cookies': True\n    }\n}\n```\n\n### 发现过程\n\n1. **搜索源代码**：访问 [pref_names.cc](https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.cc)\n2. **找到您的首选项**：搜索关键字（例如 \"download\"、\"password\"、\"notification\"）\n3. **记录常量名称**：例如 `kDownloadDefaultDirectory[] = \"download.default_directory\"`\n4. **转换为字典**：按点分割并创建嵌套结构\n\n**示例 - 查找通知首选项：**\n\n```cpp\n// 在 pref_names.cc 中搜索 \"notification\"：\nconst char kPushMessagingAppIdentifierMap[] = \n    \"gcm.push_messaging_application_id_map\";\nconst char kDefaultNotificationsSetting[] = \n    \"profile.default_content_setting_values.notifications\";\n```\n\n```python\n# 变成：\noptions.browser_preferences = {\n    'profile': {\n        'default_content_setting_values': {\n            'notifications': 2  # 2 = 阻止，1 = 允许，0 = 询问\n        }\n    }\n}\n```\n\n### 常见首选项模式\n\n| 类别 | 示例常量 | Python 字典路径 |\n|----------|-----------------|------------------|\n| 下载 | `download.default_directory` | `{'download': {'default_directory': ...}}` |\n| 内容设置 | `profile.default_content_setting_values.X` | `{'profile': {'default_content_setting_values': {'X': ...}}}` |\n| 网络 | `net.network_prediction_options` | `{'net': {'network_prediction_options': ...}}` |\n| 隐私 | `safebrowsing.enabled` | `{'safebrowsing': {'enabled': ...}}` |\n| 会话 | `session.restore_on_startup` | `{'session': {'restore_on_startup': ...}}` |\n\n!!! warning \"未记录的首选项\"\n    并非所有首选项都有文档。有些是：\n    \n    - **实验性**：可能在未来的 Chromium 版本中更改或删除\n    - **内部**：由 Chromium 的内部系统使用\n    - **平台特定**：仅在某些操作系统上工作\n    \n    在依赖未记录的首选项之前，请彻底测试。\n\n## 有用的首选项参考\n\n以下是从 Chromium 的 `pref_names.cc` 中精选的有趣且有用的首选项列表：\n\n### 内容与媒体设置\n\n```python\noptions.browser_preferences = {\n    'profile': {\n        'default_content_setting_values': {\n            # 内容控制 (0=询问，1=允许，2=阻止)\n            'cookies': 1,                    # 允许 cookie\n            'images': 1,                     # 允许图像（2 为阻止）\n            'javascript': 1,                 # 允许 JavaScript（2 为阻止）\n            'plugins': 2,                    # 阻止插件（Flash 等）\n            'popups': 0,                     # 阻止弹出窗口\n            'geolocation': 2,                # 阻止位置请求\n            'notifications': 2,              # 阻止通知\n            'media_stream': 2,               # 阻止摄像头/麦克风\n            'media_stream_mic': 2,           # 仅阻止麦克风\n            'media_stream_camera': 2,        # 仅阻止摄像头\n            'automatic_downloads': 1,        # 允许自动下载\n            'midi_sysex': 2,                 # 阻止 MIDI 访问\n            'clipboard': 1,                  # 允许剪贴板访问\n            'sensors': 2,                    # 阻止运动传感器\n            'usb_guard': 2,                  # 阻止 USB 设备访问\n            'serial_guard': 2,               # 阻止串行端口访问\n            'bluetooth_guard': 2,            # 阻止蓝牙\n            'file_system_write_guard': 2,    # 阻止文件系统写入\n        }\n    }\n}\n```\n\n### 网络与性能\n\n```python\noptions.browser_preferences = {\n    'net': {\n        # 网络预测：0=始终，1=仅 WiFi，2=从不\n        'network_prediction_options': 2,\n        \n        # 快速检查服务器可达性\n        'quick_check_enabled': False\n    },\n    \n    # DNS 预取\n    'dns_prefetching': {\n        'enabled': False  # 禁用以减少网络流量\n    },\n    \n    # 预连接到搜索结果\n    'search': {\n        'suggest_enabled': False,           # 禁用搜索建议\n        'instant_enabled': False            # 禁用即时结果\n    },\n    \n    # 备用错误页面\n    'alternate_error_pages': {\n        'enabled': False  # 不建议 404 的替代方案\n    }\n}\n```\n\n### 下载首选项\n\n```python\noptions.browser_preferences = {\n    'download': {\n        'default_directory': '/path/to/downloads',\n        'prompt_for_download': False,\n        'directory_upgrade': True,\n        'extensions_to_open': '',           # 自动打开的文件类型\n        'open_pdf_externally': True,        # 不使用内部 PDF 查看器\n    },\n    \n    'download_bubble': {\n        'partial_view_enabled': True        # 显示下载进度气泡\n    },\n    \n    'safebrowsing': {\n        'enabled': False  # 禁用安全浏览下载警告\n    }\n}\n```\n\n### 隐私与安全\n\n```python\noptions.browser_preferences = {\n    # 请勿跟踪\n    'enable_do_not_track': True,\n    \n    # 引荐来源\n    'enable_referrers': False,\n    \n    # 安全浏览\n    'safebrowsing': {\n        'enabled': False,                   # 禁用安全浏览\n        'enhanced': False                   # 禁用增强保护\n    },\n    \n    # 隐私沙盒（Google 的 cookie 替代品）\n    'privacy_sandbox': {\n        'apis_enabled': False,\n        'topics_enabled': False,\n        'fledge_enabled': False\n    },\n    \n    # 第三方 cookie\n    'profile': {\n        'block_third_party_cookies': True,\n        'cookie_controls_mode': 1,          # 在隐身模式下阻止第三方\n        \n        # 内容设置\n        'default_content_setting_values': {\n            'cookies': 1,\n            'third_party_cookie_blocking_enabled': True\n        }\n    },\n    \n    # WebRTC（可能泄露真实 IP）\n    'webrtc': {\n        'ip_handling_policy': 'default_public_interface_only',\n        'multiple_routes_enabled': False,\n        'nonproxied_udp_enabled': False\n    }\n}\n```\n\n### 自动填充与密码\n\n```python\noptions.browser_preferences = {\n    'autofill': {\n        'enabled': False,                   # 禁用表单自动填充\n        'profile_enabled': False,           # 禁用地址自动填充\n        'credit_card_enabled': False,       # 禁用信用卡自动填充\n        'credit_card_fido_auth_enabled': False\n    },\n    \n    'profile': {\n        'password_manager_enabled': False,\n        'password_manager_leak_detection': False\n    },\n    \n    'credentials_enable_service': False,\n    'credentials_enable_autosignin': False\n}\n```\n\n### 浏览器行为与 UI\n\n```python\nimport time\n\noptions.browser_preferences = {\n    # 主页和启动\n    'homepage': 'https://www.google.com',\n    'homepage_is_newtabpage': False,\n    'newtab_page_location_override': 'https://www.google.com',\n    \n    'session': {\n        'restore_on_startup': 1,            # 0=新标签页，1=恢复，4=特定 URL，5=新标签页\n        'startup_urls': ['https://www.google.com'],\n        'session_data_status': 3            # 会话数据状态（内部）\n    },\n    \n    # 欢迎页面和窗口\n    'browser': {\n        'has_seen_welcome_page': True,      # 跳过欢迎屏幕\n        'window_placement': {\n            'bottom': 1032,                 # 窗口底部位置\n            'left': 2247,                   # 窗口左侧位置\n            'right': 3192,                  # 窗口右侧位置\n            'top': 31,                      # 窗口顶部位置\n            'maximized': False,             # 窗口最大化\n            'work_area_bottom': 1080,       # 屏幕工作区底部\n            'work_area_left': 1920,         # 屏幕工作区左侧\n            'work_area_right': 3840,        # 屏幕工作区右侧\n            'work_area_top': 0              # 屏幕工作区顶部\n        }\n    },\n    \n    # 扩展\n    'extensions': {\n        'ui': {\n            'developer_mode': False\n        },\n        'alerts': {\n            'initialized': True\n        },\n        'theme': {\n            'system_theme': 2               # 0=默认，1=浅色，2=深色\n        },\n        'last_chrome_version': '130.0.6723.91'  # 必须与您的版本匹配\n    },\n    \n    # 翻译\n    'translate': {\n        'enabled': False                    # 禁用翻译提示\n    },\n    'translate_blocked_languages': ['en'],  # 从不翻译英语\n    'translate_site_blacklist': [],         # 旧版（使用 blocklist_with_time）\n    \n    # 书签\n    'bookmark_bar': {\n        'show_on_all_tabs': False\n    },\n    \n    # 标签页\n    'tabs': {\n        'new_tab_position': 0               # 0=右侧，1=当前之后\n    },\n    'pinned_tabs': [],                      # 固定标签页 URL 列表\n    \n    # 新标签页（Chrome 格式的时间戳）\n    'NewTabPage': {\n        'PrevNavigationTime': str(int(time.time() * 1000000) + 11644473600000000)  # Chrome 时间戳\n    },\n    'ntp': {\n        'num_personal_suggestions': 6       # 建议数量（0-10）\n    },\n    \n    # 工具栏自定义\n    'toolbar': {\n        'pinned_chrome_labs_migration_complete': True\n    }\n}\n```\n\n!!! info \"Chrome 时间戳格式\"\n    Chrome 使用 Windows FILETIME 格式：自 1601 年 1 月 1 日 UTC 以来的微秒。\n    \n    转换 Python 时间戳：\n    ```python\n    import time\n    chrome_time = int(time.time() * 1000000) + 11644473600000000\n    ```\n\n### 拼写与语言\n\n```python\noptions.browser_preferences = {\n    'browser': {\n        'enable_spellchecking': False       # 禁用拼写检查\n    },\n    \n    'spellcheck': {\n        'dictionaries': ['en-US', 'pt-BR'], # 拼写检查语言\n        'dictionary': '',                   # 旧版首选项（保持为空）\n        'use_spelling_service': False       # 不发送到 Google\n    },\n    \n    'intl': {\n        'accept_languages': 'pt-BR,pt,en-US,en',\n        'selected_languages': 'pt-BR,pt,en-US,en'  # 明确选择的\n    },\n    \n    # 翻译行为和历史\n    'translate': {\n        'enabled': True\n    },\n    'translate_accepted_count': {\n        'pt-BR': 0,\n        'es': 5                             # 接受了 5 次西班牙语翻译\n    },\n    'translate_denied_count_for_language': {\n        'en': 10                            # 从不翻译英语\n    },\n    'translate_ignored_count_for_language': {\n        'en': 1\n    },\n    'translate_site_blocklist_with_time': {},  # 从不翻译的网站\n    \n    # 无障碍字幕语言\n    'accessibility': {\n        'captions': {\n            'live_caption_language': 'pt-BR'\n        }\n    },\n    \n    # 语言模型计数器（使用统计）\n    'language_model_counters': {\n        'en': 2,                            # 英语单词计数\n        'pt': 10                            # 葡萄牙语单词计数\n    }\n}\n```\n\n!!! info \"语言模型计数器\"\n    这些计数器跟踪 Chrome 机器学习模型的语言使用统计信息：\n    \n    - 用于预测用户语言偏好\n    - 影响搜索建议和自动完成\n    - 更高的计数表示更频繁的使用\n    - 真实值：偶尔使用 0-1000，大量使用 1000+\n\n### 无障碍\n\n```python\noptions.browser_preferences = {\n    'accessibility': {\n        'image_labels_enabled': False       # 不从 Google 获取图像标签\n    },\n    \n    # 字体设置\n    'webkit': {\n        'webprefs': {\n            'default_font_size': 16,\n            'default_fixed_font_size': 13,\n            'minimum_font_size': 0,\n            'minimum_logical_font_size': 6,\n            'fonts': {\n                'standard': {\n                    'Zyyy': 'Arial'\n                },\n                'serif': {\n                    'Zyyy': 'Times New Roman'\n                }\n            }\n        }\n    }\n}\n```\n\n### 媒体与音频\n\n```python\noptions.browser_preferences = {\n    # 音频\n    'audio': {\n        'mute_enabled': False               # 启动时音频开/关\n    },\n    \n    # 自动播放\n    'media': {\n        'autoplay_policy': 0,               # 0=允许，1=用户手势，2=文档用户激活\n        'video_fullscreen_orientation_lock': False\n    },\n    \n    # WebGL\n    'webkit': {\n        'webprefs': {\n            'webgl_enabled': True,          # 启用/禁用 WebGL\n            'webgl2_enabled': True\n        }\n    }\n}\n```\n\n### 打印\n\n```python\noptions.browser_preferences = {\n    'printing': {\n        'print_preview_sticky_settings': {\n            'appState': '{\\\"version\\\":2,\\\"recentDestinations\\\":[{\\\"id\\\":\\\"Save as PDF\\\",\\\"origin\\\":\\\"local\\\"}],\\\"marginsType\\\":3,\\\"customMargins\\\":{\\\"marginTop\\\":63,\\\"marginRight\\\":192,\\\"marginBottom\\\":240,\\\"marginLeft\\\":260}}'\n        }\n    },\n    \n    'savefile': {\n        'default_directory': '/tmp'         # PDF 的默认保存位置\n    }\n}\n```\n\n!!! tip \"打印 appState 格式\"\n    `appState` 是一个 JSON 编码的字符串。为了更容易操作：\n    \n    ```python\n    import json\n    \n    app_state = {\n        'version': 2,\n        'recentDestinations': [{\n            'id': 'Save as PDF',\n            'origin': 'local'\n        }],\n        'marginsType': 3,                   # 0=默认，1=无边距，2=最小，3=自定义\n        'customMargins': {\n            'marginTop': 63,\n            'marginRight': 192,\n            'marginBottom': 240,\n            'marginLeft': 260\n        },\n        'isHeaderFooterEnabled': False,\n        'scaling': '100',\n        'scalingType': 3,                   # 0=默认，1=适合页面，2=适合纸张，3=自定义\n        'isColorEnabled': True,\n        'isDuplexEnabled': False,\n        'isCssBackgroundEnabled': True,\n        'dpi': {\n            'horizontal_dpi': 300,\n            'vertical_dpi': 300,\n            'is_default': True\n        },\n        'mediaSize': {\n            'name': 'ISO_A4',\n            'width_microns': 210000,\n            'height_microns': 297000,\n            'custom_display_name': 'A4',\n            'is_default': True\n        }\n    }\n    \n    # 转换为字符串用于 appState\n    options.browser_preferences = {\n        'printing': {\n            'print_preview_sticky_settings': {\n                'appState': json.dumps(app_state)\n            }\n        }\n    }\n    ```\n\n### WebRTC 与点对点\n\n```python\noptions.browser_preferences = {\n    'webrtc': {\n        # IP 处理策略\n        'ip_handling_policy': 'default_public_interface_only',\n        \n        # UDP 传输选项\n        'udp_port_range': '10000-10100',    # 限制 UDP 端口范围\n        \n        # 禁用点对点\n        'multiple_routes_enabled': False,\n        'nonproxied_udp_enabled': False,\n        \n        # 文本日志收集\n        'text_log_collection_allowed': False\n    }\n}\n```\n\n### 站点隔离与安全\n\n```python\noptions.browser_preferences = {\n    # 站点隔离\n    'site_isolation': {\n        'isolate_origins': '',              # 要隔离的逗号分隔的源\n        'site_per_process': True            # 完整站点隔离\n    },\n    \n    # 混合内容\n    'mixed_content': {\n        'auto_upgrade_enabled': True        # 将 HTTP 升级到 HTTPS\n    },\n    \n    # SSL/TLS\n    'ssl': {\n        'rev_checking': {\n            'enabled': True                 # 检查证书吊销\n        }\n    }\n}\n```\n\n### 安装与国家元数据\n\n```python\nimport uuid\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    # 安装时的国家 ID（影响默认设置和语言环境）\n    'countryid_at_install': 16978,          # 因国家而异（例如，巴西为 16978）\n    \n    # 默认应用安装状态\n    'default_apps_install_state': 3,        # 0=未安装，1=已安装，3=已迁移\n    \n    # 企业配置文件 GUID（用于托管浏览器）\n    'enterprise_profile_guid': str(uuid.uuid4()),\n    \n    # 默认搜索提供商\n    'default_search_provider': {\n        'guid': ''                          # 空表示默认（Google）\n    }\n}\n```\n\n!!! info \"国家 ID 值\"\n    `countryid_at_install` 是一个数字代码，表示首次安装 Chrome 的国家：\n    \n    - **16978**：巴西 (BR)\n    - **16965**：美国 (US)\n    - **16967**：英国 (GB)\n    - **16966**：德国 (DE)\n    - **16972**：日本 (JP)\n    - 还有许多其他...\n    \n    这会影响默认语言、货币和区域设置。为了实现逼真的指纹识别，请将其与目标区域匹配。\n\n### 实验性功能\n\n```python\noptions.browser_preferences = {\n    # Chrome Labs 实验\n    'browser': {\n        'labs': {\n            'enabled': False\n        }\n    },\n    \n    # 预加载\n    'preload': {\n        'enabled': False                    # 禁用页面预加载\n    },\n    \n    # 平滑滚动\n    'smooth_scrolling': {\n        'enabled': True\n    },\n    \n    # 硬件加速\n    'hardware_acceleration_mode': {\n        'enabled': True                     # 禁用以提高无头性能\n    }\n}\n```\n\n### DevTools 与开发者选项\n\n```python\noptions.browser_preferences = {\n    'devtools': {\n        'preferences': {\n            # DevTools 外观\n            'currentDockState': '\"right\"',              # \"bottom\"、\"right\"、\"undocked\"\n            'uiTheme': '\"dark\"',                        # \"dark\"、\"light\"、\"system\"\n            \n            # 控制台设置\n            'consoleTimestampsEnabled': 'true',\n            'preserveConsoleLog': 'true',\n            \n            # 网络面板\n            'network.disableCache': 'false',\n            'network.color-code-resource-types': 'true',\n            'network-panel-split-view-state': '{\"vertical\":{\"size\":0}}',\n            \n            # 源映射\n            'cssSourceMapsEnabled': 'true',\n            'jsSourceMapsEnabled': 'true',\n            \n            # 元素面板\n            'elements.styles.sidebar.width': '{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}',\n            \n            # 检查器版本控制\n            'inspectorVersion': '37',\n            \n            # 选定的面板\n            'panel-selected-tab': '\"network\"',          # 最后打开的面板\n            \n            # 请求信息展开的类别\n            'request-info-general-category-expanded': 'true',\n            'request-info-request-headers-category-expanded': 'true',\n            'request-info-response-headers-category-expanded': 'true'\n        },\n        'synced_preferences_sync_disabled': {\n            'adorner-settings': '[{\"adorner\":\"grid\",\"isEnabled\":true},{\"adorner\":\"flex\",\"isEnabled\":true}]',\n            'syncedInspectorVersion': '37'\n        }\n    },\n    \n    # GCM（Google Cloud Messaging）\n    'gcm': {\n        'product_category_for_subtypes': 'com.chrome.linux'  # com.chrome.windows、com.chrome.macos\n    }\n}\n```\n\n!!! tip \"DevTools 首选项格式\"\n    DevTools 首选项使用独特的格式，其中布尔值和字符串值存储为 **JSON 编码的字符串**（例如 `'true'` 而不是 `True`，`'\"dark\"'` 而不是 `'dark'`）。这是因为 DevTools 设置直接序列化为 JSON。\n    \n    对于复杂对象，双重编码：\n    ```python\n    import json\n    \n    # 创建对象\n    split_view = {'vertical': {'size': 0}}\n    \n    # 为 DevTools 双重编码\n    devtools_value = json.dumps(json.dumps(split_view))\n    # 结果：'\"{\\\\\"vertical\\\\\":{\\\\\"size\\\\\":0}}\"'\n    ```\n\n### 同步与登录控制\n\n```python\nimport time\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    'signin': {\n        'allowed': True,                        # 允许登录 Google\n        'cookie_clear_on_exit_migration_notice_complete': True\n    },\n    \n    'sync': {\n        'data_type_status_for_sync_to_signin': {\n            'bookmarks': False,\n            'history': False,\n            'passwords': False,\n            'preferences': False\n        },\n        'encryption_bootstrap_token_per_account_migration_done': True,\n        'passwords_per_account_pref_migration_done': True,\n        'feature_status_for_sync_to_signin': 5\n    },\n    \n    # Google 服务\n    'google': {\n        'services': {\n            'signin_scoped_device_id': '<your-device-id>'  # 生成唯一 ID\n        }\n    },\n    \n    # GAIA（Google 帐户基础架构）\n    'gaia_cookie': {\n        'changed_time': str(int(time.time())),\n        'hash': '',\n        'last_list_accounts_data': '[]'\n    }\n}\n```\n\n### 优化与性能跟踪\n\n```python\nimport time\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    # 优化指南（Google 的性能提示）\n    'optimization_guide': {\n        'hintsfetcher': {\n            'hosts_successfully_fetched': {}\n        },\n        'predictionmodelfetcher': {\n            'last_fetch_attempt': str(int(time.time())),\n            'last_fetch_success': str(int(time.time()))\n        },\n        'previously_registered_optimization_types': {}\n    },\n    \n    # 历史群集（分组相关浏览）\n    'history_clusters': {\n        'all_cache': {\n            'all_keywords': {},\n            'all_timestamp': str(int(time.time()))\n        },\n        'last_selected_tab': 0,\n        'short_cache': {\n            'short_keywords': {},\n            'short_timestamp': '0'\n        }\n    },\n    \n    # 域多样性指标\n    'domain_diversity': {\n        'last_reporting_timestamp': str(int(time.time()))\n    },\n    \n    # 分段平台（用户行为分析）\n    'segmentation_platform': {\n        'device_switcher_util': {\n            'result': {\n                'labels': ['NotSynced']\n            }\n        },\n        'last_db_compaction_time': str(int(time.time()))\n    },\n    \n    # 零建议（地址栏预测）\n    'zerosuggest': {\n        'cachedresults': '',\n        'cachedresults_with_url': {}\n    }\n}\n```\n\n!!! info \"性能跟踪首选项\"\n    这些首选项通常由 Chrome 用于跟踪和优化性能。对于自动化，您可以将它们留空或设置真实值以使其看起来更像正常浏览器。\n\n### 会话事件与崩溃处理\n\nChrome 跟踪会话历史以进行恢复和遥测：\n\n```python\nimport time\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.browser_preferences = {\n    'sessions': {\n        'event_log': [\n            {\n                'crashed': False,\n                'time': str(int(time.time() * 1000000) + 11644473600000000),\n                'type': 0                   # 0=会话开始\n            },\n            {\n                'crashed': False,\n                'did_schedule_command': True,\n                'first_session_service': True,\n                'tab_count': 1,\n                'time': str(int(time.time() * 1000000) + 11644473600000000),\n                'type': 2,                  # 2=会话数据已保存\n                'window_count': 1\n            }\n        ],\n        'session_data_status': 3            # 0=未知，1=无数据，2=部分数据，3=完整数据\n    },\n    \n    # 配置文件退出类型（对指纹识别很重要）\n    'profile': {\n        'exit_type': 'Crashed'              # 'Normal'、'Crashed'、'SessionEnded'\n    }\n}\n```\n\n!!! warning \"崩溃与正常\"\n    大多数真实浏览器**偶尔会崩溃**。始终显示 `'Normal'` 退出是可疑的。\n    \n    **真实策略**：为约 10-20% 的配置文件设置 `'Crashed'` 以模拟正常用户体验。具有讽刺意味的是，偶尔出现\"崩溃\"会使您的自动化看起来更像人类。\n\n!!! tip \"会话事件类型\"\n    - **类型 0**：会话开始\n    - **类型 1**：会话正常结束\n    - **类型 2**：会话数据已保存（标签页、窗口）\n    - **类型 3**：会话已恢复\n    \n    `event_log` 会随着时间的推移建立浏览器会话的历史记录。\n\n## 隐身与指纹识别\n\n创建逼真的浏览器指纹对于避免机器人检测系统至关重要。本节涵盖基本和高级技术。\n\n### 快速隐身设置\n\n对于大多数用例，这种简单的配置提供了良好的反检测：\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def quick_stealth():\n    options = ChromiumOptions()\n    \n    # 模拟 60 天前的浏览器\n    fake_timestamp = int(time.time()) - (60 * 24 * 60 * 60)\n    \n    options.browser_preferences = {\n        # 虚假使用历史\n        'profile': {\n            'last_engagement_time': fake_timestamp,\n            'exited_cleanly': True,\n            'exit_type': 'Normal'\n        },\n        \n        # 真实主页\n        'homepage': 'https://www.google.com',\n        'session': {\n            'restore_on_startup': 1,\n            'startup_urls': ['https://www.google.com']\n        },\n        \n        # 启用真实用户拥有的功能\n        'enable_do_not_track': False,  # 大多数用户不启用此功能\n        'safebrowsing': {'enabled': True},\n        'autofill': {'enabled': True},\n        'search': {'suggest_enabled': True},\n        'dns_prefetching': {'enabled': True}\n    }\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://bot-detection-site.com')\n        print(\"隐身模式已激活！\")\n\nasyncio.run(quick_stealth())\n```\n\n!!! tip \"关键隐身原则\"\n    **启用，而不是禁用**：真实用户启用了安全浏览、自动填充和搜索建议。禁用所有内容看起来可疑。\n    \n    **老化您的配置文件**：全新安装是一个危险信号。模拟已使用数周或数月的浏览器。\n    \n    **匹配大多数**：使用 90% 的用户拥有的默认设置，而不是注重隐私的配置。\n\n### 高级指纹识别\n\n为了实现最大的真实性，模拟详细的浏览器使用历史：\n\n```python\nimport time\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\ndef create_realistic_browser() -> ChromiumOptions:\n    \"\"\"创建具有全面指纹识别抵抗力的浏览器。\"\"\"\n    options = ChromiumOptions()\n    \n    # 时间戳\n    current_time = int(time.time())\n    install_time = current_time - (90 * 24 * 60 * 60)  # 90 天前\n    last_use = current_time - (3 * 60 * 60)            # 3 小时前\n    \n    options.browser_preferences = {\n        # 配置文件元数据（对指纹识别至关重要）\n        'profile': {\n            'created_by_version': '130.0.6723.91',      # 必须与您的 Chrome 版本匹配\n            'creation_time': str(install_time),\n            'last_engagement_time': str(last_use),\n            'exit_type': 'Crashed',                     # 'Normal'、'Crashed'、'SessionEnded'\n            'name': 'Pessoa 1',                         # 真实的配置文件名称\n            'avatar_index': 26,                         # 0-26 可用头像\n            \n            # 真实的内容设置\n            'default_content_setting_values': {\n                'cookies': 1,\n                'images': 1,\n                'javascript': 1,\n                'popups': 0,\n                'notifications': 2,\n                'geolocation': 0,           # 询问（不阻止）\n                'media_stream': 0           # 询问（真实）\n            },\n            \n            'password_manager_enabled': False,\n            'cookie_controls_mode': 0,\n            'content_settings': {\n                'pref_version': 1,\n                'enable_quiet_permission_ui': {\n                    'notifications': False\n                },\n                'enable_quiet_permission_ui_enabling_method': {\n                    'notifications': 1\n                }\n            },\n            \n            # 安全元数据\n            'family_member_role': 'not_in_family',\n            'managed_user_id': '',\n            'were_old_google_logins_removed': True\n        },\n        \n        # 浏览器使用元数据\n        'browser': {\n            'has_seen_welcome_page': True,\n            'window_placement': {\n                'work_area_bottom': 1080,\n                'work_area_left': 0,\n                'work_area_right': 1920,\n                'work_area_top': 0\n            }\n        },\n        \n        # 安装元数据\n        'countryid_at_install': 16978,              # 因国家而异\n        'default_apps_install_state': 3,\n        \n        # 扩展元数据\n        'extensions': {\n            'last_chrome_version': '130.0.6723.91',  # 必须与您的版本匹配\n            'alerts': {'initialized': True},\n            'theme': {'system_theme': 2}\n        },\n        \n        # 会话活动（显示定期使用）\n        'in_product_help': {\n            'session_start_time': str(current_time),\n            'session_last_active_time': str(current_time),\n            'recent_session_start_times': [\n                str(current_time - (24 * 60 * 60)),\n                str(current_time - (48 * 60 * 60)),\n                str(current_time - (72 * 60 * 60))\n            ]\n        },\n        \n        # 会话恢复\n        'session': {\n            'restore_on_startup': 1,\n            'startup_urls': ['https://www.google.com']\n        },\n        \n        # 主页\n        'homepage': 'https://www.google.com',\n        'homepage_is_newtabpage': False,\n        \n        # 翻译历史（显示多语言使用）\n        'translate': {'enabled': True},\n        'translate_accepted_count': {'es': 2, 'fr': 1},\n        'translate_denied_count_for_language': {'en': 1},\n        \n        # 拼写检查\n        'spellcheck': {\n            'dictionaries': ['en-US', 'pt-BR'],\n            'dictionary': ''\n        },\n        \n        # 语言\n        'intl': {\n            'selected_languages': 'en-US,en,pt-BR'\n        },\n        \n        # 登录元数据\n        'signin': {\n            'allowed': True,\n            'cookie_clear_on_exit_migration_notice_complete': True\n        },\n        \n        # 安全浏览（大多数用户拥有此功能）\n        'safebrowsing': {\n            'enabled': True,\n            'enhanced': False\n        },\n        \n        # 自动填充（真实用户常见）\n        'autofill': {\n            'enabled': True,\n            'profile_enabled': True\n        },\n        \n        # 搜索建议\n        'search': {'suggest_enabled': True},\n        \n        # DNS 预取\n        'dns_prefetching': {'enabled': True},\n        \n        # 请勿跟踪（通常关闭）\n        'enable_do_not_track': False,\n        \n        # WebRTC（默认设置）\n        'webrtc': {\n            'ip_handling_policy': 'default',\n            'multiple_routes_enabled': True\n        },\n        \n        # 隐私沙盒（Google 的 cookie 替代品 - 真实用户拥有此功能）\n        'privacy_sandbox': {\n            'first_party_sets_data_access_allowed_initialized': True,\n            'm1': {\n                'ad_measurement_enabled': True,\n                'fledge_enabled': True,\n                'row_notice_acknowledged': True,\n                'topics_enabled': True\n            }\n        },\n        \n        # 媒体参与度\n        'media': {\n            'engagement': {'schema_version': 5}\n        },\n        \n        # Web 应用\n        'web_apps': {\n            'did_migrate_default_chrome_apps': ['app-id'],\n            'last_preinstall_synchronize_version': '130'\n        }\n    }\n    \n    return options\n\n# 使用\nasync def advanced_stealth():\n    options = create_realistic_browser()\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://advanced-bot-detection.com')\n        # 浏览器显示为 90 天前的真实安装\n```\n\n!!! warning \"版本一致性至关重要\"\n    **始终匹配 Chrome 版本**：确保 `profile.created_by_version` 和 `extensions.last_chrome_version` 与您的实际 Chrome 版本匹配。版本不匹配是一个立即的危险信号。\n    \n    ```python\n    # 以编程方式获取您的 Chrome 版本：\n    async with Chrome() as browser:\n        tab = await browser.start()\n        version = await browser.get_version()\n        chrome_version = version['product'].split('/')[1]  # 例如 '130.0.6723.91'\n        print(f\"使用此版本：{chrome_version}\")\n    ```\n\n!!! info \"指纹识别首选项的作用\"\n    **配置文件年龄**：`creation_time` 和 `last_engagement_time` 证明浏览器不是全新安装。\n    \n    **使用历史**：`recent_session_start_times` 显示定期浏览模式。\n    \n    **翻译历史**：`translate_accepted_count` 表明真实的人使用多种语言。\n    \n    **窗口放置**：与实际显示器分辨率匹配的真实屏幕尺寸。\n    \n    **隐私沙盒**：Google 的新跟踪系统。禁用它是不寻常的和可疑的。\n\n## 性能影响\n\n了解浏览器首选项的性能影响可以帮助您针对特定用例进行优化：\n\n| 首选项类别 | 预期影响 | 用例 |\n|---------------------|----------------|----------|\n| 禁用图像 | 加载速度提高 50-70% | 抓取文本内容 |\n| 禁用预取 | 加载速度提高 10-20% | 减少带宽使用 |\n| 禁用插件 | 加载速度提高 5-10% | 安全性和性能 |\n| 阻止通知 | 消除弹出窗口 | 干净的自动化 |\n| 静默下载 | 消除提示 | 自动化文件下载 |\n\n!!! tip \"速度与隐身的权衡\"\n    **追求速度**：禁用图像、预取、插件和拼写检查。\n    \n    **追求隐身**：启用安全浏览、自动填充、搜索建议和 DNS 预取（即使它们会减慢速度）。\n    \n    **平衡方法**：启用隐身功能但禁用图像和插件。这可以提供 40-50% 的加速，同时保持真实的指纹。\n\n## 另请参阅\n\n- **[深入探讨：浏览器首选项](../../deep-dive/browser-preferences.md)** - 架构细节和内部原理\n- **[页面加载状态](page-load-state.md)** - 控制何时认为页面已加载\n- **[代理配置](proxy.md)** - 配置网络代理\n- **[Cookie 与会话](../browser-management/cookies-sessions.md)** - 管理浏览器状态\n- **[Chromium 源代码：pref_names.cc](https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.cc)** - 官方首选项常量\n- **[Chromium 源代码：pref_names.h](https://github.com/chromium/chromium/blob/main/chrome/common/pref_names.h)** - 带有定义的头文件\n\n自定义浏览器首选项为您提供了对浏览器行为的前所未有的控制，使复杂的自动化、性能优化和隐私配置成为可能，而这些在传统自动化工具中是根本不可能实现的。这种访问级别将 Pydoll 从一个简单的自动化库转变为一个完整的浏览器控制系统。\n"
  },
  {
    "path": "docs/zh/features/configuration/proxy.md",
    "content": "# 代理配置\n\n代理对于专业的 Web 自动化至关重要，它可以帮助你绕过速率限制、访问地理限制内容并保持匿名性。Pydoll 提供原生代理支持，并具有自动身份验证处理功能。\n\n!!! info \"相关文档\"\n    - **[浏览器选项](browser-options.md)** - 命令行代理参数\n    - **[请求拦截](../network/interception.md)** - 代理身份验证的内部工作原理\n    - **[隐蔽自动化](../automation/human-interactions.md)** - 将代理与反检测结合使用\n    - **[代理架构深入解析](../../deep-dive/proxy-architecture.md)** - 网络基础知识、协议、安全性和构建自己的代理\n\n## 为什么使用代理？\n\n代理为自动化提供了关键功能：\n\n| 优势 | 描述 | 用例 |\n|------|------|------|\n| **IP 轮换** | 在多个 IP 之间分配请求 | 避免速率限制，大规模抓取 |\n| **地理访问** | 访问区域锁定内容 | 测试地理定向功能，绕过限制 |\n| **匿名性** | 隐藏真实 IP 地址 | 注重隐私的自动化，竞争对手分析 |\n| **负载分配** | 将流量分散到多个端点 | 大容量抓取，压力测试 |\n| **避免封禁** | 防止永久 IP 封禁 | 长期运行的自动化，激进抓取 |\n\n!!! tip \"何时使用代理\"\n    **始终使用代理：**\n    \n    - 生产环境 Web 抓取（>100 请求/小时）\n    - 访问地理限制内容\n    - 绕过速率限制或基于 IP 的封锁\n    - 从不同地区进行测试\n    - 保持匿名性\n    \n    **可以跳过代理：**\n    \n    - 本地开发和测试\n    - 内部/企业自动化\n    - 低容量自动化（<50 请求/天）\n    - 抓取自己的基础设施时\n\n## 代理类型\n\n不同的代理协议适用于不同的目的：\n\n| 类型 | 端口 | 身份验证 | 速度 | 安全性 | 用例 |\n|------|------|---------|------|--------|------|\n| **HTTP** | 80, 8080 | 可选 | 快速 | 低 | 基本 Web 抓取，非敏感数据 |\n| **HTTPS** | 443, 8443 | 可选 | 快速 | 中等 | 安全 Web 抓取，加密流量 |\n| **SOCKS5** | 1080, 1081 | 可选 | 中等 | 高 | 完整 TCP/UDP 支持，高级用例 |\n\n### HTTP/HTTPS 代理\n\n标准 Web 代理，适用于大多数自动化任务：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def http_proxy_example():\n    options = ChromiumOptions()\n    \n    # HTTP proxy (unencrypted)\n    options.add_argument('--proxy-server=http://proxy.example.com:8080')\n    \n    # Or HTTPS proxy (encrypted)\n    # options.add_argument('--proxy-server=https://proxy.example.com:8443')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # All traffic goes through proxy\n        await tab.go_to('https://httpbin.org/ip')\n        \n        # Verify proxy IP\n        ip = await tab.execute_script('return document.body.textContent')\n        print(f\"Current IP: {ip}\")\n\nasyncio.run(http_proxy_example())\n```\n\n**优点：**\n\n- 快速高效\n- 在各种服务中广泛支持\n- 易于配置\n\n**缺点：**\n\n- HTTP：无加密（流量对代理可见）\n- 比 SOCKS5 更容易被检测\n\n### SOCKS5 代理\n\n支持完整 TCP/UDP 的高级代理：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def socks5_proxy_example():\n    options = ChromiumOptions()\n    \n    # SOCKS5 proxy\n    options.add_argument('--proxy-server=socks5://proxy.example.com:1080')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://httpbin.org/ip')\n\nasyncio.run(socks5_proxy_example())\n```\n\n**优点：**\n\n- 协议无关（适用于任何 TCP/UDP 流量）\n- 更适合高级用例（WebSockets、WebRTC）\n- 更隐蔽（更难检测）\n\n**缺点：**\n\n- 比 HTTP/HTTPS 稍慢\n- 在免费/廉价代理服务中不太常见\n\n!!! info \"SOCKS4 vs SOCKS5\"\n    推荐使用 **SOCKS5** 而不是 SOCKS4，因为它：\n    \n    - 支持身份验证（用户名/密码）\n    - 处理 UDP 流量（用于 WebRTC、DNS 等）\n    - 提供更好的错误处理\n    \n    除非你特别需要 SOCKS4（`socks4://`），否则使用 `socks5://`。\n\n## 身份验证代理\n\nPydoll 自动处理代理身份验证，无需手动干预。\n\n### 身份验证工作原理\n\n当你在代理 URL 中提供凭据时，Pydoll 会：\n\n1. **拦截身份验证挑战** 使用 Fetch 域\n2. **自动响应** 提供凭据\n3. **继续导航** 无缝衔接\n\n这一切都是透明的，你无需手动处理身份验证！\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def authenticated_proxy_example():\n    options = ChromiumOptions()\n    \n    # Proxy with authentication (username:password)\n    options.add_argument('--proxy-server=http://user:pass@proxy.example.com:8080')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Authentication handled automatically!\n        await tab.go_to('https://example.com')\n        print(\"Connected through authenticated proxy\")\n\nasyncio.run(authenticated_proxy_example())\n```\n\n!!! tip \"凭据格式\"\n    直接在代理 URL 中包含凭据：\n\n    - HTTP: `http://username:password@host:port`\n    - HTTPS: `https://username:password@host:port`\n    - SOCKS5: `socks5://username:password@host:port`\n\n    Pydoll 会自动提取并使用这些凭据。\n\n!!! warning \"SOCKS5 身份验证限制\"\n    **Chrome 不原生支持 SOCKS5 身份验证**（[Chromium Issue #40323993](https://issues.chromium.org/issues/40323993)）。嵌入在 `socks5://user:pass@host:port` 中的凭据会被静默忽略 — Chrome 只会向 SOCKS5 代理发送\"无需身份验证\"的问候。\n\n    这意味着 Pydoll 的自动代理身份验证（通过 `Fetch.authRequired`）**对 SOCKS5 不起作用**，因为 Chrome 从不会为 SOCKS5 连接发出 HTTP 407 质询。\n\n    **解决方案 — 本地代理转发器：**\n\n    运行一个本地 SOCKS5 代理（无需身份验证），将流量转发到远程的身份验证代理。Pydoll 提供了一个即用脚本：\n\n    ```python\n    import asyncio\n    from pydoll.utils import SOCKS5Forwarder\n    from pydoll.browser.chromium import Chrome\n    from pydoll.browser.options import ChromiumOptions\n\n    async def main():\n        forwarder = SOCKS5Forwarder(\n            remote_host='proxy.example.com',\n            remote_port=1080,\n            username='myuser',\n            password='mypass',\n            local_port=1081,\n        )\n        async with forwarder:\n            options = ChromiumOptions()\n            options.add_argument('--proxy-server=socks5://127.0.0.1:1081')\n\n            async with Chrome(options=options) as browser:\n                tab = await browser.start()\n                await tab.go_to('https://httpbin.org/ip')\n\n    asyncio.run(main())\n    ```\n\n    转发器负责与远程代理进行用户名/密码握手，而 Chrome 无需身份验证即可连接到本地主机。\n\n    有关此问题的完整技术解释，请参阅 **[SOCKS5 身份验证深入解析](../../deep-dive/network/socks-proxies.md#socks5-身份验证与-chrome)**。\n\n### 身份验证实现细节\n\nPydoll 在浏览器级别使用 Chrome 的 **Fetch 域** 来拦截和处理身份验证挑战：\n\n```python\n# 这是 Pydoll 内部处理的\n# 你不需要编写这段代码！\n\nasync def _handle_proxy_auth(event):\n    \"\"\"Pydoll 的内部代理身份验证处理器。\"\"\"\n    if event['params']['authChallenge']['source'] == 'Proxy':\n        await browser.continue_request_with_auth(\n            request_id=event['params']['requestId'],\n            username='user',\n            password='pass'\n        )\n```\n\n!!! info \"底层原理\"\n    有关 Pydoll 如何拦截和处理代理身份验证的技术细节，请参阅：\n    \n    - **[请求拦截](../network/interception.md)** - Fetch 域和请求处理\n    - **[事件系统](../advanced/event-system.md)** - 事件驱动的身份验证\n\n!!! warning \"Fetch 域冲突\"\n    当使用**身份验证代理** + **标签页级别请求拦截**时，请注意：\n    \n    - Pydoll 在**浏览器级别**启用 Fetch 以进行代理身份验证\n    - 如果在**标签页级别**启用 Fetch，它们共享同一个域\n    - **解决方案**：在启用标签页级别拦截之前调用一次 `tab.go_to()`\n    \n    ```python\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # 1. 首次导航触发代理身份验证（浏览器级别 Fetch）\n        await tab.go_to('https://example.com')\n        \n        # 2. 然后安全地启用标签页级别拦截\n        await tab.enable_fetch_events()\n        await tab.on('Fetch.requestPaused', my_interceptor)\n        \n        # 3. 继续自动化\n        await tab.go_to('https://example.com/page2')\n    ```\n    \n    详细信息请参阅 [请求拦截 - 代理 + 拦截](../network/interception.md#private-proxy-request-interception-fetch)。\n\n## 代理绕过列表\n\n从使用代理中排除特定域：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def proxy_bypass_example():\n    options = ChromiumOptions()\n    \n    # Use proxy for most traffic\n    options.add_argument('--proxy-server=http://proxy.example.com:8080')\n    \n    # But bypass proxy for these domains\n    options.add_argument('--proxy-bypass-list=localhost,127.0.0.1,*.local,internal.company.com')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Uses proxy\n        await tab.go_to('https://external-site.com')\n        \n        # Bypasses proxy (direct connection)\n        await tab.go_to('http://localhost:8000')\n        await tab.go_to('http://internal.company.com')\n\nasyncio.run(proxy_bypass_example())\n```\n\n**绕过列表模式：**\n\n| 模式 | 匹配 | 示例 |\n|------|------|------|\n| `localhost` | 仅本地主机 | `http://localhost` |\n| `127.0.0.1` | 回环 IP | `http://127.0.0.1` |\n| `*.local` | 所有 `.local` 域 | `http://server.local` |\n| `internal.company.com` | 特定域 | `http://internal.company.com` |\n| `192.168.1.*` | IP 范围 | `http://192.168.1.100` |\n\n!!! tip \"何时使用绕过列表\"\n    为以下情况绕过代理：\n    \n    - **本地开发服务器**（`localhost`、`127.0.0.1`）\n    - **公司内部资源**（VPN、内网）\n    - **测试环境**（`.local`、`.test` 域）\n    - **高带宽资源**（当代理较慢时）\n\n## PAC（代理自动配置）\n\n使用 PAC 文件实现复杂的代理路由规则：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def pac_proxy_example():\n    options = ChromiumOptions()\n    \n    # Load PAC file from URL\n    options.add_argument('--proxy-pac-url=http://proxy.example.com/proxy.pac')\n    \n    # Or use local PAC file\n    # options.add_argument('--proxy-pac-url=file:///path/to/proxy.pac')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n\nasyncio.run(pac_proxy_example())\n```\n\n**Example PAC file:**\n\n```javascript\nfunction FindProxyForURL(url, host) {\n    // Direct connection for local addresses\n    if (isInNet(host, \"192.168.0.0\", \"255.255.0.0\") ||\n        isInNet(host, \"127.0.0.0\", \"255.0.0.0\")) {\n        return \"DIRECT\";\n    }\n    \n    // Use specific proxy for certain domains\n    if (dnsDomainIs(host, \".example.com\")) {\n        return \"PROXY proxy1.example.com:8080\";\n    }\n    \n    // Default proxy for everything else\n    return \"PROXY proxy2.example.com:8080\";\n}\n```\n\n!!! info \"PAC 文件用例\"\n    PAC 文件适用于：\n    \n    - **复杂路由规则**（基于域名、基于 IP）\n    - **代理故障转移**（尝试多个代理）\n    - **负载均衡**（在代理池中分配）\n    - **企业环境**（集中式代理管理）\n\n## 轮换代理\n\n轮换使用多个代理以实现更好的分配：\n\n```python\nimport asyncio\nfrom itertools import cycle\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def rotating_proxy_example():\n    # List of proxies\n    proxies = [\n        'http://user:pass@proxy1.example.com:8080',\n        'http://user:pass@proxy2.example.com:8080',\n        'http://user:pass@proxy3.example.com:8080',\n    ]\n    \n    # Cycle through proxies\n    proxy_pool = cycle(proxies)\n    \n    # Scrape multiple URLs with different proxies\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n    ]\n    \n    for url in urls:\n        # Get next proxy\n        proxy = next(proxy_pool)\n        \n        # Configure options with this proxy\n        options = ChromiumOptions()\n        options.add_argument(f'--proxy-server={proxy}')\n        \n        # Use proxy for this browser instance\n        async with Chrome(options=options) as browser:\n            tab = await browser.start()\n            await tab.go_to(url)\n            \n            title = await tab.execute_script('return document.title')\n            print(f\"[{proxy.split('@')[1]}] {url}: {title}\")\n\nasyncio.run(rotating_proxy_example())\n```\n\n!!! tip \"代理轮换策略\"\n    **每个浏览器轮换**（如上）：\n\n    - 每个浏览器实例使用不同的代理\n    - 最适合隔离和避免会话冲突\n    \n    **每个请求轮换**：\n\n    - 更复杂，需要请求拦截\n    - 实现方式请参阅 [请求拦截](../network/interception.md)\n\n## 住宅代理 vs 数据中心代理\n\n理解代理类型有助于你选择正确的服务：\n\n| 特性 | 住宅代理 | 数据中心代理 |\n|------|---------|-------------|\n| **IP 来源** | 真实住宅 ISP | 数据中心 |\n| **合法性** | 高（真实用户） | 低（已知范围） |\n| **检测风险** | 非常低 | 高 |\n| **速度** | 中等（150-500ms） | 非常快（<50ms） |\n| **成本** | 昂贵（$5-15/GB） | 便宜（$0.10-1/GB） |\n| **最适合** | 反机器人网站、电商 | API、内部工具 |\n\n### 住宅代理\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def residential_proxy_example():\n    \"\"\"Use residential proxy for anti-bot sites.\"\"\"\n    options = ChromiumOptions()\n    \n    # Residential proxy with high trust score\n    options.add_argument('--proxy-server=http://user:pass@residential.proxy.com:8080')\n    \n    # Combine with stealth options\n    options.add_argument('--disable-blink-features=AutomationControlled')\n    options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Access protected site\n        await tab.go_to('https://protected-site.com')\n        print(\"Successfully accessed through residential proxy\")\n\nasyncio.run(residential_proxy_example())\n```\n\n**何时使用住宅代理：**\n\n- 具有强大反机器人保护的网站（Cloudflare、DataDome）\n- 电商抓取（Amazon、eBay 等）\n- 社交媒体自动化\n- 金融服务\n- 任何主动封锁数据中心 IP 的网站\n\n### 数据中心代理\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def datacenter_proxy_example():\n    \"\"\"Use fast datacenter proxy for APIs and unprotected sites.\"\"\"\n    options = ChromiumOptions()\n    \n    # Fast datacenter proxy\n    options.add_argument('--proxy-server=http://user:pass@datacenter.proxy.com:8080')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        \n        # Fast API scraping\n        await tab.go_to('https://api.example.com/data')\n\nasyncio.run(datacenter_proxy_example())\n```\n\n**何时使用数据中心代理：**\n\n- 无速率限制的公共 API\n- 内部/企业自动化\n- 没有反机器人措施的网站\n- 大容量、速度关键的抓取\n- 开发和测试\n\n!!! warning \"代理质量很重要\"\n    **劣质代理**带来的问题比解决的问题更多：\n    \n    - 响应时间慢（超时）\n    - 连接失败（错误率高）\n    - IP 被列入黑名单（立即封禁）\n    - 真实 IP 泄露（隐私泄露）\n    \n    **投资高质量代理**，选择信誉良好的提供商。免费代理几乎从不值得使用。\n\n## 测试你的代理\n\n在运行生产环境自动化之前验证代理配置：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def test_proxy():\n    \"\"\"Test proxy connection and configuration.\"\"\"\n    proxy_url = 'http://user:pass@proxy.example.com:8080'\n    \n    options = ChromiumOptions()\n    options.add_argument(f'--proxy-server={proxy_url}')\n    \n    try:\n        async with Chrome(options=options) as browser:\n            tab = await browser.start()\n            \n            # Test 1: Connection\n            print(\"Testing proxy connection...\")\n            await tab.go_to('https://httpbin.org/ip', timeout=10)\n            \n            # Test 2: IP verification\n            print(\"Verifying proxy IP...\")\n            ip_response = await tab.execute_script('return document.body.textContent')\n            print(f\"[OK] Proxy IP: {ip_response}\")\n            \n            # Test 3: Geographic location (if available)\n            await tab.go_to('https://ipapi.co/json/')\n            geo_data = await tab.execute_script('return document.body.textContent')\n            print(f\"[OK] Geographic data: {geo_data}\")\n            \n            # Test 4: Speed test\n            import time\n            start = time.time()\n            await tab.go_to('https://example.com')\n            load_time = time.time() - start\n            print(f\"[OK] Load time: {load_time:.2f}s\")\n            \n            if load_time > 5:\n                print(\"[WARNING] Slow proxy response time\")\n            \n            print(\"\\n[SUCCESS] All proxy tests passed!\")\n            \n    except asyncio.TimeoutError:\n        print(\"[ERROR] Proxy connection timeout\")\n    except Exception as e:\n        print(f\"[ERROR] Proxy test failed: {e}\")\n\nasyncio.run(test_proxy())\n```\n\n## 延伸阅读\n\n- **[代理架构深入解析](../../deep-dive/proxy-architecture.md)** - 网络基础知识、TCP/UDP、HTTP/2/3、SOCKS5 内部原理、安全分析以及构建自己的代理服务器\n- **[浏览器选项](browser-options.md)** - 命令行参数和配置\n- **[请求拦截](../network/interception.md)** - 代理身份验证工作原理\n- **[浏览器首选项](browser-preferences.md)** - 隐蔽性和指纹识别\n- **[上下文](../browser-management/contexts.md)** - 每个上下文使用不同的代理\n\n!!! tip \"从简单开始\"\n    从简单的代理设置开始，彻底测试，然后根据需要添加复杂性（轮换、重试逻辑、监控）。高质量的代理比复杂的轮换策略更重要。\n    \n    对于那些有兴趣深入了解代理的人，**[代理架构深入解析](../../deep-dive/proxy-architecture.md)** 提供了网络协议、安全注意事项的全面介绍，甚至指导你构建自己的代理服务器。\n"
  },
  {
    "path": "docs/zh/features/core-concepts.md",
    "content": "# 核心概念\n\n理解是什么使 Pydoll 与众不同，要从其基础设计决策开始。这些不仅仅是技术选择，它们直接影响您如何编写自动化脚本、可以解决什么问题，以及解决方案的可靠性。\n\n## 零 WebDriver\n\nPydoll 最显著的优势之一是完全消除了 WebDriver 依赖。如果您曾经遇到过\"chromedriver 版本与 Chrome 版本不匹配\"错误，或处理过神秘的驱动程序崩溃，您会欣赏这种方法。\n\n### 工作原理\n\n像 Selenium 这样的传统浏览器自动化工具依赖于 WebDriver 可执行文件，它充当代码和浏览器之间的中介。Pydoll 采用不同的路径，通过 Chrome DevTools Protocol (CDP) 直接连接到浏览器。\n\n```mermaid\ngraph LR\n    %% Pydoll 流程\n    subgraph P[\"Pydoll 流程\"]\n        direction LR\n        P1[\"💻 您的代码\"] --> P2[\"🪄 Pydoll\"]\n        P2 --> P3[\"🌐 浏览器 (通过 CDP)\"]\n    end\n\n    %% 传统 Selenium 流程\n    subgraph S[\"传统 Selenium 流程\"]\n        direction LR\n        S1[\"💻 您的代码\"] --> S2[\"🔌 WebDriver 客户端\"]\n        S2 --> S3[\"⚙️ WebDriver 可执行文件\"]\n        S3 --> S4[\"🌐 浏览器\"]\n    end\n\n```\n\n当您使用 Pydoll 启动浏览器时，底层发生的事情如下：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    # 这会创建一个 Browser 实例\n    browser = Chrome()\n    \n    # start() 使用 --remote-debugging-port 启动 Chrome\n    # 并建立到 CDP 端点的 WebSocket 连接\n    tab = await browser.start()\n    \n    # 现在您可以通过 CDP 命令控制浏览器\n    await tab.go_to('https://example.com')\n    \n    await browser.stop()\n\nasyncio.run(main())\n```\n\n在幕后，`browser.start()` 执行以下操作：\n\n1. **使用** `--remote-debugging-port=<port>` 标志**启动浏览器进程**\n2. **等待 CDP 服务器**在该端口上可用\n3. **建立 WebSocket 连接**到 `ws://localhost:<port>/devtools/...`\n4. **返回准备好自动化的 Tab 实例**\n\n!!! info \"想了解更多？\"\n    有关浏览器进程如何在内部管理的技术细节，请参阅[浏览器域](../../deep-dive/browser-domain.md#browser-process-manager)深入探讨。\n\n### 您会注意到的好处\n\n**没有版本管理的烦恼**\n```python\n# 使用 Selenium，您可能会看到：\n# SessionNotCreatedException: This version of ChromeDriver only supports Chrome version 120\n\n# 使用 Pydoll，您只需要安装 Chrome：\nasync with Chrome() as browser:\n    tab = await browser.start()  # 适用于任何 Chrome 版本\n```\n\n**更简单的设置**\n```bash\n# Selenium 设置：\n$ pip install selenium\n$ brew install chromedriver  # 或下载、chmod +x、添加到 PATH...\n$ chromedriver --version     # 它与您的 Chrome 匹配吗？\n\n# Pydoll 设置：\n$ pip install pydoll-python  # 就这样！\n```\n\n**更可靠**\n\n没有 WebDriver 作为中间层，失败点更少。您的代码通过 Chromium 开发人员自己使用和维护的定义良好的协议直接与浏览器通信。\n\n### CDP：魔法背后的协议\n\nChrome DevTools Protocol 不仅适用于 Pydoll；当您打开检查器时，它是为 Chrome DevTools 提供动力的相同协议。这意味着：\n\n- **经过实战检验的可靠性**：每天被数百万开发人员使用\n- **丰富的功能**：DevTools 能做的一切，Pydoll 都能做\n- **积极开发**：Google 持续维护和发展 CDP\n\n!!! tip \"深入探讨：理解 CDP\"\n    要全面了解 CDP 的工作原理以及为什么它优于 WebDriver，请参阅我们的 [Chrome DevTools Protocol](../../deep-dive/cdp.md) 深入探讨。\n\n## 异步优先架构\n\nPydoll 不仅仅是异步兼容；它从头开始设计以利用 Python 的 `asyncio` 框架。这不是一个复选框功能；它是 Pydoll 如何实现高性能的基础。\n\n!!! info \"异步编程新手？\"\n    如果您不熟悉 Python 的 `async`/`await` 语法或 asyncio 概念，我们强烈建议首先阅读我们的[理解 Async/Await](../../deep-dive/connection-layer.md#understanding-asyncawait) 指南。它用实际示例解释了基础知识，将帮助您理解 Pydoll 的异步架构如何工作以及为什么它对浏览器自动化如此强大。\n\n### 为什么异步对浏览器自动化很重要\n\n浏览器自动化涉及大量等待：页面加载、元素出现、网络请求完成。传统的同步工具在这些等待期间浪费 CPU 时间。异步架构让您在等待时做有用的工作。\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_page(browser, url):\n    \"\"\"抓取单个页面。\"\"\"\n    tab = await browser.new_tab()\n    await tab.go_to(url)\n    title = await tab.execute_script('return document.title')\n    await tab.close()\n    return title\n\nasync def main():\n    urls = [\n        'https://example.com/page1',\n        'https://example.com/page2',\n        'https://example.com/page3',\n    ]\n    \n    async with Chrome() as browser:\n        await browser.start()\n        \n        # 并发处理所有 URL！\n        titles = await asyncio.gather(\n            *(scrape_page(browser, url) for url in urls)\n        )\n        \n        print(titles)\n\nasyncio.run(main())\n```\n\n在这个例子中，不是一个接一个地抓取页面（可能需要 3 × 2 秒 = 6 秒），而是并发抓取所有三个页面，总共大约需要 2 秒。\n\n### 真正的并发与线程\n\n与基于线程的方法不同，Pydoll 的异步架构提供真正的并发执行，而无需线程管理的复杂性：\n\n```mermaid\nsequenceDiagram\n    participant Main as 主任务\n    participant Tab1 as 标签页 1\n    participant Tab2 as 标签页 2\n    participant Tab3 as 标签页 3\n    \n    Main->>Tab1: go_to(url1)\n    Main->>Tab2: go_to(url2)\n    Main->>Tab3: go_to(url3)\n    \n    Note over Tab1,Tab3: 所有标签页并发导航\n    \n    Tab1-->>Main: 页面 1 已加载\n    Tab2-->>Main: 页面 2 已加载\n    Tab3-->>Main: 页面 3 已加载\n    \n    Main->>Main: 处理结果\n```\n\n### 现代 Python 模式\n\nPydoll 在整个过程中采用现代 Python 习语：\n\n**上下文管理器**\n```python\n# 自动资源清理\nasync with Chrome() as browser:\n    tab = await browser.start()\n    # ... 执行工作 ...\n# 退出上下文时浏览器自动停止\n```\n\n**操作的异步上下文管理器**\n```python\n# 等待和处理下载\nasync with tab.expect_download(keep_file_at='/downloads') as dl:\n    await (await tab.find(text='Download PDF')).click()\n    pdf_data = await dl.read_bytes()\n```\n\n!!! tip \"深入探讨\"\n    想了解异步操作在底层如何工作？查看[连接层](../../deep-dive/connection-layer.md)深入探讨以获取实现细节。\n\n### 性能影响\n\n异步优先设计提供了可衡量的性能改进：\n\n```python\nimport asyncio\nimport time\nfrom pydoll.browser.chromium import Chrome\n\nasync def benchmark_concurrent():\n    \"\"\"并发抓取 10 个页面。\"\"\"\n    async with Chrome() as browser:\n        await browser.start()\n        \n        start = time.time()\n        tasks = [\n            browser.new_tab(f'https://example.com/page{i}')\n            for i in range(10)\n        ]\n        await asyncio.gather(*tasks)\n        elapsed = time.time() - start\n        \n        print(f\"10 个页面在 {elapsed:.2f}s 内加载完成\")\n        # 典型结果：约 2-3 秒，而不是顺序执行的 20+ 秒\n\nasyncio.run(benchmark_concurrent())\n```\n\n## 多浏览器支持\n\nPydoll 为所有基于 Chromium 的浏览器提供统一的 API。编写一次自动化，随处运行。\n\n### 支持的浏览器\n\n**Google Chrome**：主要目标，具有完整的功能支持。\n```python\nfrom pydoll.browser.chromium import Chrome\n\nasync with Chrome() as browser:\n    tab = await browser.start()\n```\n\n**Microsoft Edge**：完全支持，包括 Edge 特定功能。\n```python\nfrom pydoll.browser.chromium import Edge\n\nasync with Edge() as browser:\n    tab = await browser.start()\n```\n\n**其他 Chromium 浏览器**：Brave、Vivaldi、Opera 等。\n```python\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\noptions = ChromiumOptions()\noptions.binary_location = '/path/to/brave-browser'  # 或任何 Chromium 浏览器\n\nasync with Chrome(options=options) as browser:\n    tab = await browser.start()\n```\n\n关键好处：所有基于 Chromium 的浏览器共享相同的 API。编写一次自动化，它就可以在 Chrome、Edge、Brave 或任何其他 Chromium 浏览器上运行，无需更改代码。\n\n### 跨浏览器测试\n\n在多个浏览器中测试您的自动化而无需更改代码：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome, Edge\n\nasync def test_login(browser_class, browser_name):\n    \"\"\"在特定浏览器中测试登录流程。\"\"\"\n    async with browser_class() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://app.example.com/login')\n        \n        await (await tab.find(id='username')).type_text('user@example.com')\n        await (await tab.find(id='password')).type_text('password123')\n        await (await tab.find(id='login-btn')).click()\n        \n        # 验证登录成功\n        success = await tab.find(id='dashboard', raise_exc=False)\n        print(f\"{browser_name} 登录: {'✓' if success else '✗'}\")\n\nasync def main():\n    # 在 Chrome 和 Edge 中测试\n    await test_login(Chrome, \"Chrome\")\n    await test_login(Edge, \"Edge\")\n\nasyncio.run(main())\n```\n\n## 类人行为\n\n自动化浏览器通常可被检测到，因为它们的行为很机械。Pydoll 包含内置功能，使交互看起来更像人类。\n\n### 自然打字\n\n真实用户不会以完全一致的速度打字。Pydoll 的 `type_text()` 方法包括按键之间的随机延迟：\n\n```python\n# 以类人的时间打字\nusername_field = await tab.find(id='username')\nawait username_field.type_text(\n    'user@example.com',\n    interval=0.1  # 按键之间平均 100ms，带有随机化\n)\n\n# 更快的打字（仍然类人）\nawait username_field.type_text(\n    'user@example.com',\n    interval=0.05  # 更快但仍然有变化\n)\n\n# 即时（机械；仅在速度比隐蔽性更重要时使用）\nawait username_field.type_text(\n    'user@example.com',\n    interval=0\n)\n```\n\n`interval` 参数设置平均延迟，但 Pydoll 添加随机变化以使时间更自然。\n\n### 真实的点击\n\n点击不仅仅是\"触发即忘\"。Pydoll 自动分发真实用户会触发的所有鼠标事件：\n\n```python\nbutton = await tab.find(id='submit-button')\n\n# 默认行为：点击元素中心\n# 自动触发：mouseover, mouseenter, mousemove, mousedown, mouseup, click\nawait button.click()\n\n# 带偏移点击（用于避免在较大元素上被检测）\nawait button.click(offset_x=10, offset_y=5)\n```\n\n!!! info \"鼠标事件\"\n    Pydoll 按正确顺序分发完整的鼠标事件序列，模拟真实浏览器如何处理用户点击。这使得点击比简单的 JavaScript `.click()` 调用更真实。\n\n!!! warning \"检测注意事项\"\n    虽然类人行为有助于避免基本的机器人检测，但复杂的反自动化系统使用许多信号。将这些功能与以下内容结合使用：\n    \n    - 真实的浏览器指纹（通过浏览器首选项）\n    - 适当的代理配置\n    - 操作之间的合理延迟\n    - 变化的导航模式\n\n## 事件驱动设计\n\n与传统的基于轮询的自动化不同，Pydoll 允许您在浏览器事件发生时做出反应。这更高效，并且可以实现复杂的交互模式。\n\n### 实时事件监控\n\n订阅浏览器事件并在它们触发时执行回调：\n\n```python\nimport asyncio\nfrom functools import partial\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.page.events import PageEvent\nfrom pydoll.protocol.network.events import NetworkEvent\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 响应页面加载事件\n        async def on_page_load(event):\n            print(f\"页面已加载: {await tab.current_url}\")\n        \n        await tab.enable_page_events()\n        await tab.on(PageEvent.LOAD_EVENT_FIRED, on_page_load)\n        \n        # 监控网络请求\n        async def on_request(tab, event):\n            url = event['params']['request']['url']\n            if '/api/' in url:\n                print(f\"API 调用: {url}\")\n        \n        await tab.enable_network_events()\n        await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, partial(on_request, tab))\n        \n        # 导航并观察事件触发\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)  # 让事件处理\n\nasyncio.run(main())\n```\n\n### 事件类别\n\nPydoll 公开了几个您可以订阅的 CDP 事件域：\n\n| 域 | 示例事件 |\n|--------|----------------|\n| **页面事件** | 加载完成、导航、JavaScript 对话框 |\n| **网络事件** | 请求发送、响应接收、WebSocket 活动 |\n| **DOM 事件** | DOM 更改、属性修改 |\n| **Fetch 事件** | 请求暂停、需要身份验证 |\n| **运行时事件** | 控制台消息、异常 |\n\n### 实用的事件驱动模式\n\n**捕获 API 响应**\n```python\nimport json\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent\n\napi_data = []\n\nasync def capture_api(tab, event):\n    url = event['params']['response']['url']\n    if '/api/data' in url:\n        request_id = event['params']['requestId']\n        body = await tab.get_network_response_body(request_id)\n        api_data.append(json.loads(body))\n\nawait tab.enable_network_events()\nawait tab.on(NetworkEvent.RESPONSE_RECEIVED, partial(capture_api, tab))\n\n# 导航并自动捕获 API 响应\nawait tab.go_to('https://app.example.com')\nawait asyncio.sleep(2)\n\nprint(f\"捕获了 {len(api_data)} 个 API 响应\")\n```\n\n**等待特定条件**\n```python\nimport asyncio\nfrom functools import partial\nfrom pydoll.protocol.network.events import NetworkEvent\n\nasync def wait_for_api_call(tab, endpoint):\n    \"\"\"等待调用特定的 API 端点。\"\"\"\n    event_occurred = asyncio.Event()\n    \n    async def check_endpoint(tab, event):\n        url = event['params']['request']['url']\n        if endpoint in url:\n            event_occurred.set()\n    \n    await tab.enable_network_events()\n    callback_id = await tab.on(\n        NetworkEvent.REQUEST_WILL_BE_SENT,\n        partial(check_endpoint, tab),\n        temporary=True  # 首次触发后自动移除\n    )\n\n    await event_occurred.wait()\n    print(f\"API 端点 {endpoint} 被调用！\")\n\n# 用法\nawait wait_for_api_call(tab, '/api/users')\n```\n\n!!! info \"深入探讨：事件系统详情\"\n    有关事件处理、回调模式和性能注意事项的综合指南，请参阅[事件系统](../../deep-dive/event-system.md)深入探讨。\n\n### 事件性能\n\n事件很强大但会带来开销。最佳实践：\n\n```python\n# ✓ 好：仅启用您需要的\nawait tab.enable_network_events()\n\n# ✗ 避免：不必要地启用所有事件\nawait tab.enable_page_events()\nawait tab.enable_network_events()\nawait tab.enable_dom_events()\nawait tab.enable_fetch_events()\nawait tab.enable_runtime_events()\n\n# ✓ 好：在回调中提前过滤\nasync def handle_request(event):\n    url = event['params']['request']['url']\n    if '/api/' not in url:\n        return  # 提前跳过非 API 请求\n    # 处理 API 请求...\n\n# ✓ 好：完成后禁用\nawait tab.disable_network_events()\n```\n\n## 将所有内容整合在一起\n\n这些核心概念共同创建了一个强大的自动化框架：\n\n```python\nimport asyncio\nimport json\nfrom functools import partial\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import NetworkEvent\nfrom pydoll.constants import Keys\n\nasync def advanced_scraping():\n    \"\"\"演示多个核心概念协同工作。\"\"\"\n    async with Chrome() as browser:  # 异步上下文管理器\n        tab = await browser.start()\n        \n        # 事件驱动：捕获 API 数据\n        api_responses = []\n        \n        async def capture_data(tab, event):\n            url = event['params']['response']['url']\n            if '/api/products' in url:\n                request_id = event['params']['requestId']\n                body = await tab.get_network_response_body(request_id)\n                api_responses.append(json.loads(body))\n        \n        await tab.enable_network_events()\n        await tab.on(NetworkEvent.RESPONSE_RECEIVED, partial(capture_data, tab))\n        \n        # 使用零 webdriver 的简单性导航\n        await tab.go_to('https://example.com/products')\n        \n        # 类人交互\n        search = await tab.find(id='search')\n        await search.type_text('laptop', interval=0.1)  # 自然打字\n        await search.press_keyboard_key(Keys.ENTER)\n        \n        # 等待 API 响应（异步效率）\n        await asyncio.sleep(2)\n        \n        print(f\"从 API 捕获了 {len(api_responses)} 个产品\")\n        return api_responses\n\n# 多浏览器支持：适用于 Chrome、Edge 等\nasyncio.run(advanced_scraping())\n```\n\n这些基础概念贯穿于 Pydoll 的所有其他部分。当您探索特定功能时，您会看到这些原则在起作用，共同创建可靠、高效和可维护的浏览器自动化。\n\n---\n\n## 下一步是什么？\n\n现在您了解了 Pydoll 的核心设计，您已准备好探索特定功能：\n\n- **[元素查找](element-finding.md)** - 学习 Pydoll 直观的元素定位 API\n- **[网络功能](../network/monitoring.md)** - 利用事件系统进行网络分析\n- **[浏览器管理](../browser-management/tabs.md)** - 使用异步模式进行并发操作\n\n要获得更深入的技术理解，请探索[深入探讨](../../deep-dive/index.md)部分。"
  },
  {
    "path": "docs/zh/features/element-finding.md",
    "content": "# 元素查找\n\n在网页上查找元素是浏览器自动化的基础。Pydoll 引入了一种革命性的、直观的方法，使元素定位比传统的基于选择器的方法更强大且更易于使用。\n\n## 为什么 Pydoll 的方法与众不同\n\n传统的浏览器自动化工具从一开始就强迫您使用 CSS 选择器和 XPath 表达式进行思考。Pydoll 颠覆了这一点：您使用自然的 HTML 属性描述您要查找的内容，Pydoll 会找出最佳的选择器策略。\n\n```python\n# 传统方法（其他工具）\nelement = driver.find_element(By.XPATH, \"//input[@type='email' and @name='username']\")\n\n# Pydoll 的方法\nelement = await tab.find(tag_name=\"input\", type=\"email\", name=\"username\")\n```\n\n两者找到相同的元素，但 Pydoll 的语法更清晰、更易维护、更不容易出错。\n\n### 元素查找方法概述\n\nPydoll 提供三种主要的元素查找方法：\n\n| 方法 | 使用场景 | 示例 |\n|--------|----------|---------|\n| **`find()`** | 您知道 HTML 属性 | `await tab.find(id=\"username\")` |\n| **`query()`** | 您有 CSS/XPath 选择器 | `await tab.query(\"div.content\")` |\n| **遍历** | 您想从已知元素探索 | `await element.get_children_elements()` |\n\n```mermaid\nflowchart LR\n    A[需要元素?] --> B{您有什么?}\n    B -->|HTML 属性| C[find 方法]\n    B -->|CSS/XPath| D[query 方法]\n    B -->|父元素| E[遍历]\n    \n    C --> F[WebElement]\n    D --> F\n    E --> G[WebElement 列表]\n```\n\n!!! info \"深入探讨：工作原理\"\n    好奇 Pydoll 如何在底层实现元素查找？查看 [FindElements Mixin](../deep-dive/find-elements-mixin.md) 文档，了解架构、性能优化和内部选择器策略。\n\n## find() 方法：自然元素选择\n\n`find()` 方法是您定位元素的主要工具。它接受常见的 HTML 属性作为参数，并自动构建最有效的选择器。\n\n### 基本用法\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def basic_finding():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # 按 ID 查找（最常见且最快）\n        username = await tab.find(id=\"username\")\n        \n        # 按类名查找\n        submit_button = await tab.find(class_name=\"btn-primary\")\n        \n        # 按标签名查找\n        first_paragraph = await tab.find(tag_name=\"p\")\n        \n        # 按 name 属性查找\n        email_field = await tab.find(name=\"email\")\n        \n        # 按文本内容查找\n        login_link = await tab.find(text=\"Login\")\n\nasyncio.run(basic_finding())\n```\n\n### 组合属性以提高精度\n\n`find()` 的真正威力来自组合多个属性来创建精确的选择器：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def precise_finding():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/form')\n        \n        # 组合标签名和类型\n        password_input = await tab.find(tag_name=\"input\", type=\"password\")\n        \n        # 组合标签、类和自定义属性\n        submit_button = await tab.find(\n            tag_name=\"button\",\n            class_name=\"btn\",\n            type=\"submit\"\n        )\n        \n        # 使用 data 属性\n        product_card = await tab.find(\n            tag_name=\"div\",\n            data_testid=\"product-card\",\n            data_category=\"electronics\"\n        )\n        \n        # 组合多个条件\n        specific_link = await tab.find(\n            tag_name=\"a\",\n            class_name=\"nav-link\",\n            href=\"/dashboard\"\n        )\n\nasyncio.run(precise_finding())\n```\n\n!!! info \"组合逻辑：AND\"\n    在 `find()` 中组合属性作为 AND 操作。元素必须匹配**所有**提供的属性。\n    \n    对于需要 OR 逻辑的更复杂场景 - 例如查找可能具有 `id` 或不同 `name` 的元素 - 正确的方法是链接多个 `find()` 调用，如\"完整示例\"部分所示。\n\n!!! tip \"属性命名约定\"\n    对于带连字符的属性名使用下划线。例如，`data-testid` 变成 `data_testid`，`aria-label` 变成 `aria_label`。Pydoll 会自动将它们转换为正确的格式。\n\n### find() 如何选择最佳策略\n\nPydoll 根据您提供的属性自动选择最有效的选择器：\n\n| 提供的属性 | 使用的策略 | 性能 |\n|---------------------|---------------|-------------|\n| 单个：`id` | `By.ID` | ⚡ 最快 |\n| 单个：`class_name` | `By.CLASS_NAME` | ⚡ 快 |\n| 单个：`name` | `By.NAME` | ⚡ 快 |\n| 单个：`tag_name` | `By.TAG_NAME` | ⚡ 快 |\n| 单个：`text` | `By.XPATH` | ⚡ 快 |\n| 多个属性 | XPath 表达式 | ✓ 高效 |\n\n```mermaid\nflowchart LR\n    A[find 属性] --> B{单个还是多个?}\n    B -->|单个| C[直接选择器]\n    B -->|多个| D[构建 XPath]\n    C --> E[快速执行]\n    D --> E\n```\n\n### 查找多个元素\n\n使用 `find_all=True` 获取所有匹配元素的列表：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def find_multiple():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/products')\n        \n        # 查找所有产品卡片\n        products = await tab.find(class_name=\"product-card\", find_all=True)\n        print(f\"找到 {len(products)} 个产品\")\n        \n        # 查找导航中的所有链接\n        nav_links = await tab.find(\n            tag_name=\"a\",\n            class_name=\"nav-link\",\n            find_all=True\n        )\n        \n        # 处理每个元素\n        for link in nav_links:\n            text = await link.text\n            href = await link.get_attribute(\"href\")\n            print(f\"链接: {text} → {href}\")\n\nasyncio.run(find_multiple())\n```\n\n### 等待动态元素\n\n现代 Web 应用程序动态加载内容。使用 `timeout` 等待元素出现：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def wait_for_elements():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/dashboard')\n        \n        # 等待最多 10 秒让元素出现\n        dynamic_content = await tab.find(\n            class_name=\"dynamic-content\",\n            timeout=10\n        )\n        \n        # 等待 AJAX 加载的数据\n        user_profile = await tab.find(\n            id=\"user-profile\",\n            timeout=15\n        )\n        \n        # 处理可能不会出现的元素\n        optional_banner = await tab.find(\n            class_name=\"promo-banner\",\n            timeout=3,\n            raise_exc=False  # 如果未找到则返回 None\n        )\n        \n        if optional_banner:\n            await optional_banner.click()\n        else:\n            print(\"没有促销横幅\")\n\nasyncio.run(wait_for_elements())\n```\n\n!!! warning \"超时最佳实践\"\n    使用合理的超时值。太短会错过加载缓慢的元素；太长会浪费时间等待不存在的元素。对于大多数动态内容，从 5-10 秒开始。\n\n## query() 方法：直接选择器访问\n\n对于喜欢传统选择器或需要更复杂选择逻辑的开发人员，`query()` 方法提供对 CSS 选择器和 XPath 表达式的直接访问。\n\n### CSS 选择器\n\nCSS 选择器速度快、广为人知，非常适合大多数用例：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def css_selector_examples():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # 简单选择器\n        main_nav = await tab.query(\"nav.main-menu\")\n        first_article = await tab.query(\"article:first-child\")\n        \n        # 属性选择器\n        submit_button = await tab.query(\"button[type='submit']\")\n        required_inputs = await tab.query(\"input[required]\", find_all=True)\n        \n        # 复杂选择器\n        nested = await tab.query(\"div.container > .content .item:nth-child(2)\")\n        \n        # 伪类\n        first_enabled_button = await tab.query(\"button:not([disabled])\")\n\nasyncio.run(css_selector_examples())\n```\n\n### XPath 表达式\n\nXPath 擅长复杂的关系和文本匹配：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def xpath_examples():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/table')\n        \n        # 文本匹配\n        button = await tab.query(\"//button[contains(text(), 'Submit')]\")\n        \n        # 导航到父元素\n        input_parent = await tab.query(\"//input[@name='email']/parent::div\")\n        \n        # 查找兄弟元素\n        label_input = await tab.query(\n            \"//label[text()='Email:']/following-sibling::input\"\n        )\n        \n        # 复杂的表格查询\n        edit_button = await tab.query(\n            \"//tr[td[text()='John Doe']]//button[@class='btn-edit']\"\n        )\n\nasyncio.run(xpath_examples())\n```\n\n!!! info \"CSS vs XPath：使用哪个？\"\n    有关在 CSS 选择器和 XPath 之间进行选择的综合指南，包括语法参考和实际示例，请参阅[选择器指南](../deep-dive/selectors-guide.md)。\n\n## DOM 遍历：子元素和兄弟元素\n\n有时您需要从已知的起点探索 DOM 树。Pydoll 提供专门的方法来遍历元素关系。\n\n### DOM 树结构\n\n理解 DOM 树结构有助于您选择正确的遍历方法：\n\n```mermaid\ngraph TB\n    Root[文档根]\n    Root --> Container[div id='container']\n    \n    Container --> Child1[div class='card']\n    Container --> Child2[div class='card']\n    Container --> Child3[div class='card']\n    \n    Child1 --> GrandChild1[h2 标题]\n    Child1 --> GrandChild2[p 描述]\n    Child1 --> GrandChild3[button 操作]\n    \n    Child2 --> GrandChild4[h2 标题]\n    Child2 --> GrandChild5[p 描述]\n    \n    Child3 --> GrandChild6[h2 标题]\n```\n\n### 获取子元素\n\n`get_children_elements()` 方法检索元素的后代：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def traverse_children():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/cards')\n        \n        # 获取容器\n        container = await tab.find(id=\"cards-container\")\n        \n        # 仅获取直接子元素（max_depth=1）\n        direct_children = await container.get_children_elements(max_depth=1)\n        print(f\"容器有 {len(direct_children)} 个直接子元素\")\n        \n        # 包括孙元素（max_depth=2）\n        descendants = await container.get_children_elements(max_depth=2)\n        print(f\"找到 {len(descendants)} 个元素，深度最多为 2 级\")\n        \n        # 按标签名过滤\n        links = await container.get_children_elements(\n            max_depth=3,\n            tag_filter=[\"a\"]\n        )\n        print(f\"在容器中找到 {len(links)} 个链接\")\n        \n        # 组合过滤器以获取特定元素\n        nav_links = await container.get_children_elements(\n            max_depth=2,\n            tag_filter=[\"a\", \"button\"]\n        )\n\nasyncio.run(traverse_children())\n```\n\n### 获取兄弟元素\n\n`get_siblings_elements()` 方法查找同一级别的元素：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def traverse_siblings():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/list')\n        \n        # 查找活动项\n        active_item = await tab.find(class_name=\"item-active\")\n        \n        # 获取所有兄弟元素（不包括 active_item 本身）\n        all_siblings = await active_item.get_siblings_elements()\n        print(f\"活动项有 {len(all_siblings)} 个兄弟元素\")\n        \n        # 按标签过滤兄弟元素\n        link_siblings = await active_item.get_siblings_elements(\n            tag_filter=[\"a\"]\n        )\n        \n        # 处理兄弟元素\n        for sibling in all_siblings:\n            text = await sibling.text\n            print(f\"兄弟元素: {text}\")\n\nasyncio.run(traverse_siblings())\n```\n\n!!! tip \"性能注意事项\"\n    对于大型树，DOM 遍历可能很昂贵。优先使用较浅的 `max_depth` 值和特定的 `tag_filter` 参数以最小化处理的节点数。对于深度嵌套的结构，考虑使用多个有针对性的 `find()` 调用，而不是单个深度遍历。\n\n## 在元素内查找元素\n\n一旦您有了一个元素，您可以使用相同的 `find()` 和 `query()` 方法在其范围内搜索。\n\n!!! warning \"重要：搜索深度行为\"\n    当您调用 `element.find()` 或 `element.query()` 时，Pydoll 会搜索**所有后代**（子元素、孙元素、曾孙元素等），而不仅仅是直接子元素。这是 `querySelector()` 的标准行为，符合大多数开发人员的期望。\n\n### 理解搜索范围\n\n```mermaid\ngraph TB\n    Container[div id='container']\n    \n    Container --> Child1[div class='card' ✓]\n    Container --> Child2[div class='card' ✓]\n    Container --> Child3[div class='other']\n    \n    Child1 --> GrandChild1[div class='card' ✓]\n    Child1 --> GrandChild2[p class='text']\n    \n    Child3 --> GrandChild3[div class='card' ✓]\n    Child3 --> GrandChild4[div class='card' ✓]\n```\n\n```python\n# 这会在树中找到所有 5 个 class='card' 的元素\n# （2 个直接子元素 + 3 个嵌套后代）\ncards = await container.find(class_name=\"card\", find_all=True)\nprint(len(cards))  # 输出：5\n```\n\n### 基本范围搜索\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scoped_search():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/products')\n        \n        # 查找产品容器\n        product_card = await tab.find(class_name=\"product-card\")\n        \n        # 在产品卡片内搜索（搜索所有后代，仅返回第一个匹配项）\n        product_title = await product_card.find(class_name=\"title\")\n        product_price = await product_card.find(class_name=\"price\")\n        add_button = await product_card.find(tag_name=\"button\", text=\"Add to Cart\")\n        \n        # 在范围内查询\n        product_image = await product_card.query(\"img.product-image\")\n        \n        # 查找容器内的所有项目（所有后代）\n        nav_menu = await tab.find(class_name=\"nav-menu\")\n        menu_items = await nav_menu.find(tag_name=\"li\", find_all=True)\n        \n        print(f\"菜单有 {len(menu_items)} 项\")\n\nasyncio.run(scoped_search())\n```\n\n### 仅查找直接子元素\n\n如果您需要查找**仅直接子元素**（深度 1），请使用 CSS 子组合器 `>` 或 XPath：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def direct_children_only():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/cards')\n        \n        container = await tab.find(id=\"cards-container\")\n        \n        # 方法 1：CSS 子组合器（>）\n        # 仅查找具有 class='card' 的直接子元素\n        direct_cards = await container.query(\"> .card\", find_all=True)\n        print(f\"直接子元素: {len(direct_cards)}\")\n        \n        # 方法 2：XPath 直接子元素\n        direct_divs = await container.query(\"./div[@class='card']\", find_all=True)\n        \n        # 方法 3：使用 max_depth=1 的 get_children_elements()\n        # （但这只按标签过滤，不按其他属性）\n        direct_children = await container.get_children_elements(\n            max_depth=1,\n            tag_filter=[\"div\"]\n        )\n        \n        # 然后按类手动过滤\n        cards_only = [\n            child for child in direct_children\n            if 'card' in (await child.get_attribute('class') or '')\n        ]\n\nasyncio.run(direct_children_only())\n```\n\n### 比较：find() vs get_children_elements()\n\n| 特性 | `find()` / `query()` | `get_children_elements()` |\n|---------|---------------------|---------------------------|\n| **搜索深度** | 所有后代 | 使用 `max_depth` 可配置 |\n| **过滤依据** | 任何 HTML 属性 | 仅标签名 |\n| **用例** | 在子树中的任何位置查找特定元素 | 探索 DOM 结构，获取直接子元素 |\n| **性能** | 针对单个属性优化 | 适合广泛探索 |\n| **参数** | `tag_name=\"a\"`（字符串） | `tag_filter=[\"a\"]`（列表） |\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def comparison_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        container = await tab.find(id=\"container\")\n        \n        # 场景 1：我想要容器中任何位置的所有链接\n        # 使用 find() - 搜索所有后代\n        all_links = await container.find(tag_name=\"a\", find_all=True)\n        \n        # 场景 2：我只想要直接子链接\n        # 使用 CSS 子组合器\n        direct_links = await container.query(\"> a\", find_all=True)\n        \n        # 场景 3：我想要具有特定类的直接子元素\n        # 使用 CSS 子组合器\n        direct_cards = await container.query(\"> .card\", find_all=True)\n        \n        # 场景 4：我想探索 DOM 结构\n        # 使用 get_children_elements()\n        direct_children = await container.get_children_elements(max_depth=1)\n        \n        # 场景 5：我想要深度最多为 2 的所有后代，按标签过滤\n        # 使用 get_children_elements()\n        shallow_links = await container.get_children_elements(\n            max_depth=2,\n            tag_filter=[\"a\"]\n        )\n\nasyncio.run(comparison_example())\n```\n\n!!! tip \"何时使用每种方法\"\n    - **使用 `find()`**：当您知道属性（class、id 等）并想搜索整个子树时\n    - **使用 `query(\"> .class\")`**：当您只需要具有特定属性的直接子元素时\n    - **使用 `get_children_elements()`**：当探索 DOM 结构或仅按标签过滤时\n\n### 常见用例\n\n这种范围搜索对于处理重复模式非常有用，例如：\n\n- 电子商务网站中的产品卡片\n- 具有多个单元格的表格行\n- 具有多个字段的表单部分\n- 具有嵌套项的导航菜单\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def practical_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/products')\n        \n        # 查找页面上的所有产品卡片\n        product_cards = await tab.find(class_name=\"product-card\", find_all=True)\n        \n        for card in product_cards:\n            # 在每个卡片内，查找具有这些类的所有后代\n            title = await card.find(class_name=\"product-title\")\n            price = await card.find(class_name=\"product-price\")\n            \n            # 获取此卡片内任何位置的按钮\n            buy_button = await card.find(tag_name=\"button\", text=\"Buy Now\")\n            \n            title_text = await title.text\n            price_text = await price.text\n            \n            print(f\"产品: {title_text}, 价格: {price_text}\")\n            \n            # 点击购买按钮\n            await buy_button.click()\n\nasyncio.run(practical_example())\n```\n\n## Shadow DOM 支持\n\n许多现代 Web 应用程序使用 [Shadow DOM](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_shadow_DOM) 来封装组件内部结构。Pydoll 通过 `ShadowRoot` 类提供对 shadow 树内元素的无缝访问。\n\n### Shadow DOM 的工作原理\n\n```mermaid\ngraph TB\n    Host[\"div#my-component (shadow host)\"]\n    SR[\"ShadowRoot (open)\"]\n    Internal1[\"button.internal-btn\"]\n    Internal2[\"input.internal-input\"]\n\n    Host --> SR\n    SR --> Internal1\n    SR --> Internal2\n```\n\nshadow root 内的元素对常规 DOM 查询是隐藏的。您需要先访问 shadow root，然后在其中进行搜索。\n\n### 访问 Shadow Root\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def shadow_dom_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/web-components')\n\n        # 查找 shadow host 元素\n        shadow_host = await tab.find(id='my-component')\n\n        # 访问其 shadow root\n        shadow_root = await shadow_host.get_shadow_root()\n\n        # 在 shadow root 内使用 query() 和 CSS 选择器查找元素\n        button = await shadow_root.query('.internal-btn')\n        await button.click()\n\n        input_field = await shadow_root.query('input[type=\"email\"]')\n        await input_field.type_text('user@example.com')\n\nasyncio.run(shadow_dom_example())\n```\n\n### 使用 query() 和 CSS 选择器\n\n`ShadowRoot` 继承自 `FindElementsMixin`，但带有 `_css_only` 限制，这意味着仅支持使用 CSS 选择器的 `query()`。`find()` 方法和使用 XPath 的 `query()` 会抛出 `NotImplementedError`：\n\n```python\n# query() 配合 CSS 选择器 — 推荐方法\nelement = await shadow_root.query('#inner-id')\nelement = await shadow_root.query('button.primary')\nelement = await shadow_root.query('div.container > .content')\n\n# find_all 查找多个元素\nitems = await shadow_root.query('.item', find_all=True)\n\n# 带超时的等待\nelement = await shadow_root.query('#dynamic', timeout=5)\n```\n\n!!! warning \"ShadowRoot 不支持 find() 和 XPath\"\n    调用 `shadow_root.find()` 或 `shadow_root.query('//xpath')` 会抛出 `NotImplementedError`。在 shadow root 中请始终使用带 CSS 选择器的 `query()`。\n\n### 嵌套 Shadow Root\n\nWeb 组件可以包含拥有自己 shadow root 的其他 Web 组件：\n\n```python\nasync def nested_shadow():\n    outer_host = await tab.find(tag_name='outer-component')\n    outer_shadow = await outer_host.get_shadow_root()\n\n    inner_host = await outer_shadow.query('inner-component')\n    inner_shadow = await inner_host.get_shadow_root()\n\n    deep_button = await inner_shadow.query('.deep-btn')\n    await deep_button.click()\n```\n\n### 查找 Shadow Root：find_shadow_roots()\n\n当您需要探索页面上存在哪些 shadow root（对调试或 Cloudflare 挑战等动态页面很有用）时，使用 `find_shadow_roots()`：\n\n```python\n# 查找页面上的所有 shadow root\nshadow_roots = await tab.find_shadow_roots()\n\nfor sr in shadow_roots:\n    print(f'模式: {sr.mode}, 宿主: {sr.host_element}')\n    # 在每个 shadow root 内搜索\n    btn = await sr.query('button', raise_exc=False)\n    if btn:\n        await btn.click()\n```\n\n#### 等待 Shadow Root：`timeout`\n\nShadow 宿主通常是异步注入的（例如 Cloudflare Turnstile 在 OOPIF 中加载）。使用 `timeout` 进行轮询直到 shadow root 出现：\n\n```python\n# 等待最多 10 秒让 shadow root 出现\nshadow_roots = await tab.find_shadow_roots(timeout=10)\n```\n\n元素上的 `get_shadow_root()` 方法也支持 `timeout`：\n\n```python\n# 等待元素的 shadow root 出现\nhost = await tab.find(id='my-component', timeout=5)\nshadow = await host.get_shadow_root(timeout=5)\n```\n\n#### 深度遍历：跨域 IFrame（OOPIF）\n\n默认情况下，`find_shadow_roots()` 仅遍历主文档的 DOM 树（包括通过 `contentDocument` 访问的同源 iframe，但**不包括**跨域 iframe）。传入 `deep=True` 以同时发现跨域 iframe（OOPIF）内的 shadow root：\n\n```python\n# 包含跨域 iframe 中的 shadow root（例如 Cloudflare Turnstile）\nshadow_roots = await tab.find_shadow_roots(deep=True, timeout=10)\n\nfor sr in shadow_roots:\n    print(f'模式: {sr.mode}, 宿主: {sr.host_element}')\n    # 在这些 shadow root 中找到的元素会自动通过\n    # 正确的 OOPIF 会话路由 CDP 命令\n    btn = await sr.query('input[type=\"checkbox\"]', raise_exc=False)\n    if btn:\n        await btn.click()\n```\n\n!!! tip \"何时使用 `deep=True`\"\n    在自动化包含跨域嵌入式组件的页面时使用 `deep=True`，例如 Cloudflare Turnstile 验证码、第三方支付表单或社交登录按钮。这些组件通常使用跨域 iframe，其中包含关闭的 shadow root。\n\n### Shadow Root 属性\n\n```python\nshadow_root = await element.get_shadow_root()\n\n# 检查 shadow root 模式（open、closed 或 user-agent）\nprint(shadow_root.mode)  # ShadowRootType.OPEN\n\n# 访问 host 元素\nhost = shadow_root.host_element\n\n# 获取 shadow root 内部 HTML\nhtml = await shadow_root.inner_html\n```\n\n!!! note \"关闭的 Shadow Root\"\n    关闭的 shadow root（`mode='closed'`）可以通过 CDP 访问，因为协议绕过了 JavaScript 限制。但是，某些浏览器内部的 shadow root（user-agent）可能具有有限的可访问性。\n\n## 使用 iFrame\n\n!!! info \"提供完整的 IFrame 指南\"\n    本节介绍用于元素查找的基本 iframe 交互。有关包括嵌套 iframe、CAPTCHA 处理、技术深入探讨和故障排除的综合指南，请参阅**[使用 IFrame](automation/iframes.md)**。\n\niFrame 在浏览器自动化中提出了特殊的挑战，因为它们具有单独的 DOM 上下文。Pydoll 使 iframe 交互无缝：\n\n### iFrame 上下文隔离\n\n```mermaid\nflowchart TB\n    Main[tab]\n    Frame[\"iframe WebElement\"]\n    Content[\"iframe 内部元素\"]\n\n    Main -->|\"find('iframe')\"| Frame\n    Frame -->|\"find('button#submit')\"| Content\n```\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def iframe_interaction():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com/page-with-iframe')\n\n        iframe = await tab.query(\"iframe.embedded-content\", timeout=10)\n\n        # WebElement 辅助方法会自动在 iframe 内执行\n        iframe_button = await iframe.find(tag_name=\"button\", class_name=\"submit\")\n        await iframe_button.click()\n\n        iframe_input = await iframe.find(id=\"captcha-input\")\n        await iframe_input.type_text(\"verification-code\")\n\n        # 如果还有内层 iframe，继续链式查找\n        inner_iframe = await iframe.find(tag_name=\"iframe\")\n        download_link = await inner_iframe.find(text=\"下载 PDF\")\n        await download_link.click()\n\nasyncio.run(iframe_interaction())\n```\n!!! note \"iframe 中的截图\"\n    `tab.take_screenshot()` 只能作用于顶层 target。想要截取 iframe 内容，请锁定 iframe 内部的某个元素，使用 `element.take_screenshot()`。\n\n## 错误处理策略\n\n健壮的自动化需要处理元素不存在或出现时间超过预期的情况。\n\n### 元素查找流程与错误处理\n\n```mermaid\nflowchart TB\n    Start[开始查找元素] --> Immediate[尝试立即查找]\n    \n    Immediate --> Found1{找到元素?}\n    Found1 -->|是| Return1[返回 WebElement]\n    Found1 -->|否 & timeout=0| Check1{raise_exc=True?}\n    Found1 -->|否 & timeout>0| Wait[开始等待循环]\n    \n    Check1 -->|是| Error1[抛出 ElementNotFound]\n    Check1 -->|否| ReturnNone[返回 None]\n    \n    Wait --> Sleep[等待 0.5 秒]\n    Sleep --> TryAgain[再次尝试查找]\n    TryAgain --> Found2{找到元素?}\n    \n    Found2 -->|是| Return2[返回 WebElement]\n    Found2 -->|否| TimeCheck{超时?}\n    \n    TimeCheck -->|否| Sleep\n    TimeCheck -->|是| Check2{raise_exc=True?}\n    \n    Check2 -->|是| Error2[抛出 WaitElementTimeout]\n    Check2 -->|否| ReturnNone2[返回 None]\n```\n\n### 使用 raise_exc 参数\n\n控制在未找到元素时是否抛出异常：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.exceptions import ElementNotFound\n\nasync def error_handling():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n        \n        # 如果未找到则抛出异常（默认行为）\n        try:\n            critical_element = await tab.find(id=\"must-exist\")\n        except ElementNotFound:\n            print(\"缺少关键元素！无法继续。\")\n            return\n        \n        # 如果未找到则返回 None（可选元素）\n        optional_banner = await tab.find(\n            class_name=\"promo-banner\",\n            raise_exc=False\n        )\n        \n        if optional_banner:\n            print(\"找到横幅，正在关闭它\")\n            close_button = await optional_banner.find(class_name=\"close-btn\")\n            await close_button.click()\n        else:\n            print(\"没有横幅，继续\")\n\nasyncio.run(error_handling())\n```\n\n## 最佳实践\n\n### 1. 优先使用稳定的选择器\n\n使用不太可能改变的属性：\n\n```python\n# 好：语义属性\nawait tab.find(id=\"user-profile\")  # ID 通常是稳定的\nawait tab.find(data_testid=\"submit-button\")  # 测试 ID 专为自动化设计\nawait tab.find(name=\"username\")  # 表单名称是稳定的\n\n# 避免：结构依赖\nawait tab.query(\"div > div > div:nth-child(3) > input\")  # 脆弱，容易损坏\n```\n\n### 2. 使用最简单的有效选择器\n\n从简单开始，仅在需要时添加复杂性：\n\n```python\n# 好：简单明了\nawait tab.find(id=\"login-form\")\n\n# 不必要：过于复杂\nawait tab.query(\"//div[@id='content']/descendant::form[@id='login-form']\")\n```\n\n### 3. 选择正确的方法\n\n- 使用 `find()` 进行简单的基于属性的搜索\n- 使用 `query()` 进行复杂的 CSS 或 XPath 模式\n- 使用遍历方法从已知锚点探索\n\n```python\n# 使用 find() 处理简单情况\nusername = await tab.find(id=\"username\")\n\n# 使用 query() 处理复杂模式\nactive_nav_link = await tab.query(\"nav.menu a.active\")\n\n# 使用遍历进行基于关系的搜索\ncontainer = await tab.find(id=\"cards\")\nchild_links = await container.get_children_elements(tag_filter=[\"a\"])\n```\n\n### 4. 添加有意义的超时\n\n不要对动态内容使用零超时，也不要永远等待可选元素：\n\n```python\n# 好：合理的超时\ncritical_data = await tab.find(id=\"data\", timeout=10)\noptional_popup = await tab.find(class_name=\"popup\", timeout=2, raise_exc=False)\n\n# 坏：动态内容没有超时\ndynamic_element = await tab.find(class_name=\"ajax-loaded\")  # 会立即失败\n\n# 坏：可选元素的超时时间太长\nbanner = await tab.find(class_name=\"ad-banner\", timeout=60)  # 浪费时间\n```\n\n### 5. 优雅地处理错误\n\n为可能不存在的元素制定计划：\n\n```python\n# 关键元素：让异常冒泡\nsubmit_button = await tab.find(id=\"submit-btn\")\n\n# 可选元素：显式处理\ncookie_notice = await tab.find(class_name=\"cookie-notice\", raise_exc=False)\nif cookie_notice:\n    accept_button = await cookie_notice.find(text=\"Accept\")\n    await accept_button.click()\n```\n\n## 完整示例：表单自动化\n\n这是一个结合多种元素查找技术的完整示例：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.exceptions import ElementNotFound\n\nasync def automate_registration_form():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        try:\n            # 导航到注册页面\n            await tab.go_to('https://example.com/register', timeout=10)\n            \n            # 处理可选的 cookie 横幅\n            cookie_banner = await tab.find(\n                class_name=\"cookie-banner\",\n                timeout=2,\n                raise_exc=False\n            )\n            if cookie_banner:\n                accept = await cookie_banner.find(text=\"Accept\")\n                await accept.click()\n                await asyncio.sleep(1)\n            \n            # 填写注册表单\n            # 查找表单字段\n            username_field = await tab.find(name=\"username\", timeout=5)\n            email_field = await tab.find(name=\"email\")\n            password_field = await tab.find(type=\"password\", name=\"password\")\n            confirm_password = await tab.find(type=\"password\", name=\"confirm_password\")\n            \n            # 输入信息\n            await username_field.type_text(\"john_doe_2024\", interval=0.1)\n            await email_field.type_text(\"john@example.com\", interval=0.1)\n            await password_field.type_text(\"SecurePass123!\", interval=0.1)\n            await confirm_password.type_text(\"SecurePass123!\", interval=0.1)\n            \n            # 查找并勾选条款复选框\n            # 尝试多种策略\n            terms_checkbox = await tab.find(id=\"terms\", raise_exc=False)\n            if not terms_checkbox:\n                terms_checkbox = await tab.find(name=\"accept_terms\", raise_exc=False)\n            if not terms_checkbox:\n                terms_checkbox = await tab.query(\"input[type='checkbox']\")\n            \n            await terms_checkbox.click()\n            \n            # 查找并点击提交按钮\n            submit_button = await tab.find(\n                tag_name=\"button\",\n                type=\"submit\",\n                timeout=2\n            )\n            await submit_button.click()\n            \n            # 等待成功消息，超时时间更长（表单处理）\n            success_message = await tab.find(\n                class_name=\"success-message\",\n                timeout=15\n            )\n            \n            message_text = await success_message.text\n            print(f\"注册成功: {message_text}\")\n            \n            # 验证重定向到仪表板\n            await asyncio.sleep(2)\n            current_url = await tab.current_url\n            \n            if \"dashboard\" in current_url:\n                print(\"成功重定向到仪表板\")\n                \n                # 查找欢迎消息\n                welcome = await tab.find(class_name=\"welcome-message\", timeout=5)\n                welcome_text = await welcome.text\n                print(f\"欢迎消息: {welcome_text}\")\n            else:\n                print(f\"注册后的意外 URL: {current_url}\")\n                \n        except ElementNotFound as e:\n            print(f\"元素未找到: {e}\")\n            # 为调试截图\n            await tab.take_screenshot(\"error_screenshot.png\")\n        except Exception as e:\n            print(f\"意外错误: {e}\")\n            await tab.take_screenshot(\"unexpected_error.png\")\n\nasyncio.run(automate_registration_form())\n```\n\n## 了解更多\n\n想深入了解元素查找？\n\n- **[FindElements Mixin 深入探讨](../deep-dive/find-elements-mixin.md)**：了解架构、内部选择器策略和性能优化\n- **[选择器指南](../deep-dive/selectors-guide.md)**：CSS 选择器和 XPath 的综合指南，包含语法参考和实际示例\n- **[WebElement 域](../deep-dive/webelement-domain.md)**：了解找到元素后可以对元素执行的操作\n\n元素查找是成功的浏览器自动化的基础。掌握这些技术，您将能够可靠地定位任何网页上的任何元素，无论结构多么复杂。"
  },
  {
    "path": "docs/zh/features/index.md",
    "content": "# 功能指南\n\n欢迎来到 Pydoll 的综合功能文档！在这里，您将发现使 Pydoll 成为强大而灵活的浏览器自动化工具的一切。无论您是刚刚入门还是希望利用高级功能，您都将找到每个功能的详细指南、实用示例和最佳实践。\n\n## 您将在这里找到什么\n\n本指南按照逻辑部分组织，反映了您的自动化之旅：从基本概念到高级技术。每个页面都设计为独立的，因此您可以直接跳转到您感兴趣的内容，或者按顺序学习。\n\n## 核心概念\n\n在深入了解特定功能之前，值得了解是什么使 Pydoll 与众不同。这些基础概念决定了整个库的工作方式。\n\n**[核心概念](core-concepts.md)**：探索使 Pydoll 与众不同的架构决策：零 WebDriver 方法消除了兼容性问题，异步优先设计实现了真正的并发操作，以及对多个基于 Chromium 的浏览器的原生支持。\n\n## 元素查找和交互\n\n查找页面元素并与之交互是自动化的基础。Pydoll 通过现代化的 API 使这一过程变得出奇地直观。\n\n**[元素查找](element-finding.md)**：掌握 Pydoll 的元素定位策略，从使用自然 HTML 属性的直观 `find()` 方法，到用于 CSS 选择器和 XPath 的强大 `query()` 方法。您还将学习 DOM 遍历辅助工具，让您高效地导航页面结构。\n\n## 自动化能力\n\n这些功能使您的自动化栩栩如生：模拟用户交互、键盘控制、处理文件操作、使用 iframe 以及捕获视觉内容。\n\n**[类人交互](automation/human-interactions.md)**：学习如何创建真正感觉像人类的交互：具有自然时间变化的打字、具有真实鼠标移动的点击，以及像真实用户一样使用键盘快捷键。这对于避免在自动化敏感站点中被检测至关重要。\n\n**[键盘控制](automation/keyboard-control.md)**：掌握键盘交互，全面支持组合键、修饰键和特殊键。对于表单、快捷键和可访问性测试至关重要。\n\n**[文件操作](automation/file-operations.md)**：文件处理在浏览器自动化中可能很棘手。Pydoll 为上传和下载提供了强大的解决方案，`expect_download` 上下文管理器提供了优雅的异步下载完成处理。\n\n**[IFrame 交互](automation/iframes.md)**：把 iframe 当成普通元素——定位 iframe 后在其内部继续查找，无需额外 target 或 Tab。\n\n**[截图和 PDF](automation/screenshots-and-pdfs.md)**：从您的自动化会话中捕获视觉内容。无论您需要用于视觉回归测试的整页截图、用于调试的元素特定捕获，还是用于归档的 PDF 导出，Pydoll 都能满足您的需求。\n\n## 网络功能\n\nPydoll 的网络功能是它真正出色的地方，为您提供前所未有的 HTTP 流量可见性和控制。\n\n**[网络监控](network/monitoring.md)**：观察和分析浏览器会话中的所有网络活动。提取 API 响应、跟踪请求时间、识别失败的请求，并准确了解正在交换的数据。对于调试、测试和数据提取至关重要。\n\n**[请求拦截](network/interception.md)**：超越观察，主动修改网络行为。阻止不需要的资源、注入自定义标头、修改请求负载，甚至使用模拟数据满足请求。这对于测试、优化和隐私控制非常强大。\n\n**[浏览器上下文 HTTP 请求](network/http-requests.md)**：发出在浏览器的 JavaScript 上下文中执行的 HTTP 请求，自动继承会话状态、cookie 和身份验证。这种混合方法结合了 Python 的 `requests` 库的熟悉性与浏览器上下文执行的优势。\n\n## 浏览器管理\n\n有效的浏览器和标签页管理对于复杂的自动化场景、并行处理和多用户测试至关重要。\n\n**[多标签页管理](browser-management/tabs.md)**：同时使用多个浏览器标签页，确保高效的资源使用，同时让您完全控制标签页生命周期、检测用户打开的标签页以及并发抓取操作。\n\n**[浏览器上下文](browser-management/contexts.md)**：在单个浏览器进程内创建完全隔离的浏览环境。每个上下文维护单独的 cookie、存储、缓存和权限：非常适合多账户测试、A/B 测试或使用不同配置的并行抓取。\n\n**[Cookie 和会话](browser-management/cookies-sessions.md)**：在浏览器和标签页级别管理会话状态。以编程方式设置 cookie、提取会话数据，并在浏览器上下文中维护不同的会话以进行复杂的测试场景。\n\n## 配置\n\n自定义浏览器行为的各个方面以匹配您的自动化需求，从低级 Chromium 首选项到命令行参数和页面加载策略。\n\n**[浏览器选项](configuration/browser-options.md)**：配置 Chromium 的启动参数、命令行参数和页面加载状态控制。微调浏览器行为、启用实验性功能，并针对您的自动化需求优化性能。\n\n**[浏览器首选项](configuration/browser-preferences.md)**：直接访问 Chromium 的内部首选项系统，让您控制数百个设置。配置下载、禁用功能、优化性能，或为隐蔽自动化创建真实的浏览器指纹。\n\n**[代理配置](configuration/proxy.md)**：具有完整身份验证功能的原生代理支持。对于需要 IP 轮换、地理定向测试或注重隐私的自动化的网络抓取项目至关重要。\n\n## 高级功能\n\n这些复杂的功能解决了复杂的自动化挑战和专门的用例。\n\n**[行为验证码绕过](advanced/behavioral-captcha-bypass.md)**：Pydoll 的原生行为验证码处理是其最受欢迎的功能之一。学习如何使用两种方法与 Cloudflare Turnstile、reCAPTCHA v3 和 hCaptcha 隐形挑战进行交互 - 用于保证完成的同步上下文管理器，以及用于非阻塞操作的后台处理。\n\n**[事件系统](advanced/event-system.md)**：构建响应实时浏览器事件的响应式自动化。监控页面加载、网络活动、DOM 更改和 JavaScript 执行，以创建智能、自适应的自动化脚本。\n\n**[远程连接](advanced/remote-connections.md)**：通过 WebSocket 连接到已运行的浏览器以实现混合自动化场景。非常适合 CI/CD 管道、容器化环境或将 Pydoll 集成到现有的 CDP 工具中。\n\n## 如何使用本指南\n\n每个功能页面遵循一致的结构：\n\n1. **概述** - 功能的作用及其重要性\n2. **基本用法** - 通过简单示例快速入门\n3. **高级模式** - 充分利用功能的潜力\n4. **最佳实践** - 有效和高效使用的技巧\n5. **常见陷阱** - 从常见错误中学习\n\n您可以根据需要以任何顺序探索功能。代码示例是完整的并且可以直接运行 - 只需复制、粘贴并适应您的用例。\n\n准备深入了解 Pydoll 的功能了吗？选择一个您感兴趣的功能，开始探索吧！🚀"
  },
  {
    "path": "docs/zh/features/network/http-requests.md",
    "content": "# 浏览器上下文 HTTP 请求\n\n发起自动继承浏览器会话状态、Cookie 和身份验证的 HTTP 请求。非常适合结合 UI 导航和 API 效率的混合自动化。\n\n!!! tip \"混合自动化的游戏规则改变者\"\n    曾经希望您可以发起自动获取所有浏览器 Cookie 和身份验证的 HTTP 请求吗？现在您可以了！`tab.request` 属性为您提供了一个漂亮的类似 `requests` 的接口，可以**直接在浏览器的 JavaScript 上下文中**执行 HTTP 调用。\n\n## 为什么使用浏览器上下文请求？\n\n传统自动化通常需要您手动提取 Cookie 和标头以进行 API 调用。浏览器上下文请求消除了这种麻烦：\n\n| 传统方法 | 浏览器上下文请求 |\n|---------------------|-------------------------|\n| 手动提取 Cookie | Cookie 自动继承 |\n| 管理会话令牌 | 会话状态保留 |\n| 单独处理 CORS | 遵守 CORS 策略 |\n| 同时使用两个 HTTP 客户端 | 一个统一的接口 |\n| 同步身份验证状态 | 始终已认证 |\n\n**非常适合：**\n\n- 通过 UI 登录后抓取已认证的 API\n- 混合工作流，混合浏览器交互和 API 调用\n- 测试已认证的端点而无需管理令牌\n- 绕过复杂的身份验证流程\n- 使用单页应用程序（SPA）\n\n## 快速入门\n\n最简单的示例：通过 UI 登录，然后进行已认证的 API 调用：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def hybrid_automation():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 1. 通过 UI 正常登录\n        await tab.go_to('https://example.com/login')\n        await (await tab.find(id='username')).type_text('user@example.com')\n        await (await tab.find(id='password')).type_text('password123')\n        await (await tab.find(id='login-btn')).click()\n        \n        # 登录后等待重定向\n        await asyncio.sleep(2)\n        \n        # 2. 现在使用已认证的会话进行 API 调用！\n        response = await tab.request.get('https://example.com/api/user/profile')\n        user_data = response.json()\n        \n        print(f\"登录为: {user_data['name']}\")\n        print(f\"邮箱: {user_data['email']}\")\n\nasyncio.run(hybrid_automation())\n```\n\n!!! success \"无需 Cookie 管理\"\n    注意我们没有提取或传递任何 Cookie？请求自动继承了浏览器的已认证会话！\n\n## 常见用例\n\n### 1. 抓取已认证的 API\n\n使用 UI 登录，然后使用 API 提取数据：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def scrape_user_data():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 通过 UI 登录（处理复杂的认证流程）\n        await tab.go_to('https://app.example.com/login')\n        await (await tab.find(id='email')).type_text('user@example.com')\n        await (await tab.find(id='password')).type_text('password')\n        await (await tab.find(type='submit')).click()\n        await asyncio.sleep(2)\n        \n        # 现在通过 API 提取数据（比抓取 UI 快得多）\n        all_users = []\n        for page in range(1, 6):\n            response = await tab.request.get(\n                f'https://app.example.com/api/users',\n                params={'page': str(page), 'limit': '100'}\n            )\n            users = response.json()['users']\n            all_users.extend(users)\n            print(f\"第 {page} 页: 获取了 {len(users)} 个用户\")\n        \n        print(f\"抓取的总用户数: {len(all_users)}\")\n\nasyncio.run(scrape_user_data())\n```\n\n### 2. 测试受保护的端点\n\n测试 API 端点而无需管理身份验证令牌：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def test_api_endpoints():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 一次性认证\n        await tab.go_to('https://api.example.com/login')\n        # ... 执行登录 ...\n        await asyncio.sleep(2)\n        \n        # 测试多个端点\n        endpoints = [\n            '/api/users/me',\n            '/api/settings',\n            '/api/notifications',\n            '/api/dashboard/stats'\n        ]\n        \n        for endpoint in endpoints:\n            response = await tab.request.get(f'https://api.example.com{endpoint}')\n            \n            if response.ok:\n                print(f\"成功 {endpoint}: {response.status_code}\")\n            else:\n                print(f\"失败 {endpoint}: {response.status_code}\")\n                print(f\"   错误: {response.text[:100]}\")\n\nasyncio.run(test_api_endpoints())\n```\n\n### 3. 通过 API 提交表单\n\n通过直接向 API 发送来更快地填充表单：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def bulk_form_submission():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 首先登录\n        await tab.go_to('https://crm.example.com/login')\n        # ... 登录逻辑 ...\n        await asyncio.sleep(2)\n        \n        # 通过 API 提交多个条目（比填写表单快得多）\n        contacts = [\n            {'name': 'John Doe', 'email': 'john@example.com', 'company': 'Acme Inc'},\n            {'name': 'Jane Smith', 'email': 'jane@example.com', 'company': 'Tech Corp'},\n            {'name': 'Bob Wilson', 'email': 'bob@example.com', 'company': 'StartupXYZ'},\n        ]\n        \n        for contact in contacts:\n            response = await tab.request.post(\n                'https://crm.example.com/api/contacts',\n                json=contact\n            )\n            \n            if response.ok:\n                print(f\"已添加: {contact['name']}\")\n            else:\n                print(f\"失败: {contact['name']} - {response.status_code}\")\n\nasyncio.run(bulk_form_submission())\n```\n\n### 4. 使用会话下载文件\n\n下载需要身份验证的文件：\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom pydoll.browser.chromium import Chrome\n\nasync def download_authenticated_file():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 认证\n        await tab.go_to('https://portal.example.com/login')\n        # ... 登录逻辑 ...\n        await asyncio.sleep(2)\n        \n        # 下载需要身份验证的文件\n        response = await tab.request.get(\n            'https://portal.example.com/api/reports/monthly.pdf'\n        )\n        \n        if response.ok:\n            # 保存文件\n            output_path = Path('/tmp/monthly_report.pdf')\n            output_path.write_bytes(response.content)\n            print(f\"已下载: {output_path} ({len(response.content)} 字节)\")\n        else:\n            print(f\"下载失败: {response.status_code}\")\n\nasyncio.run(download_authenticated_file())\n```\n\n### 5. 使用自定义标头\n\n向您的请求添加自定义标头：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def custom_headers_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 首先登录\n        await tab.go_to('https://api.example.com/login')\n        # ... 登录逻辑 ...\n        \n        # 使用自定义标头发起请求\n        headers: list[HeaderEntry] = [\n            {'name': 'X-API-Version', 'value': '2.0'},\n            {'name': 'X-Request-ID', 'value': 'unique-id-123'},\n            {'name': 'Accept-Language', 'value': 'pt-BR,pt;q=0.9'},\n        ]\n        \n        response = await tab.request.get(\n            'https://api.example.com/data',\n            headers=headers\n        )\n        \n        print(f\"状态: {response.status_code}\")\n        print(f\"数据: {response.json()}\")\n\nasyncio.run(custom_headers_example())\n```\n\n### 6. 处理不同的响应类型\n\n以多种格式访问响应数据：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def response_formats():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://api.example.com')\n        \n        # JSON 响应\n        json_response = await tab.request.get('/api/users/1')\n        user = json_response.json()\n        print(f\"JSON: {user}\")\n        \n        # 文本响应\n        text_response = await tab.request.get('/api/status')\n        status_text = text_response.text\n        print(f\"文本: {status_text}\")\n        \n        # 二进制响应（例如，图像）\n        image_response = await tab.request.get('/api/avatar/1')\n        image_bytes = image_response.content\n        print(f\"二进制: {len(image_bytes)} 字节\")\n        \n        # 检查响应状态\n        if json_response.ok:\n            print(\"请求成功！\")\n        \n        # 访问响应 URL（在重定向后很有用）\n        print(f\"最终 URL: {json_response.url}\")\n\nasyncio.run(response_formats())\n```\n\n## HTTP 方法\n\n支持所有标准的 HTTP 方法：\n\n### GET - 检索数据\n\n```python\n# 简单的 GET\nresponse = await tab.request.get('https://api.example.com/users')\n\n# 带查询参数的 GET\nresponse = await tab.request.get(\n    'https://api.example.com/search',\n    params={'q': 'python', 'limit': '10'}\n)\n```\n\n### POST - 创建资源\n\n```python\n# 使用 JSON 数据的 POST\nresponse = await tab.request.post(\n    'https://api.example.com/users',\n    json={'name': 'John Doe', 'email': 'john@example.com'}\n)\n\n# 使用表单数据的 POST\nresponse = await tab.request.post(\n    'https://api.example.com/login',\n    data={'username': 'john', 'password': 'secret'}\n)\n```\n\n### PUT - 更新资源\n\n```python\n# 更新整个资源\nresponse = await tab.request.put(\n    'https://api.example.com/users/123',\n    json={'name': 'Jane Doe', 'email': 'jane@example.com', 'role': 'admin'}\n)\n```\n\n### PATCH - 部分更新\n\n```python\n# 更新特定字段\nresponse = await tab.request.patch(\n    'https://api.example.com/users/123',\n    json={'email': 'newemail@example.com'}\n)\n```\n\n### DELETE - 删除资源\n\n```python\n# 删除资源\nresponse = await tab.request.delete('https://api.example.com/users/123')\n```\n\n### HEAD - 仅获取标头\n\n```python\n# 检查资源是否存在而不下载它\nresponse = await tab.request.head('https://example.com/large-file.zip')\nprint(f\"Content-Length: {response.headers}\")\n```\n\n### OPTIONS - 检查功能\n\n```python\n# 检查允许的方法\nresponse = await tab.request.options('https://api.example.com/users')\nprint(f\"允许的方法: {response.headers}\")\n```\n\n!!! info \"这是如何工作的？\"\n    浏览器上下文请求使用 Fetch API 直接在浏览器的 JavaScript 上下文中执行 HTTP 调用，同时监控 CDP 网络事件以捕获全面的元数据（标头、Cookie、时序）。\n    \n    有关内部架构、事件监控和实现详细信息的详细说明，请参阅[浏览器请求架构](../../deep-dive/browser-requests-architecture.md)。\n\n## 响应对象\n\n`Response` 对象提供了类似于 `requests.Response` 的熟悉接口：\n\n```python\nresponse = await tab.request.get('https://api.example.com/users')\n\n# 状态码\nprint(response.status_code)  # 200, 404, 500 等\n\n# 检查是否成功（2xx 或 3xx）\nif response.ok:\n    print(\"成功！\")\n\n# 响应体\ntext_data = response.text      # 作为字符串\nbyte_data = response.content   # 作为字节\njson_data = response.json()    # 解析的 JSON\n\n# 标头\nfor header in response.headers:\n    print(f\"{header['name']}: {header['value']}\")\n\n# 请求标头（实际发送的内容）\nfor header in response.request_headers:\n    print(f\"{header['name']}: {header['value']}\")\n\n# 响应设置的 Cookie\nfor cookie in response.cookies:\n    print(f\"{cookie['name']} = {cookie['value']}\")\n\n# 最终 URL（在重定向后）\nprint(response.url)\n\n# 为错误状态码引发异常\nresponse.raise_for_status()  # 如果是 4xx 或 5xx 则引发 HTTPError\n```\n\n!!! note \"重定向和 URL 跟踪\"\n    `response.url` 属性仅包含所有重定向后的**最终 URL**。如果您需要跟踪完整的重定向链（中间 URL、状态码、时序），请使用[网络监控](monitoring.md)详细观察所有请求。\n\n## 标头和 Cookie\n\n### 使用标头\n\n标头表示为 `HeaderEntry` 对象：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def header_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 使用 HeaderEntry 类型以获得 IDE 自动完成和类型检查\n        headers: list[HeaderEntry] = [\n            {'name': 'Authorization', 'value': 'Bearer token-123'},\n            {'name': 'X-Custom-Header', 'value': 'custom-value'},\n        ]\n        \n        response = await tab.request.get(\n            'https://api.example.com/protected',\n            headers=headers\n        )\n        \n        # 检查响应标头（也是 HeaderEntry 类型的字典）\n        for header in response.headers:\n            if header['name'] == 'Content-Type':\n                print(f\"Content-Type: {header['value']}\")\n\nasyncio.run(header_example())\n```\n\n!!! tip \"标头的类型提示\"\n    `HeaderEntry` 是来自 `pydoll.protocol.fetch.types` 的 `TypedDict`。将其用作类型提示可为您提供：\n    \n    - **自动完成**：IDE 建议 `name` 和 `value` 键\n    - **类型安全**：在运行前捕获拼写错误和缺失的键\n    - **文档**：清晰的标头结构\n    \n    虽然您可以传递普通字典，但使用类型提示可以提高代码质量和 IDE 支持。\n\n!!! tip \"自定义标头行为\"\n    自定义标头与浏览器的自动标头（如 `User-Agent`、`Accept`、`Referer` 等）**一起**发送。\n    \n    如果您尝试设置标准浏览器标头（例如 `User-Agent`），行为取决于特定标头；有些可能会被覆盖，其他可能被忽略，有些可能会导致冲突。对于大多数用例，坚持使用自定义标头（例如 `X-API-Key`、`Authorization`）以避免意外行为。\n\n### 理解 Cookie\n\nCookie 由浏览器自动管理：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def cookie_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 第一个请求设置 Cookie\n        login_response = await tab.request.post(\n            'https://api.example.com/login',\n            json={'username': 'user', 'password': 'pass'}\n        )\n        \n        # 检查服务器设置的 Cookie\n        print(\"服务器设置的 Cookie：\")\n        for cookie in login_response.cookies:\n            print(f\"  {cookie['name']} = {cookie['value']}\")\n        \n        # 后续请求自动包含 Cookie\n        profile_response = await tab.request.get(\n            'https://api.example.com/profile'\n        )\n        # 无需传递 Cookie - 浏览器会处理！\n        \n        print(f\"配置文件数据: {profile_response.json()}\")\n\nasyncio.run(cookie_example())\n```\n\n## 与传统 Requests 的比较\n\n| 功能 | `requests` 库 | 浏览器上下文请求 |\n|---------|-------------------|-------------------------|\n| **会话管理** | 手动 Cookie 处理 | 通过浏览器自动 |\n| **身份验证** | 提取并传递令牌 | 从浏览器继承 |\n| **CORS** | 不适用 | 浏览器执行策略 |\n| **JavaScript** | 无法执行 | 完全访问浏览器上下文 |\n| **Cookie Jar** | 单独的实例 | 浏览器的原生 Cookie 存储 |\n| **标头** | 手动设置 | 浏览器自动添加标准标头 |\n| **用例** | 服务器端脚本 | 浏览器自动化 |\n| **设置** | 外部库 | 内置于 Pydoll |\n\n## 另请参阅\n\n- **[浏览器请求架构](../../deep-dive/browser-requests-architecture.md)** - 内部实现和架构\n- **[网络监控](monitoring.md)** - 观察所有网络流量\n- **[请求拦截](interception.md)** - 在发送前修改请求\n- **[事件系统](../advanced/event-system.md)** - 对浏览器事件做出反应\n- **[深入了解：网络功能](../../deep-dive/network-capabilities.md)** - 技术细节\n\n浏览器上下文请求是混合自动化的游戏规则改变者。结合 UI 自动化的强大功能和直接 API 调用的速度，同时保持完美的会话连续性！\n"
  },
  {
    "path": "docs/zh/features/network/interception.md",
    "content": "# 请求拦截\n\n请求拦截允许您实时拦截、修改、阻止或模拟 HTTP 请求和响应。这对于测试、性能优化、内容过滤和模拟各种网络条件至关重要。\n\n!!! info \"Network 域与 Fetch 域\"\n    **Network 域**用于被动监控（观察流量）。**Fetch 域**用于主动拦截（修改/阻止请求）。本指南专注于拦截。有关被动监控，请参阅[网络监控](monitoring.md)。\n\n## 理解请求拦截\n\n当您启用请求拦截时，Pydoll 会在匹配的请求发送到服务器之前（或接收响应之后）暂停它们。然后您有三个选项：\n\n1. **继续**：让请求继续（可选择性地进行修改）\n2. **阻止**：使请求失败并返回错误\n3. **模拟**：使用自定义响应满足请求\n\n```mermaid\nsequenceDiagram\n    participant Browser\n    participant Pydoll\n    participant Server\n    \n    Browser->>Pydoll: 发起请求\n    Note over Pydoll: 请求已暂停\n    Pydoll->>Pydoll: 执行回调\n    \n    alt 继续\n        Pydoll->>Server: 转发请求\n        Server-->>Browser: 响应\n    else 阻止\n        Pydoll-->>Browser: 错误响应\n    else 模拟\n        Pydoll-->>Browser: 自定义响应\n    end\n```\n\n!!! warning \"性能影响\"\n    请求拦截会为每个匹配的请求增加延迟。只拦截您需要的内容，完成后禁用以避免减慢页面加载速度。\n\n## 启用请求拦截\n\n在拦截请求之前，您必须启用 Fetch 域：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 启用 fetch 事件（默认拦截所有请求）\n        await tab.enable_fetch_events()\n        \n        await tab.go_to('https://example.com')\n        \n        # 完成后禁用\n        await tab.disable_fetch_events()\n\nasyncio.run(main())\n```\n\n### 选择性拦截\n\n您可以按资源类型过滤要拦截的请求：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def selective_interception():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 仅拦截图片和样式表\n        await tab.enable_fetch_events(\n            resource_type='Image'  # 或 'Stylesheet'、'Script' 等\n        )\n        \n        await tab.go_to('https://example.com')\n        await tab.disable_fetch_events()\n\nasyncio.run(selective_interception())\n```\n\n!!! tip \"资源类型\"\n    参见[资源类型参考](#resource-types-reference)部分以获取可拦截资源类型的完整列表。\n\n## 拦截请求\n\n使用 `RequestPaused` 事件来拦截请求：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\n\nasync def basic_interception():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 带有类型提示的回调以获得 IDE 支持\n        async def handle_request(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            print(f\"已拦截: {url}\")\n            \n            # 继续请求而不进行修改\n            await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, handle_request)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(basic_interception())\n```\n\n!!! info \"类型提示以获得更好的 IDE 支持\"\n    使用 `RequestPausedEvent` 等类型提示来获得事件键的自动完成。所有事件类型都在 `pydoll.protocol.fetch.events` 中。\n\n!!! note \"生产就绪的等待\"\n    本指南中的示例使用 `asyncio.sleep()` 以简化。在生产代码中，考虑使用更明确的等待策略，如等待特定元素或实现网络空闲检测。有关高级技术，请参阅[网络监控](monitoring.md)指南。\n\n## 常见用例\n\n### 1. 阻止资源以节省带宽\n\n阻止图片、样式表或其他资源以加快页面加载速度：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.network.types import ErrorReason\n\nasync def block_images():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        blocked_count = 0\n        \n        async def block_resource(event: RequestPausedEvent):\n            nonlocal blocked_count\n            request_id = event['params']['requestId']\n            resource_type = event['params']['resourceType']\n            url = event['params']['request']['url']\n            \n            # 阻止图片和样式表\n            if resource_type in ['Image', 'Stylesheet']:\n                blocked_count += 1\n                print(f\"🚫 已阻止 {resource_type}: {url[:60]}\")\n                await tab.fail_request(request_id, ErrorReason.BLOCKED_BY_CLIENT)\n            else:\n                # 继续其他请求\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, block_resource)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        print(f\"\\n📊 总共阻止: {blocked_count} 个资源\")\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(block_images())\n```\n\n### 2. 修改请求头\n\n在发送请求之前添加、修改或删除请求头：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def modify_headers():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def add_custom_headers(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            # 仅修改 API 请求\n            if '/api/' in url:\n                # 构建自定义请求头（使用 HeaderEntry 类型提示以获得 IDE 支持）\n                headers: list[HeaderEntry] = [\n                    {'name': 'X-Custom-Header', 'value': 'MyValue'},\n                    {'name': 'Authorization', 'value': 'Bearer my-token-123'},\n                ]\n                \n                print(f\"✨ 已修改请求头: {url}\")\n                await tab.continue_request(request_id, headers=headers)\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, add_custom_headers)\n        \n        await tab.go_to('https://your-app.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(modify_headers())\n```\n\n!!! tip \"请求头类型提示\"\n    `HeaderEntry` 是来自 `pydoll.protocol.fetch.types` 的 `TypedDict`。将其用作类型提示可为您提供 `name` 和 `value` 键的 IDE 自动完成。您也可以使用普通字典而不使用类型提示。\n\n!!! tip \"请求头管理\"\n    当您提供自定义请求头时，它们会**替换**所有现有请求头。如果需要，请确保包含必要的请求头，如 `User-Agent`、`Accept` 等。\n\n### 3. 模拟 API 响应\n\n用自定义模拟数据替换真实的 API 响应：\n\n```python\nimport asyncio\nimport json\nimport base64\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.fetch.types import HeaderEntry\n\nasync def mock_api_responses():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def mock_response(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            # 模拟特定的 API 端点\n            if '/api/users' in url:\n                # 创建模拟响应数据\n                mock_data = {\n                    'users': [\n                        {'id': 1, 'name': 'Mock User 1'},\n                        {'id': 2, 'name': 'Mock User 2'},\n                    ],\n                    'total': 2\n                }\n                \n                # 转换为 JSON 并进行 base64 编码\n                body_json = json.dumps(mock_data)\n                body_base64 = base64.b64encode(body_json.encode()).decode()\n                \n                # 响应头\n                headers: list[HeaderEntry] = [\n                    {'name': 'Content-Type', 'value': 'application/json'},\n                    {'name': 'Access-Control-Allow-Origin', 'value': '*'},\n                ]\n                \n                print(f\"🎭 已模拟响应: {url}\")\n                await tab.fulfill_request(\n                    request_id=request_id,\n                    response_code=200,\n                    response_headers=headers,\n                    body=body_base64,\n                    response_phrase='OK'\n                )\n            else:\n                # 正常继续其他请求\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, mock_response)\n        \n        await tab.go_to('https://your-app.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(mock_api_responses())\n```\n\n!!! warning \"需要 Base64 编码\"\n    `fulfill_request()` 中的 `body` 参数必须经过 base64 编码。使用 Python 的 `base64` 模块对响应数据进行编码。\n\n### 4. 修改请求 URL\n\n将请求重定向到不同的 URL：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\n\nasync def redirect_requests():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def redirect_url(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            original_url = event['params']['request']['url']\n            \n            # 将 CDN 请求重定向到本地服务器\n            if 'cdn.example.com' in original_url:\n                new_url = original_url.replace(\n                    'cdn.example.com',\n                    'localhost:8080'\n                )\n                print(f\"🔀 已重定向: {original_url} → {new_url}\")\n                await tab.continue_request(request_id, url=new_url)\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, redirect_url)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(redirect_requests())\n```\n\n### 5. 修改请求体\n\n在发送之前修改 POST 数据：\n\n```python\nimport asyncio\nimport base64\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\n\nasync def modify_post_data():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def modify_body(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            method = event['params']['request']['method']\n            url = event['params']['request']['url']\n            \n            # 修改 POST 请求\n            if method == 'POST' and '/api/submit' in url:\n                # 创建新的 POST 数据\n                new_data = '{\"modified\": true, \"timestamp\": 123456789}'\n                post_data_base64 = base64.b64encode(new_data.encode()).decode()\n                \n                print(f\"✏️  已修改 POST 数据: {url}\")\n                await tab.continue_request(\n                    request_id,\n                    post_data=post_data_base64\n                )\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, modify_body)\n        \n        await tab.go_to('https://your-app.com/form')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(modify_post_data())\n```\n\n### 6. 处理身份验证挑战\n\n手动响应 HTTP 身份验证挑战（基本身份验证、摘要身份验证等）：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, AuthRequiredEvent\nfrom pydoll.protocol.fetch.types import AuthChallengeResponseType\n\nasync def handle_auth():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        async def respond_to_auth(event: AuthRequiredEvent):\n            request_id = event['params']['requestId']\n            auth_challenge = event['params']['authChallenge']\n            \n            print(f\"🔐 来自以下来源的身份验证挑战: {auth_challenge['origin']}\")\n            print(f\"   方案: {auth_challenge['scheme']}\")\n            print(f\"   领域: {auth_challenge.get('realm', 'N/A')}\")\n            \n            # 为身份验证挑战提供凭据\n            await tab.continue_with_auth(\n                request_id=request_id,\n                auth_challenge_response=AuthChallengeResponseType.PROVIDE_CREDENTIALS,\n                proxy_username='myuser',\n                proxy_password='mypassword'\n            )\n        \n        # 启用并处理身份验证\n        await tab.enable_fetch_events(handle_auth=True)\n        await tab.on(FetchEvent.AUTH_REQUIRED, respond_to_auth)\n        \n        await tab.go_to('https://httpbin.org/basic-auth/myuser/mypassword')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(handle_auth())\n```\n\n!!! note \"自动代理身份验证\"\n    **Pydoll 在您通过浏览器选项配置代理凭据时会自动处理代理身份验证**（407 需要代理身份验证）。此示例演示了身份验证挑战的**手动处理**，这对于以下情况很有用：\n    \n    - 来自服务器的 HTTP 基本/摘要身份验证（401 未经授权）\n    - 自定义身份验证流程\n    - 基于挑战的动态凭据选择\n    - 测试身份验证失败场景\n    \n    对于标准代理使用，只需在浏览器选项中配置您的代理凭据 - 无需手动处理！\n\n### 7. 模拟网络错误\n\n测试您的应用程序如何处理网络故障：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.network.types import ErrorReason\n\nasync def simulate_errors():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        request_count = 0\n        \n        async def fail_some_requests(event: RequestPausedEvent):\n            nonlocal request_count\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            \n            request_count += 1\n            \n            # 每三个请求失败一次\n            if request_count % 3 == 0:\n                print(f\"❌ 模拟超时: {url[:60]}\")\n                await tab.fail_request(request_id, ErrorReason.TIMED_OUT)\n            else:\n                await tab.continue_request(request_id)\n        \n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, fail_some_requests)\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(simulate_errors())\n```\n\n## 请求阶段\n\n您可以在不同阶段拦截请求：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.types import RequestStage\n\nasync def intercept_responses():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 拦截响应而不是请求\n        await tab.enable_fetch_events(request_stage=RequestStage.RESPONSE)\n        \n        # 现在您可以在响应到达页面之前修改它们\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        await tab.disable_fetch_events()\n\nasyncio.run(intercept_responses())\n```\n\n| 阶段 | 拦截时机 | 用例 |\n|-------|------------------|-----------|\n| `Request`（默认） | 请求发送之前 | 修改请求头、阻止请求、更改 URL |\n| `Response` | 接收响应之后 | 修改响应体、更改状态码 |\n\n!!! tip \"响应拦截\"\n    在拦截响应时，您可以在 `continue_request()` 中使用 `intercept_response=True` 来同时拦截该特定请求的响应。\n\n## 资源类型参考\n\n| 资源类型 | 描述 | 常见文件扩展名 |\n|---------------|-------------|------------------------|\n| `Document` | HTML 文档 | `.html` |\n| `Stylesheet` | CSS 文件 | `.css` |\n| `Image` | 图片资源 | `.jpg`、`.png`、`.gif`、`.webp`、`.svg` |\n| `Media` | 音频/视频 | `.mp4`、`.webm`、`.mp3`、`.ogg` |\n| `Font` | 网络字体 | `.woff`、`.woff2`、`.ttf`、`.otf` |\n| `Script` | JavaScript | `.js` |\n| `TextTrack` | 字幕 | `.vtt`、`.srt` |\n| `XHR` | XMLHttpRequest | AJAX 请求 |\n| `Fetch` | Fetch API | 现代 API 调用 |\n| `EventSource` | 服务器发送事件 | 实时流 |\n| `WebSocket` | WebSocket | 双向通信 |\n| `Manifest` | Web 应用清单 | PWA 配置 |\n| `Other` | 其他类型 | 杂项 |\n\n## 错误原因参考\n\n在 `fail_request()` 中使用这些来模拟不同的网络故障：\n\n| 错误原因 | 描述 | 用例 |\n|--------------|-------------|----------|\n| `FAILED` | 通用失败 | 常规错误 |\n| `ABORTED` | 请求中止 | 用户取消 |\n| `TIMED_OUT` | 请求超时 | 网络超时 |\n| `ACCESS_DENIED` | 访问被拒绝 | 权限错误 |\n| `CONNECTION_CLOSED` | 连接关闭 | 服务器断开连接 |\n| `CONNECTION_RESET` | 连接重置 | 网络重置 |\n| `CONNECTION_REFUSED` | 连接被拒绝 | 服务器无法访问 |\n| `NAME_NOT_RESOLVED` | DNS 失败 | 无效的主机名 |\n| `INTERNET_DISCONNECTED` | 无互联网 | 离线模式 |\n| `BLOCKED_BY_CLIENT` | 客户端阻止 | 广告拦截器模拟 |\n| `BLOCKED_BY_RESPONSE` | 响应被阻止 | CORS/CSP 违规 |\n\n## 最佳实践\n\n### 1. 始终继续或使请求失败\n\n```python\n# 好：每个暂停的请求都得到处理\nasync def handle_request(event: RequestPausedEvent):\n    request_id = event['params']['requestId']\n    try:\n        # 您的逻辑在这里\n        await tab.continue_request(request_id)\n    except Exception as e:\n        # 出错时失败以防止挂起\n        await tab.fail_request(request_id, ErrorReason.FAILED)\n\n# 坏：如果回调引发异常，请求可能会挂起\nasync def handle_request(event: RequestPausedEvent):\n    request_id = event['params']['requestId']\n    # 如果引发异常，请求将永远挂起\n    await tab.continue_request(request_id)\n```\n\n### 2. 使用选择性拦截\n\n```python\n# 好：仅拦截您需要的内容\nawait tab.enable_fetch_events(resource_type='Image')\n\n# 坏：拦截所有内容，减慢所有请求\nawait tab.enable_fetch_events()\n```\n\n### 3. 完成后禁用\n\n```python\n# 好：完成后清理\nawait tab.enable_fetch_events()\n# ... 执行工作 ...\nawait tab.disable_fetch_events()\n\n# 坏：使拦截保持启用状态\nawait tab.enable_fetch_events()\n# ... 执行工作 ...\n# （从未禁用）\n```\n\n### 4. 优雅地处理错误\n\n```python\n# 好：包装在 try/except 中\nasync def safe_handler(event: RequestPausedEvent):\n    request_id = event['params']['requestId']\n    try:\n        # 可能失败的复杂逻辑\n        modified_url = transform_url(event['params']['request']['url'])\n        await tab.continue_request(request_id, url=modified_url)\n    except Exception as e:\n        print(f\"处理请求时出错: {e}\")\n        # 出错时继续而不进行修改\n        await tab.continue_request(request_id)\n```\n\n## 完整示例：高级请求控制\n\n这是一个结合多种拦截技术的完整示例：\n\n```python\nimport asyncio\nimport base64\nimport json\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.fetch.events import FetchEvent, RequestPausedEvent\nfrom pydoll.protocol.fetch.types import HeaderEntry\nfrom pydoll.protocol.network.types import ErrorReason\n\nasync def advanced_interception():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        stats = {\n            'blocked': 0,\n            'mocked': 0,\n            'modified': 0,\n            'continued': 0\n        }\n        \n        async def intelligent_handler(event: RequestPausedEvent):\n            request_id = event['params']['requestId']\n            url = event['params']['request']['url']\n            resource_type = event['params']['resourceType']\n            method = event['params']['request']['method']\n            \n            try:\n                # 阻止广告和跟踪器\n                if any(tracker in url for tracker in ['analytics', 'ads', 'tracking']):\n                    stats['blocked'] += 1\n                    print(f\"🚫 已阻止跟踪器: {url[:50]}\")\n                    await tab.fail_request(request_id, ErrorReason.BLOCKED_BY_CLIENT)\n                \n                # 模拟 API 响应\n                elif '/api/config' in url:\n                    stats['mocked'] += 1\n                    mock_config = {'feature_x': True, 'debug_mode': False}\n                    body = base64.b64encode(json.dumps(mock_config).encode()).decode()\n                    headers: list[HeaderEntry] = [\n                        {'name': 'Content-Type', 'value': 'application/json'},\n                    ]\n                    print(f\"🎭 已模拟配置 API\")\n                    await tab.fulfill_request(\n                        request_id, 200, headers, body, 'OK'\n                    )\n                \n                # 为 API 请求添加身份验证头\n                elif '/api/' in url and method == 'GET':\n                    stats['modified'] += 1\n                    headers: list[HeaderEntry] = [\n                        {'name': 'Authorization', 'value': 'Bearer token-123'},\n                    ]\n                    print(f\"✨ 已添加身份验证: {url[:50]}\")\n                    await tab.continue_request(request_id, headers=headers)\n                \n                # 正常继续其他所有内容\n                else:\n                    stats['continued'] += 1\n                    await tab.continue_request(request_id)\n                    \n            except Exception as e:\n                print(f\"⚠️  处理请求时出错: {e}\")\n                # 出错时始终继续以防止挂起\n                await tab.continue_request(request_id)\n        \n        # 启用拦截\n        await tab.enable_fetch_events()\n        await tab.on(FetchEvent.REQUEST_PAUSED, intelligent_handler)\n        \n        # 导航\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(5)\n        \n        # 打印统计信息\n        print(f\"\\n📊 拦截统计:\")\n        print(f\"   已阻止: {stats['blocked']}\")\n        print(f\"   已模拟: {stats['mocked']}\")\n        print(f\"   已修改: {stats['modified']}\")\n        print(f\"   已继续: {stats['continued']}\")\n        print(f\"   总计: {sum(stats.values())}\")\n        \n        # 清理\n        await tab.disable_fetch_events()\n\nasyncio.run(advanced_interception())\n```\n\n## 另请参阅\n\n- **[网络监控](monitoring.md)** - 被动网络流量观察\n- **[CDP Fetch 域](../../deep-dive/network-capabilities.md#fetch-domain)** - 深入了解 Fetch 域\n- **[事件系统](../advanced/event-system.md)** - 了解 Pydoll 的事件架构\n\n请求拦截是用于测试、优化和模拟的强大工具。掌握这些技术以构建强大、高效的浏览器自动化脚本。\n"
  },
  {
    "path": "docs/zh/features/network/monitoring.md",
    "content": "# 网络监控\n\nPydoll 中的网络监控允许您在浏览器自动化期间观察和分析 HTTP 请求、响应和其他网络活动。这对于调试、性能分析、API 测试和了解 Web 应用程序如何与服务器通信至关重要。\n\n!!! info \"Network 与 Fetch 域\"\n    **Network 域**用于被动监控（观察流量）。**Fetch 域**用于主动拦截（修改请求/响应）。本指南重点介绍监控。有关请求拦截，请参阅高级文档。\n\n## 启用网络事件\n\n在监控网络活动之前，您必须启用 Network 域：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 启用网络监控\n        await tab.enable_network_events()\n        \n        # 现在导航\n        await tab.go_to('https://api.github.com')\n        \n        # 完成后不要忘记禁用（可选但推荐）\n        await tab.disable_network_events()\n\nasyncio.run(main())\n```\n\n!!! warning \"导航前启用\"\n    始终在导航**之前**启用网络事件以捕获所有请求。在启用之前发起的请求不会被捕获。\n\n## 获取网络日志\n\n启用网络事件后，Pydoll 会自动存储网络日志。您可以使用 `get_network_logs()` 检索它们：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def analyze_requests():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # 导航到页面\n        await tab.go_to('https://httpbin.org/json')\n        \n        # 等待页面完全加载\n        await asyncio.sleep(2)\n        \n        # 获取所有网络日志\n        logs = await tab.get_network_logs()\n        \n        print(f\"捕获的总请求数: {len(logs)}\")\n        \n        for log in logs:\n            request = log['params']['request']\n            print(f\"→ {request['method']} {request['url']}\")\n\nasyncio.run(analyze_requests())\n```\n\n!!! note \"生产就绪的等待\"\n    上面的示例为简单起见使用 `asyncio.sleep(2)`。在生产代码中，请考虑使用更明确的等待策略：\n    \n    - 等待特定元素出现\n    - 使用[事件系统](../advanced/event-system.md)来检测何时加载所有资源\n    - 实现网络空闲检测（参见实时网络监控部分）\n    \n    这确保您的自动化等待的时间正好合适，不多不少。\n\n### 过滤网络日志\n\n您可以按 URL 模式过滤日志：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def filter_logs_example():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        \n        # 获取所有日志\n        all_logs = await tab.get_network_logs()\n        \n        # 获取特定域的日志\n        api_logs = await tab.get_network_logs(filter='api.example.com')\n        \n        # 获取特定端点的日志\n        user_logs = await tab.get_network_logs(filter='/api/users')\n\nasyncio.run(filter_logs_example())\n```\n\n## 理解网络事件结构\n\n网络日志包含有关每个请求的详细信息。以下是结构：\n\n### RequestWillBeSentEvent\n\n此事件在即将发送请求时触发：\n\n```python\n{\n    'method': 'Network.requestWillBeSent',\n    'params': {\n        'requestId': 'unique-request-id',\n        'loaderId': 'loader-id',\n        'documentURL': 'https://example.com',\n        'request': {\n            'url': 'https://api.example.com/data',\n            'method': 'GET',  # 或 'POST'、'PUT'、'DELETE' 等\n            'headers': {\n                'User-Agent': 'Chrome/...',\n                'Accept': 'application/json',\n                ...\n            },\n            'postData': '...',  # 仅存在于 POST/PUT 请求\n            'initialPriority': 'High',\n            'referrerPolicy': 'strict-origin-when-cross-origin'\n        },\n        'timestamp': 1234567890.123,\n        'wallTime': 1234567890.123,\n        'initiator': {\n            'type': 'script',  # 或 'parser'、'other'\n            'stack': {...}  # 如果从脚本发起则有调用堆栈\n        },\n        'type': 'XHR',  # 资源类型：Document、Script、Image、XHR 等\n        'frameId': 'frame-id',\n        'hasUserGesture': False\n    }\n}\n```\n\n### 关键字段参考\n\n| 字段 | 位置 | 类型 | 描述 |\n|-------|----------|------|-------------|\n| `requestId` | `params.requestId` | `str` | 此请求的唯一标识符 |\n| `url` | `params.request.url` | `str` | 完整的请求 URL |\n| `method` | `params.request.method` | `str` | HTTP 方法（GET、POST 等）|\n| `headers` | `params.request.headers` | `dict` | 请求标头 |\n| `postData` | `params.request.postData` | `str` | 请求体（POST/PUT）|\n| `timestamp` | `params.timestamp` | `float` | 请求开始的单调时间 |\n| `type` | `params.type` | `str` | 资源类型（Document、XHR、Image 等）|\n| `initiator` | `params.initiator` | `dict` | 触发此请求的内容 |\n\n## 获取响应体\n\n要获取实际的响应内容，请使用 `get_network_response_body()`：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def fetch_api_response():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # 导航到 API 端点\n        await tab.go_to('https://httpbin.org/json')\n        await asyncio.sleep(2)\n        \n        # 获取所有请求\n        logs = await tab.get_network_logs()\n        \n        for log in logs:\n            request_id = log['params']['requestId']\n            url = log['params']['request']['url']\n            \n            # 仅获取 JSON 端点的响应\n            if 'httpbin.org/json' in url:\n                try:\n                    # 获取响应体\n                    response_body = await tab.get_network_response_body(request_id)\n                    print(f\"来自 {url} 的响应:\")\n                    print(response_body)\n                except Exception as e:\n                    print(f\"无法获取响应体: {e}\")\n\nasyncio.run(fetch_api_response())\n```\n\n!!! warning \"响应体可用性\"\n    响应体仅适用于已完成的请求。此外，某些响应类型（如图像或重定向）可能没有可访问的响应体。\n\n## 实际用例\n\n### 1. API 测试和验证\n\n监控 API 调用以验证是否正在进行正确的请求：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def validate_api_calls():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # 导航到您的应用\n        await tab.go_to('https://your-app.com')\n        \n        # 触发某些进行 API 调用的操作\n        button = await tab.find(id='load-data-button')\n        await button.click()\n        await asyncio.sleep(2)\n        \n        # 获取 API 日志\n        api_logs = await tab.get_network_logs(filter='/api/')\n        \n        print(f\"\\n📊 API 调用摘要:\")\n        print(f\"总 API 调用数: {len(api_logs)}\")\n        \n        for log in api_logs:\n            request = log['params']['request']\n            method = request['method']\n            url = request['url']\n            \n            # 检查是否存在正确的认证标头\n            headers = request.get('headers', {})\n            has_auth = 'Authorization' in headers or 'authorization' in headers\n            \n            print(f\"\\n{method} {url}\")\n            print(f\"  ✓ 有授权: {has_auth}\")\n            \n            # 如果适用，验证 POST 数据\n            if method == 'POST' and 'postData' in request:\n                print(f\"  📤 正文: {request['postData'][:100]}...\")\n\nasyncio.run(validate_api_calls())\n```\n\n### 2. 性能分析\n\n分析请求时序并识别慢速资源：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def analyze_performance():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(5)\n        \n        logs = await tab.get_network_logs()\n        \n        # 存储时序数据\n        timings = []\n        \n        for log in logs:\n            params = log['params']\n            request_id = params['requestId']\n            url = params['request']['url']\n            resource_type = params.get('type', 'Other')\n            \n            timings.append({\n                'url': url,\n                'type': resource_type,\n                'timestamp': params['timestamp']\n            })\n        \n        # 按时间戳排序\n        timings.sort(key=lambda x: x['timestamp'])\n        \n        print(\"\\n⏱️  请求时间线:\")\n        start_time = timings[0]['timestamp'] if timings else 0\n        \n        for timing in timings[:20]:  # 显示前 20 个\n            elapsed = (timing['timestamp'] - start_time) * 1000  # 转换为毫秒\n            print(f\"{elapsed:7.0f}ms | {timing['type']:12} | {timing['url'][:80]}\")\n\nasyncio.run(analyze_performance())\n```\n\n### 3. 检测外部资源\n\n查找您的页面连接到的所有外部域：\n\n```python\nimport asyncio\nfrom urllib.parse import urlparse\nfrom collections import Counter\nfrom pydoll.browser.chromium import Chrome\n\nasync def analyze_domains():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        await tab.go_to('https://news.ycombinator.com')\n        await asyncio.sleep(5)\n        \n        logs = await tab.get_network_logs()\n        \n        # 计算每个域的请求数\n        domains = Counter()\n        \n        for log in logs:\n            url = log['params']['request']['url']\n            try:\n                domain = urlparse(url).netloc\n                if domain:\n                    domains[domain] += 1\n            except:\n                pass\n        \n        print(\"\\n🌐 外部域:\")\n        for domain, count in domains.most_common(10):\n            print(f\"  {count:3} 个请求 | {domain}\")\n\nasyncio.run(analyze_domains())\n```\n\n### 4. 监控特定资源类型\n\n跟踪特定类型的资源，如图像或脚本：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def track_resource_types():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        await tab.go_to('https://example.com')\n        await asyncio.sleep(3)\n        \n        logs = await tab.get_network_logs()\n        \n        # 按资源类型分组\n        by_type = {}\n        \n        for log in logs:\n            params = log['params']\n            resource_type = params.get('type', 'Other')\n            url = params['request']['url']\n            \n            if resource_type not in by_type:\n                by_type[resource_type] = []\n            \n            by_type[resource_type].append(url)\n        \n        print(\"\\n📦 按类型分类的资源:\")\n        for rtype in sorted(by_type.keys()):\n            urls = by_type[rtype]\n            print(f\"\\n{rtype}: {len(urls)} 个资源\")\n            for url in urls[:3]:  # 显示前 3 个\n                print(f\"  • {url}\")\n            if len(urls) > 3:\n                print(f\"  ... 还有 {len(urls) - 3} 个\")\n\nasyncio.run(track_resource_types())\n```\n\n## 实时网络监控\n\n对于实时监控，使用事件回调而不是轮询 `get_network_logs()`：\n\n!!! info \"理解事件\"\n    实时监控使用 Pydoll 的事件系统来响应发生的网络活动。要深入了解事件的工作原理，请参阅 **[事件系统](../advanced/event-system.md)**。\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import (\n    NetworkEvent,\n    RequestWillBeSentEvent,\n    ResponseReceivedEvent,\n    LoadingFailedEvent\n)\n\nasync def real_time_monitoring():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # 统计\n        stats = {\n            'requests': 0,\n            'responses': 0,\n            'failed': 0\n        }\n        \n        # 请求回调\n        async def on_request(event: RequestWillBeSentEvent):\n            stats['requests'] += 1\n            url = event['params']['request']['url']\n            method = event['params']['request']['method']\n            print(f\"→ {method:6} | {url}\")\n        \n        # 响应回调\n        async def on_response(event: ResponseReceivedEvent):\n            stats['responses'] += 1\n            response = event['params']['response']\n            status = response['status']\n            url = response['url']\n            \n            # 按状态着色\n            if 200 <= status < 300:\n                color = '\\033[92m'  # 绿色\n            elif 300 <= status < 400:\n                color = '\\033[93m'  # 黄色\n            else:\n                color = '\\033[91m'  # 红色\n            reset = '\\033[0m'\n            \n            print(f\"← {color}{status}{reset} | {url}\")\n        \n        # 失败回调\n        async def on_failed(event: LoadingFailedEvent):\n            stats['failed'] += 1\n            error = event['params']['errorText']\n            print(f\"✗ 失败: {error}\")\n        \n        # 启用并注册回调\n        await tab.enable_network_events()\n        await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, on_request)\n        await tab.on(NetworkEvent.RESPONSE_RECEIVED, on_response)\n        await tab.on(NetworkEvent.LOADING_FAILED, on_failed)\n        \n        # 导航\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(5)\n        \n        print(f\"\\n📊 摘要:\")\n        print(f\"  请求: {stats['requests']}\")\n        print(f\"  响应: {stats['responses']}\")\n        print(f\"  失败: {stats['failed']}\")\n\nasyncio.run(real_time_monitoring())\n```\n\n## 资源类型参考\n\nPydoll 捕获以下资源类型：\n\n| 类型 | 描述 | 示例 |\n|------|-------------|----------|\n| `Document` | 主 HTML 文档 | 页面加载、iframe 源 |\n| `Stylesheet` | CSS 文件 | 外部 .css、内联样式 |\n| `Image` | 图像资源 | .jpg、.png、.gif、.webp、.svg |\n| `Media` | 音频/视频文件 | .mp4、.webm、.mp3、.ogg |\n| `Font` | Web 字体 | .woff、.woff2、.ttf、.otf |\n| `Script` | JavaScript 文件 | .js 文件、内联脚本 |\n| `TextTrack` | 字幕文件 | .vtt、.srt |\n| `XHR` | XMLHttpRequest | AJAX 请求、旧版 API 调用 |\n| `Fetch` | Fetch API 请求 | 现代 API 调用 |\n| `EventSource` | 服务器发送事件 | 实时流 |\n| `WebSocket` | WebSocket 连接 | 双向通信 |\n| `Manifest` | Web 应用清单 | PWA 配置 |\n| `Other` | 其他资源类型 | 杂项 |\n\n## 高级：提取响应时序\n\n网络事件包括详细的时序信息：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.protocol.network.events import NetworkEvent, ResponseReceivedEvent\n\nasync def analyze_timing():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        \n        # 自定义回调以捕获时序\n        timing_data = []\n        \n        async def on_response(event: ResponseReceivedEvent):\n            response = event['params']['response']\n            timing = response.get('timing')\n            \n            if timing:\n                # 计算不同阶段\n                dns_time = timing.get('dnsEnd', 0) - timing.get('dnsStart', 0)\n                connect_time = timing.get('connectEnd', 0) - timing.get('connectStart', 0)\n                ssl_time = timing.get('sslEnd', 0) - timing.get('sslStart', 0)\n                send_time = timing.get('sendEnd', 0) - timing.get('sendStart', 0)\n                wait_time = timing.get('receiveHeadersStart', 0) - timing.get('sendEnd', 0)\n                receive_time = timing.get('receiveHeadersEnd', 0) - timing.get('receiveHeadersStart', 0)\n                \n                timing_data.append({\n                    'url': response['url'][:50],\n                    'dns': dns_time if dns_time > 0 else 0,\n                    'connect': connect_time if connect_time > 0 else 0,\n                    'ssl': ssl_time if ssl_time > 0 else 0,\n                    'send': send_time,\n                    'wait': wait_time,\n                    'receive': receive_time,\n                    'total': receive_time + wait_time + send_time\n                })\n        \n        await tab.on(NetworkEvent.RESPONSE_RECEIVED, on_response)\n        await tab.go_to('https://github.com')\n        await asyncio.sleep(5)\n        \n        # 打印时序分解\n        print(\"\\n⏱️  请求时序分解（毫秒）:\")\n        print(f\"{'URL':<50} | {'DNS':>6} | {'连接':>8} | {'SSL':>6} | {'发送':>6} | {'等待':>6} | {'接收':>8} | {'总计':>7}\")\n        print(\"-\" * 120)\n        \n        for data in sorted(timing_data, key=lambda x: x['total'], reverse=True)[:10]:\n            print(f\"{data['url']:<50} | {data['dns']:6.1f} | {data['connect']:8.1f} | {data['ssl']:6.1f} | \"\n                  f\"{data['send']:6.1f} | {data['wait']:6.1f} | {data['receive']:8.1f} | {data['total']:7.1f}\")\n\nasyncio.run(analyze_timing())\n```\n\n## 时序字段说明\n\n| 阶段 | 字段 | 描述 |\n|-------|--------|-------------|\n| **DNS** | `dnsStart` → `dnsEnd` | DNS 查找时间 |\n| **连接** | `connectStart` → `connectEnd` | TCP 连接建立 |\n| **SSL** | `sslStart` → `sslEnd` | SSL/TLS 握手 |\n| **发送** | `sendStart` → `sendEnd` | 发送请求的时间 |\n| **等待** | `sendEnd` → `receiveHeadersStart` | 等待服务器响应（TTFB）|\n| **接收** | `receiveHeadersStart` → `receiveHeadersEnd` | 接收响应标头的时间 |\n\n!!! tip \"首字节时间（TTFB）\"\n    TTFB 是\"等待\"阶段 - 发送请求和接收响应的第一个字节之间的时间。这对于性能分析至关重要。\n\n## 最佳实践\n\n### 1. 仅在需要时启用网络事件\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def best_practice_enable():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        \n        # ✅ 好：导航前启用，之后禁用\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        logs = await tab.get_network_logs()\n        await tab.disable_network_events()\n        \n        # ❌ 不好：在整个会话期间保持启用\n        # await tab.enable_network_events()\n        # ... 长时间的自动化会话 ...\n```\n\n### 2. 过滤日志以减少内存使用\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def best_practice_filter():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        \n        # ✅ 好：过滤特定请求\n        api_logs = await tab.get_network_logs(filter='/api/')\n        \n        # ❌ 不好：当您只需要特定日志时获取所有日志\n        all_logs = await tab.get_network_logs()\n        filtered = [log for log in all_logs if '/api/' in log['params']['request']['url']]\n```\n\n### 3. 安全地处理缺失字段\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def best_practice_safe_access():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.enable_network_events()\n        await tab.go_to('https://example.com')\n        await asyncio.sleep(2)\n        \n        logs = await tab.get_network_logs()\n        \n        # ✅ 好：使用 .get() 安全访问\n        for log in logs:\n            params = log.get('params', {})\n            request = params.get('request', {})\n            url = request.get('url', 'Unknown')\n            post_data = request.get('postData')  # 可能为 None\n            \n            if post_data:\n                print(f\"POST 数据: {post_data}\")\n        \n        # ❌ 不好：直接访问可能引发 KeyError\n        # url = log['params']['request']['url']\n        # post_data = log['params']['request']['postData']  # 可能不存在！\n```\n\n### 4. 对实时需求使用事件回调\n\n```python\nimport asyncio\nfrom pydoll.protocol.network.events import NetworkEvent, RequestWillBeSentEvent\n\n# ✅ 好：使用回调进行实时监控\nasync def on_request(event: RequestWillBeSentEvent):\n    print(f\"新请求: {event['params']['request']['url']}\")\n\nawait tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, on_request)\n\n# ❌ 不好：重复轮询日志（效率低）\nwhile True:\n    logs = await tab.get_network_logs()\n    # 处理日志...\n    await asyncio.sleep(0.5)  # 浪费！\n```\n\n## 另请参阅\n\n- **[CDP Network 域](../../deep-dive/network-capabilities.md)** - 深入了解网络功能\n- **[事件系统](../advanced/event-system.md)** - 了解 Pydoll 的事件架构\n- **[请求拦截](interception.md)** - 修改请求和响应\n"
  },
  {
    "path": "docs/zh/features/network/network-recording.md",
    "content": "# HAR 网络录制\n\n捕获浏览器会话期间的所有网络活动，并导出为标准 HAR (HTTP Archive) 1.2 文件。非常适合调试、性能分析和测试固件。\n\n!!! tip \"像专家一样调试\"\n    HAR 文件是录制网络流量的行业标准。您可以将它们直接导入 Chrome DevTools、Charles Proxy 或任何 HAR 查看器进行详细分析。\n\n## 为什么使用 HAR 录制？\n\n| 使用场景 | 优势 |\n|---------|------|\n| 调试失败的请求 | 查看确切的 headers、时序和响应体 |\n| 性能分析 | 识别慢速请求和瓶颈 |\n| API 文档 | 捕获真实的请求/响应对 |\n| 测试固件 | 录制真实流量用于测试模拟 |\n\n## 快速开始\n\n录制页面导航期间的所有网络流量：\n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def record_traffic():\n    async with Chrome() as browser:\n        tab = await browser.start()\n\n        async with tab.request.record() as capture:\n            await tab.go_to('https://example.com')\n\n        # 保存捕获为 HAR 文件\n        capture.save('flow.har')\n        print(f'捕获了 {len(capture.entries)} 个请求')\n\nasyncio.run(record_traffic())\n```\n\n## 录制 API\n\n### `tab.request.record(resource_types=None)`\n\n上下文管理器，捕获标签页上的网络流量。\n\n| 参数 | 类型 | 描述 |\n|------|------|------|\n| `resource_types` | `list[ResourceType] \\| None` | 可选的资源类型列表。当为 `None`（默认）时，捕获所有类型。 |\n\n```python\nasync with tab.request.record() as capture:\n    # 此块内的所有网络活动都会被捕获\n    await tab.go_to('https://example.com')\n    await (await tab.find(id='search')).type_text('pydoll')\n    await (await tab.find(type='submit')).click()\n```\n\n`capture` 对象（`HarCapture`）提供：\n\n| 属性/方法 | 描述 |\n|----------|------|\n| `capture.entries` | 捕获的 HAR 条目列表 |\n| `capture.to_dict()` | 完整的 HAR 1.2 字典（用于自定义处理） |\n| `capture.save(path)` | 保存为 HAR JSON 文件 |\n\n### 按资源类型过滤\n\n仅录制特定资源类型而非所有流量：\n\n```python\nfrom pydoll.protocol.network.types import ResourceType\n\n# 仅录制 fetch/XHR 请求（跳过文档、图像等）\nasync with tab.request.record(\n    resource_types=[ResourceType.FETCH, ResourceType.XHR]\n) as capture:\n    await tab.go_to('https://example.com')\n\n# 仅录制文档和样式表请求\nasync with tab.request.record(\n    resource_types=[ResourceType.DOCUMENT, ResourceType.STYLESHEET]\n) as capture:\n    await tab.go_to('https://example.com')\n```\n\n可用的 `ResourceType` 值：\n\n| 值 | 描述 |\n|----|------|\n| `ResourceType.DOCUMENT` | HTML 文档 |\n| `ResourceType.STYLESHEET` | CSS 样式表 |\n| `ResourceType.SCRIPT` | JavaScript 文件 |\n| `ResourceType.IMAGE` | 图像 |\n| `ResourceType.FONT` | Web 字体 |\n| `ResourceType.MEDIA` | 音频/视频 |\n| `ResourceType.FETCH` | Fetch API 请求 |\n| `ResourceType.XHR` | XMLHttpRequest 调用 |\n| `ResourceType.WEB_SOCKET` | WebSocket 连接 |\n| `ResourceType.OTHER` | 其他资源类型 |\n\n### 保存捕获\n\n```python\n# 保存为 HAR 文件（可以在 Chrome DevTools 中打开）\ncapture.save('flow.har')\n\n# 保存到嵌套目录（自动创建）\ncapture.save('recordings/session1/flow.har')\n\n# 访问原始 HAR 字典进行自定义处理\nhar_dict = capture.to_dict()\nprint(har_dict['log']['version'])  # \"1.2\"\n```\n\n### 检查条目\n\n```python\nasync with tab.request.record() as capture:\n    await tab.go_to('https://example.com')\n\nfor entry in capture.entries:\n    req = entry['request']\n    resp = entry['response']\n    print(f\"{req['method']} {req['url']} -> {resp['status']}\")\n```\n\n## 高级用法\n\n### 过滤捕获的条目\n\n```python\nasync with tab.request.record() as capture:\n    await tab.go_to('https://example.com')\n\n# 仅过滤 API 调用\napi_entries = [\n    e for e in capture.entries\n    if '/api/' in e['request']['url']\n]\n\n# 仅过滤失败的请求\nfailed = [\n    e for e in capture.entries\n    if e['response']['status'] >= 400\n]\n```\n\n### 自定义 HAR 处理\n\n```python\nhar = capture.to_dict()\n\n# 按类型统计请求\nfrom collections import Counter\ntypes = Counter(\n    e.get('_resourceType', 'Other')\n    for e in har['log']['entries']\n)\nprint(types)  # Counter({'Document': 1, 'Script': 5, 'Stylesheet': 3, ...})\n```\n\n## HAR 文件格式\n\n导出的 HAR 遵循 [HAR 1.2 规范](http://www.softwareishard.com/blog/har-12-spec/)。每个条目包含：\n\n- **Request**：方法、URL、headers、查询参数、POST 数据\n- **Response**：状态、headers、响应体内容（文本或 base64 编码）\n- **Timings**：DNS、连接、SSL、发送、等待（TTFB）、接收\n- **Metadata**：服务器 IP、连接 ID、资源类型\n\n!!! note \"响应体\"\n    响应体在每个请求完成后自动捕获。二进制内容（图像、字体等）存储为 base64 编码的字符串。\n"
  },
  {
    "path": "docs/zh/index.md",
    "content": "<p align=\"center\">\n    <img src=\"../resources/images/logo.png\" alt=\"Pydoll Logo\" /> <br><br>\n</p>\n\n<p align=\"center\">\n    <a href=\"https://codecov.io/gh/autoscrape-labs/pydoll\">\n        <img src=\"https://codecov.io/gh/autoscrape-labs/pydoll/graph/badge.svg?token=40I938OGM9\"/> \n    </a>\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/tests.yml/badge.svg\" alt=\"Tests\">\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/ruff-ci.yml/badge.svg\" alt=\"Ruff CI\">\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/release.yml/badge.svg\" alt=\"Release\">\n    <img src=\"https://github.com/thalissonvs/pydoll/actions/workflows/mypy.yml/badge.svg\" alt=\"MyPy CI\">\n</p>\n\n\n# 欢迎使用Pydoll\n\n欢迎来到 Pydoll 的世界～这是为 Python 量身打造的新一代浏览器自动化神器！\n\n## 什么是Pydoll?\n\nPydoll采用全新的浏览器自动化技术——完全无需 WebDriver！与其他依赖外部驱动的解决方案不同，Pydoll 通过浏览器原生 DevTools 协议直接通信，提供零依赖的自动化体验，并自带原生异步高性能支持。\n\n无论是数据采集、[Web应用测试](https://www.lambdatest.com/web-testing)，还是自动化重复任务，Pydoll 都能通过其直观的 API 和强大功能，让这些工作变得异常简单。  \n\n## 安装\n\n创建并激活一个 [虚拟环境](https://docs.python.org/3/tutorial/venv.html)，然后安装Pydoll:\n\n<div class=\"termy\">\n```bash\n$ pip install pydoll-python\n\n---> 100%\n```\n</div>\n\n你可以直接在GitHub上找到最新的开发版本:\n\n```bash\n$ pip install git+https://github.com/autoscrape-labs/pydoll.git\n```\n\n## 为何选择Pydoll?\n\n- **智能验证码绕过**: 内置Cloudflare Turnstile与reCAPTCHA v3验证码的自动破解能力，无需依赖外部服务、API密钥或复杂配置。即使遭遇防护系统，您的自动化流程仍可畅行无阻。\n- **模拟真人交互**: 通过先进算法模拟真实人类行为特征——通过随机操作间隔，到鼠标移动轨迹、页面滚动模式乃至输入速度，皆可骗过最严苛的反爬虫系统。\n- **极简哲学**: 无需浪费太多时间在配置驱动或解决兼容问题上。Pydoll开箱即用。\n- **原生异步性能**: 基于`asyncio`库深度设计, Pydoll不仅支持异步操作——更为高并发而生，可同时进行多个受防护站点的数据采集。\n- **强大的网络监控**: 轻松实现请求拦截、流量篡改与响应分析，完整掌控网络通信链路，轻松突破层层防护体系。\n- **事件驱动架构**: 实时响应页面事件、网络请求与用户交互，构建能动态适应防护系统的智能自动化流。\n- **直观的元素定位**: 使用符合人类直觉的定位方法 `find()` 和 `query()` ，面对动态加载的防护内容，定位依然精准。\n- **强类型安全**: 完备的类型系统为复杂自动化场景提供更优IDE支持和更好地预防运行时报错。\n\n\n准备好开始了吗？以下内容将带您从安装配置、基础使用到高级功能，全面掌握 Pydoll 的最佳实践。\n\n让我们以最优雅的方式，开启您的网页自动化之旅！🚀\n\n## 简单的例子上手\n\n让我们从一个实际案例开始。以下脚本将打开 Pydoll 的 GitHub 仓库并star：  \n\n```python\nimport asyncio\nfrom pydoll.browser.chromium import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://github.com/autoscrape-labs/pydoll')\n        \n        star_button = await tab.find(\n            tag_name='button',\n            timeout=5,\n            raise_exc=False\n        )\n        if not star_button:\n            print(\"Ops! The button was not found.\")\n            return\n\n        await star_button.click()\n        await asyncio.sleep(3)\n\nasyncio.run(main())\n```\n\n此示例演示了如何导航到网站、等待元素出现并与之交互。您可以使用这样的模式来自动执行许多不同的 Web 任务。\n\n??? note \"或者使用不带上下文管理器的...\"\n    如果你不想要使用上下文管理器模式，你可以手动管理浏览器实例：\n    \n    ```python\n    import asyncio\n    from pydoll.browser.chromium import Chrome\n    \n    async def main():\n        browser = Chrome()\n        tab = await browser.start()\n        await tab.go_to('https://github.com/autoscrape-labs/pydoll')\n        \n        star_button = await tab.find(\n            tag_name='button',\n            timeout=5,\n            raise_exc=False\n        )\n        if not star_button:\n            print(\"Ops! The button was not found.\")\n            return\n\n        await star_button.click()\n        await asyncio.sleep(3)\n        await browser.stop()\n    \n    asyncio.run(main())\n    ```\n    \n    Note that when not using the context manager, you'll need to explicitly call `browser.stop()` to release resources.\n\n## 补充例子: 自定义浏览器配置\n\n对于更高级的使用场景，Pydoll 允许您使用 `ChromiumOptions` 类自定义浏览器配置。此功能在您需要执行以下操作时非常有用：\n\n- 在无头模式下运行（无可见浏览器窗口）\n- 指定自定义浏览器可执行文件路径\n- 配置代理、用户代理或其他浏览器设置\n- 设置窗口尺寸或启动参数\n\n以下示例展示了如何使用 Chrome 的自定义选项：\n\n```python hl_lines=\"8-12 30-32 34-38\"\nimport asyncio\nimport os\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nasync def main():\n    options = ChromiumOptions()\n    options.binary_location = '/usr/bin/google-chrome-stable'\n    options.add_argument('--headless=new')\n    options.add_argument('--start-maximized')\n    options.add_argument('--disable-notifications')\n    \n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to('https://github.com/autoscrape-labs/pydoll')\n        \n        star_button = await tab.find(\n            tag_name='button',\n            timeout=5,\n            raise_exc=False\n        )\n        if not star_button:\n            print(\"Ops! The button was not found.\")\n            return\n\n        await star_button.click()\n        await asyncio.sleep(3)\n\n        screenshot_path = os.path.join(os.getcwd(), 'pydoll_repo.png')\n        await tab.take_screenshot(path=screenshot_path)\n        print(f\"Screenshot saved to: {screenshot_path}\")\n\n        base64_screenshot = await tab.take_screenshot(as_base64=True)\n\n        repo_description_element = await tab.find(\n            class_name='f4.my-3'\n        )\n        repo_description = await repo_description_element.text\n        print(f\"Repository description: {repo_description}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n\n此扩展示例演示了：\n\n1. 创建和配置浏览器选项\n2. 设置自定义Chrome可执行程序路径\n3. 启用无头模式以实现无痕操作\n4. 设置其他浏览器命令行flags\n5. 屏幕截图（在无头模式下尤其有用）\n\n??? info \"关于Chrome配置选项\"\n    The `options.add_argument()` 方法允许您传递任何 Chromium 命令行参数来自定义浏览器行为。有数百个可用选项可用于控制从网络到渲染行为的所有内容。\n\n    常用Chrome配置选项\n    \n    ```python\n    # 性能与行为选项\n    options.add_argument('--headless=new')         # 以无头模式运行Chrome\n    options.add_argument('--disable-gpu')          # 禁用GPU加速\n    options.add_argument('--no-sandbox')           # 禁用沙盒模式（需谨慎使用）\n    options.add_argument('--disable-dev-shm-usage') # 解决资源限制问题\n    \n    # 界面显示选项\n    options.add_argument('--start-maximized')      # 以最大化窗口启动\n    options.add_argument('--window-size=1920,1080') # 设置特定窗口尺寸\n    options.add_argument('--hide-scrollbars')      # 隐藏滚动条\n    \n    # 网络选项\n    options.add_argument('--proxy-server=socks5://127.0.0.1:9050') # 使用代理服务器\n    options.add_argument('--disable-extensions')   # 禁用扩展程序\n    options.add_argument('--disable-notifications') # 禁用通知\n    \n    # 隐私与安全\n    options.add_argument('--incognito')            # 以隐身模式运行\n    options.add_argument('--disable-infobars')     # 禁用信息栏\n    ```\n    \n    完整参考指南\n    \n    如需获取所有可用的Chrome命令行参数完整列表，请参考以下资源：\n    \n    - [Chromium Command Line Switches](https://peter.sh/experiments/chromium-command-line-switches/) - Complete reference list\n    - [Chrome Flags](chrome://flags) - Enter this in your Chrome browser address bar to see experimental features\n    - [Chromium Source Code Flags](https://source.chromium.org/chromium/chromium/src/+/main:chrome/common/chrome_switches.cc) - Direct source code reference\n    \n    请注意某些选项在不同Chrome版本中可能有差异表现，建议在升级Chrome时测试您的配置。\n\n通过这些配置，您可以在各种环境中运行 Pydoll，包括 CI/CD 流水线、无显示器的服务器或 Docker 容器。\n\n继续阅读文档，探索 Pydoll 在处理验证码、处理多个标签页、与元素交互等方面的强大功能。\n\n## 极简依赖\n\nPydoll 的优势之一是其轻量级的占用空间。与其他需要大量依赖项的浏览器自动化工具不同，Pydoll 在保留了强大的功能的同时力求精简。  \n\n### 核心依赖\n\nPydoll仅依赖少量的核心库：  \n\n```\npython = \"^3.10\"\nwebsockets = \"^13.1\"\naiohttp = \"^3.9.5\"\naiofiles = \"^23.2.1\"\nbs4 = \"^0.0.2\"\n```\n\n这种极简依赖策略带来五大核心优势：  \n\n- **⚡闪电安装** - 无需解析复杂的依赖树\n- **🧩 零冲突** - 与其他包发生版本冲突的概率极低\n- **📦 轻量化** - 更低的磁盘空间占用\n- **🔒 更好的安全** - 更小的攻击面和供应链漏洞\n- **🔄 方便升级** - 方便维护已经无破坏性更新\n\n更少的依赖项带来了： 更高的运行可靠性以及更强的性能表现。\n\n## 顶级赞助商\n\n<a href=\"https://substack.thewebscraping.club/p/pydoll-webdriver-scraping?utm_source=github&utm_medium=repo&utm_campaign=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n  <img src=\"../resources/images/banner-the-webscraping-club.png\" alt=\"The Web Scraping Club\" />\n</a>\n\n<sub>在 <b><a href=\"https://substack.thewebscraping.club/p/pydoll-webdriver-scraping?utm_source=github&utm_medium=repo&utm_campaign=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">The Web Scraping Club</a></b> 上阅读 Pydoll 的完整评测，这是排名第一的网页抓取专属通讯。</sub>\n\n## 赞助商\n\n赞助商的支持对于项目的持续发展至关重要。每一份合作都能帮助我们覆盖基础成本、推动新功能迭代，并保证项目长期维护与更新。非常感谢所有相信并支持 Pydoll 的伙伴！\n\n<div class=\"sponsors-grid\">\n  <a href=\"https://www.thordata.com/?ls=github&lk=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n    <img src=\"../resources/images/Thordata-logo.png\" alt=\"Thordata\" />\n  </a>\n  <a href=\"https://www.testmuai.com/?utm_medium=sponsor&utm_source=pydoll\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n    <img src=\"../resources/images/logo-lamda-test.svg\" alt=\"LambdaTest\" />\n  </a>\n  <a href=\"https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc\" target=\"_blank\" rel=\"noopener nofollow sponsored\">\n    <img src=\"../resources/images/capsolver-logo.png\" alt=\"CapSolver\" />\n  </a>\n</div>\n\n<p>\n  <a href=\"https://github.com/sponsors/thalissonvs\" target=\"_blank\" rel=\"noopener\">成为赞助商</a>\n</p>\n\n## 许可证\n\nPydoll 遵循 MIT 许可证（完整文本见 LICENSE 文件），主要授权条款包括：  \n\n1. 权利授予  \n   - 永久、全球范围、免版税的使用权  \n   - 允许修改创作衍生作品  \n   - 可再授权给第三方  \n\n2. 唯一责任限制  \n   - 所有修改件必须保留原版权声明  \n   - 不提供任何明示或默示担保  \n\n??? info \"View Full MIT License Text\"\n    ```\n    MIT License\n    \n    Copyright (c) 2023 Pydoll Contributors\n    \n    Permission is hereby granted, free of charge, to any person obtaining a copy\n    of this software and associated documentation files (the \"Software\"), to deal\n    in the Software without restriction, including without limitation the rights\n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n    copies of the Software, and to permit persons to whom the Software is\n    furnished to do so, subject to the following conditions:\n    \n    The above copyright notice and this permission notice shall be included in all\n    copies or substantial portions of the Software.\n    \n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n    SOFTWARE.\n    ```\n"
  },
  {
    "path": "examples/cloudflare_bypass.py",
    "content": "import asyncio\n\nfrom pydoll.browser import Chrome\n\n\nasync def example_with_context_manager():\n    \"\"\"\n    Example using the context manager approach to handle Cloudflare captcha.\n\n    This waits for the captcha to be processed before continuing.\n    \"\"\"\n    browser = Chrome()\n    await browser.start()\n    page = await browser.get_page()\n\n    print('Using context manager approach...')\n    async with page.expect_and_bypass_cloudflare_captcha():\n        await page.go_to('https://www.planetminecraft.com/account/sign_in/')\n        print('Page loaded, waiting for captcha to be handled...')\n\n    print('Captcha handling completed, now we can continue...')\n    await asyncio.sleep(3)\n    await browser.stop()\n\n\nasync def example_with_enable_disable():\n    \"\"\"\n    Example using the enable/disable approach to handle Cloudflare captcha.\n\n    This enables the auto-solving and continues execution immediately.\n    The captcha will be solved in the background when it appears.\n    \"\"\"\n    browser = Chrome()\n    await browser.start()\n    page = await browser.get_page()\n\n    print('Using enable/disable approach...')\n\n    # Enable automatic captcha solving before navigating\n    await page.enable_auto_solve_cloudflare_captcha()\n\n    # Navigate to the page - captcha will be handled automatically\n    await page.go_to('https://www.planetminecraft.com/account/sign_in/')\n    print('Page loaded, captcha will be handled in the background...')\n\n    # Continue with other operations immediately\n    # The captcha will be solved in the background when it appears\n    await asyncio.sleep(5)\n\n    # Disable auto-solving when no longer needed\n    await page.disable_auto_solve_cloudflare_captcha()\n    print('Auto-solving disabled')\n\n    await browser.stop()\n\n\nasync def main():\n    # Choose which example to run\n    await example_with_context_manager()\n    # Or uncomment the line below to run the enable/disable example\n    # await example_with_enable_disable()\n\n\nif __name__ == '__main__':\n    asyncio.run(main())\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: Pydoll - Async Web Automation Library\nsite_url: https://pydoll.tech/docs/\nrepo_url: https://github.com/autoscrape-labs/pydoll\nrepo_name: autoscrape-labs/pydoll\nuse_directory_urls: true\ndocs_dir: docs\n\n# Configuração da navegação\nnav:\n  - Pydoll: index.md\n  - Features:\n      - Overview: features/index.md\n      - Core Concepts: features/core-concepts.md\n      - Element Finding: features/element-finding.md\n      - Automation:\n          - Human-Like Interactions: features/automation/human-interactions.md\n          - Keyboard Control: features/automation/keyboard-control.md\n          - Mouse Control: features/automation/mouse-control.md\n          - File Operations: features/automation/file-operations.md\n          - IFrames: features/automation/iframes.md\n          - Screenshots & PDF: features/automation/screenshots-and-pdfs.md\n      - Network:\n          - Network Monitoring: features/network/monitoring.md\n          - Request Interception: features/network/interception.md\n          - Browser-Context HTTP Requests: features/network/http-requests.md\n          - HAR Network Recording: features/network/network-recording.md\n      - Browser Management:\n          - Multi-Tab Management: features/browser-management/tabs.md\n          - Browser Contexts: features/browser-management/contexts.md\n          - Cookies & Sessions: features/browser-management/cookies-sessions.md\n      - Configuration:\n          - Browser Options: features/configuration/browser-options.md\n          - Browser Preferences: features/configuration/browser-preferences.md\n          - Proxy Configuration: features/configuration/proxy.md\n      - Advanced:\n          - Behavioral Captcha Bypass: features/advanced/behavioral-captcha-bypass.md\n          - Event System: features/advanced/event-system.md\n          - Remote Connections: features/advanced/remote-connections.md\n          - Retry Decorator: features/advanced/decorators.md\n  - Deep Dive:\n      - Overview: deep-dive/index.md\n      - Core Fundamentals:\n          - Overview: deep-dive/fundamentals/index.md\n          - Chrome DevTools Protocol: deep-dive/fundamentals/cdp.md\n          - Connection Layer: deep-dive/fundamentals/connection-layer.md\n          - Python Type System: deep-dive/fundamentals/typing-system.md\n          - Iframes & Contexts: deep-dive/fundamentals/iframes-and-contexts.md\n      - Internal Architecture:\n          - Overview: deep-dive/architecture/index.md\n          - Browser Domain: deep-dive/architecture/browser-domain.md\n          - Tab Domain: deep-dive/architecture/tab-domain.md\n          - WebElement Domain: deep-dive/architecture/webelement-domain.md\n          - FindElements Mixin: deep-dive/architecture/find-elements-mixin.md\n          - Event Architecture: deep-dive/architecture/event-architecture.md\n          - Browser Requests Architecture: deep-dive/architecture/browser-requests-architecture.md\n          - Shadow DOM: deep-dive/architecture/shadow-dom.md\n      - Network & Security:\n          - Overview: deep-dive/network/index.md\n          - Network Fundamentals: deep-dive/network/network-fundamentals.md\n          - HTTP/HTTPS Proxies: deep-dive/network/http-proxies.md\n          - SOCKS Proxies: deep-dive/network/socks-proxies.md\n          - Proxy Detection: deep-dive/network/proxy-detection.md\n          - Building Proxy Servers: deep-dive/network/build-proxy.md\n          - Legal & Ethical: deep-dive/network/proxy-legal.md\n      - Fingerprinting:\n          - Overview: deep-dive/fingerprinting/index.md\n          - Network Fingerprinting: deep-dive/fingerprinting/network-fingerprinting.md\n          - Browser Fingerprinting: deep-dive/fingerprinting/browser-fingerprinting.md\n          - Behavioral Fingerprinting: deep-dive/fingerprinting/behavioral-fingerprinting.md\n          - Evasion Techniques: deep-dive/fingerprinting/evasion-techniques.md\n      - Practical Guides:\n          - Overview: deep-dive/guides/index.md\n          - CSS Selectors vs XPath: deep-dive/guides/selectors-guide.md\n  - API Reference:\n      - Overview: api/index.md\n      - Browser:\n          - Chrome: api/browser/chrome.md\n          - Edge: api/browser/edge.md\n          - Options: api/browser/options.md\n          - Tab: api/browser/tab.md\n          - Requests: api/browser/requests.md\n          - Managers: api/browser/managers.md\n      - Elements:\n          - WebElement: api/elements/web_element.md\n          - ShadowRoot: api/elements/shadow_root.md\n          - Mixins: api/elements/mixins.md\n      - Connection:\n          - Connection Handler: api/connection/connection.md\n          - Managers: api/connection/managers.md\n      - Commands:\n          - Overview: api/commands/index.md\n          - Browser: api/commands/browser.md\n          - DOM: api/commands/dom.md\n          - Input: api/commands/input.md\n          - Network: api/commands/network.md\n          - Page: api/commands/page.md\n          - Runtime: api/commands/runtime.md\n          - Storage: api/commands/storage.md\n          - Target: api/commands/target.md\n          - Fetch: api/commands/fetch.md\n      - Protocol:\n          - Base Types: api/protocol/base.md\n          - Browser: api/protocol/browser.md\n          - DOM: api/protocol/dom.md\n          - Fetch: api/protocol/fetch.md\n          - Input: api/protocol/input.md\n          - Network: api/protocol/network.md\n          - Page: api/protocol/page.md\n          - Runtime: api/protocol/runtime.md\n          - Storage: api/protocol/storage.md\n          - Target: api/protocol/target.md\n      - Core:\n          - Constants: api/core/constants.md\n          - Exceptions: api/core/exceptions.md\n          - Utils: api/core/utils.md\n\ntheme:\n  name: material\n  font:\n    text: Roboto\n    code: Roboto Mono\n  palette:\n    - media: \"(prefers-color-scheme: dark)\"\n      scheme: slate\n      primary: custom\n      accent: indigo\n      toggle:\n        icon: material/toggle-switch-off-outline\n        name: Dark Mode\n  \n    - media: \"(prefers-color-scheme: light)\"\n      scheme: default\n      primary: custom\n      accent: indigo\n      toggle:\n        icon: material/toggle-switch\n        name: Light Mode\n  icon:\n    repo: fontawesome/brands/github\n    logo: material/lightning-bolt\n  favicon: resources/images/favicon.png\n  features:\n    - navigation.tabs\n    - navigation.tabs.sticky\n    - navigation.sections\n    - navigation.indexes\n    - navigation.expand\n    - navigation.path\n    - navigation.top\n    - toc.follow\n    - content.code.copy\n    - content.code.select\n    - content.tooltips\n    - search.highlight\n    - search.suggest\n  \nplugins:\n  - search\n  - i18n:\n      docs_structure: folder\n      fallback_to_default: true\n      reconfigure_material: false\n      reconfigure_search: true\n      languages:\n        - locale: en\n          default: true\n          name: English\n          build: true\n          site_name: \"Pydoll - Async Web Automation Library\"\n        - locale: zh\n          name: 中文\n          build: true\n          site_name: \"Pydoll - 异步网页自动化库\"\n          nav_translations:\n            Pydoll: Pydoll\n            Features: 特性\n            Overview: 概述\n            Core Concepts: 核心概念\n            Element Finding: 元素查找\n            Automation: 自动化\n            Human-Like Interactions: 类人交互\n            Keyboard Control: 键盘控制\n            Mouse Control: 鼠标控制\n            File Operations: 文件操作\n            IFrames: IFrame交互\n            Screenshots & PDF: 截图与PDF\n            Network: 网络\n            Network Monitoring: 网络监控\n            Request Interception: 请求拦截\n            Browser-Context HTTP Requests: 浏览器上下文HTTP请求\n            HAR Network Recording: HAR网络录制\n            Browser Management: 浏览器管理\n            Multi-Tab Management: 多标签页管理\n            Browser Contexts: 浏览器上下文\n            Cookies & Sessions: Cookie与会话\n            Configuration: 配置\n            Browser Options: 浏览器选项\n            Browser Preferences: 浏览器偏好设置\n            Proxy Configuration: 代理配置\n            Advanced: 高级功能\n            Behavioral Captcha Bypass: 行为验证码绕过\n            Event System: 事件系统\n            Remote Connections: 远程连接\n            Retry Decorator: Retry 装饰器\n            Deep Dive: 深入了解\n            Core Fundamentals: 核心基础\n            Chrome DevTools Protocol: Chrome DevTools 协议\n            Connection Layer: 连接层\n            Python Type System: Python类型系统\n            Internal Architecture: 内部架构\n            Browser Domain: 浏览器域\n            Tab Domain: 标签页域\n            WebElement Domain: Web元素域\n            FindElements Mixin: 查找元素混合器\n            Event Architecture: 事件架构\n            Browser Requests Architecture: 浏览器请求架构\n            Shadow DOM: Shadow DOM 架构\n            Network & Security: 网络与安全\n            Network Fundamentals: 网络基础\n            HTTP/HTTPS Proxies: HTTP/HTTPS 代理\n            SOCKS Proxies: SOCKS 代理\n            Proxy Detection: 代理检测\n            Building Proxy Servers: 构建代理服务器\n            Legal & Ethical: 法律与道德\n            Fingerprinting: 指纹识别\n            Network Fingerprinting: 网络指纹识别\n            Browser Fingerprinting: 浏览器指纹识别\n            Behavioral Fingerprinting: 行为指纹识别\n            Evasion Techniques: 规避技术\n            Practical Guides: 实用指南\n            CSS Selectors vs XPath: CSS选择器 vs XPath\n            API Reference: API 参考\n            Browser: 浏览器\n            Chrome: Chrome\n            Edge: Edge\n            Options: 选项\n            Tab: 标签页\n            Requests: 请求\n            Managers: 管理器\n            Elements: 元素\n            WebElement: Web元素\n            ShadowRoot: Shadow根\n            Mixins: 混合器\n            Connection: 连接\n            Connection Handler: 连接处理器\n            Commands: 命令\n            DOM: DOM\n            Input: 输入\n            Network: 网络\n            Page: 页面\n            Runtime: 运行时\n            Storage: 存储\n            Target: 目标\n            Fetch: 获取\n            Protocol: 协议\n            Base Types: 基础类型\n            Overview: 概述\n            Events: 事件\n            Browser: 浏览器\n            DOM: DOM\n            Fetch: 获取\n            Input: 输入\n            Network: 网络\n            Page: 页面\n            Runtime: 运行时\n            Storage: 存储\n            Target: 目标\n            Core: 核心\n            Constants: 常量\n            Exceptions: 异常\n            Utils: 工具\n        - locale: pt\n          name: Português (BR)\n          build: true\n          site_name: \"Pydoll - Biblioteca de Automação Web Assíncrona\"\n          nav_translations:\n            Pydoll: Pydoll\n            Features: Recursos\n            Overview: Visão Geral\n            Core Concepts: Conceitos Fundamentais\n            Element Finding: Pesquisa de Elementos\n            Automation: Automação\n            Human-Like Interactions: Interações Humanas\n            Keyboard Control: Controle de Teclado\n            Mouse Control: Controle do Mouse\n            File Operations: Operações com Arquivos\n            IFrames: IFrames\n            Screenshots & PDF: Capturas e PDF\n            Network: Rede\n            Network Monitoring: Monitoramento de Rede\n            Request Interception: Interceptação de Requisições\n            Browser-Context HTTP Requests: Requisições HTTP no Contexto do Navegador\n            HAR Network Recording: Gravação de Rede HAR\n            Browser Management: Gerenciamento do Navegador\n            Multi-Tab Management: Gerenciamento de Abas\n            Browser Contexts: Contextos do Navegador\n            Cookies & Sessions: Cookies e Sessões\n            Configuration: Configuração\n            Browser Options: Opções do Navegador\n            Browser Preferences: Preferências do Navegador\n            Proxy Configuration: Configuração de Proxy\n            Advanced: Avançado\n            Behavioral Captcha Bypass: Bypass de Captcha Comportamental\n            Event System: Sistema de Eventos\n            Remote Connections: Conexões Remotas\n            Retry Decorator: Decorator Retry\n            Deep Dive: Análise Profunda\n            Core Fundamentals: Fundamentos Centrais\n            Chrome DevTools Protocol: Protocolo Chrome DevTools\n            Connection Layer: Camada de Conexão\n            Python Type System: Sistema de Tipos Python\n            Internal Architecture: Arquitetura Interna\n            Browser Domain: Domínio do Navegador\n            Tab Domain: Domínio da Aba\n            WebElement Domain: Domínio do WebElement\n            FindElements Mixin: Mixin FindElements\n            Event Architecture: Arquitetura de Eventos\n            Browser Requests Architecture: Arquitetura de Requisições do Navegador\n            Shadow DOM: Arquitetura Shadow DOM\n            Network & Security: Rede e Segurança\n            Network Fundamentals: Fundamentos de Rede\n            HTTP/HTTPS Proxies: Proxies HTTP/HTTPS\n            SOCKS Proxies: Proxies SOCKS\n            Proxy Detection: Detecção de Proxy\n            Building Proxy Servers: Construindo Servidores Proxy\n            Legal & Ethical: Legal & Ética\n            Fingerprinting: Fingerprinting\n            Network Fingerprinting: Fingerprinting de Rede\n            Browser Fingerprinting: Fingerprinting do Navegador\n            Behavioral Fingerprinting: Fingerprinting Comportamental\n            Evasion Techniques: Técnicas de Evasão\n            Practical Guides: Guias Práticos\n            CSS Selectors vs XPath: Seletores CSS vs XPath\n            API Reference: Referência da API\n            Browser: Navegador\n            Chrome: Chrome\n            Edge: Edge\n            Options: Opções\n            Tab: Aba\n            Requests: Requisições\n            Managers: Gerenciadores\n            Elements: Elementos\n            WebElement: Elemento Web\n            ShadowRoot: Shadow Root\n            Mixins: Mixins\n            Connection: Conexão\n            Connection Handler: Manipulador de Conexão\n            Commands: Comandos\n            DOM: DOM\n            Input: Entrada\n            Network: Rede\n            Page: Página\n            Runtime: Tempo de Execução\n            Storage: Armazenamento\n            Target: Alvo\n            Fetch: Fetch\n            Protocol: Protocolo\n            Base Types: Tipos Base\n            Overview: Visão Geral\n            Events: Eventos\n            Browser: Navegador\n            DOM: DOM\n            Fetch: Fetch\n            Input: Entrada\n            Network: Rede\n            Page: Página\n            Runtime: Tempo de Execução\n            Storage: Armazenamento\n            Target: Alvo\n            Core: Núcleo\n            Constants: Constantes\n            Exceptions: Exceções\n            Utils: Utilitários\n  - mkdocstrings:\n      handlers:\n        python:\n          options:\n            show_root_heading: true\n            show_if_no_docstring: true\n            inherited_members: true\n            members_order: source\n            separate_signature: true\n            filters:\n            - '!^_'\n            - '!^__'\n            merge_init_into_class: true\n            docstring_section_style: spacy\n            signature_crossrefs: true\n            show_symbol_type_heading: true\n            show_symbol_type_toc: true\n            show_source: false\n            show_bases: true\n            heading_level: 1\n\nextra:   \n  alternate:\n    - name: English\n      link: /docs/\n      lang: en\n    - name: Português (BR)\n      link: /docs/pt/\n      lang: pt\n    - name: 中文\n      link: /docs/zh/\n      lang: zh\n\nextra_css:\n  - resources/stylesheets/termynal.css\n  - resources/stylesheets/extra.css\n\nextra_javascript:\n  - resources/scripts/termynal.js\n  - resources/scripts/extra.js\n  - https://unpkg.com/mermaid@10.0.0/dist/mermaid.min.js\n\nmarkdown_extensions:\n  - pymdownx.critic\n  - pymdownx.highlight:\n      anchor_linenums: true\n      line_spans: __span\n      pygments_lang_class: true\n  - pymdownx.inlinehilite\n  - pymdownx.snippets\n  - pymdownx.superfences:\n      custom_fences:\n        - name: mermaid\n          class: mermaid\n          format: !!python/name:pymdownx.superfences.fence_code_format\n  - pymdownx.details\n  - pymdownx.keys\n  - footnotes\n  - admonition\n  - markdown.extensions.attr_list\n  - pymdownx.tabbed:\n      alternate_style: true\n  - attr_list\n  - pymdownx.emoji:\n      emoji_index: !!python/name:material.extensions.emoji.twemoji\n      emoji_generator: !!python/name:material.extensions.emoji.to_svg\n  - md_in_html"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta name=\"theme-color\" content=\"#0b1220\" />\n    <title>Pydoll - scraping, the easier way</title>\n    <meta name=\"description\" content=\"Pydoll is a Python browser automation library built on CDP with zero configuration, async performance, intuitive API, and full type safety. Simple, powerful web automation.\" />\n    <meta name=\"keywords\" content=\"pydoll, browser automation, web automation, web scraping, scraping, data scraping, data extraction, crawler, crawling, headless browser, headless chrome, chrome devtools protocol, devtools protocol, cdp, python cdp, chrome cdp, async python, asyncio, network interception, request interception, browser context requests, http requests browser context, humanized interactions, human-like interactions, automation library, scraping framework, type safety, zero configuration, concurrent automation, browser preferences, downloads, python automation, modern web scraping\" />\n    <meta name=\"robots\" content=\"index,follow\" />\n    <link rel=\"canonical\" href=\"https://pydoll.tech/\" />\n\n    <!-- Open Graph -->\n    <meta property=\"og:title\" content=\"Pydoll - scraping, the easier way\" />\n    <meta property=\"og:description\" content=\"Python browser automation with zero configuration, async performance, intuitive API, and full type safety. Simple, powerful web automation.\" />\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:image\" content=\"https://pydoll.tech/images/logo.png\" />\n    <meta property=\"og:url\" content=\"https://pydoll.tech/\" />\n    <meta property=\"og:site_name\" content=\"Pydoll\" />\n    <meta property=\"og:locale\" content=\"en_US\" />\n    <meta property=\"og:locale:alternate\" content=\"pt_BR\" />\n\n    <!-- Twitter Card -->\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta name=\"twitter:title\" content=\"Pydoll - scraping, the easier way\" />\n    <meta name=\"twitter:description\" content=\"Python browser automation with zero configuration, async performance, intuitive API, and full type safety.\" />\n    <meta name=\"twitter:image\" content=\"https://pydoll.tech/images/logo.png\" />\n\n    <!-- Performance hints -->\n    <link rel=\"preconnect\" href=\"https://cdn.tailwindcss.com\" />\n    <link rel=\"preconnect\" href=\"https://cdn.jsdelivr.net\" />\n    <link rel=\"preconnect\" href=\"https://pydoll.tech/\" />\n\n    <!-- Favicon -->\n    <link rel=\"icon\" type=\"image/png\" href=\"/images/favicon.png\" sizes=\"48x48 32x32 16x16\" />\n    <link rel=\"shortcut icon\" type=\"image/png\" href=\"/images/favicon.png\" />\n    <link rel=\"apple-touch-icon\" href=\"/images/favicon.png\" />\n\n    <!-- Tailwind CSS via CDN -->\n    <script src=\"https://cdn.tailwindcss.com\"></script>\n    <!-- Schema.org: SoftwareApplication / SoftwareSourceCode -->\n    <script type=\"application/ld+json\">\n      {\n        \"@context\": \"https://schema.org\",\n        \"@graph\": [\n          {\n            \"@type\": \"Organization\",\n            \"@id\": \"https://pydoll.tech/#org\",\n            \"name\": \"Pydoll\",\n            \"url\": \"https://pydoll.tech/\",\n            \"logo\": {\n              \"@type\": \"ImageObject\",\n              \"url\": \"https://pydoll.tech/images/logo.png\"\n            },\n            \"sameAs\": [\n              \"https://pypi.org/project/pydoll-python/\"\n            ]\n          },\n          {\n            \"@type\": \"WebSite\",\n            \"@id\": \"https://pydoll.tech/#website\",\n            \"name\": \"Pydoll\",\n            \"url\": \"https://pydoll.tech/\",\n            \"publisher\": { \"@id\": \"https://pydoll.tech/#org\" },\n            \"inLanguage\": \"en\",\n            \"potentialAction\": {\n              \"@type\": \"SearchAction\",\n              \"target\": \"https://pydoll.tech/docs/search/?q={search_term_string}\",\n              \"query-input\": \"required name=search_term_string\"\n            }\n          },\n          {\n            \"@type\": \"WebPage\",\n            \"@id\": \"https://pydoll.tech/#webpage\",\n            \"url\": \"https://pydoll.tech/\",\n            \"name\": \"Pydoll - scraping, the easier way\",\n            \"isPartOf\": { \"@id\": \"https://pydoll.tech/#website\" },\n            \"about\": { \"@id\": \"https://pydoll.tech/#software\" },\n            \"description\": \"Pydoll is a Python CDP browser automation library for web scraping, with zero configuration, async performance, and intuitive API.\"\n          },\n          {\n            \"@type\": \"SoftwareApplication\",\n            \"@id\": \"https://pydoll.tech/#software\",\n            \"name\": \"Pydoll\",\n            \"applicationCategory\": \"DeveloperApplication\",\n            \"operatingSystem\": \"Windows, macOS, Linux\",\n            \"programmingLanguage\": \"Python\",\n            \"url\": \"https://pydoll.tech/\",\n            \"image\": \"https://pydoll.tech/images/logo.png\",\n            \"description\": \"Python library for browser automation via Chrome DevTools Protocol (CDP), with zero configuration, async performance, intuitive API, and full type safety.\",\n            \"publisher\": { \"@id\": \"https://pydoll.tech/#org\" },\n            \"offers\": {\n              \"@type\": \"Offer\",\n              \"price\": 0,\n              \"priceCurrency\": \"USD\"\n            },\n            \"codeRepository\": \"https://github.com/autoscrape-labs/pydoll\",\n            \"sameAs\": [\n              \"https://pypi.org/project/pydoll-python/\"\n            ],\n            \"keywords\": [\n              \"Pydoll\",\n              \"browser automation\",\n              \"web scraping\",\n              \"Chrome DevTools Protocol\",\n              \"async Python\",\n              \"type safety\",\n              \"zero configuration\",\n              \"concurrent automation\"\n            ],\n            \"mainEntityOfPage\": {\"@id\":\"https://pydoll.tech/#webpage\"}\n          },\n          {\n            \"@type\": \"FAQPage\",\n            \"@id\": \"https://pydoll.tech/#faq\",\n            \"mainEntity\": [\n              {\n                \"@type\": \"Question\",\n                \"name\": \"What is Pydoll and why doesn't it use WebDriver?\",\n                \"acceptedAnswer\": {\n                  \"@type\": \"Answer\",\n                  \"text\": \"Pydoll is a Python library that controls the browser via the Chrome DevTools Protocol (CDP), eliminating WebDrivers. This reduces layers, improves reliability and gives direct access to page events, network interception and JavaScript execution in the real tab context.\"\n                }\n              },\n              {\n                \"@type\": \"Question\",\n                \"name\": \"What makes Pydoll's setup so simple compared to other automation tools?\",\n                \"acceptedAnswer\": {\n                  \"@type\": \"Answer\",\n                  \"text\": \"Just 'pip install pydoll-python' and you're ready. No WebDriver downloads, no PATH configuration, no version matching issues. Pydoll connects directly to Chrome via CDP, eliminating the entire driver layer that causes most automation headaches.\"\n                }\n              }\n            ]\n          }\n        ]\n      }\n      </script>\n      \n    <script>\n      tailwind.config = {\n        theme: {\n          extend: {\n            colors: {\n              brand: {\n                50: '#eef2ff',\n                100: '#e0e7ff',\n                200: '#c7d2fe',\n                300: '#a5b4fc',\n                400: '#818cf8',\n                500: '#6366f1',\n                600: '#4f46e5',\n                700: '#4338ca',\n                800: '#3730a3',\n                900: '#312e81'\n              }\n            }\n          }\n        }\n      }\n    </script>\n    <!-- Prism.js (syntax highlight) -->\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-twilight.min.css\" />\n    <style>\n      /* Harmonize code block background with Tailwind bg-slate-950 */\n      :root { --code-bg: #020617; }\n      html { scroll-behavior: smooth; }\n      /* Reveal on scroll */\n      .reveal { opacity: 0; transform: translateY(12px); transition: opacity .6s ease, transform .6s ease; will-change: transform, opacity; }\n      .revealed { opacity: 1; transform: none; }\n      /* Tilt base */\n      .tilt-card { transform-style: preserve-3d; transform: perspective(1000px); transition: transform .18s ease, box-shadow .18s ease; }\n      .tilt-card:hover { transition: transform .06s ease; }\n      pre[class*=\"language-\"], code[class*=\"language-\"] {\n        background: var(--code-bg) !important;\n        font-size: 13px !important;\n        line-height: 1.5 !important;\n      }\n      pre[class*=\"language-\"] { \n        border: 1px solid rgba(255,255,255,0.1); \n        border-radius: 8px;\n        margin: 0;\n        overflow-x: auto;\n        overflow-y: hidden; /* avoid vertical scrollbar differences (Chrome vs Firefox) */\n      }\n      /* Remove inner padding from code so pre controls spacing */\n      pre[class*=\"language-\"] > code { \n        background: transparent !important; \n        padding: 0;\n      }\n      /* Ensure syntax highlighting works */\n      .token.keyword { color: #c792ea !important; }\n      .token.string { color: #c3e88d !important; }\n      .token.function { color: #82aaff !important; }\n      .token.comment { color: #546e7a !important; }\n      .token.operator { color: #89ddff !important; }\n      .token.punctuation { color: #89ddff !important; }\n      .token.builtin { color: #ffcb6b !important; }\n      .token.class-name { color: #ffcb6b !important; }\n    </style>\n  </head>\n  <body class=\"bg-slate-950 text-slate-100 antialiased\">\n    <!-- Navbar -->\n    <header class=\"sticky top-0 z-50 backdrop-blur supports-[backdrop-filter]:bg-slate-950/60\">\n      <div class=\"mx-auto max-w-7xl px-4 sm:px-6 lg:px-8\">\n        <div class=\"flex h-16 items-center justify-between\">\n          <a href=\"/\" class=\"flex items-center gap-3\">\n            <img src=\"https://pydoll.tech/images/logo.png\" alt=\"Pydoll\" class=\"h-8 w-auto\" />\n          </a>\n          <!-- Desktop nav -->\n          <nav class=\"hidden items-center gap-2 sm:flex sm:gap-3\">\n            <a href=\"https://pydoll.tech/docs/\" class=\"px-3 py-2 text-sm font-medium text-slate-200 hover:text-white\">Docs</a>\n            <a href=\"#install\" class=\"px-3 py-2 text-sm font-medium text-slate-200 hover:text-white\">Install</a>\n            <a href=\"#sponsors\" class=\"px-3 py-2 text-sm font-medium text-slate-200 hover:text-white\">Sponsors</a>\n            <a href=\"#faq\" class=\"px-3 py-2 text-sm font-medium text-slate-200 hover:text-white\">FAQ</a>\n            <a href=\"https://github.com/sponsors/thalissonvs\" target=\"_blank\" rel=\"noopener\" class=\"px-3 py-2 text-sm font-medium text-slate-200 hover:text-white\">Sponsor</a>\n            <a href=\"https://github.com/autoscrape-labs/pydoll\" target=\"_blank\" rel=\"noopener\" aria-label=\"Star on GitHub\" class=\"inline-flex items-center gap-2 rounded-md bg-yellow-500 px-3 py-2 text-sm font-semibold text-slate-900 shadow hover:bg-yellow-400\">\n              <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 15 9 22 9 17 14 19 22 12 18 5 22 7 14 2 9 9 9 12 2\"/></svg>\n              Star\n              <span id=\"starCount\" class=\"hidden rounded px-2 py-0.5 text-slate-900 sm:inline-block\">...</span>\n            </a>\n          </nav>\n          <!-- Mobile toggle -->\n          <button id=\"mobileMenuButton\" aria-controls=\"mobileMenu\" aria-expanded=\"false\" class=\"inline-flex items-center justify-center rounded-md p-2 text-slate-200 hover:text-white hover:bg-white/5 focus:outline-none focus:ring-2 focus:ring-brand-500 sm:hidden\" type=\"button\">\n            <span class=\"sr-only\">Open main menu</span>\n            <svg id=\"iconMenu\" xmlns=\"http://www.w3.org/2000/svg\" class=\"h-6 w-6\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"3\" y1=\"6\" x2=\"21\" y2=\"6\"/><line x1=\"3\" y1=\"12\" x2=\"21\" y2=\"12\"/><line x1=\"3\" y1=\"18\" x2=\"21\" y2=\"18\"/></svg>\n            <svg id=\"iconClose\" xmlns=\"http://www.w3.org/2000/svg\" class=\"hidden h-6 w-6\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n          </button>\n        </div>\n        <!-- Mobile menu panel -->\n        <div id=\"mobileMenu\" class=\"hidden sm:hidden\">\n          <div class=\"space-y-1 border-t border-white/10 py-3\">\n            <a href=\"https://pydoll.tech/docs/\" class=\"block rounded-md px-3 py-2 text-sm font-medium text-slate-200 hover:bg-white/5\">Docs</a>\n            <a href=\"#install\" class=\"block rounded-md px-3 py-2 text-sm font-medium text-slate-200 hover:bg-white/5\">Install</a>\n            <a href=\"#sponsors\" class=\"block rounded-md px-3 py-2 text-sm font-medium text-slate-200 hover:bg-white/5\">Sponsors</a>\n            <a href=\"#faq\" class=\"block rounded-md px-3 py-2 text-sm font-medium text-slate-200 hover:bg-white/5\">FAQ</a>\n            <a href=\"https://github.com/sponsors/thalissonvs\" target=\"_blank\" rel=\"noopener\" class=\"block rounded-md px-3 py-2 text-sm font-medium text-slate-200 hover:bg-white/5\">Sponsor</a>\n            <a href=\"https://github.com/autoscrape-labs/pydoll\" target=\"_blank\" rel=\"noopener\" class=\"mt-2 inline-flex w-full items-center justify-center gap-2 rounded-md bg-yellow-500 px-3 py-2 text-sm font-semibold text-slate-900 shadow hover:bg-yellow-400\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 15 9 22 9 17 14 19 22 12 18 5 22 7 14 2 9 9 9 12 2\"/></svg> Star</a>\n          </div>\n        </div>\n      </div>\n    </header>\n\n    <!-- Hero -->\n    <section id=\"hero\" class=\"relative overflow-hidden\">\n      <div id=\"heroGradient\" class=\"pointer-events-none absolute inset-0 -z-10 h-full bg-gradient-to-b from-indigo-600/30 via-fuchsia-600/10 to-transparent blur-3xl\"></div>\n      <div id=\"cursorGlow\" aria-hidden=\"true\" class=\"pointer-events-none absolute -z-10 hidden h-72 w-72 rounded-full bg-gradient-to-tr from-brand-500/25 via-fuchsia-500/15 to-transparent blur-3xl sm:block\" style=\"left:0;top:0;\"></div>\n      <div class=\"mx-auto max-w-7xl px-4 pb-12 pt-16 sm:pb-16 sm:pt-24 lg:flex lg:items-start lg:gap-12 lg:px-8\">\n        <div class=\"mx-auto max-w-2xl lg:mx-0 lg:flex-auto\">\n          <h1 class=\"text-4xl font-bold tracking-tight sm:text-6xl\">\n            Pydoll: scraping, the easier way\n          </h1>\n          <p class=\"mt-6 text-lg leading-8 text-slate-300\">\n            Built from scratch with zero configuration complexity, Pydoll connects directly to the Chrome DevTools Protocol. \n            No WebDrivers, no setup headaches - just async performance, intuitive API, and full type safety.\n          </p>\n          <div class=\"mt-8 flex flex-col gap-3 sm:flex-row sm:items-center\">\n            <a href=\"https://github.com/autoscrape-labs/pydoll\" target=\"_blank\" rel=\"noopener\" class=\"inline-flex items-center justify-center gap-2 rounded-md bg-yellow-500 px-5 py-3 text-base font-semibold text-slate-900 shadow hover:bg-yellow-400\">\n              <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 15 9 22 9 17 14 19 22 12 18 5 22 7 14 2 9 9 9 12 2\"/></svg>\n              Star on GitHub\n            </a>\n            <a href=\"https://pydoll.tech/docs/\" class=\"inline-flex items-center justify-center gap-2 rounded-md border border-white/10 px-5 py-3 text-base font-semibold text-white/90 hover:bg-white/5\">\n              📖 View Documentation\n            </a>\n            <a href=\"#install\" class=\"inline-flex items-center justify-center gap-2 rounded-md bg-slate-800 px-5 py-3 text-base font-semibold text-white/90 hover:bg-slate-700\">\n              ⬇️ Install via pip\n            </a>\n          </div>\n          \n        </div>\n        <div class=\"mt-12 w-full lg:mt-0 lg:max-w-xl lg:flex-none reveal\">\n          <div class=\"tilt-card relative overflow-hidden rounded-xl border border-white/10 bg-slate-900/40 p-6 shadow-xl\">\n            <div class=\"space-y-4\">\n              <div class=\"flex items-center gap-3\">\n              </div>\n              <div class=\"rounded-lg bg-slate-950 p-0\">\n                <iframe \n                  class=\"w-full aspect-video rounded-lg\" \n                  src=\"https://www.youtube.com/embed/sSw5dS3dQ8k\" \n                  title=\"YouTube video player\" \n                  frameborder=\"0\" \n                  allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" \n                  allowfullscreen>\n                </iframe>\n              </div>\n              <div class=\"flex items-center justify-between text-xs text-slate-400\">\n                <span>✨ Simple, powerful, async</span>\n                <span>🚀 Ready in seconds</span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n\n    <!-- Quick Intro / Features -->\n    <section class=\"mx-auto max-w-7xl px-4 pt-10 pb-6 sm:px-6 lg:px-8\">\n      <div class=\"grid gap-6 sm:grid-cols-2 lg:grid-cols-3\">\n        <div id=\"cardZeroConfig\" role=\"button\" aria-controls=\"modalZeroConfig\" class=\"reveal tilt-card cursor-pointer rounded-xl border border-white/10 bg-slate-900/40 p-6 transition-transform duration-200 hover:bg-white/5\">\n          <h3 class=\"flex items-center gap-2 text-lg font-semibold\">\n            <svg class=\"h-5 w-5 text-slate-300\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n              <circle cx=\"12\" cy=\"12\" r=\"9\"></circle>\n              <line x1=\"5\" y1=\"5\" x2=\"19\" y2=\"19\"></line>\n            </svg>\n            <span>Zero Configuration</span>\n          </h3>\n          <p class=\"mt-2 text-sm text-slate-300\">Install and automate immediately. No drivers, no PATH variables, no setup hell.</p>\n        </div>\n        <div id=\"cardAsync\" role=\"button\" aria-controls=\"modalAsync\" class=\"reveal tilt-card cursor-pointer rounded-xl border border-white/10 bg-slate-900/40 p-6 transition-transform duration-200 hover:bg-white/5\">\n          <h3 class=\"flex items-center gap-2 text-lg font-semibold\">\n            <svg class=\"h-5 w-5 text-slate-300\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n              <polyline points=\"8,7 4,12 8,17\"></polyline>\n              <polyline points=\"16,7 20,12 16,17\"></polyline>\n            </svg>\n            <span>Async by Design</span>\n          </h3>\n          <p class=\"mt-2 text-sm text-slate-300\">Built for concurrent automation with true async/await support from day one.</p>\n        </div>\n        <div id=\"cardTypeSafety\" role=\"button\" aria-controls=\"modalTypeSafety\" class=\"reveal tilt-card cursor-pointer rounded-xl border border-white/10 bg-slate-900/40 p-6 transition-transform duration-200 hover:bg-white/5\">\n          <h3 class=\"flex items-center gap-2 text-lg font-semibold\">\n            <svg class=\"h-5 w-5 text-slate-300\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n              <path d=\"M9 12l2 2 4-4\"></path>\n              <path d=\"M21 12c0 4.97-4.03 9-9 9s-9-4.03-9-9 4.03-9 9-9c1.7 0 3.28.47 4.64 1.28\"></path>\n            </svg>\n            <span>Full Type Safety</span>\n          </h3>\n          <p class=\"mt-2 text-sm text-slate-300\">Complete type annotations. Your IDE knows exactly what each method returns.</p>\n        </div>\n        <div id=\"cardRequests\" role=\"button\" aria-controls=\"modalRequests\" class=\"reveal tilt-card cursor-pointer rounded-xl border border-white/10 bg-slate-900/40 p-6 transition-transform duration-200 hover:bg-white/5\">\n          <h3 class=\"flex items-center gap-2 text-lg font-semibold\">\n            <svg class=\"h-5 w-5 text-slate-300\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n              <circle cx=\"12\" cy=\"12\" r=\"9\"></circle>\n              <ellipse cx=\"12\" cy=\"12\" rx=\"4\" ry=\"9\"></ellipse>\n              <line x1=\"3\" y1=\"12\" x2=\"21\" y2=\"12\"></line>\n            </svg>\n            <span>Browser-context requests</span>\n          </h3>\n          <p class=\"mt-2 text-sm text-slate-300\">Use <code>tab.request</code> to automatically inherit cookies, CORS and session state.</p>\n        </div>\n        <div id=\"cardIntuitive\" role=\"button\" aria-controls=\"modalIntuitive\" class=\"reveal tilt-card cursor-pointer rounded-xl border border-white/10 bg-slate-900/40 p-6 transition-transform duration-200 hover:bg-white/5\">\n          <h3 class=\"flex items-center gap-2 text-lg font-semibold\">\n            <svg class=\"h-5 w-5 text-slate-300\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n              <line x1=\"12\" y1=\"6\" x2=\"12\" y2=\"18\"></line>\n              <line x1=\"6\" y1=\"12\" x2=\"18\" y2=\"12\"></line>\n              <circle cx=\"12\" cy=\"12\" r=\"1.5\"></circle>\n            </svg>\n            <span>Intuitive API</span>\n          </h3>\n          <p class=\"mt-2 text-sm text-slate-300\">Element finding feels like natural language. Simple, readable automation code.</p>\n        </div>\n        <div id=\"cardEvents\" role=\"button\" aria-controls=\"modalEvents\" class=\"reveal tilt-card cursor-pointer rounded-xl border border-white/10 bg-slate-900/40 p-6 transition-transform duration-200 hover:bg-white/5\">\n          <h3 class=\"flex items-center gap-2 text-lg font-semibold\">\n            <svg class=\"h-5 w-5 text-slate-300\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n              <polyline points=\"3,12 8,12 10,8 12,16 14,12 21,12\"></polyline>\n            </svg>\n            <span>Event-driven automation</span>\n          </h3>\n          <p class=\"mt-2 text-sm text-slate-300\">React to page, network and runtime events in real-time.</p>\n        </div>\n      </div>\n    </section>\n\n    <!-- Feature Modals -->\n    <div id=\"modalZeroConfig\" class=\"fixed inset-0 z-[999] hidden items-center justify-center bg-black/70 p-4\">\n      <div class=\"relative w-full max-w-3xl rounded-xl border border-white/10 bg-slate-900 p-4 sm:p-6 shadow-2xl\">\n        <button id=\"closeZeroConfigModal\" aria-label=\"Close\" class=\"absolute right-3 top-3 rounded-full border border-white/10 bg-slate-800/80 p-1.5 text-slate-300 hover:bg-slate-700 hover:text-white\">\n          <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"h-4 w-4\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n        </button>\n        <div class=\"mb-3 flex items-center justify-between pr-10\">\n          <h4 class=\"text-lg font-semibold\">Zero Configuration</h4>\n        </div>\n        <p class=\"mb-3 text-sm text-slate-300\">Install and automate with a single command. No drivers, no PATH, no setup hell.</p>\n        <pre id=\"zeroConfigCode\" class=\"language-python overflow-x-auto rounded-lg border border-white/10 bg-slate-950 p-4\"><code class=\"language-python\">from pydoll.browser import Chrome\n\nasync def run():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://example.com')\n</code></pre>\n      </div>\n    </div>\n\n    <div id=\"modalAsync\" class=\"fixed inset-0 z-[999] hidden items-center justify-center bg-black/70 p-4\">\n      <div class=\"relative w-full max-w-3xl rounded-xl border border-white/10 bg-slate-900 p-4 sm:p-6 shadow-2xl\">\n        <button id=\"closeAsyncModal\" aria-label=\"Close\" class=\"absolute right-3 top-3 rounded-full border border-white/10 bg-slate-800/80 p-1.5 text-slate-300 hover:bg-slate-700 hover:text-white\">\n          <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"h-4 w-4\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n        </button>\n        <div class=\"mb-3 flex items-center justify-between pr-10\">\n          <h4 class=\"text-lg font-semibold\">Async by Design</h4>\n        </div>\n        <p class=\"mb-3 text-sm text-slate-300\">Run multiple tabs concurrently with true async/await.</p>\n        <pre id=\"asyncCode\" class=\"language-python overflow-x-auto rounded-lg border border-white/10 bg-slate-950 p-4\"><code class=\"language-python\">import asyncio\nfrom pydoll.browser import Chrome\n\nasync def scrape(url, tab):\n    await tab.go_to(url)\n    return await tab.execute_script('return document.title')\n\nasync def main():\n    browser = Chrome()\n    tab1 = await browser.start()\n    tab2 = await browser.new_tab()\n    titles = await asyncio.gather(\n        scrape('https://google.com', tab1),\n        scrape('https://github.com', tab2),\n    )\n    print(titles)\n</code></pre>\n      </div>\n    </div>\n\n    <div id=\"modalTypeSafety\" class=\"fixed inset-0 z-[999] hidden items-center justify-center bg-black/70 p-4\">\n      <div class=\"relative w-full max-w-3xl rounded-xl border border-white/10 bg-slate-900 p-4 sm:p-6 shadow-2xl\">\n        <button id=\"closeTypeSafetyModal\" aria-label=\"Close\" class=\"absolute right-3 top-3 rounded-full border border-white/10 bg-slate-800/80 p-1.5 text-slate-300 hover:bg-slate-700 hover:text-white\">\n          <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"h-4 w-4\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n        </button>\n        <div class=\"mb-3 flex items-center justify-between pr-10\">\n          <h4 class=\"text-lg font-semibold\">Full Type Safety</h4>\n        </div>\n        <p class=\"mb-3 text-sm text-slate-300\">Your IDE knows exactly what each call returns.</p>\n        <pre id=\"typeSafetyCode\" class=\"language-python overflow-x-auto rounded-lg border border-white/10 bg-slate-950 p-4\"><code class=\"language-python\"># The IDE knows exactly what each call returns:\nawait tab.find(id='username')                 # → WebElement\nawait tab.find(id='username', find_all=True)  # → list[WebElement]\nawait tab.find(id='username', raise_exc=False)  # → WebElement | None\n</code></pre>\n      </div>\n    </div>\n\n    <div id=\"modalRequests\" class=\"fixed inset-0 z-[999] hidden items-center justify-center bg-black/70 p-4\">\n      <div class=\"relative w-full max-w-3xl rounded-xl border border-white/10 bg-slate-900 p-4 sm:p-6 shadow-2xl\">\n        <button id=\"closeRequestsModal\" aria-label=\"Close\" class=\"absolute right-3 top-3 rounded-full border border-white/10 bg-slate-800/80 p-1.5 text-slate-300 hover:bg-slate-700 hover:text-white\">\n          <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"h-4 w-4\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n        </button>\n        <div class=\"mb-3 flex items-center justify-between pr-10\">\n          <h4 class=\"text-lg font-semibold\">Browser-context requests</h4>\n        </div>\n        <p class=\"mb-3 text-sm text-slate-300\">Perform HTTP in the same context as the tab: cookies, session and CORS are inherited automatically.</p>\n        <pre id=\"requestsModalCode\" class=\"language-python overflow-x-auto rounded-lg border border-white/10 bg-slate-950 p-4\"><code class=\"language-python\">await tab.go_to('https://app.example.com/login')\nawait (await tab.find(id='email')).type_text('user@example.com')\nawait (await tab.find(id='password')).type_text('secret')\nawait (await tab.find(type='submit')).click()\n\nresponse = await tab.request.get('https://app.example.com/api/user/profile')\nprint(response.json())\n</code></pre>\n      </div>\n    </div>\n\n    <div id=\"modalIntuitive\" class=\"fixed inset-0 z-[999] hidden items-center justify-center bg-black/70 p-4\">\n      <div class=\"relative w-full max-w-3xl rounded-xl border border-white/10 bg-slate-900 p-4 sm:p-6 shadow-2xl\">\n        <button id=\"closeIntuitiveModal\" aria-label=\"Close\" class=\"absolute right-3 top-3 rounded-full border border-white/10 bg-slate-800/80 p-1.5 text-slate-300 hover:bg-slate-700 hover:text-white\">\n          <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"h-4 w-4\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n        </button>\n        <div class=\"mb-3 flex items-center justify-between pr-10\">\n          <h4 class=\"text-lg font-semibold\">Intuitive API</h4>\n        </div>\n        <p class=\"mb-3 text-sm text-slate-300\">Readable element finding using attributes, CSS or XPath.</p>\n        <pre id=\"intuitiveCode\" class=\"language-python overflow-x-auto rounded-lg border border-white/10 bg-slate-950 p-4\"><code class=\"language-python\">button = await tab.find(tag_name='button', text='Submit', class_name='btn-primary')\nawait button.click()\n\nel = await tab.query('div[data-testid=\"awesome-element\"]')\n</code></pre>\n      </div>\n    </div>\n\n    <div id=\"modalEvents\" class=\"fixed inset-0 z-[999] hidden items-center justify-center bg-black/70 p-4\">\n      <div class=\"relative w-full max-w-3xl rounded-xl border border-white/10 bg-slate-900 p-4 sm:p-6 shadow-2xl\">\n        <button id=\"closeEventsModal\" aria-label=\"Close\" class=\"absolute right-3 top-3 rounded-full border border-white/10 bg-slate-800/80 p-1.5 text-slate-300 hover:bg-slate-700 hover:text-white\">\n          <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"h-4 w-4\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n        </button>\n        <div class=\"mb-3 flex items-center justify-between pr-10\">\n          <h4 class=\"text-lg font-semibold\">Event-driven automation</h4>\n        </div>\n        <p class=\"mb-3 text-sm text-slate-300\">React to page and network changes in real-time using the built-in event system.</p>\n        <pre id=\"eventsCode\" class=\"language-python overflow-x-auto rounded-lg border border-white/10 bg-slate-950 p-4\"><code class=\"language-python\"># Enable and listen to network events\nawait tab.enable_network_events()\n\nasync def on_response(params):\n    info = params.get('response', {})\n    print('status:', info.get('status'))\n\nawait tab.on('Network.responseReceived', on_response)\n</code></pre>\n      </div>\n    </div>\n\n    <!-- Stars Goal Section -->\n    <section id=\"stars-goal\" class=\"mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8\">\n      <div class=\"reveal rounded-xl border border-white/10 bg-slate-900/60 p-5 sm:p-6 transition-transform duration-200 hover:shadow-[0_10px_30px_rgba(99,102,241,0.15)]\">\n        <div class=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n          <div class=\"min-w-0\">\n            <h3 class=\"text-xl font-semibold\">Help us reach 10k stars</h3>\n            <p class=\"mt-1 text-sm text-slate-300\">🚀 Your star means more features, more contributors, and a stronger community.</p>\n            <div class=\"mt-4\">\n              <div class=\"flex items-center justify-between text-xs text-slate-400\">\n                <span id=\"starsProgressLabel\">0 / 10,000</span>\n                <span id=\"starsProgressPct\">0%</span>\n              </div>\n              <div class=\"mt-2 h-2 w-full rounded-full bg-white/10 overflow-hidden\">\n                <div id=\"starsProgressBar\" class=\"h-2 rounded-full bg-brand-500\" style=\"width:0%\"></div>\n              </div>\n            </div>\n          </div>\n          <div class=\"flex flex-col items-start sm:items-end gap-3\">\n            <div class=\"min-w-[220px]\">\n              <p class=\"text-xs font-medium text-slate-300\">Latest stargazers</p>\n              <ul id=\"stargazersList\" class=\"mt-2 flex flex-wrap items-center gap-2\">\n                <!-- Filled via JS -->\n              </ul>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n    <!-- Install CTA -->\n    <section id=\"install\" class=\"overflow-x-hidden\">\n      <div class=\"mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8\">\n        <div class=\"grid grid-cols-1 items-center gap-8 lg:grid-cols-2\">\n          <div class=\"min-w-0\">\n            <h2 class=\"text-2xl font-bold tracking-tight\">Install and get started in seconds</h2>\n            <p class=\"mt-3 text-slate-300\">No complex setup. Install, import and automate.</p>\n            <div class=\"mt-6 rounded-lg border border-white/10 bg-slate-950 p-3 sm:p-4\">\n              <div class=\"flex items-center gap-3 flex-wrap sm:flex-nowrap\">\n                <code id=\"installCmd\" class=\"text-sm block max-w-full whitespace-nowrap overflow-x-auto\">pip install pydoll-python</code>\n                <button id=\"copyBtn\" class=\"shrink-0 rounded-md bg-slate-800 px-3 py-1.5 text-xs font-semibold text-slate-100 hover:bg-slate-700\">Copy</button>\n              </div>\n            </div>\n            <!-- Quick code example -->\n            <div class=\"mt-6\">\n              <p class=\"mb-2 text-sm text-slate-300\">Quick example:</p>\n              <pre class=\"language-python overflow-x-auto max-w-full rounded-lg border border-white/10 bg-slate-950 p-4\"><code class=\"language-python\">import asyncio\nfrom pydoll.browser import Chrome\n\nasync def main():\n    async with Chrome() as browser:\n        tab = await browser.start()\n        await tab.go_to('https://google.com')\n        \n        search_box = await tab.find(name='q')\n        await search_box.type_text('Pydoll')\n        await search_box.press_keyboard_key('Enter')\n\nasyncio.run(main())\n</code></pre>\n            </div>\n            <div class=\"mt-6 flex flex-wrap gap-3\">\n              <a href=\"https://pydoll.tech/docs/\" target=\"_blank\" rel=\"noopener\" class=\"inline-flex items-center justify-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-semibold text-slate-900 hover:bg-slate-200\">Read the docs</a>\n              <a href=\"https://github.com/sponsors/thalissonvs\" target=\"_blank\" rel=\"noopener\" class=\"inline-flex items-center justify-center gap-2 rounded-md border border-white/10 px-4 py-2 text-sm font-semibold text-white/90 hover:bg-white/5\">Support the project</a>\n            </div>\n          </div>\n          <div class=\"reveal rounded-xl p-6 shadow-xl h-full flex flex-col justify-between\">\n            <div class=\"space-y-6\">\n              <div class=\"text-center space-y-4\">\n                <div class=\"mx-auto h-16 w-16 rounded-full bg-brand-500/10 flex items-center justify-center\">\n                  <svg class=\"h-8 w-8 text-brand-400\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n                    <polyline points=\"8,7 4,12 8,17\"></polyline>\n                    <polyline points=\"16,7 20,12 16,17\"></polyline>\n                  </svg>\n                </div>\n                <h3 class=\"text-lg font-semibold text-slate-200\">Zero to automation in seconds</h3>\n                <p class=\"text-sm text-slate-300\">Install once, automate forever.<br/>No drivers, no config, no headaches.</p>\n              </div>\n              \n              <div class=\"space-y-4 border border-white/10 pt-4 p-4 rounded-lg\">\n                <div class=\"flex items-start gap-3\">\n                  <div class=\"h-2 w-2 rounded-full bg-green-400 mt-1.5\"></div>\n                  <div class=\"flex-1\">\n                    <h4 class=\"text-sm font-medium text-slate-200\">Async by design</h4>\n                    <p class=\"text-xs text-slate-400 mt-1\">Built for concurrent automation with true async/await support</p>\n                  </div>\n                </div>\n                <div class=\"flex items-start gap-3\">\n                  <div class=\"h-2 w-2 rounded-full bg-blue-400 mt-1.5\"></div>\n                  <div class=\"flex-1\">\n                    <h4 class=\"text-sm font-medium text-slate-200\">Type safe</h4>\n                    <p class=\"text-xs text-slate-400 mt-1\">Complete type annotations - your IDE knows exactly what each method returns</p>\n                  </div>\n                </div>\n                <div class=\"flex items-start gap-3\">\n                  <div class=\"h-2 w-2 rounded-full bg-purple-400 mt-1.5\"></div>\n                  <div class=\"flex-1\">\n                    <h4 class=\"text-sm font-medium text-slate-200\">CDP direct</h4>\n                    <p class=\"text-xs text-slate-400 mt-1\">No WebDrivers, no compatibility issues - direct Chrome connection</p>\n                  </div>\n                </div>\n                <div class=\"flex items-start gap-3\">\n                  <div class=\"h-2 w-2 rounded-full bg-yellow-400 mt-1.5\"></div>\n                  <div class=\"flex-1\">\n                    <h4 class=\"text-sm font-medium text-slate-200\">Intuitive API</h4>\n                    <p class=\"text-xs text-slate-400 mt-1\">Element finding feels like natural language - simple and readable</p>\n                  </div>\n                </div>\n              </div>\n            </div>\n\n          </div>\n        </div>\n      </div>\n    </section>\n\n\n    <!-- Sponsors -->\n    <section id=\"sponsors\" class=\"mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8\">\n      <div class=\"reveal rounded-xl border border-white/10 bg-slate-900/60 p-5 sm:p-6\">\n        <div class=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n          <div>\n            <h2 class=\"text-2xl font-bold tracking-tight\">Sponsors</h2>\n            <p class=\"mt-1 text-sm text-slate-300\">Companies and partners supporting Pydoll.</p>\n          </div>\n          <div>\n            <a href=\"https://github.com/sponsors/thalissonvs\" target=\"_blank\" rel=\"noopener\" class=\"inline-flex items-center gap-2 rounded-md bg-yellow-500 px-3 py-2 text-sm font-semibold text-slate-900 hover:bg-yellow-400\">Become a sponsor</a>\n          </div>\n        </div>\n        <ul id=\"sponsorsGrid\" class=\"mt-6 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3 sm:gap-4 items-center\"></ul>\n      </div>\n    </section>\n\n\n    <!-- FAQ (near footer) -->\n    <section id=\"faq\" class=\"mx-auto max-w-7xl px-4 pt-4 pb-10 sm:px-6 lg:px-8\">\n        <div class=\"mb-6\">\n          <h2 class=\"text-2xl font-bold tracking-tight\">Frequently asked questions</h2>\n          <p class=\"mt-1 text-sm text-slate-300\">Top questions about Pydoll, its features and use cases.</p>\n        </div>\n        <div class=\"grid gap-3 sm:gap-4 \">\n          <details class=\"group rounded-lg border border-white/10 bg-slate-900/40 p-4 hover:shadow-[0_10px_30px_rgba(99,102,241,0.15)]\">\n            <summary class=\"flex cursor-pointer list-none items-center gap-2 font-medium text-slate-200\">\n              <svg class=\"h-4 w-4 text-slate-400 group-open:rotate-90 transition-transform\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"9 18 15 12 9 6\"/></svg>\n              What is Pydoll and why doesn't it use WebDriver?\n            </summary>\n            <p class=\"mt-2 text-sm text-slate-300\">Pydoll is a Python library that controls the browser via the <strong>Chrome DevTools Protocol (CDP)</strong>, eliminating WebDrivers. This reduces layers, improves reliability and gives direct access to advanced capabilities like page events, network interception and JavaScript execution in the real tab context.</p>\n          </details>\n          <details class=\"group rounded-lg border border-white/10 bg-slate-900/40 p-4 hover:shadow-[0_10px_30px_rgba(99,102,241,0.15)]\">\n            <summary class=\"flex cursor-pointer list-none items-center gap-2 font-medium text-slate-200\">\n              <svg class=\"h-4 w-4 text-slate-400 group-open:rotate-90 transition-transform\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"9 18 15 12 9 6\"/></svg>\n              How does Pydoll's type safety work in practice?\n            </summary>\n            <p class=\"mt-2 text-sm text-slate-300\">Every method is fully typed with precise return annotations. Your IDE knows that <code>await tab.find(id='btn')</code> returns <code>WebElement</code>, <code>find_all=True</code> returns <code>list[WebElement]</code>, and <code>raise_exc=False</code> returns <code>WebElement | None</code>. This eliminates guesswork and catches errors before runtime.</p>\n          </details>\n          <details class=\"group rounded-lg border border-white/10 bg-slate-900/40 p-4 hover:shadow-[0_10px_30px_rgba(99,102,241,0.15)]\">\n            <summary class=\"flex cursor-pointer list-none items-center gap-2 font-medium text-slate-200\">\n              <svg class=\"h-4 w-4 text-slate-400 group-open:rotate-90 transition-transform\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"9 18 15 12 9 6\"/></svg>\n              What are “browser‑context requests” and when to use them?\n            </summary>\n            <p class=\"mt-2 text-sm text-slate-300\">With <code>tab.request</code> you perform HTTP in the <strong>same context</strong> as the tab: cookies, session, headers and CORS are automatically inherited. Ideal for hybrid automation: log in via UI and then call the app's authenticated APIs with simplicity and speed.</p>\n          </details>\n          <details class=\"group rounded-lg border border-white/10 bg-slate-900/40 p-4 hover:shadow-[0_10px_30px_rgba(99,102,241,0.15)]\">\n            <summary class=\"flex cursor-pointer list-none items-center gap-2 font-medium text-slate-200\">\n              <svg class=\"h-4 w-4 text-slate-400 group-open:rotate-90 transition-transform\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"9 18 15 12 9 6\"/></svg>\n              What makes Pydoll's setup so simple compared to other tools?\n            </summary>\n            <p class=\"mt-2 text-sm text-slate-300\">Just <code>pip install pydoll-python</code> and you're ready. No WebDriver downloads, no PATH configuration, no version matching hell. Pydoll connects directly to Chrome via CDP, eliminating the entire driver layer that causes most automation headaches.</p>\n          </details>\n          <details class=\"group rounded-lg border border-white/10 bg-slate-900/40 p-4 hover:shadow-[0_10px_30px_rgba(99,102,241,0.15)]\">\n            <summary class=\"flex cursor-pointer list-none items-center gap-2 font-medium text-slate-200\">\n              <svg class=\"h-4 w-4 text-slate-400 group-open:rotate-90 transition-transform\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"9 18 15 12 9 6\"/></svg>\n              How does concurrent automation work with multiple tabs?\n            </summary>\n            <p class=\"mt-2 text-sm text-slate-300\">Pydoll is async-first, so you can run multiple tabs simultaneously with <code>asyncio.gather</code>. Create tabs with <code>await browser.new_tab()</code>, then process them in parallel. Each tab maintains its own session and state, giving you true concurrent automation without complexity.</p>\n          </details>\n        </div>\n      </section>\n    \n    <!-- Final CTA (full width) -->\n    <section class=\"mx-auto max-w-7xl px-4 pb-10 sm:px-6 lg:px-8\">\n      <div class=\"reveal rounded-xl border border-white/10 bg-slate-900/60 p-5 sm:p-6\">\n        <div class=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n          <div>\n          <h4 class=\"text-lg font-semibold\">Enjoying Pydoll?</h4>\n          <p class=\"mt-1 text-sm text-slate-200/90\">Star it, contribute to the repo or sponsor the development.</p>\n          </div>\n          <div class=\"flex flex-wrap gap-2\">\n            <a href=\"https://github.com/autoscrape-labs/pydoll\" target=\"_blank\" rel=\"noopener\" class=\"inline-flex items-center gap-2 rounded-md bg-yellow-500 px-3 py-2 text-sm font-semibold text-slate-900 hover:bg-yellow-400\">⭐ Star</a>\n            <a href=\"https://github.com/autoscrape-labs/pydoll/blob/main/CONTRIBUTING.md\" target=\"_blank\" rel=\"noopener\" class=\"inline-flex items-center gap-2 rounded-md border border-white/10 px-3 py-2 text-sm font-semibold text-white/90 hover:bg-white/5\">👩‍💻 Contribute</a>\n            <a href=\"https://github.com/sponsors/thalissonvs\" target=\"_blank\" rel=\"noopener\" class=\"inline-flex items-center gap-2 rounded-md bg-yellow-500 px-3 py-2 text-sm font-semibold text-slate-900 hover:bg-yellow-400\">💖 Sponsor</a>\n          </div>\n        </div>\n      </div>\n    </section>\n\n    <!-- Footer -->\n    <footer class=\"border-t border-white/10\">\n      <div class=\"mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8\">\n        <div class=\"flex flex-col items-center justify-between gap-4 sm:flex-row\">\n          <div class=\"flex items-center gap-3\">\n            <img src=\"https://pydoll.tech/images/logo.png\" alt=\"Pydoll\" class=\"h-6 w-auto\" />\n            <p class=\"text-sm text-slate-400\">Pydoll - making web automation simple and powerful</p>\n          </div>\n          <div class=\"flex items-center gap-3 text-sm text-slate-400\">\n            <a href=\"https://github.com/autoscrape-labs/pydoll\" target=\"_blank\" rel=\"noopener\" class=\"hover:text-white\">GitHub</a>\n            <span aria-hidden=\"true\">·</span>\n            <a href=\"https://pydoll.tech/docs/\" class=\"hover:text-white\">Docs</a>\n            <span aria-hidden=\"true\">·</span>\n            <a href=\"https://github.com/sponsors/thalissonvs\" target=\"_blank\" rel=\"noopener\" class=\"hover:text-white\">Sponsor</a>\n          </div>\n        </div>\n      </div>\n    </footer>\n\n    <!-- JS -->\n    <script src=\"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js\"></script>\n    <script>\n      // Force Prism highlighting after DOM load\n      document.addEventListener('DOMContentLoaded', function() {\n        if (typeof Prism !== 'undefined') {\n          Prism.highlightAll();\n        }\n      });\n    </script>\n    <script src=\"./script.js\" defer></script>\n  </body>\n  </html>\n\n\n"
  },
  {
    "path": "public/robots.txt",
    "content": "User-agent: *\nDisallow: /docs/search/\nDisallow: /docs/404.html\n\nSitemap: https://pydoll.tech/sitemap.xml\nSitemap: https://pydoll.tech/docs/sitemap.xml"
  },
  {
    "path": "public/script.js",
    "content": "// Fetch stars and forks from GitHub API and wire small UX actions\n(async () => {\n  const repo = 'autoscrape-labs/pydoll'\n  const apiUrl = `https://api.github.com/repos/${repo}`\n  let repoStarsCount = 0\n\n  // Simple localStorage cache with TTL\n  const cacheGet = (key, maxAgeMs) => {\n    try {\n      const raw = localStorage.getItem(key)\n      if (!raw) return null\n      const parsed = JSON.parse(raw)\n      if (!parsed || typeof parsed !== 'object') return null\n      if (typeof parsed.t !== 'number') return null\n      const now = Date.now()\n      if (now - parsed.t > maxAgeMs) return null\n      return parsed.v\n    } catch (_) {\n      return null\n    }\n  }\n  const cacheSet = (key, value) => {\n    try {\n      localStorage.setItem(key, JSON.stringify({ t: Date.now(), v: value }))\n    } catch (_) {}\n  }\n  const TTL = 5 * 60 * 1000 // 5 minutes\n\n  // Cursor glow effect (subtle follow)\n  const glow = document.getElementById('cursorGlow')\n  const hero = document.getElementById('hero')\n  if (glow && hero) {\n    let raf = 0\n    let targetX = 0\n    let targetY = 0\n    let currentX = 0\n    let currentY = 0\n\n    const getHeroRect = () => hero.getBoundingClientRect()\n\n    const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n\n    const move = (e) => {\n      const r = getHeroRect()\n      // Coordenadas relativas à seção do herói\n      targetX = e.clientX - r.left - glow.offsetWidth / 2\n      targetY = e.clientY - r.top - glow.offsetHeight / 2\n      if (!raf && !reduceMotion) raf = requestAnimationFrame(tick)\n    }\n    const tick = () => {\n      const lerp = (a, b, t) => a + (b - a) * t\n      currentX = lerp(currentX, targetX, 0.18)\n      currentY = lerp(currentY, targetY, 0.18)\n      glow.style.transform = `translate(${currentX}px, ${currentY}px)`\n      raf = (Math.abs(currentX - targetX) + Math.abs(currentY - targetY) > 0.5) ? requestAnimationFrame(tick) : (raf = 0)\n    }\n    hero.addEventListener('mousemove', move, { passive: true })\n  }\n\n  // Stars, goal progress and latest stargazers\n  try {\n    const cacheKey = `gh:repo:${repo}`\n    let data = cacheGet(cacheKey, TTL)\n    if (!data) {\n      const res = await fetch(apiUrl, { headers: { 'Accept': 'application/vnd.github+json' } })\n      if (res.ok) {\n        data = await res.json()\n        cacheSet(cacheKey, data)\n      }\n    }\n    if (data) {\n      const starsCount = Number(data.stargazers_count ?? 0)\n      repoStarsCount = starsCount\n      const starsFmt = starsCount.toLocaleString('pt-BR')\n\n      const starCount = document.getElementById('starCount')\n      if (starCount) starCount.textContent = `${starsFmt}`\n\n      // Update progress bar to 10k\n      const GOAL = 10000\n      const pct = Math.max(0, Math.min(100, Math.round((starsCount / GOAL) * 100)))\n      const bar = document.getElementById('starsProgressBar')\n      const label = document.getElementById('starsProgressLabel')\n      const pctLabel = document.getElementById('starsProgressPct')\n      if (bar) bar.style.width = `${pct}%`\n      if (label) label.textContent = `${starsFmt} / ${GOAL.toLocaleString('pt-BR')}`\n      if (pctLabel) pctLabel.textContent = `${pct}%`\n    }\n  } catch (_) {\n    // noop: keep placeholders on failure\n  }\n\n  // Fetch latest stargazers (last 10, newest first, fill from previous page if needed)\n  try {\n    const perPage = 10\n    const lastPage = Math.max(1, Math.ceil((repoStarsCount || 1) / perPage))\n\n    const fetchPage = async (page) => {\n      const cacheKey = `gh:stargazers:${repo}:p${page}:pp${perPage}`\n      let payload = cacheGet(cacheKey, TTL)\n      if (!payload) {\n        const res = await fetch(`https://api.github.com/repos/${repo}/stargazers?per_page=${perPage}&page=${page}`, {\n          headers: { 'Accept': 'application/vnd.github.v3.star+json' }\n        })\n        if (!res.ok) return []\n        payload = await res.json()\n        cacheSet(cacheKey, payload)\n      }\n      if (!Array.isArray(payload)) return []\n      if (payload.length && (payload[0]?.user || payload[0]?.starred_at)) {\n        return payload\n          .map((it) => ({\n            login: it?.user?.login,\n            avatar_url: it?.user?.avatar_url,\n            html_url: it?.user?.html_url || (it?.user?.login ? `https://github.com/${it.user.login}` : '#'),\n            starred_at: it?.starred_at ? Date.parse(it.starred_at) : 0\n          }))\n          .filter((u) => u.login)\n      }\n      // Fallback if server ignores star+json\n      return payload.map((u) => ({\n        login: u.login,\n        avatar_url: u.avatar_url,\n        html_url: u.html_url || (u.login ? `https://github.com/${u.login}` : '#'),\n        starred_at: 0\n      }))\n    }\n\n    let entries = await fetchPage(lastPage)\n    if (entries.length < perPage && lastPage > 1) {\n      const prev = await fetchPage(lastPage - 1)\n      entries = entries.concat(prev)\n    }\n\n    // Sort newest first and cap to perPage\n    entries.sort((a, b) => b.starred_at - a.starred_at)\n    entries = entries.slice(0, perPage)\n\n    // Render\n    const list = document.getElementById('stargazersList')\n    if (list) {\n      entries.forEach((u) => {\n        const li = document.createElement('li')\n        li.className = 'flex items-center gap-2'\n        const a = document.createElement('a')\n        a.href = u.html_url\n        a.target = '_blank'\n        a.rel = 'noopener'\n        a.className = 'group inline-flex items-center gap-2 rounded-full border border-white/10 bg-slate-800/60 px-3 py-1.5 text-sm text-slate-200 hover:bg-slate-800'\n        const img = document.createElement('img')\n        img.src = u.avatar_url\n        img.alt = u.login\n        img.width = 22\n        img.height = 22\n        img.loading = 'lazy'\n        img.decoding = 'async'\n        img.className = 'h-[22px] w-[22px] rounded-full ring-1 ring-white/10'\n        const span = document.createElement('span')\n        span.textContent = u.login\n        a.appendChild(img)\n        a.appendChild(span)\n        li.appendChild(a)\n        list.appendChild(li)\n      })\n    }\n  } catch (_) {\n    // ignore\n  }\n\n  // Copy install command\n  const copyBtn = document.getElementById('copyBtn')\n  const installCmd = document.getElementById('installCmd')\n  if (copyBtn && installCmd) {\n    copyBtn.addEventListener('click', async () => {\n      try {\n        const text = installCmd.textContent ?? ''\n        await navigator.clipboard.writeText(text)\n        const old = copyBtn.textContent\n        copyBtn.textContent = 'Copiado!'\n        setTimeout(() => (copyBtn.textContent = old), 1200)\n      } catch (_) {\n        // ignore\n      }\n    })\n  }\n\n  // Reveal on scroll\n  const revealEls = Array.from(document.querySelectorAll('.reveal'))\n  if (revealEls.length) {\n    const io = new IntersectionObserver((entries) => {\n      for (const entry of entries) {\n        if (entry.isIntersecting) {\n          entry.target.classList.add('revealed')\n          io.unobserve(entry.target)\n        }\n      }\n    }, { rootMargin: '0px 0px -10% 0px', threshold: 0.12 })\n    revealEls.forEach((el) => io.observe(el))\n  }\n\n  // Tilt cards\n  const tiltCards = Array.from(document.querySelectorAll('.tilt-card'))\n  const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n  tiltCards.forEach((card) => {\n    const bounds = () => card.getBoundingClientRect()\n    let frame = 0\n    const onMove = (e) => {\n      const r = bounds()\n      const px = (e.clientX - r.left) / r.width\n      const py = (e.clientY - r.top) / r.height\n      const rotY = (px - 0.5) * 10\n      const rotX = (0.5 - py) * 8\n      const tx = (px - 0.5) * 8\n      const ty = (py - 0.5) * 8\n      if (prefersReduced) return\n      if (!frame) frame = requestAnimationFrame(() => {\n        card.style.transform = `perspective(1000px) rotateX(${rotX.toFixed(2)}deg) rotateY(${rotY.toFixed(2)}deg) translate3d(${tx.toFixed(2)}px, ${ty.toFixed(2)}px, 0)`\n        frame = 0\n      })\n    }\n    const onLeave = () => {\n      card.style.transform = 'perspective(1000px)'\n    }\n    card.addEventListener('mousemove', onMove)\n    card.addEventListener('mouseleave', onLeave)\n  })\n\n  // Modal: Automação concorrente\n  const openModalBtn = document.getElementById('openConcurrentModal')\n  const closeModalBtn = document.getElementById('closeConcurrentModal')\n  const modal = document.getElementById('concurrentModal')\n  const copyConcurrentCodeBtn = document.getElementById('copyConcurrentCode')\n  const concurrentCodeBlock = document.getElementById('concurrentCodeBlock')\n  const toggleModal = (show) => {\n    if (!modal) return\n    modal.classList.toggle('hidden', !show)\n    modal.classList.toggle('flex', show)\n  }\n  if (openModalBtn && modal) openModalBtn.addEventListener('click', () => toggleModal(true))\n  if (closeModalBtn && modal) closeModalBtn.addEventListener('click', () => toggleModal(false))\n  if (modal) {\n    modal.addEventListener('click', (e) => { if (e.target === modal) toggleModal(false) })\n    document.addEventListener('keydown', (e) => { if (e.key === 'Escape') toggleModal(false) })\n  }\n\n  // Copy concurrent code\n  if (copyConcurrentCodeBtn && concurrentCodeBlock) {\n    copyConcurrentCodeBtn.addEventListener('click', async () => {\n      try {\n        const text = concurrentCodeBlock.innerText || concurrentCodeBlock.textContent || ''\n        await navigator.clipboard.writeText(text)\n        const old = copyConcurrentCodeBtn.textContent\n        copyConcurrentCodeBtn.textContent = 'Copiado!'\n        setTimeout(() => (copyConcurrentCodeBtn.textContent = old), 1000)\n      } catch (_) {}\n    })\n  }\n\n  // (removido: bloco redundante de cópia de preferências)\n\n  // Copy buttons for vertical cards\n  const bindCopy = (btnId, codeElId) => {\n    const btn = document.getElementById(btnId)\n    const codeEl = document.getElementById(codeElId)\n    if (!btn || !codeEl) return\n    btn.addEventListener('click', async () => {\n      try {\n        const text = codeEl.innerText || codeEl.textContent || ''\n        await navigator.clipboard.writeText(text)\n        const old = btn.textContent\n        btn.textContent = 'Copiado!'\n        setTimeout(() => (btn.textContent = old), 1000)\n      } catch (_) {}\n    })\n  }\n  bindCopy('copyConcurrentBtn', 'codeConcurrent')\n  bindCopy('copyRequestsBtn', 'codeRequests')\n  bindCopy('copyPrefsBtn', 'codePrefs')\n\n  // Mobile menu toggle\n  const mobileMenuButton = document.getElementById('mobileMenuButton')\n  const mobileMenu = document.getElementById('mobileMenu')\n  const iconMenu = document.getElementById('iconMenu')\n  const iconClose = document.getElementById('iconClose')\n  if (mobileMenuButton && mobileMenu && iconMenu && iconClose) {\n    const setExpanded = (expanded) => {\n      mobileMenuButton.setAttribute('aria-expanded', String(expanded))\n      mobileMenu.classList.toggle('hidden', !expanded)\n      iconMenu.classList.toggle('hidden', expanded)\n      iconClose.classList.toggle('hidden', !expanded)\n    }\n    mobileMenuButton.addEventListener('click', () => {\n      const isOpen = mobileMenuButton.getAttribute('aria-expanded') === 'true'\n      setExpanded(!isOpen)\n    })\n    // Close on escape and when clicking links\n    document.addEventListener('keydown', (e) => {\n      if (e.key === 'Escape') setExpanded(false)\n    })\n    mobileMenu.addEventListener('click', (e) => {\n      const target = e.target\n      if (target instanceof HTMLElement && target.tagName === 'A') setExpanded(false)\n    })\n  }\n\n  // (CTA final não requer JS adicional)\n\n  // Feature cards -> modals\n  const modalMap = [\n    { card: 'cardZeroConfig', modal: 'modalZeroConfig', close: 'closeZeroConfigModal' },\n    { card: 'cardAsync', modal: 'modalAsync', close: 'closeAsyncModal' },\n    { card: 'cardTypeSafety', modal: 'modalTypeSafety', close: 'closeTypeSafetyModal' },\n    { card: 'cardRequests', modal: 'modalRequests', close: 'closeRequestsModal' },\n    { card: 'cardIntuitive', modal: 'modalIntuitive', close: 'closeIntuitiveModal' },\n    { card: 'cardEvents', modal: 'modalEvents', close: 'closeEventsModal' },\n  ]\n\n  const toggleGenericModal = (el, show) => {\n    if (!el) return\n    el.classList.toggle('hidden', !show)\n    el.classList.toggle('flex', show)\n  }\n\n  modalMap.forEach(({ card, modal, close }) => {\n    const cardEl = document.getElementById(card)\n    const modalEl = document.getElementById(modal)\n    const closeEl = document.getElementById(close)\n\n    if (cardEl && modalEl) cardEl.addEventListener('click', () => toggleGenericModal(modalEl, true))\n    if (closeEl && modalEl) closeEl.addEventListener('click', () => toggleGenericModal(modalEl, false))\n    if (modalEl) {\n      modalEl.addEventListener('click', (e) => { if (e.target === modalEl) toggleGenericModal(modalEl, false) })\n      document.addEventListener('keydown', (e) => { if (e.key === 'Escape') toggleGenericModal(modalEl, false) })\n    }\n  })\n})()\n\n\n// You can add more sponsors by pushing new objects to this array\nconst SPONSORS = [\n  {\n    name: 'The Web Scraping Club',\n    url: 'https://substack.thewebscraping.club/p/pydoll-webdriver-scraping?utm_source=github&utm_medium=repo&utm_campaign=pydoll',\n    logo: '/images/logo-the-webscraping-club.png',\n    width: 200,\n    height: 45\n  },\n  {\n    name: 'Thordata',\n    url: 'https://www.thordata.com/?ls=github&lk=pydoll',\n    logo: '/images/Thordata-logo.png',\n    width: 200,\n    height: 45\n  },\n  {\n    name: 'LambdaTest',\n    url: 'https://www.testmuai.com/?utm_medium=sponsor&utm_source=pydoll',\n    logo: '/images/logo-lamda-test.svg',\n    width: 200,\n    height: 45\n  },\n  {\n    name: 'CapSolver',\n    url: 'https://dashboard.capsolver.com/passport/register?inviteCode=WPhTbOsbXEpc',\n    logo: '/images/capsolver-logo.png',\n    width: 200,\n    height: 45\n  }\n]\n\nfunction renderSponsors(gridId = 'sponsorsGrid') {\n  const grid = document.getElementById(gridId)\n  if (!grid || !Array.isArray(SPONSORS)) return\n\n  const frag = document.createDocumentFragment()\n  for (const s of SPONSORS) {\n    const li = document.createElement('li')\n    li.className = 'flex items-center justify-center'\n\n    const a = document.createElement('a')\n    a.href = s.url\n    a.target = '_blank'\n    a.rel = 'noopener nofollow sponsored'\n    a.className = 'group block w-full rounded-lg bg-slate-900/40 px-4 py-3 hover:bg-white/5 transition-colors'\n\n    const img = document.createElement('img')\n    img.src = s.logo\n    img.alt = s.name\n    img.loading = 'lazy'\n    img.decoding = 'async'\n    img.width = s.width || 200\n    img.height = s.height || 40\n    img.className = 'mx-auto max-h-10'\n\n    a.appendChild(img)\n    li.appendChild(a)\n    frag.appendChild(li)\n  }\n  grid.innerHTML = ''\n  grid.appendChild(frag)\n}\n\ndocument.addEventListener('DOMContentLoaded', () => {\n  renderSponsors()\n})\n\n"
  },
  {
    "path": "public/scripts/extra.js",
    "content": "function setupTermynal() {\n    document.querySelectorAll(\".use-termynal\").forEach(node => {\n        node.style.display = \"block\";\n        new Termynal(node, {\n            lineDelay: 500\n        });\n    });\n    const progressLiteralStart = \"---> 100%\";\n    const promptLiteralStart = \"$ \";\n    const customPromptLiteralStart = \"# \";\n    const termynalActivateClass = \"termy\";\n    let termynals = [];\n\n    function createTermynals() {\n        document\n            .querySelectorAll(`.${termynalActivateClass} .highlight code`)\n            .forEach(node => {\n                const text = node.textContent;\n                const lines = text.split(\"\\n\");\n                const useLines = [];\n                let buffer = [];\n                function saveBuffer() {\n                    if (buffer.length) {\n                        let isBlankSpace = true;\n                        buffer.forEach(line => {\n                            if (line) {\n                                isBlankSpace = false;\n                            }\n                        });\n                        dataValue = {};\n                        if (isBlankSpace) {\n                            dataValue[\"delay\"] = 0;\n                        }\n                        if (buffer[buffer.length - 1] === \"\") {\n                            // A last single <br> won't have effect\n                            // so put an additional one\n                            buffer.push(\"\");\n                        }\n                        const bufferValue = buffer.join(\"<br>\");\n                        dataValue[\"value\"] = bufferValue;\n                        useLines.push(dataValue);\n                        buffer = [];\n                    }\n                }\n                for (let line of lines) {\n                    if (line === progressLiteralStart) {\n                        saveBuffer();\n                        useLines.push({\n                            type: \"progress\"\n                        });\n                    } else if (line.startsWith(promptLiteralStart)) {\n                        saveBuffer();\n                        const value = line.replace(promptLiteralStart, \"\").trimEnd();\n                        useLines.push({\n                            type: \"input\",\n                            value: value\n                        });\n                    } else if (line.startsWith(\"// \")) {\n                        saveBuffer();\n                        const value = \"💬 \" + line.replace(\"// \", \"\").trimEnd();\n                        useLines.push({\n                            value: value,\n                            class: \"termynal-comment\",\n                            delay: 0\n                        });\n                    } else if (line.startsWith(customPromptLiteralStart)) {\n                        saveBuffer();\n                        const promptStart = line.indexOf(promptLiteralStart);\n                        if (promptStart === -1) {\n                            console.error(\"Custom prompt found but no end delimiter\", line)\n                        }\n                        const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, \"\")\n                        let value = line.slice(promptStart + promptLiteralStart.length);\n                        useLines.push({\n                            type: \"input\",\n                            value: value,\n                            prompt: prompt\n                        });\n                    } else {\n                        buffer.push(line);\n                    }\n                }\n                saveBuffer();\n                const div = document.createElement(\"div\");\n                node.replaceWith(div);\n                const termynal = new Termynal(div, {\n                    lineData: useLines,\n                    noInit: true,\n                    lineDelay: 500\n                });\n                termynals.push(termynal);\n            });\n    }\n\n    function loadVisibleTermynals() {\n        termynals = termynals.filter(termynal => {\n            if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) {\n                termynal.init();\n                return false;\n            }\n            return true;\n        });\n    }\n    window.addEventListener(\"scroll\", loadVisibleTermynals);\n    createTermynals();\n    loadVisibleTermynals();\n}\n\nfunction shuffle(array) {\n    var currentIndex = array.length, temporaryValue, randomIndex;\n    while (0 !== currentIndex) {\n        randomIndex = Math.floor(Math.random() * currentIndex);\n        currentIndex -= 1;\n        temporaryValue = array[currentIndex];\n        array[currentIndex] = array[randomIndex];\n        array[randomIndex] = temporaryValue;\n    }\n    return array;\n}\n\nasync function showRandomAnnouncement(groupId, timeInterval) {\n    const announceFastAPI = document.getElementById(groupId);\n    if (announceFastAPI) {\n        let children = [].slice.call(announceFastAPI.children);\n        children = shuffle(children)\n        let index = 0\n        const announceRandom = () => {\n            children.forEach((el, i) => { el.style.display = \"none\" });\n            children[index].style.display = \"block\"\n            index = (index + 1) % children.length\n        }\n        announceRandom()\n        setInterval(announceRandom, timeInterval\n        )\n    }\n}\n\nasync function main() {\n    setupTermynal();\n    showRandomAnnouncement('announce-left', 5000)\n    showRandomAnnouncement('announce-right', 10000)\n}\ndocument$.subscribe(() => {\n    main()\n})"
  },
  {
    "path": "public/scripts/termynal.js",
    "content": "/**\n * termynal.js\n * A lightweight, modern and extensible animated terminal window, using\n * async/await.\n *\n * @author Ines Montani <ines@ines.io>\n * @version 0.0.1\n * @license MIT\n */\n\n'use strict';\n\n/** Generate a terminal widget. */\nclass Termynal {\n    /**\n     * Construct the widget's settings.\n     * @param {(string|Node)=} container - Query selector or container element.\n     * @param {Object=} options - Custom settings.\n     * @param {string} options.prefix - Prefix to use for data attributes.\n     * @param {number} options.startDelay - Delay before animation, in ms.\n     * @param {number} options.typeDelay - Delay between each typed character, in ms.\n     * @param {number} options.lineDelay - Delay between each line, in ms.\n     * @param {number} options.progressLength - Number of characters displayed as progress bar.\n     * @param {string} options.progressChar – Character to use for progress bar, defaults to █.\n\t * @param {number} options.progressPercent - Max percent of progress.\n     * @param {string} options.cursor – Character to use for cursor, defaults to ▋.\n     * @param {Object[]} lineData - Dynamically loaded line data objects.\n     * @param {boolean} options.noInit - Don't initialise the animation.\n     */\n    constructor(container = '#termynal', options = {}) {\n        this.container = (typeof container === 'string') ? document.querySelector(container) : container;\n        this.pfx = `data-${options.prefix || 'ty'}`;\n        this.originalStartDelay = this.startDelay = options.startDelay\n            || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600;\n        this.originalTypeDelay = this.typeDelay = options.typeDelay\n            || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90;\n        this.originalLineDelay = this.lineDelay = options.lineDelay\n            || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500;\n        this.progressLength = options.progressLength\n            || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40;\n        this.progressChar = options.progressChar\n            || this.container.getAttribute(`${this.pfx}-progressChar`) || '█';\n\t\tthis.progressPercent = options.progressPercent\n            || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100;\n        this.cursor = options.cursor\n            || this.container.getAttribute(`${this.pfx}-cursor`) || '▋';\n        this.lineData = this.lineDataToElements(options.lineData || []);\n        this.loadLines()\n        if (!options.noInit) this.init()\n    }\n\n    loadLines() {\n        // Load all the lines and create the container so that the size is fixed\n        // Otherwise it would be changing and the user viewport would be constantly\n        // moving as she/he scrolls\n        const finish = this.generateFinish()\n        finish.style.visibility = 'hidden'\n        this.container.appendChild(finish)\n        // Appends dynamically loaded lines to existing line elements.\n        this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);\n        for (let line of this.lines) {\n            line.style.visibility = 'hidden'\n            this.container.appendChild(line)\n        }\n        const restart = this.generateRestart()\n        restart.style.visibility = 'hidden'\n        this.container.appendChild(restart)\n        this.container.setAttribute('data-termynal', '');\n    }\n\n    /**\n     * Initialise the widget, get lines, clear container and start animation.\n     */\n    init() {\n        /**\n         * Calculates width and height of Termynal container.\n         * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.\n         */\n        const containerStyle = getComputedStyle(this.container);\n        this.container.style.width = containerStyle.width !== '0px' ?\n            containerStyle.width : undefined;\n        this.container.style.minHeight = containerStyle.height !== '0px' ?\n            containerStyle.height : undefined;\n\n        this.container.setAttribute('data-termynal', '');\n        this.container.innerHTML = '';\n        for (let line of this.lines) {\n            line.style.visibility = 'visible'\n        }\n        this.start();\n    }\n\n    /**\n     * Start the animation and rener the lines depending on their data attributes.\n     */\n    async start() {\n        this.addFinish()\n        await this._wait(this.startDelay);\n\n        for (let line of this.lines) {\n            const type = line.getAttribute(this.pfx);\n            const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay;\n\n            if (type == 'input') {\n                line.setAttribute(`${this.pfx}-cursor`, this.cursor);\n                await this.type(line);\n                await this._wait(delay);\n            }\n\n            else if (type == 'progress') {\n                await this.progress(line);\n                await this._wait(delay);\n            }\n\n            else {\n                this.container.appendChild(line);\n                await this._wait(delay);\n            }\n\n            line.removeAttribute(`${this.pfx}-cursor`);\n        }\n        this.addRestart()\n        this.finishElement.style.visibility = 'hidden'\n        this.lineDelay = this.originalLineDelay\n        this.typeDelay = this.originalTypeDelay\n        this.startDelay = this.originalStartDelay\n    }\n\n    generateRestart() {\n        const restart = document.createElement('a')\n        restart.onclick = (e) => {\n            e.preventDefault()\n            this.container.innerHTML = ''\n            this.init()\n        }\n        restart.href = '#'\n        restart.setAttribute('data-terminal-control', '')\n        restart.innerHTML = \"restart ↻\"\n        return restart\n    }\n\n    generateFinish() {\n        const finish = document.createElement('a')\n        finish.onclick = (e) => {\n            e.preventDefault()\n            this.lineDelay = 0\n            this.typeDelay = 0\n            this.startDelay = 0\n        }\n        finish.href = '#'\n        finish.setAttribute('data-terminal-control', '')\n        finish.innerHTML = \"fast →\"\n        this.finishElement = finish\n        return finish\n    }\n\n    addRestart() {\n        const restart = this.generateRestart()\n        this.container.appendChild(restart)\n    }\n\n    addFinish() {\n        const finish = this.generateFinish()\n        this.container.appendChild(finish)\n    }\n\n    /**\n     * Animate a typed line.\n     * @param {Node} line - The line element to render.\n     */\n    async type(line) {\n        const chars = [...line.textContent];\n        line.textContent = '';\n        this.container.appendChild(line);\n\n        for (let char of chars) {\n            const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay;\n            await this._wait(delay);\n            line.textContent += char;\n        }\n    }\n\n    /**\n     * Animate a progress bar.\n     * @param {Node} line - The line element to render.\n     */\n    async progress(line) {\n        const progressLength = line.getAttribute(`${this.pfx}-progressLength`)\n            || this.progressLength;\n        const progressChar = line.getAttribute(`${this.pfx}-progressChar`)\n            || this.progressChar;\n        const chars = progressChar.repeat(progressLength);\n\t\tconst progressPercent = line.getAttribute(`${this.pfx}-progressPercent`)\n\t\t\t|| this.progressPercent;\n        line.textContent = '';\n        this.container.appendChild(line);\n\n        for (let i = 1; i < chars.length + 1; i++) {\n            await this._wait(this.typeDelay);\n            const percent = Math.round(i / chars.length * 100);\n            line.textContent = `${chars.slice(0, i)} ${percent}%`;\n\t\t\tif (percent>progressPercent) {\n\t\t\t\tbreak;\n\t\t\t}\n        }\n    }\n\n    /**\n     * Helper function for animation delays, called with `await`.\n     * @param {number} time - Timeout, in ms.\n     */\n    _wait(time) {\n        return new Promise(resolve => setTimeout(resolve, time));\n    }\n\n    /**\n     * Converts line data objects into line elements.\n     *\n     * @param {Object[]} lineData - Dynamically loaded lines.\n     * @param {Object} line - Line data object.\n     * @returns {Element[]} - Array of line elements.\n     */\n    lineDataToElements(lineData) {\n        return lineData.map(line => {\n            let div = document.createElement('div');\n            div.innerHTML = `<span ${this._attributes(line)}>${line.value || ''}</span>`;\n\n            return div.firstElementChild;\n        });\n    }\n\n    /**\n     * Helper function for generating attributes string.\n     *\n     * @param {Object} line - Line data object.\n     * @returns {string} - String of attributes.\n     */\n    _attributes(line) {\n        let attrs = '';\n        for (let prop in line) {\n            // Custom add class\n            if (prop === 'class') {\n                attrs += ` class=${line[prop]} `\n                continue\n            }\n            if (prop === 'type') {\n                attrs += `${this.pfx}=\"${line[prop]}\" `\n            } else if (prop !== 'value') {\n                attrs += `${this.pfx}-${prop}=\"${line[prop]}\" `\n            }\n        }\n\n        return attrs;\n    }\n}\n\n/**\n* HTML API: If current script has container(s) specified, initialise Termynal.\n*/\nif (document.currentScript.hasAttribute('data-termynal-container')) {\n    const containers = document.currentScript.getAttribute('data-termynal-container');\n    containers.split('|')\n        .forEach(container => new Termynal(container))\n}"
  },
  {
    "path": "public/sitemap.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n  <url>\n    <loc>https://pydoll.tech/</loc>\n    <lastmod>2025-11-04</lastmod>\n    <changefreq>weekly</changefreq>\n    <priority>1.0</priority>\n  </url>\n  <url>\n    <loc>https://pydoll.tech/docs/</loc>\n    <lastmod>2025-11-04</lastmod>\n    <changefreq>weekly</changefreq>\n    <priority>0.9</priority>\n  </url>\n  <url>\n    <loc>https://pydoll.tech/docs/pt/</loc>\n    <lastmod>2025-11-04</lastmod>\n    <changefreq>weekly</changefreq>\n    <priority>0.8</priority>\n  </url>\n  <url>\n    <loc>https://pydoll.tech/docs/zh/</loc>\n    <lastmod>2025-11-04</lastmod>\n    <changefreq>weekly</changefreq>\n    <priority>0.8</priority>\n  </url>\n</urlset>\n"
  },
  {
    "path": "public/stylesheets/extra.css",
    "content": ".termynal-comment {\n  color: #4a968f;\n  font-style: italic;\n  display: block;\n}\n\n.termy {\n  /* For right to left languages */\n  direction: ltr;\n}\n\n.termy [data-termynal] {\n  white-space: pre-wrap;\n}\n\n.termy .linenos {\n  display: none;\n}\n\n.label-class {\n  background-color: #1e88e5;\n  color: white;\n  padding: 2px 6px;\n  font-size: 0.75em;\n  border-radius: 4px;\n  font-family: monospace;\n}\n\n.label-attr {\n  background-color: #fb8c00;\n  color: white;\n  padding: 2px 6px;\n  font-size: 0.75em;\n  border-radius: 4px;\n  font-family: monospace;\n}\n\n.label-meth {\n  background-color: #43a047;\n  color: white;\n  padding: 2px 6px;\n  font-size: 0.75em;\n  border-radius: 4px;\n  font-family: monospace;\n}\n\n\n[data-md-color-scheme=\"default\"] {\n  --md-primary-fg-color:        #0D141C;\n  --md-primary-fg-color--light: #3a7e9d;\n  --md-primary-fg-color--dark:  #004059;\n  \n  --md-accent-fg-color: #0091d0;\n  --md-accent-bg-color: rgba(0, 145, 208, 0.1);\n  \n  /* Background color personalizado */\n  --md-default-bg-color: #E2ECED;\n}\n\n[data-md-color-scheme=\"slate\"] {\n  --md-primary-fg-color:        #2b1d43;\n  --md-primary-fg-color--light: #b4b7bc;\n  --md-primary-fg-color--dark:  #2b1d43;\n\n  --md-accent-fg-color: #8caabf;\n  --md-accent-bg-color: rgba(140, 170, 191, 0.1);\n  \n  --md-default-bg-color: #0D141C;\n  --md-default-fg-color: #ffffff;\n}\n\n\n[data-md-color-scheme=\"slate\"] .md-content h3 a,\n[data-md-color-scheme=\"slate\"] .md-content h2 a,\n[data-md-color-scheme=\"slate\"] .md-content h1 a {\n  color: inherit !important;\n  text-decoration: none;\n}\n\n[data-md-color-scheme=\"slate\"] .md-content h3 a:hover,\n[data-md-color-scheme=\"slate\"] .md-content h2 a:hover,\n[data-md-color-scheme=\"slate\"] .md-content h1 a:hover {\n  text-decoration: underline;\n  opacity: 0.8;\n}\n\n/* Corrigir links dentro de cabeçalhos no modo claro */\n[data-md-color-scheme=\"default\"] .md-content h3 a,\n[data-md-color-scheme=\"default\"] .md-content h2 a,\n[data-md-color-scheme=\"default\"] .md-content h1 a {\n  color: inherit !important; /* Herdar a cor do cabeçalho pai */\n  text-decoration: none;\n}\n\n[data-md-color-scheme=\"default\"] .md-content h3 a:hover,\n[data-md-color-scheme=\"default\"] .md-content h2 a:hover,\n[data-md-color-scheme=\"default\"] .md-content h1 a:hover {\n  text-decoration: underline;\n  opacity: 0.8;\n}\n\n/* Estilo básico para links ativos - modo claro */\n.md-nav__link--active {\n  font-weight: bold;\n  color: var(--md-accent-fg-color);\n}\n\n/* Sobrescrever cor apenas para o modo escuro */\n[data-md-color-scheme=\"slate\"] .md-nav__link--active {\n  color: #b4c0dd; /* Cor clara para contraste no modo escuro */\n}\n\n/* Logo personalizado */\n.md-header__button.md-logo img,\n.md-header__button.md-logo svg {\n  display: none;\n}\n\n.md-header__button.md-logo {\n  background-image: url('../images/logo.png');\n  background-size: contain;\n  background-repeat: no-repeat;\n  background-position: center;\n  width: 100px;\n  height: 50px;\n}\n\n.md-header__button.md-logo:before {\n  content: '';\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n\n/* Ocultar o nome do site no cabeçalho */\n.md-header__topic {\n  display: none;\n}\n\n/* Logo automático baseado no tema para a página index */\n/* Ocultar todas as imagens de logo por padrão */\n.md-content img[alt=\"Pydoll Logo\"] {\n  display: none;\n}\n\n/* Modo claro - mostrar logo roxo */\n[data-md-color-scheme=\"default\"] .md-content img[alt=\"Pydoll Logo\"] {\n  display: block;\n  content: url('../images/logo-black.png');\n}\n\n/* Modo escuro - mostrar logo cinza */\n[data-md-color-scheme=\"slate\"] .md-content img[alt=\"Pydoll Logo\"] {\n  display: block;\n  content: url('../images/logo.png');\n}\n\n/* ===== MELHORIAS DE LINKS PARA MODO ESCURO ===== */\n\n/* Links gerais no conteúdo - modo escuro */\n[data-md-color-scheme=\"slate\"] .md-content a {\n  color: #64b5f6 !important; /* Azul claro para boa visibilidade */\n  text-decoration: none;\n}\n\n[data-md-color-scheme=\"slate\"] .md-content a:hover {\n  color: #90caf9 !important; /* Azul mais claro no hover */\n  text-decoration: underline;\n}\n\n/* Links na navegação lateral - modo escuro */\n[data-md-color-scheme=\"slate\"] .md-nav__link {\n  color: #e0e0e0 !important; /* Cinza claro para links normais */\n}\n\n[data-md-color-scheme=\"slate\"] .md-nav__link:hover {\n  color: #ffffff !important; /* Branco no hover */\n}\n\n[data-md-color-scheme=\"slate\"] .md-nav__link--active {\n  color: #90caf9 !important; /* Verde claro para link ativo */\n  font-weight: bold;\n}\n\n/* Links em tabelas - modo escuro */\n[data-md-color-scheme=\"slate\"] .md-typeset table a {\n  color: #64b5f6 !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset table a:hover {\n  color: #90caf9 !important;\n}\n\n/* Links em listas - modo escuro */\n[data-md-color-scheme=\"slate\"] .md-typeset ul a,\n[data-md-color-scheme=\"slate\"] .md-typeset ol a {\n  color: #64b5f6 !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset ul a:hover,\n[data-md-color-scheme=\"slate\"] .md-typeset ol a:hover {\n  color: #90caf9 !important;\n}\n\n/* Links em admonitions (caixas de aviso) - modo escuro */\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition a {\n  color: #64b5f6 !important;\n}\n\n[data-md-color-scheme=\"slate\"] .md-typeset .admonition a:hover {\n  color: #90caf9 !important;\n}\n\n/* ===== MELHORIAS DE LINKS PARA MODO CLARO ===== */\n\n/* Links gerais no conteúdo - modo claro */\n[data-md-color-scheme=\"default\"] .md-content a {\n  color: #1976d2 !important; /* Azul escuro para boa visibilidade */\n  text-decoration: none;\n}\n\n[data-md-color-scheme=\"default\"] .md-content a:hover {\n  color: #1565c0 !important; /* Azul mais escuro no hover */\n  text-decoration: underline;\n}\n\n/* Links na navegação lateral - modo claro */\n[data-md-color-scheme=\"default\"] .md-nav__link {\n  color: #424242 !important; /* Cinza escuro para links normais */\n}\n\n[data-md-color-scheme=\"default\"] .md-nav__link:hover {\n  color: #1976d2 !important; /* Azul no hover */\n}\n\n[data-md-color-scheme=\"default\"] .md-nav__link--active {\n  color: #2e7d32 !important; /* Verde escuro para link ativo */\n  font-weight: bold;\n}\n\n/* Links em tabelas - modo claro */\n[data-md-color-scheme=\"default\"] .md-typeset table a {\n  color: #1976d2 !important;\n}\n\n[data-md-color-scheme=\"default\"] .md-typeset table a:hover {\n  color: #1565c0 !important;\n}\n\n"
  },
  {
    "path": "public/stylesheets/termynal.css",
    "content": "/**\n * termynal.js\n *\n * @author Ines Montani <ines@ines.io>\n * @version 0.0.1\n * @license MIT\n */\n\n :root {\n    --color-bg: #252a33;\n    --color-text: #eee;\n    --color-text-subtle: #a2a2a2;\n}\n\n[data-termynal] {\n    width: 750px;\n    max-width: 100%;\n    background: var(--color-bg);\n    color: var(--color-text);\n    /* font-size: 18px; */\n    font-size: 15px;\n    /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */\n    font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;\n    border-radius: 4px;\n    padding: 75px 45px 35px;\n    position: relative;\n    -webkit-box-sizing: border-box;\n            box-sizing: border-box;\n    /* Custom line-height */\n    line-height: 1.2;\n}\n\n[data-termynal]:before {\n    content: '';\n    position: absolute;\n    top: 15px;\n    left: 15px;\n    display: inline-block;\n    width: 15px;\n    height: 15px;\n    border-radius: 50%;\n    /* A little hack to display the window buttons in one pseudo element. */\n    background: #d9515d;\n    -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;\n            box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;\n}\n\n[data-termynal]:after {\n    content: 'bash';\n    position: absolute;\n    color: var(--color-text-subtle);\n    top: 5px;\n    left: 0;\n    width: 100%;\n    text-align: center;\n}\n\na[data-terminal-control] {\n    text-align: right;\n    display: block;\n    color: #aebbff;\n}\n\n[data-ty] {\n    display: block;\n    line-height: 2;\n}\n\n[data-ty]:before {\n    /* Set up defaults and ensure empty lines are displayed. */\n    content: '';\n    display: inline-block;\n    vertical-align: middle;\n}\n\n[data-ty=\"input\"]:before,\n[data-ty-prompt]:before {\n    margin-right: 0.75em;\n    color: var(--color-text-subtle);\n}\n\n[data-ty=\"input\"]:before {\n    content: '$';\n}\n\n[data-ty][data-ty-prompt]:before {\n    content: attr(data-ty-prompt);\n}\n\n[data-ty-cursor]:after {\n    content: attr(data-ty-cursor);\n    font-family: monospace;\n    margin-left: 0.5em;\n    -webkit-animation: blink 1s infinite;\n            animation: blink 1s infinite;\n}\n\n\n/* Cursor animation */\n\n@-webkit-keyframes blink {\n    50% {\n        opacity: 0;\n    }\n}\n\n@keyframes blink {\n    50% {\n        opacity: 0;\n    }\n}"
  },
  {
    "path": "pydoll/__init__.py",
    "content": ""
  },
  {
    "path": "pydoll/browser/__init__.py",
    "content": "from pydoll.browser.chromium.chrome import Chrome\nfrom pydoll.browser.chromium.edge import Edge\n\n__all__ = ['Chrome', 'Edge']\n"
  },
  {
    "path": "pydoll/browser/chromium/__init__.py",
    "content": "from pydoll.browser.chromium.chrome import Chrome\nfrom pydoll.browser.chromium.edge import Edge\n\n__all__ = [\n    'Edge',\n    'Chrome',\n]\n"
  },
  {
    "path": "pydoll/browser/chromium/base.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nimport os\nimport shutil\nimport warnings\nfrom abc import ABC, abstractmethod\nfrom contextlib import suppress\nfrom functools import partial\nfrom random import randint\nfrom typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, overload\nfrom urllib.parse import urlsplit, urlunsplit\n\nfrom pydoll.browser.managers import (\n    BrowserProcessManager,\n    ProxyManager,\n    TempDirectoryManager,\n)\nfrom pydoll.browser.tab import Tab\nfrom pydoll.commands import (\n    BrowserCommands,\n    EmulationCommands,\n    FetchCommands,\n    PageCommands,\n    RuntimeCommands,\n    StorageCommands,\n    TargetCommands,\n)\nfrom pydoll.connection import ConnectionHandler\nfrom pydoll.exceptions import (\n    BrowserNotRunning,\n    FailedToStartBrowser,\n    InvalidConnectionPort,\n    InvalidWebSocketAddress,\n    MissingTargetOrWebSocket,\n    NoValidTabFound,\n)\nfrom pydoll.protocol.browser.types import DownloadBehavior\nfrom pydoll.protocol.fetch.events import FetchEvent\nfrom pydoll.protocol.fetch.types import AuthChallengeResponseType\nfrom pydoll.utils.user_agent_parser import UserAgentParser\n\nif TYPE_CHECKING:\n    from tempfile import TemporaryDirectory\n\n    from pydoll.browser.interfaces import BrowserOptionsManager\n    from pydoll.protocol.base import Command, Response, T_CommandParams, T_CommandResponse\n    from pydoll.protocol.browser.methods import (\n        GetVersionResponse,\n        GetVersionResult,\n        GetWindowForTargetResponse,\n    )\n    from pydoll.protocol.browser.types import Bounds, PermissionType\n    from pydoll.protocol.fetch.events import RequestPausedEvent\n    from pydoll.protocol.fetch.types import HeaderEntry\n    from pydoll.protocol.network.types import (\n        Cookie,\n        CookieParam,\n        ErrorReason,\n        RequestMethod,\n        ResourceType,\n    )\n    from pydoll.protocol.storage.methods import GetCookiesResponse\n    from pydoll.protocol.target.methods import (\n        CreateBrowserContextResponse,\n        CreateTargetResponse,\n        GetBrowserContextsResponse,\n        GetTargetsResponse,\n    )\n    from pydoll.protocol.target.types import TargetInfo\n\nlogger = logging.getLogger(__name__)\n\n\nclass Browser(ABC):  # noqa: PLR0904\n    \"\"\"\n    Abstract base class for browser automation using Chrome DevTools Protocol.\n\n    Provides comprehensive browser control including lifecycle management,\n    context handling, network interception, cookie management, and CDP commands.\n    \"\"\"\n\n    def __init__(\n        self,\n        options_manager: BrowserOptionsManager,\n        connection_port: Optional[int] = None,\n    ):\n        \"\"\"\n        Initialize browser instance with configuration.\n\n        Args:\n            options_manager: Manages browser options initialization and defaults.\n                Must implement initialize_options() and add_default_arguments().\n            connection_port: CDP WebSocket port. Random port (9223-9322) if None.\n\n        Note:\n            Call start() to actually launch the browser.\n        \"\"\"\n        self._validate_connection_port(connection_port)\n        self.options = options_manager.initialize_options()\n        self._proxy_manager = ProxyManager(self.options)\n        self._connection_port = connection_port if connection_port else randint(9223, 9322)\n        self._browser_process_manager = BrowserProcessManager()\n        self._temp_directory_manager = TempDirectoryManager()\n        self._ws_address: Optional[str] = None\n        self._connection_handler = ConnectionHandler(self._connection_port)\n        self._backup_preferences_dir = ''\n        self._tabs_opened: dict[str, Tab] = {}\n        self._context_proxy_auth: dict[str, tuple[str, str]] = {}\n        logger.debug(\n            f'Browser initialized: port={self._connection_port}, '\n            f'headless={getattr(self.options, \"headless\", None)}'\n        )\n\n    async def __aenter__(self) -> 'Browser':\n        \"\"\"Async context manager entry.\"\"\"\n        logger.debug('Entering browser async context')\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        \"\"\"Async context manager exit with cleanup.\"\"\"\n        logger.debug(f'Exiting browser async context: exc_type={exc_type}')\n        if self._backup_preferences_dir:\n            logger.debug(f'Restoring backup preferences directory: {self._backup_preferences_dir}')\n            user_data_dir = self._get_user_data_dir()\n            shutil.copy2(\n                self._backup_preferences_dir,\n                os.path.join(user_data_dir, 'Default', 'Preferences'),\n            )\n        if await self._is_browser_running(timeout=2):\n            await self.stop()\n\n        await self._connection_handler.close()\n\n    async def connect(self, ws_address: str) -> Tab:\n        \"\"\"\n        Connect to browser using WebSocket address. When we set\n        the _ws_address attribute, the connection handler will use\n        this address instead of resolving it from the connection port.\n\n        Args:\n            ws_address: WebSocket address of the browser.\n\n        Returns:\n            The first tab in the list of opened tabs.\n\n        Note:\n            You are supposed to use this method only if you want to connect to a browser\n            that is already running.\n        \"\"\"\n        logger.info(f'Connecting to browser via WebSocket: {ws_address}')\n        await self._setup_ws_address(ws_address)\n        tabs = await self.get_opened_tabs()\n        logger.info(f'Connected. Tabs available: {len(tabs)}')\n        return tabs[0]\n\n    async def start(self, headless: bool = False) -> Tab:\n        \"\"\"\n        Start browser process and establish CDP connection.\n\n        Args:\n            headless: Deprecated. Use `options.headless = True` instead.\n\n        Returns:\n            Initial tab for interaction.\n\n        Raises:\n            FailedToStartBrowser: If the browser fails to start or connect.\n        \"\"\"\n        if headless:\n            warnings.warn(\n                \"The 'headless' parameter is deprecated and will be removed in a future version. \"\n                'Use `options.headless = True` instead.',\n                DeprecationWarning,\n                stacklevel=2,\n            )\n            self.options.headless = headless\n\n        binary_location = self.options.binary_location or self._get_default_binary_location()\n        logger.debug('Resolved binary location: %s', binary_location)\n\n        self._setup_user_dir()\n        logger.debug('User data directory configured')\n        proxy_config = self._proxy_manager.get_proxy_credentials()\n\n        logger.info(f'Starting browser process on port {self._connection_port}')\n        self._browser_process_manager.start_browser_process(\n            binary_location, self._connection_port, self.options.arguments\n        )\n        await self._verify_browser_running()\n        logger.info('Browser process started and responsive')\n        await self._configure_proxy(proxy_config[0], proxy_config[1])\n\n        valid_tab_id = await self._get_valid_tab_id(await self.get_targets())\n        tab = Tab(self, target_id=valid_tab_id, connection_port=self._connection_port)\n        self._tabs_opened[valid_tab_id] = tab\n        await self._apply_user_agent_override(tab)\n        logger.info(f'Initial tab attached: {valid_tab_id}')\n        return tab\n\n    async def stop(self):\n        \"\"\"\n        Stop browser process and cleanup resources.\n\n        Sends Browser.close command, terminates process, removes temp directories,\n        and closes WebSocket connections.\n\n        Raises:\n            BrowserNotRunning: If the browser is not currently running.\n        \"\"\"\n        if not await self._is_browser_running():\n            logger.error('Stop called but browser is not running')\n            raise BrowserNotRunning()\n\n        logger.info('Stopping browser process')\n        await self._execute_command(BrowserCommands.close())\n        self._browser_process_manager.stop_process()\n        await self._connection_handler.close()\n        await asyncio.sleep(0.5 if os.name == 'nt' else 0.1)\n        self._temp_directory_manager.cleanup()\n        logger.info('Browser process stopped and resources cleaned up')\n\n    async def close(self):\n        \"\"\"\n        Closes the WebSocket connection and releases resources.\n        \"\"\"\n        logger.info('Closing browser WebSocket connection')\n        await self._connection_handler.close()\n\n    async def create_browser_context(\n        self, proxy_server: Optional[str] = None, proxy_bypass_list: Optional[str] = None\n    ) -> str:\n        \"\"\"\n        Create isolated browser context (like incognito).\n\n        Browser contexts provide isolated storage and don't share session data.\n        Multiple contexts can exist simultaneously.\n\n        Args:\n            proxy_server: Optional proxy for this context only (scheme://host:port).\n            proxy_bypass_list: Comma-separated hosts that bypass proxy.\n\n        Returns:\n            Browser context ID for use with other methods.\n        \"\"\"\n        # If proxy_server contains credentials, strip them and store per-context auth\n        sanitized_proxy = proxy_server\n        extracted_auth: Optional[tuple[str, str]] = None\n        if proxy_server:\n            sanitized_proxy, extracted_auth = self._sanitize_proxy_and_extract_auth(proxy_server)\n            logger.debug(\n                f'Creating browser context with proxy: {sanitized_proxy}'\n                f'(credentials provided={bool(extracted_auth)})'\n            )\n\n        response: CreateBrowserContextResponse = await self._execute_command(\n            TargetCommands.create_browser_context(\n                proxy_server=sanitized_proxy,\n                proxy_bypass_list=proxy_bypass_list,\n            )\n        )\n        context_id = response['result']['browserContextId']\n        if extracted_auth:\n            self._context_proxy_auth[context_id] = extracted_auth\n        logger.info(f'Created browser context: {context_id}')\n        return context_id\n\n    async def delete_browser_context(self, browser_context_id: str):\n        \"\"\"\n        Delete browser context and all associated tabs/resources.\n\n        Removes all storage (cookies, localStorage, etc.) and closes all tabs.\n        The default browser context cannot be deleted.\n\n        Note:\n            Closes all associated tabs immediately.\n        \"\"\"\n        logger.info(f'Deleting browser context: {browser_context_id}')\n        return await self._execute_command(\n            TargetCommands.dispose_browser_context(browser_context_id)\n        )\n\n    async def get_browser_contexts(self) -> list[str]:\n        \"\"\"Get all browser context IDs including the default context.\"\"\"\n        response: GetBrowserContextsResponse = await self._execute_command(\n            TargetCommands.get_browser_contexts()\n        )\n        logger.debug(f'Fetched {len(response[\"result\"][\"browserContextIds\"])} browser contexts')\n        return response['result']['browserContextIds']\n\n    async def new_tab(self, url: str = '', browser_context_id: Optional[str] = None) -> Tab:\n        \"\"\"\n        Create new tab for page interaction.\n\n        Args:\n            url: Initial URL (about:blank if empty).\n            browser_context_id: Context to create tab in (default if None).\n\n        Returns:\n            Tab instance for page navigation and element interaction.\n        \"\"\"\n        logger.info(f'Creating new tab (context={browser_context_id})')\n        response: CreateTargetResponse = await self._execute_command(\n            TargetCommands.create_target(\n                browser_context_id=browser_context_id,\n            )\n        )\n        target_id = response['result']['targetId']\n        tab = Tab(self, **self._get_tab_kwargs(target_id, browser_context_id))\n        self._tabs_opened[target_id] = tab\n        await self._apply_user_agent_override(tab)\n        await self._setup_context_proxy_auth_for_tab(tab, browser_context_id)\n        if url:\n            await tab.go_to(url)\n        logger.info(f'New tab created: {target_id}')\n        return tab\n\n    async def get_targets(self) -> list[TargetInfo]:\n        \"\"\"\n        Get all active targets/pages in browser.\n\n        Targets include pages, service workers, shared workers, and browser process.\n        Useful for debugging and managing multiple tabs.\n\n        Returns:\n            List of TargetInfo objects.\n        \"\"\"\n        response: GetTargetsResponse = await self._execute_command(TargetCommands.get_targets())\n        logger.debug(f'Fetched {len(response[\"result\"][\"targetInfos\"])} targets')\n        return response['result']['targetInfos']\n\n    async def get_opened_tabs(self) -> list[Tab]:\n        \"\"\"\n        Get all opened tabs that are not extensions and have the type 'page'.\n        Tabs that are already opened will be returned as is. If a new target is opened,\n        a new Tab instance will be created.\n\n        Returns:\n            List of Tab instances. The last tab is the most recent one.\n        \"\"\"\n        targets = await self.get_targets()\n        valid_tab_targets = [\n            target\n            for target in targets\n            if target['type'] == 'page' and 'extension' not in target['url']\n        ]\n        all_target_ids = [target['targetId'] for target in valid_tab_targets]\n        existing_target_ids = list(self._tabs_opened.keys())\n        remaining_target_ids = [\n            target_id for target_id in all_target_ids if target_id not in existing_target_ids\n        ]\n        existing_tabs = [self._tabs_opened[target_id] for target_id in existing_target_ids]\n        new_tabs = []\n        for target_id in reversed(remaining_target_ids):\n            tab = Tab(self, **self._get_tab_kwargs(target_id))\n            await self._apply_user_agent_override(tab)\n            new_tabs.append(tab)\n        self._tabs_opened.update(dict(zip(remaining_target_ids, new_tabs)))\n        logger.debug(\n            f'Opened tabs resolved: existing={len(existing_tabs)}, new={len(new_tabs)}',\n        )\n        return existing_tabs + new_tabs\n\n    async def get_tab_by_target(self, target: TargetInfo) -> Tab:\n        tab = Tab(self, **self._get_tab_kwargs(target['targetId']))\n        await self._apply_user_agent_override(tab)\n        return tab\n\n    async def set_download_path(self, path: str, browser_context_id: Optional[str] = None):\n        \"\"\"Set download directory path (convenience method for set_download_behavior).\"\"\"\n        logger.info(f'Setting download path: {path} (context={browser_context_id})')\n        return await self._execute_command(\n            BrowserCommands.set_download_behavior(\n                behavior=DownloadBehavior.ALLOW,\n                download_path=path,\n                browser_context_id=browser_context_id,\n            )\n        )\n\n    async def set_download_behavior(\n        self,\n        behavior: DownloadBehavior,\n        download_path: Optional[str] = None,\n        browser_context_id: Optional[str] = None,\n        events_enabled: bool = False,\n    ):\n        \"\"\"\n        Configure download handling.\n\n        Args:\n            behavior: ALLOW (save to path), DENY (cancel), or DEFAULT.\n            download_path: Required if behavior is ALLOW.\n            browser_context_id: Context to apply to (default if None).\n            events_enabled: Generate download events for progress tracking.\n        \"\"\"\n        logger.info(\n            f'Setting download behavior: behavior={behavior},'\n            f'path={download_path}, context={browser_context_id},'\n            f'events={events_enabled}'\n        )\n        return await self._execute_command(\n            BrowserCommands.set_download_behavior(\n                behavior=behavior,\n                download_path=download_path,\n                browser_context_id=browser_context_id,\n                events_enabled=events_enabled,\n            )\n        )\n\n    async def delete_all_cookies(self, browser_context_id: Optional[str] = None):\n        \"\"\"Delete all cookies (session, persistent, third-party) from browser or context.\"\"\"\n        logger.info(f'Clearing all cookies (context={browser_context_id})')\n        return await self._execute_command(StorageCommands.clear_cookies(browser_context_id))\n\n    async def set_cookies(\n        self, cookies: list[CookieParam], browser_context_id: Optional[str] = None\n    ):\n        \"\"\"Set multiple cookies in browser or context.\"\"\"\n        logger.debug(f'Setting {len(cookies)} cookies (context={browser_context_id})')\n        return await self._execute_command(StorageCommands.set_cookies(cookies, browser_context_id))\n\n    async def get_cookies(self, browser_context_id: Optional[str] = None) -> list[Cookie]:\n        \"\"\"Get all cookies from browser or context.\n\n        Note:\n            This method does not work with native incognito mode (--incognito flag).\n            For incognito mode, use ``tab.get_cookies()`` instead.\n        \"\"\"\n        response: GetCookiesResponse = await self._execute_command(\n            StorageCommands.get_cookies(browser_context_id)\n        )\n        logger.debug(\n            f'Retrieved {len(response[\"result\"][\"cookies\"])} cookies (context={browser_context_id})'\n        )\n        return response['result']['cookies']\n\n    async def get_version(self) -> GetVersionResult:\n        \"\"\"Get browser version and CDP protocol information.\"\"\"\n        response: GetVersionResponse = await self._execute_command(BrowserCommands.get_version())\n        logger.debug(f'Browser version: {response[\"result\"]}')\n        return response['result']\n\n    async def get_window_id_for_target(self, target_id: str) -> int:\n        \"\"\"Get window ID for target (used for window manipulation via CDP).\"\"\"\n        response: GetWindowForTargetResponse = await self._execute_command(\n            BrowserCommands.get_window_for_target(target_id)\n        )\n        logger.debug(f'Window id for target {target_id}: {response[\"result\"][\"windowId\"]}')\n        return response['result']['windowId']\n\n    async def get_window_id_for_tab(self, tab: Tab) -> int:\n        \"\"\"Get window ID for tab (convenience method).\"\"\"\n        target_id = tab._target_id or (tab._ws_address.split('/')[-1] if tab._ws_address else None)\n        if not target_id:\n            logger.error('Missing target id or ws address for tab when getting window id')\n            raise MissingTargetOrWebSocket()\n        return await self.get_window_id_for_target(target_id)\n\n    async def get_window_id(self) -> int:\n        \"\"\"\n        Get window ID for any valid tab.\n\n        Raises:\n            NoValidTabFound: If no valid attached tab can be found.\n        \"\"\"\n        targets = await self.get_targets()\n        valid_tab_id = await self._get_valid_tab_id(targets)\n        return await self.get_window_id_for_target(valid_tab_id)\n\n    async def set_window_maximized(self):\n        \"\"\"Maximize browser window (affects all tabs in window).\"\"\"\n        window_id = await self.get_window_id()\n        logger.info(f'Maximizing window: id={window_id}')\n        return await self._execute_command(BrowserCommands.set_window_maximized(window_id))\n\n    async def set_window_minimized(self):\n        \"\"\"Minimize browser window to taskbar/dock.\"\"\"\n        window_id = await self.get_window_id()\n        logger.info(f'Minimizing window: id={window_id}')\n        return await self._execute_command(BrowserCommands.set_window_minimized(window_id))\n\n    async def set_window_bounds(self, bounds: Bounds):\n        \"\"\"\n        Set window position and/or size.\n\n        Args:\n            bounds: Properties to modify (left, top, width, height, windowState).\n                Only specified properties are changed.\n        \"\"\"\n        window_id = await self.get_window_id()\n        logger.info(f'Setting window bounds: id={window_id}, bounds={bounds}')\n        return await self._execute_command(BrowserCommands.set_window_bounds(window_id, bounds))\n\n    async def grant_permissions(\n        self,\n        permissions: list[PermissionType],\n        origin: Optional[str] = None,\n        browser_context_id: Optional[str] = None,\n    ):\n        \"\"\"\n        Grant browser permissions (geolocation, notifications, camera, etc.).\n\n        Bypasses normal permission prompts for automated testing.\n\n        Args:\n            permissions: Permissions to grant.\n            origin: Origin to grant to (all origins if None).\n            browser_context_id: Context to apply to (default if None).\n        \"\"\"\n        logger.info(\n            f'Granting permissions: {permissions} (origin={origin}, context={browser_context_id})',\n        )\n        return await self._execute_command(\n            BrowserCommands.grant_permissions(permissions, origin, browser_context_id)\n        )\n\n    async def reset_permissions(self, browser_context_id: Optional[str] = None):\n        \"\"\"Reset all permissions to defaults and restore prompting behavior.\"\"\"\n        logger.info(f'Resetting permissions (context={browser_context_id})')\n        return await self._execute_command(BrowserCommands.reset_permissions(browser_context_id))\n\n    @overload\n    async def on(\n        self, event_name: str, callback: Callable[[Any], Any], temporary: bool = False\n    ) -> int: ...\n    @overload\n    async def on(\n        self, event_name: str, callback: Callable[[Any], Awaitable[Any]], temporary: bool = False\n    ) -> int: ...\n    async def on(self, event_name, callback, temporary: bool = False) -> int:\n        \"\"\"\n        Register CDP event listener at browser level.\n\n        Callback runs in background task to prevent blocking. Affects all pages/targets.\n\n        Args:\n            event_name: CDP event name (e.g., \"Network.responseReceived\").\n            callback: Function called on event (sync or async).\n            temporary: Remove after first invocation.\n\n        Returns:\n            Callback ID for removal.\n\n        Note:\n            For page-specific events, use Tab.on() instead.\n        \"\"\"\n\n        async def callback_wrapper(event):\n            asyncio.create_task(callback(event))\n\n        if asyncio.iscoroutinefunction(callback):\n            function_to_register = callback_wrapper\n        else:\n            function_to_register = callback\n        logger.debug(\n            f'Registering callback: event={event_name}, temporary={temporary}, '\n            f'async={asyncio.iscoroutinefunction(callback)}'\n        )\n        return await self._connection_handler.register_callback(\n            event_name, function_to_register, temporary\n        )\n\n    async def remove_callback(self, callback_id: int):\n        \"\"\"Remove callback from browser.\"\"\"\n        logger.debug(f'Removing callback: id={callback_id}')\n        return await self._connection_handler.remove_callback(callback_id)\n\n    async def enable_fetch_events(\n        self,\n        handle_auth_requests: bool = False,\n        resource_type: Optional[ResourceType] = None,\n    ):\n        \"\"\"\n        Enable network request interception via Fetch domain.\n\n        Allows monitoring, modifying, or blocking requests before they're sent.\n        All matching requests are paused until explicitly continued.\n\n        Args:\n            handle_auth_requests: Intercept authentication challenges.\n            resource_type: Filter by type (XHR, Fetch, Document, etc.). Empty = all.\n\n        Note:\n            Paused requests must be continued or they will timeout.\n        \"\"\"\n        logger.debug(\n            f'Enabling Fetch events: handle_auth={handle_auth_requests}, '\n            f'resource_type={resource_type}'\n        )\n        return await self._connection_handler.execute_command(\n            FetchCommands.enable(\n                handle_auth_requests=handle_auth_requests,\n                resource_type=resource_type,\n            )\n        )\n\n    async def disable_fetch_events(self):\n        \"\"\"Disable request interception and release any paused requests.\"\"\"\n        logger.debug('Disabling Fetch events')\n        return await self._connection_handler.execute_command(FetchCommands.disable())\n\n    async def enable_runtime_events(self):\n        \"\"\"Enable runtime events.\"\"\"\n        logger.debug('Enabling Runtime events')\n        return await self._connection_handler.execute_command(RuntimeCommands.enable())\n\n    async def disable_runtime_events(self):\n        \"\"\"Disable runtime events.\"\"\"\n        logger.debug('Disabling Runtime events')\n        return await self._connection_handler.execute_command(RuntimeCommands.disable())\n\n    async def continue_request(\n        self,\n        request_id: str,\n        url: Optional[str] = None,\n        method: Optional[RequestMethod] = None,\n        post_data: Optional[str] = None,\n        headers: Optional[list[HeaderEntry]] = None,\n        intercept_response: Optional[bool] = None,\n    ):\n        \"\"\"\n        Continue paused request without modifications.\n        \"\"\"\n        logger.debug(f'Continuing request: id={request_id}')\n        return await self._execute_command(\n            FetchCommands.continue_request(\n                request_id=request_id,\n                url=url,\n                method=method,\n                post_data=post_data,\n                headers=headers,\n                intercept_response=intercept_response,\n            )\n        )\n\n    async def fail_request(self, request_id: str, error_reason: ErrorReason):\n        \"\"\"Fail request with error code.\"\"\"\n        logger.debug(f'Failing request: id={request_id}, reason={error_reason}')\n        return await self._execute_command(FetchCommands.fail_request(request_id, error_reason))\n\n    async def fulfill_request(\n        self,\n        request_id: str,\n        response_code: int,\n        response_headers: Optional[list[HeaderEntry]] = None,\n        body: Optional[str] = None,\n        response_phrase: Optional[str] = None,\n    ):\n        \"\"\"Fulfill request with response data.\"\"\"\n        logger.debug(\n            f'Fulfilling request: id={request_id}, code={response_code}, '\n            f'headers={bool(response_headers)}, body={bool(body)}'\n        )\n        return await self._execute_command(\n            FetchCommands.fulfill_request(\n                request_id=request_id,\n                response_code=response_code,\n                response_headers=response_headers,\n                body=body,\n                response_phrase=response_phrase,\n            )\n        )\n\n    @staticmethod\n    def _validate_connection_port(connection_port: Optional[int]):\n        \"\"\"Validate connection port.\"\"\"\n        if connection_port and connection_port < 0:\n            logger.error(f'Invalid connection port: {connection_port}')\n            raise InvalidConnectionPort()\n\n    async def _continue_request_callback(self, event: RequestPausedEvent):\n        \"\"\"Internal callback to continue paused requests.\"\"\"\n        request_id = event['params']['requestId']\n        logger.debug(f'[Fetch] REQUEST_PAUSED -> continue: id={request_id}')\n        return await self.continue_request(request_id)\n\n    async def _continue_request_with_auth_callback(\n        self,\n        event: RequestPausedEvent,\n        proxy_username: Optional[str],\n        proxy_password: Optional[str],\n    ):\n        \"\"\"Internal callback for proxy authentication.\"\"\"\n        request_id = event['params']['requestId']\n        logger.debug(\n            f'[Fetch] AUTH_REQUIRED -> provide credentials: id={request_id}, '\n            f'user_set={bool(proxy_username)}'\n        )\n        response: Response = await self._execute_command(\n            FetchCommands.continue_request_with_auth(\n                request_id,\n                auth_challenge_response=AuthChallengeResponseType.PROVIDE_CREDENTIALS,\n                proxy_username=proxy_username,\n                proxy_password=proxy_password,\n            )\n        )\n        await self.disable_fetch_events()\n        return response\n\n    @staticmethod\n    async def _tab_continue_request_callback(event: RequestPausedEvent, tab: Tab):\n        \"\"\"Internal callback to continue paused requests at Tab level.\"\"\"\n        request_id = event['params']['requestId']\n        logger.debug(f'[Tab Fetch] REQUEST_PAUSED -> continue: id={request_id}')\n        return await tab.continue_request(request_id)\n\n    @staticmethod\n    async def _tab_continue_request_with_auth_callback(\n        event: RequestPausedEvent,\n        tab: Tab,\n        proxy_username: Optional[str],\n        proxy_password: Optional[str],\n    ):\n        \"\"\"Internal callback for proxy/server authentication at Tab level.\"\"\"\n        request_id = event['params']['requestId']\n        logger.debug(\n            f'[Tab Fetch] AUTH_REQUIRED -> provide credentials: id={request_id}, '\n            f'user_set={bool(proxy_username)}'\n        )\n        response: Response = await tab.continue_with_auth(\n            request_id=request_id,\n            auth_challenge_response=AuthChallengeResponseType.PROVIDE_CREDENTIALS,\n            proxy_username=proxy_username,\n            proxy_password=proxy_password,\n        )\n        await tab.disable_fetch_events()\n        return response\n\n    async def _setup_context_proxy_auth_for_tab(\n        self, tab: Tab, browser_context_id: Optional[str]\n    ) -> None:\n        \"\"\"Enable proxy auth handling for a Tab if its context has credentials stored.\"\"\"\n        if not browser_context_id:\n            return\n        creds = self._context_proxy_auth.get(browser_context_id)\n        if not creds:\n            return\n        username, password = creds\n        logger.debug(\n            f'Enabling context-level proxy auth for tab (context={browser_context_id}, '\n            f'user_set={bool(username)}'\n        )\n        await tab.enable_fetch_events(handle_auth=True)\n        await tab.on(\n            FetchEvent.REQUEST_PAUSED,\n            partial(\n                self._tab_continue_request_callback,\n                tab=tab,\n            ),\n            temporary=True,\n        )\n        await tab.on(\n            FetchEvent.AUTH_REQUIRED,\n            partial(\n                self._tab_continue_request_with_auth_callback,\n                tab=tab,\n                proxy_username=username,\n                proxy_password=password,\n            ),\n            temporary=True,\n        )\n\n    async def _apply_user_agent_override(self, tab: Tab) -> None:\n        \"\"\"Apply consistent User-Agent override to a tab if --user-agent= is set.\n\n        Detects the --user-agent= argument in browser options and automatically\n        synchronizes HTTP headers, navigator JS properties, and Client Hints\n        via CDP Emulation.setUserAgentOverride + JS injection.\n        \"\"\"\n        user_agent = self._get_user_agent_from_options()\n        if not user_agent:\n            return\n\n        parsed = UserAgentParser.parse(user_agent)\n        logger.debug('Applying User-Agent override: %s', user_agent[:60])\n\n        await tab._execute_command(\n            EmulationCommands.set_user_agent_override(\n                user_agent=user_agent,\n                platform=parsed.platform,\n                user_agent_metadata=parsed.user_agent_metadata,\n            )\n        )\n\n        if parsed.navigator_override_js:\n            await tab._execute_command(\n                PageCommands.add_script_to_evaluate_on_new_document(\n                    source=parsed.navigator_override_js,\n                    run_immediately=True,\n                )\n            )\n\n    def _get_user_agent_from_options(self) -> Optional[str]:\n        \"\"\"Extract User-Agent value from --user-agent= browser argument.\"\"\"\n        for arg in self.options.arguments:\n            if arg.startswith('--user-agent='):\n                return arg[len('--user-agent=') :]\n        return None\n\n    async def _verify_browser_running(self):\n        \"\"\"\n        Verify browser started successfully.\n\n        Raises:\n            FailedToStartBrowser: If the browser failed to start.\n        \"\"\"\n        logger.debug(f'Verifying browser is running (timeout={self.options.start_timeout})')\n        if not await self._is_browser_running(self.options.start_timeout):\n            logger.error('Browser failed to start within timeout')\n            raise FailedToStartBrowser()\n\n    async def _configure_proxy(\n        self, private_proxy: bool, proxy_credentials: tuple[Optional[str], Optional[str]]\n    ):\n        \"\"\"Setup proxy authentication handling if needed.\"\"\"\n        if not private_proxy:\n            return\n\n        logger.debug(\n            'Configuring proxy authentication: '\n            f'credentials provided={bool(proxy_credentials[0] or proxy_credentials[1])}'\n        )\n        await self.enable_fetch_events(handle_auth_requests=True)\n        await self.on(\n            FetchEvent.REQUEST_PAUSED,\n            self._continue_request_callback,\n            temporary=True,\n        )\n        await self.on(\n            FetchEvent.AUTH_REQUIRED,\n            partial(\n                self._continue_request_with_auth_callback,\n                proxy_username=proxy_credentials[0],\n                proxy_password=proxy_credentials[1],\n            ),\n            temporary=True,\n        )\n\n    @staticmethod\n    def _is_valid_tab(target: TargetInfo) -> bool:\n        \"\"\"Check if target is a valid browser tab (filters out extensions).\"\"\"\n        return target.get('type') == 'page' and 'chrome-extension://' not in target.get('url', '')\n\n    @staticmethod\n    async def _get_valid_tab_id(targets: list[TargetInfo]) -> str:\n        \"\"\"\n        Find valid attached tab ID.\n\n        Raises:\n            NoValidTabFound: If no valid attached tab is found.\n        \"\"\"\n        valid_tab = next(\n            (\n                tab\n                for tab in targets\n                if tab.get('type') == 'page' and 'extension' not in tab.get('url', '')\n            ),\n            None,\n        )\n\n        if not valid_tab:\n            logger.error(f'No valid tab found among {len(targets)} targets')\n            raise NoValidTabFound()\n\n        tab_id = valid_tab.get('targetId')\n        if not tab_id:\n            logger.error('Valid tab missing targetId')\n            raise NoValidTabFound('Tab missing targetId')\n\n        return tab_id\n\n    async def _is_browser_running(self, timeout: int = 10) -> bool:\n        \"\"\"Check if browser process is running and CDP endpoint is responsive.\"\"\"\n        for _ in range(timeout):\n            if await self._connection_handler.ping():\n                return True\n            await asyncio.sleep(1)\n\n        return False\n\n    async def _execute_command(\n        self, command: Command[T_CommandParams, T_CommandResponse], timeout: int = 60\n    ) -> T_CommandResponse:\n        \"\"\"Execute CDP command and return result (core method for browser communication).\"\"\"\n        logger.debug(f'Executing command: {command.get(\"method\")} (timeout={timeout})')\n        return await self._connection_handler.execute_command(command, timeout=timeout)\n\n    def _setup_user_dir(self):\n        \"\"\"Setup temporary user data directory if not specified in options.\"\"\"\n        user_data_dir = self._get_user_data_dir()\n        if user_data_dir and self.options.browser_preferences:\n            self._set_browser_preferences_in_user_data_dir(user_data_dir)\n        elif not user_data_dir:\n            temp_dir = self._temp_directory_manager.create_temp_dir()\n            # For all browsers, use a temporary directory\n            self.options.arguments.append(f'--user-data-dir={temp_dir.name}')\n            if self.options.browser_preferences:\n                self._set_browser_preferences_in_temp_dir(temp_dir)\n        logger.debug(f'User dir setup complete: {self._get_user_data_dir()}')\n\n    def _set_browser_preferences_in_temp_dir(self, temp_dir: TemporaryDirectory):\n        os.mkdir(os.path.join(temp_dir.name, 'Default'))\n        preferences = self.options.browser_preferences\n        with open(\n            os.path.join(temp_dir.name, 'Default', 'Preferences'), 'w', encoding='utf-8'\n        ) as json_file:\n            json.dump(preferences, json_file)\n        logger.debug('Wrote browser preferences to temp user dir')\n\n    def _set_browser_preferences_in_user_data_dir(self, user_data_dir: str):\n        \"\"\"\n        Set browser preferences in the user data directory.\n\n        This function will:\n        1. Create a backup of the existing Preferences file if it exists\n        2. Create Default directory if it doesn't exist\n        3. Write the new preferences to the Preferences file\n\n        Args:\n            user_data_dir: Path to the user data directory\n        \"\"\"\n        default_dir = os.path.join(user_data_dir, 'Default')\n        os.makedirs(default_dir, exist_ok=True)\n\n        preferences_path = os.path.join(default_dir, 'Preferences')\n        self._backup_preferences_dir = os.path.join(default_dir, 'Preferences.backup')\n\n        if os.path.exists(preferences_path):\n            # Backup existing Preferences file\n            shutil.copy2(preferences_path, self._backup_preferences_dir)\n\n        preferences = {}\n        if os.path.exists(preferences_path):\n            with suppress(json.JSONDecodeError):\n                with open(preferences_path, 'r', encoding='utf-8') as preferences_file:\n                    preferences = json.load(preferences_file)\n        preferences.update(self.options.browser_preferences)\n        with open(preferences_path, 'w', encoding='utf-8') as json_file:\n            json.dump(preferences, json_file, indent=2)\n        logger.debug(f'Updated browser preferences in user data dir: {preferences_path}')\n\n    def _get_user_data_dir(self) -> Optional[str]:\n        for arg in self.options.arguments:\n            if arg.startswith('--user-data-dir='):\n                return arg.split('=', 1)[1]\n        return None\n\n    @staticmethod\n    def _validate_ws_address(ws_address: str):\n        \"\"\"Validate WebSocket address.\"\"\"\n        min_slashes = 4\n        if not ws_address.startswith(('ws://', 'wss://')):\n            logger.error('Invalid WebSocket address: missing ws:// or wss:// prefix')\n            raise InvalidWebSocketAddress('WebSocket address must start with ws:// or wss://')\n        if len(ws_address.split('/')) < min_slashes:\n            logger.error('Invalid WebSocket address: not enough slashes')\n            raise InvalidWebSocketAddress(\n                f'WebSocket address must contain at least {min_slashes} slashes'\n            )\n\n    async def _setup_ws_address(self, ws_address: str):\n        \"\"\"Setup WebSocket address for browser.\"\"\"\n        self._validate_ws_address(ws_address)\n        self._ws_address = ws_address\n        self._connection_handler._ws_address = self._ws_address\n        await self._connection_handler._ensure_active_connection()\n        logger.info('WebSocket address set for browser-level connection')\n\n    def _get_tab_kwargs(self, target_id: str, browser_context_id: Optional[str] = None) -> dict:\n        \"\"\"\n        Get kwargs for creating a tab based on the WebSocket address.\n        If the WebSocket address is set, the tab will be created with the WebSocket address.\n        Otherwise, the tab will be created with the connection port and target ID.\n\n        Args:\n            target_id: Target ID of the tab.\n            browser_context_id: Browser context ID of the tab.\n\n        Returns:\n            Dict of kwargs for creating a tab.\n        \"\"\"\n        kwargs: dict[str, Any] = {\n            'target_id': target_id,\n            'browser_context_id': browser_context_id,\n        }\n        if self._ws_address:\n            kwargs['ws_address'] = self._get_tab_ws_address(target_id)\n        else:\n            kwargs['connection_port'] = self._connection_port\n        logger.debug(f'Tab kwargs resolved for {target_id}: using_ws={bool(self._ws_address)}')\n        return kwargs\n\n    def _get_tab_ws_address(self, tab_id: str) -> str:\n        \"\"\"\n        Get WebSocket address for a specific tab, preserving any query or fragment\n        components present in the original browser-level WebSocket URL.\n\n        This ensures authentication tokens passed via query string (e.g.,\n        ws://host/devtools/browser/abc?token=XYZ) are retained when switching\n        to the page-level endpoint (devtools/page/<tab_id>), which is critical\n        for providers like Browserless or authenticated CDP proxies.\n        \"\"\"\n        if not self._ws_address:\n            raise InvalidWebSocketAddress('WebSocket address is not set')\n\n        parts = urlsplit(self._ws_address)\n        # Preserve scheme and netloc; build the page path and keep query/fragment\n        page_path = f'/devtools/page/{tab_id}'\n        ws = urlunsplit((parts.scheme, parts.netloc, page_path, parts.query, parts.fragment))\n        logger.debug(f'Resolved tab WebSocket address: {ws}')\n        return ws\n\n    @staticmethod\n    def _sanitize_proxy_and_extract_auth(\n        proxy_server: str,\n    ) -> tuple[str, Optional[tuple[str, str]]]:\n        \"\"\"Strip credentials from a proxy URL and return sanitized URL plus (user, pass).\n\n        Accepts inputs like:\n        - username:password@host:port\n        - http://username:password@host:port\n        - socks5://username:password@host:port\n        - host:port (no credentials)\n        Returns a (sanitized_proxy, (user, pass) | None).\n        Ensures scheme is present in the sanitized URL (defaults to http).\n        \"\"\"\n        base = proxy_server if '://' in proxy_server else f'http://{proxy_server}'\n        parts = urlsplit(base)\n        netloc = parts.netloc\n        creds: Optional[tuple[str, str]] = None\n        if '@' in netloc:\n            cred_part, host_part = netloc.split('@', 1)\n            if ':' in cred_part:\n                user, pwd = cred_part.split(':', 1)\n            else:\n                user, pwd = cred_part, ''\n            creds = (user, pwd)\n            sanitized = urlunsplit((\n                parts.scheme,\n                host_part,\n                parts.path,\n                parts.query,\n                parts.fragment,\n            ))\n        else:\n            # No creds; ensure scheme\n            sanitized = urlunsplit((\n                parts.scheme,\n                parts.netloc,\n                parts.path,\n                parts.query,\n                parts.fragment,\n            ))\n        return sanitized, creds\n\n    @abstractmethod\n    def _get_default_binary_location(self) -> str:\n        \"\"\"Get default browser executable path (implemented by subclasses).\"\"\"\n        pass\n"
  },
  {
    "path": "pydoll/browser/chromium/chrome.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport platform\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.browser.chromium.base import Browser\nfrom pydoll.browser.managers import ChromiumOptionsManager\nfrom pydoll.exceptions import UnsupportedOS\nfrom pydoll.utils import validate_browser_paths\n\nif TYPE_CHECKING:\n    from pydoll.browser.options import ChromiumOptions\n\nlogger = logging.getLogger(__name__)\n\n\nclass Chrome(Browser):\n    \"\"\"Chrome browser implementation for CDP automation.\"\"\"\n\n    def __init__(\n        self,\n        options: Optional[ChromiumOptions] = None,\n        connection_port: Optional[int] = None,\n    ):\n        \"\"\"\n        Initialize Chrome browser instance.\n\n        Args:\n            options: Chrome configuration options (default if None).\n            connection_port: CDP WebSocket port (random if None).\n        \"\"\"\n        options_manager = ChromiumOptionsManager(options)\n        super().__init__(options_manager, connection_port)\n\n    @staticmethod\n    def _get_default_binary_location():\n        \"\"\"\n        Get default Chrome executable path based on OS.\n\n        Returns:\n            Path to Chrome executable.\n\n        Raises:\n            UnsupportedOS: If OS is not supported.\n            ValueError: If executable not found at default location.\n        \"\"\"\n        os_name = platform.system()\n        logger.debug(f'Resolving default Chrome binary for OS: {os_name}')\n\n        browser_paths = {\n            'Windows': [\n                r'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',\n                r'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',\n            ],\n            'Linux': [\n                '/usr/bin/google-chrome',\n                '/usr/bin/google-chrome-stable',\n            ],\n            'Darwin': [\n                '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',\n            ],\n        }\n\n        browser_path = browser_paths.get(os_name)\n\n        if not browser_path:\n            logger.error(f'Unsupported OS: {os_name}')\n            raise UnsupportedOS(f'Unsupported OS: {os_name}')\n\n        path = validate_browser_paths(browser_path)\n        logger.debug(f'Using Chrome binary: {path}')\n        return path\n"
  },
  {
    "path": "pydoll/browser/chromium/edge.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport platform\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.browser.chromium.base import Browser\nfrom pydoll.browser.managers import ChromiumOptionsManager\nfrom pydoll.exceptions import UnsupportedOS\nfrom pydoll.utils import validate_browser_paths\n\nif TYPE_CHECKING:\n    from pydoll.browser.options import Options\n\nlogger = logging.getLogger(__name__)\n\n\nclass Edge(Browser):\n    \"\"\"Edge browser implementation for CDP automation.\"\"\"\n\n    def __init__(\n        self,\n        options: Optional[Options] = None,\n        connection_port: Optional[int] = None,\n    ):\n        \"\"\"\n        Initialize Edge browser instance.\n\n        Args:\n            options: Edge configuration options (default if None).\n            connection_port: CDP WebSocket port (random if None).\n        \"\"\"\n        options_manager = ChromiumOptionsManager(options)\n        super().__init__(options_manager, connection_port)\n\n    @staticmethod\n    def _get_default_binary_location():\n        \"\"\"\n        Get default Edge executable path based on OS.\n\n        Returns:\n            Path to Edge executable.\n\n        Raises:\n            UnsupportedOS: If OS is not supported.\n            ValueError: If executable not found at default location.\n        \"\"\"\n        os_name = platform.system()\n        logger.debug(f'Resolving default Edge binary for OS: {os_name}')\n\n        browser_paths = {\n            'Windows': [\n                (\n                    r'C:\\Program Files\\Microsoft\\Edge\\Application'\n                    r'\\msedge.exe'\n                ),\n                (\n                    r'C:\\Program Files (x86)\\Microsoft\\Edge'\n                    r'\\Application\\msedge.exe'\n                ),\n            ],\n            'Linux': [\n                '/usr/bin/microsoft-edge',\n            ],\n            'Darwin': [\n                ('/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'),\n            ],\n        }\n\n        browser_path = browser_paths.get(os_name)\n\n        if not browser_path:\n            logger.error(f'Unsupported OS: {os_name}')\n            raise UnsupportedOS()\n\n        path = validate_browser_paths(browser_path)\n        logger.debug(f'Using Edge binary: {path}')\n        return path\n"
  },
  {
    "path": "pydoll/browser/interfaces.py",
    "content": "from abc import ABC, abstractmethod\n\nfrom pydoll.constants import PageLoadState\n\n\nclass Options(ABC):\n    @property\n    @abstractmethod\n    def arguments(self) -> list[str]:\n        pass\n\n    @property\n    @abstractmethod\n    def binary_location(self) -> str:\n        pass\n\n    @property\n    @abstractmethod\n    def start_timeout(self) -> int:\n        pass\n\n    @abstractmethod\n    def add_argument(self, argument: str):\n        pass\n\n    @property\n    @abstractmethod\n    def browser_preferences(self) -> dict:\n        pass\n\n    @property\n    @abstractmethod\n    def headless(self) -> bool:\n        pass\n\n    @headless.setter\n    @abstractmethod\n    def headless(self, headless: bool):\n        pass\n\n    @property\n    @abstractmethod\n    def page_load_state(self) -> PageLoadState:\n        pass\n\n    @page_load_state.setter\n    @abstractmethod\n    def page_load_state(self, state: PageLoadState):\n        pass\n\n\nclass BrowserOptionsManager(ABC):\n    @abstractmethod\n    def initialize_options(self) -> Options:\n        pass\n\n    @abstractmethod\n    def add_default_arguments(self):\n        pass\n"
  },
  {
    "path": "pydoll/browser/managers/__init__.py",
    "content": "from pydoll.browser.managers.browser_options_manager import (\n    ChromiumOptionsManager,\n)\nfrom pydoll.browser.managers.browser_process_manager import (\n    BrowserProcessManager,\n)\nfrom pydoll.browser.managers.proxy_manager import ProxyManager\nfrom pydoll.browser.managers.temp_dir_manager import TempDirectoryManager\n\n__all__ = [\n    'ChromiumOptionsManager',\n    'BrowserProcessManager',\n    'ProxyManager',\n    'TempDirectoryManager',\n]\n"
  },
  {
    "path": "pydoll/browser/managers/browser_options_manager.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.browser.interfaces import BrowserOptionsManager\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.exceptions import InvalidOptionsObject\n\nif TYPE_CHECKING:\n    from pydoll.browser.options import Options\n\nlogger = logging.getLogger(__name__)\n\n\nclass ChromiumOptionsManager(BrowserOptionsManager):\n    \"\"\"\n    Manages browser options configuration for Chromium-based browsers.\n\n    Handles options creation, validation, and applies default CDP arguments\n    for Chrome and Edge browsers.\n    \"\"\"\n\n    def __init__(self, options: Optional[Options] = None):\n        self.options = options\n        logger.debug(\n            f'ChromiumOptionsManager initialized with options='\n            f'{type(options).__name__ if options is not None else \"None\"}'\n        )\n\n    def initialize_options(\n        self,\n    ) -> ChromiumOptions:\n        \"\"\"\n        Initialize and validate browser options.\n\n        Creates ChromiumOptions if none provided, validates existing options,\n        and applies default CDP arguments.\n\n        Returns:\n            Properly configured ChromiumOptions instance.\n\n        Raises:\n            InvalidOptionsObject: If provided options is not ChromiumOptions.\n        \"\"\"\n        if self.options is None:\n            self.options = ChromiumOptions()\n            logger.debug('No options provided; created default ChromiumOptions')\n\n        if not isinstance(self.options, ChromiumOptions):\n            logger.error(f'Invalid options type: {type(self.options)}; expected ChromiumOptions')\n            raise InvalidOptionsObject(f'Expected ChromiumOptions, got {type(self.options)}')\n\n        self.add_default_arguments()\n        logger.debug('Options initialized and default arguments applied')\n        return self.options\n\n    def add_default_arguments(self):\n        \"\"\"Add default arguments required for CDP integration.\"\"\"\n        logger.debug('Adding default arguments for Chromium-based browsers')\n        self.options.add_argument('--no-first-run')\n        self.options.add_argument('--no-default-browser-check')\n"
  },
  {
    "path": "pydoll/browser/managers/browser_process_manager.py",
    "content": "import logging\nimport subprocess\nfrom typing import Callable, Optional\n\nlogger = logging.getLogger(__name__)\n\n\nclass BrowserProcessManager:\n    \"\"\"\n    Manages browser process lifecycle for CDP automation.\n\n    Handles process creation, monitoring, and termination with proper\n    resource cleanup and graceful shutdown.\n    \"\"\"\n\n    def __init__(\n        self,\n        process_creator: Optional[Callable[[list[str]], subprocess.Popen]] = None,\n    ):\n        \"\"\"\n        Initialize browser process manager.\n\n        Args:\n            process_creator: Custom function to create browser processes.\n                Must accept command list and return subprocess.Popen object.\n                Uses default subprocess implementation if None.\n        \"\"\"\n        self._process_creator = process_creator or self._default_process_creator\n        self._process: Optional[subprocess.Popen] = None\n        logger.debug(\n            f'BrowserProcessManager initialized; custom process_creator={bool(process_creator)}'\n        )\n\n    def start_browser_process(\n        self,\n        binary_location: str,\n        port: int,\n        arguments: list[str],\n    ) -> subprocess.Popen:\n        \"\"\"\n        Launch browser process with CDP debugging enabled.\n\n        Args:\n            binary_location: Path to browser executable.\n            port: TCP port for CDP WebSocket connections.\n            arguments: Additional command-line arguments.\n\n        Returns:\n            Started browser process instance.\n\n        Note:\n            Automatically adds --remote-debugging-port argument.\n        \"\"\"\n        logger.info(f'Starting browser process: {binary_location} on port {port}')\n        command = [\n            binary_location,\n            f'--remote-debugging-port={port}',\n            *arguments,\n        ]\n        logger.debug(f'Command: {command}')\n        self._process = self._process_creator(command)\n        logger.debug(\n            f'Browser process started: pid={self._process.pid if self._process else \"unknown\"}'\n        )\n        return self._process\n\n    @staticmethod\n    def _default_process_creator(command: list[str]) -> subprocess.Popen:\n        \"\"\"Create browser process with output capture to prevent console clutter.\"\"\"\n        logger.debug(f'Creating process: {command}')\n        return subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\n    def stop_process(self):\n        \"\"\"\n        Terminate browser process with graceful shutdown.\n\n        Attempts SIGTERM first, then SIGKILL after 15-second timeout.\n        Safe to call even if no process is running.\n        \"\"\"\n        if self._process:\n            logger.info(f'Stopping browser process pid={self._process.pid}')\n            self._process.terminate()\n            try:\n                self._process.wait(timeout=15)\n                logger.debug('Process terminated gracefully')\n            except subprocess.TimeoutExpired:\n                logger.warning('Process did not terminate in 15s; sending SIGKILL')\n                self._process.kill()\n                logger.debug('Process killed')\n"
  },
  {
    "path": "pydoll/browser/managers/proxy_manager.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom typing import TYPE_CHECKING, Optional\n\nif TYPE_CHECKING:\n    from pydoll.browser.options import Options\n\nlogger = logging.getLogger(__name__)\n\n\nclass ProxyManager:\n    \"\"\"\n    Manages proxy configuration and credentials for CDP automation.\n\n    Extracts embedded credentials from proxy URLs, secures authentication\n    information, and sanitizes command-line arguments.\n    \"\"\"\n\n    def __init__(self, options: Options):\n        \"\"\"\n        Initialize proxy manager with browser options.\n\n        Args:\n            options: Browser options potentially containing proxy configuration.\n                Will be modified if credentials are found.\n        \"\"\"\n        self.options = options\n        logger.debug('ProxyManager initialized with options')\n\n    def get_proxy_credentials(self) -> tuple[bool, tuple[Optional[str], Optional[str]]]:\n        \"\"\"\n        Extract and secure proxy authentication credentials.\n\n        Searches for proxy settings, extracts embedded credentials,\n        and sanitizes options to remove credential exposure.\n\n        Returns:\n            Tuple of (has_private_proxy, (username, password)).\n        \"\"\"\n        private_proxy = False\n        credentials: tuple[Optional[str], Optional[str]] = (None, None)\n\n        proxy_arg = self._find_proxy_argument()\n\n        if proxy_arg is not None:\n            index, proxy_value = proxy_arg\n            has_credentials, username, password, clean_proxy = self._parse_proxy(proxy_value)\n\n            if has_credentials:\n                self._update_proxy_argument(index, clean_proxy)\n                private_proxy = True\n                credentials = (username, password)\n                logger.debug(\n                    f'Proxy credentials extracted (user_set={bool(username)}); argument sanitized'\n                )\n            else:\n                logger.debug('Proxy configured without embedded credentials')\n\n        return private_proxy, credentials\n\n    def _find_proxy_argument(self) -> Optional[tuple[int, str]]:\n        \"\"\"\n        Find proxy server configuration in browser options.\n\n        Returns:\n            Tuple of (index, proxy_url) if found, None otherwise.\n        \"\"\"\n        for index, arg in enumerate(self.options.arguments):\n            if arg.startswith('--proxy-server='):\n                value = arg.split('=', 1)[1]\n                logger.debug(f'Found proxy argument at index {index}: {value}')\n                return index, value\n        return None\n\n    @staticmethod\n    def _parse_proxy(proxy_value: str) -> tuple[bool, Optional[str], Optional[str], str]:\n        \"\"\"\n        Parse proxy URL to extract authentication credentials.\n\n        Args:\n            proxy_value: Proxy URL potentially containing username:password@server:port.\n\n        Returns:\n            Tuple of (has_credentials, username, password, clean_proxy_url).\n        \"\"\"\n        if '@' not in proxy_value:\n            return False, None, None, proxy_value\n\n        try:\n            scheme = ''\n            has_scheme = False\n            if '://' in proxy_value:\n                scheme, proxy_value = proxy_value.split('://', 1)\n                has_scheme = True\n\n            creds_part, server_part = proxy_value.split('@', 1)\n            username, password = creds_part.split(':', 1)\n\n            clean_proxy = f'{scheme}://{server_part}' if has_scheme else server_part\n            return True, username, password, clean_proxy\n        except ValueError:\n            return False, None, None, proxy_value\n\n    def _update_proxy_argument(self, index: int, clean_proxy: str) -> None:\n        \"\"\"Replace proxy argument with credential-free version.\"\"\"\n        self.options.arguments[index] = f'--proxy-server={clean_proxy}'\n        logger.debug(f'Proxy argument updated at index {index}: {clean_proxy}')\n"
  },
  {
    "path": "pydoll/browser/managers/temp_dir_manager.py",
    "content": "import logging\nimport os\nimport shutil\nimport time\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom typing import Callable\n\nlogger = logging.getLogger(__name__)\n\n\nclass TempDirectoryManager:\n    \"\"\"\n    Manages temporary directory lifecycle for CDP browser automation.\n\n    Creates isolated temporary directories for browser profiles and handles\n    secure cleanup with retry mechanisms for locked files.\n    \"\"\"\n\n    def __init__(self, temp_dir_factory: Callable[[], TemporaryDirectory] = TemporaryDirectory):\n        \"\"\"\n        Initialize temporary directory manager.\n\n        Args:\n            temp_dir_factory: Function to create temporary directories.\n                Must return TemporaryDirectory-compatible object.\n        \"\"\"\n        self._temp_dir_factory = temp_dir_factory\n        self._temp_dirs: list[TemporaryDirectory] = []\n        logger.debug('TempDirectoryManager initialized')\n\n    def create_temp_dir(self) -> TemporaryDirectory:\n        \"\"\"\n        Create and track new temporary directory for browser use.\n\n        Returns:\n            TemporaryDirectory object for browser --user-data-dir argument.\n        \"\"\"\n        temp_dir = self._temp_dir_factory()\n        self._temp_dirs.append(temp_dir)\n        logger.debug(f'Created temp directory: {temp_dir.name}')\n        return temp_dir\n\n    @staticmethod\n    def retry_process_file(func: Callable[[str], None], path: str, retry_times: int = 10):\n        \"\"\"\n        Execute file operation with retry logic for locked files.\n\n        Args:\n            func: Function to execute on path.\n            path: File or directory path to operate on.\n            retry_times: Maximum retry attempts (negative = unlimited).\n\n        Raises:\n            PermissionError: If operation fails after all retries.\n        \"\"\"\n        retry_time = 0\n        while retry_times < 0 or retry_time < retry_times:\n            retry_time += 1\n            try:\n                func(path)\n                break\n            except PermissionError:\n                time.sleep(0.1)\n                logger.debug(\n                    f'Retrying file operation due to PermissionError (attempt {retry_time})'\n                )\n        else:\n            raise PermissionError()\n\n    def handle_cleanup_error(self, func: Callable[[str], None], path: str, exc_info: tuple):\n        \"\"\"\n        Handle errors during directory cleanup with browser-specific workarounds.\n\n        Args:\n            func: Original function that failed.\n            path: Path that could not be processed.\n            exc_info: Exception information tuple.\n\n        Note:\n            Handles Chromium-specific locked files like CrashpadMetrics.\n        \"\"\"\n        matches = ['CrashpadMetrics-active.pma']\n        match_substrings = ['Safe Browsing', 'Safe Browsing Cookies']\n        # Extra patterns commonly locked on Windows; compare case-insensitively\n        windows_locked_substrings = [\n            '\\\\cache\\\\',\n            '/cache/',\n            'no_vary_search',\n            'journal.baj',\n            '\\\\network\\\\cookies',\n            '/network/cookies',\n            'cookies-journal',\n            '\\\\local storage\\\\',\n            '/local storage/',\n            '\\\\local storage\\\\leveldb\\\\',\n            '/local storage/leveldb/',\n            'leveldb',\n            'indexeddb',\n        ]\n        exc_type, exc_value, _ = exc_info\n\n        if exc_type is PermissionError:\n            filename = Path(path).name\n            # Known Chromium files that may remain locked briefly on Windows\n            path_lc = path.lower()\n            windows_match = os.name == 'nt' and any(\n                substr in path_lc for substr in windows_locked_substrings\n            )\n            if (\n                filename in matches\n                or any(substr in path for substr in match_substrings)\n                or windows_match\n            ):\n                try:\n                    self.retry_process_file(func, path)\n                    return\n                except PermissionError:\n                    logger.warning(f'Ignoring locked Chrome file during cleanup: {path}')\n                    return\n        elif exc_type is OSError:\n            return\n        raise exc_value\n\n    def cleanup(self):\n        \"\"\"\n        Remove all tracked temporary directories with error handling.\n\n        Uses custom error handler for browser-specific file lock issues.\n        Continues cleanup even if some files resist deletion.\n        \"\"\"\n        for temp_dir in self._temp_dirs:\n            logger.info(f'Cleaning up temp directory: {temp_dir.name}')\n            shutil.rmtree(temp_dir.name, onerror=self.handle_cleanup_error)\n            remaining = Path(temp_dir.name)\n            if not remaining.exists():\n                continue\n\n            for attempt in range(10):\n                time.sleep(0.2)\n                try:\n                    shutil.rmtree(temp_dir.name, onerror=self.handle_cleanup_error)\n                except Exception:  # noqa: BLE001 - best-effort cleanup\n                    pass\n                if not remaining.exists():\n                    logger.debug(\n                        f'Temp directory removed after retry #{attempt + 1}: {temp_dir.name}'\n                    )\n                    break\n            if remaining.exists():\n                logger.warning(\n                    f'Temp directory still present after retries (leftover files may remain): '\n                    f'{temp_dir.name}'\n                )\n"
  },
  {
    "path": "pydoll/browser/options.py",
    "content": "from contextlib import suppress\n\nfrom pydoll.browser.interfaces import Options\nfrom pydoll.constants import PageLoadState\nfrom pydoll.exceptions import (\n    ArgumentAlreadyExistsInOptions,\n    ArgumentNotFoundInOptions,\n    WrongPrefsDict,\n)\n\n\nclass ChromiumOptions(Options):\n    \"\"\"\n    A class to manage command-line options for a browser instance.\n\n    This class allows the user to specify command-line arguments and\n    the binary location of the browser executable.\n    \"\"\"\n\n    def __init__(self):\n        \"\"\"\n        Initializes the Options instance.\n\n        Sets up an empty list for command-line arguments and a string\n        for the binary location of the browser.\n        \"\"\"\n        self._arguments = []\n        self._binary_location = ''\n        self._start_timeout = 10\n        self._browser_preferences = {}\n        self._headless = False\n        self._webrtc_leak_protection = False\n        self._page_load_state = PageLoadState.COMPLETE\n\n    @property\n    def arguments(self) -> list[str]:\n        \"\"\"\n        Gets the list of command-line arguments.\n\n        Returns:\n            list: A list of command-line arguments added to the options.\n        \"\"\"\n        return self._arguments\n\n    @arguments.setter\n    def arguments(self, args_list: list[str]):\n        \"\"\"\n        Sets the list of command-line arguments.\n\n        Args:\n            args_list (list): A list of command-line arguments.\n        \"\"\"\n        self._arguments = args_list\n\n    @property\n    def binary_location(self) -> str:\n        \"\"\"\n        Gets the location of the browser binary.\n\n        Returns:\n            str: The file path to the browser executable.\n        \"\"\"\n        return self._binary_location\n\n    @binary_location.setter\n    def binary_location(self, location: str):\n        \"\"\"\n        Sets the location of the browser binary.\n\n        Args:\n            location (str): The file path to the browser executable.\n        \"\"\"\n        self._binary_location = location\n\n    @property\n    def start_timeout(self) -> int:\n        \"\"\"\n        Gets the timeout to verify the browser's running state.\n\n        Returns:\n            int: The timeout in seconds.\n        \"\"\"\n        return self._start_timeout\n\n    @start_timeout.setter\n    def start_timeout(self, timeout: int):\n        \"\"\"\n        Sets the timeout to verify the browser's running state.\n\n        Args:\n            timeout (int): The timeout in seconds.\n        \"\"\"\n        self._start_timeout = timeout\n\n    def add_argument(self, argument: str):\n        \"\"\"\n        Adds a command-line argument to the options.\n\n        Args:\n            argument (str): The command-line argument to be added.\n\n        Raises:\n            ArgumentAlreadyExistsInOptions: If the argument is already in the list of arguments.\n        \"\"\"\n        if argument not in self._arguments:\n            self._arguments.append(argument)\n        else:\n            raise ArgumentAlreadyExistsInOptions(f'Argument already exists: {argument}')\n\n    def remove_argument(self, argument: str):\n        \"\"\"\n        Removes a command-line argument from the options.\n\n        Args:\n            argument (str): The command-line argument to be removed.\n\n        Raises:\n            ArgumentNotFoundInOptions: If the argument is not in the list of arguments.\n        \"\"\"\n        if argument not in self._arguments:\n            raise ArgumentNotFoundInOptions(f'Argument not found: {argument}')\n        self._arguments.remove(argument)\n\n    @property\n    def browser_preferences(self) -> dict:\n        return self._browser_preferences\n\n    @browser_preferences.setter\n    def browser_preferences(self, preferences: dict):\n        if not isinstance(preferences, dict):\n            raise ValueError('The experimental options value must be a dict.')\n\n        if preferences.get('prefs'):\n            raise WrongPrefsDict\n        self._browser_preferences = {**self._browser_preferences, **preferences}\n\n    def _set_pref_path(self, path: list, value):\n        \"\"\"\n        Safely sets a nested value in self._browser_preferences,\n        creating intermediate dicts as needed.\n\n        Arguments:\n            path -- List of keys representing the nested\n                    path (e.g., ['plugins', 'always_open_pdf_externally'])\n            value -- The value to set at the given path\n        \"\"\"\n        d = self._browser_preferences\n        for key in path[:-1]:\n            d = d.setdefault(key, {})\n        d[path[-1]] = value\n\n    def _get_pref_path(self, path: list):\n        \"\"\"\n        Safely gets a nested value from self._browser_preferences.\n\n        Arguments:\n            path -- List of keys representing the nested\n                    path (e.g., ['plugins', 'always_open_pdf_externally'])\n\n        Returns:\n            The value at the given path, or None if path doesn't exist\n        \"\"\"\n        nested_preferences = self._browser_preferences\n        with suppress(KeyError, TypeError):\n            for key in path:\n                nested_preferences = nested_preferences[key]\n            return nested_preferences\n        return None\n\n    def set_default_download_directory(self, path: str):\n        \"\"\"\n        Set the default directory where downloaded files will be saved.\n\n        Usage: Sets the 'download.default_directory' preference for Chrome.\n\n        Arguments:\n            path: Absolute path to the download destination folder.\n        \"\"\"\n        self._set_pref_path(['download', 'default_directory'], path)\n\n    def set_accept_languages(self, languages: str):\n        \"\"\"\n        Set the accepted languages for the browser.\n\n        Usage: Sets the 'intl.accept_languages' preference.\n\n        Arguments:\n            languages: A comma-separated string of language codes (e.g., 'pt-BR,pt,en-US,en').\n        \"\"\"\n        self._set_pref_path(['intl', 'accept_languages'], languages)\n\n    @property\n    def prompt_for_download(self) -> bool:\n        return self._get_pref_path(['download', 'prompt_for_download'])\n\n    @prompt_for_download.setter\n    def prompt_for_download(self, enabled: bool):\n        \"\"\"\n        Enable or disable download prompt confirmation.\n\n        Usage: Sets the 'download.prompt_for_download' preference.\n\n        Arguments:\n            enabled: If True, Chrome will ask for confirmation before downloading.\n        \"\"\"\n        self._set_pref_path(['download', 'prompt_for_download'], enabled)\n\n    @property\n    def block_popups(self) -> bool:\n        return self._get_pref_path(['profile', 'default_content_setting_values', 'popups']) == 0\n\n    @block_popups.setter\n    def block_popups(self, block: bool):\n        \"\"\"\n        Block or allow pop-up windows.\n\n        Usage: Sets the 'profile.default_content_setting_values.popups' preference.\n\n        Arguments:\n            block: If True, pop-ups will be blocked (value = 0); otherwise allowed (value = 1).\n        \"\"\"\n        self._set_pref_path(\n            ['profile', 'default_content_setting_values', 'popups'], 0 if block else 1\n        )\n\n    @property\n    def password_manager_enabled(self) -> bool:\n        return self._get_pref_path(['profile', 'password_manager_enabled'])\n\n    @password_manager_enabled.setter\n    def password_manager_enabled(self, enabled: bool):\n        \"\"\"\n        Enable or disable Chrome's password manager.\n\n        Usage: Sets the 'profile.password_manager_enabled' preference.\n\n        Arguments:\n            enabled: If True, the password manager is active.\n        \"\"\"\n        self._set_pref_path(['profile', 'password_manager_enabled'], enabled)\n        self._set_pref_path(['credentials_enable_service'], enabled)\n\n    @property\n    def block_notifications(self) -> bool:\n        block_notifications_true_value = 2\n        return (\n            self._get_pref_path(['profile', 'default_content_setting_values', 'notifications'])\n            == block_notifications_true_value\n        )\n\n    @block_notifications.setter\n    def block_notifications(self, block: bool):\n        \"\"\"\n        Block or allow site notifications.\n\n        Usage: Sets the 'profile.default_content_setting_values.notifications' preference.\n\n        Arguments:\n            block: If True, notifications will be blocked (value = 2);\n            otherwise allowed (value = 1).\n        \"\"\"\n        self._set_pref_path(\n            ['profile', 'default_content_setting_values', 'notifications'],\n            2 if block else 1,\n        )\n\n    @property\n    def allow_automatic_downloads(self) -> bool:\n        return (\n            self._get_pref_path([\n                'profile',\n                'default_content_setting_values',\n                'automatic_downloads',\n            ])\n            == 1\n        )\n\n    @allow_automatic_downloads.setter\n    def allow_automatic_downloads(self, allow: bool):\n        \"\"\"\n        Allow or block automatic multiple downloads.\n\n        Usage: Sets the 'profile.default_content_setting_values.automatic_downloads' preference.\n\n        Arguments:\n            allow: If True, automatic downloads are allowed (value = 1);\n            otherwise blocked (value = 2).\n        \"\"\"\n        self._set_pref_path(\n            ['profile', 'default_content_setting_values', 'automatic_downloads'],\n            1 if allow else 2,\n        )\n\n    @property\n    def open_pdf_externally(self) -> bool:\n        return self._get_pref_path(['plugins', 'always_open_pdf_externally'])\n\n    @open_pdf_externally.setter\n    def open_pdf_externally(self, enabled: bool):\n        \"\"\"\n        Block or allow geolocation access.\n\n        Usage: Sets the 'profile.managed_default_content_settings.geolocation' preference.\n\n        Arguments:\n            block: If True, location access is blocked (value = 2); otherwise allowed (value = 1).\n        \"\"\"\n        self._set_pref_path(['plugins', 'always_open_pdf_externally'], enabled)\n\n    @property\n    def headless(self) -> bool:\n        return self._headless\n\n    @headless.setter\n    def headless(self, headless: bool):\n        self._headless = headless\n        has_argument = '--headless' in self.arguments\n        methods_map = {True: self.add_argument, False: self.remove_argument}\n        if headless == has_argument:\n            return\n        methods_map[headless]('--headless')\n\n    @property\n    def webrtc_leak_protection(self) -> bool:\n        return self._webrtc_leak_protection\n\n    @webrtc_leak_protection.setter\n    def webrtc_leak_protection(self, enabled: bool):\n        self._webrtc_leak_protection = enabled\n        argument = '--force-webrtc-ip-handling-policy=disable_non_proxied_udp'\n        has_argument = argument in self.arguments\n        methods_map = {True: self.add_argument, False: self.remove_argument}\n        if enabled == has_argument:\n            return\n        methods_map[enabled](argument)\n\n    @property\n    def page_load_state(self) -> PageLoadState:\n        return self._page_load_state\n\n    @page_load_state.setter\n    def page_load_state(self, state: PageLoadState):\n        self._page_load_state = state\n"
  },
  {
    "path": "pydoll/browser/requests/__init__.py",
    "content": "\"\"\"\nThis module provides HTTP client functionality using the browser's fetch API.\nIt allows making HTTP requests within the browser context, reusing cookies and headers.\n\"\"\"\n\nfrom .har_recorder import HarCapture\nfrom .request import Request\nfrom .response import Response\n\n__all__ = ['HarCapture', 'Request', 'Response']\n"
  },
  {
    "path": "pydoll/browser/requests/har_recorder.py",
    "content": "\"\"\"HAR network recorder for capturing and replaying browser network traffic.\n\nThis module provides the internal recording engine (HarRecorder) and the\nuser-facing recording object (HarCapture) that together enable HAR 1.2\ncapture and export from browser sessions.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport base64\nimport json\nimport logging\nfrom datetime import datetime, timezone\nfrom importlib.metadata import version as _pkg_version\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Callable, cast\nfrom urllib.parse import parse_qs, urlparse\n\nfrom pydoll.commands.network_commands import NetworkCommands\nfrom pydoll.protocol.network.events import (\n    DataReceivedEvent,\n    LoadingFailedEvent,\n    LoadingFinishedEvent,\n    NetworkEvent,\n    RequestWillBeSentEvent,\n    RequestWillBeSentExtraInfoEvent,\n    ResponseReceivedEvent,\n    ResponseReceivedExtraInfoEvent,\n)\nfrom pydoll.protocol.network.har_types import (\n    Har,\n    HarContent,\n    HarCookie,\n    HarCreator,\n    HarEntry,\n    HarHeader,\n    HarLog,\n    HarPostData,\n    HarQueryParam,\n    HarRequest,\n    HarResponse,\n    HarTimings,\n)\nfrom pydoll.protocol.network.types import ResourceType\n\nif TYPE_CHECKING:\n    from pydoll.browser.tab import Tab\n    from pydoll.protocol.network.methods import GetResponseBodyResponse\n    from pydoll.protocol.network.types import ResourceTiming\n    from pydoll.protocol.network.types import Response as CDPResponse\n\nlogger = logging.getLogger(__name__)\n\n_PYDOLL_CREATOR_NAME = 'pydoll'\n_HTTP_NOT_MODIFIED = 304\n\n\ndef _get_pydoll_version() -> str:\n    \"\"\"Get the installed pydoll version.\"\"\"\n    try:\n        return _pkg_version('pydoll')\n    except Exception:\n        return 'unknown'\n\n\nclass HarRecorder:\n    \"\"\"Internal engine that listens to CDP network events and builds HAR entries.\n\n    This class registers callbacks for 7 CDP Network events, correlates them\n    by requestId, and builds HAR 1.2 entries. It is not intended for direct\n    use — instead, use ``tab.request.record()`` which wraps this engine.\n    \"\"\"\n\n    def __init__(self, tab: Tab, resource_types: list[ResourceType] | None = None):\n        self._tab = tab\n        self._resource_types = frozenset(resource_types) if resource_types else None\n        self._callback_ids: list[int] = []\n        self._pending: dict[str, dict[str, Any]] = {}\n        self._entries: list[HarEntry] = []\n        self._start_time: datetime | None = None\n        self._network_was_enabled: bool = False\n        self._body_tasks: list[asyncio.Task] = []\n        self._data_received_sizes: dict[str, int] = {}\n\n    async def start(self) -> None:\n        \"\"\"Start recording network traffic.\n\n        Enables network events if not already on, and registers callbacks\n        for the 7 CDP events needed to build HAR entries.\n        \"\"\"\n        if not self._tab.network_events_enabled:\n            await self._tab.enable_network_events()\n            self._network_was_enabled = True\n            logger.debug('HAR recorder enabled network events')\n\n        self._start_time = datetime.now(tz=timezone.utc)\n\n        _cb = Callable[[dict], Any]\n        events_and_handlers: list[tuple[str, _cb]] = [\n            (NetworkEvent.REQUEST_WILL_BE_SENT, cast(_cb, self._on_request_will_be_sent)),\n            (NetworkEvent.REQUEST_WILL_BE_SENT_EXTRA_INFO, cast(_cb, self._on_request_extra_info)),\n            (NetworkEvent.RESPONSE_RECEIVED, cast(_cb, self._on_response_received)),\n            (NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO, cast(_cb, self._on_response_extra_info)),\n            (NetworkEvent.DATA_RECEIVED, cast(_cb, self._on_data_received)),\n            (NetworkEvent.LOADING_FINISHED, cast(_cb, self._on_loading_finished)),\n            (NetworkEvent.LOADING_FAILED, cast(_cb, self._on_loading_failed)),\n        ]\n\n        for event_name, handler in events_and_handlers:\n            callback_id = await self._tab.on(event_name, handler)\n            self._callback_ids.append(callback_id)\n\n        logger.info('HAR recorder started, registered %d callbacks', len(self._callback_ids))\n\n    async def stop(self) -> None:\n        \"\"\"Stop recording and clean up.\n\n        Removes all registered callbacks, waits for pending body fetches,\n        flushes pending entries, and optionally disables network events.\n        \"\"\"\n        for callback_id in self._callback_ids:\n            await self._tab.remove_callback(callback_id)\n        self._callback_ids.clear()\n\n        if self._body_tasks:\n            await asyncio.gather(*self._body_tasks, return_exceptions=True)\n            self._body_tasks.clear()\n\n        self._flush_pending()\n\n        if self._network_was_enabled:\n            await self._tab.disable_network_events()\n            self._network_was_enabled = False\n\n        logger.info('HAR recorder stopped, captured %d entries', len(self._entries))\n\n    def _on_request_will_be_sent(self, event: RequestWillBeSentEvent) -> None:\n        \"\"\"Handle Network.requestWillBeSent event.\"\"\"\n        params = event['params']\n        request_id = params['requestId']\n        request_data = params['request']\n        resource_type = params.get('type', '')\n        redirect_response = params.get('redirectResponse')\n\n        if self._resource_types and resource_type not in self._resource_types:\n            return\n\n        if redirect_response and request_id in self._pending:\n            self._finalize_redirect_entry(request_id, redirect_response)\n\n        self._pending[request_id] = {\n            'url': request_data.get('url', ''),\n            'method': request_data.get('method', 'GET'),\n            'request_headers': request_data.get('headers', {}),\n            'post_data': request_data.get('postData'),\n            'wall_time': params['wallTime'],\n            'resource_type': params.get('type', ''),\n            'timestamp': params['timestamp'],\n        }\n        logger.debug('HAR: request will be sent: %s %s', request_id, request_data.get('url', ''))\n\n    def _on_request_extra_info(self, event: RequestWillBeSentExtraInfoEvent) -> None:\n        \"\"\"Handle Network.requestWillBeSentExtraInfo event.\"\"\"\n        params = event['params']\n        request_id = params['requestId']\n        pending = self._pending.get(request_id)\n        if not pending:\n            return\n\n        extra_headers = params.get('headers', {})\n        if extra_headers:\n            pending['request_headers_extra'] = extra_headers\n        logger.debug('HAR: request extra info: %s', request_id)\n\n    def _on_response_received(self, event: ResponseReceivedEvent) -> None:\n        \"\"\"Handle Network.responseReceived event.\"\"\"\n        params = event['params']\n        request_id = params['requestId']\n        pending = self._pending.get(request_id)\n        if not pending:\n            return\n\n        response = params['response']\n        pending['status'] = response['status']\n        pending['status_text'] = response['statusText']\n        pending['response_headers'] = response.get('headers', {})\n        pending['mime_type'] = response['mimeType']\n        pending['protocol'] = response.get('protocol', '')\n        pending['timing'] = response.get('timing')\n        pending['remote_ip'] = response.get('remoteIPAddress', '')\n        pending['connection_id'] = str(response.get('connectionId', ''))\n        pending['encoded_data_length'] = response.get('encodedDataLength', 0)\n        pending['response_timestamp'] = params['timestamp']\n        logger.debug('HAR: response received: %s status=%s', request_id, response['status'])\n\n    def _on_response_extra_info(self, event: ResponseReceivedExtraInfoEvent) -> None:\n        \"\"\"Handle Network.responseReceivedExtraInfo event.\"\"\"\n        params = event['params']\n        request_id = params['requestId']\n        pending = self._pending.get(request_id)\n        if not pending:\n            return\n\n        extra_headers = params.get('headers', {})\n        if extra_headers:\n            pending['response_headers_extra'] = extra_headers\n        status_code = params.get('statusCode')\n        if status_code is not None:\n            pending['extra_status_code'] = status_code\n        logger.debug('HAR: response extra info: %s', request_id)\n\n    def _on_data_received(self, event: DataReceivedEvent) -> None:\n        \"\"\"Handle Network.dataReceived event.\n\n        Accumulates body chunk bytes per requestId for accurate bodySize.\n        \"\"\"\n        params = event['params']\n        request_id = params['requestId']\n        chunk_size = params['encodedDataLength']\n        self._data_received_sizes[request_id] = (\n            self._data_received_sizes.get(request_id, 0) + chunk_size\n        )\n\n    def _on_loading_finished(self, event: LoadingFinishedEvent) -> None:\n        \"\"\"Handle Network.loadingFinished event.\"\"\"\n        params = event['params']\n        request_id = params['requestId']\n        pending = self._pending.get(request_id)\n        if not pending:\n            return\n\n        pending['transfer_size'] = params['encodedDataLength']\n        pending['finished_timestamp'] = params['timestamp']\n        pending['body_bytes'] = self._data_received_sizes.pop(request_id, -1)\n\n        task = asyncio.create_task(self._finalize_entry(request_id))\n        self._body_tasks.append(task)\n        task.add_done_callback(\n            lambda t: self._body_tasks.remove(t) if t in self._body_tasks else None\n        )\n        logger.debug('HAR: loading finished: %s', request_id)\n\n    def _on_loading_failed(self, event: LoadingFailedEvent) -> None:\n        \"\"\"Handle Network.loadingFailed event.\"\"\"\n        params = event['params']\n        request_id = params['requestId']\n        pending = self._pending.pop(request_id, None)\n        if not pending:\n            return\n\n        self._data_received_sizes.pop(request_id, None)\n        pending.setdefault('status', 0)\n        pending.setdefault('status_text', params.get('errorText', 'Failed'))\n        pending['error_text'] = params['errorText']\n        pending['canceled'] = params.get('canceled', False)\n\n        entry = self._build_entry(pending)\n        self._entries.append(entry)\n        logger.debug('HAR: loading failed: %s error=%s', request_id, params.get('errorText'))\n\n    async def _finalize_entry(self, request_id: str) -> None:\n        \"\"\"Fetch response body and build the final HAR entry.\"\"\"\n        pending = self._pending.pop(request_id, None)\n        if not pending:\n            return\n\n        body, base64_encoded = await self._fetch_response_body(request_id)\n        pending['response_body'] = body\n        pending['response_body_base64'] = base64_encoded\n\n        entry = self._build_entry(pending)\n        self._entries.append(entry)\n\n    def _finalize_redirect_entry(self, request_id: str, redirect_response: CDPResponse) -> None:\n        \"\"\"Finalize a redirect entry before starting a new pending entry.\"\"\"\n        pending = self._pending.pop(request_id, None)\n        if not pending:\n            return\n        pending['body_bytes'] = self._data_received_sizes.pop(request_id, -1)\n\n        pending['status'] = redirect_response.get('status', 302)\n        pending['status_text'] = redirect_response.get('statusText', '')\n        pending['response_headers'] = redirect_response.get('headers', {})\n        pending['mime_type'] = redirect_response.get('mimeType', '')\n        pending['protocol'] = redirect_response.get('protocol', '')\n        pending['timing'] = redirect_response.get('timing')\n\n        entry = self._build_entry(pending)\n        self._entries.append(entry)\n        logger.debug(\n            'HAR: redirect finalized: %s → %s', request_id, redirect_response.get('status')\n        )\n\n    def _flush_pending(self) -> None:\n        \"\"\"Convert remaining pending entries (requests with no response) into HAR entries.\"\"\"\n        for request_id in list(self._pending.keys()):\n            pending = self._pending.pop(request_id)\n            pending.setdefault('status', 0)\n            pending.setdefault('status_text', '(pending)')\n            entry = self._build_entry(pending)\n            self._entries.append(entry)\n        logger.debug('HAR: flushed pending entries')\n\n    async def _fetch_response_body(self, request_id: str) -> tuple[str, bool]:\n        \"\"\"Fetch the response body via Network.getResponseBody.\n\n        Returns:\n            Tuple of (body_text, is_base64_encoded). Returns ('', False) on failure.\n        \"\"\"\n        try:\n            command = NetworkCommands.get_response_body(request_id)\n            response: GetResponseBodyResponse = await self._tab._execute_command(command)\n            body_result = response['result']\n            return body_result['body'], body_result['base64Encoded']\n        except Exception:\n            logger.debug('HAR: failed to fetch response body for %s', request_id)\n            return '', False\n\n    def _build_entry(self, pending: dict[str, Any]) -> HarEntry:\n        \"\"\"Build a HAR entry from accumulated pending data.\"\"\"\n        req_hdrs = pending.get('request_headers_extra') or pending.get('request_headers', {})\n        resp_hdrs = pending.get('response_headers_extra') or pending.get('response_headers', {})\n        url = pending.get('url', '')\n        protocol = self._normalize_http_version(pending.get('protocol', ''))\n        post_data_text = pending.get('post_data')\n\n        har_request = self._build_har_request(url, pending, req_hdrs, protocol, post_data_text)\n        har_response = self._build_har_response(pending, resp_hdrs, protocol)\n\n        response_ts: float = pending.get('response_timestamp', 0)\n        finished_ts: float = pending.get('finished_timestamp', 0)\n        receive_ms: float | None = None\n        if response_ts and finished_ts and finished_ts > response_ts:\n            receive_ms = (finished_ts - response_ts) * 1000\n\n        har_timings = self._build_har_timings(pending.get('timing'), receive_ms)\n        # Sum without ssl — connect already includes it per HAR 1.2 spec\n        _phases = (\n            har_timings['blocked'],\n            har_timings['dns'],\n            har_timings['connect'],\n            har_timings['send'],\n            har_timings['wait'],\n            har_timings['receive'],\n        )\n        total_time = sum(v for v in _phases if v > 0)\n\n        entry = HarEntry(\n            startedDateTime=self._wall_time_to_iso(pending.get('wall_time', 0)),\n            time=round(total_time, 2),\n            request=har_request,\n            response=har_response,\n            cache={},\n            timings=har_timings,\n        )\n\n        for key, field in [\n            ('remote_ip', 'serverIPAddress'),\n            ('connection_id', 'connection'),\n            ('resource_type', '_resourceType'),\n        ]:\n            if pending.get(key, ''):\n                entry[field] = pending[key]  # type: ignore[literal-required]\n\n        return entry\n\n    def _build_har_request(\n        self,\n        url: str,\n        pending: dict[str, Any],\n        headers: dict[str, str],\n        protocol: str,\n        post_data_text: str | None,\n    ) -> HarRequest:\n        \"\"\"Build the HarRequest portion of an entry.\"\"\"\n        req_cookies = self._parse_request_cookies(headers)\n        har_request = HarRequest(\n            method=pending.get('method', 'GET'),\n            url=url,\n            httpVersion=protocol,\n            cookies=req_cookies,\n            headers=self._headers_dict_to_list(headers),\n            queryString=self._parse_query_string(url),\n            headersSize=-1,\n            bodySize=len(post_data_text.encode('utf-8')) if post_data_text else 0,\n        )\n        if post_data_text:\n            ct = headers.get('Content-Type', headers.get('content-type', ''))\n            har_request['postData'] = HarPostData(mimeType=ct, text=post_data_text)\n        return har_request\n\n    def _build_har_response(\n        self,\n        pending: dict[str, Any],\n        headers: dict[str, str],\n        protocol: str,\n    ) -> HarResponse:\n        \"\"\"Build the HarResponse portion of an entry.\"\"\"\n        body = pending.get('response_body', '')\n        is_base64 = pending.get('response_body_base64', False)\n        status = pending.get('extra_status_code', pending.get('status', 0))\n\n        if body and is_base64:\n            try:\n                content_size = len(base64.b64decode(body))\n            except Exception:\n                content_size = len(body)\n        elif body:\n            content_size = len(body.encode('utf-8'))\n        else:\n            content_size = 0\n\n        har_content = HarContent(size=content_size, mimeType=pending.get('mime_type', ''))\n        if body:\n            har_content['text'] = body\n            if is_base64:\n                har_content['encoding'] = 'base64'\n\n        # bodySize from dataReceived chunks (actual body bytes, no header overhead)\n        # For 304 (cache hit), bodySize must be 0 per HAR spec\n        # When body_bytes is 0 but content exists (e.g. file:// protocol),\n        # fall back to content_size for consistency with content.size/text.\n        body_bytes = pending.get('body_bytes', -1)\n        if status == _HTTP_NOT_MODIFIED:\n            body_size = 0\n        elif body_bytes > 0:\n            body_size = body_bytes\n        elif content_size > 0:\n            body_size = content_size\n        else:\n            body_size = -1\n\n        redirect = headers.get('Location', headers.get('location', ''))\n        resp_cookies = self._parse_response_cookies(headers)\n        return HarResponse(\n            status=status,\n            statusText=pending.get('status_text', ''),\n            httpVersion=protocol,\n            cookies=resp_cookies,\n            headers=self._headers_dict_to_list(headers),\n            content=har_content,\n            redirectURL=redirect,\n            headersSize=-1,\n            bodySize=body_size,\n        )\n\n    @staticmethod\n    def _build_har_timings(\n        timing: ResourceTiming | None,\n        receive_ms: float | None = None,\n    ) -> HarTimings:\n        \"\"\"Convert CDP ResourceTiming to HAR timings (in milliseconds).\n\n        Args:\n            timing: CDP ResourceTiming from responseReceived.\n            receive_ms: Calculated receive time from monotonic timestamps\n                (loadingFinished.timestamp - responseReceived.timestamp).\n                When provided, overrides the header-based calculation.\n        \"\"\"\n        rcv = round(receive_ms, 3) if receive_ms is not None else 0\n        if not timing:\n            return HarTimings(\n                blocked=-1,\n                dns=-1,\n                connect=-1,\n                ssl=-1,\n                send=0,\n                wait=0,\n                receive=rcv,\n            )\n\n        dns_s: float = timing.get('dnsStart', -1)\n        dns_e: float = timing.get('dnsEnd', -1)\n        con_s: float = timing.get('connectStart', -1)\n        con_e: float = timing.get('connectEnd', -1)\n        ssl_s: float = timing.get('sslStart', -1)\n        ssl_e: float = timing.get('sslEnd', -1)\n        snd_s: float = timing.get('sendStart', 0)\n        snd_e: float = timing.get('sendEnd', 0)\n        rh_s: float = timing.get('receiveHeadersStart', 0)\n\n        def _phase(s: float, e: float) -> float:\n            return round(max(e - s, 0), 3) if s >= 0 and e >= 0 else -1\n\n        first = dns_s if dns_s >= 0 else (con_s if con_s >= 0 else snd_s)\n        return HarTimings(\n            blocked=round(max(first, 0), 3),\n            dns=_phase(dns_s, dns_e),\n            connect=_phase(con_s, con_e),\n            ssl=_phase(ssl_s, ssl_e),\n            send=round(max(snd_e - snd_s, 0), 3),\n            wait=round(max(rh_s - snd_e, 0), 3),\n            receive=rcv,\n        )\n\n    @staticmethod\n    def _normalize_http_version(protocol: str) -> str:\n        \"\"\"Normalize CDP protocol string to HAR httpVersion format.\n\n        CDP reports protocols like 'h2', 'h3', 'http/1.0', 'http/1.1',\n        or non-HTTP strings like 'file'. HAR viewers expect uppercase\n        HTTP versions (e.g. 'HTTP/1.1', 'h2', 'h3').\n        \"\"\"\n        if not protocol:\n            return ''\n        lower = protocol.lower()\n        if lower in {'h2', 'h3', 'h2c'}:\n            return lower\n        if lower.startswith('http/'):\n            return protocol.upper()\n        return ''\n\n    @staticmethod\n    def _headers_dict_to_list(headers: dict[str, str]) -> list[HarHeader]:\n        \"\"\"Convert a CDP headers dict to a HAR headers list.\"\"\"\n        return [HarHeader(name=name, value=value) for name, value in headers.items()]\n\n    @staticmethod\n    def _parse_query_string(url: str) -> list[HarQueryParam]:\n        \"\"\"Parse URL query string into HAR query param list.\"\"\"\n        parsed = urlparse(url)\n        if not parsed.query:\n            return []\n\n        params = parse_qs(parsed.query, keep_blank_values=True)\n        result: list[HarQueryParam] = []\n        for name, values in params.items():\n            for value in values:\n                result.append(HarQueryParam(name=name, value=value))\n        return result\n\n    @staticmethod\n    def _wall_time_to_iso(wall_time: float) -> str:\n        \"\"\"Convert a CDP wallTime (seconds since epoch) to ISO 8601 string.\"\"\"\n        if not wall_time:\n            return datetime.now(tz=timezone.utc).isoformat()\n        return datetime.fromtimestamp(wall_time, tz=timezone.utc).isoformat()\n\n    @staticmethod\n    def _parse_request_cookies(headers: dict[str, str]) -> list[HarCookie]:\n        \"\"\"Parse request cookies from the Cookie header.\"\"\"\n        cookie_header = headers.get('Cookie', headers.get('cookie', ''))\n        if not cookie_header:\n            return []\n\n        cookies: list[HarCookie] = []\n        for raw_pair in cookie_header.split(';'):\n            stripped = raw_pair.strip()\n            if '=' not in stripped:\n                continue\n            name, value = stripped.split('=', 1)\n            name = name.strip()\n            if name:\n                cookies.append(HarCookie(name=name, value=value.strip()))\n        return cookies\n\n    @staticmethod\n    def _parse_response_cookies(headers: dict[str, str]) -> list[HarCookie]:\n        \"\"\"Parse response cookies from Set-Cookie headers.\"\"\"\n        set_cookie = headers.get('Set-Cookie', headers.get('set-cookie', ''))\n        if not set_cookie:\n            return []\n\n        cookies: list[HarCookie] = []\n        for raw_line in set_cookie.split('\\n'):\n            stripped_line = raw_line.strip()\n            if '=' not in stripped_line:\n                continue\n            name_value = stripped_line.split(';', 1)[0]\n            name, value = name_value.split('=', 1)\n            name = name.strip()\n            if not name:\n                continue\n            cookie = HarCookie(name=name, value=value.strip())\n            attrs = stripped_line.split(';')[1:]\n            for raw_attr in attrs:\n                attr_lower = raw_attr.strip().lower()\n                if attr_lower == 'httponly':\n                    cookie['httpOnly'] = True\n                elif attr_lower == 'secure':\n                    cookie['secure'] = True\n                elif attr_lower.startswith('path='):\n                    cookie['path'] = attr_lower.split('=', 1)[1]\n                elif attr_lower.startswith('domain='):\n                    cookie['domain'] = attr_lower.split('=', 1)[1]\n            cookies.append(cookie)\n        return cookies\n\n\nclass HarCapture:\n    \"\"\"User-facing object returned by ``tab.request.record()`` context manager.\n\n    Provides access to recorded HAR entries and methods to export the\n    recording as a HAR 1.2 file.\n    \"\"\"\n\n    def __init__(self, recorder: HarRecorder):\n        self._recorder = recorder\n\n    @property\n    def entries(self) -> list[HarEntry]:\n        \"\"\"Return a sorted copy of the recorded HAR entries.\"\"\"\n        return sorted(self._recorder._entries, key=lambda e: e['startedDateTime'])\n\n    def to_dict(self) -> Har:\n        \"\"\"Build a full HAR 1.2 dictionary from the recorded entries.\n\n        Returns:\n            A complete HAR 1.2 dict ready for JSON serialization.\n        \"\"\"\n        return Har(\n            log=HarLog(\n                version='1.2',\n                creator=HarCreator(\n                    name=_PYDOLL_CREATOR_NAME,\n                    version=_get_pydoll_version(),\n                ),\n                pages=[],\n                entries=sorted(\n                    self._recorder._entries,\n                    key=lambda e: e['startedDateTime'],\n                ),\n            )\n        )\n\n    def save(self, path: str | Path) -> None:\n        \"\"\"Save the recording as a HAR 1.2 JSON file.\n\n        Args:\n            path: File path to write the HAR file to.\n        \"\"\"\n        har_dict = self.to_dict()\n        file_path = Path(path)\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n        with open(file_path, 'w', encoding='utf-8') as f:\n            json.dump(har_dict, f, indent=2, ensure_ascii=False)\n        logger.info('HAR recording saved to %s (%d entries)', path, len(self._recorder._entries))\n"
  },
  {
    "path": "pydoll/browser/requests/request.py",
    "content": "\"\"\"\nThis module provides a Request class that mimics the behavior of requests.\nIt allows making HTTP requests using the browser's fetch API.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json as jsonlib\nimport logging\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast\nfrom urllib.parse import parse_qs, urlencode, urlparse, urlunparse\n\nfrom pydoll.browser.requests.har_recorder import HarCapture, HarRecorder\nfrom pydoll.browser.requests.response import Response\nfrom pydoll.commands.runtime_commands import RuntimeCommands\nfrom pydoll.constants import Scripts\nfrom pydoll.exceptions import HTTPError\nfrom pydoll.protocol.fetch.types import HeaderEntry\nfrom pydoll.protocol.network.events import (\n    NetworkEvent,\n    RequestWillBeSentEvent,\n    RequestWillBeSentExtraInfoEvent,\n    ResponseReceivedEvent,\n    ResponseReceivedExtraInfoEvent,\n    ResponseReceivedExtraInfoEventParams,\n)\nfrom pydoll.protocol.network.types import CookieParam, ResourceType\n\nlogger = logging.getLogger(__name__)\n\nRequestReceivedEvent = Union[\n    ResponseReceivedEvent,\n    ResponseReceivedExtraInfoEvent,\n]\nRequestSentEvent = Union[\n    RequestWillBeSentEvent,\n    RequestWillBeSentExtraInfoEvent,\n]\n\nif TYPE_CHECKING:\n    from pydoll.browser.tab import Tab\n    from pydoll.protocol.network.events import (\n        RequestWillBeSentEventParams,\n        RequestWillBeSentExtraInfoEventParams,\n        ResponseReceivedEventParams,\n    )\n    from pydoll.protocol.runtime.methods import EvaluateResponse\n\n    RequestReceivedEventParams = Union[\n        ResponseReceivedEventParams,\n        ResponseReceivedExtraInfoEventParams,\n    ]\n    RequestSentEventParams = Union[\n        RequestWillBeSentEventParams,\n        RequestWillBeSentExtraInfoEventParams,\n    ]\n\n\nclass Request:\n    \"\"\"High-level interface for making HTTP requests using the browser's fetch API.\n\n    This class provides a requests-like interface that executes HTTP requests in the\n    browser's JavaScript context. All requests inherit the browser's current session\n    state including cookies, authentication headers, and other automatic browser\n    behaviors. This allows for seamless interaction with websites that require\n    authentication or have complex cookie management.\n\n    Key Features:\n    - Executes requests in the browser's JavaScript context using fetch API\n    - Automatically includes browser cookies and session state\n    - Preserves browser's security context and CORS policies\n    - Captures both request and response headers for analysis\n    - Supports all standard HTTP methods (GET, POST, PUT, DELETE, etc.)\n\n    Note:\n    - Headers passed to methods are additional headers, not replacements\n    - Browser's automatic headers (User-Agent, Accept, etc.) are preserved\n    - Cookies are managed automatically by the browser\n    \"\"\"\n\n    def __init__(self, tab: Tab):\n        \"\"\"Initialize a new Request instance bound to a browser tab.\n\n        Args:\n            tab: The browser tab instance where requests will be executed.\n                This tab provides the JavaScript execution context and maintains\n                the browser's session state (cookies, authentication, etc.).\n        \"\"\"\n        self.tab = tab\n        self._network_events_enabled = False\n        self._callback_ids: list[int] = []\n        self._requests_sent: list[RequestSentEvent] = []\n        self._requests_received: list[RequestReceivedEvent] = []\n        logger.debug('Request helper initialized for tab')\n\n    async def request(\n        self,\n        method: str,\n        url: str,\n        params: Optional[dict[str, str]] = None,\n        data: Optional[Union[dict, list, tuple, str, bytes]] = None,\n        json: Optional[dict[str, Any]] = None,\n        headers: Optional[list[HeaderEntry]] = None,\n        **kwargs,\n    ) -> Response:\n        \"\"\"Execute an HTTP request in the browser's JavaScript context.\n\n        This method uses the browser's fetch API to make requests, inheriting all\n        browser session state including cookies, authentication, and security context.\n        The request is executed as if made by the browser itself.\n\n        Args:\n            method: HTTP method (GET, POST, PUT, DELETE, etc.). Case insensitive.\n            url: Target URL for the request. Can be relative or absolute.\n            params: Query parameters to append to the URL. These are URL-encoded\n                and merged with any existing query string in the URL.\n            data: Request body data. Behavior depends on type:\n                - dict/list/tuple: URL-encoded as form data (application/x-www-form-urlencoded)\n                - str/bytes: Sent as-is with no Content-Type modification\n                Mutually exclusive with 'json' parameter.\n            json: Data to be JSON-serialized as request body. Automatically sets\n                Content-Type to application/json. Mutually exclusive with 'data'.\n            headers: Additional headers to include. These are ADDED to browser's\n                automatic headers, not replacements.\n                Format: [{'name': 'X-Custom', 'value': 'value'}]\n            **kwargs: Additional fetch API options (e.g., credentials, mode, cache).\n\n        Returns:\n            Response object containing status, headers, content, and cookies from\n            both the request and response phases.\n\n        Raises:\n            HTTPError: If the request execution fails or network error occurs.\n\n        Note:\n            - Browser cookies are automatically included\n            - CORS policies are enforced by the browser\n            - Authentication headers are preserved from browser session\n        \"\"\"\n        final_url = self._build_url_with_params(url, params)\n        options = self._build_request_options(method, headers, json, data, **kwargs)\n        logger.info(f'Executing request: method={method.upper()}, url={final_url}')\n        logger.debug(\n            f'Executing request: method={method.upper()}, url={final_url}, '\n            f'headers={bool(headers)}, json={json is not None}, data={data is not None}'\n        )\n        try:\n            result = await self._execute_fetch_request(final_url, options)\n            received_headers = self._extract_received_headers()\n            sent_headers = self._extract_sent_headers()\n            cookies = self._extract_set_cookies()\n            return self._build_response(result, received_headers, sent_headers, cookies)\n\n        except Exception as exc:\n            logger.error(f'Request failed: {exc}')\n            raise HTTPError(f'Request failed: {str(exc)}') from exc\n\n        finally:\n            await self._clear_callbacks()\n\n    async def get(\n        self,\n        url: str,\n        params: Optional[dict[str, str]] = None,\n        **kwargs,\n    ) -> Response:\n        \"\"\"Execute a GET request for retrieving data.\n\n        Args:\n            url: Target URL to retrieve data from.\n            params: Query parameters to append to URL.\n            **kwargs: Additional fetch options.\n\n        Returns:\n            Response object with retrieved data.\n        \"\"\"\n        return await self.request('GET', url, params=params, **kwargs)\n\n    async def post(\n        self,\n        url: str,\n        data: Optional[Union[dict, list, tuple, str, bytes]] = None,\n        json: Optional[dict[str, Any]] = None,\n        **kwargs,\n    ) -> Response:\n        \"\"\"Execute a POST request for creating or submitting data.\n\n        Args:\n            url: Target URL for data submission.\n            data: Form data to submit (URL-encoded).\n            json: JSON data to submit.\n            **kwargs: Additional fetch options.\n\n        Returns:\n            Response object with server's response to the submission.\n        \"\"\"\n        return await self.request('POST', url, data=data, json=json, **kwargs)\n\n    async def put(\n        self,\n        url: str,\n        data: Optional[Union[dict, list, tuple, str, bytes]] = None,\n        json: Optional[dict[str, Any]] = None,\n        **kwargs,\n    ) -> Response:\n        \"\"\"Execute a PUT request for updating/replacing resources.\n\n        Args:\n            url: Target URL of resource to update.\n            data: Form data for the update.\n            json: JSON data for the update.\n            **kwargs: Additional fetch options.\n\n        Returns:\n            Response object confirming the update operation.\n        \"\"\"\n        return await self.request('PUT', url, data=data, json=json, **kwargs)\n\n    async def patch(\n        self,\n        url: str,\n        data: Optional[Union[dict, list, tuple, str, bytes]] = None,\n        json: Optional[dict[str, Any]] = None,\n        **kwargs,\n    ) -> Response:\n        \"\"\"Execute a PATCH request for partial resource updates.\n\n        Args:\n            url: Target URL of resource to partially update.\n            data: Form data with changes to apply.\n            json: JSON data with changes to apply.\n            **kwargs: Additional fetch options.\n\n        Returns:\n            Response object confirming the partial update.\n        \"\"\"\n        return await self.request('PATCH', url, data=data, json=json, **kwargs)\n\n    async def delete(self, url: str, **kwargs) -> Response:\n        \"\"\"Execute a DELETE request for removing resources.\n\n        Args:\n            url: Target URL of resource to delete.\n            **kwargs: Additional fetch options.\n\n        Returns:\n            Response object confirming the deletion.\n        \"\"\"\n        return await self.request('DELETE', url, **kwargs)\n\n    async def head(self, url: str, **kwargs) -> Response:\n        \"\"\"Execute a HEAD request to retrieve only response headers.\n\n        Useful for checking resource existence, size, or modification date\n        without downloading the full content.\n\n        Args:\n            url: Target URL to check headers for.\n            **kwargs: Additional fetch options.\n\n        Returns:\n            Response object with headers but no body content.\n        \"\"\"\n        return await self.request('HEAD', url, **kwargs)\n\n    async def options(self, url: str, **kwargs) -> Response:\n        \"\"\"Execute an OPTIONS request to check allowed methods and capabilities.\n\n        Used for CORS preflight checks and discovering server capabilities.\n\n        Args:\n            url: Target URL to check options for.\n            **kwargs: Additional fetch options.\n\n        Returns:\n            Response object with allowed methods and CORS headers.\n        \"\"\"\n        return await self.request('OPTIONS', url, **kwargs)\n\n    @asynccontextmanager\n    async def record(\n        self,\n        resource_types: list[ResourceType] | None = None,\n    ) -> AsyncIterator[HarCapture]:\n        \"\"\"Record network traffic as HAR.\n\n        Context manager that captures all network activity on the tab\n        and produces a HarCapture object for export.\n\n        Args:\n            resource_types: Optional list of resource types to capture.\n                When provided, only requests matching these types are\n                recorded. When None (default), all resource types are\n                captured.\n\n        Usage::\n\n            async with tab.request.record() as capture:\n                await tab.go_to('https://example.com')\n            capture.save('flow.har')\n\n            # Record only fetch and XHR requests\n            async with tab.request.record(\n                resource_types=[ResourceType.FETCH, ResourceType.XHR]\n            ) as capture:\n                await tab.go_to('https://example.com')\n            capture.save('api_calls.har')\n\n        Yields:\n            HarCapture: Object with .save(), .to_dict(), and .entries.\n        \"\"\"\n        recorder = HarRecorder(self.tab, resource_types=resource_types)\n        capture = HarCapture(recorder)\n        await recorder.start()\n        try:\n            yield capture\n        finally:\n            await recorder.stop()\n\n    @staticmethod\n    def _build_url_with_params(url: str, params: Optional[dict[str, str]]) -> str:\n        \"\"\"Build final URL with query parameters.\"\"\"\n        logger.debug(f'Building URL with params: url={url}, params={params}')\n        if not params:\n            return url\n\n        parsed = urlparse(url)\n        query = parse_qs(parsed.query)\n        for key, value in params.items():\n            query[key] = [value]\n\n        return urlunparse(parsed._replace(query=urlencode(query, doseq=True)))\n\n    def _build_request_options(\n        self,\n        method: str,\n        headers: Optional[list[HeaderEntry]],\n        json: Optional[dict[str, Any]],\n        data: Optional[Union[dict, list, tuple, str, bytes]],\n        **kwargs,\n    ) -> dict[str, Any]:\n        \"\"\"Build request options dictionary.\"\"\"\n        headers_dict = self._convert_header_entries_to_dict(headers) if headers else {}\n        options = {\n            'method': method.upper(),\n            'headers': headers_dict,\n            **kwargs,\n        }\n        logger.debug(f'Building request options: options={options}')\n        self._add_request_body(options, json, data)\n        return options\n\n    def _add_request_body(\n        self,\n        options: dict[str, Any],\n        json: Optional[dict[str, Any]],\n        data: Optional[Union[dict, list, tuple, str, bytes]],\n    ) -> None:\n        \"\"\"Add request body and appropriate Content-Type header.\"\"\"\n        if json is not None:\n            self._handle_json_options(options, json)\n        elif data is not None:\n            self._handle_data_options(options, data)\n\n    @staticmethod\n    def _handle_json_options(options: dict[str, Any], json: Optional[dict[str, Any]]) -> None:\n        \"\"\"Handle JSON options.\"\"\"\n        options['body'] = jsonlib.dumps(json)\n        options['headers'].setdefault('Content-Type', 'application/json')\n        logger.debug('Request JSON body set and content-type applied')\n\n    @staticmethod\n    def _handle_data_options(\n        options: dict[str, Any], data: Optional[Union[dict, list, tuple, str, bytes]]\n    ) -> None:\n        \"\"\"Handle data options.\"\"\"\n        if isinstance(data, (dict, list, tuple)):\n            options['body'] = urlencode(data, doseq=True)\n            options['headers'].setdefault('Content-Type', 'application/x-www-form-urlencoded')\n            logger.debug('Request data encoded as form-urlencoded')\n        else:\n            options['body'] = data\n            logger.debug('Request data set as raw payload')\n\n    async def _execute_fetch_request(self, url: str, options: dict[str, Any]) -> EvaluateResponse:\n        \"\"\"Execute the fetch request using browser's runtime.\"\"\"\n        script = Scripts.MAKE_REQUEST.format(url=jsonlib.dumps(url), options=jsonlib.dumps(options))\n        await self._register_callbacks()\n        logger.debug('Registered network callbacks and executing fetch via Runtime.evaluate')\n\n        return await self.tab._execute_command(\n            RuntimeCommands.evaluate(\n                expression=script,\n                return_by_value=True,\n                await_promise=True,\n            )\n        )\n\n    @staticmethod\n    def _build_response(\n        result: EvaluateResponse,\n        response_headers: list[HeaderEntry],\n        request_headers: list[HeaderEntry],\n        cookies: list[CookieParam],\n    ) -> Response:\n        \"\"\"Build Response object from fetch result.\"\"\"\n        result_value = result['result']['result']['value']\n        logger.debug(f'Building response: result_value={result_value}')\n        return Response(\n            status_code=result_value['status'],\n            content=bytes(result_value.get('content', b'')),\n            text=result_value['text'],\n            json=result_value['json'],\n            response_headers=response_headers,\n            request_headers=request_headers,\n            cookies=cookies,\n            url=result_value['url'],\n        )\n\n    async def _register_callbacks(self) -> None:\n        \"\"\"Register network event listeners to capture request/response metadata.\n\n        Sets up CDP event listeners to capture all network activity during the\n        request execution. This includes both outgoing request data and incoming\n        response data, which are used for header and cookie extraction.\n\n        Note:\n            Network events are only enabled if not already active on the tab.\n        \"\"\"\n        if not self.tab.network_events_enabled:\n            await self.tab.enable_network_events()\n            self._network_events_enabled = True\n            logger.debug('Network events enabled on tab for request capture')\n\n        def append_received_request(event: dict) -> None:\n            self._requests_received.append(cast(RequestReceivedEvent, event))\n            logger.debug(f'Appended received request: event={event}')\n\n        def append_sent_request(event: dict) -> None:\n            self._requests_sent.append(cast(RequestSentEvent, event))\n            logger.debug(f'Appended sent request: event={event}')\n\n        self._callback_ids = [\n            await self.tab.on(\n                NetworkEvent.REQUEST_WILL_BE_SENT,\n                callback=append_sent_request,\n            ),\n            await self.tab.on(\n                NetworkEvent.REQUEST_WILL_BE_SENT_EXTRA_INFO,\n                callback=append_sent_request,\n            ),\n            await self.tab.on(\n                NetworkEvent.RESPONSE_RECEIVED,\n                callback=append_received_request,\n            ),\n            await self.tab.on(\n                NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                callback=append_received_request,\n            ),\n        ]\n\n    async def _clear_callbacks(self) -> None:\n        \"\"\"Clean up network event listeners and disable network monitoring.\n\n        Removes only the callbacks registered by this request instance\n        (surgical removal) so other listeners (e.g. HarRecorder) are\n        not affected.\n        \"\"\"\n        for callback_id in self._callback_ids:\n            await self.tab.remove_callback(callback_id)\n        self._callback_ids.clear()\n        if self._network_events_enabled:\n            await self.tab.disable_network_events()\n            self._network_events_enabled = False\n            logger.debug('Network events disabled on tab after request')\n\n    def _extract_received_headers(self) -> list[HeaderEntry]:\n        \"\"\"Extract headers from response network events.\n\n        Returns:\n            List of headers received from the server during response.\n        \"\"\"\n        event_extractors: dict[str, Callable[[Any], list[HeaderEntry]]] = {\n            'response': self._extract_response_received_headers,\n            'blockedCookies': self._extract_response_received_extra_info_headers,\n        }\n\n        return self._extract_headers_from_events(self._requests_received, event_extractors)\n\n    def _extract_sent_headers(self) -> list[HeaderEntry]:\n        \"\"\"Extract headers from request network events.\n\n        Returns:\n            List of headers that were actually sent in the request.\n        \"\"\"\n        event_extractors: dict[str, Callable[[Any], list[HeaderEntry]]] = {\n            'request': self._extract_request_sent_headers,\n            'associatedCookies': self._extract_request_sent_extra_info_headers,\n        }\n\n        return self._extract_headers_from_events(self._requests_sent, event_extractors)\n\n    @staticmethod\n    def _extract_headers_from_events(\n        events: Union[list[RequestSentEvent], list[RequestReceivedEvent]],\n        event_extractors: dict[str, Callable[[Any], list[HeaderEntry]]],\n    ) -> list[HeaderEntry]:\n        \"\"\"Extract headers from network events using appropriate extractors.\n\n        Args:\n            events: List of network events to process.\n            event_extractors: Mapping of event keys to header extraction functions.\n\n        Returns:\n            Deduplicated list of headers from all matching events.\n\n        Note:\n            Headers are deduplicated based on name-value pairs to avoid\n            duplicate entries from multiple event types.\n        \"\"\"\n        headers: list[HeaderEntry] = []\n        seen = set()\n        logger.debug(f'Extracting headers from events: events={events}')\n        for event in events:\n            params = event['params']\n            for key, extractor in event_extractors.items():\n                if key in params:\n                    extracted_headers = extractor(params)\n                    logger.debug(f'Extracted headers: extracted_headers={extracted_headers}')\n                    for header in extracted_headers:\n                        identity = (header['name'], header['value'])\n                        logger.debug(f'Identity: identity={identity}')\n                        if identity not in seen:\n                            headers.append(header)\n                            seen.add(identity)\n                            logger.debug(f'Added header: header={header}')\n                    break\n\n        logger.debug(f'Headers extracted: headers={headers}')\n        return headers\n\n    def _extract_request_sent_headers(\n        self, params: RequestWillBeSentEventParams\n    ) -> list[HeaderEntry]:\n        \"\"\"Extract headers from main request event.\n\n        Args:\n            params: Event parameters containing request details.\n\n        Returns:\n            List of headers that were sent with the request.\n        \"\"\"\n        request = params['request']\n        logger.debug(f'Extracting request sent headers: request={request}')\n        return self._convert_dict_to_header_entries(request.get('headers', {}))\n\n    def _extract_request_sent_extra_info_headers(\n        self, params: RequestWillBeSentExtraInfoEventParams\n    ) -> list[HeaderEntry]:\n        \"\"\"Extract headers from extra request info event.\n\n        This event contains additional header information that may not be\n        present in the main request event, such as security-related headers.\n\n        Args:\n            params: Extra info event parameters containing additional headers.\n\n        Returns:\n            List of additional headers sent with the request.\n        \"\"\"\n        logger.debug(f'Extracting request sent extra info headers: params={params}')\n        return self._convert_dict_to_header_entries(params.get('headers', {}))\n\n    def _extract_response_received_headers(\n        self, params: ResponseReceivedEventParams\n    ) -> list[HeaderEntry]:\n        \"\"\"Extract headers from main response event.\n\n        Args:\n            params: Event parameters containing response details.\n\n        Returns:\n            List of headers received from the server.\n        \"\"\"\n        response = params['response']\n        logger.debug(f'Extracting response received headers: response={response}')\n        return self._convert_dict_to_header_entries(response.get('headers', {}))\n\n    def _extract_response_received_extra_info_headers(\n        self, params: ResponseReceivedExtraInfoEventParams\n    ) -> list[HeaderEntry]:\n        \"\"\"Extract headers from extra response info event.\n\n        This event contains additional response header information, including\n        Set-Cookie headers and security-related headers that may be filtered\n        from the main response event.\n\n        Args:\n            params: Extra info event parameters containing additional headers.\n\n        Returns:\n            List of additional headers received from the server.\n        \"\"\"\n        logger.debug(f'Extracting response received extra info headers: params={params}')\n        return self._convert_dict_to_header_entries(params.get('headers', {}))\n\n    @staticmethod\n    def _convert_dict_to_header_entries(headers_dict: dict) -> list[HeaderEntry]:\n        \"\"\"Convert header dictionary to standardized HeaderEntry format.\n\n        Args:\n            headers_dict: Dictionary mapping header names to values.\n\n        Returns:\n            List of HeaderEntry objects with 'name' and 'value' keys.\n        \"\"\"\n        logger.debug(f'Converting dictionary to header entries: headers_dict={headers_dict}')\n        return [HeaderEntry(name=name, value=value) for name, value in headers_dict.items()]\n\n    def _extract_set_cookies(self) -> list[CookieParam]:\n        \"\"\"Extract and parse all Set-Cookie headers from response events.\n\n        Processes response events to find Set-Cookie headers and converts them\n        into structured cookie objects. Handles multiple Set-Cookie headers\n        and multi-line cookie declarations.\n\n        Returns:\n            List of unique cookies extracted from Set-Cookie headers.\n        \"\"\"\n        cookies: list[CookieParam] = []\n        logger.debug(f'Extracting set cookies: cookies={cookies}')\n        response_extra_info_events = self._filter_response_extra_info_events()\n        logger.debug(\n            f'Filtering response extra info events: '\n            f'response_extra_info_events={response_extra_info_events}'\n        )\n        for event in response_extra_info_events:\n            params = cast(ResponseReceivedExtraInfoEventParams, event['params'])\n            headers = self._convert_dict_to_header_entries(params['headers'])\n            logger.debug(f'Converting dictionary to header entries: headers={headers}')\n            set_cookie_headers = [\n                header['value'] for header in headers if header['name'] == 'Set-Cookie'\n            ]\n            logger.debug(f'Set cookie headers: set_cookie_headers={set_cookie_headers}')\n            if set_cookie_headers:\n                for set_cookie_header in set_cookie_headers:\n                    self._add_unique_cookies(\n                        cookies, self._parse_set_cookie_header(set_cookie_header)\n                    )\n        logger.debug(f'Set cookies extracted: cookies={cookies}')\n        return cookies\n\n    def _filter_response_extra_info_events(self) -> list[RequestReceivedEvent]:\n        \"\"\"Filter network events to find those containing Set-Cookie information.\n\n        Returns:\n            List of events that contain extra response information including cookies.\n        \"\"\"\n        logger.debug(\n            f'Filtering response extra info events: requests_received={self._requests_received}'\n        )\n        return [\n            event\n            for event in self._requests_received\n            if event['method'] == NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO\n        ]\n\n    def _parse_set_cookie_header(self, set_cookie_header: str) -> list[CookieParam]:\n        \"\"\"Parse a Set-Cookie header value into individual cookie objects.\n\n        Handles both single and multi-line Set-Cookie headers, extracting\n        cookie name-value pairs while ignoring attributes like Path, Domain, etc.\n\n        Args:\n            set_cookie_header: Raw Set-Cookie header value from HTTP response.\n\n        Returns:\n            List of parsed cookie objects with name and value.\n        \"\"\"\n        cookies = []\n        lines = set_cookie_header.split('\\n')\n        logger.debug(f'Parsing set cookie header: set_cookie_header={set_cookie_header}')\n        for line in lines:\n            cookie = self._parse_cookie_line(line)\n            if cookie:\n                logger.debug(f'Parsed cookie: cookie={cookie}')\n                cookies.append(cookie)\n        logger.debug(f'Parsed cookies: cookies={cookies}')\n        return cookies\n\n    @staticmethod\n    def _parse_cookie_line(line: str) -> Optional[CookieParam]:\n        \"\"\"Parse a single cookie line to extract name and value.\n\n        Extracts only the cookie name and value, ignoring all cookie attributes\n        like Path, Domain, Secure, HttpOnly, etc. Rejects cookies with empty names.\n\n        Args:\n            line: Single line from Set-Cookie header.\n\n        Returns:\n            CookieParam object with name and value, or None if parsing fails or name is empty.\n        \"\"\"\n        if '=' not in line:\n            return None\n\n        name = line.split('=', 1)[0].strip()\n        value = line.split('=', 1)[1].split(';', 1)[0].strip()\n\n        # Reject cookies with empty names\n        if not name:\n            return None\n\n        return CookieParam(name=name, value=value)\n\n    @staticmethod\n    def _add_unique_cookies(cookies: list[CookieParam], new_cookies: list[CookieParam]) -> None:\n        \"\"\"Add cookies to list while avoiding duplicates.\n\n        Args:\n            cookies: Existing list of cookies to add to.\n            new_cookies: New cookies to add if not already present.\n        \"\"\"\n        logger.debug(f'Adding unique cookies: cookies={cookies}, new_cookies={new_cookies}')\n        for cookie in new_cookies:\n            if cookie not in cookies:\n                cookies.append(cookie)\n                logger.debug(f'Added unique cookie: cookie={cookie}')\n        logger.debug(f'Unique cookies added: cookies={cookies}')\n\n    @staticmethod\n    def _convert_header_entries_to_dict(headers: list[HeaderEntry]) -> dict[str, str]:\n        \"\"\"Convert HeaderEntry objects to a plain dictionary format.\n\n        Used for preparing headers for the JavaScript fetch API which expects\n        a simple object mapping header names to values.\n\n        Args:\n            headers: List of HeaderEntry objects with 'name' and 'value' keys.\n\n        Returns:\n            Dictionary mapping header names to values.\n        \"\"\"\n        logger.debug(f'Converting header entries to dictionary: headers={headers}')\n        return {header['name']: header['value'] for header in headers}\n"
  },
  {
    "path": "pydoll/browser/requests/response.py",
    "content": "from __future__ import annotations\n\nimport json as jsonlib\nimport logging\nfrom typing import TYPE_CHECKING, Any, Optional, Union\n\nfrom pydoll.exceptions import HTTPError\n\nif TYPE_CHECKING:\n    from pydoll.protocol.fetch.types import HeaderEntry\n    from pydoll.protocol.network.types import CookieParam\n\nlogger = logging.getLogger(__name__)\n\nSTATUS_CODE_RANGE_OK = range(200, 400)\n\n\nclass Response:\n    \"\"\"HTTP response object for browser-based fetch requests.\n\n    This class provides a standardized interface for handling HTTP responses\n    obtained through the browser's fetch API. It mimics the requests.Response\n    interface while preserving all browser-specific metadata including cookies,\n    headers, and network timing information.\n\n    Key Features:\n    - Compatible with requests.Response API for easy migration\n    - Preserves both request and response headers for analysis\n    - Automatic cookie extraction from Set-Cookie headers\n    - Lazy JSON parsing with caching\n    - Browser-context aware (respects CORS, security policies)\n    - Content available in multiple formats (text, bytes, JSON)\n\n    The response contains all data captured during the browser's fetch execution,\n    including redirects, authentication flows, and any browser-applied transformations.\n    \"\"\"\n\n    def __init__(\n        self,\n        status_code: int,\n        content: bytes = b'',\n        text: str = '',\n        json: Optional[dict[str, Any]] = None,\n        response_headers: Optional[list[HeaderEntry]] = None,\n        request_headers: Optional[list[HeaderEntry]] = None,\n        cookies: Optional[list[CookieParam]] = None,\n        url: str = '',\n    ):\n        \"\"\"Initialize a new Response instance with browser fetch results.\n\n        Args:\n            status_code: HTTP status code returned by the server (e.g., 200, 404, 500).\n            content: Raw response body as bytes. Used for binary data or when\n                text encoding is uncertain.\n            text: Response body as decoded string. Pre-decoded by browser's fetch API.\n            json: Pre-parsed JSON data if response Content-Type was application/json.\n                If None, json() method will attempt to parse from text on demand.\n            response_headers: Headers received from the server, including Set-Cookie,\n                Content-Type, and any custom headers sent by the server.\n            request_headers: Headers that were actually sent in the request, including\n                browser-generated headers (User-Agent, Accept, etc.) and custom headers.\n            cookies: Cookies extracted from Set-Cookie headers during the response.\n                These represent new/updated cookies from this specific request.\n            url: Final URL after any redirects. May differ from original request URL\n                if the server performed redirects during the request.\n        \"\"\"\n        self._status_code = status_code\n        self._content = content\n        self._text = text\n        self._json = json\n        self._response_headers = response_headers or []\n        self._request_headers = request_headers or []\n        self._cookies = cookies or []\n        self._url = url\n        self._ok = status_code in STATUS_CODE_RANGE_OK\n        logger.debug(\n            f'Response initialized: status={status_code}, url={url}, '\n            f'headers={len(self._response_headers)}, cookies={len(self._cookies)}'\n        )\n\n    @property\n    def ok(self) -> bool:\n        \"\"\"Check if the request was successful (2xx status codes).\n\n        Returns:\n            True if status code is in the 200-399 range, False otherwise.\n\n        Note:\n            This follows HTTP conventions where 2xx codes indicate success\n            and 3xx codes indicate redirection (still considered \"ok\").\n        \"\"\"\n        return self._ok\n\n    @property\n    def cookies(self) -> list[CookieParam]:\n        \"\"\"Get cookies that were set by the server during this response.\n\n        Returns:\n            List of cookies extracted from Set-Cookie headers. Each cookie\n            contains name and value, with cookie attributes (Path, Domain, etc.)\n            automatically handled by the browser.\n\n        Note:\n            These are only NEW/UPDATED cookies from this response. Existing\n            browser cookies are managed automatically by the browser context.\n        \"\"\"\n        return self._cookies\n\n    @property\n    def request_headers(self) -> list[HeaderEntry]:\n        \"\"\"Get headers that were actually sent in the HTTP request.\n\n        Returns:\n            List of headers sent to the server, including both custom headers\n            provided by the user and automatic headers added by the browser\n            (User-Agent, Accept, Authorization, etc.).\n\n        Note:\n            This shows the ACTUAL headers sent, which may differ from what\n            was originally specified due to browser modifications.\n        \"\"\"\n        return self._request_headers\n\n    @property\n    def headers(self) -> list[HeaderEntry]:\n        \"\"\"Get headers received from the server in the HTTP response.\n\n        Returns:\n            List of response headers sent by the server, including standard\n            headers (Content-Type, Content-Length, etc.) and any custom headers.\n\n        Note:\n            Some security-sensitive headers may be filtered by the browser\n            and not appear in this list due to CORS policies.\n        \"\"\"\n        return self._response_headers\n\n    @property\n    def status_code(self) -> int:\n        \"\"\"Get the HTTP status code returned by the server.\n\n        Returns:\n            Integer status code (e.g., 200 for OK, 404 for Not Found, 500 for Server Error).\n        \"\"\"\n        return self._status_code\n\n    @property\n    def text(self) -> str:\n        \"\"\"Get the response content as a decoded string.\n\n        Returns:\n            Response body decoded as UTF-8 string. If no text was provided\n            during initialization, it will be decoded from the raw content.\n\n        Note:\n            Decoding uses 'replace' error handling to avoid crashes on\n            invalid UTF-8 sequences.\n        \"\"\"\n        if not self._text and self.content:\n            self._text = self.content.decode('utf-8', errors='replace')\n        return self._text\n\n    @property\n    def content(self) -> bytes:\n        \"\"\"Get the raw response content as bytes.\n\n        Returns:\n            Unmodified response body as bytes. Useful for binary data\n            (images, files, etc.) or when you need to handle encoding manually.\n        \"\"\"\n        return self._content\n\n    @property\n    def url(self) -> str:\n        \"\"\"Get the final URL of the response after any redirects.\n\n        Returns:\n            The final URL that was accessed, which may differ from the\n            original request URL if redirects occurred.\n        \"\"\"\n        return self._url\n\n    def json(self) -> Union[dict[str, Any], list]:\n        \"\"\"Parse and return the response content as JSON data.\n\n        Attempts to parse the response text as JSON. Uses caching to avoid\n        re-parsing the same content multiple times.\n\n        Returns:\n            Parsed JSON data as dictionary, list, or other JSON-compatible type.\n\n        Raises:\n            ValueError: If the response content is not valid JSON or if parsing fails.\n\n        Note:\n            - Uses lazy parsing: JSON is only parsed when first accessed\n            - Subsequent calls return cached result for better performance\n            - If JSON was pre-parsed during initialization, that result is returned\n        \"\"\"\n        if self._json is not None:\n            return self._json\n\n        try:\n            self._json = jsonlib.loads(self.text)\n            return self._json\n        except jsonlib.JSONDecodeError as exc:\n            logger.debug('Failed to decode response as JSON')\n            raise ValueError('Response is not valid JSON') from exc\n\n    def raise_for_status(self) -> None:\n        \"\"\"Raise an HTTPError if the response indicates an HTTP error status.\n\n        Checks the status code and raises an exception for client errors (4xx)\n        and server errors (5xx). Successful responses (2xx) and redirects (3xx)\n        do not raise an exception.\n\n        Raises:\n            HTTPError: If status code is 400 or higher, indicating an error.\n\n        Note:\n            This method is compatible with requests.Response.raise_for_status()\n            for easy migration from the requests library.\n        \"\"\"\n        if self.status_code not in STATUS_CODE_RANGE_OK:\n            logger.error(\n                f'HTTP error status encountered: status={self.status_code}, url={self._url}'\n            )\n            raise HTTPError(f'{self.status_code} Client Error: for url {self._url}')\n"
  },
  {
    "path": "pydoll/browser/tab.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport base64 as _b64\nimport contextlib\nimport io\nimport logging\nimport shutil\nimport warnings\nimport zipfile\nfrom contextlib import asynccontextmanager\nfrom functools import partial\nfrom pathlib import Path\nfrom tempfile import mkdtemp\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    AsyncGenerator,\n    Awaitable,\n    Callable,\n    Optional,\n    TypeAlias,\n    Union,\n    cast,\n    overload,\n)\n\nimport aiofiles\n\nfrom pydoll.browser.requests import Request\nfrom pydoll.commands import (\n    DomCommands,\n    FetchCommands,\n    NetworkCommands,\n    PageCommands,\n    RuntimeCommands,\n    StorageCommands,\n    TargetCommands,\n)\nfrom pydoll.connection import ConnectionHandler\nfrom pydoll.constants import By, PageLoadState\nfrom pydoll.elements.mixins import FindElementsMixin\nfrom pydoll.elements.shadow_root import ShadowRoot\nfrom pydoll.elements.web_element import WebElement\nfrom pydoll.exceptions import (\n    CommandExecutionTimeout,\n    DownloadTimeout,\n    IFrameNotFound,\n    InvalidFileExtension,\n    InvalidIFrame,\n    InvalidScriptWithElement,\n    InvalidTabInitialization,\n    MissingScreenshotPath,\n    NetworkEventsNotEnabled,\n    NoDialogPresent,\n    NotAnIFrame,\n    PageLoadTimeout,\n    TopLevelTargetRequired,\n    WaitElementTimeout,\n    WebSocketConnectionClosed,\n)\nfrom pydoll.interactions import KeyboardAPI, MouseAPI, ScrollAPI\nfrom pydoll.interactions.iframe import IFrameContext\nfrom pydoll.protocol.browser.types import DownloadBehavior, DownloadProgressState\nfrom pydoll.protocol.dom.types import Node, ShadowRootType\nfrom pydoll.protocol.network.types import ResourceType\nfrom pydoll.protocol.page.events import PageEvent\nfrom pydoll.protocol.page.types import FrameResourceTree, ScreenshotFormat\nfrom pydoll.protocol.runtime.methods import (\n    CallFunctionOnResponse,\n    EvaluateResponse,\n    SerializationOptions,\n)\nfrom pydoll.protocol.runtime.types import CallArgument\nfrom pydoll.protocol.target.types import TargetInfo\nfrom pydoll.utils import (\n    decode_base64_to_bytes,\n    has_return_outside_function,\n)\nfrom pydoll.utils.bundle import (\n    build_asset_filename,\n    collect_frame_resources,\n    filter_fetchable_resources,\n    inline_all_assets,\n    rewrite_html_urls,\n)\n\nif TYPE_CHECKING:\n    from pydoll.browser.chromium.base import Browser\n    from pydoll.protocol.base import EmptyResponse, Response\n    from pydoll.protocol.browser.events import (\n        DownloadProgressEvent,\n        DownloadWillBeginEvent,\n    )\n    from pydoll.protocol.dom.methods import (\n        DescribeNodeResponse,\n        GetDocumentResponse,\n        ResolveNodeResponse,\n    )\n    from pydoll.protocol.fetch.types import AuthChallengeResponseType, HeaderEntry, RequestStage\n    from pydoll.protocol.network.events import RequestWillBeSentEvent\n    from pydoll.protocol.network.methods import GetCookiesResponse as NetworkGetCookiesResponse\n    from pydoll.protocol.network.methods import GetResponseBodyResponse\n    from pydoll.protocol.network.types import (\n        Cookie,\n        CookieParam,\n        ErrorReason,\n        RequestMethod,\n    )\n    from pydoll.protocol.page.events import FileChooserOpenedEvent\n    from pydoll.protocol.page.methods import (\n        CaptureScreenshotResponse,\n        GetResourceContentResponse,\n        GetResourceTreeResponse,\n        PrintToPDFResponse,\n    )\n    from pydoll.protocol.runtime.methods import CallFunctionOnResponse, EvaluateResponse\n    from pydoll.protocol.storage.methods import GetCookiesResponse as StorageGetCookiesResponse\n    from pydoll.protocol.target.methods import AttachToTargetResponse, GetTargetsResponse\n\nlogger = logging.getLogger(__name__)\n\nIFrame: TypeAlias = 'Tab'\n\n_CLOUDFLARE_CHALLENGE_DOMAIN = 'challenges.cloudflare.com'\n_CLOUDFLARE_IFRAME_SELECTOR = f'iframe[src*=\"{_CLOUDFLARE_CHALLENGE_DOMAIN}\"]'\n_CLOUDFLARE_CHECKBOX_SELECTOR = 'span.cb-i'\n\n\nclass Tab(FindElementsMixin):\n    \"\"\"\n    Controls a browser tab via Chrome DevTools Protocol.\n\n    Primary interface for web page automation including navigation, DOM manipulation,\n    JavaScript execution, event handling, network monitoring, and specialized tasks\n    like Cloudflare bypass.\n    \"\"\"\n\n    def __init__(\n        self,\n        browser: Browser,\n        connection_port: Optional[int] = None,\n        target_id: Optional[str] = None,\n        browser_context_id: Optional[str] = None,\n        ws_address: Optional[str] = None,\n    ):\n        \"\"\"\n        Initialize tab controller for existing browser tab.\n\n        Args:\n            browser: Browser instance that created this tab.\n            connection_port: CDP WebSocket port.\n            target_id: CDP target identifier for this tab.\n            browser_context_id: Optional browser context ID.\n            ws_address: Optional WebSocket address for this tab.\n        \"\"\"\n        if not any([connection_port, target_id, ws_address]):\n            raise InvalidTabInitialization()\n\n        self._browser = browser\n        self._connection_port = connection_port\n        self._target_id = target_id\n        self._ws_address = ws_address\n        self._browser_context_id = browser_context_id\n        self._connection_handler = self._get_connection_handler()\n        self._page_events_enabled = False\n        self._network_events_enabled = False\n        self._fetch_events_enabled = False\n        self._dom_events_enabled = False\n        self._runtime_events_enabled = False\n        self._intercept_file_chooser_dialog_enabled = False\n        self._cloudflare_captcha_callback_id: Optional[int] = None\n        self._request: Optional[Request] = None\n        self._scroll: Optional[ScrollAPI] = None\n        self._keyboard: Optional[KeyboardAPI] = None\n        self._mouse: MouseAPI = MouseAPI(self)\n        logger.debug(\n            (\n                f'Tab initialized: target_id={self._target_id}, '\n                f'ws_address_set={bool(self._ws_address)}, '\n                f'context_id={self._browser_context_id}, port={self._connection_port}'\n            )\n        )\n\n    @property\n    def page_events_enabled(self) -> bool:\n        \"\"\"Whether CDP Page domain events are enabled.\"\"\"\n        return self._page_events_enabled\n\n    @property\n    def network_events_enabled(self) -> bool:\n        \"\"\"Whether CDP Network domain events are enabled.\"\"\"\n        return self._network_events_enabled\n\n    @property\n    def fetch_events_enabled(self) -> bool:\n        \"\"\"Whether CDP Fetch domain events (request interception) are enabled.\"\"\"\n        return self._fetch_events_enabled\n\n    @property\n    def dom_events_enabled(self) -> bool:\n        \"\"\"Whether CDP DOM domain events are enabled.\"\"\"\n        return self._dom_events_enabled\n\n    @property\n    def runtime_events_enabled(self) -> bool:\n        \"\"\"Whether CDP Runtime domain events are enabled.\"\"\"\n        return self._runtime_events_enabled\n\n    @property\n    def request(self) -> Request:\n        \"\"\"\n        Get the request object for making HTTP requests using the browser's fetch API.\n\n        Returns:\n            Request: An instance of the Request class for making HTTP requests.\n        \"\"\"\n        if self._request is None:\n            self._request = Request(self)\n        return self._request\n\n    @property\n    def scroll(self) -> ScrollAPI:\n        \"\"\"\n        Get the scroll API for controlling page scroll behavior.\n\n        Returns:\n            ScrollAPI: An instance of the ScrollAPI class for scroll operations.\n        \"\"\"\n        if self._scroll is None:\n            self._scroll = ScrollAPI(self)\n        return self._scroll\n\n    @property\n    def keyboard(self) -> KeyboardAPI:\n        \"\"\"\n        Get the keyboard API for controlling keyboard input at page level.\n\n        Returns:\n            KeyboardAPI: An instance of the KeyboardAPI class for keyboard operations.\n        \"\"\"\n        if self._keyboard is None:\n            self._keyboard = KeyboardAPI(self)\n        return self._keyboard\n\n    @property\n    def mouse(self) -> MouseAPI:\n        \"\"\"\n        Get the mouse API for controlling mouse input.\n\n        Returns:\n            MouseAPI: An instance of the MouseAPI class for mouse operations.\n        \"\"\"\n        return self._mouse\n\n    @property\n    def intercept_file_chooser_dialog_enabled(self) -> bool:\n        \"\"\"Whether file chooser dialog interception is active.\"\"\"\n        return self._intercept_file_chooser_dialog_enabled\n\n    @property\n    async def current_url(self) -> str:\n        \"\"\"Get current page URL (reflects redirects and client-side navigation).\"\"\"\n        response: EvaluateResponse = await self._execute_command(\n            RuntimeCommands.evaluate('window.location.href')\n        )\n        return response['result']['result']['value']\n\n    @property\n    async def page_source(self) -> str:\n        \"\"\"Get complete HTML source of current page (live DOM state).\"\"\"\n        response: EvaluateResponse = await self._execute_command(\n            RuntimeCommands.evaluate('document.documentElement.outerHTML')\n        )\n        return response['result']['result']['value']\n\n    @property\n    async def title(self) -> str:\n        \"\"\"Get current page title.\"\"\"\n        response: EvaluateResponse = await self._execute_command(\n            RuntimeCommands.evaluate('document.title')\n        )\n        return response['result']['result'].get('value', '')\n\n    async def enable_page_events(self):\n        \"\"\"Enable CDP Page domain events (load, navigation, dialogs, etc.).\"\"\"\n        logger.debug('Enabling Page events')\n        response = await self._execute_command(PageCommands.enable())\n        self._page_events_enabled = True\n        logger.debug('Page events enabled')\n        return response\n\n    async def enable_network_events(self):\n        \"\"\"Enable CDP Network domain events (requests, responses, etc.).\"\"\"\n        logger.debug('Enabling Network events')\n        response = await self._execute_command(NetworkCommands.enable())\n        self._network_events_enabled = True\n        logger.debug('Network events enabled')\n        return response\n\n    async def enable_fetch_events(\n        self,\n        handle_auth: bool = False,\n        resource_type: Optional[ResourceType] = None,\n        request_stage: Optional[RequestStage] = None,\n    ):\n        \"\"\"\n        Enable CDP Fetch domain for request interception.\n\n        Args:\n            handle_auth: Intercept authentication challenges.\n            resource_type: Filter by resource type (all if None).\n            request_stage: When to intercept (Request/Response).\n\n        Note:\n            Intercepted requests must be explicitly continued or timeout.\n        \"\"\"\n        logger.debug(\n            f'Enabling Fetch events: handle_auth={handle_auth}, resource_type={resource_type}, '\n            f'stage={request_stage}'\n        )\n        response: Response[EmptyResponse] = await self._execute_command(\n            FetchCommands.enable(\n                handle_auth_requests=handle_auth,\n                resource_type=resource_type,\n                request_stage=request_stage,\n            )\n        )\n        self._fetch_events_enabled = True\n        logger.debug('Fetch events enabled')\n        return response\n\n    async def enable_dom_events(self):\n        \"\"\"Enable CDP DOM domain events (document structure changes).\"\"\"\n        logger.debug('Enabling DOM events')\n        response = await self._execute_command(DomCommands.enable())\n        self._dom_events_enabled = True\n        logger.debug('DOM events enabled')\n        return response\n\n    async def enable_runtime_events(self):\n        \"\"\"Enable CDP Runtime domain events.\"\"\"\n        logger.debug('Enabling Runtime events')\n        response = await self._execute_command(RuntimeCommands.enable())\n        self._runtime_events_enabled = True\n        logger.debug('Runtime events enabled')\n        return response\n\n    async def enable_intercept_file_chooser_dialog(self):\n        \"\"\"\n        Enable file chooser dialog interception for automated uploads.\n\n        Note:\n            Use expect_file_chooser context manager for convenience.\n        \"\"\"\n        logger.info('Enabling file chooser interception')\n        response = await self._execute_command(PageCommands.set_intercept_file_chooser_dialog(True))\n        self._intercept_file_chooser_dialog_enabled = True\n        logger.debug('File chooser interception enabled')\n        return response\n\n    async def enable_auto_solve_cloudflare_captcha(\n        self,\n        custom_selector: Optional[tuple[By, str]] = None,\n        time_before_click: Optional[float] = None,\n        time_to_wait_captcha: float = 5,\n    ):\n        \"\"\"\n        Enable automatic Cloudflare Turnstile captcha bypass.\n\n        Args:\n            custom_selector: Deprecated — ignored. Cloudflare Turnstile is now\n                detected automatically via shadow root inspection.\n            time_before_click: Deprecated — ignored. The checkbox is now\n                located via shadow root polling and clicked immediately.\n            time_to_wait_captcha: Timeout for captcha detection (default 5s).\n        \"\"\"\n        if custom_selector is not None:\n            warnings.warn(\n                'custom_selector is deprecated and ignored. Cloudflare Turnstile is now '\n                'detected automatically via shadow root inspection.',\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n        if time_before_click is not None:\n            warnings.warn(\n                'time_before_click is deprecated and ignored. The checkbox is now '\n                'located via shadow root polling and clicked immediately.',\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n        logger.info('Enabling Cloudflare captcha auto-solve')\n        if not self.page_events_enabled:\n            await self.enable_page_events()\n\n        callback = partial(\n            self._bypass_cloudflare,\n            time_to_wait_captcha=time_to_wait_captcha,\n        )\n\n        self._cloudflare_captcha_callback_id = await self.on(PageEvent.LOAD_EVENT_FIRED, callback)\n        logger.debug(\n            f'Cloudflare auto-solve callback registered: id={self._cloudflare_captcha_callback_id}'\n        )\n\n    async def disable_fetch_events(self):\n        \"\"\"Disable CDP Fetch domain and release paused requests.\"\"\"\n        logger.debug('Disabling Fetch events')\n        response = await self._execute_command(FetchCommands.disable())\n        self._fetch_events_enabled = False\n        logger.debug('Fetch events disabled')\n        return response\n\n    async def disable_page_events(self):\n        \"\"\"Disable CDP Page domain events.\"\"\"\n        logger.debug('Disabling Page events')\n        response = await self._execute_command(PageCommands.disable())\n        self._page_events_enabled = False\n        logger.debug('Page events disabled')\n        return response\n\n    async def disable_network_events(self):\n        \"\"\"Disable CDP Network domain events.\"\"\"\n        logger.debug('Disabling Network events')\n        response = await self._execute_command(NetworkCommands.disable())\n        self._network_events_enabled = False\n        logger.debug('Network events disabled')\n        return response\n\n    async def disable_dom_events(self):\n        \"\"\"Disable CDP DOM domain events.\"\"\"\n        logger.debug('Disabling DOM events')\n        response = await self._execute_command(DomCommands.disable())\n        self._dom_events_enabled = False\n        logger.debug('DOM events disabled')\n        return response\n\n    async def disable_runtime_events(self):\n        \"\"\"Disable CDP Runtime domain events.\"\"\"\n        logger.debug('Disabling Runtime events')\n        response = await self._execute_command(RuntimeCommands.disable())\n        self._runtime_events_enabled = False\n        logger.debug('Runtime events disabled')\n        return response\n\n    async def disable_intercept_file_chooser_dialog(self):\n        \"\"\"Disable file chooser dialog interception.\"\"\"\n        logger.info('Disabling file chooser interception')\n        response = await self._execute_command(\n            PageCommands.set_intercept_file_chooser_dialog(False)\n        )\n        self._intercept_file_chooser_dialog_enabled = False\n        logger.debug('File chooser interception disabled')\n        return response\n\n    async def disable_auto_solve_cloudflare_captcha(self):\n        \"\"\"Disable automatic Cloudflare Turnstile captcha bypass.\"\"\"\n        logger.info('Disabling Cloudflare captcha auto-solve')\n        await self._connection_handler.remove_callback(self._cloudflare_captcha_callback_id)\n        self._cloudflare_captcha_callback_id = None\n\n    async def close(self):\n        \"\"\"\n        Close this browser tab.\n\n        Note:\n            Tab instance becomes invalid after calling this method.\n        \"\"\"\n        logger.info(f'Closing tab: target_id={self._target_id}')\n        result = await self._execute_command(PageCommands.close())\n        self._browser._tabs_opened.pop(self._target_id)\n        logger.debug('Tab closed and removed from browser registry')\n        return result\n\n    async def get_frame(self, frame: 'WebElement') -> IFrame:\n        \"\"\"\n        .. deprecated:: ?.?.?\n            Use iframe `WebElement` instances directly; this method will be removed in\n            a future version.\n\n        Get Tab object for interacting with iframe content.\n\n        Args:\n            frame: Tab representing the iframe tag.\n\n        Returns:\n            Tab instance configured for iframe interaction.\n\n        Raises:\n            NotAnIFrame: If element is not an iframe.\n            InvalidIFrame: If iframe lacks valid src attribute.\n            IFrameNotFound: If iframe target not found in browser.\n        \"\"\"\n        warnings.warn(\n            'Tab.get_frame() is deprecated and will be removed in a future version. '\n            'Interact with iframe WebElements directly.',\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        logger.debug(f'Resolving iframe: tag={frame.tag_name}')\n        if not frame.tag_name == 'iframe':\n            raise NotAnIFrame\n\n        frame_url = frame.get_attribute('src')\n        logger.debug(f'Iframe src resolved: {frame_url}')\n        if not frame_url:\n            raise InvalidIFrame('The iframe does not have a valid src attribute')\n\n        targets = await self._browser.get_targets()\n        iframe_target = next((target for target in targets if target['url'] == frame_url), None)\n        if not iframe_target:\n            raise IFrameNotFound('The target for the iframe was not found')\n\n        target_id = iframe_target['targetId']\n        if target_id in self._browser._tabs_opened:\n            logger.debug(f'Iframe tab already tracked: {target_id}')\n            return self._browser._tabs_opened[target_id]\n\n        tab = Tab(\n            self._browser,\n            target_id=target_id,\n            connection_port=self._connection_port,\n        )\n        self._browser._tabs_opened[target_id] = tab\n        logger.debug(f'Iframe tab created and registered: {target_id}')\n        return tab\n\n    async def find_shadow_roots(self, deep: bool = False, timeout: float = 0) -> list[ShadowRoot]:\n        \"\"\"\n        Find all shadow roots in the page.\n\n        Traverses the entire DOM tree (including iframes and nested shadow DOMs)\n        to collect all shadow roots found. This is especially useful when the\n        shadow host element selector is unknown or dynamic (e.g., Cloudflare\n        challenge pages).\n\n        Args:\n            deep: If True, also traverses cross-origin iframes (OOPIFs) to\n                discover shadow roots inside them. The returned ShadowRoot\n                objects will automatically route CDP commands through the\n                correct OOPIF session.\n            timeout: Maximum seconds to wait for shadow roots to appear.\n                When > 0, repeatedly polls the DOM (every 0.5s) until at least\n                one shadow root is found or the timeout expires. Useful when\n                shadow hosts are injected asynchronously (e.g., Cloudflare\n                Turnstile loading inside an OOPIF).\n\n        Returns:\n            List of ShadowRoot instances found in the page.\n\n        Raises:\n            WaitElementTimeout: If timeout > 0 and no shadow roots are found\n                within the specified duration.\n        \"\"\"\n        logger.debug('Finding all shadow roots in page (timeout=%s)', timeout)\n\n        if not timeout:\n            return await self._collect_all_shadow_roots(deep)\n\n        start_time = asyncio.get_event_loop().time()\n        while True:\n            shadow_roots = await self._collect_all_shadow_roots(deep)\n            if shadow_roots:\n                return shadow_roots\n\n            if asyncio.get_event_loop().time() - start_time > timeout:\n                raise WaitElementTimeout(\n                    f'Timed out after {timeout}s waiting for shadow roots in page'\n                )\n\n            await asyncio.sleep(0.5)\n\n    async def _collect_all_shadow_roots(self, deep: bool) -> list[ShadowRoot]:\n        \"\"\"Collect shadow roots from the main document and optionally OOPIFs.\"\"\"\n        response: GetDocumentResponse = await self._execute_command(\n            DomCommands.get_document(depth=-1, pierce=True)\n        )\n        root_node = response.get('result', {}).get('root', {})\n\n        shadow_root_entries: list[tuple[Node, int | None]] = []\n        self._collect_shadow_roots_from_tree(root_node, shadow_root_entries)\n\n        shadow_roots: list[ShadowRoot] = []\n        for shadow_data, host_backend_id in shadow_root_entries:\n            backend_node_id = shadow_data.get('backendNodeId')\n            if not backend_node_id:\n                continue\n\n            try:\n                resolve_response: ResolveNodeResponse = await self._execute_command(\n                    DomCommands.resolve_node(backend_node_id=backend_node_id)\n                )\n                shadow_object_id = resolve_response['result']['object']['objectId']\n            except (CommandExecutionTimeout, WebSocketConnectionClosed, KeyError):\n                logger.debug(f'Failed to resolve shadow root: backend_node_id={backend_node_id}')\n                continue\n\n            try:\n                host_element = await self._resolve_shadow_host(host_backend_id)\n            except (CommandExecutionTimeout, WebSocketConnectionClosed, KeyError):\n                logger.debug(f'Failed to resolve shadow host: backend_node_id={host_backend_id}')\n                host_element = None\n            mode = ShadowRootType(shadow_data.get('shadowRootType', 'open'))\n            shadow_roots.append(\n                ShadowRoot(\n                    object_id=shadow_object_id,\n                    connection_handler=self._connection_handler,\n                    mode=mode,\n                    host_element=host_element,\n                )\n            )\n\n        if deep:\n            oopif_roots = await self._collect_oopif_shadow_roots()\n            shadow_roots.extend(oopif_roots)\n\n        logger.debug(f'Found {len(shadow_roots)} shadow roots')\n        return shadow_roots\n\n    async def _resolve_shadow_host(self, host_backend_id: int | None) -> WebElement | None:\n        \"\"\"Resolve the host element for a shadow root (best-effort).\"\"\"\n        if not host_backend_id:\n            return None\n\n        host_response: ResolveNodeResponse = await self._execute_command(\n            DomCommands.resolve_node(backend_node_id=host_backend_id)\n        )\n        host_object_id = host_response['result']['object']['objectId']\n        host_attrs = await self._get_object_attributes(object_id=host_object_id)\n        return WebElement(\n            host_object_id, self._connection_handler, attributes_list=host_attrs, mouse=self._mouse\n        )\n\n    async def _collect_oopif_shadow_roots(self) -> list[ShadowRoot]:\n        \"\"\"Discover shadow roots inside cross-origin iframes (OOPIFs).\"\"\"\n        browser_handler = ConnectionHandler(connection_port=self._connection_port)\n        targets_response: GetTargetsResponse = await browser_handler.execute_command(\n            TargetCommands.get_targets()\n        )\n\n        target_infos = targets_response.get('result', {}).get('targetInfos', [])\n        iframe_targets = [t for t in target_infos if t.get('type') == 'iframe']\n\n        if not iframe_targets:\n            logger.debug('No OOPIF targets found')\n            return []\n\n        shadow_roots: list[ShadowRoot] = []\n        for target in iframe_targets:\n            roots = await self._collect_shadow_roots_from_oopif_target(target, browser_handler)\n            shadow_roots.extend(roots)\n\n        logger.debug(f'Found {len(shadow_roots)} shadow roots in OOPIFs')\n        return shadow_roots\n\n    async def _collect_shadow_roots_from_oopif_target(\n        self,\n        target: TargetInfo,\n        browser_handler: ConnectionHandler,\n    ) -> list[ShadowRoot]:\n        \"\"\"Collect shadow roots from a single OOPIF target.\"\"\"\n        target_id = target.get('targetId', '')\n        try:\n            attach_response: AttachToTargetResponse = await browser_handler.execute_command(\n                TargetCommands.attach_to_target(target_id=target_id, flatten=True)\n            )\n            session_id = attach_response.get('result', {}).get('sessionId')\n            if not session_id:\n                return []\n        except (CommandExecutionTimeout, WebSocketConnectionClosed):\n            logger.debug(f'Failed to attach to OOPIF target: {target_id}')\n            return []\n\n        try:\n            get_doc_command = DomCommands.get_document(depth=-1, pierce=True)\n            get_doc_command['sessionId'] = session_id\n            doc_response: GetDocumentResponse = await browser_handler.execute_command(\n                get_doc_command\n            )\n            root_node = doc_response.get('result', {}).get('root', {})\n        except (CommandExecutionTimeout, WebSocketConnectionClosed):\n            logger.debug(f'Failed to get document from OOPIF target: {target_id}')\n            return []\n\n        entries: list[tuple[Node, int | None]] = []\n        self._collect_shadow_roots_from_tree(root_node, entries)\n\n        iframe_context = IFrameContext(\n            frame_id=target_id,\n            session_handler=browser_handler,\n            session_id=session_id,\n        )\n\n        results: list[ShadowRoot] = []\n        for shadow_data, host_backend_id in entries:\n            sr = await self._resolve_oopif_shadow_entry(\n                shadow_data, host_backend_id, browser_handler, session_id, iframe_context\n            )\n            if sr:\n                results.append(sr)\n        return results\n\n    async def _resolve_oopif_shadow_entry(\n        self,\n        shadow_data: Node,\n        host_backend_id: int | None,\n        browser_handler: ConnectionHandler,\n        session_id: str,\n        iframe_context: IFrameContext,\n    ) -> ShadowRoot | None:\n        \"\"\"Resolve a single shadow root entry from an OOPIF.\"\"\"\n        backend_node_id = shadow_data.get('backendNodeId')\n        if not backend_node_id:\n            return None\n\n        try:\n            resolve_command = DomCommands.resolve_node(backend_node_id=backend_node_id)\n            resolve_command['sessionId'] = session_id\n            resolve_response: ResolveNodeResponse = await browser_handler.execute_command(\n                resolve_command\n            )\n            shadow_object_id = resolve_response['result']['object']['objectId']\n        except (CommandExecutionTimeout, WebSocketConnectionClosed, KeyError):\n            logger.debug(f'Failed to resolve OOPIF shadow root: backend_node_id={backend_node_id}')\n            return None\n\n        host_element = await self._resolve_oopif_shadow_host(\n            host_backend_id, browser_handler, session_id\n        )\n\n        if host_element:\n            host_element._iframe_context = iframe_context\n\n        mode = ShadowRootType(shadow_data.get('shadowRootType', 'open'))\n        sr = ShadowRoot(\n            object_id=shadow_object_id,\n            connection_handler=self._connection_handler,\n            mode=mode,\n            host_element=host_element,\n        )\n\n        if not host_element:\n            sr._iframe_context = iframe_context\n\n        return sr\n\n    async def _resolve_oopif_shadow_host(\n        self,\n        host_backend_id: int | None,\n        browser_handler: ConnectionHandler,\n        session_id: str,\n    ) -> WebElement | None:\n        \"\"\"Resolve the host element for a shadow root inside an OOPIF (best-effort).\"\"\"\n        if not host_backend_id:\n            return None\n\n        try:\n            resolve_command = DomCommands.resolve_node(backend_node_id=host_backend_id)\n            resolve_command['sessionId'] = session_id\n            host_response: ResolveNodeResponse = await browser_handler.execute_command(\n                resolve_command\n            )\n            host_object_id = host_response['result']['object']['objectId']\n\n            describe_command = DomCommands.describe_node(object_id=host_object_id)\n            describe_command['sessionId'] = session_id\n            describe_response: DescribeNodeResponse = await browser_handler.execute_command(\n                describe_command\n            )\n            node_info = describe_response.get('result', {}).get('node', {})\n            attributes = node_info.get('attributes', [])\n            tag_name = node_info.get('nodeName', '').lower()\n            attributes.extend(['tag_name', tag_name])\n\n            return WebElement(\n                host_object_id,\n                self._connection_handler,\n                attributes_list=attributes,\n                mouse=self._mouse,\n            )\n        except (CommandExecutionTimeout, WebSocketConnectionClosed, KeyError):\n            logger.debug(f'Failed to resolve OOPIF shadow host: backend_node_id={host_backend_id}')\n            return None\n\n    @staticmethod\n    def _collect_shadow_roots_from_tree(node: Node, results: list[tuple[Node, int | None]]) -> None:\n        \"\"\"Recursively walk a DOM tree collecting shadow root entries.\"\"\"\n        host_backend_id = node.get('backendNodeId')\n        for shadow_root in node.get('shadowRoots', []):\n            results.append((shadow_root, host_backend_id))\n            Tab._collect_shadow_roots_from_tree(shadow_root, results)\n\n        for child in node.get('children', []):\n            Tab._collect_shadow_roots_from_tree(child, results)\n\n        content_doc = node.get('contentDocument')\n        if content_doc:\n            Tab._collect_shadow_roots_from_tree(content_doc, results)\n\n    async def bring_to_front(self):\n        \"\"\"Brings the page to front.\"\"\"\n        logger.info('Bringing page to front')\n        return await self._execute_command(PageCommands.bring_to_front())\n\n    async def get_cookies(self) -> list[Cookie]:\n        \"\"\"Get all cookies accessible from current page.\"\"\"\n        logger.debug('Fetching cookies for current page')\n        if self._browser_context_id:\n            response_storage: StorageGetCookiesResponse = await self._execute_command(\n                StorageCommands.get_cookies(self._browser_context_id)\n            )\n            cookies = response_storage['result']['cookies']\n            logger.debug(f'Fetched {len(cookies)} cookies')\n            return cookies\n\n        response_network: NetworkGetCookiesResponse = await self._execute_command(\n            NetworkCommands.get_cookies()\n        )\n        cookies = response_network['result']['cookies']\n        logger.debug(f'Fetched {len(cookies)} cookies')\n        return cookies\n\n    async def get_network_response_body(self, request_id: str) -> str:\n        \"\"\"\n        Get the response body for a given request ID.\n\n        Args:\n            request_id: Request ID to get the response body for.\n\n        Returns:\n            The response body for the given request ID.\n\n        Raises:\n            NetworkEventsNotEnabled: If network events are not enabled.\n        \"\"\"\n        if not self.network_events_enabled:\n            raise NetworkEventsNotEnabled('Network events must be enabled to get response body')\n\n        response: GetResponseBodyResponse = await self._execute_command(\n            NetworkCommands.get_response_body(request_id)\n        )\n        logger.debug(f'Retrieved network response body for request_id={request_id}')\n        return response['result']['body']\n\n    async def get_network_logs(self, filter: Optional[str] = None) -> list[RequestWillBeSentEvent]:\n        \"\"\"\n        Get network logs.\n\n        Args:\n            filter: Filter to apply to the network logs.\n\n        Returns:\n            The network logs.\n\n        Raises:\n            NetworkEventsNotEnabled: If network events are not enabled.\n        \"\"\"\n        if not self.network_events_enabled:\n            raise NetworkEventsNotEnabled('Network events must be enabled to get network logs')\n\n        logs = self._connection_handler.network_logs\n        if filter:\n            logs = [\n                log for log in logs if filter in log['params'].get('request', {}).get('url', '')\n            ]\n        logger.debug(f'Returning {len(logs)} network logs (filtered={bool(filter)})')\n        return logs\n\n    async def set_cookies(self, cookies: list[CookieParam]):\n        \"\"\"\n        Set multiple cookies for current page.\n\n        Args:\n            cookies: Cookie parameters (name/value required, others optional).\n\n        Note:\n            Defaults to current page's domain if not specified.\n        \"\"\"\n        logger.info(f'Setting {len(cookies)} cookies on current page')\n        return await self._execute_command(\n            StorageCommands.set_cookies(cookies, self._browser_context_id)\n        )\n\n    async def delete_all_cookies(self):\n        \"\"\"Delete all cookies from current browser context.\"\"\"\n        logger.info('Clearing all cookies from current browser context')\n        return await self._execute_command(StorageCommands.clear_cookies(self._browser_context_id))\n\n    async def go_to(self, url: str, timeout: int = 300):\n        \"\"\"\n        Navigate to URL and wait for loading to complete.\n\n        Refreshes if URL matches current page.\n\n        Args:\n            url: Target URL to navigate to.\n            timeout: Maximum seconds to wait for page load (default 300).\n\n        Raises:\n            PageLoadTimeout: If page doesn't finish loading within timeout.\n        \"\"\"\n        logger.info(f'Navigating to URL: {url} (timeout={timeout}s)')\n        if await self._refresh_if_url_not_changed(url):\n            logger.debug('URL matches current page; refreshing instead')\n            return\n\n        async with self._wait_page_load(timeout=timeout):\n            await self._execute_command(PageCommands.navigate(url))\n        logger.info(f'Navigation complete: {url}')\n\n    async def refresh(\n        self,\n        ignore_cache: bool = False,\n        script_to_evaluate_on_load: Optional[str] = None,\n    ):\n        \"\"\"\n        Reload current page and wait for completion.\n\n        Args:\n            ignore_cache: Bypass browser cache if True.\n            script_to_evaluate_on_load: JavaScript to execute after load.\n\n        Raises:\n            PageLoadTimeout: If page doesn't finish loading within timeout.\n        \"\"\"\n        logger.info(\n            f'Reloading page (ignore_cache={ignore_cache}, '\n            f'script_on_load={bool(script_to_evaluate_on_load)})'\n        )\n        async with self._wait_page_load():\n            await self._execute_command(\n                PageCommands.reload(\n                    ignore_cache=ignore_cache,\n                    script_to_evaluate_on_load=script_to_evaluate_on_load,\n                )\n            )\n        logger.info('Page reloaded successfully')\n\n    async def take_screenshot(\n        self,\n        path: Optional[str | Path] = None,\n        quality: int = 100,\n        beyond_viewport: bool = False,\n        as_base64: bool = False,\n    ) -> Optional[str]:\n        \"\"\"\n        Capture screenshot of current page.\n\n        Args:\n            path: File path for screenshot (extension determines format).\n            quality: Image quality 0-100 (default 100).\n            beyond_viewport: The page will be scrolled to the bottom and the screenshot will\n                include the entire page\n            as_base64: Return as base64 string instead of saving file.\n\n        Returns:\n            Base64 screenshot data if as_base64=True, None otherwise.\n\n        Raises:\n            InvalidFileExtension: If file extension not supported.\n            MissingScreenshotPath: If path is None and as_base64 is False.\n        \"\"\"\n        if not path and not as_base64:\n            raise MissingScreenshotPath()\n\n        if path and isinstance(path, str):\n            output_extension = path.split('.')[-1]\n        elif path and isinstance(path, Path):\n            output_extension = path.suffix.lstrip('.')\n        else:\n            output_extension = ScreenshotFormat.JPEG\n\n        # Normalize jpg to jpeg (CDP only accepts jpeg)\n        output_extension = (\n            output_extension.replace('jpg', 'jpeg')\n            if output_extension == 'jpg'\n            else output_extension\n        )\n\n        if not ScreenshotFormat.has_value(output_extension):\n            raise InvalidFileExtension(f'{output_extension} extension is not supported.')\n\n        output_format = ScreenshotFormat.get_value(output_extension)\n\n        logger.info(\n            f'Taking screenshot: path={path}, quality={quality}, '\n            f'beyond_viewport={beyond_viewport}, as_base64={as_base64}'\n        )\n        response: CaptureScreenshotResponse = await self._execute_command(\n            PageCommands.capture_screenshot(\n                format=output_format,\n                quality=quality,\n                capture_beyond_viewport=beyond_viewport,\n            )\n        )\n\n        try:\n            screenshot_data = response['result']['data']\n        except KeyError:\n            raise TopLevelTargetRequired(\n                'Command can only be executed on top-level targets. Please use '\n                'take_screenshot method on the WebElement object instead.'\n            )\n\n        if as_base64:\n            logger.info('Screenshot captured and returned as base64')\n            return screenshot_data\n\n        if path:\n            screenshot_bytes = decode_base64_to_bytes(screenshot_data)\n            async with aiofiles.open(str(path), 'wb') as file:\n                await file.write(screenshot_bytes)\n            logger.info(f'Screenshot saved to: {path}')\n\n        return None\n\n    async def print_to_pdf(\n        self,\n        path: Optional[str | Path] = None,\n        landscape: bool = False,\n        display_header_footer: bool = False,\n        print_background: bool = True,\n        scale: float = 1.0,\n        as_base64: bool = False,\n    ) -> Optional[str]:\n        \"\"\"\n        Generate PDF of current page.\n\n        Args:\n            path: File path for PDF output. Required if as_base64=False.\n            landscape: Use landscape orientation.\n            display_header_footer: Include header/footer.\n            print_background: Include background graphics.\n            scale: Scale factor (0.1-2.0).\n            as_base64: Return as base64 string instead of saving.\n\n        Returns:\n            Base64 PDF data if as_base64=True, None otherwise.\n\n        Raises:\n            ValueError: If path is not provided when as_base64=False.\n        \"\"\"\n        logger.info(\n            f'Generating PDF: path={path}, landscape={landscape}, '\n            f'header_footer={display_header_footer}, print_bg={print_background}, '\n            f'scale={scale}, as_base64={as_base64}'\n        )\n        response: PrintToPDFResponse = await self._execute_command(\n            PageCommands.print_to_pdf(\n                landscape=landscape,\n                display_header_footer=display_header_footer,\n                print_background=print_background,\n                scale=scale,\n            )\n        )\n        pdf_data = response['result']['data']\n        if as_base64:\n            logger.info('PDF generated and returned as base64')\n            return pdf_data\n\n        if path is None:\n            raise ValueError('path is required when as_base64=False')\n\n        pdf_bytes = decode_base64_to_bytes(pdf_data)\n        async with aiofiles.open(path, 'wb') as file:\n            await file.write(pdf_bytes)\n        logger.info(f'PDF saved to: {path}')\n\n        return None\n\n    async def save_bundle(self, path: str | Path, inline_assets: bool = False) -> None:\n        \"\"\"\n        Save current page and its assets as a .zip bundle for offline viewing.\n\n        Captures the page HTML along with CSS, JS, images, fonts, and media\n        into a single zip archive. The archive contains an ``index.html`` with\n        URLs rewritten to reference local asset files.\n\n        Args:\n            path: Destination path for the ``.zip`` file.\n            inline_assets: When True, embed all assets directly into\n                ``index.html`` using data URIs, ``<style>``, and ``<script>``\n                tags instead of saving them as separate files.\n\n        Raises:\n            InvalidFileExtension: If path does not end with ``.zip``.\n        \"\"\"\n        path = Path(path)\n        if path.suffix.lower() != '.zip':\n            raise InvalidFileExtension(f'Expected .zip extension, got {path.suffix!r}')\n\n        logger.info(f'Saving page bundle: path={path}, inline={inline_assets}')\n\n        page_was_enabled = self.page_events_enabled\n        if not page_was_enabled:\n            await self.enable_page_events()\n\n        try:\n            tree_response: GetResourceTreeResponse = await self._execute_command(\n                PageCommands.get_resource_tree()\n            )\n            frame_tree: FrameResourceTree = tree_response['result']['frameTree']\n            page_url = frame_tree['frame']['url']\n            html = await self._fetch_document_html(frame_tree)\n            asset_map = await self._fetch_bundle_assets(frame_tree, page_url)\n\n            buf = io.BytesIO()\n            with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:\n                if inline_assets:\n                    html = inline_all_assets(html, asset_map)\n                else:\n                    html = rewrite_html_urls(html, asset_map)\n                zf.writestr('index.html', html.encode('utf-8'))\n                if not inline_assets:\n                    for _url, (filename, data, _mime, _rtype) in asset_map.items():\n                        zf.writestr(f'assets/{filename}', data)\n\n            async with aiofiles.open(path, 'wb') as f:\n                await f.write(buf.getvalue())\n            logger.info(f'Page bundle saved to: {path}')\n        finally:\n            if not page_was_enabled:\n                await self.disable_page_events()\n\n    async def _fetch_document_html(self, frame_tree: FrameResourceTree) -> str:\n        \"\"\"Fetch the main document HTML from the frame tree.\"\"\"\n        frame_id = frame_tree['frame']['id']\n        page_url = frame_tree['frame']['url']\n        try:\n            doc_response: GetResourceContentResponse = await self._execute_command(\n                PageCommands.get_resource_content(frame_id, page_url)\n            )\n            result = doc_response['result']\n            html = result['content']\n            if result.get('base64Encoded'):\n                html = _b64.b64decode(html).decode('utf-8', errors='replace')\n            return html\n        except Exception:\n            logger.debug('getResourceContent failed for document, falling back to JS')\n            response = await self.execute_script('return document.documentElement.outerHTML')\n            return cast(str, response['result']['result']['value'])\n\n    async def _fetch_bundle_assets(\n        self,\n        frame_tree: FrameResourceTree,\n        page_url: str,\n    ) -> dict[str, tuple[str, bytes, str, ResourceType]]:\n        \"\"\"Fetch all bundleable resources and return an asset map.\"\"\"\n        all_resources = collect_frame_resources(frame_tree)\n        fetchable = filter_fetchable_resources(all_resources, page_url)\n\n        fetch_tasks: list[Awaitable[GetResourceContentResponse]] = [\n            self._execute_command(PageCommands.get_resource_content(fid, res['url']))\n            for fid, res in fetchable\n        ]\n        results = await asyncio.gather(*fetch_tasks, return_exceptions=True)\n\n        asset_map: dict[str, tuple[str, bytes, str, ResourceType]] = {}\n        for idx, ((_fid, res), result) in enumerate(zip(fetchable, results)):\n            if isinstance(result, BaseException):\n                logger.warning(f'Failed to fetch resource {res[\"url\"]}: {result}')\n                continue\n            response: GetResourceContentResponse = result\n            content_result = response.get('result')\n            if content_result is None:\n                logger.warning(f'No result for resource {res[\"url\"]}: {response.get(\"error\")}')\n                continue\n            raw_content: str = content_result['content']\n            is_base64: bool = content_result.get('base64Encoded', False)\n            data = _b64.b64decode(raw_content) if is_base64 else raw_content.encode('utf-8')\n            filename = build_asset_filename(res['url'], res['mimeType'], idx)\n            asset_map[res['url']] = (filename, data, res['mimeType'], res['type'])\n        return asset_map\n\n    async def has_dialog(self) -> bool:\n        \"\"\"\n        Check if JavaScript dialog is currently displayed.\n\n        Note:\n            Page events must be enabled to detect dialogs.\n        \"\"\"\n        if self._connection_handler.dialog:\n            logger.debug('Dialog present')\n            return True\n\n        return False\n\n    async def get_dialog_message(self) -> str:\n        \"\"\"\n        Get message text from current JavaScript dialog.\n\n        Raises:\n            NoDialogPresent: If no dialog is currently displayed.\n        \"\"\"\n        if not await self.has_dialog():\n            raise NoDialogPresent()\n        message = self._connection_handler.dialog['params']['message']\n        logger.debug(f'Dialog message retrieved: {message}')\n        return message\n\n    async def handle_dialog(self, accept: bool, prompt_text: Optional[str] = None):\n        \"\"\"\n        Respond to JavaScript dialog.\n\n        Args:\n            accept: Accept/confirm dialog if True, dismiss/cancel if False.\n            prompt_text: Text for prompt dialogs (ignored for alert/confirm).\n\n        Raises:\n            NoDialogPresent: If no dialog is currently displayed.\n\n        Note:\n            Page events must be enabled to handle dialogs.\n        \"\"\"\n        if not await self.has_dialog():\n            raise NoDialogPresent()\n        logger.info(f'Handling dialog: accept={accept}, has_prompt_text={bool(prompt_text)}')\n        return await self._execute_command(\n            PageCommands.handle_javascript_dialog(accept=accept, prompt_text=prompt_text)\n        )\n\n    @overload\n    async def execute_script(\n        self,\n        script: str,\n        *,\n        object_group: Optional[str] = None,\n        include_command_line_api: Optional[bool] = None,\n        silent: Optional[bool] = None,\n        context_id: Optional[int] = None,\n        return_by_value: Optional[bool] = None,\n        generate_preview: Optional[bool] = None,\n        user_gesture: Optional[bool] = None,\n        await_promise: Optional[bool] = None,\n        throw_on_side_effect: Optional[bool] = None,\n        timeout: Optional[float] = None,\n        disable_breaks: Optional[bool] = None,\n        repl_mode: Optional[bool] = None,\n        allow_unsafe_eval_blocked_by_csp: Optional[bool] = None,\n        unique_context_id: Optional[str] = None,\n        serialization_options: Optional[SerializationOptions] = None,\n    ) -> EvaluateResponse: ...\n\n    @overload\n    async def execute_script(\n        self,\n        script: str,\n        element: WebElement,\n        *,\n        arguments: Optional[list[CallArgument]] = None,\n        silent: Optional[bool] = None,\n        return_by_value: Optional[bool] = None,\n        generate_preview: Optional[bool] = None,\n        user_gesture: Optional[bool] = None,\n        await_promise: Optional[bool] = None,\n        execution_context_id: Optional[int] = None,\n        object_group: Optional[str] = None,\n        throw_on_side_effect: Optional[bool] = None,\n        unique_context_id: Optional[str] = None,\n        serialization_options: Optional[SerializationOptions] = None,\n    ) -> CallFunctionOnResponse: ...\n\n    async def execute_script(\n        self,\n        script: str,\n        element: Optional[WebElement] = None,\n        *,\n        arguments: Optional[list[CallArgument]] = None,\n        object_group: Optional[str] = None,\n        include_command_line_api: Optional[bool] = None,\n        silent: Optional[bool] = None,\n        context_id: Optional[int] = None,\n        return_by_value: Optional[bool] = None,\n        generate_preview: Optional[bool] = None,\n        user_gesture: Optional[bool] = None,\n        await_promise: Optional[bool] = None,\n        execution_context_id: Optional[int] = None,\n        throw_on_side_effect: Optional[bool] = None,\n        timeout: Optional[float] = None,\n        disable_breaks: Optional[bool] = None,\n        repl_mode: Optional[bool] = None,\n        allow_unsafe_eval_blocked_by_csp: Optional[bool] = None,\n        unique_context_id: Optional[str] = None,\n        serialization_options: Optional[SerializationOptions] = None,\n    ) -> Union[EvaluateResponse, CallFunctionOnResponse]:\n        \"\"\"\n        Execute JavaScript in page context.\n\n        Args:\n            script (str): JavaScript code to execute.\n            element (Optional[WebElement]): Optional WebElement to execute script on.\n            arguments (Optional[list[CallArgument]]): Arguments to pass to the function.\n            object_group (Optional[str]): Symbolic group name for the result (Runtime.evaluate).\n            include_command_line_api (Optional[bool]): Whether to include command line API\n                (Runtime.evaluate).\n            silent (Optional[bool]): Whether to silence exceptions (Runtime.evaluate).\n            context_id (Optional[int]): ID of the execution context to evaluate in\n                (Runtime.evaluate).\n            return_by_value (Optional[bool]): Whether to return the result by value instead of\n                reference (Runtime.evaluate).\n            generate_preview (Optional[bool]): Whether to generate a preview for the result\n                (Runtime.evaluate).\n            user_gesture (Optional[bool]): Whether to treat evaluation as initiated by user\n                gesture (Runtime.evaluate).\n            await_promise (Optional[bool]): Whether to await promise result (Runtime.evaluate).\n            execution_context_id (Optional[int]): ID of the execution context to call the\n                function in.\n            throw_on_side_effect (Optional[bool]): Whether to throw if side effect cannot be\n                ruled out (Runtime.evaluate).\n            timeout (Optional[float]): Timeout in milliseconds (Runtime.evaluate).\n            disable_breaks (Optional[bool]): Whether to disable breakpoints during evaluation\n                (Runtime.evaluate).\n            repl_mode (Optional[bool]): Whether to execute in REPL mode (Runtime.evaluate).\n            allow_unsafe_eval_blocked_by_csp (Optional[bool]): Allow unsafe evaluation\n                (Runtime.evaluate).\n            unique_context_id (Optional[str]): Unique context ID for evaluation\n                (Runtime.evaluate).\n            serialization_options (Optional[SerializationOptions]): Serialization options for\n                the result (Runtime.evaluate).\n\n        Returns:\n            Union[EvaluateResponse, CallFunctionOnResponse]: The result of the script execution.\n\n        Raises:\n            InvalidScriptWithElement: If script uses 'argument' keyword but no element is provided.\n\n        Examples:\n            # Execute a simple script to log a message\n            await page.execute_script('console.log(\"Hello World\")')\n\n            # Execute a script that returns the page title\n            await page.execute_script('return document.title')\n\n            # Execute a script on an element to click it\n            await page.execute_script('argument.click()', element)\n\n            # Execute a script on an element to set its value\n            await page.execute_script('argument.value = \"Hello\"', element)\n        \"\"\"\n        logger.debug(f'Executing script: with_element={bool(element)}, length={len(script)}')\n        if element is not None:\n            warnings.warn(\n                'Passing a WebElement to Tab.execute_script() is deprecated. '\n                'Use WebElement.execute_script() instead.',\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n            return await element.execute_script(\n                script,\n                arguments=arguments,\n                silent=silent,\n                return_by_value=return_by_value,\n                generate_preview=generate_preview,\n                user_gesture=user_gesture,\n                await_promise=await_promise,\n                execution_context_id=execution_context_id,\n                object_group=object_group,\n                throw_on_side_effect=throw_on_side_effect,\n                unique_context_id=unique_context_id,\n                serialization_options=serialization_options,\n            )\n\n        if has_return_outside_function(script):\n            script = f'(function(){{ {script} }})()'\n\n        command = self._get_evaluate_command(\n            script,\n            object_group=object_group,\n            include_command_line_api=include_command_line_api,\n            silent=silent,\n            context_id=context_id,\n            return_by_value=return_by_value,\n            generate_preview=generate_preview,\n            user_gesture=user_gesture,\n            await_promise=await_promise,\n            throw_on_side_effect=throw_on_side_effect,\n            timeout=timeout,\n            disable_breaks=disable_breaks,\n            repl_mode=repl_mode,\n            allow_unsafe_eval_blocked_by_csp=allow_unsafe_eval_blocked_by_csp,\n            unique_context_id=unique_context_id,\n            serialization_options=serialization_options,\n        )\n        logger.debug(f'Executing script without element: length={len(script)}')\n        result: Union[EvaluateResponse, CallFunctionOnResponse] = await self._execute_command(\n            command\n        )\n        self._validate_argument_error(result)\n        return result\n\n    # TODO: think about how to remove these duplications with the base class\n    async def continue_request(\n        self,\n        request_id: str,\n        url: Optional[str] = None,\n        method: Optional[RequestMethod] = None,\n        post_data: Optional[str] = None,\n        headers: Optional[list[HeaderEntry]] = None,\n        intercept_response: Optional[bool] = None,\n    ):\n        \"\"\"\n        Continue paused request without modifications.\n        \"\"\"\n        logger.debug(f'Continue request on tab: id={request_id}')\n        return await self._execute_command(\n            FetchCommands.continue_request(\n                request_id=request_id,\n                url=url,\n                method=method,\n                post_data=post_data,\n                headers=headers,\n                intercept_response=intercept_response,\n            )\n        )\n\n    async def fail_request(self, request_id: str, error_reason: ErrorReason):\n        \"\"\"Fail request with error code.\"\"\"\n        logger.debug(f'Fail request on tab: id={request_id}, reason={error_reason}')\n        return await self._execute_command(FetchCommands.fail_request(request_id, error_reason))\n\n    async def fulfill_request(\n        self,\n        request_id: str,\n        response_code: int,\n        response_headers: Optional[list[HeaderEntry]] = None,\n        body: Optional[str] = None,\n        response_phrase: Optional[str] = None,\n    ):\n        \"\"\"Fulfill request with response data.\"\"\"\n        logger.debug(\n            f'Fulfill request on tab: id={request_id}, code={response_code}, '\n            f'headers_set={bool(response_headers)}, body_set={bool(body)}'\n        )\n        return await self._execute_command(\n            FetchCommands.fulfill_request(\n                request_id=request_id,\n                response_code=response_code,\n                response_headers=response_headers,\n                body=body,\n                response_phrase=response_phrase,\n            )\n        )\n\n    async def continue_with_auth(\n        self,\n        request_id: str,\n        auth_challenge_response: AuthChallengeResponseType,\n        proxy_username: Optional[str] = None,\n        proxy_password: Optional[str] = None,\n    ):\n        \"\"\"Continue a paused request replying to an authentication challenge.\n\n        Useful for proxy auth (407) or server auth (401) when Fetch is enabled\n        with handle_auth=True.\n        \"\"\"\n        logger.debug(\n            f'Continue with auth on tab: id={request_id}, response={auth_challenge_response}, '\n            f'user_set={bool(proxy_username)}'\n        )\n        return await self._execute_command(\n            FetchCommands.continue_request_with_auth(\n                request_id=request_id,\n                auth_challenge_response=auth_challenge_response,\n                proxy_username=proxy_username,\n                proxy_password=proxy_password,\n            )\n        )\n\n    @asynccontextmanager\n    async def expect_file_chooser(\n        self, files: str | Path | list[str | Path]\n    ) -> AsyncGenerator[None, None]:\n        \"\"\"\n        Context manager for automatic file upload handling.\n\n        Args:\n            files: File path(s) for upload.\n        \"\"\"\n\n        async def event_handler(event: FileChooserOpenedEvent):\n            logger.info('File chooser opened; setting files')\n            file_list = [str(file) for file in files] if isinstance(files, list) else [str(files)]\n            await self._execute_command(\n                DomCommands.set_file_input_files(\n                    files=file_list,\n                    backend_node_id=event['params']['backendNodeId'],\n                )\n            )\n            logger.debug(f'Files set on input: {file_list}')\n\n        if self.page_events_enabled is False:\n            _before_page_events_enabled = False\n            await self.enable_page_events()\n        else:\n            _before_page_events_enabled = True\n\n        if self.intercept_file_chooser_dialog_enabled is False:\n            await self.enable_intercept_file_chooser_dialog()\n\n        logger.info('Waiting for file chooser to open')\n        await self.on(\n            PageEvent.FILE_CHOOSER_OPENED,\n            cast(Callable[[dict], Any], event_handler),\n            temporary=True,\n        )\n\n        yield\n\n        if self.intercept_file_chooser_dialog_enabled is True:\n            await self.disable_intercept_file_chooser_dialog()\n\n        if _before_page_events_enabled is False:\n            await self.disable_page_events()\n\n    @asynccontextmanager\n    async def expect_and_bypass_cloudflare_captcha(\n        self,\n        custom_selector: Optional[tuple[By, str]] = None,\n        time_before_click: Optional[float] = None,\n        time_to_wait_captcha: float = 5,\n    ) -> AsyncGenerator[None, None]:\n        \"\"\"\n        Context manager for automatic Cloudflare captcha bypass.\n\n        Args:\n            custom_selector: Deprecated — ignored. Cloudflare Turnstile is now\n                detected automatically via shadow root inspection.\n            time_before_click: Deprecated — ignored. The checkbox is now\n                located via shadow root polling and clicked immediately.\n            time_to_wait_captcha: Timeout for captcha detection (default 5s).\n        \"\"\"\n        if custom_selector is not None:\n            warnings.warn(\n                'custom_selector is deprecated and ignored. Cloudflare Turnstile is now '\n                'detected automatically via shadow root inspection.',\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n        if time_before_click is not None:\n            warnings.warn(\n                'time_before_click is deprecated and ignored. The checkbox is now '\n                'located via shadow root polling and clicked immediately.',\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n        captcha_processed = asyncio.Event()\n\n        async def bypass_cloudflare(_: dict):\n            try:\n                await self._bypass_cloudflare(\n                    _,\n                    time_to_wait_captcha=time_to_wait_captcha,\n                )\n            finally:\n                captcha_processed.set()\n\n        _before_page_events_enabled = self.page_events_enabled\n\n        if not _before_page_events_enabled:\n            await self.enable_page_events()\n\n        logger.info('Expecting and bypassing Cloudflare captcha if present')\n        callback_id = await self.on(PageEvent.LOAD_EVENT_FIRED, bypass_cloudflare)\n\n        try:\n            yield\n            await captcha_processed.wait()\n        finally:\n            await self._connection_handler.remove_callback(callback_id)\n            if not _before_page_events_enabled:\n                await self.disable_page_events()\n\n    @asynccontextmanager\n    async def expect_download(\n        self,\n        keep_file_at: Optional[Union[str, Path]] = None,\n        timeout: Optional[float] = None,\n    ) -> AsyncGenerator[_DownloadHandle, None]:\n        \"\"\"\n        Context manager for handling a file download triggered inside the block.\n\n        Behavior:\n        - If keep_file_at is provided, configure browser to save into that directory and keep file.\n        - Otherwise, a temporary directory is used and cleaned up after the context.\n\n        Args:\n            keep_file_at: Directory to persist the file. If None, uses a temporary\n                directory and cleans it up afterwards.\n            timeout: Max seconds to wait for download completion. Defaults to 60.\n\n        Yields:\n            _DownloadHandle: Handle to read the downloaded file (bytes/base64) and check its path.\n        \"\"\"\n        download_timeout = 60.0 if timeout is None else float(timeout)\n\n        cleanup_dir = False\n        if keep_file_at is None:\n            download_dir = mkdtemp(prefix='pydoll-download-')\n            cleanup_dir = True\n        else:\n            download_dir = str(Path(keep_file_at))\n            Path(download_dir).mkdir(parents=True, exist_ok=True)\n\n        logger.info(f'Expecting download (dir={download_dir}, timeout={download_timeout}s)')\n        await self._browser.set_download_behavior(\n            behavior=DownloadBehavior.ALLOW,\n            download_path=download_dir,\n            browser_context_id=self._browser_context_id,\n        )\n\n        _page_events_was_enabled = True\n        if not self._page_events_enabled:\n            _page_events_was_enabled = False\n            await self.enable_page_events()\n\n        loop = asyncio.get_event_loop()\n        will_begin: asyncio.Future[bool] = loop.create_future()\n        done: asyncio.Future[bool] = loop.create_future()\n        state: dict[str, Any] = {\n            'guid': None,\n            'url': None,\n            'suggestedFilename': None,\n            'filePath': None,\n            'dir': download_dir,\n        }\n\n        async def on_will_begin(event: DownloadWillBeginEvent):\n            params = event['params']\n            state['guid'] = params['guid']\n            state['url'] = params['url']\n            state['suggestedFilename'] = params['suggestedFilename']\n            if not will_begin.done():\n                will_begin.set_result(True)\n            logger.info(\n                f'Download will begin: url={state[\"url\"]}, filename={state[\"suggestedFilename\"]}'\n            )\n\n        async def on_progress(event: DownloadProgressEvent):\n            params = event['params']\n            guid = params['guid']\n            if (\n                state.get('guid')\n                and guid != state['guid']\n                or params['state'] != DownloadProgressState.COMPLETED\n            ):\n                return\n            file_path = params.get('filePath')\n            if not file_path:\n                file_path = str(Path(download_dir) / state['suggestedFilename'])\n            state['filePath'] = file_path\n            if not done.done():\n                done.set_result(True)\n            logger.info(f'Download completed: {file_path}')\n\n        await self.on(\n            PageEvent.DOWNLOAD_WILL_BEGIN,\n            cast(Callable[[dict], Awaitable[Any]], on_will_begin),\n            True,\n        )\n        cb_id_progress = await self.on(\n            PageEvent.DOWNLOAD_PROGRESS,\n            cast(Callable[[dict], Awaitable[Any]], on_progress),\n            False,\n        )\n\n        handle = _DownloadHandle(\n            state=state,\n            will_begin_future=will_begin,\n            done_future=done,\n            timeout=download_timeout,\n        )\n\n        try:\n            yield handle\n            try:\n                await asyncio.wait_for(done, timeout=download_timeout)\n            except asyncio.TimeoutError as exc:\n                raise DownloadTimeout() from exc\n        finally:\n            await self._cleanup_download_context(\n                cb_id_progress,\n                _page_events_was_enabled,\n                cleanup_dir,\n                state,\n                download_dir,\n            )\n\n    async def _cleanup_download_context(\n        self,\n        cb_id_progress: int,\n        page_events_was_enabled: bool,\n        cleanup_dir: bool,\n        state: dict[str, Any],\n        download_dir: str,\n    ) -> None:\n        await self.remove_callback(cb_id_progress)\n        await self._browser.set_download_behavior(\n            behavior=DownloadBehavior.DEFAULT,\n            browser_context_id=self._browser_context_id,\n        )\n\n        if cleanup_dir:\n            file_path = state['filePath']\n            if not file_path:\n                return\n            Path(file_path).unlink(missing_ok=True)\n            shutil.rmtree(download_dir, ignore_errors=True)\n\n        if not page_events_was_enabled:\n            await self.disable_page_events()\n\n    @overload\n    async def on(\n        self, event_name: str, callback: Callable[[dict], Any], temporary: bool = False\n    ) -> int: ...\n    @overload\n    async def on(\n        self, event_name: str, callback: Callable[[dict], Awaitable[Any]], temporary: bool = False\n    ) -> int: ...\n    async def on(\n        self,\n        event_name,\n        callback,\n        temporary=False,\n    ) -> int:\n        \"\"\"\n        Register CDP event listener.\n\n        Callback runs in background task to prevent blocking.\n\n        Args:\n            event_name: CDP event name (e.g., 'Page.loadEventFired').\n            callback: Function called on event (sync or async).\n            temporary: Remove after first invocation.\n\n        Returns:\n            Callback ID for removal.\n\n        Note:\n            Corresponding domain must be enabled before events fire.\n        \"\"\"\n\n        async def callback_wrapper(event):\n            asyncio.create_task(callback(event))\n\n        if asyncio.iscoroutinefunction(callback):\n            function_to_register = callback_wrapper\n        else:\n            function_to_register = callback\n\n        logger.debug(\n            f'Registering callback on tab: event={event_name}, temporary={temporary}, '\n            f'async={asyncio.iscoroutinefunction(callback)}'\n        )\n        return await self._connection_handler.register_callback(\n            event_name, function_to_register, temporary\n        )\n\n    async def remove_callback(self, callback_id: int):\n        \"\"\"Remove callback from tab.\"\"\"\n        logger.debug(f'Removing callback from tab: id={callback_id}')\n        return await self._connection_handler.remove_callback(callback_id)\n\n    async def clear_callbacks(self):\n        \"\"\"Clear all registered event callbacks.\"\"\"\n        logger.debug('Clearing all callbacks from tab')\n        await self._connection_handler.clear_callbacks()\n\n    def _get_connection_handler(self) -> ConnectionHandler:\n        if self._ws_address:\n            logger.debug('Using WebSocket address for connection handler')\n            return ConnectionHandler(ws_address=self._ws_address)\n        logger.debug(\n            'Using port/target for connection handler: '\n            f'port={self._connection_port}, target_id={self._target_id}'\n        )\n        return ConnectionHandler(self._connection_port, self._target_id)\n\n    @staticmethod\n    def _get_evaluate_command(\n        script: str,\n        *,\n        object_group: Optional[str] = None,\n        include_command_line_api: Optional[bool] = None,\n        silent: Optional[bool] = None,\n        context_id: Optional[int] = None,\n        return_by_value: Optional[bool] = None,\n        generate_preview: Optional[bool] = None,\n        user_gesture: Optional[bool] = None,\n        await_promise: Optional[bool] = None,\n        throw_on_side_effect: Optional[bool] = None,\n        timeout: Optional[float] = None,\n        disable_breaks: Optional[bool] = None,\n        repl_mode: Optional[bool] = None,\n        allow_unsafe_eval_blocked_by_csp: Optional[bool] = None,\n        unique_context_id: Optional[str] = None,\n        serialization_options: Optional[SerializationOptions] = None,\n    ):\n        \"\"\"Create an evaluate command with the given parameters.\"\"\"\n        return RuntimeCommands.evaluate(\n            expression=script,\n            object_group=object_group,\n            include_command_line_api=include_command_line_api,\n            silent=silent,\n            context_id=context_id,\n            return_by_value=return_by_value,\n            generate_preview=generate_preview,\n            user_gesture=user_gesture,\n            await_promise=await_promise,\n            throw_on_side_effect=throw_on_side_effect,\n            timeout=timeout,\n            disable_breaks=disable_breaks,\n            repl_mode=repl_mode,\n            allow_unsafe_eval_blocked_by_csp=allow_unsafe_eval_blocked_by_csp,\n            unique_context_id=unique_context_id,\n            serialization_options=serialization_options,\n        )\n\n    async def _refresh_if_url_not_changed(self, url: str) -> bool:\n        \"\"\"Refresh page if URL hasn't changed.\"\"\"\n        current_url = await self.current_url\n        if current_url == url:\n            await self.refresh()\n            return True\n        return False\n\n    @staticmethod\n    def _validate_argument_error(response: EvaluateResponse) -> None:\n        \"\"\"\n        Validate that script didn't fail with ReferenceError about 'argument' being undefined.\n\n        Raises:\n            InvalidScriptWithElement: If script uses 'argument' keyword but no element was provided.\n        \"\"\"\n        evaluate_result = response.get('result')\n        if not isinstance(evaluate_result, dict):\n            return\n\n        remote_object = evaluate_result.get('result')\n        if not isinstance(remote_object, dict):\n            return\n\n        if not (\n            remote_object.get('type') == 'object'\n            and remote_object.get('subtype') == 'error'\n            and remote_object.get('className') == 'ReferenceError'\n        ):\n            return\n\n        description = remote_object.get('description', '')\n        if 'argument is not defined' in description:\n            raise InvalidScriptWithElement('Script contains \"argument\" but no element was provided')\n\n    _PAGE_LOAD_EVENT_MAP = {\n        PageLoadState.INTERACTIVE: PageEvent.DOM_CONTENT_EVENT_FIRED,\n        PageLoadState.COMPLETE: PageEvent.LOAD_EVENT_FIRED,\n    }\n\n    @asynccontextmanager\n    async def _wait_page_load(self, timeout: int = 300):\n        \"\"\"Wait for page to reach the configured load state using CDP events.\n\n        Registers a CDP event listener **before** yielding so the navigation\n        command can be issued inside the ``async with`` block without race\n        conditions.  This replaces the former ``document.readyState`` polling\n        loop, eliminating the dependency on ``Runtime.evaluate`` during page\n        load and the risk of inner command timeouts.\n\n        The CDP event used depends on ``browser.options.page_load_state``:\n\n        * ``INTERACTIVE`` — waits for ``Page.domContentEventFired``.\n        * ``COMPLETE`` — waits for ``Page.loadEventFired``.\n\n        Args:\n            timeout: Maximum seconds to wait for the target load state.\n\n        Raises:\n            PageLoadTimeout: If the page doesn't reach the target state in time.\n        \"\"\"\n        target_state = self._browser.options.page_load_state\n\n        page_loaded = asyncio.Event()\n        event_name = self._PAGE_LOAD_EVENT_MAP[target_state]\n        cleanup_page_events = not self._page_events_enabled\n\n        if cleanup_page_events:\n            await self.enable_page_events()\n\n        def on_loaded(_: dict):\n            page_loaded.set()\n\n        callback_id = await self.on(event_name, on_loaded)\n        logger.debug(f'Waiting for page load via {event_name} (timeout={timeout}s)')\n\n        try:\n            yield\n            await asyncio.wait_for(page_loaded.wait(), timeout=timeout)\n            logger.debug(f'Page load event received: {event_name}')\n        except asyncio.TimeoutError:\n            logger.error(f'Page load timeout after {timeout}s waiting for {event_name}')\n            raise PageLoadTimeout()\n        finally:\n            with contextlib.suppress(Exception):\n                await self.remove_callback(callback_id)\n            if cleanup_page_events:\n                with contextlib.suppress(Exception):\n                    await self.disable_page_events()\n\n    async def _find_cloudflare_shadow_root(self, timeout: float) -> ShadowRoot:\n        \"\"\"Poll for the Cloudflare Turnstile shadow root.\n\n        Repeatedly calls ``find_shadow_roots(deep=False)`` and checks each\n        shadow root's ``inner_html`` for the Cloudflare challenge domain.\n\n        Args:\n            timeout: Maximum seconds to wait for the shadow root.\n\n        Returns:\n            The first ShadowRoot whose inner HTML contains\n            ``challenges.cloudflare.com``.\n\n        Raises:\n            WaitElementTimeout: If no matching shadow root is found within\n                *timeout* seconds.\n        \"\"\"\n        start_time = asyncio.get_event_loop().time()\n        while True:\n            shadow_roots = await self.find_shadow_roots(deep=False)\n            for sr in shadow_roots:\n                html = await sr.inner_html\n                if _CLOUDFLARE_CHALLENGE_DOMAIN in html:\n                    return sr\n\n            if asyncio.get_event_loop().time() - start_time > timeout:\n                raise WaitElementTimeout(\n                    f'Timed out after {timeout}s waiting for Cloudflare Turnstile shadow root'\n                )\n            await asyncio.sleep(0.5)\n\n    async def _bypass_cloudflare(\n        self,\n        event: dict,\n        time_to_wait_captcha: float = 5,\n    ) -> None:\n        \"\"\"Attempt to bypass Cloudflare Turnstile captcha via shadow root traversal.\n\n        Traverses shadow roots to locate the Cloudflare iframe, navigates into\n        it, and clicks the actual checkbox element (``span.cb-i``).\n        \"\"\"\n        try:\n            timeout_int = int(time_to_wait_captcha)\n            shadow_root = await self._find_cloudflare_shadow_root(\n                timeout=time_to_wait_captcha,\n            )\n            iframe = await shadow_root.query(_CLOUDFLARE_IFRAME_SELECTOR, timeout=timeout_int)\n            body = await iframe.find(tag_name='body', timeout=timeout_int)\n            inner_shadow = await body.get_shadow_root(timeout=time_to_wait_captcha)\n            checkbox = await inner_shadow.query(_CLOUDFLARE_CHECKBOX_SELECTOR, timeout=timeout_int)\n            await checkbox.click()\n        except Exception as exc:\n            logger.error(f'Error in cloudflare bypass: {exc}')\n\n\nclass _DownloadHandle:\n    \"\"\"Handle returned by expect_download to access the downloaded file.\"\"\"\n\n    def __init__(\n        self,\n        state: dict[str, Any],\n        will_begin_future: asyncio.Future[bool],\n        done_future: asyncio.Future[bool],\n        timeout: float,\n    ) -> None:\n        self._state = state\n        self._will_begin_future = will_begin_future\n        self._done_future = done_future\n        self._timeout = timeout\n\n    @property\n    def file_path(self) -> Optional[str]:\n        return self._state.get('filePath')\n\n    async def wait_started(self, timeout: Optional[float] = None) -> None:\n        await asyncio.wait_for(self._will_begin_future, timeout=timeout or self._timeout)\n\n    async def wait_finished(self, timeout: Optional[float] = None) -> None:\n        await asyncio.wait_for(self._done_future, timeout=timeout or self._timeout)\n\n    async def read_bytes(self) -> bytes:\n        await self.wait_finished()\n        if not self.file_path:\n            raise FileNotFoundError('Download file path not available')\n        async with aiofiles.open(self.file_path, 'rb') as f:  # type: ignore[arg-type]\n            return await f.read()\n\n    async def read_base64(self) -> str:\n        data = await self.read_bytes()\n        return _b64.b64encode(data).decode('ascii')\n"
  },
  {
    "path": "pydoll/commands/__init__.py",
    "content": "# global imports\nfrom pydoll.commands.browser_commands import BrowserCommands\nfrom pydoll.commands.dom_commands import DomCommands\nfrom pydoll.commands.emulation_commands import EmulationCommands\nfrom pydoll.commands.fetch_commands import FetchCommands\nfrom pydoll.commands.input_commands import InputCommands\nfrom pydoll.commands.network_commands import NetworkCommands\nfrom pydoll.commands.page_commands import PageCommands\nfrom pydoll.commands.runtime_commands import RuntimeCommands\nfrom pydoll.commands.storage_commands import StorageCommands\nfrom pydoll.commands.target_commands import TargetCommands\n\n__all__ = [\n    'DomCommands',\n    'EmulationCommands',\n    'FetchCommands',\n    'InputCommands',\n    'NetworkCommands',\n    'PageCommands',\n    'RuntimeCommands',\n    'StorageCommands',\n    'BrowserCommands',\n    'TargetCommands',\n]\n"
  },
  {
    "path": "pydoll/commands/browser_commands.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.protocol.base import Command\nfrom pydoll.protocol.browser.methods import (\n    AddPrivacySandboxCoordinatorKeyConfigParams,\n    AddPrivacySandboxEnrollmentOverrideParams,\n    BrowserMethod,\n    CancelDownloadParams,\n    ExecuteBrowserCommandParams,\n    GetHistogramParams,\n    GetHistogramsParams,\n    GetWindowBoundsParams,\n    GetWindowForTargetParams,\n    GrantPermissionsParams,\n    ResetPermissionsParams,\n    SetContentsSizeParams,\n    SetDockTileParams,\n    SetDownloadBehaviorParams,\n    SetPermissionParams,\n    SetWindowBoundsParams,\n)\nfrom pydoll.protocol.browser.types import (\n    Bounds,\n    WindowState,\n)\n\nif TYPE_CHECKING:\n    from pydoll.protocol.browser.methods import (\n        AddPrivacySandboxCoordinatorKeyConfigCommand,\n        AddPrivacySandboxEnrollmentOverrideCommand,\n        CancelDownloadCommand,\n        CloseCommand,\n        CrashCommand,\n        CrashGpuProcessCommand,\n        DownloadBehavior,\n        ExecuteBrowserCommandCommand,\n        GetBrowserCommandLineCommand,\n        GetHistogramCommand,\n        GetHistogramsCommand,\n        GetVersionCommand,\n        GetWindowBoundsCommand,\n        GetWindowForTargetCommand,\n        GrantPermissionsCommand,\n        ResetPermissionsCommand,\n        SetContentsSizeCommand,\n        SetDockTileCommand,\n        SetDownloadBehaviorCommand,\n        SetPermissionCommand,\n        SetWindowBoundsCommand,\n    )\n    from pydoll.protocol.browser.types import (\n        BrowserCommandId,\n        BrowserContextID,\n        PermissionDescriptor,\n        PermissionSetting,\n        PermissionType,\n        PrivacySandboxAPI,\n        WindowID,\n    )\n\n\nclass BrowserCommands:\n    \"\"\"\n    BrowserCommands class provides a set of commands to interact with the\n    browser's main functionality based on CDP. These commands allow for\n    managing browser windows, such as closing windows, retrieving window IDs,\n    and adjusting window bounds (size and state).\n\n    The commands defined in this class provide functionality for:\n    - Managing browser windows and targets.\n    - Setting permissions and download behavior.\n    - Controlling browser windows (size, state).\n    - Retrieving browser information and versioning.\n    \"\"\"\n\n    @staticmethod\n    def get_version() -> GetVersionCommand:\n        \"\"\"\n        Generates a command to get browser version information.\n\n        Returns:\n            GetVersionCommand: The CDP command that returns browser version details\n                including protocol version, product name, revision, and user agent.\n        \"\"\"\n        return Command(method=BrowserMethod.GET_VERSION)\n\n    @staticmethod\n    def get_browser_command_line() -> GetBrowserCommandLineCommand:\n        \"\"\"\n        Returns the command line switches for the browser process.\n\n        Returns:\n            GetBrowserCommandLineCommand: The CDP command that returns command line arguments.\n\n        Note: Only works if --enable-automation is on the command line.\n        \"\"\"\n        return Command(method=BrowserMethod.GET_BROWSER_COMMAND_LINE)\n\n    @staticmethod\n    def get_histograms(\n        query: Optional[str] = None,\n        delta: bool = False,\n    ) -> GetHistogramsCommand:\n        \"\"\"\n        Get Chrome histograms.\n\n        Args:\n            query: Requested substring in name. Only histograms which have query as a\n                   substring in their name are extracted. An empty or absent query returns\n                   all histograms.\n            delta: If true, retrieve delta since last delta call.\n\n        Returns:\n            GetHistogramsCommand: The CDP command that returns histogram data.\n        \"\"\"\n        params = GetHistogramsParams()\n        if query is not None:\n            params['query'] = query\n        if delta:\n            params['delta'] = delta\n        return Command(method=BrowserMethod.GET_HISTOGRAMS, params=params)\n\n    @staticmethod\n    def get_histogram(\n        name: str,\n        delta: bool = False,\n    ) -> GetHistogramCommand:\n        \"\"\"\n        Get a Chrome histogram by name.\n\n        Args:\n            name: Requested histogram name.\n            delta: If true, retrieve delta since last delta call.\n\n        Returns:\n            GetHistogramCommand: The CDP command that returns histogram data.\n        \"\"\"\n        params = GetHistogramParams(name=name)\n        if delta:\n            params['delta'] = delta\n        return Command(method=BrowserMethod.GET_HISTOGRAM, params=params)\n\n    @staticmethod\n    def get_window_bounds(window_id: WindowID) -> GetWindowBoundsCommand:\n        \"\"\"\n        Get position and size of the browser window.\n\n        Args:\n            window_id: Browser window id.\n\n        Returns:\n            GetWindowBoundsCommand: The CDP command that returns window bounds information.\n        \"\"\"\n        params = GetWindowBoundsParams(windowId=window_id)\n        return Command(method=BrowserMethod.GET_WINDOW_BOUNDS, params=params)\n\n    @staticmethod\n    def get_window_for_target(\n        target_id: Optional[str] = None,\n    ) -> GetWindowForTargetCommand:\n        \"\"\"\n        Get the browser window that contains the devtools target.\n\n        Args:\n            target_id: Devtools agent host id. If called as a part of the session,\n                      associated targetId is used.\n\n        Returns:\n            GetWindowForTargetCommand: The CDP command that returns window information\n                including windowId and bounds.\n        \"\"\"\n        params = GetWindowForTargetParams()\n        if target_id is not None:\n            params['targetId'] = target_id\n        return Command(method=BrowserMethod.GET_WINDOW_FOR_TARGET, params=params)\n\n    @staticmethod\n    def set_window_bounds(window_id: WindowID, bounds: Bounds) -> SetWindowBoundsCommand:\n        \"\"\"\n        Set position and/or size of the browser window.\n\n        Args:\n            window_id: Browser window id.\n            bounds: New window bounds. The 'minimized', 'maximized' and 'fullscreen' states\n                   cannot be combined with 'left', 'top', 'width' or 'height'. Leaves\n                   unspecified fields unchanged.\n\n        Returns:\n            SetWindowBoundsCommand: The CDP command that sets window bounds.\n        \"\"\"\n        params = SetWindowBoundsParams(windowId=window_id, bounds=bounds)\n        return Command(method=BrowserMethod.SET_WINDOW_BOUNDS, params=params)\n\n    @staticmethod\n    def set_contents_size(\n        window_id: WindowID,\n        width: Optional[int] = None,\n        height: Optional[int] = None,\n    ) -> SetContentsSizeCommand:\n        \"\"\"\n        Set size of the browser contents resizing browser window as necessary.\n\n        Args:\n            window_id: Browser window id.\n            width: The window contents width in DIP. Assumes current width if omitted.\n                  Must be specified if 'height' is omitted.\n            height: The window contents height in DIP. Assumes current height if omitted.\n                   Must be specified if 'width' is omitted.\n\n        Returns:\n            SetContentsSizeCommand: The CDP command that sets window contents size.\n        \"\"\"\n        params = SetContentsSizeParams(windowId=window_id)\n        if width is not None:\n            params['width'] = width\n        if height is not None:\n            params['height'] = height\n        return Command(method=BrowserMethod.SET_CONTENTS_SIZE, params=params)\n\n    @staticmethod\n    def set_dock_tile(\n        badge_label: Optional[str] = None,\n        image: Optional[str] = None,\n    ) -> SetDockTileCommand:\n        \"\"\"\n        Set dock tile details, platform-specific.\n\n        Args:\n            badge_label: Optional badge label.\n            image: Png encoded image (base64 string when passed over JSON).\n\n        Returns:\n            SetDockTileCommand: The CDP command that sets dock tile details.\n        \"\"\"\n        params = SetDockTileParams()\n        if badge_label is not None:\n            params['badgeLabel'] = badge_label\n        if image is not None:\n            params['image'] = image\n        return Command(method=BrowserMethod.SET_DOCK_TILE, params=params)\n\n    @staticmethod\n    def execute_browser_command(command_id: BrowserCommandId) -> ExecuteBrowserCommandCommand:\n        \"\"\"\n        Invoke custom browser commands used by telemetry.\n\n        Args:\n            command_id: Browser command identifier.\n\n        Returns:\n            ExecuteBrowserCommandCommand: The CDP command that executes browser command.\n        \"\"\"\n        params = ExecuteBrowserCommandParams(commandId=command_id)\n        return Command(method=BrowserMethod.EXECUTE_BROWSER_COMMAND, params=params)\n\n    @staticmethod\n    def add_privacy_sandbox_enrollment_override(\n        url: str,\n    ) -> AddPrivacySandboxEnrollmentOverrideCommand:\n        \"\"\"\n        Allows a site to use privacy sandbox features that require enrollment\n        without the site actually being enrolled. Only supported on page targets.\n\n        Args:\n            url: Site URL.\n\n        Returns:\n            AddPrivacySandboxEnrollmentOverrideCommand: The CDP command that adds enrollment\n            override.\n        \"\"\"\n        params = AddPrivacySandboxEnrollmentOverrideParams(url=url)\n        return Command(method=BrowserMethod.ADD_PRIVACY_SANDBOX_ENROLLMENT_OVERRIDE, params=params)\n\n    @staticmethod\n    def add_privacy_sandbox_coordinator_key_config(\n        api: PrivacySandboxAPI,\n        coordinator_origin: str,\n        key_config: str,\n        browser_context_id: Optional[BrowserContextID] = None,\n    ) -> AddPrivacySandboxCoordinatorKeyConfigCommand:\n        \"\"\"\n        Configures encryption keys used with a given privacy sandbox API to talk\n        to a trusted coordinator. Since this is intended for test automation only,\n        coordinatorOrigin must be a .test domain. No existing coordinator\n        configuration for the origin may exist.\n\n        Args:\n            api: Privacy Sandbox API type.\n            coordinator_origin: Coordinator origin (must be .test domain).\n            key_config: Key configuration string.\n            browser_context_id: BrowserContext to perform the action in. When omitted,\n                               default browser context is used.\n\n        Returns:\n            AddPrivacySandboxCoordinatorKeyConfigCommand: The CDP command that adds key config.\n        \"\"\"\n        params = AddPrivacySandboxCoordinatorKeyConfigParams(\n            api=api,\n            coordinatorOrigin=coordinator_origin,\n            keyConfig=key_config,\n        )\n        if browser_context_id is not None:\n            params['browserContextId'] = browser_context_id\n        return Command(\n            method=BrowserMethod.ADD_PRIVACY_SANDBOX_COORDINATOR_KEY_CONFIG, params=params\n        )\n\n    @staticmethod\n    def set_permission(\n        permission: PermissionDescriptor,\n        setting: PermissionSetting,\n        origin: Optional[str] = None,\n        browser_context_id: Optional[BrowserContextID] = None,\n    ) -> SetPermissionCommand:\n        \"\"\"\n        Set permission settings for given origin.\n\n        Args:\n            permission: Descriptor of permission to override.\n            setting: Setting of the permission.\n            origin: Origin the permission applies to, all origins if not specified.\n            browser_context_id: Context to override. When omitted, default browser context is used.\n\n        Returns:\n            SetPermissionCommand: The CDP command that sets permission.\n        \"\"\"\n        params = SetPermissionParams(permission=permission, setting=setting)\n        if origin is not None:\n            params['origin'] = origin\n        if browser_context_id is not None:\n            params['browserContextId'] = browser_context_id\n        return Command(method=BrowserMethod.SET_PERMISSION, params=params)\n\n    @staticmethod\n    def grant_permissions(\n        permissions: list['PermissionType'],\n        origin: Optional[str] = None,\n        browser_context_id: Optional['BrowserContextID'] = None,\n    ) -> GrantPermissionsCommand:\n        \"\"\"\n        Grant specific permissions to the given origin and reject all others.\n\n        Args:\n            permissions: List of permissions to grant.\n            origin: Origin the permission applies to, all origins if not specified.\n            browser_context_id: BrowserContext to override permissions. When omitted,\n                               default browser context is used.\n\n        Returns:\n            GrantPermissionsCommand: The CDP command that grants permissions.\n        \"\"\"\n        params = GrantPermissionsParams(permissions=permissions)\n        if origin is not None:\n            params['origin'] = origin\n        if browser_context_id is not None:\n            params['browserContextId'] = browser_context_id\n        return Command(method=BrowserMethod.GRANT_PERMISSIONS, params=params)\n\n    @staticmethod\n    def reset_permissions(\n        browser_context_id: Optional['BrowserContextID'] = None,\n    ) -> ResetPermissionsCommand:\n        \"\"\"\n        Reset all permission management for all origins.\n\n        Args:\n            browser_context_id: BrowserContext to reset permissions. When omitted,\n                               default browser context is used.\n\n        Returns:\n            ResetPermissionsCommand: The CDP command that resets permissions.\n        \"\"\"\n        params = ResetPermissionsParams()\n        if browser_context_id is not None:\n            params['browserContextId'] = browser_context_id\n        return Command(method=BrowserMethod.RESET_PERMISSIONS, params=params)\n\n    @staticmethod\n    def set_download_behavior(\n        behavior: DownloadBehavior,\n        browser_context_id: Optional['BrowserContextID'] = None,\n        download_path: Optional[str] = None,\n        events_enabled: bool = False,\n    ) -> SetDownloadBehaviorCommand:\n        \"\"\"\n        Set the behavior when downloading a file.\n\n        Args:\n            behavior: Whether to allow all or deny all download requests, or use default\n                     Chrome behavior if available (otherwise deny). allowAndName allows\n                     download and names files according to their download guids.\n            browser_context_id: BrowserContext to set download behavior. When omitted,\n                               default browser context is used.\n            download_path: The default path to save downloaded files to. This is required\n                          if behavior is set to 'allow' or 'allowAndName'.\n            events_enabled: Whether to emit download events (defaults to false).\n\n        Returns:\n            SetDownloadBehaviorCommand: The CDP command that sets download behavior.\n        \"\"\"\n        params = SetDownloadBehaviorParams(behavior=behavior)\n        if browser_context_id is not None:\n            params['browserContextId'] = browser_context_id\n        if download_path is not None:\n            params['downloadPath'] = download_path\n        if events_enabled is not None:\n            params['eventsEnabled'] = events_enabled\n        return Command(method=BrowserMethod.SET_DOWNLOAD_BEHAVIOR, params=params)\n\n    @staticmethod\n    def cancel_download(\n        guid: str,\n        browser_context_id: Optional['BrowserContextID'] = None,\n    ) -> CancelDownloadCommand:\n        \"\"\"\n        Cancel a download if in progress.\n\n        Args:\n            guid: Global unique identifier of the download.\n            browser_context_id: BrowserContext to perform the action in. When omitted,\n                               default browser context is used.\n\n        Returns:\n            CancelDownloadCommand: The CDP command that cancels download.\n        \"\"\"\n        params = CancelDownloadParams(guid=guid)\n        if browser_context_id is not None:\n            params['browserContextId'] = browser_context_id\n        return Command(method=BrowserMethod.CANCEL_DOWNLOAD, params=params)\n\n    @staticmethod\n    def close() -> CloseCommand:\n        \"\"\"\n        Close browser gracefully.\n\n        Returns:\n            CloseCommand: The CDP command that closes the browser.\n        \"\"\"\n        return Command(method=BrowserMethod.CLOSE)\n\n    @staticmethod\n    def crash() -> CrashCommand:\n        \"\"\"\n        Crashes browser on the main thread.\n\n        Returns:\n            CrashCommand: The CDP command that crashes the browser.\n        \"\"\"\n        return Command(method=BrowserMethod.CRASH)\n\n    @staticmethod\n    def crash_gpu_process() -> CrashGpuProcessCommand:\n        \"\"\"\n        Crashes GPU process.\n\n        Returns:\n            CrashGpuProcessCommand: The CDP command that crashes the GPU process.\n        \"\"\"\n        return Command(method=BrowserMethod.CRASH_GPU_PROCESS)\n\n    # Helper methods for common window operations\n    @staticmethod\n    def set_window_maximized(window_id: WindowID) -> SetWindowBoundsCommand:\n        \"\"\"\n        Maximize a browser window.\n\n        Args:\n            window_id: Browser window id.\n\n        Returns:\n            SetWindowBoundsCommand: The CDP command that maximizes the window.\n        \"\"\"\n        bounds = Bounds(windowState=WindowState.MAXIMIZED)\n        return BrowserCommands.set_window_bounds(window_id, bounds)\n\n    @staticmethod\n    def set_window_minimized(window_id: WindowID) -> SetWindowBoundsCommand:\n        \"\"\"\n        Minimize a browser window.\n\n        Args:\n            window_id: Browser window id.\n\n        Returns:\n            SetWindowBoundsCommand: The CDP command that minimizes the window.\n        \"\"\"\n        bounds = Bounds(windowState=WindowState.MINIMIZED)\n        return BrowserCommands.set_window_bounds(window_id, bounds)\n\n    @staticmethod\n    def set_window_fullscreen(window_id: WindowID) -> SetWindowBoundsCommand:\n        \"\"\"\n        Set a browser window to fullscreen.\n\n        Args:\n            window_id: Browser window id.\n\n        Returns:\n            SetWindowBoundsCommand: The CDP command that sets window to fullscreen.\n        \"\"\"\n        bounds = Bounds(windowState=WindowState.FULLSCREEN)\n        return BrowserCommands.set_window_bounds(window_id, bounds)\n\n    @staticmethod\n    def set_window_normal(window_id: WindowID) -> SetWindowBoundsCommand:\n        \"\"\"\n        Set a browser window to normal state.\n\n        Args:\n            window_id: Browser window id.\n\n        Returns:\n            SetWindowBoundsCommand: The CDP command that sets window to normal state.\n        \"\"\"\n        bounds = Bounds(windowState=WindowState.NORMAL)\n        return BrowserCommands.set_window_bounds(window_id, bounds)\n"
  },
  {
    "path": "pydoll/commands/dom_commands.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.protocol.base import Command\nfrom pydoll.protocol.dom.methods import (\n    CollectClassNamesFromSubtreeParams,\n    CopyToParams,\n    DescribeNodeParams,\n    DiscardSearchResultsParams,\n    DomMethod,\n    EnableParams,\n    FocusParams,\n    GetAnchorElementParams,\n    GetAttributesParams,\n    GetBoxModelParams,\n    GetContainerForNodeParams,\n    GetContentQuadsParams,\n    GetDocumentParams,\n    GetElementByRelationParams,\n    GetFileInfoParams,\n    GetFrameOwnerParams,\n    GetNodeForLocationParams,\n    GetNodesForSubtreeByStyleParams,\n    GetNodeStackTracesParams,\n    GetOuterHTMLParams,\n    GetQueryingDescendantsForContainerParams,\n    GetRelayoutBoundaryParams,\n    GetSearchResultsParams,\n    MoveToParams,\n    PerformSearchParams,\n    PushNodeByPathToFrontendParams,\n    PushNodesByBackendIdsToFrontendParams,\n    QuerySelectorAllParams,\n    QuerySelectorParams,\n    RemoveAttributeParams,\n    RemoveNodeParams,\n    RequestChildNodesParams,\n    RequestNodeParams,\n    ResolveNodeParams,\n    ScrollIntoViewIfNeededParams,\n    SetAttributesAsTextParams,\n    SetAttributeValueParams,\n    SetFileInputFilesParams,\n    SetInspectedNodeParams,\n    SetNodeNameParams,\n    SetNodeStackTracesEnabledParams,\n    SetNodeValueParams,\n    SetOuterHTMLParams,\n)\n\nif TYPE_CHECKING:\n    from pydoll.protocol.dom.methods import (\n        CollectClassNamesFromSubtreeCommand,\n        CopyToCommand,\n        CSSComputedStyleProperty,\n        DescribeNodeCommand,\n        DisableCommand,\n        DiscardSearchResultsCommand,\n        EnableCommand,\n        FocusCommand,\n        GetAnchorElementCommand,\n        GetAttributesCommand,\n        GetBoxModelCommand,\n        GetContainerForNodeCommand,\n        GetContentQuadsCommand,\n        GetDetachedDomNodesCommand,\n        GetDocumentCommand,\n        GetElementByRelationCommand,\n        GetFileInfoCommand,\n        GetFrameOwnerCommand,\n        GetNodeForLocationCommand,\n        GetNodesForSubtreeByStyleCommand,\n        GetNodeStackTracesCommand,\n        GetOuterHTMLCommand,\n        GetQueryingDescendantsForContainerCommand,\n        GetRelayoutBoundaryCommand,\n        GetSearchResultsCommand,\n        GetTopLayerElementsCommand,\n        HideHighlightCommand,\n        HighlightNodeCommand,\n        HighlightRectCommand,\n        MarkUndoableStateCommand,\n        MoveToCommand,\n        PerformSearchCommand,\n        PushNodeByPathToFrontendCommand,\n        PushNodesByBackendIdsToFrontendCommand,\n        QuerySelectorAllCommand,\n        QuerySelectorCommand,\n        Rect,\n        RedoCommand,\n        RemoveAttributeCommand,\n        RemoveNodeCommand,\n        RequestChildNodesCommand,\n        RequestNodeCommand,\n        ResolveNodeCommand,\n        ScrollIntoViewIfNeededCommand,\n        SetAttributesAsTextCommand,\n        SetAttributeValueCommand,\n        SetFileInputFilesCommand,\n        SetInspectedNodeCommand,\n        SetNodeNameCommand,\n        SetNodeStackTracesEnabledCommand,\n        SetNodeValueCommand,\n        SetOuterHTMLCommand,\n        UndoCommand,\n    )\n    from pydoll.protocol.dom.types import (\n        IncludeWhitespace,\n        LogicalAxes,\n        PhysicalAxes,\n        RelationType,\n    )\n\n\nclass DomCommands:\n    \"\"\"\n    Implementation of Chrome DevTools Protocol for the DOM domain.\n\n    This class provides commands for interacting with the Document Object Model (DOM) in the\n    browser, enabling access and manipulation of the element structure in a web page.\n    The DOM domain in Chrome DevTools Protocol exposes operations for reading and writing to the\n    DOM, which is fundamental for browser automation, testing, and debugging.\n\n    Each DOM element is represented by a mirror object with a unique ID. This ID can be used\n    to gather additional information about the node, resolve it into JavaScript object wrappers,\n    manipulate attributes, and perform various other operations on the DOM structure.\n    \"\"\"\n\n    @staticmethod\n    def describe_node(\n        node_id: Optional[int] = None,\n        backend_node_id: Optional[int] = None,\n        object_id: Optional[str] = None,\n        depth: Optional[int] = None,\n        pierce: Optional[bool] = None,\n    ) -> DescribeNodeCommand:\n        \"\"\"\n        Describes a DOM node identified by its ID without requiring domain to be enabled.\n\n        The describe_node command is particularly useful in scenarios where you need to quickly\n        gather information about a specific element without subscribing to DOM change events,\n        making it more lightweight for isolated element inspection operations.\n\n        Args:\n            node_id: Identifier of the node known to the client.\n            backend_node_id: Identifier of the backend node used internally by the browser.\n            object_id: JavaScript object id of the node wrapper.\n            depth: Maximum depth at which children should be retrieved (default is 1).\n                  Use -1 for the entire subtree or provide an integer greater than 0.\n            pierce: Whether iframes and shadow roots should be traversed when returning\n                   the subtree (default is false).\n\n        Returns:\n            Command: CDP command that returns detailed information about the requested node.\n        \"\"\"\n        params = DescribeNodeParams()\n        if node_id is not None:\n            params['nodeId'] = node_id\n        if backend_node_id is not None:\n            params['backendNodeId'] = backend_node_id\n        if object_id is not None:\n            params['objectId'] = object_id\n        if depth:\n            params['depth'] = depth\n        if pierce is not None:\n            params['pierce'] = pierce\n        return Command(method=DomMethod.DESCRIBE_NODE, params=params)\n\n    @staticmethod\n    def disable() -> DisableCommand:\n        \"\"\"\n        Disables DOM agent for the current page.\n\n        Disabling the DOM domain stops the CDP from sending DOM-related events and\n        prevents further DOM manipulation operations until the domain is enabled again.\n        This can be important for optimizing performance when you're done with DOM\n        operations and want to minimize background processing.\n\n        Returns:\n            Command: CDP command to disable the DOM domain.\n        \"\"\"\n        return Command(method=DomMethod.DISABLE)\n\n    @staticmethod\n    def enable(include_whitespace: Optional['IncludeWhitespace'] = None) -> EnableCommand:\n        \"\"\"\n        Enables DOM agent for the current page.\n\n        Enabling the DOM domain is a prerequisite for receiving DOM events and using most DOM\n        manipulation methods. The DOM events include changes to the DOM tree structure,\n        attribute modifications, and many others. Without enabling this domain first,\n        many DOM operations would fail or provide incomplete information.\n\n        Args:\n            include_whitespace: Whether to include whitespace-only text nodes in the\n                               children array of returned Nodes. Allowed values: \"none\", \"all\".\n\n        Returns:\n            Command: CDP command to enable the DOM domain.\n        \"\"\"\n        params = EnableParams()\n        if include_whitespace:\n            params['includeWhitespace'] = include_whitespace\n        return Command(method=DomMethod.ENABLE, params=params)\n\n    @staticmethod\n    def focus(\n        node_id: Optional[int] = None,\n        backend_node_id: Optional[int] = None,\n        object_id: Optional[str] = None,\n    ) -> FocusCommand:\n        \"\"\"\n        Focuses the given element.\n\n        The focus command is crucial for simulating realistic user interactions, as many\n        events (like keyboard input) require that an element has focus first. It's also\n        important for testing proper tab order and keyboard accessibility of web pages.\n\n        Args:\n            node_id: Identifier of the node to focus.\n            backend_node_id: Identifier of the backend node to focus.\n            object_id: JavaScript object id of the node wrapper.\n\n        Returns:\n            Command: CDP command to focus on the specified element.\n        \"\"\"\n        params = FocusParams()\n        if node_id:\n            params['nodeId'] = node_id\n        if backend_node_id:\n            params['backendNodeId'] = backend_node_id\n        if object_id:\n            params['objectId'] = object_id\n        return Command(method=DomMethod.FOCUS, params=params)\n\n    @staticmethod\n    def get_attributes(node_id: int) -> GetAttributesCommand:\n        \"\"\"\n        Returns attributes for the specified node.\n\n        Attribute information is essential in web testing and automation because attributes\n        often contain crucial information about element state, behavior, and metadata.\n        This command provides an efficient way to access all attributes of an element\n        without parsing HTML or using JavaScript evaluation.\n\n        Args:\n            node_id: Id of the node to retrieve attributes for.\n\n        Returns:\n            Command: CDP command that returns an interleaved array of node attribute\n                    names and values [name1, value1, name2, value2, ...].\n        \"\"\"\n        params = GetAttributesParams(nodeId=node_id)\n        return Command(method=DomMethod.GET_ATTRIBUTES, params=params)\n\n    @staticmethod\n    def get_box_model(\n        node_id: Optional[int] = None,\n        backend_node_id: Optional[int] = None,\n        object_id: Optional[str] = None,\n    ) -> GetBoxModelCommand:\n        \"\"\"\n        Returns box model information for the specified node.\n\n        The box model is a fundamental concept in CSS that describes how elements are\n        rendered with content, padding, borders, and margins. This command provides\n        detailed information about these dimensions and coordinates, which is invaluable\n        for spatial analysis and precision interactions with elements on the page.\n\n        Args:\n            node_id: Identifier of the node.\n            backend_node_id: Identifier of the backend node.\n            object_id: JavaScript object id of the node wrapper.\n\n        Returns:\n            Command: CDP command that returns the box model for the node, including\n                    coordinates for content, padding, border, and margin boxes.\n        \"\"\"\n        params = GetBoxModelParams()\n        if node_id is not None:\n            params['nodeId'] = node_id\n        if backend_node_id is not None:\n            params['backendNodeId'] = backend_node_id\n        if object_id is not None:\n            params['objectId'] = object_id\n        return Command(method=DomMethod.GET_BOX_MODEL, params=params)\n\n    @staticmethod\n    def get_document(\n        depth: Optional[int] = None, pierce: Optional[bool] = None\n    ) -> GetDocumentCommand:\n        \"\"\"\n        Returns the root DOM node (and optionally the subtree) to the caller.\n\n        This is typically the first command called when interacting with the DOM, as it\n        provides access to the document's root node. From this root, you can traverse to\n        any other element on the page. This command implicitly enables DOM domain events\n        for the current target, making it a good starting point for DOM interaction.\n\n        Args:\n            depth: Maximum depth at which children should be retrieved (default is 1).\n                  Use -1 for the entire subtree or provide an integer greater than 0.\n            pierce: Whether iframes and shadow roots should be traversed when returning\n                  the subtree (default is false).\n\n        Returns:\n            Command: CDP command that returns the root DOM node.\n        \"\"\"\n        params = GetDocumentParams()\n        if depth is not None:\n            params['depth'] = depth\n        if pierce is not None:\n            params['pierce'] = pierce\n        return Command(method=DomMethod.GET_DOCUMENT, params=params)\n\n    @staticmethod\n    def get_node_for_location(\n        x: int,\n        y: int,\n        include_user_agent_shadow_dom: Optional[bool] = None,\n        ignore_pointer_events_none: Optional[bool] = None,\n    ) -> GetNodeForLocationCommand:\n        \"\"\"\n        Returns node id at given location on the page.\n\n        This command is particularly useful for bridging the gap between visual/pixel-based\n        information and the DOM structure. It allows you to convert screen coordinates to\n        actual DOM elements, which is essential for creating inspection tools or for testing\n        spatially-oriented interactions.\n\n        Args:\n            x: X coordinate relative to the main frame's viewport.\n            y: Y coordinate relative to the main frame's viewport.\n            include_user_agent_shadow_dom: Whether to include nodes in user agent shadow roots.\n            ignore_pointer_events_none: Whether to ignore pointer-events:none and test elements\n                                       underneath them.\n\n        Returns:\n            Command: CDP command that returns the node at the given location, including\n                   frame information when available.\n        \"\"\"\n        params = GetNodeForLocationParams(x=x, y=y)\n        if include_user_agent_shadow_dom is not None:\n            params['includeUserAgentShadowDOM'] = include_user_agent_shadow_dom\n        if ignore_pointer_events_none is not None:\n            params['ignorePointerEventsNone'] = ignore_pointer_events_none\n        return Command(method=DomMethod.GET_NODE_FOR_LOCATION, params=params)\n\n    @staticmethod\n    def get_outer_html(\n        node_id: Optional[int] = None,\n        backend_node_id: Optional[int] = None,\n        object_id: Optional[str] = None,\n    ) -> GetOuterHTMLCommand:\n        \"\"\"\n        Returns node's HTML markup, including the node itself and all its children.\n\n        This command provides a way to access the complete HTML representation of an\n        element, making it valuable for when you need to extract, analyze, or verify\n        HTML content. It's more comprehensive than just getting text content as it\n        preserves the full markup structure including tags, attributes, and child elements.\n\n        Args:\n            node_id: Identifier of the node.\n            backend_node_id: Identifier of the backend node.\n            object_id: JavaScript object id of the node wrapper.\n\n        Returns:\n            Command: CDP command that returns the outer HTML markup of the node.\n        \"\"\"\n        params = GetOuterHTMLParams()\n        if node_id is not None:\n            params['nodeId'] = node_id\n        if backend_node_id is not None:\n            params['backendNodeId'] = backend_node_id\n        if object_id is not None:\n            params['objectId'] = object_id\n        return Command(method=DomMethod.GET_OUTER_HTML, params=params)\n\n    @staticmethod\n    def hide_highlight() -> HideHighlightCommand:\n        \"\"\"\n        Hides any DOM element highlight.\n\n        This command is particularly useful in automation workflows where multiple elements\n        are highlighted in sequence, and you need to clear previous highlights before\n        proceeding to the next element to avoid visual clutter or interference.\n\n        Returns:\n            Command: CDP command to hide DOM element highlights.\n        \"\"\"\n        return Command(method=DomMethod.HIDE_HIGHLIGHT)\n\n    @staticmethod\n    def highlight_node() -> HighlightNodeCommand:\n        \"\"\"\n        Highlights DOM node.\n\n        Highlighting nodes is especially valuable during development and debugging sessions\n        to visually confirm which elements are being selected by selectors or coordinates.\n\n        Returns:\n            Command: CDP command to highlight a DOM node.\n        \"\"\"\n        return Command(method=DomMethod.HIGHLIGHT_NODE)\n\n    @staticmethod\n    def highlight_rect() -> HighlightRectCommand:\n        \"\"\"\n        Highlights given rectangle.\n\n        Unlike node highlighting, rectangle highlighting allows highlighting arbitrary\n        regions of the page, which is useful for highlighting computed areas or\n        regions that don't correspond directly to DOM elements.\n\n        Returns:\n            Command: CDP command to highlight a rectangular area.\n        \"\"\"\n        return Command(method=DomMethod.HIGHLIGHT_RECT)\n\n    @staticmethod\n    def move_to(\n        node_id: int,\n        target_node_id: int,\n        insert_before_node_id: Optional[int] = None,\n    ) -> MoveToCommand:\n        \"\"\"\n        Moves node into the new container, placing it before the given anchor.\n\n        This command allows for more complex DOM restructuring than simple attribute or\n        content changes. It's particularly useful when testing applications that involve\n        rearranging elements, such as sortable lists, kanban boards, or drag-and-drop interfaces.\n\n        Args:\n            node_id: Id of the node to move.\n            target_node_id: Id of the element to drop the moved node into.\n            insert_before_node_id: Drop node before this one (if absent, the moved node\n                                 becomes the last child of target_node_id).\n\n        Returns:\n            Command: CDP command to move a node, returning the new id of the moved node.\n        \"\"\"\n        params = MoveToParams(nodeId=node_id, targetNodeId=target_node_id)\n        if insert_before_node_id is not None:\n            params['insertBeforeNodeId'] = insert_before_node_id\n        return Command(method=DomMethod.MOVE_TO, params=params)\n\n    @staticmethod\n    def query_selector(\n        node_id: int,\n        selector: str,\n    ) -> QuerySelectorCommand:\n        \"\"\"\n        Executes querySelector on a given node.\n\n        This method is one of the most fundamental tools for element location, allowing\n        the use of standard CSS selectors to find elements in the DOM. Unlike JavaScript's\n        querySelector, this can be executed on any node (not just document), enabling\n        scoped searches within specific sections of the page.\n\n        Args:\n            node_id: Id of the node to query upon.\n            selector: CSS selector string.\n\n        Returns:\n            Command: CDP command that returns the first element matching the selector.\n        \"\"\"\n        params = QuerySelectorParams(nodeId=node_id, selector=selector)\n        return Command(method=DomMethod.QUERY_SELECTOR, params=params)\n\n    @staticmethod\n    def query_selector_all(\n        node_id: int,\n        selector: str,\n    ) -> QuerySelectorAllCommand:\n        \"\"\"\n        Executes querySelectorAll on a given node.\n\n        This method extends querySelector by returning all matching elements rather than just\n        the first one. This is essential for operations that need to process multiple elements,\n        such as extracting data from tables, lists, or grids, or verifying that the correct\n        number of elements are present.\n\n        Args:\n            node_id: Id of the node to query upon.\n            selector: CSS selector string.\n\n        Returns:\n            Command: CDP command that returns all elements matching the selector.\n        \"\"\"\n        params = QuerySelectorAllParams(nodeId=node_id, selector=selector)\n        return Command(method=DomMethod.QUERY_SELECTOR_ALL, params=params)\n\n    @staticmethod\n    def remove_attribute(\n        node_id: int,\n        name: str,\n    ) -> RemoveAttributeCommand:\n        \"\"\"\n        Removes attribute with given name from an element with given id.\n\n        This command allows direct manipulation of element attributes without using JavaScript\n        in the page context. It's useful for testing how elements behave when specific\n        attributes are removed or for preparing elements for specific test conditions.\n\n        Args:\n            node_id: Id of the element to remove attribute from.\n            name: Name of the attribute to remove.\n\n        Returns:\n            Command: CDP command to remove the specified attribute.\n        \"\"\"\n        params = RemoveAttributeParams(nodeId=node_id, name=name)\n        return Command(method=DomMethod.REMOVE_ATTRIBUTE, params=params)\n\n    @staticmethod\n    def remove_node(node_id: int) -> RemoveNodeCommand:\n        \"\"\"\n        Removes node with given id.\n\n        This command allows direct removal of DOM elements, which can be useful when\n        testing how an application responds to missing elements or when simplifying\n        a page for focused testing scenarios.\n\n        Args:\n            node_id: Id of the node to remove.\n\n        Returns:\n            Command: CDP command to remove the specified node.\n        \"\"\"\n        params = RemoveNodeParams(nodeId=node_id)\n        return Command(method=DomMethod.REMOVE_NODE, params=params)\n\n    @staticmethod\n    def request_child_nodes(\n        node_id: int,\n        depth: Optional[int] = None,\n        pierce: Optional[bool] = None,\n    ) -> RequestChildNodesCommand:\n        \"\"\"\n        Requests that children of the node with given id are returned to the caller.\n\n        This method is particularly useful when dealing with large DOM trees, as it allows\n        for more efficient exploration by loading children on demand rather than loading\n        the entire tree at once. Child nodes are returned as setChildNodes events.\n\n        Args:\n            node_id: Id of the node to get children for.\n            depth: The maximum depth at which children should be retrieved,\n                  defaults to 1. Use -1 for the entire subtree.\n            pierce: Whether or not iframes and shadow roots should be traversed.\n\n        Returns:\n            Command: CDP command to request child nodes.\n        \"\"\"\n        params = RequestChildNodesParams(nodeId=node_id)\n        if depth is not None:\n            params['depth'] = depth\n        if pierce is not None:\n            params['pierce'] = pierce\n        return Command(method=DomMethod.REQUEST_CHILD_NODES, params=params)\n\n    @staticmethod\n    def request_node(\n        object_id: str,\n    ) -> RequestNodeCommand:\n        \"\"\"\n        Requests that the node is sent to the caller given the JavaScript node object reference.\n\n        This method bridges the gap between JavaScript objects in the page context and the\n        CDP's node representation system, allowing automation to work with elements that\n        might only be available as JavaScript references (e.g., from event handlers).\n\n        Args:\n            object_id: JavaScript object id to convert into a Node.\n\n        Returns:\n            Command: CDP command that returns the Node id for the given object.\n        \"\"\"\n        params = RequestNodeParams(objectId=object_id)\n        return Command(method=DomMethod.REQUEST_NODE, params=params)\n\n    @staticmethod\n    def resolve_node(\n        node_id: Optional[int] = None,\n        backend_node_id: Optional[int] = None,\n        object_group: Optional[str] = None,\n        execution_context_id: Optional[int] = None,\n    ) -> ResolveNodeCommand:\n        \"\"\"\n        Resolves the JavaScript node object for a given NodeId or BackendNodeId.\n\n        This method provides the opposite functionality of requestNode - instead of getting\n        a CDP node from a JavaScript object, it gets a JavaScript object from a CDP node.\n        This enables executing JavaScript operations on nodes identified through CDP.\n\n        Args:\n            node_id: Id of the node to resolve.\n            backend_node_id: Backend id of the node to resolve.\n            object_group: Symbolic group name that can be used to release multiple objects.\n            execution_context_id: Execution context in which to resolve the node.\n\n        Returns:\n            Command: CDP command that returns a JavaScript object wrapper for the node.\n        \"\"\"\n        params = ResolveNodeParams()\n        if node_id is not None:\n            params['nodeId'] = node_id\n        if backend_node_id is not None:\n            params['backendNodeId'] = backend_node_id\n        if object_group is not None:\n            params['objectGroup'] = object_group\n        if execution_context_id is not None:\n            params['executionContextId'] = execution_context_id\n        return Command(method=DomMethod.RESOLVE_NODE, params=params)\n\n    @staticmethod\n    def scroll_into_view_if_needed(\n        node_id: Optional[int] = None,\n        backend_node_id: Optional[int] = None,\n        object_id: Optional[str] = None,\n        rect: Optional[Rect] = None,\n    ) -> ScrollIntoViewIfNeededCommand:\n        \"\"\"\n        Scrolls the specified node into view if not already visible.\n\n        This command is crucial for reliable web automation, as it ensures elements\n        are actually visible in the viewport before attempting interactions. Modern\n        websites often use lazy loading and have long scrollable areas, making this\n        command essential for working with elements that may not be initially visible.\n\n        Args:\n            node_id: Identifier of the node.\n            backend_node_id: Identifier of the backend node.\n            object_id: JavaScript object id of the node wrapper.\n            rect: Optional rect to scroll into view, relative to the node bounds.\n\n        Returns:\n            Command: CDP command to scroll the element into view.\n        \"\"\"\n        params = ScrollIntoViewIfNeededParams()\n        if node_id is not None:\n            params['nodeId'] = node_id\n        if backend_node_id is not None:\n            params['backendNodeId'] = backend_node_id\n        if object_id is not None:\n            params['objectId'] = object_id\n        if rect is not None:\n            params['rect'] = rect\n        return Command(method=DomMethod.SCROLL_INTO_VIEW_IF_NEEDED, params=params)\n\n    @staticmethod\n    def set_attributes_as_text(\n        node_id: int,\n        text: str,\n        name: Optional[str] = None,\n    ) -> SetAttributesAsTextCommand:\n        \"\"\"\n        Sets attribute for an element with given id, using text representation.\n\n        This command allows for more complex attribute manipulation than set_attribute_value,\n        as it accepts a text representation that can potentially define multiple attributes\n        or include special formatting. It's particularly useful when trying to replicate\n        exactly how attributes would be defined in HTML source code.\n\n        Args:\n            node_id: Id of the element to set attribute for.\n            text: Text with a new attribute value.\n            name: Attribute name to replace with new text value.\n\n        Returns:\n            Command: CDP command to set an attribute as text.\n        \"\"\"\n        params = SetAttributesAsTextParams(nodeId=node_id, text=text)\n        if name is not None:\n            params['name'] = name\n        return Command(method=DomMethod.SET_ATTRIBUTES_AS_TEXT, params=params)\n\n    @staticmethod\n    def set_attribute_value(\n        node_id: int,\n        name: str,\n        value: str,\n    ) -> SetAttributeValueCommand:\n        \"\"\"\n        Sets attribute for element with given id.\n\n        This command provides direct control over element attributes without using JavaScript,\n        which is essential for testing how applications respond to attribute changes or for\n        setting up specific test conditions by controlling element attributes directly.\n\n        Args:\n            node_id: Id of the element to set attribute for.\n            name: Attribute name.\n            value: Attribute value.\n\n        Returns:\n            Command: CDP command to set an attribute value.\n        \"\"\"\n        params = SetAttributeValueParams(nodeId=node_id, name=name, value=value)\n        return Command(method=DomMethod.SET_ATTRIBUTE_VALUE, params=params)\n\n    @staticmethod\n    def set_file_input_files(\n        files: list[str],\n        node_id: Optional[int] = None,\n        backend_node_id: Optional[int] = None,\n        object_id: Optional[str] = None,\n    ) -> SetFileInputFilesCommand:\n        \"\"\"\n        Sets files for the given file input element.\n\n        This command solves one of the most challenging automation problems: working with\n        file inputs. It bypasses the OS-level file dialog that normally appears when clicking\n        a file input, allowing automated tests to provide files programmatically.\n\n        Args:\n            files: list of file paths to set.\n            node_id: Identifier of the node.\n            backend_node_id: Identifier of the backend node.\n            object_id: JavaScript object id of the node wrapper.\n\n        Returns:\n            Command: CDP command to set files for a file input element.\n        \"\"\"\n        params = SetFileInputFilesParams(files=files)\n        if node_id is not None:\n            params['nodeId'] = node_id\n        if backend_node_id is not None:\n            params['backendNodeId'] = backend_node_id\n        if object_id is not None:\n            params['objectId'] = object_id\n        return Command(method=DomMethod.SET_FILE_INPUT_FILES, params=params)\n\n    @staticmethod\n    def set_node_name(\n        node_id: int,\n        name: str,\n    ) -> SetNodeNameCommand:\n        \"\"\"\n        Sets node name for a node with given id.\n\n        This command allows changing the actual tag name of an element, which can be useful\n        for testing how applications handle different types of elements or for testing the\n        impact of semantic HTML choices on accessibility and behavior.\n\n        Args:\n            node_id: Id of the node to set name for.\n            name: New node name.\n\n        Returns:\n            Command: CDP command that returns the new node id after the name change.\n        \"\"\"\n        params = SetNodeNameParams(nodeId=node_id, name=name)\n        return Command(method=DomMethod.SET_NODE_NAME, params=params)\n\n    @staticmethod\n    def set_node_value(\n        node_id: int,\n        value: str,\n    ) -> SetNodeValueCommand:\n        \"\"\"\n        Sets node value for a node with given id.\n\n        This command is particularly useful for updating the content of text nodes and\n        comments, allowing direct manipulation of text content without changing the\n        surrounding HTML structure.\n\n        Args:\n            node_id: Id of the node to set value for.\n            value: New node value.\n\n        Returns:\n            Command: CDP command to set a node's value.\n        \"\"\"\n        params = SetNodeValueParams(nodeId=node_id, value=value)\n        return Command(method=DomMethod.SET_NODE_VALUE, params=params)\n\n    @staticmethod\n    def set_outer_html(\n        node_id: int,\n        outer_html: str,\n    ) -> SetOuterHTMLCommand:\n        \"\"\"\n        Sets node HTML markup, replacing existing one.\n\n        This is one of the most powerful DOM manipulation commands, as it allows completely\n        replacing an element and all its children with new HTML. This is useful for making\n        major structural changes to the page or for testing how applications handle\n        dynamically inserted content.\n\n        Args:\n            node_id: Id of the node to set outer HTML for.\n            outer_html: HTML markup to set.\n\n        Returns:\n            Command: CDP command to set the outer HTML of a node.\n        \"\"\"\n        params = SetOuterHTMLParams(nodeId=node_id, outerHTML=outer_html)\n        return Command(method=DomMethod.SET_OUTER_HTML, params=params)\n\n    @staticmethod\n    def collect_class_names_from_subtree(\n        node_id: int,\n    ) -> CollectClassNamesFromSubtreeCommand:\n        \"\"\"\n        Collects class names for the node with given id and all of its children.\n\n        This method is valuable for understanding the styling landscape of a page,\n        especially in complex applications where multiple CSS frameworks might be\n        in use or where classes are dynamically applied.\n\n        Args:\n            node_id: Id of the node to collect class names for.\n\n        Returns:\n            Command: CDP command that returns a list of all unique class names in the subtree.\n        \"\"\"\n        params = CollectClassNamesFromSubtreeParams(nodeId=node_id)\n        return Command(method=DomMethod.COLLECT_CLASS_NAMES_FROM_SUBTREE, params=params)\n\n    @staticmethod\n    def copy_to(\n        node_id: int,\n        target_node_id: int,\n        insert_before_node_id: Optional[int] = None,\n    ) -> CopyToCommand:\n        \"\"\"\n        Creates a deep copy of the specified node and places it into the target container.\n\n        Unlike move_to, this command creates a copy of the node, leaving the original intact.\n        This is useful when you want to duplicate content rather than move it, such as when\n        testing how multiple instances of the same component behave.\n\n        Args:\n            node_id: Id of the node to copy.\n            target_node_id: Id of the element to drop the copy into.\n            insert_before_node_id: Drop the copy before this node (if absent, the copy becomes\n                                 the last child of target_node_id).\n\n        Returns:\n            Command: CDP command that returns the id of the new copy.\n        \"\"\"\n        params = CopyToParams(nodeId=node_id, targetNodeId=target_node_id)\n        if insert_before_node_id is not None:\n            params['insertBeforeNodeId'] = insert_before_node_id\n        return Command(method=DomMethod.COPY_TO, params=params)\n\n    @staticmethod\n    def discard_search_results(\n        search_id: str,\n    ) -> DiscardSearchResultsCommand:\n        \"\"\"\n        Discards search results from the session with the given id.\n\n        This method helps manage resources when performing multiple searches during\n        a session, allowing explicit cleanup of search results that are no longer needed.\n\n        Args:\n            search_id: Unique search session identifier.\n\n        Returns:\n            Command: CDP command to discard search results.\n        \"\"\"\n        params = DiscardSearchResultsParams(searchId=search_id)\n        return Command(method=DomMethod.DISCARD_SEARCH_RESULTS, params=params)\n\n    @staticmethod\n    def get_anchor_element(\n        node_id: int,\n        anchor_specifier: Optional[str] = None,\n    ) -> GetAnchorElementCommand:\n        \"\"\"\n        Finds the closest ancestor node that is an anchor element for the given node.\n\n        This method is useful when working with content inside links or when you need to\n        find the enclosing link element for text or other elements. This helps in cases\n        where you might locate text but need to find the actual link around it.\n\n        Args:\n            node_id: Id of the node to search for an anchor around.\n            anchor_specifier: Optional specifier for anchor tag properties.\n\n        Returns:\n            Command: CDP command that returns the anchor element node information.\n        \"\"\"\n        params = GetAnchorElementParams(nodeId=node_id)\n        if anchor_specifier is not None:\n            params['anchorSpecifier'] = anchor_specifier\n        return Command(method=DomMethod.GET_ANCHOR_ELEMENT, params=params)\n\n    @staticmethod\n    def get_container_for_node(\n        node_id: int,\n        container_name: Optional[str] = None,\n        physical_axes: Optional['PhysicalAxes'] = None,\n        logical_axes: Optional['LogicalAxes'] = None,\n        queries_scroll_state: Optional[bool] = None,\n    ) -> GetContainerForNodeCommand:\n        \"\"\"\n        Finds a containing element for the given node based on specified parameters.\n\n        This method helps in understanding the structural and layout context of elements,\n        particularly in complex layouts using CSS features like flexbox, grid, or when\n        dealing with scrollable containers.\n\n        Args:\n            node_id: Id of the node to find the container for.\n            container_name: Name of the container to look for (e.g., 'scrollable', 'flex').\n            physical_axes: Physical axes to consider (Horizontal, Vertical, Both).\n            logical_axes: Logical axes to consider (Inline, Block, Both).\n            queries_scroll_state: Whether to query scroll state or not.\n\n        Returns:\n            Command: CDP command that returns information about the containing element.\n        \"\"\"\n        params = GetContainerForNodeParams(nodeId=node_id)\n        if container_name is not None:\n            params['containerName'] = container_name\n        if physical_axes is not None:\n            params['physicalAxes'] = physical_axes\n        if logical_axes is not None:\n            params['logicalAxes'] = logical_axes\n        if queries_scroll_state is not None:\n            params['queriesScrollState'] = queries_scroll_state\n        return Command(method=DomMethod.GET_CONTAINER_FOR_NODE, params=params)\n\n    @staticmethod\n    def get_content_quads(\n        node_id: Optional[int] = None,\n        backend_node_id: Optional[int] = None,\n        object_id: Optional[str] = None,\n    ) -> GetContentQuadsCommand:\n        \"\"\"\n        Returns quads that describe node position on the page.\n\n        This method provides detailed geometric information about an element's position\n        on the page, accounting for any transformations, rotations, or other CSS effects.\n        This is more precise than getBoxModel for complex layouts.\n\n        Args:\n            node_id: Identifier of the node.\n            backend_node_id: Identifier of the backend node.\n            object_id: JavaScript object id of the node wrapper.\n\n        Returns:\n            Command: CDP command that returns the quads describing the node position.\n        \"\"\"\n        params = GetContentQuadsParams()\n        if node_id is not None:\n            params['nodeId'] = node_id\n        if backend_node_id is not None:\n            params['backendNodeId'] = backend_node_id\n        if object_id is not None:\n            params['objectId'] = object_id\n        return Command(method=DomMethod.GET_CONTENT_QUADS, params=params)\n\n    @staticmethod\n    def get_detached_dom_nodes() -> GetDetachedDomNodesCommand:\n        \"\"\"\n        Returns information about detached DOM tree elements.\n\n        This method is primarily useful for debugging memory issues related to the DOM,\n        as detached DOM nodes (nodes no longer in the document but still referenced in\n        JavaScript) are a common cause of memory leaks in web applications.\n\n        Returns:\n            Command: CDP command that returns information about detached DOM nodes.\n        \"\"\"\n        return Command(method=DomMethod.GET_DETACHED_DOM_NODES)\n\n    @staticmethod\n    def get_element_by_relation(\n        node_id: int,\n        relation: RelationType,\n    ) -> GetElementByRelationCommand:\n        \"\"\"\n        Retrieves an element related to the given one in a specified way.\n\n        This method provides a way to find elements based on their relationships to other\n        elements, such as finding the next focusable element after a given one. This is\n        useful for simulating keyboard navigation or for analyzing element relationships.\n\n        Args:\n            node_id: Id of the reference node.\n            relation: Type of relationship (e.g., nextSibling, previousSibling, firstChild).\n\n        Returns:\n            Command: CDP command that returns the related element node.\n        \"\"\"\n        params = GetElementByRelationParams(nodeId=node_id, relation=relation)\n        return Command(method=DomMethod.GET_ELEMENT_BY_RELATION, params=params)\n\n    @staticmethod\n    def get_file_info(\n        object_id: str,\n    ) -> GetFileInfoCommand:\n        \"\"\"\n        Returns file information for the given File object.\n\n        This method is useful when working with file inputs and the File API, providing\n        access to file metadata like name, size, and MIME type for files selected in\n        file input elements or created programmatically.\n\n        Args:\n            object_id: JavaScript object id of the File object to get info for.\n\n        Returns:\n            Command: CDP command that returns file information.\n        \"\"\"\n        params = GetFileInfoParams(objectId=object_id)\n        return Command(method=DomMethod.GET_FILE_INFO, params=params)\n\n    @staticmethod\n    def get_frame_owner(\n        frame_id: str,\n    ) -> GetFrameOwnerCommand:\n        \"\"\"\n        Returns iframe element that owns the given frame.\n\n        This method is essential when working with pages that contain iframes, as it\n        allows mapping between frame IDs (used in CDP) and the actual iframe elements\n        in the parent document.\n\n        Args:\n            frame_id: Id of the frame to get the owner element for.\n\n        Returns:\n            Command: CDP command that returns the frame owner element.\n        \"\"\"\n        params = GetFrameOwnerParams(frameId=frame_id)\n        return Command(method=DomMethod.GET_FRAME_OWNER, params=params)\n\n    @staticmethod\n    def get_nodes_for_subtree_by_style(\n        node_id: int,\n        computed_styles: list[CSSComputedStyleProperty],\n        pierce: Optional[bool] = None,\n    ) -> GetNodesForSubtreeByStyleCommand:\n        \"\"\"\n        Finds nodes with a given computed style in a subtree.\n\n        This method allows finding elements based on their computed styles rather than just\n        structure or attributes. This is powerful for testing visual aspects of a page or\n        for finding elements that match specific visual criteria.\n\n        Args:\n            node_id: Node to start the search from.\n            computed_styles: list of computed style properties to match against.\n            pierce: Whether or not iframes and shadow roots should be traversed.\n\n        Returns:\n            Command: CDP command that returns nodes matching the specified styles.\n        \"\"\"\n        params = GetNodesForSubtreeByStyleParams(nodeId=node_id, computedStyles=computed_styles)\n        if pierce is not None:\n            params['pierce'] = pierce\n        return Command(method=DomMethod.GET_NODES_FOR_SUBTREE_BY_STYLE, params=params)\n\n    @staticmethod\n    def get_node_stack_traces(\n        node_id: int,\n    ) -> GetNodeStackTracesCommand:\n        \"\"\"\n        Gets stack traces associated with a specific node.\n\n        This method is powerful for debugging, as it reveals the JavaScript execution paths\n        that led to the creation of specific DOM elements, helping developers understand\n        the relationship between their code and the resulting DOM structure.\n\n        Args:\n            node_id: Id of the node to get stack traces for.\n\n        Returns:\n            Command: CDP command that returns stack traces related to the node.\n        \"\"\"\n        params = GetNodeStackTracesParams(nodeId=node_id)\n        return Command(method=DomMethod.GET_NODE_STACK_TRACES, params=params)\n\n    @staticmethod\n    def get_querying_descendants_for_container(\n        node_id: int,\n    ) -> GetQueryingDescendantsForContainerCommand:\n        \"\"\"\n        Returns the querying descendants for container.\n\n        This method is particularly useful for working with CSS Container Queries, helping\n        to identify which descendant elements are affected by or querying a particular\n        container element.\n\n        Args:\n            node_id: Id of the container node to find querying descendants for.\n\n        Returns:\n            Command: CDP command that returns querying descendant information.\n        \"\"\"\n        params = GetQueryingDescendantsForContainerParams(nodeId=node_id)\n        return Command(method=DomMethod.GET_QUERYING_DESCENDANTS_FOR_CONTAINER, params=params)\n\n    @staticmethod\n    def get_relayout_boundary(\n        node_id: int,\n    ) -> GetRelayoutBoundaryCommand:\n        \"\"\"\n        Returns the root of the relayout boundary for the given node.\n\n        This method helps in understanding layout performance by identifying the boundary\n        of layout recalculations when a particular element changes. This is valuable for\n        optimizing rendering performance.\n\n        Args:\n            node_id: Id of the node to find relayout boundary for.\n\n        Returns:\n            Command: CDP command that returns the relayout boundary node.\n        \"\"\"\n        params = GetRelayoutBoundaryParams(nodeId=node_id)\n        return Command(method=DomMethod.GET_RELAYOUT_BOUNDARY, params=params)\n\n    @staticmethod\n    def get_search_results(\n        search_id: str,\n        from_index: int,\n        to_index: int,\n    ) -> GetSearchResultsCommand:\n        \"\"\"\n        Returns search results from given `fromIndex` to given `toIndex` from a search.\n\n        This method is used in conjunction with performSearch to retrieve search results\n        in batches, which is essential when dealing with large result sets that might\n        be inefficient to transfer all at once.\n\n        Args:\n            search_id: Unique search session identifier from performSearch.\n            from_index: Start index to retrieve results from.\n            to_index: End index to retrieve results to (exclusive).\n\n        Returns:\n            Command: CDP command that returns the requested search results.\n        \"\"\"\n        params = GetSearchResultsParams(searchId=search_id, fromIndex=from_index, toIndex=to_index)\n        return Command(method=DomMethod.GET_SEARCH_RESULTS, params=params)\n\n    @staticmethod\n    def get_top_layer_elements() -> GetTopLayerElementsCommand:\n        \"\"\"\n        Returns all top layer elements in the document.\n\n        This method is valuable for working with modern web UIs that make extensive use\n        of overlays, modals, dropdowns, and other elements that need to appear above\n        the normal document flow.\n\n        Returns:\n            Command: CDP command that returns the top layer element information.\n        \"\"\"\n        return Command(method=DomMethod.GET_TOP_LAYER_ELEMENTS)\n\n    @staticmethod\n    def mark_undoable_state() -> MarkUndoableStateCommand:\n        \"\"\"\n        Marks last undoable state.\n\n        This method helps in managing DOM manipulation state, allowing the creation of\n        savepoints that can be reverted to with the undo command. This is useful for\n        complex sequences of DOM operations that should be treated as a unit.\n\n        Returns:\n            Command: CDP command to mark the current state as undoable.\n        \"\"\"\n        return Command(method=DomMethod.MARK_UNDOABLE_STATE)\n\n    @staticmethod\n    def perform_search(\n        query: str,\n        include_user_agent_shadow_dom: Optional[bool] = None,\n    ) -> PerformSearchCommand:\n        \"\"\"\n        Searches for a given string in the DOM tree.\n\n        This method initiates a search across the DOM tree, supporting plain text,\n        CSS selectors, or XPath expressions. It's a powerful way to find elements\n        or content across the entire document without knowing the exact structure.\n\n        Args:\n            query: Plain text or query selector or XPath search query.\n            include_user_agent_shadow_dom: True to include user agent shadow DOM in the search.\n\n        Returns:\n            Command: CDP command that returns search results identifier and count.\n        \"\"\"\n        params = PerformSearchParams(query=query)\n        if include_user_agent_shadow_dom is not None:\n            params['includeUserAgentShadowDOM'] = include_user_agent_shadow_dom\n        return Command(method=DomMethod.PERFORM_SEARCH, params=params)\n\n    @staticmethod\n    def push_node_by_path_to_frontend(\n        path: str,\n    ) -> PushNodeByPathToFrontendCommand:\n        \"\"\"\n        Requests that the node is sent to the caller given its path.\n\n        This method provides an alternative way to reference nodes when node IDs aren't\n        available, using path expressions instead. This can be useful when integrating\n        with systems that identify elements by path rather than by ID.\n\n        Args:\n            path: Path to node in the proprietary format.\n\n        Returns:\n            Command: CDP command that returns the node id for the node.\n        \"\"\"\n        params = PushNodeByPathToFrontendParams(path=path)\n        return Command(method=DomMethod.PUSH_NODE_BY_PATH_TO_FRONTEND, params=params)\n\n    @staticmethod\n    def push_nodes_by_backend_ids_to_frontend(\n        backend_node_ids: list[int],\n    ) -> PushNodesByBackendIdsToFrontendCommand:\n        \"\"\"\n        Requests that a batch of nodes is sent to the caller given their backend node ids.\n\n        This method allows for efficient batch processing when you have multiple backend\n        node IDs and need to convert them to frontend node IDs for further operations.\n\n        Args:\n            backend_node_ids: The array of backend node ids.\n\n        Returns:\n            Command: CDP command that returns an array of node ids.\n        \"\"\"\n        params = PushNodesByBackendIdsToFrontendParams(backendNodeIds=backend_node_ids)\n        return Command(method=DomMethod.PUSH_NODES_BY_BACKEND_IDS_TO_FRONTEND, params=params)\n\n    @staticmethod\n    def redo() -> RedoCommand:\n        \"\"\"\n        Re-does the last undone action.\n\n        This method works in conjunction with undo and markUndoableState to provide\n        a transactional approach to DOM manipulations, allowing for stepping back and\n        forth through a sequence of changes.\n\n        Returns:\n            Command: CDP command to redo the last undone action.\n        \"\"\"\n        return Command(method=DomMethod.REDO)\n\n    @staticmethod\n    def set_inspected_node(\n        node_id: int,\n    ) -> SetInspectedNodeCommand:\n        \"\"\"\n        Enables console to refer to the node with given id via $x command line API.\n\n        This method creates a bridge between automated testing/scripting and manual console\n        interaction, making it easy to reference specific nodes in the console for\n        debugging or experimentation.\n\n        Args:\n            node_id: DOM node id to be accessible by means of $x command line API.\n\n        Returns:\n            Command: CDP command to set the inspected node.\n        \"\"\"\n        params = SetInspectedNodeParams(nodeId=node_id)\n        return Command(method=DomMethod.SET_INSPECTED_NODE, params=params)\n\n    @staticmethod\n    def set_node_stack_traces_enabled(\n        enable: bool,\n    ) -> SetNodeStackTracesEnabledCommand:\n        \"\"\"\n        Sets if stack traces should be captured for Nodes.\n\n        This method enables or disables the collection of stack traces when DOM nodes\n        are created, which can be extremely valuable for debugging complex applications\n        to understand where and why specific DOM elements are being created.\n\n        Args:\n            enable: Enable or disable stack trace collection.\n\n        Returns:\n            Command: CDP command to enable or disable node stack traces.\n        \"\"\"\n        params = SetNodeStackTracesEnabledParams(enable=enable)\n        return Command(method=DomMethod.SET_NODE_STACK_TRACES_ENABLED, params=params)\n\n    @staticmethod\n    def undo() -> UndoCommand:\n        \"\"\"\n        Undoes the last performed action.\n\n        This method works in conjunction with redo and markUndoableState to provide\n        transactional control over DOM manipulations, allowing for reverting changes\n        when needed.\n\n        Returns:\n            Command: CDP command to undo the last performed action.\n        \"\"\"\n        return Command(method=DomMethod.UNDO)\n"
  },
  {
    "path": "pydoll/commands/emulation_commands.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.protocol.base import Command\nfrom pydoll.protocol.emulation.methods import (\n    EmulationMethod,\n    SetUserAgentOverrideParams,\n)\n\nif TYPE_CHECKING:\n    from pydoll.protocol.emulation.methods import SetUserAgentOverrideCommand\n    from pydoll.protocol.emulation.types import UserAgentMetadata\n\n\nclass EmulationCommands:\n    \"\"\"\n    Implementation of Chrome DevTools Protocol for the Emulation domain.\n\n    This class provides commands for emulating different environments,\n    including user agent overrides, device metrics, and other browser\n    characteristics useful for testing and automation.\n\n    See https://chromedevtools.github.io/devtools-protocol/tot/Emulation/\n    \"\"\"\n\n    @staticmethod\n    def set_user_agent_override(\n        user_agent: str,\n        accept_language: Optional[str] = None,\n        platform: Optional[str] = None,\n        user_agent_metadata: Optional[UserAgentMetadata] = None,\n    ) -> SetUserAgentOverrideCommand:\n        \"\"\"\n        Overrides the browser's User-Agent string via the Emulation domain.\n\n        This is the canonical CDP method for User-Agent override. It modifies\n        both HTTP headers and navigator JavaScript properties, ensuring\n        consistency between all layers.\n\n        When userAgentMetadata is provided, Client Hint headers (Sec-CH-UA-*)\n        will also be sent consistently with the overridden User-Agent.\n\n        Args:\n            user_agent: Complete User-Agent string to use.\n            accept_language: Browser language preference (e.g., 'en-US,en;q=0.9').\n            platform: Value for navigator.platform (e.g., 'Win32', 'MacIntel').\n            user_agent_metadata: Client Hints metadata for Sec-CH-UA-* headers\n                and navigator.userAgentData.\n\n        Returns:\n            SetUserAgentOverrideCommand: CDP command to override user agent.\n        \"\"\"\n        params = SetUserAgentOverrideParams(userAgent=user_agent)\n        if accept_language is not None:\n            params['acceptLanguage'] = accept_language\n        if platform is not None:\n            params['platform'] = platform\n        if user_agent_metadata is not None:\n            params['userAgentMetadata'] = user_agent_metadata\n        return Command(method=EmulationMethod.SET_USER_AGENT_OVERRIDE, params=params)\n"
  },
  {
    "path": "pydoll/commands/fetch_commands.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.protocol.base import Command\nfrom pydoll.protocol.fetch.methods import (\n    AuthChallengeResponse,\n    ContinueRequestParams,\n    ContinueResponseParams,\n    ContinueWithAuthParams,\n    EnableParams,\n    FailRequestParams,\n    FetchMethod,\n    FulfillRequestParams,\n    GetResponseBodyParams,\n    TakeResponseBodyAsStreamParams,\n)\nfrom pydoll.protocol.fetch.types import RequestPattern\n\nif TYPE_CHECKING:\n    from pydoll.protocol.fetch.methods import (\n        ContinueRequestCommand,\n        ContinueResponseCommand,\n        ContinueWithAuthCommand,\n        DisableCommand,\n        EnableCommand,\n        FailRequestCommand,\n        FulfillRequestCommand,\n        GetResponseBodyCommand,\n        TakeResponseBodyAsStreamCommand,\n    )\n    from pydoll.protocol.fetch.types import (\n        AuthChallengeResponseType,\n        HeaderEntry,\n        RequestStage,\n        ResourceType,\n    )\n    from pydoll.protocol.network.types import ErrorReason, RequestMethod\n\n\nclass FetchCommands:\n    \"\"\"\n    This class encapsulates the fetch commands of the Chrome DevTools Protocol (CDP).\n\n    CDP's Fetch domain allows interception and modification of network requests\n    at the application layer. This enables developers to examine, modify, and\n    control network traffic, which is particularly useful for testing, debugging,\n    and advanced automation scenarios.\n\n    The commands defined in this class provide functionality for:\n    - Enabling and disabling fetch request interception\n    - Continuing, fulfilling, or failing intercepted requests\n    - Handling authentication challenges\n    - Retrieving and modifying response bodies\n    - Processing response data as streams\n    \"\"\"\n\n    @staticmethod\n    def continue_request(\n        request_id: str,\n        url: Optional[str] = None,\n        method: Optional['RequestMethod'] = None,\n        post_data: Optional[str] = None,\n        headers: Optional[list['HeaderEntry']] = None,\n        intercept_response: Optional[bool] = None,\n    ) -> ContinueRequestCommand:\n        \"\"\"\n        Creates a command to continue a paused fetch request.\n\n        This command allows the browser to resume a fetch operation that has\n        been intercepted. You can modify the fetch request URL, method,\n        headers, and body before continuing.\n\n        Args:\n            request_id (str): The ID of the fetch request to continue.\n            url (Optional[str]): The new URL for the fetch request. Defaults to None.\n            method (Optional[RequestMethod]): The HTTP method to use (e.g., 'GET',\n                'POST'). Defaults to None.\n            post_data (Optional[dict]): The body data to send with the fetch\n                request. Defaults to None.\n            headers (Optional[list[HeaderEntry]]): A list of HTTP headers to include\n                in the fetch request. Defaults to None.\n            intercept_response (Optional[bool]): Indicates if the response\n                should be intercepted. Defaults to None.\n\n        Returns:\n            Command[Response]: A command for continuing the fetch request.\n        \"\"\"\n        params = ContinueRequestParams(requestId=request_id)\n        if url is not None:\n            params['url'] = url\n        if method is not None:\n            params['method'] = method\n        if post_data is not None:\n            params['postData'] = post_data\n        if headers is not None:\n            params['headers'] = headers\n        if intercept_response is not None:\n            params['interceptResponse'] = intercept_response\n        return Command(method=FetchMethod.CONTINUE_REQUEST, params=params)\n\n    @staticmethod\n    def continue_request_with_auth(\n        request_id: str,\n        auth_challenge_response: AuthChallengeResponseType,\n        proxy_username: Optional[str] = None,\n        proxy_password: Optional[str] = None,\n    ) -> ContinueWithAuthCommand:\n        \"\"\"\n        Creates a command to continue a paused fetch request with\n        authentication.\n\n        This command is used when the fetch operation requires authentication.\n        It provides the necessary credentials to continue the request.\n\n        Args:\n            request_id (str): The ID of the fetch request to continue.\n            auth_challenge_response (AuthChallengeResponseType): The authentication\n                challenge response type.\n            proxy_username (Optional[str]): The username for proxy authentication.\n                Defaults to None.\n            proxy_password (Optional[str]): The password for proxy authentication.\n                Defaults to None.\n\n        Returns:\n            Command[Response]: A command for continuing the fetch request with\n                authentication.\n        \"\"\"\n        auth_challenge_response_dict = AuthChallengeResponse(response=auth_challenge_response)\n        if proxy_username is not None:\n            auth_challenge_response_dict['username'] = proxy_username\n        if proxy_password is not None:\n            auth_challenge_response_dict['password'] = proxy_password\n\n        params = ContinueWithAuthParams(\n            requestId=request_id,\n            authChallengeResponse=auth_challenge_response_dict,\n        )\n        return Command(method=FetchMethod.CONTINUE_WITH_AUTH, params=params)\n\n    @staticmethod\n    def disable() -> DisableCommand:\n        \"\"\"\n        Creates a command to disable fetch interception.\n\n        This command stops the browser from intercepting fetch requests.\n\n        Returns:\n            Command[Response]: A command for disabling fetch interception.\n        \"\"\"\n        return Command(method=FetchMethod.DISABLE)\n\n    @staticmethod\n    def enable(\n        handle_auth_requests: bool,\n        url_pattern: str = '*',\n        resource_type: Optional['ResourceType'] = None,\n        request_stage: Optional['RequestStage'] = None,\n    ) -> EnableCommand:\n        \"\"\"\n        Creates a command to enable fetch interception.\n\n        This command allows the browser to start intercepting fetch requests.\n        You can specify whether to handle authentication challenges and the\n        types of resources to intercept.\n\n        Args:\n            handle_auth_requests (bool): Indicates if authentication requests\n                should be handled.\n            url_pattern (str): Pattern to match URLs for interception. Defaults to '*'.\n            resource_type (Optional[ResourceType]): The type of resource to intercept.\n                Defaults to None.\n            request_stage (Optional[RequestStage]): The stage of the request to intercept.\n                Defaults to None.\n\n        Returns:\n            Command[Response]: A command for enabling fetch interception.\n        \"\"\"\n        request_pattern = RequestPattern(urlPattern=url_pattern)\n        if resource_type is not None:\n            request_pattern['resourceType'] = resource_type\n        if request_stage is not None:\n            request_pattern['requestStage'] = request_stage\n\n        params = EnableParams(patterns=[request_pattern], handleAuthRequests=handle_auth_requests)\n        return Command(method=FetchMethod.ENABLE, params=params)\n\n    @staticmethod\n    def fail_request(request_id: str, error_reason: ErrorReason) -> FailRequestCommand:\n        \"\"\"\n        Creates a command to simulate a failure in a fetch request.\n\n        This command allows you to simulate a failure for a specific fetch\n        operation, providing a reason for the failure.\n\n        Args:\n            request_id (str): The ID of the fetch request to fail.\n            error_reason (ErrorReason): The reason for the failure.\n\n        Returns:\n            Command[Response]: A command for failing the fetch request.\n        \"\"\"\n        params = FailRequestParams(requestId=request_id, errorReason=error_reason)\n        return Command(method=FetchMethod.FAIL_REQUEST, params=params)\n\n    @staticmethod\n    def fulfill_request(\n        request_id: str,\n        response_code: int,\n        response_headers: Optional[list['HeaderEntry']] = None,\n        body: Optional[str] = None,\n        response_phrase: Optional[str] = None,\n    ) -> FulfillRequestCommand:\n        \"\"\"\n        Creates a command to fulfill a fetch request with a custom response.\n\n        This command allows you to provide a custom response for a fetch\n        operation, including the HTTP status code, headers, and body content.\n\n        Args:\n            request_id (str): The ID of the fetch request to fulfill.\n            response_code (int): The HTTP status code to return.\n            response_headers (Optional[list[HeaderEntry]]): A list of response headers.\n                Defaults to None.\n            body (Optional[dict]): The body content of the response. Defaults to None.\n            response_phrase (Optional[str]): The response phrase (e.g., 'OK',\n                'Not Found'). Defaults to None.\n\n        Returns:\n            Command[Response]: A command for fulfilling the fetch request.\n        \"\"\"\n        params = FulfillRequestParams(\n            requestId=request_id,\n            responseCode=response_code,\n        )\n        if response_headers is not None:\n            params['responseHeaders'] = response_headers\n        if body is not None:\n            params['body'] = body\n        if response_phrase is not None:\n            params['responsePhrase'] = response_phrase\n        return Command(method=FetchMethod.FULFILL_REQUEST, params=params)\n\n    @staticmethod\n    def get_response_body(request_id: str) -> GetResponseBodyCommand:\n        \"\"\"\n        Creates a command to retrieve the response body of a fetch request.\n\n        This command allows you to access the body of a completed fetch\n        operation, which can be useful for analyzing the response data.\n\n        Args:\n            request_id (str): The ID of the fetch request to retrieve the body\n                from.\n\n        Returns:\n            Command[GetResponseBodyResponse]: A command for getting the response body.\n        \"\"\"\n        params = GetResponseBodyParams(requestId=request_id)\n        return Command(method=FetchMethod.GET_RESPONSE_BODY, params=params)\n\n    @staticmethod\n    def continue_response(\n        request_id: str,\n        response_code: Optional[int] = None,\n        response_headers: Optional[list['HeaderEntry']] = None,\n        response_phrase: Optional[str] = None,\n    ) -> ContinueResponseCommand:\n        \"\"\"\n        Creates a command to continue a fetch response for an intercepted\n        request.\n\n        This command allows the browser to continue the response flow for a\n        specific fetch request, including customizing the HTTP status code,\n        headers, and response phrase.\n\n        Args:\n            request_id (str): The ID of the fetch request to continue the\n                response for.\n            response_code (Optional[int]): The HTTP status code to send.\n                Defaults to None.\n            response_headers (Optional[list[HeaderEntry]]): A list of response headers.\n                Defaults to None.\n            response_phrase (Optional[str]): The response phrase (e.g., 'OK').\n                Defaults to None.\n\n        Returns:\n            Command[Response]: A command for continuing the fetch response.\n        \"\"\"\n        params = ContinueResponseParams(requestId=request_id)\n        if response_code is not None:\n            params['responseCode'] = response_code\n        if response_headers is not None:\n            params['responseHeaders'] = response_headers\n        if response_phrase is not None:\n            params['responsePhrase'] = response_phrase\n        return Command(method=FetchMethod.CONTINUE_RESPONSE, params=params)\n\n    @staticmethod\n    def take_response_body_as_stream(\n        request_id: str,\n    ) -> TakeResponseBodyAsStreamCommand:\n        \"\"\"\n        Creates a command to take the response body as a stream.\n\n        This command allows you to receive the response body as a stream\n        which can be useful for handling large responses.\n\n        Args:\n            request_id (str): The ID of the fetch request to take the response\n                body stream from.\n\n        Returns:\n            Command[TakeResponseBodyAsStreamResponse]: A command for taking the response\n                body as a stream.\n        \"\"\"\n        params = TakeResponseBodyAsStreamParams(requestId=request_id)\n        return Command(method=FetchMethod.TAKE_RESPONSE_BODY_AS_STREAM, params=params)\n"
  },
  {
    "path": "pydoll/commands/input_commands.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.protocol.base import Command\nfrom pydoll.protocol.input.methods import (\n    DispatchDragEventParams,\n    DispatchKeyEventParams,\n    DispatchMouseEventParams,\n    DispatchTouchEventParams,\n    EmulateTouchFromMouseEventParams,\n    ImeSetCompositionParams,\n    InputMethod,\n    InsertTextParams,\n    SetIgnoreInputEventsParams,\n    SetInterceptDragsParams,\n    SynthesizePinchGestureParams,\n    SynthesizeScrollGestureParams,\n    SynthesizeTapGestureParams,\n)\n\nif TYPE_CHECKING:\n    from pydoll.protocol.input.methods import (\n        CancelDraggingCommand,\n        DispatchDragEventCommand,\n        DispatchKeyEventCommand,\n        DispatchMouseEventCommand,\n        DispatchTouchEventCommand,\n        DragData,\n        EmulateTouchFromMouseEventCommand,\n        ImeSetCompositionCommand,\n        InsertTextCommand,\n        SetIgnoreInputEventsCommand,\n        SetInterceptDragsCommand,\n        SynthesizePinchGestureCommand,\n        SynthesizeScrollGestureCommand,\n        SynthesizeTapGestureCommand,\n        TouchPoint,\n    )\n    from pydoll.protocol.input.types import (\n        DragEventType,\n        GestureSourceType,\n        KeyEventType,\n        KeyLocation,\n        KeyModifier,\n        MouseButton,\n        MouseEventType,\n        PointerType,\n        TouchEventType,\n    )\n\n\nclass InputCommands:\n    \"\"\"\n    A class for simulating user input events using Chrome DevTools Protocol.\n\n    The Input domain provides methods for simulating user input, including:\n    - Keyboard events (key presses, releases)\n    - Mouse events (clicks, movements, wheel)\n    - Touch events (taps, multi-touch gestures)\n    - Drag and drop events\n    - Synthetic gestures (pinch, scroll, tap)\n\n    These methods allow for programmatic control of input events without requiring\n    actual user interaction, making it useful for testing and automation.\n    \"\"\"\n\n    @staticmethod\n    def cancel_dragging() -> CancelDraggingCommand:\n        \"\"\"\n        Generates a command to cancel any active dragging in the page.\n\n        This is useful when you need to interrupt an ongoing drag operation\n        that might have been started with dispatchDragEvent or by other means.\n\n        Returns:\n            Command: The CDP command to cancel dragging.\n        \"\"\"\n        return Command(method=InputMethod.CANCEL_DRAGGING)\n\n    @staticmethod\n    def dispatch_key_event(  # noqa: PLR0912\n        type: KeyEventType,\n        modifiers: Optional[KeyModifier] = None,\n        timestamp: Optional[float] = None,\n        text: Optional[str] = None,\n        unmodified_text: Optional[str] = None,\n        key_identifier: Optional[str] = None,\n        code: Optional[str] = None,\n        key: Optional[str] = None,\n        windows_virtual_key_code: Optional[int] = None,\n        native_virtual_key_code: Optional[int] = None,\n        auto_repeat: Optional[bool] = None,\n        is_keypad: Optional[bool] = None,\n        is_system_key: Optional[bool] = None,\n        location: Optional[KeyLocation] = None,\n        commands: Optional[list[str]] = None,\n    ) -> DispatchKeyEventCommand:\n        \"\"\"\n        Generates a command to dispatch a key event to the page.\n\n        This method can simulate various types of keyboard events such as key presses,\n        key releases, and character inputs.\n\n        Args:\n            type: Type of the key event. Allowed values: keyDown, keyUp, rawKeyDown, char.\n                 - keyDown: Corresponds to a user pressing a key\n                 - keyUp: Corresponds to a user releasing a key\n                 - rawKeyDown: A physical key press, without the text processing\n                 - char: Generates a character without explicit key events\n            modifiers: Bit field representing pressed modifier keys. Values:\n                      Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).\n                      For example, to simulate Ctrl+Shift, use 10.\n            timestamp: Time at which the event occurred, in seconds since epoch.\n            text: Text as generated by processing a virtual key code with a keyboard layout.\n                 Not needed for 'keyUp' and 'rawKeyDown' events (default: \"\").\n            unmodified_text: Text that would have been generated by the keyboard without modifiers\n                           (except for shift). Useful for shortcut key handling (default: \"\").\n            key_identifier: Unique key identifier (e.g., 'U+0041') (default: \"\").\n            code: Unique DOM defined string value for each physical key (e.g., 'KeyA')\n                (default: \"\").\n            key: Unique DOM defined string value describing the meaning of the key in the\n                context of active modifiers, keyboard layout, etc. (e.g., 'AltGr')\n                (default: \"\").\n            windows_virtual_key_code: Windows virtual key code (default: 0).\n            native_virtual_key_code: Native virtual key code (default: 0).\n            auto_repeat: Whether the event was generated from auto repeat (default: false).\n            is_keypad: Whether the event was generated from the keypad (default: false).\n            is_system_key: Whether the event was a system key event (default: false).\n            location: Whether the event was from the left or right side of the keyboard:\n                     0=Default, 1=Left, 2=Right (default: 0).\n            commands: Editing commands to send with the key event (e.g., 'selectAll')\n                     (default: []). These are related to but not equal to the command names\n                     used in `document.execCommand` and NSStandardKeyBindingResponding.\n\n        Returns:\n            Command: The CDP command to dispatch the key event.\n        \"\"\"\n        params = DispatchKeyEventParams(type=type)\n        if modifiers is not None:\n            params['modifiers'] = modifiers\n        if timestamp is not None:\n            params['timestamp'] = timestamp\n        if text is not None:\n            params['text'] = text\n        if unmodified_text is not None:\n            params['unmodifiedText'] = unmodified_text\n        if key_identifier is not None:\n            params['keyIdentifier'] = key_identifier\n        if code is not None:\n            params['code'] = code\n        if key is not None:\n            params['key'] = key\n        if windows_virtual_key_code is not None:\n            params['windowsVirtualKeyCode'] = windows_virtual_key_code\n        if native_virtual_key_code is not None:\n            params['nativeVirtualKeyCode'] = native_virtual_key_code\n        if auto_repeat is not None:\n            params['autoRepeat'] = auto_repeat\n        if is_keypad is not None:\n            params['isKeypad'] = is_keypad\n        if is_system_key is not None:\n            params['isSystemKey'] = is_system_key\n        if location is not None:\n            params['location'] = location\n        if commands is not None:\n            params['commands'] = commands\n        return Command(method=InputMethod.DISPATCH_KEY_EVENT, params=params)\n\n    @staticmethod\n    def dispatch_mouse_event(\n        type: MouseEventType,\n        x: int,\n        y: int,\n        modifiers: Optional[KeyModifier] = None,\n        timestamp: Optional[float] = None,\n        button: Optional[MouseButton] = None,\n        click_count: Optional[int] = None,\n        force: Optional[float] = None,\n        tangential_pressure: Optional[float] = None,\n        tilt_x: Optional[float] = None,\n        tilt_y: Optional[float] = None,\n        twist: Optional[int] = None,\n        delta_x: Optional[float] = None,\n        delta_y: Optional[float] = None,\n        pointer_type: Optional[PointerType] = None,\n    ) -> DispatchMouseEventCommand:\n        \"\"\"\n        Generates a command to dispatch a mouse event to the page.\n\n        This method allows simulating various mouse interactions such as clicks,\n        movements, and wheel scrolling.\n\n        Args:\n            type: Type of the mouse event. Allowed values:\n                 - mousePressed: Mouse button pressed\n                 - mouseReleased: Mouse button released\n                 - mouseMoved: Mouse moved\n                 - mouseWheel: Mouse wheel rotated\n            x: X coordinate of the event relative to the main frame's viewport in CSS pixels.\n            y: Y coordinate of the event relative to the main frame's viewport in CSS pixels.\n                0 refers to the top of the viewport, and Y increases going down.\n            modifiers: Bit field representing pressed modifier keys. Values:\n                Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).\n            timestamp: Time at which the event occurred, in seconds since epoch.\n            button: Mouse button being pressed/released. Default is \"none\".\n                Allowed values: \"none\", \"left\", \"middle\", \"right\", \"back\", \"forward\".\n            click_count: Number of times the mouse button was clicked (default: 0).\n                For example, 2 for a double-click.\n            force: The normalized pressure, which has a range of [0,1] (default: 0).\n                Used primarily for pressure-sensitive inputs.\n            tangential_pressure: The normalized tangential pressure, which has a range\n                of [-1,1] (default: 0). Used for stylus input.\n            tilt_x: The plane angle between the Y-Z plane and the plane containing both the stylus\n                axis and the Y axis, in degrees of the range [-90,90]. A positive tiltX is\n                to the right (default: 0).\n            tilt_y: The plane angle between the X-Z plane and the plane containing both the stylus\n                axis and the X axis, in degrees of the range [-90,90]. A positive tiltY is\n                towards the user (default: 0).\n            twist: The clockwise rotation of a pen stylus around its own major axis,\n                in degrees in the range [0,359] (default: 0).\n            delta_x: X delta in CSS pixels for mouse wheel event (default: 0).\n                Positive values scroll right.\n            delta_y: Y delta in CSS pixels for mouse wheel event (default: 0).\n                Positive values scroll up.\n            pointer_type: Pointer type (default: \"mouse\"). Allowed values: \"mouse\", \"pen\".\n\n        Returns:\n            Command: The CDP command to dispatch the mouse event.\n        \"\"\"\n        params = DispatchMouseEventParams(type=type, x=x, y=y)\n        if modifiers is not None:\n            params['modifiers'] = modifiers\n        if timestamp is not None:\n            params['timestamp'] = timestamp\n        if button is not None:\n            params['button'] = button\n        if click_count is not None:\n            params['clickCount'] = click_count\n        if force is not None:\n            params['force'] = force\n        if tangential_pressure is not None:\n            params['tangentialPressure'] = tangential_pressure\n        if tilt_x is not None:\n            params['tiltX'] = tilt_x\n        if tilt_y is not None:\n            params['tiltY'] = tilt_y\n        if twist is not None:\n            params['twist'] = twist\n        if delta_x is not None:\n            params['deltaX'] = delta_x\n        if delta_y is not None:\n            params['deltaY'] = delta_y\n        if pointer_type is not None:\n            params['pointerType'] = pointer_type\n        return Command(method=InputMethod.DISPATCH_MOUSE_EVENT, params=params)\n\n    @staticmethod\n    def dispatch_touch_event(\n        type: TouchEventType,\n        touch_points: list[TouchPoint],\n        modifiers: Optional[KeyModifier] = None,\n        timestamp: Optional[float] = None,\n    ) -> DispatchTouchEventCommand:\n        \"\"\"\n        Generates a command to dispatch a touch event to the page.\n\n        This method allows simulating touch interactions on touch-enabled devices\n        or emulated touch environments.\n\n        Args:\n            type: Type of the touch event. Allowed values:\n                 - touchStart: Touch started - at least one point must be specified\n                 - touchEnd: Touch ended - points that are no longer pressed should be removed\n                 - touchMove: Touch moved - active points should be updated\n                 - touchCancel: Touch canceled - clears all touch points\n                 Touch end and cancel events must not contain any touch points,\n                 while touch start and move must contain at least one.\n            touch_points: list of active touch points. One event per any changed point\n                        (compared to previous event) is generated, emulating\n                        pressing/moving/releasing points one by one.\n                        Each point includes coordinates and other properties.\n            modifiers: Bit field representing pressed modifier keys. Values:\n                      Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).\n            timestamp: Time at which the event occurred, in seconds since epoch.\n\n        Returns:\n            Command: The CDP command to dispatch the touch event.\n        \"\"\"\n        params = DispatchTouchEventParams(type=type, touchPoints=touch_points)\n        if modifiers is not None:\n            params['modifiers'] = modifiers\n        if timestamp is not None:\n            params['timestamp'] = timestamp\n        return Command(method=InputMethod.DISPATCH_TOUCH_EVENT, params=params)\n\n    @staticmethod\n    def set_ignore_input_events(ignore: bool) -> SetIgnoreInputEventsCommand:\n        \"\"\"\n        Generates a command to ignore input events (useful while auditing page).\n\n        When ignore is true, all input events will be ignored, which can be useful\n        during automated tests or when you want to prevent user interaction\n        while performing certain operations.\n\n        Args:\n            ignore: If true, input events processing will be ignored.\n\n        Returns:\n            Command: The CDP command to set ignore input events.\n        \"\"\"\n        params = SetIgnoreInputEventsParams(ignore=ignore)\n        return Command(method=InputMethod.SET_IGNORE_INPUT_EVENTS, params=params)\n\n    @staticmethod\n    def dispatch_drag_event(\n        type: DragEventType,\n        x: int,\n        y: int,\n        data: DragData,\n        modifiers: Optional[KeyModifier] = None,\n    ) -> DispatchDragEventCommand:\n        \"\"\"\n        Generates a command to dispatch a drag event into the page.\n\n        This experimental method allows simulating drag and drop operations\n        by dispatching drag events at specific coordinates.\n\n        Args:\n            type: Type of the drag event. Allowed values:\n                 - dragEnter: Fired when a dragged item enters a valid drop target\n                 - dragOver: Fired when a dragged item is being dragged over a valid drop target\n                 - drop: Fired when an item is dropped on a valid drop target\n                 - dragCancel: Fired when a drag operation is being canceled\n            x: X coordinate of the event relative to the main frame's viewport in CSS pixels.\n            y: Y coordinate of the event relative to the main frame's viewport in CSS pixels.\n                0 refers to the top of the viewport, and Y increases going down.\n            data: Drag data containing items being dragged, their MIME types, and other information.\n            modifiers: Bit field representing pressed modifier keys. Values:\n                      Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).\n\n        Returns:\n            Command: The CDP command to dispatch the drag event.\n        \"\"\"\n        params = DispatchDragEventParams(type=type, data=data, x=x, y=y)\n        if modifiers is not None:\n            params['modifiers'] = modifiers\n        return Command(method=InputMethod.DISPATCH_DRAG_EVENT, params=params)\n\n    @staticmethod\n    def emulate_touch_from_mouse_event(  # noqa: PLR0913, PLR0917\n        type: MouseEventType,\n        x: int,\n        y: int,\n        button: MouseButton,\n        timestamp: Optional[float] = None,\n        delta_x: Optional[float] = None,\n        delta_y: Optional[float] = None,\n        modifiers: Optional[KeyModifier] = None,\n        click_count: Optional[int] = None,\n    ) -> EmulateTouchFromMouseEventCommand:\n        \"\"\"\n        Generates a command to emulate touch event from the mouse event parameters.\n\n        This experimental method allows converting mouse events into touch events,\n        useful for testing touch interactions in environments where touch is not available.\n\n        Args:\n            type: Type of the mouse event to convert. Allowed values:\n                 - mousePressed: Converted to touchStart\n                 - mouseReleased: Converted to touchEnd\n                 - mouseMoved: Converted to touchMove\n                 - mouseWheel: May trigger scrolling\n            x: X coordinate of the mouse pointer in device-independent pixels (DIP).\n            y: Y coordinate of the mouse pointer in DIP.\n            button: Mouse button. Only \"none\", \"left\", \"right\" are supported.\n            timestamp: Time at which the event occurred, in seconds since epoch.\n                      Default is current time.\n            delta_x: X delta in DIP for mouse wheel event (default: 0). Used for scrolling.\n            delta_y: Y delta in DIP for mouse wheel event (default: 0). Used for scrolling.\n            modifiers: Bit field representing pressed modifier keys. Values:\n                      Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).\n            click_count: Number of times the mouse button was clicked (default: 0).\n                       For example, 2 for a double-click.\n\n        Returns:\n            Command: The CDP command to emulate touch from mouse event.\n        \"\"\"\n        params = EmulateTouchFromMouseEventParams(type=type, x=x, y=y, button=button)\n        if timestamp is not None:\n            params['timestamp'] = timestamp\n        if delta_x is not None:\n            params['deltaX'] = delta_x\n        if delta_y is not None:\n            params['deltaY'] = delta_y\n        if modifiers is not None:\n            params['modifiers'] = modifiers\n        if click_count is not None:\n            params['clickCount'] = click_count\n        return Command(method=InputMethod.EMULATE_TOUCH_FROM_MOUSE_EVENT, params=params)\n\n    @staticmethod\n    def ime_set_composition(\n        text: str,\n        selection_start: int,\n        selection_end: int,\n        replacement_start: Optional[int] = None,\n        replacement_end: Optional[int] = None,\n    ) -> ImeSetCompositionCommand:\n        \"\"\"\n        Generates a command to set the current candidate text for IME.\n\n        This experimental method sets the text for Input Method Editors (IME),\n        which are used for entering characters in languages that require more\n        keystrokes than the number of characters (like Chinese, Japanese, Korean).\n\n        Use imeCommitComposition to commit the final text.\n        Use imeSetComposition with empty string as text to cancel composition.\n\n        Args:\n            text: The text to insert as the IME composition.\n            selection_start: Start position of the selection within the composition text.\n            selection_end: End position of the selection within the composition text.\n            replacement_start: Start position of the text to be replaced\n                (default: same as selection_start).\n            replacement_end: End position of the text to be replaced\n                (default: same as selection_end).\n\n        Returns:\n            Command: The CDP command to set IME composition.\n        \"\"\"\n        params = ImeSetCompositionParams(\n            text=text,\n            selectionStart=selection_start,\n            selectionEnd=selection_end,\n        )\n        if replacement_start is not None:\n            params['replacementStart'] = replacement_start\n        if replacement_end is not None:\n            params['replacementEnd'] = replacement_end\n        return Command(method=InputMethod.IME_SET_COMPOSITION, params=params)\n\n    @staticmethod\n    def insert_text(\n        text: str,\n    ) -> InsertTextCommand:\n        \"\"\"\n        Generates a command to emulate inserting text that doesn't come from a key press.\n\n        This experimental method is useful for inserting text that would normally\n        come from sources other than keyboard, such as emoji pickers, IMEs, or\n        clipboard pastes.\n\n        Args:\n            text: The text to insert.\n\n        Returns:\n            Command: The CDP command to insert text.\n        \"\"\"\n        params = InsertTextParams(text=text)\n        return Command(method=InputMethod.INSERT_TEXT, params=params)\n\n    @staticmethod\n    def set_intercept_drags(enabled: bool) -> SetInterceptDragsCommand:\n        \"\"\"\n        Generates a command to control interception of drag and drop events.\n\n        This experimental method prevents default drag and drop behavior and instead\n        emits Input.dragIntercepted events. Drag and drop behavior can then be\n        directly controlled via Input.dispatchDragEvent.\n\n        This is useful for implementing custom drag and drop logic or for testing\n        drag and drop behavior in automated tests.\n\n        Args:\n            enabled: If true, drag events will be intercepted and reported as\n                    dragIntercepted events, preventing the default behavior.\n\n        Returns:\n            Command: The CDP command to set drag interception.\n        \"\"\"\n        params = SetInterceptDragsParams(enabled=enabled)\n        return Command(method=InputMethod.SET_INTERCEPT_DRAGS, params=params)\n\n    @staticmethod\n    def synthesize_pinch_gesture(\n        x: int,\n        y: int,\n        scale_factor: float,\n        relative_speed: Optional[int] = None,\n        gesture_source_type: Optional[GestureSourceType] = None,\n    ) -> SynthesizePinchGestureCommand:\n        \"\"\"\n        Generates a command to synthesize a pinch gesture over a time period.\n\n        This experimental method creates a synthetic pinch gesture (zoom in/out)\n        by issuing appropriate touch events over time. This is useful for testing\n        pinch-to-zoom functionality in web applications.\n\n        Args:\n            x: X coordinate of the start of the gesture in CSS pixels.\n            y: Y coordinate of the start of the gesture in CSS pixels.\n            scale_factor: Relative scale factor after zooming:\n                        - >1.0 zooms in (fingers moving apart)\n                        - <1.0 zooms out (fingers moving together)\n            relative_speed: Relative pointer speed in pixels per second (default: 800).\n                          Controls how fast the gesture happens.\n            gesture_source_type: Which type of input events to be generated:\n                              - 'default': Platform's preferred input type\n                              - 'touch': Touch input\n                              - 'mouse': Mouse input\n\n        Returns:\n            Command: The CDP command to synthesize a pinch gesture.\n        \"\"\"\n        params = SynthesizePinchGestureParams(x=x, y=y, scaleFactor=scale_factor)\n        if relative_speed is not None:\n            params['relativeSpeed'] = relative_speed\n        if gesture_source_type is not None:\n            params['gestureSourceType'] = gesture_source_type\n        return Command(method=InputMethod.SYNTHESIZE_PINCH_GESTURE, params=params)\n\n    @staticmethod\n    def synthesize_scroll_gesture(\n        x: int,\n        y: int,\n        x_distance: Optional[float] = None,\n        y_distance: Optional[float] = None,\n        x_overscroll: Optional[float] = None,\n        y_overscroll: Optional[float] = None,\n        prevent_fling: Optional[bool] = None,\n        speed: Optional[int] = None,\n        gesture_source_type: Optional[GestureSourceType] = None,\n        repeat_count: Optional[int] = None,\n        repeat_delay_ms: Optional[int] = None,\n        interaction_marker_name: Optional[str] = None,\n    ) -> SynthesizeScrollGestureCommand:\n        \"\"\"\n        Generates a command to synthesize a scroll gesture over a time period.\n\n        This experimental method creates a synthetic scroll gesture by issuing\n        appropriate touch events over time. This is useful for testing scrolling\n        behavior in web applications.\n\n        Args:\n            x: X coordinate of the start of the gesture in CSS pixels.\n            y: Y coordinate of the start of the gesture in CSS pixels.\n            x_distance: The distance to scroll along the X axis (positive to scroll left).\n            y_distance: The distance to scroll along the Y axis (positive to scroll up).\n            x_overscroll: The number of additional pixels to scroll back along the X axis,\n                        in addition to the given distance. This creates an overscroll\n                        effect (rubber-banding).\n            y_overscroll: The number of additional pixels to scroll back along the Y axis,\n                        in addition to the given distance. This creates an overscroll\n                        effect (rubber-banding).\n            prevent_fling: Prevent fling (default: true). If false, a fling animation might\n                         continue after the gesture.\n            speed: Swipe speed in pixels per second (default: 800).\n            gesture_source_type: Which type of input events to be generated:\n                              - 'default': Platform's preferred input type\n                              - 'touch': Touch input\n                              - 'mouse': Mouse input\n            repeat_count: The number of times to repeat the gesture (default: 0).\n            repeat_delay_ms: The number of milliseconds delay between each repeat (default: 250).\n            interaction_marker_name: The name of the interaction markers to generate, if not empty.\n                                  Used for tracking gesture timing in performance measurements.\n\n        Returns:\n            Command: The CDP command to synthesize a scroll gesture.\n        \"\"\"\n        params = SynthesizeScrollGestureParams(x=x, y=y)\n        if x_distance is not None:\n            params['xDistance'] = x_distance\n        if y_distance is not None:\n            params['yDistance'] = y_distance\n        if x_overscroll is not None:\n            params['xOverscroll'] = x_overscroll\n        if y_overscroll is not None:\n            params['yOverscroll'] = y_overscroll\n        if prevent_fling is not None:\n            params['preventFling'] = prevent_fling\n        if speed is not None:\n            params['speed'] = speed\n        if gesture_source_type is not None:\n            params['gestureSourceType'] = gesture_source_type\n        if repeat_count is not None:\n            params['repeatCount'] = repeat_count\n        if repeat_delay_ms is not None:\n            params['repeatDelayMs'] = repeat_delay_ms\n        if interaction_marker_name is not None:\n            params['interactionMarkerName'] = interaction_marker_name\n        return Command(method=InputMethod.SYNTHESIZE_SCROLL_GESTURE, params=params)\n\n    @staticmethod\n    def synthesize_tap_gesture(\n        x: int,\n        y: int,\n        duration: Optional[int] = None,\n        tap_count: Optional[int] = None,\n        gesture_source_type: Optional[GestureSourceType] = None,\n    ) -> SynthesizeTapGestureCommand:\n        \"\"\"\n        Generates a command to synthesize a tap gesture over a time period.\n\n        This experimental method creates a synthetic tap gesture by issuing\n        appropriate touch events over time. This is useful for testing\n        touch interaction in web applications.\n\n        Args:\n            x: X coordinate of the start of the gesture in CSS pixels.\n            y: Y coordinate of the start of the gesture in CSS pixels.\n            duration: Duration between touchdown and touchup events in milliseconds (default: 50).\n                     Controls how long the tap gesture takes.\n            tap_count: Number of times to perform the tap (e.g., 2 for a double tap, default: 1).\n            gesture_source_type: Which type of input events to be generated:\n                              - 'default': Platform's preferred input type\n                              - 'touch': Touch input\n                              - 'mouse': Mouse input\n\n        Returns:\n            Command: The CDP command to synthesize a tap gesture.\n        \"\"\"\n        params = SynthesizeTapGestureParams(x=x, y=y)\n        if duration is not None:\n            params['duration'] = duration\n        if tap_count is not None:\n            params['tapCount'] = tap_count\n        if gesture_source_type is not None:\n            params['gestureSourceType'] = gesture_source_type\n        return Command(method=InputMethod.SYNTHESIZE_TAP_GESTURE, params=params)\n"
  },
  {
    "path": "pydoll/commands/network_commands.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.protocol.base import Command\nfrom pydoll.protocol.network.methods import (\n    DeleteCookiesParams,\n    EmulateNetworkConditionsParams,\n    EnableReportingApiParams,\n    GetCertificateParams,\n    GetCookiesParams,\n    GetRequestPostDataParams,\n    GetResponseBodyForInterceptionParams,\n    GetResponseBodyParams,\n    GetSecurityIsolationStatusParams,\n    LoadNetworkResourceParams,\n    NetworkEnableParams,\n    NetworkMethod,\n    ReplayXHRParams,\n    SearchInResponseBodyParams,\n    SetAcceptedEncodingsParams,\n    SetAttachDebugStackParams,\n    SetBlockedURLsParams,\n    SetBypassServiceWorkerParams,\n    SetCacheDisabledParams,\n    SetCookieControlsParams,\n    SetCookieParams,\n    SetCookiesParams,\n    SetExtraHTTPHeadersParams,\n    SetUserAgentOverrideParams,\n    StreamResourceContentParams,\n    TakeResponseBodyForInterceptionAsStreamParams,\n)\n\nif TYPE_CHECKING:\n    from pydoll.protocol.emulation.types import UserAgentMetadata\n    from pydoll.protocol.network.methods import (\n        ClearAcceptedEncodingsOverrideCommand,\n        ClearBrowserCacheCommand,\n        ClearBrowserCookiesCommand,\n        ClearCookiesCommand,\n        DisableCommand,\n        EmulateNetworkConditionsCommand,\n        EnableCommand,\n        EnableReportingApiCommand,\n        GetCertificateCommand,\n        GetCookiesCommand,\n        GetRequestPostDataCommand,\n        GetResponseBodyCommand,\n        GetResponseBodyForInterceptionCommand,\n        GetSecurityIsolationStatusCommand,\n        HeaderEntry,\n        LoadNetworkResourceCommand,\n        ReplayXHRCommand,\n        SearchInResponseBodyCommand,\n        SetAcceptedEncodingsCommand,\n        SetAttachDebugStackCommand,\n        SetBlockedURLsCommand,\n        SetBypassServiceWorkerCommand,\n        SetCacheDisabledCommand,\n        SetCookieCommand,\n        SetCookieControlsCommand,\n        SetCookiesCommand,\n        SetExtraHTTPHeadersCommand,\n        SetUserAgentOverrideCommand,\n        StreamResourceContentCommand,\n        TakeResponseBodyForInterceptionAsStreamCommand,\n    )\n    from pydoll.protocol.network.types import (\n        ConnectionType,\n        ContentEncoding,\n        CookiePartitionKey,\n        CookiePriority,\n        CookieSameSite,\n        CookieSourceScheme,\n        LoadNetworkResourceOptions,\n    )\n\n\nclass NetworkCommands:\n    \"\"\"\n    Implementation of Chrome DevTools Protocol for the Network domain.\n\n    This class provides commands for monitoring and manipulating network activities,\n    enabling detailed inspection and control over HTTP requests and responses.\n    The Network domain exposes comprehensive network-related information including:\n    - Request/response headers and bodies\n    - Resource timing and caching behavior\n    - Cookie management and security details\n    - Network conditions emulation\n    - Traffic interception and modification\n\n    The commands allow developers to analyze performance, debug network issues,\n    and test application behavior under various network conditions.\n    \"\"\"\n\n    @staticmethod\n    def clear_browser_cache() -> ClearBrowserCacheCommand:\n        \"\"\"\n        Clears browser cache storage.\n\n        This command is essential for testing cache behavior and ensuring fresh\n        resource loading. It affects all cached resources including:\n        - CSS/JavaScript files\n        - Images and media assets\n        - API response caching\n\n        Use cases:\n        - Testing cache invalidation strategies\n        - Reproducing issues with stale content\n        - Performance benchmarking without cache influence\n\n        Returns:\n            Command: CDP command to clear the entire browser cache\n        \"\"\"\n        return Command(method=NetworkMethod.CLEAR_BROWSER_CACHE)\n\n    @staticmethod\n    def clear_browser_cookies() -> ClearBrowserCookiesCommand:\n        \"\"\"\n        Command to clear all cookies stored in the browser.\n\n        This can be beneficial for testing scenarios where you need\n        to simulate a fresh user session without any previously stored\n        cookies that might affect the application's behavior.\n\n        Returns:\n            Command[Response]: A command to clear all cookies in the browser.\n        \"\"\"\n        return Command(method=NetworkMethod.CLEAR_BROWSER_COOKIES)\n\n    @staticmethod\n    def delete_cookies(\n        name: str,\n        url: Optional[str] = None,\n        domain: Optional[str] = None,\n        path: Optional[str] = None,\n        partition_key: Optional[CookiePartitionKey] = None,\n    ) -> ClearCookiesCommand:\n        \"\"\"\n        Deletes browser cookies with matching criteria.\n\n        Provides granular control over cookie removal through multiple parameters:\n        - Delete by name only (affects all matching cookies)\n        - Scope deletion using URL, domain, or path\n        - Handle partitioned cookies for privacy-aware applications\n\n        Args:\n            name: Name of the cookies to remove (required)\n            url: Delete cookies for specific URL (domain/path must match)\n            domain: Exact domain for cookie deletion\n            path: Exact path for cookie deletion\n            partition_key: Partition key attributes for cookie isolation\n\n        Returns:\n            Command: CDP command to execute selective cookie deletion\n        \"\"\"\n        params = DeleteCookiesParams(name=name)\n        if url is not None:\n            params['url'] = url\n        if domain is not None:\n            params['domain'] = domain\n        if path is not None:\n            params['path'] = path\n        if partition_key is not None:\n            params['partitionKey'] = partition_key\n        return Command(method=NetworkMethod.DELETE_COOKIES, params=params)\n\n    @staticmethod\n    def disable() -> DisableCommand:\n        \"\"\"\n        Stops network monitoring and event reporting.\n\n        Preserves network state but stops:\n        - Request/response events\n        - WebSocket message tracking\n        - Loading progress notifications\n\n        Use when:\n        - Reducing overhead during non-network operations\n        - Pausing monitoring temporarily\n        - Finalizing network-related tests\n\n        Returns:\n            Command: CDP command to disable network monitoring\n        \"\"\"\n        return Command(method=NetworkMethod.DISABLE)\n\n    @staticmethod\n    def enable(\n        max_total_buffer_size: Optional[int] = None,\n        max_resource_buffer_size: Optional[int] = None,\n        max_post_data_size: Optional[int] = None,\n    ) -> EnableCommand:\n        \"\"\"\n        Enables network monitoring with configurable buffers.\n\n        Args:\n            max_total_buffer_size: Total memory buffer for network data (bytes)\n            max_resource_buffer_size: Per-resource buffer limit (bytes)\n            max_post_data_size: Maximum POST payload to capture (bytes)\n\n        Recommended settings:\n        - Increase buffers for long-running sessions\n        - Adjust post size for API testing\n        - Monitor memory usage with large buffers\n\n        Returns:\n            Command: CDP command to enable network monitoring\n        \"\"\"\n        params = NetworkEnableParams()\n        if max_total_buffer_size is not None:\n            params['maxTotalBufferSize'] = max_total_buffer_size\n        if max_resource_buffer_size is not None:\n            params['maxResourceBufferSize'] = max_resource_buffer_size\n        if max_post_data_size is not None:\n            params['maxPostDataSize'] = max_post_data_size\n        return Command(method=NetworkMethod.ENABLE, params=params)\n\n    @staticmethod\n    def get_cookies(\n        urls: Optional[list[str]] = None,\n    ) -> GetCookiesCommand:\n        \"\"\"\n        Retrieves cookies matching specified URLs.\n\n        Args:\n            urls: list of URLs to scope cookie retrieval\n\n        Returns:\n            Command: CDP command returning cookie details including:\n                - Name, value, and attributes\n                - Security and scope parameters\n                - Expiration and size information\n\n        Usage notes:\n        - Empty URL list returns all cookies\n        - Includes HTTP-only and secure cookies\n        - Shows partitioned cookie status\n        \"\"\"\n        params = GetCookiesParams()\n        if urls is not None:\n            params['urls'] = urls\n        return Command(method=NetworkMethod.GET_COOKIES, params=params)\n\n    @staticmethod\n    def get_request_post_data(\n        request_id: str,\n    ) -> GetRequestPostDataCommand:\n        \"\"\"\n        Retrieves POST data from a specific network request.\n\n        Essential for:\n        - Form submission analysis\n        - API request debugging\n        - File upload monitoring\n        - Security testing\n\n        Args:\n            request_id: Unique identifier for the network request\n\n        Returns:\n            Command: CDP command that returns:\n                - Raw POST data content\n                - Multipart form data (excluding file contents)\n                - Content encoding information\n\n        Note: Large POST bodies may be truncated based on buffer settings\n        \"\"\"\n        params = GetRequestPostDataParams(requestId=request_id)\n        return Command(method=NetworkMethod.GET_REQUEST_POST_DATA, params=params)\n\n    @staticmethod\n    def get_response_body(\n        request_id: str,\n    ) -> GetResponseBodyCommand:\n        \"\"\"\n        Retrieves the full content of a network response.\n\n        Supports various content types:\n        - Text-based resources (HTML, CSS, JSON)\n        - Base64-encoded binary content (images, media)\n        - Gzip/deflate compressed responses\n\n        Args:\n            request_id: Unique network request identifier\n\n        Important considerations:\n        - Response must be available in browser memory\n        - Large responses may require streaming approaches\n        - Sensitive data should be handled securely\n\n        Returns:\n            Command: CDP command returning response body and encoding details\n        \"\"\"\n        params = GetResponseBodyParams(requestId=request_id)\n        return Command(method=NetworkMethod.GET_RESPONSE_BODY, params=params)\n\n    @staticmethod\n    def set_cache_disabled(cache_disabled: bool) -> SetCacheDisabledCommand:\n        \"\"\"\n        Controls browser's cache mechanism.\n\n        Use cases:\n        - Testing resource update behavior\n        - Forcing fresh content loading\n        - Performance impact analysis\n        - Cache-busting scenarios\n\n        Args:\n            cache_disabled: True to disable caching, False to enable\n\n        Returns:\n            Command: CDP command to modify cache behavior\n\n        Note: Affects all requests until re-enabled\n        \"\"\"\n        params = SetCacheDisabledParams(cacheDisabled=cache_disabled)\n        return Command(method=NetworkMethod.SET_CACHE_DISABLED, params=params)\n\n    @staticmethod\n    def set_cookie(\n        name: str,\n        value: str,\n        url: Optional[str] = None,\n        domain: Optional[str] = None,\n        path: Optional[str] = None,\n        secure: Optional[bool] = None,\n        http_only: Optional[bool] = None,\n        same_site: Optional[CookieSameSite] = None,\n        expires: Optional[float] = None,\n        priority: Optional[CookiePriority] = None,\n        same_party: Optional[bool] = None,\n        source_scheme: Optional[CookieSourceScheme] = None,\n        source_port: Optional[int] = None,\n        partition_key: Optional[CookiePartitionKey] = None,\n    ) -> SetCookieCommand:\n        \"\"\"\n        Creates or updates a cookie with specified attributes.\n\n        Comprehensive cookie control supporting:\n        - Session and persistent cookies\n        - Security attributes (Secure, HttpOnly)\n        - SameSite policies\n        - Cookie partitioning\n        - Priority levels\n\n        Args:\n            name: Cookie name\n            value: Cookie value\n            url: Target URL for the cookie\n            domain: Cookie domain scope\n            path: Cookie path scope\n            secure: Require HTTPS\n            http_only: Prevent JavaScript access\n            same_site: Cross-site access policy\n            expires: Expiration timestamp\n            priority: Cookie priority level\n            same_party: First-Party Sets flag\n            source_scheme: Cookie source context\n            source_port: Source port restriction\n            partition_key: Storage partition key\n\n        Returns:\n            Command: CDP command that returns success status\n\n        Security considerations:\n        - Use secure flag for sensitive data\n        - Consider SameSite policies\n        - Be aware of cross-site implications\n        \"\"\"\n        params = SetCookieParams(name=name, value=value)\n\n        if url is not None:\n            params['url'] = url\n        if domain is not None:\n            params['domain'] = domain\n        if path is not None:\n            params['path'] = path\n        if secure is not None:\n            params['secure'] = secure\n        if http_only is not None:\n            params['httpOnly'] = http_only\n        if same_site is not None:\n            params['sameSite'] = same_site\n        if expires is not None:\n            params['expires'] = expires\n        if priority is not None:\n            params['priority'] = priority\n        if same_party is not None:\n            params['sameParty'] = same_party\n        if source_scheme is not None:\n            params['sourceScheme'] = source_scheme\n        if source_port is not None:\n            params['sourcePort'] = source_port\n        if partition_key is not None:\n            params['partitionKey'] = partition_key\n\n        return Command(method=NetworkMethod.SET_COOKIE, params=params)\n\n    @staticmethod\n    def set_cookies(cookies: list[SetCookieParams]) -> SetCookiesCommand:\n        \"\"\"\n        Sets multiple cookies in a single operation.\n\n        Efficient for:\n        - Batch cookie operations\n        - Session state restoration\n        - Testing multiple authentication states\n        - Cross-domain cookie setup\n\n        Args:\n            cookies: list of cookie parameters including\n                    name, value, and attributes\n\n        Returns:\n            Command: CDP command for bulk cookie setting\n\n        Performance note:\n        - More efficient than multiple set_cookie calls\n        - Consider memory impact with large batches\n        \"\"\"\n        params = SetCookiesParams(cookies=cookies)\n        return Command(method=NetworkMethod.SET_COOKIES, params=params)\n\n    @staticmethod\n    def set_extra_http_headers(\n        headers: list[HeaderEntry],\n    ) -> SetExtraHTTPHeadersCommand:\n        \"\"\"\n        Applies custom HTTP headers to all subsequent requests.\n\n        Enables advanced scenarios:\n        - A/B testing with custom headers\n        - Authentication bypass for testing\n        - Content negotiation simulations\n        - Security header validation\n\n        Args:\n            headers: list of key-value header pairs\n\n        Security notes:\n        - Headers are applied browser-wide\n        - Sensitive headers (e.g., Authorization) persist until cleared\n        - Use with caution in shared environments\n\n        Returns:\n            Command: CDP command to set global HTTP headers\n        \"\"\"\n        params = SetExtraHTTPHeadersParams(headers=headers)\n        return Command(method=NetworkMethod.SET_EXTRA_HTTP_HEADERS, params=params)\n\n    @staticmethod\n    def set_useragent_override(\n        user_agent: str,\n        accept_language: Optional[str] = None,\n        platform: Optional[str] = None,\n        user_agent_metadata: Optional[UserAgentMetadata] = None,\n    ) -> SetUserAgentOverrideCommand:\n        \"\"\"\n        Overrides the browser's User-Agent string.\n\n        Use cases:\n        - Device/browser simulation\n        - Compatibility testing\n        - Content negotiation\n        - Bot detection bypass\n\n        Args:\n            user_agent: Complete User-Agent string\n            accept_language: Language preference header\n            platform: Platform identifier\n            user_agent_metadata: Detailed UA metadata\n\n        Returns:\n            Command: CDP command to override user agent\n\n        Testing considerations:\n        - Affects all subsequent requests\n        - May impact server-side behavior\n        - Consider mobile/desktop differences\n        \"\"\"\n        params = SetUserAgentOverrideParams(userAgent=user_agent)\n        if accept_language is not None:\n            params['acceptLanguage'] = accept_language\n        if platform is not None:\n            params['platform'] = platform\n        if user_agent_metadata is not None:\n            params['userAgentMetadata'] = user_agent_metadata\n        return Command(method=NetworkMethod.SET_USER_AGENT_OVERRIDE, params=params)\n\n    @staticmethod\n    def clear_accepted_encodings_override() -> ClearAcceptedEncodingsOverrideCommand:\n        \"\"\"\n        Restores default content encoding acceptance.\n\n        Effects:\n        - Resets compression preferences\n        - Restores default Accept-Encoding header\n        - Allows server-chosen encoding\n\n        Use when:\n        - Testing encoding fallbacks\n        - Debugging compression issues\n        - Resetting after encoding tests\n\n        Returns:\n            Command: CDP command to clear encoding overrides\n        \"\"\"\n        return Command(method=NetworkMethod.CLEAR_ACCEPTED_ENCODINGS_OVERRIDE)\n\n    @staticmethod\n    def enable_reporting_api(\n        enabled: bool,\n    ) -> EnableReportingApiCommand:\n        \"\"\"\n        Controls the Reporting API functionality.\n\n        Features:\n        - Network error reporting\n        - Deprecation notices\n        - CSP violation reports\n        - CORS issues\n\n        Args:\n            enabled: True to enable, False to disable\n\n        Returns:\n            Command: CDP command to configure Reporting API\n\n        Note: Requires browser support for Reporting API\n        \"\"\"\n        params = EnableReportingApiParams(enabled=enabled)\n        return Command(method=NetworkMethod.ENABLE_REPORTING_API, params=params)\n\n    @staticmethod\n    def search_in_response_body(\n        request_id: str,\n        query: str,\n        case_sensitive: bool = False,\n        is_regex: bool = False,\n    ) -> SearchInResponseBodyCommand:\n        \"\"\"\n        Searches for content within response bodies.\n\n        Powerful for:\n        - Content verification\n        - Security scanning\n        - Data extraction\n        - Response validation\n\n        Args:\n            request_id: Target response identifier\n            query: Search string or pattern\n            case_sensitive: Match case sensitivity\n            is_regex: Use regular expression matching\n\n        Returns:\n            Command: CDP command returning match results\n\n        Performance tip:\n        - Use specific queries for large responses\n        - Consider regex complexity\n        \"\"\"\n        params = SearchInResponseBodyParams(requestId=request_id, query=query)\n        if case_sensitive is not None:\n            params['caseSensitive'] = case_sensitive\n        if is_regex is not None:\n            params['isRegex'] = is_regex\n        return Command(method=NetworkMethod.SEARCH_IN_RESPONSE_BODY, params=params)\n\n    @staticmethod\n    def set_blocked_urls(urls: list[str]) -> SetBlockedURLsCommand:\n        \"\"\"\n        Blocks specified URLs from loading.\n\n        Key features:\n        - Pattern-based URL blocking\n        - Resource type filtering\n        - Network request prevention\n        - Error simulation\n\n        Args:\n            urls: list of URL patterns to block\n                 Supports wildcards and pattern matching\n\n        Returns:\n            Command: CDP command to set URL blocking rules\n\n        Common applications:\n        - Ad/tracker blocking simulation\n        - Resource loading control\n        - Error handling testing\n        - Network isolation testing\n        \"\"\"\n        params = SetBlockedURLsParams(urls=urls)\n        return Command(method=NetworkMethod.SET_BLOCKED_URLS, params=params)\n\n    @staticmethod\n    def set_bypass_service_worker(\n        bypass: bool,\n    ) -> SetBypassServiceWorkerCommand:\n        \"\"\"\n        Controls Service Worker interception of network requests.\n\n        Use cases:\n        - Testing direct network behavior\n        - Bypassing offline functionality\n        - Debug caching issues\n        - Performance comparison\n\n        Args:\n            bypass: True to skip Service Worker, False to allow\n\n        Returns:\n            Command: CDP command to configure Service Worker behavior\n\n        Impact:\n        - Affects offline capabilities\n        - Changes caching behavior\n        - Modifies push notifications\n        \"\"\"\n        params = SetBypassServiceWorkerParams(bypass=bypass)\n        return Command(method=NetworkMethod.SET_BYPASS_SERVICE_WORKER, params=params)\n\n    @staticmethod\n    def get_certificate(origin: str) -> GetCertificateCommand:\n        \"\"\"\n        Retrieves SSL/TLS certificate information for a domain.\n\n        Provides:\n        - Certificate chain details\n        - Validation status\n        - Expiration information\n        - Issuer details\n\n        Args:\n            origin: Target domain for certificate inspection\n\n        Returns:\n            Command: CDP command returning certificate data\n\n        Security applications:\n        - Certificate validation\n        - SSL/TLS verification\n        - Security assessment\n        - Chain of trust verification\n        \"\"\"\n        params = GetCertificateParams(origin=origin)\n        return Command(method=NetworkMethod.GET_CERTIFICATE, params=params)\n\n    @staticmethod\n    def get_response_body_for_interception(\n        interception_id: str,\n    ) -> GetResponseBodyForInterceptionCommand:\n        \"\"\"\n        Retrieves response body from an intercepted request.\n\n        Essential for:\n        - Response modification\n        - Content inspection\n        - Security testing\n        - API response validation\n\n        Args:\n            interception_id: Identifier for intercepted request\n\n        Returns:\n            Command: CDP command providing intercepted response content\n\n        Note:\n        - Must be used with interception enabled\n        - Supports streaming responses\n        - Handles various content types\n        \"\"\"\n        params = GetResponseBodyForInterceptionParams(interceptionId=interception_id)\n        return Command(method=NetworkMethod.GET_RESPONSE_BODY_FOR_INTERCEPTION, params=params)\n\n    @staticmethod\n    def set_accepted_encodings(\n        encodings: list[ContentEncoding],\n    ) -> SetAcceptedEncodingsCommand:\n        \"\"\"\n        Specifies accepted content encodings for requests.\n\n        Controls:\n        - Compression algorithms\n        - Transfer encoding\n        - Content optimization\n\n        Args:\n            encodings: list of accepted encoding methods\n                     (gzip, deflate, br, etc.)\n\n        Returns:\n            Command: CDP command to set encoding preferences\n\n        Performance implications:\n        - Affects bandwidth usage\n        - Impacts response time\n        - Changes server behavior\n        \"\"\"\n        params = SetAcceptedEncodingsParams(encodings=encodings)\n        return Command(method=NetworkMethod.SET_ACCEPTED_ENCODINGS, params=params)\n\n    @staticmethod\n    def set_attach_debug_stack(enabled: bool) -> SetAttachDebugStackCommand:\n        \"\"\"\n        Enables/disables debug stack attachment to requests.\n\n        Debug features:\n        - Stack trace collection\n        - Request origin tracking\n        - Initialization context\n        - Call site identification\n\n        Args:\n            enabled: True to attach debug info, False to disable\n\n        Returns:\n            Command: CDP command to configure debug stack attachment\n\n        Performance note:\n        - May impact performance when enabled\n        - Useful for development/debugging\n        - Consider memory usage\n        \"\"\"\n        params = SetAttachDebugStackParams(enabled=enabled)\n        return Command(method=NetworkMethod.SET_ATTACH_DEBUG_STACK, params=params)\n\n    @staticmethod\n    def set_cookie_controls(\n        enable_third_party_cookie_restriction: bool,\n        disable_third_party_cookie_metadata: Optional[bool] = None,\n        disable_third_party_cookie_heuristics: Optional[bool] = None,\n    ) -> SetCookieControlsCommand:\n        \"\"\"\n        Configures third-party cookie handling policies.\n\n        Privacy features:\n        - Cookie access control\n        - Third-party restrictions\n        - Tracking prevention\n        - Privacy policy enforcement\n\n        Args:\n            enable_third_party_cookie_restriction: Enable restrictions\n            disable_third_party_cookie_metadata: Skip metadata checks\n            disable_third_party_cookie_heuristics: Disable detection logic\n\n        Returns:\n            Command: CDP command to set cookie control policies\n\n        Security implications:\n        - Affects cross-site tracking\n        - Changes authentication behavior\n        - Impacts embedded content\n        \"\"\"\n        params = SetCookieControlsParams(\n            enableThirdPartyCookieRestriction=enable_third_party_cookie_restriction\n        )\n        if disable_third_party_cookie_metadata is not None:\n            params['disableThirdPartyCookieMetadata'] = disable_third_party_cookie_metadata\n        if disable_third_party_cookie_heuristics is not None:\n            params['disableThirdPartyCookieHeuristics'] = disable_third_party_cookie_heuristics\n        return Command(method=NetworkMethod.SET_COOKIE_CONTROLS, params=params)\n\n    @staticmethod\n    def stream_resource_content(\n        request_id: str,\n    ) -> StreamResourceContentCommand:\n        \"\"\"\n        Enables streaming of response content.\n\n        Useful for:\n        - Large file downloads\n        - Progressive loading\n        - Memory optimization\n        - Real-time processing\n\n        Args:\n            request_id: Target request identifier\n\n        Returns:\n            Command: CDP command to initiate content streaming\n\n        Best practices:\n        - Monitor memory usage\n        - Handle stream chunks efficiently\n        - Consider error recovery\n        \"\"\"\n        params = StreamResourceContentParams(requestId=request_id)\n        return Command(method=NetworkMethod.STREAM_RESOURCE_CONTENT, params=params)\n\n    @staticmethod\n    def take_response_body_for_interception_as_stream(\n        interception_id: str,\n    ) -> TakeResponseBodyForInterceptionAsStreamCommand:\n        \"\"\"\n        Creates a stream for intercepted response body.\n\n        Applications:\n        - Large response handling\n        - Content modification\n        - Bandwidth optimization\n        - Progressive processing\n\n        Args:\n            interception_id: Intercepted response identifier\n\n        Returns:\n            Command: CDP command returning stream handle\n\n        Stream handling:\n        - Supports chunked transfer\n        - Manages memory efficiently\n        - Enables real-time processing\n        \"\"\"\n        params = TakeResponseBodyForInterceptionAsStreamParams(interceptionId=interception_id)\n        return Command(\n            method=NetworkMethod.TAKE_RESPONSE_BODY_FOR_INTERCEPTION_AS_STREAM,\n            params=params,\n        )\n\n    @staticmethod\n    def emulate_network_conditions(\n        offline: bool,\n        latency: float,\n        download_throughput: float,\n        upload_throughput: float,\n        connection_type: Optional[ConnectionType] = None,\n        packet_loss: Optional[float] = None,\n        packet_queue_length: Optional[int] = None,\n        packet_reordering: Optional[bool] = None,\n    ) -> EmulateNetworkConditionsCommand:\n        \"\"\"\n        Emulates custom network conditions for realistic testing scenarios.\n\n        Simulates various network profiles including:\n        - Offline mode\n        - High-latency connections\n        - Bandwidth throttling\n        - Unreliable network characteristics\n\n        Args:\n            offline: Simulate complete network disconnection\n            latency: Minimum latency in milliseconds (round-trip time)\n            download_throughput: Max download speed (bytes/sec, -1 to disable)\n            upload_throughput: Max upload speed (bytes/sec, -1 to disable)\n            connection_type: Network connection type (cellular, wifi, etc.)\n            packet_loss: Simulated packet loss percentage (0-100)\n            packet_queue_length: Network buffer size simulation\n            packet_reordering: Enable packet order randomization\n\n        Typical use cases:\n        - Testing progressive loading states\n        - Validating offline-first functionality\n        - Performance optimization under constrained networks\n\n        Returns:\n            Command: CDP command to activate network emulation\n        \"\"\"\n        params = EmulateNetworkConditionsParams(\n            offline=offline,\n            latency=latency,\n            downloadThroughput=download_throughput,\n            uploadThroughput=upload_throughput,\n        )\n        if connection_type is not None:\n            params['connectionType'] = connection_type\n        if packet_loss is not None:\n            params['packetLoss'] = packet_loss\n        if packet_queue_length is not None:\n            params['packetQueueLength'] = packet_queue_length\n        if packet_reordering is not None:\n            params['packetReordering'] = packet_reordering\n        return Command(method=NetworkMethod.EMULATE_NETWORK_CONDITIONS, params=params)\n\n    @staticmethod\n    def get_security_isolation_status(\n        frame_id: Optional[str] = None,\n    ) -> GetSecurityIsolationStatusCommand:\n        \"\"\"\n        Retrieves security isolation information.\n\n        Provides:\n        - CORS status\n        - Cross-origin isolation\n        - Security context\n        - Frame isolation\n\n        Args:\n            frame_id: Optional frame to check\n\n        Returns:\n            Command: CDP command returning isolation status\n\n        Security aspects:\n        - Cross-origin policies\n        - Iframe security\n        - Site isolation\n        - Content protection\n        \"\"\"\n        params = GetSecurityIsolationStatusParams()\n        if frame_id is not None:\n            params['frameId'] = frame_id\n        return Command(method=NetworkMethod.GET_SECURITY_ISOLATION_STATUS, params=params)\n\n    @staticmethod\n    def load_network_resource(\n        url: str,\n        options: LoadNetworkResourceOptions,\n        frame_id: Optional[str] = None,\n    ) -> LoadNetworkResourceCommand:\n        \"\"\"\n        Loads a network resource with specific options.\n\n        Features:\n        - Custom request configuration\n        - Resource loading control\n        - Frame-specific loading\n        - Error handling\n\n        Args:\n            url: Resource URL to load\n            options: Loading configuration\n            frame_id: Target frame context\n\n        Returns:\n            Command: CDP command to load resource\n\n        Usage considerations:\n        - Respects CORS policies\n        - Handles authentication\n        - Manages redirects\n        - Supports streaming\n        \"\"\"\n        params = LoadNetworkResourceParams(url=url, options=options)\n        if frame_id is not None:\n            params['frameId'] = frame_id\n        return Command(method=NetworkMethod.LOAD_NETWORK_RESOURCE, params=params)\n\n    @staticmethod\n    def replay_xhr(\n        request_id: str,\n    ) -> ReplayXHRCommand:\n        \"\"\"\n        Replays an XHR request.\n\n        Applications:\n        - Request debugging\n        - Response testing\n        - Race condition analysis\n        - API verification\n\n        Args:\n            request_id: XHR request to replay\n\n        Returns:\n            Command: CDP command to replay XHR\n\n        Note:\n        - Maintains original headers\n        - Preserves request body\n        - Updates timestamps\n        - Creates new request ID\n        \"\"\"\n        params = ReplayXHRParams(requestId=request_id)\n        return Command(method=NetworkMethod.REPLAY_XHR, params=params)\n"
  },
  {
    "path": "pydoll/commands/page_commands.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Literal, Optional\n\nfrom pydoll.protocol.base import Command\nfrom pydoll.protocol.page.methods import (\n    AddCompilationCacheParams,\n    AddScriptToEvaluateOnNewDocumentParams,\n    CaptureScreenshotParams,\n    CaptureSnapshotParams,\n    CreateIsolatedWorldParams,\n    EnableParams,\n    GenerateTestReportParams,\n    GetAdScriptAncestryIdsParams,\n    GetAppIdParams,\n    GetAppManifestParams,\n    GetOriginTrialsParams,\n    GetPermissionsPolicyStateParams,\n    GetResourceContentParams,\n    HandleJavaScriptDialogParams,\n    NavigateParams,\n    NavigateToHistoryEntryParams,\n    PageMethod,\n    PrintToPDFParams,\n    ProduceCompilationCacheParams,\n    ReloadParams,\n    RemoveScriptToEvaluateOnNewDocumentParams,\n    ScreencastFrameAckParams,\n    SearchInResourceParams,\n    SetAdBlockingEnabledParams,\n    SetBypassCSPParams,\n    SetDocumentContentParams,\n    SetFontFamiliesParams,\n    SetFontSizesParams,\n    SetInterceptFileChooserDialogParams,\n    SetLifecycleEventsEnabledParams,\n    SetPrerenderingAllowedParams,\n    SetRPHRegistrationModeParams,\n    SetSPCTransactionModeParams,\n    SetWebLifecycleStateParams,\n    StartScreencastParams,\n)\nfrom pydoll.protocol.page.types import (\n    CompilationCacheParams,\n    FontFamilies,\n    FontSizes,\n    ScriptFontFamilies,\n)\n\nif TYPE_CHECKING:\n    from pydoll.protocol.page.methods import (\n        AddCompilationCacheCommand,\n        AddScriptToEvaluateOnNewDocumentCommand,\n        BringToFrontCommand,\n        CaptureScreenshotCommand,\n        CaptureSnapshotCommand,\n        ClearCompilationCacheCommand,\n        CloseCommand,\n        CrashCommand,\n        CreateIsolatedWorldCommand,\n        DisableCommand,\n        EnableCommand,\n        GenerateTestReportCommand,\n        GetAdScriptAncestryIdsCommand,\n        GetAppIdCommand,\n        GetAppManifestCommand,\n        GetFrameTreeCommand,\n        GetInstallabilityErrorsCommand,\n        GetLayoutMetricsCommand,\n        GetNavigationHistoryCommand,\n        GetOriginTrialsCommand,\n        GetPermissionsPolicyStateCommand,\n        GetResourceContentCommand,\n        GetResourceTreeCommand,\n        HandleJavaScriptDialogCommand,\n        NavigateCommand,\n        NavigateToHistoryEntryCommand,\n        PrintToPDFCommand,\n        ProduceCompilationCacheCommand,\n        ReloadCommand,\n        RemoveScriptToEvaluateOnNewDocumentCommand,\n        ResetNavigationHistoryCommand,\n        ScreencastFrameAckCommand,\n        SearchInResourceCommand,\n        SetAdBlockingEnabledCommand,\n        SetBypassCSPCommand,\n        SetDocumentContentCommand,\n        SetFontFamiliesCommand,\n        SetFontSizesCommand,\n        SetInterceptFileChooserDialogCommand,\n        SetLifecycleEventsEnabledCommand,\n        SetPrerenderingAllowedCommand,\n        SetRPHRegistrationModeCommand,\n        SetSPCTransactionModeCommand,\n        SetWebLifecycleStateCommand,\n        StartScreencastCommand,\n        StopLoadingCommand,\n        StopScreencastCommand,\n        WaitForDebuggerCommand,\n    )\n    from pydoll.protocol.page.types import (\n        AutoResponseMode,\n        ReferrerPolicy,\n        ScreencastFormat,\n        ScreenshotFormat,\n        TransferMode,\n        TransitionType,\n        Viewport,\n        WebLifecycleState,\n    )\n\n\nclass PageCommands:\n    \"\"\"\n    This class encapsulates the page commands of the Chrome DevTools Protocol (CDP).\n\n    CDP's Page domain allows for interacting with browser pages, including navigation,\n    content manipulation, and page state monitoring. These commands provide powerful\n    capabilities for web automation, testing, and debugging.\n\n    The commands defined in this class provide functionality for:\n    - Navigating to URLs and managing page history\n    - Capturing screenshots and generating PDFs\n    - Handling JavaScript dialogs\n    - Enabling and controlling page events\n    - Managing download behavior\n    - Manipulating page content and state\n    \"\"\"\n\n    @staticmethod\n    def add_script_to_evaluate_on_new_document(\n        source: str,\n        world_name: Optional[str] = None,\n        include_command_line_api: Optional[bool] = None,\n        run_immediately: Optional[bool] = None,\n    ) -> AddScriptToEvaluateOnNewDocumentCommand:\n        \"\"\"\n        Creates a command to add a script that will be evaluated when a new document is created.\n\n        Args:\n            source (str): Script source to be evaluated when a new document is created.\n            world_name (Optional[str]): If specified, creates an isolated world with the given name.\n            include_command_line_api (Optional[bool]): Whether to include command line API.\n            run_immediately (Optional[bool]): Whether to run the script immediately on\n                existing contexts.\n\n        Returns:\n            AddScriptToEvaluateOnNewDocumentCommand: Command object with the identifier\n                of the added script.\n        \"\"\"\n        params = AddScriptToEvaluateOnNewDocumentParams(source=source)\n        if world_name is not None:\n            params['worldName'] = world_name\n        if include_command_line_api is not None:\n            params['includeCommandLineAPI'] = include_command_line_api\n        if run_immediately is not None:\n            params['runImmediately'] = run_immediately\n\n        return Command(method=PageMethod.ADD_SCRIPT_TO_EVALUATE_ON_NEW_DOCUMENT, params=params)\n\n    @staticmethod\n    def bring_to_front() -> BringToFrontCommand:\n        \"\"\"\n        Brings the page to front.\n        \"\"\"\n        return Command(method=PageMethod.BRING_TO_FRONT)\n\n    @staticmethod\n    def capture_screenshot(\n        format: Optional[ScreenshotFormat] = None,\n        quality: Optional[int] = None,\n        clip: Optional[Viewport] = None,\n        from_surface: Optional[bool] = None,\n        capture_beyond_viewport: Optional[bool] = None,\n        optimize_for_speed: Optional[bool] = None,\n    ) -> CaptureScreenshotCommand:\n        \"\"\"\n        Creates a command to capture a screenshot of the current page.\n\n        Args:\n            format (Optional[str]): Image compression format (jpeg, png, or webp).\n            quality (Optional[int]): Compression quality from 0-100 (jpeg only).\n            clip (Optional[Viewport]): Region of the page to capture.\n            from_surface (Optional[bool]): Capture from the surface, not the view.\n            capture_beyond_viewport (Optional[bool]): Capture beyond the viewport.\n            optimize_for_speed (Optional[bool]): Optimize for speed, not for size.\n\n        Returns:\n            CaptureScreenshotCommand: Command object with base64-encoded image data.\n        \"\"\"\n        params = CaptureScreenshotParams()\n        if format is not None:\n            params['format'] = format\n        if quality is not None:\n            params['quality'] = quality\n        if clip is not None:\n            params['clip'] = clip\n        if from_surface is not None:\n            params['fromSurface'] = from_surface\n        if capture_beyond_viewport is not None:\n            params['captureBeyondViewport'] = capture_beyond_viewport\n        if optimize_for_speed is not None:\n            params['optimizeForSpeed'] = optimize_for_speed\n\n        return Command(method=PageMethod.CAPTURE_SCREENSHOT, params=params)\n\n    @staticmethod\n    def close() -> CloseCommand:\n        \"\"\"\n        Creates a command to close the current page.\n\n        Returns:\n            CloseCommand: Command object to close the page.\n        \"\"\"\n        return Command(method=PageMethod.CLOSE)\n\n    @staticmethod\n    def create_isolated_world(\n        frame_id: str,\n        world_name: Optional[str] = None,\n        grant_universal_access: Optional[bool] = None,\n    ) -> CreateIsolatedWorldCommand:\n        \"\"\"\n        Creates a command to create an isolated world for the given frame.\n\n        Args:\n            frame_id (str): ID of the frame in which to create the isolated world.\n            world_name (Optional[str]): Name to be reported in the Execution Context.\n            grant_universal_access (Optional[bool]): Whether to grant universal access.\n\n        Returns:\n            CreateIsolatedWorldCommand: Command object with the execution context ID.\n        \"\"\"\n        params = CreateIsolatedWorldParams(frameId=frame_id)\n        if world_name is not None:\n            params['worldName'] = world_name\n        if grant_universal_access is not None:\n            params['grantUniveralAccess'] = grant_universal_access\n\n        return Command(method=PageMethod.CREATE_ISOLATED_WORLD, params=params)\n\n    @staticmethod\n    def disable() -> DisableCommand:\n        \"\"\"\n        Creates a command to disable page domain notifications.\n\n        Returns:\n            DisableCommand: Command object to disable the Page domain.\n        \"\"\"\n        return Command(method=PageMethod.DISABLE)\n\n    @staticmethod\n    def enable(\n        enable_file_chooser_opened_event: Optional[bool] = None,\n    ) -> EnableCommand:\n        \"\"\"\n        Creates a command to enable page domain notifications.\n\n        Args:\n            enable_file_chooser_opened_event (Optional[bool]): Whether to emit\n                Page.fileChooserOpened event.\n\n        Returns:\n            EnableCommand: Command object to enable the Page domain.\n        \"\"\"\n        params = EnableParams()\n        if enable_file_chooser_opened_event is not None:\n            params['enableFileChooserOpenedEvent'] = enable_file_chooser_opened_event\n\n        return Command(method=PageMethod.ENABLE, params=params)\n\n    @staticmethod\n    def get_app_manifest(\n        manifest_id: Optional[str] = None,\n    ) -> GetAppManifestCommand:\n        \"\"\"\n        Creates a command to get the manifest for the current document.\n\n        Returns:\n            GetAppManifestCommand: Command object with manifest information.\n        \"\"\"\n        params = GetAppManifestParams()\n        if manifest_id is not None:\n            params['manifestId'] = manifest_id\n        return Command(method=PageMethod.GET_APP_MANIFEST, params=params)\n\n    @staticmethod\n    def get_frame_tree() -> GetFrameTreeCommand:\n        \"\"\"\n        Creates a command to get the frame tree for the current page.\n\n        Returns:\n            GetFrameTreeCommand: Command object with frame tree information.\n        \"\"\"\n        return Command(method=PageMethod.GET_FRAME_TREE)\n\n    @staticmethod\n    def get_layout_metrics() -> GetLayoutMetricsCommand:\n        \"\"\"\n        Creates a command to get layout metrics for the page.\n\n        Returns:\n            GetLayoutMetricsCommand: Command object with layout metrics.\n        \"\"\"\n        return Command(method=PageMethod.GET_LAYOUT_METRICS)\n\n    @staticmethod\n    def get_navigation_history() -> GetNavigationHistoryCommand:\n        \"\"\"\n        Creates a command to get the navigation history for the current page.\n\n        Returns:\n            GetNavigationHistoryCommand: Command object with navigation history.\n        \"\"\"\n        return Command(method=PageMethod.GET_NAVIGATION_HISTORY)\n\n    @staticmethod\n    def handle_javascript_dialog(\n        accept: bool, prompt_text: Optional[str] = None\n    ) -> HandleJavaScriptDialogCommand:\n        \"\"\"\n        Creates a command to handle a JavaScript dialog.\n\n        Args:\n            accept (bool): Whether to accept or dismiss the dialog.\n            prompt_text (Optional[str]): Text to enter in prompt dialogs.\n\n        Returns:\n            HandleJavaScriptDialogCommand: Command object to handle a JavaScript dialog.\n        \"\"\"\n        params = HandleJavaScriptDialogParams(accept=accept)\n        if prompt_text is not None:\n            params['promptText'] = prompt_text\n\n        return Command(method=PageMethod.HANDLE_JAVASCRIPT_DIALOG, params=params)\n\n    @staticmethod\n    def navigate(\n        url: str,\n        referrer: Optional[str] = None,\n        transition_type: Optional[TransitionType] = None,\n        frame_id: Optional[str] = None,\n        referrer_policy: Optional[ReferrerPolicy] = None,\n    ) -> NavigateCommand:\n        \"\"\"\n        Creates a command to navigate to a specific URL.\n\n        Args:\n            url (str): URL to navigate to.\n            referrer (Optional[str]): Referrer URL.\n            transition_type (Optional[str]): Intended transition type.\n            frame_id (Optional[str]): Frame ID to navigate.\n            referrer_policy (Optional[str]): Referrer policy.\n\n        Returns:\n            NavigateCommand: Command object to navigate to a URL.\n        \"\"\"\n        params = NavigateParams(url=url)\n        if referrer is not None:\n            params['referrer'] = referrer\n        if transition_type is not None:\n            params['transitionType'] = transition_type\n        if frame_id is not None:\n            params['frameId'] = frame_id\n        if referrer_policy is not None:\n            params['referrerPolicy'] = referrer_policy\n\n        return Command(method=PageMethod.NAVIGATE, params=params)\n\n    @staticmethod\n    def navigate_to_history_entry(entry_id: int) -> NavigateToHistoryEntryCommand:\n        \"\"\"\n        Creates a command to navigate to a specific history entry.\n\n        Args:\n            entry_id (int): ID of the history entry to navigate to.\n\n        Returns:\n            NavigateToHistoryEntryCommand: Command object to navigate to a history entry.\n        \"\"\"\n        params = NavigateToHistoryEntryParams(entryId=entry_id)\n        return Command(method=PageMethod.NAVIGATE_TO_HISTORY_ENTRY, params=params)\n\n    @staticmethod\n    def print_to_pdf(  # noqa: PLR0912\n        landscape: Optional[bool] = None,\n        display_header_footer: Optional[bool] = None,\n        print_background: Optional[bool] = None,\n        scale: Optional[float] = None,\n        paper_width: Optional[float] = None,\n        paper_height: Optional[float] = None,\n        margin_top: Optional[float] = None,\n        margin_bottom: Optional[float] = None,\n        margin_left: Optional[float] = None,\n        margin_right: Optional[float] = None,\n        page_ranges: Optional[str] = None,\n        header_template: Optional[str] = None,\n        footer_template: Optional[str] = None,\n        prefer_css_page_size: Optional[bool] = None,\n        transfer_mode: Optional[TransferMode] = None,\n        generate_tagged_pdf: Optional[bool] = None,\n        generate_document_outline: Optional[bool] = None,\n    ) -> PrintToPDFCommand:\n        \"\"\"\n        Creates a command to print the current page to PDF.\n\n        Args:\n            landscape (Optional[bool]): Paper orientation.\n            display_header_footer (Optional[bool]): Display header and footer.\n            print_background (Optional[bool]): Print background graphics.\n            scale (Optional[float]): Scale of the webpage rendering.\n            paper_width (Optional[float]): Paper width in inches.\n            paper_height (Optional[float]): Paper height in inches.\n            margin_top (Optional[float]): Top margin in inches.\n            margin_bottom (Optional[float]): Bottom margin in inches.\n            margin_left (Optional[float]): Left margin in inches.\n            margin_right (Optional[float]): Right margin in inches.\n            page_ranges (Optional[str]): Paper ranges to print, e.g., '1-5, 8, 11-13'.\n            header_template (Optional[str]): HTML template for the print header.\n            footer_template (Optional[str]): HTML template for the print footer.\n            prefer_css_page_size (Optional[bool]): Whether to prefer page size as defined by CSS.\n            transfer_mode (Optional[str]): Transfer mode.\n\n        Returns:\n            PrintToPDFCommand: Command object to print the page to PDF.\n        \"\"\"\n        params = PrintToPDFParams()\n        if landscape is not None:\n            params['landscape'] = landscape\n        if display_header_footer is not None:\n            params['displayHeaderFooter'] = display_header_footer\n        if print_background is not None:\n            params['printBackground'] = print_background\n        if scale is not None:\n            params['scale'] = scale\n        if paper_width is not None:\n            params['paperWidth'] = paper_width\n        if paper_height is not None:\n            params['paperHeight'] = paper_height\n        if margin_top is not None:\n            params['marginTop'] = margin_top\n        if margin_bottom is not None:\n            params['marginBottom'] = margin_bottom\n        if margin_left is not None:\n            params['marginLeft'] = margin_left\n        if margin_right is not None:\n            params['marginRight'] = margin_right\n        if page_ranges is not None:\n            params['pageRanges'] = page_ranges\n        if header_template is not None:\n            params['headerTemplate'] = header_template\n        if footer_template is not None:\n            params['footerTemplate'] = footer_template\n        if prefer_css_page_size is not None:\n            params['preferCSSPageSize'] = prefer_css_page_size\n        if transfer_mode is not None:\n            params['transferMode'] = transfer_mode\n        if generate_tagged_pdf is not None:\n            params['generateTaggedPDF'] = generate_tagged_pdf\n        if generate_document_outline is not None:\n            params['generateDocumentOutline'] = generate_document_outline\n\n        return Command(method=PageMethod.PRINT_TO_PDF, params=params)\n\n    @staticmethod\n    def reload(\n        ignore_cache: Optional[bool] = None,\n        script_to_evaluate_on_load: Optional[str] = None,\n        loader_id: Optional[str] = None,\n    ) -> ReloadCommand:\n        \"\"\"\n        Creates a command to reload the current page.\n\n        Args:\n            ignore_cache (Optional[bool]): If true, browser cache is ignored.\n            script_to_evaluate_on_load (Optional[str]): Script to be injected into the page on load.\n\n        Returns:\n            ReloadCommand: Command object to reload the page.\n        \"\"\"\n        params = ReloadParams()\n        if ignore_cache is not None:\n            params['ignoreCache'] = ignore_cache\n        if script_to_evaluate_on_load is not None:\n            params['scriptToEvaluateOnLoad'] = script_to_evaluate_on_load\n        if loader_id is not None:\n            params['loaderId'] = loader_id\n\n        return Command(method=PageMethod.RELOAD, params=params)\n\n    @staticmethod\n    def reset_navigation_history() -> ResetNavigationHistoryCommand:\n        \"\"\"\n        Creates a command to reset the navigation history.\n        \"\"\"\n        return Command(method=PageMethod.RESET_NAVIGATION_HISTORY)\n\n    @staticmethod\n    def remove_script_to_evaluate_on_new_document(\n        identifier: str,\n    ) -> RemoveScriptToEvaluateOnNewDocumentCommand:\n        \"\"\"\n        Creates a command to remove a script that was added to be evaluated on new documents.\n\n        Args:\n            identifier (str): Identifier of the script to remove.\n\n        Returns:\n            RemoveScriptToEvaluateOnNewDocumentCommand: Command object to remove a script.\n        \"\"\"\n        params = RemoveScriptToEvaluateOnNewDocumentParams(identifier=identifier)\n        return Command(method=PageMethod.REMOVE_SCRIPT_TO_EVALUATE_ON_NEW_DOCUMENT, params=params)\n\n    @staticmethod\n    def set_bypass_csp(enabled: bool) -> SetBypassCSPCommand:\n        \"\"\"\n        Creates a command to toggle bypassing page CSP.\n\n        Args:\n            enabled (bool): Whether to bypass page CSP.\n\n        Returns:\n            SetBypassCSPCommand: Command object to toggle bypassing page CSP.\n        \"\"\"\n        params = SetBypassCSPParams(enabled=enabled)\n        return Command(method=PageMethod.SET_BYPASS_CSP, params=params)\n\n    @staticmethod\n    def set_document_content(frame_id: str, html: str) -> SetDocumentContentCommand:\n        \"\"\"\n        Creates a command to set the document content of a frame.\n\n        Args:\n            frame_id (str): Frame ID to set the document content for.\n            html (str): HTML content to set.\n\n        Returns:\n            SetDocumentContentCommand: Command object to set the document content.\n        \"\"\"\n        params = SetDocumentContentParams(frameId=frame_id, html=html)\n        return Command(method=PageMethod.SET_DOCUMENT_CONTENT, params=params)\n\n    @staticmethod\n    def set_intercept_file_chooser_dialog(enabled: bool) -> SetInterceptFileChooserDialogCommand:\n        \"\"\"\n        Creates a command to set whether to intercept file chooser dialogs.\n\n        Args:\n            enabled (bool): Whether to intercept file chooser dialogs.\n\n        Returns:\n            SetInterceptFileChooserDialogCommand: Command object to set file chooser dialog\n                interception.\n        \"\"\"\n        params = SetInterceptFileChooserDialogParams(enabled=enabled)\n        return Command(method=PageMethod.SET_INTERCEPT_FILE_CHOOSER_DIALOG, params=params)\n\n    @staticmethod\n    def set_lifecycle_events_enabled(enabled: bool) -> SetLifecycleEventsEnabledCommand:\n        \"\"\"\n        Creates a command to enable/disable lifecycle events.\n\n        Args:\n            enabled (bool): Whether to enable lifecycle events.\n\n        Returns:\n            SetLifecycleEventsEnabledCommand: Command object to enable/disable lifecycle events.\n        \"\"\"\n        params = SetLifecycleEventsEnabledParams(enabled=enabled)\n        return Command(method=PageMethod.SET_LIFECYCLE_EVENTS_ENABLED, params=params)\n\n    @staticmethod\n    def stop_loading() -> StopLoadingCommand:\n        \"\"\"\n        Creates a command to stop loading the page.\n\n        Returns:\n            StopLoadingCommand: Command object to stop loading the page.\n        \"\"\"\n        return Command(method=PageMethod.STOP_LOADING)\n\n    @staticmethod\n    def add_compilation_cache(url: str, data: str) -> AddCompilationCacheCommand:\n        \"\"\"\n        Creates a command to add a compilation cache entry.\n\n        Experimental: This method is experimental and may be subject to change.\n\n        Args:\n            url (str): URL for which to add the compilation cache entry.\n            data (str): Base64-encoded data.\n\n        Returns:\n            AddCompilationCacheCommand: Command object to add a compilation cache entry.\n        \"\"\"\n        params = AddCompilationCacheParams(url=url, data=data)\n        return Command(method=PageMethod.ADD_COMPILATION_CACHE, params=params)\n\n    @staticmethod\n    def capture_snapshot(\n        format: Literal['mhtml'] = 'mhtml',\n    ) -> CaptureSnapshotCommand:\n        \"\"\"\n        Creates a command to capture a snapshot of the page.\n\n        Experimental: This method is experimental and may be subject to change.\n\n        Args:\n            format (Literal['mhtml']): Format of the snapshot (only 'mhtml' is supported).\n\n        Returns:\n            CaptureSnapshotCommand: Command object to capture a snapshot.\n        \"\"\"\n        params = CaptureSnapshotParams(format=format)\n        return Command(method=PageMethod.CAPTURE_SNAPSHOT, params=params)\n\n    @staticmethod\n    def clear_compilation_cache() -> ClearCompilationCacheCommand:\n        \"\"\"\n        Creates a command to clear the compilation cache.\n        \"\"\"\n        return Command(method=PageMethod.CLEAR_COMPILATION_CACHE)\n\n    @staticmethod\n    def crash() -> CrashCommand:\n        \"\"\"\n        Creates a command to crash the page.\n        \"\"\"\n        return Command(method=PageMethod.CRASH)\n\n    @staticmethod\n    def generate_test_report(\n        message: str, group: Optional[str] = None\n    ) -> GenerateTestReportCommand:\n        \"\"\"\n        Creates a command to generate a test report.\n\n        Experimental: This method is experimental and may be subject to change.\n\n        Args:\n            message (str): Message to be displayed in the report.\n            group (Optional[str]): Group label for the report.\n\n        Returns:\n            GenerateTestReportCommand: Command object to generate a test report.\n        \"\"\"\n        params = GenerateTestReportParams(message=message)\n        if group is not None:\n            params['group'] = group\n        return Command(method=PageMethod.GENERATE_TEST_REPORT, params=params)\n\n    @staticmethod\n    def get_ad_script_ancestry_ids(\n        frame_id: str,\n    ) -> GetAdScriptAncestryIdsCommand:\n        \"\"\"\n        Creates a command to get the ad script ancestry IDs for a given frame.\n\n        Experimental: This method is experimental and may be subject to change.\n\n        Args:\n            frame_id (str): ID of the frame to get ad script ancestry IDs for.\n\n        Returns:\n            GetAdScriptAncestryIdsCommand: Command object to get ad script ancestry IDs.\n        \"\"\"\n        params = GetAdScriptAncestryIdsParams(frameId=frame_id)\n        return Command(method=PageMethod.GET_AD_SCRIPT_ANCESTRY_IDS, params=params)\n\n    @staticmethod\n    def get_app_id(\n        app_id: Optional[str] = None, recommended_id: Optional[str] = None\n    ) -> GetAppIdCommand:\n        \"\"\"\n        Creates a command to get the app ID.\n\n        Experimental: This method is experimental and may be subject to change.\n\n        Args:\n            app_id (Optional[str]): App ID for verification.\n            recommended_id (Optional[str]): Recommended app ID.\n\n        Returns:\n            GetAppIdCommand: Command object to get the app ID.\n        \"\"\"\n        params = GetAppIdParams()\n        if app_id is not None:\n            params['appId'] = app_id\n        if recommended_id is not None:\n            params['recommendedId'] = recommended_id\n        return Command(method=PageMethod.GET_APP_ID, params=params)\n\n    @staticmethod\n    def get_installability_errors() -> GetInstallabilityErrorsCommand:\n        \"\"\"\n        Creates a command to get the installability errors.\n        \"\"\"\n        return Command(method=PageMethod.GET_INSTALLABILITY_ERRORS)\n\n    @staticmethod\n    def get_origin_trials(frame_id: str) -> GetOriginTrialsCommand:\n        \"\"\"\n        Creates a command to get origin trials for a given origin.\n\n        Experimental: This method is experimental and may be subject to change.\n\n        Args:\n            frame_id (Optional[str]): Frame ID to get trials for.\n\n        Returns:\n            GetOriginTrialsCommand: Command object to get origin trials.\n        \"\"\"\n        params = GetOriginTrialsParams(frameId=frame_id)\n        return Command(method=PageMethod.GET_ORIGIN_TRIALS, params=params)\n\n    @staticmethod\n    def get_permissions_policy_state(\n        frame_id: str,\n    ) -> GetPermissionsPolicyStateCommand:\n        \"\"\"\n        Creates a command to get the permissions policy state.\n        \"\"\"\n        params = GetPermissionsPolicyStateParams(frameId=frame_id)\n        return Command(method=PageMethod.GET_PERMISSIONS_POLICY_STATE, params=params)\n\n    @staticmethod\n    def get_resource_content(\n        frame_id: str,\n        url: str,\n    ) -> GetResourceContentCommand:\n        \"\"\"\n        Creates a command to get the resource content.\n        \"\"\"\n        params = GetResourceContentParams(frameId=frame_id, url=url)\n        return Command(method=PageMethod.GET_RESOURCE_CONTENT, params=params)\n\n    @staticmethod\n    def get_resource_tree() -> GetResourceTreeCommand:\n        \"\"\"\n        Creates a command to get the resource tree.\n        \"\"\"\n        return Command(method=PageMethod.GET_RESOURCE_TREE)\n\n    @staticmethod\n    def produce_compilation_cache(\n        scripts: list[CompilationCacheParams],\n    ) -> ProduceCompilationCacheCommand:\n        \"\"\"\n        Creates a command to produce a compilation cache entry.\n        \"\"\"\n        params = ProduceCompilationCacheParams(scripts=scripts)\n        return Command(method=PageMethod.PRODUCE_COMPILATION_CACHE, params=params)\n\n    @staticmethod\n    def screencast_frame_ack(\n        session_id: int,\n    ) -> ScreencastFrameAckCommand:\n        \"\"\"\n        Creates a command to acknowledge a screencast frame.\n        \"\"\"\n        params = ScreencastFrameAckParams(sessionId=session_id)\n        return Command(method=PageMethod.SCREENCAST_FRAME_ACK, params=params)\n\n    @staticmethod\n    def search_in_resource(\n        frame_id: str,\n        url: str,\n        query: str,\n        case_sensitive: Optional[bool] = None,\n        is_regex: Optional[bool] = None,\n    ) -> SearchInResourceCommand:\n        \"\"\"\n        Creates a command to search for a string in a resource.\n        \"\"\"\n        params = SearchInResourceParams(frameId=frame_id, url=url, query=query)\n        if case_sensitive is not None:\n            params['caseSensitive'] = case_sensitive\n        if is_regex is not None:\n            params['isRegex'] = is_regex\n        return Command(method=PageMethod.SEARCH_IN_RESOURCE, params=params)\n\n    @staticmethod\n    def set_ad_blocking_enabled(\n        enabled: bool,\n    ) -> SetAdBlockingEnabledCommand:\n        \"\"\"\n        Creates a command to set ad blocking enabled.\n        \"\"\"\n        params = SetAdBlockingEnabledParams(enabled=enabled)\n        return Command(method=PageMethod.SET_AD_BLOCKING_ENABLED, params=params)\n\n    @staticmethod\n    def set_font_families(\n        font_families: FontFamilies,\n        for_scripts: list[ScriptFontFamilies],\n    ) -> SetFontFamiliesCommand:\n        \"\"\"\n        Creates a command to set font families.\n        \"\"\"\n        params = SetFontFamiliesParams(fontFamilies=font_families, forScripts=for_scripts)\n        return Command(method=PageMethod.SET_FONT_FAMILIES, params=params)\n\n    @staticmethod\n    def set_font_sizes(\n        font_sizes: FontSizes,\n    ) -> SetFontSizesCommand:\n        \"\"\"\n        Creates a command to set font sizes.\n        \"\"\"\n        params = SetFontSizesParams(fontSizes=font_sizes)\n        return Command(method=PageMethod.SET_FONT_SIZES, params=params)\n\n    @staticmethod\n    def set_prerendering_allowed(\n        is_allowed: bool,\n    ) -> SetPrerenderingAllowedCommand:\n        \"\"\"\n        Creates a command to set prerendering allowed.\n        \"\"\"\n        params = SetPrerenderingAllowedParams(isAllowed=is_allowed)\n        return Command(method=PageMethod.SET_PRERENDERING_ALLOWED, params=params)\n\n    @staticmethod\n    def set_rph_registration_mode(\n        mode: AutoResponseMode,\n    ) -> SetRPHRegistrationModeCommand:\n        \"\"\"\n        Creates a command to set the RPH registration mode.\n        \"\"\"\n        params = SetRPHRegistrationModeParams(mode=mode)\n        return Command(method=PageMethod.SET_RPH_REGISTRATION_MODE, params=params)\n\n    @staticmethod\n    def set_spc_transaction_mode(\n        mode: AutoResponseMode,\n    ) -> SetSPCTransactionModeCommand:\n        \"\"\"\n        Creates a command to set the SPC transaction mode.\n        \"\"\"\n        params = SetSPCTransactionModeParams(mode=mode)\n        return Command(method=PageMethod.SET_SPC_TRANSACTION_MODE, params=params)\n\n    @staticmethod\n    def set_web_lifecycle_state(\n        state: WebLifecycleState,\n    ) -> SetWebLifecycleStateCommand:\n        \"\"\"\n        Creates a command to set the web lifecycle state.\n        \"\"\"\n        params = SetWebLifecycleStateParams(state=state)\n        return Command(method=PageMethod.SET_WEB_LIFECYCLE_STATE, params=params)\n\n    @staticmethod\n    def start_screencast(\n        format: ScreencastFormat,\n        quality: Optional[int] = None,\n        max_width: Optional[int] = None,\n        max_height: Optional[int] = None,\n        every_nth_frame: Optional[int] = None,\n    ) -> StartScreencastCommand:\n        \"\"\"\n        Creates a command to start a screencast.\n        \"\"\"\n        params = StartScreencastParams(format=format)\n        if quality is not None:\n            params['quality'] = quality\n        if max_width is not None:\n            params['maxWidth'] = max_width\n        if max_height is not None:\n            params['maxHeight'] = max_height\n        if every_nth_frame is not None:\n            params['everyNthFrame'] = every_nth_frame\n        return Command(method=PageMethod.START_SCREENCAST, params=params)\n\n    @staticmethod\n    def stop_screencast() -> StopScreencastCommand:\n        \"\"\"\n        Creates a command to stop a screencast.\n        \"\"\"\n        return Command(method=PageMethod.STOP_SCREENCAST)\n\n    @staticmethod\n    def wait_for_debugger() -> WaitForDebuggerCommand:\n        \"\"\"\n        Creates a command to wait for a debugger.\n        \"\"\"\n        return Command(method=PageMethod.WAIT_FOR_DEBUGGER)\n"
  },
  {
    "path": "pydoll/commands/runtime_commands.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.protocol.base import Command\nfrom pydoll.protocol.runtime.methods import (\n    AddBindingParams,\n    AwaitPromiseParams,\n    CallFunctionOnParams,\n    CompileScriptParams,\n    EvaluateParams,\n    GetPropertiesParams,\n    GlobalLexicalScopeNamesParams,\n    QueryObjectsParams,\n    ReleaseObjectGroupParams,\n    ReleaseObjectParams,\n    RemoveBindingParams,\n    RunScriptParams,\n    RuntimeMethod,\n    SetAsyncCallStackDepthParams,\n    SetCustomObjectFormatterEnabledParams,\n    SetMaxCallStackSizeToCaptureParams,\n)\n\nif TYPE_CHECKING:\n    from pydoll.protocol.runtime.methods import (\n        AddBindingCommand,\n        AwaitPromiseCommand,\n        CallArgument,\n        CallFunctionOnCommand,\n        CompileScriptCommand,\n        DisableCommand,\n        EnableCommand,\n        EvaluateCommand,\n        GetPropertiesCommand,\n        GlobalLexicalScopeNamesCommand,\n        QueryObjectsCommand,\n        ReleaseObjectCommand,\n        ReleaseObjectGroupCommand,\n        RemoveBindingCommand,\n        RunScriptCommand,\n        SerializationOptions,\n        SetAsyncCallStackDepthCommand,\n        SetCustomObjectFormatterEnabledCommand,\n        SetMaxCallStackSizeToCaptureCommand,\n    )\n\n\nclass RuntimeCommands:\n    \"\"\"\n    A class for interacting with the JavaScript runtime using Chrome\n    DevTools Protocol.\n\n    This class provides methods to create commands for evaluating JavaScript\n    expressions, calling functions on JavaScript objects, and retrieving\n    object properties through CDP.\n\n    Attributes:\n        EVALUATE_TEMPLATE (dict): Template for the Runtime.evaluate command.\n        CALL_FUNCTION_ON_TEMPLATE (dict): Template for the\n            Runtime.callFunctionOn command.\n        GET_PROPERTIES (dict): Template for the Runtime.getProperties command.\n    \"\"\"\n\n    @staticmethod\n    def add_binding(name: str, execution_context_name: Optional[str] = None) -> AddBindingCommand:\n        \"\"\"\n        Creates a command to add a JavaScript binding.\n\n        Args:\n            name (str): Name of the binding to add.\n            execution_context_name (Optional[str]): Name of the execution context to bind to.\n\n        Returns:\n            AddBindingCommand: Command object to add a JavaScript binding.\n        \"\"\"\n        params = AddBindingParams(name=name)\n        if execution_context_name is not None:\n            params['executionContextName'] = execution_context_name\n\n        return Command(method=RuntimeMethod.ADD_BINDING, params=params)\n\n    @staticmethod\n    def await_promise(\n        promise_object_id: str,\n        return_by_value: Optional[bool] = None,\n        generate_preview: Optional[bool] = None,\n    ) -> AwaitPromiseCommand:\n        \"\"\"\n        Creates a command to await a JavaScript promise and return its result.\n\n        Args:\n            promise_object_id (str): ID of the promise to await.\n            return_by_value (Optional[bool]): Whether to return the result by value instead\n                of reference.\n            generate_preview (Optional[bool]): Whether to generate a preview for the result.\n\n        Returns:\n            AwaitPromiseCommand: Command object to await a promise.\n        \"\"\"\n        params = AwaitPromiseParams(promiseObjectId=promise_object_id)\n        if return_by_value is not None:\n            params['returnByValue'] = return_by_value\n        if generate_preview is not None:\n            params['generatePreview'] = generate_preview\n\n        return Command(method=RuntimeMethod.AWAIT_PROMISE, params=params)\n\n    @staticmethod\n    def call_function_on(\n        function_declaration: str,\n        object_id: Optional[str] = None,\n        arguments: Optional[list[CallArgument]] = None,\n        silent: Optional[bool] = None,\n        return_by_value: Optional[bool] = None,\n        generate_preview: Optional[bool] = None,\n        user_gesture: Optional[bool] = None,\n        await_promise: Optional[bool] = None,\n        execution_context_id: Optional[int] = None,\n        object_group: Optional[str] = None,\n        throw_on_side_effect: Optional[bool] = None,\n        unique_context_id: Optional[str] = None,\n        serialization_options: Optional[SerializationOptions] = None,\n    ) -> CallFunctionOnCommand:\n        \"\"\"\n        Creates a command to call a function with a given declaration on a specific object.\n\n        Args:\n            function_declaration (str): Declaration of the function to call.\n            object_id (Optional[str]): ID of the object to call the function on.\n            arguments (Optional[list[CallArgument]]): Arguments to pass to the function.\n            silent (Optional[bool]): Whether to silence exceptions.\n            return_by_value (Optional[bool]): Whether to return the result by value instead\n                of reference.\n            generate_preview (Optional[bool]): Whether to generate a preview for the result.\n            user_gesture (Optional[bool]): Whether to treat the call as initiated by user gesture.\n            await_promise (Optional[bool]): Whether to await promise result.\n            execution_context_id (Optional[int]): ID of the execution context to call the\n                function in.\n            object_group (Optional[str]): Symbolic group name for the result.\n            throw_on_side_effect (Optional[bool]): Whether to throw if side effect cannot be\n                ruled out.\n            unique_context_id (Optional[str]): Unique context ID for the function call.\n            serialization_options (Optional[SerializationOptions]): Serialization options for\n                the result.\n\n        Returns:\n            CallFunctionOnCommand: Command object to call a function on an object.\n        \"\"\"\n        params = CallFunctionOnParams(functionDeclaration=function_declaration)\n        if object_id is not None:\n            params['objectId'] = object_id\n        if arguments is not None:\n            params['arguments'] = arguments\n        if silent is not None:\n            params['silent'] = silent\n        if return_by_value is not None:\n            params['returnByValue'] = return_by_value\n        if generate_preview is not None:\n            params['generatePreview'] = generate_preview\n        if user_gesture is not None:\n            params['userGesture'] = user_gesture\n        if await_promise is not None:\n            params['awaitPromise'] = await_promise\n        if execution_context_id is not None:\n            params['executionContextId'] = execution_context_id\n        if object_group is not None:\n            params['objectGroup'] = object_group\n        if throw_on_side_effect is not None:\n            params['throwOnSideEffect'] = throw_on_side_effect\n        if unique_context_id is not None:\n            params['uniqueContextId'] = unique_context_id\n        if serialization_options is not None:\n            params['serializationOptions'] = serialization_options\n\n        return Command(method=RuntimeMethod.CALL_FUNCTION_ON, params=params)\n\n    @staticmethod\n    def compile_script(\n        expression: str,\n        source_url: str,\n        persist_script: bool = False,\n        execution_context_id: Optional[int] = None,\n    ) -> CompileScriptCommand:\n        \"\"\"\n        Creates a command to compile a JavaScript expression.\n\n        Args:\n            expression (str): JavaScript expression to compile.\n            source_url (str): URL of the source file for the script.\n            persist_script (bool): Whether to persist the compiled script.\n            execution_context_id (Optional[int]): ID of the execution context to compile\n                the script in.\n\n        Returns:\n            CompileScriptCommand: Command object to compile a script.\n        \"\"\"\n        params = CompileScriptParams(\n            expression=expression, sourceURL=source_url, persistScript=persist_script\n        )\n        if execution_context_id is not None:\n            params['executionContextId'] = execution_context_id\n\n        return Command(method=RuntimeMethod.COMPILE_SCRIPT, params=params)\n\n    @staticmethod\n    def disable() -> DisableCommand:\n        \"\"\"\n        Disables the runtime domain.\n\n        Returns:\n            DisableCommand: Command object to disable the runtime domain.\n        \"\"\"\n        return Command(method=RuntimeMethod.DISABLE)\n\n    @staticmethod\n    def enable() -> EnableCommand:\n        \"\"\"\n        Enables the runtime domain.\n\n        Returns:\n            EnableCommand: Command object to enable the runtime domain.\n        \"\"\"\n        return Command(method=RuntimeMethod.ENABLE)\n\n    @staticmethod\n    def evaluate(  # noqa: PLR0912\n        expression: str,\n        object_group: Optional[str] = None,\n        include_command_line_api: Optional[bool] = None,\n        silent: Optional[bool] = None,\n        context_id: Optional[int] = None,\n        return_by_value: Optional[bool] = None,\n        generate_preview: Optional[bool] = None,\n        user_gesture: Optional[bool] = None,\n        await_promise: Optional[bool] = None,\n        throw_on_side_effect: Optional[bool] = None,\n        timeout: Optional[float] = None,\n        disable_breaks: Optional[bool] = None,\n        repl_mode: Optional[bool] = None,\n        allow_unsafe_eval_blocked_by_csp: Optional[bool] = None,\n        unique_context_id: Optional[str] = None,\n        serialization_options: Optional[SerializationOptions] = None,\n    ) -> EvaluateCommand:\n        \"\"\"\n        Creates a command to evaluate a JavaScript expression in the global context.\n\n        Args:\n            expression (str): JavaScript expression to evaluate.\n            object_group (Optional[str]): Symbolic group name for the result.\n            include_command_line_api (Optional[bool]): Whether to include command line API.\n            silent (Optional[bool]): Whether to silence exceptions.\n            context_id (Optional[int]): ID of the execution context to evaluate in.\n            return_by_value (Optional[bool]): Whether to return the result by value instead\n                of reference.\n            generate_preview (Optional[bool]): Whether to generate a preview for the result.\n            user_gesture (Optional[bool]): Whether to treat evaluation as initiated by user gesture.\n            await_promise (Optional[bool]): Whether to await promise result.\n            throw_on_side_effect (Optional[bool]): Whether to throw if side effect cannot be\n                ruled out.\n            timeout (Optional[float]): Timeout in milliseconds.\n            disable_breaks (Optional[bool]): Whether to disable breakpoints during evaluation.\n            repl_mode (Optional[bool]): Whether to execute in REPL mode.\n            allow_unsafe_eval_blocked_by_csp (Optional[bool]): Allow unsafe evaluation.\n            unique_context_id (Optional[str]): Unique context ID for evaluation.\n            serialization_options (Optional[SerializationOptions]): Serialization\n                for the result.\n\n        Returns:\n            EvaluateCommand: Command object to evaluate JavaScript.\n        \"\"\"\n        params = EvaluateParams(expression=expression)\n        if object_group is not None:\n            params['objectGroup'] = object_group\n        if include_command_line_api is not None:\n            params['includeCommandLineAPI'] = include_command_line_api\n        if silent is not None:\n            params['silent'] = silent\n        if context_id is not None:\n            params['contextId'] = context_id\n        if return_by_value is not None:\n            params['returnByValue'] = return_by_value\n        if generate_preview is not None:\n            params['generatePreview'] = generate_preview\n        if user_gesture is not None:\n            params['userGesture'] = user_gesture\n        if await_promise is not None:\n            params['awaitPromise'] = await_promise\n        if throw_on_side_effect is not None:\n            params['throwOnSideEffect'] = throw_on_side_effect\n        if timeout is not None:\n            params['timeout'] = timeout\n        if disable_breaks is not None:\n            params['disableBreaks'] = disable_breaks\n        if repl_mode is not None:\n            params['replMode'] = repl_mode\n        if allow_unsafe_eval_blocked_by_csp is not None:\n            params['allowUnsafeEvalBlockedByCSP'] = allow_unsafe_eval_blocked_by_csp\n        if unique_context_id is not None:\n            params['uniqueContextId'] = unique_context_id\n        if serialization_options is not None:\n            params['serializationOptions'] = serialization_options\n\n        return Command(method=RuntimeMethod.EVALUATE, params=params)\n\n    @staticmethod\n    def get_properties(\n        object_id: str,\n        own_properties: Optional[bool] = None,\n        accessor_properties_only: Optional[bool] = None,\n        generate_preview: Optional[bool] = None,\n        non_indexed_properties_only: Optional[bool] = None,\n    ) -> GetPropertiesCommand:\n        \"\"\"\n        Creates a command to get properties of a JavaScript object.\n\n        Args:\n            object_id (str): ID of the object to get properties for.\n            own_properties (Optional[bool]): Whether to return only own properties.\n            accessor_properties_only (Optional[bool]): Whether to return only accessor properties.\n            generate_preview (Optional[bool]): Whether to generate previews for property values.\n            non_indexed_properties_only (Optional[bool]): Whether to return only non-indexed\n                properties.\n\n        Returns:\n            GetPropertiesCommand: Command object to get object properties.\n        \"\"\"\n        params = GetPropertiesParams(objectId=object_id)\n        if own_properties is not None:\n            params['ownProperties'] = own_properties\n        if accessor_properties_only is not None:\n            params['accessorPropertiesOnly'] = accessor_properties_only\n        if generate_preview is not None:\n            params['generatePreview'] = generate_preview\n        if non_indexed_properties_only is not None:\n            params['nonIndexedPropertiesOnly'] = non_indexed_properties_only\n\n        return Command(method=RuntimeMethod.GET_PROPERTIES, params=params)\n\n    @staticmethod\n    def global_lexical_scope_names(\n        execution_context_id: Optional[int] = None,\n    ) -> GlobalLexicalScopeNamesCommand:\n        \"\"\"\n        Creates a command to retrieve names of variables from global lexical scope.\n\n        Args:\n            execution_context_id (Optional[int]): ID of the execution context to get scope\n                names from.\n\n        Returns:\n            GlobalLexicalScopeNamesCommand: Command object to get global lexical\n                scope names.\n        \"\"\"\n        params = GlobalLexicalScopeNamesParams()\n        if execution_context_id is not None:\n            params['executionContextId'] = execution_context_id\n\n        return Command(method=RuntimeMethod.GLOBAL_LEXICAL_SCOPE_NAMES, params=params)\n\n    @staticmethod\n    def query_objects(\n        prototype_object_id: str,\n        object_group: Optional[str] = None,\n    ) -> QueryObjectsCommand:\n        \"\"\"\n        Creates a command to query objects with a given prototype.\n\n        Args:\n            prototype_object_id (str): ID of the prototype object.\n            object_group (Optional[str]): Symbolic group name for the results.\n\n        Returns:\n            QueryObjectsCommand: Command object to query objects.\n        \"\"\"\n        params = QueryObjectsParams(prototypeObjectId=prototype_object_id)\n        if object_group is not None:\n            params['objectGroup'] = object_group\n\n        return Command(method=RuntimeMethod.QUERY_OBJECTS, params=params)\n\n    @staticmethod\n    def release_object(\n        object_id: str,\n    ) -> ReleaseObjectCommand:\n        \"\"\"\n        Creates a command to release a JavaScript object.\n\n        Args:\n            object_id (str): ID of the object to release.\n\n        Returns:\n            ReleaseObjectCommand: Command object to release an object.\n        \"\"\"\n        params = ReleaseObjectParams(objectId=object_id)\n\n        return Command(method=RuntimeMethod.RELEASE_OBJECT, params=params)\n\n    @staticmethod\n    def release_object_group(\n        object_group: str,\n    ) -> ReleaseObjectGroupCommand:\n        \"\"\"\n        Creates a command to release all objects in a group.\n\n        Args:\n            object_group (str): Name of the object group to release.\n\n        Returns:\n            ReleaseObjectGroupCommand: Command object to release an object group.\n        \"\"\"\n        params = ReleaseObjectGroupParams(objectGroup=object_group)\n        return Command(method=RuntimeMethod.RELEASE_OBJECT_GROUP, params=params)\n\n    @staticmethod\n    def remove_binding(\n        name: str,\n    ) -> RemoveBindingCommand:\n        \"\"\"\n        Creates a command to remove a JavaScript binding.\n\n        Args:\n            name (str): Name of the binding to remove.\n\n        Returns:\n            RemoveBindingCommand: Command object to remove a JavaScript binding.\n        \"\"\"\n        params = RemoveBindingParams(name=name)\n        return Command(method=RuntimeMethod.REMOVE_BINDING, params=params)\n\n    @staticmethod\n    def run_script(\n        script_id: str,\n        execution_context_id: Optional[int] = None,\n        object_group: Optional[str] = None,\n        silent: Optional[bool] = None,\n        include_command_line_api: Optional[bool] = None,\n        return_by_value: Optional[bool] = None,\n        generate_preview: Optional[bool] = None,\n        await_promise: Optional[bool] = None,\n    ) -> RunScriptCommand:\n        \"\"\"\n        Creates a command to run a compiled script.\n\n        Args:\n            script_id (str): ID of the compiled script to run.\n            execution_context_id (Optional[int]): ID of the execution context to run the script in.\n            object_group (Optional[str]): Symbolic group name for the result.\n            silent (Optional[bool]): Whether to silence exceptions.\n            include_command_line_api (Optional[bool]): Whether to include command line API.\n            return_by_value (Optional[bool]): Whether to return the result by value instead\n                of reference.\n            generate_preview (Optional[bool]): Whether to generate a preview for the result.\n            await_promise (Optional[bool]): Whether to await promise result.\n\n        Returns:\n            RunScriptCommand: Command object to run a script.\n        \"\"\"\n        params = RunScriptParams(scriptId=script_id)\n        if execution_context_id is not None:\n            params['executionContextId'] = execution_context_id\n        if object_group is not None:\n            params['objectGroup'] = object_group\n        if silent is not None:\n            params['silent'] = silent\n        if include_command_line_api is not None:\n            params['includeCommandLineAPI'] = include_command_line_api\n        if return_by_value is not None:\n            params['returnByValue'] = return_by_value\n        if generate_preview is not None:\n            params['generatePreview'] = generate_preview\n        if await_promise is not None:\n            params['awaitPromise'] = await_promise\n\n        return Command(method=RuntimeMethod.RUN_SCRIPT, params=params)\n\n    @staticmethod\n    def set_async_call_stack_depth(\n        max_depth: int,\n    ) -> SetAsyncCallStackDepthCommand:\n        \"\"\"\n        Creates a command to set the async call stack depth.\n\n        Args:\n            max_depth (int): Maximum depth of async call stacks.\n\n        Returns:\n            SetAsyncCallStackDepthCommand: Command object to set async call stack depth.\n        \"\"\"\n        params = SetAsyncCallStackDepthParams(maxDepth=max_depth)\n        return Command(method=RuntimeMethod.SET_ASYNC_CALL_STACK_DEPTH, params=params)\n\n    @staticmethod\n    def set_custom_object_formatter_enabled(\n        enabled: bool,\n    ) -> SetCustomObjectFormatterEnabledCommand:\n        \"\"\"\n        Creates a command to enable or disable custom object formatters.\n\n        Args:\n            enabled (bool): Whether to enable custom object formatters.\n\n        Returns:\n            SetCustomObjectFormatterEnabledCommand: Command object to enable/disable custom\n                object formatters.\n        \"\"\"\n        params = SetCustomObjectFormatterEnabledParams(enabled=enabled)\n        return Command(method=RuntimeMethod.SET_CUSTOM_OBJECT_FORMATTER_ENABLED, params=params)\n\n    @staticmethod\n    def set_max_call_stack_size_to_capture(\n        size: int,\n    ) -> SetMaxCallStackSizeToCaptureCommand:\n        \"\"\"\n        Creates a command to set the maximum call stack size to capture.\n\n        Args:\n            size (int): Maximum call stack size to capture.\n\n        Returns:\n            SetMaxCallStackSizeToCaptureCommand: Command object to set max call stack size.\n        \"\"\"\n        params = SetMaxCallStackSizeToCaptureParams(size=size)\n        return Command(method=RuntimeMethod.SET_MAX_CALL_STACK_SIZE_TO_CAPTURE, params=params)\n"
  },
  {
    "path": "pydoll/commands/storage_commands.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.protocol.base import Command\nfrom pydoll.protocol.storage.methods import (\n    ClearCookiesParams,\n    ClearDataForOriginParams,\n    ClearDataForStorageKeyParams,\n    ClearSharedStorageEntriesParams,\n    ClearTrustTokensParams,\n    DeleteSharedStorageEntryParams,\n    DeleteStorageBucketParams,\n    GetAffectedUrlsForThirdPartyCookieMetadataParams,\n    GetCookiesParams,\n    GetInterestGroupDetailsParams,\n    GetSharedStorageEntriesParams,\n    GetSharedStorageMetadataParams,\n    GetStorageKeyForFrameParams,\n    GetUsageAndQuotaParams,\n    OverrideQuotaForOriginParams,\n    ResetSharedStorageBudgetParams,\n    SetAttributionReportingLocalTestingModeParams,\n    SetAttributionReportingTrackingParams,\n    SetCookiesParams,\n    SetInterestGroupAuctionTrackingParams,\n    SetInterestGroupTrackingParams,\n    SetProtectedAudienceKAnonymityParams,\n    SetSharedStorageEntryParams,\n    SetSharedStorageTrackingParams,\n    SetStorageBucketTrackingParams,\n    StorageMethod,\n    TrackCacheStorageForOriginParams,\n    TrackCacheStorageForStorageKeyParams,\n    TrackIndexedDBForOriginParams,\n    TrackIndexedDBForStorageKeyParams,\n    UntrackCacheStorageForOriginParams,\n    UntrackCacheStorageForStorageKeyParams,\n    UntrackIndexedDBForOriginParams,\n    UntrackIndexedDBForStorageKeyParams,\n)\n\nif TYPE_CHECKING:\n    from pydoll.protocol.network.types import CookieParam\n    from pydoll.protocol.storage.methods import (\n        ClearCookiesCommand,\n        ClearDataForOriginCommand,\n        ClearDataForStorageKeyCommand,\n        ClearSharedStorageEntriesCommand,\n        ClearTrustTokensCommand,\n        DeleteSharedStorageEntryCommand,\n        DeleteStorageBucketCommand,\n        GetAffectedUrlsForThirdPartyCookieMetadataCommand,\n        GetCookiesCommand,\n        GetInterestGroupDetailsCommand,\n        GetRelatedWebsiteSetsCommand,\n        GetSharedStorageEntriesCommand,\n        GetSharedStorageMetadataCommand,\n        GetStorageKeyForFrameCommand,\n        GetTrustTokensCommand,\n        GetUsageAndQuotaCommand,\n        OverrideQuotaForOriginCommand,\n        ResetSharedStorageBudgetCommand,\n        RunBounceTrackingMitigationsCommand,\n        SendPendingAttributionReportsCommand,\n        SetAttributionReportingLocalTestingModeCommand,\n        SetAttributionReportingTrackingCommand,\n        SetCookiesCommand,\n        SetInterestGroupAuctionTrackingCommand,\n        SetInterestGroupTrackingCommand,\n        SetProtectedAudienceKAnonymityCommand,\n        SetSharedStorageEntryCommand,\n        SetSharedStorageTrackingCommand,\n        SetStorageBucketTrackingCommand,\n        TrackCacheStorageForOriginCommand,\n        TrackCacheStorageForStorageKeyCommand,\n        TrackIndexedDBForOriginCommand,\n        TrackIndexedDBForStorageKeyCommand,\n        UntrackCacheStorageForOriginCommand,\n        UntrackCacheStorageForStorageKeyCommand,\n        UntrackIndexedDBForOriginCommand,\n        UntrackIndexedDBForStorageKeyCommand,\n    )\n    from pydoll.protocol.storage.types import StorageBucket\n\n\nclass StorageCommands:  # noqa: PLR0904\n    \"\"\"\n    A class for interacting with browser storage using Chrome DevTools Protocol (CDP).\n\n    The Storage domain of CDP allows managing various types of browser storage, including:\n    - Cookies\n    - Cache Storage\n    - IndexedDB\n    - Web Storage (localStorage/sessionStorage)\n    - Shared Storage\n    - Storage Buckets\n    - Trust Tokens\n    - Interest Groups\n    - Attribution Reporting\n\n    This class provides static methods that generate CDP commands to manage these types\n    of storage without the need for traditional webdrivers.\n    \"\"\"\n\n    @staticmethod\n    def clear_cookies(browser_context_id: Optional[str] = None) -> ClearCookiesCommand:\n        \"\"\"\n        Generates a command to clear all browser cookies.\n\n        Args:\n            browser_context_id: Browser context ID (optional). Useful when working\n                               with multiple contexts (e.g., multiple windows or tabs).\n\n        Returns:\n            ClearCookiesCommand: The CDP command to clear all cookies.\n        \"\"\"\n        params = ClearCookiesParams()\n        if browser_context_id is not None:\n            params['browserContextId'] = browser_context_id\n        return Command(method=StorageMethod.CLEAR_COOKIES, params=params)\n\n    @staticmethod\n    def clear_data_for_origin(origin: str, storage_types: str) -> ClearDataForOriginCommand:\n        \"\"\"\n        Generates a command to clear storage data for a specific origin.\n\n        Args:\n            origin: The security origin (e.g., \"https://example.com\").\n            storage_types: Comma-separated list of storage types to clear.\n                          Possible values include: \"cookies\", \"local_storage\", \"indexeddb\",\n                          \"cache_storage\", etc. Use \"all\" to clear all types.\n\n        Returns:\n            ClearDataForOriginCommand: The CDP command to clear data for the specified origin.\n        \"\"\"\n        params = ClearDataForOriginParams(origin=origin, storageTypes=storage_types)\n        return Command(method=StorageMethod.CLEAR_DATA_FOR_ORIGIN, params=params)\n\n    @staticmethod\n    def clear_data_for_storage_key(\n        storage_key: str, storage_types: str\n    ) -> ClearDataForStorageKeyCommand:\n        \"\"\"\n        Generates a command to clear data for a specific storage key.\n\n        Args:\n            storage_key: The storage key for which to clear data.\n                        Unlike origin, a storage key is a more specific identifier\n                        that may include partition isolation.\n            storage_types: Comma-separated list of storage types to clear.\n                          Possible values include: \"cookies\", \"local_storage\", \"indexeddb\",\n                          \"cache_storage\", etc. Use \"all\" to clear all types.\n\n        Returns:\n            ClearDataForStorageKeyCommand: The CDP command to clear data for the specified storage\n                key.\n        \"\"\"\n        params = ClearDataForStorageKeyParams(storageKey=storage_key, storageTypes=storage_types)\n        return Command(method=StorageMethod.CLEAR_DATA_FOR_STORAGE_KEY, params=params)\n\n    @staticmethod\n    def get_cookies(browser_context_id: Optional[str] = None) -> GetCookiesCommand:\n        \"\"\"\n        Generates a command to get all browser cookies.\n\n        Args:\n            browser_context_id: Browser context ID (optional). Useful when working\n                               with multiple contexts (e.g., multiple windows or tabs).\n\n        Returns:\n            GetCookiesCommand: The CDP command to get all cookies, which will return an array\n                of Cookie objects.\n        \"\"\"\n        params = GetCookiesParams()\n        if browser_context_id is not None:\n            params['browserContextId'] = browser_context_id\n        return Command(method=StorageMethod.GET_COOKIES, params=params)\n\n    @staticmethod\n    def get_storage_key_for_frame(frame_id: str) -> GetStorageKeyForFrameCommand:\n        \"\"\"\n        Generates a command to get the storage key for a specific frame.\n\n        Storage keys are used to isolate data between different origins or\n        partitions in the browser.\n\n        Args:\n            frame_id: The ID of the frame for which to get the storage key.\n\n        Returns:\n            GetStorageKeyForFrameCommand: The CDP command to get the storage key for the specified\n                frame.\n        \"\"\"\n        params = GetStorageKeyForFrameParams(frameId=frame_id)\n        return Command(method=StorageMethod.GET_STORAGE_KEY_FOR_FRAME, params=params)\n\n    @staticmethod\n    def get_usage_and_quota(origin: str) -> GetUsageAndQuotaCommand:\n        \"\"\"\n        Generates a command to get storage usage and quota information for an origin.\n\n        Useful for monitoring or debugging storage consumption of a site.\n\n        Args:\n            origin: The security origin (e.g., \"https://example.com\") for which to get information.\n\n        Returns:\n            GetUsageAndQuotaCommand: The CDP command that will return:\n                - usage: Storage usage in bytes\n                - quota: Storage quota in bytes\n                - usageBreakdown: Breakdown of usage by storage type\n                - overrideActive: Whether there is an active quota override\n        \"\"\"\n        params = GetUsageAndQuotaParams(origin=origin)\n        return Command(method=StorageMethod.GET_USAGE_AND_QUOTA, params=params)\n\n    @staticmethod\n    def set_cookies(\n        cookies: list[CookieParam], browser_context_id: Optional[str] = None\n    ) -> SetCookiesCommand:\n        \"\"\"\n        Generates a command to set browser cookies.\n\n        Args:\n            cookies: list of Cookie objects to set.\n            browser_context_id: Browser context ID (optional). Useful when working\n                               with multiple contexts (e.g., multiple windows or tabs).\n\n        Returns:\n            SetCookiesCommand: The CDP command to set the specified cookies.\n        \"\"\"\n        params = SetCookiesParams(cookies=cookies)\n        if browser_context_id is not None:\n            params['browserContextId'] = browser_context_id\n        return Command(method=StorageMethod.SET_COOKIES, params=params)\n\n    @staticmethod\n    def set_protected_audience_k_anonymity(\n        owner: str, name: str, hashes: list[str]\n    ) -> SetProtectedAudienceKAnonymityCommand:\n        \"\"\"\n        Generates a command to set K-anonymity for protected audience.\n\n        This command is used to configure anonymity in privacy-preserving advertising\n        systems (part of Google's Privacy Sandbox).\n\n        Args:\n            owner: Owner of the K-anonymity configuration.\n            name: Name of the K-anonymity configuration.\n            hashes: list of hashes for the configuration.\n\n        Returns:\n            SetProtectedAudienceKAnonymityCommand: The CDP command to set protected audience\n                K-anonymity.\n        \"\"\"\n        params = SetProtectedAudienceKAnonymityParams(owner=owner, name=name, hashes=hashes)\n        return Command(method=StorageMethod.SET_PROTECTED_AUDIENCE_K_ANONYMITY, params=params)\n\n    @staticmethod\n    def track_cache_storage_for_origin(origin: str) -> TrackCacheStorageForOriginCommand:\n        \"\"\"\n        Generates a command to register an origin to receive notifications about changes\n        to its Cache Storage.\n\n        Cache Storage is primarily used by Service Workers to store resources for offline use.\n\n        Args:\n            origin: The security origin (e.g., \"https://example.com\") to monitor.\n\n        Returns:\n            TrackCacheStorageForOriginCommand: The CDP command to register monitoring of the\n                origin's Cache Storage.\n        \"\"\"\n        params = TrackCacheStorageForOriginParams(origin=origin)\n        return Command(method=StorageMethod.TRACK_CACHE_STORAGE_FOR_ORIGIN, params=params)\n\n    @staticmethod\n    def track_cache_storage_for_storage_key(\n        storage_key: str,\n    ) -> TrackCacheStorageForStorageKeyCommand:\n        \"\"\"\n        Generates a command to register a storage key to receive notifications\n        about changes to its Cache Storage.\n\n        Similar to track_cache_storage_for_origin, but uses the storage key\n        for more precise isolation.\n\n        Args:\n            storage_key: The storage key to monitor.\n\n        Returns:\n            TrackCacheStorageForStorageKeyCommand: The CDP command to register monitoring of\n                the key's Cache Storage.\n        \"\"\"\n        params = TrackCacheStorageForStorageKeyParams(storageKey=storage_key)\n        return Command(method=StorageMethod.TRACK_CACHE_STORAGE_FOR_STORAGE_KEY, params=params)\n\n    @staticmethod\n    def track_indexed_db_for_origin(origin: str) -> TrackIndexedDBForOriginCommand:\n        \"\"\"\n        Generates a command to register an origin to receive notifications about changes\n        to its IndexedDB.\n\n        IndexedDB is a NoSQL database system in the browser for storing\n        large amounts of structured data.\n\n        Args:\n            origin: The security origin (e.g., \"https://example.com\") to monitor.\n\n        Returns:\n            TrackIndexedDBForOriginCommand: The CDP command to register monitoring of\n                the origin's IndexedDB.\n        \"\"\"\n        params = TrackIndexedDBForOriginParams(origin=origin)\n        return Command(method=StorageMethod.TRACK_INDEXED_DB_FOR_ORIGIN, params=params)\n\n    @staticmethod\n    def track_indexed_db_for_storage_key(storage_key: str) -> TrackIndexedDBForStorageKeyCommand:\n        \"\"\"\n        Generates a command to register a storage key to receive notifications\n        about changes to its IndexedDB.\n\n        Similar to track_indexed_db_for_origin, but uses the storage key\n        for more precise isolation.\n\n        Args:\n            storage_key: The storage key to monitor.\n\n        Returns:\n            TrackIndexedDBForStorageKeyCommand: The CDP command to register monitoring of\n                the key's IndexedDB.\n        \"\"\"\n        params = TrackIndexedDBForStorageKeyParams(storageKey=storage_key)\n        return Command(method=StorageMethod.TRACK_INDEXED_DB_FOR_STORAGE_KEY, params=params)\n\n    @staticmethod\n    def untrack_cache_storage_for_origin(origin: str) -> UntrackCacheStorageForOriginCommand:\n        \"\"\"\n        Generates a command to unregister an origin from receiving notifications\n        about changes to its Cache Storage.\n\n        Use this method to stop monitoring Cache Storage after using track_cache_storage_for_origin.\n\n        Args:\n            origin: The security origin (e.g., \"https://example.com\") to stop monitoring.\n\n        Returns:\n            UntrackCacheStorageForOriginCommand: The CDP command to cancel monitoring of the\n                origin's Cache Storage.\n        \"\"\"\n        params = UntrackCacheStorageForOriginParams(origin=origin)\n        return Command(method=StorageMethod.UNTRACK_CACHE_STORAGE_FOR_ORIGIN, params=params)\n\n    @staticmethod\n    def untrack_cache_storage_for_storage_key(\n        storage_key: str,\n    ) -> UntrackCacheStorageForStorageKeyCommand:\n        \"\"\"\n        Generates a command to unregister a storage key from receiving notifications\n        about changes to its Cache Storage.\n\n        Use this method to stop monitoring Cache Storage after using\n        track_cache_storage_for_storage_key.\n\n        Args:\n            storage_key: The storage key to stop monitoring.\n\n        Returns:\n            UntrackCacheStorageForStorageKeyCommand: The CDP command to cancel monitoring of\n                the key's Cache Storage.\n        \"\"\"\n        params = UntrackCacheStorageForStorageKeyParams(storageKey=storage_key)\n        return Command(method=StorageMethod.UNTRACK_CACHE_STORAGE_FOR_STORAGE_KEY, params=params)\n\n    @staticmethod\n    def untrack_indexed_db_for_origin(origin: str) -> UntrackIndexedDBForOriginCommand:\n        \"\"\"\n        Generates a command to unregister an origin from receiving notifications\n        about changes to its IndexedDB.\n\n        Use this method to stop monitoring IndexedDB after using track_indexed_db_for_origin.\n\n        Args:\n            origin: The security origin (e.g., \"https://example.com\") to stop monitoring.\n\n        Returns:\n            UntrackIndexedDBForOriginCommand: The CDP command to cancel monitoring of\n                the origin's IndexedDB.\n        \"\"\"\n        params = UntrackIndexedDBForOriginParams(origin=origin)\n        return Command(method=StorageMethod.UNTRACK_INDEXED_DB_FOR_ORIGIN, params=params)\n\n    @staticmethod\n    def untrack_indexed_db_for_storage_key(\n        storage_key: str,\n    ) -> UntrackIndexedDBForStorageKeyCommand:\n        \"\"\"\n        Generates a command to unregister a storage key from receiving notifications\n        about changes to its IndexedDB.\n\n        Use this method to stop monitoring IndexedDB after using track_indexed_db_for_storage_key.\n\n        Args:\n            storage_key: The storage key to stop monitoring.\n\n        Returns:\n            UntrackIndexedDBForStorageKeyCommand: The CDP command to cancel monitoring\n                of the key's IndexedDB.\n        \"\"\"\n        params = UntrackIndexedDBForStorageKeyParams(storageKey=storage_key)\n        return Command(method=StorageMethod.UNTRACK_INDEXED_DB_FOR_STORAGE_KEY, params=params)\n\n    @staticmethod\n    def clear_shared_storage_entries(owner_origin: str) -> ClearSharedStorageEntriesCommand:\n        \"\"\"\n        Generates a command to clear all Shared Storage entries for a specific origin.\n\n        Shared Storage is an experimental API that allows cross-origin shared storage\n        with privacy protections.\n\n        Args:\n            owner_origin: The owner origin of the Shared Storage to clear.\n\n        Returns:\n            ClearSharedStorageEntriesCommand: The CDP command to clear the Shared Storage entries.\n        \"\"\"\n        params = ClearSharedStorageEntriesParams(ownerOrigin=owner_origin)\n        return Command(method=StorageMethod.CLEAR_SHARED_STORAGE_ENTRIES, params=params)\n\n    @staticmethod\n    def clear_trust_tokens(issuer_origin: str) -> ClearTrustTokensCommand:\n        \"\"\"\n        Generates a command to remove all Trust Tokens issued by the specified origin.\n\n        Trust Tokens are an experimental API for combating fraud while preserving user\n        privacy. This command keeps other stored data, including the issuer's redemption\n        records, intact.\n\n        Args:\n            issuer_origin: The issuer origin of the tokens to remove.\n\n        Returns:\n            ClearTrustTokensCommand: The CDP command to clear Trust Tokens, which will return:\n                - didDeleteTokens: True if any tokens were deleted, False otherwise.\n        \"\"\"\n        params = ClearTrustTokensParams(issuerOrigin=issuer_origin)\n        return Command(method=StorageMethod.CLEAR_TRUST_TOKENS, params=params)\n\n    @staticmethod\n    def delete_shared_storage_entry(owner_origin: str, key: str) -> DeleteSharedStorageEntryCommand:\n        \"\"\"\n        Generates a command to delete a specific Shared Storage entry.\n\n        Args:\n            owner_origin: The owner origin of the Shared Storage.\n            key: The key of the entry to delete.\n\n        Returns:\n            DeleteSharedStorageEntryCommand: The CDP command to delete the Shared Storage entry.\n        \"\"\"\n        params = DeleteSharedStorageEntryParams(ownerOrigin=owner_origin, key=key)\n        return Command(method=StorageMethod.DELETE_SHARED_STORAGE_ENTRY, params=params)\n\n    @staticmethod\n    def delete_storage_bucket(bucket: StorageBucket) -> DeleteStorageBucketCommand:\n        \"\"\"\n        Generates a command to delete a Storage Bucket with the specified key and name.\n\n        Storage Buckets are an experimental API for managing storage data with\n        greater granularity and expiration control.\n\n        Args:\n            bucket: A StorageBucket object containing the storageKey and name of the bucket\n                to delete.\n\n        Returns:\n            DeleteStorageBucketCommand: The CDP command to delete the Storage Bucket.\n        \"\"\"\n        params = DeleteStorageBucketParams(bucket=bucket)\n        return Command(method=StorageMethod.DELETE_STORAGE_BUCKET, params=params)\n\n    @staticmethod\n    def get_affected_urls_for_third_party_cookie_metadata(\n        first_party_url: str, third_party_urls: list[str]\n    ) -> GetAffectedUrlsForThirdPartyCookieMetadataCommand:\n        \"\"\"\n        Generates a command to get the list of URLs from a page and its embedded resources\n        that match existing grace period URL pattern rules.\n\n        This command is useful for monitoring which URLs would be affected by the\n        Privacy Sandbox's third-party cookie policies.\n\n        Args:\n            first_party_url: The URL of the page being visited (first-party).\n            third_party_urls: Optional list of embedded third-party resource URLs.\n\n        Returns:\n            GetAffectedUrlsForThirdPartyCookieMetadataCommand: The CDP command to get URLs\n                affected by third-party cookie metadata.\n        \"\"\"\n        params = GetAffectedUrlsForThirdPartyCookieMetadataParams(\n            firstPartyUrl=first_party_url, thirdPartyUrls=third_party_urls\n        )\n        return Command(\n            method=StorageMethod.GET_AFFECTED_URLS_FOR_THIRD_PARTY_COOKIE_METADATA, params=params\n        )\n\n    @staticmethod\n    def get_interest_group_details(owner_origin: str, name: str) -> GetInterestGroupDetailsCommand:\n        \"\"\"\n        Generates a command to get details of a specific interest group.\n\n        Interest Groups are part of the FLEDGE/Protected Audience API for privacy-preserving\n        advertising, enabling in-browser ad auctions.\n\n        Args:\n            owner_origin: The owner origin of the interest group.\n            name: The name of the interest group.\n\n        Returns:\n            GetInterestGroupDetailsCommand: The CDP command to get interest group details.\n        \"\"\"\n        params = GetInterestGroupDetailsParams(ownerOrigin=owner_origin, name=name)\n        return Command(method=StorageMethod.GET_INTEREST_GROUP_DETAILS, params=params)\n\n    @staticmethod\n    def get_related_website_sets() -> GetRelatedWebsiteSetsCommand:\n        \"\"\"\n        Generates a command to get related website sets.\n\n        Related Website Sets are an API that allows sites under the same entity\n        to share some data, despite third-party cookie restrictions.\n\n        Returns:\n            GetRelatedWebsiteSetsCommand: The CDP command to get related website sets.\n        \"\"\"\n        return Command(method=StorageMethod.GET_RELATED_WEBSITE_SETS)\n\n    @staticmethod\n    def get_shared_storage_entries(owner_origin: str) -> GetSharedStorageEntriesCommand:\n        \"\"\"\n        Generates a command to get all Shared Storage entries for an origin.\n\n        Args:\n            owner_origin: The owner origin of the Shared Storage.\n\n        Returns:\n            GetSharedStorageEntriesCommand: The CDP command to get the Shared Storage entries.\n        \"\"\"\n        params = GetSharedStorageEntriesParams(ownerOrigin=owner_origin)\n        return Command(method=StorageMethod.GET_SHARED_STORAGE_ENTRIES, params=params)\n\n    @staticmethod\n    def get_shared_storage_metadata(owner_origin: str) -> GetSharedStorageMetadataCommand:\n        \"\"\"\n        Generates a command to get Shared Storage metadata for an origin.\n\n        Metadata includes information such as usage, budget, and creation time.\n\n        Args:\n            owner_origin: The owner origin of the Shared Storage.\n\n        Returns:\n            GetSharedStorageMetadataCommand: The CDP command to get Shared Storage metadata.\n        \"\"\"\n        params = GetSharedStorageMetadataParams(ownerOrigin=owner_origin)\n        return Command(method=StorageMethod.GET_SHARED_STORAGE_METADATA, params=params)\n\n    @staticmethod\n    def get_trust_tokens() -> GetTrustTokensCommand:\n        \"\"\"\n        Generates a command to get all available Trust Tokens.\n\n        Returns:\n            GetTrustTokensCommand: The CDP command to get Trust Tokens, which will return pairs\n                    of issuer origin and count of available tokens.\n        \"\"\"\n        return Command(method=StorageMethod.GET_TRUST_TOKENS, params={})\n\n    @staticmethod\n    def override_quota_for_origin(\n        origin: str, quota_size: Optional[float] = None\n    ) -> OverrideQuotaForOriginCommand:\n        \"\"\"\n        Generates a command to override the storage quota for a specific origin.\n\n        This command is useful for storage exhaustion testing or simulating\n        different storage conditions.\n\n        Args:\n            origin: The origin for which to override the quota.\n            quota_size: The size of the new quota in bytes (optional).\n                       If not specified, any existing override will be removed.\n\n        Returns:\n            OverrideQuotaForOriginCommand: The CDP command to override the origin's quota.\n        \"\"\"\n        params = OverrideQuotaForOriginParams(origin=origin)\n        if quota_size is not None:\n            params['quotaSize'] = quota_size\n        return Command(method=StorageMethod.OVERRIDE_QUOTA_FOR_ORIGIN, params=params)\n\n    @staticmethod\n    def reset_shared_storage_budget(owner_origin: str) -> ResetSharedStorageBudgetCommand:\n        \"\"\"\n        Generates a command to reset the Shared Storage budget for an origin.\n\n        Shared Storage uses a budget system to limit the amount of operations\n        or specific operations to preserve user privacy.\n\n        Args:\n            owner_origin: The owner origin of the Shared Storage.\n\n        Returns:\n            ResetSharedStorageBudgetCommand: The CDP command to reset the Shared Storage budget.\n        \"\"\"\n        params = ResetSharedStorageBudgetParams(ownerOrigin=owner_origin)\n        return Command(method=StorageMethod.RESET_SHARED_STORAGE_BUDGET, params=params)\n\n    @staticmethod\n    def run_bounce_tracking_mitigations() -> RunBounceTrackingMitigationsCommand:\n        \"\"\"\n        Generates a command to run bounce tracking mitigations.\n\n        Bounce tracking is a tracking technique that involves redirecting users\n        through intermediate URLs to establish tracking cookies.\n        This command activates protections against this technique.\n\n        Returns:\n            RunBounceTrackingMitigationsCommand: The CDP command to run bounce tracking mitigations.\n        \"\"\"\n        return Command(method=StorageMethod.RUN_BOUNCE_TRACKING_MITIGATIONS, params={})\n\n    @staticmethod\n    def send_pending_attribution_reports() -> SendPendingAttributionReportsCommand:\n        \"\"\"\n        Generates a command to send pending attribution reports.\n\n        Attribution Reporting is an API that allows measuring conversions while\n        preserving user privacy. This command forces sending reports that\n        are waiting to be sent.\n\n        Returns:\n            SendPendingAttributionReportsCommand: The CDP command to send pending attribution\n                reports.\n        \"\"\"\n        return Command(method=StorageMethod.SEND_PENDING_ATTRIBUTION_REPORTS, params={})\n\n    @staticmethod\n    def set_attribution_reporting_local_testing_mode(\n        enabled: bool,\n    ) -> SetAttributionReportingLocalTestingModeCommand:\n        \"\"\"\n        Generates a command to enable or disable local testing mode for Attribution Reporting.\n\n        Testing mode makes it easier to develop and test the Attribution Reporting API\n        by removing restrictions like delays and rate limits that would normally apply.\n\n        Args:\n            enabled: True to enable local testing mode, False to disable it.\n\n        Returns:\n            SetAttributionReportingLocalTestingModeCommand: The CDP command to set Attribution\n                Reporting local testing mode.\n        \"\"\"\n        params = SetAttributionReportingLocalTestingModeParams(enabled=enabled)\n        return Command(\n            method=StorageMethod.SET_ATTRIBUTION_REPORTING_LOCAL_TESTING_MODE, params=params\n        )\n\n    @staticmethod\n    def set_attribution_reporting_tracking(enable: bool) -> SetAttributionReportingTrackingCommand:\n        \"\"\"\n        Generates a command to enable or disable Attribution Reporting tracking.\n\n        Args:\n            enable: True to enable tracking, False to disable it.\n\n        Returns:\n            SetAttributionReportingTrackingCommand: The CDP command to set Attribution\n                Reporting tracking.\n        \"\"\"\n        params = SetAttributionReportingTrackingParams(enable=enable)\n        return Command(method=StorageMethod.SET_ATTRIBUTION_REPORTING_TRACKING, params=params)\n\n    @staticmethod\n    def set_interest_group_auction_tracking(enable: bool) -> SetInterestGroupAuctionTrackingCommand:\n        \"\"\"\n        Generates a command to enable or disable interest group auction tracking.\n\n        Interest group auctions are part of the FLEDGE/Protected Audience API and\n        allow for in-browser ad auctions in a privacy-preserving way.\n\n        Args:\n            enable: True to enable tracking, False to disable it.\n\n        Returns:\n            SetInterestGroupAuctionTrackingCommand: The CDP command to set interest group\n                auction tracking.\n        \"\"\"\n        params = SetInterestGroupAuctionTrackingParams(enable=enable)\n        return Command(method=StorageMethod.SET_INTEREST_GROUP_AUCTION_TRACKING, params=params)\n\n    @staticmethod\n    def set_interest_group_tracking(enable: bool) -> SetInterestGroupTrackingCommand:\n        \"\"\"\n        Generates a command to enable or disable interest group tracking.\n\n        Args:\n            enable: True to enable tracking, False to disable it.\n\n        Returns:\n            SetInterestGroupTrackingCommand: The CDP command to set interest group tracking.\n        \"\"\"\n        params = SetInterestGroupTrackingParams(enable=enable)\n        return Command(method=StorageMethod.SET_INTEREST_GROUP_TRACKING, params=params)\n\n    @staticmethod\n    def set_shared_storage_entry(\n        owner_origin: str, key: str, value: str, ignore_if_present: Optional[bool] = None\n    ) -> SetSharedStorageEntryCommand:\n        \"\"\"\n        Generates a command to set an entry in Shared Storage.\n\n        Args:\n            owner_origin: The owner origin of the Shared Storage.\n            key: The key of the entry to set.\n            value: The value of the entry to set.\n            ignore_if_present: If True, won't replace an existing entry with the same key.\n\n        Returns:\n            SetSharedStorageEntryCommand: The CDP command to set a Shared Storage entry.\n        \"\"\"\n        params = SetSharedStorageEntryParams(ownerOrigin=owner_origin, key=key, value=value)\n        if ignore_if_present is not None:\n            params['ignoreIfPresent'] = ignore_if_present\n        return Command(method=StorageMethod.SET_SHARED_STORAGE_ENTRY, params=params)\n\n    @staticmethod\n    def set_shared_storage_tracking(enable: bool) -> SetSharedStorageTrackingCommand:\n        \"\"\"\n        Generates a command to enable or disable Shared Storage tracking.\n\n        When enabled, events related to Shared Storage usage will be emitted.\n\n        Args:\n            enable: True to enable tracking, False to disable it.\n\n        Returns:\n            SetSharedStorageTrackingCommand: The CDP command to set Shared Storage tracking.\n        \"\"\"\n        params = SetSharedStorageTrackingParams(enable=enable)\n        return Command(method=StorageMethod.SET_SHARED_STORAGE_TRACKING, params=params)\n\n    @staticmethod\n    def set_storage_bucket_tracking(\n        storage_key: str, enable: bool\n    ) -> SetStorageBucketTrackingCommand:\n        \"\"\"\n        Generates a command to enable or disable Storage Bucket tracking.\n\n        When enabled, events related to changes in storage buckets will be emitted.\n\n        Args:\n            storage_key: The storage key for which to set tracking.\n            enable: True to enable tracking, False to disable it.\n\n        Returns:\n            SetStorageBucketTrackingCommand: The CDP command to set Storage Bucket tracking.\n        \"\"\"\n        params = SetStorageBucketTrackingParams(storageKey=storage_key, enable=enable)\n        return Command(method=StorageMethod.SET_STORAGE_BUCKET_TRACKING, params=params)\n"
  },
  {
    "path": "pydoll/commands/target_commands.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.protocol.base import Command\nfrom pydoll.protocol.target.methods import (\n    ActivateTargetParams,\n    AttachToBrowserTargetParams,\n    AttachToTargetParams,\n    CloseTargetParams,\n    CreateBrowserContextParams,\n    CreateTargetParams,\n    DetachFromTargetParams,\n    DisposeBrowserContextParams,\n    GetTargetInfoParams,\n    GetTargetsParams,\n    SetAutoAttachParams,\n    SetDiscoverTargetsParams,\n    SetRemoteLocationsParams,\n    TargetMethod,\n)\n\nif TYPE_CHECKING:\n    from pydoll.protocol.browser.types import WindowState\n    from pydoll.protocol.target.methods import (\n        ActivateTargetCommand,\n        AttachToBrowserTargetCommand,\n        AttachToTargetCommand,\n        CloseTargetCommand,\n        CreateBrowserContextCommand,\n        CreateTargetCommand,\n        DetachFromTargetCommand,\n        DisposeBrowserContextCommand,\n        GetBrowserContextsCommand,\n        GetTargetInfoCommand,\n        GetTargetsCommand,\n        SetAutoAttachCommand,\n        SetDiscoverTargetsCommand,\n        SetRemoteLocationsCommand,\n    )\n    from pydoll.protocol.target.types import RemoteLocation\n\n\nclass TargetCommands:\n    \"\"\"\n    A class for managing browser targets using Chrome DevTools Protocol.\n\n    The Target domain of CDP supports additional targets discovery and allows to attach to them.\n    Targets can represent browser tabs, windows, frames, web workers, service workers, etc.\n    The domain provides methods to create, discover, and control these targets.\n\n    This class provides methods to create commands for interacting with browser targets,\n    including creating, activating, attaching to, and closing targets through CDP commands.\n    \"\"\"\n\n    @staticmethod\n    def activate_target(target_id: str) -> ActivateTargetCommand:\n        \"\"\"\n        Generates a command to activate (focus) a target.\n\n        Args:\n            target_id: ID of the target to activate.\n\n        Returns:\n            Command: The CDP command to activate the target.\n        \"\"\"\n        params = ActivateTargetParams(targetId=target_id)\n        return Command(method=TargetMethod.ACTIVATE_TARGET, params=params)\n\n    @staticmethod\n    def attach_to_target(target_id: str, flatten: Optional[bool] = None) -> AttachToTargetCommand:\n        \"\"\"\n        Generates a command to attach to a target with the given ID.\n\n        When attached to a target, you can send commands to it and receive events from it.\n        This is essential for controlling and automating targets like browser tabs.\n\n        Args:\n            target_id: ID of the target to attach to.\n            flatten: If true, enables \"flat\" access to the session via specifying sessionId\n                    attribute in the commands. This is recommended as the non-flattened\n                    mode is being deprecated. See https://crbug.com/991325\n\n        Returns:\n            Command: The CDP command to attach to the target, which will return a sessionId.\n        \"\"\"\n        params = AttachToTargetParams(targetId=target_id)\n        if flatten is not None:\n            params['flatten'] = flatten\n        return Command(method=TargetMethod.ATTACH_TO_TARGET, params=params)\n\n    @staticmethod\n    def close_target(target_id: str) -> CloseTargetCommand:\n        \"\"\"\n        Generates a command to close a target.\n\n        If the target is a page or a tab, it will be closed. This is equivalent to\n        clicking the close button on a browser tab.\n\n        Args:\n            target_id: ID of the target to close.\n\n        Returns:\n            Command: The CDP command to close the target, which will return a success flag.\n        \"\"\"\n        params = CloseTargetParams(targetId=target_id)\n        return Command(method=TargetMethod.CLOSE_TARGET, params=params)\n\n    @staticmethod\n    def create_browser_context(\n        dispose_on_detach: Optional[bool] = None,\n        proxy_server: Optional[str] = None,\n        proxy_bypass_list: Optional[str] = None,\n        origins_with_universal_network_access: Optional[list[str]] = None,\n    ) -> CreateBrowserContextCommand:\n        \"\"\"\n        Generates a command to create a new empty browser context.\n\n        A browser context is similar to an incognito profile but you can have more than one.\n        Each context has its own set of cookies, local storage, and other browser data.\n        This is useful for testing multiple users or isolating sessions.\n\n        Args:\n            dispose_on_detach: If specified, the context will be disposed when the\n                              debugging session disconnects.\n            proxy_server: Proxy server string, similar to the one passed to --proxy-server\n                         command line argument (e.g., \"socks5://192.168.1.100:1080\").\n            proxy_bypass_list: Proxy bypass list, similar to the one passed to\n                               --proxy-bypass-list command line argument\n                               (e.g., \"*.example.com,localhost\").\n            origins_with_universal_network_access: An optional list of origins to grant\n                                                  unlimited cross-origin access to.\n                                                  Parts of the URL other than those\n                                                  constituting origin are ignored.\n\n        Returns:\n            Command: The CDP command to create a browser context, which will return\n                    the ID of the created context.\n        \"\"\"\n        params = CreateBrowserContextParams()\n        if dispose_on_detach is not None:\n            params['disposeOnDetach'] = dispose_on_detach\n        if proxy_server is not None:\n            params['proxyServer'] = proxy_server\n        if proxy_bypass_list is not None:\n            params['proxyBypassList'] = proxy_bypass_list\n        if origins_with_universal_network_access is not None:\n            params['originsWithUniversalNetworkAccess'] = origins_with_universal_network_access\n        return Command(method=TargetMethod.CREATE_BROWSER_CONTEXT, params=params)\n\n    @staticmethod\n    def create_target(\n        url: str = 'about:blank',\n        left: Optional[int] = None,\n        top: Optional[int] = None,\n        width: Optional[int] = None,\n        height: Optional[int] = None,\n        window_state: Optional[WindowState] = None,\n        browser_context_id: Optional[str] = None,\n        enable_begin_frame_control: Optional[bool] = None,\n        new_window: Optional[bool] = None,\n        background: Optional[bool] = None,\n        for_tab: Optional[bool] = None,\n        hidden: Optional[bool] = None,\n    ) -> CreateTargetCommand:\n        \"\"\"\n        Generates a command to create a new page (target).\n\n        This is one of the primary methods to open a new tab or window with specific\n        properties such as position, size, and browser context.\n\n        Args:\n            url: The initial URL the page will navigate to. An empty string indicates about:blank.\n            left: Frame left position in device-independent pixels (DIP).\n                 Requires newWindow to be true or in headless mode.\n            top: Frame top position in DIP. Requires newWindow to be true or in headless mode.\n            width: Frame width in DIP.\n            height: Frame height in DIP.\n            window_state: Frame window state: normal, minimized, maximized, or fullscreen.\n                         Default is normal.\n            browser_context_id: The browser context to create the page in.\n                               If not specified, the default browser context is used.\n            enable_begin_frame_control: Whether BeginFrames for this target will be controlled\n                                       via DevTools (headless shell only, not supported on\n                                       MacOS yet, false by default).\n            new_window: Whether to create a new window or tab (false by default,\n                       not supported by headless shell).\n            background: Whether to create the target in background or foreground\n                       (false by default, not supported by headless shell).\n            for_tab: Whether to create the target of type \"tab\".\n            hidden: Whether to create a hidden target. The hidden target is observable via\n                   protocol, but not present in the tab UI strip. Cannot be created with\n                   forTab:true, newWindow:true or background:false. The life-time of the\n                   tab is limited to the life-time of the session.\n\n        Returns:\n            Command: The CDP command to create a target, which will return the ID\n                of the created target.\n        \"\"\"\n        params = CreateTargetParams(url=url)\n        if left is not None:\n            params['left'] = left\n        if top is not None:\n            params['top'] = top\n        if width is not None:\n            params['width'] = width\n        if height is not None:\n            params['height'] = height\n        if window_state is not None:\n            params['windowState'] = window_state\n        if browser_context_id is not None:\n            params['browserContextId'] = browser_context_id\n        if enable_begin_frame_control is not None:\n            params['enableBeginFrameControl'] = enable_begin_frame_control\n        if new_window is not None:\n            params['newWindow'] = new_window\n        if background is not None:\n            params['background'] = background\n        if for_tab is not None:\n            params['forTab'] = for_tab\n        if hidden is not None:\n            params['hidden'] = hidden\n        return Command(method=TargetMethod.CREATE_TARGET, params=params)\n\n    @staticmethod\n    def detach_from_target(session_id: Optional[str] = None) -> DetachFromTargetCommand:\n        \"\"\"\n        Generates a command to detach a session from its target.\n\n        After detaching, you will no longer receive events from the target and\n        cannot send commands to it.\n\n        Args:\n            session_id: Session ID to detach. If not specified, detaches all sessions.\n\n        Returns:\n            Command: The CDP command to detach from the target.\n        \"\"\"\n        params = DetachFromTargetParams()\n        if session_id is not None:\n            params['sessionId'] = session_id\n        return Command(method=TargetMethod.DETACH_FROM_TARGET, params=params)\n\n    @staticmethod\n    def dispose_browser_context(browser_context_id: str) -> DisposeBrowserContextCommand:\n        \"\"\"\n        Generates a command to delete a browser context.\n\n        All pages belonging to the browser context will be closed without calling\n        their beforeunload hooks. This is similar to closing an incognito profile.\n\n        Args:\n            browser_context_id: The ID of the browser context to dispose.\n\n        Returns:\n            Command: The CDP command to dispose the browser context.\n        \"\"\"\n        params = DisposeBrowserContextParams(browserContextId=browser_context_id)\n        return Command(method=TargetMethod.DISPOSE_BROWSER_CONTEXT, params=params)\n\n    @staticmethod\n    def get_browser_contexts() -> GetBrowserContextsCommand:\n        \"\"\"\n        Generates a command to get all browser contexts created with createBrowserContext.\n\n        This is useful for obtaining a list of all available contexts for managing\n        multiple isolated browser sessions.\n\n        Returns:\n            Command: The CDP command to get all browser contexts, which will return\n                    an array of browser context IDs.\n        \"\"\"\n        return Command(method=TargetMethod.GET_BROWSER_CONTEXTS, params={})\n\n    @staticmethod\n    def get_targets(filter: Optional[list] = None) -> GetTargetsCommand:\n        \"\"\"\n        Generates a command to retrieve a list of available targets.\n\n        Targets include tabs, extensions, web workers, and other attachable entities\n        in the browser. This is useful for discovering what targets exist before\n        attaching to them.\n\n        Args:\n            filter: Only targets matching the filter will be reported. If filter is not\n                   specified and target discovery is currently enabled, a filter used for\n                   target discovery is used for consistency.\n\n        Returns:\n            Command: The CDP command to get targets, which will return a list of\n                    TargetInfo objects with details about each target.\n        \"\"\"\n        params = GetTargetsParams()\n        if filter is not None:\n            params['filter'] = filter\n        return Command(method=TargetMethod.GET_TARGETS, params=params)\n\n    @staticmethod\n    def set_auto_attach(\n        auto_attach: bool,\n        wait_for_debugger_on_start: bool = False,\n        flatten: Optional[bool] = None,\n        filter: Optional[list] = None,\n    ) -> SetAutoAttachCommand:\n        \"\"\"\n        Generates a command to control whether to automatically attach to new targets.\n\n        This method controls whether to automatically attach to new targets which are\n        considered to be directly related to the current one (for example, iframes or workers).\n        When turned on, it also attaches to all existing related targets. When turned off,\n        it automatically detaches from all currently attached targets.\n\n        Args:\n            auto_attach: Whether to auto-attach to related targets.\n            wait_for_debugger_on_start: Whether to pause new targets when attaching to them.\n                                       Use Runtime.runIfWaitingForDebugger to run paused targets.\n            flatten: Enables \"flat\" access to the session via specifying sessionId attribute\n                    in the commands. This mode is being preferred, and non-flattened mode\n                    is being deprecated (see crbug.com/991325).\n            filter: Only targets matching filter will be attached.\n\n        Returns:\n            Command: The CDP command to set auto-attach behavior.\n        \"\"\"\n        params = SetAutoAttachParams(\n            autoAttach=auto_attach, waitForDebuggerOnStart=wait_for_debugger_on_start\n        )\n        if flatten is not None:\n            params['flatten'] = flatten\n        if filter is not None:\n            params['filter'] = filter\n        return Command(method=TargetMethod.SET_AUTO_ATTACH, params=params)\n\n    @staticmethod\n    def set_discover_targets(\n        discover: bool, filter: Optional[list] = None\n    ) -> SetDiscoverTargetsCommand:\n        \"\"\"\n        Generates a command to control target discovery.\n\n        This method controls whether to discover available targets and notify via\n        targetCreated/targetInfoChanged/targetDestroyed events. Target discovery is useful\n        for monitoring when new tabs, workers, or other targets are created or destroyed.\n\n        Args:\n            discover: Whether to discover available targets.\n            filter: Only targets matching filter will be discovered. If discover is false,\n                   filter must be omitted or empty.\n\n        Returns:\n            Command: The CDP command to set target discovery.\n        \"\"\"\n        params = SetDiscoverTargetsParams(discover=discover)\n        if filter is not None:\n            params['filter'] = filter\n        return Command(method=TargetMethod.SET_DISCOVER_TARGETS, params=params)\n\n    @staticmethod\n    def attach_to_browser_target(session_id: str) -> AttachToBrowserTargetCommand:\n        \"\"\"\n        Generates a command to attach to the browser target.\n\n        This is an experimental method that attaches to the browser target,\n        only using flat sessionId mode. The browser target is a special target that\n        represents the browser itself rather than a page or other content.\n\n        Args:\n            session_id: ID of the session to attach to the browser target.\n\n        Returns:\n            Command: The CDP command to attach to the browser target,\n                    which will return a new session ID.\n        \"\"\"\n        params = AttachToBrowserTargetParams(sessionId=session_id)\n        return Command(method=TargetMethod.ATTACH_TO_BROWSER_TARGET, params=params)\n\n    @staticmethod\n    def get_target_info(target_id: str) -> GetTargetInfoCommand:\n        \"\"\"\n        Generates a command to get information about a specific target.\n\n        This experimental method returns detailed information about a target,\n        such as its type, URL, title, and other properties.\n\n        Args:\n            target_id: ID of the target to get information about.\n\n        Returns:\n            Command: The CDP command to get target information, which will return\n                    a TargetInfo object with details about the target.\n        \"\"\"\n        params = GetTargetInfoParams(targetId=target_id)\n        return Command(method=TargetMethod.GET_TARGET_INFO, params=params)\n\n    @staticmethod\n    def set_remote_locations(locations: list[RemoteLocation]) -> SetRemoteLocationsCommand:\n        \"\"\"\n        Generates a command to enable target discovery for specified remote locations.\n\n        This experimental method enables target discovery for remote locations when\n        setDiscoverTargets was set to true. This is useful for discovering targets\n        on remote devices or in different browser instances.\n\n        Args:\n            locations: list of remote locations, each containing a host and port.\n\n        Returns:\n            Command: The CDP command to set remote locations for target discovery.\n        \"\"\"\n        params = SetRemoteLocationsParams(locations=locations)\n        return Command(method=TargetMethod.SET_REMOTE_LOCATIONS, params=params)\n"
  },
  {
    "path": "pydoll/connection/__init__.py",
    "content": "from pydoll.connection.connection_handler import ConnectionHandler\n\n__all__ = [\n    'ConnectionHandler',\n]\n"
  },
  {
    "path": "pydoll/connection/connection_handler.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nfrom contextlib import suppress\nfrom typing import TYPE_CHECKING, cast\n\nimport websockets\nfrom websockets.asyncio.client import ClientConnection\nfrom websockets.protocol import State\n\nfrom pydoll.connection.managers import CommandsManager, EventsManager\nfrom pydoll.exceptions import (\n    CommandExecutionTimeout,\n    WebSocketConnectionClosed,\n)\nfrom pydoll.protocol.base import CDPEvent, Response\nfrom pydoll.utils import get_browser_ws_address\n\nif TYPE_CHECKING:\n    from typing import Any, AsyncGenerator, Awaitable, Callable, Coroutine, Optional, Union\n\n    from websockets.asyncio.client import connect as Connect\n\n    from pydoll.protocol.base import Command, T_CommandParams, T_CommandResponse\n\nlogger = logging.getLogger(__name__)\n\n\nclass ConnectionHandler:\n    \"\"\"\n    WebSocket connection manager for Chrome DevTools Protocol endpoints.\n\n    Handles connection lifecycle, command execution, and event subscription\n    for both browser-level and page-level CDP endpoints.\n    \"\"\"\n\n    def __init__(\n        self,\n        connection_port: Optional[int] = None,\n        page_id: Optional[str] = None,\n        ws_address_resolver: Callable[[int], Coroutine[Any, Any, str]] = get_browser_ws_address,\n        ws_connector: type[Connect] = websockets.connect,\n        ws_address: Optional[str] = None,\n    ):\n        \"\"\"\n        Initialize connection handler.\n\n        Args:\n            connection_port: Browser's debugging server port.\n            page_id: Target page ID. If None, connects to browser-level endpoint.\n            ws_address_resolver: Function to resolve WebSocket URL from port.\n            ws_connector: WebSocket connection factory (mainly for testing).\n            ws_address: WebSocket address. It has priority over connection_port and page_id.\n        \"\"\"\n        self._connection_port = connection_port\n        self._page_id = page_id\n        self._ws_address_resolver = ws_address_resolver\n        self._ws_connector = ws_connector\n        self._ws_address = ws_address\n        self._ws_connection: Optional[ClientConnection] = None\n        self._command_manager = CommandsManager()\n        self._events_handler = EventsManager()\n        self._receive_task: Optional[asyncio.Task] = None\n        logger.info('ConnectionHandler initialized.')\n        logger.debug(\n            f'Init params: port={self._connection_port}, page_id={self._page_id}, '\n            f'ws_address_set={bool(self._ws_address)}'\n        )\n\n    @property\n    def network_logs(self):\n        \"\"\"Access captured network request and response logs.\"\"\"\n        return self._events_handler.network_logs\n\n    @property\n    def dialog(self):\n        \"\"\"Access currently active JavaScript dialog information.\"\"\"\n        return self._events_handler.dialog\n\n    async def ping(self) -> bool:\n        \"\"\"Test if WebSocket connection is active and responsive.\"\"\"\n        with suppress(Exception):\n            logger.debug('Pinging WebSocket connection')\n            await self._ensure_active_connection()\n            await cast(ClientConnection, self._ws_connection).ping()\n            logger.debug('Ping OK')\n            return True\n        return False\n\n    async def execute_command(\n        self, command: Command[T_CommandParams, T_CommandResponse], timeout: int = 60\n    ) -> T_CommandResponse:\n        \"\"\"\n        Send CDP command and await response.\n\n        Args:\n            command: CDP command to send.\n            timeout: Maximum seconds to wait for response.\n\n        Returns:\n            Parsed response object matching command's expected type.\n\n        Raises:\n            CommandExecutionTimeout: If browser doesn't respond within timeout.\n            WebSocketConnectionClosed: If connection closes during execution.\n        \"\"\"\n        await self._ensure_active_connection()\n        future = self._command_manager.create_command_future(command)\n        command_str = json.dumps(command)\n\n        try:\n            ws = cast(ClientConnection, self._ws_connection)\n            logger.debug(\n                f'Sending command: id={command.get(\"id\")}, method={command.get(\"method\")}, '\n                f'timeout={timeout}s'\n            )\n            start = asyncio.get_event_loop().time()\n            await ws.send(command_str)\n            response: str = await asyncio.wait_for(future, timeout)\n            elapsed = asyncio.get_event_loop().time() - start\n            logger.debug(f'Command completed: id={command.get(\"id\")} in {elapsed:.3f}s')\n            return json.loads(response)\n        except asyncio.TimeoutError:\n            self._command_manager.remove_pending_command(command['id'])\n            logger.error(\n                f'Command timeout: id={command.get(\"id\")}, method={command.get(\"method\")}, '\n                f'timeout={timeout}s'\n            )\n            raise CommandExecutionTimeout()\n        except websockets.ConnectionClosed:\n            await self._handle_connection_loss()\n            logger.warning(f'WebSocket connection closed during command: id={command.get(\"id\")}')\n            raise WebSocketConnectionClosed()\n\n    async def register_callback(\n        self,\n        event_name: str,\n        callback: Callable[[dict], Awaitable[None]],\n        temporary: bool = False,\n    ) -> int:\n        \"\"\"\n        Register event listener for CDP events.\n\n        Args:\n            event_name: CDP event name (e.g., 'Page.loadEventFired').\n            callback: Async function called when event occurs.\n            temporary: If True, callback removed after first trigger.\n\n        Returns:\n            Callback ID for later removal.\n\n        Note:\n            Corresponding CDP domain must be enabled before events fire.\n        \"\"\"\n        callback_id = self._events_handler.register_callback(event_name, callback, temporary)\n        logger.debug(\n            f'Registered callback: id={callback_id}, event={event_name}, temporary={temporary}'\n        )\n        return callback_id\n\n    async def remove_callback(self, callback_id: int) -> bool:\n        \"\"\"Remove registered event callback by ID.\"\"\"\n        removed = self._events_handler.remove_callback(callback_id)\n        logger.debug(f'Removed callback: id={callback_id}, removed={removed}')\n        return removed\n\n    async def clear_callbacks(self):\n        \"\"\"Remove all registered event callbacks.\"\"\"\n        logger.debug('Clearing all callbacks')\n        self._events_handler.clear_callbacks()\n\n    async def close(self):\n        \"\"\"Close WebSocket connection and release resources.\"\"\"\n        await self.clear_callbacks()\n        if self._ws_connection is None:\n            logger.debug('Close called but no active WebSocket connection')\n            return\n\n        with suppress(websockets.ConnectionClosed):\n            await self._ws_connection.close()\n        logger.info('WebSocket connection closed.')\n\n    async def _ensure_active_connection(self):\n        \"\"\"Ensure active connection exists, establishing new one if needed.\"\"\"\n        if self._ws_connection is None or self._ws_connection.state is State.CLOSED:\n            logger.debug('No active WebSocket connection; establishing new one')\n            await self._establish_new_connection()\n\n    async def _establish_new_connection(self):\n        \"\"\"Create fresh WebSocket connection and start event listening.\"\"\"\n        ws_address = await self._resolve_ws_address()\n        logger.info(f'Connecting to {ws_address}')\n        self._ws_connection = await self._ws_connector(\n            ws_address,\n            max_size=1024 * 1024 * 10,  # 10MB\n        )\n        self._receive_task = asyncio.create_task(self._receive_events())\n        logger.debug('WebSocket connection established')\n\n    async def _resolve_ws_address(self):\n        \"\"\"Determine correct WebSocket address based on page ID.\"\"\"\n        if self._ws_address:\n            logger.debug('Using provided WebSocket address')\n            return self._ws_address\n        if not self._page_id:\n            resolved = await self._ws_address_resolver(self._connection_port)\n            logger.debug(f'Resolved browser-level WebSocket address: {resolved}')\n            return resolved\n        address = f'ws://localhost:{self._connection_port}/devtools/page/{self._page_id}'\n        logger.debug(f'Resolved page-level WebSocket address: {address}')\n        return address\n\n    async def _handle_connection_loss(self):\n        \"\"\"Clean up resources after connection loss.\"\"\"\n        if self._ws_connection and self._ws_connection.state is not State.CLOSED:\n            await self._ws_connection.close()\n        self._ws_connection = None\n\n        if self._receive_task and not self._receive_task.done():\n            self._receive_task.cancel()\n\n        logger.info('Connection resources cleaned up')\n\n    async def _receive_events(self):\n        \"\"\"Main loop for receiving and processing WebSocket messages.\"\"\"\n        try:\n            async for raw_message in self._incoming_messages():\n                await self._process_single_message(raw_message)\n        except websockets.ConnectionClosed as e:\n            logger.info(f'Connection closed gracefully: {e}')\n        except Exception as e:\n            logger.error(f'Unexpected error in event loop: {e}')\n            raise\n\n    async def _incoming_messages(self) -> AsyncGenerator[Union[str, bytes], None]:\n        \"\"\"Generator yielding raw messages from WebSocket connection.\"\"\"\n        ws = cast(ClientConnection, self._ws_connection)\n\n        while ws.state is not State.CLOSED:\n            yield await ws.recv()\n\n    async def _process_single_message(self, raw_message: str):\n        \"\"\"Process single raw WebSocket message.\"\"\"\n        message = self._parse_message(raw_message)\n        if not message:\n            return\n\n        if self._is_command_response(message):\n            message = cast(Response, message)\n            await self._handle_command_message(message)\n        else:\n            message = cast(CDPEvent, message)\n            await self._handle_event_message(message)\n\n    @staticmethod\n    def _parse_message(raw_message: str) -> Union[CDPEvent, Response, None]:\n        \"\"\"Parse raw message string into JSON object.\"\"\"\n        try:\n            return json.loads(raw_message)\n        except json.JSONDecodeError:\n            logger.warning(f'Failed to parse message: {raw_message[:200]}...')\n            return None\n\n    @staticmethod\n    def _is_command_response(message: Union[CDPEvent, Response]) -> bool:\n        \"\"\"Determine if message is command response or event notification.\"\"\"\n        return 'id' in message and isinstance(message.get('id'), int)\n\n    async def _handle_command_message(self, message: Response):\n        \"\"\"Process command response messages.\"\"\"\n        logger.debug(f'Processing command response: {message.get(\"id\")}')\n        self._command_manager.resolve_command(message['id'], json.dumps(message))\n\n    async def _handle_event_message(self, message: CDPEvent):\n        \"\"\"Process event notification messages.\"\"\"\n        event_type = message.get('method', 'unknown-event')\n        logger.debug(f'Processing {event_type} event')\n        await self._events_handler.process_event(message)\n\n    def __repr__(self):\n        \"\"\"String representation for debugging.\"\"\"\n        return f'ConnectionHandler(port={self._connection_port})'\n\n    def __str__(self):\n        \"\"\"User-friendly string representation.\"\"\"\n        return f'ConnectionHandler(port={self._connection_port})'\n\n    async def __aenter__(self):\n        \"\"\"Async context manager entry.\"\"\"\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        \"\"\"Async context manager exit with cleanup.\"\"\"\n        await self.close()\n"
  },
  {
    "path": "pydoll/connection/managers/__init__.py",
    "content": "from pydoll.connection.managers.commands_manager import CommandsManager\nfrom pydoll.connection.managers.events_manager import EventsManager\n\n__all__ = [\n    'CommandsManager',\n    'EventsManager',\n]\n"
  },
  {
    "path": "pydoll/connection/managers/commands_manager.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport logging\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from pydoll.protocol.base import Command\n\nlogger = logging.getLogger(__name__)\n\n\nclass CommandsManager:\n    \"\"\"\n    Manages command lifecycle and ID assignment for CDP commands.\n\n    Handles command future creation, ID generation, and response resolution\n    for asynchronous command execution.\n    \"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize command manager with empty state.\"\"\"\n        self._pending_commands: dict[int, asyncio.Future] = {}\n        self._id = 1\n        logger.debug('CommandsManager initialized')\n\n    def create_command_future(self, command: Command) -> asyncio.Future:\n        \"\"\"\n        Create future for command and assign unique ID.\n\n        Args:\n            command: Command to prepare for execution.\n\n        Returns:\n            Future that resolves when command completes.\n        \"\"\"\n        command['id'] = self._id\n        future = asyncio.Future()  # type: ignore\n        self._pending_commands[self._id] = future\n        self._id += 1\n        logger.debug(\n            f'Created future for command id={command[\"id\"]} method={command.get(\"method\")}'\n        )\n        return future\n\n    def resolve_command(self, response_id: int, result: str):\n        \"\"\"Resolve pending command with its result.\"\"\"\n        if response_id in self._pending_commands:\n            self._pending_commands[response_id].set_result(result)\n            del self._pending_commands[response_id]\n            logger.debug(f'Resolved command future id={response_id}')\n\n    def remove_pending_command(self, command_id: int):\n        \"\"\"Remove pending command without resolving (for timeouts/cancellations).\"\"\"\n        if command_id in self._pending_commands:\n            del self._pending_commands[command_id]\n            logger.debug(f'Removed pending command id={command_id}')\n"
  },
  {
    "path": "pydoll/connection/managers/events_manager.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport logging\nfrom typing import TYPE_CHECKING, cast\n\nfrom pydoll.protocol.page.events import (\n    JavascriptDialogOpeningEvent,\n    JavascriptDialogOpeningEventParams,\n)\n\nif TYPE_CHECKING:\n    from typing import Any, Callable\n\n    from pydoll.protocol.base import CDPEvent\n    from pydoll.protocol.network.events import RequestWillBeSentEvent\n\nlogger = logging.getLogger(__name__)\n\n\nclass EventsManager:\n    \"\"\"\n    Manages event callbacks, processing, and network logs.\n\n    Handles event callback registration, triggering, and maintains state\n    for network logs and dialog information.\n    \"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize events manager with empty state.\"\"\"\n        self._event_callbacks: dict[int, dict] = {}\n        self._callback_id = 0\n        self.network_logs: list[RequestWillBeSentEvent] = []\n        self.dialog = JavascriptDialogOpeningEvent()  # type: ignore\n        logger.info('EventsManager initialized')\n        logger.debug('Initial state: callbacks=0, logs=0, dialog=empty')\n\n    def register_callback(\n        self, event_name: str, callback: Callable[[dict], Any], temporary: bool = False\n    ) -> int:\n        \"\"\"\n        Register callback for specific event type.\n\n        Args:\n            event_name: Event name to listen for.\n            callback: Function called when event occurs.\n            temporary: If True, callback removed after first trigger.\n\n        Returns:\n            Callback ID for later removal.\n        \"\"\"\n        self._callback_id += 1\n        self._event_callbacks[self._callback_id] = {\n            'event': event_name,\n            'callback': callback,\n            'temporary': temporary,\n        }\n        logger.info(f\"Registered callback '{event_name}' with ID {self._callback_id}\")\n        logger.debug(\n            f'Callback details: temporary={temporary}, total_callbacks={len(self._event_callbacks)}'\n        )\n        return self._callback_id\n\n    def remove_callback(self, callback_id: int) -> bool:\n        \"\"\"Remove callback by ID.\"\"\"\n        if callback_id not in self._event_callbacks:\n            logger.warning(f'Callback ID {callback_id} not found')\n            return False\n\n        del self._event_callbacks[callback_id]\n        logger.info(f'Removed callback ID {callback_id}')\n        logger.debug(f'Remaining callbacks: {len(self._event_callbacks)}')\n        return True\n\n    def clear_callbacks(self):\n        \"\"\"Remove all registered callbacks.\"\"\"\n        self._event_callbacks.clear()\n        logger.info('All callbacks cleared')\n        logger.debug('Callbacks store is now empty')\n\n    async def process_event(self, event_data: CDPEvent):\n        \"\"\"\n        Process received event and trigger callbacks.\n\n        Handles special events (network requests, dialogs) and updates\n        internal state before triggering registered callbacks.\n        \"\"\"\n        event_name = event_data['method']\n        logger.debug(f'Processing event: {event_name}')\n\n        if 'Network.requestWillBeSent' in event_name:\n            self._update_network_logs(event_data)\n\n        if 'Page.javascriptDialogOpening' in event_name:\n            self.dialog = JavascriptDialogOpeningEvent(\n                method=event_data['method'],\n                params=cast(JavascriptDialogOpeningEventParams, event_data['params']),\n            )\n\n        if 'Page.javascriptDialogClosed' in event_name:\n            self.dialog = JavascriptDialogOpeningEvent()  # type: ignore\n\n        await self._trigger_callbacks(event_name, event_data)\n\n    def _update_network_logs(self, event_data: RequestWillBeSentEvent):\n        \"\"\"Add network event to logs (keeps last 10000 entries).\"\"\"\n        self.network_logs.append(event_data)\n        self.network_logs = self.network_logs[-10000:]  # keep only last 10000 logs\n\n    async def _trigger_callbacks(self, event_name: str, event_data: CDPEvent):\n        \"\"\"Trigger all registered callbacks for event, removing temporary ones.\"\"\"\n        callbacks_to_remove = []\n\n        for cb_id, cb_data in list(self._event_callbacks.items()):\n            if cb_data['event'] == event_name:\n                try:\n                    if asyncio.iscoroutinefunction(cb_data['callback']):\n                        await cb_data['callback'](event_data)\n                    else:\n                        cb_data['callback'](event_data)\n                except Exception as e:\n                    logger.error(f'Error in callback {cb_id}: {str(e)}')\n\n                if cb_data['temporary']:\n                    callbacks_to_remove.append(cb_id)\n\n        for cb_id in callbacks_to_remove:\n            self.remove_callback(cb_id)\n        logger.debug(\n            f\"Triggered callbacks for '{event_name}'. Removed temporaries: {callbacks_to_remove}\"\n        )\n"
  },
  {
    "path": "pydoll/constants.py",
    "content": "from enum import Enum, auto\n\n\nclass By(str, Enum):\n    CSS_SELECTOR = 'css'\n    XPATH = 'xpath'\n    CLASS_NAME = 'class_name'\n    ID = 'id'\n    TAG_NAME = 'tag_name'\n    NAME = 'name'\n\n\nclass PageLoadState(str, Enum):\n    COMPLETE = 'complete'\n    INTERACTIVE = 'interactive'\n    LOADING = 'loading'\n\n\nclass ScrollPosition(str, Enum):\n    UP = 'up'\n    DOWN = 'down'\n    LEFT = 'left'\n    RIGHT = 'right'\n\n\nclass Scripts:\n    ELEMENT_VISIBLE = \"\"\"\n    function() {\n        const rect = this.getBoundingClientRect();\n        return (\n            rect.width > 0 && rect.height > 0\n            && getComputedStyle(this).visibility !== 'hidden'\n            && getComputedStyle(this).display !== 'none'\n        )\n    }\n    \"\"\"\n\n    ELEMENT_ON_TOP = \"\"\"\n    function() {\n        const rect = this.getBoundingClientRect();\n        const x = rect.x + rect.width / 2;\n        const y = rect.y + rect.height / 2;\n        const elementFromPoint = document.elementFromPoint(x, y);\n        if (!elementFromPoint) {\n            return false;\n        }\n        return elementFromPoint === this || this.contains(elementFromPoint);\n    }\n    \"\"\"\n\n    ELEMENT_INTERACTIVE = \"\"\"\n    function() {\n        const style = window.getComputedStyle(this);\n        const rect = this.getBoundingClientRect();\n        if (\n            rect.width <= 0 ||\n            rect.height <= 0 ||\n            style.visibility === 'hidden' ||\n            style.display === 'none' ||\n            style.pointerEvents === 'none'\n        ) {\n            return false;\n        }\n        const x = rect.x + rect.width / 2;\n        const y = rect.y + rect.height / 2;\n        const elementFromPoint = document.elementFromPoint(x, y);\n        if (!elementFromPoint || (elementFromPoint !== this && !this.contains(elementFromPoint))) {\n            return false;\n        }\n        if (this.disabled) {\n            return false;\n        }\n        return true;\n    }\n    \"\"\"\n\n    CLICK = \"\"\"\n    function(){\n        clicked = false;\n        this.addEventListener('click', function(){\n            clicked = true;\n        });\n        this.click();\n        return clicked;\n    }\n    \"\"\"\n\n    CLICK_OPTION_TAG = \"\"\"\n    function() {\n        var select = this && this.parentElement ? this.parentElement.closest('select') : null;\n        if (!select) { return false; }\n        for (var i = 0; i < select.options.length; i++) {\n            select.options[i].selected = false;\n        }\n        this.selected = true;\n        select.value = this.value;\n        select.dispatchEvent(new Event('input', { bubbles: true }));\n        select.dispatchEvent(new Event('change', { bubbles: true }));\n        return true;\n    }\n    \"\"\"\n\n    BOUNDS = \"\"\"\n    function() {\n        return JSON.stringify(this.getBoundingClientRect());\n    }\n    \"\"\"\n\n    FIND_RELATIVE_XPATH_ELEMENT = \"\"\"\n        function() {\n            return document.evaluate(\n                \"{escaped_value}\", this, null,\n                XPathResult.FIRST_ORDERED_NODE_TYPE, null\n            ).singleNodeValue;\n        }\n    \"\"\"\n\n    FIND_XPATH_ELEMENT = \"\"\"\n        var element = document.evaluate(\n            \"{escaped_value}\", document, null,\n            XPathResult.FIRST_ORDERED_NODE_TYPE, null\n        ).singleNodeValue;\n        element;\n    \"\"\"\n\n    FIND_RELATIVE_XPATH_ELEMENTS = \"\"\"\n        function() {\n            var elements = document.evaluate(\n                \"{escaped_value}\", this, null,\n                XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null\n            );\n            var results = [];\n            for (var i = 0; i < elements.snapshotLength; i++) {\n                results.push(elements.snapshotItem(i));\n            }\n            return results;\n        }\n    \"\"\"\n\n    FIND_XPATH_ELEMENTS = \"\"\"\n        var elements = document.evaluate(\n            \"{escaped_value}\", document, null,\n            XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null\n        );\n        var results = [];\n        for (var i = 0; i < elements.snapshotLength; i++) {\n            results.push(elements.snapshotItem(i));\n        }\n        results;\n    \"\"\"\n\n    QUERY_SELECTOR = 'document.querySelector(\"{selector}\");'\n\n    RELATIVE_QUERY_SELECTOR = \"\"\"\n        function() {\n            return this.querySelector(\"{selector}\");\n        }\n    \"\"\"\n\n    QUERY_SELECTOR_ALL = 'document.querySelectorAll(\"{selector}\");'\n\n    RELATIVE_QUERY_SELECTOR_ALL = \"\"\"\n        function() {\n            return this.querySelectorAll(\"{selector}\");\n        }\n    \"\"\"\n\n    GET_TEXT_BY_XPATH = \"\"\"\n        (() => {\n            const node = document.evaluate(\n                \"{escaped_value}\",\n                document,\n                null,\n                XPathResult.FIRST_ORDERED_NODE_TYPE,\n                null\n            ).singleNodeValue;\n            return node ? (node.textContent || \"\") : \"\";\n        })()\n    \"\"\"\n\n    GET_TEXT_BY_CSS = \"\"\"\n        (() => {\n            const el = document.querySelector(\"{selector}\");\n            return el ? (el.textContent || \"\") : \"\";\n        })()\n    \"\"\"\n\n    GET_PARENT_NODE = \"\"\"\n        function() {\n            return this.parentElement;\n        }\n    \"\"\"\n\n    GET_CHILDREN_NODE = \"\"\"\n        function() {{\n            function getChildrenUntilDepth(element, maxDepth, tagFilter = [], currentDepth = 1)\n            {{\n                if (currentDepth > maxDepth) return [];\n\n                const children = Array.from(element.children);\n                let filtered = tagFilter.length === 0\n                    ? children\n                : children.filter(child => tagFilter.includes(child.tagName.toLowerCase()));\n\n                let allDescendants = [...filtered];\n\n                for (let child of children)\n                {{\n                    allDescendants.push(\n                    ...getChildrenUntilDepth(child, maxDepth, tagFilter, currentDepth + 1)\n                    );\n                }}\n\n                return allDescendants;\n            }}\n\n            return getChildrenUntilDepth(this, {max_depth}, {tag_filter});\n        }}\n    \"\"\"\n\n    GET_SIBLINGS_NODE = \"\"\"\n        function() {{\n            function getSiblingsUntilDepth(element, tagFilter = [])\n            {{\n                const parent = element.parentElement;\n                const siblings = Array.from(parent.children);\n                let filtered = tagFilter.length === 0\n                    ? siblings.filter(child => child !== element)\n                : siblings.filter(child =>\n                    tagFilter.includes(child.tagName.toLowerCase()) && child !== element);\n\n                let allDescendants = [...filtered];\n\n                return allDescendants;\n            }}\n\n            return getSiblingsUntilDepth(this, {tag_filter});\n        }}\n    \"\"\"\n\n    MAKE_REQUEST = \"\"\"\n(async function() {{\n    async function makeRequest(url, options) {{\n        try {{\n            const response = await fetch(url, options, {{\n                credentials: 'include',\n            }});\n            const headers = {{}};\n            response.headers.forEach((value, key) => {{\n                headers[key] = value;\n            }});\n\n            // Extract cookies from set-cookie header\n            const cookies = document.cookie;\n            let text = await response.text();\n            const possiblePrefixes = [\")]}}'\\\\n\", \")]}}'\\\\n\", \")]}}\\\\n\"];\n            for (let prefix of possiblePrefixes) {{\n                if (text.startsWith(prefix)) {{\n                    text = text.substring(prefix.length);\n                    break;\n                }}\n            }}\n            let content, jsonData;\n            const contentType = response.headers.get('content-type') || '';\n\n            if (contentType.includes('application/json')) {{\n                try {{\n                    jsonData = JSON.parse(text);\n                    text = JSON.stringify(jsonData);\n                }} catch (e) {{\n                    jsonData = null;\n                    // Keep original text if parsing fails\n                }}\n                content = new TextEncoder().encode(text).buffer;\n            }} else {{\n                // For non-JSON, keep original text handling\n                content = new TextEncoder().encode(text).buffer;\n                jsonData = null;\n            }}\n\n            return {{\n                status: response.status,\n                ok: response.ok,\n                url: response.url,\n                headers: headers,\n                cookies: cookies,\n                content: Array.from(new Uint8Array(content)),\n                text: text,\n                json: jsonData\n            }};\n        }} catch (error) {{\n            return {{\n                error: error.toString(),\n                status: 0\n            }};\n        }}\n    }}\n\n    const url = {url};\n    const options = {options};\n    return await makeRequest(url, options);\n}})();\n\"\"\"\n\n    SCROLL_BY = \"\"\"\nnew Promise((resolve) => {{\n    const behavior = '{behavior}';\n    if (behavior === 'auto') {{\n        window.scrollBy({{\n            {axis}: {distance},\n            behavior: 'auto'\n        }});\n        resolve();\n    }} else {{\n        const onScrollEnd = () => {{\n            window.removeEventListener('scrollend', onScrollEnd);\n            resolve();\n        }};\n        window.addEventListener('scrollend', onScrollEnd);\n        window.scrollBy({{\n            {axis}: {distance},\n            behavior: 'smooth'\n        }});\n        setTimeout(() => {{\n            window.removeEventListener('scrollend', onScrollEnd);\n            resolve();\n        }}, 2000);\n    }}\n}});\n\"\"\"\n\n    SCROLL_TO_TOP = \"\"\"\nnew Promise((resolve) => {{\n    const behavior = '{behavior}';\n    if (behavior === 'auto') {{\n        window.scrollTo({{\n            top: 0,\n            behavior: 'auto'\n        }});\n        resolve();\n    }} else {{\n        const onScrollEnd = () => {{\n            window.removeEventListener('scrollend', onScrollEnd);\n            resolve();\n        }};\n        window.addEventListener('scrollend', onScrollEnd);\n        window.scrollTo({{\n            top: 0,\n            behavior: 'smooth'\n        }});\n        setTimeout(() => {{\n            window.removeEventListener('scrollend', onScrollEnd);\n            resolve();\n        }}, 2000);\n    }}\n}});\n\"\"\"\n\n    SCROLL_TO_BOTTOM = \"\"\"\nnew Promise((resolve) => {{\n    const behavior = '{behavior}';\n    if (behavior === 'auto') {{\n        window.scrollTo({{\n            top: document.body.scrollHeight,\n            behavior: 'auto'\n        }});\n        resolve();\n    }} else {{\n        const onScrollEnd = () => {{\n            window.removeEventListener('scrollend', onScrollEnd);\n            resolve();\n        }};\n        window.addEventListener('scrollend', onScrollEnd);\n        window.scrollTo({{\n            top: document.body.scrollHeight,\n            behavior: 'smooth'\n        }});\n        setTimeout(() => {{\n            window.removeEventListener('scrollend', onScrollEnd);\n            resolve();\n        }}, 2000);\n    }}\n}});\n\"\"\"\n\n    GET_SCROLL_Y = 'window.scrollY || window.pageYOffset || 0'\n\n    GET_REMAINING_SCROLL_TO_BOTTOM = \"\"\"\n(function() {\n    const scrollHeight = Math.max(\n        document.body.scrollHeight,\n        document.documentElement.scrollHeight\n    );\n    const clientHeight = window.innerHeight;\n    const scrollTop = window.scrollY || window.pageYOffset || 0;\n    return Math.max(0, scrollHeight - clientHeight - scrollTop);\n})()\n\"\"\"\n\n    GET_VIEWPORT_CENTER = 'JSON.stringify([window.innerWidth / 2, window.innerHeight / 2])'\n\n    INSERT_TEXT = \"\"\"\n    function() {\n        const el = this;\n        const text = arguments[0];\n\n        // Standard input/textarea\n        if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {\n            const start = el.selectionStart || el.value.length;\n            const end = el.selectionEnd || el.value.length;\n            const before = el.value.substring(0, start);\n            const after = el.value.substring(end);\n            el.value = before + text + after;\n            el.selectionStart = el.selectionEnd = start + text.length;\n            el.dispatchEvent(new Event('input', { bubbles: true }));\n            el.dispatchEvent(new Event('change', { bubbles: true }));\n            return true;\n        }\n\n        // ContentEditable elements\n        if (el.isContentEditable) {\n            el.focus();\n            const selection = window.getSelection();\n            const range = selection.getRangeAt(0);\n            range.deleteContents();\n            const textNode = document.createTextNode(text);\n            range.insertNode(textNode);\n            range.setStartAfter(textNode);\n            range.setEndAfter(textNode);\n            selection.removeAllRanges();\n            selection.addRange(range);\n            el.dispatchEvent(new Event('input', { bubbles: true }));\n            return true;\n        }\n\n        return false;\n    }\n    \"\"\"\n\n    CLEAR_INPUT = \"\"\"\n    function() {\n        const el = this;\n\n        // Standard input/textarea\n        if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {\n            el.value = '';\n            el.dispatchEvent(new Event('input', { bubbles: true }));\n            el.dispatchEvent(new Event('change', { bubbles: true }));\n            return true;\n        }\n\n        // ContentEditable elements\n        if (el.isContentEditable) {\n            el.focus();\n            el.innerHTML = '';\n            el.dispatchEvent(new Event('input', { bubbles: true }));\n            return true;\n        }\n\n        return false;\n    }\n    \"\"\"\n\n    IS_EDITABLE = \"\"\"\n    function() {\n        const el = this;\n\n        // Check standard input elements\n        if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {\n            return !el.disabled && !el.readOnly;\n        }\n\n        // Check contenteditable (including inherited)\n        let current = el;\n        while (current) {\n            if (current.isContentEditable) {\n                return true;\n            }\n            current = current.parentElement;\n        }\n\n        return false;\n    }\n    \"\"\"\n\n    IS_OPTION_TAG = \"\"\"\n    function() {\n        return !!(this && this.tagName && this.tagName.toLowerCase() === 'option');\n    }\n    \"\"\"\n\n\nclass Key(tuple[str, int], Enum):\n    BACKSPACE = ('Backspace', 8)\n    TAB = ('Tab', 9)\n    ENTER = ('Enter', 13)\n    SHIFT = ('Shift', 16)\n    CONTROL = ('Control', 17)\n    ALT = ('Alt', 18)\n    PAUSE = ('Pause', 19)\n    CAPSLOCK = ('CapsLock', 20)\n    ESCAPE = ('Escape', 27)\n    SPACE = ('Space', 32)\n    PAGEUP = ('PageUp', 33)\n    PAGEDOWN = ('PageDown', 34)\n    END = ('End', 35)\n    HOME = ('Home', 36)\n    ARROWLEFT = ('ArrowLeft', 37)\n    ARROWUP = ('ArrowUp', 38)\n    ARROWRIGHT = ('ArrowRight', 39)\n    ARROWDOWN = ('ArrowDown', 40)\n    PRINTSCREEN = ('PrintScreen', 44)\n    INSERT = ('Insert', 45)\n    DELETE = ('Delete', 46)\n\n    DIGIT0 = ('0', 48)\n    DIGIT1 = ('1', 49)\n    DIGIT2 = ('2', 50)\n    DIGIT3 = ('3', 51)\n    DIGIT4 = ('4', 52)\n    DIGIT5 = ('5', 53)\n    DIGIT6 = ('6', 54)\n    DIGIT7 = ('7', 55)\n    DIGIT8 = ('8', 56)\n    DIGIT9 = ('9', 57)\n\n    A = ('A', 65)\n    B = ('B', 66)\n    C = ('C', 67)\n    D = ('D', 68)\n    E = ('E', 69)\n    F = ('F', 70)\n    G = ('G', 71)\n    H = ('H', 72)\n    I = ('I', 73)  # noqa: E741\n    J = ('J', 74)\n    K = ('K', 75)\n    L = ('L', 76)\n    M = ('M', 77)\n    N = ('N', 78)\n    O = ('O', 79)  # noqa: E741\n    P = ('P', 80)\n    Q = ('Q', 81)\n    R = ('R', 82)\n    S = ('S', 83)\n    T = ('T', 84)\n    U = ('U', 85)\n    V = ('V', 86)\n    W = ('W', 87)\n    X = ('X', 88)\n    Y = ('Y', 89)\n    Z = ('Z', 90)\n\n    META = ('Meta', 91)\n    METARIGHT = ('MetaRight', 92)\n    CONTEXTMENU = ('ContextMenu', 93)\n\n    NUMPAD0 = ('Numpad0', 96)\n    NUMPAD1 = ('Numpad1', 97)\n    NUMPAD2 = ('Numpad2', 98)\n    NUMPAD3 = ('Numpad3', 99)\n    NUMPAD4 = ('Numpad4', 100)\n    NUMPAD5 = ('Numpad5', 101)\n    NUMPAD6 = ('Numpad6', 102)\n    NUMPAD7 = ('Numpad7', 103)\n    NUMPAD8 = ('Numpad8', 104)\n    NUMPAD9 = ('Numpad9', 105)\n    NUMPADMULTIPLY = ('NumpadMultiply', 106)\n    NUMPADADD = ('NumpadAdd', 107)\n    NUMPADSUBTRACT = ('NumpadSubtract', 109)\n    NUMPADDECIMAL = ('NumpadDecimal', 110)\n    NUMPADDIVIDE = ('NumpadDivide', 111)\n\n    F1 = ('F1', 112)\n    F2 = ('F2', 113)\n    F3 = ('F3', 114)\n    F4 = ('F4', 115)\n    F5 = ('F5', 116)\n    F6 = ('F6', 117)\n    F7 = ('F7', 118)\n    F8 = ('F8', 119)\n    F9 = ('F9', 120)\n    F10 = ('F10', 121)\n    F11 = ('F11', 122)\n    F12 = ('F12', 123)\n\n    NUMLOCK = ('NumLock', 144)\n    SCROLLLOCK = ('ScrollLock', 145)\n\n    SEMICOLON = ('Semicolon', 186)\n    EQUALSIGN = ('EqualSign', 187)\n    COMMA = ('Comma', 188)\n    MINUS = ('Minus', 189)\n    PERIOD = ('Period', 190)\n    SLASH = ('Slash', 191)\n    GRAVEACCENT = ('GraveAccent', 192)\n    BRACKETLEFT = ('BracketLeft', 219)\n    BACKSLASH = ('Backslash', 220)\n    BRACKETRIGHT = ('BracketRight', 221)\n    QUOTE = ('Quote', 222)\n\n\nclass BrowserType(Enum):\n    CHROME = auto()\n    EDGE = auto()\n\n\nclass TypoType(str, Enum):\n    \"\"\"Types of realistic typing errors.\"\"\"\n\n    ADJACENT = 'adjacent'\n    TRANSPOSE = 'transpose'\n    DOUBLE = 'double'\n    SKIP = 'skip'\n    MISSED_SPACE = 'missed_space'\n\n\nDEFAULT_TYPO_PROBABILITY = 0.02\n\n\n# Mapping from typeable character to (key, code, keycode).\n# key:     DOM KeyboardEvent.key value\n# code:    DOM KeyboardEvent.code (physical key on US QWERTY)\n# keycode: legacy KeyboardEvent.keyCode / virtual key code\n#\n# Lowercase and uppercase letters share the same code/keycode; only `key` differs.\n# Shifted symbol variants (e.g. '!' from '1') use the base key's code/keycode.\nCHAR_TO_KEY_INFO: dict[str, tuple[str, str, int]] = {\n    # Letters (lowercase)\n    'a': ('a', 'KeyA', 65),\n    'b': ('b', 'KeyB', 66),\n    'c': ('c', 'KeyC', 67),\n    'd': ('d', 'KeyD', 68),\n    'e': ('e', 'KeyE', 69),\n    'f': ('f', 'KeyF', 70),\n    'g': ('g', 'KeyG', 71),\n    'h': ('h', 'KeyH', 72),\n    'i': ('i', 'KeyI', 73),\n    'j': ('j', 'KeyJ', 74),\n    'k': ('k', 'KeyK', 75),\n    'l': ('l', 'KeyL', 76),\n    'm': ('m', 'KeyM', 77),\n    'n': ('n', 'KeyN', 78),\n    'o': ('o', 'KeyO', 79),\n    'p': ('p', 'KeyP', 80),\n    'q': ('q', 'KeyQ', 81),\n    'r': ('r', 'KeyR', 82),\n    's': ('s', 'KeyS', 83),\n    't': ('t', 'KeyT', 84),\n    'u': ('u', 'KeyU', 85),\n    'v': ('v', 'KeyV', 86),\n    'w': ('w', 'KeyW', 87),\n    'x': ('x', 'KeyX', 88),\n    'y': ('y', 'KeyY', 89),\n    'z': ('z', 'KeyZ', 90),\n    # Letters (uppercase)\n    'A': ('A', 'KeyA', 65),\n    'B': ('B', 'KeyB', 66),\n    'C': ('C', 'KeyC', 67),\n    'D': ('D', 'KeyD', 68),\n    'E': ('E', 'KeyE', 69),\n    'F': ('F', 'KeyF', 70),\n    'G': ('G', 'KeyG', 71),\n    'H': ('H', 'KeyH', 72),\n    'I': ('I', 'KeyI', 73),\n    'J': ('J', 'KeyJ', 74),\n    'K': ('K', 'KeyK', 75),\n    'L': ('L', 'KeyL', 76),\n    'M': ('M', 'KeyM', 77),\n    'N': ('N', 'KeyN', 78),\n    'O': ('O', 'KeyO', 79),\n    'P': ('P', 'KeyP', 80),\n    'Q': ('Q', 'KeyQ', 81),\n    'R': ('R', 'KeyR', 82),\n    'S': ('S', 'KeyS', 83),\n    'T': ('T', 'KeyT', 84),\n    'U': ('U', 'KeyU', 85),\n    'V': ('V', 'KeyV', 86),\n    'W': ('W', 'KeyW', 87),\n    'X': ('X', 'KeyX', 88),\n    'Y': ('Y', 'KeyY', 89),\n    'Z': ('Z', 'KeyZ', 90),\n    # Digits\n    '0': ('0', 'Digit0', 48),\n    '1': ('1', 'Digit1', 49),\n    '2': ('2', 'Digit2', 50),\n    '3': ('3', 'Digit3', 51),\n    '4': ('4', 'Digit4', 52),\n    '5': ('5', 'Digit5', 53),\n    '6': ('6', 'Digit6', 54),\n    '7': ('7', 'Digit7', 55),\n    '8': ('8', 'Digit8', 56),\n    '9': ('9', 'Digit9', 57),\n    # Shifted digits (symbols on number row)\n    ')': (')', 'Digit0', 48),\n    '!': ('!', 'Digit1', 49),\n    '@': ('@', 'Digit2', 50),\n    '#': ('#', 'Digit3', 51),\n    '$': ('$', 'Digit4', 52),\n    '%': ('%', 'Digit5', 53),\n    '^': ('^', 'Digit6', 54),\n    '&': ('&', 'Digit7', 55),\n    '*': ('*', 'Digit8', 56),\n    '(': ('(', 'Digit9', 57),\n    # Punctuation and symbols (unshifted)\n    ' ': (' ', 'Space', 32),\n    '-': ('-', 'Minus', 189),\n    '=': ('=', 'Equal', 187),\n    '[': ('[', 'BracketLeft', 219),\n    ']': (']', 'BracketRight', 221),\n    '\\\\': ('\\\\', 'Backslash', 220),\n    ';': (';', 'Semicolon', 186),\n    \"'\": (\"'\", 'Quote', 222),\n    '`': ('`', 'Backquote', 192),\n    ',': (',', 'Comma', 188),\n    '.': ('.', 'Period', 190),\n    '/': ('/', 'Slash', 191),\n    # Punctuation and symbols (shifted)\n    '_': ('_', 'Minus', 189),\n    '+': ('+', 'Equal', 187),\n    '{': ('{', 'BracketLeft', 219),\n    '}': ('}', 'BracketRight', 221),\n    '|': ('|', 'Backslash', 220),\n    ':': (':', 'Semicolon', 186),\n    '\"': ('\"', 'Quote', 222),\n    '~': ('~', 'Backquote', 192),\n    '<': ('<', 'Comma', 188),\n    '>': ('>', 'Period', 190),\n    '?': ('?', 'Slash', 191),\n    # Whitespace\n    '\\n': ('Enter', 'Enter', 13),\n    '\\t': ('Tab', 'Tab', 9),\n}\n\n\nQWERTY_NEIGHBORS: dict[str, list[str]] = {\n    '1': ['2', 'q'],\n    '2': ['1', '3', 'q', 'w'],\n    '3': ['2', '4', 'w', 'e'],\n    '4': ['3', '5', 'e', 'r'],\n    '5': ['4', '6', 'r', 't'],\n    '6': ['5', '7', 't', 'y'],\n    '7': ['6', '8', 'y', 'u'],\n    '8': ['7', '9', 'u', 'i'],\n    '9': ['8', '0', 'i', 'o'],\n    '0': ['9', '-', 'o', 'p'],\n    '-': ['0', '=', 'p', '['],\n    '=': ['-', '[', ']'],\n    'q': ['1', '2', 'w', 'a', 's'],\n    'w': ['q', '2', '3', 'e', 'a', 's', 'd'],\n    'e': ['w', '3', '4', 'r', 's', 'd', 'f'],\n    'r': ['e', '4', '5', 't', 'd', 'f', 'g'],\n    't': ['r', '5', '6', 'y', 'f', 'g', 'h'],\n    'y': ['t', '6', '7', 'u', 'g', 'h', 'j'],\n    'u': ['y', '7', '8', 'i', 'h', 'j', 'k'],\n    'i': ['u', '8', '9', 'o', 'j', 'k', 'l'],\n    'o': ['i', '9', '0', 'p', 'k', 'l', ';'],\n    'p': ['o', '0', '-', '[', 'l', ';', \"'\"],\n    '[': ['p', '-', '=', ']', ';', \"'\"],\n    ']': ['[', '=', \"'\"],\n    'a': ['q', 'w', 's', 'z', 'x'],\n    's': ['q', 'w', 'e', 'a', 'd', 'z', 'x', 'c'],\n    'd': ['w', 'e', 'r', 's', 'f', 'x', 'c', 'v'],\n    'f': ['e', 'r', 't', 'd', 'g', 'c', 'v', 'b'],\n    'g': ['r', 't', 'y', 'f', 'h', 'v', 'b', 'n'],\n    'h': ['t', 'y', 'u', 'g', 'j', 'b', 'n', 'm'],\n    'j': ['y', 'u', 'i', 'h', 'k', 'n', 'm', ','],\n    'k': ['u', 'i', 'o', 'j', 'l', 'm', ',', '.'],\n    'l': ['i', 'o', 'p', 'k', ';', ',', '.', '/'],\n    ';': ['o', 'p', '[', 'l', \"'\", '.', '/'],\n    \"'\": ['p', '[', ']', ';', '/'],\n    'z': ['a', 's', 'x'],\n    'x': ['z', 'a', 's', 'd', 'c'],\n    'c': ['x', 's', 'd', 'f', 'v'],\n    'v': ['c', 'd', 'f', 'g', 'b'],\n    'b': ['v', 'f', 'g', 'h', 'n'],\n    'n': ['b', 'g', 'h', 'j', 'm'],\n    'm': ['n', 'h', 'j', 'k', ','],\n    ',': ['m', 'j', 'k', 'l', '.'],\n    '.': [',', 'k', 'l', ';', '/'],\n    '/': ['.', 'l', ';', \"'\"],\n    ' ': ['c', 'v', 'b', 'n', 'm'],\n}\n"
  },
  {
    "path": "pydoll/decorators.py",
    "content": "import asyncio\nimport logging\nimport traceback\nfrom functools import wraps\nfrom typing import Any, Callable, Coroutine, List, Optional, Type, TypeVar, Union\n\nlogger = logging.getLogger(__name__)\n\nT = TypeVar('T')\n\n\nclass RetryConfig:\n    def __init__(\n        self,\n        max_retries: int = 5,\n        exceptions: Union[Type[Exception], List[Type[Exception]]] = Exception,\n        on_retry: Optional[Callable] = None,\n        delay: float = 0,\n        exponential_backoff: bool = False,\n    ):\n        self.max_retries = max_retries\n        self.exceptions = exceptions\n        self.on_retry = on_retry\n        self.delay = delay\n        self.exponential_backoff = exponential_backoff\n\n    def calculate_delay(self, attempt: int) -> float:\n        if not self.delay:\n            return 0\n        return self.delay * (2**attempt if self.exponential_backoff else 1)\n\n    async def call_callback(self, caller_instance: Any) -> None:\n        if not self.on_retry:\n            return\n\n        try:\n            await self.on_retry(caller_instance)\n        except TypeError as e:\n            error_msg = str(e)\n            if (\n                'takes 1 positional argument but 2 were given' in error_msg\n                or 'takes 0 positional arguments but 1 was given' in error_msg\n            ):\n                try:\n                    await self.on_retry()\n                    return\n                except Exception as e_inner:\n                    raise e_inner\n            raise e\n        except Exception as e:\n            raise e\n\n    async def handle_delay(self, attempt: int) -> None:\n        \"\"\"\n        Wait for delay.\n\n        Args:\n            attempt (int): The current attempt number\n        \"\"\"\n        wait_time = self.calculate_delay(attempt)\n        if wait_time:\n            await asyncio.sleep(wait_time)\n\n    def is_matching_exception(self, exc: Exception) -> bool:\n        if isinstance(self.exceptions, (list, tuple)):\n            return any(isinstance(exc, e) for e in self.exceptions)\n        return isinstance(exc, self.exceptions)\n\n\ndef retry(\n    max_retries: int = 5,\n    exceptions: Union[Type[Exception], List[Type[Exception]]] = Exception,\n    on_retry: Optional[Callable] = None,\n    delay: float = 0,\n    exponential_backoff: bool = False,\n    exception_to_raise: Optional[Exception] = None,\n):\n    \"\"\"\n    Decorator to try to execute a function again in case of exception.\n    For greater control, it is a good practice to specify the exceptions that should be handled.\n\n    Args:\n        max_retries (int): Maximum number of attempts\n        exceptions (Union[Type[Exception], List[Type[Exception]]]): Exception types that should be\n            handled\n        on_retry (Optional[Callable], optional): Function called after each failed attempt\n        delay (float): Delay between attempts in seconds\n        exponential_backoff (bool): If True, increase the delay exponentially\n\n    Usage:\n        @retry_on_exception(\n            max_retries=3,\n            exceptions=[ValueError, TypeError],\n            delay=1\n        )\n        def my_function():\n            ...\n    \"\"\"\n    config = RetryConfig(\n        max_retries=max_retries,\n        exceptions=exceptions,\n        on_retry=on_retry,\n        delay=delay,\n        exponential_backoff=exponential_backoff,\n    )\n\n    def decorator(\n        func: Callable[..., Coroutine[Any, Any, T]],\n    ) -> Callable[..., Coroutine[Any, Any, T]]:\n        @wraps(func)\n        async def wrapper(*args: Any, **kwargs: Any) -> T:\n            last_exception: Optional[Exception] = None\n            caller_instance = args[0] if args else None\n\n            for attempt in range(config.max_retries + 1):\n                try:\n                    return await func(*args, **kwargs)\n                except Exception as exc:\n                    logger.error(\n                        f'Error trying to execute the function {func.__name__}: '\n                        f'{traceback.format_exc()}'\n                    )\n                    if not config.is_matching_exception(exc):\n                        raise exc\n\n                    last_exception = exc\n\n                    if attempt < config.max_retries:\n                        await config.handle_delay(attempt + 1)\n                        await config.call_callback(caller_instance)\n                    continue\n\n            if last_exception is not None:\n                raise exception_to_raise or last_exception\n\n            raise RuntimeError('Unreachable: all retries exhausted without exception')\n\n        return wrapper\n\n    return decorator\n"
  },
  {
    "path": "pydoll/elements/__init__.py",
    "content": ""
  },
  {
    "path": "pydoll/elements/mixins/__init__.py",
    "content": "from pydoll.elements.mixins.find_elements_mixin import FindElementsMixin\n\n__all__ = [\n    'FindElementsMixin',\n]\n"
  },
  {
    "path": "pydoll/elements/mixins/find_elements_mixin.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport logging\nfrom typing import TYPE_CHECKING, Optional, Union, cast, overload\n\nfrom pydoll.commands import (\n    DomCommands,\n    RuntimeCommands,\n)\nfrom pydoll.connection.connection_handler import ConnectionHandler\nfrom pydoll.constants import By, Scripts\nfrom pydoll.elements.utils import SelectorParser\nfrom pydoll.exceptions import ElementNotFound, WaitElementTimeout\n\nif TYPE_CHECKING:\n    from typing import Literal, Optional, Union\n\n    from pydoll.elements.web_element import WebElement\n    from pydoll.interactions.iframe import IFrameContext\n    from pydoll.protocol.base import Command, T_CommandParams, T_CommandResponse\n    from pydoll.protocol.dom.methods import DescribeNodeResponse\n    from pydoll.protocol.dom.types import Node\n    from pydoll.protocol.runtime.methods import (\n        CallFunctionOnParams,\n        CallFunctionOnResponse,\n        EvaluateParams,\n        EvaluateResponse,\n        GetPropertiesResponse,\n    )\n\n\nlogger = logging.getLogger(__name__)\n\n\ndef create_web_element(*args, **kwargs):\n    \"\"\"\n    Create WebElement instance avoiding circular imports.\n\n    Factory method that dynamically imports WebElement at runtime\n    to prevent circular import dependencies.\n    \"\"\"\n    from pydoll.elements.web_element import WebElement  # noqa: PLC0415\n\n    return WebElement(*args, **kwargs)\n\n\nclass FindElementsMixin:\n    \"\"\"\n    Mixin providing comprehensive element finding and waiting capabilities.\n\n    Implements DOM element location using various selector strategies (CSS, XPath, etc.)\n    with support for single/multiple element finding and configurable waiting.\n    Classes using this mixin gain powerful element discovery without implementing\n    complex location logic themselves.\n    \"\"\"\n\n    _css_only: bool = False\n\n    if TYPE_CHECKING:\n        _connection_handler: ConnectionHandler\n\n    @staticmethod\n    def _build_text_expression(selector: str, method: str) -> Optional[str]:\n        \"\"\"\n        Build JS expression using Scripts to extract textContent based on selector type.\n        \"\"\"\n        return SelectorParser.build_text_expression(selector, method)\n\n    @overload\n    async def find(\n        self,\n        id: Optional[str] = ...,\n        class_name: Optional[str] = ...,\n        name: Optional[str] = ...,\n        tag_name: Optional[str] = ...,\n        text: Optional[str] = ...,\n        timeout: int = ...,\n        find_all: Literal[False] = False,\n        raise_exc: Literal[True] = True,\n        **attributes,\n    ) -> WebElement: ...\n\n    @overload\n    async def find(\n        self,\n        id: Optional[str] = ...,\n        class_name: Optional[str] = ...,\n        name: Optional[str] = ...,\n        tag_name: Optional[str] = ...,\n        text: Optional[str] = ...,\n        timeout: int = ...,\n        find_all: Literal[False] = False,\n        raise_exc: Literal[False] = False,\n        **attributes,\n    ) -> Optional[WebElement]: ...\n\n    @overload\n    async def find(\n        self,\n        id: Optional[str] = ...,\n        class_name: Optional[str] = ...,\n        name: Optional[str] = ...,\n        tag_name: Optional[str] = ...,\n        text: Optional[str] = ...,\n        timeout: int = ...,\n        find_all: Literal[True] = True,\n        raise_exc: Literal[True] = True,\n        **attributes,\n    ) -> list[WebElement]: ...\n\n    @overload\n    async def find(\n        self,\n        id: Optional[str] = ...,\n        class_name: Optional[str] = ...,\n        name: Optional[str] = ...,\n        tag_name: Optional[str] = ...,\n        text: Optional[str] = ...,\n        timeout: int = ...,\n        find_all: Literal[True] = True,\n        raise_exc: Literal[False] = False,\n        **attributes,\n    ) -> Optional[list[WebElement]]: ...\n\n    @overload\n    async def find(\n        self,\n        id: Optional[str] = ...,\n        class_name: Optional[str] = ...,\n        name: Optional[str] = ...,\n        tag_name: Optional[str] = ...,\n        text: Optional[str] = ...,\n        timeout: int = ...,\n        find_all: bool = ...,\n        raise_exc: bool = ...,\n        **attributes,\n    ) -> Union[WebElement, list[WebElement], None]: ...\n\n    async def find(\n        self,\n        id: Optional[str] = None,\n        class_name: Optional[str] = None,\n        name: Optional[str] = None,\n        tag_name: Optional[str] = None,\n        text: Optional[str] = None,\n        timeout: int = 0,\n        find_all: bool = False,\n        raise_exc: bool = True,\n        **attributes: dict[str, str],\n    ) -> Union[WebElement, list[WebElement], None]:\n        \"\"\"\n        Find element(s) using combination of common HTML attributes.\n\n        Flexible element location using standard attributes. Multiple attributes\n        can be combined for specific selectors (builds XPath when multiple specified).\n\n        Args:\n            id: Element ID attribute value.\n            class_name: CSS class name to match.\n            name: Element name attribute value.\n            tag_name: HTML tag name (e.g., \"div\", \"input\").\n            text: Text content to match within element.\n            timeout: Maximum seconds to wait for elements to appear.\n            find_all: If True, returns all matches; if False, first match only.\n            raise_exc: Whether to raise exception if no elements found.\n            **attributes: Additional HTML attributes to match.\n\n        Returns:\n            WebElement, list[WebElement], or None based on find_all and raise_exc.\n\n        Raises:\n            ValueError: If no search criteria provided.\n            ElementNotFound: If no elements found and raise_exc=True.\n            WaitElementTimeout: If timeout specified and no elements appear in time.\n            NotImplementedError: If called on a ShadowRoot (use query() with CSS instead).\n        \"\"\"\n        if self._css_only:\n            raise NotImplementedError(\n                'find() is not supported on ShadowRoot. Use query() with a CSS selector instead.'\n            )\n\n        logger.debug(\n            f'find() called with id={id}, class_name={class_name}, name={name}, '\n            f'tag_name={tag_name}, text={text}, timeout={timeout}, '\n            f'find_all={find_all}, raise_exc={raise_exc}, attrs={attributes}'\n        )\n        if not any([id, class_name, name, tag_name, text, *attributes.keys()]):\n            raise ValueError(\n                'At least one of the following arguments must be provided: id, '\n                'class_name, name, tag_name, text'\n            )\n\n        by_map = {\n            'id': By.ID,\n            'class_name': By.CLASS_NAME,\n            'name': By.NAME,\n            'tag_name': By.TAG_NAME,\n            'xpath': By.XPATH,\n        }\n        by, value = self._get_by_and_value(\n            by_map, id, class_name, name, tag_name, text, **attributes\n        )\n        logger.debug(f'find() resolved to by={by} value={value}')\n        return await self.find_or_wait_element(\n            by, value, timeout=timeout, find_all=find_all, raise_exc=raise_exc\n        )\n\n    @overload\n    async def query(\n        self,\n        expression: str,\n        timeout: int = ...,\n        find_all: Literal[False] = False,\n        raise_exc: Literal[True] = True,\n    ) -> WebElement: ...\n\n    @overload\n    async def query(\n        self,\n        expression: str,\n        timeout: int = ...,\n        find_all: Literal[False] = False,\n        raise_exc: Literal[False] = False,\n    ) -> Optional[WebElement]: ...\n\n    @overload\n    async def query(\n        self,\n        expression: str,\n        timeout: int = ...,\n        find_all: Literal[True] = True,\n        raise_exc: Literal[True] = True,\n    ) -> list[WebElement]: ...\n\n    @overload\n    async def query(\n        self,\n        expression: str,\n        timeout: int = ...,\n        find_all: Literal[True] = True,\n        raise_exc: Literal[False] = False,\n    ) -> Optional[list[WebElement]]: ...\n\n    @overload\n    async def query(\n        self,\n        expression: str,\n        timeout: int = ...,\n        find_all: bool = ...,\n        raise_exc: bool = ...,\n    ) -> Union[WebElement, list[WebElement], None]: ...\n\n    async def query(\n        self, expression: str, timeout: int = 0, find_all: bool = False, raise_exc: bool = True\n    ) -> Union[WebElement, list[WebElement], None]:\n        \"\"\"\n        Find element(s) using raw CSS selector or XPath expression.\n\n        Direct access using CSS or XPath syntax. Selector type automatically\n        determined based on expression pattern.\n\n        Args:\n            expression: Selector expression (CSS, XPath, ID with #, class with .).\n            timeout: Maximum seconds to wait for elements to appear.\n            find_all: If True, returns all matches; if False, first match only.\n            raise_exc: Whether to raise exception if no elements found.\n\n        Returns:\n            WebElement, list[WebElement], or None based on find_all and raise_exc.\n\n        Raises:\n            ElementNotFound: If no elements found and raise_exc=True.\n            WaitElementTimeout: If timeout specified and no elements appear in time.\n            NotImplementedError: If called with XPath on a ShadowRoot.\n        \"\"\"\n        if self._css_only and self._get_expression_type(expression) == By.XPATH:\n            raise NotImplementedError(\n                'XPath is not supported on ShadowRoot. Use a CSS selector instead.'\n            )\n\n        logger.debug(\n            f'query() called with expression={expression}, timeout={timeout}, '\n            f'find_all={find_all}, raise_exc={raise_exc}'\n        )\n        by = self._get_expression_type(expression)\n        logger.debug(f'query() resolved to by={by}')\n        return await self.find_or_wait_element(\n            by=by, value=expression, timeout=timeout, find_all=find_all, raise_exc=raise_exc\n        )\n\n    async def find_or_wait_element(\n        self,\n        by: By,\n        value: str,\n        timeout: int = 0,\n        find_all: bool = False,\n        raise_exc: bool = True,\n    ) -> Union[WebElement, list[WebElement], None]:\n        \"\"\"\n        Core element finding method with optional waiting capability.\n\n        Searches for elements with flexible waiting. If timeout specified,\n        repeatedly attempts to find elements with 0.5s delays until success or timeout.\n        Used by higher-level find() and query() methods.\n\n        Args:\n            by: Selector strategy (CSS_SELECTOR, XPATH, ID, etc.).\n            value: Selector value to locate element(s).\n            timeout: Maximum seconds to wait (0 = no waiting).\n            find_all: If True, returns all matches; if False, first match only.\n            raise_exc: Whether to raise exception if no elements found.\n\n        Returns:\n            WebElement, list[WebElement], or None based on find_all and raise_exc.\n\n        Raises:\n            ElementNotFound: If no elements found with timeout=0 and raise_exc=True.\n            WaitElementTimeout: If elements not found within timeout and raise_exc=True.\n        \"\"\"\n        logger.debug(\n            f'find_or_wait_element(): by={by}, value={value}, timeout={timeout}, '\n            f'find_all={find_all}, raise_exc={raise_exc}'\n        )\n\n        if by == By.XPATH:\n            segments = SelectorParser.parse_iframe_segments_xpath(value)\n        elif by == By.CSS_SELECTOR:\n            segments = SelectorParser.parse_iframe_segments_css(value)\n        else:\n            segments = [(by, value)]\n\n        if len(segments) > 1:\n            return await self._find_across_iframes(segments, timeout, find_all, raise_exc)\n\n        find_method = self._find_element if not find_all else self._find_elements\n        start_time = asyncio.get_event_loop().time()\n\n        if not timeout:\n            logger.debug('No timeout specified; performing single attempt')\n            return await find_method(by, value, raise_exc=raise_exc)\n\n        while True:\n            element = await find_method(by, value, raise_exc=False)\n            if element:\n                if isinstance(element, list):\n                    logger.debug(f'Found {len(element)} elements within timeout window')\n                else:\n                    logger.debug('Found 1 element within timeout window')\n                return element\n\n            if asyncio.get_event_loop().time() - start_time > timeout:\n                if raise_exc:\n                    logger.error('Timeout while waiting for elements')\n                    raise WaitElementTimeout(\n                        f'Timed out after {timeout}s waiting for element '\n                        f'(by={by.value}, value={value!r})'\n                    )\n                return None\n\n            await asyncio.sleep(0.5)\n\n    async def _find_across_iframes(\n        self,\n        segments: list[tuple[By, str]],\n        timeout: int,\n        find_all: bool,\n        raise_exc: bool,\n    ) -> Union[WebElement, list[WebElement], None]:\n        \"\"\"\n        Retry loop for iframe-crossing element searches.\n\n        Repeatedly calls :meth:`_attempt_find_across_iframes` until the target\n        element is found or the *timeout* expires.\n\n        Args:\n            segments: Ordered ``(By, selector)`` pairs — one per iframe boundary\n                plus a final selector for the target element(s).\n            timeout: Maximum seconds to wait (0 = single attempt).\n            find_all: If ``True``, the last segment uses ``_find_elements``.\n            raise_exc: Whether to raise on failure.\n\n        Returns:\n            The found element(s), or ``None`` / ``[]`` on failure.\n\n        Raises:\n            ElementNotFound: If ``timeout=0``, nothing found, and ``raise_exc=True``.\n            WaitElementTimeout: If timeout expires and ``raise_exc=True``.\n        \"\"\"\n        start_time = asyncio.get_event_loop().time()\n        selector_repr = ' -> '.join(seg for _, seg in segments)\n\n        while True:\n            result = await self._attempt_find_across_iframes(segments, find_all)\n            if result is not None and result != []:\n                return result\n\n            if not timeout:\n                if raise_exc:\n                    raise ElementNotFound(f'Element not found across iframes: {selector_repr}')\n                return [] if find_all else None\n\n            if asyncio.get_event_loop().time() - start_time > timeout:\n                if raise_exc:\n                    raise WaitElementTimeout(\n                        f'Timed out after {timeout}s waiting for element '\n                        f'across iframes: {selector_repr}'\n                    )\n                return [] if find_all else None\n\n            await asyncio.sleep(0.5)\n\n    async def _attempt_find_across_iframes(\n        self,\n        segments: list[tuple[By, str]],\n        find_all: bool,\n    ) -> Union[WebElement, list[WebElement], None]:\n        \"\"\"\n        Single attempt to walk iframe segments and find the target element.\n\n        For each intermediate segment, finds a single iframe element and uses it\n        as the search context for the next segment. The last segment respects\n        *find_all*.\n\n        Args:\n            segments: Ordered ``(By, selector)`` pairs.\n            find_all: Whether the final segment should return all matches.\n\n        Returns:\n            Found element(s) or ``None`` / ``[]`` if any intermediate step fails.\n        \"\"\"\n        current_context: FindElementsMixin = self\n        for i, (by, selector) in enumerate(segments):\n            is_last = i == len(segments) - 1\n            if is_last:\n                if find_all:\n                    result = await current_context._find_elements(by, selector, raise_exc=False)\n                    return result if result else []\n                return await current_context._find_element(by, selector, raise_exc=False)\n\n            element = await current_context._find_element(by, selector, raise_exc=False)\n            if not element or not getattr(element, 'is_iframe', False):\n                return None\n            current_context = element\n        return None\n\n    async def _find_element(\n        self, by: By, value: str, raise_exc: bool = True\n    ) -> Optional[WebElement]:\n        \"\"\"\n        Find first element matching selector.\n\n        Internal method performing actual element search. Can be called directly\n        for fine-grained control. Searches in document context or relative to\n        current element (when used from WebElement).\n\n        Args:\n            by: Selector strategy (CSS_SELECTOR, XPATH, ID, etc.).\n            value: Selector value to locate element.\n            raise_exc: Whether to raise ElementNotFound if not found.\n\n        Returns:\n            WebElement instance or None if not found and raise_exc=False.\n\n        Raises:\n            ElementNotFound: If element not found and raise_exc=True.\n        \"\"\"\n        logger.debug(f'_find_element(): by={by}, value={value}, raise_exc={raise_exc}')\n        iframe_context = None\n        if getattr(self, 'is_iframe', False):\n            element_self = cast('WebElement', self)\n            iframe_context = await element_self.iframe_context\n\n        if iframe_context:\n            command = self._get_find_element_command(\n                by,\n                value,\n                object_id=iframe_context.document_object_id or '',\n                execution_context_id=iframe_context.execution_context_id,\n            )\n        elif hasattr(self, '_object_id'):\n            command = self._get_find_element_command(by, value, self._object_id)\n        else:\n            command = self._get_find_element_command(by, value)\n\n        response_for_command: Union[\n            EvaluateResponse, CallFunctionOnResponse\n        ] = await self._execute_command(command)\n\n        if not self._has_object_id_key(response_for_command):\n            if raise_exc:\n                logger.debug('Element not found and raise_exc=True')\n                raise ElementNotFound()\n            return None\n\n        object_id = response_for_command['result']['result']['objectId']\n        attributes = await self._get_object_attributes(object_id=object_id)\n        logger.debug(f'_find_element() found object_id={object_id}')\n        element = create_web_element(\n            object_id,\n            self._connection_handler,\n            by,\n            value,\n            attributes,\n            mouse=getattr(self, '_mouse', None),\n        )\n        self._apply_iframe_context_to_element(\n            element, iframe_context or getattr(self, '_iframe_context', None)\n        )\n        return element\n\n    async def _find_elements(self, by: By, value: str, raise_exc: bool = True) -> list[WebElement]:\n        \"\"\"\n        Find all elements matching selector.\n\n        Internal method performing actual multi-element search. Can be called directly\n        for fine-grained control. Searches in document context or relative to\n        current element (when used from WebElement).\n\n        Args:\n            by: Selector strategy (CSS_SELECTOR, XPATH, ID, etc.).\n            value: Selector value to locate elements.\n            raise_exc: Whether to raise ElementNotFound if none found.\n\n        Returns:\n            list of WebElement instances (empty if none found and raise_exc=False).\n\n        Raises:\n            ElementNotFound: If no elements found and raise_exc=True.\n        \"\"\"\n        logger.debug(f'_find_elements(): by={by}, value={value}, raise_exc={raise_exc}')\n        iframe_context = None\n        if getattr(self, 'is_iframe', False):\n            element_self = cast('WebElement', self)\n            iframe_context = await element_self.iframe_context\n\n        if iframe_context:\n            command = self._get_find_elements_command(\n                by,\n                value,\n                object_id=iframe_context.document_object_id or '',\n                execution_context_id=iframe_context.execution_context_id,\n            )\n        elif hasattr(self, '_object_id'):\n            command = self._get_find_elements_command(by, value, self._object_id)\n        else:\n            command = self._get_find_elements_command(by, value)\n\n        response_for_command: Union[\n            EvaluateResponse, CallFunctionOnResponse\n        ] = await self._execute_command(command)\n\n        if not response_for_command.get('result', {}).get('result', {}).get('objectId'):\n            if raise_exc:\n                logger.debug('No elements found and raise_exc=True')\n                raise ElementNotFound()\n            return []\n\n        object_id = response_for_command['result']['result']['objectId']\n        query_response: GetPropertiesResponse = await self._execute_command(\n            RuntimeCommands.get_properties(object_id=object_id)\n        )\n        response: list[str] = []\n        for query in query_response['result']['result']:\n            if not (query['name'].isdigit() and 'objectId' in query['value']):\n                continue\n            response.append(query['value']['objectId'])\n\n        inherited_context = iframe_context or getattr(self, '_iframe_context', None)\n        elements = []\n        for object_id in response:\n            try:\n                node_description = await self._describe_node(object_id=object_id)\n            except KeyError:\n                continue\n\n            attributes = node_description.get('attributes', [])\n            tag_name = node_description.get('nodeName', '').lower()\n            attributes.extend(['tag_name', tag_name])\n\n            child = create_web_element(\n                object_id,\n                self._connection_handler,\n                by,\n                value,\n                attributes,\n                mouse=getattr(self, '_mouse', None),\n            )\n            self._apply_iframe_context_to_element(child, inherited_context)\n            elements.append(child)\n        logger.debug(f'_find_elements() returning {len(elements)} elements')\n        return elements\n\n    async def _get_object_attributes(self, object_id: str) -> list[str]:\n        \"\"\"\n        Get attributes of a DOM node.\n        \"\"\"\n        node_description = await self._describe_node(object_id=object_id)\n        if not node_description:\n            # If the node couldn't be described (e.g., object id doesn't reference a Node),\n            # return minimal attributes to keep the flow stable.\n            return ['tag_name', '']\n        attributes = node_description.get('attributes', [])\n        tag_name = node_description.get('nodeName', '').lower()\n        attributes.extend(['tag_name', tag_name])\n        return attributes\n\n    def _get_by_and_value(\n        self,\n        by_map: dict[str, By],\n        id: Optional[str] = None,\n        class_name: Optional[str] = None,\n        name: Optional[str] = None,\n        tag_name: Optional[str] = None,\n        text: Optional[str] = None,\n        **attributes,\n    ) -> tuple[By, str]:\n        \"\"\"\n        Determine appropriate selector strategy and value from provided arguments.\n\n        For single attribute: uses direct selector strategy.\n        For multiple attributes: builds XPath expression.\n        \"\"\"\n        logger.debug(\n            f'_get_by_and_value(): id={id}, class_name={class_name}, name={name}, '\n            f'tag_name={tag_name}, text={text}, attrs={attributes}'\n        )\n        xpath_raw = attributes.get('xpath')\n        if isinstance(xpath_raw, str) and xpath_raw:\n            logger.debug(f'Explicit XPath provided; using raw expression: {xpath_raw}')\n            return By.XPATH, xpath_raw\n\n        simple_selectors = {\n            'id': id,\n            'class_name': class_name,\n            'name': name,\n            'tag_name': tag_name,\n        }\n        provided_selectors = {key: value for key, value in simple_selectors.items() if value}\n\n        if len(provided_selectors) == 1 and not text and not attributes:\n            key, value = next(iter(provided_selectors.items()))\n            by = by_map[key]\n            logger.debug(f'Simple selector resolved: by={by}, value={value}')\n            return by, value\n\n        xpath = self._build_xpath(id, class_name, name, tag_name, text, **attributes)\n        logger.debug(f'Complex selector resolved to XPath: {xpath}')\n        return By.XPATH, xpath\n\n    @staticmethod\n    def _build_xpath(\n        id: Optional[str] = None,\n        class_name: Optional[str] = None,\n        name: Optional[str] = None,\n        tag_name: Optional[str] = None,\n        text: Optional[str] = None,\n        **attributes: str,\n    ) -> str:\n        \"\"\"\n        Build XPath expression from multiple attribute criteria.\n\n        Constructs complex XPath combining multiple conditions with 'and' operators.\n        Handles class names correctly for space-separated class lists.\n        Uses contains() for text matching (partial text support).\n\n        Note:\n            Attribute names with underscores are automatically converted to hyphens\n            to match HTML attribute naming conventions (e.g., data_test -> data-test).\n        \"\"\"\n        return SelectorParser.build_xpath(id, class_name, name, tag_name, text, **attributes)\n\n    @staticmethod\n    def _get_expression_type(expression: str) -> By:\n        \"\"\"\n        Auto-detect selector type from expression syntax.\n\n        Patterns:\n        - XPath: starts with ./, or /\n        - Default: CSS_SELECTOR\n        \"\"\"\n        return SelectorParser.get_expression_type(expression)\n\n    async def _describe_node(self, object_id: str = '') -> Node:\n        \"\"\"\n        Get detailed DOM node information using CDP DOM.describeNode.\n\n        Used internally to gather data for WebElement initialization.\n        \"\"\"\n        response: DescribeNodeResponse = await self._execute_command(\n            DomCommands.describe_node(object_id=object_id)\n        )\n        if 'error' in response:\n            # Return empty node structure when CDP reports that the objectId\n            # doesn't reference a Node or any other describe error occurs.\n            return {}\n        return response.get('result', {}).get('node', {})\n\n    def _apply_iframe_context_to_element(\n        self, element: WebElement, iframe_context: IFrameContext | None\n    ) -> None:\n        \"\"\"\n        Propagate iframe context to the newly created element.\n        - If the element is also an iframe, configure session routing.\n        - Otherwise, inject the iframe's own context.\n        \"\"\"\n        if not iframe_context:\n            return\n        if getattr(element, 'is_iframe', False):\n            routing_handler = iframe_context.session_handler or self._connection_handler\n            element._routing_session_handler = routing_handler\n            element._routing_session_id = iframe_context.session_id\n            element._routing_parent_frame_id = iframe_context.frame_id\n            return\n        element._iframe_context = iframe_context\n\n    def _resolve_routing(self) -> tuple[ConnectionHandler, Optional[str]]:\n        \"\"\"\n        Resolve handler and sessionId for the current context (iframe routed or default).\n        \"\"\"\n        iframe_context = getattr(self, '_iframe_context', None)\n        if iframe_context and getattr(iframe_context, 'session_handler', None):\n            return iframe_context.session_handler, getattr(iframe_context, 'session_id', None)\n        routing_handler = getattr(self, '_routing_session_handler', None)\n        if routing_handler is not None:\n            return routing_handler, getattr(self, '_routing_session_id', None)\n        return self._connection_handler, None\n\n    async def _execute_command(\n        self, command: Command[T_CommandParams, T_CommandResponse]\n    ) -> T_CommandResponse:\n        \"\"\"Execute CDP command via resolved handler (60s timeout).\"\"\"\n        handler, session_id = self._resolve_routing()\n        if session_id:\n            command['sessionId'] = session_id\n        return await handler.execute_command(command, timeout=60)\n\n    def _get_find_element_command(\n        self,\n        by: By,\n        value: str,\n        object_id: str = '',\n        execution_context_id: Optional[int] = None,\n    ):\n        \"\"\"\n        Create CDP command for finding single element.\n\n        Handles special cases for different selector types and contexts:\n        - CLASS_NAME/ID: converts to CSS selector\n        - Relative searches: uses different scripts for context element\n        - XPath: requires special handling\n        - NAME: converts to XPath expression\n        \"\"\"\n        escaped_value = value.replace('\"', '\\\\\"')\n        command: Union[\n            Command[CallFunctionOnParams, CallFunctionOnResponse],\n            Command[EvaluateParams, EvaluateResponse],\n        ]\n        match by:\n            case By.CLASS_NAME:\n                selector = f'.{escaped_value}'\n            case By.ID:\n                selector = f'#{escaped_value}'\n            case _:\n                selector = escaped_value\n        if object_id and not by == By.XPATH:\n            script = Scripts.RELATIVE_QUERY_SELECTOR.replace('{selector}', selector)\n            command = RuntimeCommands.call_function_on(\n                function_declaration=script,\n                object_id=object_id,\n                return_by_value=False,\n            )\n        elif by == By.XPATH:\n            command = self._get_find_element_by_xpath_command(\n                value, object_id=object_id, execution_context_id=execution_context_id\n            )\n        elif by == By.NAME:\n            command = self._get_find_element_by_xpath_command(\n                f'//*[@name=\"{escaped_value}\"]',\n                object_id=object_id,\n                execution_context_id=execution_context_id,\n            )\n        else:\n            command = RuntimeCommands.evaluate(\n                expression=Scripts.QUERY_SELECTOR.replace('{selector}', selector),\n                context_id=execution_context_id,\n            )\n        return command\n\n    def _get_find_elements_command(\n        self,\n        by: By,\n        value: str,\n        object_id: str = '',\n        execution_context_id: Optional[int] = None,\n    ):\n        \"\"\"\n        Create CDP command for finding multiple elements.\n\n        Similar to _get_find_element_command but for multiple element searches.\n        Handles same special cases and selector type conversions.\n        \"\"\"\n        escaped_value = value.replace('\"', '\\\\\"')\n        command: Union[\n            Command[CallFunctionOnParams, CallFunctionOnResponse],\n            Command[EvaluateParams, EvaluateResponse],\n        ]\n        match by:\n            case By.CLASS_NAME:\n                selector = f'.{escaped_value}'\n            case By.ID:\n                selector = f'#{escaped_value}'\n            case _:\n                selector = escaped_value\n        if object_id and not by == By.XPATH:\n            script = Scripts.RELATIVE_QUERY_SELECTOR_ALL.replace('{selector}', selector)\n            command = RuntimeCommands.call_function_on(\n                function_declaration=script,\n                object_id=object_id,\n                return_by_value=False,\n            )\n        elif by == By.XPATH:\n            command = self._get_find_elements_by_xpath_command(\n                value, object_id=object_id, execution_context_id=execution_context_id\n            )\n        else:\n            command = RuntimeCommands.evaluate(\n                expression=Scripts.QUERY_SELECTOR_ALL.replace('{selector}', selector),\n                context_id=execution_context_id,\n            )\n        return command\n\n    def _get_find_element_by_xpath_command(\n        self,\n        xpath: str,\n        object_id: str,\n        execution_context_id: Optional[int] = None,\n    ):\n        \"\"\"\n        Create CDP command specifically for XPath single element finding.\n\n        XPath requires special handling vs CSS selectors. Ensures relative\n        XPath for context-based searches.\n        \"\"\"\n        command: Union[\n            Command[CallFunctionOnParams, CallFunctionOnResponse],\n            Command[EvaluateParams, EvaluateResponse],\n        ]\n        escaped_value = xpath.replace('\"', '\\\\\"')\n        if object_id:\n            escaped_value = self._ensure_relative_xpath(escaped_value)\n            script = Scripts.FIND_RELATIVE_XPATH_ELEMENT.replace('{escaped_value}', escaped_value)\n            command = RuntimeCommands.call_function_on(\n                function_declaration=script,\n                object_id=object_id,\n                return_by_value=False,\n            )\n        else:\n            script = Scripts.FIND_XPATH_ELEMENT.replace('{escaped_value}', escaped_value)\n            command = RuntimeCommands.evaluate(expression=script, context_id=execution_context_id)\n        return command\n\n    def _get_find_elements_by_xpath_command(\n        self,\n        xpath: str,\n        object_id: str,\n        execution_context_id: Optional[int] = None,\n    ):\n        \"\"\"\n        Create CDP command specifically for XPath multiple element finding.\n\n        XPath requires special handling vs CSS selectors. Ensures relative\n        XPath for context-based searches.\n        \"\"\"\n        escaped_value = xpath.replace('\"', '\\\\\"')\n        command: Union[\n            Command[CallFunctionOnParams, CallFunctionOnResponse],\n            Command[EvaluateParams, EvaluateResponse],\n        ]\n        if object_id:\n            escaped_value = self._ensure_relative_xpath(escaped_value)\n            script = Scripts.FIND_RELATIVE_XPATH_ELEMENTS.replace('{escaped_value}', escaped_value)\n            command = RuntimeCommands.call_function_on(\n                function_declaration=script,\n                object_id=object_id,\n                return_by_value=False,\n            )\n        else:\n            script = Scripts.FIND_XPATH_ELEMENTS.replace('{escaped_value}', escaped_value)\n            command = RuntimeCommands.evaluate(expression=script, context_id=execution_context_id)\n        return command\n\n    @staticmethod\n    def _ensure_relative_xpath(xpath: str) -> str:\n        \"\"\"\n        Ensure XPath is relative by prepending dot if needed.\n\n        Converts absolute XPath to relative for context-based searches.\n        \"\"\"\n        return SelectorParser.ensure_relative_xpath(xpath)\n\n    @staticmethod\n    def _has_object_id_key(response: Union[EvaluateResponse, CallFunctionOnResponse]) -> bool:\n        \"\"\"\n        Check if response has objectId key.\n        \"\"\"\n        return bool(response.get('result', {}).get('result', {}).get('objectId'))\n"
  },
  {
    "path": "pydoll/elements/shadow_root.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom typing import TYPE_CHECKING\n\nfrom pydoll.commands import DomCommands\nfrom pydoll.connection import ConnectionHandler\nfrom pydoll.elements.mixins import FindElementsMixin\nfrom pydoll.protocol.dom.types import ShadowRootType\n\nif TYPE_CHECKING:\n    from pydoll.elements.web_element import WebElement\n    from pydoll.protocol.dom.methods import GetOuterHTMLResponse\n\nlogger = logging.getLogger(__name__)\n\n\nclass ShadowRoot(FindElementsMixin):\n    \"\"\"\n    Shadow root wrapper for shadow DOM traversal.\n\n    Provides element finding capabilities within shadow DOM boundaries\n    using query() with CSS selectors. Use query() instead of find() —\n    find() and XPath are not supported inside shadow roots.\n\n    Usage:\n        shadow_host = await tab.find(id='my-component')\n        shadow_root = await shadow_host.get_shadow_root()\n        button = await shadow_root.query('#internal-button')\n        await button.click()\n    \"\"\"\n\n    _css_only = True\n\n    def __init__(\n        self,\n        object_id: str,\n        connection_handler: ConnectionHandler,\n        mode: ShadowRootType = ShadowRootType.OPEN,\n        host_element: WebElement | None = None,\n    ):\n        \"\"\"\n        Initialize shadow root wrapper.\n\n        Args:\n            object_id: CDP object ID for the shadow root node.\n            connection_handler: Browser connection for CDP commands.\n            mode: Shadow root mode (open, closed, or user-agent).\n            host_element: Reference to the shadow host element.\n        \"\"\"\n        self._object_id = object_id\n        self._connection_handler = connection_handler\n        self._mode = mode\n        self._host_element = host_element\n\n        # Inherit iframe/routing context from host element if present\n        if host_element:\n            self._iframe_context = getattr(host_element, '_iframe_context', None)\n            self._routing_session_handler = getattr(host_element, '_routing_session_handler', None)\n            self._routing_session_id = getattr(host_element, '_routing_session_id', None)\n            self._routing_parent_frame_id = getattr(host_element, '_routing_parent_frame_id', None)\n\n        logger.debug(\n            f'ShadowRoot initialized: object_id={self._object_id}, mode={self._mode.value}'\n        )\n\n    @property\n    def mode(self) -> ShadowRootType:\n        \"\"\"Shadow root mode (open, closed, or user-agent).\"\"\"\n        return self._mode\n\n    @property\n    def host_element(self) -> WebElement | None:\n        \"\"\"Reference to the shadow host element, if available.\"\"\"\n        return self._host_element\n\n    @property\n    async def inner_html(self) -> str:\n        \"\"\"HTML content of the shadow root.\"\"\"\n        response: GetOuterHTMLResponse = await self._execute_command(\n            DomCommands.get_outer_html(object_id=self._object_id)\n        )\n        return response['result']['outerHTML']\n\n    def __repr__(self) -> str:\n        return f'ShadowRoot(mode={self._mode.value}, object_id={self._object_id})'\n\n    def __str__(self) -> str:\n        return f'ShadowRoot({self._mode.value})'\n"
  },
  {
    "path": "pydoll/elements/utils/__init__.py",
    "content": "from pydoll.elements.utils.selector_parser import SelectorParser\n\n__all__ = ['SelectorParser']\n"
  },
  {
    "path": "pydoll/elements/utils/selector_parser.py",
    "content": "\"\"\"\nSelector parsing and building utilities for element finding.\n\nCentralises all logic that inspects, builds, or transforms CSS and XPath\nselector strings. This keeps the mixin layer focused on orchestration\n(finding elements, managing timeouts, issuing CDP commands) while the\npure string-manipulation lives here.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport re\nfrom typing import Optional\n\nfrom pydoll.constants import By, Scripts\nfrom pydoll.utils import normalize_synthetic_xpath\n\nlogger = logging.getLogger(__name__)\n\n# ---------------------------------------------------------------------------\n# Compiled patterns\n# ---------------------------------------------------------------------------\n\n_IFRAME_XPATH_NODE_RE = re.compile(r'^(?:\\w+::)?iframe(?:\\[|$)', re.IGNORECASE)\n_IFRAME_XPATH_GROUPED_RE = re.compile(r'\\biframe\\b', re.IGNORECASE)\n_CSS_TAG_NAME_RE = re.compile(r'^([a-zA-Z][a-zA-Z0-9-]*)')\n_XPATH_PREFIXES: list[tuple[str, int]] = [('.//', 3), ('//', 2), ('./', 2), ('/', 1)]\n\n# Lookup tables for the nesting-depth tracker\n_QUOTE_TRANSITIONS: dict[str, tuple[int, bool]] = {\"'\": (0, True), '\"': (1, True)}\n_DEPTH_TRANSITIONS: dict[str, tuple[int, int]] = {\n    '[': (0, 1),\n    ']': (0, -1),\n    '(': (1, 1),\n    ')': (1, -1),\n}\n\n\nclass SelectorParser:\n    \"\"\"\n    Stateless helper that parses, builds and classifies CSS / XPath selectors.\n\n    Every method is a ``@staticmethod`` — the class is used purely as a\n    namespace to keep the parsing surface area together. ``FindElementsMixin``\n    delegates all selector string work here.\n    \"\"\"\n\n    # ------------------------------------------------------------------\n    # Expression type detection\n    # ------------------------------------------------------------------\n\n    @staticmethod\n    def get_expression_type(expression: str) -> By:\n        \"\"\"\n        Auto-detect selector type from expression syntax.\n\n        Patterns:\n        - XPath: starts with ``./``, ``/`` or ``(/``\n        - Default: ``By.CSS_SELECTOR``\n        \"\"\"\n        if expression.startswith(('./', '/', '(/')):\n            return By.XPATH\n        return By.CSS_SELECTOR\n\n    # ------------------------------------------------------------------\n    # XPath building from keyword criteria\n    # ------------------------------------------------------------------\n\n    @staticmethod\n    def build_xpath(\n        id: Optional[str] = None,\n        class_name: Optional[str] = None,\n        name: Optional[str] = None,\n        tag_name: Optional[str] = None,\n        text: Optional[str] = None,\n        **attributes: str,\n    ) -> str:\n        \"\"\"\n        Build XPath expression from multiple attribute criteria.\n\n        Constructs complex XPath combining multiple conditions with ``and``\n        operators. Handles class names correctly for space-separated class\n        lists. Uses ``contains()`` for text matching (partial text support).\n\n        Note:\n            Attribute names with underscores are automatically converted to\n            hyphens to match HTML attribute naming conventions\n            (e.g. ``data_test`` -> ``data-test``).\n        \"\"\"\n        xpath_conditions: list[str] = []\n        base_xpath = f'//{tag_name}' if tag_name else '//*'\n        if id:\n            xpath_conditions.append(f'@id=\"{id}\"')\n        if class_name:\n            xpath_conditions.append(\n                f'contains(concat(\" \", normalize-space(@class), \" \"), \" {class_name} \")'\n            )\n        if name:\n            xpath_conditions.append(f'@name=\"{name}\"')\n        if text:\n            xpath_conditions.append(f'contains(text(), \"{text}\")')\n        for attribute, value in attributes.items():\n            html_attribute = attribute.replace('_', '-')\n            xpath_conditions.append(f'@{html_attribute}=\"{value}\"')\n\n        xpath = (\n            f'{base_xpath}[{\" and \".join(xpath_conditions)}]' if xpath_conditions else base_xpath\n        )\n        logger.debug(f'build_xpath() -> {xpath}')\n        return xpath\n\n    # ------------------------------------------------------------------\n    # XPath helpers\n    # ------------------------------------------------------------------\n\n    @staticmethod\n    def ensure_relative_xpath(xpath: str) -> str:\n        \"\"\"\n        Ensure XPath is relative by prepending dot if needed.\n\n        Converts absolute XPath to relative for context-based searches.\n        \"\"\"\n        return f'.{xpath}' if not xpath.startswith('.') else xpath\n\n    # ------------------------------------------------------------------\n    # JS text-expression builder\n    # ------------------------------------------------------------------\n\n    @staticmethod\n    def build_text_expression(selector: str, method: str) -> Optional[str]:\n        \"\"\"\n        Build JS expression using ``Scripts`` to extract ``textContent``\n        based on selector type.\n        \"\"\"\n        raw = str(selector)\n        method_lc = (method or '').lower()\n\n        if 'xpath' in method_lc:\n            normalized_xpath = normalize_synthetic_xpath(raw)\n            escaped_xpath = normalized_xpath.replace('\"', '\\\\\"')\n            return Scripts.GET_TEXT_BY_XPATH.replace('{escaped_value}', escaped_xpath)\n\n        if method_lc == 'name':\n            escaped_name = raw.replace('\"', '\\\\\"')\n            xpath = f'//*[@name=\"{escaped_name}\"]'\n            return Scripts.GET_TEXT_BY_XPATH.replace('{escaped_value}', xpath)\n\n        escaped = raw.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"')\n        if method_lc == 'id':\n            css = f'#{escaped}'\n        elif method_lc == 'class_name':\n            css = f'.{escaped}'\n        elif method_lc == 'tag_name':\n            css = escaped\n        else:\n            css = escaped\n        return Scripts.GET_TEXT_BY_CSS.replace('{selector}', css)\n\n    # ------------------------------------------------------------------\n    # Iframe-crossing: XPath\n    # ------------------------------------------------------------------\n\n    @staticmethod\n    def parse_iframe_segments_xpath(expression: str) -> list[tuple[By, str]]:\n        \"\"\"\n        Split an XPath expression at iframe boundaries for cross-iframe\n        traversal.\n\n        Parses the XPath into steps separated by ``/`` or ``//``, respecting\n        quoted strings, brackets and parentheses. Steps whose node test is\n        ``iframe`` (case-insensitive) act as split points: everything up to\n        and including the iframe step becomes one segment, and the remainder\n        starts a new segment prefixed with ``//``.\n\n        Args:\n            expression: Raw XPath expression.\n\n        Returns:\n            List of ``(By.XPATH, segment)`` tuples.  A single-element list\n            when no iframe crossing is detected.\n        \"\"\"\n        xpath_steps = SelectorParser._tokenize_xpath_steps(expression)\n        if not xpath_steps:\n            return [(By.XPATH, expression)]\n\n        iframe_split_indices: list[int] = [\n            step_index\n            for step_index, (_sep, step_text) in enumerate(xpath_steps)\n            if SelectorParser._is_iframe_xpath_step(step_text) and step_index < len(xpath_steps) - 1\n        ]\n\n        if not iframe_split_indices:\n            return [(By.XPATH, expression)]\n\n        return SelectorParser._build_xpath_segments(xpath_steps, iframe_split_indices)\n\n    # ------------------------------------------------------------------\n    # Iframe-crossing: CSS\n    # ------------------------------------------------------------------\n\n    @staticmethod\n    def parse_iframe_segments_css(expression: str) -> list[tuple[By, str]]:\n        \"\"\"\n        Split a CSS selector at iframe boundaries for cross-iframe traversal.\n\n        Tokenises the selector into compound selectors separated by\n        combinators (space, ``>``, ``+``, ``~``), respecting quoted strings,\n        brackets and parentheses. Compounds whose tag name is ``iframe``\n        (case-insensitive) act as split points.\n\n        Args:\n            expression: Raw CSS selector.\n\n        Returns:\n            List of ``(By.CSS_SELECTOR, segment)`` tuples.  A single-element\n            list when no iframe crossing is detected.\n        \"\"\"\n        css_compounds = SelectorParser._tokenize_css_compounds(expression)\n        if not css_compounds:\n            return [(By.CSS_SELECTOR, expression)]\n\n        iframe_split_indices: list[int] = [\n            compound_index\n            for compound_index, (compound_text, _comb) in enumerate(css_compounds)\n            if SelectorParser._is_iframe_css_compound(compound_text)\n            and compound_index < len(css_compounds) - 1\n        ]\n\n        if not iframe_split_indices:\n            return [(By.CSS_SELECTOR, expression)]\n\n        return SelectorParser._build_css_segments(css_compounds, iframe_split_indices)\n\n    # ==================================================================\n    # Private helpers\n    # ==================================================================\n\n    @staticmethod\n    def _is_at_nesting_depth_zero(\n        char: str,\n        quote_state: list[bool],\n        depth_state: list[int],\n    ) -> bool:\n        \"\"\"\n        Track quote/bracket/paren nesting and return whether char is at\n        depth 0.  Mutates *quote_state* and *depth_state* in place.\n        \"\"\"\n        if quote_state[0] or quote_state[1]:\n            if quote_state[0]:\n                quote_state[0] = char != \"'\"\n            else:\n                quote_state[1] = char != '\"'\n            return False\n\n        if char in _QUOTE_TRANSITIONS:\n            index, value = _QUOTE_TRANSITIONS[char]\n            quote_state[index] = value\n            return False\n\n        if char in _DEPTH_TRANSITIONS:\n            index, delta = _DEPTH_TRANSITIONS[char]\n            depth_state[index] += delta\n            return False\n\n        return depth_state[0] == 0 and depth_state[1] == 0\n\n    # -- XPath tokenizer -----------------------------------------------\n\n    @staticmethod\n    def _detect_xpath_leading_separator(expression: str) -> tuple[str, int]:\n        \"\"\"Return ``(separator, start_index)`` for the XPath prefix.\"\"\"\n        if expression.startswith('('):\n            return '', 0\n        for prefix, length in _XPATH_PREFIXES:\n            if expression.startswith(prefix):\n                return prefix, length\n        return '', 0\n\n    @staticmethod\n    def _tokenize_xpath_steps(expression: str) -> list[tuple[str, str]]:\n        \"\"\"Tokenize XPath into ``(separator, step_text)`` pairs.\"\"\"\n        xpath_steps: list[tuple[str, str]] = []\n        current_separator, token_start = SelectorParser._detect_xpath_leading_separator(expression)\n        char_index = token_start\n        quote_state = [False, False]\n        depth_state = [0, 0]\n\n        while char_index < len(expression):\n            char = expression[char_index]\n            at_depth_zero = SelectorParser._is_at_nesting_depth_zero(char, quote_state, depth_state)\n\n            if at_depth_zero and char == '/':\n                step_text = expression[token_start:char_index]\n                if step_text:\n                    xpath_steps.append((current_separator, step_text))\n                is_double_slash = (\n                    char_index + 1 < len(expression) and expression[char_index + 1] == '/'\n                )\n                current_separator = '//' if is_double_slash else '/'\n                char_index += 2 if is_double_slash else 1\n                token_start = char_index\n                continue\n            char_index += 1\n\n        remaining_text = expression[token_start:]\n        if remaining_text:\n            xpath_steps.append((current_separator, remaining_text))\n\n        return xpath_steps\n\n    @staticmethod\n    def _is_iframe_xpath_step(step_text: str) -> bool:\n        \"\"\"Return whether a single XPath step's node test is ``iframe``.\"\"\"\n        if step_text.startswith('('):\n            return bool(_IFRAME_XPATH_GROUPED_RE.search(step_text))\n        return bool(_IFRAME_XPATH_NODE_RE.match(step_text))\n\n    @staticmethod\n    def _build_xpath_segments(\n        xpath_steps: list[tuple[str, str]],\n        iframe_split_indices: list[int],\n    ) -> list[tuple[By, str]]:\n        \"\"\"Reassemble XPath steps into segments split at iframe indices.\"\"\"\n        segments: list[tuple[By, str]] = []\n        segment_start = 0\n\n        for split_index in iframe_split_indices:\n            segment_parts: list[str] = []\n            for step_index in range(segment_start, split_index + 1):\n                separator, step_text = xpath_steps[step_index]\n                if step_index == segment_start and segment_start != 0:\n                    segment_parts.append('//' + step_text)\n                else:\n                    segment_parts.append(separator + step_text)\n            segments.append((By.XPATH, ''.join(segment_parts)))\n            segment_start = split_index + 1\n\n        if segment_start < len(xpath_steps):\n            segment_parts = []\n            for step_index in range(segment_start, len(xpath_steps)):\n                separator, step_text = xpath_steps[step_index]\n                if step_index == segment_start:\n                    segment_parts.append('//' + step_text)\n                else:\n                    segment_parts.append(separator + step_text)\n            segments.append((By.XPATH, ''.join(segment_parts)))\n\n        return segments\n\n    # -- CSS tokenizer --------------------------------------------------\n\n    @staticmethod\n    def _tokenize_css_compounds(expression: str) -> list[tuple[str, str | None]]:\n        \"\"\"Tokenize CSS selector into ``(compound_text, combinator_after)`` pairs.\"\"\"\n        css_compounds: list[tuple[str, str | None]] = []\n        token_start = 0\n        char_index = 0\n        quote_state = [False, False]\n        depth_state = [0, 0]\n\n        while char_index < len(expression):\n            char = expression[char_index]\n            at_depth_zero = SelectorParser._is_at_nesting_depth_zero(char, quote_state, depth_state)\n\n            if at_depth_zero and char in ' >+~':\n                compound_text = expression[token_start:char_index]\n                if not compound_text.strip():\n                    char_index += 1\n                    continue\n                combinator, char_index = SelectorParser._consume_css_combinator(\n                    expression, char_index\n                )\n                css_compounds.append((compound_text, combinator))\n                token_start = char_index\n                continue\n            char_index += 1\n\n        remaining_text = expression[token_start:].strip()\n        if remaining_text:\n            css_compounds.append((remaining_text, None))\n\n        return css_compounds\n\n    @staticmethod\n    def _consume_css_combinator(expression: str, start: int) -> tuple[str, int]:\n        \"\"\"Consume a CSS combinator region and return ``(combinator, next_index)``.\"\"\"\n        char_index = start\n        while char_index < len(expression) and expression[char_index] == ' ':\n            char_index += 1\n        if char_index < len(expression) and expression[char_index] in '>+~':\n            combinator = expression[char_index]\n            char_index += 1\n            while char_index < len(expression) and expression[char_index] == ' ':\n                char_index += 1\n        else:\n            combinator = ' '\n        return combinator, char_index\n\n    @staticmethod\n    def _is_iframe_css_compound(compound_text: str) -> bool:\n        \"\"\"Return whether a CSS compound selector's tag name is ``iframe``.\"\"\"\n        stripped = compound_text.strip()\n        if stripped and stripped[0] in '.#[:':\n            return False\n        match = _CSS_TAG_NAME_RE.match(stripped)\n        if not match:\n            return False\n        return match.group(1).lower() == 'iframe'\n\n    @staticmethod\n    def _format_css_combinator(combinator: str) -> str:\n        \"\"\"Format a CSS combinator for human-readable output.\"\"\"\n        if combinator == ' ':\n            return ' '\n        return f' {combinator} '\n\n    @staticmethod\n    def _build_css_segments(\n        css_compounds: list[tuple[str, str | None]],\n        iframe_split_indices: list[int],\n    ) -> list[tuple[By, str]]:\n        \"\"\"Reassemble CSS compounds into segments split at iframe indices.\"\"\"\n        segments: list[tuple[By, str]] = []\n        segment_start = 0\n\n        for split_index in iframe_split_indices:\n            segment_parts: list[str] = []\n            for compound_index in range(segment_start, split_index + 1):\n                compound_text, _combinator = css_compounds[compound_index]\n                if compound_index > segment_start:\n                    previous_combinator = css_compounds[compound_index - 1][1] or ' '\n                    segment_parts.append(SelectorParser._format_css_combinator(previous_combinator))\n                segment_parts.append(compound_text)\n            segments.append((By.CSS_SELECTOR, ''.join(segment_parts)))\n            segment_start = split_index + 1\n\n        if segment_start < len(css_compounds):\n            segment_parts = []\n            for compound_index in range(segment_start, len(css_compounds)):\n                compound_text, _combinator = css_compounds[compound_index]\n                if compound_index > segment_start:\n                    previous_combinator = css_compounds[compound_index - 1][1] or ' '\n                    segment_parts.append(SelectorParser._format_css_combinator(previous_combinator))\n                segment_parts.append(compound_text)\n            segments.append((By.CSS_SELECTOR, ''.join(segment_parts)))\n\n        return segments\n"
  },
  {
    "path": "pydoll/elements/web_element.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nimport warnings\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Optional\n\nimport aiofiles\n\nfrom pydoll.commands import (\n    DomCommands,\n    InputCommands,\n    PageCommands,\n    RuntimeCommands,\n)\nfrom pydoll.connection import ConnectionHandler\nfrom pydoll.constants import (\n    Key,\n    Scripts,\n)\nfrom pydoll.elements.mixins import FindElementsMixin\nfrom pydoll.elements.shadow_root import ShadowRoot\nfrom pydoll.exceptions import (\n    ElementNotAFileInput,\n    ElementNotFound,\n    ElementNotInteractable,\n    ElementNotVisible,\n    InvalidFileExtension,\n    InvalidIFrame,\n    MissingScreenshotPath,\n    ShadowRootNotFound,\n    WaitElementTimeout,\n)\nfrom pydoll.interactions.iframe import IFrameContext, IFrameContextResolver\nfrom pydoll.interactions.keyboard import Keyboard\nfrom pydoll.protocol.dom.types import ShadowRootType\nfrom pydoll.protocol.input.types import (\n    KeyEventType,\n    KeyModifier,\n    MouseButton,\n    MouseEventType,\n)\nfrom pydoll.protocol.page.types import ScreenshotFormat, Viewport\nfrom pydoll.protocol.runtime.methods import (\n    CallFunctionOnResponse,\n    EvaluateResponse,\n    GetPropertiesResponse,\n    SerializationOptions,\n)\nfrom pydoll.protocol.runtime.types import CallArgument\nfrom pydoll.utils import (\n    decode_base64_to_bytes,\n    extract_text_from_html,\n    is_script_already_function,\n)\n\nif TYPE_CHECKING:\n    from pydoll.interactions.mouse import Mouse as MouseType\n    from pydoll.protocol.dom.methods import (\n        DescribeNodeResponse,\n        GetBoxModelResponse,\n        GetOuterHTMLResponse,\n        ResolveNodeResponse,\n    )\n    from pydoll.protocol.dom.types import Quad\n    from pydoll.protocol.page.methods import CaptureScreenshotResponse\n    from pydoll.protocol.runtime.methods import GetPropertiesResponse\n\nlogger = logging.getLogger(__name__)\n\n\nclass WebElement(FindElementsMixin):  # noqa: PLR0904\n    \"\"\"\n    DOM element wrapper for browser automation.\n\n    Provides comprehensive functionality for element interaction, inspection,\n    and manipulation using Chrome DevTools Protocol commands.\n    \"\"\"\n\n    if TYPE_CHECKING:\n        _routing_session_handler: Optional[ConnectionHandler]\n        _routing_session_id: Optional[str]\n        _routing_parent_frame_id: Optional[str]\n\n    def __init__(\n        self,\n        object_id: str,\n        connection_handler: ConnectionHandler,\n        method: Optional[str] = None,\n        selector: Optional[str] = None,\n        attributes_list: list[str] = [],\n        mouse: Optional['MouseType'] = None,\n    ):\n        \"\"\"\n        Initialize WebElement wrapper.\n\n        Args:\n            object_id: Unique CDP object identifier for this DOM element.\n            connection_handler: Connection instance for browser communication.\n            method: Search method used to find this element (for debugging).\n            selector: Selector string used to find this element (for debugging).\n            attributes_list: Flat list of alternating attribute names and values.\n            mouse: Optional Mouse instance for humanized click behavior.\n\n        Note:\n            Mouse and Keyboard follow different ownership strategies. Mouse is a shared\n            instance from Tab, passed down to elements to preserve cursor position state\n            across interactions. It dispatches commands through Tab._execute_command, which\n            means it has no iframe context awareness. Keyboard is created per-element and\n            routes commands through the element's own _execute_command, correctly handling\n            iframe routing. For iframe elements, the mouse is intentionally skipped during\n            humanized clicks (see click()) to avoid dispatching events to the wrong frame.\n        \"\"\"\n        self._object_id = object_id\n        self._search_method = method\n        self._selector = selector\n        self._connection_handler = connection_handler\n        self._attributes: dict[str, str] = {}\n        self._keyboard: Optional[Keyboard] = None\n        self._mouse = mouse\n        self._iframe_context: Optional[IFrameContext] = None\n        self._iframe_resolver: Optional[IFrameContextResolver] = None\n        self._def_attributes(attributes_list)\n        logger.debug(\n            f'WebElement initialized: object_id={self._object_id}, '\n            f'method={self._search_method}, selector={self._selector}, '\n            f'attributes={len(self._attributes)}'\n        )\n\n    def _get_keyboard(self) -> Keyboard:\n        \"\"\"Get or create the keyboard controller.\"\"\"\n        if self._keyboard is None:\n            self._keyboard = Keyboard(self)\n        return self._keyboard\n\n    def _get_iframe_resolver(self) -> IFrameContextResolver:\n        \"\"\"Get or create the iframe context resolver.\"\"\"\n        if self._iframe_resolver is None:\n            self._iframe_resolver = IFrameContextResolver(self)\n        return self._iframe_resolver\n\n    @property\n    def attributes(self) -> dict[str, str]:\n        \"\"\"Read-only copy of the element's cached attributes.\"\"\"\n        return dict(self._attributes)\n\n    @property\n    def value(self) -> Optional[str]:\n        \"\"\"Element's value attribute (for form elements).\"\"\"\n        return self._attributes.get('value')\n\n    @property\n    def class_name(self) -> Optional[str]:\n        \"\"\"Element's CSS class name(s).\"\"\"\n        return self._attributes.get('class_name')\n\n    @property\n    def id(self) -> Optional[str]:\n        \"\"\"Element's ID attribute.\"\"\"\n        return self._attributes.get('id')\n\n    @property\n    def tag_name(self) -> Optional[str]:\n        \"\"\"Element's HTML tag name.\"\"\"\n        return self._attributes.get('tag_name')\n\n    @property\n    def is_iframe(self) -> bool:\n        \"\"\"Whether the element represents an iframe.\"\"\"\n        return self.tag_name in {'iframe', 'frame'}\n\n    @property\n    def is_enabled(self) -> bool:\n        \"\"\"Whether element is enabled (not disabled).\"\"\"\n        return bool('disabled' not in self._attributes.keys())\n\n    @property\n    async def text(self) -> str:\n        \"\"\"Visible text content of the element.\"\"\"\n        if self._is_inside_iframe():\n            response: CallFunctionOnResponse = await self.execute_script(\n                'return (this.textContent || \"\").trim()', return_by_value=True\n            )\n            text_value = response.get('result', {}).get('result', {}).get('value', '') or ''\n            logger.debug(f'Extracted text length (iframe ctx): {len(text_value)}')\n            return text_value\n\n        outer_html = await self.inner_html\n        text_value = extract_text_from_html(outer_html, strip=True)\n        logger.debug(f'Extracted text length: {len(text_value)}')\n        return text_value\n\n    @property\n    async def bounds(self) -> Quad:\n        \"\"\"\n        Element's bounding box coordinates.\n\n        Returns coordinates in CSS pixels relative to document origin.\n        \"\"\"\n        command = DomCommands.get_box_model(object_id=self._object_id)\n        response: GetBoxModelResponse = await self._execute_command(command)\n        content = response['result']['model']['content']\n        logger.debug(f'Bounds retrieved (points={len(content)})')\n        return content\n\n    @property\n    async def inner_html(self) -> str:\n        if self.is_iframe:\n            return await self._get_iframe_inner_html()\n\n        if self._is_inside_iframe():\n            response: CallFunctionOnResponse = await self.execute_script(\n                'return this.outerHTML', return_by_value=True\n            )\n            return response.get('result', {}).get('result', {}).get('value', '')\n\n        command = DomCommands.get_outer_html(object_id=self._object_id)\n        response_get_outer_html: GetOuterHTMLResponse = await self._execute_command(command)\n        return response_get_outer_html['result']['outerHTML']\n\n    @property\n    async def iframe_context(self) -> Optional[IFrameContext]:\n        \"\"\"\n        Return the resolved iframe context for this element when it is an <iframe>.\n\n        The context includes: frame_id, document_url, execution_context_id,\n        document_object_id and, for OOPIF targets, the session_id and\n        session_handler used for routing commands. The context is always freshly\n        resolved to avoid stale execution contexts after iframe navigations or\n        reloads. Non-iframe elements return None.\n\n        Returns:\n            IFrameContext | None: Resolved iframe context or None for non-iframes.\n        \"\"\"\n        if not self.is_iframe:\n            return None\n\n        resolver = self._get_iframe_resolver()\n        self._iframe_context = await resolver.resolve()\n        self._apply_routing_from_context()\n        return self._iframe_context\n\n    def get_attribute(self, name: str) -> Optional[str]:\n        \"\"\"\n        Get element attribute value.\n\n        Note:\n            Only provides attributes available when element was located.\n            For dynamic attributes, consider using JavaScript execution.\n        \"\"\"\n        if name == 'class' and 'class_name' in self._attributes:\n            return self._attributes.get('class_name')\n        return self._attributes.get(name)\n\n    async def get_bounds_using_js(self) -> dict[str, int]:\n        \"\"\"\n        Get element bounds using JavaScript getBoundingClientRect().\n\n        Returns coordinates relative to viewport (alternative to bounds property).\n        \"\"\"\n        response = await self.execute_script(Scripts.BOUNDS, return_by_value=True)\n        bounds = json.loads(response['result']['result']['value'])\n        logger.debug(f'Bounds via JS: {bounds}')\n        return bounds\n\n    async def get_parent_element(self) -> WebElement:\n        \"\"\"Element's parent element.\"\"\"\n        logger.debug(f'Getting parent element for object_id={self._object_id}')\n        result = await self.execute_script(Scripts.GET_PARENT_NODE)\n        if not self._has_object_id_key(result):\n            raise ElementNotFound(f'Parent element not found for element: {self}')\n\n        object_id = result['result']['result']['objectId']\n        attributes = await self._get_object_attributes(object_id=object_id)\n        logger.debug(f'Parent element resolved: object_id={object_id}')\n        return WebElement(\n            object_id, self._connection_handler, attributes_list=attributes, mouse=self._mouse\n        )\n\n    async def get_shadow_root(self, timeout: float = 0) -> ShadowRoot:\n        \"\"\"\n        Get the shadow root attached to this element.\n\n        Args:\n            timeout: Maximum seconds to wait for the shadow root to appear.\n                When > 0, repeatedly polls (every 0.5s) until a shadow root\n                is found or the timeout expires.\n\n        Returns:\n            ShadowRoot instance for traversing the shadow DOM.\n\n        Raises:\n            ShadowRootNotFound: If no shadow root is attached (when timeout=0).\n            WaitElementTimeout: If timeout > 0 and no shadow root appears\n                within the specified duration.\n        \"\"\"\n        if not timeout:\n            return await self._get_shadow_root()\n\n        start_time = asyncio.get_event_loop().time()\n        while True:\n            try:\n                return await self._get_shadow_root()\n            except ShadowRootNotFound:\n                pass\n\n            if asyncio.get_event_loop().time() - start_time > timeout:\n                raise WaitElementTimeout(\n                    f'Timed out after {timeout}s waiting for shadow root on element'\n                )\n\n            await asyncio.sleep(0.5)\n\n    async def _get_shadow_root(self) -> ShadowRoot:\n        \"\"\"Get the shadow root attached to this element (single attempt).\"\"\"\n        response: DescribeNodeResponse = await self._execute_command(\n            DomCommands.describe_node(object_id=self._object_id, depth=1, pierce=True)\n        )\n        node_info = response.get('result', {}).get('node', {})\n        shadow_roots = node_info.get('shadowRoots', [])\n        if not shadow_roots:\n            raise ShadowRootNotFound()\n\n        shadow_root_data = shadow_roots[0]\n        backend_node_id = shadow_root_data.get('backendNodeId')\n        if not backend_node_id:\n            raise ShadowRootNotFound('Shadow root found but backend node ID is unavailable')\n\n        resolve_response: ResolveNodeResponse = await self._execute_command(\n            DomCommands.resolve_node(backend_node_id=backend_node_id)\n        )\n        shadow_object_id = resolve_response['result']['object']['objectId']\n\n        mode = ShadowRootType(shadow_root_data.get('shadowRootType', 'open'))\n\n        logger.debug(f'Shadow root resolved: object_id={shadow_object_id}, mode={mode.value}')\n        return ShadowRoot(\n            object_id=shadow_object_id,\n            connection_handler=self._connection_handler,\n            mode=mode,\n            host_element=self,\n        )\n\n    async def get_children_elements(\n        self, max_depth: int = 1, tag_filter: list[str] = [], raise_exc: bool = False\n    ) -> list[WebElement]:\n        \"\"\"\n        Retrieve all direct and nested child elements of this element.\n\n        Args:\n            max_depth (int, optional): Maximum depth to traverse when finding children.\n                Defaults to 1 for direct children only.\n            tag_filter (list[str], optional): List of HTML tag names to filter results.\n                If empty, returns all child elements regardless of tag. Defaults to [].\n\n        Returns:\n            list[WebElement]: List of child WebElement objects found within the specified\n                depth and matching the tag filter criteria.\n\n        Raises:\n            ElementNotFound: If no child elements are found for this element and raise_exc is True.\n        \"\"\"\n        logger.debug(\n            f'Getting children: max_depth={max_depth}, '\n            f'tag_filter={tag_filter}, raise_exc={raise_exc}'\n        )\n        children = await self._get_family_elements(\n            script=Scripts.GET_CHILDREN_NODE, max_depth=max_depth, tag_filter=tag_filter\n        )\n        if not children and raise_exc:\n            raise ElementNotFound(f'Child element not found for element: {self}')\n        logger.debug(f'Children found: {len(children)}')\n        return children\n\n    async def get_siblings_elements(\n        self, tag_filter: list[str] = [], raise_exc: bool = False\n    ) -> list[WebElement]:\n        \"\"\"\n        Retrieve all sibling elements of this element (elements at the same DOM level).\n\n        Args:\n            tag_filter (list[str], optional): List of HTML tag names to filter results.\n                If empty, returns all sibling elements regardless of tag. Defaults to [].\n\n        Returns:\n            list[WebElement]: List of sibling WebElement objects that share the same\n                parent as this element and match the tag filter criteria.\n\n        Raises:\n            ElementNotFound: If no sibling elements are found for this element\n            and raise_exc is True.\n        \"\"\"\n        logger.debug(f'Getting siblings: tag_filter={tag_filter}, raise_exc={raise_exc}')\n        siblings = await self._get_family_elements(\n            script=Scripts.GET_SIBLINGS_NODE, tag_filter=tag_filter\n        )\n        if not siblings and raise_exc:\n            raise ElementNotFound(f'Sibling element not found for element: {self}')\n        logger.debug(f'Siblings found: {len(siblings)}')\n        return siblings\n\n    async def take_screenshot(\n        self,\n        path: Optional[str | Path] = None,\n        quality: int = 100,\n        as_base64: bool = False,\n    ) -> Optional[str]:\n        \"\"\"\n        Capture screenshot of this element only.\n\n        Automatically scrolls element into view before capturing.\n\n        Args:\n            path: File path for screenshot (extension determines format).\n            quality: Image quality 0-100 (default 100).\n            as_base64: Return as base64 string instead of saving file.\n\n        Returns:\n            Base64 screenshot data if as_base64=True, None otherwise.\n\n        Raises:\n            InvalidFileExtension: If file extension not supported.\n            MissingScreenshotPath: If path is None and as_base64 is False.\n        \"\"\"\n        if not path and not as_base64:\n            raise MissingScreenshotPath()\n\n        if path and isinstance(path, str):\n            output_extension = path.split('.')[-1]\n        elif path and isinstance(path, Path):\n            output_extension = path.suffix.lstrip('.')\n        else:\n            output_extension = ScreenshotFormat.JPEG\n\n        # Normalize jpg to jpeg (CDP only accepts jpeg)\n        if output_extension == 'jpg':\n            output_extension = 'jpeg'\n\n        if not ScreenshotFormat.has_value(output_extension):\n            raise InvalidFileExtension(f'{output_extension} extension is not supported.')\n\n        file_format = ScreenshotFormat.get_value(output_extension)\n\n        bounds = await self.get_bounds_using_js()\n        clip = Viewport(\n            x=bounds['x'],\n            y=bounds['y'],\n            width=bounds['width'],\n            height=bounds['height'],\n            scale=1,\n        )\n        logger.debug(\n            f'Taking element screenshot: path={path}, quality={quality}, as_base64={as_base64}, '\n            f'clip={{x: {clip[\"x\"]}, y: {clip[\"y\"]}, w: {clip[\"width\"]}, h: {clip[\"height\"]}}}'\n        )\n\n        screenshot: CaptureScreenshotResponse = await self._connection_handler.execute_command(\n            PageCommands.capture_screenshot(format=file_format, clip=clip, quality=quality)\n        )\n\n        screenshot_data = screenshot['result']['data']\n\n        if as_base64:\n            logger.info('Element screenshot captured and returned as base64')\n            return screenshot_data\n\n        if path:\n            image_bytes = decode_base64_to_bytes(screenshot_data)\n            async with aiofiles.open(str(path), 'wb') as file:\n                await file.write(image_bytes)\n            logger.info(f'Element screenshot saved: {path}')\n\n        return None\n\n    async def scroll_into_view(self):\n        \"\"\"Scroll element into visible viewport.\"\"\"\n        command = DomCommands.scroll_into_view_if_needed(object_id=self._object_id)\n        logger.info(f'Scrolling element into view: object_id={self._object_id}')\n        await self._execute_command(command)\n\n    async def wait_until(\n        self,\n        *,\n        is_visible: bool = False,\n        is_interactable: bool = False,\n        timeout: int = 0,\n    ):\n        \"\"\"Wait for element to meet specified conditions.\n\n        Raises:\n            ValueError: If neither ``is_visible`` nor ``is_interactable`` is True.\n            WaitElementTimeout: If the condition is not met within ``timeout``.\n        \"\"\"\n        checks_map = [\n            (is_visible, self.is_visible),\n            (is_interactable, self.is_interactable),\n        ]\n        checks = [func for flag, func in checks_map if flag]\n        if not checks:\n            raise ValueError('At least one of is_visible or is_interactable must be True')\n\n        condition_parts = []\n        if is_visible:\n            condition_parts.append('visible')\n        if is_interactable:\n            condition_parts.append('interactable')\n        condition_msg = ' and '.join(condition_parts)\n\n        logger.info(\n            f'Waiting for element: visible={is_visible}, '\n            f'interactable={is_interactable}, timeout={timeout}s'\n        )\n        loop = asyncio.get_event_loop()\n        start_time = loop.time()\n        while True:\n            results = await asyncio.gather(*(check() for check in checks))\n            if all(results):\n                logger.info(f'Element condition satisfied: {condition_msg}')\n                return\n\n            if timeout and loop.time() - start_time > timeout:\n                logger.error(f'Timeout waiting for element to become {condition_msg}')\n                raise WaitElementTimeout(f'Timed out waiting for element to become {condition_msg}')\n\n            await asyncio.sleep(0.5)\n\n    async def click_using_js(self):\n        \"\"\"\n        Click element using JavaScript click() method.\n\n        Raises:\n            ElementNotVisible: If element is not visible.\n            ElementNotInteractable: If element couldn't be clicked.\n\n        Note:\n            For <option> elements, uses specialized selection approach.\n            Element is automatically scrolled into view.\n        \"\"\"\n        if await self._is_option_element():\n            return await self._click_option_tag()\n\n        await self.scroll_into_view()\n\n        if not await self.is_visible():\n            raise ElementNotVisible()\n\n        logger.info(f'Clicking element via JS: object_id={self._object_id}')\n        result = await self.execute_script(Scripts.CLICK, return_by_value=True)\n        clicked = result['result']['result']['value']\n        if not clicked:\n            raise ElementNotInteractable()\n\n    async def click(\n        self,\n        x_offset: int = 0,\n        y_offset: int = 0,\n        hold_time: float = 0.1,\n        humanize: bool = False,\n    ):\n        \"\"\"\n        Click element using simulated mouse events.\n\n        Args:\n            x_offset: Horizontal offset from element center.\n            y_offset: Vertical offset from element center.\n            hold_time: Duration to hold mouse button down (used when humanize=False).\n            humanize: When True and a Mouse instance is available, uses humanized\n                Bezier curve movement from the current tracked position to the\n                element center before clicking. When False, dispatches raw CDP\n                mousePressed/mouseReleased events directly.\n\n        Raises:\n            ElementNotVisible: If element is not visible.\n\n        Note:\n            For <option> elements, delegates to specialized JavaScript approach.\n            Element is automatically scrolled into view.\n        \"\"\"\n        if await self._is_option_element():\n            return await self._click_option_tag()\n\n        if not await self.is_visible():\n            raise ElementNotVisible()\n\n        await self.scroll_into_view()\n\n        try:\n            element_bounds = await self.bounds\n            position_to_click = self._calculate_center(element_bounds)\n            position_to_click = (\n                position_to_click[0] + x_offset,\n                position_to_click[1] + y_offset,\n            )\n        except KeyError:\n            element_bounds_js = await self.get_bounds_using_js()\n            position_to_click = (\n                element_bounds_js['x'] + element_bounds_js['width'] / 2 + x_offset,\n                element_bounds_js['y'] + element_bounds_js['height'] / 2 + y_offset,\n            )\n\n        has_iframe_context = getattr(self, '_iframe_context', None) is not None\n        if humanize and self._mouse is not None and not has_iframe_context:\n            logger.info(\n                f'Clicking element (humanized): x={position_to_click[0]}, y={position_to_click[1]}'\n            )\n            await self._mouse.click(position_to_click[0], position_to_click[1])\n            return\n\n        logger.info(\n            f'Clicking element: x={position_to_click[0]}, '\n            f'y={position_to_click[1]}, hold={hold_time}s'\n        )\n        press_command = InputCommands.dispatch_mouse_event(\n            type=MouseEventType.MOUSE_PRESSED,\n            x=int(position_to_click[0]),\n            y=int(position_to_click[1]),\n            button=MouseButton.LEFT,\n            click_count=1,\n        )\n        release_command = InputCommands.dispatch_mouse_event(\n            type=MouseEventType.MOUSE_RELEASED,\n            x=int(position_to_click[0]),\n            y=int(position_to_click[1]),\n            button=MouseButton.LEFT,\n            click_count=1,\n        )\n        await self._execute_command(press_command)\n        await asyncio.sleep(hold_time)\n        await self._execute_command(release_command)\n\n    async def focus(self):\n        \"\"\"Focus this element via CDP DOM.focus command.\"\"\"\n        await self._execute_command(DomCommands.focus(object_id=self._object_id))\n\n    async def clear(self):\n        \"\"\"\n        Clear the current value of the element.\n\n        Supports standard inputs, textareas, and contenteditable elements.\n        Dispatches ``input`` and ``change`` events so frameworks detect the update.\n\n        Raises:\n            ElementNotInteractable: If the element does not accept text input.\n        \"\"\"\n        logger.info('Clearing element value')\n        result = await self.execute_script(Scripts.CLEAR_INPUT, return_by_value=True)\n        success = result['result'].get('result', {}).get('value', False)\n        if not success:\n            logger.error('Element does not accept text input')\n            raise ElementNotInteractable('Element does not accept text input')\n        if self._attributes.get('tag_name', '').lower() in {'input', 'textarea'}:\n            self._attributes['value'] = ''\n\n    async def insert_text(self, text: str):\n        \"\"\"\n        Insert text into element using JavaScript.\n\n        Supports standard inputs, textareas, contenteditable elements, and rich text editors.\n        Inserts text at cursor position or replaces selected text.\n\n        Args:\n            text: Text to insert.\n\n        Raises:\n            ElementNotInteractable: If element does not accept text input.\n\n        Note:\n            Uses JavaScript for maximum compatibility with all input types.\n            Automatically handles input/textarea and contenteditable elements.\n        \"\"\"\n        logger.info(f'Inserting text (length={len(text)})')\n        result = await self.execute_script(\n            Scripts.INSERT_TEXT, return_by_value=True, arguments=[CallArgument(value=text)]\n        )\n        logger.debug(f'Insert text result: {result}')\n        success = result['result'].get('result', {}).get('value', False)\n\n        if not success:\n            logger.error('Element does not accept text input')\n            raise ElementNotInteractable('Element does not accept text input')\n        # Keep cached attributes coherent for common cases (e.g., input value)\n        # This avoids forcing a DOM round-trip for simple assertions.\n        if self._attributes.get('tag_name', '').lower() in {'input', 'textarea'}:\n            # When inserting into an empty field, resulting value equals inserted text.\n            # For complex cases (non-empty with caret), tests usually check non-empty.\n            self._attributes['value'] = text\n\n    async def set_input_files(self, files: str | Path | list[str | Path]):\n        \"\"\"\n        Set file paths for file input element.\n\n        Args:\n            files: list of absolute file paths to existing files.\n\n        Raises:\n            ElementNotAFileInput: If element is not a file input.\n        \"\"\"\n        if (\n            self._attributes.get('tag_name', '').lower() != 'input'\n            or self._attributes.get('type', '').lower() != 'file'\n        ):\n            raise ElementNotAFileInput()\n        files_list = [str(file) for file in files] if isinstance(files, list) else [str(files)]\n        logger.info(f'Setting input files: count={len(files_list)}')\n        await self._execute_command(\n            DomCommands.set_file_input_files(files=files_list, object_id=self._object_id)\n        )\n\n    async def type_text(\n        self,\n        text: str,\n        humanize: bool = False,\n        interval: Optional[float] = None,\n    ):\n        \"\"\"\n        Type text character by character.\n\n        Args:\n            text: Text to type into the element.\n            humanize: When True, simulates human-like typing.\n            interval: Deprecated. Use humanize=True instead.\n        \"\"\"\n        logger.info(f'Typing text (length={len(text)}, humanize={humanize})')\n        await self.click(humanize=humanize)\n        keyboard = self._get_keyboard()\n        await keyboard.type_text(text, humanize=humanize, interval=interval)\n\n    async def key_down(self, key: Key, modifiers: Optional[KeyModifier] = None):\n        \"\"\"\n        Send key down event.\n\n        .. deprecated::\n            This method is deprecated. Use ``tab.keyboard.down()`` instead.\n\n        Note:\n            Only sends key down without release. Pair with key_up() for complete keypress.\n        \"\"\"\n        warnings.warn(\n            'WebElement.key_down() is deprecated. '\n            'Use tab.keyboard API instead: await tab.keyboard.down(key, modifiers)',\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        key_name, code = key\n        logger.info(f'Key down: key={key_name} code={code} modifiers={modifiers}')\n        await self._execute_command(\n            InputCommands.dispatch_key_event(\n                type=KeyEventType.KEY_DOWN,\n                key=key_name,\n                windows_virtual_key_code=code,\n                native_virtual_key_code=code,\n                modifiers=modifiers,\n            )\n        )\n\n    async def key_up(self, key: Key):\n        \"\"\"\n        Send key up event (should follow corresponding key_down()).\n\n        .. deprecated::\n            This method is deprecated. Use ``tab.keyboard.up()`` instead.\n        \"\"\"\n        warnings.warn(\n            'WebElement.key_up() is deprecated. '\n            'Use tab.keyboard API instead: await tab.keyboard.up(key)',\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        key_name, code = key\n        logger.info(f'Key up: key={key_name} code={code}')\n        await self._execute_command(\n            InputCommands.dispatch_key_event(\n                type=KeyEventType.KEY_UP,\n                key=key_name,\n                windows_virtual_key_code=code,\n                native_virtual_key_code=code,\n            )\n        )\n\n    async def press_keyboard_key(\n        self,\n        key: Key,\n        modifiers: Optional[KeyModifier] = None,\n        interval: float = 0.1,\n    ):\n        \"\"\"\n        Press and release keyboard key with configurable timing.\n\n        .. deprecated::\n            This method is deprecated. Use ``tab.keyboard.press()`` instead.\n\n        Better for special keys (Enter, Tab, etc.) than type_text().\n        \"\"\"\n        warnings.warn(\n            'WebElement.press_keyboard_key() is deprecated. '\n            'Use tab.keyboard API instead: await tab.keyboard.press(key, modifiers, interval)',\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        await self.key_down(key, modifiers)\n        await asyncio.sleep(interval)\n        await self.key_up(key)\n\n    async def is_editable(self) -> bool:\n        \"\"\"\n        Check if element can accept text input.\n\n        Returns:\n            True if element is editable (input, textarea, or contenteditable).\n        \"\"\"\n        result = await self.execute_script(Scripts.IS_EDITABLE, return_by_value=True)\n        is_editable = result['result']['result']['value']\n        logger.debug(f'Element editable check: {is_editable}')\n        return is_editable\n\n    async def is_visible(self):\n        \"\"\"Check if element is visible using comprehensive JavaScript visibility test.\"\"\"\n        result = await self.execute_script(Scripts.ELEMENT_VISIBLE, return_by_value=True)\n        if 'error' in result:\n            return False\n        return bool(result.get('result', {}).get('result', {}).get('value', False))\n\n    async def is_on_top(self):\n        \"\"\"Check if element is topmost at its center point (not covered by overlays).\"\"\"\n        result = await self.execute_script(Scripts.ELEMENT_ON_TOP, return_by_value=True)\n        if 'error' in result:\n            return False\n        return bool(result.get('result', {}).get('result', {}).get('value', False))\n\n    async def is_interactable(self):\n        \"\"\"Check if element is interactable based on visibility and position.\"\"\"\n        result = await self.execute_script(Scripts.ELEMENT_INTERACTIVE, return_by_value=True)\n        if 'error' in result:\n            return False\n        return bool(result.get('result', {}).get('result', {}).get('value', False))\n\n    async def execute_script(\n        self,\n        script: str,\n        *,\n        arguments: Optional[list[CallArgument]] = None,\n        silent: Optional[bool] = None,\n        return_by_value: Optional[bool] = None,\n        generate_preview: Optional[bool] = None,\n        user_gesture: Optional[bool] = None,\n        await_promise: Optional[bool] = None,\n        execution_context_id: Optional[int] = None,\n        object_group: Optional[str] = None,\n        throw_on_side_effect: Optional[bool] = None,\n        unique_context_id: Optional[str] = None,\n        serialization_options: Optional[SerializationOptions] = None,\n    ) -> CallFunctionOnResponse:\n        \"\"\"\n        Execute JavaScript in element context.\n\n        Args:\n            script (str): JavaScript code to execute. Use 'this' to reference this element.\n            arguments (Optional[list[CallArgument]]): Arguments to pass to the function\n                (Runtime.callFunctionOn).\n            silent (Optional[bool]): Whether to silence exceptions (Runtime.callFunctionOn).\n            return_by_value (Optional[bool]): Whether to return the result by value instead of\n                reference (Runtime.callFunctionOn).\n            generate_preview (Optional[bool]): Whether to generate a preview for the result\n                (Runtime.callFunctionOn).\n            user_gesture (Optional[bool]): Whether to treat the call as initiated by user\n                gesture (Runtime.callFunctionOn).\n            await_promise (Optional[bool]): Whether to await promise result\n                (Runtime.callFunctionOn).\n            execution_context_id (Optional[int]): ID of the execution context to call the\n                function in (Runtime.callFunctionOn).\n            object_group (Optional[str]): Symbolic group name for the result\n                (Runtime.callFunctionOn).\n            throw_on_side_effect (Optional[bool]): Whether to throw if side effect cannot be\n                ruled out (Runtime.callFunctionOn).\n            unique_context_id (Optional[str]): Unique context ID for the function call\n                (Runtime.callFunctionOn).\n            serialization_options (Optional[SerializationOptions]): Serialization options for\n                the result (Runtime.callFunctionOn).\n\n        Returns:\n            CallFunctionOnResponse: The result of the script execution.\n\n        Examples:\n            # Click the element\n            await element.execute_script('this.click()')\n\n            # Modify element style\n            await element.execute_script('this.style.border = \"2px solid red\"')\n\n            # Get element text\n            result = await element.execute_script('return this.textContent', return_by_value=True)\n\n            # Set element content\n            await element.execute_script('this.textContent = \"Hello World\"')\n        \"\"\"\n        if not is_script_already_function(script):\n            script = f'function(){{ {script} }}'\n\n        logger.debug(\n            f'Executing script on element: return_by_value={return_by_value}, '\n            f'length={len(script)}, args={len(arguments) if arguments else 0}'\n        )\n        command = RuntimeCommands.call_function_on(\n            function_declaration=script,\n            object_id=self._object_id,\n            arguments=arguments,\n            silent=silent,\n            return_by_value=return_by_value,\n            generate_preview=generate_preview,\n            user_gesture=user_gesture,\n            await_promise=await_promise,\n            execution_context_id=execution_context_id,\n            object_group=object_group,\n            throw_on_side_effect=throw_on_side_effect,\n            unique_context_id=unique_context_id,\n            serialization_options=serialization_options,\n        )\n        return await self._execute_command(command)\n\n    def __repr__(self):\n        \"\"\"String representation showing attributes and object ID.\"\"\"\n        attrs = ', '.join(f'{k}={v!r}' for k, v in self._attributes.items())\n        return f'{self.__class__.__name__}({attrs})(object_id={self._object_id})'\n\n    def _is_inside_iframe(self) -> bool:\n        \"\"\"Check if this element is inside an iframe context (not the iframe itself).\"\"\"\n        return self._iframe_context is not None and not self.is_iframe\n\n    async def _get_iframe_inner_html(self) -> str:\n        \"\"\"Get inner HTML of an iframe element.\"\"\"\n        iframe_context = await self.iframe_context\n        if iframe_context is None:\n            raise InvalidIFrame('Unable to resolve iframe context')\n        response: EvaluateResponse = await self._execute_command(\n            RuntimeCommands.evaluate(\n                expression='document.documentElement.outerHTML',\n                context_id=iframe_context.execution_context_id,\n                return_by_value=True,\n            )\n        )\n        return response['result']['result'].get('value', '')\n\n    def _apply_routing_from_context(self) -> None:\n        \"\"\"Apply routing attributes from iframe context.\n\n        After iframe context resolution, commands targeting the *content* of\n        the iframe should route through ``_iframe_context`` (handled by\n        ``_resolve_routing`` which prioritises ``_iframe_context`` over\n        ``_routing_session_*``).\n\n        The ``_routing_session_handler`` / ``_routing_session_id`` attributes\n        must be preserved: they identify the parent OOPIF session where the\n        ``<iframe>`` *element itself* lives.  The resolver needs them to\n        re-describe the element on subsequent re-resolutions.\n        \"\"\"\n\n    async def _click_option_tag(self):\n        \"\"\"Specialized method for clicking <option> elements in dropdowns.\"\"\"\n        await self._execute_command(\n            RuntimeCommands.call_function_on(\n                object_id=self._object_id,\n                function_declaration=Scripts.CLICK_OPTION_TAG,\n                return_by_value=True,\n            )\n        )\n\n    async def _get_family_elements(\n        self, script: str, max_depth: int = 1, tag_filter: list[str] = []\n    ) -> list[WebElement]:\n        \"\"\"\n        Retrieve all family elements of this element (elements at the same DOM level).\n\n        Args:\n            script (str): CDP script to execute for retrieving family elements.\n            tag_filter (list[str], optional): List of HTML tag names to filter results.\n                If empty, returns all family elements regardless of tag. Defaults to [].\n\n        Returns:\n            list[WebElement]: List of family WebElement objects that share the same\n                parent as this element and match the tag filter criteria.\n        \"\"\"\n        result = await self.execute_script(\n            script.format(tag_filter=tag_filter, max_depth=max_depth)\n        )\n        if not self._has_object_id_key(result):\n            return []\n\n        array_object_id = result['result']['result']['objectId']\n\n        get_properties_command = RuntimeCommands.get_properties(object_id=array_object_id)\n        properties_response: GetPropertiesResponse = await self._execute_command(\n            get_properties_command\n        )\n\n        family_elements: list[WebElement] = []\n        for prop in properties_response['result']['result']:\n            if not (prop['name'].isdigit() and 'objectId' in prop['value']):\n                continue\n            child_object_id = prop['value']['objectId']\n            attributes = await self._get_object_attributes(object_id=child_object_id)\n            family_elements.append(\n                WebElement(\n                    child_object_id,\n                    self._connection_handler,\n                    attributes_list=attributes,\n                    mouse=self._mouse,\n                )\n            )\n\n        logger.debug(f'Family elements found: {len(family_elements)}')\n        return family_elements\n\n    def _def_attributes(self, attributes_list: list[str]):\n        \"\"\"Process flat attribute list into dictionary (renames 'class' to 'class_name').\"\"\"\n        for i in range(0, len(attributes_list), 2):\n            key = attributes_list[i]\n            key = key if key != 'class' else 'class_name'\n            value = attributes_list[i + 1]\n            self._attributes[key] = value\n        logger.debug(f'Attributes defined: count={len(self._attributes)}')\n\n    def _is_option_tag(self):\n        \"\"\"Check if element is an <option> tag.\"\"\"\n        return self._attributes.get('tag_name', '').lower() == 'option'\n\n    async def _is_option_element(self) -> bool:\n        \"\"\"\n        Robust check for <option> elements, falling back to JS when tag_name is missing.\n        \"\"\"\n        tag = self._attributes.get('tag_name', '')\n        if tag:\n            return tag.lower() == 'option'\n\n        # Heuristic from original selector/method\n        selector = str(getattr(self, '_selector', '') or '')\n        method_raw = getattr(self, '_search_method', '')\n        method = str(getattr(method_raw, 'value', method_raw) or '').lower()\n        if method == 'tag_name' and selector.lower() == 'option':\n            return True\n        if method == 'xpath' and 'option' in selector.lower():\n            return True\n\n        result = await self.execute_script(Scripts.IS_OPTION_TAG, return_by_value=True)\n        is_option = result.get('result', {}).get('result', {}).get('value', False)\n        if is_option and not self._attributes.get('tag_name'):\n            self._attributes['tag_name'] = 'option'\n        return bool(is_option)\n\n    @staticmethod\n    def _calculate_center(bounds: list) -> tuple:\n        \"\"\"Calculate center point from bounding box coordinates.\"\"\"\n        x_values = [bounds[i] for i in range(0, len(bounds), 2)]\n        y_values = [bounds[i] for i in range(1, len(bounds), 2)]\n        x_center = sum(x_values) / len(x_values)\n        y_center = sum(y_values) / len(y_values)\n        return x_center, y_center\n"
  },
  {
    "path": "pydoll/exceptions.py",
    "content": "\"\"\"\nPydoll Exception Classes\n\nThis module contains all exception classes used throughout the Pydoll library,\norganized into logical categories based on their function and usage patterns.\nEach category uses a base class to provide common functionality for related exceptions.\n\"\"\"\n\n\nclass PydollException(Exception):\n    \"\"\"Base class for all Pydoll exceptions.\"\"\"\n\n    message = 'An error occurred in Pydoll'\n\n    def __init__(self, message: str = ''):\n        self.message = message or self.message\n\n    def __str__(self):\n        return self.message\n\n\nclass ConnectionException(PydollException):\n    \"\"\"Base class for exceptions related to browser connection.\"\"\"\n\n    message = 'A connection error occurred'\n\n\nclass ConnectionFailed(ConnectionException):\n    \"\"\"Raised when connection to the browser cannot be established.\"\"\"\n\n    message = 'Failed to connect to the browser'\n\n\nclass ReconnectionFailed(ConnectionException):\n    \"\"\"Raised when an attempt to reconnect to the browser fails.\"\"\"\n\n    message = 'Failed to reconnect to the browser'\n\n\nclass WebSocketConnectionClosed(ConnectionException):\n    \"\"\"Raised when the WebSocket connection to the browser is closed unexpectedly.\"\"\"\n\n    message = 'The WebSocket connection is closed'\n\n\nclass NetworkError(ConnectionException):\n    \"\"\"Raised when a general network error occurs during browser communication.\"\"\"\n\n    message = 'A network error occurred'\n\n\nclass BrowserException(PydollException):\n    \"\"\"Base class for exceptions related to browser process management.\"\"\"\n\n    message = 'A browser error occurred'\n\n\nclass BrowserNotRunning(BrowserException):\n    \"\"\"Raised when attempting to interact with a browser that is not running.\"\"\"\n\n    message = 'The browser is not running'\n\n\nclass FailedToStartBrowser(BrowserException):\n    \"\"\"Raised when the browser process cannot be started.\"\"\"\n\n    message = 'Failed to start the browser'\n\n\nclass UnsupportedOS(BrowserException):\n    \"\"\"Raised when attempting to run on an unsupported operating system.\"\"\"\n\n    message = 'Unsupported OS'\n\n\nclass NoValidTabFound(BrowserException):\n    \"\"\"Raised when no valid browser tab can be found or created.\"\"\"\n\n    message = 'No valid attached tab found'\n\n\nclass InvalidConnectionPort(BrowserException):\n    \"\"\"Raised when an invalid (non-positive) connection port is provided.\"\"\"\n\n    message = 'Connection port must be a positive integer'\n\n\nclass InvalidWebSocketAddress(BrowserException):\n    \"\"\"Raised when an invalid WebSocket address is provided or required but missing.\"\"\"\n\n    message = 'Invalid WebSocket address'\n\n\nclass MissingTargetOrWebSocket(BrowserException):\n    \"\"\"Raised when a Tab has neither target ID nor WebSocket address available.\"\"\"\n\n    message = 'Tab has no target ID or WebSocket address'\n\n\nclass ProtocolException(PydollException):\n    \"\"\"Base class for exceptions related to CDP protocol communication.\"\"\"\n\n    message = 'A protocol error occurred'\n\n\nclass TopLevelTargetRequired(ProtocolException):\n    \"\"\"Raised when a command can only be executed on top-level targets.\"\"\"\n\n    message = 'Command can only be executed on top-level targets.'\n\n\nclass InvalidCommand(ProtocolException):\n    \"\"\"Raised when an invalid command is sent to the browser.\"\"\"\n\n    message = 'The command provided is invalid'\n\n\nclass InvalidResponse(ProtocolException):\n    \"\"\"Raised when an invalid response is received from the browser.\"\"\"\n\n    message = 'The response received is invalid'\n\n\nclass ResendCommandFailed(ProtocolException):\n    \"\"\"Raised when an attempt to resend a failed command fails.\"\"\"\n\n    message = 'Failed to resend the command'\n\n\nclass CommandExecutionTimeout(ProtocolException):\n    \"\"\"Raised when a command execution times out.\"\"\"\n\n    message = 'The command execution timed out'\n\n\nclass InvalidCallback(ProtocolException):\n    \"\"\"Raised when an invalid callback is provided for an event.\"\"\"\n\n    message = 'The callback provided is invalid'\n\n\nclass EventNotSupported(ProtocolException):\n    \"\"\"Raised when an attempt is made to subscribe to an unsupported event.\"\"\"\n\n    message = 'The event is not supported'\n\n\nclass ElementException(PydollException):\n    \"\"\"Base class for exceptions related to element interactions.\"\"\"\n\n    message = 'An element interaction error occurred'\n\n\nclass ElementNotFound(ElementException):\n    \"\"\"Raised when an element cannot be found in the DOM.\"\"\"\n\n    message = 'The specified element was not found'\n\n\nclass ElementNotVisible(ElementException):\n    \"\"\"Raised when attempting to interact with an element that is not visible.\"\"\"\n\n    message = 'The element is not visible'\n\n\nclass ElementNotInteractable(ElementException):\n    \"\"\"Raised when attempting to interact with an element that cannot receive interaction.\"\"\"\n\n    message = 'The element is not interactable'\n\n\nclass ClickIntercepted(ElementException):\n    \"\"\"Raised when a click operation is intercepted by another element.\"\"\"\n\n    message = 'The click was intercepted'\n\n\nclass ElementNotAFileInput(ElementException):\n    \"\"\"Raised when attempting to use file input methods on a non-file input element.\"\"\"\n\n    message = 'The element is not a file input'\n\n\nclass ShadowRootNotFound(ElementException):\n    \"\"\"Raised when an element does not have an attached shadow root.\"\"\"\n\n    message = 'No shadow root attached to this element'\n\n\nclass TimeoutException(PydollException):\n    \"\"\"Base class for exceptions related to timeouts.\"\"\"\n\n    message = 'A timeout occurred'\n\n\nclass PageLoadTimeout(TimeoutException):\n    \"\"\"Raised when a page load operation times out.\"\"\"\n\n    message = 'Page load timed out'\n\n\nclass WaitElementTimeout(TimeoutException):\n    \"\"\"Raised when waiting for an element times out.\"\"\"\n\n    message = 'Timed out waiting for element to appear'\n\n\nclass DownloadTimeout(TimeoutException):\n    \"\"\"Raised when waiting for a file download to complete times out.\"\"\"\n\n    message = 'Timed out waiting for download to complete'\n\n\nclass ConfigurationException(PydollException):\n    \"\"\"Base class for exceptions related to configuration and options.\"\"\"\n\n    message = 'A configuration error occurred'\n\n\nclass InvalidOptionsObject(ConfigurationException):\n    \"\"\"Raised when an invalid options object is provided.\"\"\"\n\n    message = 'The options object provided is invalid'\n\n\nclass InvalidBrowserPath(ConfigurationException):\n    \"\"\"Raised when an invalid browser executable path is provided.\"\"\"\n\n    message = 'The browser path provided is invalid'\n\n\nclass ArgumentAlreadyExistsInOptions(ConfigurationException):\n    \"\"\"Raised when attempting to add a duplicate argument to browser options.\"\"\"\n\n    message = 'The argument already exists in the options'\n\n\nclass ArgumentNotFoundInOptions(ConfigurationException):\n    \"\"\"Raised when attempting to remove an argument that does not exist in browser options.\"\"\"\n\n    message = 'The argument does not exist in the options'\n\n\nclass InvalidFileExtension(ConfigurationException):\n    \"\"\"Raised when an unsupported file extension is provided.\"\"\"\n\n    message = 'The file extension provided is not supported'\n\n\nclass InvalidTabInitialization(ConfigurationException):\n    \"\"\"Raised when creating a Tab without connection_port, target_id or ws_address.\"\"\"\n\n    message = 'Either connection_port, target_id, or ws_address must be provided'\n\n\nclass MissingScreenshotPath(ConfigurationException):\n    \"\"\"Raised when take_screenshot is called without path and not returning base64.\"\"\"\n\n    message = 'path is required when as_base64 is False'\n\n\nclass DialogException(PydollException):\n    \"\"\"Base class for exceptions related to browser dialogs.\"\"\"\n\n    message = 'A dialog error occurred'\n\n\nclass NoDialogPresent(DialogException):\n    \"\"\"Raised when attempting to interact with a dialog that doesn't exist.\"\"\"\n\n    message = 'No dialog present on the page'\n\n\nclass NotAnIFrame(PydollException):\n    \"\"\"Raised when an element is not an iframe.\"\"\"\n\n    message = 'The element is not an iframe'\n\n\nclass InvalidIFrame(PydollException):\n    \"\"\"Raised when an iframe is not valid.\"\"\"\n\n    message = 'The iframe is not valid'\n\n\nclass IFrameNotFound(PydollException):\n    \"\"\"Raised when an iframe is not found.\"\"\"\n\n    message = 'The iframe was not found'\n\n\nclass NetworkEventsNotEnabled(PydollException):\n    \"\"\"Raised when network events are not enabled.\"\"\"\n\n    message = 'Network events not enabled'\n\n\nclass RequestException(PydollException):\n    \"\"\"Base class for exceptions related to HTTP requests.\"\"\"\n\n    message = 'An HTTP request error occurred'\n\n\nclass HTTPError(RequestException):\n    \"\"\"Exception raised for HTTP error responses (4xx and 5xx status codes).\"\"\"\n\n    message = 'An HTTP error occurred'\n\n\nclass HarRecordingError(RequestException):\n    \"\"\"Raised when HAR recording fails.\"\"\"\n\n    message = 'HAR recording error occurred'\n\n\nclass ScriptException(PydollException):\n    \"\"\"Base class for exceptions related to JavaScript execution.\"\"\"\n\n    message = 'A script execution error occurred'\n\n\nclass InvalidScriptWithElement(ScriptException):\n    \"\"\"Raised when a script contains 'argument' but no element is provided.\"\"\"\n\n    message = 'Script contains \"argument\" but no element was provided'\n\n\nclass WrongPrefsDict(PydollException):\n    \"\"\"Raised when the prefs dict provided contains the 'prefs' key\"\"\"\n\n    message = 'The dict can not contain \"prefs\" key, provide only the prefs options'\n\n\nclass ElementPreconditionError(ElementException):\n    \"\"\"Raised when invalid or missing preconditions are provided for element operations.\"\"\"\n\n    message = 'Invalid element preconditions'\n"
  },
  {
    "path": "pydoll/interactions/__init__.py",
    "content": "from pydoll.constants import DEFAULT_TYPO_PROBABILITY, TypoType\nfrom pydoll.interactions.iframe import IFrameContext, IFrameContextResolver\nfrom pydoll.interactions.keyboard import (\n    Keyboard,\n    KeyboardAPI,\n    TimingConfig,\n    TypoConfig,\n    TypoResult,\n)\nfrom pydoll.interactions.mouse import Mouse, MouseAPI, MouseTimingConfig\nfrom pydoll.interactions.scroll import Scroll, ScrollAPI, ScrollTimingConfig\n\n__all__ = [\n    'DEFAULT_TYPO_PROBABILITY',\n    'IFrameContext',\n    'IFrameContextResolver',\n    'Keyboard',\n    'KeyboardAPI',\n    'Mouse',\n    'MouseAPI',\n    'MouseTimingConfig',\n    'Scroll',\n    'ScrollAPI',\n    'ScrollTimingConfig',\n    'TimingConfig',\n    'TypoConfig',\n    'TypoResult',\n    'TypoType',\n]\n"
  },
  {
    "path": "pydoll/interactions/iframe.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING, Iterable, Optional\n\nfrom pydoll.commands import DomCommands, PageCommands, RuntimeCommands, TargetCommands\nfrom pydoll.connection import ConnectionHandler\nfrom pydoll.exceptions import InvalidIFrame\nfrom pydoll.protocol.dom.methods import DescribeNodeResponse, GetFrameOwnerResponse\nfrom pydoll.protocol.dom.types import Node\nfrom pydoll.protocol.page.methods import CreateIsolatedWorldResponse, GetFrameTreeResponse\nfrom pydoll.protocol.page.types import Frame, FrameTree\nfrom pydoll.protocol.runtime.methods import EvaluateResponse\nfrom pydoll.protocol.target.methods import AttachToTargetResponse, GetTargetsResponse\n\nif TYPE_CHECKING:\n    from pydoll.elements.web_element import WebElement\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass IFrameContext:\n    \"\"\"Context information for an iframe element.\"\"\"\n\n    frame_id: str\n    document_url: Optional[str] = None\n    execution_context_id: Optional[int] = None\n    document_object_id: Optional[str] = None\n    session_handler: Optional[ConnectionHandler] = None\n    session_id: Optional[str] = None\n\n\nclass IFrameContextResolver:\n    \"\"\"Resolves iframe context for WebElement.\"\"\"\n\n    def __init__(self, element: WebElement):\n        self._element = element\n\n    async def resolve(self) -> IFrameContext:\n        \"\"\"\n        Resolve and return iframe context.\n\n        Returns:\n            IFrameContext with frame_id, document_url, execution_context_id,\n            document_object_id and session info for OOPIF targets.\n\n        Raises:\n            InvalidIFrame: If unable to resolve the iframe context.\n        \"\"\"\n        base_handler, base_session_id = self._get_base_session()\n        node_info = await self._describe_element_node(base_handler, base_session_id)\n        frame_id, document_url, content_frame_id, backend_node_id = (\n            self._extract_frame_metadata(node_info)\n        )\n\n        if not frame_id and backend_node_id is not None:\n            frame_id, document_url = await self._resolve_frame_by_owner(\n                base_handler, base_session_id, backend_node_id, document_url\n            )\n\n        session_handler, session_id, frame_id, document_url = await self._resolve_oopif_if_needed(\n            frame_id,\n            content_frame_id,\n            backend_node_id,\n            current_document_url=document_url,\n            base_handler=base_handler,\n            base_session_id=base_session_id,\n        )\n\n        if not frame_id:\n            raise InvalidIFrame('Unable to resolve frameId for the iframe element')\n\n        context = IFrameContext(frame_id=frame_id, document_url=document_url)\n\n        if session_handler and session_id:\n            context.session_handler = session_handler\n            context.session_id = session_id\n\n        effective_handler = session_handler or base_handler\n        effective_session_id = session_id or base_session_id\n\n        execution_context_id = await self._create_isolated_world_for_frame(\n            frame_id, effective_handler, effective_session_id\n        )\n        context.execution_context_id = execution_context_id\n\n        document_object_id = await self._get_document_object_id(execution_context_id, context)\n        context.document_object_id = document_object_id\n\n        return context\n\n    def _get_base_session(self) -> tuple[ConnectionHandler, Optional[str]]:\n        \"\"\"Return the default handler and session id for routing commands.\"\"\"\n        handler = (\n            getattr(self._element, '_routing_session_handler', None)\n            or self._element._connection_handler\n        )\n        session_id = getattr(self._element, '_routing_session_id', None)\n        return handler, session_id\n\n    async def _describe_element_node(\n        self,\n        handler: ConnectionHandler,\n        session_id: Optional[str],\n    ) -> Node:\n        \"\"\"Describe the iframe element using the given handler/session.\n\n        This bypasses ``_resolve_routing()`` which, after a previous\n        resolution, may return the iframe *content* session instead of\n        the parent session where the element actually lives.\n        \"\"\"\n        command = DomCommands.describe_node(object_id=self._element._object_id)\n        if session_id:\n            command['sessionId'] = session_id\n        response: DescribeNodeResponse = await handler.execute_command(command)\n        if 'error' in response:\n            return {}\n        return response.get('result', {}).get('node', {})\n\n    @staticmethod\n    def _extract_frame_metadata(\n        node_info: Node,\n    ) -> tuple[Optional[str], Optional[str], Optional[str], Optional[int]]:\n        \"\"\"Extract iframe-related metadata from DOM node info.\n\n        Returns:\n            Tuple of (frame_id, document_url, content_frame_id, backend_node_id).\n            ``content_frame_id`` is the frame ID of the frame *created* by the\n            ``<iframe>`` element (``node_info['frameId']`` on frame-owner\n            elements).  For same-origin iframes it equals\n            ``contentDocument.frameId``; for OOPIFs ``contentDocument`` is\n            absent but ``content_frame_id`` is still set by the browser.\n        \"\"\"\n        content_document = node_info.get('contentDocument') or {}\n        content_frame_id = node_info.get('frameId')\n        backend_node_id = node_info.get('backendNodeId')\n        frame_id = content_document.get('frameId')\n        document_url = (\n            content_document.get('documentURL')\n            or content_document.get('baseURL')\n            or node_info.get('documentURL')\n            or node_info.get('baseURL')\n        )\n        return frame_id, document_url, content_frame_id, backend_node_id\n\n    async def _resolve_frame_by_owner(\n        self,\n        base_handler: ConnectionHandler,\n        base_session_id: Optional[str],\n        backend_node_id: int,\n        current_document_url: Optional[str],\n    ) -> tuple[Optional[str], Optional[str]]:\n        \"\"\"Resolve frame id and URL by matching owner backend_node_id.\"\"\"\n        owner_frame_id, owner_url = await self._find_frame_by_owner(\n            base_handler, base_session_id, backend_node_id\n        )\n        if not owner_frame_id:\n            return None, current_document_url\n        return owner_frame_id, owner_url or current_document_url\n\n    async def _find_frame_by_owner(\n        self,\n        handler: ConnectionHandler,\n        session_id: Optional[str],\n        backend_node_id: int,\n    ) -> tuple[Optional[str], Optional[str]]:\n        \"\"\"Find frame by matching owner backend_node_id.\"\"\"\n        frame_tree = await self._get_frame_tree_for(handler, session_id)\n        for frame_node in self._walk_frames(frame_tree):\n            candidate_frame_id = frame_node.get('id', '')\n            if not candidate_frame_id:\n                continue\n            owner_backend_id = await self._owner_backend_for(\n                handler, session_id, candidate_frame_id\n            )\n            if owner_backend_id == backend_node_id:\n                return candidate_frame_id, frame_node.get('url')\n        return None, None\n\n    @staticmethod\n    async def _get_frame_tree_for(\n        handler: ConnectionHandler,\n        session_id: Optional[str],\n    ) -> FrameTree:\n        \"\"\"Get Page frame tree for the given connection/target.\"\"\"\n        command = PageCommands.get_frame_tree()\n        if session_id:\n            command['sessionId'] = session_id\n        response: GetFrameTreeResponse = await handler.execute_command(command)\n        return response['result']['frameTree']\n\n    @staticmethod\n    def _walk_frames(tree: FrameTree) -> Iterable[Frame]:\n        \"\"\"Recursively traverse FrameTree and collect all frame descriptors.\"\"\"\n        if not tree:\n            return []\n        frames: list[Frame] = [tree['frame']]\n        for child_frame in tree.get('childFrames', []) or []:\n            frames.extend(IFrameContextResolver._walk_frames(child_frame))\n        return [frame_node for frame_node in frames if frame_node]\n\n    @staticmethod\n    async def _owner_backend_for(\n        handler: ConnectionHandler,\n        session_id: Optional[str],\n        frame_id: str,\n    ) -> Optional[int]:\n        \"\"\"Get backendNodeId of the DOM element that owns the given frame.\"\"\"\n        command = DomCommands.get_frame_owner(frame_id=frame_id)\n        if session_id:\n            command['sessionId'] = session_id\n        response: GetFrameOwnerResponse = await handler.execute_command(command)\n        return response.get('result', {}).get('backendNodeId')\n\n    async def _resolve_oopif_if_needed(\n        self,\n        current_frame_id: Optional[str],\n        content_frame_id: Optional[str],\n        backend_node_id: Optional[int],\n        current_document_url: Optional[str],\n        base_handler: Optional[ConnectionHandler] = None,\n        base_session_id: Optional[str] = None,\n    ) -> tuple[Optional[ConnectionHandler], Optional[str], Optional[str], Optional[str]]:\n        \"\"\"Resolve OOPIF and routing when needed.\"\"\"\n        if not content_frame_id or (current_frame_id and backend_node_id is None):\n            return None, None, current_frame_id, current_document_url\n\n        (\n            session_handler,\n            session_id,\n            resolved_frame_id,\n            resolved_url,\n        ) = await self._resolve_oopif_by_parent(\n            content_frame_id, backend_node_id, base_handler, base_session_id\n        )\n\n        if session_handler and session_id and resolved_url:\n            return (\n                session_handler,\n                session_id,\n                resolved_frame_id or current_frame_id,\n                resolved_url or current_document_url,\n            )\n\n        return (\n            None,\n            None,\n            current_frame_id or resolved_frame_id,\n            current_document_url or resolved_url,\n        )\n\n    async def _resolve_oopif_by_parent(\n        self,\n        content_frame_id: str,\n        backend_node_id: Optional[int],\n        base_handler: Optional[ConnectionHandler] = None,\n        base_session_id: Optional[str] = None,\n    ) -> tuple[Optional[ConnectionHandler], Optional[str], Optional[str], Optional[str]]:\n        \"\"\"Resolve out-of-process iframe using content frame id.\n\n        ``content_frame_id`` is the frame ID of the frame *created* by the\n        ``<iframe>`` element (obtained from ``DOM.describeNode``'s\n        ``node.frameId``).  For OOPIF targets the root frame of the target\n        shares this ID, so we can match directly without needing\n        ``DOM.getFrameOwner``.\n\n        When a direct frame-ID match is not possible (e.g. nested sub-frames\n        inside the OOPIF), the method falls back to ``DOM.getFrameOwner``\n        using the routing handler/session that has DOM visibility into the\n        parent context.\n        \"\"\"\n        browser_handler = ConnectionHandler(\n            connection_port=self._element._connection_handler._connection_port\n        )\n        targets_response: GetTargetsResponse = await browser_handler.execute_command(\n            TargetCommands.get_targets()\n        )\n        target_infos = targets_response.get('result', {}).get('targetInfos', [])\n\n        # The handler/session that can resolve DOM.getFrameOwner for the\n        # element's context.  When the <iframe> lives inside a nested OOPIF\n        # the Tab-level handler has no visibility; we must route through the\n        # session that originally found the element.\n        owner_handler = base_handler or self._element._connection_handler\n        owner_session_id = base_session_id\n\n        direct_children = [\n            target_info\n            for target_info in target_infos\n            if target_info.get('type') in {'iframe', 'page'}\n            and target_info.get('parentFrameId') == content_frame_id\n        ]\n\n        is_single_child = len(direct_children) == 1\n        for child_target in direct_children:\n            attach_response: AttachToTargetResponse = await browser_handler.execute_command(\n                TargetCommands.attach_to_target(target_id=child_target['targetId'], flatten=True)\n            )\n            attached_session_id = attach_response.get('result', {}).get('sessionId')\n            if not attached_session_id:\n                continue\n\n            frame_tree = await self._get_frame_tree_for(browser_handler, attached_session_id)\n            root_frame = (frame_tree or {}).get('frame', {})\n            root_frame_id = root_frame.get('id', '')\n\n            if is_single_child and root_frame_id and backend_node_id is None:\n                return (\n                    browser_handler,\n                    attached_session_id,\n                    root_frame_id,\n                    root_frame.get('url'),\n                )\n\n            if root_frame_id and backend_node_id is not None:\n                owner_backend_id = await self._owner_backend_for(\n                    owner_handler, owner_session_id, root_frame_id\n                )\n                if owner_backend_id == backend_node_id:\n                    return (\n                        browser_handler,\n                        attached_session_id,\n                        root_frame_id,\n                        root_frame.get('url'),\n                    )\n\n        for target_info in target_infos:\n            if target_info.get('type') not in {'iframe', 'page'}:\n                continue\n            attach_response = await browser_handler.execute_command(\n                TargetCommands.attach_to_target(\n                    target_id=target_info.get('targetId', ''), flatten=True\n                )\n            )\n            attached_session_id = attach_response.get('result', {}).get('sessionId')\n            if not attached_session_id:\n                continue\n\n            frame_tree = await self._get_frame_tree_for(browser_handler, attached_session_id)\n            root_frame = (frame_tree or {}).get('frame', {})\n            root_frame_id = root_frame.get('id', '')\n\n            # Direct match: the <iframe> element's frameId (content_frame_id)\n            # equals this target's root frame ID.  This handles nested OOPIFs\n            # where DOM.getFrameOwner cannot be resolved through the main\n            # page handler.\n            if root_frame_id and root_frame_id == content_frame_id:\n                return (\n                    browser_handler,\n                    attached_session_id,\n                    root_frame_id,\n                    root_frame.get('url'),\n                )\n\n            if root_frame_id and backend_node_id is not None:\n                owner_backend_id = await self._owner_backend_for(\n                    owner_handler, owner_session_id, root_frame_id\n                )\n                if owner_backend_id == backend_node_id:\n                    return (\n                        browser_handler,\n                        attached_session_id,\n                        root_frame_id,\n                        root_frame.get('url'),\n                    )\n\n            child_frame_id = self._find_child_by_parent(frame_tree, content_frame_id)\n            if child_frame_id:\n                return browser_handler, attached_session_id, child_frame_id, None\n\n        return None, None, None, None\n\n    @staticmethod\n    def _find_child_by_parent(tree: FrameTree, parent_id: str) -> Optional[str]:\n        \"\"\"Find id of child frame whose parentId equals the given one.\"\"\"\n        if not tree:\n            return None\n        for child in tree.get('childFrames', []) or []:\n            child_frame = child.get('frame', {})\n            if child_frame.get('parentId') == parent_id:\n                return child_frame.get('id')\n            found = IFrameContextResolver._find_child_by_parent(child, parent_id)\n            if found:\n                return found\n        return None\n\n    @staticmethod\n    async def _create_isolated_world_for_frame(\n        frame_id: str,\n        handler: ConnectionHandler,\n        session_id: Optional[str],\n    ) -> int:\n        \"\"\"Create isolated world for the given frame.\"\"\"\n        create_command = PageCommands.create_isolated_world(\n            frame_id=frame_id,\n            world_name=f'pydoll::iframe::{frame_id}',\n            grant_universal_access=True,\n        )\n        if session_id:\n            create_command['sessionId'] = session_id\n        create_response: CreateIsolatedWorldResponse = await handler.execute_command(create_command)\n        execution_context_id = create_response.get('result', {}).get('executionContextId')\n        if not execution_context_id:\n            raise InvalidIFrame('Unable to create isolated world for iframe')\n        return execution_context_id\n\n    async def _get_document_object_id(\n        self,\n        execution_context_id: int,\n        context: IFrameContext,\n    ) -> str:\n        \"\"\"Get document.documentElement object id in iframe context.\"\"\"\n        evaluate_command = RuntimeCommands.evaluate(\n            expression='document.documentElement',\n            context_id=execution_context_id,\n        )\n        if context.session_id:\n            evaluate_command['sessionId'] = context.session_id\n\n        handler = context.session_handler or self._element._connection_handler\n        evaluate_response: EvaluateResponse = await handler.execute_command(evaluate_command)\n\n        result_object = evaluate_response.get('result', {}).get('result', {})\n        document_object_id = result_object.get('objectId')\n        if not document_object_id:\n            raise InvalidIFrame('Unable to obtain document reference for iframe')\n        return document_object_id\n"
  },
  {
    "path": "pydoll/interactions/keyboard.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport logging\nimport random\nimport warnings\nfrom dataclasses import dataclass\nfrom typing import Any, Optional, Protocol, cast\n\nfrom pydoll.commands import InputCommands\nfrom pydoll.constants import (\n    CHAR_TO_KEY_INFO,\n    DEFAULT_TYPO_PROBABILITY,\n    QWERTY_NEIGHBORS,\n    Key,\n    TypoType,\n)\nfrom pydoll.protocol.input.types import KeyEventType, KeyModifier\n\nlogger = logging.getLogger(__name__)\n\n\nclass CommandExecutor(Protocol):\n    \"\"\"Protocol for objects that can execute CDP commands.\"\"\"\n\n    async def _execute_command(self, command: Any) -> Any: ...\n\n\n@dataclass(frozen=True)\nclass TypoResult:\n    \"\"\"Result of a typo generation.\"\"\"\n\n    typo_type: TypoType\n    wrong_char: str = ''\n\n\n@dataclass(frozen=True)\nclass TimingConfig:\n    \"\"\"Configuration for realistic typing timing.\"\"\"\n\n    keystroke_min: float = 0.03\n    keystroke_max: float = 0.12\n    punctuation_min: float = 0.08\n    punctuation_max: float = 0.18\n    thinking_probability: float = 0.02\n    thinking_min: float = 0.3\n    thinking_max: float = 0.7\n    distraction_probability: float = 0.005\n    distraction_min: float = 0.5\n    distraction_max: float = 1.2\n    mistake_realize_min: float = 0.1\n    mistake_realize_max: float = 0.25\n    after_correction_min: float = 0.03\n    after_correction_max: float = 0.08\n    double_press_min: float = 0.02\n    double_press_max: float = 0.05\n    hesitation_min: float = 0.15\n    hesitation_max: float = 0.3\n\n\n@dataclass(frozen=True)\nclass TypoConfig:\n    \"\"\"Configuration for typo generation weights.\"\"\"\n\n    adjacent_weight: float = 0.55\n    transpose_weight: float = 0.20\n    double_weight: float = 0.12\n    skip_weight: float = 0.08\n    missed_space_weight: float = 0.05\n\n\nclass Keyboard:\n    \"\"\"\n    Keyboard input controller for Tab and WebElement.\n\n    Provides methods for:\n    - Tab: Public keyboard simulation (press, down, up, hotkey)\n    - WebElement: Private text typing with optional humanization\n    \"\"\"\n\n    PAUSE_CHARS = frozenset(' .,!?;:\\n')\n\n    def __init__(\n        self,\n        executor: CommandExecutor,\n        timing: Optional[TimingConfig] = None,\n        typo_config: Optional[TypoConfig] = None,\n    ):\n        \"\"\"\n        Initialize keyboard controller.\n\n        Args:\n            executor: Object with _execute_command method (Tab or WebElement).\n            timing: Optional custom timing configuration.\n            typo_config: Optional custom typo weights configuration.\n        \"\"\"\n        self._executor = executor\n        self._timing = timing or TimingConfig()\n        self._typo_config = typo_config or TypoConfig()\n        self._has_focus = hasattr(executor, 'focus')\n\n    async def _ensure_focus(self):\n        \"\"\"Re-focus the executor element before a keystroke if it supports focus.\"\"\"\n        if self._has_focus:\n            await self._executor.focus()\n\n    async def press(\n        self,\n        key: Key,\n        modifiers: Optional[KeyModifier] = None,\n        interval: float = 0.1,\n    ):\n        \"\"\"\n        Press and release a key (down + wait + up).\n\n        Args:\n            key: Key to press (from Key enum).\n            modifiers: Optional key modifiers (Alt=1, Ctrl=2, Meta=4, Shift=8).\n            interval: Time to hold the key down in seconds.\n\n        Example:\n            await tab.keyboard.press(Key.ENTER)\n            await tab.keyboard.press(Key.A, modifiers=KeyModifier.CTRL)\n        \"\"\"\n        logger.info(f'Pressing key: {key} with modifiers: {modifiers}')\n        await self.down(key, modifiers)\n        await asyncio.sleep(interval)\n        await self.up(key)\n\n    async def down(self, key: Key, modifiers: Optional[KeyModifier] = None):\n        \"\"\"\n        Press a key down (without releasing).\n\n        Args:\n            key: Key to press down (from Key enum).\n            modifiers: Optional key modifiers.\n        \"\"\"\n        key_name, code = key\n        logger.debug(f'Key down: {key_name}')\n        command = InputCommands.dispatch_key_event(\n            type=KeyEventType.KEY_DOWN,\n            key=key_name,\n            windows_virtual_key_code=code,\n            native_virtual_key_code=code,\n            modifiers=modifiers,\n        )\n        await self._executor._execute_command(command)\n\n    async def up(self, key: Key):\n        \"\"\"\n        Release a key (key up event).\n\n        Args:\n            key: Key to release (from Key enum).\n        \"\"\"\n        key_name, code = key\n        logger.debug(f'Key up: {key_name}')\n        command = InputCommands.dispatch_key_event(\n            type=KeyEventType.KEY_UP,\n            key=key_name,\n            windows_virtual_key_code=code,\n            native_virtual_key_code=code,\n        )\n        await self._executor._execute_command(command)\n\n    async def hotkey(self, key1: Key, key2: Key, key3: Optional[Key] = None):\n        \"\"\"\n        Execute a key combination (hotkey) with up to 3 keys.\n\n        Args:\n            key1: First key (usually a modifier like Ctrl, Shift, Alt).\n            key2: Second key.\n            key3: Optional third key.\n\n        Example:\n            await tab.keyboard.hotkey(Key.CONTROL, Key.C)  # Ctrl+C\n        \"\"\"\n        logger.info(f'Hotkey: {key1} + {key2}' + (f' + {key3}' if key3 else ''))\n        keys = [key1, key2]\n        if key3 is not None:\n            keys.append(key3)\n\n        modifiers, non_modifiers = self._split_modifiers_and_keys(keys)\n        modifier_value = self._calculate_modifier_value(modifiers)\n\n        for key in non_modifiers:\n            await self.down(key, modifiers=modifier_value)\n            await asyncio.sleep(0.05)\n\n        await asyncio.sleep(0.1)\n\n        for key in reversed(non_modifiers):\n            await self.up(key)\n            await asyncio.sleep(0.05)\n\n    async def type_text(\n        self,\n        text: str,\n        humanize: bool = False,\n        interval: Optional[float] = None,\n    ):\n        \"\"\"\n        Type text character by character.\n\n        Args:\n            text: Text to type.\n            humanize: When True, simulates human-like typing with\n                variable delays and occasional typos (~2%).\n            interval: Deprecated. Use humanize=True instead.\n\n        Example:\n            await tab.keyboard.type_text(\"Hello World\", humanize=True)\n            await tab.keyboard.type_text(\"Hello World\")\n        \"\"\"\n        if interval is not None:\n            warnings.warn(\n                'The \"interval\" parameter is deprecated and will be removed '\n                'in a future version. Use \"humanize=True\" for realistic typing.',\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n        if humanize:\n            await self._type_text_humanized(text)\n            return\n\n        for current_char in text:\n            await self._type_char(current_char)\n            await asyncio.sleep(0.05)\n\n    async def _type_text_humanized(self, text: str):\n        \"\"\"Type text with realistic human-like behavior.\"\"\"\n        char_index = 0\n        while char_index < len(text):\n            current_char = text[char_index]\n            next_char = text[char_index + 1] if char_index + 1 < len(text) else None\n\n            should_skip_next = await self._process_char_with_typo(current_char, next_char)\n\n            if should_skip_next:\n                char_index += 1\n\n            await self._apply_realistic_delay(current_char)\n            char_index += 1\n\n    async def _type_char(self, char: str):\n        \"\"\"Type a single character, re-focusing the element before each keystroke.\"\"\"\n        await self._ensure_focus()\n        key, code, keycode = CHAR_TO_KEY_INFO.get(char, (char, '', 0))\n        command_down = InputCommands.dispatch_key_event(\n            type=KeyEventType.KEY_DOWN,\n            key=key,\n            code=code,\n            text=char,\n            unmodified_text=char,\n            windows_virtual_key_code=keycode,\n            native_virtual_key_code=keycode,\n        )\n        await self._executor._execute_command(command_down)\n\n        command_up = InputCommands.dispatch_key_event(\n            type=KeyEventType.KEY_UP,\n            key=key,\n            code=code,\n            windows_virtual_key_code=keycode,\n            native_virtual_key_code=keycode,\n        )\n        await self._executor._execute_command(command_up)\n\n    async def _type_backspace(self):\n        \"\"\"Send backspace keypress.\"\"\"\n        await self._ensure_focus()\n        await self.down(Key.BACKSPACE)\n        await self.up(Key.BACKSPACE)\n\n    async def _process_char_with_typo(\n        self,\n        current_char: str,\n        next_char: Optional[str],\n    ) -> bool:\n        \"\"\"Process character, potentially with typo. Returns True if next should be skipped.\"\"\"\n        if not self._should_make_typo():\n            await self._type_char(current_char)\n            return False\n\n        typo = self._generate_typo(current_char, next_char)\n        return await self._handle_typo(current_char, next_char, typo)\n\n    async def _handle_typo(\n        self,\n        current_char: str,\n        next_char: Optional[str],\n        typo: TypoResult,\n    ) -> bool:\n        \"\"\"Handle typo. Returns True if next char should be skipped.\"\"\"\n        if typo.typo_type == TypoType.ADJACENT:\n            await self._do_adjacent_typo(current_char, typo.wrong_char)\n            return False\n\n        if typo.typo_type == TypoType.TRANSPOSE and next_char:\n            await self._do_transpose_typo(current_char, next_char)\n            return True\n\n        if typo.typo_type == TypoType.DOUBLE:\n            await self._do_double_typo(current_char)\n            return False\n\n        if typo.typo_type == TypoType.SKIP:\n            await self._do_skip_typo(current_char)\n            return False\n\n        if typo.typo_type == TypoType.MISSED_SPACE and current_char == ' ' and next_char:\n            await self._do_missed_space_typo(current_char, next_char)\n            return True\n\n        await self._type_char(current_char)\n        return False\n\n    async def _do_adjacent_typo(self, correct_char: str, wrong_char: str):\n        \"\"\"Type wrong adjacent key, pause, backspace, correct.\"\"\"\n        timing = self._timing\n        await self._type_char(wrong_char)\n        await asyncio.sleep(random.uniform(timing.mistake_realize_min, timing.mistake_realize_max))\n        await self._type_backspace()\n        await asyncio.sleep(\n            random.uniform(timing.after_correction_min, timing.after_correction_max)\n        )\n        await self._type_char(correct_char)\n\n    async def _do_transpose_typo(self, current_char: str, next_char: str):\n        \"\"\"Type chars in wrong order, then fix.\"\"\"\n        timing = self._timing\n        await self._type_char(next_char)\n        await asyncio.sleep(random.uniform(timing.keystroke_min, timing.keystroke_max))\n        await self._type_char(current_char)\n\n        await asyncio.sleep(random.uniform(timing.mistake_realize_min, timing.mistake_realize_max))\n        await self._type_backspace()\n        await self._type_backspace()\n        await asyncio.sleep(\n            random.uniform(timing.after_correction_min, timing.after_correction_max)\n        )\n\n        await self._type_char(current_char)\n        await asyncio.sleep(random.uniform(timing.keystroke_min, timing.keystroke_max))\n        await self._type_char(next_char)\n\n    async def _do_double_typo(self, current_char: str):\n        \"\"\"Type character twice, then backspace.\"\"\"\n        timing = self._timing\n        await self._type_char(current_char)\n        await asyncio.sleep(random.uniform(timing.double_press_min, timing.double_press_max))\n        await self._type_char(current_char)\n        await asyncio.sleep(random.uniform(timing.mistake_realize_min, timing.mistake_realize_max))\n        await self._type_backspace()\n\n    async def _do_skip_typo(self, current_char: str):\n        \"\"\"Hesitate, then type normally.\"\"\"\n        timing = self._timing\n        await asyncio.sleep(random.uniform(timing.hesitation_min, timing.hesitation_max))\n        await self._type_char(current_char)\n\n    async def _do_missed_space_typo(self, space_char: str, next_char: str):\n        \"\"\"Miss space, type next char, realize, go back and fix.\"\"\"\n        timing = self._timing\n        await self._type_char(next_char)\n        await asyncio.sleep(random.uniform(timing.mistake_realize_min, timing.mistake_realize_max))\n        await self._type_backspace()\n        await asyncio.sleep(\n            random.uniform(timing.after_correction_min, timing.after_correction_max)\n        )\n        await self._type_char(space_char)\n        await asyncio.sleep(\n            random.uniform(timing.after_correction_min, timing.after_correction_max)\n        )\n        await self._type_char(next_char)\n\n    async def _apply_realistic_delay(self, typed_char: str):\n        \"\"\"Apply realistic delay after typing a character.\"\"\"\n        timing = self._timing\n        delay = random.uniform(timing.keystroke_min, timing.keystroke_max)\n\n        if typed_char in self.PAUSE_CHARS:\n            delay += random.uniform(timing.punctuation_min, timing.punctuation_max)\n\n        if random.random() < timing.thinking_probability:\n            delay += random.uniform(timing.thinking_min, timing.thinking_max)\n\n        if random.random() < timing.distraction_probability:\n            delay += random.uniform(timing.distraction_min, timing.distraction_max)\n\n        await asyncio.sleep(delay)\n\n    @staticmethod\n    def _should_make_typo() -> bool:\n        \"\"\"Determine if a typo should occur.\"\"\"\n        return random.random() < DEFAULT_TYPO_PROBABILITY\n\n    def _generate_typo(self, current_char: str, next_char: Optional[str]) -> TypoResult:\n        \"\"\"Generate a realistic typo based on QWERTY layout.\"\"\"\n        typo_type = self._select_typo_type()\n        return self._create_typo(typo_type, current_char, next_char)\n\n    def _select_typo_type(self) -> TypoType:\n        \"\"\"Select typo type based on weights.\"\"\"\n        config = self._typo_config\n        typo_types = [\n            TypoType.ADJACENT,\n            TypoType.TRANSPOSE,\n            TypoType.DOUBLE,\n            TypoType.SKIP,\n            TypoType.MISSED_SPACE,\n        ]\n        typo_weights = [\n            config.adjacent_weight,\n            config.transpose_weight,\n            config.double_weight,\n            config.skip_weight,\n            config.missed_space_weight,\n        ]\n        return random.choices(typo_types, weights=typo_weights, k=1)[0]\n\n    def _create_typo(\n        self,\n        typo_type: TypoType,\n        current_char: str,\n        next_char: Optional[str],\n    ) -> TypoResult:\n        \"\"\"Create typo result based on type.\"\"\"\n        typo_handlers = {\n            TypoType.ADJACENT: lambda: self._create_adjacent_typo(current_char),\n            TypoType.TRANSPOSE: lambda: self._create_transpose_typo(current_char, next_char),\n            TypoType.MISSED_SPACE: lambda: self._create_missed_space_typo(current_char),\n            TypoType.DOUBLE: lambda: TypoResult(typo_type=TypoType.DOUBLE, wrong_char=current_char),\n            TypoType.SKIP: lambda: TypoResult(typo_type=TypoType.SKIP),\n        }\n        handler = typo_handlers.get(typo_type, typo_handlers[TypoType.SKIP])\n        return handler()\n\n    def _create_transpose_typo(self, current_char: str, next_char: Optional[str]) -> TypoResult:\n        \"\"\"Create transpose typo, falling back to adjacent if not possible.\"\"\"\n        if next_char and next_char.isalpha():\n            return TypoResult(typo_type=TypoType.TRANSPOSE, wrong_char=next_char)\n        return self._create_adjacent_typo(current_char)\n\n    def _create_missed_space_typo(self, current_char: str) -> TypoResult:\n        \"\"\"Create missed space typo, falling back to adjacent if not a space.\"\"\"\n        if current_char == ' ':\n            return TypoResult(typo_type=TypoType.MISSED_SPACE)\n        return self._create_adjacent_typo(current_char)\n\n    @staticmethod\n    def _create_adjacent_typo(original_char: str) -> TypoResult:\n        \"\"\"Create adjacent key typo.\"\"\"\n        lowercase_char = original_char.lower()\n\n        if lowercase_char not in QWERTY_NEIGHBORS:\n            return TypoResult(typo_type=TypoType.DOUBLE, wrong_char=original_char)\n\n        adjacent_char = random.choice(QWERTY_NEIGHBORS[lowercase_char])\n\n        if original_char.isupper():\n            adjacent_char = adjacent_char.upper()\n\n        return TypoResult(typo_type=TypoType.ADJACENT, wrong_char=adjacent_char)\n\n    @staticmethod\n    def _split_modifiers_and_keys(keys: list[Key]) -> tuple[list[Key], list[Key]]:\n        \"\"\"Split keys into modifiers and non-modifiers.\"\"\"\n        modifier_keys = {Key.CONTROL, Key.SHIFT, Key.ALT, Key.META}\n        modifiers = [key for key in keys if key in modifier_keys]\n        non_modifiers = [key for key in keys if key not in modifier_keys]\n        return modifiers, non_modifiers\n\n    @staticmethod\n    def _calculate_modifier_value(modifiers: list[Key]) -> Optional[KeyModifier]:\n        \"\"\"Calculate KeyModifier value from modifier keys.\"\"\"\n        if not modifiers:\n            return None\n\n        modifier_map = {\n            Key.ALT: 1,\n            Key.CONTROL: 2,\n            Key.META: 4,\n            Key.SHIFT: 8,\n        }\n\n        value = sum(modifier_map.get(mod, 0) for mod in modifiers)\n        return cast(KeyModifier, value) if value > 0 else None\n\n\nKeyboardAPI = Keyboard\n"
  },
  {
    "path": "pydoll/interactions/mouse.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport logging\nimport math\nimport random\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.commands import InputCommands, RuntimeCommands\nfrom pydoll.interactions.utils import (\n    bezier_2d,\n    fitts_duration,\n    minimum_jerk,\n    random_control_points,\n)\nfrom pydoll.protocol.input.types import MouseButton, MouseEventType\n\nif TYPE_CHECKING:\n    from pydoll.browser.tab import Tab\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass(frozen=True)\nclass MouseTimingConfig:\n    \"\"\"Configuration for realistic mouse movement physics.\"\"\"\n\n    fitts_a: float = 0.070\n    fitts_b: float = 0.150\n\n    frame_interval: float = 0.012\n    frame_interval_variance: float = 0.004\n\n    curvature_min: float = 0.10\n    curvature_max: float = 0.30\n    curvature_asymmetry: float = 0.6\n\n    short_distance_threshold: float = 50.0\n\n    tremor_amplitude: float = 1.0\n\n    overshoot_probability: float = 0.70\n    overshoot_distance_min: float = 0.03\n    overshoot_distance_max: float = 0.12\n    overshoot_speed_threshold: float = 200.0\n\n    pre_click_pause_min: float = 0.05\n    pre_click_pause_max: float = 0.20\n    click_hold_min: float = 0.05\n    click_hold_max: float = 0.15\n    double_click_interval_min: float = 0.05\n    double_click_interval_max: float = 0.10\n    drag_start_pause_min: float = 0.08\n    drag_start_pause_max: float = 0.20\n    drag_end_pause_min: float = 0.05\n    drag_end_pause_max: float = 0.15\n\n    micro_pause_probability: float = 0.03\n    micro_pause_min: float = 0.015\n    micro_pause_max: float = 0.04\n\n    min_duration: float = 0.08\n    max_duration: float = 2.5\n\n\nclass Mouse:\n    \"\"\"\n    Mouse input controller with realistic humanized simulation.\n\n    Provides methods for mouse movement, clicking, double-clicking,\n    and dragging with optional humanized simulation using Bezier curves,\n    Fitts's Law timing, minimum-jerk velocity profiles, physiological\n    tremor, and overshoot correction.\n    \"\"\"\n\n    _DEBUG_INIT_JS = \"\"\"\n    (() => {\n        if (document.getElementById('__pydoll_mouse_debug')) return;\n        const canvas = document.createElement('canvas');\n        canvas.id = '__pydoll_mouse_debug';\n        canvas.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;'\n            + 'pointer-events:none;z-index:2147483647;';\n        canvas.width = window.innerWidth;\n        canvas.height = window.innerHeight;\n        document.body.appendChild(canvas);\n        window.__pydoll_debug_ctx = canvas.getContext('2d');\n    })();\n    \"\"\"\n\n    _DEBUG_DOT_JS = \"\"\"\n    (() => {{\n        const ctx = window.__pydoll_debug_ctx;\n        if (!ctx) return;\n        ctx.beginPath();\n        ctx.arc({x}, {y}, {radius}, 0, 2 * Math.PI);\n        ctx.fillStyle = '{color}';\n        ctx.fill();\n    }})();\n    \"\"\"\n\n    def __init__(\n        self,\n        tab: Tab,\n        timing: Optional[MouseTimingConfig] = None,\n        debug: bool = False,\n    ):\n        \"\"\"\n        Initialize mouse controller.\n\n        Args:\n            tab: Tab instance to execute mouse commands on.\n            timing: Optional custom timing configuration for humanized movement.\n            debug: Draw colored dots on the page to visualize mouse path.\n        \"\"\"\n        self._tab = tab\n        self._timing = timing or MouseTimingConfig()\n        self._position: tuple[float, float] = (0.0, 0.0)\n        self._debug = debug\n        self._debug_initialized = False\n\n    @property\n    def timing(self) -> MouseTimingConfig:\n        \"\"\"Current timing configuration for humanized movement.\"\"\"\n        return self._timing\n\n    @timing.setter\n    def timing(self, config: MouseTimingConfig) -> None:\n        \"\"\"Replace the timing configuration.\n\n        Args:\n            config: New MouseTimingConfig to use for future operations.\n        \"\"\"\n        self._timing = config\n\n    @property\n    def debug(self) -> bool:\n        \"\"\"Whether to draw debug dots on the page.\"\"\"\n        return self._debug\n\n    @debug.setter\n    def debug(self, value: bool) -> None:\n        \"\"\"Set whether to draw debug dots on the page.\"\"\"\n        self._debug = value\n        self._debug_initialized = False\n\n    async def move(\n        self,\n        x: float,\n        y: float,\n        *,\n        humanize: bool = False,\n    ) -> None:\n        \"\"\"\n        Move mouse cursor to the specified position.\n\n        Args:\n            x: Target X coordinate (CSS pixels).\n            y: Target Y coordinate (CSS pixels).\n            humanize: Simulate human-like curved movement with natural timing.\n        \"\"\"\n        if humanize:\n            await self._move_humanized(x, y)\n            return\n\n        await self._dispatch_move(x, y)\n\n    async def click(\n        self,\n        x: float,\n        y: float,\n        *,\n        button: MouseButton = MouseButton.LEFT,\n        click_count: int = 1,\n        humanize: bool = False,\n    ) -> None:\n        \"\"\"\n        Click at the specified position.\n\n        Args:\n            x: Target X coordinate (CSS pixels).\n            y: Target Y coordinate (CSS pixels).\n            button: Mouse button to click.\n            click_count: Number of clicks (2 for double-click).\n            humanize: Simulate human-like movement and click timing.\n        \"\"\"\n        if humanize:\n            await self._click_humanized(x, y, button, click_count)\n            return\n\n        await self._dispatch_move(x, y)\n        await self._dispatch_button(MouseEventType.MOUSE_PRESSED, button, click_count)\n        await self._dispatch_button(MouseEventType.MOUSE_RELEASED, button, click_count)\n\n    async def double_click(\n        self,\n        x: float,\n        y: float,\n        *,\n        button: MouseButton = MouseButton.LEFT,\n        humanize: bool = False,\n    ) -> None:\n        \"\"\"\n        Double-click at the specified position.\n\n        Args:\n            x: Target X coordinate (CSS pixels).\n            y: Target Y coordinate (CSS pixels).\n            button: Mouse button to click.\n            humanize: Simulate human-like movement and click timing.\n        \"\"\"\n        await self.click(x, y, button=button, click_count=2, humanize=humanize)\n\n    async def down(self, button: MouseButton = MouseButton.LEFT) -> None:\n        \"\"\"\n        Press mouse button down at the current position.\n\n        Args:\n            button: Mouse button to press.\n        \"\"\"\n        await self._dispatch_button(MouseEventType.MOUSE_PRESSED, button)\n\n    async def up(self, button: MouseButton = MouseButton.LEFT) -> None:\n        \"\"\"\n        Release mouse button at the current position.\n\n        Args:\n            button: Mouse button to release.\n        \"\"\"\n        await self._dispatch_button(MouseEventType.MOUSE_RELEASED, button)\n\n    async def drag(\n        self,\n        start_x: float,\n        start_y: float,\n        end_x: float,\n        end_y: float,\n        *,\n        humanize: bool = False,\n    ) -> None:\n        \"\"\"\n        Drag from one position to another.\n\n        Args:\n            start_x: Start X coordinate.\n            start_y: Start Y coordinate.\n            end_x: End X coordinate.\n            end_y: End Y coordinate.\n            humanize: Simulate human-like drag movement.\n        \"\"\"\n        if humanize:\n            await self._drag_humanized(start_x, start_y, end_x, end_y)\n            return\n\n        await self._dispatch_move(start_x, start_y)\n        await self._dispatch_button(MouseEventType.MOUSE_PRESSED, MouseButton.LEFT)\n        await self._dispatch_move(end_x, end_y)\n        await self._dispatch_button(MouseEventType.MOUSE_RELEASED, MouseButton.LEFT)\n\n    async def _move_humanized(self, target_x: float, target_y: float) -> None:\n        \"\"\"Move mouse with realistic curved path, timing, tremor, and overshoot.\"\"\"\n        start = self._position\n        target = (target_x, target_y)\n        distance = math.hypot(target_x - start[0], target_y - start[1])\n\n        if distance < 1.0:\n            await self._dispatch_move(target_x, target_y)\n            return\n\n        config = self._timing\n        duration = fitts_duration(distance, 20.0, config.fitts_a, config.fitts_b)\n        duration = max(config.min_duration, min(duration, config.max_duration))\n\n        should_overshoot = (\n            distance > config.overshoot_speed_threshold\n            and random.random() < config.overshoot_probability\n        )\n\n        if should_overshoot:\n            await self._move_with_overshoot(start, target, duration)\n        else:\n            cp1, cp2 = self._get_control_points(start, target)\n            await self._perform_movement_loop(start, target, duration, cp1, cp2)\n\n        await self._dispatch_move(target_x, target_y)\n\n    async def _move_with_overshoot(\n        self,\n        start: tuple[float, float],\n        target: tuple[float, float],\n        duration: float,\n    ) -> None:\n        \"\"\"Execute a movement that overshoots the target, then corrects.\"\"\"\n        config = self._timing\n        overshoot_fraction = random.uniform(\n            config.overshoot_distance_min, config.overshoot_distance_max\n        )\n        dx = target[0] - start[0]\n        dy = target[1] - start[1]\n        overshoot = (target[0] + dx * overshoot_fraction, target[1] + dy * overshoot_fraction)\n\n        cp1, cp2 = self._get_control_points(start, overshoot)\n        await self._perform_movement_loop(start, overshoot, duration * 0.85, cp1, cp2)\n\n        cp1, cp2 = self._get_control_points(overshoot, target)\n        await self._perform_movement_loop(overshoot, target, duration * 0.15, cp1, cp2)\n\n    async def _perform_movement_loop(\n        self,\n        start: tuple[float, float],\n        end: tuple[float, float],\n        duration: float,\n        cp1: tuple[float, float],\n        cp2: tuple[float, float],\n    ) -> None:\n        \"\"\"Execute the frame-by-frame movement loop using Bezier path and minimum jerk.\"\"\"\n        config = self._timing\n        loop = asyncio.get_running_loop()\n        start_time = loop.time()\n        prev = (start[0], start[1], start_time)\n\n        while True:\n            now = loop.time()\n            elapsed = now - start_time\n\n            if elapsed >= duration:\n                break\n\n            t = minimum_jerk(elapsed / duration)\n            x, y = bezier_2d(t, start, cp1, cp2, end)\n\n            sigma = self._compute_tremor_sigma(x, y, now, prev, config)\n            x += random.gauss(0, sigma)\n            y += random.gauss(0, sigma)\n\n            await self._dispatch_move(x, y)\n            prev = (x, y, now)\n\n            frame_delay = config.frame_interval + random.uniform(\n                -config.frame_interval_variance, config.frame_interval_variance\n            )\n            await asyncio.sleep(max(0.001, frame_delay))\n\n            if random.random() < config.micro_pause_probability:\n                pause = random.uniform(config.micro_pause_min, config.micro_pause_max)\n                await asyncio.sleep(pause)\n                start_time += pause\n\n    @staticmethod\n    def _compute_tremor_sigma(\n        x: float,\n        y: float,\n        now: float,\n        prev: tuple[float, float, float],\n        config: MouseTimingConfig,\n    ) -> float:\n        \"\"\"Compute tremor amplitude scaled inversely with cursor velocity.\"\"\"\n        dt = now - prev[2]\n        if dt > 0:\n            velocity = math.hypot(x - prev[0], y - prev[1]) / dt\n            speed_factor = max(0.2, 1.0 - velocity / 500.0)\n        else:\n            speed_factor = 1.0\n        return config.tremor_amplitude * speed_factor\n\n    async def _click_humanized(\n        self,\n        x: float,\n        y: float,\n        button: MouseButton,\n        click_count: int,\n    ) -> None:\n        \"\"\"Click with realistic movement and timing.\"\"\"\n        config = self._timing\n\n        await self._move_humanized(x, y)\n\n        pre_pause = random.uniform(config.pre_click_pause_min, config.pre_click_pause_max)\n        await asyncio.sleep(pre_pause)\n\n        for i in range(click_count):\n            current_count = i + 1\n            await self._dispatch_button(MouseEventType.MOUSE_PRESSED, button, current_count)\n\n            hold = random.uniform(config.click_hold_min, config.click_hold_max)\n            await asyncio.sleep(hold)\n\n            await self._dispatch_button(MouseEventType.MOUSE_RELEASED, button, current_count)\n\n            if current_count < click_count:\n                interval = random.uniform(\n                    config.double_click_interval_min,\n                    config.double_click_interval_max,\n                )\n                await asyncio.sleep(interval)\n\n    async def _drag_humanized(\n        self,\n        start_x: float,\n        start_y: float,\n        end_x: float,\n        end_y: float,\n    ) -> None:\n        \"\"\"Drag with realistic movement, pauses, and timing.\"\"\"\n        config = self._timing\n\n        await self._move_humanized(start_x, start_y)\n        await self._dispatch_button(MouseEventType.MOUSE_PRESSED, MouseButton.LEFT)\n\n        drag_start_pause = random.uniform(config.drag_start_pause_min, config.drag_start_pause_max)\n        await asyncio.sleep(drag_start_pause)\n\n        start = self._position\n        distance = math.hypot(end_x - start[0], end_y - start[1])\n        duration = fitts_duration(distance, 20.0, config.fitts_a, config.fitts_b)\n        duration = max(config.min_duration, min(duration, config.max_duration))\n\n        cp1, cp2 = self._get_control_points(start, (end_x, end_y))\n        await self._perform_movement_loop(start, (end_x, end_y), duration, cp1, cp2)\n        await self._dispatch_move(end_x, end_y)\n\n        drag_end_pause = random.uniform(config.drag_end_pause_min, config.drag_end_pause_max)\n        await asyncio.sleep(drag_end_pause)\n\n        await self._dispatch_button(MouseEventType.MOUSE_RELEASED, MouseButton.LEFT)\n\n    def _get_control_points(\n        self,\n        start: tuple[float, float],\n        end: tuple[float, float],\n    ) -> tuple[tuple[float, float], tuple[float, float]]:\n        \"\"\"Generate Bezier control points using current timing config.\"\"\"\n        config = self._timing\n        return random_control_points(\n            start,\n            end,\n            config.curvature_min,\n            config.curvature_max,\n            config.curvature_asymmetry,\n            config.short_distance_threshold,\n        )\n\n    async def _dispatch_move(self, x: float, y: float) -> None:\n        \"\"\"Dispatch a mouseMoved event and update internal position.\"\"\"\n        command = InputCommands.dispatch_mouse_event(\n            type=MouseEventType.MOUSE_MOVED,\n            x=int(round(x)),\n            y=int(round(y)),\n        )\n        await self._tab._execute_command(command)\n        self._position = (x, y)\n\n        if self._debug:\n            await self._debug_draw_dot(x, y, radius=2, color='rgba(0,150,255,0.6)')\n\n    async def _dispatch_button(\n        self,\n        event_type: MouseEventType,\n        button: MouseButton,\n        click_count: int = 1,\n    ) -> None:\n        \"\"\"Dispatch mousePressed or mouseReleased at current position.\"\"\"\n        command = InputCommands.dispatch_mouse_event(\n            type=event_type,\n            x=int(round(self._position[0])),\n            y=int(round(self._position[1])),\n            button=button,\n            click_count=click_count,\n        )\n        await self._tab._execute_command(command)\n\n        if self._debug and event_type == MouseEventType.MOUSE_PRESSED:\n            await self._debug_draw_dot(\n                self._position[0], self._position[1], radius=6, color='rgba(255,50,50,0.9)'\n            )\n\n    async def _debug_draw_dot(self, x: float, y: float, radius: int, color: str) -> None:\n        \"\"\"Draw a debug dot on the page overlay canvas.\"\"\"\n        if not self._debug_initialized:\n            await self._tab._execute_command(RuntimeCommands.evaluate(self._DEBUG_INIT_JS))\n            self._debug_initialized = True\n\n        script = self._DEBUG_DOT_JS.format(\n            x=int(round(x)), y=int(round(y)), radius=radius, color=color\n        )\n        await self._tab._execute_command(RuntimeCommands.evaluate(script))\n\n\nMouseAPI = Mouse\n"
  },
  {
    "path": "pydoll/interactions/scroll.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport json\nimport random\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING, Optional\n\nfrom pydoll.commands import InputCommands, RuntimeCommands\nfrom pydoll.constants import Scripts, ScrollPosition\nfrom pydoll.interactions.utils import CubicBezier\nfrom pydoll.protocol.input.types import MouseEventType\nfrom pydoll.protocol.runtime.methods import EvaluateResponse\n\nif TYPE_CHECKING:\n    from pydoll.browser.tab import Tab\n\n\n@dataclass(frozen=True)\nclass ScrollTimingConfig:\n    \"\"\"Configuration for realistic scroll physics.\"\"\"\n\n    min_duration: float = 0.5\n    max_duration: float = 1.5\n\n    bezier_points: tuple[float, float, float, float] = (0.645, 0.045, 0.355, 1.0)\n\n    frame_interval: float = 0.012\n\n    delta_jitter: int = 3\n\n    micro_pause_probability: float = 0.05\n    micro_pause_min: float = 0.02\n    micro_pause_max: float = 0.05\n\n    overshoot_probability: float = 0.15\n    overshoot_factor_min: float = 1.02\n    overshoot_factor_max: float = 1.08\n\n\nclass Scroll:\n    \"\"\"\n    API for controlling page scroll behavior.\n\n    Provides methods for scrolling the page in different directions,\n    to specific positions, or by relative distances. Supports humanized\n    scrolling with realistic physics simulation.\n    \"\"\"\n\n    def __init__(\n        self,\n        tab: Tab,\n        timing: Optional[ScrollTimingConfig] = None,\n    ):\n        \"\"\"\n        Initialize the Scroll with a Tab instance.\n\n        Args:\n            tab: Tab instance to execute scroll commands on.\n            timing: Optional custom timing configuration for humanized scroll.\n        \"\"\"\n        self._tab = tab\n        self._timing = timing or ScrollTimingConfig()\n\n    async def by(\n        self,\n        position: ScrollPosition,\n        distance: int | float,\n        smooth: bool = True,\n        humanize: bool = False,\n    ):\n        \"\"\"\n        Scroll the page by a relative distance in the specified direction.\n\n        Args:\n            position: Direction to scroll (UP, DOWN, LEFT, RIGHT).\n            distance: Number of pixels to scroll.\n            smooth: Use smooth scrolling animation if True, instant if False.\n            humanize: Simulate human-like scrolling with momentum and inertia.\n        \"\"\"\n        if humanize:\n            await self._scroll_humanized(position, distance)\n            return\n\n        axis, scroll_distance = self._get_axis_and_distance(position, distance)\n        behavior = self._get_behavior(smooth)\n\n        script = Scripts.SCROLL_BY.format(\n            axis=axis,\n            distance=scroll_distance,\n            behavior=behavior,\n        )\n\n        await self._execute_script_await_promise(script)\n\n    async def to_top(self, smooth: bool = True, humanize: bool = False):\n        \"\"\"\n        Scroll to the top of the page (Y=0).\n\n        Args:\n            smooth: Use smooth scrolling animation if True, instant if False.\n            humanize: Simulate human-like scrolling with momentum and inertia.\n        \"\"\"\n        if humanize:\n            await self._scroll_to_end_humanized(ScrollPosition.UP)\n            return\n\n        behavior = self._get_behavior(smooth)\n        script = Scripts.SCROLL_TO_TOP.format(behavior=behavior)\n        await self._execute_script_await_promise(script)\n\n    async def to_bottom(self, smooth: bool = True, humanize: bool = False):\n        \"\"\"\n        Scroll to the bottom of the page (Y=document.body.scrollHeight).\n\n        Args:\n            smooth: Use smooth scrolling animation if True, instant if False.\n            humanize: Simulate human-like scrolling with momentum and inertia.\n        \"\"\"\n        if humanize:\n            await self._scroll_to_end_humanized(ScrollPosition.DOWN)\n            return\n\n        behavior = self._get_behavior(smooth)\n        script = Scripts.SCROLL_TO_BOTTOM.format(behavior=behavior)\n        await self._execute_script_await_promise(script)\n\n    async def _scroll_to_end_humanized(self, position: ScrollPosition):\n        \"\"\"\n        Scroll to top or bottom with multiple human-like flicks.\n\n        Humans don't scroll thousands of pixels in one motion - they do\n        multiple scroll gestures with brief pauses in between.\n        \"\"\"\n        max_flick_distance = random.uniform(600, 1200)\n        min_remaining_threshold = 30\n        min_stuck_threshold = 5\n        min_flick_distance = 100\n\n        # Failsafe for stuck scroll\n        last_remaining = float('inf')\n        stuck_counter = 0\n        max_stuck_attempts = 10\n\n        while True:\n            if position == ScrollPosition.DOWN:\n                remaining = await self._get_remaining_scroll_to_bottom()\n            else:\n                remaining = await self._get_current_scroll_y()\n\n            if remaining <= min_remaining_threshold:\n                break\n\n            # Check if we are stuck\n            has_progressed = abs(remaining - last_remaining) >= min_stuck_threshold\n\n            if has_progressed:\n                stuck_counter = 0\n\n            if not has_progressed:\n                stuck_counter += 1\n                if stuck_counter >= max_stuck_attempts:\n                    break\n\n            last_remaining = remaining\n\n            flick_distance = min(remaining, max_flick_distance)\n            if flick_distance < min_flick_distance and remaining > min_flick_distance:\n                flick_distance = min_flick_distance\n\n            await self._scroll_humanized(position, flick_distance)\n\n            pause = random.uniform(0.05, 0.15)\n            await asyncio.sleep(pause)\n\n            max_flick_distance = random.uniform(600, 1200)\n\n    async def _scroll_humanized(self, position: ScrollPosition, target_distance: float):\n        \"\"\"\n        Perform scroll with realistic human-like physics.\n\n        Simulates momentum-based scrolling with:\n        - Smooth deceleration curve\n        - Variable frame intervals\n        - Random jitter in scroll deltas\n        - Occasional micro-pauses\n        - Optional overshoot and correction\n        \"\"\"\n        is_vertical = position in {ScrollPosition.UP, ScrollPosition.DOWN}\n        direction = -1 if position in {ScrollPosition.UP, ScrollPosition.LEFT} else 1\n\n        effective_distance = self._calculate_effective_distance(target_distance)\n        duration = self._calculate_duration(effective_distance)\n\n        scrolled_so_far = await self._perform_scroll_loop(\n            effective_distance, duration, is_vertical, direction\n        )\n\n        if effective_distance > target_distance and scrolled_so_far > target_distance:\n            correction_distance = scrolled_so_far - target_distance\n            correction_direction = -direction\n\n            await asyncio.sleep(random.uniform(0.1, 0.2))\n\n            await self._scroll_correction(\n                is_vertical=is_vertical,\n                direction=correction_direction,\n                distance=correction_distance,\n            )\n\n    async def _perform_scroll_loop(\n        self,\n        effective_distance: float,\n        duration: float,\n        is_vertical: bool,\n        direction: int,\n    ) -> float:\n        \"\"\"Execute the main scroll loop using Bezier timing.\"\"\"\n        timing = self._timing\n        bezier = CubicBezier(*timing.bezier_points)\n\n        start_time = asyncio.get_running_loop().time()\n        current_time = 0.0\n        scrolled_so_far = 0.0\n\n        while current_time < duration:\n            now = asyncio.get_running_loop().time()\n            current_time = now - start_time\n\n            if current_time >= duration:\n                break\n\n            progress = current_time / duration\n            eased_progress = bezier.solve(progress)\n\n            target_pos = effective_distance * eased_progress\n            delta = target_pos - scrolled_so_far\n\n            jitter = random.randint(-timing.delta_jitter, timing.delta_jitter)\n            delta += jitter\n\n            delta = max(delta, 0)\n\n            if delta >= 1:\n                await self._dispatch_scroll_event(\n                    delta_x=0 if is_vertical else int(delta * direction),\n                    delta_y=int(delta * direction) if is_vertical else 0,\n                )\n                scrolled_so_far += delta\n\n            frame_delay = timing.frame_interval + random.uniform(-0.002, 0.002)\n            await asyncio.sleep(frame_delay)\n\n            if random.random() < timing.micro_pause_probability:\n                pause_duration = random.uniform(timing.micro_pause_min, timing.micro_pause_max)\n                await asyncio.sleep(pause_duration)\n                start_time += pause_duration\n\n        return scrolled_so_far\n\n    def _calculate_effective_distance(self, target_distance: float) -> float:\n        \"\"\"Calculate effective distance including overshoot.\"\"\"\n        timing = self._timing\n        should_overshoot = random.random() < timing.overshoot_probability\n        overshoot_factor = (\n            random.uniform(timing.overshoot_factor_min, timing.overshoot_factor_max)\n            if should_overshoot\n            else 1.0\n        )\n        return target_distance * overshoot_factor\n\n    def _calculate_duration(self, distance: float) -> float:\n        \"\"\"Calculate scroll duration based on distance.\"\"\"\n        timing = self._timing\n        base_duration = random.uniform(timing.min_duration, timing.max_duration)\n        duration = base_duration * (1 + 0.2 * (distance / 1000))\n        return min(duration, 3.0)\n\n    async def _scroll_correction(self, is_vertical: bool, direction: int, distance: float):\n        \"\"\"Perform small correction scroll after overshoot.\"\"\"\n        timing = self._timing\n        scrolled = 0.0\n\n        min_correction_velocity = (distance * (0.15)) / timing.frame_interval\n        correction_velocity = random.uniform(\n            max(200, min_correction_velocity), max(400, min_correction_velocity * 1.5)\n        )\n\n        while scrolled < distance:\n            frame_delta = correction_velocity * timing.frame_interval\n            frame_delta = min(frame_delta, distance - scrolled)\n\n            await self._dispatch_scroll_event(\n                delta_x=0 if is_vertical else int(frame_delta * direction),\n                delta_y=int(frame_delta * direction) if is_vertical else 0,\n            )\n\n            scrolled += frame_delta\n            correction_velocity *= 0.85\n\n            await asyncio.sleep(timing.frame_interval)\n\n    async def _dispatch_scroll_event(self, delta_x: int, delta_y: int):\n        \"\"\"Dispatch a mouse wheel event for scrolling.\"\"\"\n        viewport = await self._get_viewport_center()\n        command = InputCommands.dispatch_mouse_event(\n            type=MouseEventType.MOUSE_WHEEL,\n            x=viewport[0],\n            y=viewport[1],\n            delta_x=delta_x,\n            delta_y=delta_y,\n        )\n        await self._tab._execute_command(command)\n\n    async def _get_viewport_center(self) -> tuple[int, int]:\n        \"\"\"Get the center coordinates of the viewport.\"\"\"\n        command = RuntimeCommands.evaluate(expression=Scripts.GET_VIEWPORT_CENTER)\n        result: EvaluateResponse = await self._tab._execute_command(command)\n\n        value_str = result.get('result', {}).get('result', {}).get('value', '[]')\n        expected_dimensions = 2\n        try:\n            value = json.loads(value_str)\n            if value and isinstance(value, list) and len(value) == expected_dimensions:\n                return (int(value[0]), int(value[1]))\n        except (json.JSONDecodeError, TypeError):\n            pass\n        return (400, 300)\n\n    async def _get_current_scroll_y(self) -> float:\n        \"\"\"Get current vertical scroll position.\"\"\"\n        command = RuntimeCommands.evaluate(expression=Scripts.GET_SCROLL_Y)\n        result: EvaluateResponse = await self._tab._execute_command(command)\n        return float(result.get('result', {}).get('result', {}).get('value', 0))\n\n    async def _get_remaining_scroll_to_bottom(self) -> float:\n        \"\"\"Get remaining distance to scroll to reach the bottom.\"\"\"\n        command = RuntimeCommands.evaluate(expression=Scripts.GET_REMAINING_SCROLL_TO_BOTTOM)\n        result: EvaluateResponse = await self._tab._execute_command(command)\n        return float(result.get('result', {}).get('result', {}).get('value', 0))\n\n    @staticmethod\n    def _get_axis_and_distance(\n        position: ScrollPosition, distance: int | float\n    ) -> tuple[str, int | float]:\n        \"\"\"\n        Convert scroll position to axis and signed distance.\n\n        Args:\n            position: Direction to scroll.\n            distance: Absolute distance to scroll.\n\n        Returns:\n            Tuple of (axis, signed_distance) where axis is 'left' or 'top'\n            and signed_distance is positive or negative based on direction.\n        \"\"\"\n        if position in {ScrollPosition.UP, ScrollPosition.DOWN}:\n            axis = 'top'\n            scroll_distance = -distance if position == ScrollPosition.UP else distance\n            return axis, scroll_distance\n\n        axis = 'left'\n        scroll_distance = -distance if position == ScrollPosition.LEFT else distance\n        return axis, scroll_distance\n\n    @staticmethod\n    def _get_behavior(smooth: bool) -> str:\n        \"\"\"\n        Convert smooth boolean to CSS scroll behavior value.\n\n        Args:\n            smooth: Whether to use smooth scrolling.\n\n        Returns:\n            'smooth' if smooth is True, 'auto' otherwise.\n        \"\"\"\n        return 'smooth' if smooth else 'auto'\n\n    async def _execute_script_await_promise(self, script: str):\n        \"\"\"\n        Execute JavaScript and await promise resolution.\n\n        Args:\n            script: JavaScript code that returns a Promise.\n        \"\"\"\n        command = RuntimeCommands.evaluate(expression=script, await_promise=True)\n        return await self._tab._execute_command(command)\n\n\n# Backward compatibility alias\nScrollAPI = Scroll\n"
  },
  {
    "path": "pydoll/interactions/utils.py",
    "content": "from __future__ import annotations\n\nimport math\nimport random\n\n\nclass CubicBezier:\n    \"\"\"Cubic Bezier curve solver for smooth animation timing.\n\n    Based on UnitBezier from WebKit/Chromium. Maps a time progress value\n    to an eased progress value using a cubic Bezier curve.\n    \"\"\"\n\n    def __init__(self, point1_x: float, point1_y: float, point2_x: float, point2_y: float):\n        self.coefficient_c_x = 3.0 * point1_x\n        self.coefficient_b_x = 3.0 * (point2_x - point1_x) - self.coefficient_c_x\n        self.coefficient_a_x = 1.0 - self.coefficient_c_x - self.coefficient_b_x\n\n        self.coefficient_c_y = 3.0 * point1_y\n        self.coefficient_b_y = 3.0 * (point2_y - point1_y) - self.coefficient_c_y\n        self.coefficient_a_y = 1.0 - self.coefficient_c_y - self.coefficient_b_y\n\n    def sample_curve_x(self, time_progress: float) -> float:\n        return (\n            (self.coefficient_a_x * time_progress + self.coefficient_b_x) * time_progress\n            + self.coefficient_c_x\n        ) * time_progress\n\n    def sample_curve_y(self, time_progress: float) -> float:\n        return (\n            (self.coefficient_a_y * time_progress + self.coefficient_b_y) * time_progress\n            + self.coefficient_c_y\n        ) * time_progress\n\n    def sample_curve_derivative_x(self, time_progress: float) -> float:\n        return (\n            3.0 * self.coefficient_a_x * time_progress + 2.0 * self.coefficient_b_x\n        ) * time_progress + self.coefficient_c_x\n\n    def solve_curve_x(self, target_x: float, epsilon: float = 1e-6) -> float:\n        \"\"\"Given an x value, find the corresponding t value.\"\"\"\n        estimated_t = target_x\n\n        for _ in range(8):\n            current_x = self.sample_curve_x(estimated_t) - target_x\n            if abs(current_x) < epsilon:\n                return estimated_t\n            derivative = self.sample_curve_derivative_x(estimated_t)\n            if abs(derivative) < epsilon:\n                break\n            estimated_t -= current_x / derivative\n\n        lower_bound = 0.0\n        upper_bound = 1.0\n        estimated_t = target_x\n\n        if estimated_t < lower_bound:\n            return lower_bound\n        if estimated_t > upper_bound:\n            return upper_bound\n\n        while lower_bound < upper_bound:\n            current_x = self.sample_curve_x(estimated_t)\n            if abs(current_x - target_x) < epsilon:\n                return estimated_t\n            if target_x > current_x:\n                lower_bound = estimated_t\n            else:\n                upper_bound = estimated_t\n            estimated_t = (upper_bound - lower_bound) * 0.5 + lower_bound\n\n        return estimated_t\n\n    def solve(self, input_x: float) -> float:\n        \"\"\"Get y value for a given x (time progress).\"\"\"\n        return self.sample_curve_y(self.solve_curve_x(input_x))\n\n\ndef minimum_jerk(t: float) -> float:\n    \"\"\"Minimum jerk position at normalized time t in [0,1].\n\n    Returns 10t^3 - 15t^4 + 6t^5 which produces a bell-shaped velocity\n    profile: slow start, peak in middle, slow end.\n    \"\"\"\n    t2 = t * t\n    t3 = t2 * t\n    return 10.0 * t3 - 15.0 * t3 * t + 6.0 * t3 * t2\n\n\ndef bezier_2d(\n    t: float,\n    p0: tuple[float, float],\n    p1: tuple[float, float],\n    p2: tuple[float, float],\n    p3: tuple[float, float],\n) -> tuple[float, float]:\n    \"\"\"Evaluate 2D cubic Bezier at parameter t.\n\n    B(t) = (1-t)^3*P0 + 3(1-t)^2*t*P1 + 3(1-t)*t^2*P2 + t^3*P3\n    \"\"\"\n    u = 1.0 - t\n    u2 = u * u\n    u3 = u2 * u\n    t2 = t * t\n    t3 = t2 * t\n    x = u3 * p0[0] + 3.0 * u2 * t * p1[0] + 3.0 * u * t2 * p2[0] + t3 * p3[0]\n    y = u3 * p0[1] + 3.0 * u2 * t * p1[1] + 3.0 * u * t2 * p2[1] + t3 * p3[1]\n    return (x, y)\n\n\ndef fitts_duration(\n    distance: float,\n    target_width: float,\n    a: float,\n    b: float,\n) -> float:\n    \"\"\"Fitts's Law: MT = a + b * log2(D/W + 1).\"\"\"\n    if distance <= 0:\n        return a\n    return a + b * math.log2(distance / target_width + 1.0)\n\n\ndef random_control_points(\n    start: tuple[float, float],\n    end: tuple[float, float],\n    curvature_min: float,\n    curvature_max: float,\n    curvature_asymmetry: float,\n    short_distance_threshold: float,\n) -> tuple[tuple[float, float], tuple[float, float]]:\n    \"\"\"Generate randomized 2D Bezier control points for a curved mouse path.\n\n    Control points are offset perpendicular to the start-end line.\n    The first control point is biased earlier along the path\n    (ballistic phase asymmetry).\n    \"\"\"\n    dx = end[0] - start[0]\n    dy = end[1] - start[1]\n    distance = math.hypot(dx, dy)\n\n    if distance < 1.0:\n        return (start, end)\n\n    perp = (-dy / distance, dx / distance)\n\n    scale = min(1.0, distance / short_distance_threshold)\n    offsets = (\n        random.uniform(curvature_min, curvature_max) * distance * scale,\n        random.uniform(curvature_min, curvature_max) * distance * scale,\n    )\n\n    sign = random.choice([-1.0, 1.0])\n    t1 = random.uniform(0.2, curvature_asymmetry)\n    t2 = random.uniform(curvature_asymmetry, 0.8)\n\n    cp1 = (\n        start[0] + dx * t1 + perp[0] * offsets[0] * sign,\n        start[1] + dy * t1 + perp[1] * offsets[0] * sign,\n    )\n\n    counter = random.uniform(0.3, 1.0)\n    cp2 = (\n        start[0] + dx * t2 + perp[0] * offsets[1] * sign * counter,\n        start[1] + dy * t2 + perp[1] * offsets[1] * sign * counter,\n    )\n\n    return (cp1, cp2)\n"
  },
  {
    "path": "pydoll/protocol/__init__.py",
    "content": "\"\"\"Chrome DevTools Protocol implementation.\"\"\"\n"
  },
  {
    "path": "pydoll/protocol/base.py",
    "content": "from typing import Generic, TypeVar\n\n# TODO: typeddict comes from typing_extensions\nfrom typing_extensions import NotRequired, TypedDict\n\nT_CommandParams = TypeVar('T_CommandParams')\nT_CommandResponse = TypeVar('T_CommandResponse')\nT_EventParams = TypeVar('T_EventParams')\n\n\nclass EmptyParams(TypedDict):\n    \"\"\"Empty parameters for commands.\"\"\"\n\n    pass\n\n\nclass EmptyResponse(TypedDict):\n    \"\"\"Empty response for commands.\"\"\"\n\n    pass\n\n\nclass Command(TypedDict, Generic[T_CommandParams, T_CommandResponse]):\n    \"\"\"Base structure for all commands.\n\n    Attributes:\n        method: The command method name\n        params: Optional dictionary of parameters for the command\n        sessionId: Optional target session identifier (flattened sessions)\n    \"\"\"\n\n    id: NotRequired[int]\n    method: str\n    params: NotRequired[T_CommandParams]\n    sessionId: NotRequired[str]\n\n\nclass Response(TypedDict, Generic[T_CommandResponse]):\n    \"\"\"Base structure for all responses.\n\n    Attributes:\n        id: The ID that matches the command ID\n        result: The result data for the command\n        sessionId: Optional target session identifier (flattened sessions)\n    \"\"\"\n\n    id: int\n    result: T_CommandResponse\n    sessionId: NotRequired[str]\n\n\nclass CDPEvent(TypedDict, Generic[T_EventParams]):\n    \"\"\"Base structure for all events.\"\"\"\n\n    method: str\n    params: NotRequired[T_EventParams]\n    sessionId: NotRequired[str]\n"
  },
  {
    "path": "pydoll/protocol/browser/__init__.py",
    "content": "\"\"\"Browser domain implementation.\"\"\"\n"
  },
  {
    "path": "pydoll/protocol/browser/events.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import CDPEvent\nfrom pydoll.protocol.browser.types import DownloadProgressState\n\n\nclass BrowserEvent(str, Enum):\n    \"\"\"\n    Events from the Browser domain of the Chrome DevTools Protocol.\n\n    This enumeration contains the names of browser-related events that can be\n    received from the Chrome DevTools Protocol. These events provide information\n    about browser activities and state changes.\n    \"\"\"\n\n    DOWNLOAD_PROGRESS = 'Browser.downloadProgress'\n    \"\"\"\n    Fired when download makes progress. The last call has |done| == true.\n\n    Args:\n        guid (str): Global unique identifier of the download.\n        totalBytes (int): Total expected bytes to download.\n        receivedBytes (int): Total bytes received.\n        state (str): Download status.\n            Allowed values: 'inProgress', 'completed', 'canceled'\n    \"\"\"\n\n    DOWNLOAD_WILL_BEGIN = 'Browser.downloadWillBegin'\n    \"\"\"\n    Fired when page is about to start a download.\n\n    Args:\n        frameId (str): Id of the frame that caused the download to begin.\n        guid (str): Global unique identifier of the download.\n        url (str): URL of the resource being downloaded.\n        suggestedFilename (str): Suggested file name of the resource\n            (the actual name of the file saved on disk may differ).\n    \"\"\"\n\n\nclass DownloadProgressEventParams(TypedDict):\n    guid: str\n    totalBytes: float\n    receivedBytes: float\n    state: DownloadProgressState\n    filePath: NotRequired[str]\n\n\nclass DownloadWillBeginEventParams(TypedDict):\n    frameId: str\n    guid: str\n    url: str\n    suggestedFilename: str\n\n\nDownloadProgressEvent = CDPEvent[DownloadProgressEventParams]\nDownloadWillBeginEvent = CDPEvent[DownloadWillBeginEventParams]\n"
  },
  {
    "path": "pydoll/protocol/browser/methods.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import Command, EmptyParams, EmptyResponse, Response\nfrom pydoll.protocol.browser.types import (\n    Bounds,\n    BrowserCommandId,\n    BrowserContextID,\n    DownloadBehavior,\n    Histogram,\n    PermissionDescriptor,\n    PermissionSetting,\n    PermissionType,\n    PrivacySandboxAPI,\n    WindowID,\n)\n\n\nclass BrowserMethod(str, Enum):\n    \"\"\"Browser domain method names.\"\"\"\n\n    ADD_PRIVACY_SANDBOX_COORDINATOR_KEY_CONFIG = 'Browser.addPrivacySandboxCoordinatorKeyConfig'\n    ADD_PRIVACY_SANDBOX_ENROLLMENT_OVERRIDE = 'Browser.addPrivacySandboxEnrollmentOverride'\n    CANCEL_DOWNLOAD = 'Browser.cancelDownload'\n    CLOSE = 'Browser.close'\n    CRASH = 'Browser.crash'\n    CRASH_GPU_PROCESS = 'Browser.crashGpuProcess'\n    EXECUTE_BROWSER_COMMAND = 'Browser.executeBrowserCommand'\n    GET_BROWSER_COMMAND_LINE = 'Browser.getBrowserCommandLine'\n    GET_HISTOGRAM = 'Browser.getHistogram'\n    GET_HISTOGRAMS = 'Browser.getHistograms'\n    GET_VERSION = 'Browser.getVersion'\n    GET_WINDOW_BOUNDS = 'Browser.getWindowBounds'\n    GET_WINDOW_FOR_TARGET = 'Browser.getWindowForTarget'\n    GRANT_PERMISSIONS = 'Browser.grantPermissions'\n    RESET_PERMISSIONS = 'Browser.resetPermissions'\n    SET_CONTENTS_SIZE = 'Browser.setContentsSize'\n    SET_DOCK_TILE = 'Browser.setDockTile'\n    SET_DOWNLOAD_BEHAVIOR = 'Browser.setDownloadBehavior'\n    SET_PERMISSION = 'Browser.setPermission'\n    SET_WINDOW_BOUNDS = 'Browser.setWindowBounds'\n\n\nclass SetPermissionParams(TypedDict):\n    \"\"\"Parameters for setting permission settings for given origin.\"\"\"\n\n    permission: PermissionDescriptor\n    setting: PermissionSetting\n    origin: NotRequired[str]\n    browserContextId: NotRequired[BrowserContextID]\n\n\nclass GrantPermissionsParams(TypedDict):\n    \"\"\"Parameters for granting specific permissions to the given origin.\"\"\"\n\n    permissions: list[PermissionType]\n    origin: NotRequired[str]\n    browserContextId: NotRequired[BrowserContextID]\n\n\nclass ResetPermissionsParams(TypedDict):\n    \"\"\"Parameters for resetting all permission management for all origins.\"\"\"\n\n    browserContextId: NotRequired[BrowserContextID]\n\n\nclass SetDownloadBehaviorParams(TypedDict):\n    \"\"\"Parameters for setting the behavior when downloading a file.\"\"\"\n\n    behavior: DownloadBehavior\n    browserContextId: NotRequired[BrowserContextID]\n    downloadPath: NotRequired[str]\n    eventsEnabled: NotRequired[bool]\n\n\nclass CancelDownloadParams(TypedDict):\n    \"\"\"Parameters for cancelling a download if in progress.\"\"\"\n\n    guid: str\n    browserContextId: NotRequired[BrowserContextID]\n\n\nclass GetHistogramsParams(TypedDict):\n    \"\"\"Parameters for getting Chrome histograms.\"\"\"\n\n    query: NotRequired[str]\n    delta: NotRequired[bool]\n\n\nclass GetHistogramParams(TypedDict):\n    \"\"\"Parameters for getting a Chrome histogram by name.\"\"\"\n\n    name: str\n    delta: NotRequired[bool]\n\n\nclass GetWindowBoundsParams(TypedDict):\n    \"\"\"Parameters for getting position and size of the browser window.\"\"\"\n\n    windowId: WindowID\n\n\nclass GetWindowForTargetParams(TypedDict):\n    \"\"\"Parameters for getting the browser window that contains the devtools target.\"\"\"\n\n    targetId: NotRequired[str]  # Target.TargetID\n\n\nclass SetWindowBoundsParams(TypedDict):\n    \"\"\"Parameters for setting position and/or size of the browser window.\"\"\"\n\n    windowId: WindowID\n    bounds: Bounds\n\n\nclass SetContentsSizeParams(TypedDict):\n    \"\"\"Parameters for setting size of the browser contents.\"\"\"\n\n    windowId: WindowID\n    width: NotRequired[int]\n    height: NotRequired[int]\n\n\nclass SetDockTileParams(TypedDict):\n    \"\"\"Parameters for setting dock tile details, platform-specific.\"\"\"\n\n    badgeLabel: NotRequired[str]\n    image: NotRequired[str]  # Png encoded image (base64)\n\n\nclass ExecuteBrowserCommandParams(TypedDict):\n    \"\"\"Parameters for invoking custom browser commands used by telemetry.\"\"\"\n\n    commandId: BrowserCommandId\n\n\nclass AddPrivacySandboxEnrollmentOverrideParams(TypedDict):\n    \"\"\"Parameters for allowing a site to use privacy sandbox features without enrollment.\"\"\"\n\n    url: str\n\n\nclass AddPrivacySandboxCoordinatorKeyConfigParams(TypedDict):\n    \"\"\"Parameters for configuring encryption keys for privacy sandbox API.\"\"\"\n\n    api: PrivacySandboxAPI\n    coordinatorOrigin: str\n    keyConfig: str\n    browserContextId: NotRequired[BrowserContextID]\n\n\n# Result types\nclass GetVersionResult(TypedDict):\n    \"\"\"Result for getVersion command.\"\"\"\n\n    protocolVersion: str\n    product: str\n    revision: str\n    userAgent: str\n    jsVersion: str\n\n\nclass GetBrowserCommandLineResult(TypedDict):\n    \"\"\"Result for getBrowserCommandLine command.\"\"\"\n\n    arguments: list[str]\n\n\nclass GetHistogramsResult(TypedDict):\n    \"\"\"Result for getHistograms command.\"\"\"\n\n    histograms: list[Histogram]\n\n\nclass GetHistogramResult(TypedDict):\n    \"\"\"Result for getHistogram command.\"\"\"\n\n    histogram: Histogram\n\n\nclass GetWindowBoundsResult(TypedDict):\n    \"\"\"Result for getWindowBounds command.\"\"\"\n\n    bounds: Bounds\n\n\nclass GetWindowForTargetResult(TypedDict):\n    \"\"\"Result for getWindowForTarget command.\"\"\"\n\n    windowId: WindowID\n    bounds: Bounds\n\n\n# Response types\nGetVersionResponse = Response[GetVersionResult]\nGetBrowserCommandLineResponse = Response[GetBrowserCommandLineResult]\nGetHistogramsResponse = Response[GetHistogramsResult]\nGetHistogramResponse = Response[GetHistogramResult]\nGetWindowBoundsResponse = Response[GetWindowBoundsResult]\nGetWindowForTargetResponse = Response[GetWindowForTargetResult]\n\n\n# Command types\nAddPrivacySandboxCoordinatorKeyConfigCommand = Command[\n    AddPrivacySandboxCoordinatorKeyConfigParams, Response[EmptyResponse]\n]\nAddPrivacySandboxEnrollmentOverrideCommand = Command[\n    AddPrivacySandboxEnrollmentOverrideParams, Response[EmptyResponse]\n]\nCancelDownloadCommand = Command[CancelDownloadParams, Response[EmptyResponse]]\nCloseCommand = Command[EmptyParams, Response[EmptyResponse]]\nCrashCommand = Command[EmptyParams, Response[EmptyResponse]]\nCrashGpuProcessCommand = Command[EmptyParams, Response[EmptyResponse]]\nExecuteBrowserCommandCommand = Command[ExecuteBrowserCommandParams, Response[EmptyResponse]]\nGetBrowserCommandLineCommand = Command[EmptyParams, GetBrowserCommandLineResponse]\nGetHistogramCommand = Command[GetHistogramParams, GetHistogramResponse]\nGetHistogramsCommand = Command[GetHistogramsParams, GetHistogramsResponse]\nGetVersionCommand = Command[EmptyParams, GetVersionResponse]\nGetWindowBoundsCommand = Command[GetWindowBoundsParams, GetWindowBoundsResponse]\nGetWindowForTargetCommand = Command[GetWindowForTargetParams, GetWindowForTargetResponse]\nGrantPermissionsCommand = Command[GrantPermissionsParams, Response[EmptyResponse]]\nResetPermissionsCommand = Command[ResetPermissionsParams, Response[EmptyResponse]]\nSetContentsSizeCommand = Command[SetContentsSizeParams, Response[EmptyResponse]]\nSetDockTileCommand = Command[SetDockTileParams, Response[EmptyResponse]]\nSetDownloadBehaviorCommand = Command[SetDownloadBehaviorParams, Response[EmptyResponse]]\nSetPermissionCommand = Command[SetPermissionParams, Response[EmptyResponse]]\nSetWindowBoundsCommand = Command[SetWindowBoundsParams, Response[EmptyResponse]]\n"
  },
  {
    "path": "pydoll/protocol/browser/types.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import TypedDict\n\nBrowserContextID = str\nWindowID = int\n\n\nclass WindowState(str, Enum):\n    \"\"\"The state of the browser window.\"\"\"\n\n    NORMAL = 'normal'\n    MINIMIZED = 'minimized'\n    MAXIMIZED = 'maximized'\n    FULLSCREEN = 'fullscreen'\n\n\nclass DownloadBehavior(str, Enum):\n    \"\"\"Download behavior options.\"\"\"\n\n    DENY = 'deny'\n    ALLOW = 'allow'\n    ALLOW_AND_NAME = 'allowAndName'\n    DEFAULT = 'default'\n\n\nclass DownloadProgressState(str, Enum):\n    \"\"\"Download progress state.\"\"\"\n\n    IN_PROGRESS = 'inProgress'\n    COMPLETED = 'completed'\n    CANCELED = 'canceled'\n\n\nclass Bounds(TypedDict, total=False):\n    \"\"\"Browser window bounds information.\"\"\"\n\n    left: int  # The offset from the left edge of the screen to the window in pixels.\n    top: int  # The offset from the top edge of the screen to the window in pixels.\n    width: int  # The window width in pixels.\n    height: int  # The window height in pixels.\n    windowState: WindowState  # The window state. Default to normal.\n\n\nclass PermissionType(str, Enum):\n    \"\"\"Permission types.\"\"\"\n\n    AR = 'ar'\n    AUDIO_CAPTURE = 'audioCapture'\n    AUTOMATIC_FULLSCREEN = 'automaticFullscreen'\n    BACKGROUND_FETCH = 'backgroundFetch'\n    BACKGROUND_SYNC = 'backgroundSync'\n    CAMERA_PAN_TILT_ZOOM = 'cameraPanTiltZoom'\n    CAPTURED_SURFACE_CONTROL = 'capturedSurfaceControl'\n    CLIPBOARD_READ_WRITE = 'clipboardReadWrite'\n    CLIPBOARD_SANITIZED_WRITE = 'clipboardSanitizedWrite'\n    DISPLAY_CAPTURE = 'displayCapture'\n    DURABLE_STORAGE = 'durableStorage'\n    GEOLOCATION = 'geolocation'\n    HAND_TRACKING = 'handTracking'\n    IDLE_DETECTION = 'idleDetection'\n    KEYBOARD_LOCK = 'keyboardLock'\n    LOCAL_FONTS = 'localFonts'\n    LOCAL_NETWORK_ACCESS = 'localNetworkAccess'\n    MIDI = 'midi'\n    MIDI_SYSEX = 'midiSysex'\n    NFC = 'nfc'\n    NOTIFICATIONS = 'notifications'\n    PAYMENT_HANDLER = 'paymentHandler'\n    PERIODIC_BACKGROUND_SYNC = 'periodicBackgroundSync'\n    POINTER_LOCK = 'pointerLock'\n    PROTECTED_MEDIA_IDENTIFIER = 'protectedMediaIdentifier'\n    SENSORS = 'sensors'\n    SMART_CARD = 'smartCard'\n    SPEAKER_SELECTION = 'speakerSelection'\n    STORAGE_ACCESS = 'storageAccess'\n    TOP_LEVEL_STORAGE_ACCESS = 'topLevelStorageAccess'\n    VIDEO_CAPTURE = 'videoCapture'\n    VR = 'vr'\n    WAKE_LOCK_SCREEN = 'wakeLockScreen'\n    WAKE_LOCK_SYSTEM = 'wakeLockSystem'\n    WEB_APP_INSTALLATION = 'webAppInstallation'\n    WEB_PRINTING = 'webPrinting'\n    WINDOW_MANAGEMENT = 'windowManagement'\n\n\nclass PermissionSetting(str, Enum):\n    \"\"\"Permission setting values.\"\"\"\n\n    GRANTED = 'granted'\n    DENIED = 'denied'\n    PROMPT = 'prompt'\n\n\nclass PermissionDescriptor(TypedDict, total=False):\n    \"\"\"Definition of PermissionDescriptor defined in the Permissions API.\n\n    See https://w3c.github.io/permissions/#dom-permissiondescriptor.\n    \"\"\"\n\n    name: str  # Name of permission.\n    sysex: bool  # For \"midi\" permission, may also specify sysex control.\n    userVisibleOnly: bool  # For \"push\" permission, may specify userVisibleOnly.\n    allowWithoutSanitization: (\n        bool  # For \"clipboard\" permission, may specify allowWithoutSanitization.\n    )\n    allowWithoutGesture: bool  # For \"fullscreen\" permission, must specify allowWithoutGesture:true.\n    panTiltZoom: bool  # For \"camera\" permission, may specify panTiltZoom.\n\n\nclass BrowserCommandId(str, Enum):\n    \"\"\"Browser command ids used by executeBrowserCommand.\"\"\"\n\n    OPEN_TAB_SEARCH = 'openTabSearch'\n    CLOSE_TAB_SEARCH = 'closeTabSearch'\n    OPEN_GLIC = 'openGlic'\n\n\nclass Bucket(TypedDict):\n    \"\"\"Chrome histogram bucket.\"\"\"\n\n    low: int  # Minimum value (inclusive).\n    high: int  # Maximum value (exclusive).\n    count: int  # Number of samples.\n\n\nclass Histogram(TypedDict):\n    \"\"\"Chrome histogram.\"\"\"\n\n    name: str  # Name.\n    sum: int  # Sum of sample values.\n    count: int  # Total number of samples.\n    buckets: list['Bucket']  # Buckets.\n\n\nclass PrivacySandboxAPI(str, Enum):\n    \"\"\"Privacy Sandbox API types.\"\"\"\n\n    BIDDING_AND_AUCTION_SERVICES = 'BiddingAndAuctionServices'\n    TRUSTED_KEY_VALUE = 'TrustedKeyValue'\n"
  },
  {
    "path": "pydoll/protocol/debugger/types.py",
    "content": "from typing_extensions import TypedDict\n\n\nclass SearchMatch(TypedDict):\n    lineNumber: float\n    lineContent: str\n"
  },
  {
    "path": "pydoll/protocol/dom/__init__.py",
    "content": "\"\"\"DOM domain implementation.\"\"\"\n"
  },
  {
    "path": "pydoll/protocol/dom/events.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import TypedDict\n\nfrom pydoll.protocol.base import CDPEvent\nfrom pydoll.protocol.dom.types import BackendNode, Node, NodeId\n\n\nclass DomEvent(str, Enum):\n    \"\"\"\n    Events from the DOM domain of the Chrome DevTools Protocol.\n\n    This enumeration contains the names of DOM-related events that can be\n    received from the Chrome DevTools Protocol. These events provide information\n    about changes to the DOM structure, attributes, and other DOM-related activities.\n    \"\"\"\n\n    ATTRIBUTE_MODIFIED = 'DOM.attributeModified'\n    \"\"\"\n    Fired when Element's attribute is modified.\n\n    Args:\n        nodeId (NodeId): Id of the node that has changed.\n        name (str): Attribute name.\n        value (str): Attribute value.\n    \"\"\"\n\n    ATTRIBUTE_REMOVED = 'DOM.attributeRemoved'\n    \"\"\"\n    Fired when Element's attribute is removed.\n\n    Args:\n        nodeId (NodeId): Id of the node that has changed.\n        name (str): Attribute name.\n    \"\"\"\n\n    CHARACTER_DATA_MODIFIED = 'DOM.characterDataModified'\n    \"\"\"\n    Mirrors DOMCharacterDataModified event.\n\n    Args:\n        nodeId (NodeId): Id of the node that has changed.\n        characterData (str): New text value.\n    \"\"\"\n\n    CHILD_NODE_COUNT_UPDATED = 'DOM.childNodeCountUpdated'\n    \"\"\"\n    Fired when Container's child node count has changed.\n\n    Args:\n        nodeId (NodeId): Id of the node that has changed.\n        childNodeCount (int): New node count.\n    \"\"\"\n\n    CHILD_NODE_INSERTED = 'DOM.childNodeInserted'\n    \"\"\"\n    Mirrors DOMNodeInserted event.\n\n    Args:\n        parentNodeId (NodeId): Id of the node that has changed.\n        previousNodeId (NodeId): Id of the previous sibling.\n        node (Node): Inserted node data.\n    \"\"\"\n\n    CHILD_NODE_REMOVED = 'DOM.childNodeRemoved'\n    \"\"\"\n    Mirrors DOMNodeRemoved event.\n\n    Args:\n        parentNodeId (NodeId): Parent id.\n        nodeId (NodeId): Id of the node that has been removed.\n    \"\"\"\n\n    DISTRIBUTED_NODES_UPDATED = 'DOM.distributedNodesUpdated'\n    \"\"\"\n    Called when distribution is changed.\n\n    Args:\n        insertionPointId (NodeId): Insertion point where distributed nodes were updated.\n        distributedNodes (array[BackendNode]): Distributed nodes for given insertion point.\n    \"\"\"\n\n    DOCUMENT_UPDATED = 'DOM.documentUpdated'\n    \"\"\"\n    Fired when Document has been totally updated. Node ids are no longer valid.\n    \"\"\"\n\n    INLINE_STYLE_INVALIDATED = 'DOM.inlineStyleInvalidated'\n    \"\"\"\n    Fired when Element's inline style is modified via a CSS property modification.\n\n    Args:\n        nodeIds (array[NodeId]): Ids of the nodes for which the inline styles have been invalidated.\n    \"\"\"\n\n    PSEUDO_ELEMENT_ADDED = 'DOM.pseudoElementAdded'\n    \"\"\"\n    Called when a pseudo element is added to an element.\n\n    Args:\n        parentId (NodeId): Pseudo element's parent element id.\n        pseudoElement (Node): The added pseudo element.\n    \"\"\"\n\n    PSEUDO_ELEMENT_REMOVED = 'DOM.pseudoElementRemoved'\n    \"\"\"\n    Called when a pseudo element is removed from an element.\n\n    Args:\n        parentId (NodeId): Pseudo element's parent element id.\n        pseudoElementId (NodeId): The removed pseudo element id.\n    \"\"\"\n\n    SCROLLABLE_FLAG_UPDATED = 'DOM.scrollableFlagUpdated'\n    \"\"\"\n    Fired when a node's scrollability state changes.\n\n    Args:\n        nodeId (DOM.NodeId): The id of the node.\n        isScrollable (bool): If the node is scrollable.\n    \"\"\"\n\n    SHADOW_ROOT_POPPED = 'DOM.shadowRootPopped'\n    \"\"\"\n    Called when shadow root is popped from the element.\n\n    Args:\n        hostId (NodeId): Host element id.\n        rootId (NodeId): Shadow root id.\n    \"\"\"\n\n    SHADOW_ROOT_PUSHED = 'DOM.shadowRootPushed'\n    \"\"\"\n    Called when shadow root is pushed into the element.\n\n    Args:\n        hostId (NodeId): Host element id.\n        root (Node): Shadow root.\n    \"\"\"\n\n    SET_CHILD_NODES = 'DOM.setChildNodes'\n    \"\"\"\n    Fired when backend wants to provide client with the missing DOM structure.\n    This happens upon most of the calls requesting node ids.\n\n    Args:\n        parentId (NodeId): Parent node id to populate with children.\n        nodes (array[Node]): Child nodes array.\n    \"\"\"\n\n    TOP_LAYER_ELEMENTS_UPDATED = 'DOM.topLayerElementsUpdated'\n    \"\"\"\n    Called when top layer elements are changed.\n    \"\"\"\n\n\n# Event parameter types\nclass AttributeModifiedEventParams(TypedDict):\n    \"\"\"Parameters for attributeModified event.\"\"\"\n\n    nodeId: NodeId\n    name: str\n    value: str\n\n\nclass AttributeRemovedEventParams(TypedDict):\n    \"\"\"Parameters for attributeRemoved event.\"\"\"\n\n    nodeId: NodeId\n    name: str\n\n\nclass CharacterDataModifiedEventParams(TypedDict):\n    \"\"\"Parameters for characterDataModified event.\"\"\"\n\n    nodeId: NodeId\n    characterData: str\n\n\nclass ChildNodeCountUpdatedEventParams(TypedDict):\n    \"\"\"Parameters for childNodeCountUpdated event.\"\"\"\n\n    nodeId: NodeId\n    childNodeCount: int\n\n\nclass ChildNodeInsertedEventParams(TypedDict):\n    \"\"\"Parameters for childNodeInserted event.\"\"\"\n\n    parentNodeId: NodeId\n    previousNodeId: NodeId\n    node: Node\n\n\nclass ChildNodeRemovedEventParams(TypedDict):\n    \"\"\"Parameters for childNodeRemoved event.\"\"\"\n\n    parentNodeId: NodeId\n    nodeId: NodeId\n\n\nclass DistributedNodesUpdatedEventParams(TypedDict):\n    \"\"\"Parameters for distributedNodesUpdated event.\"\"\"\n\n    insertionPointId: NodeId\n    distributedNodes: list[BackendNode]\n\n\nclass DocumentUpdatedEventParams(TypedDict):\n    \"\"\"Parameters for documentUpdated event.\"\"\"\n\n    pass\n\n\nclass InlineStyleInvalidatedEventParams(TypedDict):\n    \"\"\"Parameters for inlineStyleInvalidated event.\"\"\"\n\n    nodeIds: list[NodeId]\n\n\nclass PseudoElementAddedEventParams(TypedDict):\n    \"\"\"Parameters for pseudoElementAdded event.\"\"\"\n\n    parentId: NodeId\n    pseudoElement: Node\n\n\nclass PseudoElementRemovedEventParams(TypedDict):\n    \"\"\"Parameters for pseudoElementRemoved event.\"\"\"\n\n    parentId: NodeId\n    pseudoElementId: NodeId\n\n\nclass ScrollableFlagUpdatedEventParams(TypedDict):\n    \"\"\"Parameters for scrollableFlagUpdated event.\"\"\"\n\n    nodeId: NodeId\n    isScrollable: bool\n\n\nclass ShadowRootPoppedEventParams(TypedDict):\n    \"\"\"Parameters for shadowRootPopped event.\"\"\"\n\n    hostId: NodeId\n    rootId: NodeId\n\n\nclass ShadowRootPushedEventParams(TypedDict):\n    \"\"\"Parameters for shadowRootPushed event.\"\"\"\n\n    hostId: NodeId\n    root: Node\n\n\nclass SetChildNodesEventParams(TypedDict):\n    \"\"\"Parameters for setChildNodes event.\"\"\"\n\n    parentId: NodeId\n    nodes: list[Node]\n\n\nclass TopLayerElementsUpdatedEventParams(TypedDict):\n    \"\"\"Parameters for topLayerElementsUpdated event.\"\"\"\n\n    pass\n\n\n# Event types\nAttributeModifiedEvent = CDPEvent[AttributeModifiedEventParams]\nAttributeRemovedEvent = CDPEvent[AttributeRemovedEventParams]\nCharacterDataModifiedEvent = CDPEvent[CharacterDataModifiedEventParams]\nChildNodeCountUpdatedEvent = CDPEvent[ChildNodeCountUpdatedEventParams]\nChildNodeInsertedEvent = CDPEvent[ChildNodeInsertedEventParams]\nChildNodeRemovedEvent = CDPEvent[ChildNodeRemovedEventParams]\nDistributedNodesUpdatedEvent = CDPEvent[DistributedNodesUpdatedEventParams]\nDocumentUpdatedEvent = CDPEvent[DocumentUpdatedEventParams]\nInlineStyleInvalidatedEvent = CDPEvent[InlineStyleInvalidatedEventParams]\nPseudoElementAddedEvent = CDPEvent[PseudoElementAddedEventParams]\nPseudoElementRemovedEvent = CDPEvent[PseudoElementRemovedEventParams]\nScrollableFlagUpdatedEvent = CDPEvent[ScrollableFlagUpdatedEventParams]\nShadowRootPoppedEvent = CDPEvent[ShadowRootPoppedEventParams]\nShadowRootPushedEvent = CDPEvent[ShadowRootPushedEventParams]\nSetChildNodesEvent = CDPEvent[SetChildNodesEventParams]\nTopLayerElementsUpdatedEvent = CDPEvent[TopLayerElementsUpdatedEventParams]\n"
  },
  {
    "path": "pydoll/protocol/dom/methods.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import TypedDict\n\nfrom pydoll.protocol.base import Command, EmptyParams, EmptyResponse, Response\nfrom pydoll.protocol.dom.types import (\n    BackendNodeId,\n    BoxModel,\n    CSSComputedStyleProperty,\n    DetachedElementInfo,\n    IncludeWhitespace,\n    LogicalAxes,\n    Node,\n    NodeId,\n    PhysicalAxes,\n    Quad,\n    Rect,\n    RelationType,\n)\nfrom pydoll.protocol.page.types import FrameId\nfrom pydoll.protocol.runtime.types import (\n    ExecutionContextId,\n    RemoteObject,\n    RemoteObjectId,\n    StackTrace,\n)\n\n\nclass DomMethod(str, Enum):\n    \"\"\"DOM domain method names.\"\"\"\n\n    COLLECT_CLASS_NAMES_FROM_SUBTREE = 'DOM.collectClassNamesFromSubtree'\n    COPY_TO = 'DOM.copyTo'\n    DESCRIBE_NODE = 'DOM.describeNode'\n    DISABLE = 'DOM.disable'\n    DISCARD_SEARCH_RESULTS = 'DOM.discardSearchResults'\n    ENABLE = 'DOM.enable'\n    FOCUS = 'DOM.focus'\n    FORCE_SHOW_POPOVER = 'DOM.forceShowPopover'\n    GET_ANCHOR_ELEMENT = 'DOM.getAnchorElement'\n    GET_ATTRIBUTES = 'DOM.getAttributes'\n    GET_BOX_MODEL = 'DOM.getBoxModel'\n    GET_CONTAINER_FOR_NODE = 'DOM.getContainerForNode'\n    GET_CONTENT_QUADS = 'DOM.getContentQuads'\n    GET_DETACHED_DOM_NODES = 'DOM.getDetachedDomNodes'\n    GET_DOCUMENT = 'DOM.getDocument'\n    GET_ELEMENT_BY_RELATION = 'DOM.getElementByRelation'\n    GET_FILE_INFO = 'DOM.getFileInfo'\n    GET_FLATTENED_DOCUMENT = 'DOM.getFlattenedDocument'\n    GET_FRAME_OWNER = 'DOM.getFrameOwner'\n    GET_NODE_FOR_LOCATION = 'DOM.getNodeForLocation'\n    GET_NODE_STACK_TRACES = 'DOM.getNodeStackTraces'\n    GET_NODES_FOR_SUBTREE_BY_STYLE = 'DOM.getNodesForSubtreeByStyle'\n    GET_OUTER_HTML = 'DOM.getOuterHTML'\n    GET_QUERYING_DESCENDANTS_FOR_CONTAINER = 'DOM.getQueryingDescendantsForContainer'\n    GET_RELAYOUT_BOUNDARY = 'DOM.getRelayoutBoundary'\n    GET_SEARCH_RESULTS = 'DOM.getSearchResults'\n    GET_TOP_LAYER_ELEMENTS = 'DOM.getTopLayerElements'\n    HIDE_HIGHLIGHT = 'DOM.hideHighlight'\n    HIGHLIGHT_NODE = 'DOM.highlightNode'\n    HIGHLIGHT_RECT = 'DOM.highlightRect'\n    MARK_UNDOABLE_STATE = 'DOM.markUndoableState'\n    MOVE_TO = 'DOM.moveTo'\n    PERFORM_SEARCH = 'DOM.performSearch'\n    PUSH_NODE_BY_PATH_TO_FRONTEND = 'DOM.pushNodeByPathToFrontend'\n    PUSH_NODES_BY_BACKEND_IDS_TO_FRONTEND = 'DOM.pushNodesByBackendIdsToFrontend'\n    QUERY_SELECTOR = 'DOM.querySelector'\n    QUERY_SELECTOR_ALL = 'DOM.querySelectorAll'\n    REDO = 'DOM.redo'\n    REMOVE_ATTRIBUTE = 'DOM.removeAttribute'\n    REMOVE_NODE = 'DOM.removeNode'\n    REQUEST_CHILD_NODES = 'DOM.requestChildNodes'\n    REQUEST_NODE = 'DOM.requestNode'\n    RESOLVE_NODE = 'DOM.resolveNode'\n    SCROLL_INTO_VIEW_IF_NEEDED = 'DOM.scrollIntoViewIfNeeded'\n    SET_ATTRIBUTE_VALUE = 'DOM.setAttributeValue'\n    SET_ATTRIBUTES_AS_TEXT = 'DOM.setAttributesAsText'\n    SET_FILE_INPUT_FILES = 'DOM.setFileInputFiles'\n    SET_INSPECTED_NODE = 'DOM.setInspectedNode'\n    SET_NODE_NAME = 'DOM.setNodeName'\n    SET_NODE_STACK_TRACES_ENABLED = 'DOM.setNodeStackTracesEnabled'\n    SET_NODE_VALUE = 'DOM.setNodeValue'\n    SET_OUTER_HTML = 'DOM.setOuterHTML'\n    UNDO = 'DOM.undo'\n\n\nclass CollectClassNamesFromSubtreeParams(TypedDict):\n    \"\"\"Parameters for collecting class names from subtree.\"\"\"\n\n    nodeId: NodeId\n\n\nclass CopyToParams(TypedDict, total=False):\n    \"\"\"Parameters for copying a node.\"\"\"\n\n    nodeId: NodeId\n    targetNodeId: NodeId\n    insertBeforeNodeId: NodeId\n\n\nclass DescribeNodeParams(TypedDict, total=False):\n    \"\"\"Parameters for describing a node.\"\"\"\n\n    nodeId: NodeId\n    backendNodeId: BackendNodeId\n    objectId: RemoteObjectId\n    depth: int\n    pierce: bool\n\n\nclass ScrollIntoViewIfNeededParams(TypedDict, total=False):\n    \"\"\"Parameters for scrolling into view if needed.\"\"\"\n\n    nodeId: NodeId\n    backendNodeId: BackendNodeId\n    objectId: RemoteObjectId\n    rect: Rect\n\n\nclass DiscardSearchResultsParams(TypedDict):\n    \"\"\"Parameters for discarding search results.\"\"\"\n\n    searchId: str\n\n\nclass EnableParams(TypedDict, total=False):\n    \"\"\"Parameters for enabling DOM agent.\"\"\"\n\n    includeWhitespace: IncludeWhitespace\n\n\nclass FocusParams(TypedDict, total=False):\n    \"\"\"Parameters for focusing an element.\"\"\"\n\n    nodeId: NodeId\n    backendNodeId: BackendNodeId\n    objectId: RemoteObjectId\n\n\nclass GetAttributesParams(TypedDict):\n    \"\"\"Parameters for getting attributes.\"\"\"\n\n    nodeId: NodeId\n\n\nclass GetBoxModelParams(TypedDict, total=False):\n    \"\"\"Parameters for getting box model.\"\"\"\n\n    nodeId: NodeId\n    backendNodeId: BackendNodeId\n    objectId: RemoteObjectId\n\n\nclass GetContentQuadsParams(TypedDict, total=False):\n    \"\"\"Parameters for getting content quads.\"\"\"\n\n    nodeId: NodeId\n    backendNodeId: BackendNodeId\n    objectId: RemoteObjectId\n\n\nclass GetDocumentParams(TypedDict, total=False):\n    \"\"\"Parameters for getting document.\"\"\"\n\n    depth: int\n    pierce: bool\n\n\nclass GetFlattenedDocumentParams(TypedDict, total=False):\n    \"\"\"Parameters for getting flattened document.\"\"\"\n\n    depth: int\n    pierce: bool\n\n\nclass GetNodesForSubtreeByStyleParams(TypedDict, total=False):\n    \"\"\"Parameters for getting nodes by style.\"\"\"\n\n    nodeId: NodeId\n    computedStyles: list[CSSComputedStyleProperty]\n    pierce: bool\n\n\nclass GetNodeForLocationParams(TypedDict, total=False):\n    \"\"\"Parameters for getting node for location.\"\"\"\n\n    x: int\n    y: int\n    includeUserAgentShadowDOM: bool\n    ignorePointerEventsNone: bool\n\n\nclass GetOuterHTMLParams(TypedDict, total=False):\n    \"\"\"Parameters for getting outer HTML.\"\"\"\n\n    nodeId: NodeId\n    backendNodeId: BackendNodeId\n    objectId: RemoteObjectId\n    includeShadowDOM: bool\n\n\nclass GetRelayoutBoundaryParams(TypedDict):\n    \"\"\"Parameters for getting relayout boundary.\"\"\"\n\n    nodeId: NodeId\n\n\nclass GetSearchResultsParams(TypedDict):\n    \"\"\"Parameters for getting search results.\"\"\"\n\n    searchId: str\n    fromIndex: int\n    toIndex: int\n\n\nclass MoveToParams(TypedDict, total=False):\n    \"\"\"Parameters for moving a node.\"\"\"\n\n    nodeId: NodeId\n    targetNodeId: NodeId\n    insertBeforeNodeId: NodeId\n\n\nclass PerformSearchParams(TypedDict, total=False):\n    \"\"\"Parameters for performing search.\"\"\"\n\n    query: str\n    includeUserAgentShadowDOM: bool\n\n\nclass PushNodeByPathToFrontendParams(TypedDict):\n    \"\"\"Parameters for pushing node by path to frontend.\"\"\"\n\n    path: str\n\n\nclass PushNodesByBackendIdsToFrontendParams(TypedDict):\n    \"\"\"Parameters for pushing nodes by backend IDs to frontend.\"\"\"\n\n    backendNodeIds: list[BackendNodeId]\n\n\nclass QuerySelectorParams(TypedDict):\n    \"\"\"Parameters for querySelector.\"\"\"\n\n    nodeId: NodeId\n    selector: str\n\n\nclass QuerySelectorAllParams(TypedDict):\n    \"\"\"Parameters for querySelectorAll.\"\"\"\n\n    nodeId: NodeId\n    selector: str\n\n\nclass GetElementByRelationParams(TypedDict):\n    \"\"\"Parameters for getting element by relation.\"\"\"\n\n    nodeId: NodeId\n    relation: RelationType\n\n\nclass RemoveAttributeParams(TypedDict):\n    \"\"\"Parameters for removing attribute.\"\"\"\n\n    nodeId: NodeId\n    name: str\n\n\nclass RemoveNodeParams(TypedDict):\n    \"\"\"Parameters for removing node.\"\"\"\n\n    nodeId: NodeId\n\n\nclass RequestChildNodesParams(TypedDict, total=False):\n    \"\"\"Parameters for requesting child nodes.\"\"\"\n\n    nodeId: NodeId\n    depth: int\n    pierce: bool\n\n\nclass RequestNodeParams(TypedDict):\n    \"\"\"Parameters for requesting node.\"\"\"\n\n    objectId: RemoteObjectId\n\n\nclass ResolveNodeParams(TypedDict, total=False):\n    \"\"\"Parameters for resolving node.\"\"\"\n\n    nodeId: NodeId\n    backendNodeId: BackendNodeId\n    objectGroup: str\n    executionContextId: ExecutionContextId\n\n\nclass SetAttributeValueParams(TypedDict):\n    \"\"\"Parameters for setting attribute value.\"\"\"\n\n    nodeId: NodeId\n    name: str\n    value: str\n\n\nclass SetAttributesAsTextParams(TypedDict, total=False):\n    \"\"\"Parameters for setting attributes as text.\"\"\"\n\n    nodeId: NodeId\n    text: str\n    name: str\n\n\nclass SetFileInputFilesParams(TypedDict, total=False):\n    \"\"\"Parameters for setting file input files.\"\"\"\n\n    files: list[str]\n    nodeId: NodeId\n    backendNodeId: BackendNodeId\n    objectId: RemoteObjectId\n\n\nclass SetNodeStackTracesEnabledParams(TypedDict):\n    \"\"\"Parameters for setting node stack traces enabled.\"\"\"\n\n    enable: bool\n\n\nclass GetNodeStackTracesParams(TypedDict):\n    \"\"\"Parameters for getting node stack traces.\"\"\"\n\n    nodeId: NodeId\n\n\nclass GetFileInfoParams(TypedDict):\n    \"\"\"Parameters for getting file info.\"\"\"\n\n    objectId: RemoteObjectId\n\n\nclass SetInspectedNodeParams(TypedDict):\n    \"\"\"Parameters for setting inspected node.\"\"\"\n\n    nodeId: NodeId\n\n\nclass SetNodeNameParams(TypedDict):\n    \"\"\"Parameters for setting node name.\"\"\"\n\n    nodeId: NodeId\n    name: str\n\n\nclass SetNodeValueParams(TypedDict):\n    \"\"\"Parameters for setting node value.\"\"\"\n\n    nodeId: NodeId\n    value: str\n\n\nclass SetOuterHTMLParams(TypedDict):\n    \"\"\"Parameters for setting outer HTML.\"\"\"\n\n    nodeId: NodeId\n    outerHTML: str\n\n\nclass GetFrameOwnerParams(TypedDict):\n    \"\"\"Parameters for getting frame owner.\"\"\"\n\n    frameId: FrameId\n\n\nclass GetContainerForNodeParams(TypedDict, total=False):\n    \"\"\"Parameters for getting container for node.\"\"\"\n\n    nodeId: NodeId\n    containerName: str\n    physicalAxes: PhysicalAxes\n    logicalAxes: LogicalAxes\n    queriesScrollState: bool\n    queriesAnchored: bool\n\n\nclass GetQueryingDescendantsForContainerParams(TypedDict):\n    \"\"\"Parameters for getting querying descendants for container.\"\"\"\n\n    nodeId: NodeId\n\n\nclass GetAnchorElementParams(TypedDict, total=False):\n    \"\"\"Parameters for getting anchor element.\"\"\"\n\n    nodeId: NodeId\n    anchorSpecifier: str\n\n\nclass ForceShowPopoverParams(TypedDict):\n    \"\"\"Parameters for forcing show popover.\"\"\"\n\n    nodeId: NodeId\n    enable: bool\n\n\n# Result types\nclass CollectClassNamesFromSubtreeResult(TypedDict):\n    \"\"\"Result for collectClassNamesFromSubtree command.\"\"\"\n\n    classNames: list[str]\n\n\nclass CopyToResult(TypedDict):\n    \"\"\"Result for copyTo command.\"\"\"\n\n    nodeId: NodeId\n\n\nclass DescribeNodeResult(TypedDict):\n    \"\"\"Result for describeNode command.\"\"\"\n\n    node: Node\n\n\nclass GetAttributesResult(TypedDict):\n    \"\"\"Result for getAttributes command.\"\"\"\n\n    attributes: list[str]\n\n\nclass GetBoxModelResult(TypedDict):\n    \"\"\"Result for getBoxModel command.\"\"\"\n\n    model: BoxModel\n\n\nclass GetContentQuadsResult(TypedDict):\n    \"\"\"Result for getContentQuads command.\"\"\"\n\n    quads: list[Quad]\n\n\nclass GetDocumentResult(TypedDict):\n    \"\"\"Result for getDocument command.\"\"\"\n\n    root: Node\n\n\nclass GetFlattenedDocumentResult(TypedDict):\n    \"\"\"Result for getFlattenedDocument command.\"\"\"\n\n    nodes: list[Node]\n\n\nclass GetNodesForSubtreeByStyleResult(TypedDict):\n    \"\"\"Result for getNodesForSubtreeByStyle command.\"\"\"\n\n    nodeIds: list[NodeId]\n\n\nclass GetNodeForLocationResult(TypedDict, total=False):\n    \"\"\"Result for getNodeForLocation command.\"\"\"\n\n    backendNodeId: BackendNodeId\n    frameId: FrameId\n    nodeId: NodeId\n\n\nclass GetOuterHTMLResult(TypedDict):\n    \"\"\"Result for getOuterHTML command.\"\"\"\n\n    outerHTML: str\n\n\nclass GetRelayoutBoundaryResult(TypedDict):\n    \"\"\"Result for getRelayoutBoundary command.\"\"\"\n\n    nodeId: NodeId\n\n\nclass GetSearchResultsResult(TypedDict):\n    \"\"\"Result for getSearchResults command.\"\"\"\n\n    nodeIds: list[NodeId]\n\n\nclass GetTopLayerElementsResult(TypedDict):\n    \"\"\"Result for getTopLayerElements command.\"\"\"\n\n    nodeIds: list[NodeId]\n\n\nclass GetElementByRelationResult(TypedDict):\n    \"\"\"Result for getElementByRelation command.\"\"\"\n\n    nodeId: NodeId\n\n\nclass MoveToResult(TypedDict):\n    \"\"\"Result for moveTo command.\"\"\"\n\n    nodeId: NodeId\n\n\nclass PerformSearchResult(TypedDict):\n    \"\"\"Result for performSearch command.\"\"\"\n\n    searchId: str\n    resultCount: int\n\n\nclass PushNodeByPathToFrontendResult(TypedDict):\n    \"\"\"Result for pushNodeByPathToFrontend command.\"\"\"\n\n    nodeId: NodeId\n\n\nclass PushNodesByBackendIdsToFrontendResult(TypedDict):\n    \"\"\"Result for pushNodesByBackendIdsToFrontend command.\"\"\"\n\n    nodeIds: list[NodeId]\n\n\nclass QuerySelectorResult(TypedDict):\n    \"\"\"Result for querySelector command.\"\"\"\n\n    nodeId: NodeId\n\n\nclass QuerySelectorAllResult(TypedDict):\n    \"\"\"Result for querySelectorAll command.\"\"\"\n\n    nodeIds: list[NodeId]\n\n\nclass RequestNodeResult(TypedDict):\n    \"\"\"Result for requestNode command.\"\"\"\n\n    nodeId: NodeId\n\n\nclass ResolveNodeResult(TypedDict):\n    \"\"\"Result for resolveNode command.\"\"\"\n\n    object: RemoteObject\n\n\nclass SetNodeNameResult(TypedDict):\n    \"\"\"Result for setNodeName command.\"\"\"\n\n    nodeId: NodeId\n\n\nclass GetNodeStackTracesResult(TypedDict, total=False):\n    \"\"\"Result for getNodeStackTraces command.\"\"\"\n\n    creation: StackTrace\n\n\nclass GetFileInfoResult(TypedDict):\n    \"\"\"Result for getFileInfo command.\"\"\"\n\n    path: str\n\n\nclass GetDetachedDomNodesResult(TypedDict):\n    \"\"\"Result for getDetachedDomNodes command.\"\"\"\n\n    detachedNodes: list[DetachedElementInfo]\n\n\nclass GetFrameOwnerResult(TypedDict, total=False):\n    \"\"\"Result for getFrameOwner command.\"\"\"\n\n    backendNodeId: BackendNodeId\n    nodeId: NodeId\n\n\nclass GetContainerForNodeResult(TypedDict, total=False):\n    \"\"\"Result for getContainerForNode command.\"\"\"\n\n    nodeId: NodeId\n\n\nclass GetQueryingDescendantsForContainerResult(TypedDict):\n    \"\"\"Result for getQueryingDescendantsForContainer command.\"\"\"\n\n    nodeIds: list[NodeId]\n\n\nclass GetAnchorElementResult(TypedDict):\n    \"\"\"Result for getAnchorElement command.\"\"\"\n\n    nodeId: NodeId\n\n\nclass ForceShowPopoverResult(TypedDict):\n    \"\"\"Result for forceShowPopover command.\"\"\"\n\n    nodeIds: list[NodeId]\n\n\n# Response types\nCollectClassNamesFromSubtreeResponse = Response[CollectClassNamesFromSubtreeResult]\nCopyToResponse = Response[CopyToResult]\nDescribeNodeResponse = Response[DescribeNodeResult]\nGetAttributesResponse = Response[GetAttributesResult]\nGetBoxModelResponse = Response[GetBoxModelResult]\nGetContentQuadsResponse = Response[GetContentQuadsResult]\nGetDocumentResponse = Response[GetDocumentResult]\nGetFlattenedDocumentResponse = Response[GetFlattenedDocumentResult]\nGetNodesForSubtreeByStyleResponse = Response[GetNodesForSubtreeByStyleResult]\nGetNodeForLocationResponse = Response[GetNodeForLocationResult]\nGetOuterHTMLResponse = Response[GetOuterHTMLResult]\nGetRelayoutBoundaryResponse = Response[GetRelayoutBoundaryResult]\nGetSearchResultsResponse = Response[GetSearchResultsResult]\nGetTopLayerElementsResponse = Response[GetTopLayerElementsResult]\nGetElementByRelationResponse = Response[GetElementByRelationResult]\nMoveToResponse = Response[MoveToResult]\nPerformSearchResponse = Response[PerformSearchResult]\nPushNodeByPathToFrontendResponse = Response[PushNodeByPathToFrontendResult]\nPushNodesByBackendIdsToFrontendResponse = Response[PushNodesByBackendIdsToFrontendResult]\nQuerySelectorResponse = Response[QuerySelectorResult]\nQuerySelectorAllResponse = Response[QuerySelectorAllResult]\nRequestNodeResponse = Response[RequestNodeResult]\nResolveNodeResponse = Response[ResolveNodeResult]\nSetNodeNameResponse = Response[SetNodeNameResult]\nGetNodeStackTracesResponse = Response[GetNodeStackTracesResult]\nGetFileInfoResponse = Response[GetFileInfoResult]\nGetDetachedDomNodesResponse = Response[GetDetachedDomNodesResult]\nGetFrameOwnerResponse = Response[GetFrameOwnerResult]\nGetContainerForNodeResponse = Response[GetContainerForNodeResult]\nGetQueryingDescendantsForContainerResponse = Response[GetQueryingDescendantsForContainerResult]\nGetAnchorElementResponse = Response[GetAnchorElementResult]\nForceShowPopoverResponse = Response[ForceShowPopoverResult]\n\n\n# Command types\nCollectClassNamesFromSubtreeCommand = Command[\n    CollectClassNamesFromSubtreeParams, CollectClassNamesFromSubtreeResponse\n]\nCopyToCommand = Command[CopyToParams, CopyToResponse]\nDescribeNodeCommand = Command[DescribeNodeParams, DescribeNodeResponse]\nDisableCommand = Command[EmptyParams, Response[EmptyResponse]]\nDiscardSearchResultsCommand = Command[DiscardSearchResultsParams, Response[EmptyResponse]]\nEnableCommand = Command[EnableParams, Response[EmptyResponse]]\nFocusCommand = Command[FocusParams, Response[EmptyResponse]]\nForceShowPopoverCommand = Command[ForceShowPopoverParams, ForceShowPopoverResponse]\nGetAnchorElementCommand = Command[GetAnchorElementParams, GetAnchorElementResponse]\nGetAttributesCommand = Command[GetAttributesParams, GetAttributesResponse]\nGetBoxModelCommand = Command[GetBoxModelParams, GetBoxModelResponse]\nGetContainerForNodeCommand = Command[GetContainerForNodeParams, GetContainerForNodeResponse]\nGetContentQuadsCommand = Command[GetContentQuadsParams, GetContentQuadsResponse]\nGetDetachedDomNodesCommand = Command[EmptyParams, Response[GetDetachedDomNodesResponse]]\nGetDocumentCommand = Command[GetDocumentParams, GetDocumentResponse]\nGetElementByRelationCommand = Command[GetElementByRelationParams, GetElementByRelationResponse]\nGetFileInfoCommand = Command[GetFileInfoParams, GetFileInfoResponse]\nGetFlattenedDocumentCommand = Command[GetFlattenedDocumentParams, GetFlattenedDocumentResponse]\nGetFrameOwnerCommand = Command[GetFrameOwnerParams, GetFrameOwnerResponse]\nGetNodeForLocationCommand = Command[GetNodeForLocationParams, GetNodeForLocationResponse]\nGetNodeStackTracesCommand = Command[GetNodeStackTracesParams, GetNodeStackTracesResponse]\nGetNodesForSubtreeByStyleCommand = Command[\n    GetNodesForSubtreeByStyleParams, GetNodesForSubtreeByStyleResponse\n]\nGetOuterHTMLCommand = Command[GetOuterHTMLParams, GetOuterHTMLResponse]\nGetQueryingDescendantsForContainerCommand = Command[\n    GetQueryingDescendantsForContainerParams, GetQueryingDescendantsForContainerResponse\n]\nGetRelayoutBoundaryCommand = Command[GetRelayoutBoundaryParams, GetRelayoutBoundaryResponse]\nGetSearchResultsCommand = Command[GetSearchResultsParams, GetSearchResultsResponse]\nGetTopLayerElementsCommand = Command[EmptyParams, GetTopLayerElementsResponse]\nHideHighlightCommand = Command[EmptyParams, Response[EmptyResponse]]\nHighlightNodeCommand = Command[EmptyParams, Response[EmptyResponse]]  # redirect to Overlay\nHighlightRectCommand = Command[EmptyParams, Response[EmptyResponse]]  # redirect to Overlay\nMarkUndoableStateCommand = Command[EmptyParams, Response[EmptyResponse]]\nMoveToCommand = Command[MoveToParams, MoveToResponse]\nPerformSearchCommand = Command[PerformSearchParams, PerformSearchResponse]\nPushNodeByPathToFrontendCommand = Command[\n    PushNodeByPathToFrontendParams, PushNodeByPathToFrontendResponse\n]\nPushNodesByBackendIdsToFrontendCommand = Command[\n    PushNodesByBackendIdsToFrontendParams, PushNodesByBackendIdsToFrontendResponse\n]\nQuerySelectorCommand = Command[QuerySelectorParams, QuerySelectorResponse]\nQuerySelectorAllCommand = Command[QuerySelectorAllParams, QuerySelectorAllResponse]\nRedoCommand = Command[EmptyParams, Response[EmptyResponse]]\nRemoveAttributeCommand = Command[RemoveAttributeParams, Response[EmptyResponse]]\nRemoveNodeCommand = Command[RemoveNodeParams, Response[EmptyResponse]]\nRequestChildNodesCommand = Command[RequestChildNodesParams, Response[EmptyResponse]]\nRequestNodeCommand = Command[RequestNodeParams, RequestNodeResponse]\nResolveNodeCommand = Command[ResolveNodeParams, ResolveNodeResponse]\nScrollIntoViewIfNeededCommand = Command[ScrollIntoViewIfNeededParams, Response[EmptyResponse]]\nSetAttributeValueCommand = Command[SetAttributeValueParams, Response[EmptyResponse]]\nSetAttributesAsTextCommand = Command[SetAttributesAsTextParams, Response[EmptyResponse]]\nSetFileInputFilesCommand = Command[SetFileInputFilesParams, Response[EmptyResponse]]\nSetInspectedNodeCommand = Command[SetInspectedNodeParams, Response[EmptyResponse]]\nSetNodeNameCommand = Command[SetNodeNameParams, SetNodeNameResponse]\nSetNodeStackTracesEnabledCommand = Command[SetNodeStackTracesEnabledParams, Response[EmptyResponse]]\nSetNodeValueCommand = Command[SetNodeValueParams, Response[EmptyResponse]]\nSetOuterHTMLCommand = Command[SetOuterHTMLParams, Response[EmptyResponse]]\nUndoCommand = Command[EmptyParams, Response[EmptyResponse]]\n"
  },
  {
    "path": "pydoll/protocol/dom/types.py",
    "content": "from enum import Enum\nfrom typing import Annotated, Any\n\nfrom typing_extensions import TypedDict\n\nNodeId = int\nBackendNodeId = int\nQuad = Annotated[list[float], 'Format: [x1, y1, x2, y2, x3, y3, x4, y4]']\n\n\nclass PseudoType(str, Enum):\n    \"\"\"Pseudo element type.\"\"\"\n\n    FIRST_LINE = 'first-line'\n    FIRST_LETTER = 'first-letter'\n    CHECKMARK = 'checkmark'\n    BEFORE = 'before'\n    AFTER = 'after'\n    PICKER_ICON = 'picker-icon'\n    MARKER = 'marker'\n    BACKDROP = 'backdrop'\n    COLUMN = 'column'\n    SELECTION = 'selection'\n    SEARCH_TEXT = 'search-text'\n    TARGET_TEXT = 'target-text'\n    SPELLING_ERROR = 'spelling-error'\n    GRAMMAR_ERROR = 'grammar-error'\n    HIGHLIGHT = 'highlight'\n    FIRST_LINE_INHERITED = 'first-line-inherited'\n    SCROLL_MARKER = 'scroll-marker'\n    SCROLL_MARKER_GROUP = 'scroll-marker-group'\n    SCROLL_BUTTON = 'scroll-button'\n    SCROLLBAR = 'scrollbar'\n    SCROLLBAR_THUMB = 'scrollbar-thumb'\n    SCROLLBAR_BUTTON = 'scrollbar-button'\n    SCROLLBAR_TRACK = 'scrollbar-track'\n    SCROLLBAR_TRACK_PIECE = 'scrollbar-track-piece'\n    SCROLLBAR_CORNER = 'scrollbar-corner'\n    RESIZER = 'resizer'\n    INPUT_LIST_BUTTON = 'input-list-button'\n    VIEW_TRANSITION = 'view-transition'\n    VIEW_TRANSITION_GROUP = 'view-transition-group'\n    VIEW_TRANSITION_IMAGE_PAIR = 'view-transition-image-pair'\n    VIEW_TRANSITION_GROUP_CHILDREN = 'view-transition-group-children'\n    VIEW_TRANSITION_OLD = 'view-transition-old'\n    VIEW_TRANSITION_NEW = 'view-transition-new'\n    PLACEHOLDER = 'placeholder'\n    FILE_SELECTOR_BUTTON = 'file-selector-button'\n    DETAILS_CONTENT = 'details-content'\n    PICKER = 'picker'\n    PERMISSION_ICON = 'permission-icon'\n\n\nclass ShadowRootType(str, Enum):\n    \"\"\"Shadow root type.\"\"\"\n\n    USER_AGENT = 'user-agent'\n    OPEN = 'open'\n    CLOSED = 'closed'\n\n\nclass CompatibilityMode(str, Enum):\n    \"\"\"Document compatibility mode.\"\"\"\n\n    QUIRKS_MODE = 'QuirksMode'\n    LIMITED_QUIRKS_MODE = 'LimitedQuirksMode'\n    NO_QUIRKS_MODE = 'NoQuirksMode'\n\n\nclass PhysicalAxes(str, Enum):\n    \"\"\"ContainerSelector physical axes.\"\"\"\n\n    HORIZONTAL = 'Horizontal'\n    VERTICAL = 'Vertical'\n    BOTH = 'Both'\n\n\nclass LogicalAxes(str, Enum):\n    \"\"\"ContainerSelector logical axes.\"\"\"\n\n    INLINE = 'Inline'\n    BLOCK = 'Block'\n    BOTH = 'Both'\n\n\nclass ScrollOrientation(str, Enum):\n    \"\"\"Physical scroll orientation.\"\"\"\n\n    HORIZONTAL = 'horizontal'\n    VERTICAL = 'vertical'\n\n\nclass IncludeWhitespace(str, Enum):\n    \"\"\"Include whitespace options.\"\"\"\n\n    NONE = 'none'\n    ALL = 'all'\n\n\nclass RelationType(str, Enum):\n    \"\"\"Element relation types.\"\"\"\n\n    POPOVER_TARGET = 'PopoverTarget'\n    INTEREST_TARGET = 'InterestTarget'\n    COMMAND_FOR = 'CommandFor'\n\n\nclass BackendNode(TypedDict):\n    \"\"\"Backend node with a friendly name.\"\"\"\n\n    nodeType: int\n    nodeName: str\n    backendNodeId: BackendNodeId\n\n\nclass Node(TypedDict, total=False):\n    \"\"\"DOM interaction is implemented in terms of mirror objects that represent the actual DOM\n    nodes.\"\"\"\n\n    nodeId: NodeId\n    parentId: NodeId\n    backendNodeId: BackendNodeId\n    nodeType: int\n    nodeName: str\n    localName: str\n    nodeValue: str\n    childNodeCount: int\n    children: list['Node']\n    attributes: list[str]\n    documentURL: str\n    baseURL: str\n    publicId: str\n    systemId: str\n    internalSubset: str\n    xmlVersion: str\n    name: str\n    value: str\n    pseudoType: PseudoType\n    pseudoIdentifier: str\n    shadowRootType: ShadowRootType\n    frameId: str\n    contentDocument: 'Node'\n    shadowRoots: list['Node']\n    templateContent: 'Node'\n    pseudoElements: list['Node']\n    importedDocument: 'Node'  # deprecated\n    distributedNodes: list[BackendNode]\n    isSVG: bool\n    compatibilityMode: CompatibilityMode\n    assignedSlot: BackendNode\n    isScrollable: bool\n\n\nclass DetachedElementInfo(TypedDict):\n    \"\"\"A structure to hold the top-level node of a detached tree and an array of its retained\n    descendants.\"\"\"\n\n    treeNode: Node\n    retainedNodeIds: list[NodeId]\n\n\nclass RGBA(TypedDict, total=False):\n    \"\"\"A structure holding an RGBA color.\"\"\"\n\n    r: int  # The red component, in the [0-255] range.\n    g: int  # The green component, in the [0-255] range.\n    b: int  # The blue component, in the [0-255] range.\n    a: float  # The alpha component, in the [0-1] range (default: 1).\n\n\nclass BoxModel(TypedDict, total=False):\n    \"\"\"Box model.\"\"\"\n\n    content: Quad\n    padding: Quad\n    border: Quad\n    margin: Quad\n    width: int\n    height: int\n    shapeOutside: 'ShapeOutsideInfo'\n\n\nclass ShapeOutsideInfo(TypedDict):\n    \"\"\"CSS Shape Outside details.\"\"\"\n\n    bounds: Quad\n    shape: list[Any]\n    marginShape: list[Any]\n\n\nclass Rect(TypedDict):\n    \"\"\"Rectangle.\"\"\"\n\n    x: float\n    y: float\n    width: float\n    height: float\n\n\nclass CSSComputedStyleProperty(TypedDict):\n    \"\"\"CSS computed style property.\"\"\"\n\n    name: str\n    value: str\n"
  },
  {
    "path": "pydoll/protocol/emulation/__init__.py",
    "content": "\"\"\"Emulation domain implementation.\"\"\"\n"
  },
  {
    "path": "pydoll/protocol/emulation/methods.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import Command, EmptyResponse, Response\nfrom pydoll.protocol.emulation.types import UserAgentMetadata\n\n\nclass EmulationMethod(str, Enum):\n    SET_USER_AGENT_OVERRIDE = 'Emulation.setUserAgentOverride'\n\n\nclass SetUserAgentOverrideParams(TypedDict):\n    \"\"\"Parameters for overriding user agent string.\n\n    See https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setUserAgentOverride\n    \"\"\"\n\n    userAgent: str\n    acceptLanguage: NotRequired[str]\n    platform: NotRequired[str]\n    userAgentMetadata: NotRequired[UserAgentMetadata]\n\n\nSetUserAgentOverrideCommand = Command[SetUserAgentOverrideParams, Response[EmptyResponse]]\n"
  },
  {
    "path": "pydoll/protocol/emulation/types.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\n\nclass ScreenOrientationType(str, Enum):\n    \"\"\"Orientation type.\"\"\"\n\n    PORTRAIT_PRIMARY = 'portraitPrimary'\n    PORTRAIT_SECONDARY = 'portraitSecondary'\n    LANDSCAPE_PRIMARY = 'landscapePrimary'\n    LANDSCAPE_SECONDARY = 'landscapeSecondary'\n\n\nclass DisplayFeatureOrientation(str, Enum):\n    \"\"\"Orientation of a display feature in relation to screen.\"\"\"\n\n    VERTICAL = 'vertical'\n    HORIZONTAL = 'horizontal'\n\n\nclass DevicePostureType(str, Enum):\n    \"\"\"Current posture of the device.\"\"\"\n\n    CONTINUOUS = 'continuous'\n    FOLDED = 'folded'\n\n\nclass VirtualTimePolicy(str, Enum):\n    \"\"\"advance: If the scheduler runs out of immediate work, the virtual time base may fast forward\n    to allow the next delayed task (if any) to run; pause: The virtual time base may not advance;\n    pauseIfNetworkFetchesPending: The virtual time base may not advance if there are any pending\n    resource fetches.\"\"\"\n\n    ADVANCE = 'advance'\n    PAUSE = 'pause'\n    PAUSE_IF_NETWORK_FETCHES_PENDING = 'pauseIfNetworkFetchesPending'\n\n\nclass SensorType(str, Enum):\n    \"\"\"Used to specify sensor types to emulate.\n    See https://w3c.github.io/sensors/#automation for more information.\"\"\"\n\n    ABSOLUTE_ORIENTATION = 'absolute-orientation'\n    ACCELEROMETER = 'accelerometer'\n    AMBIENT_LIGHT = 'ambient-light'\n    GRAVITY = 'gravity'\n    GYROSCOPE = 'gyroscope'\n    LINEAR_ACCELERATION = 'linear-acceleration'\n    MAGNETOMETER = 'magnetometer'\n    RELATIVE_ORIENTATION = 'relative-orientation'\n\n\nclass PressureSource(str, Enum):\n    \"\"\"Pressure source type.\"\"\"\n\n    CPU = 'cpu'\n\n\nclass PressureState(str, Enum):\n    \"\"\"Pressure state.\"\"\"\n\n    NOMINAL = 'nominal'\n    FAIR = 'fair'\n    SERIOUS = 'serious'\n    CRITICAL = 'critical'\n\n\nclass DisabledImageType(str, Enum):\n    \"\"\"Enum of image types that can be disabled.\"\"\"\n\n    AVIF = 'avif'\n    WEBP = 'webp'\n\n\nclass SafeAreaInsets(TypedDict, total=False):\n    \"\"\"Safe area insets configuration.\"\"\"\n\n    top: int  # Overrides safe-area-inset-top\n    topMax: int  # Overrides safe-area-max-inset-top\n    left: int  # Overrides safe-area-inset-left\n    leftMax: int  # Overrides safe-area-max-inset-left\n    bottom: int  # Overrides safe-area-inset-bottom\n    bottomMax: int  # Overrides safe-area-max-inset-bottom\n    right: int  # Overrides safe-area-inset-right\n    rightMax: int  # Overrides safe-area-max-inset-right\n\n\nclass ScreenOrientation(TypedDict):\n    \"\"\"Screen orientation.\"\"\"\n\n    type: ScreenOrientationType  # Orientation type\n    angle: int  # Orientation angle\n\n\nclass DisplayFeature(TypedDict):\n    \"\"\"Display feature configuration.\"\"\"\n\n    # Orientation of a display feature in relation to screen\n    orientation: DisplayFeatureOrientation\n    # The offset from the screen origin in either the x or y\n    offset: int\n    # A display feature may mask content such that it is not physically displayed\n    # this length along with the offset describes this area. A display feature that only split\n    # content will have a 0 mask_length\n    maskLength: int\n\n\nclass DevicePosture(TypedDict):\n    \"\"\"Device posture configuration.\"\"\"\n\n    type: DevicePostureType  # Current posture of the device\n\n\nclass MediaFeature(TypedDict):\n    \"\"\"Media feature configuration.\"\"\"\n\n    name: str\n    value: str\n\n\nclass UserAgentBrandVersion(TypedDict):\n    \"\"\"Used to specify User Agent Client Hints to emulate.\n    See https://wicg.github.io/ua-client-hints\"\"\"\n\n    brand: str\n    version: str\n\n\nclass UserAgentMetadata(TypedDict):\n    \"\"\"Used to specify User Agent Client Hints to emulate.\n    See https://wicg.github.io/ua-client-hints\n    Missing optional values will be filled in by the target with what it would normally use.\"\"\"\n\n    platform: str\n    platformVersion: str\n    architecture: str\n    model: str\n    mobile: bool\n    brands: NotRequired[list[UserAgentBrandVersion]]  # Brands appearing in Sec-CH-UA\n    fullVersionList: NotRequired[\n        list[UserAgentBrandVersion]\n    ]  # Brands appearing in Sec-CH-UA-Full-Version-List\n    fullVersion: NotRequired[str]  # deprecated\n    bitness: NotRequired[str]\n    wow64: NotRequired[bool]\n    formFactors: NotRequired[list[str]]  # Used to specify User Agent form-factor values.\n    # See https://wicg.github.io/ua-client-hints/#sec-ch-ua-form-factors\n\n\nclass SensorMetadata(TypedDict, total=False):\n    \"\"\"Sensor metadata configuration.\"\"\"\n\n    available: bool\n    minimumFrequency: float\n    maximumFrequency: float\n\n\nclass SensorReadingSingle(TypedDict):\n    \"\"\"Single sensor reading value.\"\"\"\n\n    value: float\n\n\nclass SensorReadingXYZ(TypedDict):\n    \"\"\"XYZ sensor reading values.\"\"\"\n\n    x: float\n    y: float\n    z: float\n\n\nclass SensorReadingQuaternion(TypedDict):\n    \"\"\"Quaternion sensor reading values.\"\"\"\n\n    x: float\n    y: float\n    z: float\n    w: float\n\n\nclass SensorReading(TypedDict, total=False):\n    \"\"\"Sensor reading configuration.\"\"\"\n\n    single: 'SensorReadingSingle'\n    xyz: 'SensorReadingXYZ'\n    quaternion: 'SensorReadingQuaternion'\n\n\nclass PressureMetadata(TypedDict, total=False):\n    \"\"\"Pressure metadata configuration.\"\"\"\n\n    available: bool\n"
  },
  {
    "path": "pydoll/protocol/fetch/__init__.py",
    "content": "\"\"\"Fetch domain implementation.\"\"\"\n"
  },
  {
    "path": "pydoll/protocol/fetch/events.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import TypedDict\n\nfrom pydoll.protocol.base import CDPEvent\nfrom pydoll.protocol.fetch.types import AuthChallenge\nfrom pydoll.protocol.network.types import ErrorReason, Request, ResourceType\n\n\nclass FetchEvent(str, Enum):\n    \"\"\"\n    Events from the Fetch domain of the Chrome DevTools Protocol.\n\n    This enumeration contains the names of Fetch-related events that can be\n    received from the Chrome DevTools Protocol. These events provide information\n    about network requests that can be intercepted, modified, or responded to\n    by the client.\n    \"\"\"\n\n    AUTH_REQUIRED = 'Fetch.authRequired'\n    \"\"\"\n    Issued when the domain is enabled with handleAuthRequests set to true.\n    The request is paused until client responds with continueWithAuth.\n\n    Args:\n        requestId (RequestId): Each request the page makes will have a unique id.\n        request (Network.Request): The details of the request.\n        frameId (Page.FrameId): The id of the frame that initiated the request.\n        resourceType (Network.ResourceType): How the requested resource will be used.\n        authChallenge (AuthChallenge): Details of the Authorization Challenge encountered.\n            If this is set, client should respond with continueRequest that contains\n            AuthChallengeResponse.\n    \"\"\"\n\n    REQUEST_PAUSED = 'Fetch.requestPaused'\n    \"\"\"\n    Issued when the domain is enabled and the request URL matches the specified filter.\n\n    The request is paused until the client responds with one of continueRequest,\n    failRequest or fulfillRequest. The stage of the request can be determined by\n    presence of responseErrorReason and responseStatusCode -- the request is at the\n    response stage if either of these fields is present and in the request stage otherwise.\n\n    Redirect responses and subsequent requests are reported similarly to regular responses\n    and requests. Redirect responses may be distinguished by the value of responseStatusCode\n    (which is one of 301, 302, 303, 307, 308) along with presence of the location header.\n    Requests resulting from a redirect will have redirectedRequestId field set.\n\n    Args:\n        requestId (RequestId): Each request the page makes will have a unique id.\n        request (Network.Request): The details of the request.\n        frameId (Page.FrameId): The id of the frame that initiated the request.\n        resourceType (Network.ResourceType): How the requested resource will be used.\n        responseErrorReason (Network.ErrorReason): Response error if intercepted at response stage.\n        responseStatusCode (int): Response code if intercepted at response stage.\n        responseStatusText (str): Response status text if intercepted at response stage.\n        responseHeaders (array[HeaderEntry]): Response headers if intercepted at the response stage.\n        networkId (Network.RequestId): If the intercepted request had a corresponding\n            Network.requestWillBeSent event fired for it, then this networkId will be\n            the same as the requestId present in the requestWillBeSent event.\n        redirectedRequestId (RequestId): If the request is due to a redirect response\n            from the server, the id of the request that has caused the redirect.\n    \"\"\"\n\n\nclass AuthRequiredEventParams(TypedDict):\n    \"\"\"Parameters for the AuthRequired event.\"\"\"\n\n    requestId: str\n    request: Request\n    frameId: str\n    resourceType: ResourceType\n    authChallenge: AuthChallenge\n\n\nclass RequestPausedEventParams(TypedDict):\n    \"\"\"Parameters for the RequestPaused event.\"\"\"\n\n    requestId: str\n    request: Request\n    frameId: str\n    resourceType: ResourceType\n    responseErrorReason: ErrorReason\n    responseStatusCode: int\n    responseStatusText: str\n\n\nRequestPausedEvent = CDPEvent[RequestPausedEventParams]\nAuthRequiredEvent = CDPEvent[AuthRequiredEventParams]\n"
  },
  {
    "path": "pydoll/protocol/fetch/methods.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import TypedDict\n\nfrom pydoll.protocol.base import Command, EmptyParams, EmptyResponse, Response\nfrom pydoll.protocol.fetch.types import (\n    AuthChallengeResponse,\n    HeaderEntry,\n    RequestPattern,\n)\nfrom pydoll.protocol.io.types import StreamHandle\nfrom pydoll.protocol.network.types import ErrorReason\n\n\nclass FetchMethod(str, Enum):\n    \"\"\"Fetch domain method names.\"\"\"\n\n    CONTINUE_REQUEST = 'Fetch.continueRequest'\n    CONTINUE_RESPONSE = 'Fetch.continueResponse'\n    CONTINUE_WITH_AUTH = 'Fetch.continueWithAuth'\n    DISABLE = 'Fetch.disable'\n    ENABLE = 'Fetch.enable'\n    FAIL_REQUEST = 'Fetch.failRequest'\n    FULFILL_REQUEST = 'Fetch.fulfillRequest'\n    GET_RESPONSE_BODY = 'Fetch.getResponseBody'\n    TAKE_RESPONSE_BODY_AS_STREAM = 'Fetch.takeResponseBodyAsStream'\n\n\nRequestId = str\n\n\n# Parameter types\nclass EnableParams(TypedDict, total=False):\n    \"\"\"Parameters for enabling the fetch domain.\"\"\"\n\n    patterns: list[RequestPattern]\n    handleAuthRequests: bool\n\n\nclass FailRequestParams(TypedDict):\n    \"\"\"Parameters for failing a request.\"\"\"\n\n    requestId: RequestId\n    errorReason: ErrorReason\n\n\nclass FulfillRequestParams(TypedDict, total=False):\n    \"\"\"Parameters for fulfilling a request.\"\"\"\n\n    requestId: RequestId\n    responseCode: int\n    responseHeaders: list[HeaderEntry]\n    binaryResponseHeaders: str  # \\0-separated name:value pairs (base64)\n    body: str  # base64 encoded\n    responsePhrase: str\n\n\nclass ContinueRequestParams(TypedDict, total=False):\n    \"\"\"Parameters for continuing a request.\"\"\"\n\n    requestId: RequestId\n    url: str\n    method: str\n    postData: str  # base64 encoded\n    headers: list[HeaderEntry]\n    interceptResponse: bool\n\n\nclass ContinueWithAuthParams(TypedDict):\n    \"\"\"Parameters for continuing a request with authentication.\"\"\"\n\n    requestId: RequestId\n    authChallengeResponse: AuthChallengeResponse\n\n\nclass ContinueResponseParams(TypedDict, total=False):\n    \"\"\"Parameters for continuing a response.\"\"\"\n\n    requestId: RequestId\n    responseCode: int\n    responsePhrase: str\n    responseHeaders: list[HeaderEntry]\n    binaryResponseHeaders: str  # \\0-separated name:value pairs (base64)\n\n\nclass GetResponseBodyParams(TypedDict):\n    \"\"\"Parameters for getting response body.\"\"\"\n\n    requestId: RequestId\n\n\nclass TakeResponseBodyAsStreamParams(TypedDict):\n    \"\"\"Parameters for taking response body as stream.\"\"\"\n\n    requestId: RequestId\n\n\n# Result types\nclass GetResponseBodyResult(TypedDict):\n    \"\"\"Result for getResponseBody command.\"\"\"\n\n    body: str\n    base64Encoded: bool\n\n\nclass TakeResponseBodyAsStreamResult(TypedDict):\n    \"\"\"Result for takeResponseBodyAsStream command.\"\"\"\n\n    stream: StreamHandle\n\n\n# Response types\nGetResponseBodyResponse = Response[GetResponseBodyResult]\nTakeResponseBodyAsStreamResponse = Response[TakeResponseBodyAsStreamResult]\n\n\n# Command types\nContinueRequestCommand = Command[ContinueRequestParams, Response[EmptyResponse]]\nContinueResponseCommand = Command[ContinueResponseParams, Response[EmptyResponse]]\nContinueWithAuthCommand = Command[ContinueWithAuthParams, Response[EmptyResponse]]\nDisableCommand = Command[EmptyParams, Response[EmptyResponse]]\nEnableCommand = Command[EnableParams, Response[EmptyResponse]]\nFailRequestCommand = Command[FailRequestParams, Response[EmptyResponse]]\nFulfillRequestCommand = Command[FulfillRequestParams, Response[EmptyResponse]]\nGetResponseBodyCommand = Command[GetResponseBodyParams, GetResponseBodyResponse]\nTakeResponseBodyAsStreamCommand = Command[\n    TakeResponseBodyAsStreamParams, TakeResponseBodyAsStreamResponse\n]\n"
  },
  {
    "path": "pydoll/protocol/fetch/types.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.network.types import ResourceType\n\n\nclass RequestStage(str, Enum):\n    \"\"\"Stages of the request to handle.\"\"\"\n\n    REQUEST = 'Request'\n    RESPONSE = 'Response'\n\n\nclass AuthChallengeSource(str, Enum):\n    \"\"\"Source of the authentication challenge.\"\"\"\n\n    SERVER = 'Server'\n    PROXY = 'Proxy'\n\n\nclass AuthChallengeResponseType(str, Enum):\n    \"\"\"The decision on what to do in response to the authorization challenge.\"\"\"\n\n    DEFAULT = 'Default'\n    CANCEL_AUTH = 'CancelAuth'\n    PROVIDE_CREDENTIALS = 'ProvideCredentials'\n\n\nclass RequestPattern(TypedDict, total=False):\n    \"\"\"Pattern for request interception.\"\"\"\n\n    urlPattern: str  # Wildcards allowed. Omitting is equivalent to \"*\".\n    resourceType: ResourceType\n    requestStage: RequestStage\n\n\nclass HeaderEntry(TypedDict):\n    \"\"\"Response HTTP header entry.\"\"\"\n\n    name: str\n    value: str\n\n\nclass AuthChallenge(TypedDict):\n    \"\"\"Authorization challenge for HTTP status code 401 or 407.\"\"\"\n\n    source: NotRequired[AuthChallengeSource]\n    origin: str\n    scheme: str  # e.g. basic, digest\n    realm: str\n\n\nclass AuthChallengeResponse(TypedDict):\n    \"\"\"Response to an AuthChallenge.\"\"\"\n\n    response: AuthChallengeResponseType\n    username: NotRequired[str]\n    password: NotRequired[str]\n"
  },
  {
    "path": "pydoll/protocol/input/__init__.py",
    "content": "\"\"\"Input domain implementation.\"\"\"\n"
  },
  {
    "path": "pydoll/protocol/input/events.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import TypedDict\n\nfrom pydoll.protocol.base import CDPEvent\nfrom pydoll.protocol.input.types import DragData\n\n\nclass InputEvent(str, Enum):\n    \"\"\"\n    Events from the Input domain of the Chrome DevTools Protocol.\n\n    This enumeration contains the names of Input-related events that can be\n    received from the Chrome DevTools Protocol. These events provide information\n    about user input interactions that can be intercepted or simulated.\n    \"\"\"\n\n    DRAG_INTERCEPTED = 'Input.dragIntercepted'\n    \"\"\"\n    Emitted only when Input.setInterceptDrags is enabled. Use this data with\n    Input.dispatchDragEvent to restore normal drag and drop behavior.\n\n    Args:\n        data (DragData): Contains information about the dragged data.\n    \"\"\"\n\n\nclass DragInterceptedEventParams(TypedDict):\n    \"\"\"Parameters for dragIntercepted event.\"\"\"\n\n    data: DragData\n\n\nDragInterceptedEvent = CDPEvent[DragInterceptedEventParams]\n"
  },
  {
    "path": "pydoll/protocol/input/methods.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import Command, EmptyParams, EmptyResponse, Response\nfrom pydoll.protocol.input.types import (\n    DragData,\n    DragEventType,\n    GestureSourceType,\n    KeyEventType,\n    MouseButton,\n    MouseEventType,\n    PointerType,\n    TimeSinceEpoch,\n    TouchEventType,\n    TouchPoint,\n)\n\n\nclass InputMethod(str, Enum):\n    CANCEL_DRAGGING = 'Input.cancelDragging'\n    DISPATCH_KEY_EVENT = 'Input.dispatchKeyEvent'\n    DISPATCH_MOUSE_EVENT = 'Input.dispatchMouseEvent'\n    DISPATCH_TOUCH_EVENT = 'Input.dispatchTouchEvent'\n    SET_IGNORE_INPUT_EVENTS = 'Input.setIgnoreInputEvents'\n    DISPATCH_DRAG_EVENT = 'Input.dispatchDragEvent'\n    EMULATE_TOUCH_FROM_MOUSE_EVENT = 'Input.emulateTouchFromMouseEvent'\n    IME_SET_COMPOSITION = 'Input.imeSetComposition'\n    INSERT_TEXT = 'Input.insertText'\n    SET_INTERCEPT_DRAGS = 'Input.setInterceptDrags'\n    SYNTHESIZE_PINCH_GESTURE = 'Input.synthesizePinchGesture'\n    SYNTHESIZE_SCROLL_GESTURE = 'Input.synthesizeScrollGesture'\n    SYNTHESIZE_TAP_GESTURE = 'Input.synthesizeTapGesture'\n\n\nclass CancelDraggingParams(TypedDict):\n    \"\"\"Parameters for cancelDragging command.\"\"\"\n\n    pass\n\n\nclass DispatchDragEventParams(TypedDict):\n    \"\"\"Parameters for dispatchDragEvent command.\"\"\"\n\n    type: DragEventType\n    x: float\n    y: float\n    data: DragData\n    modifiers: NotRequired[int]\n\n\nclass DispatchKeyEventParams(TypedDict):\n    \"\"\"Parameters for dispatchKeyEvent command.\"\"\"\n\n    type: KeyEventType\n    modifiers: NotRequired[int]\n    timestamp: NotRequired[TimeSinceEpoch]\n    text: NotRequired[str]\n    unmodifiedText: NotRequired[str]\n    keyIdentifier: NotRequired[str]\n    code: NotRequired[str]\n    key: NotRequired[str]\n    windowsVirtualKeyCode: NotRequired[int]\n    nativeVirtualKeyCode: NotRequired[int]\n    autoRepeat: NotRequired[bool]\n    isKeypad: NotRequired[bool]\n    isSystemKey: NotRequired[bool]\n    location: NotRequired[int]\n    commands: NotRequired[list[str]]\n\n\nclass DispatchMouseEventParams(TypedDict):\n    \"\"\"Parameters for dispatchMouseEvent command.\"\"\"\n\n    type: MouseEventType\n    x: float\n    y: float\n    modifiers: NotRequired[int]\n    timestamp: NotRequired[TimeSinceEpoch]\n    button: NotRequired[MouseButton]\n    buttons: NotRequired[int]\n    clickCount: NotRequired[int]\n    force: NotRequired[float]\n    tangentialPressure: NotRequired[float]\n    tiltX: NotRequired[float]\n    tiltY: NotRequired[float]\n    twist: NotRequired[int]\n    deltaX: NotRequired[float]\n    deltaY: NotRequired[float]\n    pointerType: NotRequired[PointerType]\n\n\nclass DispatchTouchEventParams(TypedDict):\n    \"\"\"Parameters for dispatchTouchEvent command.\"\"\"\n\n    type: TouchEventType\n    touchPoints: list[TouchPoint]\n    modifiers: NotRequired[int]\n    timestamp: NotRequired[TimeSinceEpoch]\n\n\nclass EmulateTouchFromMouseEventParams(TypedDict):\n    \"\"\"Parameters for emulateTouchFromMouseEvent command.\"\"\"\n\n    type: MouseEventType\n    x: int\n    y: int\n    button: MouseButton\n    timestamp: NotRequired[TimeSinceEpoch]\n    deltaX: NotRequired[float]\n    deltaY: NotRequired[float]\n    modifiers: NotRequired[int]\n    clickCount: NotRequired[int]\n\n\nclass ImeSetCompositionParams(TypedDict):\n    \"\"\"Parameters for imeSetComposition command.\"\"\"\n\n    text: str\n    selectionStart: int\n    selectionEnd: int\n    replacementStart: NotRequired[int]\n    replacementEnd: NotRequired[int]\n\n\nclass InsertTextParams(TypedDict):\n    \"\"\"Parameters for insertText command.\"\"\"\n\n    text: str\n\n\nclass SetIgnoreInputEventsParams(TypedDict):\n    \"\"\"Parameters for setIgnoreInputEvents command.\"\"\"\n\n    ignore: bool\n\n\nclass SetInterceptDragsParams(TypedDict):\n    \"\"\"Parameters for setInterceptDrags command.\"\"\"\n\n    enabled: bool\n\n\nclass SynthesizePinchGestureParams(TypedDict):\n    \"\"\"Parameters for synthesizePinchGesture command.\"\"\"\n\n    x: float\n    y: float\n    scaleFactor: float\n    relativeSpeed: NotRequired[int]\n    gestureSourceType: NotRequired[GestureSourceType]\n\n\nclass SynthesizeScrollGestureParams(TypedDict):\n    \"\"\"Parameters for synthesizeScrollGesture command.\"\"\"\n\n    x: float\n    y: float\n    xDistance: NotRequired[float]\n    yDistance: NotRequired[float]\n    xOverscroll: NotRequired[float]\n    yOverscroll: NotRequired[float]\n    preventFling: NotRequired[bool]\n    speed: NotRequired[int]\n    gestureSourceType: NotRequired[GestureSourceType]\n    repeatCount: NotRequired[int]\n    repeatDelayMs: NotRequired[int]\n    interactionMarkerName: NotRequired[str]\n\n\nclass SynthesizeTapGestureParams(TypedDict):\n    \"\"\"Parameters for synthesizeTapGesture command.\"\"\"\n\n    x: float\n    y: float\n    duration: NotRequired[int]\n    tapCount: NotRequired[int]\n    gestureSourceType: NotRequired[GestureSourceType]\n\n\n# Command types\nCancelDraggingCommand = Command[EmptyParams, Response[EmptyResponse]]\nDispatchDragEventCommand = Command[DispatchDragEventParams, Response[EmptyResponse]]\nDispatchKeyEventCommand = Command[DispatchKeyEventParams, Response[EmptyResponse]]\nDispatchMouseEventCommand = Command[DispatchMouseEventParams, Response[EmptyResponse]]\nDispatchTouchEventCommand = Command[DispatchTouchEventParams, Response[EmptyResponse]]\nEmulateTouchFromMouseEventCommand = Command[\n    EmulateTouchFromMouseEventParams, Response[EmptyResponse]\n]\nImeSetCompositionCommand = Command[ImeSetCompositionParams, Response[EmptyResponse]]\nInsertTextCommand = Command[InsertTextParams, Response[EmptyResponse]]\nSetIgnoreInputEventsCommand = Command[SetIgnoreInputEventsParams, Response[EmptyResponse]]\nSetInterceptDragsCommand = Command[SetInterceptDragsParams, Response[EmptyResponse]]\nSynthesizePinchGestureCommand = Command[SynthesizePinchGestureParams, Response[EmptyResponse]]\nSynthesizeScrollGestureCommand = Command[SynthesizeScrollGestureParams, Response[EmptyResponse]]\nSynthesizeTapGestureCommand = Command[SynthesizeTapGestureParams, Response[EmptyResponse]]\n"
  },
  {
    "path": "pydoll/protocol/input/types.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nTimeSinceEpoch = float\n\n\nclass GestureSourceType(str, Enum):\n    \"\"\"Gesture source types.\"\"\"\n\n    DEFAULT = 'default'\n    TOUCH = 'touch'\n    MOUSE = 'mouse'\n\n\nclass MouseButton(str, Enum):\n    \"\"\"Mouse button types.\"\"\"\n\n    NONE = 'none'\n    LEFT = 'left'\n    MIDDLE = 'middle'\n    RIGHT = 'right'\n    BACK = 'back'\n    FORWARD = 'forward'\n\n\nclass DragEventType(str, Enum):\n    \"\"\"Drag event types.\"\"\"\n\n    DRAG_ENTER = 'dragEnter'\n    DRAG_OVER = 'dragOver'\n    DROP = 'drop'\n    DRAG_CANCEL = 'dragCancel'\n\n\nclass KeyEventType(str, Enum):\n    \"\"\"Key event types.\"\"\"\n\n    KEY_DOWN = 'keyDown'\n    KEY_UP = 'keyUp'\n    RAW_KEY_DOWN = 'rawKeyDown'\n    CHAR = 'char'\n\n\nclass MouseEventType(str, Enum):\n    \"\"\"Mouse event types.\"\"\"\n\n    MOUSE_PRESSED = 'mousePressed'\n    MOUSE_RELEASED = 'mouseReleased'\n    MOUSE_MOVED = 'mouseMoved'\n    MOUSE_WHEEL = 'mouseWheel'\n\n\nclass TouchEventType(str, Enum):\n    \"\"\"Touch event types.\"\"\"\n\n    TOUCH_START = 'touchStart'\n    TOUCH_END = 'touchEnd'\n    TOUCH_MOVE = 'touchMove'\n    TOUCH_CANCEL = 'touchCancel'\n\n\nclass KeyModifier(int, Enum):\n    ALT = 1\n    CTRL = 2\n    META = 4\n    SHIFT = 8\n\n\nclass KeyLocation(int, Enum):\n    LEFT = 1\n    RIGHT = 2\n\n\nclass PointerType(str, Enum):\n    \"\"\"Pointer types.\"\"\"\n\n    MOUSE = 'mouse'\n    PEN = 'pen'\n\n\nclass TouchPoint(TypedDict):\n    \"\"\"Touch point data.\"\"\"\n\n    x: float\n    y: float\n    radiusX: NotRequired[float]\n    radiusY: NotRequired[float]\n    rotationAngle: NotRequired[float]\n    force: NotRequired[float]\n    tangentialPressure: NotRequired[float]\n    tiltX: NotRequired[float]\n    tiltY: NotRequired[float]\n    twist: NotRequired[int]\n    id: NotRequired[float]\n\n\nclass DragDataItem(TypedDict):\n    \"\"\"Drag data item.\"\"\"\n\n    mimeType: str\n    data: str\n    title: NotRequired[str]\n    baseURL: NotRequired[str]\n\n\nclass DragData(TypedDict):\n    \"\"\"Drag data.\"\"\"\n\n    items: list[DragDataItem]\n    dragOperationsMask: int\n    files: NotRequired[list[str]]\n"
  },
  {
    "path": "pydoll/protocol/io/types.py",
    "content": "StreamHandle = str\n"
  },
  {
    "path": "pydoll/protocol/network/__init__.py",
    "content": "\"\"\"Network domain implementation.\"\"\"\n"
  },
  {
    "path": "pydoll/protocol/network/events.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import CDPEvent\nfrom pydoll.protocol.network.types import (\n    AssociatedCookie,\n    AuthChallenge,\n    BlockedReason,\n    BlockedSetCookieWithReason,\n    ClientSecurityState,\n    ConnectTiming,\n    CookiePartitionKey,\n    CorsErrorStatus,\n    DirectTCPSocketOptions,\n    DirectUDPMessage,\n    DirectUDPSocketOptions,\n    ErrorReason,\n    ExemptedSetCookieWithReason,\n    Headers,\n    Initiator,\n    InterceptionId,\n    IPAddressSpace,\n    LoaderId,\n    MonotonicTime,\n    ReportingApiEndpoint,\n    ReportingApiReport,\n    Request,\n    RequestId,\n    ResourcePriority,\n    ResourceType,\n    Response,\n    SignedExchangeInfo,\n    TimeSinceEpoch,\n    TrustTokenOperationType,\n    WebSocketFrame,\n    WebSocketRequest,\n    WebSocketResponse,\n)\n\n\nclass NetworkEvent(str, Enum):\n    \"\"\"\n    Events from the Network domain of the Chrome DevTools Protocol.\n\n    This enumeration contains the names of Network-related events that can be\n    received from the Chrome DevTools Protocol. These events provide information\n    about network activities, such as requests, responses, and WebSocket communications.\n    \"\"\"\n\n    DATA_RECEIVED = 'Network.dataReceived'\n    \"\"\"\n    Fired when data chunk was received over the network.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n        dataLength (int): Data chunk length.\n        encodedDataLength (int): Actual bytes received (might be less than dataLength\n            for compressed encodings).\n        data (str): Data that was received. (Encoded as a base64 string when passed over JSON)\n    \"\"\"\n\n    EVENT_SOURCE_MESSAGE_RECEIVED = 'Network.eventSourceMessageReceived'\n    \"\"\"\n    Fired when EventSource message is received.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n        eventName (str): Message type.\n        eventId (str): Message identifier.\n        data (str): Message content.\n    \"\"\"\n\n    LOADING_FAILED = 'Network.loadingFailed'\n    \"\"\"\n    Fired when HTTP request has failed to load.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n        type (ResourceType): Resource type.\n        errorText (str): Error message. List of network errors: https://cs.chromium.org/chromium/src/net/base/net_error_list.h\n        canceled (bool): True if loading was canceled.\n        blockedReason (BlockedReason): The reason why loading was blocked, if any.\n        corsErrorStatus (CorsErrorStatus): The reason why loading was blocked by CORS, if any.\n    \"\"\"\n\n    LOADING_FINISHED = 'Network.loadingFinished'\n    \"\"\"\n    Fired when HTTP request has finished loading.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n        encodedDataLength (number): Total number of bytes received for this request.\n    \"\"\"\n\n    REQUEST_SERVED_FROM_CACHE = 'Network.requestServedFromCache'\n    \"\"\"\n    Fired if request ended up loading from cache.\n\n    Args:\n        requestId (RequestId): Request identifier.\n    \"\"\"\n\n    REQUEST_WILL_BE_SENT = 'Network.requestWillBeSent'\n    \"\"\"\n    Fired when page is about to send HTTP request.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        loaderId (LoaderId): Loader identifier. Empty string if the request is fetched from worker.\n        documentURL (str): URL of the document this request is loaded for.\n        request (Request): Request data.\n        timestamp (MonotonicTime): Timestamp.\n        wallTime (TimeSinceEpoch): Timestamp.\n        initiator (Initiator): Request initiator.\n        redirectHasExtraInfo (bool): In the case that redirectResponse is populated, this flag\n            indicates whether requestWillBeSentExtraInfo and responseReceivedExtraInfo events\n            will be or were emitted for the request which was just redirected.\n        redirectResponse (Response): Redirect response data.\n        type (ResourceType): Type of this resource.\n        frameId (Page.FrameId): Frame identifier.\n        hasUserGesture (bool): Whether the request is initiated by a user gesture.\n            Defaults to false.\n    \"\"\"\n\n    RESPONSE_RECEIVED = 'Network.responseReceived'\n    \"\"\"\n    Fired when HTTP response is available.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        loaderId (LoaderId): Loader identifier. Empty string if the request is fetched from worker.\n        timestamp (MonotonicTime): Timestamp.\n        type (ResourceType): Resource type.\n        response (Response): Response data.\n        hasExtraInfo (bool): Indicates whether requestWillBeSentExtraInfo and\n            responseReceivedExtraInfo events will be or were emitted for this request.\n        frameId (Page.FrameId): Frame identifier.\n    \"\"\"\n\n    WEBSOCKET_CLOSED = 'Network.webSocketClosed'\n    \"\"\"\n    Fired when WebSocket is closed.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    WEBSOCKET_CREATED = 'Network.webSocketCreated'\n    \"\"\"\n    Fired upon WebSocket creation.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        url (str): WebSocket request URL.\n        initiator (Initiator): Request initiator.\n    \"\"\"\n\n    WEBSOCKET_FRAME_ERROR = 'Network.webSocketFrameError'\n    \"\"\"\n    Fired when WebSocket message error occurs.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n        errorMessage (str): WebSocket error message.\n    \"\"\"\n\n    WEBSOCKET_FRAME_RECEIVED = 'Network.webSocketFrameReceived'\n    \"\"\"\n    Fired when WebSocket message is received.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n        response (WebSocketFrame): WebSocket response data.\n    \"\"\"\n\n    WEBSOCKET_FRAME_SENT = 'Network.webSocketFrameSent'\n    \"\"\"\n    Fired when WebSocket message is sent.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n        response (WebSocketFrame): WebSocket response data.\n    \"\"\"\n\n    WEBSOCKET_HANDSHAKE_RESPONSE_RECEIVED = 'Network.webSocketHandshakeResponseReceived'\n    \"\"\"\n    Fired when WebSocket handshake response becomes available.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n        response (WebSocketResponse): WebSocket response data.\n    \"\"\"\n\n    WEBSOCKET_WILL_SEND_HANDSHAKE_REQUEST = 'Network.webSocketWillSendHandshakeRequest'\n    \"\"\"\n    Fired when WebSocket is about to initiate handshake.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n        wallTime (TimeSinceEpoch): UTC Timestamp.\n        request (WebSocketRequest): WebSocket request data.\n    \"\"\"\n\n    WEBTRANSPORT_CLOSED = 'Network.webTransportClosed'\n    \"\"\"\n    Fired when WebTransport is disposed.\n\n    Args:\n        transportId (RequestId): WebTransport identifier.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    WEBTRANSPORT_CONNECTION_ESTABLISHED = 'Network.webTransportConnectionEstablished'\n    \"\"\"\n    Fired when WebTransport handshake is finished.\n\n    Args:\n        transportId (RequestId): WebTransport identifier.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    WEBTRANSPORT_CREATED = 'Network.webTransportCreated'\n    \"\"\"\n    Fired upon WebTransport creation.\n\n    Args:\n        transportId (RequestId): WebTransport identifier.\n        url (str): WebTransport request URL.\n        timestamp (MonotonicTime): Timestamp.\n        initiator (Initiator): Request initiator.\n    \"\"\"\n\n    DIRECT_TCP_SOCKET_ABORTED = 'Network.directTCPSocketAborted'\n    \"\"\"\n    Fired when direct_socket.TCPSocket is aborted.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        errorMessage (str): Error message.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    DIRECT_TCP_SOCKET_CHUNK_RECEIVED = 'Network.directTCPSocketChunkReceived'\n    \"\"\"\n    Fired when data is received from tcp direct socket stream.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        data (str): Data received.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    DIRECT_TCP_SOCKET_CHUNK_SENT = 'Network.directTCPSocketChunkSent'\n    \"\"\"\n    Fired when data is sent to tcp direct socket stream.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        data (str): Data sent.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    DIRECT_TCP_SOCKET_CLOSED = 'Network.directTCPSocketClosed'\n    \"\"\"\n    Fired when direct_socket.TCPSocket is closed.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    DIRECT_TCP_SOCKET_CREATED = 'Network.directTCPSocketCreated'\n    \"\"\"\n    Fired upon direct_socket.TCPSocket creation.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        remoteAddr (str): Remote address.\n        remotePort (int): Remote port. Unsigned int 16.\n        options (DirectTCPSocketOptions): Socket options.\n        timestamp (MonotonicTime): Timestamp.\n        initiator (Initiator): Request initiator.\n    \"\"\"\n\n    DIRECT_TCP_SOCKET_OPENED = 'Network.directTCPSocketOpened'\n    \"\"\"\n    Fired when direct_socket.TCPSocket connection is opened.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        remoteAddr (str): Remote address.\n        remotePort (int): Remote port. Expected to be unsigned integer.\n        timestamp (MonotonicTime): Timestamp.\n        localAddr (str): Local address.\n        localPort (int): Local port. Expected to be unsigned integer.\n    \"\"\"\n\n    DIRECT_UDP_SOCKET_ABORTED = 'Network.directUDPSocketAborted'\n    \"\"\"\n    Fired when direct_socket.UDPSocket is aborted.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        errorMessage (str): Error message.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    DIRECT_UDP_SOCKET_CHUNK_RECEIVED = 'Network.directUDPSocketChunkReceived'\n    \"\"\"\n    Fired when message is received from udp direct socket stream.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        message (DirectUDPMessage): Message data.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    DIRECT_UDP_SOCKET_CHUNK_SENT = 'Network.directUDPSocketChunkSent'\n    \"\"\"\n    Fired when message is sent to udp direct socket stream.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        message (DirectUDPMessage): Message data.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    DIRECT_UDP_SOCKET_CLOSED = 'Network.directUDPSocketClosed'\n    \"\"\"\n    Fired when direct_socket.UDPSocket is closed.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    DIRECT_UDP_SOCKET_CREATED = 'Network.directUDPSocketCreated'\n    \"\"\"\n    Fired upon direct_socket.UDPSocket creation.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        options (DirectUDPSocketOptions): Socket options.\n        timestamp (MonotonicTime): Timestamp.\n        initiator (Initiator): Request initiator.\n    \"\"\"\n\n    DIRECT_UDP_SOCKET_OPENED = 'Network.directUDPSocketOpened'\n    \"\"\"\n    Fired when direct_socket.UDPSocket connection is opened.\n\n    Args:\n        identifier (RequestId): Request identifier.\n        localAddr (str): Local address.\n        localPort (int): Local port. Expected to be unsigned integer.\n        timestamp (MonotonicTime): Timestamp.\n        remoteAddr (str): Remote address.\n        remotePort (int): Remote port. Expected to be unsigned integer.\n    \"\"\"\n\n    POLICY_UPDATED = 'Network.policyUpdated'\n    \"\"\"\n    Fired once security policy has been updated.\n    \"\"\"\n\n    REPORTING_API_ENDPOINTS_CHANGED_FOR_ORIGIN = 'Network.reportingApiEndpointsChangedForOrigin'\n    \"\"\"\n    Fired when Reporting API endpoints change for an origin.\n\n    Args:\n        origin (str): Origin of the document(s) which configured the endpoints.\n        endpoints (array[ReportingApiEndpoint]): The endpoints configured for the origin.\n    \"\"\"\n\n    REPORTING_API_REPORT_ADDED = 'Network.reportingApiReportAdded'\n    \"\"\"\n    Is sent whenever a new report is added. And after 'enableReportingApi' for all existing reports.\n\n    Args:\n        report (ReportingApiReport): The report that was added.\n    \"\"\"\n\n    REPORTING_API_REPORT_UPDATED = 'Network.reportingApiReportUpdated'\n    \"\"\"\n    Fired when a report is updated.\n\n    Args:\n        report (ReportingApiReport): The report that was updated.\n    \"\"\"\n\n    REQUEST_WILL_BE_SENT_EXTRA_INFO = 'Network.requestWillBeSentExtraInfo'\n    \"\"\"\n    Fired when additional information about a requestWillBeSent event is available from the network\n    stack.\n    Not every requestWillBeSent event will have an additional requestWillBeSentExtraInfo fired for\n    it, and there is no guarantee whether requestWillBeSent or requestWillBeSentExtraInfo will be\n    fired first for the same request.\n\n    Args:\n        requestId (RequestId): Request identifier. Used to match this information to an existing\n            requestWillBeSent event.\n        associatedCookies (array[AssociatedCookie]): A list of cookies potentially associated to\n            the requested URL. This includes both cookies sent with the request and the ones\n            not sent; the latter are distinguished by having blockedReasons field set.\n        headers (Headers): Raw request headers as they will be sent over the wire.\n        connectTiming (ConnectTiming): Connection timing information for the request.\n        clientSecurityState (ClientSecurityState): The client security state set for the request.\n        siteHasCookieInOtherPartition (bool): Whether the site has partitioned cookies stored\n            in a partition different than the current one.\n    \"\"\"\n\n    RESOURCE_CHANGED_PRIORITY = 'Network.resourceChangedPriority'\n    \"\"\"\n    Fired when resource loading priority is changed.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        newPriority (ResourcePriority): New priority.\n        timestamp (MonotonicTime): Timestamp.\n    \"\"\"\n\n    RESPONSE_RECEIVED_EARLY_HINTS = 'Network.responseReceivedEarlyHints'\n    \"\"\"\n    Fired when 103 Early Hints headers is received in addition to the common response.\n    Not every responseReceived event will have an responseReceivedEarlyHints fired.\n    Only one responseReceivedEarlyHints may be fired for eached responseReceived event.\n\n    Args:\n        requestId (RequestId): Request identifier. Used to match this information to another\n            responseReceived event.\n        headers (Headers): Raw response headers as they were received over the wire. Duplicate\n            headers in the response are represented as a single key with their values\n            concatentated using \\\\n as the separator. See also headersText that contains\n            verbatim text for HTTP/1.*.\n    \"\"\"\n\n    RESPONSE_RECEIVED_EXTRA_INFO = 'Network.responseReceivedExtraInfo'\n    \"\"\"\n    Fired when additional information about a responseReceived event is available from the\n    network stack.\n    Not every responseReceived event will have an additional responseReceivedExtraInfo for it,\n    and responseReceivedExtraInfo may be fired before or after responseReceived.\n\n    Args:\n        requestId (RequestId): Request identifier. Used to match this information to another\n            responseReceived event.\n        blockedCookies (array[BlockedSetCookieWithReason]): A list of cookies which were\n            not stored from the response along with the corresponding reasons for blocking.\n            The cookies here may not be valid due to syntax errors, which are represented by\n            the invalid cookie line string instead of a proper cookie.\n        headers (Headers): Raw response headers as they were received over the wire. Duplicate\n            headers in the response are represented as a single key with their values concatentated\n            using \\\\n as the separator. See also headersText that contains verbatim\n            text for HTTP/1.*.\n        resourceIPAddressSpace (IPAddressSpace): The IP address space of the resource. The address\n            space can only be determined once the transport established the connection, so we\n            can't send it in requestWillBeSentExtraInfo.\n        statusCode (int): The status code of the response. This is useful in cases the request\n            failed and no responseReceived event is triggered, which is the case for, e.g.,\n            CORS errors. This is also the correct status code for cached requests, where the\n            status in responseReceived is a 200 and this will be 304.\n        headersText (str): Raw response header text as it was received over the wire. The raw text\n            may not always be available, such as in the case of HTTP/2 or QUIC.\n        cookiePartitionKey (CookiePartitionKey): The cookie partition key that will be used to\n            store partitioned cookies set in this response. Only sent when partitioned\n            cookies are enabled.\n        cookiePartitionKeyOpaque (bool): True if partitioned cookies are enabled, but the\n            partition key is not serializable to string.\n        exemptedCookies (array[ExemptedSetCookieWithReason]): A list of cookies which should have\n            been blocked by 3PCD but are exempted and stored from the response with the\n            corresponding reason.\n    \"\"\"\n\n    SIGNED_EXCHANGE_RECEIVED = 'Network.signedExchangeReceived'\n    \"\"\"\n    Fired when a signed exchange was received over the network.\n\n    Args:\n        requestId (RequestId): Request identifier.\n        info (SignedExchangeInfo): Information about the signed exchange response.\n    \"\"\"\n\n    SUBRESOURCE_WEB_BUNDLE_INNER_RESPONSE_ERROR = 'Network.subresourceWebBundleInnerResponseError'\n    \"\"\"\n    Fired when request for resources within a .wbn file failed.\n\n    Args:\n        innerRequestId (RequestId): Request identifier of the subresource request.\n        innerRequestURL (str): URL of the subresource resource.\n        errorMessage (str): Error message.\n        bundleRequestId (RequestId): Bundle request identifier. Used to match this information\n            to another event. This made be absent in case when the instrumentation was enabled\n            only after webbundle was parsed.\n    \"\"\"\n\n    SUBRESOURCE_WEB_BUNDLE_INNER_RESPONSE_PARSED = 'Network.subresourceWebBundleInnerResponseParsed'\n    \"\"\"\n    Fired when handling requests for resources within a .wbn file.\n    Note: this will only be fired for resources that are requested by the webpage.\n\n    Args:\n        innerRequestId (RequestId): Request identifier of the subresource request.\n        innerRequestURL (str): URL of the subresource resource.\n        bundleRequestId (RequestId): Bundle request identifier. Used to match this information\n            to another event. This made be absent in case when the instrumentation was enabled\n            only after webbundle was parsed.\n    \"\"\"\n\n    SUBRESOURCE_WEB_BUNDLE_METADATA_ERROR = 'Network.subresourceWebBundleMetadataError'\n    \"\"\"\n    Fired once when parsing the .wbn file has failed.\n\n    Args:\n        requestId (RequestId): Request identifier. Used to match this information to another event.\n        errorMessage (str): Error message.\n    \"\"\"\n\n    SUBRESOURCE_WEB_BUNDLE_METADATA_RECEIVED = 'Network.subresourceWebBundleMetadataReceived'\n    \"\"\"\n    Fired once when parsing the .wbn file has succeeded. The event contains the information\n    about the web bundle contents.\n\n    Args:\n        requestId (RequestId): Request identifier. Used to match this information to another event.\n        urls (array[str]): A list of URLs of resources in the subresource Web Bundle.\n    \"\"\"\n\n    TRUST_TOKEN_OPERATION_DONE = 'Network.trustTokenOperationDone'\n    \"\"\"\n    Fired exactly once for each Trust Token operation. Depending on the type of the operation\n    and whether the operation succeeded or failed, the event is fired before the corresponding\n    request was sent or after the response was received.\n\n    Args:\n        status (str): Detailed success or error status of the operation.\n            Allowed Values: Ok, InvalidArgument, MissingIssuerKeys, FailedPrecondition,\n            ResourceExhausted, AlreadyExists, ResourceLimited, Unauthorized, BadResponse,\n            InternalError, UnknownError, FulfilledLocally, SiteIssuerLimit\n        type (TrustTokenOperationType): Type of Trust Token operation.\n        requestId (RequestId): Request identifier.\n        topLevelOrigin (str): Top level origin. The context in which the operation was attempted.\n        issuerOrigin (str): Origin of the issuer in case of a \"Issuance\" or \"Redemption\" operation.\n        issuedTokenCount (int): The number of obtained Trust Tokens on a successful\n            \"Issuance\" operation.\n    \"\"\"\n\n\nclass DataReceivedEventParams(TypedDict):\n    requestId: RequestId\n    timestamp: MonotonicTime\n    dataLength: int\n    encodedDataLength: int\n    data: NotRequired[str]\n\n\nclass EventSourceMessageReceivedEventParams(TypedDict):\n    requestId: RequestId\n    timestamp: MonotonicTime\n    eventName: str\n    eventId: str\n    data: str\n\n\nclass LoadingFailedEventParams(TypedDict):\n    requestId: RequestId\n    timestamp: MonotonicTime\n    type: ResourceType\n    errorText: str\n    canceled: NotRequired[bool]\n    blockedReason: NotRequired[BlockedReason]\n    corsErrorStatus: NotRequired[CorsErrorStatus]\n\n\nclass LoadingFinishedEventParams(TypedDict):\n    requestId: RequestId\n    timestamp: MonotonicTime\n    encodedDataLength: float\n\n\nclass RequestInterceptedEventParams(TypedDict):\n    interceptionId: InterceptionId\n    request: Request\n    frameId: str\n    resourceType: ResourceType\n    isNavigationRequest: bool\n    isDownload: NotRequired[bool]\n    redirectUrl: NotRequired[str]\n    authChallenge: NotRequired[AuthChallenge]\n    responseErrorReason: NotRequired[ErrorReason]\n    responseStatusCode: NotRequired[int]\n    responseHeaders: NotRequired[Headers]\n    requestId: NotRequired[RequestId]\n\n\nclass RequestServedFromCacheEventParams(TypedDict):\n    requestId: RequestId\n\n\nclass RequestWillBeSentEventParams(TypedDict):\n    requestId: RequestId\n    loaderId: LoaderId\n    documentURL: str\n    request: Request\n    timestamp: MonotonicTime\n    wallTime: TimeSinceEpoch\n    initiator: Initiator\n    redirectHasExtraInfo: bool\n    redirectResponse: NotRequired[Response]\n    type: NotRequired[ResourceType]\n    frameId: NotRequired[str]\n    hasUserGesture: NotRequired[bool]\n\n\nclass ResourceChangedPriorityEventParams(TypedDict):\n    requestId: RequestId\n    newPriority: ResourcePriority\n    timestamp: MonotonicTime\n\n\nclass SignedExchangeReceivedEventParams(TypedDict):\n    requestId: RequestId\n    info: SignedExchangeInfo\n\n\nclass ResponseReceivedEventParams(TypedDict):\n    requestId: RequestId\n    loaderId: LoaderId\n    timestamp: MonotonicTime\n    type: ResourceType\n    response: Response\n    hasExtraInfo: bool\n    frameId: NotRequired[str]\n\n\nclass WebSocketClosedEventParams(TypedDict):\n    requestId: RequestId\n    timestamp: MonotonicTime\n\n\nclass WebSocketCreatedEventParams(TypedDict):\n    requestId: RequestId\n    url: str\n    initiator: NotRequired[Initiator]\n\n\nclass WebSocketFrameErrorEventParams(TypedDict):\n    requestId: RequestId\n    timestamp: MonotonicTime\n    errorMessage: str\n\n\nclass WebSocketFrameReceivedEventParams(TypedDict):\n    requestId: RequestId\n    timestamp: MonotonicTime\n    response: WebSocketFrame\n\n\nclass WebSocketFrameSentEventParams(TypedDict):\n    requestId: RequestId\n    timestamp: MonotonicTime\n    response: WebSocketFrame\n\n\nclass WebSocketHandshakeResponseReceivedEventParams(TypedDict):\n    requestId: RequestId\n    timestamp: MonotonicTime\n    response: WebSocketResponse\n\n\nclass WebSocketWillSendHandshakeRequestEventParams(TypedDict):\n    requestId: RequestId\n    timestamp: MonotonicTime\n    wallTime: TimeSinceEpoch\n    request: WebSocketRequest\n\n\nclass WebTransportCreatedEventParams(TypedDict):\n    transportId: RequestId\n    url: str\n    timestamp: MonotonicTime\n    initiator: NotRequired[Initiator]\n\n\nclass WebTransportConnectionEstablishedEventParams(TypedDict):\n    transportId: RequestId\n    timestamp: MonotonicTime\n\n\nclass WebTransportClosedEventParams(TypedDict):\n    transportId: RequestId\n    timestamp: MonotonicTime\n\n\nclass DirectTCPSocketCreatedEventParams(TypedDict):\n    identifier: RequestId\n    remoteAddr: str\n    remotePort: int\n    options: DirectTCPSocketOptions\n    timestamp: MonotonicTime\n    initiator: NotRequired[Initiator]\n\n\nclass DirectTCPSocketOpenedEventParams(TypedDict):\n    identifier: RequestId\n    remoteAddr: str\n    remotePort: int\n    timestamp: MonotonicTime\n    localAddr: NotRequired[str]\n    localPort: NotRequired[int]\n\n\nclass DirectTCPSocketAbortedEventParams(TypedDict):\n    identifier: RequestId\n    errorMessage: str\n    timestamp: MonotonicTime\n\n\nclass DirectTCPSocketClosedEventParams(TypedDict):\n    identifier: RequestId\n    timestamp: MonotonicTime\n\n\nclass DirectTCPSocketChunkSentEventParams(TypedDict):\n    identifier: RequestId\n    data: str\n    timestamp: MonotonicTime\n\n\nclass DirectTCPSocketChunkReceivedEventParams(TypedDict):\n    identifier: RequestId\n    data: str\n    timestamp: MonotonicTime\n\n\nclass DirectUDPSocketCreatedEventParams(TypedDict):\n    identifier: RequestId\n    options: DirectUDPSocketOptions\n    timestamp: MonotonicTime\n    initiator: NotRequired[Initiator]\n\n\nclass DirectUDPSocketOpenedEventParams(TypedDict):\n    identifier: RequestId\n    localAddr: str\n    localPort: int\n    timestamp: MonotonicTime\n    remoteAddr: NotRequired[str]\n    remotePort: NotRequired[int]\n\n\nclass DirectUDPSocketAbortedEventParams(TypedDict):\n    identifier: RequestId\n    errorMessage: str\n    timestamp: MonotonicTime\n\n\nclass DirectUDPSocketClosedEventParams(TypedDict):\n    identifier: RequestId\n    timestamp: MonotonicTime\n\n\nclass DirectUDPSocketChunkSentEventParams(TypedDict):\n    identifier: RequestId\n    message: DirectUDPMessage\n    timestamp: MonotonicTime\n\n\nclass DirectUDPSocketChunkReceivedEventParams(TypedDict):\n    identifier: RequestId\n    message: DirectUDPMessage\n    timestamp: MonotonicTime\n\n\nclass RequestWillBeSentExtraInfoEventParams(TypedDict):\n    requestId: RequestId\n    associatedCookies: list[AssociatedCookie]\n    headers: Headers\n    connectTiming: ConnectTiming\n    clientSecurityState: NotRequired[ClientSecurityState]\n    siteHasCookieInOtherPartition: NotRequired[bool]\n\n\nclass ResponseReceivedExtraInfoEventParams(TypedDict):\n    requestId: RequestId\n    blockedCookies: list[BlockedSetCookieWithReason]\n    headers: Headers\n    resourceIPAddressSpace: IPAddressSpace\n    statusCode: int\n    headersText: NotRequired[str]\n    cookiePartitionKey: NotRequired[CookiePartitionKey]\n    cookiePartitionKeyOpaque: NotRequired[bool]\n    exemptedCookies: NotRequired[list[ExemptedSetCookieWithReason]]\n\n\nclass ResponseReceivedEarlyHintsEventParams(TypedDict):\n    requestId: RequestId\n    headers: Headers\n\n\nclass TrustTokenOperationDoneEventParams(TypedDict):\n    status: str  # enum values: Ok, InvalidArgument, etc.\n    type: TrustTokenOperationType\n    requestId: RequestId\n    topLevelOrigin: NotRequired[str]\n    issuerOrigin: NotRequired[str]\n    issuedTokenCount: NotRequired[int]\n\n\nclass PolicyUpdatedEventParams(TypedDict):\n    pass\n\n\nclass SubresourceWebBundleMetadataReceivedEventParams(TypedDict):\n    requestId: RequestId\n    urls: list[str]\n\n\nclass SubresourceWebBundleMetadataErrorEventParams(TypedDict):\n    requestId: RequestId\n    errorMessage: str\n\n\nclass SubresourceWebBundleInnerResponseParsedEventParams(TypedDict):\n    innerRequestId: RequestId\n    innerRequestURL: str\n    bundleRequestId: NotRequired[RequestId]\n\n\nclass SubresourceWebBundleInnerResponseErrorEventParams(TypedDict):\n    innerRequestId: RequestId\n    innerRequestURL: str\n    errorMessage: str\n    bundleRequestId: NotRequired[RequestId]\n\n\nclass ReportingApiReportAddedEventParams(TypedDict):\n    report: ReportingApiReport\n\n\nclass ReportingApiReportUpdatedEventParams(TypedDict):\n    report: ReportingApiReport\n\n\nclass ReportingApiEndpointsChangedForOriginEventParams(TypedDict):\n    origin: str\n    endpoints: list[ReportingApiEndpoint]\n\n\nDataReceivedEvent = CDPEvent[DataReceivedEventParams]\nEventSourceMessageReceivedEvent = CDPEvent[EventSourceMessageReceivedEventParams]\nLoadingFailedEvent = CDPEvent[LoadingFailedEventParams]\nLoadingFinishedEvent = CDPEvent[LoadingFinishedEventParams]\nRequestInterceptedEvent = CDPEvent[RequestInterceptedEventParams]\nRequestServedFromCacheEvent = CDPEvent[RequestServedFromCacheEventParams]\nRequestWillBeSentEvent = CDPEvent[RequestWillBeSentEventParams]\nResourceChangedPriorityEvent = CDPEvent[ResourceChangedPriorityEventParams]\nSignedExchangeReceivedEvent = CDPEvent[SignedExchangeReceivedEventParams]\nResponseReceivedEvent = CDPEvent[ResponseReceivedEventParams]\nWebSocketClosedEvent = CDPEvent[WebSocketClosedEventParams]\nWebSocketCreatedEvent = CDPEvent[WebSocketCreatedEventParams]\nWebSocketFrameErrorEvent = CDPEvent[WebSocketFrameErrorEventParams]\nWebSocketFrameReceivedEvent = CDPEvent[WebSocketFrameReceivedEventParams]\nWebSocketFrameSentEvent = CDPEvent[WebSocketFrameSentEventParams]\nWebSocketHandshakeResponseReceivedEvent = CDPEvent[WebSocketHandshakeResponseReceivedEventParams]\nWebSocketWillSendHandshakeRequestEvent = CDPEvent[WebSocketWillSendHandshakeRequestEventParams]\nWebTransportCreatedEvent = CDPEvent[WebTransportCreatedEventParams]\nWebTransportConnectionEstablishedEvent = CDPEvent[WebTransportConnectionEstablishedEventParams]\nWebTransportClosedEvent = CDPEvent[WebTransportClosedEventParams]\nDirectTCPSocketCreatedEvent = CDPEvent[DirectTCPSocketCreatedEventParams]\nDirectTCPSocketOpenedEvent = CDPEvent[DirectTCPSocketOpenedEventParams]\nDirectTCPSocketAbortedEvent = CDPEvent[DirectTCPSocketAbortedEventParams]\nDirectTCPSocketClosedEvent = CDPEvent[DirectTCPSocketClosedEventParams]\nDirectTCPSocketChunkSentEvent = CDPEvent[DirectTCPSocketChunkSentEventParams]\nDirectTCPSocketChunkReceivedEvent = CDPEvent[DirectTCPSocketChunkReceivedEventParams]\nDirectUDPSocketCreatedEvent = CDPEvent[DirectUDPSocketCreatedEventParams]\nDirectUDPSocketOpenedEvent = CDPEvent[DirectUDPSocketOpenedEventParams]\nDirectUDPSocketAbortedEvent = CDPEvent[DirectUDPSocketAbortedEventParams]\nDirectUDPSocketClosedEvent = CDPEvent[DirectUDPSocketClosedEventParams]\nDirectUDPSocketChunkSentEvent = CDPEvent[DirectUDPSocketChunkSentEventParams]\nDirectUDPSocketChunkReceivedEvent = CDPEvent[DirectUDPSocketChunkReceivedEventParams]\nRequestWillBeSentExtraInfoEvent = CDPEvent[RequestWillBeSentExtraInfoEventParams]\nResponseReceivedExtraInfoEvent = CDPEvent[ResponseReceivedExtraInfoEventParams]\nResponseReceivedEarlyHintsEvent = CDPEvent[ResponseReceivedEarlyHintsEventParams]\nTrustTokenOperationDoneEvent = CDPEvent[TrustTokenOperationDoneEventParams]\nPolicyUpdatedEvent = CDPEvent[PolicyUpdatedEventParams]\nSubresourceWebBundleMetadataReceivedEvent = CDPEvent[\n    SubresourceWebBundleMetadataReceivedEventParams\n]\nSubresourceWebBundleMetadataErrorEvent = CDPEvent[SubresourceWebBundleMetadataErrorEventParams]\nSubresourceWebBundleInnerResponseParsedEvent = CDPEvent[\n    SubresourceWebBundleInnerResponseParsedEventParams\n]\nSubresourceWebBundleInnerResponseErrorEvent = CDPEvent[\n    SubresourceWebBundleInnerResponseErrorEventParams\n]\nReportingApiReportAddedEvent = CDPEvent[ReportingApiReportAddedEventParams]\nReportingApiReportUpdatedEvent = CDPEvent[ReportingApiReportUpdatedEventParams]\nReportingApiEndpointsChangedForOriginEvent = CDPEvent[\n    ReportingApiEndpointsChangedForOriginEventParams\n]\n"
  },
  {
    "path": "pydoll/protocol/network/har_types.py",
    "content": "\"\"\"HAR 1.2 format type definitions.\n\nBased on the HAR 1.2 specification: http://www.softwareishard.com/blog/har-12-spec/\nThese TypedDicts define the structure of HAR (HTTP Archive) files used for\nrecording and replaying network traffic.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing_extensions import NotRequired, TypedDict\n\n\nclass HarTimings(TypedDict):\n    \"\"\"Timing information about a request/response round trip.\"\"\"\n\n    blocked: float\n    dns: float\n    connect: float\n    ssl: float\n    send: float\n    wait: float\n    receive: float\n\n\nclass HarCookie(TypedDict):\n    \"\"\"Cookie used in a request or response.\"\"\"\n\n    name: str\n    value: str\n    path: NotRequired[str]\n    domain: NotRequired[str]\n    expires: NotRequired[str]\n    httpOnly: NotRequired[bool]\n    secure: NotRequired[bool]\n\n\nclass HarHeader(TypedDict):\n    \"\"\"HTTP header name-value pair.\"\"\"\n\n    name: str\n    value: str\n\n\nclass HarQueryParam(TypedDict):\n    \"\"\"URL query string parameter.\"\"\"\n\n    name: str\n    value: str\n\n\nclass HarPostData(TypedDict):\n    \"\"\"Posted data info.\"\"\"\n\n    mimeType: str\n    text: str\n    params: NotRequired[list[dict]]\n\n\nclass HarRequest(TypedDict):\n    \"\"\"Detailed info about the request.\"\"\"\n\n    method: str\n    url: str\n    httpVersion: str\n    cookies: list[HarCookie]\n    headers: list[HarHeader]\n    queryString: list[HarQueryParam]\n    headersSize: int\n    bodySize: int\n    postData: NotRequired[HarPostData]\n\n\nclass HarContent(TypedDict):\n    \"\"\"Response content body info.\"\"\"\n\n    size: int\n    mimeType: str\n    text: NotRequired[str]\n    encoding: NotRequired[str]\n\n\nclass HarResponse(TypedDict):\n    \"\"\"Detailed info about the response.\"\"\"\n\n    status: int\n    statusText: str\n    httpVersion: str\n    cookies: list[HarCookie]\n    headers: list[HarHeader]\n    content: HarContent\n    redirectURL: str\n    headersSize: int\n    bodySize: int\n\n\nclass HarCache(TypedDict, total=False):\n    \"\"\"Cache state for a request/response pair.\"\"\"\n\n    beforeRequest: dict\n    afterRequest: dict\n\n\nclass HarEntry(TypedDict):\n    \"\"\"Represents an exported HTTP request.\"\"\"\n\n    startedDateTime: str\n    time: float\n    request: HarRequest\n    response: HarResponse\n    cache: HarCache\n    timings: HarTimings\n    serverIPAddress: NotRequired[str]\n    connection: NotRequired[str]\n    _resourceType: NotRequired[str]\n\n\nclass HarPage(TypedDict):\n    \"\"\"Represents an exported page.\"\"\"\n\n    startedDateTime: str\n    id: str\n    title: str\n\n\nclass HarCreator(TypedDict):\n    \"\"\"Information about the creator of the HAR file.\"\"\"\n\n    name: str\n    version: str\n\n\nclass HarLog(TypedDict):\n    \"\"\"Root of the HAR data.\"\"\"\n\n    version: str\n    creator: HarCreator\n    pages: list[HarPage]\n    entries: list[HarEntry]\n\n\nclass Har(TypedDict):\n    \"\"\"Top-level HAR object.\"\"\"\n\n    log: HarLog\n"
  },
  {
    "path": "pydoll/protocol/network/methods.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import Command, EmptyParams, EmptyResponse, Response\nfrom pydoll.protocol.debugger.types import SearchMatch\nfrom pydoll.protocol.emulation.types import UserAgentMetadata\nfrom pydoll.protocol.fetch.types import HeaderEntry, RequestPattern\nfrom pydoll.protocol.network.types import (\n    ConnectionType,\n    ContentEncoding,\n    Cookie,\n    CookiePartitionKey,\n    CookiePriority,\n    CookieSameSite,\n    CookieSourceScheme,\n    LoadNetworkResourceOptions,\n    SecurityIsolationStatus,\n)\n\n\nclass NetworkMethod(str, Enum):\n    CLEAR_BROWSER_CACHE = 'Network.clearBrowserCache'\n    CLEAR_BROWSER_COOKIES = 'Network.clearBrowserCookies'\n    DELETE_COOKIES = 'Network.deleteCookies'\n    DISABLE = 'Network.disable'\n    EMULATE_NETWORK_CONDITIONS = 'Network.emulateNetworkConditions'\n    ENABLE = 'Network.enable'\n    GET_COOKIES = 'Network.getCookies'\n    GET_REQUEST_POST_DATA = 'Network.getRequestPostData'\n    GET_RESPONSE_BODY = 'Network.getResponseBody'\n    SET_BYPASS_SERVICE_WORKER = 'Network.setBypassServiceWorker'\n    SET_CACHE_DISABLED = 'Network.setCacheDisabled'\n    SET_COOKIE = 'Network.setCookie'\n    SET_COOKIES = 'Network.setCookies'\n    SET_EXTRA_HTTP_HEADERS = 'Network.setExtraHTTPHeaders'\n    SET_USER_AGENT_OVERRIDE = 'Network.setUserAgentOverride'\n    CLEAR_ACCEPTED_ENCODINGS_OVERRIDE = 'Network.clearAcceptedEncodingsOverride'\n    ENABLE_REPORTING_API = 'Network.enableReportingApi'\n    GET_CERTIFICATE = 'Network.getCertificate'\n    GET_RESPONSE_BODY_FOR_INTERCEPTION = 'Network.getResponseBodyForInterception'\n    GET_SECURITY_ISOLATION_STATUS = 'Network.getSecurityIsolationStatus'\n    LOAD_NETWORK_RESOURCE = 'Network.loadNetworkResource'\n    REPLAY_XHR = 'Network.replayXHR'\n    SEARCH_IN_RESPONSE_BODY = 'Network.searchInResponseBody'\n    SET_ACCEPTED_ENCODINGS = 'Network.setAcceptedEncodings'\n    SET_ATTACH_DEBUG_STACK = 'Network.setAttachDebugStack'\n    SET_BLOCKED_URLS = 'Network.setBlockedURLs'\n    SET_COOKIE_CONTROLS = 'Network.setCookieControls'\n    STREAM_RESOURCE_CONTENT = 'Network.streamResourceContent'\n    TAKE_RESPONSE_BODY_FOR_INTERCEPTION_AS_STREAM = (\n        'Network.takeResponseBodyForInterceptionAsStream'\n    )\n\n\nclass DeleteCookiesParams(TypedDict):\n    \"\"\"Parameters for deleting browser cookies.\"\"\"\n\n    name: str\n    url: NotRequired[str]\n    domain: NotRequired[str]\n    path: NotRequired[str]\n    partitionKey: NotRequired[CookiePartitionKey]\n\n\nclass EmulateNetworkConditionsParams(TypedDict):\n    \"\"\"Parameters for emulating network conditions.\"\"\"\n\n    offline: bool\n    latency: float\n    downloadThroughput: float\n    uploadThroughput: float\n    connectionType: NotRequired[ConnectionType]\n    packetLoss: NotRequired[float]\n    packetQueueLength: NotRequired[int]\n    packetReordering: NotRequired[bool]\n\n\nclass NetworkEnableParams(TypedDict):\n    \"\"\"Parameters for enabling network tracking.\"\"\"\n\n    maxTotalBufferSize: NotRequired[int]\n    maxResourceBufferSize: NotRequired[int]\n    maxPostDataSize: NotRequired[int]\n\n\nclass GetCookiesParams(TypedDict):\n    \"\"\"Parameters for retrieving browser cookies.\"\"\"\n\n    urls: NotRequired[list[str]]\n\n\nclass GetRequestPostDataParams(TypedDict):\n    \"\"\"Parameters for retrieving request POST data.\"\"\"\n\n    requestId: str\n\n\nclass GetResponseBodyParams(TypedDict):\n    \"\"\"Parameters for retrieving response body.\"\"\"\n\n    requestId: str\n\n\nclass GetCertificateParams(TypedDict):\n    \"\"\"Parameters for retrieving DER-encoded certificate.\"\"\"\n\n    origin: str\n\n\nclass GetResponseBodyForInterceptionParams(TypedDict):\n    \"\"\"Parameters for retrieving response body for intercepted request.\"\"\"\n\n    interceptionId: str\n\n\nclass SearchInResponseBodyParams(TypedDict):\n    \"\"\"Parameters for searching in response content.\"\"\"\n\n    requestId: str\n    query: str\n    caseSensitive: NotRequired[bool]\n    isRegex: NotRequired[bool]\n\n\nclass SetBypassServiceWorkerParams(TypedDict):\n    \"\"\"Parameters for toggling service worker bypass.\"\"\"\n\n    bypass: bool\n\n\nclass SetCacheDisabledParams(TypedDict):\n    \"\"\"Parameters for toggling cache for requests.\"\"\"\n\n    cacheDisabled: bool\n\n\nclass SetCookieParams(TypedDict):\n    \"\"\"Parameters for setting a cookie.\"\"\"\n\n    name: str\n    value: str\n    url: NotRequired[str]\n    domain: NotRequired[str]\n    path: NotRequired[str]\n    secure: NotRequired[bool]\n    httpOnly: NotRequired[bool]\n    sameSite: NotRequired[CookieSameSite]\n    expires: NotRequired[float]\n    priority: NotRequired[CookiePriority]\n    sameParty: NotRequired[bool]\n    sourceScheme: NotRequired[CookieSourceScheme]\n    sourcePort: NotRequired[int]\n    partitionKey: NotRequired[CookiePartitionKey]\n\n\nclass SetCookiesParams(TypedDict):\n    \"\"\"Parameters for setting multiple cookies.\"\"\"\n\n    cookies: list[SetCookieParams]\n\n\nclass SetExtraHTTPHeadersParams(TypedDict):\n    \"\"\"Parameters for setting extra HTTP headers.\"\"\"\n\n    headers: list[HeaderEntry]\n\n\nclass SetUserAgentOverrideParams(TypedDict):\n    \"\"\"Parameters for overriding user agent string.\"\"\"\n\n    userAgent: str\n    acceptLanguage: NotRequired[str]\n    platform: NotRequired[str]\n    userAgentMetadata: NotRequired[UserAgentMetadata]\n\n\nclass SetBlockedURLsParams(TypedDict):\n    \"\"\"Parameters for blocking URLs from loading.\"\"\"\n\n    urls: list[str]\n\n\nclass SetAcceptedEncodingsParams(TypedDict):\n    \"\"\"Parameters for setting accepted content encodings.\"\"\"\n\n    encodings: list[ContentEncoding]\n\n\nclass SetAttachDebugStackParams(TypedDict):\n    \"\"\"Parameters for attaching a page script stack in requests.\"\"\"\n\n    enabled: bool\n\n\nclass SetCookieControlsParams(TypedDict):\n    \"\"\"Parameters for setting controls for third-party cookie access.\"\"\"\n\n    enableThirdPartyCookieRestriction: bool\n    disableThirdPartyCookieMetadata: NotRequired[bool]\n    disableThirdPartyCookieHeuristics: NotRequired[bool]\n\n\nclass StreamResourceContentParams(TypedDict):\n    \"\"\"Parameters for enabling streaming of the response.\"\"\"\n\n    requestId: str\n\n\nclass TakeResponseBodyForInterceptionAsStreamParams(TypedDict):\n    \"\"\"Parameters for taking response body for interception as a stream.\"\"\"\n\n    interceptionId: str\n\n\nclass SetRequestInterceptionParams(TypedDict):\n    \"\"\"Parameters for setting request interception patterns.\"\"\"\n\n    patterns: list[RequestPattern]\n\n\nclass AuthChallengeResponseParams(TypedDict):\n    \"\"\"Parameters for responding to an auth challenge.\"\"\"\n\n    response: str\n    username: NotRequired[str]\n    password: NotRequired[str]\n\n\nclass EnableReportingApiParams(TypedDict):\n    \"\"\"Parameters for enabling Reporting API.\"\"\"\n\n    enabled: bool\n\n\nclass GetSecurityIsolationStatusParams(TypedDict):\n    frameId: NotRequired[str]\n\n\nclass LoadNetworkResourceParams(TypedDict):\n    \"\"\"Parameters for loading a network resource.\"\"\"\n\n    url: str\n    options: LoadNetworkResourceOptions\n    frameId: NotRequired[str]\n\n\nclass ReplayXHRParams(TypedDict):\n    \"\"\"Parameters for replaying an XMLHttpRequest.\"\"\"\n\n    requestId: str\n\n\nclass GetCookiesResult(TypedDict):\n    \"\"\"Response result for getCookies command.\"\"\"\n\n    cookies: list[Cookie]\n\n\nclass GetRequestPostDataResult(TypedDict):\n    \"\"\"Response result for getRequestPostData command.\"\"\"\n\n    postData: str\n\n\nclass GetResponseBodyResult(TypedDict):\n    \"\"\"Response result for getResponseBody command.\"\"\"\n\n    body: str\n    base64Encoded: bool\n\n\nclass GetResponseBodyForInterceptionResult(TypedDict):\n    \"\"\"Response result for getResponseBodyForInterception command.\"\"\"\n\n    body: str\n    base64Encoded: bool\n\n\nclass GetCertificateResult(TypedDict):\n    \"\"\"Response result for getCertificate command.\"\"\"\n\n    tableNames: list[str]\n\n\nclass SearchInResponseBodyResult(TypedDict):\n    \"\"\"Response result for searchInResponseBody command.\"\"\"\n\n    result: list[SearchMatch]\n\n\nclass SetCookieResult(TypedDict):\n    \"\"\"Response result for setCookie command.\"\"\"\n\n    success: bool\n\n\nclass StreamResourceContentResult(TypedDict):\n    \"\"\"Response result for streamResourceContent command.\"\"\"\n\n    bufferedData: str\n\n\nclass TakeResponseBodyForInterceptionAsStreamResult(TypedDict):\n    \"\"\"Response result for takeResponseBodyForInterceptionAsStream command.\"\"\"\n\n    stream: str\n\n\nclass CanClearBrowserCacheResult(TypedDict):\n    \"\"\"Response result for canClearBrowserCache command.\"\"\"\n\n    result: bool\n\n\nclass CanClearBrowserCookiesResult(TypedDict):\n    \"\"\"Response result for canClearBrowserCookies command.\"\"\"\n\n    result: bool\n\n\nclass CanEmulateNetworkConditionsResult(TypedDict):\n    \"\"\"Response result for canEmulateNetworkConditions command.\"\"\"\n\n    result: bool\n\n\nclass GetSecurityIsolationStatusResult(TypedDict):\n    \"\"\"Response result for getSecurityIsolationStatus command.\"\"\"\n\n    status: SecurityIsolationStatus\n\n\nclass LoadNetworkResourceResult(TypedDict):\n    \"\"\"Response result for loadNetworkResource command.\"\"\"\n\n    success: bool\n    netError: NotRequired[float]\n    netErrorName: NotRequired[str]\n    httpStatusCode: NotRequired[float]\n    stream: NotRequired[str]\n    headers: NotRequired[list[HeaderEntry]]\n\n\nGetCookiesResponse = Response[GetCookiesResult]\nSetCookieResponse = Response[SetCookieResult]\nGetRequestPostDataResponse = Response[GetRequestPostDataResult]\nGetResponseBodyResponse = Response[GetResponseBodyResult]\nGetResponseBodyForInterceptionResponse = Response[GetResponseBodyForInterceptionResult]\nSearchInResponseBodyResponse = Response[SearchInResponseBodyResult]\nStreamResourceContentResponse = Response[StreamResourceContentResult]\nTakeResponseBodyForInterceptionAsStreamResponse = Response[\n    TakeResponseBodyForInterceptionAsStreamResult\n]\nGetCertificateResponse = Response[GetCertificateResult]\nCanClearBrowserCacheResponse = Response[CanClearBrowserCacheResult]\nCanClearBrowserCookiesResponse = Response[CanClearBrowserCookiesResult]\nCanEmulateNetworkConditionsResponse = Response[CanEmulateNetworkConditionsResult]\nGetSecurityIsolationStatusResponse = Response[GetSecurityIsolationStatusResult]\nLoadNetworkResourceResponse = Response[LoadNetworkResourceResult]\n\n\nClearBrowserCacheCommand = Command[EmptyParams, Response[EmptyResponse]]\nClearBrowserCookiesCommand = Command[EmptyParams, Response[EmptyResponse]]\nClearCookiesCommand = Command[DeleteCookiesParams, Response[EmptyResponse]]\nDisableCommand = Command[EmptyParams, Response[EmptyResponse]]\nEmulateNetworkConditionsCommand = Command[EmulateNetworkConditionsParams, Response[EmptyResponse]]\nEnableCommand = Command[NetworkEnableParams, Response[EmptyResponse]]\nGetCookiesCommand = Command[GetCookiesParams, GetCookiesResponse]\nGetRequestPostDataCommand = Command[GetRequestPostDataParams, GetRequestPostDataResponse]\nGetResponseBodyCommand = Command[GetResponseBodyParams, GetResponseBodyResponse]\nSetCacheDisabledCommand = Command[SetCacheDisabledParams, Response[EmptyResponse]]\nSetCookieCommand = Command[SetCookieParams, SetCookieResponse]\nSetCookiesCommand = Command[SetCookiesParams, Response[EmptyResponse]]\nSetExtraHTTPHeadersCommand = Command[SetExtraHTTPHeadersParams, Response[EmptyResponse]]\nSetUserAgentOverrideCommand = Command[SetUserAgentOverrideParams, Response[EmptyResponse]]\nClearAcceptedEncodingsOverrideCommand = Command[EmptyParams, Response[EmptyResponse]]\nEnableReportingApiCommand = Command[EnableReportingApiParams, Response[EmptyResponse]]\nSearchInResponseBodyCommand = Command[SearchInResponseBodyParams, SearchInResponseBodyResponse]\nSetBlockedURLsCommand = Command[SetBlockedURLsParams, Response[EmptyResponse]]\nSetBypassServiceWorkerCommand = Command[SetBypassServiceWorkerParams, Response[EmptyResponse]]\nGetCertificateCommand = Command[GetCertificateParams, GetCertificateResponse]\nGetResponseBodyForInterceptionCommand = Command[\n    GetResponseBodyForInterceptionParams, GetResponseBodyForInterceptionResponse\n]\nSetAcceptedEncodingsCommand = Command[SetAcceptedEncodingsParams, Response[EmptyResponse]]\nSetAttachDebugStackCommand = Command[SetAttachDebugStackParams, Response[EmptyResponse]]\nSetCookieControlsCommand = Command[SetCookieControlsParams, Response[EmptyResponse]]\nStreamResourceContentCommand = Command[StreamResourceContentParams, StreamResourceContentResponse]\nTakeResponseBodyForInterceptionAsStreamCommand = Command[\n    TakeResponseBodyForInterceptionAsStreamParams, TakeResponseBodyForInterceptionAsStreamResponse\n]\nGetSecurityIsolationStatusCommand = Command[\n    GetSecurityIsolationStatusParams, GetSecurityIsolationStatusResponse\n]\nLoadNetworkResourceCommand = Command[LoadNetworkResourceParams, LoadNetworkResourceResponse]\nReplayXHRCommand = Command[ReplayXHRParams, Response[EmptyResponse]]\n"
  },
  {
    "path": "pydoll/protocol/network/types.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.runtime.types import StackTrace\nfrom pydoll.protocol.security.types import MixedContentType, SecurityState\n\n\nclass ResourceType(str, Enum):\n    \"\"\"Resource type as it was perceived by the rendering engine.\"\"\"\n\n    DOCUMENT = 'Document'\n    STYLESHEET = 'Stylesheet'\n    IMAGE = 'Image'\n    MEDIA = 'Media'\n    FONT = 'Font'\n    SCRIPT = 'Script'\n    TEXT_TRACK = 'TextTrack'\n    XHR = 'XHR'\n    FETCH = 'Fetch'\n    PREFETCH = 'Prefetch'\n    EVENT_SOURCE = 'EventSource'\n    WEB_SOCKET = 'WebSocket'\n    MANIFEST = 'Manifest'\n    SIGNED_EXCHANGE = 'SignedExchange'\n    PING = 'Ping'\n    CSP_VIOLATION_REPORT = 'CSPViolationReport'\n    PREFLIGHT = 'Preflight'\n    FED_CM = 'FedCM'\n    OTHER = 'Other'\n\n\nLoaderId = str\nRequestId = str\nInterceptionId = str\n\n\nclass ErrorReason(str, Enum):\n    \"\"\"Network level fetch failure reason.\"\"\"\n\n    FAILED = 'Failed'\n    ABORTED = 'Aborted'\n    TIMED_OUT = 'TimedOut'\n    ACCESS_DENIED = 'AccessDenied'\n    CONNECTION_CLOSED = 'ConnectionClosed'\n    CONNECTION_RESET = 'ConnectionReset'\n    CONNECTION_REFUSED = 'ConnectionRefused'\n    CONNECTION_ABORTED = 'ConnectionAborted'\n    CONNECTION_FAILED = 'ConnectionFailed'\n    NAME_NOT_RESOLVED = 'NameNotResolved'\n    INTERNET_DISCONNECTED = 'InternetDisconnected'\n    ADDRESS_UNREACHABLE = 'AddressUnreachable'\n    BLOCKED_BY_CLIENT = 'BlockedByClient'\n    BLOCKED_BY_RESPONSE = 'BlockedByResponse'\n\n\nTimeSinceEpoch = float\nMonotonicTime = float\nHeaders = dict[str, str]\n\n\nclass RequestMethod(str, Enum):\n    \"\"\"HTTP request method.\"\"\"\n\n    GET = 'GET'\n    POST = 'POST'\n    PUT = 'PUT'\n    DELETE = 'DELETE'\n    PATCH = 'PATCH'\n\n\nclass ConnectionType(str, Enum):\n    \"\"\"The underlying connection technology that the browser is supposedly using.\"\"\"\n\n    NONE = 'none'\n    CELLULAR2G = 'cellular2g'\n    CELLULAR3G = 'cellular3g'\n    CELLULAR4G = 'cellular4g'\n    BLUETOOTH = 'bluetooth'\n    ETHERNET = 'ethernet'\n    WIFI = 'wifi'\n    WIMAX = 'wimax'\n    OTHER = 'other'\n\n\nclass CookieSameSite(str, Enum):\n    \"\"\"Represents the cookie's 'SameSite' status\"\"\"\n\n    STRICT = 'Strict'\n    LAX = 'Lax'\n    NONE = 'None'\n\n\nclass CookiePriority(str, Enum):\n    \"\"\"Represents the cookie's 'Priority' status\"\"\"\n\n    LOW = 'Low'\n    MEDIUM = 'Medium'\n    HIGH = 'High'\n\n\nclass CookieSourceScheme(str, Enum):\n    \"\"\"\n    Represents the source scheme of the origin that originally set the cookie.\n    A value of \"Unset\" allows protocol clients to emulate legacy cookie scope for the scheme.\n    This is a temporary ability and it will be removed in the future.\"\"\"\n\n    UNSET = 'Unset'\n    NON_SECURE = 'NonSecure'\n    SECURE = 'Secure'\n\n\nclass ResourceTiming(TypedDict):\n    \"\"\"Timing information for the request.\"\"\"\n\n    requestTime: float\n    proxyStart: float\n    proxyEnd: float\n    dnsStart: float\n    dnsEnd: float\n    connectStart: float\n    connectEnd: float\n    sslStart: float\n    sslEnd: float\n    workerStart: float\n    workerReady: float\n    workerFetchStart: float\n    workerRespondWithSettled: float\n    workerRouterEvaluationStart: NotRequired[float]\n    workerCacheLookupStart: NotRequired[float]\n    sendStart: float\n    sendEnd: float\n    pushStart: float\n    pushEnd: float\n    receiveHeadersStart: float\n    receiveHeadersEnd: float\n\n\nclass ResourcePriority(str, Enum):\n    \"\"\"Loading priority of a resource request.\"\"\"\n\n    VERY_LOW = 'VeryLow'\n    LOW = 'Low'\n    MEDIUM = 'Medium'\n    HIGH = 'High'\n    VERY_HIGH = 'VeryHigh'\n\n\nclass PostDataEntry(TypedDict):\n    \"\"\"Post data entry for HTTP request\"\"\"\n\n    bytes: NotRequired[str]\n\n\nclass Request(TypedDict):\n    \"\"\"HTTP request data.\"\"\"\n\n    url: str\n    urlFragment: NotRequired[str]\n    method: str\n    headers: 'Headers'\n    postData: NotRequired[str]\n    hasPostData: NotRequired[bool]\n    postDataEntries: NotRequired[list['PostDataEntry']]\n    mixedContentType: NotRequired['MixedContentType']\n    initialPriority: 'ResourcePriority'\n    referrerPolicy: str\n    isLinkPreload: NotRequired[bool]\n    trustTokenParams: NotRequired['TrustTokenParams']\n    isSameSite: NotRequired[bool]\n\n\nclass SignedCertificateTimestamp(TypedDict):\n    \"\"\"Details of a signed certificate timestamp (SCT).\"\"\"\n\n    status: str\n    origin: str\n    logDescription: str\n    logId: str\n    timestamp: float\n    hashAlgorithm: str\n    signatureAlgorithm: str\n    signatureData: str\n\n\nclass SecurityDetails(TypedDict):\n    \"\"\"Security details about a request.\"\"\"\n\n    protocol: str\n    keyExchange: str\n    keyExchangeGroup: NotRequired[str]\n    cipher: str\n    mac: NotRequired[str]\n    certificateId: int\n    subjectName: str\n    sanList: list[str]\n    issuer: str\n    validFrom: 'TimeSinceEpoch'\n    validTo: 'TimeSinceEpoch'\n    signedCertificateTimestampList: list['SignedCertificateTimestamp']\n    certificateTransparencyCompliance: 'CertificateTransparencyCompliance'\n    serverSignatureAlgorithm: NotRequired[int]\n    encryptedClientHello: bool\n\n\nclass CertificateTransparencyCompliance(str, Enum):\n    \"\"\"Whether the request complied with Certificate Transparency policy.\"\"\"\n\n    UNKNOWN = 'unknown'\n    NOT_COMPLIANT = 'not-compliant'\n    COMPLIANT = 'compliant'\n\n\nclass BlockedReason(str, Enum):\n    \"\"\"The reason why request was blocked.\"\"\"\n\n    OTHER = 'other'\n    CSP = 'csp'\n    MIXED_CONTENT = 'mixed-content'\n    ORIGIN = 'origin'\n    INSPECTOR = 'inspector'\n    INTEGRITY = 'integrity'\n    SUBRESOURCE_FILTER = 'subresource-filter'\n    CONTENT_TYPE = 'content-type'\n    COEP_FRAME_RESOURCE_NEEDS_COEP_HEADER = 'coep-frame-resource-needs-coep-header'\n    COOP_SANDBOXED_IFRAME_CANNOT_NAVIGATE_TO_COOP_PAGE = (\n        'coop-sandboxed-iframe-cannot-navigate-to-coop-page'\n    )\n    CORP_NOT_SAME_ORIGIN = 'corp-not-same-origin'\n    CORP_NOT_SAME_ORIGIN_AFTER_DEFAULTED_TO_SAME_ORIGIN_BY_COEP = (\n        'corp-not-same-origin-after-defaulted-to-same-origin-by-coep'\n    )\n    CORP_NOT_SAME_ORIGIN_AFTER_DEFAULTED_TO_SAME_ORIGIN_BY_DIP = (\n        'corp-not-same-origin-after-defaulted-to-same-origin-by-dip'\n    )\n    CORP_NOT_SAME_ORIGIN_AFTER_DEFAULTED_TO_SAME_ORIGIN_BY_COEP_AND_DIP = (\n        'corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip'\n    )\n    CORP_NOT_SAME_SITE = 'corp-not-same-site'\n    SRI_MESSAGE_SIGNATURE_MISMATCH = 'sri-message-signature-mismatch'\n\n\nclass CorsError(str, Enum):\n    \"\"\"The reason why request was blocked.\"\"\"\n\n    DISALLOWED_BY_MODE = 'DisallowedByMode'\n    INVALID_RESPONSE = 'InvalidResponse'\n    WILDCARD_ORIGIN_NOT_ALLOWED = 'WildcardOriginNotAllowed'\n    MISSING_ALLOW_ORIGIN_HEADER = 'MissingAllowOriginHeader'\n    MULTIPLE_ALLOW_ORIGIN_VALUES = 'MultipleAllowOriginValues'\n    INVALID_ALLOW_ORIGIN_VALUE = 'InvalidAllowOriginValue'\n    ALLOW_ORIGIN_MISMATCH = 'AllowOriginMismatch'\n    INVALID_ALLOW_CREDENTIALS = 'InvalidAllowCredentials'\n    CORS_DISABLED_SCHEME = 'CorsDisabledScheme'\n    PREFLIGHT_INVALID_STATUS = 'PreflightInvalidStatus'\n    PREFLIGHT_DISALLOWED_REDIRECT = 'PreflightDisallowedRedirect'\n    PREFLIGHT_WILDCARD_ORIGIN_NOT_ALLOWED = 'PreflightWildcardOriginNotAllowed'\n    PREFLIGHT_MISSING_ALLOW_ORIGIN_HEADER = 'PreflightMissingAllowOriginHeader'\n    PREFLIGHT_MULTIPLE_ALLOW_ORIGIN_VALUES = 'PreflightMultipleAllowOriginValues'\n    PREFLIGHT_INVALID_ALLOW_ORIGIN_VALUE = 'PreflightInvalidAllowOriginValue'\n    PREFLIGHT_ALLOW_ORIGIN_MISMATCH = 'PreflightAllowOriginMismatch'\n    PREFLIGHT_INVALID_ALLOW_CREDENTIALS = 'PreflightInvalidAllowCredentials'\n    PREFLIGHT_MISSING_ALLOW_EXTERNAL = 'PreflightMissingAllowExternal'\n    PREFLIGHT_INVALID_ALLOW_EXTERNAL = 'PreflightInvalidAllowExternal'\n    PREFLIGHT_MISSING_ALLOW_PRIVATE_NETWORK = 'PreflightMissingAllowPrivateNetwork'\n    PREFLIGHT_INVALID_ALLOW_PRIVATE_NETWORK = 'PreflightInvalidAllowPrivateNetwork'\n    INVALID_ALLOW_METHODS_PREFLIGHT_RESPONSE = 'InvalidAllowMethodsPreflightResponse'\n    INVALID_ALLOW_HEADERS_PREFLIGHT_RESPONSE = 'InvalidAllowHeadersPreflightResponse'\n    METHOD_DISALLOWED_BY_PREFLIGHT_RESPONSE = 'MethodDisallowedByPreflightResponse'\n    HEADER_DISALLOWED_BY_PREFLIGHT_RESPONSE = 'HeaderDisallowedByPreflightResponse'\n    REDIRECT_CONTAINS_CREDENTIALS = 'RedirectContainsCredentials'\n    INSECURE_PRIVATE_NETWORK = 'InsecurePrivateNetwork'\n    INVALID_PRIVATE_NETWORK_ACCESS = 'InvalidPrivateNetworkAccess'\n    UNEXPECTED_PRIVATE_NETWORK_ACCESS = 'UnexpectedPrivateNetworkAccess'\n    NO_CORS_REDIRECT_MODE_NOT_FOLLOW = 'NoCorsRedirectModeNotFollow'\n    PREFLIGHT_MISSING_PRIVATE_NETWORK_ACCESS_ID = 'PreflightMissingPrivateNetworkAccessId'\n    PREFLIGHT_MISSING_PRIVATE_NETWORK_ACCESS_NAME = 'PreflightMissingPrivateNetworkAccessName'\n    PRIVATE_NETWORK_ACCESS_PERMISSION_UNAVAILABLE = 'PrivateNetworkAccessPermissionUnavailable'\n    PRIVATE_NETWORK_ACCESS_PERMISSION_DENIED = 'PrivateNetworkAccessPermissionDenied'\n    LOCAL_NETWORK_ACCESS_PERMISSION_DENIED = 'LocalNetworkAccessPermissionDenied'\n\n\nclass CorsErrorStatus(TypedDict):\n    corsError: CorsError\n    failedParameter: str\n\n\nclass ServiceWorkerResponseSource(str, Enum):\n    \"\"\"Source of serviceworker response.\"\"\"\n\n    CACHE_STORAGE = 'cache-storage'\n    HTTP_CACHE = 'http-cache'\n    FALLBACK_CODE = 'fallback-code'\n    NETWORK = 'network'\n\n\nclass TrustTokenParams(TypedDict):\n    \"\"\"\n    Determines what type of Trust Token operation is executed and depending on the type,\n    some additional parameters. The values are specified in\n    third_party/blink/renderer/core/fetch/trust_token.idl.\n    \"\"\"\n\n    operation: 'TrustTokenOperationType'\n    refreshPolicy: str\n    issuers: NotRequired[list[str]]\n\n\nclass TrustTokenOperationType(str, Enum):\n    ISSUANCE = 'Issuance'\n    REDEMPTION = 'Redemption'\n    SIGNING = 'Signing'\n\n\nclass AlternateProtocolUsage(str, Enum):\n    \"\"\"The reason why Chrome uses a specific transport protocol for HTTP semantics.\"\"\"\n\n    ALTERNATIVE_JOB_WON_WITHOUT_RACE = 'alternativeJobWonWithoutRace'\n    ALTERNATIVE_JOB_WON_RACE = 'alternativeJobWonRace'\n    MAIN_JOB_WON_RACE = 'mainJobWonRace'\n    MAPPING_MISSING = 'mappingMissing'\n    BROKEN = 'broken'\n    DNS_ALPN_H3_JOB_WON_WITHOUT_RACE = 'dnsAlpnH3JobWonWithoutRace'\n    DNS_ALPN_H3_JOB_WON_RACE = 'dnsAlpnH3JobWonRace'\n    UNSPECIFIED_REASON = 'unspecifiedReason'\n\n\nclass ServiceWorkerRouterSource(str, Enum):\n    \"\"\"Source of service worker router.\"\"\"\n\n    NETWORK = 'network'\n    CACHE = 'cache'\n    FETCH_EVENT = 'fetch-event'\n    RACE_NETWORK_AND_FETCH_HANDLER = 'race-network-and-fetch-handler'\n\n\nclass ServiceWorkerRouterInfo(TypedDict):\n    ruleIdMatched: NotRequired[int]\n    matchedSourceType: NotRequired['ServiceWorkerRouterSource']\n    actualSourceType: NotRequired['ServiceWorkerRouterSource']\n\n\nclass Response(TypedDict):\n    \"\"\"HTTP response data.\"\"\"\n\n    url: str\n    status: int\n    statusText: str\n    headers: 'Headers'\n    headersText: NotRequired[str]\n    mimeType: str\n    charset: str\n    requestHeaders: NotRequired['Headers']\n    requestHeadersText: NotRequired[str]\n    connectionReused: bool\n    connectionId: float\n    remoteIPAddress: NotRequired[str]\n    remotePort: NotRequired[int]\n    fromDiskCache: NotRequired[bool]\n    fromServiceWorker: NotRequired[bool]\n    fromPrefetchCache: NotRequired[bool]\n    fromEarlyHints: NotRequired[bool]\n    serviceWorkerRouterInfo: NotRequired['ServiceWorkerRouterInfo']\n    encodedDataLength: float\n    timing: NotRequired['ResourceTiming']\n    serviceWorkerResponseSource: NotRequired[ServiceWorkerResponseSource]\n    responseTime: NotRequired['TimeSinceEpoch']\n    cacheStorageCacheName: NotRequired[str]\n    protocol: NotRequired[str]\n    alternateProtocolUsage: NotRequired[AlternateProtocolUsage]\n    securityState: SecurityState\n    securityDetails: NotRequired['SecurityDetails']\n    isIpProtectionUsed: NotRequired[bool]\n\n\nclass WebSocketRequest(TypedDict):\n    \"\"\"WebSocket request data.\"\"\"\n\n    headers: 'Headers'\n\n\nclass WebSocketResponse(TypedDict):\n    \"\"\"WebSocket response data.\"\"\"\n\n    status: int\n    statusText: str\n    headers: 'Headers'\n    headersText: NotRequired[str]\n    requestHeaders: NotRequired['Headers']\n    requestHeadersText: NotRequired[str]\n\n\nclass WebSocketFrame(TypedDict):\n    \"\"\"\n    WebSocket message data. This represents an entire WebSocket message,\n    not just a fragmented frame as the name suggests.\n    \"\"\"\n\n    opcode: float\n    mask: bool\n    payloadData: str\n\n\nclass CachedResource(TypedDict):\n    \"\"\"Information about the cached resource.\"\"\"\n\n    url: str\n    type: ResourceType\n    response: NotRequired['Response']\n    bodySize: float\n\n\nclass Initiator(TypedDict):\n    \"\"\"Information about the request initiator.\"\"\"\n\n    type: str\n    stack: NotRequired[StackTrace]\n    url: NotRequired[str]\n    lineNumber: NotRequired[float]\n    columnNumber: NotRequired[float]\n    requestId: NotRequired[RequestId]\n\n\nclass CookiePartitionKey(TypedDict):\n    \"\"\"\n    cookiePartitionKey object. The representation of the components of the key that are created\n    by the cookiePartitionKey class contained in net/cookies/cookie_partition_key.h.\n    \"\"\"\n\n    topLevelSite: str\n    hasCrossSiteAncestor: bool\n\n\nclass Cookie(TypedDict):\n    \"\"\"Cookie object\"\"\"\n\n    name: str\n    value: str\n    domain: str\n    path: str\n    expires: float\n    size: int\n    httpOnly: bool\n    secure: bool\n    session: bool\n    sameSite: NotRequired[CookieSameSite]\n    priority: NotRequired[CookiePriority]\n    sameParty: NotRequired[bool]\n    sourceScheme: NotRequired[CookieSourceScheme]\n    sourcePort: int\n    partitionKey: NotRequired['CookiePartitionKey']\n\n\nclass SetCookieBlockedReason(str, Enum):\n    \"\"\"Types of reasons why a cookie may not be stored from a response.\"\"\"\n\n    SECURE_ONLY = 'SecureOnly'\n    SAME_SITE_STRICT = 'SameSiteStrict'\n    SAME_SITE_LAX = 'SameSiteLax'\n    SAME_SITE_UNSPECIFIED_TREATED_AS_LAX = 'SameSiteUnspecifiedTreatedAsLax'\n    SAME_SITE_NONE_INSECURE = 'SameSiteNoneInsecure'\n    USER_PREFERENCES = 'UserPreferences'\n    THIRD_PARTY_PHASEOUT = 'ThirdPartyPhaseout'\n    THIRD_PARTY_BLOCKED_IN_FIRST_PARTY_SET = 'ThirdPartyBlockedInFirstPartySet'\n    SYNTAX_ERROR = 'SyntaxError'\n    SCHEME_NOT_SUPPORTED = 'SchemeNotSupported'\n    OVERWRITE_SECURE = 'OverwriteSecure'\n    INVALID_DOMAIN = 'InvalidDomain'\n    INVALID_PREFIX = 'InvalidPrefix'\n    UNKNOWN_ERROR = 'UnknownError'\n    SCHEMEFUL_SAME_SITE_STRICT = 'SchemefulSameSiteStrict'\n    SCHEMEFUL_SAME_SITE_LAX = 'SchemefulSameSiteLax'\n    SCHEMEFUL_SAME_SITE_UNSPECIFIED_TREATED_AS_LAX = 'SchemefulSameSiteUnspecifiedTreatedAsLax'\n    SAME_PARTY_FROM_CROSS_PARTY_CONTEXT = 'SamePartyFromCrossPartyContext'\n    SAME_PARTY_CONFLICTS_WITH_OTHER_ATTRIBUTES = 'SamePartyConflictsWithOtherAttributes'\n    NAME_VALUE_PAIR_EXCEEDS_MAX_SIZE = 'NameValuePairExceedsMaxSize'\n    DISALLOWED_CHARACTER = 'DisallowedCharacter'\n    NO_COOKIE_CONTENT = 'NoCookieContent'\n\n\nclass CookieBlockedReason(str, Enum):\n    \"\"\"Types of reasons why a cookie may not be sent with a request.\"\"\"\n\n    SECURE_ONLY = 'SecureOnly'\n    NOT_ON_PATH = 'NotOnPath'\n    DOMAIN_MISMATCH = 'DomainMismatch'\n    SAME_SITE_STRICT = 'SameSiteStrict'\n    SAME_SITE_LAX = 'SameSiteLax'\n    SAME_SITE_UNSPECIFIED_TREATED_AS_LAX = 'SameSiteUnspecifiedTreatedAsLax'\n    SAME_SITE_NONE_INSECURE = 'SameSiteNoneInsecure'\n    USER_PREFERENCES = 'UserPreferences'\n    THIRD_PARTY_PHASEOUT = 'ThirdPartyPhaseout'\n    THIRD_PARTY_BLOCKED_IN_FIRST_PARTY_SET = 'ThirdPartyBlockedInFirstPartySet'\n    UNKNOWN_ERROR = 'UnknownError'\n    SCHEMEFUL_SAME_SITE_STRICT = 'SchemefulSameSiteStrict'\n    SCHEMEFUL_SAME_SITE_LAX = 'SchemefulSameSiteLax'\n    SCHEMEFUL_SAME_SITE_UNSPECIFIED_TREATED_AS_LAX = 'SchemefulSameSiteUnspecifiedTreatedAsLax'\n    SAME_PARTY_FROM_CROSS_PARTY_CONTEXT = 'SamePartyFromCrossPartyContext'\n    NAME_VALUE_PAIR_EXCEEDS_MAX_SIZE = 'NameValuePairExceedsMaxSize'\n    PORT_MISMATCH = 'PortMismatch'\n    SCHEME_MISMATCH = 'SchemeMismatch'\n    ANONYMOUS_CONTEXT = 'AnonymousContext'\n\n\nclass CookieExemptionReason(str, Enum):\n    \"\"\"\n    Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request.\n    \"\"\"\n\n    NONE = 'None'\n    USER_SETTING = 'UserSetting'\n    TPCD_METADATA = 'TPCDMetadata'\n    TPCD_DEPRECATION_TRIAL = 'TPCDDeprecationTrial'\n    TOP_LEVEL_TPCD_DEPRECATION_TRIAL = 'TopLevelTPCDDeprecationTrial'\n    TPCD_HEURISTICS = 'TPCDHeuristics'\n    ENTERPRISE_POLICY = 'EnterprisePolicy'\n    STORAGE_ACCESS = 'StorageAccess'\n    TOP_LEVEL_STORAGE_ACCESS = 'TopLevelStorageAccess'\n    SCHEME = 'Scheme'\n    SAME_SITE_NONE_COOKIES_IN_SANDBOX = 'SameSiteNoneCookiesInSandbox'\n\n\nclass BlockedSetCookieWithReason(TypedDict):\n    \"\"\"A cookie which was not stored from a response with the corresponding reason.\"\"\"\n\n    blockedReasons: list[SetCookieBlockedReason]\n    cookieLine: str\n    cookie: NotRequired['Cookie']\n\n\nclass ExemptedSetCookieWithReason(TypedDict):\n    \"\"\"\n    A cookie should have been blocked by 3PCD but is exempted and stored from a response with\n    the corresponding reason. A cookie could only have at most one exemption reason.\n    \"\"\"\n\n    exemptionReason: CookieExemptionReason\n    cookieLine: str\n    cookie: 'Cookie'\n\n\nclass AssociatedCookie(TypedDict):\n    \"\"\"\n    A cookie associated with the request which may or may not be sent with it.\n    Includes the cookies itself and reasons for blocking or exemption.\n    \"\"\"\n\n    cookie: 'Cookie'\n    blockedReasons: list[CookieBlockedReason]\n    exemptionReason: NotRequired[CookieExemptionReason]\n\n\nclass CookieParam(TypedDict):\n    \"\"\"Cookie parameter object\"\"\"\n\n    name: str\n    value: str\n    url: NotRequired[str]\n    domain: NotRequired[str]\n    path: NotRequired[str]\n    secure: NotRequired[bool]\n    httpOnly: NotRequired[bool]\n    sameSite: NotRequired[CookieSameSite]\n    expires: NotRequired['TimeSinceEpoch']\n    priority: NotRequired[CookiePriority]\n    sameParty: NotRequired[bool]\n    sourceScheme: NotRequired[CookieSourceScheme]\n    sourcePort: NotRequired[int]\n    partitionKey: NotRequired['CookiePartitionKey']\n\n\nclass AuthChallenge(TypedDict):\n    \"\"\"Authorization challenge for HTTP status code 401 or 407.\"\"\"\n\n    source: NotRequired[str]\n    origin: str\n    scheme: str\n    realm: str\n\n\nclass AuthChallengeResponse(TypedDict):\n    \"\"\"Response to an AuthChallenge.\"\"\"\n\n    response: str\n    username: NotRequired[str]\n    password: NotRequired[str]\n\n\nclass InterceptionStage(str, Enum):\n    \"\"\"\n    Stages of the interception to begin intercepting. Request will intercept before the request\n    is sent. Response will intercept after the response is received.\n    \"\"\"\n\n    REQUEST = 'Request'\n    HEADERS_RECEIVED = 'HeadersReceived'\n\n\nclass RequestPattern(TypedDict):\n    \"\"\"Request pattern for interception.\"\"\"\n\n    urlPattern: NotRequired[str]\n    resourceType: NotRequired[ResourceType]\n    interceptionStage: NotRequired[InterceptionStage]\n\n\nclass SignedExchangeSignature(TypedDict):\n    \"\"\"Information about a signed exchange signature.\"\"\"\n\n    label: str\n    signature: str\n    integrity: str\n    certUrl: NotRequired[str]\n    certSha256: NotRequired[str]\n    validityUrl: str\n    date: int\n    expires: int\n    certificates: NotRequired[list[str]]\n\n\nclass SignedExchangeHeader(TypedDict):\n    \"\"\"Information about a signed exchange header.\"\"\"\n\n    requestUrl: str\n    responseCode: int\n    responseHeaders: 'Headers'\n    signatures: list[SignedExchangeSignature]\n    headerIntegrity: str\n\n\nclass SignedExchangeErrorField(str, Enum):\n    \"\"\"Field type for a signed exchange related error.\"\"\"\n\n    SIGNATURE_SIG = 'signatureSig'\n    SIGNATURE_INTEGRITY = 'signatureIntegrity'\n    SIGNATURE_CERT_URL = 'signatureCertUrl'\n    SIGNATURE_CERT_SHA256 = 'signatureCertSha256'\n    SIGNATURE_VALIDITY_URL = 'signatureValidityUrl'\n    SIGNATURE_TIMESTAMPS = 'signatureTimestamps'\n\n\nclass SignedExchangeError(TypedDict):\n    \"\"\"Information about a signed exchange response.\"\"\"\n\n    message: str\n    signatureIndex: NotRequired[int]\n    errorField: NotRequired[SignedExchangeErrorField]\n\n\nclass SignedExchangeInfo(TypedDict):\n    \"\"\"Information about a signed exchange response.\"\"\"\n\n    outerResponse: 'Response'\n    hasExtraInfo: bool\n    header: NotRequired[SignedExchangeHeader]\n    securityDetails: NotRequired['SecurityDetails']\n    errors: NotRequired[list[SignedExchangeError]]\n\n\nclass ContentEncoding(str, Enum):\n    \"\"\"List of content encodings supported by the backend.\"\"\"\n\n    DEFLATE = 'deflate'\n    GZIP = 'gzip'\n    BR = 'br'\n    ZSTD = 'zstd'\n\n\nclass DirectSocketDnsQueryType(str, Enum):\n    IPV4 = 'ipv4'\n    IPV6 = 'ipv6'\n\n\nclass DirectTCPSocketOptions(TypedDict):\n    noDelay: bool\n    keepAliveDelay: NotRequired[float]\n    sendBufferSize: NotRequired[float]\n    receiveBufferSize: NotRequired[float]\n    dnsQueryType: NotRequired[DirectSocketDnsQueryType]\n\n\nclass DirectUDPSocketOptions(TypedDict):\n    remoteAddr: NotRequired[str]\n    remotePort: NotRequired[int]\n    localAddr: NotRequired[str]\n    localPort: NotRequired[int]\n    dnsQueryType: NotRequired[DirectSocketDnsQueryType]\n    sendBufferSize: NotRequired[float]\n    receiveBufferSize: NotRequired[float]\n\n\nclass DirectUDPMessage(TypedDict):\n    data: str\n    remoteAddr: NotRequired[str]\n    remotePort: NotRequired[int]\n\n\nclass PrivateNetworkRequestPolicy(str, Enum):\n    ALLOW = 'Allow'\n    BLOCK_FROM_INSECURE_TO_MORE_PRIVATE = 'BlockFromInsecureToMorePrivate'\n    WARN_FROM_INSECURE_TO_MORE_PRIVATE = 'WarnFromInsecureToMorePrivate'\n    PREFLIGHT_BLOCK = 'PreflightBlock'\n    PREFLIGHT_WARN = 'PreflightWarn'\n\n\nclass IPAddressSpace(str, Enum):\n    LOOPBACK = 'Loopback'\n    LOCAL = 'Local'\n    PUBLIC = 'Public'\n    UNKNOWN = 'Unknown'\n\n\nclass ConnectTiming(TypedDict):\n    requestTime: float\n\n\nclass ClientSecurityState(TypedDict):\n    initiatorIsSecureContext: bool\n    initiatorIPAddressSpace: IPAddressSpace\n    privateNetworkRequestPolicy: PrivateNetworkRequestPolicy\n\n\nclass CrossOriginOpenerPolicyValue(str, Enum):\n    SAME_ORIGIN = 'SameOrigin'\n    SAME_ORIGIN_ALLOW_POPUPS = 'SameOriginAllowPopups'\n    RESTRICT_PROPERTIES = 'RestrictProperties'\n    UNSAFE_NONE = 'UnsafeNone'\n    SAME_ORIGIN_PLUS_COEP = 'SameOriginPlusCoep'\n    RESTRICT_PROPERTIES_PLUS_COEP = 'RestrictPropertiesPlusCoep'\n    NO_OPENER_ALLOW_POPUPS = 'NoopenerAllowPopups'\n\n\nclass CrossOriginOpenerPolicyStatus(TypedDict):\n    value: CrossOriginOpenerPolicyValue\n    reportOnlyValue: CrossOriginOpenerPolicyValue\n    reportingEndpoint: NotRequired[str]\n    reportOnlyReportingEndpoint: NotRequired[str]\n\n\nclass CrossOriginEmbedderPolicyValue(str, Enum):\n    NONE = 'None'\n    CREDENTIALLESS = 'Credentialless'\n    REQUIRE_CORP = 'RequireCorp'\n\n\nclass CrossOriginEmbedderPolicyStatus(TypedDict):\n    value: CrossOriginEmbedderPolicyValue\n    reportOnlyValue: CrossOriginEmbedderPolicyValue\n    reportingEndpoint: NotRequired[str]\n    reportOnlyReportingEndpoint: NotRequired[str]\n\n\nclass ContentSecurityPolicySource(str, Enum):\n    HTTP = 'HTTP'\n    META = 'Meta'\n\n\nclass ContentSecurityPolicyStatus(TypedDict):\n    effectiveDirectives: str\n    isEnforced: bool\n    source: ContentSecurityPolicySource\n\n\nclass SecurityIsolationStatus(TypedDict):\n    coop: NotRequired[CrossOriginOpenerPolicyStatus]\n    coep: NotRequired[CrossOriginEmbedderPolicyStatus]\n    csp: NotRequired[list[ContentSecurityPolicyStatus]]\n\n\nclass ReportStatus(str, Enum):\n    \"\"\"The status of a Reporting API report.\"\"\"\n\n    QUEUED = 'Queued'\n    PENDING = 'Pending'\n    MARKED_FOR_REMOVAL = 'MarkedForRemoval'\n    SUCCESS = 'Success'\n\n\nclass ReportId(str):\n    pass\n\n\nclass ReportingApiReport(TypedDict):\n    \"\"\"An object representing a report generated by the Reporting API.\"\"\"\n\n    id: ReportId\n    initiatorUrl: str\n    destination: str\n    type: str\n    timestamp: TimeSinceEpoch\n    depth: int\n    completedAttempts: int\n    body: dict\n    status: ReportStatus\n\n\nclass ReportingApiEndpoint(TypedDict):\n    url: str\n    groupName: str\n\n\nclass LoadNetworkResourcePageResult(TypedDict):\n    \"\"\"An object providing the result of a network resource load.\"\"\"\n\n    success: bool\n    netError: NotRequired[float]\n    netErrorName: NotRequired[str]\n    httpStatusCode: NotRequired[float]\n    stream: NotRequired[str]\n    headers: NotRequired['Headers']\n\n\nclass LoadNetworkResourceOptions(TypedDict):\n    \"\"\"An options object that may be extended later to better support CORS, CORB and streaming.\"\"\"\n\n    disableCache: bool\n    includeCredentials: bool\n"
  },
  {
    "path": "pydoll/protocol/page/__init__.py",
    "content": "\"\"\"Page domain implementation.\"\"\"\n"
  },
  {
    "path": "pydoll/protocol/page/events.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import CDPEvent\nfrom pydoll.protocol.dom.types import BackendNodeId\nfrom pydoll.protocol.network.types import LoaderId, MonotonicTime\nfrom pydoll.protocol.page.types import (\n    BackForwardCacheNotRestoredExplanation,\n    BackForwardCacheNotRestoredExplanationTree,\n    ClientNavigationDisposition,\n    ClientNavigationReason,\n    DialogType,\n    Frame,\n    FrameId,\n    NavigationType,\n    ScreencastFrameMetadata,\n)\nfrom pydoll.protocol.runtime.types import StackTrace\n\n\nclass PageEvent(str, Enum):\n    \"\"\"\n    Events from the Page domain of the Chrome DevTools Protocol.\n\n    This enumeration contains the names of Page-related events that can be\n    received from the Chrome DevTools Protocol. These events provide information\n    about page lifecycle, frame navigation, JavaScript dialogs, and other\n    page-related activities.\n    \"\"\"\n\n    DOM_CONTENT_EVENT_FIRED = 'Page.domContentEventFired'\n    \"\"\"\n    Fired when DOMContentLoaded event is fired.\n\n    Args:\n        timestamp (Network.MonotonicTime): Timestamp when the event occurred.\n    \"\"\"\n\n    FILE_CHOOSER_OPENED = 'Page.fileChooserOpened'\n    \"\"\"\n    Emitted only when page.interceptFileChooser is enabled.\n\n    Args:\n        frameId (FrameId): Id of the frame containing input node.\n        mode (str): Input mode. Allowed Values: selectSingle, selectMultiple\n        backendNodeId (DOM.BackendNodeId): Input node id. Only present for file choosers\n            opened via an <input type=\"file\"> element.\n    \"\"\"\n\n    FRAME_ATTACHED = 'Page.frameAttached'\n    \"\"\"\n    Fired when frame has been attached to its parent.\n\n    Args:\n        frameId (FrameId): Id of the frame that has been attached.\n        parentFrameId (FrameId): Parent frame identifier.\n        stack (Runtime.StackTrace): JavaScript stack trace of when frame was attached,\n            only set if frame initiated from script.\n    \"\"\"\n\n    FRAME_DETACHED = 'Page.frameDetached'\n    \"\"\"\n    Fired when frame has been detached from its parent.\n\n    Args:\n        frameId (FrameId): Id of the frame that has been detached.\n        reason (str): Reason why the frame was detached.\n            Allowed Values: remove, swap\n    \"\"\"\n\n    FRAME_NAVIGATED = 'Page.frameNavigated'\n    \"\"\"\n    Fired once navigation of the frame has completed. Frame is now associated with the new loader.\n\n    Args:\n        frame (Frame): Frame object.\n        type (NavigationType): Type of navigation.\n    \"\"\"\n\n    INTERSTITIAL_HIDDEN = 'Page.interstitialHidden'\n    \"\"\"\n    Fired when interstitial page was hidden.\n    \"\"\"\n\n    INTERSTITIAL_SHOWN = 'Page.interstitialShown'\n    \"\"\"\n    Fired when interstitial page was shown.\n    \"\"\"\n\n    JAVASCRIPT_DIALOG_CLOSED = 'Page.javascriptDialogClosed'\n    \"\"\"\n    Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload)\n    has been closed.\n\n    Args:\n        frameId (FrameId): Frame id.\n        result (bool): Whether dialog was confirmed.\n        userInput (str): User input in case of prompt.\n    \"\"\"\n\n    JAVASCRIPT_DIALOG_OPENING = 'Page.javascriptDialogOpening'\n    \"\"\"\n    Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload)\n    is about to open.\n\n    Args:\n        url (str): Frame url.\n        frameId (FrameId): Frame id.\n        message (str): Message that will be displayed by the dialog.\n        type (DialogType): Dialog type.\n        hasBrowserHandler (bool): True if browser is capable showing or acting on the given dialog.\n            When browser has no dialog handler for given target, calling alert while Page domain\n            is engaged will stall the page execution. Execution can be resumed via calling\n            Page.handleJavaScriptDialog.\n        defaultPrompt (str): Default dialog prompt.\n    \"\"\"\n\n    LIFECYCLE_EVENT = 'Page.lifecycleEvent'\n    \"\"\"\n    Fired for lifecycle events (navigation, load, paint, etc) in the current target\n    (including local frames).\n\n    Args:\n        frameId (FrameId): Id of the frame.\n        loaderId (Network.LoaderId): Loader identifier. Empty string if the request is\n            fetched from worker.\n        name (str): Lifecycle event name.\n        timestamp (Network.MonotonicTime): Timestamp when the event occurred.\n    \"\"\"\n\n    LOAD_EVENT_FIRED = 'Page.loadEventFired'\n    \"\"\"\n    Fired when the page load event has fired.\n\n    Args:\n        timestamp (Network.MonotonicTime): Timestamp when the event occurred.\n    \"\"\"\n\n    WINDOW_OPEN = 'Page.windowOpen'\n    \"\"\"\n    Fired when a new window is going to be opened, via window.open(), link click,\n    form submission, etc.\n\n    Args:\n        url (str): The URL for the new window.\n        windowName (str): Window name.\n        windowFeatures (array[str]): An array of enabled window features.\n        userGesture (bool): Whether or not it was triggered by user gesture.\n    \"\"\"\n\n    BACK_FORWARD_CACHE_NOT_USED = 'Page.backForwardCacheNotUsed'\n    \"\"\"\n    Fired for failed bfcache history navigations if BackForwardCache feature is enabled.\n    Do not assume any ordering with the Page.frameNavigated event. This event is fired\n    only for main-frame history navigation where the document changes (non-same-document\n    navigations), when bfcache navigation fails.\n\n    Args:\n        loaderId (Network.LoaderId): The loader id for the associated navigation.\n        frameId (FrameId): The frame id of the associated frame.\n        notRestoredExplanations (array[BackForwardCacheNotRestoredExplanation]): Array of reasons\n            why the page could not be cached. This must not be empty.\n        notRestoredExplanationsTree (BackForwardCacheNotRestoredExplanationTree): Tree structure\n            of reasons why the page could not be cached for each frame.\n    \"\"\"\n\n    COMPILATION_CACHE_PRODUCED = 'Page.compilationCacheProduced'\n    \"\"\"\n    Issued for every compilation cache generated. Is only available if\n    Page.setGenerateCompilationCache is enabled.\n\n    Args:\n        url (str): The URL of the document whose compilation cache was produced.\n        data (str): Base64-encoded data (Encoded as a base64 string when passed over JSON).\n    \"\"\"\n\n    DOCUMENT_OPENED = 'Page.documentOpened'\n    \"\"\"\n    Fired when opening document to write to.\n\n    Args:\n        frame (Frame): Frame object.\n    \"\"\"\n\n    FRAME_REQUESTED_NAVIGATION = 'Page.frameRequestedNavigation'\n    \"\"\"\n    Fired when a renderer-initiated navigation is requested.\n    Navigation may still be cancelled after the event is issued.\n\n    Args:\n        frameId (FrameId): Id of the frame that is being navigated.\n        reason (ClientNavigationReason): The reason for the navigation.\n        url (str): The destination URL for the requested navigation.\n        disposition (ClientNavigationDisposition): The disposition for the navigation.\n    \"\"\"\n\n    FRAME_RESIZED = 'Page.frameResized'\n    \"\"\"\n    Fired when frame has been resized.\n    \"\"\"\n\n    FRAME_STARTED_LOADING = 'Page.frameStartedLoading'\n    \"\"\"\n    Fired when frame has started loading.\n\n    Args:\n        frameId (FrameId): Id of the frame that has started loading.\n    \"\"\"\n\n    FRAME_STARTED_NAVIGATING = 'Page.frameStartedNavigating'\n    \"\"\"\n    Fired when a navigation starts. This event is fired for both renderer-initiated\n    and browser-initiated navigations. For renderer-initiated navigations, the event\n    is fired after frameRequestedNavigation. Navigation may still be cancelled after\n    the event is issued. Multiple events can be fired for a single navigation, for example,\n    when a same-document navigation becomes a cross-document navigation (such as in the\n    case of a frameset).\n\n    Args:\n        frameId (FrameId): ID of the frame that is being navigated.\n        url (str): The URL the navigation started with. The final URL can be different.\n        loaderId (Network.LoaderId): Loader identifier. Even though it is present in case\n            of same-document navigation, the previously committed loaderId would not change\n            unless the navigation changes from a same-document to a cross-document navigation.\n        navigationType (str): Type of navigation.\n            Allowed Values: reload, reloadBypassingCache, restore, restoreWithPost,\n            historySameDocument, historyDifferentDocument, sameDocument, differentDocument\n    \"\"\"\n\n    FRAME_STOPPED_LOADING = 'Page.frameStoppedLoading'\n    \"\"\"\n    Fired when frame has stopped loading.\n\n    Args:\n        frameId (FrameId): Id of the frame that has stopped loading.\n    \"\"\"\n\n    FRAME_SUBTREE_WILL_BE_DETACHED = 'Page.frameSubtreeWillBeDetached'\n    \"\"\"\n    Fired before frame subtree is detached. Emitted before any frame of the subtree\n    is actually detached.\n\n    Args:\n        frameId (FrameId): Id of the frame that is the root of the subtree that will be detached.\n    \"\"\"\n\n    NAVIGATED_WITHIN_DOCUMENT = 'Page.navigatedWithinDocument'\n    \"\"\"\n    Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation.\n\n    Args:\n        frameId (FrameId): Id of the frame.\n        url (str): Frame's new url.\n        navigationType (str): Navigation type.\n            Allowed Values: fragment, historyApi, other\n    \"\"\"\n\n    SCREENCAST_FRAME = 'Page.screencastFrame'\n    \"\"\"\n    Compressed image data requested by the startScreencast.\n\n    Args:\n        data (str): Base64-encoded compressed image.\n        metadata (ScreencastFrameMetadata): Screencast frame metadata.\n        sessionId (int): Frame number.\n    \"\"\"\n\n    SCREENCAST_VISIBILITY_CHANGED = 'Page.screencastVisibilityChanged'\n    \"\"\"\n    Fired when the page with currently enabled screencast was shown or hidden.\n\n    Args:\n        visible (bool): True if the page is visible.\n    \"\"\"\n    DOWNLOAD_WILL_BEGIN = 'Page.downloadWillBegin'\n    DOWNLOAD_PROGRESS = 'Page.downloadProgress'\n\n\nclass DomContentEventFiredEventParams(TypedDict):\n    timestamp: MonotonicTime\n\n\nclass FileChooserOpenedEventParams(TypedDict):\n    frameId: FrameId\n    mode: str\n    backendNodeId: NotRequired[BackendNodeId]\n\n\nclass FrameAttachedEventParams(TypedDict):\n    frameId: FrameId\n    parentFrameId: FrameId\n    stack: NotRequired[StackTrace]\n\n\nclass FrameClearedScheduledNavigationEventParams(TypedDict):\n    frameId: FrameId\n\n\nclass FrameDetachedEventParams(TypedDict):\n    frameId: FrameId\n    reason: str\n\n\nclass FrameSubtreeWillBeDetachedEventParams(TypedDict):\n    frameId: FrameId\n\n\nclass FrameNavigatedEventParams(TypedDict):\n    frame: Frame\n    type: NavigationType\n\n\nclass DocumentOpenedEventParams(TypedDict):\n    frame: Frame\n\n\nclass FrameResizedEventParams(TypedDict):\n    pass\n\n\nclass FrameStartedNavigatingEventParams(TypedDict):\n    frameId: FrameId\n    url: str\n    loaderId: LoaderId\n    navigationType: str\n\n\nclass FrameRequestedNavigationEventParams(TypedDict):\n    frameId: FrameId\n    reason: ClientNavigationReason\n    url: str\n    disposition: ClientNavigationDisposition\n\n\nclass FrameScheduledNavigationEventParams(TypedDict):\n    frameId: FrameId\n    delay: float\n    reason: ClientNavigationReason\n    url: str\n\n\nclass FrameStartedLoadingEventParams(TypedDict):\n    frameId: FrameId\n\n\nclass FrameStoppedLoadingEventParams(TypedDict):\n    frameId: FrameId\n\n\nclass DownloadWillBeginEventParams(TypedDict):\n    frameId: FrameId\n    guid: str\n    url: str\n    suggestedFilename: str\n\n\nclass DownloadProgressEventParams(TypedDict):\n    guid: str\n    totalBytes: float\n    receivedBytes: float\n    state: str\n\n\nclass InterstitialHiddenEventParams(TypedDict):\n    pass\n\n\nclass InterstitialShownEventParams(TypedDict):\n    pass\n\n\nclass JavascriptDialogClosedEventParams(TypedDict):\n    frameId: FrameId\n    result: bool\n    userInput: str\n\n\nclass JavascriptDialogOpeningEventParams(TypedDict):\n    url: str\n    frameId: FrameId\n    message: str\n    type: DialogType\n    hasBrowserHandler: bool\n    defaultPrompt: NotRequired[str]\n\n\nclass LifecycleEventEventParams(TypedDict):\n    frameId: FrameId\n    loaderId: LoaderId\n    name: str\n    timestamp: MonotonicTime\n\n\nclass BackForwardCacheNotUsedEventParams(TypedDict):\n    loaderId: LoaderId\n    frameId: FrameId\n    notRestoredExplanations: list[BackForwardCacheNotRestoredExplanation]\n    notRestoredExplanationsTree: NotRequired[BackForwardCacheNotRestoredExplanationTree]\n\n\nclass LoadEventFiredEventParams(TypedDict):\n    timestamp: MonotonicTime\n\n\nclass NavigatedWithinDocumentEventParams(TypedDict):\n    frameId: FrameId\n    url: str\n    navigationType: str\n\n\nclass ScreencastFrameEventParams(TypedDict):\n    data: str\n    metadata: ScreencastFrameMetadata\n    sessionId: int\n\n\nclass ScreencastVisibilityChangedEventParams(TypedDict):\n    visible: bool\n\n\nclass WindowOpenEventParams(TypedDict):\n    url: str\n    windowName: str\n    windowFeatures: list[str]\n    userGesture: bool\n\n\nclass CompilationCacheProducedEventParams(TypedDict):\n    url: str\n    data: str\n\n\nDomContentEventFiredEvent = CDPEvent[DomContentEventFiredEventParams]\nFileChooserOpenedEvent = CDPEvent[FileChooserOpenedEventParams]\nFrameAttachedEvent = CDPEvent[FrameAttachedEventParams]\nFrameClearedScheduledNavigationEvent = CDPEvent[FrameClearedScheduledNavigationEventParams]\nFrameDetachedEvent = CDPEvent[FrameDetachedEventParams]\nFrameSubtreeWillBeDetachedEvent = CDPEvent[FrameSubtreeWillBeDetachedEventParams]\nFrameNavigatedEvent = CDPEvent[FrameNavigatedEventParams]\nDocumentOpenedEvent = CDPEvent[DocumentOpenedEventParams]\nFrameResizedEvent = CDPEvent[FrameResizedEventParams]\nFrameStartedNavigatingEvent = CDPEvent[FrameStartedNavigatingEventParams]\nFrameRequestedNavigationEvent = CDPEvent[FrameRequestedNavigationEventParams]\nFrameScheduledNavigationEvent = CDPEvent[FrameScheduledNavigationEventParams]\nFrameStartedLoadingEvent = CDPEvent[FrameStartedLoadingEventParams]\nFrameStoppedLoadingEvent = CDPEvent[FrameStoppedLoadingEventParams]\nDownloadWillBeginEvent = CDPEvent[DownloadWillBeginEventParams]\nDownloadProgressEvent = CDPEvent[DownloadProgressEventParams]\nInterstitialHiddenEvent = CDPEvent[InterstitialHiddenEventParams]\nInterstitialShownEvent = CDPEvent[InterstitialShownEventParams]\nJavascriptDialogClosedEvent = CDPEvent[JavascriptDialogClosedEventParams]\nJavascriptDialogOpeningEvent = CDPEvent[JavascriptDialogOpeningEventParams]\nLifecycleEventEvent = CDPEvent[LifecycleEventEventParams]\nBackForwardCacheNotUsedEvent = CDPEvent[BackForwardCacheNotUsedEventParams]\nLoadEventFiredEvent = CDPEvent[LoadEventFiredEventParams]\nNavigatedWithinDocumentEvent = CDPEvent[NavigatedWithinDocumentEventParams]\nScreencastFrameEvent = CDPEvent[ScreencastFrameEventParams]\nScreencastVisibilityChangedEvent = CDPEvent[ScreencastVisibilityChangedEventParams]\nWindowOpenEvent = CDPEvent[WindowOpenEventParams]\nCompilationCacheProducedEvent = CDPEvent[CompilationCacheProducedEventParams]\n"
  },
  {
    "path": "pydoll/protocol/page/methods.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import Command, EmptyParams, EmptyResponse, Response\nfrom pydoll.protocol.debugger.types import SearchMatch\nfrom pydoll.protocol.dom.types import Rect\nfrom pydoll.protocol.io.types import StreamHandle\nfrom pydoll.protocol.network.types import LoaderId\nfrom pydoll.protocol.page.types import (\n    AdScriptAncestry,\n    AppManifestError,\n    AppManifestParsedProperties,\n    AutoResponseMode,\n    CompilationCacheParams,\n    FontFamilies,\n    FontSizes,\n    FrameId,\n    FrameResourceTree,\n    FrameTree,\n    InstallabilityError,\n    LayoutViewport,\n    NavigationEntry,\n    OriginTrial,\n    PermissionsPolicyFeatureState,\n    ReferrerPolicy,\n    ScreencastFormat,\n    ScreenshotFormat,\n    ScriptFontFamilies,\n    ScriptIdentifier,\n    TransferMode,\n    TransitionType,\n    Viewport,\n    VisualViewport,\n    WebAppManifest,\n    WebLifecycleState,\n)\nfrom pydoll.protocol.runtime.types import ExecutionContextId\n\n\nclass PageMethod(str, Enum):\n    ADD_SCRIPT_TO_EVALUATE_ON_LOAD = 'Page.addScriptToEvaluateOnLoad'\n    ADD_SCRIPT_TO_EVALUATE_ON_NEW_DOCUMENT = 'Page.addScriptToEvaluateOnNewDocument'\n    BRING_TO_FRONT = 'Page.bringToFront'\n    CAPTURE_SCREENSHOT = 'Page.captureScreenshot'\n    CAPTURE_SNAPSHOT = 'Page.captureSnapshot'\n    CLEAR_COMPILATION_CACHE = 'Page.clearCompilationCache'\n    CLOSE = 'Page.close'\n    CRASH = 'Page.crash'\n    CREATE_ISOLATED_WORLD = 'Page.createIsolatedWorld'\n    DISABLE = 'Page.disable'\n    ENABLE = 'Page.enable'\n    GENERATE_TEST_REPORT = 'Page.generateTestReport'\n    GET_AD_SCRIPT_ANCESTRY_IDS = 'Page.getAdScriptAncestryIds'\n    GET_APP_ID = 'Page.getAppId'\n    GET_APP_MANIFEST = 'Page.getAppManifest'\n    GET_FRAME_TREE = 'Page.getFrameTree'\n    GET_INSTALLABILITY_ERRORS = 'Page.getInstallabilityErrors'\n    GET_LAYOUT_METRICS = 'Page.getLayoutMetrics'\n    GET_MANIFEST_ICONS = 'Page.getManifestIcons'\n    GET_NAVIGATION_HISTORY = 'Page.getNavigationHistory'\n    GET_ORIGIN_TRIALS = 'Page.getOriginTrials'\n    GET_PERMISSIONS_POLICY_STATE = 'Page.getPermissionsPolicyState'\n    GET_RESOURCE_CONTENT = 'Page.getResourceContent'\n    GET_RESOURCE_TREE = 'Page.getResourceTree'\n    HANDLE_JAVASCRIPT_DIALOG = 'Page.handleJavaScriptDialog'\n    NAVIGATE = 'Page.navigate'\n    NAVIGATE_TO_HISTORY_ENTRY = 'Page.navigateToHistoryEntry'\n    PRINT_TO_PDF = 'Page.printToPDF'\n    PRODUCE_COMPILATION_CACHE = 'Page.produceCompilationCache'\n    RELOAD = 'Page.reload'\n    REMOVE_SCRIPT_TO_EVALUATE_ON_LOAD = 'Page.removeScriptToEvaluateOnLoad'\n    REMOVE_SCRIPT_TO_EVALUATE_ON_NEW_DOCUMENT = 'Page.removeScriptToEvaluateOnNewDocument'\n    RESET_NAVIGATION_HISTORY = 'Page.resetNavigationHistory'\n    SCREENCAST_FRAME_ACK = 'Page.screencastFrameAck'\n    SEARCH_IN_RESOURCE = 'Page.searchInResource'\n    SET_AD_BLOCKING_ENABLED = 'Page.setAdBlockingEnabled'\n    SET_BYPASS_CSP = 'Page.setBypassCSP'\n    SET_DOCUMENT_CONTENT = 'Page.setDocumentContent'\n    SET_FONT_FAMILIES = 'Page.setFontFamilies'\n    SET_FONT_SIZES = 'Page.setFontSizes'\n    SET_INTERCEPT_FILE_CHOOSER_DIALOG = 'Page.setInterceptFileChooserDialog'\n    SET_LIFECYCLE_EVENTS_ENABLED = 'Page.setLifecycleEventsEnabled'\n    SET_PRERENDERING_ALLOWED = 'Page.setPrerenderingAllowed'\n    SET_RPH_REGISTRATION_MODE = 'Page.setRPHRegistrationMode'\n    SET_SPC_TRANSACTION_MODE = 'Page.setSPCTransactionMode'\n    SET_WEB_LIFECYCLE_STATE = 'Page.setWebLifecycleState'\n    START_SCREENCAST = 'Page.startScreencast'\n    STOP_LOADING = 'Page.stopLoading'\n    STOP_SCREENCAST = 'Page.stopScreencast'\n    WAIT_FOR_DEBUGGER = 'Page.waitForDebugger'\n    ADD_COMPILATION_CACHE = 'Page.addCompilationCache'\n\n\nclass AddScriptToEvaluateOnNewDocumentParams(TypedDict):\n    \"\"\"Parameters for addScriptToEvaluateOnNewDocument.\"\"\"\n\n    source: str\n    worldName: NotRequired[str]\n    includeCommandLineAPI: NotRequired[bool]\n    runImmediately: NotRequired[bool]\n\n\nclass CaptureScreenshotParams(TypedDict, total=False):\n    \"\"\"Parameters for captureScreenshot.\"\"\"\n\n    format: ScreenshotFormat\n    quality: int\n    clip: Viewport\n    fromSurface: bool\n    captureBeyondViewport: bool\n    optimizeForSpeed: bool\n\n\nclass CaptureSnapshotParams(TypedDict, total=False):\n    \"\"\"Parameters for captureSnapshot.\"\"\"\n\n    format: str\n\n\nclass CreateIsolatedWorldParams(TypedDict):\n    \"\"\"Parameters for createIsolatedWorld.\"\"\"\n\n    frameId: FrameId\n    worldName: NotRequired[str]\n    grantUniveralAccess: NotRequired[bool]\n\n\nclass GetAppManifestParams(TypedDict, total=False):\n    \"\"\"Parameters for getAppManifest.\"\"\"\n\n    manifestId: str\n\n\nclass GetAdScriptAncestryParams(TypedDict):\n    \"\"\"Parameters for getAdScriptAncestry.\"\"\"\n\n    frameId: FrameId\n\n\nclass GetPermissionsPolicyStateParams(TypedDict):\n    \"\"\"Parameters for getPermissionsPolicyState.\"\"\"\n\n    frameId: FrameId\n\n\nclass GetOriginTrialsParams(TypedDict):\n    \"\"\"Parameters for getOriginTrials.\"\"\"\n\n    frameId: FrameId\n\n\nclass GetResourceContentParams(TypedDict):\n    \"\"\"Parameters for getResourceContent.\"\"\"\n\n    frameId: FrameId\n    url: str\n\n\nclass HandleJavaScriptDialogParams(TypedDict):\n    \"\"\"Parameters for handleJavaScriptDialog.\"\"\"\n\n    accept: bool\n    promptText: NotRequired[str]\n\n\nclass NavigateParams(TypedDict):\n    \"\"\"Parameters for navigate.\"\"\"\n\n    url: str\n    referrer: NotRequired[str]\n    transitionType: NotRequired[TransitionType]\n    frameId: NotRequired[FrameId]\n    referrerPolicy: NotRequired[ReferrerPolicy]\n\n\nclass NavigateToHistoryEntryParams(TypedDict):\n    \"\"\"Parameters for navigateToHistoryEntry.\"\"\"\n\n    entryId: int\n\n\nclass EnableParams(TypedDict):\n    enableFileChooserOpenedEvent: NotRequired[bool]\n\n\nclass PrintToPDFParams(TypedDict, total=False):\n    \"\"\"Parameters for printToPDF.\"\"\"\n\n    landscape: bool\n    displayHeaderFooter: bool\n    printBackground: bool\n    scale: float\n    paperWidth: float\n    paperHeight: float\n    marginTop: float\n    marginBottom: float\n    marginLeft: float\n    marginRight: float\n    pageRanges: str\n    headerTemplate: str\n    footerTemplate: str\n    preferCSSPageSize: bool\n    transferMode: TransferMode\n    generateTaggedPDF: bool\n    generateDocumentOutline: bool\n\n\nclass ReloadParams(TypedDict, total=False):\n    \"\"\"Parameters for reload.\"\"\"\n\n    ignoreCache: bool\n    scriptToEvaluateOnLoad: str\n    loaderId: LoaderId\n\n\nclass RemoveScriptToEvaluateOnNewDocumentParams(TypedDict):\n    \"\"\"Parameters for removeScriptToEvaluateOnNewDocument.\"\"\"\n\n    identifier: ScriptIdentifier\n\n\nclass ScreencastFrameAckParams(TypedDict):\n    \"\"\"Parameters for screencastFrameAck.\"\"\"\n\n    sessionId: int\n\n\nclass SearchInResourceParams(TypedDict):\n    \"\"\"Parameters for searchInResource.\"\"\"\n\n    frameId: FrameId\n    url: str\n    query: str\n    caseSensitive: NotRequired[bool]\n    isRegex: NotRequired[bool]\n\n\nclass SetAdBlockingEnabledParams(TypedDict):\n    \"\"\"Parameters for setAdBlockingEnabled.\"\"\"\n\n    enabled: bool\n\n\nclass SetBypassCSPParams(TypedDict):\n    \"\"\"Parameters for setBypassCSP.\"\"\"\n\n    enabled: bool\n\n\nclass AddScriptToEvaluateOnLoadParams(TypedDict):\n    \"\"\"Parameters for addScriptToEvaluateOnLoad.\"\"\"\n\n    scriptSource: str\n\n\nclass SetDocumentContentParams(TypedDict):\n    \"\"\"Parameters for setDocumentContent.\"\"\"\n\n    frameId: FrameId\n    html: str\n\n\nclass SetInterceptFileChooserDialogParams(TypedDict):\n    \"\"\"Parameters for setInterceptFileChooserDialog.\"\"\"\n\n    enabled: bool\n    cancel: NotRequired[bool]\n\n\nclass SetLifecycleEventsEnabledParams(TypedDict):\n    \"\"\"Parameters for setLifecycleEventsEnabled.\"\"\"\n\n    enabled: bool\n\n\nclass AddCompilationCacheParams(TypedDict):\n    \"\"\"Parameters for addCompilationCache.\"\"\"\n\n    url: str\n    data: str\n\n\nclass GenerateTestReportParams(TypedDict):\n    \"\"\"Parameters for generateTestReport.\"\"\"\n\n    message: str\n    group: NotRequired[str]\n\n\nclass GetAdScriptAncestryIdsParams(TypedDict):\n    \"\"\"Parameters for getAdScriptAncestryIds.\"\"\"\n\n    frameId: FrameId\n\n\nclass GetAppIdParams(TypedDict, total=False):\n    \"\"\"Parameters for getAppId.\"\"\"\n\n    appId: str\n    recommendedId: str\n\n\nclass GetManifestIconsParams(TypedDict):\n    \"\"\"Parameters for getManifestIcons.\"\"\"\n\n    pass\n\n\nclass RemoveScriptToEvaluateOnLoadParams(TypedDict):\n    \"\"\"Parameters for removeScriptToEvaluateOnLoad.\"\"\"\n\n    identifier: ScriptIdentifier\n\n\nclass SetFontFamiliesParams(TypedDict):\n    \"\"\"Parameters for setFontFamilies.\"\"\"\n\n    fontFamilies: FontFamilies\n    forScripts: NotRequired[list[ScriptFontFamilies]]\n\n\nclass SetFontSizesParams(TypedDict):\n    \"\"\"Parameters for setFontSizes.\"\"\"\n\n    fontSizes: FontSizes\n\n\nclass SetPrerenderingAllowedParams(TypedDict):\n    \"\"\"Parameters for setPrerenderingAllowed.\"\"\"\n\n    isAllowed: bool\n\n\nclass SetRPHRegistrationModeParams(TypedDict):\n    \"\"\"Parameters for setRPHRegistrationMode.\"\"\"\n\n    mode: AutoResponseMode\n\n\nclass SetSPCTransactionModeParams(TypedDict):\n    \"\"\"Parameters for setSPCTransactionMode.\"\"\"\n\n    mode: AutoResponseMode\n\n\nclass SetWebLifecycleStateParams(TypedDict):\n    \"\"\"Parameters for setWebLifecycleState.\"\"\"\n\n    state: WebLifecycleState\n\n\nclass StartScreencastParams(TypedDict, total=False):\n    \"\"\"Parameters for startScreencast.\"\"\"\n\n    format: ScreencastFormat\n    quality: int\n    maxWidth: int\n    maxHeight: int\n    everyNthFrame: int\n\n\nclass ProduceCompilationCacheParams(TypedDict):\n    \"\"\"Parameters for produceCompilationCache.\"\"\"\n\n    scripts: list[CompilationCacheParams]\n\n\nclass AddScriptToEvaluateOnNewDocumentResult(TypedDict):\n    identifier: ScriptIdentifier\n\n\nclass CaptureScreenshotResult(TypedDict):\n    data: str\n\n\nclass CaptureSnapshotResult(TypedDict):\n    data: str\n\n\nclass CreateIsolatedWorldResult(TypedDict):\n    executionContextId: ExecutionContextId\n\n\nclass GetAppManifestResult(TypedDict):\n    url: str\n    errors: list[AppManifestError]\n    data: NotRequired[str]\n    parsed: NotRequired[AppManifestParsedProperties]\n    manifest: NotRequired[WebAppManifest]\n\n\nclass GetInstallabilityErrorsResult(TypedDict):\n    installabilityErrors: list[InstallabilityError]\n\n\nclass GetAppIdResult(TypedDict, total=False):\n    \"\"\"Result for getAppId.\"\"\"\n\n    appId: str\n    recommendedId: str\n\n\nclass GetAdScriptAncestryResult(TypedDict, total=False):\n    adScriptAncestry: AdScriptAncestry\n\n\nclass GetFrameTreeResult(TypedDict):\n    frameTree: FrameTree\n\n\nclass GetLayoutMetricsResult(TypedDict):\n    layoutViewport: LayoutViewport\n    visualViewport: VisualViewport\n    contentSize: Rect\n    cssLayoutViewport: LayoutViewport\n    cssVisualViewport: VisualViewport\n    cssContentSize: Rect\n\n\nclass GetNavigationHistoryResult(TypedDict):\n    currentIndex: int\n    entries: list[NavigationEntry]\n\n\nclass GetPermissionsPolicyStateResult(TypedDict):\n    states: list[PermissionsPolicyFeatureState]\n\n\nclass GetOriginTrialsResult(TypedDict):\n    originTrials: list[OriginTrial]\n\n\nclass GetResourceContentResult(TypedDict):\n    content: str\n    base64Encoded: bool\n\n\nclass GetResourceTreeResult(TypedDict):\n    frameTree: FrameResourceTree\n\n\nclass PrintToPDFResult(TypedDict):\n    data: str\n    stream: NotRequired[StreamHandle]\n\n\nclass SearchInResourceResult(TypedDict):\n    result: list[SearchMatch]\n\n\nclass NavigateResult(TypedDict):\n    \"\"\"Result for navigate.\"\"\"\n\n    frameId: FrameId\n    loaderId: NotRequired[LoaderId]\n    errorText: NotRequired[str]\n    isDownload: NotRequired[bool]\n\n\nclass AddScriptToEvaluateOnLoadResult(TypedDict):\n    \"\"\"Result for addScriptToEvaluateOnLoad.\"\"\"\n\n    identifier: ScriptIdentifier\n\n\nclass GetManifestIconsResult(TypedDict):\n    \"\"\"Result for getManifestIcons.\"\"\"\n\n    primaryIcon: NotRequired[str]\n\n\nclass GetAdScriptAncestryIdsResult(TypedDict):\n    \"\"\"Result for getAdScriptAncestryIds.\"\"\"\n\n    adScriptAncestry: NotRequired[AdScriptAncestry]\n\n\nAddScriptToEvaluateOnLoadResponse = Response[AddScriptToEvaluateOnLoadResult]\nAddScriptToEvaluateOnNewDocumentResponse = Response[AddScriptToEvaluateOnNewDocumentResult]\nCaptureScreenshotResponse = Response[CaptureScreenshotResult]\nCaptureSnapshotResponse = Response[CaptureSnapshotResult]\nCreateIsolatedWorldResponse = Response[CreateIsolatedWorldResult]\nGetAdScriptAncestryIdsResponse = Response[GetAdScriptAncestryIdsResult]\nGetAdScriptAncestryResponse = Response[GetAdScriptAncestryResult]\nGetAppIdResponse = Response[GetAppIdResult]\nGetAppManifestResponse = Response[GetAppManifestResult]\nGetFrameTreeResponse = Response[GetFrameTreeResult]\nGetInstallabilityErrorsResponse = Response[GetInstallabilityErrorsResult]\nGetLayoutMetricsResponse = Response[GetLayoutMetricsResult]\nGetManifestIconsResponse = Response[GetManifestIconsResult]\nGetNavigationHistoryResponse = Response[GetNavigationHistoryResult]\nGetOriginTrialsResponse = Response[GetOriginTrialsResult]\nGetPermissionsPolicyStateResponse = Response[GetPermissionsPolicyStateResult]\nGetResourceContentResponse = Response[GetResourceContentResult]\nGetResourceTreeResponse = Response[GetResourceTreeResult]\nNavigateResponse = Response[NavigateResult]\nPrintToPDFResponse = Response[PrintToPDFResult]\nSearchInResourceResponse = Response[SearchInResourceResult]\n\n\nAddCompilationCacheCommand = Command[AddCompilationCacheParams, Response[EmptyResponse]]\nAddScriptToEvaluateOnLoadCommand = Command[\n    AddScriptToEvaluateOnLoadParams, AddScriptToEvaluateOnLoadResponse\n]\nAddScriptToEvaluateOnNewDocumentCommand = Command[\n    AddScriptToEvaluateOnNewDocumentParams, AddScriptToEvaluateOnNewDocumentResponse\n]\nBringToFrontCommand = Command[EmptyParams, Response[EmptyResponse]]\nCaptureScreenshotCommand = Command[CaptureScreenshotParams, CaptureScreenshotResponse]\nCaptureSnapshotCommand = Command[CaptureSnapshotParams, CaptureSnapshotResponse]\nClearCompilationCacheCommand = Command[EmptyParams, Response[EmptyResponse]]\nCloseCommand = Command[EmptyParams, Response[EmptyResponse]]\nCrashCommand = Command[EmptyParams, Response[EmptyResponse]]\nCreateIsolatedWorldCommand = Command[CreateIsolatedWorldParams, CreateIsolatedWorldResponse]\nDisableCommand = Command[EmptyParams, Response[EmptyResponse]]\nEnableCommand = Command[EnableParams, Response[EmptyResponse]]\nGenerateTestReportCommand = Command[GenerateTestReportParams, Response[EmptyResponse]]\nGetAdScriptAncestryCommand = Command[GetAdScriptAncestryParams, GetAdScriptAncestryResponse]\nGetAdScriptAncestryIdsCommand = Command[\n    GetAdScriptAncestryIdsParams, GetAdScriptAncestryIdsResponse\n]\nGetAppIdCommand = Command[GetAppIdParams, GetAppIdResponse]\nGetAppManifestCommand = Command[GetAppManifestParams, GetAppManifestResponse]\nGetFrameTreeCommand = Command[EmptyParams, GetFrameTreeResponse]\nGetInstallabilityErrorsCommand = Command[EmptyParams, GetInstallabilityErrorsResponse]\nGetLayoutMetricsCommand = Command[EmptyParams, GetLayoutMetricsResponse]\nGetManifestIconsCommand = Command[EmptyParams, GetManifestIconsResponse]\nGetNavigationHistoryCommand = Command[EmptyParams, GetNavigationHistoryResponse]\nGetOriginTrialsCommand = Command[GetOriginTrialsParams, GetOriginTrialsResponse]\nGetPermissionsPolicyStateCommand = Command[\n    GetPermissionsPolicyStateParams, GetPermissionsPolicyStateResponse\n]\nGetResourceContentCommand = Command[GetResourceContentParams, GetResourceContentResponse]\nGetResourceTreeCommand = Command[EmptyParams, GetResourceTreeResponse]\nHandleJavaScriptDialogCommand = Command[HandleJavaScriptDialogParams, Response[EmptyResponse]]\nNavigateCommand = Command[NavigateParams, NavigateResponse]\nNavigateToHistoryEntryCommand = Command[NavigateToHistoryEntryParams, Response[EmptyResponse]]\nPrintToPDFCommand = Command[PrintToPDFParams, PrintToPDFResponse]\nProduceCompilationCacheCommand = Command[ProduceCompilationCacheParams, Response[EmptyResponse]]\nReloadCommand = Command[ReloadParams, Response[EmptyResponse]]\nRemoveScriptToEvaluateOnLoadCommand = Command[\n    RemoveScriptToEvaluateOnLoadParams, Response[EmptyResponse]\n]\nRemoveScriptToEvaluateOnNewDocumentCommand = Command[\n    RemoveScriptToEvaluateOnNewDocumentParams, Response[EmptyResponse]\n]\nResetNavigationHistoryCommand = Command[EmptyParams, Response[EmptyResponse]]\nScreencastFrameAckCommand = Command[ScreencastFrameAckParams, Response[EmptyResponse]]\nSearchInResourceCommand = Command[SearchInResourceParams, SearchInResourceResponse]\nSetAdBlockingEnabledCommand = Command[SetAdBlockingEnabledParams, Response[EmptyResponse]]\nSetBypassCSPCommand = Command[SetBypassCSPParams, Response[EmptyResponse]]\nSetDocumentContentCommand = Command[SetDocumentContentParams, Response[EmptyResponse]]\nSetFontFamiliesCommand = Command[SetFontFamiliesParams, Response[EmptyResponse]]\nSetFontSizesCommand = Command[SetFontSizesParams, Response[EmptyResponse]]\nSetInterceptFileChooserDialogCommand = Command[\n    SetInterceptFileChooserDialogParams, Response[EmptyResponse]\n]\nSetLifecycleEventsEnabledCommand = Command[SetLifecycleEventsEnabledParams, Response[EmptyResponse]]\nSetPrerenderingAllowedCommand = Command[SetPrerenderingAllowedParams, Response[EmptyResponse]]\nSetRPHRegistrationModeCommand = Command[SetRPHRegistrationModeParams, Response[EmptyResponse]]\nSetSPCTransactionModeCommand = Command[SetSPCTransactionModeParams, Response[EmptyResponse]]\nSetWebLifecycleStateCommand = Command[SetWebLifecycleStateParams, Response[EmptyResponse]]\nStartScreencastCommand = Command[StartScreencastParams, Response[EmptyResponse]]\nStopLoadingCommand = Command[EmptyParams, Response[EmptyResponse]]\nStopScreencastCommand = Command[EmptyParams, Response[EmptyResponse]]\nWaitForDebuggerCommand = Command[EmptyParams, Response[EmptyResponse]]\n"
  },
  {
    "path": "pydoll/protocol/page/types.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.network.types import LoaderId, ResourceType, TimeSinceEpoch\nfrom pydoll.protocol.runtime.types import ScriptId, UniqueDebuggerId\n\nFrameId = str\nScriptIdentifier = str\n\n\nclass AdFrameType(str, Enum):\n    \"\"\"Ad frame types.\"\"\"\n\n    NONE = 'none'\n    CHILD = 'child'\n    ROOT = 'root'\n\n\nclass AdFrameExplanation(str, Enum):\n    \"\"\"Ad frame explanation types.\"\"\"\n\n    PARENT_IS_AD = 'ParentIsAd'\n    CREATED_BY_AD_SCRIPT = 'CreatedByAdScript'\n    MATCHED_BLOCKING_RULE = 'MatchedBlockingRule'\n\n\nclass SecureContextType(str, Enum):\n    \"\"\"Secure context types.\"\"\"\n\n    SECURE = 'Secure'\n    SECURE_LOCALHOST = 'SecureLocalhost'\n    INSECURE_SCHEME = 'InsecureScheme'\n    INSECURE_ANCESTOR = 'InsecureAncestor'\n\n\nclass CrossOriginIsolatedContextType(str, Enum):\n    \"\"\"Cross-origin isolated context types.\"\"\"\n\n    ISOLATED = 'Isolated'\n    NOT_ISOLATED = 'NotIsolated'\n    NOT_ISOLATED_FEATURE_DISABLED = 'NotIsolatedFeatureDisabled'\n\n\nclass GatedAPIFeatures(str, Enum):\n    \"\"\"Gated API features.\"\"\"\n\n    SHARED_ARRAY_BUFFERS = 'SharedArrayBuffers'\n    SHARED_ARRAY_BUFFERS_TRANSFER_ALLOWED = 'SharedArrayBuffersTransferAllowed'\n    PERFORMANCE_MEASURE_MEMORY = 'PerformanceMeasureMemory'\n    PERFORMANCE_PROFILE = 'PerformanceProfile'\n\n\nclass PermissionsPolicyFeature(str, Enum):\n    \"\"\"Permissions policy features.\"\"\"\n\n    ACCELEROMETER = 'accelerometer'\n    ALL_SCREENS_CAPTURE = 'all-screens-capture'\n    AMBIENT_LIGHT_SENSOR = 'ambient-light-sensor'\n    ARIA_NOTIFY = 'aria-notify'\n    ATTRIBUTION_REPORTING = 'attribution-reporting'\n    AUTOPLAY = 'autoplay'\n    BLUETOOTH = 'bluetooth'\n    BROWSING_TOPICS = 'browsing-topics'\n    CAMERA = 'camera'\n    CAPTURED_SURFACE_CONTROL = 'captured-surface-control'\n    CH_DPR = 'ch-dpr'\n    CH_DEVICE_MEMORY = 'ch-device-memory'\n    CH_DOWNLINK = 'ch-downlink'\n    CH_ECT = 'ch-ect'\n    CH_PREFERS_COLOR_SCHEME = 'ch-prefers-color-scheme'\n    CH_PREFERS_REDUCED_MOTION = 'ch-prefers-reduced-motion'\n    CH_PREFERS_REDUCED_TRANSPARENCY = 'ch-prefers-reduced-transparency'\n    CH_RTT = 'ch-rtt'\n    CH_SAVE_DATA = 'ch-save-data'\n    CH_UA = 'ch-ua'\n    CH_UA_ARCH = 'ch-ua-arch'\n    CH_UA_BITNESS = 'ch-ua-bitness'\n    CH_UA_HIGH_ENTROPY_VALUES = 'ch-ua-high-entropy-values'\n    CH_UA_PLATFORM = 'ch-ua-platform'\n    CH_UA_MODEL = 'ch-ua-model'\n    CH_UA_MOBILE = 'ch-ua-mobile'\n    CH_UA_FORM_FACTORS = 'ch-ua-form-factors'\n    CH_UA_FULL_VERSION = 'ch-ua-full-version'\n    CH_UA_FULL_VERSION_LIST = 'ch-ua-full-version-list'\n    CH_UA_PLATFORM_VERSION = 'ch-ua-platform-version'\n    CH_UA_WOW64 = 'ch-ua-wow64'\n    CH_VIEWPORT_HEIGHT = 'ch-viewport-height'\n    CH_VIEWPORT_WIDTH = 'ch-viewport-width'\n    CH_WIDTH = 'ch-width'\n    CLIPBOARD_READ = 'clipboard-read'\n    CLIPBOARD_WRITE = 'clipboard-write'\n    COMPUTE_PRESSURE = 'compute-pressure'\n    CONTROLLED_FRAME = 'controlled-frame'\n    CROSS_ORIGIN_ISOLATED = 'cross-origin-isolated'\n    DEFERRED_FETCH = 'deferred-fetch'\n    DEFERRED_FETCH_MINIMAL = 'deferred-fetch-minimal'\n    DEVICE_ATTRIBUTES = 'device-attributes'\n    DIGITAL_CREDENTIALS_GET = 'digital-credentials-get'\n    DIRECT_SOCKETS = 'direct-sockets'\n    DIRECT_SOCKETS_PRIVATE = 'direct-sockets-private'\n    DISPLAY_CAPTURE = 'display-capture'\n    DOCUMENT_DOMAIN = 'document-domain'\n    ENCRYPTED_MEDIA = 'encrypted-media'\n    EXECUTION_WHILE_OUT_OF_VIEWPORT = 'execution-while-out-of-viewport'\n    EXECUTION_WHILE_NOT_RENDERED = 'execution-while-not-rendered'\n    FENCED_UNPARTITIONED_STORAGE_READ = 'fenced-unpartitioned-storage-read'\n    FOCUS_WITHOUT_USER_ACTIVATION = 'focus-without-user-activation'\n    FULLSCREEN = 'fullscreen'\n    FROBULATE = 'frobulate'\n    GAMEPAD = 'gamepad'\n    GEOLOCATION = 'geolocation'\n    GYROSCOPE = 'gyroscope'\n    HID = 'hid'\n    IDENTITY_CREDENTIALS_GET = 'identity-credentials-get'\n    IDLE_DETECTION = 'idle-detection'\n    INTEREST_COHORT = 'interest-cohort'\n    JOIN_AD_INTEREST_GROUP = 'join-ad-interest-group'\n    KEYBOARD_MAP = 'keyboard-map'\n    LANGUAGE_DETECTOR = 'language-detector'\n    LANGUAGE_MODEL = 'language-model'\n    LOCAL_FONTS = 'local-fonts'\n    LOCAL_NETWORK_ACCESS = 'local-network-access'\n    MAGNETOMETER = 'magnetometer'\n    MEDIA_PLAYBACK_WHILE_NOT_VISIBLE = 'media-playback-while-not-visible'\n    MICROPHONE = 'microphone'\n    MIDI = 'midi'\n    ON_DEVICE_SPEECH_RECOGNITION = 'on-device-speech-recognition'\n    OTP_CREDENTIALS = 'otp-credentials'\n    PAYMENT = 'payment'\n    PICTURE_IN_PICTURE = 'picture-in-picture'\n    POPINS = 'popins'\n    PRIVATE_AGGREGATION = 'private-aggregation'\n    PRIVATE_STATE_TOKEN_ISSUANCE = 'private-state-token-issuance'\n    PRIVATE_STATE_TOKEN_REDEMPTION = 'private-state-token-redemption'\n    PUBLICKEY_CREDENTIALS_CREATE = 'publickey-credentials-create'\n    PUBLICKEY_CREDENTIALS_GET = 'publickey-credentials-get'\n    RECORD_AD_AUCTION_EVENTS = 'record-ad-auction-events'\n    REWRITER = 'rewriter'\n    RUN_AD_AUCTION = 'run-ad-auction'\n    SCREEN_WAKE_LOCK = 'screen-wake-lock'\n    SERIAL = 'serial'\n    SHARED_AUTOFILL = 'shared-autofill'\n    SHARED_STORAGE = 'shared-storage'\n    SHARED_STORAGE_SELECT_URL = 'shared-storage-select-url'\n    SMART_CARD = 'smart-card'\n    SPEAKER_SELECTION = 'speaker-selection'\n    STORAGE_ACCESS = 'storage-access'\n    SUB_APPS = 'sub-apps'\n    SUMMARIZER = 'summarizer'\n    SYNC_XHR = 'sync-xhr'\n    TRANSLATOR = 'translator'\n    UNLOAD = 'unload'\n    USB = 'usb'\n    USB_UNRESTRICTED = 'usb-unrestricted'\n    VERTICAL_SCROLL = 'vertical-scroll'\n    WEB_APP_INSTALLATION = 'web-app-installation'\n    WEB_PRINTING = 'web-printing'\n    WEB_SHARE = 'web-share'\n    WINDOW_MANAGEMENT = 'window-management'\n    WRITER = 'writer'\n    XR_SPATIAL_TRACKING = 'xr-spatial-tracking'\n\n\nclass PermissionsPolicyBlockReason(str, Enum):\n    \"\"\"Permissions policy block reasons.\"\"\"\n\n    HEADER = 'Header'\n    IFRAME_ATTRIBUTE = 'IframeAttribute'\n    IN_FENCED_FRAME_TREE = 'InFencedFrameTree'\n    IN_ISOLATED_APP = 'InIsolatedApp'\n\n\nclass BackForwardCacheNotRestoredReasonType(str, Enum):\n    \"\"\"Back/forward cache not restored explanation type.\"\"\"\n\n    SUPPORT_PENDING = 'SupportPending'\n    PAGE_SUPPORT_NEEDED = 'PageSupportNeeded'\n    CIRCUMSTANTIAL = 'Circumstantial'\n\n\nclass BackForwardCacheNotRestoredReason(str, Enum):\n    NOT_PRIMARY_MAIN_FRAME = 'NotPrimaryMainFrame'\n    BACK_FORWARD_CACHE_DISABLED = 'BackForwardCacheDisabled'\n    RELATED_ACTIVE_CONTENTS_EXIST = 'RelatedActiveContentsExist'\n    HTTP_STATUS_NOT_OK = 'HTTPStatusNotOK'\n    SCHEME_NOT_HTTP_OR_HTTPS = 'SchemeNotHTTPOrHTTPS'\n    LOADING = 'Loading'\n    WAS_GRANTED_MEDIA_ACCESS = 'WasGrantedMediaAccess'\n    DISABLE_FOR_RENDER_FRAME_HOST_CALLED = 'DisableForRenderFrameHostCalled'\n    DOMAIN_NOT_ALLOWED = 'DomainNotAllowed'\n    HTTP_METHOD_NOT_GET = 'HTTPMethodNotGET'\n    SUBFRAME_IS_NAVIGATING = 'SubframeIsNavigating'\n    TIMEOUT = 'Timeout'\n    CACHE_LIMIT = 'CacheLimit'\n    JAVASCRIPT_EXECUTION = 'JavaScriptExecution'\n    RENDERER_PROCESS_KILLED = 'RendererProcessKilled'\n    RENDERER_PROCESS_CRASHED = 'RendererProcessCrashed'\n    SCHEDULER_TRACKED_FEATURE_USED = 'SchedulerTrackedFeatureUsed'\n    CONFLICTING_BROWSING_INSTANCE = 'ConflictingBrowsingInstance'\n    CACHE_FLUSHED = 'CacheFlushed'\n    SERVICE_WORKER_VERSION_ACTIVATION = 'ServiceWorkerVersionActivation'\n    SESSION_RESTORED = 'SessionRestored'\n    SERVICE_WORKER_POST_MESSAGE = 'ServiceWorkerPostMessage'\n    ENTERED_BACK_FORWARD_CACHE_BEFORE_SERVICE_WORKER_HOST_ADDED = (\n        'EnteredBackForwardCacheBeforeServiceWorkerHostAdded'\n    )\n    RENDER_FRAME_HOST_REUSED_SAME_SITE = 'RenderFrameHostReused_SameSite'\n    RENDER_FRAME_HOST_REUSED_CROSS_SITE = 'RenderFrameHostReused_CrossSite'\n    SERVICE_WORKER_CLAIM = 'ServiceWorkerClaim'\n    IGNORE_EVENT_AND_EVICT = 'IgnoreEventAndEvict'\n    HAVE_INNER_CONTENTS = 'HaveInnerContents'\n    TIMEOUT_PUTTING_IN_CACHE = 'TimeoutPuttingInCache'\n    BACK_FORWARD_CACHE_DISABLED_BY_LOW_MEMORY = 'BackForwardCacheDisabledByLowMemory'\n    BACK_FORWARD_CACHE_DISABLED_BY_COMMAND_LINE = 'BackForwardCacheDisabledByCommandLine'\n    NETWORK_REQUEST_DATAPIPE_DRAINED_AS_BYTES_CONSUMER = (\n        'NetworkRequestDatapipeDrainedAsBytesConsumer'\n    )\n    NETWORK_REQUEST_REDIRECTED = 'NetworkRequestRedirected'\n    NETWORK_REQUEST_TIMEOUT = 'NetworkRequestTimeout'\n    NETWORK_EXCEEDS_BUFFER_LIMIT = 'NetworkExceedsBufferLimit'\n    NAVIGATION_CANCELLED_WHILE_RESTORING = 'NavigationCancelledWhileRestoring'\n    NOT_MOST_RECENT_NAVIGATION_ENTRY = 'NotMostRecentNavigationEntry'\n    BACK_FORWARD_CACHE_DISABLED_FOR_PRERENDER = 'BackForwardCacheDisabledForPrerender'\n    USER_AGENT_OVERRIDE_DIFFERS = 'UserAgentOverrideDiffers'\n    FOREGROUND_CACHE_LIMIT = 'ForegroundCacheLimit'\n    BROWSING_INSTANCE_NOT_SWAPPED = 'BrowsingInstanceNotSwapped'\n    BACK_FORWARD_CACHE_DISABLED_FOR_DELEGATE = 'BackForwardCacheDisabledForDelegate'\n    UNLOAD_HANDLER_EXISTS_IN_MAIN_FRAME = 'UnloadHandlerExistsInMainFrame'\n    UNLOAD_HANDLER_EXISTS_IN_SUB_FRAME = 'UnloadHandlerExistsInSubFrame'\n    SERVICE_WORKER_UNREGISTRATION = 'ServiceWorkerUnregistration'\n    CACHE_CONTROL_NO_STORE = 'CacheControlNoStore'\n    CACHE_CONTROL_NO_STORE_COOKIE_MODIFIED = 'CacheControlNoStoreCookieModified'\n    CACHE_CONTROL_NO_STORE_HTTP_ONLY_COOKIE_MODIFIED = 'CacheControlNoStoreHTTPOnlyCookieModified'\n    NO_RESPONSE_HEAD = 'NoResponseHead'\n    UNKNOWN = 'Unknown'\n    ACTIVATION_NAVIGATIONS_DISALLOWED_FOR_BUG_1234857 = (\n        'ActivationNavigationsDisallowedForBug1234857'\n    )\n    ERROR_DOCUMENT = 'ErrorDocument'\n    FENCED_FRAMES_EMBEDDER = 'FencedFramesEmbedder'\n    COOKIE_DISABLED = 'CookieDisabled'\n    HTTP_AUTH_REQUIRED = 'HTTPAuthRequired'\n    COOKIE_FLUSHED = 'CookieFlushed'\n    BROADCAST_CHANNEL_ON_MESSAGE = 'BroadcastChannelOnMessage'\n    WEB_VIEW_SETTINGS_CHANGED = 'WebViewSettingsChanged'\n    WEB_VIEW_JAVASCRIPT_OBJECT_CHANGED = 'WebViewJavaScriptObjectChanged'\n    WEB_VIEW_MESSAGE_LISTENER_INJECTED = 'WebViewMessageListenerInjected'\n    WEB_VIEW_SAFE_BROWSING_ALLOWLIST_CHANGED = 'WebViewSafeBrowsingAllowlistChanged'\n    WEB_VIEW_DOCUMENT_START_JAVASCRIPT_CHANGED = 'WebViewDocumentStartJavascriptChanged'\n    WEB_SOCKET = 'WebSocket'\n    WEB_TRANSPORT = 'WebTransport'\n    WEB_RTC = 'WebRTC'\n    MAIN_RESOURCE_HAS_CACHE_CONTROL_NO_STORE = 'MainResourceHasCacheControlNoStore'\n    MAIN_RESOURCE_HAS_CACHE_CONTROL_NO_CACHE = 'MainResourceHasCacheControlNoCache'\n    SUBRESOURCE_HAS_CACHE_CONTROL_NO_STORE = 'SubresourceHasCacheControlNoStore'\n    SUBRESOURCE_HAS_CACHE_CONTROL_NO_CACHE = 'SubresourceHasCacheControlNoCache'\n    CONTAINS_PLUGINS = 'ContainsPlugins'\n    DOCUMENT_LOADED = 'DocumentLoaded'\n    OUTSTANDING_NETWORK_REQUEST_OTHERS = 'OutstandingNetworkRequestOthers'\n    REQUESTED_MIDI_PERMISSION = 'RequestedMIDIPermission'\n    REQUESTED_AUDIO_CAPTURE_PERMISSION = 'RequestedAudioCapturePermission'\n    REQUESTED_VIDEO_CAPTURE_PERMISSION = 'RequestedVideoCapturePermission'\n    REQUESTED_BACK_FORWARD_CACHE_BLOCKED_SENSORS = 'RequestedBackForwardCacheBlockedSensors'\n    REQUESTED_BACKGROUND_WORK_PERMISSION = 'RequestedBackgroundWorkPermission'\n    BROADCAST_CHANNEL = 'BroadcastChannel'\n    WEB_XR = 'WebXR'\n    SHARED_WORKER = 'SharedWorker'\n    SHARED_WORKER_MESSAGE = 'SharedWorkerMessage'\n    WEB_LOCKS = 'WebLocks'\n    WEB_HID = 'WebHID'\n    WEB_SHARE = 'WebShare'\n    REQUESTED_STORAGE_ACCESS_GRANT = 'RequestedStorageAccessGrant'\n    WEB_NFC = 'WebNfc'\n    OUTSTANDING_NETWORK_REQUEST_FETCH = 'OutstandingNetworkRequestFetch'\n    OUTSTANDING_NETWORK_REQUEST_XHR = 'OutstandingNetworkRequestXHR'\n    APP_BANNER = 'AppBanner'\n    PRINTING = 'Printing'\n    WEB_DATABASE = 'WebDatabase'\n    PICTURE_IN_PICTURE = 'PictureInPicture'\n    SPEECH_RECOGNIZER = 'SpeechRecognizer'\n    IDLE_MANAGER = 'IdleManager'\n    PAYMENT_MANAGER = 'PaymentManager'\n    SPEECH_SYNTHESIS = 'SpeechSynthesis'\n    KEYBOARD_LOCK = 'KeyboardLock'\n    WEB_OTP_SERVICE = 'WebOTPService'\n    OUTSTANDING_NETWORK_REQUEST_DIRECT_SOCKET = 'OutstandingNetworkRequestDirectSocket'\n    INJECTED_JAVASCRIPT = 'InjectedJavascript'\n    INJECTED_STYLE_SHEET = 'InjectedStyleSheet'\n    KEEPALIVE_REQUEST = 'KeepaliveRequest'\n    INDEXED_DB_EVENT = 'IndexedDBEvent'\n    DUMMY = 'Dummy'\n    JS_NETWORK_REQUEST_RECEIVED_CACHE_CONTROL_NO_STORE_RESOURCE = (\n        'JsNetworkRequestReceivedCacheControlNoStoreResource'\n    )\n    WEB_RTC_STICKY = 'WebRTCSticky'\n    WEB_TRANSPORT_STICKY = 'WebTransportSticky'\n    WEB_SOCKET_STICKY = 'WebSocketSticky'\n    SMART_CARD = 'SmartCard'\n    LIVE_MEDIA_STREAM_TRACK = 'LiveMediaStreamTrack'\n    UNLOAD_HANDLER = 'UnloadHandler'\n    PARSER_ABORTED = 'ParserAborted'\n    CONTENT_SECURITY_HANDLER = 'ContentSecurityHandler'\n    CONTENT_WEB_AUTHENTICATION_API = 'ContentWebAuthenticationAPI'\n    CONTENT_FILE_CHOOSER = 'ContentFileChooser'\n    CONTENT_SERIAL = 'ContentSerial'\n    CONTENT_FILE_SYSTEM_ACCESS = 'ContentFileSystemAccess'\n    CONTENT_MEDIA_DEVICES_DISPATCHER_HOST = 'ContentMediaDevicesDispatcherHost'\n    CONTENT_WEB_BLUETOOTH = 'ContentWebBluetooth'\n    CONTENT_WEB_USB = 'ContentWebUSB'\n    CONTENT_MEDIA_SESSION_SERVICE = 'ContentMediaSessionService'\n    CONTENT_SCREEN_READER = 'ContentScreenReader'\n    CONTENT_DISCARDED = 'ContentDiscarded'\n    EMBEDDER_POPUP_BLOCKER_TAB_HELPER = 'EmbedderPopupBlockerTabHelper'\n    EMBEDDER_SAFE_BROWSING_TRIGGERED_POPUP_BLOCKER = 'EmbedderSafeBrowsingTriggeredPopupBlocker'\n    EMBEDDER_SAFE_BROWSING_THREAT_DETAILS = 'EmbedderSafeBrowsingThreatDetails'\n    EMBEDDER_APP_BANNER_MANAGER = 'EmbedderAppBannerManager'\n    EMBEDDER_DOM_DISTILLER_VIEWER_SOURCE = 'EmbedderDomDistillerViewerSource'\n    EMBEDDER_DOM_DISTILLER_SELF_DELETING_REQUEST_DELEGATE = (\n        'EmbedderDomDistillerSelfDeletingRequestDelegate'\n    )\n    EMBEDDER_OOM_INTERVENTION_TAB_HELPER = 'EmbedderOomInterventionTabHelper'\n    EMBEDDER_OFFLINE_PAGE = 'EmbedderOfflinePage'\n    EMBEDDER_CHROME_PASSWORD_MANAGER_CLIENT_BIND_CREDENTIAL_MANAGER = (\n        'EmbedderChromePasswordManagerClientBindCredentialManager'\n    )\n    EMBEDDER_PERMISSION_REQUEST_MANAGER = 'EmbedderPermissionRequestManager'\n    EMBEDDER_MODAL_DIALOG = 'EmbedderModalDialog'\n    EMBEDDER_EXTENSIONS = 'EmbedderExtensions'\n    EMBEDDER_EXTENSION_MESSAGING = 'EmbedderExtensionMessaging'\n    EMBEDDER_EXTENSION_MESSAGING_FOR_OPEN_PORT = 'EmbedderExtensionMessagingForOpenPort'\n    EMBEDDER_EXTENSION_SENT_MESSAGE_TO_CACHED_FRAME = 'EmbedderExtensionSentMessageToCachedFrame'\n    REQUESTED_BY_WEB_VIEW_CLIENT = 'RequestedByWebViewClient'\n    POST_MESSAGE_BY_WEB_VIEW_CLIENT = 'PostMessageByWebViewClient'\n    CACHE_CONTROL_NO_STORE_DEVICE_BOUND_SESSION_TERMINATED = (\n        'CacheControlNoStoreDeviceBoundSessionTerminated'\n    )\n    CACHE_LIMIT_PRUNED_ON_MODERATE_MEMORY_PRESSURE = 'CacheLimitPrunedOnModerateMemoryPressure'\n    CACHE_LIMIT_PRUNED_ON_CRITICAL_MEMORY_PRESSURE = 'CacheLimitPrunedOnCriticalMemoryPressure'\n\n\nclass BackForwardCacheBlockingDetails(TypedDict):\n    url: NotRequired[str]\n    function: NotRequired[str]\n    lineNumber: int\n    columnNumber: int\n\n\nclass BackForwardCacheNotRestoredExplanation(TypedDict):\n    \"\"\"Back/forward cache not restored explanation.\"\"\"\n\n    type: BackForwardCacheNotRestoredReasonType\n    reason: BackForwardCacheNotRestoredReason\n    context: NotRequired[str]\n    details: NotRequired[list[BackForwardCacheBlockingDetails]]\n\n\nclass BackForwardCacheNotRestoredExplanationTree(TypedDict):\n    url: str\n    explanations: list[BackForwardCacheNotRestoredExplanation]\n    children: NotRequired[list['BackForwardCacheNotRestoredExplanationTree']]\n\n\nclass OriginTrialTokenStatus(str, Enum):\n    \"\"\"Origin trial token status.\"\"\"\n\n    SUCCESS = 'Success'\n    NOT_SUPPORTED = 'NotSupported'\n    INSECURE = 'Insecure'\n    EXPIRED = 'Expired'\n    WRONG_ORIGIN = 'WrongOrigin'\n    INVALID_SIGNATURE = 'InvalidSignature'\n    MALFORMED = 'Malformed'\n    WRONG_VERSION = 'WrongVersion'\n    FEATURE_DISABLED = 'FeatureDisabled'\n    TOKEN_DISABLED = 'TokenDisabled'\n    FEATURE_DISABLED_FOR_USER = 'FeatureDisabledForUser'\n    UNKNOWN_TRIAL = 'UnknownTrial'\n\n\nclass OriginTrialStatus(str, Enum):\n    \"\"\"Origin trial status.\"\"\"\n\n    ENABLED = 'Enabled'\n    VALID_TOKEN_NOT_PROVIDED = 'ValidTokenNotProvided'\n    OS_NOT_SUPPORTED = 'OSNotSupported'\n    TRIAL_NOT_ALLOWED = 'TrialNotAllowed'\n\n\nclass OriginTrialUsageRestriction(str, Enum):\n    \"\"\"Origin trial usage restriction.\"\"\"\n\n    NONE = 'None'\n    SUBSET = 'Subset'\n\n\nclass TransitionType(str, Enum):\n    \"\"\"Transition types.\"\"\"\n\n    LINK = 'link'\n    TYPED = 'typed'\n    ADDRESS_BAR = 'address_bar'\n    AUTO_BOOKMARK = 'auto_bookmark'\n    AUTO_SUBFRAME = 'auto_subframe'\n    MANUAL_SUBFRAME = 'manual_subframe'\n    GENERATED = 'generated'\n    AUTO_TOPLEVEL = 'auto_toplevel'\n    FORM_SUBMIT = 'form_submit'\n    RELOAD = 'reload'\n    KEYWORD = 'keyword'\n    KEYWORD_GENERATED = 'keyword_generated'\n    OTHER = 'other'\n\n\nclass DialogType(str, Enum):\n    \"\"\"Dialog types.\"\"\"\n\n    ALERT = 'alert'\n    CONFIRM = 'confirm'\n    PROMPT = 'prompt'\n    BEFOREUNLOAD = 'beforeunload'\n\n\nclass ClientNavigationReason(Enum):\n    \"\"\"Client navigation reasons.\"\"\"\n\n    ANCHOR_CLICK = 'anchorClick'\n    FORM_SUBMISSION_GET = 'formSubmissionGet'\n    FORM_SUBMISSION_POST = 'formSubmissionPost'\n    HTTP_HEADER_REFRESH = 'httpHeaderRefresh'\n    INITIAL_FRAME_NAVIGATION = 'initialFrameNavigation'\n    META_TAG_REFRESH = 'metaTagRefresh'\n    OTHER = 'other'\n    PAGE_BLOCK_INTERSTITIAL = 'pageBlockInterstitial'\n    RELOAD = 'reload'\n    SCRIPT_INITIATED = 'scriptInitiated'\n\n\nclass ClientNavigationDisposition(str, Enum):\n    \"\"\"Client navigation dispositions.\"\"\"\n\n    CURRENT_TAB = 'currentTab'\n    NEW_TAB = 'newTab'\n    NEW_WINDOW = 'newWindow'\n    DOWNLOAD = 'download'\n\n\nclass ReferrerPolicy(str, Enum):\n    \"\"\"Referrer policy types.\"\"\"\n\n    NO_REFERRER = 'noReferrer'\n    NO_REFERRER_WHEN_DOWNGRADE = 'noReferrerWhenDowngrade'\n    ORIGIN = 'origin'\n    ORIGIN_WHEN_CROSS_ORIGIN = 'originWhenCrossOrigin'\n    SAME_ORIGIN = 'sameOrigin'\n    STRICT_ORIGIN = 'strictOrigin'\n    STRICT_ORIGIN_WHEN_CROSS_ORIGIN = 'strictOriginWhenCrossOrigin'\n    UNSAFE_URL = 'unsafeUrl'\n\n\nclass NavigationType(str, Enum):\n    \"\"\"Navigation types.\"\"\"\n\n    NAVIGATION = 'Navigation'\n    BACK_FORWARD_CACHE_RESTORE = 'BackForwardCacheRestore'\n\n\nclass AdFrameStatus(TypedDict):\n    \"\"\"Ad frame status.\"\"\"\n\n    adFrameType: AdFrameType\n    explanations: NotRequired[list[AdFrameExplanation]]\n\n\nclass AdScriptId(TypedDict):\n    \"\"\"Ad script identifier.\"\"\"\n\n    scriptId: ScriptId\n    debuggerId: UniqueDebuggerId\n\n\nclass AdScriptAncestry(TypedDict):\n    \"\"\"Ad script ancestry.\"\"\"\n\n    ancestryChain: list[AdScriptId]\n    rootScriptFilterlistRule: NotRequired[str]\n\n\nclass PermissionsPolicyBlockLocator(TypedDict):\n    \"\"\"Permissions policy block locator.\"\"\"\n\n    frameId: FrameId\n    blockReason: PermissionsPolicyBlockReason\n\n\nclass PermissionsPolicyFeatureState(TypedDict):\n    \"\"\"Permissions policy feature state.\"\"\"\n\n    feature: PermissionsPolicyFeature\n    allowed: bool\n    locator: NotRequired[PermissionsPolicyBlockLocator]\n\n\nclass OriginTrialToken(TypedDict):\n    \"\"\"Origin trial token.\"\"\"\n\n    origin: str\n    matchSubDomains: bool\n    trialName: str\n    expiryTime: TimeSinceEpoch\n    isThirdParty: bool\n    usageRestriction: OriginTrialUsageRestriction\n\n\nclass OriginTrialTokenWithStatus(TypedDict):\n    \"\"\"Origin trial token with status.\"\"\"\n\n    rawTokenText: str\n    status: OriginTrialTokenStatus\n    parsedToken: NotRequired[OriginTrialToken]\n\n\nclass OriginTrial(TypedDict):\n    \"\"\"Origin trial.\"\"\"\n\n    trialName: str\n    status: OriginTrialStatus\n    tokensWithStatus: list[OriginTrialTokenWithStatus]\n\n\nclass SecurityOriginDetails(TypedDict):\n    \"\"\"Security origin details.\"\"\"\n\n    isLocalhost: bool\n\n\nclass Frame(TypedDict):\n    \"\"\"Frame information.\"\"\"\n\n    id: FrameId\n    loaderId: LoaderId\n    url: str\n    domainAndRegistry: str\n    securityOrigin: str\n    mimeType: str\n    secureContextType: SecureContextType\n    crossOriginIsolatedContextType: CrossOriginIsolatedContextType\n    gatedAPIFeatures: list[GatedAPIFeatures]\n    parentId: NotRequired[FrameId]\n    name: NotRequired[str]\n    urlFragment: NotRequired[str]\n    securityOriginDetails: NotRequired[SecurityOriginDetails]\n    unreachableUrl: NotRequired[str]\n    adFrameStatus: NotRequired[AdFrameStatus]\n\n\nclass FrameResource(TypedDict):\n    \"\"\"Frame resource information.\"\"\"\n\n    url: str\n    type: ResourceType\n    mimeType: str\n    lastModified: NotRequired[TimeSinceEpoch]\n    contentSize: NotRequired[float]\n    failed: NotRequired[bool]\n    canceled: NotRequired[bool]\n\n\nclass FrameResourceTree(TypedDict):\n    \"\"\"Frame resource tree.\"\"\"\n\n    frame: Frame\n    resources: list[FrameResource]\n    childFrames: NotRequired[list['FrameResourceTree']]\n\n\nclass FrameTree(TypedDict):\n    \"\"\"Frame tree.\"\"\"\n\n    frame: Frame\n    childFrames: NotRequired[list['FrameTree']]\n\n\nclass NavigationEntry(TypedDict):\n    \"\"\"Navigation entry.\"\"\"\n\n    id: int\n    url: str\n    userTypedURL: str\n    title: str\n    transitionType: TransitionType\n\n\nclass ScreencastFrameMetadata(TypedDict):\n    \"\"\"Screencast frame metadata.\"\"\"\n\n    offsetTop: float\n    pageScaleFactor: float\n    deviceWidth: float\n    deviceHeight: float\n    scrollOffsetX: float\n    scrollOffsetY: float\n    timestamp: NotRequired[TimeSinceEpoch]\n\n\nclass AppManifestError(TypedDict):\n    \"\"\"App manifest error.\"\"\"\n\n    message: str\n    critical: int\n    line: int\n    column: int\n\n\nclass AppManifestParsedProperties(TypedDict):\n    \"\"\"App manifest parsed properties.\"\"\"\n\n    scope: str\n\n\nclass LayoutViewport(TypedDict):\n    \"\"\"Layout viewport.\"\"\"\n\n    pageX: int\n    pageY: int\n    clientWidth: int\n    clientHeight: int\n\n\nclass VisualViewport(TypedDict):\n    \"\"\"Visual viewport.\"\"\"\n\n    offsetX: float\n    offsetY: float\n    pageX: float\n    pageY: float\n    clientWidth: float\n    clientHeight: float\n    scale: float\n    zoom: NotRequired[float]\n\n\nclass Viewport(TypedDict):\n    \"\"\"Viewport for capturing screenshot.\"\"\"\n\n    x: float\n    y: float\n    width: float\n    height: float\n    scale: float\n\n\nclass FontFamilies(TypedDict, total=False):\n    \"\"\"Font families.\"\"\"\n\n    standard: str\n    fixed: str\n    serif: str\n    sansSerif: str\n    cursive: str\n    fantasy: str\n    math: str\n\n\nclass ScriptFontFamilies(TypedDict):\n    \"\"\"Script font families.\"\"\"\n\n    script: str\n    fontFamilies: FontFamilies\n\n\nclass FontSizes(TypedDict, total=False):\n    \"\"\"Font sizes.\"\"\"\n\n    standard: int\n    fixed: int\n\n\nclass CompilationCacheParams(TypedDict):\n    \"\"\"Compilation cache parameters.\"\"\"\n\n    url: str\n    eager: NotRequired[bool]\n\n\nclass FileFilter(TypedDict, total=False):\n    \"\"\"File filter.\"\"\"\n\n    name: str\n    accepts: list[str]\n\n\nclass ImageResource(TypedDict):\n    \"\"\"Image resource.\"\"\"\n\n    url: str\n    sizes: NotRequired[str]\n    type: NotRequired[str]\n\n\nclass FileHandler(TypedDict):\n    \"\"\"File handler.\"\"\"\n\n    action: str\n    name: str\n    launchType: str\n    icons: NotRequired[list[ImageResource]]\n    accepts: NotRequired[list[FileFilter]]\n\n\nclass LaunchHandler(TypedDict):\n    \"\"\"Launch handler.\"\"\"\n\n    clientMode: str\n\n\nclass ProtocolHandler(TypedDict):\n    \"\"\"Protocol handler.\"\"\"\n\n    protocol: str\n    url: str\n\n\nclass RelatedApplication(TypedDict):\n    \"\"\"Related application.\"\"\"\n\n    url: str\n    id: NotRequired[str]\n\n\nclass ScopeExtension(TypedDict):\n    \"\"\"Scope extension.\"\"\"\n\n    origin: str\n    hasOriginWildcard: bool\n\n\nclass Screenshot(TypedDict):\n    \"\"\"Screenshot.\"\"\"\n\n    image: ImageResource\n    formFactor: str\n    label: NotRequired[str]\n\n\nclass ShareTarget(TypedDict):\n    \"\"\"Share target.\"\"\"\n\n    action: str\n    method: str\n    enctype: str\n    title: NotRequired[str]\n    text: NotRequired[str]\n    url: NotRequired[str]\n    files: NotRequired[list[FileFilter]]\n\n\nclass Shortcut(TypedDict):\n    \"\"\"Shortcut.\"\"\"\n\n    name: str\n    url: str\n\n\nclass WebAppManifest(TypedDict, total=False):\n    \"\"\"Web app manifest.\"\"\"\n\n    backgroundColor: str\n    description: str\n    dir: str\n    display: str\n    displayOverrides: list[str]\n    fileHandlers: list[FileHandler]\n    icons: list[ImageResource]\n    id: str\n    lang: str\n    launchHandler: LaunchHandler\n    name: str\n    orientation: str\n    preferRelatedApplications: bool\n    protocolHandlers: list[ProtocolHandler]\n    relatedApplications: list[RelatedApplication]\n    scope: str\n    scopeExtensions: list[ScopeExtension]\n    screenshots: list[Screenshot]\n    shareTarget: ShareTarget\n    shortName: str\n    shortcuts: list[Shortcut]\n    startUrl: str\n    themeColor: str\n\n\nclass InstallabilityErrorArgument(TypedDict):\n    \"\"\"Installability error argument.\"\"\"\n\n    name: str\n    value: str\n\n\nclass InstallabilityError(TypedDict):\n    \"\"\"Installability error.\"\"\"\n\n    errorId: str\n    errorArguments: list[InstallabilityErrorArgument]\n\n\nclass AutoResponseMode(str, Enum):\n    \"\"\"Auto response mode values.\"\"\"\n\n    NONE = 'none'\n    AUTO_ACCEPT = 'autoAccept'\n    AUTO_CHOOSE_TO_AUTH_ANOTHER_WAY = 'autoChooseToAuthAnotherWay'\n    AUTO_REJECT = 'autoReject'\n    AUTO_OPT_OUT = 'autoOptOut'\n\n\nclass WebLifecycleState(str, Enum):\n    \"\"\"Web lifecycle state values.\"\"\"\n\n    FROZEN = 'frozen'\n    ACTIVE = 'active'\n\n\nclass ScreenshotFormat(str, Enum):\n    \"\"\"Screenshot format values.\"\"\"\n\n    JPEG = 'jpeg'\n    PNG = 'png'\n    WEBP = 'webp'\n\n    @classmethod\n    def has_value(cls, value: str) -> bool:\n        \"\"\"Check if value is a valid screenshot format.\"\"\"\n        return value in cls._value2member_map_\n\n    @classmethod\n    def get_value(cls, value: str) -> 'ScreenshotFormat':\n        \"\"\"Get the value of the screenshot format.\"\"\"\n        return cls(value)\n\n\nclass ScreencastFormat(str, Enum):\n    \"\"\"Screencast format values.\"\"\"\n\n    JPEG = 'jpeg'\n    PNG = 'png'\n\n\nclass TransferMode(str, Enum):\n    \"\"\"Transfer mode values.\"\"\"\n\n    RETURN_AS_BASE64 = 'ReturnAsBase64'\n    RETURN_AS_STREAM = 'ReturnAsStream'\n"
  },
  {
    "path": "pydoll/protocol/runtime/__init__.py",
    "content": "\"\"\"Runtime domain implementation.\"\"\"\n"
  },
  {
    "path": "pydoll/protocol/runtime/events.py",
    "content": "from enum import Enum\nfrom typing import Any\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import CDPEvent\nfrom pydoll.protocol.runtime.types import (\n    ExceptionDetails,\n    ExecutionContextDescription,\n    ExecutionContextId,\n    RemoteObject,\n    StackTrace,\n    Timestamp,\n)\n\n\nclass RuntimeEvent(str, Enum):\n    \"\"\"\n    Events from the Runtime domain of the Chrome DevTools Protocol.\n\n    This enumeration contains the names of Runtime-related events that can be\n    received from the Chrome DevTools Protocol. These events provide information\n    about JavaScript execution, console API calls, exceptions, and execution contexts.\n    \"\"\"\n\n    CONSOLE_API_CALLED = 'Runtime.consoleAPICalled'\n    \"\"\"\n    Issued when console API was called.\n\n    Args:\n        type (str): Type of the call.\n            Allowed Values: log, debug, info, error, warning, dir, dirxml, table, trace,\n            clear, startGroup, startGroupCollapsed, endGroup, assert, profile, profileEnd,\n            count, timeEnd\n        args (array[RemoteObject]): Call arguments.\n        executionContextId (ExecutionContextId): Identifier of the context where the call was made.\n        timestamp (Timestamp): Call timestamp.\n        stackTrace (StackTrace): Stack trace captured when the call was made. The async stack\n            chain is automatically reported for the following call types: assert, error,\n            trace, warning. For other types the async call chain can be retrieved using\n            Debugger.getStackTrace and stackTrace.parentId field.\n        context (str): Console context descriptor for calls on non-default console context\n            (not console.*): 'anonymous#unique-logger-id' for call on unnamed context,\n            'name#unique-logger-id' for call on named context.\n    \"\"\"\n\n    EXCEPTION_REVOKED = 'Runtime.exceptionRevoked'\n    \"\"\"\n    Issued when unhandled exception was revoked.\n\n    Args:\n        reason (str): Reason describing why exception was revoked.\n        exceptionId (int): The id of revoked exception, as reported in exceptionThrown.\n    \"\"\"\n\n    EXCEPTION_THROWN = 'Runtime.exceptionThrown'\n    \"\"\"\n    Issued when exception was thrown and unhandled.\n\n    Args:\n        timestamp (Timestamp): Timestamp of the exception.\n        exceptionDetails (ExceptionDetails): Details about the exception.\n    \"\"\"\n\n    EXECUTION_CONTEXT_CREATED = 'Runtime.executionContextCreated'\n    \"\"\"\n    Issued when new execution context is created.\n\n    Args:\n        context (ExecutionContextDescription): A newly created execution context.\n    \"\"\"\n\n    EXECUTION_CONTEXT_DESTROYED = 'Runtime.executionContextDestroyed'\n    \"\"\"\n    Issued when execution context is destroyed.\n\n    Args:\n        executionContextId (ExecutionContextId): Id of the destroyed context.\n        executionContextUniqueId (str): Unique Id of the destroyed context.\n    \"\"\"\n\n    EXECUTION_CONTEXTS_CLEARED = 'Runtime.executionContextsCleared'\n    \"\"\"\n    Issued when all executionContexts were cleared in browser.\n    \"\"\"\n\n    INSPECT_REQUESTED = 'Runtime.inspectRequested'\n    \"\"\"\n    Issued when object should be inspected\n    (for example, as a result of inspect() command line API call).\n\n    Args:\n        object (RemoteObject): Object to inspect.\n        hints (object): Hints.\n        executionContextId (ExecutionContextId): Identifier of the context where the call was made.\n    \"\"\"\n\n    BINDING_CALLED = 'Runtime.bindingCalled'\n    \"\"\"\n    Notification is issued every time when binding is called.\n\n    Args:\n        name (str): Name of the binding.\n        payload (str): Payload of the binding.\n        executionContextId (ExecutionContextId): Identifier of the context where the call was made.\n    \"\"\"\n\n\nclass ConsoleAPICallType(str, Enum):\n    \"\"\"Console API call types.\"\"\"\n\n    LOG = 'log'\n    DEBUG = 'debug'\n    INFO = 'info'\n    ERROR = 'error'\n    WARNING = 'warning'\n    DIR = 'dir'\n    DIRXML = 'dirxml'\n    TABLE = 'table'\n    TRACE = 'trace'\n    CLEAR = 'clear'\n    START_GROUP = 'startGroup'\n    START_GROUP_COLLAPSED = 'startGroupCollapsed'\n    END_GROUP = 'endGroup'\n    ASSERT = 'assert'\n    PROFILE = 'profile'\n    PROFILE_END = 'profileEnd'\n    COUNT = 'count'\n    TIME_END = 'timeEnd'\n\n\nclass BindingCalledEventParams(TypedDict):\n    \"\"\"Parameters for bindingCalled event.\"\"\"\n\n    name: str\n    payload: str\n    executionContextId: ExecutionContextId\n\n\nclass ConsoleAPICalledEventParams(TypedDict):\n    \"\"\"Parameters for consoleAPICalled event.\"\"\"\n\n    type: ConsoleAPICallType\n    args: list[RemoteObject]\n    executionContextId: ExecutionContextId\n    timestamp: Timestamp\n    stackTrace: NotRequired[StackTrace]\n    context: NotRequired[str]\n\n\nclass ExceptionRevokedEventParams(TypedDict):\n    \"\"\"Parameters for exceptionRevoked event.\"\"\"\n\n    reason: str\n    exceptionId: int\n\n\nclass ExceptionThrownEventParams(TypedDict):\n    \"\"\"Parameters for exceptionThrown event.\"\"\"\n\n    timestamp: Timestamp\n    exceptionDetails: ExceptionDetails\n\n\nclass ExecutionContextCreatedEventParams(TypedDict):\n    \"\"\"Parameters for executionContextCreated event.\"\"\"\n\n    context: ExecutionContextDescription\n\n\nclass ExecutionContextDestroyedEventParams(TypedDict):\n    \"\"\"Parameters for executionContextDestroyed event.\"\"\"\n\n    executionContextId: ExecutionContextId\n    executionContextUniqueId: str\n\n\nclass ExecutionContextsClearedEventParams(TypedDict):\n    \"\"\"Parameters for executionContextsCleared event.\"\"\"\n\n    pass\n\n\nclass InspectRequestedEventParams(TypedDict):\n    \"\"\"Parameters for inspectRequested event.\"\"\"\n\n    object: RemoteObject\n    hints: dict[str, Any]\n    executionContextId: NotRequired[ExecutionContextId]\n\n\n# Event type aliases\nBindingCalledEvent = CDPEvent[BindingCalledEventParams]\nConsoleAPICalledEvent = CDPEvent[ConsoleAPICalledEventParams]\nExceptionRevokedEvent = CDPEvent[ExceptionRevokedEventParams]\nExceptionThrownEvent = CDPEvent[ExceptionThrownEventParams]\nExecutionContextCreatedEvent = CDPEvent[ExecutionContextCreatedEventParams]\nExecutionContextDestroyedEvent = CDPEvent[ExecutionContextDestroyedEventParams]\nExecutionContextsClearedEvent = CDPEvent[ExecutionContextsClearedEventParams]\nInspectRequestedEvent = CDPEvent[InspectRequestedEventParams]\n"
  },
  {
    "path": "pydoll/protocol/runtime/methods.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import Command, EmptyParams, EmptyResponse, Response\nfrom pydoll.protocol.runtime.types import (\n    CallArgument,\n    ExceptionDetails,\n    ExecutionContextId,\n    InternalPropertyDescriptor,\n    PrivatePropertyDescriptor,\n    PropertyDescriptor,\n    RemoteObject,\n    RemoteObjectId,\n    ScriptId,\n    SerializationOptions,\n    TimeDelta,\n)\n\n\nclass RuntimeMethod(str, Enum):\n    \"\"\"Runtime domain method names.\"\"\"\n\n    ADD_BINDING = 'Runtime.addBinding'\n    AWAIT_PROMISE = 'Runtime.awaitPromise'\n    CALL_FUNCTION_ON = 'Runtime.callFunctionOn'\n    COMPILE_SCRIPT = 'Runtime.compileScript'\n    DISABLE = 'Runtime.disable'\n    DISCARD_CONSOLE_ENTRIES = 'Runtime.discardConsoleEntries'\n    ENABLE = 'Runtime.enable'\n    EVALUATE = 'Runtime.evaluate'\n    GET_EXCEPTION_DETAILS = 'Runtime.getExceptionDetails'\n    GET_HEAP_USAGE = 'Runtime.getHeapUsage'\n    GET_ISOLATE_ID = 'Runtime.getIsolateId'\n    GET_PROPERTIES = 'Runtime.getProperties'\n    GLOBAL_LEXICAL_SCOPE_NAMES = 'Runtime.globalLexicalScopeNames'\n    QUERY_OBJECTS = 'Runtime.queryObjects'\n    RELEASE_OBJECT = 'Runtime.releaseObject'\n    RELEASE_OBJECT_GROUP = 'Runtime.releaseObjectGroup'\n    REMOVE_BINDING = 'Runtime.removeBinding'\n    RUN_IF_WAITING_FOR_DEBUGGER = 'Runtime.runIfWaitingForDebugger'\n    RUN_SCRIPT = 'Runtime.runScript'\n    SET_ASYNC_CALL_STACK_DEPTH = 'Runtime.setAsyncCallStackDepth'\n    SET_CUSTOM_OBJECT_FORMATTER_ENABLED = 'Runtime.setCustomObjectFormatterEnabled'\n    SET_MAX_CALL_STACK_SIZE_TO_CAPTURE = 'Runtime.setMaxCallStackSizeToCapture'\n    TERMINATE_EXECUTION = 'Runtime.terminateExecution'\n\n\n# Parameter types\nclass AddBindingParams(TypedDict):\n    \"\"\"Parameters for addBinding command.\"\"\"\n\n    name: str\n    executionContextId: NotRequired[ExecutionContextId]\n    executionContextName: NotRequired[str]\n\n\nclass AwaitPromiseParams(TypedDict):\n    \"\"\"Parameters for awaitPromise command.\"\"\"\n\n    promiseObjectId: RemoteObjectId\n    returnByValue: NotRequired[bool]\n    generatePreview: NotRequired[bool]\n\n\nclass CallFunctionOnParams(TypedDict):\n    \"\"\"Parameters for callFunctionOn command.\"\"\"\n\n    functionDeclaration: str\n    objectId: NotRequired[RemoteObjectId]\n    arguments: NotRequired[list[CallArgument]]\n    silent: NotRequired[bool]\n    returnByValue: NotRequired[bool]\n    generatePreview: NotRequired[bool]\n    userGesture: NotRequired[bool]\n    awaitPromise: NotRequired[bool]\n    executionContextId: NotRequired[ExecutionContextId]\n    objectGroup: NotRequired[str]\n    throwOnSideEffect: NotRequired[bool]\n    uniqueContextId: NotRequired[str]\n    serializationOptions: NotRequired[SerializationOptions]\n\n\nclass CompileScriptParams(TypedDict):\n    \"\"\"Parameters for compileScript command.\"\"\"\n\n    expression: str\n    sourceURL: str\n    persistScript: bool\n    executionContextId: NotRequired[ExecutionContextId]\n\n\nclass EvaluateParams(TypedDict):\n    \"\"\"Parameters for evaluate command.\"\"\"\n\n    expression: str\n    objectGroup: NotRequired[str]\n    includeCommandLineAPI: NotRequired[bool]\n    silent: NotRequired[bool]\n    contextId: NotRequired[ExecutionContextId]\n    returnByValue: NotRequired[bool]\n    generatePreview: NotRequired[bool]\n    userGesture: NotRequired[bool]\n    awaitPromise: NotRequired[bool]\n    throwOnSideEffect: NotRequired[bool]\n    timeout: NotRequired[TimeDelta]\n    disableBreaks: NotRequired[bool]\n    replMode: NotRequired[bool]\n    allowUnsafeEvalBlockedByCSP: NotRequired[bool]\n    uniqueContextId: NotRequired[str]\n    serializationOptions: NotRequired[SerializationOptions]\n\n\nclass GetExceptionDetailsParams(TypedDict):\n    \"\"\"Parameters for getExceptionDetails command.\"\"\"\n\n    errorObjectId: RemoteObjectId\n\n\nclass GetPropertiesParams(TypedDict):\n    \"\"\"Parameters for getProperties command.\"\"\"\n\n    objectId: RemoteObjectId\n    ownProperties: NotRequired[bool]\n    accessorPropertiesOnly: NotRequired[bool]\n    generatePreview: NotRequired[bool]\n    nonIndexedPropertiesOnly: NotRequired[bool]\n\n\nclass GlobalLexicalScopeNamesParams(TypedDict, total=False):\n    \"\"\"Parameters for globalLexicalScopeNames command.\"\"\"\n\n    executionContextId: ExecutionContextId\n\n\nclass QueryObjectsParams(TypedDict):\n    \"\"\"Parameters for queryObjects command.\"\"\"\n\n    prototypeObjectId: RemoteObjectId\n    objectGroup: NotRequired[str]\n\n\nclass ReleaseObjectParams(TypedDict):\n    \"\"\"Parameters for releaseObject command.\"\"\"\n\n    objectId: RemoteObjectId\n\n\nclass ReleaseObjectGroupParams(TypedDict):\n    \"\"\"Parameters for releaseObjectGroup command.\"\"\"\n\n    objectGroup: str\n\n\nclass RemoveBindingParams(TypedDict):\n    \"\"\"Parameters for removeBinding command.\"\"\"\n\n    name: str\n\n\nclass RunScriptParams(TypedDict):\n    \"\"\"Parameters for runScript command.\"\"\"\n\n    scriptId: ScriptId\n    executionContextId: NotRequired[ExecutionContextId]\n    objectGroup: NotRequired[str]\n    silent: NotRequired[bool]\n    includeCommandLineAPI: NotRequired[bool]\n    returnByValue: NotRequired[bool]\n    generatePreview: NotRequired[bool]\n    awaitPromise: NotRequired[bool]\n\n\nclass SetAsyncCallStackDepthParams(TypedDict):\n    \"\"\"Parameters for setAsyncCallStackDepth command.\"\"\"\n\n    maxDepth: int\n\n\nclass SetCustomObjectFormatterEnabledParams(TypedDict):\n    \"\"\"Parameters for setCustomObjectFormatterEnabled command.\"\"\"\n\n    enabled: bool\n\n\nclass SetMaxCallStackSizeToCaptureParams(TypedDict):\n    \"\"\"Parameters for setMaxCallStackSizeToCapture command.\"\"\"\n\n    size: int\n\n\n# Result types\nclass AwaitPromiseResult(TypedDict):\n    \"\"\"Result for awaitPromise command.\"\"\"\n\n    result: RemoteObject\n    exceptionDetails: NotRequired[ExceptionDetails]\n\n\nclass CallFunctionOnResult(TypedDict):\n    \"\"\"Result for callFunctionOn command.\"\"\"\n\n    result: RemoteObject\n    exceptionDetails: NotRequired[ExceptionDetails]\n\n\nclass CompileScriptResult(TypedDict, total=False):\n    \"\"\"Result for compileScript command.\"\"\"\n\n    scriptId: ScriptId\n    exceptionDetails: ExceptionDetails\n\n\nclass EvaluateResult(TypedDict):\n    \"\"\"Result for evaluate command.\"\"\"\n\n    result: RemoteObject\n    exceptionDetails: NotRequired[ExceptionDetails]\n\n\nclass GetExceptionDetailsResult(TypedDict, total=False):\n    \"\"\"Result for getExceptionDetails command.\"\"\"\n\n    exceptionDetails: ExceptionDetails\n\n\nclass GetHeapUsageResult(TypedDict):\n    \"\"\"Result for getHeapUsage command.\"\"\"\n\n    usedSize: float\n    totalSize: float\n    embedderHeapUsedSize: float\n    backingStorageSize: float\n\n\nclass GetIsolateIdResult(TypedDict):\n    \"\"\"Result for getIsolateId command.\"\"\"\n\n    id: str\n\n\nclass GetPropertiesResult(TypedDict):\n    \"\"\"Result for getProperties command.\"\"\"\n\n    result: list[PropertyDescriptor]\n    internalProperties: NotRequired[list[InternalPropertyDescriptor]]\n    privateProperties: NotRequired[list[PrivatePropertyDescriptor]]\n    exceptionDetails: NotRequired[ExceptionDetails]\n\n\nclass GlobalLexicalScopeNamesResult(TypedDict):\n    \"\"\"Result for globalLexicalScopeNames command.\"\"\"\n\n    names: list[str]\n\n\nclass QueryObjectsResult(TypedDict):\n    \"\"\"Result for queryObjects command.\"\"\"\n\n    objects: RemoteObject\n\n\nclass RunScriptResult(TypedDict):\n    \"\"\"Result for runScript command.\"\"\"\n\n    result: RemoteObject\n    exceptionDetails: NotRequired[ExceptionDetails]\n\n\n# Response types\nAwaitPromiseResponse = Response[AwaitPromiseResult]\nCallFunctionOnResponse = Response[CallFunctionOnResult]\nCompileScriptResponse = Response[CompileScriptResult]\nEvaluateResponse = Response[EvaluateResult]\nGetExceptionDetailsResponse = Response[GetExceptionDetailsResult]\nGetHeapUsageResponse = Response[GetHeapUsageResult]\nGetIsolateIdResponse = Response[GetIsolateIdResult]\nGetPropertiesResponse = Response[GetPropertiesResult]\nGlobalLexicalScopeNamesResponse = Response[GlobalLexicalScopeNamesResult]\nQueryObjectsResponse = Response[QueryObjectsResult]\nRunScriptResponse = Response[RunScriptResult]\n\n\n# Command types\nAddBindingCommand = Command[AddBindingParams, Response[EmptyResponse]]\nAwaitPromiseCommand = Command[AwaitPromiseParams, AwaitPromiseResponse]\nCallFunctionOnCommand = Command[CallFunctionOnParams, CallFunctionOnResponse]\nCompileScriptCommand = Command[CompileScriptParams, CompileScriptResponse]\nDisableCommand = Command[EmptyParams, Response[EmptyResponse]]\nDiscardConsoleEntriesCommand = Command[EmptyParams, Response[EmptyResponse]]\nEnableCommand = Command[EmptyParams, Response[EmptyResponse]]\nEvaluateCommand = Command[EvaluateParams, EvaluateResponse]\nGetExceptionDetailsCommand = Command[GetExceptionDetailsParams, GetExceptionDetailsResponse]\nGetHeapUsageCommand = Command[EmptyParams, GetHeapUsageResponse]\nGetIsolateIdCommand = Command[EmptyParams, GetIsolateIdResponse]\nGetPropertiesCommand = Command[GetPropertiesParams, GetPropertiesResponse]\nGlobalLexicalScopeNamesCommand = Command[\n    GlobalLexicalScopeNamesParams, GlobalLexicalScopeNamesResponse\n]\nQueryObjectsCommand = Command[QueryObjectsParams, QueryObjectsResponse]\nReleaseObjectCommand = Command[ReleaseObjectParams, Response[EmptyResponse]]\nReleaseObjectGroupCommand = Command[ReleaseObjectGroupParams, Response[EmptyResponse]]\nRemoveBindingCommand = Command[RemoveBindingParams, Response[EmptyResponse]]\nRunIfWaitingForDebuggerCommand = Command[EmptyParams, Response[EmptyResponse]]\nRunScriptCommand = Command[RunScriptParams, RunScriptResponse]\nSetAsyncCallStackDepthCommand = Command[SetAsyncCallStackDepthParams, Response[EmptyResponse]]\nSetCustomObjectFormatterEnabledCommand = Command[\n    SetCustomObjectFormatterEnabledParams, Response[EmptyResponse]\n]\nSetMaxCallStackSizeToCaptureCommand = Command[\n    SetMaxCallStackSizeToCaptureParams, Response[EmptyResponse]\n]\nTerminateExecutionCommand = Command[EmptyParams, Response[EmptyResponse]]\n"
  },
  {
    "path": "pydoll/protocol/runtime/types.py",
    "content": "from enum import Enum\nfrom typing import Any\n\nfrom typing_extensions import NotRequired, TypedDict\n\nScriptId = str\nRemoteObjectId = str\nUnserializableValue = str\nExecutionContextId = int\nTimestamp = float\nTimeDelta = float\nUniqueDebuggerId = str\n\n\nclass SerializationType(str, Enum):\n    \"\"\"Serialization types.\"\"\"\n\n    DEEP = 'deep'\n    JSON = 'json'\n    ID_ONLY = 'idOnly'\n\n\nclass DeepSerializedValueType(str, Enum):\n    \"\"\"Deep serialized value types.\"\"\"\n\n    UNDEFINED = 'undefined'\n    NULL = 'null'\n    STRING = 'string'\n    NUMBER = 'number'\n    BOOLEAN = 'boolean'\n    BIGINT = 'bigint'\n    REGEXP = 'regexp'\n    DATE = 'date'\n    SYMBOL = 'symbol'\n    ARRAY = 'array'\n    OBJECT = 'object'\n    FUNCTION = 'function'\n    MAP = 'map'\n    SET = 'set'\n    WEAKMAP = 'weakmap'\n    WEAKSET = 'weakset'\n    ERROR = 'error'\n    PROXY = 'proxy'\n    PROMISE = 'promise'\n    TYPEDARRAY = 'typedarray'\n    ARRAYBUFFER = 'arraybuffer'\n    NODE = 'node'\n    WINDOW = 'window'\n    GENERATOR = 'generator'\n\n\nclass RemoteObjectType(str, Enum):\n    \"\"\"Remote object types.\"\"\"\n\n    OBJECT = 'object'\n    FUNCTION = 'function'\n    UNDEFINED = 'undefined'\n    STRING = 'string'\n    NUMBER = 'number'\n    BOOLEAN = 'boolean'\n    SYMBOL = 'symbol'\n    BIGINT = 'bigint'\n\n\nclass RemoteObjectSubtype(str, Enum):\n    \"\"\"Remote object subtypes.\"\"\"\n\n    ARRAY = 'array'\n    NULL = 'null'\n    NODE = 'node'\n    REGEXP = 'regexp'\n    DATE = 'date'\n    MAP = 'map'\n    SET = 'set'\n    WEAKMAP = 'weakmap'\n    WEAKSET = 'weakset'\n    ITERATOR = 'iterator'\n    GENERATOR = 'generator'\n    ERROR = 'error'\n    PROXY = 'proxy'\n    PROMISE = 'promise'\n    TYPEDARRAY = 'typedarray'\n    ARRAYBUFFER = 'arraybuffer'\n    DATAVIEW = 'dataview'\n    WEBASSEMBLYMEMORY = 'webassemblymemory'\n    WASMVALUE = 'wasmvalue'\n\n\nclass ObjectPreviewType(str, Enum):\n    \"\"\"Object preview types.\"\"\"\n\n    OBJECT = 'object'\n    FUNCTION = 'function'\n    UNDEFINED = 'undefined'\n    STRING = 'string'\n    NUMBER = 'number'\n    BOOLEAN = 'boolean'\n    SYMBOL = 'symbol'\n    BIGINT = 'bigint'\n\n\nclass ObjectPreviewSubtype(str, Enum):\n    \"\"\"Object preview subtypes.\"\"\"\n\n    ARRAY = 'array'\n    NULL = 'null'\n    NODE = 'node'\n    REGEXP = 'regexp'\n    DATE = 'date'\n    MAP = 'map'\n    SET = 'set'\n    WEAKMAP = 'weakmap'\n    WEAKSET = 'weakset'\n    ITERATOR = 'iterator'\n    GENERATOR = 'generator'\n    ERROR = 'error'\n    PROXY = 'proxy'\n    PROMISE = 'promise'\n    TYPEDARRAY = 'typedarray'\n    ARRAYBUFFER = 'arraybuffer'\n    DATAVIEW = 'dataview'\n    WEBASSEMBLYMEMORY = 'webassemblymemory'\n    WASMVALUE = 'wasmvalue'\n\n\nclass PropertyPreviewType(str, Enum):\n    \"\"\"Property preview types.\"\"\"\n\n    OBJECT = 'object'\n    FUNCTION = 'function'\n    UNDEFINED = 'undefined'\n    STRING = 'string'\n    NUMBER = 'number'\n    BOOLEAN = 'boolean'\n    SYMBOL = 'symbol'\n    ACCESSOR = 'accessor'\n    BIGINT = 'bigint'\n\n\nclass PropertyPreviewSubtype(str, Enum):\n    \"\"\"Property preview subtypes.\"\"\"\n\n    ARRAY = 'array'\n    NULL = 'null'\n    NODE = 'node'\n    REGEXP = 'regexp'\n    DATE = 'date'\n    MAP = 'map'\n    SET = 'set'\n    WEAKMAP = 'weakmap'\n    WEAKSET = 'weakset'\n    ITERATOR = 'iterator'\n    GENERATOR = 'generator'\n    ERROR = 'error'\n    PROXY = 'proxy'\n    PROMISE = 'promise'\n    TYPEDARRAY = 'typedarray'\n    ARRAYBUFFER = 'arraybuffer'\n    DATAVIEW = 'dataview'\n    WEBASSEMBLYMEMORY = 'webassemblymemory'\n    WASMVALUE = 'wasmvalue'\n\n\nclass SerializationOptions(TypedDict):\n    \"\"\"Represents options for serialization.\"\"\"\n\n    serialization: SerializationType\n    maxDepth: NotRequired[int]\n    additionalParameters: NotRequired[dict[str, Any]]\n\n\nclass DeepSerializedValue(TypedDict):\n    \"\"\"Represents deep serialized value.\"\"\"\n\n    type: DeepSerializedValueType\n    value: NotRequired[Any]\n    objectId: NotRequired[str]\n    weakLocalObjectReference: NotRequired[int]\n\n\nclass CustomPreview(TypedDict):\n    \"\"\"Custom preview for objects.\"\"\"\n\n    header: str\n    bodyGetterId: NotRequired[RemoteObjectId]\n\n\nclass PropertyPreview(TypedDict):\n    \"\"\"Property preview for objects.\"\"\"\n\n    name: str\n    type: PropertyPreviewType\n    value: NotRequired[str]\n    valuePreview: NotRequired['ObjectPreview']\n    subtype: NotRequired[PropertyPreviewSubtype]\n\n\nclass EntryPreview(TypedDict):\n    \"\"\"Entry preview for collections.\"\"\"\n\n    value: 'ObjectPreview'\n    key: NotRequired['ObjectPreview']\n\n\nclass ObjectPreview(TypedDict):\n    \"\"\"Object containing abbreviated remote object value.\"\"\"\n\n    type: ObjectPreviewType\n    overflow: bool\n    properties: list[PropertyPreview]\n    subtype: NotRequired[ObjectPreviewSubtype]\n    description: NotRequired[str]\n    entries: NotRequired[list[EntryPreview]]\n\n\nclass RemoteObject(TypedDict):\n    \"\"\"Mirror object referencing original JavaScript object.\"\"\"\n\n    type: RemoteObjectType\n    subtype: NotRequired[RemoteObjectSubtype]\n    className: NotRequired[str]\n    value: NotRequired[Any]\n    unserializableValue: NotRequired[UnserializableValue]\n    description: NotRequired[str]\n    deepSerializedValue: NotRequired[DeepSerializedValue]\n    objectId: NotRequired[RemoteObjectId]\n    preview: NotRequired[ObjectPreview]\n    customPreview: NotRequired[CustomPreview]\n\n\nclass PropertyDescriptor(TypedDict):\n    \"\"\"Object property descriptor.\"\"\"\n\n    name: str\n    configurable: bool\n    enumerable: bool\n    value: NotRequired[RemoteObject]\n    writable: NotRequired[bool]\n    get: NotRequired[RemoteObject]\n    set: NotRequired[RemoteObject]\n    wasThrown: NotRequired[bool]\n    isOwn: NotRequired[bool]\n    symbol: NotRequired[RemoteObject]\n\n\nclass InternalPropertyDescriptor(TypedDict):\n    \"\"\"Object internal property descriptor.\"\"\"\n\n    name: str\n    value: NotRequired[RemoteObject]\n\n\nclass PrivatePropertyDescriptor(TypedDict):\n    \"\"\"Object private field descriptor.\"\"\"\n\n    name: str\n    value: NotRequired[RemoteObject]\n    get: NotRequired[RemoteObject]\n    set: NotRequired[RemoteObject]\n\n\nclass CallArgument(TypedDict, total=False):\n    \"\"\"Represents function call argument.\"\"\"\n\n    value: Any\n    unserializableValue: UnserializableValue\n    objectId: RemoteObjectId\n\n\nclass ExecutionContextDescription(TypedDict):\n    \"\"\"Description of an isolated world.\"\"\"\n\n    id: ExecutionContextId\n    origin: str\n    name: str\n    uniqueId: str\n    auxData: NotRequired[dict[str, Any]]\n\n\nclass ExceptionDetails(TypedDict):\n    \"\"\"Detailed information about exception.\"\"\"\n\n    exceptionId: int\n    text: str\n    lineNumber: int\n    columnNumber: int\n    scriptId: NotRequired[ScriptId]\n    url: NotRequired[str]\n    stackTrace: NotRequired['StackTrace']\n    exception: NotRequired[RemoteObject]\n    executionContextId: NotRequired[ExecutionContextId]\n    exceptionMetaData: NotRequired[dict[str, Any]]\n\n\nclass CallFrame(TypedDict):\n    \"\"\"Stack entry for runtime errors and assertions.\"\"\"\n\n    functionName: str\n    scriptId: ScriptId\n    url: str\n    lineNumber: int\n    columnNumber: int\n\n\nclass StackTraceId(TypedDict):\n    \"\"\"Stack trace identifier.\"\"\"\n\n    id: str\n    debuggerId: NotRequired[UniqueDebuggerId]\n\n\nclass StackTrace(TypedDict):\n    \"\"\"Call frames for assertions or error messages.\"\"\"\n\n    callFrames: list[CallFrame]\n    description: NotRequired[str]\n    parent: NotRequired['StackTrace']\n    parentId: NotRequired[StackTraceId]\n"
  },
  {
    "path": "pydoll/protocol/security/types.py",
    "content": "from enum import Enum\n\n\nclass MixedContentType(str, Enum):\n    \"\"\"\n    The mixed content type of the request.\n    \"\"\"\n\n    BLOCKABLE = 'blockable'\n    OPTIONALLY_BLOCKABLE = 'optionally-blockable'\n    NONE = 'none'\n\n\nclass SecurityState(str, Enum):\n    \"\"\"\n    The security state of the page.\n    \"\"\"\n\n    UNKNOWN = 'unknown'\n    NEUTRAL = 'neutral'\n    SAFE = 'safe'\n    INSECURE = 'insecure'\n    SECURE = 'secure'\n    INFO = 'info'\n    INSECURE_BROKEN = 'insecure-broken'\n"
  },
  {
    "path": "pydoll/protocol/storage/__init__.py",
    "content": "\"\"\"Storage domain implementation.\"\"\"\n"
  },
  {
    "path": "pydoll/protocol/storage/events.py",
    "content": "from enum import Enum\nfrom typing import Any\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import CDPEvent\nfrom pydoll.protocol.network.types import RequestId, TimeSinceEpoch\nfrom pydoll.protocol.page.types import FrameId\nfrom pydoll.protocol.storage.types import (\n    AttributionReportingAggregatableResult,\n    AttributionReportingEventLevelResult,\n    AttributionReportingReportResult,\n    AttributionReportingSourceRegistration,\n    AttributionReportingSourceRegistrationResult,\n    AttributionReportingTriggerRegistration,\n    InterestGroupAccessType,\n    InterestGroupAuctionEventType,\n    InterestGroupAuctionFetchType,\n    InterestGroupAuctionId,\n    SharedStorageAccessMethod,\n    SharedStorageAccessParams,\n    SharedStorageAccessScope,\n    StorageBucketInfo,\n)\nfrom pydoll.protocol.target.types import TargetID\n\n\nclass StorageEvent(str, Enum):\n    \"\"\"\n    Events from the Storage domain of the Chrome DevTools Protocol.\n\n    This enumeration contains the names of Storage-related events that can be\n    received from the Chrome DevTools Protocol. These events provide information\n    about changes to various browser storage mechanisms including Cache Storage,\n    IndexedDB, Interest Groups, Shared Storage, and Storage Buckets.\n    \"\"\"\n\n    CACHE_STORAGE_CONTENT_UPDATED = 'Storage.cacheStorageContentUpdated'\n    \"\"\"\n    A cache's contents have been modified.\n\n    Args:\n        origin (str): Origin to update.\n        storageKey (str): Storage key to update.\n        bucketId (str): Storage bucket to update.\n        cacheName (str): Name of cache in origin.\n    \"\"\"\n\n    CACHE_STORAGE_LIST_UPDATED = 'Storage.cacheStorageListUpdated'\n    \"\"\"\n    A cache has been added/deleted.\n\n    Args:\n        origin (str): Origin to update.\n        storageKey (str): Storage key to update.\n        bucketId (str): Storage bucket to update.\n    \"\"\"\n\n    INDEXED_DB_CONTENT_UPDATED = 'Storage.indexedDBContentUpdated'\n    \"\"\"\n    The origin's IndexedDB object store has been modified.\n\n    Args:\n        origin (str): Origin to update.\n        storageKey (str): Storage key to update.\n        bucketId (str): Storage bucket to update.\n        databaseName (str): Database to update.\n        objectStoreName (str): ObjectStore to update.\n    \"\"\"\n\n    INDEXED_DB_LIST_UPDATED = 'Storage.indexedDBListUpdated'\n    \"\"\"\n    The origin's IndexedDB database list has been modified.\n\n    Args:\n        origin (str): Origin to update.\n        storageKey (str): Storage key to update.\n        bucketId (str): Storage bucket to update.\n    \"\"\"\n\n    INTEREST_GROUP_ACCESSED = 'Storage.interestGroupAccessed'\n    \"\"\"\n    One of the interest groups was accessed. Note that these events are global\n    to all targets sharing an interest group store.\n\n    Args:\n        accessTime (Network.TimeSinceEpoch): Time of the access.\n        type (InterestGroupAccessType): Type of access.\n        ownerOrigin (str): Owner origin.\n        name (str): Name of the interest group.\n        componentSellerOrigin (str): For topLevelBid/topLevelAdditionalBid, and when\n            appropriate, win and additionalBidWin.\n        bid (number): For bid or somethingBid event, if done locally and not on a server.\n        bidCurrency (str): Currency of the bid.\n        uniqueAuctionId (InterestGroupAuctionId): For non-global events --- links\n            to interestGroupAuctionEvent.\n    \"\"\"\n\n    INTEREST_GROUP_AUCTION_EVENT_OCCURRED = 'Storage.interestGroupAuctionEventOccurred'\n    \"\"\"\n    An auction involving interest groups is taking place. These events are target-specific.\n\n    Args:\n        eventTime (Network.TimeSinceEpoch): Time of the event.\n        type (InterestGroupAuctionEventType): Type of auction event.\n        uniqueAuctionId (InterestGroupAuctionId): Unique identifier for the auction.\n        parentAuctionId (InterestGroupAuctionId): Set for child auctions.\n        auctionConfig (object): Set for started and configResolved.\n    \"\"\"\n\n    INTEREST_GROUP_AUCTION_NETWORK_REQUEST_CREATED = (\n        'Storage.interestGroupAuctionNetworkRequestCreated'\n    )\n    \"\"\"\n    Specifies which auctions a particular network fetch may be related to, and in what role.\n    Note that it is not ordered with respect to Network.requestWillBeSent (but will happen\n    before loadingFinished loadingFailed).\n\n    Args:\n        type (InterestGroupAuctionFetchType): Type of fetch.\n        requestId (Network.RequestId): Request identifier.\n        auctions (array[InterestGroupAuctionId]): This is the set of the auctions using the\n            worklet that issued this request. In the case of trusted signals, it's possible\n            that only some of them actually care about the keys being queried.\n    \"\"\"\n\n    SHARED_STORAGE_ACCESSED = 'Storage.sharedStorageAccessed'\n    \"\"\"\n    Shared storage was accessed by the associated page. The following parameters\n    are included in all events.\n\n    Args:\n        accessTime (Network.TimeSinceEpoch): Time of the access.\n        scope (SharedStorageAccessScope): Enum value indicating the access scope.\n        method (SharedStorageAccessMethod): Enum value indicating the Shared Storage API\n            method invoked.\n        mainFrameId (Page.FrameId): DevTools Frame Token for the primary frame tree's root.\n        ownerOrigin (str): Serialization of the origin owning the Shared Storage data.\n        ownerSite (str): Serialization of the site owning the Shared Storage data.\n        params (SharedStorageAccessParams): The sub-parameters wrapped by params are all\n            optional and their presence/absence depends on type.\n    \"\"\"\n\n    SHARED_STORAGE_WORKLET_OPERATION_EXECUTION_FINISHED = (\n        'Storage.sharedStorageWorkletOperationExecutionFinished'\n    )\n    \"\"\"\n    A shared storage run or selectURL operation finished its execution.\n    The following parameters are included in all events.\n\n    Args:\n        finishedTime (Network.TimeSinceEpoch): Time that the operation finished.\n        executionTime (int): Time, in microseconds, from start of shared storage JS API\n            call until end of operation execution in the worklet.\n        method (SharedStorageAccessMethod): Enum value indicating the Shared Storage API\n            method invoked.\n        operationId (str): ID of the operation call.\n        workletTargetId (Target.TargetID): Hex representation of the DevTools token used\n            as the TargetID for the associated shared storage worklet.\n        mainFrameId (Page.FrameId): DevTools Frame Token for the primary frame tree's root.\n        ownerOrigin (str): Serialization of the origin owning the Shared Storage data.\n    \"\"\"\n\n    STORAGE_BUCKET_CREATED_OR_UPDATED = 'Storage.storageBucketCreatedOrUpdated'\n    \"\"\"\n    Fired when a storage bucket is created or updated.\n\n    Args:\n        bucketInfo (StorageBucketInfo): Information about the storage bucket.\n    \"\"\"\n\n    STORAGE_BUCKET_DELETED = 'Storage.storageBucketDeleted'\n    \"\"\"\n    Fired when a storage bucket is deleted.\n\n    Args:\n        bucketId (str): ID of the deleted storage bucket.\n    \"\"\"\n\n    ATTRIBUTION_REPORTING_SOURCE_REGISTERED = 'Storage.attributionReportingSourceRegistered'\n    \"\"\"\n    Fired when an attribution source is registered.\n\n    Args:\n        registration (AttributionReportingSourceRegistration): Registration details.\n        result (AttributionReportingSourceRegistrationResult): Result of the registration.\n    \"\"\"\n\n    ATTRIBUTION_REPORTING_TRIGGER_REGISTERED = 'Storage.attributionReportingTriggerRegistered'\n    \"\"\"\n    Fired when an attribution trigger is registered.\n\n    Args:\n        registration (AttributionReportingTriggerRegistration): Registration details.\n        eventLevel (AttributionReportingEventLevelResult): Event level result.\n        aggregatable (AttributionReportingAggregatableResult): Aggregatable result.\n    \"\"\"\n\n    ATTRIBUTION_REPORTING_REPORT_SENT = 'Storage.attributionReportingReportSent'\n    \"\"\"\n    Fired when an attribution report is sent.\n\n    Args:\n        url (str): URL the report was sent to.\n        body (object): Body of the report.\n        result (AttributionReportingReportResult): Result of the report sending.\n        netError (int): If result is sent, populated with net/HTTP status.\n        netErrorName (str): Name of the network error if any.\n        httpStatusCode (int): HTTP status code if available.\n    \"\"\"\n\n    ATTRIBUTION_REPORTING_VERBOSE_DEBUG_REPORT_SENT = (\n        'Storage.attributionReportingVerboseDebugReportSent'\n    )\n    \"\"\"\n    Fired when a verbose debug report is sent for an attribution source.\n\n    Args:\n        url (str): URL the report was sent to.\n        body (array[object]): Body of the report.\n        netError (int): If result is sent, populated with net/HTTP status.\n        netErrorName (str): Name of the network error if any.\n        httpStatusCode (int): HTTP status code if available.\n    \"\"\"\n\n\nclass CacheStorageContentUpdatedEventParams(TypedDict):\n    origin: str\n    storageKey: str\n    bucketId: str\n    cacheName: str\n\n\nclass CacheStorageListUpdatedEventParams(TypedDict):\n    origin: str\n    storageKey: str\n    bucketId: str\n\n\nclass IndexedDBContentUpdatedEventParams(TypedDict):\n    origin: str\n    storageKey: str\n    bucketId: str\n    databaseName: str\n    objectStoreName: str\n\n\nclass IndexedDBListUpdatedEventParams(TypedDict):\n    origin: str\n    storageKey: str\n    bucketId: str\n\n\nclass InterestGroupAccessedEventParams(TypedDict):\n    accessTime: TimeSinceEpoch\n    type: InterestGroupAccessType\n    ownerOrigin: str\n    name: str\n    componentSellerOrigin: NotRequired[str]\n    bid: NotRequired[float]\n    bidCurrency: NotRequired[str]\n    uniqueAuctionId: NotRequired[InterestGroupAuctionId]\n\n\nclass InterestGroupAuctionEventOccurredEventParams(TypedDict):\n    eventTime: TimeSinceEpoch\n    type: InterestGroupAuctionEventType\n    uniqueAuctionId: InterestGroupAuctionId\n    parentAuctionId: NotRequired[InterestGroupAuctionId]\n    auctionConfig: NotRequired[dict[str, Any]]\n\n\nclass InterestGroupAuctionNetworkRequestCreatedEventParams(TypedDict):\n    type: InterestGroupAuctionFetchType\n    requestId: RequestId\n    auctions: list[InterestGroupAuctionId]\n\n\nclass SharedStorageAccessedEventParams(TypedDict):\n    accessTime: TimeSinceEpoch\n    scope: SharedStorageAccessScope\n    method: SharedStorageAccessMethod\n    mainFrameId: FrameId\n    ownerOrigin: str\n    ownerSite: str\n    params: SharedStorageAccessParams\n\n\nclass SharedStorageWorkletOperationExecutionFinishedEventParams(TypedDict):\n    finishedTime: TimeSinceEpoch\n    executionTime: int\n    method: SharedStorageAccessMethod\n    operationId: str\n    workletTargetId: TargetID\n    mainFrameId: FrameId\n    ownerOrigin: str\n\n\nclass StorageBucketCreatedOrUpdatedEventParams(TypedDict):\n    bucketInfo: StorageBucketInfo\n\n\nclass StorageBucketDeletedEventParams(TypedDict):\n    bucketId: str\n\n\nclass AttributionReportingSourceRegisteredEventParams(TypedDict):\n    registration: AttributionReportingSourceRegistration\n    result: AttributionReportingSourceRegistrationResult\n\n\nclass AttributionReportingTriggerRegisteredEventParams(TypedDict):\n    registration: AttributionReportingTriggerRegistration\n    eventLevel: AttributionReportingEventLevelResult\n    aggregatable: AttributionReportingAggregatableResult\n\n\nclass AttributionReportingReportSentEventParams(TypedDict):\n    url: str\n    body: dict[str, Any]\n    result: AttributionReportingReportResult\n    netError: NotRequired[int]\n    netErrorName: NotRequired[str]\n    httpStatusCode: NotRequired[int]\n\n\nclass AttributionReportingVerboseDebugReportSentEventParams(TypedDict):\n    url: str\n    body: NotRequired[list[dict[str, Any]]]\n    netError: NotRequired[int]\n    netErrorName: NotRequired[str]\n    httpStatusCode: NotRequired[int]\n\n\nCacheStorageContentUpdated = CDPEvent[CacheStorageContentUpdatedEventParams]\nCacheStorageListUpdated = CDPEvent[CacheStorageListUpdatedEventParams]\nIndexedDBContentUpdated = CDPEvent[IndexedDBContentUpdatedEventParams]\nIndexedDBListUpdated = CDPEvent[IndexedDBListUpdatedEventParams]\nInterestGroupAccessed = CDPEvent[InterestGroupAccessedEventParams]\nInterestGroupAuctionEventOccurred = CDPEvent[InterestGroupAuctionEventOccurredEventParams]\nInterestGroupAuctionNetworkRequestCreated = CDPEvent[\n    InterestGroupAuctionNetworkRequestCreatedEventParams\n]\nSharedStorageAccessed = CDPEvent[SharedStorageAccessedEventParams]\nSharedStorageWorkletOperationExecutionFinished = CDPEvent[\n    SharedStorageWorkletOperationExecutionFinishedEventParams\n]\nStorageBucketCreatedOrUpdated = CDPEvent[StorageBucketCreatedOrUpdatedEventParams]\nStorageBucketDeleted = CDPEvent[StorageBucketDeletedEventParams]\nAttributionReportingSourceRegistered = CDPEvent[AttributionReportingSourceRegisteredEventParams]\nAttributionReportingTriggerRegistered = CDPEvent[AttributionReportingTriggerRegisteredEventParams]\nAttributionReportingReportSent = CDPEvent[AttributionReportingReportSentEventParams]\nAttributionReportingVerboseDebugReportSent = CDPEvent[\n    AttributionReportingVerboseDebugReportSentEventParams\n]\n"
  },
  {
    "path": "pydoll/protocol/storage/methods.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import Command, EmptyParams, EmptyResponse, Response\nfrom pydoll.protocol.browser.types import BrowserContextID\nfrom pydoll.protocol.network.types import Cookie, CookieParam\nfrom pydoll.protocol.page.types import FrameId\nfrom pydoll.protocol.storage.types import (\n    RelatedWebsiteSet,\n    SerializedStorageKey,\n    SharedStorageEntry,\n    SharedStorageMetadata,\n    StorageBucket,\n    TrustTokens,\n    UsageForType,\n)\n\n\nclass StorageMethod(str, Enum):\n    CLEAR_COOKIES = 'Storage.clearCookies'\n    CLEAR_DATA_FOR_ORIGIN = 'Storage.clearDataForOrigin'\n    CLEAR_DATA_FOR_STORAGE_KEY = 'Storage.clearDataForStorageKey'\n    GET_COOKIES = 'Storage.getCookies'\n    GET_STORAGE_KEY_FOR_FRAME = 'Storage.getStorageKeyForFrame'\n    GET_USAGE_AND_QUOTA = 'Storage.getUsageAndQuota'\n    SET_COOKIES = 'Storage.setCookies'\n    SET_PROTECTED_AUDIENCE_K_ANONYMITY = 'Storage.setProtectedAudienceKAnonymity'\n    TRACK_CACHE_STORAGE_FOR_ORIGIN = 'Storage.trackCacheStorageForOrigin'\n    TRACK_CACHE_STORAGE_FOR_STORAGE_KEY = 'Storage.trackCacheStorageForStorageKey'\n    TRACK_INDEXED_DB_FOR_ORIGIN = 'Storage.trackIndexedDBForOrigin'\n    TRACK_INDEXED_DB_FOR_STORAGE_KEY = 'Storage.trackIndexedDBForStorageKey'\n    UNTRACK_CACHE_STORAGE_FOR_ORIGIN = 'Storage.untrackCacheStorageForOrigin'\n    UNTRACK_CACHE_STORAGE_FOR_STORAGE_KEY = 'Storage.untrackCacheStorageForStorageKey'\n    UNTRACK_INDEXED_DB_FOR_ORIGIN = 'Storage.untrackIndexedDBForOrigin'\n    UNTRACK_INDEXED_DB_FOR_STORAGE_KEY = 'Storage.untrackIndexedDBForStorageKey'\n    CLEAR_SHARED_STORAGE_ENTRIES = 'Storage.clearSharedStorageEntries'\n    CLEAR_TRUST_TOKENS = 'Storage.clearTrustTokens'\n    DELETE_SHARED_STORAGE_ENTRY = 'Storage.deleteSharedStorageEntry'\n    DELETE_STORAGE_BUCKET = 'Storage.deleteStorageBucket'\n    GET_AFFECTED_URLS_FOR_THIRD_PARTY_COOKIE_METADATA = (\n        'Storage.getAffectedUrlsForThirdPartyCookieMetadata'\n    )\n    GET_INTEREST_GROUP_DETAILS = 'Storage.getInterestGroupDetails'\n    GET_RELATED_WEBSITE_SETS = 'Storage.getRelatedWebsiteSets'\n    GET_SHARED_STORAGE_ENTRIES = 'Storage.getSharedStorageEntries'\n    GET_SHARED_STORAGE_METADATA = 'Storage.getSharedStorageMetadata'\n    GET_TRUST_TOKENS = 'Storage.getTrustTokens'\n    OVERRIDE_QUOTA_FOR_ORIGIN = 'Storage.overrideQuotaForOrigin'\n    RESET_SHARED_STORAGE_BUDGET = 'Storage.resetSharedStorageBudget'\n    RUN_BOUNCE_TRACKING_MITIGATIONS = 'Storage.runBounceTrackingMitigations'\n    SEND_PENDING_ATTRIBUTION_REPORTS = 'Storage.sendPendingAttributionReports'\n    SET_ATTRIBUTION_REPORTING_LOCAL_TESTING_MODE = 'Storage.setAttributionReportingLocalTestingMode'\n    SET_ATTRIBUTION_REPORTING_TRACKING = 'Storage.setAttributionReportingTracking'\n    SET_INTEREST_GROUP_AUCTION_TRACKING = 'Storage.setInterestGroupAuctionTracking'\n    SET_INTEREST_GROUP_TRACKING = 'Storage.setInterestGroupTracking'\n    SET_SHARED_STORAGE_ENTRY = 'Storage.setSharedStorageEntry'\n    SET_SHARED_STORAGE_TRACKING = 'Storage.setSharedStorageTracking'\n    SET_STORAGE_BUCKET_TRACKING = 'Storage.setStorageBucketTracking'\n\n\nclass GetStorageKeyForFrameParams(TypedDict):\n    frameId: FrameId\n\n\nclass GetStorageKeyForFrameResult(TypedDict):\n    storageKey: SerializedStorageKey\n\n\nclass ClearDataForOriginParams(TypedDict):\n    origin: str\n    storageTypes: str\n\n\nclass ClearDataForStorageKeyParams(TypedDict):\n    storageKey: str\n    storageTypes: str\n\n\nclass GetCookiesParams(TypedDict):\n    browserContextId: NotRequired[BrowserContextID]\n\n\nclass GetCookiesResult(TypedDict):\n    cookies: list[Cookie]\n\n\nclass SetCookiesParams(TypedDict):\n    cookies: list[CookieParam]\n    browserContextId: NotRequired[BrowserContextID]\n\n\nclass ClearCookiesParams(TypedDict):\n    browserContextId: NotRequired[BrowserContextID]\n\n\nclass GetUsageAndQuotaParams(TypedDict):\n    origin: str\n\n\nclass GetUsageAndQuotaResult(TypedDict):\n    usage: float\n    quota: float\n    overrideActive: bool\n    usageBreakdown: list[UsageForType]\n\n\nclass OverrideQuotaForOriginParams(TypedDict):\n    origin: str\n    quotaSize: NotRequired[float]\n\n\nclass TrackCacheStorageForOriginParams(TypedDict):\n    origin: str\n\n\nclass TrackCacheStorageForStorageKeyParams(TypedDict):\n    storageKey: str\n\n\nclass TrackIndexedDBForOriginParams(TypedDict):\n    origin: str\n\n\nclass TrackIndexedDBForStorageKeyParams(TypedDict):\n    storageKey: str\n\n\nclass UntrackCacheStorageForOriginParams(TypedDict):\n    origin: str\n\n\nclass UntrackCacheStorageForStorageKeyParams(TypedDict):\n    storageKey: str\n\n\nclass UntrackIndexedDBForOriginParams(TypedDict):\n    origin: str\n\n\nclass UntrackIndexedDBForStorageKeyParams(TypedDict):\n    storageKey: str\n\n\nclass GetTrustTokensResult(TypedDict):\n    tokens: list[TrustTokens]\n\n\nclass ClearTrustTokensParams(TypedDict):\n    issuerOrigin: str\n\n\nclass ClearTrustTokensResult(TypedDict):\n    didDeleteTokens: bool\n\n\nclass GetInterestGroupDetailsParams(TypedDict):\n    ownerOrigin: str\n    name: str\n\n\nclass GetInterestGroupDetailsResult(TypedDict):\n    details: dict\n\n\nclass SetInterestGroupTrackingParams(TypedDict):\n    enable: bool\n\n\nclass SetInterestGroupAuctionTrackingParams(TypedDict):\n    enable: bool\n\n\nclass GetSharedStorageMetadataParams(TypedDict):\n    ownerOrigin: str\n\n\nclass GetSharedStorageMetadataResult(TypedDict):\n    metadata: SharedStorageMetadata\n\n\nclass GetSharedStorageEntriesParams(TypedDict):\n    ownerOrigin: str\n\n\nclass GetSharedStorageEntriesResult(TypedDict):\n    entries: list[SharedStorageEntry]\n\n\nclass SetSharedStorageEntryParams(TypedDict):\n    ownerOrigin: str\n    key: str\n    value: str\n    ignoreIfPresent: NotRequired[bool]\n\n\nclass DeleteSharedStorageEntryParams(TypedDict):\n    ownerOrigin: str\n    key: str\n\n\nclass ClearSharedStorageEntriesParams(TypedDict):\n    ownerOrigin: str\n\n\nclass ResetSharedStorageBudgetParams(TypedDict):\n    ownerOrigin: str\n\n\nclass SetSharedStorageTrackingParams(TypedDict):\n    enable: bool\n\n\nclass SetStorageBucketTrackingParams(TypedDict):\n    storageKey: str\n    enable: bool\n\n\nclass DeleteStorageBucketParams(TypedDict):\n    bucket: StorageBucket\n\n\nclass RunBounceTrackingMitigationsResult(TypedDict):\n    deletedSites: list[str]\n\n\nclass SetAttributionReportingLocalTestingModeParams(TypedDict):\n    enabled: bool\n\n\nclass SetAttributionReportingTrackingParams(TypedDict):\n    enable: bool\n\n\nclass SendPendingAttributionReportsResult(TypedDict):\n    numSent: int\n\n\nclass GetRelatedWebsiteSetsResult(TypedDict):\n    sets: list[RelatedWebsiteSet]\n\n\nclass GetAffectedUrlsForThirdPartyCookieMetadataParams(TypedDict):\n    firstPartyUrl: str\n    thirdPartyUrls: list[str]\n\n\nclass GetAffectedUrlsForThirdPartyCookieMetadataResult(TypedDict):\n    matchedUrls: list[str]\n\n\nclass SetProtectedAudienceKAnonymityParams(TypedDict):\n    owner: str\n    name: str\n    hashes: list[str]\n\n\nGetStorageKeyForFrameResponse = Response[GetStorageKeyForFrameResult]\nGetCookiesResponse = Response[GetCookiesResult]\nGetUsageAndQuotaResponse = Response[GetUsageAndQuotaResult]\nGetTrustTokensResponse = Response[GetTrustTokensResult]\nGetInterestGroupDetailsResponse = Response[GetInterestGroupDetailsResult]\nGetSharedStorageMetadataResponse = Response[GetSharedStorageMetadataResult]\nGetSharedStorageEntriesResponse = Response[GetSharedStorageEntriesResult]\nRunBounceTrackingMitigationsResponse = Response[RunBounceTrackingMitigationsResult]\nSendPendingAttributionReportsResponse = Response[SendPendingAttributionReportsResult]\nGetRelatedWebsiteSetsResponse = Response[GetRelatedWebsiteSetsResult]\nGetAffectedUrlsForThirdPartyCookieMetadataResponse = Response[\n    GetAffectedUrlsForThirdPartyCookieMetadataResult\n]\n\n\nGetStorageKeyForFrameCommand = Command[GetStorageKeyForFrameParams, GetStorageKeyForFrameResponse]\nClearDataForOriginCommand = Command[ClearDataForOriginParams, Response[EmptyResponse]]\nClearDataForStorageKeyCommand = Command[ClearDataForStorageKeyParams, Response[EmptyResponse]]\nGetCookiesCommand = Command[GetCookiesParams, GetCookiesResponse]\nSetCookiesCommand = Command[SetCookiesParams, Response[EmptyResponse]]\nClearCookiesCommand = Command[ClearCookiesParams, Response[EmptyResponse]]\nGetUsageAndQuotaCommand = Command[GetUsageAndQuotaParams, GetUsageAndQuotaResponse]\nOverrideQuotaForOriginCommand = Command[OverrideQuotaForOriginParams, Response[EmptyResponse]]\nTrackCacheStorageForOriginCommand = Command[\n    TrackCacheStorageForOriginParams, Response[EmptyResponse]\n]\nTrackCacheStorageForStorageKeyCommand = Command[\n    TrackCacheStorageForStorageKeyParams, Response[EmptyResponse]\n]\nTrackIndexedDBForOriginCommand = Command[TrackIndexedDBForOriginParams, Response[EmptyResponse]]\nTrackIndexedDBForStorageKeyCommand = Command[\n    TrackIndexedDBForStorageKeyParams, Response[EmptyResponse]\n]\nUntrackCacheStorageForOriginCommand = Command[\n    UntrackCacheStorageForOriginParams, Response[EmptyResponse]\n]\nUntrackCacheStorageForStorageKeyCommand = Command[\n    UntrackCacheStorageForStorageKeyParams, Response[EmptyResponse]\n]\nUntrackIndexedDBForOriginCommand = Command[UntrackIndexedDBForOriginParams, Response[EmptyResponse]]\nUntrackIndexedDBForStorageKeyCommand = Command[\n    UntrackIndexedDBForStorageKeyParams, Response[EmptyResponse]\n]\nGetTrustTokensCommand = Command[EmptyParams, GetTrustTokensResponse]\nClearTrustTokensCommand = Command[ClearTrustTokensParams, Response[EmptyResponse]]\nGetInterestGroupDetailsCommand = Command[\n    GetInterestGroupDetailsParams, GetInterestGroupDetailsResponse\n]\nSetInterestGroupTrackingCommand = Command[SetInterestGroupTrackingParams, Response[EmptyResponse]]\nSetInterestGroupAuctionTrackingCommand = Command[\n    SetInterestGroupAuctionTrackingParams, Response[EmptyResponse]\n]\nGetSharedStorageMetadataCommand = Command[\n    GetSharedStorageMetadataParams, GetSharedStorageMetadataResponse\n]\nGetSharedStorageEntriesCommand = Command[\n    GetSharedStorageEntriesParams, GetSharedStorageEntriesResponse\n]\nSetSharedStorageEntryCommand = Command[SetSharedStorageEntryParams, Response[EmptyResponse]]\nDeleteSharedStorageEntryCommand = Command[DeleteSharedStorageEntryParams, Response[EmptyResponse]]\nClearSharedStorageEntriesCommand = Command[ClearSharedStorageEntriesParams, Response[EmptyResponse]]\nResetSharedStorageBudgetCommand = Command[ResetSharedStorageBudgetParams, Response[EmptyResponse]]\nSetSharedStorageTrackingCommand = Command[SetSharedStorageTrackingParams, Response[EmptyResponse]]\nSetStorageBucketTrackingCommand = Command[SetStorageBucketTrackingParams, Response[EmptyResponse]]\nDeleteStorageBucketCommand = Command[DeleteStorageBucketParams, Response[EmptyResponse]]\nRunBounceTrackingMitigationsCommand = Command[EmptyParams, RunBounceTrackingMitigationsResponse]\nSetAttributionReportingLocalTestingModeCommand = Command[\n    SetAttributionReportingLocalTestingModeParams, Response[EmptyResponse]\n]\nSetAttributionReportingTrackingCommand = Command[\n    SetAttributionReportingTrackingParams, Response[EmptyResponse]\n]\nSendPendingAttributionReportsCommand = Command[EmptyParams, SendPendingAttributionReportsResponse]\nGetRelatedWebsiteSetsCommand = Command[EmptyParams, GetRelatedWebsiteSetsResponse]\nGetAffectedUrlsForThirdPartyCookieMetadataCommand = Command[\n    GetAffectedUrlsForThirdPartyCookieMetadataParams,\n    GetAffectedUrlsForThirdPartyCookieMetadataResponse,\n]\nSetProtectedAudienceKAnonymityCommand = Command[\n    SetProtectedAudienceKAnonymityParams, Response[EmptyResponse]\n]\n"
  },
  {
    "path": "pydoll/protocol/storage/types.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.network.types import TimeSinceEpoch\nfrom pydoll.protocol.target.types import TargetID\n\nSerializedStorageKey = str\nInterestGroupAuctionId = str\n\n\nclass StorageType(str, Enum):\n    COOKIES = 'cookies'\n    FILE_SYSTEMS = 'file_systems'\n    INDEXEDDB = 'indexeddb'\n    LOCAL_STORAGE = 'local_storage'\n    SHADER_CACHE = 'shader_cache'\n    WEBSQL = 'websql'\n    SERVICE_WORKERS = 'service_workers'\n    CACHE_STORAGE = 'cache_storage'\n    INTEREST_GROUPS = 'interest_groups'\n    SHARED_STORAGE = 'shared_storage'\n    STORAGE_BUCKETS = 'storage_buckets'\n    ALL = 'all'\n    OTHER = 'other'\n\n\nclass UsageForType(TypedDict):\n    \"\"\"Usage for a storage type.\"\"\"\n\n    storageType: StorageType\n    usage: float\n\n\nclass TrustTokens(TypedDict):\n    \"\"\"Pair of issuer origin and number of available (signed, but not used) Trust\n    Tokens from that issuer.\"\"\"\n\n    issuerOrigin: str\n    count: float\n\n\nclass InterestGroupAccessType(str, Enum):\n    \"\"\"Enum of interest group access types.\"\"\"\n\n    JOIN = 'join'\n    LEAVE = 'leave'\n    UPDATE = 'update'\n    LOADED = 'loaded'\n    BID = 'bid'\n    WIN = 'win'\n    ADDITIONAL_BID = 'additionalBid'\n    ADDITIONAL_BID_WIN = 'additionalBidWin'\n    TOP_LEVEL_BID = 'topLevelBid'\n    TOP_LEVEL_ADDITIONAL_BID = 'topLevelAdditionalBid'\n    CLEAR = 'clear'\n\n\nclass InterestGroupAuctionEventType(str, Enum):\n    \"\"\"Enum of auction events.\"\"\"\n\n    STARTED = 'started'\n    CONFIG_RESOLVED = 'configResolved'\n\n\nclass InterestGroupAuctionFetchType(str, Enum):\n    \"\"\"Enum of network fetches auctions can do.\"\"\"\n\n    BIDDER_JS = 'bidderJs'\n    BIDDER_WASM = 'bidderWasm'\n    SELLER_JS = 'sellerJs'\n    BIDDER_TRUSTED_SIGNALS = 'bidderTrustedSignals'\n    SELLER_TRUSTED_SIGNALS = 'sellerTrustedSignals'\n\n\nclass SharedStorageAccessScope(str, Enum):\n    \"\"\"Enum of shared storage access scopes.\"\"\"\n\n    WINDOW = 'window'\n    SHARED_STORAGE_WORKLET = 'sharedStorageWorklet'\n    PROTECTED_AUDIENCE_WORKLET = 'protectedAudienceWorklet'\n    HEADER = 'header'\n\n\nclass SharedStorageAccessMethod(str, Enum):\n    \"\"\"Enum of shared storage access methods.\"\"\"\n\n    ADD_MODULE = 'addModule'\n    CREATE_WORKLET = 'createWorklet'\n    SELECT_URL = 'selectURL'\n    RUN = 'run'\n    BATCH_UPDATE = 'batchUpdate'\n    SET = 'set'\n    APPEND = 'append'\n    DELETE = 'delete'\n    CLEAR = 'clear'\n    GET = 'get'\n    KEYS = 'keys'\n    VALUES = 'values'\n    ENTRIES = 'entries'\n    LENGTH = 'length'\n    REMAINING_BUDGET = 'remainingBudget'\n\n\nclass SharedStorageEntry(TypedDict):\n    \"\"\"Struct for a single key-value pair in an origin's shared storage.\"\"\"\n\n    key: str\n    value: str\n\n\nclass SharedStorageMetadata(TypedDict):\n    \"\"\"Details for an origin's shared storage.\"\"\"\n\n    creationTime: TimeSinceEpoch\n    length: int\n    remainingBudget: float\n    bytesUsed: int\n\n\nclass SharedStoragePrivateAggregationConfig(TypedDict):\n    \"\"\"Represents a dictionary object passed in as privateAggregationConfig to\n    run or selectURL.\"\"\"\n\n    filteringIdMaxBytes: int\n    aggregationCoordinatorOrigin: NotRequired[str]\n    contextId: NotRequired[str]\n    maxContributions: NotRequired[int]\n\n\nclass SharedStorageReportingMetadata(TypedDict):\n    \"\"\"Pair of reporting metadata details for a candidate URL for `selectURL()`.\"\"\"\n\n    eventType: str\n    reportingUrl: str\n\n\nclass SharedStorageUrlWithMetadata(TypedDict):\n    \"\"\"Bundles a candidate URL with its reporting metadata.\"\"\"\n\n    url: str\n    reportingMetadata: list[SharedStorageReportingMetadata]\n\n\nclass SharedStorageAccessParams(TypedDict, total=False):\n    \"\"\"Bundles the parameters for shared storage access events whose\n    presence/absence can vary according to SharedStorageAccessType.\"\"\"\n\n    scriptSourceUrl: str\n    dataOrigin: str\n    operationName: str\n    operationId: str\n    keepAlive: bool\n    privateAggregationConfig: SharedStoragePrivateAggregationConfig\n    serializedData: str\n    urlsWithMetadata: list[SharedStorageUrlWithMetadata]\n    urnUuid: str\n    key: str\n    value: str\n    ignoreIfPresent: bool\n    workletOrdinal: int\n    workletTargetId: TargetID\n    withLock: str\n    batchUpdateId: str\n    batchSize: int\n\n\nclass StorageBucketsDurability(str, Enum):\n    RELAXED = 'relaxed'\n    STRICT = 'strict'\n\n\nclass StorageBucket(TypedDict):\n    storageKey: SerializedStorageKey\n    name: NotRequired[str]\n\n\nclass StorageBucketInfo(TypedDict):\n    bucket: StorageBucket\n    id: str\n    expiration: TimeSinceEpoch\n    quota: float\n    persistent: bool\n    durability: StorageBucketsDurability\n\n\nclass AttributionReportingSourceType(str, Enum):\n    NAVIGATION = 'navigation'\n    EVENT = 'event'\n\n\nUnsignedInt64AsBase10 = str\nUnsignedInt128AsBase16 = str\nSignedInt64AsBase10 = str\n\n\nclass AttributionReportingFilterDataEntry(TypedDict):\n    key: str\n    values: list[str]\n\n\nclass AttributionReportingFilterConfig(TypedDict):\n    filterValues: list[AttributionReportingFilterDataEntry]\n    lookbackWindow: NotRequired[int]\n\n\nclass AttributionReportingFilterPair(TypedDict):\n    filters: list[AttributionReportingFilterConfig]\n    notFilters: list[AttributionReportingFilterConfig]\n\n\nclass AttributionReportingAggregationKeysEntry(TypedDict):\n    key: str\n    value: UnsignedInt128AsBase16\n\n\nclass AttributionReportingEventReportWindows(TypedDict):\n    start: int\n    ends: list[int]\n\n\nclass AttributionReportingTriggerDataMatching(str, Enum):\n    EXACT = 'exact'\n    MODULUS = 'modulus'\n\n\nclass AttributionReportingAggregatableDebugReportingData(TypedDict):\n    keyPiece: UnsignedInt128AsBase16\n    value: float\n    types: list[str]\n\n\nclass AttributionReportingAggregatableDebugReportingConfig(TypedDict):\n    keyPiece: UnsignedInt128AsBase16\n    debugData: list[AttributionReportingAggregatableDebugReportingData]\n    budget: NotRequired[float]\n    aggregationCoordinatorOrigin: NotRequired[str]\n\n\nclass AttributionScopesData(TypedDict):\n    values: list[str]\n    limit: float\n    maxEventStates: float\n\n\nclass AttributionReportingNamedBudgetDef(TypedDict):\n    name: str\n    budget: int\n\n\nclass AttributionReportingSourceRegistration(TypedDict):\n    time: TimeSinceEpoch\n    expiry: int\n    triggerData: list[float]\n    eventReportWindows: AttributionReportingEventReportWindows\n    aggregatableReportWindow: int\n    type: AttributionReportingSourceType\n    sourceOrigin: str\n    reportingOrigin: str\n    destinationSites: list[str]\n    eventId: UnsignedInt64AsBase10\n    priority: SignedInt64AsBase10\n    filterData: list[AttributionReportingFilterDataEntry]\n    aggregationKeys: list[AttributionReportingAggregationKeysEntry]\n    triggerDataMatching: AttributionReportingTriggerDataMatching\n    destinationLimitPriority: SignedInt64AsBase10\n    aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig\n    maxEventLevelReports: int\n    namedBudgets: list[AttributionReportingNamedBudgetDef]\n    debugReporting: bool\n    eventLevelEpsilon: float\n    debugKey: NotRequired[UnsignedInt64AsBase10]\n    scopesData: NotRequired[AttributionScopesData]\n\n\nclass AttributionReportingSourceRegistrationResult(str, Enum):\n    SUCCESS = 'success'\n    INTERNAL_ERROR = 'internalError'\n    INSUFFICIENT_SOURCE_CAPACITY = 'insufficientSourceCapacity'\n    INSUFFICIENT_UNIQUE_DESTINATION_CAPACITY = 'insufficientUniqueDestinationCapacity'\n    EXCESSIVE_REPORTING_ORIGINS = 'excessiveReportingOrigins'\n    PROHIBITED_BY_BROWSER_POLICY = 'prohibitedByBrowserPolicy'\n    SUCCESS_NOISED = 'successNoised'\n    DESTINATION_REPORTING_LIMIT_REACHED = 'destinationReportingLimitReached'\n    DESTINATION_GLOBAL_LIMIT_REACHED = 'destinationGlobalLimitReached'\n    DESTINATION_BOTH_LIMITS_REACHED = 'destinationBothLimitsReached'\n    REPORTING_ORIGINS_PER_SITE_LIMIT_REACHED = 'reportingOriginsPerSiteLimitReached'\n    EXCEEDS_MAX_CHANNEL_CAPACITY = 'exceedsMaxChannelCapacity'\n    EXCEEDS_MAX_SCOPES_CHANNEL_CAPACITY = 'exceedsMaxScopesChannelCapacity'\n    EXCEEDS_MAX_TRIGGER_STATE_CARDINALITY = 'exceedsMaxTriggerStateCardinality'\n    EXCEEDS_MAX_EVENT_STATES_LIMIT = 'exceedsMaxEventStatesLimit'\n    DESTINATION_PER_DAY_REPORTING_LIMIT_REACHED = 'destinationPerDayReportingLimitReached'\n\n\nclass AttributionReportingSourceRegistrationTimeConfig(str, Enum):\n    INCLUDE = 'include'\n    EXCLUDE = 'exclude'\n\n\nclass AttributionReportingAggregatableValueDictEntry(TypedDict):\n    key: str\n    value: float\n    filteringId: UnsignedInt64AsBase10\n\n\nclass AttributionReportingAggregatableValueEntry(TypedDict):\n    values: list[AttributionReportingAggregatableValueDictEntry]\n    filters: AttributionReportingFilterPair\n\n\nclass AttributionReportingEventTriggerData(TypedDict):\n    data: UnsignedInt64AsBase10\n    priority: SignedInt64AsBase10\n    filters: AttributionReportingFilterPair\n    dedupKey: NotRequired[UnsignedInt64AsBase10]\n\n\nclass AttributionReportingAggregatableTriggerData(TypedDict):\n    keyPiece: UnsignedInt128AsBase16\n    sourceKeys: list[str]\n    filters: AttributionReportingFilterPair\n\n\nclass AttributionReportingAggregatableDedupKey(TypedDict):\n    filters: AttributionReportingFilterPair\n    dedupKey: NotRequired[UnsignedInt64AsBase10]\n\n\nclass AttributionReportingNamedBudgetCandidate(TypedDict):\n    filters: AttributionReportingFilterPair\n    name: NotRequired[str]\n\n\nclass AttributionReportingTriggerRegistration(TypedDict):\n    filters: AttributionReportingFilterPair\n    aggregatableDedupKeys: list[AttributionReportingAggregatableDedupKey]\n    eventTriggerData: list[AttributionReportingEventTriggerData]\n    aggregatableTriggerData: list[AttributionReportingAggregatableTriggerData]\n    aggregatableValues: list[AttributionReportingAggregatableValueEntry]\n    aggregatableFilteringIdMaxBytes: int\n    debugReporting: bool\n    sourceRegistrationTimeConfig: AttributionReportingSourceRegistrationTimeConfig\n    aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig\n    scopes: list[str]\n    namedBudgets: list[AttributionReportingNamedBudgetCandidate]\n    debugKey: NotRequired[UnsignedInt64AsBase10]\n    aggregationCoordinatorOrigin: NotRequired[str]\n    triggerContextId: NotRequired[str]\n\n\nclass AttributionReportingEventLevelResult(str, Enum):\n    SUCCESS = 'success'\n    SUCCESS_DROPPED_LOWER_PRIORITY = 'successDroppedLowerPriority'\n    INTERNAL_ERROR = 'internalError'\n    NO_CAPACITY_FOR_ATTRIBUTION_DESTINATION = 'noCapacityForAttributionDestination'\n    NO_MATCHING_SOURCES = 'noMatchingSources'\n    DEDUPLICATED = 'deduplicated'\n    EXCESSIVE_ATTRIBUTIONS = 'excessiveAttributions'\n    PRIORITY_TOO_LOW = 'priorityTooLow'\n    NEVER_ATTRIBUTED_SOURCE = 'neverAttributedSource'\n    EXCESSIVE_REPORTING_ORIGINS = 'excessiveReportingOrigins'\n    NO_MATCHING_SOURCE_FILTER_DATA = 'noMatchingSourceFilterData'\n    PROHIBITED_BY_BROWSER_POLICY = 'prohibitedByBrowserPolicy'\n    NO_MATCHING_CONFIGURATIONS = 'noMatchingConfigurations'\n    EXCESSIVE_REPORTS = 'excessiveReports'\n    FALSELY_ATTRIBUTED_SOURCE = 'falselyAttributedSource'\n    REPORT_WINDOW_PASSED = 'reportWindowPassed'\n    NOT_REGISTERED = 'notRegistered'\n    REPORT_WINDOW_NOT_STARTED = 'reportWindowNotStarted'\n    NO_MATCHING_TRIGGER_DATA = 'noMatchingTriggerData'\n\n\nclass AttributionReportingAggregatableResult(str, Enum):\n    SUCCESS = 'success'\n    INTERNAL_ERROR = 'internalError'\n    NO_CAPACITY_FOR_ATTRIBUTION_DESTINATION = 'noCapacityForAttributionDestination'\n    NO_MATCHING_SOURCES = 'noMatchingSources'\n    EXCESSIVE_ATTRIBUTIONS = 'excessiveAttributions'\n    EXCESSIVE_REPORTING_ORIGINS = 'excessiveReportingOrigins'\n    NO_HISTOGRAMS = 'noHistograms'\n    INSUFFICIENT_BUDGET = 'insufficientBudget'\n    INSUFFICIENT_NAMED_BUDGET = 'insufficientNamedBudget'\n    NO_MATCHING_SOURCE_FILTER_DATA = 'noMatchingSourceFilterData'\n    NOT_REGISTERED = 'notRegistered'\n    PROHIBITED_BY_BROWSER_POLICY = 'prohibitedByBrowserPolicy'\n    DEDUPLICATED = 'deduplicated'\n    REPORT_WINDOW_PASSED = 'reportWindowPassed'\n    EXCESSIVE_REPORTS = 'excessiveReports'\n\n\nclass AttributionReportingReportResult(str, Enum):\n    SENT = 'sent'\n    PROHIBITED = 'prohibited'\n    FAILED_TO_ASSEMBLE = 'failedToAssemble'\n    EXPIRED = 'expired'\n\n\nclass RelatedWebsiteSet(TypedDict):\n    primarySites: list[str]\n    associatedSites: list[str]\n    serviceSites: list[str]\n"
  },
  {
    "path": "pydoll/protocol/target/__init__.py",
    "content": "\"\"\"Target domain implementation.\"\"\"\n"
  },
  {
    "path": "pydoll/protocol/target/events.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import CDPEvent\nfrom pydoll.protocol.target.types import SessionID, TargetID, TargetInfo\n\n\nclass TargetEvent(str, Enum):\n    \"\"\"\n    Events from the Target domain of the Chrome DevTools Protocol.\n\n    This enumeration contains the names of Target-related events that can be\n    received from the Chrome DevTools Protocol. These events provide information\n    about target creation, destruction, and communication between targets.\n    \"\"\"\n\n    RECEIVED_MESSAGE_FROM_TARGET = 'Target.receivedMessageFromTarget'\n    \"\"\"\n    Notifies about a new protocol message received from the session\n    (as reported in attachedToTarget event).\n\n    Args:\n        sessionId (SessionID): Identifier of a session which sends a message.\n        message (str): The message content.\n        targetId (TargetID): Deprecated.\n    \"\"\"\n\n    TARGET_CRASHED = 'Target.targetCrashed'\n    \"\"\"\n    Issued when a target has crashed.\n\n    Args:\n        targetId (TargetID): Identifier of the crashed target.\n        status (str): Termination status type.\n        errorCode (int): Termination error code.\n    \"\"\"\n\n    TARGET_CREATED = 'Target.targetCreated'\n    \"\"\"\n    Issued when a possible inspection target is created.\n\n    Args:\n        targetInfo (TargetInfo): Information about the created target.\n    \"\"\"\n\n    TARGET_DESTROYED = 'Target.targetDestroyed'\n    \"\"\"\n    Issued when a target is destroyed.\n\n    Args:\n        targetId (TargetID): Identifier of the destroyed target.\n    \"\"\"\n\n    TARGET_INFO_CHANGED = 'Target.targetInfoChanged'\n    \"\"\"\n    Issued when some information about a target has changed.\n    This only happens between targetCreated and targetDestroyed.\n\n    Args:\n        targetInfo (TargetInfo): Updated information about the target.\n    \"\"\"\n\n    ATTACHED_TO_TARGET = 'Target.attachedToTarget'\n    \"\"\"\n    Issued when attached to target because of auto-attach or attachToTarget command.\n\n    Args:\n        sessionId (SessionID): Identifier assigned to the session used to send/receive messages.\n        targetInfo (TargetInfo): Information about the target.\n        waitingForDebugger (bool): Whether the target is waiting for debugger to attach.\n    \"\"\"\n\n    DETACHED_FROM_TARGET = 'Target.detachedFromTarget'\n    \"\"\"\n    Issued when detached from target for any reason (including detachFromTarget command).\n    Can be issued multiple times per target if multiple sessions have been attached to it.\n\n    Args:\n        sessionId (SessionID): Detached session identifier.\n        targetId (TargetID): Deprecated.\n    \"\"\"\n\n\nclass AttachedToTargetParams(TypedDict):\n    \"\"\"Parameters for the `attachedToTarget` event.\"\"\"\n\n    sessionId: SessionID\n    targetInfo: TargetInfo\n    waitingForDebugger: bool\n\n\nclass DetachedFromTargetParams(TypedDict):\n    \"\"\"Parameters for the `detachedFromTarget` event.\"\"\"\n\n    sessionId: SessionID\n    targetId: NotRequired[TargetID]\n\n\nclass ReceivedMessageFromTargetParams(TypedDict):\n    \"\"\"Parameters for the `receivedMessageFromTarget` event.\"\"\"\n\n    sessionId: SessionID\n    message: str\n    targetId: NotRequired[TargetID]\n\n\nclass TargetCreatedParams(TypedDict):\n    \"\"\"Parameters for the `targetCreated` event.\"\"\"\n\n    targetInfo: TargetInfo\n\n\nclass TargetDestroyedParams(TypedDict):\n    \"\"\"Parameters for the `targetDestroyed` event.\"\"\"\n\n    targetId: TargetID\n\n\nclass TargetCrashedParams(TypedDict):\n    \"\"\"Parameters for the `targetCrashed` event.\"\"\"\n\n    targetId: TargetID\n    status: str\n    errorCode: int\n\n\nclass TargetInfoChangedParams(TypedDict):\n    \"\"\"Parameters for the `targetInfoChanged` event.\"\"\"\n\n    targetInfo: TargetInfo\n\n\nAttachedToTargetEvent = CDPEvent[AttachedToTargetParams]\nDetachedFromTargetEvent = CDPEvent[DetachedFromTargetParams]\nReceivedMessageFromTargetEvent = CDPEvent[ReceivedMessageFromTargetParams]\nTargetCreatedEvent = CDPEvent[TargetCreatedParams]\nTargetDestroyedEvent = CDPEvent[TargetDestroyedParams]\nTargetCrashedEvent = CDPEvent[TargetCrashedParams]\nTargetInfoChangedEvent = CDPEvent[TargetInfoChangedParams]\n"
  },
  {
    "path": "pydoll/protocol/target/methods.py",
    "content": "from enum import Enum\n\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.base import Command, EmptyParams, EmptyResponse, Response\nfrom pydoll.protocol.browser.types import BrowserContextID, WindowState\nfrom pydoll.protocol.target.types import (\n    RemoteLocation,\n    SessionID,\n    TargetFilter,\n    TargetID,\n    TargetInfo,\n)\n\n\nclass TargetMethod(str, Enum):\n    \"\"\"Target domain method names.\"\"\"\n\n    ACTIVATE_TARGET = 'Target.activateTarget'\n    ATTACH_TO_TARGET = 'Target.attachToTarget'\n    ATTACH_TO_BROWSER_TARGET = 'Target.attachToBrowserTarget'\n    CLOSE_TARGET = 'Target.closeTarget'\n    EXPOSE_DEV_TOOLS_PROTOCOL = 'Target.exposeDevToolsProtocol'\n    CREATE_BROWSER_CONTEXT = 'Target.createBrowserContext'\n    GET_BROWSER_CONTEXTS = 'Target.getBrowserContexts'\n    CREATE_TARGET = 'Target.createTarget'\n    DETACH_FROM_TARGET = 'Target.detachFromTarget'\n    DISPOSE_BROWSER_CONTEXT = 'Target.disposeBrowserContext'\n    GET_TARGET_INFO = 'Target.getTargetInfo'\n    GET_TARGETS = 'Target.getTargets'\n    SEND_MESSAGE_TO_TARGET = 'Target.sendMessageToTarget'\n    SET_AUTO_ATTACH = 'Target.setAutoAttach'\n    AUTO_ATTACH_RELATED = 'Target.autoAttachRelated'\n    SET_DISCOVER_TARGETS = 'Target.setDiscoverTargets'\n    SET_REMOTE_LOCATIONS = 'Target.setRemoteLocations'\n    OPEN_DEV_TOOLS = 'Target.openDevTools'\n\n\n# Parameter types\nclass ActivateTargetParams(TypedDict):\n    \"\"\"Parameters for the activateTarget command.\"\"\"\n\n    targetId: TargetID\n\n\nclass AttachToTargetParams(TypedDict):\n    \"\"\"Parameters for the attachToTarget command.\"\"\"\n\n    targetId: TargetID\n    flatten: NotRequired[bool]\n\n\nclass AttachToBrowserTargetParams(TypedDict):\n    \"\"\"Parameters for the attachToBrowserTarget command.\"\"\"\n\n    sessionId: SessionID\n\n\nclass CloseTargetParams(TypedDict):\n    \"\"\"Parameters for the closeTarget command.\"\"\"\n\n    targetId: TargetID\n\n\nclass ExposeDevToolsProtocolParams(TypedDict):\n    \"\"\"Parameters for the exposeDevToolsProtocol command.\"\"\"\n\n    targetId: TargetID\n    bindingName: NotRequired[str]\n    inheritPermissions: NotRequired[bool]\n\n\nclass CreateBrowserContextParams(TypedDict):\n    \"\"\"Parameters for the createBrowserContext command.\"\"\"\n\n    disposeOnDetach: NotRequired[bool]\n    proxyServer: NotRequired[str]\n    proxyBypassList: NotRequired[str]\n    originsWithUniversalNetworkAccess: NotRequired[list[str]]\n\n\nclass CreateTargetParams(TypedDict):\n    \"\"\"Parameters for the createTarget command.\"\"\"\n\n    url: str\n    left: NotRequired[int]\n    top: NotRequired[int]\n    width: NotRequired[int]\n    height: NotRequired[int]\n    windowState: NotRequired[WindowState]\n    browserContextId: NotRequired[BrowserContextID]\n    enableBeginFrameControl: NotRequired[bool]\n    newWindow: NotRequired[bool]\n    background: NotRequired[bool]\n    forTab: NotRequired[bool]\n    hidden: NotRequired[bool]\n\n\nclass DetachFromTargetParams(TypedDict):\n    \"\"\"Parameters for the detachFromTarget command.\"\"\"\n\n    sessionId: NotRequired[SessionID]\n    targetId: NotRequired[TargetID]\n\n\nclass DisposeBrowserContextParams(TypedDict):\n    \"\"\"Parameters for the disposeBrowserContext command.\"\"\"\n\n    browserContextId: BrowserContextID\n\n\nclass GetTargetInfoParams(TypedDict):\n    \"\"\"Parameters for the getTargetInfo command.\"\"\"\n\n    targetId: NotRequired[TargetID]\n\n\nclass GetTargetsParams(TypedDict):\n    \"\"\"Parameters for the getTargets command.\"\"\"\n\n    filter: NotRequired[TargetFilter]\n\n\nclass SendMessageToTargetParams(TypedDict):\n    \"\"\"Parameters for the sendMessageToTarget command.\"\"\"\n\n    message: str\n    sessionId: NotRequired[SessionID]\n    targetId: NotRequired[TargetID]\n\n\nclass SetAutoAttachParams(TypedDict):\n    \"\"\"Parameters for the setAutoAttach command.\"\"\"\n\n    autoAttach: bool\n    waitForDebuggerOnStart: bool\n    flatten: NotRequired[bool]\n    filter: NotRequired[TargetFilter]\n\n\nclass AutoAttachRelatedParams(TypedDict):\n    \"\"\"Parameters for the autoAttachRelated command.\"\"\"\n\n    targetId: TargetID\n    waitForDebuggerOnStart: bool\n    filter: NotRequired[TargetFilter]\n\n\nclass SetDiscoverTargetsParams(TypedDict):\n    \"\"\"Parameters for the setDiscoverTargets command.\"\"\"\n\n    discover: bool\n    filter: NotRequired[TargetFilter]\n\n\nclass SetRemoteLocationsParams(TypedDict):\n    \"\"\"Parameters for the setRemoteLocations command.\"\"\"\n\n    locations: list[RemoteLocation]\n\n\nclass OpenDevToolsParams(TypedDict):\n    \"\"\"Parameters for the openDevTools command.\"\"\"\n\n    targetId: TargetID\n\n\n# Result types\nclass AttachToTargetResult(TypedDict):\n    \"\"\"Result for the attachToTarget command.\"\"\"\n\n    sessionId: SessionID\n\n\nclass AttachToBrowserTargetResult(TypedDict):\n    \"\"\"Result for the attachToBrowserTarget command.\"\"\"\n\n    sessionId: SessionID\n\n\nclass CloseTargetResult(TypedDict):\n    \"\"\"Result for the closeTarget command.\"\"\"\n\n    success: bool\n\n\nclass CreateBrowserContextResult(TypedDict):\n    \"\"\"Result for the createBrowserContext command.\"\"\"\n\n    browserContextId: BrowserContextID\n\n\nclass GetBrowserContextsResult(TypedDict):\n    \"\"\"Result for the getBrowserContexts command.\"\"\"\n\n    browserContextIds: list[BrowserContextID]\n\n\nclass CreateTargetResult(TypedDict):\n    \"\"\"Result for the createTarget command.\"\"\"\n\n    targetId: TargetID\n\n\nclass GetTargetInfoResult(TypedDict):\n    \"\"\"Result for the getTargetInfo command.\"\"\"\n\n    targetInfo: TargetInfo\n\n\nclass GetTargetsResult(TypedDict):\n    \"\"\"Result for the getTargets command.\"\"\"\n\n    targetInfos: list[TargetInfo]\n\n\nclass OpenDevToolsResult(TypedDict):\n    \"\"\"Result for the openDevTools command.\"\"\"\n\n    targetId: TargetID\n\n\n# Response types\nAttachToTargetResponse = Response[AttachToTargetResult]\nAttachToBrowserTargetResponse = Response[AttachToBrowserTargetResult]\nCloseTargetResponse = Response[CloseTargetResult]\nCreateBrowserContextResponse = Response[CreateBrowserContextResult]\nGetBrowserContextsResponse = Response[GetBrowserContextsResult]\nCreateTargetResponse = Response[CreateTargetResult]\nGetTargetInfoResponse = Response[GetTargetInfoResult]\nGetTargetsResponse = Response[GetTargetsResult]\nOpenDevToolsResponse = Response[OpenDevToolsResult]\n\n\n# Command types\nActivateTargetCommand = Command[ActivateTargetParams, Response[EmptyResponse]]\nAttachToTargetCommand = Command[AttachToTargetParams, AttachToTargetResponse]\nAttachToBrowserTargetCommand = Command[EmptyParams, AttachToBrowserTargetResponse]\nCloseTargetCommand = Command[CloseTargetParams, CloseTargetResponse]\nExposeDevToolsProtocolCommand = Command[ExposeDevToolsProtocolParams, Response[EmptyResponse]]\nCreateBrowserContextCommand = Command[CreateBrowserContextParams, CreateBrowserContextResponse]\nGetBrowserContextsCommand = Command[EmptyParams, GetBrowserContextsResponse]\nCreateTargetCommand = Command[CreateTargetParams, CreateTargetResponse]\nDetachFromTargetCommand = Command[DetachFromTargetParams, Response[EmptyResponse]]\nDisposeBrowserContextCommand = Command[DisposeBrowserContextParams, Response[EmptyResponse]]\nGetTargetInfoCommand = Command[GetTargetInfoParams, GetTargetInfoResponse]\nGetTargetsCommand = Command[GetTargetsParams, GetTargetsResponse]\nSendMessageToTargetCommand = Command[SendMessageToTargetParams, Response[EmptyResponse]]\nSetAutoAttachCommand = Command[SetAutoAttachParams, Response[EmptyResponse]]\nAutoAttachRelatedCommand = Command[AutoAttachRelatedParams, Response[EmptyResponse]]\nSetDiscoverTargetsCommand = Command[SetDiscoverTargetsParams, Response[EmptyResponse]]\nSetRemoteLocationsCommand = Command[SetRemoteLocationsParams, Response[EmptyResponse]]\nOpenDevToolsCommand = Command[OpenDevToolsParams, OpenDevToolsResponse]\n"
  },
  {
    "path": "pydoll/protocol/target/types.py",
    "content": "from typing_extensions import NotRequired, TypedDict\n\nfrom pydoll.protocol.browser.types import BrowserContextID\nfrom pydoll.protocol.page.types import FrameId\n\nTargetID = str\nSessionID = str\n\n\nclass TargetInfo(TypedDict):\n    targetId: TargetID\n    type: str\n    title: str\n    url: str\n    attached: bool\n    openerId: NotRequired[TargetID]\n    canAccessOpener: NotRequired[bool]\n    openerFrameId: NotRequired[FrameId]\n    browserContextId: NotRequired[BrowserContextID]\n    subtype: NotRequired[str]\n\n\nclass FilterEntry(TypedDict, total=False):\n    \"\"\"A filter used by target query/discovery/auto-attach operations.\"\"\"\n\n    exclude: bool\n    type: str\n\n\nTargetFilter = list[FilterEntry]\n\n\nclass RemoteLocation(TypedDict):\n    host: str\n    port: int\n"
  },
  {
    "path": "pydoll/py.typed",
    "content": ""
  },
  {
    "path": "pydoll/utils/__init__.py",
    "content": "from pydoll.utils.general import (\n    TextExtractor,\n    clean_script_for_analysis,\n    decode_base64_to_bytes,\n    extract_text_from_html,\n    get_browser_ws_address,\n    has_return_outside_function,\n    is_script_already_function,\n    normalize_synthetic_xpath,\n    validate_browser_paths,\n)\nfrom pydoll.utils.socks5_proxy_forwarder import SOCKS5Forwarder\nfrom pydoll.utils.user_agent_parser import UserAgentParser\n\n__all__ = [\n    'TextExtractor',\n    'clean_script_for_analysis',\n    'decode_base64_to_bytes',\n    'extract_text_from_html',\n    'get_browser_ws_address',\n    'has_return_outside_function',\n    'is_script_already_function',\n    'normalize_synthetic_xpath',\n    'validate_browser_paths',\n    'SOCKS5Forwarder',\n    'UserAgentParser',\n]\n"
  },
  {
    "path": "pydoll/utils/bundle.py",
    "content": "\"\"\"Utility functions for saving page bundles (HTML + assets as .zip).\"\"\"\n\nfrom __future__ import annotations\n\nimport base64 as _b64\nimport posixpath\nimport re\nfrom urllib.parse import urljoin, urlparse\n\nfrom pydoll.protocol.network.types import ResourceType\nfrom pydoll.protocol.page.types import FrameResource, FrameResourceTree\n\n_BUNDLEABLE_RESOURCE_TYPES: frozenset[ResourceType] = frozenset({\n    ResourceType.DOCUMENT,\n    ResourceType.STYLESHEET,\n    ResourceType.SCRIPT,\n    ResourceType.IMAGE,\n    ResourceType.FONT,\n    ResourceType.MEDIA,\n})\n\n_MIME_TO_EXT: dict[str, str] = {\n    'text/css': '.css',\n    'text/javascript': '.js',\n    'application/javascript': '.js',\n    'application/x-javascript': '.js',\n    'text/html': '.html',\n    'text/plain': '.txt',\n    'image/png': '.png',\n    'image/jpeg': '.jpg',\n    'image/gif': '.gif',\n    'image/svg+xml': '.svg',\n    'image/webp': '.webp',\n    'image/x-icon': '.ico',\n    'image/vnd.microsoft.icon': '.ico',\n    'font/woff': '.woff',\n    'font/woff2': '.woff2',\n    'application/font-woff': '.woff',\n    'application/font-woff2': '.woff2',\n    'font/ttf': '.ttf',\n    'font/otf': '.otf',\n    'application/x-font-ttf': '.ttf',\n    'application/x-font-otf': '.otf',\n    'video/mp4': '.mp4',\n    'video/webm': '.webm',\n    'audio/mpeg': '.mp3',\n    'audio/ogg': '.ogg',\n    'application/json': '.json',\n    'application/xml': '.xml',\n    'text/xml': '.xml',\n}\n\n_CSS_URL_RE = re.compile(r'url\\(\\s*([\"\\']?)(.*?)\\1\\s*\\)', re.IGNORECASE)\n\n\ndef filter_fetchable_resources(\n    all_resources: list[tuple[str, FrameResource]],\n    page_url: str,\n) -> list[tuple[str, FrameResource]]:\n    \"\"\"Filter resources to only those that should be bundled.\"\"\"\n    fetchable: list[tuple[str, FrameResource]] = []\n    for fid, res in all_resources:\n        if res.get('failed') or res.get('canceled'):\n            continue\n        url = res['url']\n        if url == page_url or url.startswith('data:'):\n            continue\n        if res['type'] not in _BUNDLEABLE_RESOURCE_TYPES:\n            continue\n        fetchable.append((fid, res))\n    return fetchable\n\n\ndef collect_frame_resources(\n    frame_tree: FrameResourceTree,\n) -> list[tuple[str, FrameResource]]:\n    \"\"\"Recursively collect all resources from a frame tree.\"\"\"\n    frame_id = frame_tree['frame']['id']\n    result: list[tuple[str, FrameResource]] = [\n        (frame_id, res) for res in frame_tree.get('resources', [])\n    ]\n    for child in frame_tree.get('childFrames', []):\n        result.extend(collect_frame_resources(child))\n    return result\n\n\ndef build_asset_filename(url: str, mime_type: str, index: int) -> str:\n    \"\"\"Build a unique filename from a URL, MIME type, and index.\"\"\"\n    parsed = urlparse(url)\n    basename = posixpath.basename(parsed.path) if parsed.path else ''\n    if not basename or basename == '/':\n        basename = 'resource'\n    if '.' not in basename:\n        ext = _MIME_TO_EXT.get(mime_type.split(';')[0].strip(), '')\n        basename = f'{basename}{ext}'\n    return f'{index:04d}_{basename}'\n\n\ndef rewrite_css_urls(\n    css_text: str,\n    css_url: str,\n    asset_map: dict[str, tuple[str, bytes, str, ResourceType]],\n) -> str:\n    \"\"\"Rewrite url() references in CSS to point to local asset paths.\"\"\"\n\n    def _replace(match: re.Match[str]) -> str:\n        raw_url = match.group(2)\n        if raw_url.startswith('data:'):\n            return match.group(0)\n        absolute = urljoin(css_url, raw_url)\n        entry = asset_map.get(absolute)\n        if entry is None:\n            return match.group(0)\n        filename = entry[0]\n        return f'url(\"{filename}\")'\n\n    return _CSS_URL_RE.sub(_replace, css_text)\n\n\ndef inline_css_urls(\n    css_text: str,\n    css_url: str,\n    asset_map: dict[str, tuple[str, bytes, str, ResourceType]],\n) -> str:\n    \"\"\"Replace url() references in CSS with data URIs.\"\"\"\n\n    def _replace(match: re.Match[str]) -> str:\n        raw_url = match.group(2)\n        if raw_url.startswith('data:'):\n            return match.group(0)\n        absolute = urljoin(css_url, raw_url)\n        entry = asset_map.get(absolute)\n        if entry is None:\n            return match.group(0)\n        _fname, data, mime, _rtype = entry\n        b64 = _b64.b64encode(data).decode('ascii')\n        return f'url(\"data:{mime};base64,{b64}\")'\n\n    return _CSS_URL_RE.sub(_replace, css_text)\n\n\ndef replace_stylesheet_with_inline(html: str, url: str, css_text: str) -> str:\n    \"\"\"Replace a <link> stylesheet tag with an inline <style> block.\"\"\"\n    escaped = re.escape(url)\n    pattern = re.compile(\n        rf'<link\\b[^>]*href=[\"\\']?{escaped}[\"\\']?[^>]*/?>',\n        re.IGNORECASE,\n    )\n    replacement = f'<style>{css_text}</style>'\n    return pattern.sub(lambda _: replacement, html, count=1)\n\n\ndef replace_script_with_inline(html: str, url: str, js_text: str) -> str:\n    \"\"\"Replace a <script src=...> tag with an inline <script> block.\"\"\"\n    escaped = re.escape(url)\n    pattern = re.compile(\n        rf'<script\\b[^>]*src=[\"\\']?{escaped}[\"\\']?[^>]*>\\s*</script>',\n        re.IGNORECASE,\n    )\n    safe_js = js_text.replace('</script>', '<\\\\/script>')\n    replacement = f'<script>{safe_js}</script>'\n    return pattern.sub(lambda _: replacement, html, count=1)\n\n\ndef rewrite_html_urls(\n    html: str,\n    asset_map: dict[str, tuple[str, bytes, str, ResourceType]],\n) -> str:\n    \"\"\"Rewrite asset URLs in HTML to point to local assets/ directory.\"\"\"\n    for url, (filename, data, mime, rtype) in asset_map.items():\n        if rtype == ResourceType.STYLESHEET:\n            css_text = data.decode('utf-8', errors='replace')\n            rewritten_css = rewrite_css_urls(css_text, url, asset_map)\n            asset_map[url] = (filename, rewritten_css.encode('utf-8'), mime, rtype)\n        html = html.replace(url, f'assets/{filename}')\n    return html\n\n\ndef inline_all_assets(\n    html: str,\n    asset_map: dict[str, tuple[str, bytes, str, ResourceType]],\n) -> str:\n    \"\"\"Embed all assets inline into the HTML.\"\"\"\n    for url, (_, data, mime, rtype) in asset_map.items():\n        if rtype == ResourceType.STYLESHEET:\n            css_text = data.decode('utf-8', errors='replace')\n            css_text = inline_css_urls(css_text, url, asset_map)\n            html = replace_stylesheet_with_inline(html, url, css_text)\n        elif rtype == ResourceType.SCRIPT:\n            js_text = data.decode('utf-8', errors='replace')\n            html = replace_script_with_inline(html, url, js_text)\n        else:\n            b64 = _b64.b64encode(data).decode('ascii')\n            data_uri = f'data:{mime};base64,{b64}'\n            html = html.replace(url, data_uri)\n    return html\n"
  },
  {
    "path": "pydoll/utils/general.py",
    "content": "import base64\nimport logging\nimport os\nimport re\nfrom html import unescape\nfrom html.parser import HTMLParser\n\nimport aiohttp\n\nfrom pydoll.exceptions import InvalidBrowserPath, InvalidResponse, NetworkError\n\nlogger = logging.getLogger(__name__)\n\n\nclass TextExtractor(HTMLParser):\n    \"\"\"\n    HTML parser for text extraction.\n\n    Extracts visible text content from an HTML string, excluding the contents of\n    tags specified in _skip_tags.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self._parts = []\n        self._skip = False\n        self._skip_tags = {'script', 'style', 'template'}\n\n    def handle_starttag(self, tag, attrs):\n        \"\"\"\n        Marks the parser to skip content inside tags specified in _skip_tags.\n\n        Args:\n            tag (str): The tag name.\n            attrs (list): A list of (attribute, value) pairs.\n        \"\"\"\n        if tag in self._skip_tags:\n            self._skip = True\n\n    def handle_endtag(self, tag):\n        \"\"\"\n        Marks the parser the end of skip tags.\n\n        Args:\n            tag (str): The tag name.\n        \"\"\"\n        if tag in self._skip_tags:\n            self._skip = False\n\n    def handle_data(self, data):\n        \"\"\"\n        Handles text nodes. Adds them to the result unless they are within a skip tag.\n\n        Args:\n            data (str): The text data.\n        \"\"\"\n        if not self._skip:\n            self._parts.append(unescape(data))\n\n    def get_strings(self, strip: bool):\n        \"\"\"\n        Yields all collected visible text fragments.\n\n        Args:\n            strip (bool): Whether to strip leading/trailing whitespace from each fragment.\n\n        Yields:\n            str: Visible text fragments.\n        \"\"\"\n        for text in self._parts:\n            yield text.strip() if strip else text\n\n    def get_text(self, separator: str, strip: bool) -> str:\n        \"\"\"\n        Returns all visible text.\n\n        Args:\n            separator (str): String inserted between extracted text fragments.\n            strip (bool): Whether to strip whitespace from each fragment.\n\n        Returns:\n            str: The visible text.\n        \"\"\"\n        return separator.join(self.get_strings(strip=strip))\n\n\ndef extract_text_from_html(html: str, separator: str = '', strip: bool = False) -> str:\n    \"\"\"\n    Extracts visible text content from an HTML string.\n\n    Args:\n        html (str): The HTML string to extract text from.\n        separator (str, optional): String inserted between extracted text fragments. Defaults to ''.\n        strip (bool, optional): Whether to strip whitespace from text fragments. Defaults to False.\n\n    Returns:\n        str: The extracted visible text.\n    \"\"\"\n    parser = TextExtractor()\n    parser.feed(html)\n    return parser.get_text(separator=separator, strip=strip)\n\n\ndef decode_base64_to_bytes(image: str) -> bytes:\n    \"\"\"\n    Decodes a base64 image string to bytes.\n\n    Args:\n        image (str): The base64 image string to decode.\n\n    Returns:\n        bytes: The decoded image as bytes.\n    \"\"\"\n    return base64.b64decode(image.encode('utf-8'))\n\n\nasync def get_browser_ws_address(port: int) -> str:\n    \"\"\"\n    Fetches the WebSocket address for the browser instance.\n\n    Returns:\n        str: The WebSocket address for the browser.\n\n    Raises:\n        NetworkError: If the address cannot be fetched due to network errors\n            or missing data.\n        InvalidResponse: If the response is not valid JSON.\n    \"\"\"\n    try:\n        async with aiohttp.ClientSession() as session:\n            async with session.get(f'http://localhost:{port}/json/version') as response:\n                response.raise_for_status()\n                data = await response.json()\n                return data['webSocketDebuggerUrl']\n\n    except aiohttp.ClientError as e:\n        raise NetworkError(f'Failed to get browser ws address: {e}')\n\n    except KeyError as e:\n        raise InvalidResponse(f'Failed to get browser ws address: {e}')\n\n\ndef validate_browser_paths(paths: list[str]) -> str:\n    \"\"\"\n    Validates potential browser executable paths and returns the first valid one.\n\n    Checks a list of possible browser binary locations to find an existing,\n    executable browser. This is used by browser-specific subclasses to locate\n    the browser executable when no explicit binary path is provided.\n\n    Args:\n        paths: List of potential file paths to check for the browser executable.\n            These should be absolute paths appropriate for the current OS.\n\n    Returns:\n        str: The first valid browser executable path found.\n\n    Raises:\n        InvalidBrowserPath: If the browser executable is not found at the path.\n    \"\"\"\n    for path in paths:\n        if os.path.isfile(path) and os.access(path, os.X_OK):\n            return path\n    raise InvalidBrowserPath(f'No valid browser path found in: {paths}')\n\n\ndef clean_script_for_analysis(script: str) -> str:\n    \"\"\"\n    Clean JavaScript code by removing comments and string literals.\n\n    This helps avoid false positives when analyzing script structure.\n\n    Args:\n        script: JavaScript code to clean.\n\n    Returns:\n        str: Cleaned script with comments and strings removed.\n    \"\"\"\n    # Remove line comments\n    cleaned = re.sub(r'//.*?$', '', script, flags=re.MULTILINE)\n    # Remove block comments\n    cleaned = re.sub(r'/\\*.*?\\*/', '', cleaned, flags=re.DOTALL)\n    # Remove double quoted strings\n    cleaned = re.sub(r'\"[^\"]*\"', '\"\"', cleaned)\n    # Remove single quoted strings\n    cleaned = re.sub(r\"'[^']*'\", \"''\", cleaned)\n    # Remove template literals\n    cleaned = re.sub(r'`[^`]*`', '``', cleaned)\n\n    return cleaned\n\n\ndef is_script_already_function(script: str) -> bool:\n    \"\"\"\n    Check if a JavaScript script is already wrapped in a function.\n\n    Args:\n        script: JavaScript code to analyze.\n\n    Returns:\n        bool: True if script is already a function, False otherwise.\n    \"\"\"\n    cleaned_script = clean_script_for_analysis(script)\n\n    function_pattern = r'^\\s*function\\s*\\([^)]*\\)\\s*\\{'\n    arrow_function_pattern = r'^\\s*\\([^)]*\\)\\s*=>\\s*\\{'\n\n    return bool(\n        re.match(function_pattern, cleaned_script.strip())\n        or re.match(arrow_function_pattern, cleaned_script.strip())\n    )\n\n\ndef has_return_outside_function(script: str) -> bool:\n    \"\"\"\n    Check if a JavaScript script has return statements outside of functions.\n\n    Args:\n        script: JavaScript code to analyze.\n\n    Returns:\n        bool: True if script has return outside function, False otherwise.\n    \"\"\"\n    cleaned_script = clean_script_for_analysis(script)\n\n    # If already a function, no need to check\n    if is_script_already_function(cleaned_script):\n        return False\n\n    # Look for 'return' statements\n    return_pattern = r'\\breturn\\b'\n    if not re.search(return_pattern, cleaned_script):\n        return False\n\n    # Check if return is inside a function by counting braces\n    lines = cleaned_script.split('\\n')\n    brace_count = 0\n    in_function = False\n\n    for line in lines:\n        # Check for function declarations\n        if re.search(r'\\bfunction\\b', line) or re.search(r'=>', line):\n            in_function = True\n\n        # Count braces\n        brace_count += line.count('{') - line.count('}')\n\n        # Check for return statement\n        if re.search(return_pattern, line):\n            if not in_function or brace_count <= 0:\n                return True\n\n        # Reset function flag if we're back to top level\n        if brace_count <= 0:\n            in_function = False\n\n    return False\n\n\ndef normalize_synthetic_xpath(selector: str) -> str:\n    \"\"\"\n    Normalize synthetic XPath selector produced by the builder.\n\n    Converts selectors of the form //*[@xpath=\"...\"] back into the original\n    XPath string between the quotes. Returns the input unchanged if the\n    pattern is not present or cannot be parsed safely.\n\n    Args:\n        selector: The selector string that may contain the synthetic XPath format.\n\n    Returns:\n        str: The normalized original XPath or the input selector if no normalization applies.\n    \"\"\"\n    s = selector.strip()\n    if not s.startswith('//*[@xpath='):\n        return selector\n    prefix = '//*[@xpath=\"'\n    start_idx = s.find(prefix)\n    if start_idx == -1:\n        return selector\n    start_idx += len(prefix)\n    end_idx = s.rfind('\"]')\n    if end_idx == -1 or end_idx <= start_idx:\n        return selector\n    return s[start_idx:end_idx]\n"
  },
  {
    "path": "pydoll/utils/socks5_proxy_forwarder.py",
    "content": "\"\"\"\nSOCKS5 Proxy Forwarder — Local no-auth proxy that forwards to a remote\nauthenticated SOCKS5 proxy.\n\nChrome/Chromium does NOT support SOCKS5 authentication natively\n(Chromium issue #40323993). This module works around that limitation by\nrunning a lightweight local SOCKS5 proxy (no authentication required)\nthat performs the SOCKS5 handshake with username/password on behalf of\nthe browser.\n\nData flow:\n    Chrome ──► localhost:{local_port} (no auth)\n                    │\n              SOCKS5Forwarder\n                    │  (authenticates with remote)\n                    ▼\n           remote_host:remote_port (user/pass auth)\n                    │\n                    ▼\n              destination server\n\nUsage as CLI:\n    python -m pydoll.utils.socks5_proxy_forwarder \\\\\n        --remote-host proxy.example.com \\\\\n        --remote-port 1080 \\\\\n        --username myuser \\\\\n        --password mypass \\\\\n        --local-port 1081\n\nUsage with Pydoll:\n    import asyncio\n    from pydoll.utils import SOCKS5Forwarder\n    from pydoll.browser.chromium import Chrome\n    from pydoll.browser.options import ChromiumOptions\n\n    async def main():\n        forwarder = SOCKS5Forwarder(\n            remote_host='proxy.example.com',\n            remote_port=1080,\n            username='myuser',\n            password='mypass',\n            local_port=1081,\n        )\n        async with forwarder:\n            options = ChromiumOptions()\n            options.add_argument('--proxy-server=socks5://127.0.0.1:1081')\n            async with Chrome(options=options) as browser:\n                tab = await browser.start()\n                await tab.go_to('https://httpbin.org/ip')\n\n    asyncio.run(main())\n\nRequirements: Python >= 3.10, no external dependencies.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\nimport ipaddress\nimport logging\nimport signal\nimport struct\nfrom types import TracebackType\n\nlogger = logging.getLogger(__name__)\n\nSOCKS5_VERSION = 0x05\nAUTH_NO_AUTH = 0x00\nAUTH_USERNAME_PASSWORD = 0x02\nAUTH_NO_ACCEPTABLE = 0xFF\n\nCMD_CONNECT = 0x01\n\nATYP_IPV4 = 0x01\nATYP_DOMAIN = 0x03\nATYP_IPV6 = 0x04\n\nREPLY_SUCCESS = 0x00\nREPLY_GENERAL_FAILURE = 0x01\nREPLY_CONNECTION_REFUSED = 0x05\nREPLY_COMMAND_NOT_SUPPORTED = 0x07\nREPLY_ADDRESS_TYPE_NOT_SUPPORTED = 0x08\n\nBUFFER_SIZE = 65536\nHANDSHAKE_TIMEOUT = 30\nMAX_CREDENTIAL_BYTES = 255\n\n\nclass _suppress_closed:\n    \"\"\"Tiny context manager that silences errors on already-closed transports.\"\"\"\n\n    def __enter__(self) -> None:\n        return None\n\n    def __exit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_val: BaseException | None,\n        exc_tb: TracebackType | None,\n    ) -> bool:\n        return exc_type is not None and issubclass(exc_type, OSError)\n\n\nasync def _close_writer(writer: asyncio.StreamWriter) -> None:\n    \"\"\"Close a stream writer and wait for the transport to finish.\"\"\"\n    with _suppress_closed():\n        writer.close()\n        await writer.wait_closed()\n\n\nasync def _pipe(\n    reader: asyncio.StreamReader,\n    writer: asyncio.StreamWriter,\n    label: str,\n) -> None:\n    \"\"\"Forward data from *reader* to *writer* until EOF.\"\"\"\n    try:\n        while True:\n            data = await reader.read(BUFFER_SIZE)\n            if not data:\n                break\n            writer.write(data)\n            await writer.drain()\n    except (ConnectionResetError, BrokenPipeError, OSError):\n        pass\n    finally:\n        await _close_writer(writer)\n\n\nclass SOCKS5Forwarder:\n    \"\"\"Local SOCKS5 proxy (no auth) that forwards to a remote authenticated\n    SOCKS5 proxy.\n\n    Can be used as an async context manager::\n\n        async with SOCKS5Forwarder(...) as fwd:\n            # fwd.local_port is now listening\n            ...\n    \"\"\"\n\n    def __init__(\n        self,\n        remote_host: str,\n        remote_port: int,\n        username: str,\n        password: str,\n        local_host: str = '127.0.0.1',\n        local_port: int = 0,\n    ) -> None:\n        if len(username.encode()) > MAX_CREDENTIAL_BYTES:\n            raise ValueError('SOCKS5 username must be at most 255 bytes (UTF-8 encoded)')\n        if len(password.encode()) > MAX_CREDENTIAL_BYTES:\n            raise ValueError('SOCKS5 password must be at most 255 bytes (UTF-8 encoded)')\n        self.remote_host = remote_host\n        self.remote_port = remote_port\n        self.username = username\n        self.password = password\n        self.local_host = local_host\n        self.local_port = local_port\n        self._server: asyncio.Server | None = None\n\n    async def __aenter__(self) -> SOCKS5Forwarder:\n        await self.start()\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_val: BaseException | None,\n        exc_tb: TracebackType | None,\n    ) -> None:\n        await self.stop()\n\n    async def start(self) -> None:\n        \"\"\"Start accepting connections on *local_host*:*local_port*.\"\"\"\n        try:\n            addr = ipaddress.ip_address(self.local_host)\n        except ValueError:\n            addr = None\n\n        if addr is not None and not addr.is_loopback:\n            logger.warning(\n                'Binding to non-loopback address %s — the forwarder will be '\n                'accessible from the network without authentication!',\n                self.local_host,\n            )\n        elif addr is None and self.local_host != 'localhost':\n            logger.debug(\n                'local_host=%r is not an IP literal; skipping loopback check',\n                self.local_host,\n            )\n        self._server = await asyncio.start_server(\n            self._handle_client,\n            self.local_host,\n            self.local_port,\n        )\n        sockets = list(self._server.sockets or [])\n        ports = {s.getsockname()[1] for s in sockets}\n        if len(ports) != 1:\n            await self.stop()\n            raise RuntimeError(\n                f'start_server created sockets with different ports: {sorted(ports)}. '\n                \"Use an explicit IP (e.g. '127.0.0.1' or '::1') instead of a hostname, \"\n                'or specify --local-port explicitly.'\n            )\n        self.local_port = ports.pop()\n        logger.info(\n            'SOCKS5 forwarder listening on %s:%s -> %s:%s',\n            self.local_host,\n            self.local_port,\n            self.remote_host,\n            self.remote_port,\n        )\n\n    async def stop(self) -> None:\n        \"\"\"Gracefully shut down the server.\"\"\"\n        if self._server is not None:\n            self._server.close()\n            await self._server.wait_closed()\n            self._server = None\n            logger.info('SOCKS5 forwarder stopped')\n\n    async def serve_forever(self) -> None:\n        \"\"\"Block until the server is closed (useful for CLI mode).\"\"\"\n        if self._server is None:\n            raise RuntimeError('Server not started — call start() first')\n        async with self._server:\n            await self._server.serve_forever()\n\n    async def _handle_client(\n        self,\n        client_reader: asyncio.StreamReader,\n        client_writer: asyncio.StreamWriter,\n    ) -> None:\n        \"\"\"Handle one incoming browser connection.\"\"\"\n        remote_writer: asyncio.StreamWriter | None = None\n        try:\n            addr_payload, dest_port = await self._accept_local_handshake(\n                client_reader,\n                client_writer,\n            )\n            r_reader, r_writer = await asyncio.wait_for(\n                asyncio.open_connection(self.remote_host, self.remote_port),\n                timeout=HANDSHAKE_TIMEOUT,\n            )\n            remote_writer = r_writer\n            await self._remote_handshake(\n                r_reader,\n                r_writer,\n                addr_payload,\n                dest_port,\n            )\n            await self._send_reply(client_writer, REPLY_SUCCESS)\n            await asyncio.gather(\n                _pipe(client_reader, r_writer, 'client->remote'),\n                _pipe(r_reader, client_writer, 'remote->client'),\n            )\n        except _HandshakeError as exc:\n            logger.warning('Handshake failed: %s', exc)\n            if exc.send_reply:\n                with _suppress_closed():\n                    await self._send_reply(client_writer, exc.reply_code)\n        except asyncio.TimeoutError:\n            logger.warning('Connection to remote proxy timed out')\n            with _suppress_closed():\n                await self._send_reply(client_writer, REPLY_GENERAL_FAILURE)\n        except (ConnectionRefusedError, OSError) as exc:\n            logger.warning('Connection to remote proxy failed: %s', exc)\n            reply = (\n                REPLY_CONNECTION_REFUSED\n                if isinstance(exc, ConnectionRefusedError)\n                else REPLY_GENERAL_FAILURE\n            )\n            with _suppress_closed():\n                await self._send_reply(client_writer, reply)\n        except asyncio.CancelledError:\n            raise\n        except Exception:\n            logger.exception('Unexpected error in client handler')\n        finally:\n            await _close_writer(client_writer)\n            if remote_writer is not None:\n                await _close_writer(remote_writer)\n\n    async def _accept_local_handshake(\n        self,\n        reader: asyncio.StreamReader,\n        writer: asyncio.StreamWriter,\n    ) -> tuple[bytes, int]:\n        \"\"\"Accept the SOCKS5 greeting from Chrome (no-auth) and read the\n        CONNECT request.\n\n        Returns ``(addr_payload, dest_port)`` where *addr_payload* is the raw\n        SOCKS5 address field (ATYP byte + address bytes) exactly as Chrome\n        sent it, ready to be forwarded verbatim to the remote proxy.\"\"\"\n        try:\n            header = await _read_exact(reader, 2, peer='client')\n        except _HandshakeError as exc:\n            raise _HandshakeError(str(exc), send_reply=False) from exc\n        version, nmethods = header[0], header[1]\n        if version != SOCKS5_VERSION:\n            raise _HandshakeError(\n                f'Unsupported SOCKS version from client: {version}', send_reply=False\n            )\n\n        try:\n            methods = await _read_exact(reader, nmethods, peer='client')\n        except _HandshakeError as exc:\n            raise _HandshakeError(str(exc), send_reply=False) from exc\n        if AUTH_NO_AUTH not in methods:\n            writer.write(bytes([SOCKS5_VERSION, AUTH_NO_ACCEPTABLE]))\n            await writer.drain()\n            raise _HandshakeError('Client does not offer no-auth method', send_reply=False)\n\n        writer.write(bytes([SOCKS5_VERSION, AUTH_NO_AUTH]))\n        await writer.drain()\n\n        req = await _read_exact(reader, 4, peer='client')\n        if req[0] != SOCKS5_VERSION:\n            raise _HandshakeError('Bad SOCKS version in request')\n        if req[1] != CMD_CONNECT:\n            raise _HandshakeError(\n                f'Unsupported command: {req[1]}',\n                reply_code=REPLY_COMMAND_NOT_SUPPORTED,\n            )\n\n        atyp = req[3]\n        addr_payload = await self._read_raw_address(reader, atyp, peer='client')\n        dest_port = struct.unpack('!H', await _read_exact(reader, 2, peer='client'))[0]\n        logger.debug('Client CONNECT to %s port %d', addr_payload.hex(), dest_port)\n        return addr_payload, dest_port\n\n    async def _remote_handshake(\n        self,\n        reader: asyncio.StreamReader,\n        writer: asyncio.StreamWriter,\n        addr_payload: bytes,\n        dest_port: int,\n    ) -> None:\n        \"\"\"Perform full SOCKS5 handshake with the remote proxy including\n        username/password authentication, then send the CONNECT request.\n\n        *addr_payload* is the raw ATYP + address bytes from the client,\n        forwarded verbatim so the address type is preserved.\"\"\"\n        greeting = bytes([SOCKS5_VERSION, 0x02, AUTH_NO_AUTH, AUTH_USERNAME_PASSWORD])\n        writer.write(greeting)\n        await writer.drain()\n        logger.debug('-> greeting: %s', greeting.hex())\n\n        resp = await _read_exact(reader, 2, peer='remote proxy')\n        logger.debug('<- method selection: %s', resp.hex())\n\n        if resp[0] != SOCKS5_VERSION:\n            raise _HandshakeError(f'Remote proxy bad version (response: {resp.hex()})')\n\n        selected_method = resp[1]\n        if selected_method == AUTH_NO_ACCEPTABLE:\n            raise _HandshakeError('Remote proxy rejected all auth methods')\n\n        if selected_method == AUTH_USERNAME_PASSWORD:\n            uname = self.username.encode()\n            passwd = self.password.encode()\n            auth_req = bytes([0x01, len(uname)]) + uname + bytes([len(passwd)]) + passwd\n            writer.write(auth_req)\n            await writer.drain()\n            logger.debug('-> auth request: ulen=%d plen=%d', len(uname), len(passwd))\n\n            auth_resp = await _read_exact(reader, 2, peer='remote proxy')\n            logger.debug('<- auth response: %s', auth_resp.hex())\n            if auth_resp[1] != 0x00:\n                raise _HandshakeError(\n                    f'Remote proxy authentication failed (status: {auth_resp[1]:#04x})'\n                )\n        elif selected_method == AUTH_NO_AUTH:\n            logger.debug('Remote proxy selected no-auth (0x00)')\n        else:\n            raise _HandshakeError(\n                f'Remote proxy selected unsupported method: {selected_method:#04x}'\n            )\n\n        connect_req = bytes([SOCKS5_VERSION, CMD_CONNECT, 0x00])\n        connect_req += addr_payload\n        connect_req += struct.pack('!H', dest_port)\n        writer.write(connect_req)\n        await writer.drain()\n        logger.debug('-> CONNECT: %s', connect_req.hex())\n\n        reply_header = await _read_exact(reader, 4, peer='remote proxy')\n        logger.debug('<- reply header: %s', reply_header.hex())\n\n        rep = reply_header[1]\n        if rep != REPLY_SUCCESS:\n            extra = b''\n            try:\n                extra = await asyncio.wait_for(reader.read(256), timeout=0.5)\n            except (asyncio.TimeoutError, OSError):\n                pass\n            raise _HandshakeError(\n                f'Remote proxy CONNECT failed '\n                f'(rep={rep:#04x}, reply: {reply_header.hex()}, '\n                f'extra: {extra.hex() if extra else \"none\"})',\n                reply_code=rep,\n            )\n\n        atyp = reply_header[3]\n        await self._read_raw_address(reader, atyp, peer='remote proxy')\n        await _read_exact(reader, 2, peer='remote proxy')\n\n    @staticmethod\n    async def _read_raw_address(\n        reader: asyncio.StreamReader,\n        atyp: int,\n        *,\n        peer: str = 'peer',\n    ) -> bytes:\n        \"\"\"Read a SOCKS5 address field and return raw bytes including the\n        ATYP prefix, suitable for forwarding verbatim to another proxy.\"\"\"\n        if atyp == ATYP_IPV4:\n            raw = await _read_exact(reader, 4, peer=peer)\n            return bytes([atyp]) + raw\n        if atyp == ATYP_DOMAIN:\n            length_byte = await _read_exact(reader, 1, peer=peer)\n            domain = await _read_exact(reader, length_byte[0], peer=peer)\n            return bytes([atyp]) + length_byte + domain\n        if atyp == ATYP_IPV6:\n            raw = await _read_exact(reader, 16, peer=peer)\n            return bytes([atyp]) + raw\n        raise _HandshakeError(\n            f'Unsupported address type: {atyp}',\n            reply_code=REPLY_ADDRESS_TYPE_NOT_SUPPORTED,\n        )\n\n    @staticmethod\n    async def _send_reply(\n        writer: asyncio.StreamWriter,\n        reply_code: int,\n    ) -> None:\n        \"\"\"Send a minimal SOCKS5 reply to the client.\"\"\"\n        writer.write(\n            bytes([\n                SOCKS5_VERSION,\n                reply_code,\n                0x00,\n                ATYP_IPV4,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n            ])\n        )\n        await writer.drain()\n\n\nclass _HandshakeError(Exception):\n    \"\"\"Raised when a SOCKS5 handshake step fails.\"\"\"\n\n    def __init__(\n        self,\n        message: str,\n        reply_code: int = REPLY_GENERAL_FAILURE,\n        send_reply: bool = True,\n    ) -> None:\n        super().__init__(message)\n        self.reply_code = reply_code\n        self.send_reply = send_reply\n\n\nasync def _read_exact(reader: asyncio.StreamReader, n: int, *, peer: str = 'peer') -> bytes:\n    \"\"\"Read exactly *n* bytes or raise ``_HandshakeError``.\"\"\"\n    try:\n        return await asyncio.wait_for(reader.readexactly(n), timeout=HANDSHAKE_TIMEOUT)\n    except asyncio.IncompleteReadError as exc:\n        raise _HandshakeError(\n            f'Connection closed prematurely (expected {n} bytes, '\n            f'got {len(exc.partial)} from {peer})'\n        ) from exc\n    except asyncio.TimeoutError as exc:\n        raise _HandshakeError(\n            f'Timed out reading {n} bytes from {peer}',\n        ) from exc\n\n\nasync def _skip_bnd_address(reader: asyncio.StreamReader, atyp: int, *, peer: str = 'peer') -> None:\n    \"\"\"Consume BND.ADDR + BND.PORT from a SOCKS5 reply.\"\"\"\n    if atyp == ATYP_IPV4:\n        await _read_exact(reader, 4 + 2, peer=peer)\n    elif atyp == ATYP_DOMAIN:\n        length = (await _read_exact(reader, 1, peer=peer))[0]\n        await _read_exact(reader, length + 2, peer=peer)\n    elif atyp == ATYP_IPV6:\n        await _read_exact(reader, 16 + 2, peer=peer)\n\n\nasync def _main(args: argparse.Namespace) -> None:\n    forwarder = SOCKS5Forwarder(\n        remote_host=args.remote_host,\n        remote_port=args.remote_port,\n        username=args.username,\n        password=args.password,\n        local_host=args.local_host,\n        local_port=args.local_port,\n    )\n    await forwarder.start()\n\n    loop = asyncio.get_running_loop()\n    stop = loop.create_future()\n\n    try:\n        for sig in (signal.SIGINT, signal.SIGTERM):\n            loop.add_signal_handler(sig, stop.set_result, None)\n    except NotImplementedError:\n        pass  # Windows / ProactorEventLoop — fall back to KeyboardInterrupt\n\n    logger.info(\n        'Forwarding socks5://127.0.0.1:%s -> socks5://%s:***@%s:%s',\n        forwarder.local_port,\n        args.username,\n        args.remote_host,\n        args.remote_port,\n    )\n    logger.info('Press Ctrl+C to stop.')\n\n    try:\n        await stop\n    finally:\n        await forwarder.stop()\n\n\nasync def _test_negotiate_auth(\n    reader: asyncio.StreamReader,\n    writer: asyncio.StreamWriter,\n    username: str,\n    password: str,\n) -> bool:\n    \"\"\"Perform greeting + auth for the --test diagnostic. Returns True on success.\"\"\"\n    greeting = bytes([SOCKS5_VERSION, 0x02, AUTH_NO_AUTH, AUTH_USERNAME_PASSWORD])\n    writer.write(greeting)\n    await writer.drain()\n    logger.info('-> Greeting:  %s', greeting.hex())\n\n    resp = await asyncio.wait_for(reader.readexactly(2), timeout=10)\n    logger.info('<- Method:    %s  (selected method: %#04x)', resp.hex(), resp[1])\n\n    if resp[0] != SOCKS5_VERSION:\n        logger.error('Bad version byte: %#04x', resp[0])\n        return False\n\n    if resp[1] == AUTH_USERNAME_PASSWORD:\n        uname = username.encode()\n        passwd = password.encode()\n        auth_req = bytes([0x01, len(uname)]) + uname + bytes([len(passwd)]) + passwd\n        writer.write(auth_req)\n        await writer.drain()\n        logger.info('-> Auth:      ulen=%d plen=%d', len(uname), len(passwd))\n\n        auth_resp = await asyncio.wait_for(reader.readexactly(2), timeout=10)\n        logger.info('<- Auth resp: %s  (status: %#04x)', auth_resp.hex(), auth_resp[1])\n        if auth_resp[1] != 0x00:\n            logger.error('Authentication rejected')\n            return False\n        logger.info('Authentication succeeded')\n    elif resp[1] == AUTH_NO_AUTH:\n        logger.info('Proxy selected no-auth')\n    elif resp[1] == AUTH_NO_ACCEPTABLE:\n        logger.error('Proxy rejected all auth methods')\n        return False\n\n    return True\n\n\nasync def _test_connect_and_verify(\n    reader: asyncio.StreamReader,\n    writer: asyncio.StreamWriter,\n) -> bool:\n    \"\"\"Send CONNECT to httpbin.org:80 and verify with an HTTP request.\"\"\"\n    target = b'httpbin.org'\n    connect_req = (\n        bytes([SOCKS5_VERSION, CMD_CONNECT, 0x00, ATYP_DOMAIN, len(target)])\n        + target\n        + struct.pack('!H', 80)\n    )\n    writer.write(connect_req)\n    await writer.drain()\n    logger.info('-> CONNECT:   %s  (httpbin.org:80)', connect_req.hex())\n\n    reply = await asyncio.wait_for(reader.readexactly(4), timeout=15)\n    logger.info('<- Reply:     %s  (rep: %#04x)', reply.hex(), reply[1])\n\n    if reply[1] != REPLY_SUCCESS:\n        extra = b''\n        try:\n            extra = await asyncio.wait_for(reader.read(256), timeout=1)\n        except (asyncio.TimeoutError, OSError):\n            pass\n        logger.error('CONNECT rejected — reply code %#04x', reply[1])\n        if extra:\n            logger.error('Extra data: %s', extra.hex())\n        logger.error(\n            'Possible causes: invalid/expired credentials, quota exceeded, '\n            'IP not whitelisted, or wrong port'\n        )\n        return False\n\n    await _skip_bnd_address(reader, reply[3], peer='remote proxy')\n    logger.info('CONNECT established')\n\n    http_req = b'GET /ip HTTP/1.1\\r\\nHost: httpbin.org\\r\\nConnection: close\\r\\n\\r\\n'\n    writer.write(http_req)\n    await writer.drain()\n    logger.info('-> HTTP GET /ip sent')\n\n    http_resp = await asyncio.wait_for(reader.read(4096), timeout=15)\n    decoded = http_resp.decode(errors='replace')\n    logger.info('<- HTTP response (%d bytes):\\n%s', len(http_resp), decoded)\n    logger.info('Proxy is fully working!')\n    return True\n\n\nasync def _test_proxy(args: argparse.Namespace) -> None:\n    \"\"\"Perform a direct SOCKS5 handshake test against the remote proxy.\"\"\"\n    logger.info('=== SOCKS5 Direct Test: %s:%s ===', args.remote_host, args.remote_port)\n\n    try:\n        reader, writer = await asyncio.wait_for(\n            asyncio.open_connection(args.remote_host, args.remote_port),\n            timeout=HANDSHAKE_TIMEOUT,\n        )\n    except asyncio.TimeoutError:\n        logger.error('TCP connection timed out')\n        return\n    except OSError as exc:\n        logger.error('TCP connection failed: %s', exc)\n        return\n\n    logger.info('TCP connection established')\n\n    try:\n        if not await _test_negotiate_auth(reader, writer, args.username, args.password):\n            return\n        await _test_connect_and_verify(reader, writer)\n    except _HandshakeError as exc:\n        logger.error('SOCKS5 test failed: %s', exc)\n    except asyncio.TimeoutError:\n        logger.error('Timed out waiting for proxy response')\n    except asyncio.IncompleteReadError as exc:\n        logger.error('Connection closed prematurely (got %d bytes)', len(exc.partial))\n    except OSError as exc:\n        logger.error('Network error: %s', exc)\n    finally:\n        await _close_writer(writer)\n\n\ndef cli() -> None:\n    parser = argparse.ArgumentParser(\n        description='Local SOCKS5 forwarder for authenticated remote proxies.',\n    )\n    parser.add_argument('--remote-host', required=True, help='Remote SOCKS5 proxy host')\n    parser.add_argument('--remote-port', type=int, default=1080, help='Remote SOCKS5 proxy port')\n    parser.add_argument('--username', required=True, help='Remote proxy username')\n    parser.add_argument('--password', required=True, help='Remote proxy password')\n    parser.add_argument('--local-host', default='127.0.0.1', help='Local bind address')\n    parser.add_argument('--local-port', type=int, default=1081, help='Local bind port (0 = random)')\n    parser.add_argument('--verbose', '-v', action='store_true', help='Enable debug logging')\n    parser.add_argument(\n        '--test',\n        action='store_true',\n        help='Test the remote proxy directly (no local server, no Chrome needed)',\n    )\n    args = parser.parse_args()\n\n    logging.basicConfig(\n        level=logging.DEBUG if args.verbose else logging.INFO,\n        format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',\n    )\n\n    if args.test:\n        asyncio.run(_test_proxy(args))\n    else:\n        asyncio.run(_main(args))\n\n\nif __name__ == '__main__':\n    cli()\n"
  },
  {
    "path": "pydoll/utils/user_agent_parser.py",
    "content": "import re\nfrom dataclasses import dataclass, field\n\nfrom pydoll.protocol.emulation.types import UserAgentBrandVersion, UserAgentMetadata\n\n_CHROME_RE = re.compile(r'Chrome/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)')\n_EDGE_RE = re.compile(r'Edg/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)')\n\n_GREASE_BRANDS = [\n    'Not/A)Brand',\n    'Not A;Brand',\n    'Not.A/Brand',\n    'Not)A;Brand',\n    'Not=A?Brand',\n]\n\n_GREASE_MODULO = 100\n\n_PLATFORM_MAP = {\n    'windows': 'Win32',\n    'macintosh': 'MacIntel',\n    'linux': 'Linux x86_64',\n    'android': 'Linux armv81',\n    'iphone': 'iPhone',\n    'ipad': 'iPad',\n    'cros': 'Linux x86_64',\n}\n\n_UA_PLATFORM_MAP = {\n    'windows': 'Windows',\n    'macintosh': 'macOS',\n    'linux': 'Linux',\n    'android': 'Android',\n    'iphone': 'iOS',\n    'ipad': 'iOS',\n    'cros': 'Chrome OS',\n}\n\n_ARCHITECTURE_MAP = {\n    'windows': 'x86',\n    'macintosh': 'arm',\n    'linux': 'x86',\n    'android': 'arm',\n    'iphone': 'arm',\n    'ipad': 'arm',\n    'cros': 'x86',\n}\n\n_WINDOWS_VERSION_MAP = {\n    '6.1': '0.1.0',\n    '6.2': '0.2.0',\n    '6.3': '0.3.0',\n    '10.0': '15.0.0',\n}\n\n_DEFAULT_PLATFORM_VERSIONS = {\n    'windows': '15.0.0',\n    'macintosh': '14.0.0',\n    'android': '14.0.0',\n    'iphone': '17.0.0',\n    'ipad': '17.0.0',\n    'linux': '6.1.0',\n    'cros': '14541.0.0',\n}\n\n_OS_KEYWORDS = [\n    ('android', 'android'),\n    ('iphone', 'iphone'),\n    ('ipad', 'ipad'),\n    ('cros', 'cros'),\n    ('windows', 'windows'),\n    ('macintosh', 'macintosh'),\n    ('mac os x', 'macintosh'),\n    ('linux', 'linux'),\n]\n\n_MOBILE_KEYWORDS = frozenset({'mobile', 'android', 'iphone', 'ipad'})\n\n_VERSION_PATTERNS = {\n    'windows': (r'Windows NT (\\d+\\.\\d+)', None),\n    'macintosh': (r'Mac OS X (\\d+)[_.](\\d+)[_.]?(\\d+)?', None),\n    'android': (r'Android (\\d+(?:\\.\\d+)*)', None),\n    'iphone': (r'OS (\\d+)[_.](\\d+)[_.]?(\\d+)?', None),\n    'ipad': (r'OS (\\d+)[_.](\\d+)[_.]?(\\d+)?', None),\n}\n\n\n@dataclass\nclass ParsedUserAgent:\n    \"\"\"Result of parsing a User-Agent string into consistent metadata.\"\"\"\n\n    platform: str\n    vendor: str\n    app_version: str\n    user_agent_metadata: UserAgentMetadata\n    navigator_override_js: str = field(default='', repr=False)\n\n\nclass UserAgentParser:\n    \"\"\"Stateless parser that extracts consistent metadata from a User-Agent string.\n\n    Given a UA string like:\n        Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\n        (KHTML, like Gecko) Chrome/120.0.6099.109 Safari/537.36\n\n    It produces all the metadata needed for CDP Emulation.setUserAgentOverride\n    and JavaScript navigator property overrides, ensuring full consistency\n    between HTTP headers and JS properties.\n    \"\"\"\n\n    @staticmethod\n    def parse(user_agent: str) -> ParsedUserAgent:\n        \"\"\"Parse a User-Agent string into consistent browser metadata.\n\n        Args:\n            user_agent: Full User-Agent string.\n\n        Returns:\n            ParsedUserAgent with platform, vendor, appVersion,\n            userAgentMetadata, and JS override script.\n        \"\"\"\n        os_key = UserAgentParser._detect_os_key(user_agent)\n        browser_name, major_version, full_version = UserAgentParser._detect_browser(user_agent)\n        is_mobile = UserAgentParser._detect_mobile(user_agent)\n        metadata = UserAgentParser._build_metadata(\n            user_agent, os_key, browser_name, major_version, full_version, is_mobile\n        )\n        vendor = 'Google Inc.'\n        app_version = UserAgentParser._build_app_version(user_agent)\n\n        return ParsedUserAgent(\n            platform=_PLATFORM_MAP.get(os_key, 'Win32'),\n            vendor=vendor,\n            app_version=app_version,\n            user_agent_metadata=metadata,\n            navigator_override_js=UserAgentParser._build_navigator_override_js(vendor, app_version),\n        )\n\n    @staticmethod\n    def _build_metadata(\n        user_agent: str,\n        os_key: str,\n        browser_name: str,\n        major_version: str,\n        full_version: str,\n        is_mobile: bool,\n    ) -> UserAgentMetadata:\n        return UserAgentMetadata(\n            platform=_UA_PLATFORM_MAP.get(os_key, 'Windows'),\n            platformVersion=UserAgentParser._get_platform_version(user_agent, os_key),\n            architecture=_ARCHITECTURE_MAP.get(os_key, 'x86'),\n            model=UserAgentParser._extract_model(user_agent) if is_mobile else '',\n            mobile=is_mobile,\n            brands=UserAgentParser._build_brands(browser_name, major_version),\n            fullVersionList=UserAgentParser._build_full_version_list(browser_name, full_version),\n            bitness='64',\n            wow64=False,\n        )\n\n    @staticmethod\n    def _detect_os_key(user_agent: str) -> str:\n        ua_lower = user_agent.lower()\n        for keyword, os_key in _OS_KEYWORDS:\n            if keyword in ua_lower:\n                return os_key\n        return 'windows'\n\n    @staticmethod\n    def _detect_browser(user_agent: str) -> tuple[str, str, str]:\n        edge_match = _EDGE_RE.search(user_agent)\n        if edge_match:\n            return 'Microsoft Edge', edge_match.group(1), '.'.join(edge_match.groups())\n\n        chrome_match = _CHROME_RE.search(user_agent)\n        if chrome_match:\n            return (\n                'Google Chrome',\n                chrome_match.group(1),\n                '.'.join(chrome_match.groups()),\n            )\n\n        return 'Google Chrome', '120', '120.0.0.0'\n\n    @staticmethod\n    def _detect_mobile(user_agent: str) -> bool:\n        ua_lower = user_agent.lower()\n        return any(keyword in ua_lower for keyword in _MOBILE_KEYWORDS)\n\n    @staticmethod\n    def _build_app_version(user_agent: str) -> str:\n        if user_agent.startswith('Mozilla/'):\n            return user_agent[len('Mozilla/') :]\n        return user_agent\n\n    @staticmethod\n    def _get_platform_version(user_agent: str, os_key: str) -> str:\n        default = _DEFAULT_PLATFORM_VERSIONS.get(os_key, '0.0.0')\n\n        if os_key == 'windows':\n            return UserAgentParser._parse_windows_version(user_agent, default)\n\n        if os_key in {'macintosh', 'iphone', 'ipad'}:\n            pattern = _VERSION_PATTERNS[os_key][0]\n            return UserAgentParser._parse_dotted_version(user_agent, pattern, default)\n\n        if os_key == 'android':\n            match = re.search(r'Android (\\d+(?:\\.\\d+)*)', user_agent)\n            return match.group(1) if match else default\n\n        return default\n\n    @staticmethod\n    def _parse_windows_version(user_agent: str, default: str) -> str:\n        match = re.search(r'Windows NT (\\d+\\.\\d+)', user_agent)\n        if not match:\n            return default\n        return _WINDOWS_VERSION_MAP.get(match.group(1), '15.0.0')\n\n    @staticmethod\n    def _parse_dotted_version(user_agent: str, pattern: str, default: str) -> str:\n        match = re.search(pattern, user_agent)\n        if not match:\n            return default\n        major = match.group(1)\n        minor = match.group(2)\n        patch = match.group(3) or '0'\n        return f'{major}.{minor}.{patch}'\n\n    @staticmethod\n    def _build_grease(major_int: int) -> tuple[str, str, str]:\n        \"\"\"Build GREASE brand, short version, and full version.\"\"\"\n        grease_index = major_int % len(_GREASE_BRANDS)\n        brand = _GREASE_BRANDS[grease_index]\n        short_ver = str(major_int % _GREASE_MODULO) if major_int >= _GREASE_MODULO else '99'\n        full_ver = (\n            f'{major_int % _GREASE_MODULO}.0.0.0' if major_int >= _GREASE_MODULO else '99.0.0.0'\n        )\n        return brand, short_ver, full_ver\n\n    @staticmethod\n    def _build_brands(browser_name: str, major_version: str) -> list[UserAgentBrandVersion]:\n        major_int = int(major_version) if major_version.isdigit() else 120\n        grease_brand, grease_version, _ = UserAgentParser._build_grease(major_int)\n\n        brands: list[UserAgentBrandVersion] = [\n            UserAgentBrandVersion(brand=grease_brand, version=grease_version),\n            UserAgentBrandVersion(brand='Chromium', version=major_version),\n        ]\n\n        if browser_name in {'Google Chrome', 'Microsoft Edge'}:\n            brands.append(UserAgentBrandVersion(brand=browser_name, version=major_version))\n\n        return brands\n\n    @staticmethod\n    def _build_full_version_list(\n        browser_name: str, full_version: str\n    ) -> list[UserAgentBrandVersion]:\n        major = full_version.split('.')[0] if '.' in full_version else full_version\n        major_int = int(major) if major.isdigit() else 120\n        grease_brand, _, grease_full_version = UserAgentParser._build_grease(major_int)\n\n        versions: list[UserAgentBrandVersion] = [\n            UserAgentBrandVersion(brand=grease_brand, version=grease_full_version),\n            UserAgentBrandVersion(brand='Chromium', version=full_version),\n        ]\n\n        if browser_name in {'Google Chrome', 'Microsoft Edge'}:\n            versions.append(UserAgentBrandVersion(brand=browser_name, version=full_version))\n\n        return versions\n\n    @staticmethod\n    def _extract_model(user_agent: str) -> str:\n        match = re.search(r';\\s*([A-Za-z0-9_ ]+)\\s*Build/', user_agent)\n        if match:\n            return match.group(1).strip()\n        return ''\n\n    @staticmethod\n    def _build_navigator_override_js(vendor: str, app_version: str) -> str:\n        safe_vendor = vendor.replace(\"'\", \"\\\\'\")\n        safe_app_version = app_version.replace('\\\\', '\\\\\\\\').replace(\"'\", \"\\\\'\")\n        return (\n            \"Object.defineProperty(Navigator.prototype, 'vendor', \"\n            f\"{{get: () => '{safe_vendor}'}});\\n\"\n            \"Object.defineProperty(Navigator.prototype, 'appVersion', \"\n            f\"{{get: () => '{safe_app_version}'}});\"\n        )\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"pydoll-python\"\nversion = \"2.21.3\"\ndescription = \"Pydoll is a library for automating chromium-based browsers without a WebDriver, offering realistic interactions.\"\nauthors = [\"Thalison Fernandes <thalissfernandes99@gmail.com>\"]\nreadme = \"README.md\"\npackages = [\n    {include = \"pydoll\"}\n]\ninclude = [\"pydoll/py.typed\"]\n\n[tool.poetry.dependencies]\npython = \"^3.10\"\nwebsockets = \"^14\"\naiohttp = \"^3.9.5\"\naiofiles = \"^25.1.0\"\ntyping_extensions = \"^4.14.0\"\n\n\n[tool.poetry.group.dev.dependencies]\nruff = \"^0.7.1\"\npytest = \"^8.3.3\"\ntaskipy = \"^1.14.0\"\npytest-asyncio = \"^0.24.0\"\npytest-cov = \"^6.0.0\"\naioresponses = \"^0.7.7\"\nmkdocs = \"^1.6.1\"\nmkdocs-material = \"^9.6.11\"\npymdown-extensions = \"^10.14.3\"\nmkdocstrings = {extras = [\"python\"], version = \"^0.29.1\"}\ngriffe-typingdoc = \"^0.2.8\"\nmkdocs-static-i18n = \"^1.3.0\"\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n[tool.ruff]\nline-length = 100\ntarget-version = \"py310\"\n\n\n[tool.ruff.lint]\npreview = true\nselect = ['I', 'F', 'E', 'W', 'PL', 'PT']\nignore = ['PLR0913', 'PLR0917', 'PLR0904', 'E701']\nexclude = ['tests', 'tests/*']\n\n[tool.ruff.format]\npreview = true\nquote-style = 'single'\ndocstring-code-format = true\ndocstring-code-line-length = 79\nexclude = ['tests', 'tests/*']\n\n[tool.pytest.ini_options]\npythonpath = \".\"\naddopts = '-p no:warnings'\n\n[tool.taskipy.tasks]\nlint = 'ruff check .; ruff check . --diff'\nformat = 'ruff check . --fix; ruff format .'\ntest = 'pytest -s -x --cov=pydoll -vv'\npost_test = 'coverage html'\n\n[tool.mypy]\nexclude = [\n    \"tests/\",\n]\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "\"\"\"Shared pytest fixtures for all tests.\"\"\"\n\nimport pytest\n\nfrom pydoll.browser.options import ChromiumOptions as Options\n\n\n@pytest.fixture\ndef ci_chrome_options():\n    \"\"\"Chrome options optimized for CI environments.\"\"\"\n    options = Options()\n    options.headless = True\n    options.start_timeout = 60  # Increased timeout for CI\n\n    # CI-specific arguments - essentials only\n    options.add_argument('--no-sandbox')\n    options.add_argument('--disable-dev-shm-usage')\n    options.add_argument('--disable-gpu')\n    options.add_argument('--disable-extensions')\n    options.add_argument('--disable-background-timer-throttling')\n    options.add_argument('--disable-backgrounding-occluded-windows')\n    options.add_argument('--disable-renderer-backgrounding')\n    options.add_argument('--disable-default-apps')\n\n    # Memory optimization\n    options.add_argument('--memory-pressure-off')\n    options.add_argument('--max_old_space_size=4096')\n\n    return options\n\n"
  },
  {
    "path": "tests/pages/oopif/oopif_content.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>OOPIF Content</title>\n    <style>\n        body { font-family: sans-serif; padding: 10px; }\n        .click-counter { color: green; font-weight: bold; }\n    </style>\n</head>\n<body>\n    <h1 id=\"oopif-heading\">Cross-Origin Content</h1>\n    <p id=\"oopif-text\">Content from different origin</p>\n\n    <button id=\"oopif-btn\">OOPIF Button</button>\n    <span id=\"oopif-btn-count\" class=\"click-counter\">0</span>\n\n    <!-- Nested iframe (same origin as this content page) -->\n    <iframe id=\"nested-iframe\" src=\"oopif_nested.html\"\n            style=\"width:600px;height:200px;border:1px solid #666;\"></iframe>\n\n    <!-- Shadow root with elements and a nested iframe -->\n    <div id=\"shadow-host\"></div>\n\n    <script>\n        // Click counter\n        (function() {\n            var count = 0;\n            document.getElementById('oopif-btn').addEventListener('click', function() {\n                count++;\n                document.getElementById('oopif-btn-count').textContent = String(count);\n            });\n        })();\n\n        // Shadow root containing text, button, and a nested iframe\n        (function() {\n            var host = document.getElementById('shadow-host');\n            var shadow = host.attachShadow({ mode: 'open' });\n            shadow.innerHTML = [\n                '<p id=\"shadow-text\">Shadow content inside OOPIF</p>',\n                '<button id=\"shadow-btn\">Shadow Button</button>',\n                '<span id=\"shadow-btn-count\" class=\"click-counter\">0</span>',\n                '<iframe id=\"shadow-iframe\" src=\"oopif_shadow_iframe.html\" ',\n                'style=\"width:500px;height:150px;border:1px solid #999;\"></iframe>',\n            ].join('');\n\n            var count = 0;\n            shadow.getElementById('shadow-btn').addEventListener('click', function() {\n                count++;\n                shadow.getElementById('shadow-btn-count').textContent = String(count);\n            });\n        })();\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "tests/pages/oopif/oopif_main.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>OOPIF Test - Main Page</title>\n</head>\n<body>\n    <h1 id=\"main-heading\">Main Page</h1>\n    <iframe id=\"cross-origin-iframe\" style=\"width:800px;height:600px;border:1px solid #ccc;\"></iframe>\n    <script>\n        var port = new URLSearchParams(location.search).get('port');\n        if (port) {\n            document.getElementById('cross-origin-iframe').src =\n                'http://127.0.0.1:' + port + '/oopif_content.html';\n        }\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "tests/pages/oopif/oopif_nested.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Nested Iframe Content</title>\n</head>\n<body>\n    <h2 id=\"nested-heading\">Nested Iframe Content</h2>\n    <p id=\"nested-text\">Nested inside OOPIF</p>\n    <input id=\"nested-input\" type=\"text\" placeholder=\"Type here\">\n</body>\n</html>\n"
  },
  {
    "path": "tests/pages/oopif/oopif_shadow_iframe.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Shadow Iframe Content</title>\n</head>\n<body>\n    <h2 id=\"shadow-iframe-heading\">Shadow Iframe Content</h2>\n    <p id=\"shadow-iframe-text\">Inside iframe within shadow root in OOPIF</p>\n    <input id=\"shadow-iframe-input\" type=\"text\" placeholder=\"Type in shadow iframe\">\n</body>\n</html>\n"
  },
  {
    "path": "tests/pages/shadow_dom_test.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Shadow DOM Test Page</title>\n</head>\n<body>\n    <h1>Shadow DOM Test</h1>\n\n    <div id=\"open-host\"></div>\n    <div id=\"closed-host\"></div>\n    <div id=\"nested-host\"></div>\n\n    <script>\n        // Open shadow root\n        const openHost = document.getElementById('open-host');\n        const openShadow = openHost.attachShadow({ mode: 'open' });\n        openShadow.innerHTML = `\n            <style>p { color: blue; }</style>\n            <p class=\"open-text\">Open shadow content</p>\n            <button id=\"open-btn\" class=\"shadow-btn\">Open Button</button>\n            <input type=\"email\" name=\"open-email\" placeholder=\"open email\">\n        `;\n\n        // Closed shadow root\n        const closedHost = document.getElementById('closed-host');\n        const closedShadow = closedHost.attachShadow({ mode: 'closed' });\n        closedShadow.innerHTML = `\n            <style>p { color: red; }</style>\n            <p class=\"closed-text\">Closed shadow content</p>\n            <button id=\"closed-btn\" class=\"shadow-btn\">Closed Button</button>\n            <input type=\"password\" name=\"closed-pass\" placeholder=\"closed password\">\n        `;\n\n        // Nested: outer open -> inner component with closed shadow\n        const nestedHost = document.getElementById('nested-host');\n        const outerShadow = nestedHost.attachShadow({ mode: 'open' });\n        outerShadow.innerHTML = `\n            <p class=\"outer-text\">Outer shadow</p>\n            <div id=\"inner-host\"></div>\n        `;\n        const innerHost = outerShadow.getElementById('inner-host');\n        const innerShadow = innerHost.attachShadow({ mode: 'closed' });\n        innerShadow.innerHTML = `\n            <p class=\"inner-text\">Inner closed shadow</p>\n            <button id=\"deep-btn\">Deep Button</button>\n        `;\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "tests/pages/test_children.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Test Children Elements</title>\n</head>\n<body>\n    <div id=\"parent-element\">\n        <div id=\"child1\" class=\"child\">Child 1</div>\n        <span id=\"child2\" class=\"child\">Child 2</span>\n        <p id=\"child3\" class=\"child\">Child 3</p>\n        <a href=\"#link1\" id=\"link1\" class=\"link\">Link 1</a>\n        <a href=\"#link2\" id=\"link2\" class=\"link\">Link 2</a>\n        <div id=\"nested-parent\">\n            <div id=\"nested-child1\">Nested Child 1</div>\n            <span id=\"nested-child2\">Nested Child 2</span>\n            <a href=\"#nested-link\" id=\"nested-link\">Nested Link</a>\n        </div>\n    </div>\n    \n    <div id=\"another-parent\">\n        <button id=\"button1\">Button 1</button>\n        <input id=\"input1\" type=\"text\" value=\"test\">\n        <a href=\"#another-link\" id=\"another-link\">Another Link</a>\n    </div>\n</body>\n</html>\n"
  },
  {
    "path": "tests/pages/test_click_nested.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Nested Click Test Page</title>\n    <style>\n        body { font-family: sans-serif; padding: 20px; }\n        .click-counter { color: green; font-weight: bold; }\n        .section { margin: 20px 0; padding: 10px; border: 1px solid #ccc; }\n    </style>\n</head>\n<body>\n    <h1 id=\"main-heading\">Nested Click Test</h1>\n\n    <!-- Section 1: Regular button with click counter -->\n    <div class=\"section\">\n        <h2>Regular Element</h2>\n        <button id=\"regular-btn\">Regular Button</button>\n        <span id=\"regular-btn-count\" class=\"click-counter\">0</span>\n    </div>\n\n    <!-- Section 2: Shadow root with button -->\n    <div class=\"section\">\n        <h2>Shadow Root Element</h2>\n        <div id=\"shadow-host\"></div>\n    </div>\n\n    <!-- Section 3: Iframe with elements -->\n    <div class=\"section\">\n        <h2>Iframe Element</h2>\n        <iframe id=\"test-iframe\" src=\"test_click_nested_iframe_content.html\"\n                style=\"width: 600px; height: 300px; border: 1px solid #999;\"></iframe>\n    </div>\n\n    <!-- Section 4: Nested shadow roots -->\n    <div class=\"section\">\n        <h2>Nested Shadow Roots</h2>\n        <div id=\"nested-shadow-host\"></div>\n    </div>\n\n    <script>\n        // Regular button click counter\n        (function() {\n            var count = 0;\n            document.getElementById('regular-btn').addEventListener('click', function() {\n                count++;\n                document.getElementById('regular-btn-count').textContent = String(count);\n            });\n        })();\n\n        // Shadow root with clickable button\n        (function() {\n            var host = document.getElementById('shadow-host');\n            var shadow = host.attachShadow({ mode: 'open' });\n            shadow.innerHTML = [\n                '<style>.shadow-btn { padding: 8px 16px; cursor: pointer; }</style>',\n                '<p class=\"shadow-text\">Content inside shadow root</p>',\n                '<button id=\"shadow-btn\" class=\"shadow-btn\">Shadow Button</button>',\n                '<span id=\"shadow-btn-count\" class=\"click-counter\">0</span>',\n            ].join('');\n\n            var count = 0;\n            shadow.getElementById('shadow-btn').addEventListener('click', function() {\n                count++;\n                shadow.getElementById('shadow-btn-count').textContent = String(count);\n            });\n        })();\n\n        // Nested shadow roots: outer -> inner with button\n        (function() {\n            var outerHost = document.getElementById('nested-shadow-host');\n            var outerShadow = outerHost.attachShadow({ mode: 'open' });\n            outerShadow.innerHTML = [\n                '<p class=\"outer-text\">Outer shadow content</p>',\n                '<div id=\"inner-shadow-host\"></div>',\n            ].join('');\n\n            var innerHost = outerShadow.getElementById('inner-shadow-host');\n            var innerShadow = innerHost.attachShadow({ mode: 'closed' });\n            innerShadow.innerHTML = [\n                '<style>.deep-btn { padding: 8px 16px; cursor: pointer; }</style>',\n                '<p class=\"inner-text\">Inner shadow content</p>',\n                '<button id=\"deep-btn\" class=\"deep-btn\">Deep Nested Button</button>',\n                '<span id=\"deep-btn-count\" class=\"click-counter\">0</span>',\n            ].join('');\n\n            var count = 0;\n            innerShadow.getElementById('deep-btn').addEventListener('click', function() {\n                count++;\n                innerShadow.getElementById('deep-btn-count').textContent = String(count);\n            });\n        })();\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "tests/pages/test_click_nested_iframe_content.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Iframe Content with Shadow DOM</title>\n    <style>\n        body { font-family: sans-serif; padding: 10px; }\n        .click-counter { color: green; font-weight: bold; }\n    </style>\n</head>\n<body>\n    <h2 id=\"iframe-heading\">Iframe Content</h2>\n\n    <button id=\"iframe-btn\">Iframe Button</button>\n    <span id=\"iframe-btn-count\" class=\"click-counter\">0</span>\n\n    <div id=\"shadow-host-in-iframe\"></div>\n\n    <script>\n        // Click counter for iframe button\n        (function() {\n            var count = 0;\n            document.getElementById('iframe-btn').addEventListener('click', function() {\n                count++;\n                document.getElementById('iframe-btn-count').textContent = String(count);\n            });\n        })();\n\n        // Shadow root inside the iframe\n        var host = document.getElementById('shadow-host-in-iframe');\n        var shadow = host.attachShadow({ mode: 'open' });\n        shadow.innerHTML = [\n            '<style>.shadow-btn { padding: 8px 16px; cursor: pointer; }</style>',\n            '<p class=\"shadow-text\">Shadow content inside iframe</p>',\n            '<button id=\"shadow-btn-in-iframe\" class=\"shadow-btn\">Shadow Button in Iframe</button>',\n            '<span id=\"shadow-btn-count\" class=\"click-counter\">0</span>',\n        ].join('');\n\n        // Click counter for shadow button inside iframe\n        (function() {\n            var count = 0;\n            shadow.getElementById('shadow-btn-in-iframe').addEventListener('click', function() {\n                count++;\n                shadow.getElementById('shadow-btn-count').textContent = String(count);\n            });\n        })();\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "tests/pages/test_core_simple.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Core Test Page</title>\n    <style>\n        .item { color: #333; }\n        .list-item { margin: 4px 0; }\n        #hidden-button { display: none; }\n        #deep-section { margin-top: 16px; }\n        #click-area { margin: 12px 0; }\n        #btn-1 { padding: 6px 10px; cursor: pointer; }\n        #simple-select { margin-top: 8px; }\n    </style>\n    <script>\n        document.addEventListener('DOMContentLoaded', function(){\n            // click counter\n            var count = 0;\n            var btn = document.getElementById('btn-1');\n            var counter = document.getElementById('btn-1-count');\n            btn.addEventListener('click', function(){\n                count += 1;\n                counter.textContent = String(count);\n            });\n        });\n    </script>\n    <!-- Ensure ready -->\n</head>\n<body>\n    <h1 id=\"main-heading\">Core Test Page</h1>\n\n    <div id=\"content\">\n        <p id=\"intro\" class=\"item\">This page is used by core integration tests.</p>\n\n        <div id=\"click-area\">\n            <button id=\"btn-1\" name=\"primary-button\" class=\"action-btn\">Click Me</button>\n            <span id=\"btn-1-count\">0</span>\n        </div>\n\n        <form id=\"form\">\n            <input id=\"text-input\" name=\"username\" type=\"text\" placeholder=\"Type your name\">\n            <textarea id=\"text-area\" name=\"message\" placeholder=\"Type your message\"></textarea>\n        </form>\n\n        <div id=\"list-container\">\n            <ul id=\"list\">\n                <li id=\"li-1\" class=\"list-item item\">Item 1</li>\n                <li id=\"li-2\" class=\"list-item item\">Item 2</li>\n                <li id=\"li-3\" class=\"list-item item\">Item 3</li>\n            </ul>\n        </div>\n\n        <div id=\"deep-section\">\n            <div id=\"level1\">\n                <div id=\"level2\">\n                    <div id=\"level3\">\n                        <span id=\"deep-span\">Deep nested element</span>\n                    </div>\n                </div>\n            </div>\n        </div>\n\n        <div id=\"select-container\">\n            <label for=\"simple-select\">Choose:</label>\n            <select id=\"simple-select\" name=\"choices\">\n                <option value=\"alpha\">Alpha</option>\n                <option value=\"beta\">Beta</option>\n                <option value=\"gamma\">Gamma</option>\n            </select>\n        </div>\n\n        <button id=\"hidden-button\" class=\"action-btn\">Hidden</button>\n    </div>\n</body>\n</html>\n\n\n"
  },
  {
    "path": "tests/pages/test_frame_content.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Frame Content</title>\n</head>\n<body>\n    <h1 id=\"frame-heading\">Frame Content</h1>\n    <p id=\"frame-paragraph\">This is content inside a frame.</p>\n    <input id=\"frame-input\" type=\"text\" placeholder=\"Frame input\">\n    <button id=\"frame-button\">Frame Button</button>\n</body>\n</html>\n"
  },
  {
    "path": "tests/pages/test_frameset.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Test Frameset</title>\n</head>\n<frameset cols=\"50%,50%\">\n    <frame id=\"left-frame\" src=\"test_frame_content.html\" name=\"leftFrame\">\n    <frame id=\"right-frame\" src=\"test_iframe_content.html\" name=\"rightFrame\">\n</frameset>\n</html>\n"
  },
  {
    "path": "tests/pages/test_har_recording.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>HAR Recording Test Page</title>\n    <style>\n        body { font-family: sans-serif; margin: 20px; }\n        #status { margin-top: 10px; }\n        .result { margin: 5px 0; font-family: monospace; font-size: 13px; }\n    </style>\n</head>\n<body>\n    <h1 id=\"heading\">HAR Recording Test</h1>\n    <div id=\"status\">waiting</div>\n    <div id=\"results\"></div>\n\n    <script>\n        // The base URL is injected via query param: ?base=http://localhost:PORT\n        const params = new URLSearchParams(window.location.search);\n        const base = params.get('base');\n\n        async function runRequests() {\n            const results = document.getElementById('results');\n            const status = document.getElementById('status');\n\n            if (!base) {\n                status.textContent = 'error: no base URL';\n                return;\n            }\n\n            const endpoints = [\n                { path: '/api/users', label: 'GET /api/users' },\n                { path: '/api/data', label: 'GET /api/data' },\n                {\n                    path: '/api/submit',\n                    label: 'POST /api/submit',\n                    options: {\n                        method: 'POST',\n                        headers: { 'Content-Type': 'application/json' },\n                        body: JSON.stringify({ key: 'value' })\n                    }\n                }\n            ];\n\n            for (const ep of endpoints) {\n                try {\n                    const resp = await fetch(base + ep.path, ep.options || {});\n                    const text = await resp.text();\n                    const div = document.createElement('div');\n                    div.className = 'result';\n                    div.textContent = `${ep.label}: ${resp.status} - ${text.substring(0, 80)}`;\n                    results.appendChild(div);\n                } catch (e) {\n                    const div = document.createElement('div');\n                    div.className = 'result';\n                    div.textContent = `${ep.label}: ERROR - ${e.message}`;\n                    results.appendChild(div);\n                }\n            }\n\n            status.textContent = 'done';\n        }\n\n        window.addEventListener('load', runRequests);\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "tests/pages/test_iframe_content.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Iframe Content</title>\n    <style>\n        body {\n            font-family: Arial, sans-serif;\n            padding: 20px;\n        }\n        .hidden {\n            display: none;\n        }\n    </style>\n</head>\n<body>\n    <h1 id=\"iframe-heading\">Iframe Content</h1>\n    \n    <div id=\"iframe-container\">\n        <p id=\"iframe-paragraph\">This is content inside the iframe.</p>\n        \n        <form id=\"iframe-form\">\n            <label for=\"iframe-input\">Name:</label>\n            <input id=\"iframe-input\" type=\"text\" name=\"name\" placeholder=\"Enter your name\">\n            \n            <label for=\"iframe-email\">Email:</label>\n            <input id=\"iframe-email\" type=\"email\" name=\"email\" placeholder=\"Enter your email\">\n            \n            <label for=\"iframe-textarea\">Message:</label>\n            <textarea id=\"iframe-textarea\" name=\"message\" rows=\"4\">Default message</textarea>\n            \n            <button id=\"iframe-submit\" type=\"submit\">Submit</button>\n            <button id=\"iframe-reset\" type=\"reset\">Reset</button>\n        </form>\n        \n        <div id=\"iframe-links\">\n            <a href=\"#link1\" id=\"iframe-link1\" class=\"iframe-link\">Link 1</a>\n            <a href=\"#link2\" id=\"iframe-link2\" class=\"iframe-link\">Link 2</a>\n            <a href=\"#link3\" id=\"iframe-link3\" class=\"iframe-link\">Link 3</a>\n        </div>\n        \n        <div id=\"iframe-buttons\">\n            <button id=\"iframe-button1\" class=\"action-btn\">Button 1</button>\n            <button id=\"iframe-button2\" class=\"action-btn\">Button 2</button>\n            <button id=\"iframe-button3\" class=\"action-btn hidden\">Hidden Button</button>\n        </div>\n        \n        <div id=\"iframe-list\">\n            <ul>\n                <li id=\"item1\" class=\"list-item\">Item 1</li>\n                <li id=\"item2\" class=\"list-item\">Item 2</li>\n                <li id=\"item3\" class=\"list-item\">Item 3</li>\n            </ul>\n        </div>\n        \n        <select id=\"iframe-select\">\n            <option value=\"option1\">Option 1</option>\n            <option value=\"option2\" selected>Option 2</option>\n            <option value=\"option3\">Option 3</option>\n        </select>\n        \n        <div id=\"nested-elements\">\n            <div id=\"level1\">\n                <div id=\"level2\">\n                    <div id=\"level3\">\n                        <span id=\"deep-span\">Deep nested element</span>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n    \n    <script>\n        // Add some interactivity\n        document.getElementById('iframe-submit').addEventListener('click', function(e) {\n            e.preventDefault();\n            console.log('Form submitted');\n        });\n        \n        document.querySelectorAll('.action-btn').forEach(btn => {\n            btn.addEventListener('click', function() {\n                console.log('Button clicked:', this.id);\n            });\n        });\n    </script>\n</body>\n</html>\n\n"
  },
  {
    "path": "tests/pages/test_iframe_nested.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Test Nested Iframes</title>\n</head>\n<body>\n    <h1 id=\"main-heading\">Main Page with Nested Iframes</h1>\n    \n    <div id=\"main-content\">\n        <p id=\"main-paragraph\">Main page content</p>\n        <button id=\"main-button\">Main Button</button>\n    </div>\n    \n    <iframe id=\"parent-iframe\" src=\"test_iframe_parent_level.html\" style=\"width: 100%; height: 800px;\"></iframe>\n    \n    <div id=\"after-iframe\">\n        <p>Content after parent iframe</p>\n    </div>\n</body>\n</html>\n\n"
  },
  {
    "path": "tests/pages/test_iframe_nested_level.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Nested Iframe Level</title>\n    <style>\n        body {\n            background-color: #e0e0ff;\n            padding: 20px;\n        }\n    </style>\n</head>\n<body>\n    <h3 id=\"nested-iframe-heading\">Nested Iframe Content</h3>\n    \n    <div id=\"nested-iframe-content\">\n        <p id=\"nested-paragraph\">This is the nested iframe level (child of parent iframe).</p>\n        <input id=\"nested-input\" type=\"text\" placeholder=\"Nested input\">\n        <button id=\"nested-button\">Nested Button</button>\n        \n        <div id=\"nested-links\">\n            <a href=\"#nested-link1\" id=\"nested-link1\">Nested Link 1</a>\n            <a href=\"#nested-link2\" id=\"nested-link2\">Nested Link 2</a>\n        </div>\n        \n        <form id=\"nested-form\">\n            <input id=\"nested-form-input\" type=\"text\" name=\"username\" placeholder=\"Username\">\n            <input id=\"nested-form-password\" type=\"password\" name=\"password\" placeholder=\"Password\">\n            <button id=\"nested-form-submit\" type=\"submit\">Login</button>\n        </form>\n    </div>\n</body>\n</html>\n\n"
  },
  {
    "path": "tests/pages/test_iframe_parent_level.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Parent Iframe Level</title>\n    <style>\n        body {\n            background-color: #f0f0f0;\n            padding: 20px;\n        }\n    </style>\n</head>\n<body>\n    <h2 id=\"parent-iframe-heading\">Parent Iframe Content</h2>\n    \n    <div id=\"parent-iframe-content\">\n        <p id=\"parent-paragraph\">This is the parent iframe level.</p>\n        <input id=\"parent-input\" type=\"text\" placeholder=\"Parent input\">\n        <button id=\"parent-button\">Parent Button</button>\n    </div>\n    \n    <div id=\"nested-iframe-container\">\n        <h3>Nested Iframe Below:</h3>\n        <iframe id=\"nested-iframe\" src=\"test_iframe_nested_level.html\" style=\"width: 100%; height: 400px;\"></iframe>\n    </div>\n    \n    <div id=\"parent-footer\">\n        <p>Parent iframe footer</p>\n    </div>\n</body>\n</html>\n\n"
  },
  {
    "path": "tests/pages/test_iframe_simple.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Test Simple Iframe</title>\n</head>\n<body>\n    <h1 id=\"main-heading\">Main Page</h1>\n    <div id=\"main-content\">\n        <p id=\"main-paragraph\">This is the main page content.</p>\n        <button id=\"main-button\">Main Button</button>\n        <input id=\"main-input\" type=\"text\" placeholder=\"Main input\">\n    </div>\n    \n    <iframe id=\"simple-iframe\" src=\"test_iframe_content.html\" style=\"width: 800px; height: 600px;\"></iframe>\n    \n    <div id=\"after-iframe\">\n        <p>Content after iframe</p>\n    </div>\n</body>\n</html>\n\n"
  },
  {
    "path": "tests/pages/test_multiple_iframes.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Multiple Iframes Test</title>\n    <style>\n        body {\n            font-family: Arial, sans-serif;\n            padding: 20px;\n        }\n        .iframe-container {\n            margin: 20px 0;\n            border: 2px solid #333;\n            padding: 10px;\n        }\n        h1, h2 {\n            color: #333;\n        }\n        iframe {\n            border: 1px solid #999;\n        }\n    </style>\n</head>\n<body>\n    <h1 id=\"main-heading\">Multiple Iframes Test Page</h1>\n    <p id=\"main-paragraph\">This page contains multiple iframes to test iframe selection.</p>\n    \n    <div class=\"iframe-container\">\n        <h2>First Iframe (Cookie Tracker - should be ignored)</h2>\n        <iframe \n            id=\"cookie-iframe\" \n            src=\"test_iframe_content.html\" \n            width=\"300\" \n            height=\"100\"\n            data-purpose=\"cookie-tracking\">\n        </iframe>\n    </div>\n    \n    <div class=\"iframe-container\">\n        <h2>Second Iframe (Login Form - target iframe)</h2>\n        <iframe \n            id=\"login-iframe\" \n            src=\"test_iframe_content.html\" \n            width=\"400\" \n            height=\"300\"\n            data-purpose=\"login\">\n        </iframe>\n    </div>\n    \n    <div class=\"iframe-container\">\n        <h2>Third Iframe (Analytics - should be ignored)</h2>\n        <iframe \n            id=\"analytics-iframe\" \n            src=\"test_iframe_content.html\" \n            width=\"300\" \n            height=\"100\"\n            data-purpose=\"analytics\">\n        </iframe>\n    </div>\n</body>\n</html>\n\n"
  },
  {
    "path": "tests/test_browser/test_browser_base.py",
    "content": "import asyncio\nimport base64\nfrom unittest.mock import ANY, AsyncMock, MagicMock, patch\n\nimport pytest\nimport pytest_asyncio\n\nfrom pydoll import exceptions\nfrom pydoll.browser.chromium.chrome import Chrome\nfrom pydoll.browser.chromium.base import Browser\nfrom pydoll.browser.managers import (\n    ProxyManager,\n    ChromiumOptionsManager,\n    BrowserProcessManager,\n    TempDirectoryManager,\n)\nfrom pydoll.browser.options import ChromiumOptions as Options\nfrom pydoll.browser.tab import Tab\nfrom pydoll.commands import (\n    BrowserCommands,\n    FetchCommands,\n    RuntimeCommands,\n    StorageCommands,\n    TargetCommands,\n)\nfrom pydoll.protocol.fetch.events import FetchEvent\nfrom pydoll.connection.connection_handler import ConnectionHandler\nfrom pydoll.exceptions import (\n    MissingTargetOrWebSocket,\n    InvalidWebSocketAddress,\n)\n\nfrom pydoll.protocol.network.types import RequestMethod, ErrorReason\nfrom pydoll.protocol.browser.types import DownloadBehavior, PermissionType\n\nclass ConcreteBrowser(Browser):\n    def _get_default_binary_location(self) -> str:\n        return '/fake/path/to/browser'\n\n\n@pytest_asyncio.fixture\nasync def mock_browser():\n    with (\n        patch.multiple(\n            Browser,\n            _get_default_binary_location=MagicMock(return_value='/fake/path/to/browser'),\n        ),\n        patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ) as mock_process_manager,\n        patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ) as mock_temp_dir_manager,\n        patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ) as mock_conn_handler,\n        patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ) as mock_proxy_manager,\n    ):\n        options = Options()\n        options.binary_location = None\n\n        options_manager = ChromiumOptionsManager(options)\n        browser = ConcreteBrowser(options_manager)\n        browser._browser_process_manager = mock_process_manager.return_value\n        browser._temp_directory_manager = mock_temp_dir_manager.return_value\n        browser._proxy_manager = mock_proxy_manager.return_value\n        browser._connection_handler = mock_conn_handler.return_value\n        browser._connection_handler.execute_command = AsyncMock()\n        browser._connection_handler.register_callback = AsyncMock()\n\n        mock_temp_dir_manager.return_value.create_temp_dir.return_value = MagicMock(name='temp_dir')\n\n        yield browser\n\n\n@pytest.mark.asyncio\nasync def test_browser_initialization(mock_browser):\n    assert isinstance(mock_browser.options, Options)\n    assert isinstance(mock_browser._proxy_manager, ProxyManager)\n    assert isinstance(mock_browser._browser_process_manager, BrowserProcessManager)\n    assert isinstance(mock_browser._temp_directory_manager, TempDirectoryManager)\n    assert isinstance(mock_browser._connection_handler, ConnectionHandler)\n    assert mock_browser._connection_port in range(9223, 9323)\n\n\n@pytest.mark.asyncio\nasync def test_start_browser_success(mock_browser):\n    mock_browser._connection_handler.ping.return_value = True\n    mock_browser._get_valid_tab_id = AsyncMock(return_value='page1')\n\n    tab = await mock_browser.start()\n    assert isinstance(tab, Tab)\n\n    mock_browser._browser_process_manager.start_browser_process.assert_called_once_with(\n        '/fake/path/to/browser',\n        mock_browser._connection_port,\n        mock_browser.options.arguments,\n    )\n\n    assert '--user-data-dir=' in str(\n        mock_browser.options.arguments\n    ), 'Temporary directory not configured'\n\n\n@pytest.mark.asyncio\nasync def test_start_browser_failure(mock_browser):\n    mock_browser._connection_handler.ping.return_value = False\n    with patch('pydoll.browser.chromium.base.asyncio.sleep', AsyncMock()) as mock_sleep:\n        mock_sleep.return_value = False\n        with pytest.raises(exceptions.FailedToStartBrowser):\n            await mock_browser.start()\n\n\n@pytest.mark.asyncio\nasync def test_start_browser_failure_with_start_timeout(mock_browser):\n    browser_launched = False\n\n    async def launch_browser_later():\n        nonlocal browser_launched\n        await asyncio.sleep(2)\n        browser_launched = True\n\n    def start_browser_process_side_effect(*args, **kwargs):\n        asyncio.create_task(launch_browser_later())\n\n    async def ping_side_effect():\n        nonlocal browser_launched\n        return browser_launched\n\n    mock_browser.options.start_timeout = 1\n    mock_browser._get_valid_tab_id = AsyncMock(return_value='page1')\n    mock_browser._browser_process_manager.start_browser_process.side_effect = (\n        start_browser_process_side_effect\n    )\n    mock_browser._connection_handler.ping = AsyncMock(side_effect=ping_side_effect)\n\n    with pytest.raises(exceptions.FailedToStartBrowser):\n        await mock_browser.start()\n\n\n@pytest.mark.asyncio\nasync def test_start_browser_success_with_start_timeout(mock_browser):\n    browser_launched = False\n\n    async def launch_browser_later():\n        nonlocal browser_launched\n        await asyncio.sleep(2)\n        browser_launched = True\n\n    def start_browser_process_side_effect(*args, **kwargs):\n        asyncio.create_task(launch_browser_later())\n\n    async def ping_side_effect():\n        nonlocal browser_launched\n        return browser_launched\n\n    mock_browser.options.start_timeout = 3\n    mock_browser._get_valid_tab_id = AsyncMock(return_value='page1')\n    mock_browser._browser_process_manager.start_browser_process.side_effect = (\n        start_browser_process_side_effect\n    )\n    mock_browser._connection_handler.ping = AsyncMock(side_effect=ping_side_effect)\n\n    await mock_browser.start()\n\n\n@pytest.mark.asyncio\nasync def test_proxy_configuration(mock_browser):\n    mock_browser._proxy_manager.get_proxy_credentials = MagicMock(\n        return_value=(True, ('user', 'pass'))\n    )\n    mock_browser._get_valid_tab_id = AsyncMock(return_value='page1')\n    await mock_browser.start()\n\n    mock_browser._connection_handler.execute_command.assert_any_call(\n        FetchCommands.enable(handle_auth_requests=True, resource_type=None)\n    )\n    mock_browser._connection_handler.register_callback.assert_any_call(\n        FetchEvent.REQUEST_PAUSED, ANY, True\n    )\n    mock_browser._connection_handler.register_callback.assert_any_call(\n        FetchEvent.AUTH_REQUIRED,\n        ANY,\n        True,\n    )\n\n\n@pytest.mark.asyncio\nasync def test_new_tab(mock_browser):\n    mock_browser._connection_handler.execute_command.return_value = {\n        'result': {'targetId': 'new_page'}\n    }\n    tab = await mock_browser.new_tab()\n    assert tab._target_id == 'new_page'\n    assert isinstance(tab, Tab)\n\n\n@pytest.mark.asyncio\nasync def test_connect_with_ws_address_returns_tab_and_sets_handler_ws(mock_browser):\n    ws_browser = 'ws://localhost:9222/devtools/browser/abcdef'\n    mock_browser.get_targets = AsyncMock(return_value=[{'type': 'page', 'url': 'https://example', 'targetId': 'p1'}])\n    mock_browser._get_valid_tab_id = AsyncMock(return_value='p1')\n    mock_browser._connection_handler._ensure_active_connection = AsyncMock()\n\n    tab = await mock_browser.connect(ws_browser)\n\n    assert mock_browser._ws_address == ws_browser\n    assert mock_browser._connection_handler._ws_address == ws_browser\n    mock_browser._connection_handler._ensure_active_connection.assert_awaited_once()\n\n    # The returned Tab should connect using page ws address derived from browser ws\n    assert isinstance(tab, Tab)\n    assert tab._ws_address == 'ws://localhost:9222/devtools/page/p1'\n\n\n@pytest.mark.asyncio\nasync def test_connect_with_ws_address_preserves_token_in_tab_ws(mock_browser):\n    ws_browser = 'ws://localhost:9222/devtools/browser/abcdef?token=secrettoken'\n    mock_browser.get_targets = AsyncMock(return_value=[{'type': 'page', 'url': 'https://example', 'targetId': 'p1'}])\n    mock_browser._get_valid_tab_id = AsyncMock(return_value='p1')\n    mock_browser._connection_handler._ensure_active_connection = AsyncMock()\n\n    tab = await mock_browser.connect(ws_browser)\n\n    assert mock_browser._ws_address == ws_browser\n    assert mock_browser._connection_handler._ws_address == ws_browser\n    mock_browser._connection_handler._ensure_active_connection.assert_awaited_once()\n\n    # Token should be preserved in page-level ws URL\n    assert isinstance(tab, Tab)\n    assert tab._ws_address == 'ws://localhost:9222/devtools/page/p1?token=secrettoken'\n\n\n@pytest.mark.asyncio\nasync def test_new_tab_uses_ws_base_when_ws_address_present(mock_browser):\n    # Simulate browser connected via ws\n    mock_browser._ws_address = 'ws://127.0.0.1:9222/devtools/browser/xyz'\n    mock_browser._connection_handler.execute_command.return_value = {\n        'result': {'targetId': 'new_page'}\n    }\n\n    tab = await mock_browser.new_tab()\n\n    assert isinstance(tab, Tab)\n    assert tab._ws_address == 'ws://127.0.0.1:9222/devtools/page/new_page'\n    # When ws_address is used, target_id can be known from create_target response\n    assert tab._target_id == 'new_page'\n\n\n@pytest.mark.asyncio\nasync def test_get_window_id_for_tab_uses_ws_target_when_no_target_id(mock_browser):\n    # Tab created only with ws address\n    tab = Tab(mock_browser, ws_address='ws://localhost:9222/devtools/page/targetXYZ')\n    mock_browser._connection_handler.execute_command.return_value = {\n        'result': {'windowId': 'win1'}\n    }\n\n    window_id = await mock_browser.get_window_id_for_tab(tab)\n    assert window_id == 'win1'\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        BrowserCommands.get_window_for_target('targetXYZ'), timeout=60\n    )\n\n\n@pytest.mark.asyncio\nasync def test_cookie_management(mock_browser):\n    cookies = [{'name': 'test', 'value': '123'}]\n    await mock_browser.set_cookies(cookies)\n    mock_browser._connection_handler.execute_command.assert_any_call(\n        StorageCommands.set_cookies(cookies=cookies, browser_context_id=None), timeout=60\n    )\n\n    mock_browser._connection_handler.execute_command.return_value = {'result': {'cookies': cookies}}\n    result = await mock_browser.get_cookies()\n    assert result == cookies\n\n    await mock_browser.delete_all_cookies()\n    mock_browser._connection_handler.execute_command.assert_any_await(\n        StorageCommands.clear_cookies(), timeout=60\n    )\n\n\n@pytest.mark.asyncio\nasync def test_event_registration(mock_browser):\n    callback = MagicMock()\n    mock_browser._connection_handler.register_callback.return_value = 123\n\n    callback_id = await mock_browser.on('test_event', callback, temporary=True)\n    assert callback_id == 123\n\n    mock_browser._connection_handler.register_callback.assert_called_with('test_event', ANY, True)\n\n\n@pytest.mark.asyncio\nasync def test_remove_callback_success(mock_browser):\n    \"\"\"Browser.remove_callback should forward to connection handler and return True.\"\"\"\n    mock_browser._connection_handler.remove_callback = AsyncMock(return_value=True)\n\n    result = await mock_browser.remove_callback(42)\n\n    mock_browser._connection_handler.remove_callback.assert_called_with(42)\n    assert result is True\n\n\n@pytest.mark.asyncio\nasync def test_remove_callback_false(mock_browser):\n    \"\"\"Browser.remove_callback should return False when handler returns False.\"\"\"\n    mock_browser._connection_handler.remove_callback = AsyncMock(return_value=False)\n\n    result = await mock_browser.remove_callback(77)\n\n    mock_browser._connection_handler.remove_callback.assert_called_with(77)\n    assert result is False\n\n\n@pytest.mark.asyncio\nasync def test_window_management(mock_browser):\n    mock_browser._connection_handler.execute_command.return_value = {\n        'result': {'windowId': 'window1'}\n    }\n    mock_browser.get_window_id = AsyncMock(return_value='window1')\n\n    bounds = {'width': 800, 'height': 600}\n    await mock_browser.set_window_bounds(bounds)\n    mock_browser._connection_handler.execute_command.assert_any_await(\n        BrowserCommands.set_window_bounds('window1', bounds), timeout=60\n    )\n\n    await mock_browser.set_window_maximized()\n    mock_browser._connection_handler.execute_command.assert_any_await(\n        BrowserCommands.set_window_maximized('window1'), timeout=60\n    )\n\n    await mock_browser.set_window_minimized()\n    mock_browser._connection_handler.execute_command.assert_any_await(\n        BrowserCommands.set_window_minimized('window1'), timeout=60\n    )\n\n\n@pytest.mark.asyncio\nasync def test_get_window_id_for_target(mock_browser):\n    mock_browser._connection_handler.ping.return_value = True\n    mock_browser._get_valid_tab_id = AsyncMock(return_value='page1')\n\n    tab = await mock_browser.start()\n    mock_browser._connection_handler.execute_command.return_value = {\n        'result': {'windowId': 'page1'}\n    }\n    window_id = await mock_browser.get_window_id_for_tab(tab)\n    assert window_id == 'page1'\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        BrowserCommands.get_window_for_target('page1'), timeout=60\n    )\n\n\n@pytest.mark.asyncio\nasync def test_get_window_id_for_tab_raises_when_no_target_id_and_no_ws(mock_browser):\n    # Tab created only with connection_port, without target_id and ws\n    tab = Tab(mock_browser, connection_port=9222)\n    with pytest.raises(MissingTargetOrWebSocket):\n        await mock_browser.get_window_id_for_tab(tab)\n\n\ndef test__validate_ws_address_raises_on_invalid_scheme():\n    with pytest.raises(InvalidWebSocketAddress):\n        Browser._validate_ws_address('http://localhost:9222/devtools/browser/abc')\n\n\ndef test__validate_ws_address_accepts_ws_scheme():\n    Browser._validate_ws_address('ws://localhost:9222/devtools/browser/abc')\n\n\ndef test__validate_ws_address_accepts_wss_scheme():\n    Browser._validate_ws_address('wss://connect.browserbase.com/devtools/browser/abc')\n\n\ndef test__validate_ws_address_raises_on_insufficient_slashes():\n    with pytest.raises(InvalidWebSocketAddress):\n        Browser._validate_ws_address('ws://localhost')\n\n\ndef test__validate_ws_address_raises_on_insufficient_slashes_wss():\n    with pytest.raises(InvalidWebSocketAddress):\n        Browser._validate_ws_address('wss://localhost')\n\n\ndef test__get_tab_ws_address_raises_when_ws_not_set(mock_browser):\n    mock_browser._ws_address = None\n    with pytest.raises(InvalidWebSocketAddress):\n        mock_browser._get_tab_ws_address('some-tab')\n\n\ndef test__get_tab_ws_address_preserves_query_and_fragment(mock_browser):\n    mock_browser._ws_address = 'ws://host:9222/devtools/browser/abc?token=XYZ#frag'\n    result = mock_browser._get_tab_ws_address('tab1')\n    assert result == 'ws://host:9222/devtools/page/tab1?token=XYZ#frag'\n\n\ndef test__get_tab_ws_address_preserves_wss_scheme(mock_browser):\n    mock_browser._ws_address = 'wss://connect.browserbase.com/devtools/browser/abc?token=secret'\n    result = mock_browser._get_tab_ws_address('tab1')\n    assert result == 'wss://connect.browserbase.com/devtools/page/tab1?token=secret'\n\n\n@pytest.mark.asyncio\nasync def test_get_window_id(mock_browser):\n    mock_browser.get_targets = AsyncMock(return_value=[{'targetId': 'target1', 'type': 'page'}])\n    mock_browser._connection_handler.execute_command.return_value = {\n        'result': {'windowId': 'window1'}\n    }\n    window_id = await mock_browser.get_window_id()\n    assert window_id == 'window1'\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        BrowserCommands.get_window_for_target('target1'), timeout=60\n    )\n\n\n@pytest.mark.asyncio\nasync def test_stop_browser(mock_browser):\n    await mock_browser.stop()\n    mock_browser._connection_handler.execute_command.assert_any_await(\n        BrowserCommands.close(), timeout=60\n    )\n    mock_browser._browser_process_manager.stop_process.assert_called_once()\n    mock_browser._temp_directory_manager.cleanup.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_stop_browser_not_running(mock_browser):\n    mock_browser._connection_handler.ping.return_value = False\n    with patch('pydoll.browser.chromium.base.asyncio.sleep', AsyncMock()) as mock_sleep:\n        mock_sleep.return_value = False\n        with pytest.raises(exceptions.BrowserNotRunning):\n            await mock_browser.stop()\n\n\n@pytest.mark.asyncio\nasync def test_context_manager(mock_browser):\n    async with mock_browser as browser:\n        assert browser == mock_browser\n\n    mock_browser._temp_directory_manager.cleanup.assert_called_once()\n    mock_browser._browser_process_manager.stop_process.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_enable_events(mock_browser):\n    await mock_browser.enable_fetch_events(handle_auth_requests=True, resource_type='XHR')\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        FetchCommands.enable(handle_auth_requests=True, resource_type='XHR')\n    )\n\n\n@pytest.mark.asyncio\nasync def test_disable_events(mock_browser):\n    await mock_browser.disable_fetch_events()\n    mock_browser._connection_handler.execute_command.assert_called_with(FetchCommands.disable())\n\n\n@pytest.mark.asyncio\nasync def test__continue_request_callback(mock_browser):\n    await mock_browser._continue_request_callback({'params': {'requestId': 'request1'}})\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        FetchCommands.continue_request('request1'), timeout=60\n    )\n\n\n@pytest.mark.asyncio\nasync def test__continue_request_auth_required_callback(mock_browser):\n    await mock_browser._continue_request_with_auth_callback(\n        event={'params': {'requestId': 'request1'}},\n        proxy_username='user',\n        proxy_password='pass',\n    )\n\n    mock_browser._connection_handler.execute_command.assert_any_call(\n        FetchCommands.continue_request_with_auth('request1', 'ProvideCredentials', 'user', 'pass'),\n        timeout=60,\n    )\n\n    mock_browser._connection_handler.execute_command.assert_any_call(FetchCommands.disable())\n\n\ndef test__is_valid_tab(mock_browser):\n    result = mock_browser._is_valid_tab(\n        {\n            'type': 'page',\n            'url': 'chrome://newtab/',\n        }\n    )\n    assert result is True\n\n\ndef test__is_valid_tab_not_a_tab(mock_browser):\n    result = mock_browser._is_valid_tab(\n        {\n            'type': 'tab',\n            'url': 'chrome://newtab/',\n        }\n    )\n    assert result is False\n\n\n@pytest.mark.parametrize(\n    'os_name, expected_browser_paths, mock_return_value',\n    [\n        (\n            'Windows',\n            [\n                r'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',\n                r'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',\n            ],\n            r'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',\n        ),\n        ('Linux', ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable'], '/usr/bin/google-chrome'),\n        (\n            'Darwin',\n            ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'],\n            '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',\n        ),\n    ],\n)\n@patch('pydoll.browser.chromium.chrome.validate_browser_paths')\n@patch('platform.system')\ndef test__get_default_binary_location(\n    mock_platform_system,\n    mock_validate_browser_paths,\n    os_name,\n    expected_browser_paths,\n    mock_return_value,\n):\n    mock_platform_system.return_value = os_name\n    mock_validate_browser_paths.return_value = mock_return_value\n    path = Chrome._get_default_binary_location()\n    mock_validate_browser_paths.assert_called_once_with(expected_browser_paths)\n\n    assert path == mock_return_value\n\n\ndef test__get_default_binary_location_unsupported_os():\n    with patch('platform.system', return_value='SomethingElse'):\n        with pytest.raises(exceptions.UnsupportedOS, match='Unsupported OS: SomethingElse'):\n            Chrome._get_default_binary_location()\n\n\n@patch('platform.system')\ndef test__get_default_binary_location_throws_exception_if_os_not_supported(\n    mock_platform_system,\n):\n    mock_platform_system.return_value = 'FreeBSD'\n\n    with pytest.raises(exceptions.UnsupportedOS, match='Unsupported OS: FreeBSD'):\n        Chrome._get_default_binary_location()\n\n\n@pytest.mark.asyncio\nasync def test_create_browser_context(mock_browser):\n    mock_browser._execute_command = AsyncMock()\n    mock_browser._execute_command.return_value = {'result': {'browserContextId': 'context1'}}\n\n    context_id = await mock_browser.create_browser_context()\n    assert context_id == 'context1'\n\n    mock_browser._execute_command.assert_called_with(TargetCommands.create_browser_context())\n\n    # Test with proxy\n    mock_browser._execute_command.return_value = {'result': {'browserContextId': 'context2'}}\n    context_id = await mock_browser.create_browser_context(\n        proxy_server='http://proxy.example.com:8080', proxy_bypass_list='localhost'\n    )\n    assert context_id == 'context2'\n    mock_browser._execute_command.assert_called_with(\n        TargetCommands.create_browser_context(\n            proxy_server='http://proxy.example.com:8080', proxy_bypass_list='localhost'\n        )\n    )\n\n\n@pytest.mark.asyncio\nasync def test_create_browser_context_with_private_proxy_sanitizes_and_stores_auth(mock_browser):\n    mock_browser._execute_command = AsyncMock()\n    mock_browser._execute_command.return_value = {'result': {'browserContextId': 'ctx1'}}\n\n    context_id = await mock_browser.create_browser_context(\n        proxy_server='http://user:pass@proxy.example.com:8080',\n        proxy_bypass_list='localhost',\n    )\n\n    assert context_id == 'ctx1'\n    # Should send sanitized proxy (without credentials) to CDP\n    mock_browser._execute_command.assert_called_with(\n        TargetCommands.create_browser_context(\n            proxy_server='http://proxy.example.com:8080', proxy_bypass_list='localhost'\n        )\n    )\n    # Credentials must be stored per-context for later Tab setup\n    assert mock_browser._context_proxy_auth['ctx1'] == ('user', 'pass')\n\n\n@pytest.mark.asyncio\nasync def test_create_browser_context_with_private_proxy_no_scheme_sanitizes_and_stores_auth(\n    mock_browser,\n):\n    mock_browser._execute_command = AsyncMock()\n    mock_browser._execute_command.return_value = {'result': {'browserContextId': 'ctx2'}}\n\n    # Without scheme -> should default to http://\n    context_id = await mock_browser.create_browser_context(\n        proxy_server='user:pwd@host.local:9000'\n    )\n\n    assert context_id == 'ctx2'\n    mock_browser._execute_command.assert_called_with(\n        TargetCommands.create_browser_context(proxy_server='http://host.local:9000', proxy_bypass_list=None)\n    )\n    assert mock_browser._context_proxy_auth['ctx2'] == ('user', 'pwd')\n\n\n@pytest.mark.parametrize(\n    'input_proxy, expected_sanitized, expected_creds',\n    [\n        ('username:password@host:8080', 'http://host:8080', ('username', 'password')),\n        ('http://username:password@host:8080', 'http://host:8080', ('username', 'password')),\n        ('socks5://user:pass@10.0.0.1:1080', 'socks5://10.0.0.1:1080', ('user', 'pass')),\n        ('user@host:3128', 'http://host:3128', ('user', '')),\n        ('http://user@host:8080', 'http://host:8080', ('user', '')),\n        ('host:3128', 'http://host:3128', None),\n    ],\n)\ndef test__sanitize_proxy_and_extract_auth_variants(input_proxy, expected_sanitized, expected_creds):\n    sanitized, creds = Browser._sanitize_proxy_and_extract_auth(input_proxy)\n    assert sanitized == expected_sanitized\n    assert creds == expected_creds\n\n\n@pytest.mark.asyncio\n@patch('pydoll.browser.chromium.base.Tab')\nasync def test_new_tab_sets_up_context_proxy_auth_handlers(MockTab, mock_browser):\n    # Arrange context credentials\n    context_id = 'ctx-auth'\n    mock_browser._context_proxy_auth[context_id] = ('u1', 'p1')\n\n    # Mock CDP create_target response\n    mock_browser._connection_handler.execute_command.return_value = {\n        'result': {'targetId': 'new_page_ctx'}\n    }\n\n    # Fake Tab with async methods\n    fake_tab = MagicMock()\n    fake_tab.enable_fetch_events = AsyncMock()\n    fake_tab.on = AsyncMock()\n    MockTab.return_value = fake_tab\n\n    # Act\n    tab = await mock_browser.new_tab(browser_context_id=context_id)\n\n    # Assert: enable fetch events with auth handling\n    fake_tab.enable_fetch_events.assert_awaited_once()\n    enable_call = fake_tab.enable_fetch_events.await_args\n    assert enable_call.kwargs.get('handle_auth') is True\n\n    # Assert: event handlers registered with temporary=True\n    from pydoll.protocol.fetch.events import FetchEvent as FE\n    # First: request paused\n    assert any(\n        (c.args[0] == FE.REQUEST_PAUSED and c.kwargs.get('temporary') is True)\n        for c in fake_tab.on.await_args_list\n    )\n    # Second: auth required\n    auth_calls = [c for c in fake_tab.on.await_args_list if c.args[0] == FE.AUTH_REQUIRED]\n    assert len(auth_calls) == 1\n    cb = auth_calls[0].args[1]\n    from functools import partial as _partial\n    assert isinstance(cb, _partial)\n    assert cb.keywords.get('proxy_username') == 'u1'\n    assert cb.keywords.get('proxy_password') == 'p1'\n    assert cb.keywords.get('tab') is fake_tab\n\n    # Returned tab is the fake\n    assert tab is fake_tab\n\n\n@pytest.mark.asyncio\n@patch('pydoll.browser.chromium.base.Tab')\nasync def test_new_tab_without_context_proxy_auth_does_not_setup_handlers(MockTab, mock_browser):\n    # No credentials stored for this context\n    context_id = 'ctx-no-auth'\n    mock_browser._context_proxy_auth.pop(context_id, None)\n\n    mock_browser._connection_handler.execute_command.return_value = {\n        'result': {'targetId': 'new_page2'}\n    }\n\n    fake_tab = MagicMock()\n    fake_tab.enable_fetch_events = AsyncMock()\n    fake_tab.on = AsyncMock()\n    MockTab.return_value = fake_tab\n\n    await mock_browser.new_tab(browser_context_id=context_id)\n\n    fake_tab.enable_fetch_events.assert_not_called()\n    fake_tab.on.assert_not_called()\n\n\n@pytest.mark.asyncio\nasync def test_delete_browser_context(mock_browser):\n    mock_browser._execute_command = AsyncMock()\n    await mock_browser.delete_browser_context('context1')\n    mock_browser._execute_command.assert_called_with(\n        TargetCommands.dispose_browser_context('context1')\n    )\n\n\n@pytest.mark.asyncio\nasync def test_get_browser_contexts(mock_browser):\n    mock_browser._execute_command = AsyncMock()\n    mock_browser._execute_command.return_value = {\n        'result': {'browserContextIds': ['context1', 'context2']}\n    }\n\n    contexts = await mock_browser.get_browser_contexts()\n    assert contexts == ['context1', 'context2']\n    mock_browser._execute_command.assert_called_with(TargetCommands.get_browser_contexts())\n\n\n@pytest.mark.asyncio\nasync def test_set_download_behavior(mock_browser):\n    await mock_browser.set_download_behavior(\n        behavior=DownloadBehavior.ALLOW, download_path='/downloads', events_enabled=True\n    )\n\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        BrowserCommands.set_download_behavior(\n            behavior=DownloadBehavior.ALLOW,\n            download_path='/downloads',\n            browser_context_id=None,\n            events_enabled=True,\n        ),\n        timeout=60,\n    )\n\n\n@pytest.mark.asyncio\nasync def test_set_download_path(mock_browser):\n    mock_browser._execute_command = AsyncMock()\n    await mock_browser.set_download_path(path='/downloads')\n    mock_browser._execute_command.assert_called_with(\n        BrowserCommands.set_download_behavior(\n            behavior=DownloadBehavior.ALLOW,\n            download_path='/downloads',\n            browser_context_id=None,\n        )\n    )\n\n\n@pytest.mark.asyncio\nasync def test_grant_permissions(mock_browser):\n    permissions = [PermissionType.GEOLOCATION, PermissionType.NOTIFICATIONS]\n\n    await mock_browser.grant_permissions(permissions=permissions, origin='https://example.com')\n\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        BrowserCommands.grant_permissions(\n            permissions=permissions, origin='https://example.com', browser_context_id=None\n        ),\n        timeout=60,\n    )\n\n\n@pytest.mark.asyncio\nasync def test_reset_permissions(mock_browser):\n    await mock_browser.reset_permissions()\n\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        BrowserCommands.reset_permissions(browser_context_id=None), timeout=60\n    )\n\n\n@pytest.mark.asyncio\nasync def test_get_version(mock_browser):\n    mock_browser._connection_handler.execute_command.return_value = {\n        'result': {\n            'protocolVersion': '1.3',\n            'product': 'Chrome/90.0.4430.93',\n            'revision': '@abcdef',\n            'userAgent': 'Mozilla/5.0...',\n            'jsVersion': '9.0',\n        }\n    }\n\n    version = await mock_browser.get_version()\n    assert version['protocolVersion'] == '1.3'\n    assert version['product'] == 'Chrome/90.0.4430.93'\n\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        BrowserCommands.get_version(), timeout=60\n    )\n\n\n@pytest.mark.asyncio\nasync def test_headless_mode(mock_browser):\n    mock_browser._connection_handler.ping.return_value = True\n    mock_browser._get_valid_tab_id = AsyncMock(return_value='page1')\n\n    await mock_browser.start(headless=True)\n\n    assert '--headless' in mock_browser.options.arguments\n    mock_browser._browser_process_manager.start_browser_process.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_multiple_tab_handling(mock_browser):\n    # Simulate getting multiple tabs\n    mock_browser._connection_handler.execute_command.side_effect = [\n        {'result': {'targetId': 'tab1'}},\n        {'result': {'targetId': 'tab2'}},\n    ]\n\n    tab1 = await mock_browser.new_tab()\n    tab2 = await mock_browser.new_tab()\n\n    assert tab1._target_id == 'tab1'\n    assert tab2._target_id == 'tab2'\n\n    # Verify that correct calls were made\n    calls = mock_browser._connection_handler.execute_command.call_args_list\n    assert len(calls) == 2\n\n\n# New tests for _get_valid_tab_id\n@pytest.mark.asyncio\nasync def test_get_valid_tab_id_success():\n    \"\"\"Test _get_valid_tab_id with a valid tab.\"\"\"\n    targets = [\n        {'type': 'page', 'url': 'https://example.com', 'targetId': 'valid_tab_1'},\n        {'type': 'extension', 'url': 'chrome-extension://abc123', 'targetId': 'ext_1'},\n        {'type': 'page', 'url': 'chrome://newtab/', 'targetId': 'valid_tab_2'},\n    ]\n\n    result = await Browser._get_valid_tab_id(targets)\n    assert result == 'valid_tab_1'\n\n\n@pytest.mark.asyncio\nasync def test_get_valid_tab_id_no_valid_tabs():\n    \"\"\"Test _get_valid_tab_id when there are no valid tabs.\"\"\"\n    targets = [\n        {'type': 'extension', 'url': 'chrome-extension://abc123', 'targetId': 'ext_1'},\n        {'type': 'background_page', 'url': 'chrome://background', 'targetId': 'bg_1'},\n    ]\n\n    with pytest.raises(exceptions.NoValidTabFound):\n        await Browser._get_valid_tab_id(targets)\n\n\n@pytest.mark.asyncio\nasync def test_get_valid_tab_id_empty_targets():\n    \"\"\"Test _get_valid_tab_id with empty targets list.\"\"\"\n    targets = []\n\n    with pytest.raises(exceptions.NoValidTabFound):\n        await Browser._get_valid_tab_id(targets)\n\n\n@pytest.mark.asyncio\nasync def test_get_valid_tab_id_missing_target_id():\n    \"\"\"Test _get_valid_tab_id when valid tab has no targetId.\"\"\"\n    targets = [\n        {'type': 'page', 'url': 'https://example.com'},  # No targetId\n        {'type': 'extension', 'url': 'chrome-extension://abc123', 'targetId': 'ext_1'},\n    ]\n\n    with pytest.raises(exceptions.NoValidTabFound, match='Tab missing targetId'):\n        await Browser._get_valid_tab_id(targets)\n\n\n@pytest.mark.asyncio\nasync def test_get_valid_tab_id_filters_extensions():\n    \"\"\"Test if _get_valid_tab_id correctly filters extensions.\"\"\"\n    targets = [\n        {'type': 'page', 'url': 'chrome-extension://abc123/popup.html', 'targetId': 'ext_page'},\n        {'type': 'page', 'url': 'https://example.com', 'targetId': 'valid_tab'},\n    ]\n\n    result = await Browser._get_valid_tab_id(targets)\n    assert result == 'valid_tab'\n\n\n# Tests for enable_runtime_events and disable_runtime_events\n@pytest.mark.asyncio\nasync def test_enable_runtime_events(mock_browser):\n    \"\"\"Test enable_runtime_events.\"\"\"\n    await mock_browser.enable_runtime_events()\n\n    mock_browser._connection_handler.execute_command.assert_called_with(RuntimeCommands.enable())\n\n\n@pytest.mark.asyncio\nasync def test_disable_runtime_events(mock_browser):\n    \"\"\"Test disable_runtime_events.\"\"\"\n    await mock_browser.disable_runtime_events()\n\n    mock_browser._connection_handler.execute_command.assert_called_with(RuntimeCommands.disable())\n\n\n@pytest.mark.asyncio\nasync def test_get_tab_by_target(mock_browser):\n    \"\"\"Test get_tab_by_target creates Tab with correct target info.\"\"\"\n    target_info = {\n        'targetId': 'test_target_123',\n        'type': 'page',\n        'url': 'https://example.com',\n    }\n    \n    tab = await mock_browser.get_tab_by_target(target_info)\n    \n    assert isinstance(tab, Tab)\n    assert tab._target_id == 'test_target_123'\n\n\n# Tests for continue_request, fail_request and fulfill_request\n@pytest.mark.asyncio\nasync def test_continue_request(mock_browser):\n    \"\"\"Test continue_request with minimal parameters.\"\"\"\n    request_id = 'test_request_123'\n\n    await mock_browser.continue_request(request_id)\n\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        FetchCommands.continue_request(\n            request_id=request_id,\n            url=None,\n            method=None,\n            post_data=None,\n            headers=None,\n            intercept_response=None,\n        ),\n        timeout=60,\n    )\n\n\n@pytest.mark.asyncio\nasync def test_continue_request_with_all_params(mock_browser):\n    \"\"\"Test continue_request with all parameters.\"\"\"\n    request_id = 'test_request_123'\n    url = 'https://modified-example.com'\n    method = RequestMethod.POST\n    post_data = 'modified_data=test'\n    headers = [{'name': 'Authorization', 'value': 'Bearer token123'}]\n    intercept_response = True\n\n    await mock_browser.continue_request(\n        request_id=request_id,\n        url=url,\n        method=method,\n        post_data=post_data,\n        headers=headers,\n        intercept_response=intercept_response,\n    )\n\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        FetchCommands.continue_request(\n            request_id=request_id,\n            url=url,\n            method=method,\n            post_data=post_data,\n            headers=headers,\n            intercept_response=intercept_response,\n        ),\n        timeout=60,\n    )\n\n\n@pytest.mark.asyncio\nasync def test_fail_request(mock_browser):\n    \"\"\"Test fail_request.\"\"\"\n    request_id = 'test_request_123'\n    error_reason = ErrorReason.FAILED\n    await mock_browser.fail_request(request_id, error_reason)\n\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        FetchCommands.fail_request(request_id, error_reason), timeout=60\n    )\n\n\n@pytest.mark.asyncio\nasync def test_fulfill_request(mock_browser):\n    \"\"\"Test fulfill_request with minimal parameters.\"\"\"\n    request_id = 'test_request_123'\n    response_code = 200\n\n    await mock_browser.fulfill_request(request_id, response_code)\n\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        FetchCommands.fulfill_request(\n            request_id=request_id,\n            response_code=response_code,\n            response_headers=None,\n            body=None,\n            response_phrase=None,\n        ),\n        timeout=60,\n    )\n\n\n@pytest.mark.asyncio\nasync def test_fulfill_request_with_all_params(mock_browser):\n    \"\"\"Test fulfill_request with all parameters.\"\"\"\n    request_id = 'test_request_123'\n    response_code = 200\n    response_headers = [{'name': 'Content-Type', 'value': 'application/json'}]\n    json_response = '{\"status\": \"success\", \"data\": \"test\"}'\n    body = base64.b64encode(json_response.encode('utf-8')).decode('utf-8')\n    response_phrase = 'OK'\n\n    await mock_browser.fulfill_request(\n        request_id=request_id,\n        response_code=response_code,\n        response_headers=response_headers,\n        body=body,\n        response_phrase=response_phrase,\n    )\n\n    mock_browser._connection_handler.execute_command.assert_called_with(\n        FetchCommands.fulfill_request(\n            request_id=request_id,\n            response_code=response_code,\n            response_headers=response_headers,\n            body=body,\n            response_phrase=response_phrase,\n        ),\n        timeout=60,\n    )\n\n\n# Additional test for 'on' with async callback\n@pytest.mark.asyncio\nasync def test_event_registration_with_async_callback(mock_browser):\n    \"\"\"Test async callback registration.\"\"\"\n    mock_browser._connection_handler.register_callback.return_value = 456\n\n    async def async_test_callback(event):\n        \"\"\"Test async callback.\"\"\"\n        return f\"Processed event: {event}\"\n\n    callback_id = await mock_browser.on('test_async_event', async_test_callback, temporary=False)\n    assert callback_id == 456\n\n    mock_browser._connection_handler.register_callback.assert_called_with(\n        'test_async_event', ANY, False\n    )\n\n    # Verify that callback was registered correctly\n    call_args = mock_browser._connection_handler.register_callback.call_args\n    registered_callback = call_args[0][1]  # Second argument (callback)\n\n    # The registered callback should be a function\n    assert callable(registered_callback)\n\n\n@pytest.mark.asyncio\nasync def test_event_registration_sync_callback(mock_browser):\n    \"\"\"Test sync callback registration.\"\"\"\n    mock_browser._connection_handler.register_callback.return_value = 789\n\n    def sync_test_callback(event):\n        \"\"\"Test sync callback.\"\"\"\n        return f\"Processed sync event: {event}\"\n\n    callback_id = await mock_browser.on('test_sync_event', sync_test_callback, temporary=True)\n    assert callback_id == 789\n\n    mock_browser._connection_handler.register_callback.assert_called_with(\n        'test_sync_event', ANY, True\n    )\n\n\n# Tests for get_opened_tabs method\n@pytest.mark.asyncio\nasync def test_get_opened_tabs_success(mock_browser):\n    \"\"\"Test get_opened_tabs with multiple valid tabs.\"\"\"\n    # Mock get_targets to return various target types\n    mock_targets = [\n        {'targetId': 'tab3', 'type': 'page', 'url': 'https://example.com', 'title': 'Example Site'},\n        {\n            'targetId': 'ext1',\n            'type': 'page',\n            'url': 'chrome-extension://abc123/popup.html',\n            'title': 'Extension Popup',\n        },\n        {'targetId': 'tab2', 'type': 'page', 'url': 'https://google.com', 'title': 'Google'},\n        {\n            'targetId': 'bg1',\n            'type': 'background_page',\n            'url': 'chrome://background',\n            'title': 'Background Page',\n        },\n        {'targetId': 'tab1', 'type': 'page', 'url': 'chrome://newtab/', 'title': 'New Tab'},\n    ]\n\n    mock_browser.get_targets = AsyncMock(return_value=mock_targets)\n\n    tabs = await mock_browser.get_opened_tabs()\n\n    # Should return 3 tabs (excluding extension and background_page)\n    assert len(tabs) == 3\n\n    # Verify all returned objects are Tab instances\n    for tab in tabs:\n        assert isinstance(tab, Tab)\n\n    # Verify target IDs are correct (should be in reversed order)\n    expected_target_ids = ['tab1', 'tab2', 'tab3']  # reversed order\n    actual_target_ids = [tab._target_id for tab in tabs]\n    assert actual_target_ids == expected_target_ids\n\n    # Verify get_targets was called\n    mock_browser.get_targets.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_get_opened_tabs_no_valid_tabs(mock_browser):\n    \"\"\"Test get_opened_tabs when no valid tabs exist.\"\"\"\n    # Mock get_targets to return only non-page targets\n    mock_targets = [\n        {\n            'targetId': 'ext1',\n            'type': 'page',\n            'url': 'chrome-extension://abc123/popup.html',\n            'title': 'Extension Popup',\n        },\n        {\n            'targetId': 'bg1',\n            'type': 'background_page',\n            'url': 'chrome://background',\n            'title': 'Background Page',\n        },\n        {\n            'targetId': 'worker1',\n            'type': 'service_worker',\n            'url': 'https://example.com/sw.js',\n            'title': 'Service Worker',\n        },\n    ]\n\n    mock_browser.get_targets = AsyncMock(return_value=mock_targets)\n\n    tabs = await mock_browser.get_opened_tabs()\n\n    # Should return empty list\n    assert len(tabs) == 0\n    assert tabs == []\n\n    mock_browser.get_targets.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_get_opened_tabs_empty_targets(mock_browser):\n    \"\"\"Test get_opened_tabs when no targets exist.\"\"\"\n    mock_browser.get_targets = AsyncMock(return_value=[])\n\n    tabs = await mock_browser.get_opened_tabs()\n\n    assert len(tabs) == 0\n    assert tabs == []\n\n    mock_browser.get_targets.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_get_opened_tabs_filters_extensions(mock_browser):\n    \"\"\"Test that get_opened_tabs correctly filters out extension pages.\"\"\"\n    mock_targets = [\n        {'targetId': 'tab1', 'type': 'page', 'url': 'https://example.com', 'title': 'Example Site'},\n        {\n            'targetId': 'ext1',\n            'type': 'page',\n            'url': 'chrome-extension://abc123/popup.html',\n            'title': 'Extension Popup',\n        },\n        {\n            'targetId': 'ext2',\n            'type': 'page',\n            'url': 'moz-extension://def456/options.html',\n            'title': 'Extension Options',\n        },\n        {'targetId': 'tab2', 'type': 'page', 'url': 'https://github.com', 'title': 'GitHub'},\n    ]\n\n    mock_browser.get_targets = AsyncMock(return_value=mock_targets)\n\n    tabs = await mock_browser.get_opened_tabs()\n\n    # Should return only 2 tabs (excluding extensions)\n    assert len(tabs) == 2\n\n    # Verify no extension URLs in results\n    for tab in tabs:\n        assert 'extension' not in tab._target_id\n\n    # Verify correct target IDs (reversed order)\n    expected_target_ids = ['tab2', 'tab1']\n    actual_target_ids = [tab._target_id for tab in tabs]\n    assert actual_target_ids == expected_target_ids\n\n\n@pytest.mark.asyncio\nasync def test_get_opened_tabs_filters_non_page_types(mock_browser):\n    \"\"\"Test that get_opened_tabs only returns 'page' type targets.\"\"\"\n    mock_targets = [\n        {'targetId': 'tab1', 'type': 'page', 'url': 'https://example.com', 'title': 'Example Site'},\n        {\n            'targetId': 'worker1',\n            'type': 'service_worker',\n            'url': 'https://example.com/sw.js',\n            'title': 'Service Worker',\n        },\n        {\n            'targetId': 'shared1',\n            'type': 'shared_worker',\n            'url': 'https://example.com/shared.js',\n            'title': 'Shared Worker',\n        },\n        {'targetId': 'browser1', 'type': 'browser', 'url': '', 'title': 'Browser Process'},\n        {'targetId': 'tab2', 'type': 'page', 'url': 'https://google.com', 'title': 'Google'},\n    ]\n\n    mock_browser.get_targets = AsyncMock(return_value=mock_targets)\n\n    tabs = await mock_browser.get_opened_tabs()\n\n    # Should return only 2 tabs (only 'page' type)\n    assert len(tabs) == 2\n\n    # Verify all are Tab instances\n    for tab in tabs:\n        assert isinstance(tab, Tab)\n\n    # Verify correct target IDs (reversed order)\n    expected_target_ids = ['tab2', 'tab1']\n    actual_target_ids = [tab._target_id for tab in tabs]\n    assert actual_target_ids == expected_target_ids\n\n\n@pytest.mark.asyncio\nasync def test_get_opened_tabs_order_is_reversed(mock_browser):\n    \"\"\"Test that get_opened_tabs returns tabs in reversed order (most recent first).\"\"\"\n    mock_targets = [\n        {\n            'targetId': 'oldest_tab',\n            'type': 'page',\n            'url': 'https://first.com',\n            'title': 'First Tab',\n        },\n        {\n            'targetId': 'middle_tab',\n            'type': 'page',\n            'url': 'https://second.com',\n            'title': 'Second Tab',\n        },\n        {\n            'targetId': 'newest_tab',\n            'type': 'page',\n            'url': 'https://third.com',\n            'title': 'Third Tab',\n        },\n    ]\n\n    mock_browser.get_targets = AsyncMock(return_value=mock_targets)\n\n    tabs = await mock_browser.get_opened_tabs()\n\n    # Should return in reversed order (newest first)\n    expected_order = ['newest_tab', 'middle_tab', 'oldest_tab']\n    actual_order = [tab._target_id for tab in tabs]\n\n    assert actual_order == expected_order\n\n\n@pytest.mark.asyncio\nasync def test_get_opened_tabs_with_mixed_valid_invalid_targets(mock_browser):\n    \"\"\"Test get_opened_tabs with a mix of valid and invalid targets.\"\"\"\n    mock_targets = [\n        {\n            'targetId': 'valid_tab1',\n            'type': 'page',\n            'url': 'https://example.com',\n            'title': 'Valid Tab 1',\n        },\n        {\n            'targetId': 'extension_page',\n            'type': 'page',\n            'url': 'chrome-extension://abc123/popup.html',\n            'title': 'Extension Page',\n        },\n        {\n            'targetId': 'service_worker',\n            'type': 'service_worker',\n            'url': 'https://example.com/sw.js',\n            'title': 'Service Worker',\n        },\n        {\n            'targetId': 'valid_tab2',\n            'type': 'page',\n            'url': 'https://github.com',\n            'title': 'Valid Tab 2',\n        },\n        {\n            'targetId': 'background_page',\n            'type': 'background_page',\n            'url': 'chrome://background',\n            'title': 'Background',\n        },\n        {'targetId': 'valid_tab3', 'type': 'page', 'url': 'chrome://newtab/', 'title': 'New Tab'},\n    ]\n\n    mock_browser.get_targets = AsyncMock(return_value=mock_targets)\n\n    tabs = await mock_browser.get_opened_tabs()\n\n    # Should return only 3 valid tabs\n    assert len(tabs) == 3\n\n    # Verify correct filtering and order\n    expected_target_ids = ['valid_tab3', 'valid_tab2', 'valid_tab1']\n    actual_target_ids = [tab._target_id for tab in tabs]\n    assert actual_target_ids == expected_target_ids\n\n    # Verify all are Tab instances\n    for tab in tabs:\n        assert isinstance(tab, Tab)\n\n\n@pytest.mark.asyncio\nasync def test_get_opened_tabs_integration_with_new_tab(mock_browser):\n    \"\"\"Test get_opened_tabs integration with new_tab method.\"\"\"\n    # Mock initial targets (empty)\n    mock_browser.get_targets = AsyncMock(return_value=[])\n\n    # Initially no tabs\n    tabs = await mock_browser.get_opened_tabs()\n    assert len(tabs) == 0\n\n    # Mock new_tab creation\n    mock_browser._connection_handler.execute_command.return_value = {\n        'result': {'targetId': 'new_tab_1'}\n    }\n\n    # Create a new tab\n    new_tab = await mock_browser.new_tab()\n    assert new_tab._target_id == 'new_tab_1'\n\n    # Mock updated targets after tab creation\n    mock_browser.get_targets = AsyncMock(\n        return_value=[\n            {\n                'targetId': 'new_tab_1',\n                'type': 'page',\n                'url': 'https://example.com',\n                'title': 'Example',\n            }\n        ]\n    )\n\n    # Now get_opened_tabs should return the new tab\n    tabs = await mock_browser.get_opened_tabs()\n    assert len(tabs) == 1\n    assert tabs[0]._target_id == 'new_tab_1'\n\n    # Without singleton, instance identity can differ but ids should match\n    assert tabs[0]._target_id == new_tab._target_id\n\n\n@pytest.mark.asyncio\nasync def test_headless_parameter_deprecation_warning(mock_browser):\n    mock_browser._connection_handler.ping.return_value = True\n    mock_browser._get_valid_tab_id = AsyncMock(return_value='page1')\n    \n    with pytest.warns(\n        DeprecationWarning,\n        match=\"The 'headless' parameter is deprecated and will be removed in a future version\"\n    ):\n        await mock_browser.start(headless=True)\n    \n    assert mock_browser.options.headless is True\n    assert '--headless' in mock_browser.options.arguments\n\n\n# --- User-Agent Override Tests ---\n\n\n@pytest.mark.asyncio\nasync def test_get_user_agent_from_options_found(mock_browser):\n    mock_browser.options.add_argument(\n        '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.6099.109'\n    )\n    result = mock_browser._get_user_agent_from_options()\n    assert result == 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.6099.109'\n\n\n@pytest.mark.asyncio\nasync def test_get_user_agent_from_options_not_found(mock_browser):\n    result = mock_browser._get_user_agent_from_options()\n    assert result is None\n\n\n@pytest.mark.asyncio\nasync def test_apply_user_agent_override_no_ua_set(mock_browser):\n    tab = MagicMock(spec=Tab)\n    tab._execute_command = AsyncMock()\n\n    await mock_browser._apply_user_agent_override(tab)\n\n    tab._execute_command.assert_not_called()\n\n\n@pytest.mark.asyncio\nasync def test_apply_user_agent_override_with_ua_set(mock_browser):\n    custom_ua = (\n        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '\n        'AppleWebKit/537.36 (KHTML, like Gecko) '\n        'Chrome/120.0.6099.109 Safari/537.36'\n    )\n    mock_browser.options.add_argument(f'--user-agent={custom_ua}')\n\n    tab = MagicMock(spec=Tab)\n    tab._execute_command = AsyncMock()\n\n    await mock_browser._apply_user_agent_override(tab)\n\n    assert tab._execute_command.call_count == 2\n\n    emulation_call = tab._execute_command.call_args_list[0]\n    command = emulation_call[0][0]\n    assert command['method'] == 'Emulation.setUserAgentOverride'\n    assert command['params']['userAgent'] == custom_ua\n    assert command['params']['platform'] == 'Win32'\n    assert 'userAgentMetadata' in command['params']\n\n    js_call = tab._execute_command.call_args_list[1]\n    js_command = js_call[0][0]\n    assert js_command['method'] == 'Page.addScriptToEvaluateOnNewDocument'\n    assert \"Navigator.prototype, 'vendor'\" in js_command['params']['source']\n    assert \"Navigator.prototype, 'appVersion'\" in js_command['params']['source']\n\n\n@pytest.mark.asyncio\nasync def test_apply_user_agent_override_metadata_consistency(mock_browser):\n    custom_ua = (\n        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\n        'AppleWebKit/537.36 (KHTML, like Gecko) '\n        'Chrome/121.0.6167.85 Safari/537.36'\n    )\n    mock_browser.options.add_argument(f'--user-agent={custom_ua}')\n\n    tab = MagicMock(spec=Tab)\n    tab._execute_command = AsyncMock()\n\n    await mock_browser._apply_user_agent_override(tab)\n\n    emulation_call = tab._execute_command.call_args_list[0]\n    command = emulation_call[0][0]\n    metadata = command['params']['userAgentMetadata']\n    assert metadata['platform'] == 'macOS'\n    assert metadata['mobile'] is False\n    assert command['params']['platform'] == 'MacIntel'\n    brands = metadata['brands']\n    brand_names = [b['brand'] for b in brands]\n    assert 'Chromium' in brand_names\n    assert 'Google Chrome' in brand_names\n\n\n@pytest.mark.asyncio\nasync def test_start_applies_user_agent_override(mock_browser):\n    custom_ua = (\n        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '\n        'Chrome/120.0.6099.109 Safari/537.36'\n    )\n    mock_browser.options.add_argument(f'--user-agent={custom_ua}')\n    mock_browser._connection_handler.ping.return_value = True\n    mock_browser._get_valid_tab_id = AsyncMock(return_value='page1')\n    mock_browser._apply_user_agent_override = AsyncMock()\n\n    tab = await mock_browser.start()\n\n    mock_browser._apply_user_agent_override.assert_called_once_with(tab)\n\n\n@pytest.mark.asyncio\nasync def test_new_tab_applies_user_agent_override(mock_browser):\n    custom_ua = 'Mozilla/5.0 Chrome/120.0.6099.109'\n    mock_browser.options.add_argument(f'--user-agent={custom_ua}')\n    mock_browser._connection_handler.execute_command = AsyncMock(\n        return_value={'result': {'targetId': 'new_tab_1'}}\n    )\n    mock_browser._apply_user_agent_override = AsyncMock()\n\n    tab = await mock_browser.new_tab()\n\n    mock_browser._apply_user_agent_override.assert_called_once_with(tab)\n"
  },
  {
    "path": "tests/test_browser/test_browser_chrome.py",
    "content": "import json\nimport os\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom pydoll.browser.chromium.chrome import Chrome\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.exceptions import InvalidBrowserPath, UnsupportedOS, InvalidConnectionPort\n\n\nclass TestChromeInitialization:\n    \"\"\"Tests for Chrome class initialization.\"\"\"\n\n    def test_chrome_initialization_default_options(self):\n        \"\"\"Test Chrome initialization with default options.\"\"\"\n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            chrome = Chrome()\n            \n            assert isinstance(chrome.options, ChromiumOptions)\n            assert chrome._connection_port in range(9223, 9323)\n\n    def test_chrome_initialization_custom_options(self):\n        \"\"\"Test Chrome initialization with custom options.\"\"\"\n        custom_options = ChromiumOptions()\n        custom_options.add_argument('--disable-web-security')\n        custom_options.binary_location = '/custom/chrome/path'\n        \n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            chrome = Chrome(options=custom_options)\n            \n            assert chrome.options == custom_options\n            assert '--disable-web-security' in chrome.options.arguments\n            assert chrome.options.binary_location == '/custom/chrome/path'\n\n    def test_chrome_initialization_custom_port(self):\n        \"\"\"Test Chrome initialization with custom port.\"\"\"\n        custom_port = 9999\n        \n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            chrome = Chrome(connection_port=custom_port)\n            \n            assert chrome._connection_port == custom_port\n\n    def test_chrome_initialization_both_custom(self):\n        \"\"\"Test Chrome initialization with both custom options and port.\"\"\"\n        custom_options = ChromiumOptions()\n        custom_options.add_argument('--headless')\n        custom_port = 8888\n        \n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            chrome = Chrome(options=custom_options, connection_port=custom_port)\n            \n            assert chrome.options == custom_options\n            assert chrome._connection_port == custom_port\n            assert '--headless' in chrome.options.arguments\n\n\nclass TestChromeDefaultBinaryLocation:\n    \"\"\"Tests for Chrome default binary location detection.\"\"\"\n\n    @pytest.mark.parametrize(\n        'os_name, expected_paths',\n        [\n            (\n                'Windows',\n                [\n                    r'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',\n                    r'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',\n                ]\n            ),\n            (\n                'Linux',\n                ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable']\n            ),\n            (\n                'Darwin',\n                ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome']\n            ),\n        ],\n    )\n    @patch('pydoll.browser.chromium.chrome.validate_browser_paths')\n    @patch('platform.system')\n    def test_get_default_binary_location_success(\n        self, mock_platform_system, mock_validate_browser_paths, os_name, expected_paths\n    ):\n        \"\"\"Test successful default binary detection for different operating systems.\"\"\"\n        mock_platform_system.return_value = os_name\n        expected_path = expected_paths[0]  # First path in the list\n        mock_validate_browser_paths.return_value = expected_path\n        \n        result = Chrome._get_default_binary_location()\n        \n        mock_platform_system.assert_called_once()\n        mock_validate_browser_paths.assert_called_once_with(expected_paths)\n        assert result == expected_path\n\n    @patch('platform.system')\n    def test_get_default_binary_location_unsupported_os(self, mock_platform_system):\n        \"\"\"Test exception for unsupported operating system.\"\"\"\n        mock_platform_system.return_value = 'FreeBSD'\n        \n        with pytest.raises(UnsupportedOS, match='Unsupported OS: FreeBSD'):\n            Chrome._get_default_binary_location()\n\n    @patch('platform.system')\n    def test_get_default_binary_location_unknown_os(self, mock_platform_system):\n        \"\"\"Test exception for unknown operating system.\"\"\"\n        mock_platform_system.return_value = 'UnknownOS'\n        \n        with pytest.raises(UnsupportedOS, match='Unsupported OS: UnknownOS'):\n            Chrome._get_default_binary_location()\n\n    @patch('pydoll.browser.chromium.chrome.validate_browser_paths')\n    @patch('platform.system')\n    def test_get_default_binary_location_validation_error(\n        self, mock_platform_system, mock_validate_browser_paths\n    ):\n        \"\"\"Test when path validation fails.\"\"\"\n        mock_platform_system.return_value = 'Linux'\n        mock_validate_browser_paths.side_effect = InvalidBrowserPath('Chrome executable not found')\n        \n        with pytest.raises(InvalidBrowserPath, match='Chrome executable not found'):\n            Chrome._get_default_binary_location()\n\n    @patch('pydoll.browser.chromium.chrome.validate_browser_paths')\n    @patch('platform.system')\n    def test_get_default_binary_location_windows_fallback(\n        self, mock_platform_system, mock_validate_browser_paths\n    ):\n        \"\"\"Test fallback for different paths on Windows.\"\"\"\n        mock_platform_system.return_value = 'Windows'\n        expected_path = r'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'\n        mock_validate_browser_paths.return_value = expected_path\n        \n        result = Chrome._get_default_binary_location()\n        \n        expected_paths = [\n            r'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',\n            r'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',\n        ]\n        mock_validate_browser_paths.assert_called_once_with(expected_paths)\n        assert result == expected_path\n\n\nclass TestChromeOptionsManager:\n    \"\"\"Tests for ChromiumOptionsManager integration.\"\"\"\n\n    def test_options_manager_creation(self):\n        \"\"\"Test options manager creation.\"\"\"\n        custom_options = ChromiumOptions()\n        custom_options.add_argument('--no-sandbox')\n        \n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            chrome = Chrome(options=custom_options)\n            \n            # Verify that options were configured correctly\n            assert chrome.options == custom_options\n            assert '--no-sandbox' in chrome.options.arguments\n\n    def test_options_manager_with_none_options(self):\n        \"\"\"Test options manager creation with None options.\"\"\"\n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            chrome = Chrome(options=None)\n            \n            # Verify that default options were created\n            assert isinstance(chrome.options, ChromiumOptions)\n\n\nclass TestChromeInheritance:\n    \"\"\"Tests to verify correct inheritance from Browser class.\"\"\"\n\n    def test_chrome_inherits_from_browser(self):\n        \"\"\"Test if Chrome correctly inherits from Browser.\"\"\"\n        from pydoll.browser.chromium.base import Browser\n        \n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            chrome = Chrome()\n            \n            assert isinstance(chrome, Browser)\n            assert hasattr(chrome, 'start')\n            assert hasattr(chrome, 'stop')\n            assert hasattr(chrome, 'new_tab')\n\n    def test_chrome_overrides_get_default_binary_location(self):\n        \"\"\"Test if Chrome overrides the _get_default_binary_location method.\"\"\"\n        # Verify that the method is static and exists\n        assert hasattr(Chrome, '_get_default_binary_location')\n        assert callable(Chrome._get_default_binary_location)\n        \n        # Verify that it's different from the base implementation\n        from pydoll.browser.chromium.base import Browser\n        assert Chrome._get_default_binary_location != Browser._get_default_binary_location\n\n\nclass TestChromeEdgeCases:\n    \"\"\"Tests for edge cases and special situations.\"\"\"\n\n    def test_chrome_with_empty_options(self):\n        \"\"\"Test Chrome with empty options.\"\"\"\n        empty_options = ChromiumOptions()\n        \n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            chrome = Chrome(options=empty_options)\n            \n            assert chrome.options == empty_options\n            assert len(chrome.options.arguments) >= 0  # May have default arguments\n\n    def test_chrome_with_zero_port(self):\n        \"\"\"Test Chrome with zero port (should generate random port since 0 is falsy).\"\"\"\n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            chrome = Chrome(connection_port=0)\n            \n            # Port 0 is falsy, so should generate a random port\n            assert chrome._connection_port in range(9223, 9323)\n\n    def test_chrome_with_negative_port(self):\n        \"\"\"Test Chrome with negative port (should raise InvalidConnectionPort).\"\"\"\n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            with pytest.raises(InvalidConnectionPort):\n                Chrome(connection_port=-1)\n\n\nclass TestChromeIntegration:\n    \"\"\"Integration tests to verify components working together.\"\"\"\n\n    def test_chrome_full_initialization_flow(self):\n        \"\"\"Test complete Chrome initialization flow.\"\"\"\n        custom_options = ChromiumOptions()\n        custom_options.add_argument('--disable-gpu')\n        custom_options.add_argument('--no-sandbox')\n        custom_options.browser_preferences = {\n        \"download\": {\"directory_upgrade\": True},\n        }\n        custom_options.set_default_download_directory('/tmp/all')\n        custom_options.block_notifications = True\n        custom_options.binary_location = '/custom/chrome'\n        custom_port = 9876\n        \n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ) as mock_process_manager, patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ) as mock_temp_manager, patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ) as mock_connection, patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ) as mock_proxy_manager:\n            \n            chrome = Chrome(options=custom_options, connection_port=custom_port)\n            chrome._setup_user_dir()\n            with open(\n                os.path.join(chrome._temp_directory_manager._temp_dirs[0].name, 'Default', 'Preferences'), 'r'\n            ) as json_file:\n                preferences = json.loads(json_file.read())\n            assert preferences == custom_options.browser_preferences\n\n            # Verify correct initialization\n            assert chrome.options == custom_options\n            assert chrome._connection_port == custom_port\n            assert '--disable-gpu' in chrome.options.arguments\n            assert '--no-sandbox' in chrome.options.arguments\n            assert chrome.options.binary_location == '/custom/chrome'\n            \n            # Verify that managers were created\n            assert chrome._browser_process_manager is not None\n            assert chrome._temp_directory_manager is not None\n            assert chrome._connection_handler is not None\n            assert chrome._proxy_manager is not None\n\n    def test_chrome_options_initialization_flow(self):\n        \"\"\"Test Chrome options initialization flow.\"\"\"\n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            # Test with None options (should create default options)\n            chrome = Chrome(options=None)\n            assert isinstance(chrome.options, ChromiumOptions)\n            \n            # Test with custom options\n            custom_options = ChromiumOptions()\n            custom_options.add_argument('--test-arg')\n            chrome2 = Chrome(options=custom_options)\n            assert chrome2.options == custom_options\n            assert '--test-arg' in chrome2.options.arguments\n\n    @pytest.mark.asyncio\n    async def test_chrome_user_data_dir_and_preferences(self, tmp_path):\n        \"\"\"Test Chrome with user data directory and preferences.\"\"\"\n        user_data_dir = tmp_path / 'chrome_profile'\n        user_data_dir.mkdir()\n        \n        prefs_dir = user_data_dir / 'Default'\n        prefs_dir.mkdir()\n        prefs_file = prefs_dir / 'Preferences'\n        \n        initial_prefs = {\n            'profile': {\n                'exit_type': 'Normal',\n                'exited_cleanly': True\n            },\n            'test_pref': 'initial_value'\n        }\n        \n        prefs_file.write_text(json.dumps(initial_prefs), encoding='utf-8')\n        \n        custom_options = ChromiumOptions()\n        custom_options.add_argument(f'--user-data-dir={user_data_dir}')\n        custom_options.browser_preferences = {\n            'test_pref': 'new_value',\n            'new_pref': 'some_value'\n        }\n        custom_options.prompt_for_download = False # ok\n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            async with Chrome(options=custom_options) as chrome:\n                chrome._setup_user_dir()\n                assert f'--user-data-dir={user_data_dir}' in chrome.options.arguments\n\n                with open(prefs_file, 'r', encoding='utf-8') as f:\n                    updated_prefs = json.load(f)\n                assert updated_prefs['test_pref'] == 'new_value'\n                assert updated_prefs['new_pref'] == 'some_value'\n                \n                assert updated_prefs['profile']['exit_type'] == 'Normal'\n                assert updated_prefs['profile']['exited_cleanly'] is True\n                backup_file = user_data_dir / 'Default' / 'Preferences.backup'\n                assert backup_file.exists()\n                with open(backup_file, 'r', encoding='utf-8') as f:\n                    backup_prefs = json.load(f)\n                assert backup_prefs == initial_prefs\n            with open(prefs_file, 'r', encoding='utf-8') as f:\n                final_prefs = json.load(f)\n            assert final_prefs == initial_prefs\n\n    @pytest.mark.asyncio\n    async def test_chrome_user_data_dir_with_invalid_json_preferences(self, tmp_path):\n        \"\"\"Test Chrome with user data directory containing invalid JSON preferences.\"\"\"\n        user_data_dir = tmp_path / 'chrome_profile'\n        user_data_dir.mkdir()\n        \n        prefs_dir = user_data_dir / 'Default'\n        prefs_dir.mkdir()\n        prefs_file = prefs_dir / 'Preferences'\n        \n        # Write invalid JSON to the Preferences file\n        invalid_json = '{ \"profile\": { \"exit_type\": \"Normal\", \"exited_cleanly\": true, } }' # trailing comma makes it invalid\n        prefs_file.write_text(invalid_json, encoding='utf-8')\n        \n        custom_options = ChromiumOptions()\n        custom_options.add_argument(f'--user-data-dir={user_data_dir}')\n        custom_options.browser_preferences = {\n            'test_pref': 'new_value',\n            'new_pref': 'some_value'\n        }\n        custom_options.prompt_for_download = False\n        \n        with patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            async with Chrome(options=custom_options) as chrome:\n                chrome._setup_user_dir()\n                assert f'--user-data-dir={user_data_dir}' in chrome.options.arguments\n\n                # The invalid JSON should be handled gracefully by suppress(json.JSONDecodeError)\n                # and the preferences should be written with only the new preferences\n                with open(prefs_file, 'r', encoding='utf-8') as f:\n                    updated_prefs = json.load(f)\n                assert updated_prefs['test_pref'] == 'new_value'\n                assert updated_prefs['new_pref'] == 'some_value'\n                \n                # The original invalid JSON should be backed up\n                backup_file = user_data_dir / 'Default' / 'Preferences.backup'\n                assert backup_file.exists()\n                with open(backup_file, 'r', encoding='utf-8') as f:\n                    backup_content = f.read()\n                assert backup_content == invalid_json\n"
  },
  {
    "path": "tests/test_browser/test_browser_edge.py",
    "content": "import platform\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom pydoll.browser.chromium.edge import Edge\nfrom pydoll.browser.managers import ChromiumOptionsManager\nfrom pydoll.browser.options import ChromiumOptions\nfrom pydoll.exceptions import UnsupportedOS, InvalidBrowserPath, InvalidConnectionPort\n\n\nclass TestEdgeInitialization:\n    \"\"\"Tests for Edge class initialization.\"\"\"\n\n    def test_edge_initialization_default_options(self):\n        \"\"\"Test Edge initialization with default options.\"\"\"\n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge()\n            \n            assert isinstance(edge.options, ChromiumOptions)\n            assert edge._connection_port in range(9223, 9323)\n\n    def test_edge_initialization_custom_options(self):\n        \"\"\"Test Edge initialization with custom options.\"\"\"\n        custom_options = ChromiumOptions()\n        custom_options.add_argument('--disable-web-security')\n        custom_options.binary_location = '/custom/edge/path'\n        \n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge(options=custom_options)\n            \n            assert edge.options == custom_options\n            assert '--disable-web-security' in edge.options.arguments\n            assert edge.options.binary_location == '/custom/edge/path'\n\n    def test_edge_initialization_custom_port(self):\n        \"\"\"Test Edge initialization with custom port.\"\"\"\n        custom_port = 9999\n        \n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge(connection_port=custom_port)\n            \n            assert edge._connection_port == custom_port\n\n    def test_edge_initialization_both_custom(self):\n        \"\"\"Test Edge initialization with both custom options and port.\"\"\"\n        custom_options = ChromiumOptions()\n        custom_options.add_argument('--headless')\n        custom_port = 8888\n        \n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge(options=custom_options, connection_port=custom_port)\n            \n            assert edge.options == custom_options\n            assert edge._connection_port == custom_port\n            assert '--headless' in edge.options.arguments\n\n\nclass TestEdgeDefaultBinaryLocation:\n    \"\"\"Tests for Edge default binary location detection.\"\"\"\n\n    @pytest.mark.parametrize(\n        'os_name, expected_paths',\n        [\n            (\n                'Windows',\n                [\n                    r'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',\n                    r'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',\n                ]\n            ),\n            (\n                'Linux',\n                ['/usr/bin/microsoft-edge']\n            ),\n            (\n                'Darwin',\n                ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge']\n            ),\n        ],\n    )\n    @patch('pydoll.browser.chromium.edge.validate_browser_paths')\n    @patch('platform.system')\n    def test_get_default_binary_location_success(\n        self, mock_platform_system, mock_validate_browser_paths, os_name, expected_paths\n    ):\n        \"\"\"Test successful default binary detection for different operating systems.\"\"\"\n        mock_platform_system.return_value = os_name\n        expected_path = expected_paths[0]  # First path in the list\n        mock_validate_browser_paths.return_value = expected_path\n        \n        result = Edge._get_default_binary_location()\n        \n        mock_platform_system.assert_called_once()\n        mock_validate_browser_paths.assert_called_once_with(expected_paths)\n        assert result == expected_path\n\n    @patch('platform.system')\n    def test_get_default_binary_location_unsupported_os(self, mock_platform_system):\n        \"\"\"Test exception for unsupported operating system.\"\"\"\n        mock_platform_system.return_value = 'FreeBSD'\n        \n        with pytest.raises(UnsupportedOS):\n            Edge._get_default_binary_location()\n\n    @patch('platform.system')\n    def test_get_default_binary_location_unknown_os(self, mock_platform_system):\n        \"\"\"Test exception for unknown operating system.\"\"\"\n        mock_platform_system.return_value = 'UnknownOS'\n        \n        with pytest.raises(UnsupportedOS):\n            Edge._get_default_binary_location()\n\n    @patch('pydoll.browser.chromium.edge.validate_browser_paths')\n    @patch('platform.system')\n    def test_get_default_binary_location_validation_error(\n        self, mock_platform_system, mock_validate_browser_paths\n    ):\n        \"\"\"Test when path validation fails.\"\"\"\n        mock_platform_system.return_value = 'Linux'\n        mock_validate_browser_paths.side_effect = InvalidBrowserPath('Edge executable not found')\n        \n        with pytest.raises(InvalidBrowserPath, match='Edge executable not found'):\n            Edge._get_default_binary_location()\n\n    @patch('pydoll.browser.chromium.edge.validate_browser_paths')\n    @patch('platform.system')\n    def test_get_default_binary_location_windows_fallback(\n        self, mock_platform_system, mock_validate_browser_paths\n    ):\n        \"\"\"Test fallback for different paths on Windows.\"\"\"\n        mock_platform_system.return_value = 'Windows'\n        expected_path = r'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'\n        mock_validate_browser_paths.return_value = expected_path\n        \n        result = Edge._get_default_binary_location()\n        \n        expected_paths = [\n            r'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',\n            r'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',\n        ]\n        mock_validate_browser_paths.assert_called_once_with(expected_paths)\n        assert result == expected_path\n\n    @patch('pydoll.browser.chromium.edge.validate_browser_paths')\n    @patch('platform.system')\n    def test_get_default_binary_location_linux_path(\n        self, mock_platform_system, mock_validate_browser_paths\n    ):\n        \"\"\"Test Linux-specific Edge path.\"\"\"\n        mock_platform_system.return_value = 'Linux'\n        expected_path = '/usr/bin/microsoft-edge'\n        mock_validate_browser_paths.return_value = expected_path\n        \n        result = Edge._get_default_binary_location()\n        \n        mock_validate_browser_paths.assert_called_once_with(['/usr/bin/microsoft-edge'])\n        assert result == expected_path\n\n    @patch('pydoll.browser.chromium.edge.validate_browser_paths')\n    @patch('platform.system')\n    def test_get_default_binary_location_macos_path(\n        self, mock_platform_system, mock_validate_browser_paths\n    ):\n        \"\"\"Test macOS-specific Edge path.\"\"\"\n        mock_platform_system.return_value = 'Darwin'\n        expected_path = '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'\n        mock_validate_browser_paths.return_value = expected_path\n        \n        result = Edge._get_default_binary_location()\n        \n        mock_validate_browser_paths.assert_called_once_with([expected_path])\n        assert result == expected_path\n\n\nclass TestEdgeOptionsManager:\n    \"\"\"Tests for ChromiumOptionsManager integration.\"\"\"\n\n    def test_options_manager_creation(self):\n        \"\"\"Test options manager creation.\"\"\"\n        custom_options = ChromiumOptions()\n        custom_options.add_argument('--no-sandbox')\n        \n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge(options=custom_options)\n            \n            # Verify that options were configured correctly\n            assert edge.options == custom_options\n            assert '--no-sandbox' in edge.options.arguments\n\n    def test_options_manager_with_none_options(self):\n        \"\"\"Test options manager creation with None options.\"\"\"\n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge(options=None)\n            \n            # Verify that default options were created\n            assert isinstance(edge.options, ChromiumOptions)\n\n\nclass TestEdgeInheritance:\n    \"\"\"Tests to verify correct inheritance from Browser class.\"\"\"\n\n    def test_edge_inherits_from_browser(self):\n        \"\"\"Test if Edge correctly inherits from Browser.\"\"\"\n        from pydoll.browser.chromium.base import Browser\n        \n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge()\n            \n            assert isinstance(edge, Browser)\n            assert hasattr(edge, 'start')\n            assert hasattr(edge, 'stop')\n            assert hasattr(edge, 'new_tab')\n\n    def test_edge_overrides_get_default_binary_location(self):\n        \"\"\"Test if Edge overrides the _get_default_binary_location method.\"\"\"\n        # Verify that the method is static and exists\n        assert hasattr(Edge, '_get_default_binary_location')\n        assert callable(Edge._get_default_binary_location)\n        \n        # Verify that it's different from the base implementation\n        from pydoll.browser.chromium.base import Browser\n        assert Edge._get_default_binary_location != Browser._get_default_binary_location\n\n    def test_edge_uses_chromium_options_manager(self):\n        \"\"\"Test if Edge uses ChromiumOptionsManager like Chrome.\"\"\"\n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge()\n            \n            # Edge should use the same options type as Chrome since it's Chromium-based\n            assert isinstance(edge.options, ChromiumOptions)\n\n\nclass TestEdgeEdgeCases:\n    \"\"\"Tests for edge cases and special situations.\"\"\"\n\n    def test_edge_with_empty_options(self):\n        \"\"\"Test Edge with empty options.\"\"\"\n        empty_options = ChromiumOptions()\n        \n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge(options=empty_options)\n            \n            assert edge.options == empty_options\n            assert len(edge.options.arguments) >= 0  # May have default arguments\n\n    def test_edge_with_zero_port(self):\n        \"\"\"Test Edge with zero port (should generate random port since 0 is falsy).\"\"\"\n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge(connection_port=0)\n            \n            # Port 0 is falsy, so should generate a random port\n            assert edge._connection_port in range(9223, 9323)\n\n    def test_edge_with_negative_port(self):\n        \"\"\"Test Edge with negative port (should raise InvalidConnectionPort).\"\"\"\n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            with pytest.raises(InvalidConnectionPort):\n                Edge(connection_port=-1)\n\n    def test_edge_with_edge_specific_arguments(self):\n        \"\"\"Test Edge with Edge-specific command line arguments.\"\"\"\n        custom_options = ChromiumOptions()\n        custom_options.add_argument('--enable-features=msEdgeEnhancedSecurity')\n        custom_options.add_argument('--edge-webview-enable-builtin-background-extensions')\n        \n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge(options=custom_options)\n            \n            assert '--enable-features=msEdgeEnhancedSecurity' in edge.options.arguments\n            assert '--edge-webview-enable-builtin-background-extensions' in edge.options.arguments\n\n\nclass TestEdgeIntegration:\n    \"\"\"Integration tests to verify components working together.\"\"\"\n\n    def test_edge_full_initialization_flow(self):\n        \"\"\"Test complete Edge initialization flow.\"\"\"\n        custom_options = ChromiumOptions()\n        custom_options.add_argument('--disable-gpu')\n        custom_options.add_argument('--no-sandbox')\n        custom_options.binary_location = '/custom/edge'\n        custom_port = 9876\n        \n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ) as mock_process_manager, patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ) as mock_temp_manager, patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ) as mock_connection, patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ) as mock_proxy_manager:\n            \n            edge = Edge(options=custom_options, connection_port=custom_port)\n            \n            # Verify correct initialization\n            assert edge.options == custom_options\n            assert edge._connection_port == custom_port\n            assert '--disable-gpu' in edge.options.arguments\n            assert '--no-sandbox' in edge.options.arguments\n            assert edge.options.binary_location == '/custom/edge'\n            \n            # Verify that managers were created\n            assert edge._browser_process_manager is not None\n            assert edge._temp_directory_manager is not None\n            assert edge._connection_handler is not None\n            assert edge._proxy_manager is not None\n\n    def test_edge_options_initialization_flow(self):\n        \"\"\"Test Edge options initialization flow.\"\"\"\n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            # Test with None options (should create default options)\n            edge = Edge(options=None)\n            assert isinstance(edge.options, ChromiumOptions)\n            \n            # Test with custom options\n            custom_options = ChromiumOptions()\n            custom_options.add_argument('--test-arg')\n            edge2 = Edge(options=custom_options)\n            assert edge2.options == custom_options\n            assert '--test-arg' in edge2.options.arguments\n\n    def test_edge_vs_chrome_compatibility(self):\n        \"\"\"Test that Edge and Chrome use compatible interfaces.\"\"\"\n        from pydoll.browser.chromium.chrome import Chrome\n        \n        with patch.multiple(\n            Edge,\n            _get_default_binary_location=MagicMock(return_value='/fake/edge'),\n        ), patch.multiple(\n            Chrome,\n            _get_default_binary_location=MagicMock(return_value='/fake/chrome'),\n        ), patch(\n            'pydoll.browser.managers.browser_process_manager.BrowserProcessManager',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.temp_dir_manager.TempDirectoryManager',\n            autospec=True,\n        ), patch(\n            'pydoll.connection.connection_handler.ConnectionHandler',\n            autospec=True,\n        ), patch(\n            'pydoll.browser.managers.proxy_manager.ProxyManager',\n            autospec=True,\n        ):\n            edge = Edge()\n            chrome = Chrome()\n            \n            # Both should have the same interface\n            assert type(edge.options) == type(chrome.options)\n            assert hasattr(edge, 'start') and hasattr(chrome, 'start')\n            assert hasattr(edge, 'stop') and hasattr(chrome, 'stop')\n            assert hasattr(edge, 'new_tab') and hasattr(chrome, 'new_tab')\n"
  },
  {
    "path": "tests/test_browser/test_browser_options.py",
    "content": "import pytest\n\nfrom pydoll.browser.interfaces import Options as OptionsInterface\nfrom pydoll.browser.options import ChromiumOptions as Options\nfrom pydoll.constants import PageLoadState\nfrom pydoll.exceptions import (\n    ArgumentAlreadyExistsInOptions,\n    ArgumentNotFoundInOptions,\n    WrongPrefsDict,\n)\n\n\ndef test_initial_arguments():\n    options = Options()\n    assert options.arguments == []\n\n\ndef test_initial_binary_location():\n    options = Options()\n    assert not options.binary_location\n\n\ndef test_set_binary_location():\n    options = Options()\n    options.binary_location = '/path/to/browser'\n    assert options.binary_location == '/path/to/browser'\n\n\ndef test_set_start_timeout():\n    options = Options()\n    options.start_timeout = 30\n    assert options.start_timeout == 30\n\n\ndef test_initial_page_load_state():\n    options = Options()\n    assert options.page_load_state == PageLoadState.COMPLETE\n\n\ndef test_set_page_load_state():\n    options = Options()\n    options.page_load_state = PageLoadState.INTERACTIVE\n    assert options.page_load_state == PageLoadState.INTERACTIVE\n\n\ndef test_add_argument():\n    options = Options()\n    options.add_argument('--headless')\n    assert options.arguments == ['--headless']\n\n\ndef test_add_duplicate_argument():\n    options = Options()\n    options.add_argument('--headless')\n    with pytest.raises(ArgumentAlreadyExistsInOptions, match='Argument already exists: --headless'):\n        options.add_argument('--headless')\n\ndef test_remove_argument():\n    options = Options()\n    options.add_argument('--headless')\n    options.remove_argument('--headless')\n    assert options.arguments == []\n\ndef test_remove_argument_not_exists():\n    options = Options()\n    with pytest.raises(ArgumentNotFoundInOptions, match='Argument not found: --headless'):\n        options.remove_argument('--headless')\n\ndef test_add_multiple_arguments():\n    options = Options()\n    options.add_argument('--headless')\n    options.add_argument('--no-sandbox')\n    assert options.arguments == ['--headless', '--no-sandbox']\n\n\ndef test_set_default_download_directory():\n    options = Options()\n    options.set_default_download_directory('/tmp/downloads')\n    assert options.browser_preferences['download']['default_directory'] == '/tmp/downloads'\n\n\ndef test_set_prompt_for_download():\n    options = Options()\n    options.prompt_for_download = False\n    assert options.browser_preferences['download']['prompt_for_download'] is False\n    assert options.prompt_for_download is False\n\n\ndef test_set_block_popups():\n    options = Options()\n    options.block_popups = True\n    assert options.browser_preferences['profile']['default_content_setting_values']['popups'] == 0\n    assert options.block_popups == True\n\n\ndef test_set_password_manager_enabled():\n    options = Options()\n    options.password_manager_enabled = False\n    assert options.browser_preferences['profile']['password_manager_enabled'] is False\n    assert options.password_manager_enabled is False\n\n\ndef test_set_block_notifications():\n    options = Options()\n    options.block_notifications = True\n    assert (\n        options.browser_preferences['profile']['default_content_setting_values']['notifications']\n        == 2\n    )\n    assert options.block_notifications == True\n\n\ndef test_set_allow_automatic_downloads():\n    options = Options()\n    options.allow_automatic_downloads = True\n    assert (\n        options.browser_preferences['profile']['default_content_setting_values'][\n            'automatic_downloads'\n        ]\n        == 1\n    )\n    assert options.allow_automatic_downloads == True\n\n\ndef test_set_open_pdf_externally():\n    options = Options()\n    options.open_pdf_externally = True\n    assert options.browser_preferences['plugins']['always_open_pdf_externally'] is True\n    assert options.open_pdf_externally is True\n\n\ndef test_set_accept_languages():\n    options = Options()\n    options.set_accept_languages('pt-BR,pt,en-US,en')\n    assert options.browser_preferences['intl']['accept_languages'] == 'pt-BR,pt,en-US,en'\n\n\ndef test_set_multiple_prefs():\n    options = Options()\n    options.set_default_download_directory('/tmp/all')\n    options.prompt_for_download = False\n    options.block_popups = True\n    options.password_manager_enabled = False\n    options.block_notifications = True\n    options.allow_automatic_downloads = True\n    options.open_pdf_externally = True\n    options.set_accept_languages('pt-BR,pt,en-US,en')\n\n    assert options.browser_preferences['download']['default_directory'] == '/tmp/all'\n    assert options.browser_preferences['download']['prompt_for_download'] is False\n    assert options.browser_preferences['profile']['default_content_setting_values']['popups'] == 0\n    assert options.browser_preferences['profile']['password_manager_enabled'] is False\n    assert (\n        options.browser_preferences['profile']['default_content_setting_values']['notifications']\n        == 2\n    )\n    assert (\n        options.browser_preferences['profile']['default_content_setting_values'][\n            'automatic_downloads'\n        ]\n        == 1\n    )\n    assert options.browser_preferences['plugins']['always_open_pdf_externally'] is True\n    assert options.browser_preferences['intl']['accept_languages'] == 'pt-BR,pt,en-US,en'\n\n\ndef test_dict_prefs():\n    options = Options()\n    options.browser_preferences = {\n        \"download\": {\"directory_upgrade\": True},\n    }\n    assert options.browser_preferences['download']['directory_upgrade'] == True\n\n\ndef test_not_dict_prefs_error():\n    with pytest.raises(ValueError, match='The experimental options value must be a dict.'):\n        options = Options()\n        options.browser_preferences = [\"download\", \"directory_upgrade\"]\n\n\ndef test_wrong_dict_prefs_error():\n    with pytest.raises(WrongPrefsDict):\n        options = Options()\n        options.browser_preferences = {\n            'prefs': {\n                \"download\": {\"directory_upgrade\": True},\n            }\n        }\n\ndef test_set_arguments():\n    options = Options()\n    options.arguments = ['--headless']\n    assert options.arguments == ['--headless']\n\ndef test_get_pref_path():\n    options = Options()\n    options.set_default_download_directory('/tmp/downloads')\n    assert options._get_pref_path(['download', 'default_directory']) == '/tmp/downloads'\n\n\ndef test_get_pref_path_none():\n    options = Options()\n    assert options._get_pref_path(['download', 'default_directory']) is None\n\n\ndef test_options_interface_enforcement():\n    with pytest.raises(TypeError):\n        OptionsInterface()\n\n    class IncompleteOptions(OptionsInterface):\n        pass\n\n    with pytest.raises(TypeError):\n        IncompleteOptions()\n\n    class CompleteOptions(OptionsInterface):\n        @property\n        def arguments(self):\n            return []\n\n        @property\n        def binary_location(self):\n            return ''\n\n        @property\n        def start_timeout(self):\n            return 0\n\n        def add_argument(self, argument):\n            pass\n\n        @property\n        def browser_preferences(self):\n            return {}\n\n        @property\n        def headless(self):\n            return False\n\n        @property\n        def page_load_state(self):\n            return PageLoadState.COMPLETE\n\n        @page_load_state.setter\n        def page_load_state(self, state):\n            pass\n\n    CompleteOptions()\n\ndef test_set_headless():\n    options = Options()\n    options.headless = True\n    assert options.headless is True\n    assert options.arguments == ['--headless']\n\ndef test_set_headless_false():\n    options = Options()\n    options.headless = True\n    assert options.headless is True\n    assert options.arguments == ['--headless']\n    options.headless = False\n    assert options.headless is False\n    assert options.arguments == []\n\ndef test_set_headless_true_twice():\n    options = Options()\n    options.headless = True\n    assert options.headless is True\n    assert options.arguments == ['--headless']\n    options.headless = True\n    assert options.headless is True\n    assert options.arguments == ['--headless']\n\ndef test_set_headless_false_twice():\n    options = Options()\n    options.headless = False\n    assert options.headless is False\n    assert options.arguments == []\n    options.headless = False\n    assert options.headless is False\n    assert options.arguments == []\n\ndef test_set_webrtc_leak_protection():\n    options = Options()\n    options.webrtc_leak_protection = True\n    assert options.webrtc_leak_protection is True\n    assert options.arguments == ['--force-webrtc-ip-handling-policy=disable_non_proxied_udp']\n\ndef test_set_webrtc_leak_protection_false():\n    options = Options()\n    options.webrtc_leak_protection = True\n    assert options.webrtc_leak_protection is True\n    assert options.arguments == ['--force-webrtc-ip-handling-policy=disable_non_proxied_udp']\n    options.webrtc_leak_protection = False\n    assert options.webrtc_leak_protection is False\n    assert options.arguments == []\n\ndef test_set_webrtc_leak_protection_true_twice():\n    options = Options()\n    options.webrtc_leak_protection = True\n    assert options.webrtc_leak_protection is True\n    assert options.arguments == ['--force-webrtc-ip-handling-policy=disable_non_proxied_udp']\n    options.webrtc_leak_protection = True\n    assert options.webrtc_leak_protection is True\n    assert options.arguments == ['--force-webrtc-ip-handling-policy=disable_non_proxied_udp']\n\ndef test_set_webrtc_leak_protection_false_twice():\n    options = Options()\n    options.webrtc_leak_protection = False\n    assert options.webrtc_leak_protection is False\n    assert options.arguments == []\n    options.webrtc_leak_protection = False\n    assert options.webrtc_leak_protection is False\n    assert options.arguments == []\n"
  },
  {
    "path": "tests/test_browser/test_browser_tab.py",
    "content": "import base64\nimport asyncio\nimport pytest\nimport pytest_asyncio\nimport uuid\nfrom unittest.mock import AsyncMock, MagicMock, Mock, patch, ANY\nfrom pathlib import Path\n\nfrom pydoll.elements.web_element import WebElement\nfrom pydoll.protocol.runtime.types import CallArgument, SerializationOptions\nfrom pydoll.browser.options import ChromiumOptions\n\nfrom pydoll.protocol.network.types import ResourceType, RequestMethod\nfrom pydoll.protocol.fetch.types import RequestStage\nfrom pydoll.constants import By, PageLoadState\nfrom pydoll.browser.tab import Tab\nfrom pydoll.utils.bundle import (\n    build_asset_filename,\n    collect_frame_resources,\n    rewrite_css_urls,\n)\nfrom pydoll.protocol.page.events import PageEvent\nfrom pydoll.protocol.browser.types import DownloadBehavior\nfrom pydoll.exceptions import DownloadTimeout, InvalidTabInitialization\nfrom pydoll.exceptions import (\n    NoDialogPresent,\n    PageLoadTimeout,\n    IFrameNotFound,\n    InvalidIFrame,\n    NotAnIFrame,\n    InvalidFileExtension,\n    WaitElementTimeout,\n    NetworkEventsNotEnabled,\n    TopLevelTargetRequired,\n    InvalidScriptWithElement,\n)\n\n@pytest_asyncio.fixture\nasync def mock_connection_handler():\n    \"\"\"Mock connection handler for Tab tests.\"\"\"\n    with patch('pydoll.connection.ConnectionHandler', autospec=True) as mock:\n        handler = mock.return_value\n        handler.execute_command = AsyncMock()\n        handler.register_callback = AsyncMock()\n        handler.remove_callback = AsyncMock()\n        handler.clear_callbacks = AsyncMock()\n        handler.network_logs = []\n        handler.dialog = None\n        yield handler\n\n\n@pytest_asyncio.fixture\nasync def mock_browser():\n    \"\"\"Mock browser instance.\"\"\"\n    browser = MagicMock()\n    browser.close_tab = AsyncMock()\n    browser.options = ChromiumOptions()\n    return browser\n\n\n@pytest_asyncio.fixture\nasync def tab(mock_browser, mock_connection_handler):\n    \"\"\"Tab fixture with mocked dependencies.\"\"\"\n    unique_target_id = f'test-target-{uuid.uuid4().hex[:8]}'\n    with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_connection_handler):\n        created = Tab(\n            browser=mock_browser,\n            connection_port=9222,\n            target_id=unique_target_id,\n            browser_context_id='test-context-id'\n        )\n        return created\n\n\ndef assert_mock_called_at_least_once(mock_obj, method_name='execute_command'):\n    \"\"\"\n    Helper function to assert that a mock was called at least once.\n    This is more robust than assert_called_once() for singleton tests.\n    \"\"\"\n    mock_method = getattr(mock_obj, method_name)\n    mock_method.assert_called()\n    assert mock_method.call_count >= 1\n\n\n@pytest.fixture(autouse=True)\ndef cleanup_tab_registry():\n    \"\"\"No-op: singleton removed; keep fixture for compatibility.\"\"\"\n    yield\n\n\nclass TestTabInitialization:\n    \"\"\"Test Tab initialization and basic properties.\"\"\"\n\n    def test_tab_initialization(self, tab, mock_browser):\n        \"\"\"Test basic Tab initialization.\"\"\"\n        assert tab._browser == mock_browser\n        assert tab._connection_port == 9222\n        assert tab._target_id.startswith('test-target-')\n        assert tab._browser_context_id == 'test-context-id'\n        assert not tab.page_events_enabled\n        assert not tab.network_events_enabled\n        assert not tab.fetch_events_enabled\n        assert not tab.dom_events_enabled\n        assert not tab.runtime_events_enabled\n        assert not tab.intercept_file_chooser_dialog_enabled\n\n    def test_tab_init_raises_when_no_identifiers(self, mock_browser):\n        with pytest.raises(InvalidTabInitialization):\n            Tab(browser=mock_browser)\n\n    def test_tab_properties(self, tab):\n        \"\"\"Test Tab boolean properties.\"\"\"\n        # Initially all should be False\n        assert tab.page_events_enabled is False\n        assert tab.network_events_enabled is False\n        assert tab.fetch_events_enabled is False\n        assert tab.dom_events_enabled is False\n        assert tab.runtime_events_enabled is False\n        assert tab.intercept_file_chooser_dialog_enabled is False\n\n        # Test setting properties\n        tab._page_events_enabled = True\n        tab._network_events_enabled = True\n        tab._fetch_events_enabled = True\n        tab._dom_events_enabled = True\n        tab._runtime_events_enabled = True\n        tab._intercept_file_chooser_dialog_enabled = True\n\n        assert tab.page_events_enabled is True\n        assert tab.network_events_enabled is True\n        assert tab.fetch_events_enabled is True\n        assert tab.dom_events_enabled is True\n        assert tab.runtime_events_enabled is True\n        assert tab.intercept_file_chooser_dialog_enabled is True\n\n\nclass TestTabProperties:\n    \"\"\"Test Tab async properties.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_current_url(self, tab):\n        \"\"\"Test current_url property.\"\"\"\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': 'https://example.com'}}\n        }\n\n        url = await tab.current_url\n        assert url == 'https://example.com'\n        # Reset mock before assertion to avoid singleton interference\n        tab._connection_handler.execute_command.assert_called()\n        assert tab._connection_handler.execute_command.call_count >= 1\n\n    @pytest.mark.asyncio\n    async def test_page_source(self, tab):\n        \"\"\"Test page_source property.\"\"\"\n        expected_html = '<html><body>Test Content</body></html>'\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': expected_html}}\n        }\n\n        source = await tab.page_source\n        assert source == expected_html\n        tab._connection_handler.execute_command.assert_called()\n        assert tab._connection_handler.execute_command.call_count >= 1\n\n    @pytest.mark.asyncio\n    async def test_title(self, tab):\n        \"\"\"Test title property.\"\"\"\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': 'My Page Title'}}\n        }\n\n        title = await tab.title\n        assert title == 'My Page Title'\n        tab._connection_handler.execute_command.assert_called()\n        assert tab._connection_handler.execute_command.call_count >= 1\n\n    @pytest.mark.asyncio\n    async def test_title_empty(self, tab):\n        \"\"\"Test title property when page has no title.\"\"\"\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'result': {}}\n        }\n\n        title = await tab.title\n        assert title == ''\n\n\nclass TestTabEventManagement:\n    \"\"\"Test Tab event enabling/disabling methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_enable_page_events(self, tab):\n        \"\"\"Test enabling page events.\"\"\"\n        await tab.enable_page_events()\n        assert tab.page_events_enabled is True\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_enable_network_events(self, tab):\n        \"\"\"Test enabling network events.\"\"\"\n        await tab.enable_network_events()\n        assert tab.network_events_enabled is True\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_enable_fetch_events(self, tab):\n        \"\"\"Test enabling fetch events with default parameters.\"\"\"\n        await tab.enable_fetch_events()\n        assert tab.fetch_events_enabled is True\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_enable_fetch_events_with_params(self, tab):\n        \"\"\"Test enabling fetch events with custom parameters.\"\"\"\n        await tab.enable_fetch_events(\n            handle_auth=True,\n            resource_type=ResourceType.DOCUMENT,\n            request_stage=RequestStage.REQUEST\n        )\n        assert tab.fetch_events_enabled is True\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_enable_dom_events(self, tab):\n        \"\"\"Test enabling DOM events.\"\"\"\n        await tab.enable_dom_events()\n        assert tab.dom_events_enabled is True\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_enable_runtime_events(self, tab):\n        \"\"\"Test enabling runtime events.\"\"\"\n        await tab.enable_runtime_events()\n        assert tab.runtime_events_enabled is True\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_enable_intercept_file_chooser_dialog(self, tab):\n        \"\"\"Test enabling file chooser dialog interception.\"\"\"\n        await tab.enable_intercept_file_chooser_dialog()\n        assert tab.intercept_file_chooser_dialog_enabled is True\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_disable_fetch_events(self, tab):\n        \"\"\"Test disabling fetch events.\"\"\"\n        tab._fetch_events_enabled = True\n        await tab.disable_fetch_events()\n        assert tab.fetch_events_enabled is False\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_disable_page_events(self, tab):\n        \"\"\"Test disabling page events.\"\"\"\n        tab._page_events_enabled = True\n        await tab.disable_page_events()\n        assert tab.page_events_enabled is False\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_disable_network_events(self, tab):\n        \"\"\"Test disabling network events.\"\"\"\n        tab._network_events_enabled = True\n        await tab.disable_network_events()\n        assert tab.network_events_enabled is False\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_disable_dom_events(self, tab):\n        \"\"\"Test disabling DOM events.\"\"\"\n        tab._dom_events_enabled = True\n        await tab.disable_dom_events()\n        assert tab.dom_events_enabled is False\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_disable_runtime_events(self, tab):\n        \"\"\"Test disabling runtime events.\"\"\"\n        tab._runtime_events_enabled = True\n        await tab.disable_runtime_events()\n        assert tab.runtime_events_enabled is False\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_disable_intercept_file_chooser_dialog(self, tab):\n        \"\"\"Test disabling file chooser dialog interception.\"\"\"\n        tab._intercept_file_chooser_dialog_enabled = True\n        await tab.disable_intercept_file_chooser_dialog()\n        assert tab.intercept_file_chooser_dialog_enabled is False\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n\nclass TestTabCookieManagement:\n    \"\"\"Test Tab cookie management methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_cookies(self, tab):\n        \"\"\"Test getting cookies.\"\"\"\n        test_cookies = [{'name': 'test', 'value': 'value', 'domain': 'example.com'}]\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'cookies': test_cookies}\n        }\n\n        cookies = await tab.get_cookies()\n        assert cookies == test_cookies\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_get_cookies_uses_storage_commands_with_browser_context_id(\n        self, mock_browser, mock_connection_handler\n    ):\n        \"\"\"Test that get_cookies uses StorageCommands when browser_context_id is set.\n        \n        This ensures proper cookie isolation for explicit browser contexts.\n        \"\"\"\n        test_cookies = [{'name': 'isolated', 'value': 'cookie', 'domain': 'example.com'}]\n        mock_connection_handler.execute_command.return_value = {\n            'result': {'cookies': test_cookies}\n        }\n        \n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_connection_handler):\n            tab_with_context = Tab(\n                browser=mock_browser,\n                connection_port=9222,\n                target_id='test-target-with-context',\n                browser_context_id='explicit-context-id'\n            )\n        \n        cookies = await tab_with_context.get_cookies()\n        \n        assert cookies == test_cookies\n        # Verify StorageCommands was used (method contains 'Storage.getCookies')\n        call_args = mock_connection_handler.execute_command.call_args[0][0]\n        assert call_args['method'] == 'Storage.getCookies'\n        assert call_args['params']['browserContextId'] == 'explicit-context-id'\n\n    @pytest.mark.asyncio\n    async def test_get_cookies_uses_network_commands_without_browser_context_id(\n        self, mock_browser, mock_connection_handler\n    ):\n        \"\"\"Test that get_cookies uses NetworkCommands when browser_context_id is None.\n        \n        This is important for incognito mode (--incognito flag) where Storage.getCookies\n        does not work properly, but Network.getCookies does.\n        \"\"\"\n        test_cookies = [{'name': 'incognito', 'value': 'cookie', 'domain': 'example.com'}]\n        mock_connection_handler.execute_command.return_value = {\n            'result': {'cookies': test_cookies}\n        }\n        \n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_connection_handler):\n            tab_without_context = Tab(\n                browser=mock_browser,\n                connection_port=9222,\n                target_id='test-target-no-context',\n                browser_context_id=None  # No explicit context (incognito/default mode)\n            )\n        \n        cookies = await tab_without_context.get_cookies()\n        \n        assert cookies == test_cookies\n        # Verify NetworkCommands was used (method contains 'Network.getCookies')\n        call_args = mock_connection_handler.execute_command.call_args[0][0]\n        assert call_args['method'] == 'Network.getCookies'\n\n    @pytest.mark.asyncio\n    async def test_set_cookies(self, tab):\n        \"\"\"Test setting cookies.\"\"\"\n        test_cookies = [{'name': 'test', 'value': 'value', 'domain': 'example.com'}]\n        await tab.set_cookies(test_cookies)\n        \n        # Should call Network command for each cookie\n        assert tab._connection_handler.execute_command.call_count == 1\n\n    @pytest.mark.asyncio\n    async def test_delete_all_cookies(self, tab):\n        \"\"\"Test deleting all cookies.\"\"\"\n        await tab.delete_all_cookies()\n        \n        # Should call Network command to clear cookies\n        assert tab._connection_handler.execute_command.call_count == 1\n\n\nclass TestTabNavigation:\n    \"\"\"Test Tab navigation methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_go_to_new_url(self, tab):\n        \"\"\"Test navigating to a new URL.\"\"\"\n        tab._connection_handler.execute_command.side_effect = [\n            {'result': {'result': {'value': 'https://old-url.com'}}},  # current_url\n            {'result': {}},  # Page.enable\n            {'result': {'frameId': 'frame-id'}},  # navigate command\n            {'result': {}},  # Page.disable\n        ]\n\n        async def fire_callback(event_name, callback, temporary=False):\n            callback({'method': event_name, 'params': {}})\n            return 1\n\n        tab._connection_handler.register_callback = AsyncMock(side_effect=fire_callback)\n\n        await tab.go_to('https://example.com')\n\n        assert tab._connection_handler.execute_command.call_count == 4\n\n    @pytest.mark.asyncio\n    async def test_go_to_same_url(self, tab):\n        \"\"\"Test navigating to the same URL (should refresh).\"\"\"\n        tab._connection_handler.execute_command.side_effect = [\n            {'result': {'result': {'value': 'https://example.com'}}},  # current_url\n            {'result': {}},  # Page.enable\n            {'result': {}},  # refresh command\n            {'result': {}},  # Page.disable\n        ]\n\n        async def fire_callback(event_name, callback, temporary=False):\n            callback({'method': event_name, 'params': {}})\n            return 1\n\n        tab._connection_handler.register_callback = AsyncMock(side_effect=fire_callback)\n\n        await tab.go_to('https://example.com')\n\n        assert tab._connection_handler.execute_command.call_count == 4\n\n    @pytest.mark.asyncio\n    async def test_go_to_timeout(self, tab):\n        \"\"\"Test navigation timeout.\"\"\"\n        tab._connection_handler.execute_command.side_effect = [\n            {'result': {'result': {'value': 'https://old-url.com'}}},  # current_url\n            {'result': {}},  # Page.enable\n            {'result': {'frameId': 'frame-id'}},  # navigate command\n            {'result': {}},  # Page.disable\n        ]\n\n        # Don't fire the callback so the wait times out\n        tab._connection_handler.register_callback = AsyncMock(return_value=1)\n\n        with pytest.raises(PageLoadTimeout):\n            await tab.go_to('https://example.com', timeout=0.1)\n\n    @pytest.mark.asyncio\n    async def test_refresh(self, tab):\n        \"\"\"Test page refresh.\"\"\"\n        tab._connection_handler.execute_command.side_effect = [\n            {'result': {}},  # Page.enable\n            {'result': {}},  # refresh command\n            {'result': {}},  # Page.disable\n        ]\n\n        async def fire_callback(event_name, callback, temporary=False):\n            callback({'method': event_name, 'params': {}})\n            return 1\n\n        tab._connection_handler.register_callback = AsyncMock(side_effect=fire_callback)\n\n        await tab.refresh()\n\n        assert tab._connection_handler.execute_command.call_count == 3\n\n    @pytest.mark.asyncio\n    async def test_refresh_with_params(self, tab):\n        \"\"\"Test page refresh with parameters.\"\"\"\n        tab._connection_handler.execute_command.side_effect = [\n            {'result': {}},  # Page.enable\n            {'result': {}},  # refresh command\n            {'result': {}},  # Page.disable\n        ]\n\n        async def fire_callback(event_name, callback, temporary=False):\n            callback({'method': event_name, 'params': {}})\n            return 1\n\n        tab._connection_handler.register_callback = AsyncMock(side_effect=fire_callback)\n\n        await tab.refresh(ignore_cache=True, script_to_evaluate_on_load='console.log(\"test\")')\n\n        assert tab._connection_handler.execute_command.call_count == 3\n\n\nclass TestTabScreenshotAndPDF:\n    \"\"\"Test Tab screenshot and PDF methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_take_screenshot_to_file(self, tab, tmp_path):\n        \"\"\"Test taking screenshot and saving to file.\"\"\"\n        screenshot_data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAgAB/edzE+oAAAAASUVORK5CYII='\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'data': screenshot_data}\n        }\n        \n        screenshot_path = tmp_path / 'screenshot.png'\n        \n        # Mock aiofiles.open properly for async context manager\n        mock_file = AsyncMock()\n        mock_file.write = AsyncMock()\n        \n        with patch('aiofiles.open') as mock_aiofiles_open:\n            mock_aiofiles_open.return_value.__aenter__.return_value = mock_file\n            result = await tab.take_screenshot(str(screenshot_path))\n        \n        assert result is None  # Should return None when saving to file\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_take_screenshot_as_base64(self, tab):\n        \"\"\"Test taking screenshot and returning as base64.\"\"\"\n        screenshot_data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAgAB/edzE+oAAAAASUVORK5CYII='\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'data': screenshot_data}\n        }\n        \n        result = await tab.take_screenshot('screenshot.png', as_base64=True)\n        \n        assert result == screenshot_data\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_take_screenshot_beyond_viewport(self, tab):\n        \"\"\"Test capture_beyond_viewport flag is forwarded to command.\"\"\"\n        screenshot_data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAgAB/edzE+oAAAAASUVORK5CYII='\n\n        with patch.object(tab, '_execute_command', AsyncMock(return_value={\n            'result': {'data': screenshot_data}\n        })) as mock_execute:\n            result = await tab.take_screenshot(\n                path=None,\n                beyond_viewport=True,\n                as_base64=True,\n            )\n\n            mock_execute.assert_called_once()\n            command = mock_execute.call_args[0][0]\n            assert command['method'] == 'Page.captureScreenshot'\n            assert command['params']['captureBeyondViewport'] is True\n            assert result == screenshot_data\n\n    @pytest.mark.asyncio\n    async def test_take_screenshot_in_iframe_raises_top_level_required(self, tab):\n        \"\"\"Tab.take_screenshot must be called on top-level targets; iframe Tab raises.\"\"\"\n        # Simulate CDP returning no image data (missing 'data' key) for non top-level target\n        with patch.object(tab, '_execute_command', AsyncMock(return_value={'result': {}})):\n            with pytest.raises(TopLevelTargetRequired):\n                await tab.take_screenshot(path=None, as_base64=True)\n\n    @pytest.mark.asyncio\n    async def test_print_to_pdf_to_file(self, tab, tmp_path):\n        \"\"\"Test printing to PDF and saving to file.\"\"\"\n        pdf_data = 'JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwo+PgplbmRvYmoKdHJhaWxlcgo8PAovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKMTgKJSVFT0Y='\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'data': pdf_data}\n        }\n        \n        pdf_path = tmp_path / 'document.pdf'\n        \n        # Mock aiofiles.open properly for async context manager\n        mock_file = AsyncMock()\n        mock_file.write = AsyncMock()\n        \n        with patch('aiofiles.open') as mock_aiofiles_open:\n            mock_aiofiles_open.return_value.__aenter__.return_value = mock_file\n            result = await tab.print_to_pdf(str(pdf_path))\n        \n        assert result is None  # Should return None when saving to file\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_print_to_pdf_as_base64(self, tab):\n        \"\"\"Test printing to PDF and returning as base64.\"\"\"\n        pdf_data = 'JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwo+PgplbmRvYmoKdHJhaWxlcgo8PAovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKMTgKJSVFT0Y='\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'data': pdf_data}\n        }\n        \n        result = await tab.print_to_pdf(as_base64=True)\n        \n        assert result == pdf_data\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_print_to_pdf_with_options(self, tab, tmp_path):\n        \"\"\"Test printing to PDF with custom options.\"\"\"\n        pdf_data = 'JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwo+PgplbmRvYmoKdHJhaWxlcgo8PAovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKMTgKJSVFT0Y='\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'data': pdf_data}\n        }\n        \n        pdf_path = tmp_path / 'document.pdf'\n        \n        # Mock aiofiles.open properly for async context manager\n        mock_file = AsyncMock()\n        mock_file.write = AsyncMock()\n        \n        with patch('aiofiles.open') as mock_aiofiles_open:\n            mock_aiofiles_open.return_value.__aenter__.return_value = mock_file\n            result = await tab.print_to_pdf(\n                str(pdf_path),\n                landscape=True,\n                display_header_footer=True,\n                print_background=False,\n                scale=0.8\n            )\n        \n        assert result is None\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n\nclass TestTabDialogHandling:\n    \"\"\"Test Tab dialog handling methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_has_dialog_true(self, tab):\n        \"\"\"Test has_dialog when dialog is present.\"\"\"\n        tab._connection_handler.dialog = {'params': {'type': 'alert', 'message': 'Test'}}\n        \n        result = await tab.has_dialog()\n        assert result is True\n\n    @pytest.mark.asyncio\n    async def test_has_dialog_false(self, tab):\n        \"\"\"Test has_dialog when no dialog is present.\"\"\"\n        tab._connection_handler.dialog = None\n        \n        result = await tab.has_dialog()\n        assert result is False\n\n    @pytest.mark.asyncio\n    async def test_get_dialog_message_success(self, tab):\n        \"\"\"Test getting dialog message when dialog is present.\"\"\"\n        tab._connection_handler.dialog = {'params': {'message': 'Test message'}}\n        \n        message = await tab.get_dialog_message()\n        assert message == 'Test message'\n\n    @pytest.mark.asyncio\n    async def test_get_dialog_message_no_dialog(self, tab):\n        \"\"\"Test getting dialog message when no dialog is present.\"\"\"\n        tab._connection_handler.dialog = None\n        \n        with pytest.raises(NoDialogPresent):\n            await tab.get_dialog_message()\n\n    @pytest.mark.asyncio\n    async def test_handle_dialog_accept(self, tab):\n        \"\"\"Test accepting a dialog.\"\"\"\n        tab._connection_handler.dialog = {'params': {'type': 'alert'}}\n        \n        await tab.handle_dialog(accept=True)\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_handle_dialog_dismiss(self, tab):\n        \"\"\"Test dismissing a dialog.\"\"\"\n        tab._connection_handler.dialog = {'params': {'type': 'confirm'}}\n        \n        await tab.handle_dialog(accept=False)\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_handle_dialog_with_prompt_text(self, tab):\n        \"\"\"Test handling a prompt dialog with text.\"\"\"\n        tab._connection_handler.dialog = {'params': {'type': 'prompt'}}\n        \n        await tab.handle_dialog(accept=True, prompt_text='Test input')\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_handle_dialog_no_dialog(self, tab):\n        \"\"\"Test handling dialog when none is present.\"\"\"\n        tab._connection_handler.dialog = None\n        \n        with pytest.raises(NoDialogPresent):\n            await tab.handle_dialog(accept=True)\n\n\nclass TestTabScriptExecution:\n    \"\"\"Test Tab script execution methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_execute_script_simple(self, tab):\n        \"\"\"Test execute_script with simple JavaScript.\"\"\"\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': 'Test Result'}}\n        }\n        \n        result = await tab.execute_script('return \"Test Result\"')\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_execute_script_return_outside_function(self, tab):\n        \"\"\"Test execute_script wraps return statement outside function.\"\"\"\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': 'Wrapped result'}}\n        }\n        \n        # Script with return outside function should be wrapped\n        result = await tab.execute_script('return document.title')\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_execute_script_return_inside_function(self, tab):\n        \"\"\"Test execute_script doesn't wrap when return is inside function.\"\"\"\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': 'Function result'}}\n        }\n        \n        # Script with return inside function should not be wrapped\n        script = '''\n        function getTitle() {\n            return document.title;\n        }\n        getTitle();\n        '''\n        result = await tab.execute_script(script)\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_execute_script_no_return_statement(self, tab):\n        \"\"\"Test execute_script without return statement.\"\"\"\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': None}}\n        }\n        \n        # Script without return should not be wrapped\n        result = await tab.execute_script('console.log(\"Hello World\")')\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_execute_script_with_comments_and_strings(self, tab):\n        \"\"\"Test execute_script handles comments and strings correctly.\"\"\"\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': 'Test with comments'}}\n        }\n        \n        # Script with comments and strings containing 'return'\n        script = '''\n        // This comment has return in it\n        var message = \"This string has return in it\";\n        /* This block comment also has return */\n        return \"actual return\";\n        '''\n        result = await tab.execute_script(script)\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_execute_script_already_wrapped_function(self, tab):\n        \"\"\"Test execute_script with already wrapped function.\"\"\"\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': 'Already wrapped'}}\n        }\n        \n        # Script already wrapped in function should not be wrapped again\n        script = 'function() { console.log(\"test\"); return \"done\"; }'\n        result = await tab.execute_script(script)\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_execute_script_with_webelement_deprecation_warning(self, tab):\n        \"\"\"Test execute_script with WebElement triggers deprecation warning.\"\"\"\n        mock_element = Mock(spec=WebElement)\n        mock_element.execute_script.return_value = {'result': {'value': 'element result'}}\n        \n        with pytest.warns(DeprecationWarning, match=\"Passing a WebElement to Tab.execute_script\\\\(\\\\) is deprecated\"):\n            result = await tab.execute_script('return this.tagName', element=mock_element)\n        \n        mock_element.execute_script.assert_called_once_with(\n            'return this.tagName',\n            arguments=None,\n            silent=None,\n            return_by_value=None,\n            generate_preview=None,\n            user_gesture=None,\n            await_promise=None,\n            execution_context_id=None,\n            object_group=None,\n            throw_on_side_effect=None,\n            unique_context_id=None,\n            serialization_options=None,\n        )\n        \n        assert result == {'result': {'value': 'element result'}}\n\n    @pytest.mark.asyncio\n    async def test_execute_script_with_webelement_all_parameters(self, tab):\n        \"\"\"Test execute_script with WebElement passes all parameters correctly.\"\"\"\n        mock_element = Mock(spec=WebElement)\n        mock_element.execute_script.return_value = {'result': {'value': 'element result'}}\n        \n        arguments = [CallArgument(value=\"test\")]\n        serialization_options = SerializationOptions(serialization=\"deep\")\n        \n        with pytest.warns(DeprecationWarning):\n            result = await tab.execute_script(\n                'return this.tagName',\n                element=mock_element,\n                arguments=arguments,\n                silent=True,\n                return_by_value=True,\n                generate_preview=True,\n                user_gesture=True,\n                await_promise=True,\n                execution_context_id=123,\n                object_group=\"test_group\",\n                throw_on_side_effect=True,\n                unique_context_id=\"unique_123\",\n                serialization_options=serialization_options,\n            )\n        \n        mock_element.execute_script.assert_called_once_with(\n            'return this.tagName',\n            arguments=arguments,\n            silent=True,\n            return_by_value=True,\n            generate_preview=True,\n            user_gesture=True,\n            await_promise=True,\n            execution_context_id=123,\n            object_group=\"test_group\",\n            throw_on_side_effect=True,\n            unique_context_id=\"unique_123\",\n            serialization_options=serialization_options,\n        )\n        \n        assert result == {'result': {'value': 'element result'}}\n\n    @pytest.mark.parametrize('response', [\n        {},\n        {'result': 'not a dict'},\n        {'result': {}},\n        {'result': {'result': 'not a dict'}},\n        {'result': {'result': {'type': 'string', 'subtype': 'error', 'className': 'ReferenceError', 'description': 'argument is not defined'}}},\n        {'result': {'result': {'type': 'object', 'subtype': 'not_error', 'className': 'ReferenceError', 'description': 'argument is not defined'}}},\n        {'result': {'result': {'type': 'object', 'subtype': 'error', 'className': 'TypeError', 'description': 'argument is not defined'}}},\n        {'result': {'result': {'type': 'object', 'subtype': 'error', 'className': 'ReferenceError', 'description': 'some other error'}}},\n        {'result': {'result': {'type': 'object', 'subtype': 'error', 'className': 'ReferenceError', 'description': ''}}},\n        {'result': {'result': {'type': 'object', 'subtype': 'error', 'className': 'ReferenceError'}}},\n    ])\n    def test_validate_argument_error_early_returns(self, tab, response):\n        \"\"\"Test _validate_argument_error returns early for invalid responses.\"\"\"\n        tab._validate_argument_error(response)\n\n    @pytest.mark.parametrize('description', [\n        'argument is not defined',\n        'Error: argument is not defined at line 1',\n    ])\n    def test_validate_argument_error_raises_on_match(self, tab, description):\n        \"\"\"Test _validate_argument_error raises InvalidScriptWithElement when all conditions match.\"\"\"\n        response = {\n            'result': {\n                'result': {\n                    'type': 'object',\n                    'subtype': 'error',\n                    'className': 'ReferenceError',\n                    'description': description,\n                }\n            }\n        }\n        with pytest.raises(InvalidScriptWithElement, match='Script contains \"argument\" but no element was provided'):\n            tab._validate_argument_error(response)\n\n    @pytest.mark.asyncio\n    async def test_execute_script_triggers_validation(self, tab):\n        \"\"\"Test that execute_script calls _validate_argument_error when script fails with ReferenceError.\"\"\"\n        tab._connection_handler.execute_command.return_value = {\n            'result': {\n                'result': {\n                    'type': 'object',\n                    'subtype': 'error',\n                    'className': 'ReferenceError',\n                    'description': 'argument is not defined'\n                }\n            }\n        }\n        with pytest.raises(InvalidScriptWithElement, match='Script contains \"argument\" but no element was provided'):\n            await tab.execute_script('argument.click()')\n\n\nclass TestTabEventCallbacks:\n    \"\"\"Test Tab event callback management.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_on_callback_registration(self, tab):\n        \"\"\"Test registering event callbacks.\"\"\"\n        callback_id = 123\n        tab._connection_handler.register_callback.return_value = callback_id\n        \n        async def test_callback(event):\n            pass\n        \n        result = await tab.on('Page.loadEventFired', test_callback)\n        \n        assert result == callback_id\n        assert_mock_called_at_least_once(tab._connection_handler, 'register_callback')\n\n    @pytest.mark.asyncio\n    async def test_on_temporary_callback(self, tab):\n        \"\"\"Test registering temporary event callbacks.\"\"\"\n        callback_id = 456\n        tab._connection_handler.register_callback.return_value = callback_id\n        \n        async def test_callback(event):\n            pass\n        \n        result = await tab.on('Page.loadEventFired', test_callback, temporary=True)\n        \n        assert result == callback_id\n        tab._connection_handler.register_callback.assert_called_with(\n            'Page.loadEventFired', ANY, True\n        )\n        assert tab._connection_handler.register_callback.call_count >= 1\n\n    @pytest.mark.asyncio\n    async def test_remove_callback_success(self, tab):\n        \"\"\"Tab.remove_callback should forward to connection handler and return True.\"\"\"\n        tab._connection_handler.remove_callback.return_value = True\n\n        result = await tab.remove_callback(123)\n\n        tab._connection_handler.remove_callback.assert_called_with(123)\n        assert result is True\n\n    @pytest.mark.asyncio\n    async def test_remove_callback_false(self, tab):\n        \"\"\"Tab.remove_callback should return False when handler returns False.\"\"\"\n        tab._connection_handler.remove_callback.return_value = False\n\n        result = await tab.remove_callback(999)\n\n        tab._connection_handler.remove_callback.assert_called_with(999)\n        assert result is False\n\n\nclass TestTabFileChooser:\n    \"\"\"Test Tab file chooser functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_expect_file_chooser_single_file(self, tab):\n        \"\"\"Test expect_file_chooser with single file.\"\"\"\n        tab._connection_handler.register_callback.return_value = 123\n        \n        # Set initial state to False so methods get called\n        tab._page_events_enabled = False\n        tab._intercept_file_chooser_dialog_enabled = False\n        \n        mock_enable_page_events = AsyncMock()\n        mock_enable_intercept = AsyncMock(side_effect=lambda: setattr(tab, '_intercept_file_chooser_dialog_enabled', True))\n        mock_disable_intercept = AsyncMock()\n        mock_on = AsyncMock()\n        \n        with patch.object(tab, 'enable_page_events', mock_enable_page_events):\n            with patch.object(tab, 'enable_intercept_file_chooser_dialog', mock_enable_intercept):\n                with patch.object(tab, 'disable_intercept_file_chooser_dialog', mock_disable_intercept):\n                    with patch.object(tab, 'on', mock_on):\n                        async with tab.expect_file_chooser('test.txt'):\n                            pass\n        \n        mock_enable_page_events.assert_awaited_once()\n        mock_enable_intercept.assert_awaited_once()\n        mock_disable_intercept.assert_awaited_once()\n\n    @pytest.mark.asyncio\n    async def test_expect_file_chooser_multiple_files(self, tab):\n        \"\"\"Test expect_file_chooser with multiple files.\"\"\"\n        tab._connection_handler.register_callback.return_value = 456\n        \n        files = ['file1.txt', 'file2.txt', 'file3.txt']\n        \n        # Set initial state to False so methods get called\n        tab._page_events_enabled = False\n        tab._intercept_file_chooser_dialog_enabled = False\n        \n        mock_enable_page_events = AsyncMock()\n        mock_enable_intercept = AsyncMock(side_effect=lambda: setattr(tab, '_intercept_file_chooser_dialog_enabled', True))\n        mock_disable_intercept = AsyncMock()\n        mock_on = AsyncMock()\n        \n        with patch.object(tab, 'enable_page_events', mock_enable_page_events):\n            with patch.object(tab, 'enable_intercept_file_chooser_dialog', mock_enable_intercept):\n                with patch.object(tab, 'disable_intercept_file_chooser_dialog', mock_disable_intercept):\n                    with patch.object(tab, 'on', mock_on):\n                        async with tab.expect_file_chooser(files):\n                            pass\n        \n        mock_enable_page_events.assert_called_once()\n        mock_enable_intercept.assert_called_once()\n        mock_disable_intercept.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_expect_file_chooser_with_path_objects(self, tab):\n        \"\"\"Test expect_file_chooser with Path objects.\"\"\"\n        tab._connection_handler.register_callback.return_value = 789\n        \n        files = [Path('file1.txt'), Path('file2.txt')]\n        \n        # Set initial state to False so methods get called\n        tab._page_events_enabled = False\n        tab._intercept_file_chooser_dialog_enabled = False\n        \n        mock_enable_page_events = AsyncMock()\n        mock_enable_intercept = AsyncMock(side_effect=lambda: setattr(tab, '_intercept_file_chooser_dialog_enabled', True))\n        mock_disable_intercept = AsyncMock()\n        mock_on = AsyncMock()\n        \n        with patch.object(tab, 'enable_page_events', mock_enable_page_events):\n            with patch.object(tab, 'enable_intercept_file_chooser_dialog', mock_enable_intercept):\n                with patch.object(tab, 'disable_intercept_file_chooser_dialog', mock_disable_intercept):\n                    with patch.object(tab, 'on', mock_on):\n                        async with tab.expect_file_chooser(files):\n                            pass\n        \n        mock_enable_page_events.assert_called_once()\n        mock_enable_intercept.assert_called_once()\n        mock_disable_intercept.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_expect_file_chooser_event_handler_single_file(self, tab):\n        \"\"\"Test the real event_handler function with single file.\"\"\"\n        from pydoll.protocol.page.events import FileChooserOpenedEvent, PageEvent\n        \n        # Mock execute_command to capture the call\n        tab._execute_command = AsyncMock()\n        \n        # Create mock event data\n        mock_event: FileChooserOpenedEvent = {\n            'method': 'Page.fileChooserOpened',\n            'params': {\n                'frameId': 'test-frame-id',\n                'mode': 'selectSingle',\n                'backendNodeId': 12345\n            }\n        }\n        \n        # Capture the real event handler from expect_file_chooser\n        captured_handler = None\n        \n        async def mock_on(event_name, handler, temporary=False):\n            nonlocal captured_handler\n            if event_name == PageEvent.FILE_CHOOSER_OPENED:\n                captured_handler = handler\n            return 123\n        \n        # Mock the required methods\n        with patch.object(tab, 'enable_page_events', AsyncMock()):\n            with patch.object(tab, 'enable_intercept_file_chooser_dialog', AsyncMock()):\n                with patch.object(tab, 'disable_intercept_file_chooser_dialog', AsyncMock()):\n                    with patch.object(tab, 'disable_page_events', AsyncMock()):\n                        with patch.object(tab, 'on', mock_on):\n                            async with tab.expect_file_chooser('test.txt'):\n                                # Execute the captured real handler\n                                assert captured_handler is not None\n                                await captured_handler(mock_event)\n        \n        # Verify the command was called correctly\n        tab._execute_command.assert_called_once()\n        call_args = tab._execute_command.call_args[0][0]\n        assert call_args['method'] == 'DOM.setFileInputFiles'\n        assert call_args['params']['files'] == ['test.txt']\n        assert call_args['params']['backendNodeId'] == 12345\n\n    @pytest.mark.asyncio\n    async def test_expect_file_chooser_event_handler_multiple_files(self, tab):\n        \"\"\"Test the real event_handler function with multiple files.\"\"\"\n        from pydoll.protocol.page.events import FileChooserOpenedEvent, PageEvent\n        \n        # Mock execute_command to capture the call\n        tab._execute_command = AsyncMock()\n        \n        # Create mock event data\n        mock_event: FileChooserOpenedEvent = {\n            'method': 'Page.fileChooserOpened',\n            'params': {\n                'frameId': 'test-frame-id',\n                'mode': 'selectMultiple',\n                'backendNodeId': 67890\n            }\n        }\n\n        # Capture the real event handler from expect_file_chooser\n        captured_handler = None\n        \n        async def mock_on(event_name, handler, temporary=False):\n            nonlocal captured_handler\n            if event_name == PageEvent.FILE_CHOOSER_OPENED:\n                captured_handler = handler\n            return 123\n        \n        # Mock the required methods\n        with patch.object(tab, 'enable_page_events', AsyncMock()):\n            with patch.object(tab, 'enable_intercept_file_chooser_dialog', AsyncMock()):\n                with patch.object(tab, 'disable_intercept_file_chooser_dialog', AsyncMock()):\n                    with patch.object(tab, 'disable_page_events', AsyncMock()):\n                        with patch.object(tab, 'on', mock_on):\n                            async with tab.expect_file_chooser(['file1.txt', 'file2.pdf', 'file3.jpg']):\n                                # Execute the captured real handler\n                                assert captured_handler is not None\n                                await captured_handler(mock_event)\n        \n        # Verify the command was called correctly\n        tab._execute_command.assert_called_once()\n        call_args = tab._execute_command.call_args[0][0]\n        assert call_args['method'] == 'DOM.setFileInputFiles'\n        assert call_args['params']['files'] == ['file1.txt', 'file2.pdf', 'file3.jpg']\n        assert call_args['params']['backendNodeId'] == 67890\n\n    async def _test_event_handler_with_files(self, tab, files, expected_files, backend_node_id):\n        \"\"\"Helper method to test event handler with different file types.\"\"\"\n        from pydoll.protocol.page.events import FileChooserOpenedEvent, PageEvent\n        \n        # Mock execute_command to capture the call\n        tab._execute_command = AsyncMock()\n        \n        # Create mock event data\n        mock_event: FileChooserOpenedEvent = {\n            'method': 'Page.fileChooserOpened',\n            'params': {\n                'frameId': 'test-frame-id',\n                'mode': 'selectMultiple',\n                'backendNodeId': backend_node_id\n            }\n        }\n        \n        # Capture the real event handler from expect_file_chooser\n        captured_handler = None\n        \n        async def mock_on(event_name, handler, temporary=False):\n            nonlocal captured_handler\n            if event_name == PageEvent.FILE_CHOOSER_OPENED:\n                captured_handler = handler\n            return 123\n        \n        # Mock the required methods\n        with patch.object(tab, 'enable_page_events', AsyncMock()):\n            with patch.object(tab, 'enable_intercept_file_chooser_dialog', AsyncMock()):\n                with patch.object(tab, 'disable_intercept_file_chooser_dialog', AsyncMock()):\n                    with patch.object(tab, 'disable_page_events', AsyncMock()):\n                        with patch.object(tab, 'on', mock_on):\n                            async with tab.expect_file_chooser(files):\n                                # Execute the captured real handler\n                                assert captured_handler is not None\n                                await captured_handler(mock_event)\n        \n        # Verify the command was called correctly\n        tab._execute_command.assert_called_once()\n        call_args = tab._execute_command.call_args[0][0]\n        assert call_args['method'] == 'DOM.setFileInputFiles'\n        assert call_args['params']['files'] == expected_files\n        assert call_args['params']['backendNodeId'] == backend_node_id\n\n    @pytest.mark.asyncio\n    async def test_expect_file_chooser_event_handler_path_objects(self, tab):\n        \"\"\"Test the real event_handler function with Path objects.\"\"\"\n        from pathlib import Path\n        \n        files = [Path('documents/file1.txt'), Path('images/file2.jpg')]\n        expected_files = [str(file) for file in files]\n        \n        await self._test_event_handler_with_files(tab, files, expected_files, 54321)\n\n    @pytest.mark.asyncio\n    async def test_expect_file_chooser_event_handler_single_path_object(self, tab):\n        \"\"\"Test the real event_handler function with single Path object.\"\"\"\n        from pathlib import Path\n        \n        files = Path('documents/important.pdf')\n        expected_files = [str(files)]\n        \n        await self._test_event_handler_with_files(tab, files, expected_files, 98765)\n\n    @pytest.mark.asyncio\n    async def test_expect_file_chooser_event_handler_empty_list(self, tab):\n        \"\"\"Test the real event_handler function with empty file list.\"\"\"\n        files = []\n        expected_files = []\n        \n        await self._test_event_handler_with_files(tab, files, expected_files, 11111)\n\n\n\n\nclass TestTabCloudflareBypass:\n    \"\"\"Test Tab Cloudflare bypass functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_enable_auto_solve_cloudflare_captcha(self, tab):\n        \"\"\"Test enabling auto-solve Cloudflare captcha.\"\"\"\n        callback_id = 999\n        tab._connection_handler.register_callback.return_value = callback_id\n\n        mock_enable_page_events = AsyncMock()\n        with patch.object(tab, 'enable_page_events', mock_enable_page_events):\n            await tab.enable_auto_solve_cloudflare_captcha()\n\n        mock_enable_page_events.assert_called_once()\n        assert_mock_called_at_least_once(tab._connection_handler, 'register_callback')\n        assert tab._cloudflare_captcha_callback_id == callback_id\n\n    @pytest.mark.asyncio\n    async def test_enable_auto_solve_cloudflare_captcha_with_params(self, tab):\n        \"\"\"Test enabling auto-solve Cloudflare captcha with timing parameters.\"\"\"\n        callback_id = 888\n        tab._connection_handler.register_callback.return_value = callback_id\n\n        mock_enable_page_events = AsyncMock()\n        with patch.object(tab, 'enable_page_events', mock_enable_page_events):\n            await tab.enable_auto_solve_cloudflare_captcha(\n                time_to_wait_captcha=10,\n            )\n\n        mock_enable_page_events.assert_called_once()\n        assert_mock_called_at_least_once(tab._connection_handler, 'register_callback')\n        assert tab._cloudflare_captcha_callback_id == callback_id\n\n    @pytest.mark.asyncio\n    async def test_disable_auto_solve_cloudflare_captcha(self, tab):\n        \"\"\"Test disabling auto-solve Cloudflare captcha.\"\"\"\n        tab._cloudflare_captcha_callback_id = 777\n        tab._connection_handler.remove_callback.return_value = True\n\n        await tab.disable_auto_solve_cloudflare_captcha()\n\n        tab._connection_handler.remove_callback.assert_called_with(777)\n\n    @pytest.mark.asyncio\n    async def test_expect_and_bypass_cloudflare_captcha(self, tab):\n        \"\"\"Test expect_and_bypass_cloudflare_captcha context manager.\"\"\"\n        mock_event = MagicMock()\n        mock_event.wait = AsyncMock()\n\n        callback_id = 666\n        tab._connection_handler.register_callback.return_value = callback_id\n\n        mock_enable_page_events = AsyncMock()\n        mock_disable_page_events = AsyncMock()\n\n        with patch.object(tab, 'enable_page_events', mock_enable_page_events):\n            with patch.object(tab, 'disable_page_events', mock_disable_page_events):\n                with patch('asyncio.Event', return_value=mock_event):\n                    async with tab.expect_and_bypass_cloudflare_captcha():\n                        pass\n\n        mock_enable_page_events.assert_called_once()\n        mock_disable_page_events.assert_called_once()\n        assert_mock_called_at_least_once(tab._connection_handler, 'register_callback')\n        tab._connection_handler.remove_callback.assert_called_with(callback_id)\n\n    @pytest.mark.asyncio\n    async def test_bypass_cloudflare_with_shadow_root_traversal(self, tab):\n        \"\"\"Test _bypass_cloudflare traverses shadow roots to click checkbox.\"\"\"\n        mock_checkbox = AsyncMock()\n        mock_inner_shadow = AsyncMock()\n        mock_inner_shadow.query = AsyncMock(return_value=mock_checkbox)\n        mock_body = AsyncMock()\n        mock_body.get_shadow_root = AsyncMock(return_value=mock_inner_shadow)\n        mock_iframe = AsyncMock()\n        mock_iframe.find = AsyncMock(return_value=mock_body)\n        mock_shadow_root = AsyncMock()\n        mock_shadow_root.query = AsyncMock(return_value=mock_iframe)\n\n        mock_find_cf = AsyncMock(return_value=mock_shadow_root)\n\n        with patch.object(tab, '_find_cloudflare_shadow_root', mock_find_cf):\n            await tab._bypass_cloudflare({})\n\n        mock_find_cf.assert_called_once_with(timeout=5)\n        mock_shadow_root.query.assert_called_once()\n        mock_iframe.find.assert_called_once()\n        mock_body.get_shadow_root.assert_called_once()\n        mock_inner_shadow.query.assert_called_once()\n        mock_checkbox.click.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_bypass_cloudflare_no_shadow_root_found(self, tab):\n        \"\"\"Test _bypass_cloudflare logs error when shadow root not found.\"\"\"\n        mock_find_cf = AsyncMock(\n            side_effect=WaitElementTimeout('Timed out')\n        )\n\n        with patch.object(tab, '_find_cloudflare_shadow_root', mock_find_cf):\n            # Should not raise — error is caught and logged\n            await tab._bypass_cloudflare({})\n\n        mock_find_cf.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_bypass_cloudflare_custom_selector_emits_deprecation(self, tab):\n        \"\"\"Test that passing custom_selector emits DeprecationWarning.\"\"\"\n        callback_id = 111\n        tab._connection_handler.register_callback.return_value = callback_id\n\n        mock_enable_page_events = AsyncMock()\n        with patch.object(tab, 'enable_page_events', mock_enable_page_events):\n            with pytest.warns(DeprecationWarning, match='custom_selector is deprecated'):\n                await tab.enable_auto_solve_cloudflare_captcha(\n                    custom_selector=(By.ID, 'custom-captcha'),\n                )\n\n    @pytest.mark.asyncio\n    async def test_time_before_click_emits_deprecation(self, tab):\n        \"\"\"Test that passing time_before_click emits DeprecationWarning.\"\"\"\n        callback_id = 112\n        tab._connection_handler.register_callback.return_value = callback_id\n\n        mock_enable_page_events = AsyncMock()\n        with patch.object(tab, 'enable_page_events', mock_enable_page_events):\n            with pytest.warns(DeprecationWarning, match='time_before_click is deprecated'):\n                await tab.enable_auto_solve_cloudflare_captcha(\n                    time_before_click=3,\n                )\n\n    @pytest.mark.asyncio\n    async def test_expect_bypass_time_before_click_emits_deprecation(self, tab):\n        \"\"\"Test that expect_and_bypass with time_before_click emits DeprecationWarning.\"\"\"\n        mock_event = MagicMock()\n        mock_event.wait = AsyncMock()\n\n        tab._connection_handler.register_callback.return_value = 223\n\n        mock_enable_page_events = AsyncMock()\n        mock_disable_page_events = AsyncMock()\n\n        with patch.object(tab, 'enable_page_events', mock_enable_page_events):\n            with patch.object(tab, 'disable_page_events', mock_disable_page_events):\n                with patch('asyncio.Event', return_value=mock_event):\n                    with pytest.warns(DeprecationWarning, match='time_before_click is deprecated'):\n                        async with tab.expect_and_bypass_cloudflare_captcha(\n                            time_before_click=2,\n                        ):\n                            pass\n\n    @pytest.mark.asyncio\n    async def test_expect_bypass_custom_selector_emits_deprecation(self, tab):\n        \"\"\"Test that expect_and_bypass with custom_selector emits DeprecationWarning.\"\"\"\n        mock_event = MagicMock()\n        mock_event.wait = AsyncMock()\n\n        tab._connection_handler.register_callback.return_value = 222\n\n        mock_enable_page_events = AsyncMock()\n        mock_disable_page_events = AsyncMock()\n\n        with patch.object(tab, 'enable_page_events', mock_enable_page_events):\n            with patch.object(tab, 'disable_page_events', mock_disable_page_events):\n                with patch('asyncio.Event', return_value=mock_event):\n                    with pytest.warns(DeprecationWarning, match='custom_selector is deprecated'):\n                        async with tab.expect_and_bypass_cloudflare_captcha(\n                            custom_selector=(By.ID, 'old-sel'),\n                        ):\n                            pass\n\n    @pytest.mark.asyncio\n    async def test_find_cloudflare_shadow_root_polls_until_found(self, tab):\n        \"\"\"Test _find_cloudflare_shadow_root polls until CF shadow root appears.\"\"\"\n\n        class MockShadowRoot:\n            def __init__(self, html):\n                self._html = html\n\n            @property\n            async def inner_html(self):\n                return self._html\n\n        non_cf_sr = MockShadowRoot('<div>other content</div>')\n        cf_sr = MockShadowRoot(\n            '<iframe src=\"https://challenges.cloudflare.com/cdn-cgi/\"></iframe>'\n        )\n\n        call_count = 0\n\n        async def mock_find_shadow_roots(deep=False):\n            nonlocal call_count\n            call_count += 1\n            if call_count == 1:\n                return [non_cf_sr]\n            return [non_cf_sr, cf_sr]\n\n        with patch.object(tab, 'find_shadow_roots', side_effect=mock_find_shadow_roots):\n            with patch('asyncio.sleep', AsyncMock()):\n                result = await tab._find_cloudflare_shadow_root(timeout=10)\n\n        assert result is cf_sr\n        assert call_count == 2\n\n    @pytest.mark.asyncio\n    async def test_find_cloudflare_shadow_root_timeout(self, tab):\n        \"\"\"Test _find_cloudflare_shadow_root raises WaitElementTimeout on timeout.\"\"\"\n\n        class MockShadowRoot:\n            @property\n            async def inner_html(self):\n                return '<div>other content</div>'\n\n        non_cf_sr = MockShadowRoot()\n        mock_find = AsyncMock(return_value=[non_cf_sr])\n\n        # Simulate time progressing past the timeout\n        time_values = iter([0, 0.5, 1.0, 100.0])\n\n        mock_loop = MagicMock()\n        mock_loop.time = lambda: next(time_values)\n\n        with patch.object(tab, 'find_shadow_roots', mock_find):\n            with patch('asyncio.get_event_loop', return_value=mock_loop):\n                with patch('asyncio.sleep', AsyncMock()):\n                    with pytest.raises(WaitElementTimeout, match='Timed out'):\n                        await tab._find_cloudflare_shadow_root(timeout=5)\n\n\nclass TestTabDownload:\n    \"\"\"Tests for Tab.expect_download context manager.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_expect_download_keeps_file_when_path_provided(self, tab, tmp_path):\n        target_dir = tmp_path / \"dl\"\n        tab._browser.set_download_behavior = AsyncMock()\n\n        # Prepare to capture callbacks and trigger them\n        handlers = {}\n\n        async def fake_on(event_name, handler, temporary=False):\n            handlers[event_name] = handler\n            return 100 if event_name == PageEvent.DOWNLOAD_WILL_BEGIN else 101\n\n        with patch.object(tab, 'on', fake_on):\n            async with tab.expect_download(keep_file_at=str(target_dir)) as download:\n                # Simulate willBegin\n                await handlers[PageEvent.DOWNLOAD_WILL_BEGIN]({\n                    'method': PageEvent.DOWNLOAD_WILL_BEGIN,\n                    'params': {\n                        'frameId': 'frame-1',\n                        'guid': 'guid-1',\n                        'url': 'https://example.com/file.txt',\n                        'suggestedFilename': 'file.txt',\n                    }\n                })\n                # Simulate progress Completed without filePath (fallback to suggested)\n                await handlers[PageEvent.DOWNLOAD_PROGRESS]({\n                    'method': PageEvent.DOWNLOAD_PROGRESS,\n                    'params': {\n                        'guid': 'guid-1',\n                        'totalBytes': 10,\n                        'receivedBytes': 10,\n                        'state': 'completed',\n                    }\n                })\n\n                # Create the expected file to allow read\n                expected_path = target_dir / 'file.txt'\n                expected_path.parent.mkdir(parents=True, exist_ok=True)\n                expected_path.write_bytes(b'content')\n\n                data = await download.read_bytes()\n                assert data == b'content'\n                assert str(download.file_path).endswith('file.txt')\n\n        # Ensure behavior reset called\n        tab._browser.set_download_behavior.assert_awaited()\n\n    @pytest.mark.asyncio\n    async def test_expect_download_timeout_raises(self, tab, tmp_path):\n        tab._browser.set_download_behavior = AsyncMock()\n\n        handlers = {}\n\n        async def fake_on(event_name, handler, temporary=False):\n            handlers[event_name] = handler\n            return 200 if event_name == PageEvent.DOWNLOAD_WILL_BEGIN else 201\n\n        with patch.object(tab, 'on', fake_on):\n            with pytest.raises(DownloadTimeout):\n                async with tab.expect_download(keep_file_at=str(tmp_path), timeout=0.01):\n                    # Trigger will begin but never complete\n                    await handlers[PageEvent.DOWNLOAD_WILL_BEGIN]({\n                        'method': PageEvent.DOWNLOAD_WILL_BEGIN,\n                        'params': {\n                            'frameId': 'frame-1',\n                            'guid': 'guid-2',\n                            'url': 'https://example.com/slow.bin',\n                            'suggestedFilename': 'slow.bin',\n                        }\n                    })\n                    # Do not trigger completed\n                    await asyncio.sleep(0.02)\n\n    @pytest.mark.asyncio\n    async def test_expect_download_cleans_temp_directory(self, tab, tmp_path):\n        tab._browser.set_download_behavior = AsyncMock()\n        handlers = {}\n\n        async def fake_on(event_name, handler, temporary=False):\n            handlers[event_name] = handler\n            return 300 if event_name == PageEvent.DOWNLOAD_WILL_BEGIN else 301\n\n        with patch.object(tab, 'on', fake_on):\n            # Use None to create temp dir and ensure cleanup occurs\n            async with tab.expect_download(keep_file_at=None) as download:\n                await handlers[PageEvent.DOWNLOAD_WILL_BEGIN]({\n                    'method': PageEvent.DOWNLOAD_WILL_BEGIN,\n                    'params': {\n                        'frameId': 'frame-1',\n                        'guid': 'guid-3',\n                        'url': 'https://example.com/tmp.txt',\n                        'suggestedFilename': 'tmp.txt',\n                    }\n                })\n                await handlers[PageEvent.DOWNLOAD_PROGRESS]({\n                    'method': PageEvent.DOWNLOAD_PROGRESS,\n                    'params': {\n                        'guid': 'guid-3',\n                        'totalBytes': 3,\n                        'receivedBytes': 3,\n                        'state': 'completed',\n                    }\n                })\n\n                # Create the expected file inside the dynamically chosen dir\n                assert download.file_path is not None\n                file_path = Path(download.file_path)\n                file_path.parent.mkdir(parents=True, exist_ok=True)\n                file_path.write_bytes(b'abc')\n                assert (await download.read_base64()) == base64.b64encode(b'abc').decode('ascii')\n\n            # After context, temp dir should be removed\n            # We cannot know the exact temp dir path (random), but ensure file is gone\n            assert not file_path.exists()\n\n    @pytest.mark.asyncio\n    async def test_expect_download_ignores_progress_with_different_guid(self, tab, tmp_path):\n        tab._browser.set_download_behavior = AsyncMock()\n\n        handlers = {}\n\n        async def fake_on(event_name, handler, temporary=False):\n            handlers[event_name] = handler\n            return 400 if event_name == PageEvent.DOWNLOAD_WILL_BEGIN else 401\n\n        with patch.object(tab, 'on', fake_on):\n            async with tab.expect_download(keep_file_at=str(tmp_path)) as download:\n                await handlers[PageEvent.DOWNLOAD_WILL_BEGIN]({\n                    'method': PageEvent.DOWNLOAD_WILL_BEGIN,\n                    'params': {\n                        'frameId': 'frame-1',\n                        'guid': 'guid-x',\n                        'url': 'https://example.com/file.bin',\n                        'suggestedFilename': 'file.bin',\n                    }\n                })\n\n                # Wrong guid should be ignored and not mark as done\n                await handlers[PageEvent.DOWNLOAD_PROGRESS]({\n                    'method': PageEvent.DOWNLOAD_PROGRESS,\n                    'params': {\n                        'guid': 'wrong-guid',\n                        'totalBytes': 1,\n                        'receivedBytes': 1,\n                        'state': 'completed',\n                    }\n                })\n\n                # Still not finished\n                assert download.file_path is None\n\n                # Correct guid completes\n                await handlers[PageEvent.DOWNLOAD_PROGRESS]({\n                    'method': PageEvent.DOWNLOAD_PROGRESS,\n                    'params': {\n                        'guid': 'guid-x',\n                        'totalBytes': 10,\n                        'receivedBytes': 10,\n                        'state': 'completed',\n                        'filePath': str(tmp_path / 'file.bin'),\n                    }\n                })\n\n                await download.wait_finished()\n\n    @pytest.mark.asyncio\n    async def test_expect_download_page_events_auto_enable_disable(self, tab, tmp_path):\n        \"\"\"When page events are disabled, expect_download should enable and then disable them.\"\"\"\n        tab._browser.set_download_behavior = AsyncMock()\n        tab._page_events_enabled = False\n\n        enable_page_events = AsyncMock()\n        disable_page_events = AsyncMock()\n\n        handlers = {}\n\n        async def fake_on(event_name, handler, temporary=False):\n            handlers[event_name] = handler\n            return 500 if event_name == PageEvent.DOWNLOAD_WILL_BEGIN else 501\n\n        with patch.object(tab, 'enable_page_events', enable_page_events), \\\n             patch.object(tab, 'disable_page_events', disable_page_events), \\\n             patch.object(tab, 'on', fake_on):\n            async with tab.expect_download(keep_file_at=str(tmp_path)):\n                await handlers[PageEvent.DOWNLOAD_WILL_BEGIN]({\n                    'method': PageEvent.DOWNLOAD_WILL_BEGIN,\n                    'params': {\n                        'frameId': 'frame-1',\n                        'guid': 'guid-y',\n                        'url': 'https://example.com/auto.bin',\n                        'suggestedFilename': 'auto.bin',\n                    }\n                })\n                await handlers[PageEvent.DOWNLOAD_PROGRESS]({\n                    'method': PageEvent.DOWNLOAD_PROGRESS,\n                    'params': {\n                        'guid': 'guid-y',\n                        'totalBytes': 2,\n                        'receivedBytes': 2,\n                        'state': 'completed',\n                        'filePath': str(tmp_path / 'auto.bin'),\n                    }\n                })\n\n        enable_page_events.assert_awaited_once()\n        disable_page_events.assert_awaited_once()\n\n    @pytest.mark.asyncio\n    async def test_expect_download_keeps_page_events_enabled_when_already_enabled(self, tab, tmp_path):\n        \"\"\"When page events already enabled, expect_download should not disable them on exit.\"\"\"\n        tab._browser.set_download_behavior = AsyncMock()\n        tab._page_events_enabled = True\n\n        enable_page_events = AsyncMock()\n        disable_page_events = AsyncMock()\n\n        handlers = {}\n\n        async def fake_on(event_name, handler, temporary=False):\n            handlers[event_name] = handler\n            return 600 if event_name == PageEvent.DOWNLOAD_WILL_BEGIN else 601\n\n        with patch.object(tab, 'enable_page_events', enable_page_events), \\\n             patch.object(tab, 'disable_page_events', disable_page_events), \\\n             patch.object(tab, 'on', fake_on):\n            async with tab.expect_download(keep_file_at=str(tmp_path)):\n                await handlers[PageEvent.DOWNLOAD_WILL_BEGIN]({\n                    'method': PageEvent.DOWNLOAD_WILL_BEGIN,\n                    'params': {\n                        'frameId': 'frame-1',\n                        'guid': 'guid-z',\n                        'url': 'https://example.com/enabled.bin',\n                        'suggestedFilename': 'enabled.bin',\n                    }\n                })\n                await handlers[PageEvent.DOWNLOAD_PROGRESS]({\n                    'method': PageEvent.DOWNLOAD_PROGRESS,\n                    'params': {\n                        'guid': 'guid-z',\n                        'totalBytes': 2,\n                        'receivedBytes': 2,\n                        'state': 'completed',\n                        'filePath': str(tmp_path / 'enabled.bin'),\n                    }\n                })\n\n        enable_page_events.assert_not_awaited()\n        disable_page_events.assert_not_awaited()\n\n\nclass TestTabFrameHandling:\n    \"\"\"Test Tab iframe handling methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_frame_success(self, tab, mock_browser):\n        \"\"\"Test getting frame from iframe element.\"\"\"\n        mock_iframe_element = MagicMock()\n        mock_iframe_element.tag_name = 'iframe'\n        mock_iframe_element.get_attribute.return_value = 'https://example.com/iframe'\n        mock_iframe_element._object_id = 'iframe-object-id'\n        \n        mock_browser.get_targets = AsyncMock(return_value=[\n            {'targetId': 'iframe-target-id', 'url': 'https://example.com/iframe'}\n        ])\n\n        with pytest.warns(DeprecationWarning):\n            frame = await tab.get_frame(mock_iframe_element)\n        \n        assert isinstance(frame, Tab)\n        mock_browser.get_targets.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_get_frame_uses_cache_on_subsequent_calls(self, tab, mock_browser):\n        \"\"\"Subsequent calls to get_frame should return cached Tab instance.\"\"\"\n        # Prepare iframe element\n        mock_iframe_element = MagicMock()\n        mock_iframe_element.tag_name = 'iframe'\n        frame_url = 'https://example.com/iframe'\n        mock_iframe_element.get_attribute.return_value = frame_url\n        # Prepare browser targets and cache\n        mock_browser.get_targets = AsyncMock(return_value=[\n            {'targetId': 'iframe-target-id', 'url': frame_url, 'type': 'page'}\n        ])\n        tab._browser._tabs_opened = {}\n\n        with patch('pydoll.browser.tab.ConnectionHandler', autospec=True):\n            with pytest.warns(DeprecationWarning):\n                frame1 = await tab.get_frame(mock_iframe_element)\n            # Second call should reuse from cache and not create a new Tab\n            with pytest.warns(DeprecationWarning):\n                frame2 = await tab.get_frame(mock_iframe_element)\n\n        assert isinstance(frame1, Tab)\n        assert frame1 is frame2\n        assert tab._browser._tabs_opened['iframe-target-id'] is frame1\n\n    @pytest.mark.asyncio\n    async def test_get_frame_not_iframe(self, tab):\n        \"\"\"Test getting frame from non-iframe element.\"\"\"\n        mock_element = MagicMock()\n        mock_element.tag_name = 'div'  # Mock the property directly\n        \n        with pytest.warns(DeprecationWarning):\n            with pytest.raises(NotAnIFrame):\n                await tab.get_frame(mock_element)\n\n    @pytest.mark.asyncio\n    async def test_get_frame_no_frame_id(self, tab, mock_browser):\n        \"\"\"Test getting frame when no frame ID is found.\"\"\"\n        mock_iframe_element = MagicMock()\n        mock_iframe_element.tag_name = 'iframe'  # Mock the _attributes dict\n        mock_iframe_element.get_attribute.return_value = 'https://example.com/iframe'\n        mock_iframe_element._object_id = 'iframe-object-id'\n\n        mock_browser.get_targets = AsyncMock(return_value=[])\n        \n        with pytest.warns(DeprecationWarning):\n            with pytest.raises(IFrameNotFound):\n                await tab.get_frame(mock_iframe_element)\n\n\nclass TestTabUtilityMethods:\n    \"\"\"Test Tab utility and helper methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_bring_to_front(self, tab):\n        \"\"\"Test bringing the tab to front sends the correct command.\"\"\"\n        with patch.object(tab, '_execute_command', AsyncMock()) as mock_execute:\n            await tab.bring_to_front()\n\n            mock_execute.assert_called_once()\n            command = mock_execute.call_args[0][0]\n            assert command['method'] == 'Page.bringToFront'\n\n    @pytest.mark.asyncio\n    async def test_close(self, tab, mock_browser):\n        \"\"\"Test closing the tab.\"\"\"\n        with patch.object(tab, '_execute_command', AsyncMock()) as mock_execute:\n            await tab.close()\n            \n            # Should call _execute_command with PageCommands.close()\n            mock_execute.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_wait_page_load_complete(self, tab):\n        \"\"\"Test _wait_page_load waits for LOAD_EVENT_FIRED via CDP events.\"\"\"\n        tab._connection_handler.execute_command.return_value = {'result': {}}\n\n        async def fire_callback(event_name, callback, temporary=False):\n            callback({'method': event_name, 'params': {}})\n            return 1\n\n        tab._connection_handler.register_callback = AsyncMock(side_effect=fire_callback)\n\n        async with tab._wait_page_load():\n            pass\n\n        tab._connection_handler.register_callback.assert_called_once()\n        call_args = tab._connection_handler.register_callback.call_args\n        assert call_args[0][0] == PageEvent.LOAD_EVENT_FIRED\n\n    @pytest.mark.asyncio\n    async def test_wait_page_load_interactive(self, tab):\n        \"\"\"Test _wait_page_load waits for DOM_CONTENT_EVENT_FIRED when\n        page_load_state is INTERACTIVE.\"\"\"\n        tab._browser.options.page_load_state = PageLoadState.INTERACTIVE\n        tab._connection_handler.execute_command.return_value = {'result': {}}\n\n        async def fire_callback(event_name, callback, temporary=False):\n            callback({'method': event_name, 'params': {}})\n            return 1\n\n        tab._connection_handler.register_callback = AsyncMock(side_effect=fire_callback)\n\n        async with tab._wait_page_load():\n            pass\n\n        tab._connection_handler.register_callback.assert_called_once()\n        call_args = tab._connection_handler.register_callback.call_args\n        assert call_args[0][0] == PageEvent.DOM_CONTENT_EVENT_FIRED\n\n    @pytest.mark.asyncio\n    async def test_wait_page_load_timeout(self, tab):\n        \"\"\"Test _wait_page_load raises PageLoadTimeout on timeout.\"\"\"\n        tab._connection_handler.execute_command.return_value = {'result': {}}\n        tab._connection_handler.register_callback = AsyncMock(return_value=1)\n\n        with pytest.raises(PageLoadTimeout):\n            async with tab._wait_page_load(timeout=0.1):\n                pass\n\n    @pytest.mark.asyncio\n    async def test_wait_page_load_cleans_up_page_events(self, tab):\n        \"\"\"Test _wait_page_load enables/disables page events when needed.\"\"\"\n        assert tab._page_events_enabled is False\n        tab._connection_handler.execute_command.return_value = {'result': {}}\n\n        async def fire_callback(event_name, callback, temporary=False):\n            callback({'method': event_name, 'params': {}})\n            return 1\n\n        tab._connection_handler.register_callback = AsyncMock(side_effect=fire_callback)\n\n        async with tab._wait_page_load():\n            assert tab._page_events_enabled is True\n\n        assert tab._page_events_enabled is False\n\n    @pytest.mark.asyncio\n    async def test_refresh_if_url_not_changed_same_url(self, tab):\n        \"\"\"Test _refresh_if_url_not_changed with same URL.\"\"\"\n        tab._connection_handler.execute_command.side_effect = [\n            {'result': {'result': {'value': 'https://example.com'}}},  # current_url call\n            {'result': {}},  # Page.enable\n            {'result': {}},  # refresh call\n            {'result': {}},  # Page.disable\n        ]\n\n        async def fire_callback(event_name, callback, temporary=False):\n            callback({'method': event_name, 'params': {}})\n            return 1\n\n        tab._connection_handler.register_callback = AsyncMock(side_effect=fire_callback)\n\n        result = await tab._refresh_if_url_not_changed('https://example.com')\n\n        assert result is True\n        assert tab._connection_handler.execute_command.call_count == 4\n\n    @pytest.mark.asyncio\n    async def test_refresh_if_url_not_changed_different_url(self, tab):\n        \"\"\"Test _refresh_if_url_not_changed with different URL.\"\"\"\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': 'https://different.com'}}\n        }\n        \n        result = await tab._refresh_if_url_not_changed('https://example.com')\n        \n        assert result is False\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n\nclass TestTabRequestManagement:\n    \"\"\"Test Tab request management methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_continue_request(self, tab):\n        \"\"\"Test continue_request method with minimal parameters.\"\"\"\n        request_id = 'test_request_123'\n        \n        await tab.continue_request(request_id)\n        \n        # Verify the command was executed with correct parameters\n        assert_mock_called_at_least_once(tab._connection_handler)\n        \n        # Get the call arguments to verify the command\n        call_args = tab._connection_handler.execute_command.call_args_list[-1]\n        command = call_args[0][0]  # First argument is the command\n        \n        # Verify it's a FetchCommands.continue_request command\n        assert command['method'] == 'Fetch.continueRequest'\n        assert command['params']['requestId'] == request_id\n        # Verify optional parameters are None/not set\n        params = command['params']\n        assert params.get('url') is None\n        assert params.get('method') is None\n        assert params.get('postData') is None\n        assert params.get('headers') is None\n        assert params.get('interceptResponse') is None\n\n    @pytest.mark.asyncio\n    async def test_fail_request(self, tab):\n        \"\"\"Test fail_request method.\"\"\"\n        from pydoll.protocol.network.types import ErrorReason\n        \n        request_id = 'test_request_456'\n        error_reason = ErrorReason.FAILED\n        \n        await tab.fail_request(request_id, error_reason)\n        \n        # Verify the command was executed with correct parameters\n        assert_mock_called_at_least_once(tab._connection_handler)\n        \n        # Get the call arguments to verify the command\n        call_args = tab._connection_handler.execute_command.call_args_list[-1]\n        command = call_args[0][0]  # First argument is the command\n        \n        # Verify it's a FetchCommands.fail_request command\n        assert command['method'] == 'Fetch.failRequest'\n        assert command['params']['requestId'] == request_id\n        assert command['params']['errorReason'] == error_reason\n\n    @pytest.mark.asyncio\n    async def test_fulfill_request(self, tab):\n        \"\"\"Test fulfill_request method with minimal parameters.\"\"\"\n        request_id = 'test_request_789'\n        response_code = 200\n        \n        await tab.fulfill_request(request_id, response_code)\n        \n        # Verify the command was executed with correct parameters\n        assert_mock_called_at_least_once(tab._connection_handler)\n        \n        # Get the call arguments to verify the command\n        call_args = tab._connection_handler.execute_command.call_args_list[-1]\n        command = call_args[0][0]  # First argument is the command\n        \n        # Verify it's a FetchCommands.fulfill_request command\n        assert command['method'] == 'Fetch.fulfillRequest'\n        assert command['params']['requestId'] == request_id\n        assert command['params']['responseCode'] == response_code\n        # Verify optional parameters are None/not set\n        params = command['params']\n        assert params.get('responseHeaders') is None\n        assert params.get('body') is None\n        assert params.get('responsePhrase') is None\n\n    @pytest.mark.asyncio\n    async def test_continue_request_with_all_params(self, tab):\n        \"\"\"Test continue_request with all parameters.\"\"\"\n        from pydoll.protocol.network.types import RequestMethod\n        \n        request_id = 'test_request_456'\n        url = 'https://modified-example.com'\n        method = RequestMethod.POST\n        post_data = 'modified_data=test'\n        headers = [{'name': 'Authorization', 'value': 'Bearer token123'}]\n        intercept_response = True\n        \n        await tab.continue_request(\n            request_id=request_id,\n            url=url,\n            method=method,\n            post_data=post_data,\n            headers=headers,\n            intercept_response=intercept_response,\n        )\n        \n        # Verify the command was executed with correct parameters\n        assert_mock_called_at_least_once(tab._connection_handler)\n        \n        # Get the call arguments to verify the command\n        call_args = tab._connection_handler.execute_command.call_args_list[-1]\n        command = call_args[0][0]  # First argument is the command\n        \n        # Verify all parameters\n        params = command['params']\n        assert params['requestId'] == request_id\n        assert params['url'] == url\n        assert params['method'] == method\n        assert params['postData'] == post_data\n        assert params['headers'] == headers\n        assert params['interceptResponse'] == intercept_response\n\n    @pytest.mark.asyncio\n    async def test_continue_request_with_different_id(self, tab):\n        \"\"\"Test continue_request with different request ID.\"\"\"\n        request_id = 'another_request_id_xyz'\n        \n        await tab.continue_request(request_id)\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n        \n        # Verify the request ID was passed correctly\n        call_args = tab._connection_handler.execute_command.call_args_list[-1]\n        command = call_args[0][0]\n        assert command['params']['requestId'] == request_id\n\n    @pytest.mark.asyncio\n    async def test_fail_request_with_different_error(self, tab):\n        \"\"\"Test fail_request with different error reason.\"\"\"\n        from pydoll.protocol.network.types import ErrorReason\n        \n        request_id = 'test_request_error'\n        error_reason = ErrorReason.ABORTED\n        \n        await tab.fail_request(request_id, error_reason)\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n        \n        # Verify the error reason was passed correctly\n        call_args = tab._connection_handler.execute_command.call_args_list[-1]\n        command = call_args[0][0]\n        assert command['params']['errorReason'] == error_reason\n\n    @pytest.mark.asyncio\n    async def test_fulfill_request_with_all_params(self, tab):\n        \"\"\"Test fulfill_request with all parameters.\"\"\"\n        request_id = 'test_request_complete'\n        response_code = 200\n        response_headers = [{'name': 'Content-Type', 'value': 'application/json'}]\n        json_response = '{\"status\": \"success\", \"data\": \"test\"}'\n        body = base64.b64encode(json_response.encode('utf-8')).decode('utf-8')\n        response_phrase = 'OK'\n        \n        await tab.fulfill_request(\n            request_id=request_id,\n            response_code=response_code,\n            response_headers=response_headers,\n            body=body,\n            response_phrase=response_phrase,\n        )\n        \n        # Verify the command was executed with correct parameters\n        assert_mock_called_at_least_once(tab._connection_handler)\n        \n        # Get the call arguments to verify the command\n        call_args = tab._connection_handler.execute_command.call_args_list[-1]\n        command = call_args[0][0]  # First argument is the command\n        \n        # Verify all parameters\n        params = command['params']\n        assert params['requestId'] == request_id\n        assert params['responseCode'] == response_code\n        assert params['responseHeaders'] == response_headers\n        assert params['body'] == body\n        assert params['responsePhrase'] == response_phrase\n\n    @pytest.mark.asyncio\n    async def test_fulfill_request_with_different_status_code(self, tab):\n        \"\"\"Test fulfill_request with different status code.\"\"\"\n        request_id = 'test_request_404'\n        response_code = 404\n        response_headers = [{'name': 'Content-Type', 'value': 'text/html'}]\n        html_response = '<html><body><h1>404 - Not Found</h1></body></html>'\n        response_body = base64.b64encode(html_response.encode('utf-8')).decode('utf-8')\n        \n        await tab.fulfill_request(\n            request_id, response_code, response_headers, response_body\n        )\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n        \n        # Verify all parameters were passed correctly\n        call_args = tab._connection_handler.execute_command.call_args_list[-1]\n        command = call_args[0][0]\n        assert command['params']['responseCode'] == response_code\n        assert command['params']['responseHeaders'] == response_headers\n        assert command['params']['body'] == response_body\n\n    @pytest.mark.asyncio\n    async def test_fulfill_request_empty_headers(self, tab):\n        \"\"\"Test fulfill_request with empty headers.\"\"\"\n        request_id = 'test_request_empty_headers'\n        response_code = 200\n        response_headers = []\n        json_response = '{\"message\": \"success\"}'\n        response_body = base64.b64encode(json_response.encode('utf-8')).decode('utf-8')\n        \n        await tab.fulfill_request(\n            request_id, response_code, response_headers, response_body\n        )\n        \n        assert_mock_called_at_least_once(tab._connection_handler)\n        \n        # Verify empty headers are handled correctly\n        call_args = tab._connection_handler.execute_command.call_args_list[-1]\n        command = call_args[0][0]\n        assert command['params']['responseHeaders'] == []\n        assert command['params']['body'] == response_body\n\n\nclass TestTabEdgeCases:\n    \"\"\"Test Tab edge cases and error conditions.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_take_screenshot_invalid_extension(self, tab):\n        \"\"\"Test take_screenshot with invalid file extension.\"\"\"\n        with pytest.raises(InvalidFileExtension):\n            await tab.take_screenshot('screenshot.txt')\n\n    @pytest.mark.asyncio\n    async def test_print_to_pdf_with_invalid_path(self, tab):\n        \"\"\"Test print_to_pdf with missing path when not using base64.\"\"\"\n        # Mock the response\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'data': 'JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwo+PgplbmRvYmoKdHJhaWxlcgo8PAovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKMTgKJSVFT0Y='}\n        }\n        \n        # Should raise ValueError when path is not provided and as_base64=False\n        with pytest.raises(ValueError, match=\"path is required when as_base64=False\"):\n            await tab.print_to_pdf(as_base64=False)\n\n    @pytest.mark.asyncio\n    async def test_network_logs_property(self, tab):\n        \"\"\"Test network_logs property access.\"\"\"\n        test_logs = [{'request': {'url': 'https://example.com'}}]\n        tab._connection_handler.network_logs = test_logs\n        \n        logs = tab._connection_handler.network_logs\n        assert logs == test_logs\n\n    @pytest.mark.asyncio\n    async def test_dialog_property(self, tab):\n        \"\"\"Test dialog property access.\"\"\"\n        test_dialog = {'type': 'alert', 'message': 'Test message'}\n        tab._connection_handler.dialog = test_dialog\n        \n        dialog = tab._connection_handler.dialog\n        assert dialog == test_dialog\n\n\nclass TestTabNetworkMethods:\n    \"\"\"Test Tab network-related methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_network_response_body_success(self, tab):\n        \"\"\"Test get_network_response_body with network events enabled.\"\"\"\n        # Enable network events\n        tab._network_events_enabled = True\n        \n        # Mock the response\n        expected_body = '<html><body>Response content</body></html>'\n        tab._connection_handler.execute_command.return_value = {\n            'result': {'body': expected_body}\n        }\n        \n        result = await tab.get_network_response_body('test_request_123')\n        \n        assert result == expected_body\n        assert_mock_called_at_least_once(tab._connection_handler)\n\n    @pytest.mark.asyncio\n    async def test_get_network_response_body_events_not_enabled(self, tab):\n        \"\"\"Test get_network_response_body when network events are not enabled.\"\"\"\n        # Ensure network events are disabled\n        tab._network_events_enabled = False\n        \n        with pytest.raises(NetworkEventsNotEnabled) as exc_info:\n            await tab.get_network_response_body('test_request_123')\n        \n        assert str(exc_info.value) == 'Network events must be enabled to get response body'\n        tab._connection_handler.execute_command.assert_not_called()\n\n    @pytest.mark.asyncio\n    async def test_get_network_logs_success_no_filter(self, tab):\n        \"\"\"Test get_network_logs without filter.\"\"\"\n        # Enable network events\n        tab._network_events_enabled = True\n        \n        # Mock network logs\n        test_logs = [\n            {\n                'method': 'Network.requestWillBeSent',\n                'params': {\n                    'request': {'url': 'https://example.com/api/data'},\n                    'requestId': 'req_1'\n                }\n            },\n            {\n                'method': 'Network.responseReceived',\n                'params': {\n                    'request': {'url': 'https://example.com/static/style.css'},\n                    'requestId': 'req_2'\n                }\n            }\n        ]\n        tab._connection_handler.network_logs = test_logs\n        \n        result = await tab.get_network_logs()\n        \n        assert result == test_logs\n        assert len(result) == 2\n\n    @pytest.mark.asyncio\n    async def test_get_network_logs_success_with_filter(self, tab):\n        \"\"\"Test get_network_logs with URL filter.\"\"\"\n        # Enable network events\n        tab._network_events_enabled = True\n        \n        # Mock network logs\n        test_logs = [\n            {\n                'method': 'Network.requestWillBeSent',\n                'params': {\n                    'request': {'url': 'https://example.com/api/data'},\n                    'requestId': 'req_1'\n                }\n            },\n            {\n                'method': 'Network.responseReceived',\n                'params': {\n                    'request': {'url': 'https://example.com/static/style.css'},\n                    'requestId': 'req_2'\n                }\n            },\n            {\n                'method': 'Network.requestWillBeSent',\n                'params': {\n                    'request': {'url': 'https://api.example.com/users'},\n                    'requestId': 'req_3'\n                }\n            }\n        ]\n        tab._connection_handler.network_logs = test_logs\n        \n        result = await tab.get_network_logs(filter='api')\n        \n        # Should return only logs with 'api' in the URL\n        assert len(result) == 2\n        assert result[0]['params']['request']['url'] == 'https://example.com/api/data'\n        assert result[1]['params']['request']['url'] == 'https://api.example.com/users'\n\n    @pytest.mark.asyncio\n    async def test_get_network_logs_empty_filter_result(self, tab):\n        \"\"\"Test get_network_logs with filter that matches no logs.\"\"\"\n        # Enable network events\n        tab._network_events_enabled = True\n        \n        # Mock network logs\n        test_logs = [\n            {\n                'method': 'Network.requestWillBeSent',\n                'params': {\n                    'request': {'url': 'https://example.com/static/style.css'},\n                    'requestId': 'req_1'\n                }\n            }\n        ]\n        tab._connection_handler.network_logs = test_logs\n        \n        result = await tab.get_network_logs(filter='nonexistent')\n        \n        assert result == []\n\n    @pytest.mark.asyncio\n    async def test_get_network_logs_events_not_enabled(self, tab):\n        \"\"\"Test get_network_logs when network events are not enabled.\"\"\"\n        # Ensure network events are disabled\n        tab._network_events_enabled = False\n        \n        with pytest.raises(NetworkEventsNotEnabled) as exc_info:\n            await tab.get_network_logs()\n        \n        assert str(exc_info.value) == 'Network events must be enabled to get network logs'\n\n    @pytest.mark.asyncio\n    async def test_get_network_logs_missing_request_params(self, tab):\n        \"\"\"Test get_network_logs with logs missing request parameters.\"\"\"\n        # Enable network events\n        tab._network_events_enabled = True\n        \n        # Mock network logs with missing request data\n        test_logs = [\n            {\n                'method': 'Network.requestWillBeSent',\n                'params': {\n                    'requestId': 'req_1'\n                    # Missing 'request' key\n                }\n            },\n            {\n                'method': 'Network.responseReceived',\n                'params': {\n                    'request': {},  # Empty request object\n                    'requestId': 'req_2'\n                }\n            }\n        ]\n        tab._connection_handler.network_logs = test_logs\n\n        result = await tab.get_network_logs(filter='example')\n\n        # Should handle missing request data gracefully\n        assert result == []\n\n\nclass TestTabSaveBundle:\n    \"\"\"Tests for Tab.save_bundle() page bundle export.\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def _enable_page_events(self, tab):\n        \"\"\"Pre-enable page events so save_bundle skips enable/disable calls.\"\"\"\n        tab._page_events_enabled = True\n\n    def _make_frame_tree(self, frame_id='F1', page_url='https://example.com/',\n                          resources=None, child_frames=None):\n        tree = {\n            'frame': {\n                'id': frame_id,\n                'url': page_url,\n                'loaderId': 'L1',\n                'domainAndRegistry': 'example.com',\n                'securityOrigin': 'https://example.com',\n                'mimeType': 'text/html',\n                'secureContextType': 'Secure',\n                'crossOriginIsolatedContextType': 'NotIsolated',\n                'gatedAPIFeatures': [],\n            },\n            'resources': resources or [],\n        }\n        if child_frames:\n            tree['childFrames'] = child_frames\n        return tree\n\n    def _make_resource(self, url, rtype='Stylesheet', mime='text/css',\n                        failed=False, canceled=False):\n        res = {'url': url, 'type': rtype, 'mimeType': mime}\n        if failed:\n            res['failed'] = True\n        if canceled:\n            res['canceled'] = True\n        return res\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_invalid_extension(self, tab):\n        with pytest.raises(InvalidFileExtension, match=r'\\.zip'):\n            await tab.save_bundle('output.tar.gz')\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_separate_assets(self, tab, tmp_path):\n        page_url = 'https://example.com/'\n        css_url = 'https://example.com/style.css'\n        js_url = 'https://example.com/app.js'\n\n        resources = [\n            self._make_resource(css_url, 'Stylesheet', 'text/css'),\n            self._make_resource(js_url, 'Script', 'text/javascript'),\n        ]\n        frame_tree = self._make_frame_tree(resources=resources)\n\n        html_content = (\n            '<html><head>'\n            f'<link rel=\"stylesheet\" href=\"{css_url}\">'\n            f'<script src=\"{js_url}\"></script>'\n            '</head><body>Hello</body></html>'\n        )\n        css_content = 'body { color: red; }'\n        js_content = 'console.log(\"hi\");'\n\n        responses = [\n            {'result': {'frameTree': frame_tree}},\n            {'result': {'content': html_content, 'base64Encoded': False}},\n            {'result': {'content': css_content, 'base64Encoded': False}},\n            {'result': {'content': js_content, 'base64Encoded': False}},\n        ]\n        tab._connection_handler.execute_command = AsyncMock(side_effect=responses)\n\n        zip_path = tmp_path / 'bundle.zip'\n        await tab.save_bundle(str(zip_path))\n\n        assert zip_path.exists()\n        import zipfile\n        with zipfile.ZipFile(zip_path) as zf:\n            names = zf.namelist()\n            assert 'index.html' in names\n            assert any('style.css' in n for n in names)\n            assert any('app.js' in n for n in names)\n            index = zf.read('index.html').decode('utf-8')\n            assert 'assets/' in index\n            assert css_url not in index\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_inline_assets(self, tab, tmp_path):\n        page_url = 'https://example.com/'\n        css_url = 'https://example.com/style.css'\n        js_url = 'https://example.com/app.js'\n        img_url = 'https://example.com/logo.png'\n\n        resources = [\n            self._make_resource(css_url, 'Stylesheet', 'text/css'),\n            self._make_resource(js_url, 'Script', 'text/javascript'),\n            self._make_resource(img_url, 'Image', 'image/png'),\n        ]\n        frame_tree = self._make_frame_tree(resources=resources)\n\n        html_content = (\n            '<html><head>'\n            f'<link rel=\"stylesheet\" href=\"{css_url}\">'\n            f'<script src=\"{js_url}\"></script>'\n            '</head><body>'\n            f'<img src=\"{img_url}\">'\n            '</body></html>'\n        )\n        css_content = 'body { color: red; }'\n        js_content = 'console.log(\"hi\");'\n        img_b64 = base64.b64encode(b'\\x89PNG').decode()\n\n        responses = [\n            {'result': {'frameTree': frame_tree}},\n            {'result': {'content': html_content, 'base64Encoded': False}},\n            {'result': {'content': css_content, 'base64Encoded': False}},\n            {'result': {'content': js_content, 'base64Encoded': False}},\n            {'result': {'content': img_b64, 'base64Encoded': True}},\n        ]\n        tab._connection_handler.execute_command = AsyncMock(side_effect=responses)\n\n        zip_path = tmp_path / 'bundle.zip'\n        await tab.save_bundle(str(zip_path), inline_assets=True)\n\n        import zipfile\n        with zipfile.ZipFile(zip_path) as zf:\n            names = zf.namelist()\n            assert names == ['index.html']\n            index = zf.read('index.html').decode('utf-8')\n            assert '<style>' in index\n            assert '<script>' in index\n            assert 'data:image/png;base64,' in index\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_skips_failed_resources(self, tab, tmp_path):\n        page_url = 'https://example.com/'\n        resources = [\n            self._make_resource('https://example.com/ok.css', 'Stylesheet', 'text/css'),\n            self._make_resource('https://example.com/bad.css', 'Stylesheet', 'text/css',\n                                failed=True),\n        ]\n        frame_tree = self._make_frame_tree(resources=resources)\n\n        html = '<html><head></head><body></body></html>'\n        responses = [\n            {'result': {'frameTree': frame_tree}},\n            {'result': {'content': html, 'base64Encoded': False}},\n            {'result': {'content': 'body{}', 'base64Encoded': False}},\n        ]\n        tab._connection_handler.execute_command = AsyncMock(side_effect=responses)\n\n        zip_path = tmp_path / 'bundle.zip'\n        await tab.save_bundle(str(zip_path))\n\n        # Only 3 calls: getResourceTree, getResourceContent(doc), getResourceContent(ok.css)\n        assert tab._connection_handler.execute_command.call_count == 3\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_handles_fetch_exceptions(self, tab, tmp_path):\n        page_url = 'https://example.com/'\n        resources = [\n            self._make_resource('https://example.com/style.css', 'Stylesheet', 'text/css'),\n        ]\n        frame_tree = self._make_frame_tree(resources=resources)\n        html = '<html><body></body></html>'\n\n        responses = [\n            {'result': {'frameTree': frame_tree}},\n            {'result': {'content': html, 'base64Encoded': False}},\n            RuntimeError('fetch failed'),\n        ]\n        tab._connection_handler.execute_command = AsyncMock(side_effect=responses)\n\n        zip_path = tmp_path / 'bundle.zip'\n        await tab.save_bundle(str(zip_path))\n\n        import zipfile\n        with zipfile.ZipFile(zip_path) as zf:\n            assert 'index.html' in zf.namelist()\n            assert not any(n.startswith('assets/') for n in zf.namelist())\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_handles_cdp_error_responses(self, tab, tmp_path):\n        \"\"\"CDP returns {'error': ...} instead of {'result': ...} for some resources.\"\"\"\n        resources = [\n            self._make_resource('https://example.com/style.css', 'Stylesheet', 'text/css'),\n        ]\n        frame_tree = self._make_frame_tree(resources=resources)\n        html = '<html><body></body></html>'\n\n        responses = [\n            {'result': {'frameTree': frame_tree}},\n            {'result': {'content': html, 'base64Encoded': False}},\n            {'error': {'code': -32000, 'message': 'No resource with given URL'}},\n        ]\n        tab._connection_handler.execute_command = AsyncMock(side_effect=responses)\n\n        zip_path = tmp_path / 'bundle.zip'\n        await tab.save_bundle(str(zip_path))\n\n        import zipfile\n        with zipfile.ZipFile(zip_path) as zf:\n            assert 'index.html' in zf.namelist()\n            assert not any(n.startswith('assets/') for n in zf.namelist())\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_empty_resources(self, tab, tmp_path):\n        frame_tree = self._make_frame_tree(resources=[])\n        html = '<html><body>Hello</body></html>'\n\n        responses = [\n            {'result': {'frameTree': frame_tree}},\n            {'result': {'content': html, 'base64Encoded': False}},\n        ]\n        tab._connection_handler.execute_command = AsyncMock(side_effect=responses)\n\n        zip_path = tmp_path / 'bundle.zip'\n        await tab.save_bundle(str(zip_path))\n\n        import zipfile\n        with zipfile.ZipFile(zip_path) as zf:\n            assert zf.namelist() == ['index.html']\n            assert zf.read('index.html').decode() == html\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_base64_encoded_resource(self, tab, tmp_path):\n        page_url = 'https://example.com/'\n        img_url = 'https://example.com/image.png'\n        resources = [\n            self._make_resource(img_url, 'Image', 'image/png'),\n        ]\n        frame_tree = self._make_frame_tree(resources=resources)\n        html = f'<html><body><img src=\"{img_url}\"></body></html>'\n        img_bytes = b'\\x89PNG\\r\\n\\x1a\\n' + b'\\x00' * 16\n        img_b64 = base64.b64encode(img_bytes).decode()\n\n        responses = [\n            {'result': {'frameTree': frame_tree}},\n            {'result': {'content': html, 'base64Encoded': False}},\n            {'result': {'content': img_b64, 'base64Encoded': True}},\n        ]\n        tab._connection_handler.execute_command = AsyncMock(side_effect=responses)\n\n        zip_path = tmp_path / 'bundle.zip'\n        await tab.save_bundle(str(zip_path))\n\n        import zipfile\n        with zipfile.ZipFile(zip_path) as zf:\n            asset_names = [n for n in zf.namelist() if n.startswith('assets/')]\n            assert len(asset_names) == 1\n            assert zf.read(asset_names[0]) == img_bytes\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_css_url_rewriting(self, tab, tmp_path):\n        page_url = 'https://example.com/'\n        css_url = 'https://example.com/css/style.css'\n        font_url = 'https://example.com/css/font.woff2'\n\n        resources = [\n            self._make_resource(css_url, 'Stylesheet', 'text/css'),\n            self._make_resource(font_url, 'Font', 'font/woff2'),\n        ]\n        frame_tree = self._make_frame_tree(resources=resources)\n\n        html = f'<html><head><link rel=\"stylesheet\" href=\"{css_url}\"></head><body></body></html>'\n        css_content = 'body { font-family: url(\"font.woff2\"); }'\n        font_bytes = b'woff2data'\n\n        responses = [\n            {'result': {'frameTree': frame_tree}},\n            {'result': {'content': html, 'base64Encoded': False}},\n            {'result': {'content': css_content, 'base64Encoded': False}},\n            {'result': {'content': base64.b64encode(font_bytes).decode(), 'base64Encoded': True}},\n        ]\n        tab._connection_handler.execute_command = AsyncMock(side_effect=responses)\n\n        zip_path = tmp_path / 'bundle.zip'\n        await tab.save_bundle(str(zip_path))\n\n        import zipfile\n        with zipfile.ZipFile(zip_path) as zf:\n            css_files = [n for n in zf.namelist() if n.endswith('.css')]\n            assert len(css_files) == 1\n            css_data = zf.read(css_files[0]).decode('utf-8')\n            # CSS url() should reference the font's local filename\n            assert 'font.woff2' not in css_data or 'assets/' not in css_data\n            # The font.woff2 reference should have been rewritten\n            assert 'url(\"' in css_data\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_child_frames(self, tab, tmp_path):\n        child_resources = [\n            self._make_resource('https://example.com/child.css', 'Stylesheet', 'text/css'),\n        ]\n        child_frame_tree = self._make_frame_tree(\n            frame_id='F2',\n            page_url='https://example.com/child.html',\n            resources=child_resources,\n        )\n        parent_resources = [\n            self._make_resource('https://example.com/main.css', 'Stylesheet', 'text/css'),\n        ]\n        frame_tree = self._make_frame_tree(\n            resources=parent_resources,\n            child_frames=[child_frame_tree],\n        )\n\n        html = '<html><body></body></html>'\n        responses = [\n            {'result': {'frameTree': frame_tree}},\n            {'result': {'content': html, 'base64Encoded': False}},\n            {'result': {'content': 'body{}', 'base64Encoded': False}},\n            {'result': {'content': 'p{}', 'base64Encoded': False}},\n        ]\n        tab._connection_handler.execute_command = AsyncMock(side_effect=responses)\n\n        zip_path = tmp_path / 'bundle.zip'\n        await tab.save_bundle(str(zip_path))\n\n        import zipfile\n        with zipfile.ZipFile(zip_path) as zf:\n            asset_names = [n for n in zf.namelist() if n.startswith('assets/')]\n            assert len(asset_names) == 2\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_skips_data_urls(self, tab, tmp_path):\n        resources = [\n            self._make_resource('data:image/png;base64,abc', 'Image', 'image/png'),\n            self._make_resource('https://example.com/real.css', 'Stylesheet', 'text/css'),\n        ]\n        frame_tree = self._make_frame_tree(resources=resources)\n        html = '<html><body></body></html>'\n\n        responses = [\n            {'result': {'frameTree': frame_tree}},\n            {'result': {'content': html, 'base64Encoded': False}},\n            {'result': {'content': 'body{}', 'base64Encoded': False}},\n        ]\n        tab._connection_handler.execute_command = AsyncMock(side_effect=responses)\n\n        zip_path = tmp_path / 'bundle.zip'\n        await tab.save_bundle(str(zip_path))\n\n        # Only 3 calls: tree, doc content, real.css — data: URL was skipped\n        assert tab._connection_handler.execute_command.call_count == 3\n\n    def test_collect_frame_resources_recursive(self):\n        child = {\n            'frame': {'id': 'F2', 'url': 'https://example.com/child',\n                       'loaderId': 'L2', 'domainAndRegistry': '', 'securityOrigin': '',\n                       'mimeType': 'text/html', 'secureContextType': 'Secure',\n                       'crossOriginIsolatedContextType': 'NotIsolated',\n                       'gatedAPIFeatures': []},\n            'resources': [\n                {'url': 'https://example.com/c.css', 'type': 'Stylesheet', 'mimeType': 'text/css'},\n            ],\n        }\n        parent = {\n            'frame': {'id': 'F1', 'url': 'https://example.com/',\n                       'loaderId': 'L1', 'domainAndRegistry': '', 'securityOrigin': '',\n                       'mimeType': 'text/html', 'secureContextType': 'Secure',\n                       'crossOriginIsolatedContextType': 'NotIsolated',\n                       'gatedAPIFeatures': []},\n            'resources': [\n                {'url': 'https://example.com/p.css', 'type': 'Stylesheet', 'mimeType': 'text/css'},\n            ],\n            'childFrames': [child],\n        }\n        result = collect_frame_resources(parent)\n        assert len(result) == 2\n        assert result[0][0] == 'F1'\n        assert result[1][0] == 'F2'\n\n    def test_build_asset_filename(self):\n        name = build_asset_filename(\n            'https://example.com/css/style.css', 'text/css', 0\n        )\n        assert name == '0000_style.css'\n\n    def test_build_asset_filename_no_extension(self):\n        name = build_asset_filename(\n            'https://example.com/api/image', 'image/png', 5\n        )\n        assert name == '0005_image.png'\n\n    def test_build_asset_filename_no_path(self):\n        name = build_asset_filename(\n            'https://example.com/', 'text/css', 1\n        )\n        assert name == '0001_resource.css'\n\n    def test_rewrite_css_urls(self):\n        asset_map = {\n            'https://example.com/fonts/bold.woff2': (\n                '0001_bold.woff2', b'data', 'font/woff2', 'Font'\n            ),\n        }\n        css = 'body { font: url(\"https://example.com/fonts/bold.woff2\"); }'\n        result = rewrite_css_urls(\n            css, 'https://example.com/css/style.css', asset_map\n        )\n        assert '0001_bold.woff2' in result\n\n    def test_rewrite_css_urls_relative(self):\n        asset_map = {\n            'https://example.com/css/bg.png': (\n                '0002_bg.png', b'data', 'image/png', 'Image'\n            ),\n        }\n        css = 'div { background: url(\"bg.png\"); }'\n        result = rewrite_css_urls(\n            css, 'https://example.com/css/style.css', asset_map\n        )\n        assert '0002_bg.png' in result\n\n    def test_rewrite_css_urls_skips_data_uris(self):\n        css = 'div { background: url(\"data:image/png;base64,abc\"); }'\n        result = rewrite_css_urls(css, 'https://example.com/style.css', {})\n        assert 'data:image/png;base64,abc' in result\n\n    @pytest.mark.asyncio\n    async def test_save_bundle_js_fallback_when_resource_content_fails(self, tab, tmp_path):\n        \"\"\"When getResourceContent fails for the document, fall back to JS.\"\"\"\n        frame_tree = self._make_frame_tree(resources=[])\n        html = '<html><body>Fallback</body></html>'\n\n        call_count = 0\n\n        async def side_effect(*args, **kwargs):\n            nonlocal call_count\n            call_count += 1\n            if call_count == 1:\n                # getResourceTree succeeds\n                return {'result': {'frameTree': frame_tree}}\n            if call_count == 2:\n                # getResourceContent for document fails (no 'result' key)\n                return {'error': {'code': -32000, 'message': 'No resource'}}\n            if call_count == 3:\n                # execute_script fallback\n                return {'result': {'result': {'value': html}}}\n            return {}\n\n        tab._connection_handler.execute_command = AsyncMock(side_effect=side_effect)\n\n        zip_path = tmp_path / 'bundle.zip'\n        await tab.save_bundle(str(zip_path))\n\n        import zipfile\n        with zipfile.ZipFile(zip_path) as zf:\n            assert zf.read('index.html').decode() == html"
  },
  {
    "path": "tests/test_browser/test_har_recorder.py",
    "content": "\"\"\"Tests for pydoll.browser.requests.har_recorder module.\"\"\"\n\nimport json\nimport pytest\nimport pytest_asyncio\nfrom pathlib import Path\nfrom unittest.mock import AsyncMock, Mock, patch\n\nfrom pydoll.browser.requests.har_recorder import HarRecorder, HarCapture\nfrom pydoll.browser.requests.request import Request\nfrom pydoll.protocol.network.events import NetworkEvent\n\n\n@pytest_asyncio.fixture\nasync def mock_tab():\n    \"\"\"Create a mock Tab instance for testing.\"\"\"\n    tab = Mock()\n    tab.network_events_enabled = False\n    tab.enable_network_events = AsyncMock()\n    tab.disable_network_events = AsyncMock()\n    tab.on = AsyncMock(side_effect=lambda *a, **kw: len(tab.on.call_args_list))\n    tab.remove_callback = AsyncMock()\n    tab.clear_callbacks = AsyncMock()\n    tab._execute_command = AsyncMock(\n        return_value={'result': {'body': '', 'base64Encoded': False}}\n    )\n    return tab\n\n\n@pytest_asyncio.fixture\nasync def recorder(mock_tab):\n    \"\"\"Create a HarRecorder instance for testing.\"\"\"\n    return HarRecorder(mock_tab)\n\n\n@pytest_asyncio.fixture\nasync def request_instance(mock_tab):\n    \"\"\"Create a Request instance for testing.\"\"\"\n    return Request(mock_tab)\n\n\ndef _make_request_will_be_sent_event(\n    request_id='req-1',\n    url='https://example.com',\n    method='GET',\n    wall_time=1700000000.0,\n    resource_type='Document',\n    redirect_response=None,\n):\n    \"\"\"Helper to build a requestWillBeSent CDP event.\"\"\"\n    event = {\n        'method': NetworkEvent.REQUEST_WILL_BE_SENT,\n        'params': {\n            'requestId': request_id,\n            'request': {\n                'url': url,\n                'method': method,\n                'headers': {'User-Agent': 'TestBrowser'},\n            },\n            'wallTime': wall_time,\n            'timestamp': 12345.0,\n            'type': resource_type,\n            'loaderId': 'loader-1',\n            'documentURL': url,\n            'initiator': {'type': 'other'},\n            'redirectHasExtraInfo': False,\n        },\n    }\n    if redirect_response:\n        event['params']['redirectResponse'] = redirect_response\n    return event\n\n\ndef _make_request_extra_info_event(request_id='req-1'):\n    \"\"\"Helper to build a requestWillBeSentExtraInfo CDP event.\"\"\"\n    return {\n        'method': NetworkEvent.REQUEST_WILL_BE_SENT_EXTRA_INFO,\n        'params': {\n            'requestId': request_id,\n            'headers': {'Cookie': 'session=abc123'},\n            'associatedCookies': [],\n            'connectTiming': {'requestTime': 12345.0},\n        },\n    }\n\n\ndef _make_response_received_event(\n    request_id='req-1',\n    status=200,\n    status_text='OK',\n    mime_type='text/html',\n    protocol='h2',\n    timing=None,\n    remote_ip='93.184.216.34',\n):\n    \"\"\"Helper to build a responseReceived CDP event.\"\"\"\n    response = {\n        'url': 'https://example.com',\n        'status': status,\n        'statusText': status_text,\n        'headers': {'Content-Type': 'text/html'},\n        'mimeType': mime_type,\n        'charset': 'utf-8',\n        'connectionReused': False,\n        'connectionId': 42,\n        'encodedDataLength': 1234,\n        'securityState': 'secure',\n    }\n    if protocol:\n        response['protocol'] = protocol\n    if timing:\n        response['timing'] = timing\n    if remote_ip:\n        response['remoteIPAddress'] = remote_ip\n    return {\n        'method': NetworkEvent.RESPONSE_RECEIVED,\n        'params': {\n            'requestId': request_id,\n            'loaderId': 'loader-1',\n            'timestamp': 12346.0,\n            'type': 'Document',\n            'response': response,\n            'hasExtraInfo': True,\n        },\n    }\n\n\ndef _make_response_extra_info_event(request_id='req-1'):\n    \"\"\"Helper to build a responseReceivedExtraInfo CDP event.\"\"\"\n    return {\n        'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n        'params': {\n            'requestId': request_id,\n            'headers': {'Content-Type': 'text/html', 'Set-Cookie': 'id=val'},\n            'blockedCookies': [],\n            'resourceIPAddressSpace': 'Public',\n            'statusCode': 200,\n        },\n    }\n\n\ndef _make_data_received_event(request_id='req-1', encoded_data_length=500):\n    \"\"\"Helper to build a dataReceived CDP event.\"\"\"\n    return {\n        'method': NetworkEvent.DATA_RECEIVED,\n        'params': {\n            'requestId': request_id,\n            'timestamp': 12346.5,\n            'dataLength': encoded_data_length,\n            'encodedDataLength': encoded_data_length,\n        },\n    }\n\n\ndef _make_loading_finished_event(request_id='req-1', encoded_data_length=1234):\n    \"\"\"Helper to build a loadingFinished CDP event.\"\"\"\n    return {\n        'method': NetworkEvent.LOADING_FINISHED,\n        'params': {\n            'requestId': request_id,\n            'timestamp': 12347.0,\n            'encodedDataLength': float(encoded_data_length),\n        },\n    }\n\n\ndef _make_loading_failed_event(\n    request_id='req-1', error_text='net::ERR_FAILED', canceled=False\n):\n    \"\"\"Helper to build a loadingFailed CDP event.\"\"\"\n    return {\n        'method': NetworkEvent.LOADING_FAILED,\n        'params': {\n            'requestId': request_id,\n            'timestamp': 12347.0,\n            'type': 'Document',\n            'errorText': error_text,\n            'canceled': canceled,\n        },\n    }\n\n\nclass TestHarRecorderStart:\n    \"\"\"Test HarRecorder.start().\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_start_registers_seven_callbacks(self, recorder, mock_tab):\n        await recorder.start()\n        assert mock_tab.on.call_count == 7\n\n    @pytest.mark.asyncio\n    async def test_start_stores_callback_ids(self, recorder, mock_tab):\n        await recorder.start()\n        assert len(recorder._callback_ids) == 7\n\n    @pytest.mark.asyncio\n    async def test_start_enables_network_events_if_not_enabled(self, recorder, mock_tab):\n        mock_tab.network_events_enabled = False\n        await recorder.start()\n        mock_tab.enable_network_events.assert_called_once()\n        assert recorder._network_was_enabled is True\n\n    @pytest.mark.asyncio\n    async def test_start_skips_network_enable_if_already_enabled(self, recorder, mock_tab):\n        mock_tab.network_events_enabled = True\n        await recorder.start()\n        mock_tab.enable_network_events.assert_not_called()\n        assert recorder._network_was_enabled is False\n\n    @pytest.mark.asyncio\n    async def test_start_registers_correct_events(self, recorder, mock_tab):\n        await recorder.start()\n        registered_events = [call.args[0] for call in mock_tab.on.call_args_list]\n        assert NetworkEvent.REQUEST_WILL_BE_SENT in registered_events\n        assert NetworkEvent.REQUEST_WILL_BE_SENT_EXTRA_INFO in registered_events\n        assert NetworkEvent.RESPONSE_RECEIVED in registered_events\n        assert NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO in registered_events\n        assert NetworkEvent.DATA_RECEIVED in registered_events\n        assert NetworkEvent.LOADING_FINISHED in registered_events\n        assert NetworkEvent.LOADING_FAILED in registered_events\n\n    @pytest.mark.asyncio\n    async def test_start_sets_start_time(self, recorder, mock_tab):\n        assert recorder._start_time is None\n        await recorder.start()\n        assert recorder._start_time is not None\n\n\nclass TestHarRecorderStop:\n    \"\"\"Test HarRecorder.stop().\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_stop_removes_all_callbacks(self, recorder, mock_tab):\n        await recorder.start()\n        await recorder.stop()\n        assert mock_tab.remove_callback.call_count == 7\n\n    @pytest.mark.asyncio\n    async def test_stop_clears_callback_ids(self, recorder, mock_tab):\n        await recorder.start()\n        await recorder.stop()\n        assert recorder._callback_ids == []\n\n    @pytest.mark.asyncio\n    async def test_stop_disables_network_events_if_we_enabled(self, recorder, mock_tab):\n        mock_tab.network_events_enabled = False\n        await recorder.start()\n        await recorder.stop()\n        mock_tab.disable_network_events.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_stop_does_not_disable_network_events_if_not_ours(self, recorder, mock_tab):\n        mock_tab.network_events_enabled = True\n        await recorder.start()\n        await recorder.stop()\n        mock_tab.disable_network_events.assert_not_called()\n\n    @pytest.mark.asyncio\n    async def test_stop_flushes_pending_entries(self, recorder, mock_tab):\n        await recorder.start()\n        recorder._pending['req-1'] = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n        }\n        await recorder.stop()\n        assert len(recorder._entries) == 1\n        assert recorder._pending == {}\n\n\nclass TestHarRecorderEventHandlers:\n    \"\"\"Test individual event handler methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_request_will_be_sent_creates_pending(self, recorder):\n        event = _make_request_will_be_sent_event()\n        recorder._on_request_will_be_sent(event)\n        assert 'req-1' in recorder._pending\n        assert recorder._pending['req-1']['url'] == 'https://example.com'\n        assert recorder._pending['req-1']['method'] == 'GET'\n\n    @pytest.mark.asyncio\n    async def test_request_extra_info_merges_headers(self, recorder):\n        recorder._on_request_will_be_sent(_make_request_will_be_sent_event())\n        recorder._on_request_extra_info(_make_request_extra_info_event())\n        assert 'request_headers_extra' in recorder._pending['req-1']\n        assert recorder._pending['req-1']['request_headers_extra']['Cookie'] == 'session=abc123'\n\n    @pytest.mark.asyncio\n    async def test_request_extra_info_skips_unknown_request(self, recorder):\n        recorder._on_request_extra_info(_make_request_extra_info_event('unknown-req'))\n        assert 'unknown-req' not in recorder._pending\n\n    @pytest.mark.asyncio\n    async def test_response_received_stores_data(self, recorder):\n        recorder._on_request_will_be_sent(_make_request_will_be_sent_event())\n        recorder._on_response_received(_make_response_received_event())\n        pending = recorder._pending['req-1']\n        assert pending['status'] == 200\n        assert pending['status_text'] == 'OK'\n        assert pending['mime_type'] == 'text/html'\n        assert pending['protocol'] == 'h2'\n        assert pending['remote_ip'] == '93.184.216.34'\n\n    @pytest.mark.asyncio\n    async def test_response_received_skips_unknown_request(self, recorder):\n        recorder._on_response_received(_make_response_received_event('unknown-req'))\n        assert 'unknown-req' not in recorder._pending\n\n    @pytest.mark.asyncio\n    async def test_response_extra_info_merges_headers(self, recorder):\n        recorder._on_request_will_be_sent(_make_request_will_be_sent_event())\n        recorder._on_response_extra_info(_make_response_extra_info_event())\n        assert 'response_headers_extra' in recorder._pending['req-1']\n\n    @pytest.mark.asyncio\n    async def test_loading_finished_creates_entry(self, recorder, mock_tab):\n        recorder._on_request_will_be_sent(_make_request_will_be_sent_event())\n        recorder._on_response_received(_make_response_received_event())\n        recorder._on_loading_finished(_make_loading_finished_event())\n\n        # Wait for the background task to complete\n        if recorder._body_tasks:\n            import asyncio\n            await asyncio.gather(*recorder._body_tasks, return_exceptions=True)\n\n        assert len(recorder._entries) == 1\n        assert 'req-1' not in recorder._pending\n        entry = recorder._entries[0]\n        assert entry['request']['url'] == 'https://example.com'\n        assert entry['response']['status'] == 200\n\n    @pytest.mark.asyncio\n    async def test_loading_finished_skips_unknown_request(self, recorder):\n        recorder._on_loading_finished(_make_loading_finished_event('unknown-req'))\n        assert len(recorder._entries) == 0\n\n    @pytest.mark.asyncio\n    async def test_loading_failed_creates_entry(self, recorder):\n        recorder._on_request_will_be_sent(_make_request_will_be_sent_event())\n        recorder._on_loading_failed(_make_loading_failed_event())\n        assert len(recorder._entries) == 1\n        assert 'req-1' not in recorder._pending\n        entry = recorder._entries[0]\n        assert entry['response']['status'] == 0\n        assert entry['response']['statusText'] == 'net::ERR_FAILED'\n\n    @pytest.mark.asyncio\n    async def test_loading_failed_skips_unknown_request(self, recorder):\n        recorder._on_loading_failed(_make_loading_failed_event('unknown-req'))\n        assert len(recorder._entries) == 0\n\n    @pytest.mark.asyncio\n    async def test_redirect_handling(self, recorder, mock_tab):\n        redirect_response = {\n            'url': 'https://example.com',\n            'status': 301,\n            'statusText': 'Moved Permanently',\n            'headers': {'Location': 'https://www.example.com'},\n            'mimeType': 'text/html',\n            'charset': 'utf-8',\n            'connectionReused': False,\n            'connectionId': 42,\n            'encodedDataLength': 200,\n            'securityState': 'secure',\n        }\n        event1 = _make_request_will_be_sent_event(request_id='req-1')\n        recorder._on_request_will_be_sent(event1)\n\n        event2 = _make_request_will_be_sent_event(\n            request_id='req-1',\n            url='https://www.example.com',\n            redirect_response=redirect_response,\n        )\n        recorder._on_request_will_be_sent(event2)\n\n        # First entry is the redirect\n        assert len(recorder._entries) == 1\n        assert recorder._entries[0]['response']['status'] == 301\n\n        # req-1 still pending for the final URL\n        assert 'req-1' in recorder._pending\n        assert recorder._pending['req-1']['url'] == 'https://www.example.com'\n\n\nclass TestHarRecorderHelpers:\n    \"\"\"Test static helper methods.\"\"\"\n\n    def test_headers_dict_to_list(self):\n        headers = {'Content-Type': 'text/html', 'Accept': '*/*'}\n        result = HarRecorder._headers_dict_to_list(headers)\n        assert len(result) == 2\n        assert {'name': 'Content-Type', 'value': 'text/html'} in result\n        assert {'name': 'Accept', 'value': '*/*'} in result\n\n    def test_headers_dict_to_list_empty(self):\n        assert HarRecorder._headers_dict_to_list({}) == []\n\n    def test_parse_query_string(self):\n        url = 'https://example.com/search?q=test&page=1'\n        result = HarRecorder._parse_query_string(url)\n        assert len(result) == 2\n        names = [p['name'] for p in result]\n        assert 'q' in names\n        assert 'page' in names\n\n    def test_parse_query_string_no_query(self):\n        assert HarRecorder._parse_query_string('https://example.com') == []\n\n    def test_parse_query_string_empty_values(self):\n        url = 'https://example.com?flag='\n        result = HarRecorder._parse_query_string(url)\n        assert len(result) == 1\n        assert result[0]['name'] == 'flag'\n        assert result[0]['value'] == ''\n\n    def test_wall_time_to_iso(self):\n        result = HarRecorder._wall_time_to_iso(1700000000.0)\n        assert '2023-11-14' in result\n        assert '+00:00' in result or 'Z' in result\n\n    def test_wall_time_to_iso_zero(self):\n        result = HarRecorder._wall_time_to_iso(0)\n        # Should return current time ISO string\n        assert 'T' in result\n\n    def test_build_har_timings_none(self):\n        result = HarRecorder._build_har_timings(None)\n        assert result['blocked'] == -1\n        assert result['dns'] == -1\n        assert result['connect'] == -1\n        assert result['ssl'] == -1\n        assert result['send'] == 0\n        assert result['wait'] == 0\n        assert result['receive'] == 0\n\n    def test_build_har_timings_with_data(self):\n        timing = {\n            'requestTime': 12345.0,\n            'proxyStart': -1,\n            'proxyEnd': -1,\n            'dnsStart': 0.5,\n            'dnsEnd': 5.0,\n            'connectStart': 5.0,\n            'connectEnd': 50.0,\n            'sslStart': 10.0,\n            'sslEnd': 45.0,\n            'workerStart': -1,\n            'workerReady': -1,\n            'workerFetchStart': -1,\n            'workerRespondWithSettled': -1,\n            'sendStart': 50.0,\n            'sendEnd': 51.0,\n            'pushStart': 0,\n            'pushEnd': 0,\n            'receiveHeadersStart': 100.0,\n            'receiveHeadersEnd': 105.0,\n        }\n        result = HarRecorder._build_har_timings(timing)\n        assert result['dns'] == 4.5\n        assert result['connect'] == 45.0\n        assert result['ssl'] == 35.0\n        assert result['send'] == 1.0\n        assert result['wait'] == 49.0\n        # receive defaults to 0 when no receive_ms is provided\n        assert result['receive'] == 0\n\n    def test_build_har_timings_with_receive_ms(self):\n        timing = {\n            'requestTime': 12345.0,\n            'proxyStart': -1,\n            'proxyEnd': -1,\n            'dnsStart': 0.5,\n            'dnsEnd': 5.0,\n            'connectStart': 5.0,\n            'connectEnd': 50.0,\n            'sslStart': 10.0,\n            'sslEnd': 45.0,\n            'workerStart': -1,\n            'workerReady': -1,\n            'workerFetchStart': -1,\n            'workerRespondWithSettled': -1,\n            'sendStart': 50.0,\n            'sendEnd': 51.0,\n            'pushStart': 0,\n            'pushEnd': 0,\n            'receiveHeadersStart': 100.0,\n            'receiveHeadersEnd': 105.0,\n        }\n        # Providing receive_ms overrides any header-based calculation\n        result = HarRecorder._build_har_timings(timing, receive_ms=250.5)\n        assert result['receive'] == 250.5\n        assert result['dns'] == 4.5\n        assert result['send'] == 1.0\n\n    def test_build_har_timings_no_ssl(self):\n        timing = {\n            'requestTime': 12345.0,\n            'proxyStart': -1,\n            'proxyEnd': -1,\n            'dnsStart': -1,\n            'dnsEnd': -1,\n            'connectStart': -1,\n            'connectEnd': -1,\n            'sslStart': -1,\n            'sslEnd': -1,\n            'workerStart': -1,\n            'workerReady': -1,\n            'workerFetchStart': -1,\n            'workerRespondWithSettled': -1,\n            'sendStart': 10.0,\n            'sendEnd': 11.0,\n            'pushStart': 0,\n            'pushEnd': 0,\n            'receiveHeadersStart': 50.0,\n            'receiveHeadersEnd': 55.0,\n        }\n        result = HarRecorder._build_har_timings(timing, receive_ms=500.0)\n        assert result['dns'] == -1\n        assert result['connect'] == -1\n        assert result['ssl'] == -1\n        assert result['send'] == 1.0\n        assert result['wait'] == 39.0\n        assert result['receive'] == 500.0\n\n\nclass TestHarRecorderBuildEntry:\n    \"\"\"Test the entry building logic.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_build_entry_basic(self, recorder):\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {'User-Agent': 'Test'},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {'Content-Type': 'text/html'},\n            'mime_type': 'text/html',\n            'protocol': 'h2',\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['request']['method'] == 'GET'\n        assert entry['request']['url'] == 'https://example.com'\n        assert entry['response']['status'] == 200\n\n    @pytest.mark.asyncio\n    async def test_build_entry_with_post_data(self, recorder):\n        pending = {\n            'url': 'https://example.com/api',\n            'method': 'POST',\n            'request_headers': {'Content-Type': 'application/json'},\n            'post_data': '{\"key\": \"value\"}',\n            'wall_time': 1700000000.0,\n            'status': 201,\n            'status_text': 'Created',\n            'response_headers': {},\n            'mime_type': 'application/json',\n            'protocol': 'h2',\n        }\n        entry = recorder._build_entry(pending)\n        assert 'postData' in entry['request']\n        assert entry['request']['postData']['text'] == '{\"key\": \"value\"}'\n        assert entry['request']['postData']['mimeType'] == 'application/json'\n        assert entry['request']['bodySize'] == len('{\"key\": \"value\"}')\n\n    @pytest.mark.asyncio\n    async def test_build_entry_with_response_body(self, recorder):\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/html',\n            'protocol': 'h2',\n            'response_body': '<html></html>',\n            'response_body_base64': False,\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['response']['content']['text'] == '<html></html>'\n        assert 'encoding' not in entry['response']['content']\n\n    @pytest.mark.asyncio\n    async def test_build_entry_with_base64_body(self, recorder):\n        pending = {\n            'url': 'https://example.com/image.png',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'image/png',\n            'protocol': 'h2',\n            'response_body': 'iVBORw0KGgo=',\n            'response_body_base64': True,\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['response']['content']['encoding'] == 'base64'\n\n    @pytest.mark.asyncio\n    async def test_build_entry_with_server_ip(self, recorder):\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/html',\n            'remote_ip': '93.184.216.34',\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['serverIPAddress'] == '93.184.216.34'\n\n    @pytest.mark.asyncio\n    async def test_build_entry_with_resource_type(self, recorder):\n        pending = {\n            'url': 'https://example.com/style.css',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/css',\n            'resource_type': 'Stylesheet',\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['_resourceType'] == 'Stylesheet'\n\n    @pytest.mark.asyncio\n    async def test_build_entry_uses_extra_headers_when_available(self, recorder):\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {'User-Agent': 'original'},\n            'request_headers_extra': {'User-Agent': 'actual', 'Cookie': 'x=1'},\n            'response_headers': {'Content-Type': 'text/html'},\n            'response_headers_extra': {'Content-Type': 'text/html', 'Set-Cookie': 'y=2'},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'mime_type': 'text/html',\n        }\n        entry = recorder._build_entry(pending)\n        req_header_names = [h['name'] for h in entry['request']['headers']]\n        assert 'Cookie' in req_header_names\n        resp_header_names = [h['name'] for h in entry['response']['headers']]\n        assert 'Set-Cookie' in resp_header_names\n\n\nclass TestHarCapture:\n    \"\"\"Test HarCapture user-facing class.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_entries_returns_copy(self, recorder):\n        recorder._entries.append(\n            {\n                'startedDateTime': '2023-01-01T00:00:00+00:00',\n                'time': 100.0,\n                'request': {},\n                'response': {},\n                'timings': {},\n            }\n        )\n        recording = HarCapture(recorder)\n        entries = recording.entries\n        assert len(entries) == 1\n        entries.clear()\n        # Original entries should not be affected\n        assert len(recording.entries) == 1\n\n    @pytest.mark.asyncio\n    async def test_to_dict_structure(self, recorder):\n        recording = HarCapture(recorder)\n        har = recording.to_dict()\n        assert 'log' in har\n        assert har['log']['version'] == '1.2'\n        assert har['log']['creator']['name'] == 'pydoll'\n        assert isinstance(har['log']['pages'], list)\n        assert isinstance(har['log']['entries'], list)\n\n    @pytest.mark.asyncio\n    async def test_to_dict_includes_entries(self, recorder):\n        recorder._entries.append(\n            {\n                'startedDateTime': '2023-01-01T00:00:00+00:00',\n                'time': 100.0,\n                'request': {'method': 'GET', 'url': 'https://example.com'},\n                'response': {'status': 200},\n                'timings': {},\n            }\n        )\n        recording = HarCapture(recorder)\n        har = recording.to_dict()\n        assert len(har['log']['entries']) == 1\n\n    @pytest.mark.asyncio\n    async def test_save_writes_json_file(self, recorder, tmp_path):\n        recorder._entries.append(\n            {\n                'startedDateTime': '2023-01-01T00:00:00+00:00',\n                'time': 100.0,\n                'request': {'method': 'GET', 'url': 'https://example.com'},\n                'response': {'status': 200},\n                'timings': {},\n            }\n        )\n        recording = HarCapture(recorder)\n        file_path = tmp_path / 'test.har'\n        recording.save(file_path)\n\n        assert file_path.exists()\n        with open(file_path) as f:\n            data = json.load(f)\n        assert data['log']['version'] == '1.2'\n        assert len(data['log']['entries']) == 1\n\n    @pytest.mark.asyncio\n    async def test_save_with_string_path(self, recorder, tmp_path):\n        recording = HarCapture(recorder)\n        file_path = str(tmp_path / 'test.har')\n        recording.save(file_path)\n        assert Path(file_path).exists()\n\n    @pytest.mark.asyncio\n    async def test_save_creates_parent_directories(self, recorder, tmp_path):\n        recording = HarCapture(recorder)\n        file_path = tmp_path / 'sub' / 'dir' / 'test.har'\n        recording.save(file_path)\n        assert file_path.exists()\n\n\nclass TestRequestRecord:\n    \"\"\"Test Request.record() context manager.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_record_yields_har_recording(self, request_instance):\n        async with request_instance.record() as recording:\n            assert isinstance(recording, HarCapture)\n\n    @pytest.mark.asyncio\n    async def test_record_enables_network_events(self, request_instance, mock_tab):\n        mock_tab.network_events_enabled = False\n        async with request_instance.record():\n            pass\n        mock_tab.enable_network_events.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_record_registers_and_removes_callbacks(self, request_instance, mock_tab):\n        async with request_instance.record():\n            assert mock_tab.on.call_count == 7\n        assert mock_tab.remove_callback.call_count == 7\n\n    @pytest.mark.asyncio\n    async def test_record_cleans_up_on_exception(self, request_instance, mock_tab):\n        with pytest.raises(ValueError, match='test error'):\n            async with request_instance.record():\n                raise ValueError('test error')\n        # Cleanup should still happen\n        assert mock_tab.remove_callback.call_count == 7\n\n    @pytest.mark.asyncio\n    async def test_record_disables_network_events_if_enabled_by_recorder(\n        self, request_instance, mock_tab\n    ):\n        mock_tab.network_events_enabled = False\n        async with request_instance.record():\n            pass\n        mock_tab.disable_network_events.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_record_does_not_disable_network_events_if_already_enabled(\n        self, request_instance, mock_tab\n    ):\n        mock_tab.network_events_enabled = True\n        async with request_instance.record():\n            pass\n        mock_tab.disable_network_events.assert_not_called()\n\n\nclass TestResourceTypeFiltering:\n    \"\"\"Test resource type filtering in HarRecorder.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_filter_skips_non_matching_types(self, mock_tab):\n        from pydoll.protocol.network.types import ResourceType\n        recorder = HarRecorder(mock_tab, resource_types=[ResourceType.FETCH])\n        await recorder.start()\n\n        event = {\n            'params': {\n                'requestId': 'req1',\n                'request': {'url': 'https://example.com', 'method': 'GET', 'headers': {}},\n                'wallTime': 1000.0,\n                'timestamp': 100.0,\n                'type': 'Document',\n            }\n        }\n        recorder._on_request_will_be_sent(event)\n        assert 'req1' not in recorder._pending\n\n    @pytest.mark.asyncio\n    async def test_filter_accepts_matching_types(self, mock_tab):\n        from pydoll.protocol.network.types import ResourceType\n        recorder = HarRecorder(mock_tab, resource_types=[ResourceType.FETCH])\n        await recorder.start()\n\n        event = {\n            'params': {\n                'requestId': 'req1',\n                'request': {'url': 'https://example.com/api', 'method': 'GET', 'headers': {}},\n                'wallTime': 1000.0,\n                'timestamp': 100.0,\n                'type': 'Fetch',\n            }\n        }\n        recorder._on_request_will_be_sent(event)\n        assert 'req1' in recorder._pending\n\n    @pytest.mark.asyncio\n    async def test_no_filter_accepts_all(self, mock_tab):\n        recorder = HarRecorder(mock_tab)\n        await recorder.start()\n\n        event = {\n            'params': {\n                'requestId': 'req1',\n                'request': {'url': 'https://example.com', 'method': 'GET', 'headers': {}},\n                'wallTime': 1000.0,\n                'timestamp': 100.0,\n                'type': 'Document',\n            }\n        }\n        recorder._on_request_will_be_sent(event)\n        assert 'req1' in recorder._pending\n\n    @pytest.mark.asyncio\n    async def test_record_passes_resource_types(self, request_instance, mock_tab):\n        from pydoll.protocol.network.types import ResourceType\n        async with request_instance.record(\n            resource_types=[ResourceType.XHR, ResourceType.FETCH]\n        ) as capture:\n            assert isinstance(capture, HarCapture)\n\n\nclass TestHarRecorderFetchResponseBody:\n    \"\"\"Test response body fetching.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_fetch_response_body_success(self, recorder, mock_tab):\n        mock_tab._execute_command.return_value = {\n            'result': {'body': '<html>Hello</html>', 'base64Encoded': False}\n        }\n        body, is_base64 = await recorder._fetch_response_body('req-1')\n        assert body == '<html>Hello</html>'\n        assert is_base64 is False\n\n    @pytest.mark.asyncio\n    async def test_fetch_response_body_base64(self, recorder, mock_tab):\n        mock_tab._execute_command.return_value = {\n            'result': {'body': 'aW1hZ2VkYXRh', 'base64Encoded': True}\n        }\n        body, is_base64 = await recorder._fetch_response_body('req-1')\n        assert body == 'aW1hZ2VkYXRh'\n        assert is_base64 is True\n\n    @pytest.mark.asyncio\n    async def test_fetch_response_body_failure(self, recorder, mock_tab):\n        mock_tab._execute_command.side_effect = Exception('Network error')\n        body, is_base64 = await recorder._fetch_response_body('req-1')\n        assert body == ''\n        assert is_base64 is False\n\n\nclass TestHarRecorderEndToEnd:\n    \"\"\"End-to-end tests simulating full request lifecycle.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_full_request_lifecycle(self, recorder, mock_tab):\n        mock_tab._execute_command.return_value = {\n            'result': {'body': '<html>Test</html>', 'base64Encoded': False}\n        }\n\n        # Simulate a full request lifecycle\n        recorder._on_request_will_be_sent(_make_request_will_be_sent_event())\n        recorder._on_request_extra_info(_make_request_extra_info_event())\n        recorder._on_response_received(_make_response_received_event())\n        recorder._on_response_extra_info(_make_response_extra_info_event())\n        recorder._on_loading_finished(_make_loading_finished_event())\n\n        # Wait for async body fetch\n        import asyncio\n        if recorder._body_tasks:\n            await asyncio.gather(*recorder._body_tasks, return_exceptions=True)\n\n        assert len(recorder._entries) == 1\n        entry = recorder._entries[0]\n        assert entry['request']['method'] == 'GET'\n        assert entry['request']['url'] == 'https://example.com'\n        assert entry['response']['status'] == 200\n        assert entry['response']['content']['text'] == '<html>Test</html>'\n        assert entry['_resourceType'] == 'Document'\n        assert entry['serverIPAddress'] == '93.184.216.34'\n        # Extra headers should be preferred\n        req_headers = {h['name']: h['value'] for h in entry['request']['headers']}\n        assert 'Cookie' in req_headers\n\n    @pytest.mark.asyncio\n    async def test_multiple_concurrent_requests(self, recorder, mock_tab):\n        mock_tab._execute_command.return_value = {\n            'result': {'body': '', 'base64Encoded': False}\n        }\n\n        # Two concurrent requests\n        recorder._on_request_will_be_sent(\n            _make_request_will_be_sent_event('req-1', 'https://example.com/a')\n        )\n        recorder._on_request_will_be_sent(\n            _make_request_will_be_sent_event('req-2', 'https://example.com/b')\n        )\n        recorder._on_response_received(_make_response_received_event('req-1'))\n        recorder._on_response_received(_make_response_received_event('req-2'))\n        recorder._on_loading_finished(_make_loading_finished_event('req-1'))\n        recorder._on_loading_finished(_make_loading_finished_event('req-2'))\n\n        import asyncio\n        if recorder._body_tasks:\n            await asyncio.gather(*recorder._body_tasks, return_exceptions=True)\n\n        assert len(recorder._entries) == 2\n        urls = [e['request']['url'] for e in recorder._entries]\n        assert 'https://example.com/a' in urls\n        assert 'https://example.com/b' in urls\n\n\nclass TestHarRecorderCookieParsing:\n    \"\"\"Test cookie parsing from headers.\"\"\"\n\n    def test_parse_request_cookies(self):\n        headers = {'Cookie': 'session=abc123; user=john; theme=dark'}\n        result = HarRecorder._parse_request_cookies(headers)\n        assert len(result) == 3\n        names = [c['name'] for c in result]\n        assert 'session' in names\n        assert 'user' in names\n        assert 'theme' in names\n\n    def test_parse_request_cookies_empty(self):\n        assert HarRecorder._parse_request_cookies({}) == []\n\n    def test_parse_request_cookies_lowercase_header(self):\n        headers = {'cookie': 'token=xyz'}\n        result = HarRecorder._parse_request_cookies(headers)\n        assert len(result) == 1\n        assert result[0]['name'] == 'token'\n\n    def test_parse_response_cookies(self):\n        headers = {'Set-Cookie': 'id=val; Path=/; HttpOnly; Secure'}\n        result = HarRecorder._parse_response_cookies(headers)\n        assert len(result) == 1\n        assert result[0]['name'] == 'id'\n        assert result[0]['value'] == 'val'\n        assert result[0].get('httpOnly') is True\n        assert result[0].get('secure') is True\n        assert result[0].get('path') == '/'\n\n    def test_parse_response_cookies_multiple(self):\n        headers = {'Set-Cookie': 'a=1; Path=/\\nb=2; Domain=.example.com'}\n        result = HarRecorder._parse_response_cookies(headers)\n        assert len(result) == 2\n        names = [c['name'] for c in result]\n        assert 'a' in names\n        assert 'b' in names\n\n    def test_parse_response_cookies_empty(self):\n        assert HarRecorder._parse_response_cookies({}) == []\n\n    def test_parse_response_cookies_with_domain(self):\n        headers = {'Set-Cookie': 'sess=abc; Domain=.example.com; Path=/api'}\n        result = HarRecorder._parse_response_cookies(headers)\n        assert len(result) == 1\n        assert result[0].get('domain') == '.example.com'\n        assert result[0].get('path') == '/api'\n\n\nclass TestHarRecorderBodySizes:\n    \"\"\"Test correct body size calculations.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_response_body_size_uses_data_received_bytes(self, recorder):\n        \"\"\"bodySize should come from dataReceived chunks, not transfer_size.\"\"\"\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/html',\n            'response_body': '<html>Hello</html>',\n            'response_body_base64': False,\n            'body_bytes': 3200,\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['response']['bodySize'] == 3200\n\n    @pytest.mark.asyncio\n    async def test_response_body_size_unknown_returns_negative_one(self, recorder):\n        \"\"\"bodySize should be -1 when no dataReceived data is available.\"\"\"\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/html',\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['response']['bodySize'] == -1\n\n    @pytest.mark.asyncio\n    async def test_response_body_size_304_is_zero(self, recorder):\n        \"\"\"For 304 (cache hit), bodySize must be 0 per HAR spec.\"\"\"\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 304,\n            'status_text': 'Not Modified',\n            'response_headers': {},\n            'mime_type': 'text/html',\n            'body_bytes': 100,\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['response']['bodySize'] == 0\n\n    @pytest.mark.asyncio\n    async def test_content_size_base64_decoded(self, recorder):\n        import base64\n        original = b'binary data here'\n        b64_body = base64.b64encode(original).decode()\n        pending = {\n            'url': 'https://example.com/img.png',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'image/png',\n            'response_body': b64_body,\n            'response_body_base64': True,\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['response']['content']['size'] == len(original)\n        assert entry['response']['content']['encoding'] == 'base64'\n\n    @pytest.mark.asyncio\n    async def test_request_body_size_bytes(self, recorder):\n        pending = {\n            'url': 'https://example.com/api',\n            'method': 'POST',\n            'request_headers': {'Content-Type': 'application/json'},\n            'post_data': '{\"emoji\": \"\\u2764\"}',\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'application/json',\n        }\n        entry = recorder._build_entry(pending)\n        # UTF-8 encoded size, not len(str)\n        expected = len('{\"emoji\": \"\\u2764\"}'.encode('utf-8'))\n        assert entry['request']['bodySize'] == expected\n\n\nclass TestHarRecorderCacheField:\n    \"\"\"Test that entries include the cache field.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_entry_has_cache_field(self, recorder):\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/html',\n        }\n        entry = recorder._build_entry(pending)\n        assert 'cache' in entry\n        assert entry['cache'] == {}\n\n\nclass TestHarRecorderCookiesInEntries:\n    \"\"\"Test that cookies are populated from headers in entries.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_request_cookies_from_cookie_header(self, recorder):\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {'Cookie': 'session=abc; user=john'},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/html',\n        }\n        entry = recorder._build_entry(pending)\n        assert len(entry['request']['cookies']) == 2\n        names = [c['name'] for c in entry['request']['cookies']]\n        assert 'session' in names\n        assert 'user' in names\n\n    @pytest.mark.asyncio\n    async def test_response_cookies_from_set_cookie(self, recorder):\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {'Set-Cookie': 'id=val; HttpOnly'},\n            'mime_type': 'text/html',\n        }\n        entry = recorder._build_entry(pending)\n        assert len(entry['response']['cookies']) == 1\n        assert entry['response']['cookies'][0]['name'] == 'id'\n        assert entry['response']['cookies'][0].get('httpOnly') is True\n\n\nclass TestHarRecorderDataReceived:\n    \"\"\"Test Network.dataReceived handling for accurate bodySize.\"\"\"\n\n    def test_data_received_accumulates_bytes(self, recorder):\n        recorder._on_data_received(_make_data_received_event('req-1', 500))\n        recorder._on_data_received(_make_data_received_event('req-1', 300))\n        assert recorder._data_received_sizes['req-1'] == 800\n\n    def test_data_received_separate_requests(self, recorder):\n        recorder._on_data_received(_make_data_received_event('req-1', 500))\n        recorder._on_data_received(_make_data_received_event('req-2', 700))\n        assert recorder._data_received_sizes['req-1'] == 500\n        assert recorder._data_received_sizes['req-2'] == 700\n\n    @pytest.mark.asyncio\n    async def test_loading_finished_consumes_data_received(self, recorder, mock_tab):\n        recorder._on_request_will_be_sent(_make_request_will_be_sent_event())\n        recorder._on_response_received(_make_response_received_event())\n        recorder._on_data_received(_make_data_received_event('req-1', 1000))\n        recorder._on_data_received(_make_data_received_event('req-1', 500))\n        recorder._on_loading_finished(_make_loading_finished_event())\n\n        import asyncio\n        if recorder._body_tasks:\n            await asyncio.gather(*recorder._body_tasks, return_exceptions=True)\n\n        assert 'req-1' not in recorder._data_received_sizes\n        assert len(recorder._entries) == 1\n        assert recorder._entries[0]['response']['bodySize'] == 1500\n\n    def test_loading_failed_cleans_up_data_received(self, recorder):\n        recorder._on_request_will_be_sent(_make_request_will_be_sent_event())\n        recorder._on_data_received(_make_data_received_event('req-1', 200))\n        recorder._on_loading_failed(_make_loading_failed_event())\n        assert 'req-1' not in recorder._data_received_sizes\n\n\nclass TestHarRecorderExtraStatusCode:\n    \"\"\"Test that responseReceivedExtraInfo statusCode overrides responseReceived status.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_extra_status_code_overrides_response_status(self, recorder, mock_tab):\n        \"\"\"For cached requests, extraInfo statusCode (304) should win over responseReceived (200).\"\"\"\n        recorder._on_request_will_be_sent(_make_request_will_be_sent_event())\n        recorder._on_response_received(_make_response_received_event(status=200))\n\n        # extraInfo says the real status is 304\n        extra_event = {\n            'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n            'params': {\n                'requestId': 'req-1',\n                'headers': {'Content-Type': 'text/html'},\n                'blockedCookies': [],\n                'resourceIPAddressSpace': 'Public',\n                'statusCode': 304,\n            },\n        }\n        recorder._on_response_extra_info(extra_event)\n        recorder._on_loading_finished(_make_loading_finished_event())\n\n        import asyncio\n        if recorder._body_tasks:\n            await asyncio.gather(*recorder._body_tasks, return_exceptions=True)\n\n        assert len(recorder._entries) == 1\n        assert recorder._entries[0]['response']['status'] == 304\n        assert recorder._entries[0]['response']['bodySize'] == 0\n\n    @pytest.mark.asyncio\n    async def test_normal_status_when_no_extra(self, recorder, mock_tab):\n        recorder._on_request_will_be_sent(_make_request_will_be_sent_event())\n        recorder._on_response_received(_make_response_received_event(status=200))\n        recorder._on_loading_finished(_make_loading_finished_event())\n\n        import asyncio\n        if recorder._body_tasks:\n            await asyncio.gather(*recorder._body_tasks, return_exceptions=True)\n\n        assert recorder._entries[0]['response']['status'] == 200\n\n\nclass TestHarRecorderReceiveTiming:\n    \"\"\"Test that receive timing uses monotonic timestamps.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_receive_from_monotonic_timestamps(self, recorder, mock_tab):\n        \"\"\"receive = (loadingFinished.timestamp - responseReceived.timestamp) * 1000.\"\"\"\n        recorder._on_request_will_be_sent(_make_request_will_be_sent_event())\n        # responseReceived has timestamp=12346.0 (from helper)\n        recorder._on_response_received(_make_response_received_event())\n        # loadingFinished has timestamp=12347.0 (from helper)\n        recorder._on_loading_finished(_make_loading_finished_event())\n\n        import asyncio\n        if recorder._body_tasks:\n            await asyncio.gather(*recorder._body_tasks, return_exceptions=True)\n\n        entry = recorder._entries[0]\n        # (12347.0 - 12346.0) * 1000 = 1000ms\n        assert entry['timings']['receive'] == 1000.0\n\n    def test_receive_fallback_zero_without_timestamps(self, recorder):\n        \"\"\"When no timestamps available, receive should be 0.\"\"\"\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/html',\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['timings']['receive'] == 0\n\n\nclass TestHarRecorderEntryTimeSslExclusion:\n    \"\"\"Test that entry.time excludes ssl from sum (connect includes it).\"\"\"\n\n    def test_entry_time_excludes_ssl(self, recorder):\n        timing = {\n            'requestTime': 12345.0,\n            'proxyStart': -1,\n            'proxyEnd': -1,\n            'dnsStart': 0.5,\n            'dnsEnd': 5.0,\n            'connectStart': 5.0,\n            'connectEnd': 50.0,\n            'sslStart': 10.0,\n            'sslEnd': 45.0,\n            'workerStart': -1,\n            'workerReady': -1,\n            'workerFetchStart': -1,\n            'workerRespondWithSettled': -1,\n            'sendStart': 50.0,\n            'sendEnd': 51.0,\n            'pushStart': 0,\n            'pushEnd': 0,\n            'receiveHeadersStart': 100.0,\n            'receiveHeadersEnd': 105.0,\n        }\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/html',\n            'timing': timing,\n            'response_timestamp': 12346.0,\n            'finished_timestamp': 12346.5,\n        }\n        entry = recorder._build_entry(pending)\n        timings = entry['timings']\n        # ssl=35.0, connect=45.0 (connect includes ssl time)\n        # entry.time should NOT include ssl separately\n        expected = (\n            timings['blocked']\n            + timings['dns']\n            + timings['connect']\n            + timings['send']\n            + timings['wait']\n            + timings['receive']\n        )\n        assert entry['time'] == round(expected, 2)\n        # Verify ssl is NOT counted in total\n        assert timings['ssl'] == 35.0\n        assert timings['ssl'] not in (\n            entry['time'] - timings['blocked'] - timings['dns']\n            - timings['connect'] - timings['send'] - timings['wait']\n            - timings['receive'],\n        )\n\n\nclass TestHarRecorderEntryOrdering:\n    \"\"\"Test that entries are sorted by startedDateTime.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_entries_sorted_by_started_date_time(self, recorder):\n        recorder._entries.append({\n            'startedDateTime': '2023-11-14T12:00:02+00:00',\n            'time': 100.0,\n            'request': {'method': 'GET', 'url': 'https://example.com/second'},\n            'response': {'status': 200},\n            'cache': {},\n            'timings': {},\n        })\n        recorder._entries.append({\n            'startedDateTime': '2023-11-14T12:00:01+00:00',\n            'time': 50.0,\n            'request': {'method': 'GET', 'url': 'https://example.com/first'},\n            'response': {'status': 200},\n            'cache': {},\n            'timings': {},\n        })\n        recording = HarCapture(recorder)\n\n        # entries property should be sorted\n        entries = recording.entries\n        assert entries[0]['request']['url'] == 'https://example.com/first'\n        assert entries[1]['request']['url'] == 'https://example.com/second'\n\n        # to_dict() should also be sorted\n        har = recording.to_dict()\n        assert har['log']['entries'][0]['request']['url'] == 'https://example.com/first'\n        assert har['log']['entries'][1]['request']['url'] == 'https://example.com/second'\n\n\nclass TestHarRecorderBodySizeFallback:\n    \"\"\"Test bodySize fallback to content_size when body_bytes is 0.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_body_size_falls_back_to_content_size(self, recorder):\n        \"\"\"When body_bytes=0 but content exists (e.g. file://), use content_size.\"\"\"\n        pending = {\n            'url': 'file:///page.html',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/html',\n            'response_body': '<html>Hello World</html>',\n            'response_body_base64': False,\n            'body_bytes': 0,\n        }\n        entry = recorder._build_entry(pending)\n        expected_size = len('<html>Hello World</html>'.encode('utf-8'))\n        assert entry['response']['bodySize'] == expected_size\n        assert entry['response']['content']['size'] == expected_size\n\n    @pytest.mark.asyncio\n    async def test_body_size_negative_one_when_no_body_and_no_bytes(self, recorder):\n        \"\"\"When body_bytes=-1 and no content, bodySize should be -1.\"\"\"\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/html',\n            'body_bytes': -1,\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['response']['bodySize'] == -1\n\n\nclass TestHarRecorderHttpVersionNormalization:\n    \"\"\"Test httpVersion normalization for HAR compatibility.\"\"\"\n\n    def test_h2_stays_lowercase(self):\n        assert HarRecorder._normalize_http_version('h2') == 'h2'\n\n    def test_h3_stays_lowercase(self):\n        assert HarRecorder._normalize_http_version('h3') == 'h3'\n\n    def test_http_1_1_uppercased(self):\n        assert HarRecorder._normalize_http_version('http/1.1') == 'HTTP/1.1'\n\n    def test_http_1_0_uppercased(self):\n        assert HarRecorder._normalize_http_version('http/1.0') == 'HTTP/1.0'\n\n    def test_already_uppercase(self):\n        assert HarRecorder._normalize_http_version('HTTP/1.1') == 'HTTP/1.1'\n\n    def test_file_protocol_returns_empty(self):\n        assert HarRecorder._normalize_http_version('file') == ''\n\n    def test_empty_string(self):\n        assert HarRecorder._normalize_http_version('') == ''\n\n    def test_unknown_protocol_returns_empty(self):\n        assert HarRecorder._normalize_http_version('blob') == ''\n\n    def test_entry_uses_normalized_version(self, recorder):\n        \"\"\"Entry httpVersion should be normalized.\"\"\"\n        pending = {\n            'url': 'https://example.com',\n            'method': 'GET',\n            'request_headers': {},\n            'wall_time': 1700000000.0,\n            'status': 200,\n            'status_text': 'OK',\n            'response_headers': {},\n            'mime_type': 'text/html',\n            'protocol': 'http/1.1',\n        }\n        entry = recorder._build_entry(pending)\n        assert entry['request']['httpVersion'] == 'HTTP/1.1'\n        assert entry['response']['httpVersion'] == 'HTTP/1.1'\n"
  },
  {
    "path": "tests/test_browser/test_requests_request.py",
    "content": "\"\"\"\nTests for pydoll.browser.requests.request module.\n\"\"\"\n\nimport json\nimport pytest\nimport pytest_asyncio\nfrom unittest.mock import AsyncMock, Mock, patch\nfrom urllib.parse import urlencode\n\nfrom pydoll.browser.requests.request import Request\nfrom pydoll.browser.requests.response import Response\nfrom pydoll.exceptions import HTTPError\nfrom pydoll.protocol.fetch.types import HeaderEntry\nfrom pydoll.protocol.network.events import NetworkEvent\nfrom pydoll.protocol.network.types import CookieParam\n\n\n@pytest_asyncio.fixture\nasync def mock_tab():\n    \"\"\"Create a mock Tab instance for testing.\"\"\"\n    tab = Mock()\n    tab.network_events_enabled = False\n    tab.enable_network_events = AsyncMock()\n    tab.disable_network_events = AsyncMock()\n    tab.remove_callback = AsyncMock()\n    tab.on = AsyncMock(side_effect=lambda *a, **kw: len(tab.on.call_args_list))\n    tab._execute_command = AsyncMock()\n    return tab\n\n\n@pytest_asyncio.fixture\nasync def request_instance(mock_tab):\n    \"\"\"Create a Request instance for testing.\"\"\"\n    return Request(mock_tab)\n\n\nclass TestRequestInitialization:\n    \"\"\"Test Request class initialization.\"\"\"\n\n    def test_request_initialization(self, mock_tab):\n        \"\"\"Test Request initialization with tab.\"\"\"\n        request = Request(mock_tab)\n        \n        assert request.tab == mock_tab\n        assert request._network_events_enabled is False\n        assert request._requests_sent == []\n        assert request._requests_received == []\n\n    def test_request_initialization_preserves_tab_reference(self, mock_tab):\n        \"\"\"Test that Request maintains reference to provided tab.\"\"\"\n        request = Request(mock_tab)\n        assert request.tab is mock_tab\n\n\nclass TestRequestMethods:\n    \"\"\"Test HTTP method convenience functions.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_method(self, request_instance):\n        \"\"\"Test GET request method.\"\"\"\n        with patch.object(request_instance, 'request', new_callable=AsyncMock) as mock_request:\n            mock_request.return_value = Mock()\n            \n            await request_instance.get('https://example.com', params={'q': 'test'})\n            \n            mock_request.assert_called_once_with(\n                'GET', 'https://example.com', params={'q': 'test'}\n            )\n\n    @pytest.mark.asyncio\n    async def test_post_method(self, request_instance):\n        \"\"\"Test POST request method.\"\"\"\n        with patch.object(request_instance, 'request', new_callable=AsyncMock) as mock_request:\n            mock_request.return_value = Mock()\n            \n            await request_instance.post(\n                'https://example.com', \n                data={'key': 'value'}, \n                json={'json_key': 'json_value'}\n            )\n            \n            mock_request.assert_called_once_with(\n                'POST', \n                'https://example.com', \n                data={'key': 'value'}, \n                json={'json_key': 'json_value'}\n            )\n\n    @pytest.mark.asyncio\n    async def test_put_method(self, request_instance):\n        \"\"\"Test PUT request method.\"\"\"\n        with patch.object(request_instance, 'request', new_callable=AsyncMock) as mock_request:\n            mock_request.return_value = Mock()\n            \n            await request_instance.put('https://example.com', json={'update': 'data'})\n            \n            mock_request.assert_called_once_with(\n                'PUT', 'https://example.com', data=None, json={'update': 'data'}\n            )\n\n    @pytest.mark.asyncio\n    async def test_patch_method(self, request_instance):\n        \"\"\"Test PATCH request method.\"\"\"\n        with patch.object(request_instance, 'request', new_callable=AsyncMock) as mock_request:\n            mock_request.return_value = Mock()\n            \n            await request_instance.patch('https://example.com', data='patch_data')\n            \n            mock_request.assert_called_once_with(\n                'PATCH', 'https://example.com', data='patch_data', json=None\n            )\n\n    @pytest.mark.asyncio\n    async def test_delete_method(self, request_instance):\n        \"\"\"Test DELETE request method.\"\"\"\n        with patch.object(request_instance, 'request', new_callable=AsyncMock) as mock_request:\n            mock_request.return_value = Mock()\n            \n            await request_instance.delete('https://example.com')\n            \n            mock_request.assert_called_once_with('DELETE', 'https://example.com')\n\n    @pytest.mark.asyncio\n    async def test_head_method(self, request_instance):\n        \"\"\"Test HEAD request method.\"\"\"\n        with patch.object(request_instance, 'request', new_callable=AsyncMock) as mock_request:\n            mock_request.return_value = Mock()\n            \n            await request_instance.head('https://example.com')\n            \n            mock_request.assert_called_once_with('HEAD', 'https://example.com')\n\n    @pytest.mark.asyncio\n    async def test_options_method(self, request_instance):\n        \"\"\"Test OPTIONS request method.\"\"\"\n        with patch.object(request_instance, 'request', new_callable=AsyncMock) as mock_request:\n            mock_request.return_value = Mock()\n            \n            await request_instance.options('https://example.com')\n            \n            mock_request.assert_called_once_with('OPTIONS', 'https://example.com')\n\n\nclass TestRequestMainMethod:\n    \"\"\"Test main request method functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_request_success_flow(self, request_instance, mock_tab):\n        \"\"\"Test successful request execution flow.\"\"\"\n        # Mock execute_command response\n        mock_result = {\n            'result': {\n                'result': {\n                    'value': {\n                        'status': 200,\n                        'content': [72, 101, 108, 108, 111],  # \"Hello\" as bytes\n                        'text': 'Hello',\n                        'json': {'message': 'success'},\n                        'url': 'https://example.com'\n                    }\n                }\n            }\n        }\n        mock_tab._execute_command.return_value = mock_result\n        \n        # Mock helper methods\n        with patch.object(request_instance, '_extract_received_headers') as mock_extract_headers, \\\n             patch.object(request_instance, '_extract_sent_headers') as mock_extract_sent, \\\n             patch.object(request_instance, '_extract_set_cookies') as mock_extract_cookies:\n            \n            mock_extract_headers.return_value = [HeaderEntry(name='Content-Type', value='application/json')]\n            mock_extract_sent.return_value = [HeaderEntry(name='User-Agent', value='Test-Agent')]\n            mock_extract_cookies.return_value = [CookieParam(name='session', value='abc123')]\n            \n            response = await request_instance.request('GET', 'https://example.com')\n            \n            assert isinstance(response, Response)\n            assert response.status_code == 200\n            assert response.text == 'Hello'\n            assert response.json() == {'message': 'success'}\n            assert response.url == 'https://example.com'\n\n    @pytest.mark.asyncio\n    async def test_request_with_params(self, request_instance):\n        \"\"\"Test request with query parameters.\"\"\"\n        with patch.object(request_instance, '_build_url_with_params') as mock_build_url, \\\n             patch.object(request_instance, '_execute_fetch_request') as mock_execute, \\\n             patch.object(request_instance, '_extract_received_headers') as mock_headers, \\\n             patch.object(request_instance, '_extract_sent_headers') as mock_sent, \\\n             patch.object(request_instance, '_extract_set_cookies') as mock_cookies, \\\n             patch.object(request_instance, '_build_response') as mock_build_response, \\\n             patch.object(request_instance, '_clear_callbacks') as mock_clear:\n            \n            mock_build_url.return_value = 'https://example.com?q=test'\n            mock_execute.return_value = {'result': {'result': {'value': {}}}}\n            mock_headers.return_value = []\n            mock_sent.return_value = []\n            mock_cookies.return_value = []\n            mock_build_response.return_value = Mock()\n            \n            await request_instance.request('GET', 'https://example.com', params={'q': 'test'})\n            \n            mock_build_url.assert_called_once_with('https://example.com', {'q': 'test'})\n\n    @pytest.mark.asyncio\n    async def test_request_with_json_data(self, request_instance):\n        \"\"\"Test request with JSON data.\"\"\"\n        with patch.object(request_instance, '_build_request_options') as mock_build_options, \\\n             patch.object(request_instance, '_execute_fetch_request') as mock_execute, \\\n             patch.object(request_instance, '_extract_received_headers') as mock_headers, \\\n             patch.object(request_instance, '_extract_sent_headers') as mock_sent, \\\n             patch.object(request_instance, '_extract_set_cookies') as mock_cookies, \\\n             patch.object(request_instance, '_build_response') as mock_build_response, \\\n             patch.object(request_instance, '_clear_callbacks') as mock_clear:\n            \n            mock_execute.return_value = {'result': {'result': {'value': {}}}}\n            mock_headers.return_value = []\n            mock_sent.return_value = []\n            mock_cookies.return_value = []\n            mock_build_response.return_value = Mock()\n            \n            json_data = {'key': 'value'}\n            await request_instance.request('POST', 'https://example.com', json=json_data)\n            \n            mock_build_options.assert_called_once_with(\n                'POST', None, json_data, None\n            )\n\n    @pytest.mark.asyncio\n    async def test_request_failure_raises_http_error(self, request_instance, mock_tab):\n        \"\"\"Test that request failures raise HTTPError.\"\"\"\n        mock_tab._execute_command.side_effect = Exception(\"Network error\")\n        \n        with pytest.raises(HTTPError, match=\"Request failed: Network error\"):\n            await request_instance.request('GET', 'https://example.com')\n\n    @pytest.mark.asyncio\n    async def test_request_always_clears_callbacks(self, request_instance, mock_tab):\n        \"\"\"Test that callbacks are always cleared, even on error.\"\"\"\n        mock_tab._execute_command.side_effect = Exception(\"Network error\")\n        \n        with patch.object(request_instance, '_clear_callbacks') as mock_clear:\n            with pytest.raises(HTTPError):\n                await request_instance.request('GET', 'https://example.com')\n            \n            mock_clear.assert_called_once()\n\n\nclass TestRequestHelperMethods:\n    \"\"\"Test Request helper methods.\"\"\"\n\n    def test_build_url_with_params_no_params(self, request_instance):\n        \"\"\"Test URL building without parameters.\"\"\"\n        url = 'https://example.com'\n        result = request_instance._build_url_with_params(url, None)\n        assert result == url\n\n    def test_build_url_with_params_simple(self, request_instance):\n        \"\"\"Test URL building with simple parameters.\"\"\"\n        url = 'https://example.com'\n        params = {'q': 'test', 'page': '1'}\n        result = request_instance._build_url_with_params(url, params)\n        \n        assert 'https://example.com?' in result\n        assert 'q=test' in result\n        assert 'page=1' in result\n\n    def test_build_url_with_params_existing_query(self, request_instance):\n        \"\"\"Test URL building with existing query string.\"\"\"\n        url = 'https://example.com?existing=param'\n        params = {'new': 'value'}\n        result = request_instance._build_url_with_params(url, params)\n        \n        assert 'existing=param' in result\n        assert 'new=value' in result\n\n    def test_build_request_options_basic(self, request_instance):\n        \"\"\"Test basic request options building.\"\"\"\n        options = request_instance._build_request_options(\n            'GET', None, None, None\n        )\n        \n        assert options['method'] == 'GET'\n        assert options['headers'] == {}\n\n    def test_build_request_options_with_headers(self, request_instance):\n        \"\"\"Test request options building with headers.\"\"\"\n        headers = [HeaderEntry(name='Authorization', value='Bearer token')]\n        \n        with patch.object(request_instance, '_convert_header_entries_to_dict') as mock_convert:\n            mock_convert.return_value = {'Authorization': 'Bearer token'}\n            \n            options = request_instance._build_request_options(\n                'POST', headers, None, None\n            )\n            \n            assert options['headers'] == {'Authorization': 'Bearer token'}\n            mock_convert.assert_called_once_with(headers)\n\n    def test_handle_json_options(self, request_instance):\n        \"\"\"Test JSON data handling.\"\"\"\n        options = {'headers': {}}\n        json_data = {'key': 'value'}\n        \n        request_instance._handle_json_options(options, json_data)\n        \n        assert options['body'] == json.dumps(json_data)\n        assert options['headers']['Content-Type'] == 'application/json'\n\n    def test_handle_data_options_form_data(self, request_instance):\n        \"\"\"Test form data handling.\"\"\"\n        options = {'headers': {}}\n        data = {'key': 'value', 'key2': 'value2'}\n        \n        request_instance._handle_data_options(options, data)\n        \n        assert options['body'] == urlencode(data, doseq=True)\n        assert options['headers']['Content-Type'] == 'application/x-www-form-urlencoded'\n\n    def test_handle_data_options_raw_data(self, request_instance):\n        \"\"\"Test raw data handling.\"\"\"\n        options = {'headers': {}}\n        data = 'raw string data'\n        \n        request_instance._handle_data_options(options, data)\n        \n        assert options['body'] == data\n        assert 'Content-Type' not in options['headers']\n\n    def test_convert_header_entries_to_dict(self, request_instance):\n        \"\"\"Test header entries conversion to dictionary.\"\"\"\n        headers = [\n            HeaderEntry(name='Content-Type', value='application/json'),\n            HeaderEntry(name='Authorization', value='Bearer token')\n        ]\n        \n        result = request_instance._convert_header_entries_to_dict(headers)\n        \n        expected = {\n            'Content-Type': 'application/json',\n            'Authorization': 'Bearer token'\n        }\n        assert result == expected\n\n    def test_convert_dict_to_header_entries(self, request_instance):\n        \"\"\"Test dictionary conversion to header entries.\"\"\"\n        headers_dict = {\n            'Content-Type': 'application/json',\n            'Authorization': 'Bearer token'\n        }\n        \n        result = request_instance._convert_dict_to_header_entries(headers_dict)\n        \n        assert len(result) == 2\n        # Check that each result is a dictionary with the expected keys\n        for header in result:\n            assert 'name' in header\n            assert 'value' in header\n        assert {header['name']: header['value'] for header in result} == headers_dict\n\n\nclass TestRequestCallbackManagement:\n    \"\"\"Test callback registration and management.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_register_callbacks_enables_network_events(self, request_instance, mock_tab):\n        \"\"\"Test that registering callbacks enables network events.\"\"\"\n        mock_tab.network_events_enabled = False\n        \n        await request_instance._register_callbacks()\n        \n        mock_tab.enable_network_events.assert_called_once()\n        assert request_instance._network_events_enabled is True\n\n    @pytest.mark.asyncio\n    async def test_register_callbacks_skips_if_already_enabled(self, request_instance, mock_tab):\n        \"\"\"Test that network events are not re-enabled if already active.\"\"\"\n        mock_tab.network_events_enabled = True\n        \n        await request_instance._register_callbacks()\n        \n        mock_tab.enable_network_events.assert_not_called()\n        assert request_instance._network_events_enabled is False\n\n    @pytest.mark.asyncio\n    async def test_register_callbacks_subscribes_to_events(self, request_instance, mock_tab):\n        \"\"\"Test that all required network events are subscribed to.\"\"\"\n        await request_instance._register_callbacks()\n        \n        expected_events = [\n            NetworkEvent.REQUEST_WILL_BE_SENT,\n            NetworkEvent.REQUEST_WILL_BE_SENT_EXTRA_INFO,\n            NetworkEvent.RESPONSE_RECEIVED,\n            NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO\n        ]\n        \n        assert mock_tab.on.call_count == len(expected_events)\n        called_events = [call[0][0] for call in mock_tab.on.call_args_list]\n        \n        for event in expected_events:\n            assert event in called_events\n\n    @pytest.mark.asyncio\n    async def test_clear_callbacks_disables_network_events(self, request_instance, mock_tab):\n        \"\"\"Test that clearing callbacks disables network events if they were enabled.\"\"\"\n        request_instance._network_events_enabled = True\n        request_instance._callback_ids = [10, 11, 12, 13]\n\n        await request_instance._clear_callbacks()\n\n        mock_tab.disable_network_events.assert_called_once()\n        assert mock_tab.remove_callback.call_count == 4\n        assert request_instance._network_events_enabled is False\n        assert request_instance._callback_ids == []\n\n    @pytest.mark.asyncio\n    async def test_clear_callbacks_skips_disable_if_not_enabled(self, request_instance, mock_tab):\n        \"\"\"Test that network events are not disabled if not enabled by request.\"\"\"\n        request_instance._network_events_enabled = False\n        request_instance._callback_ids = [10, 11]\n\n        await request_instance._clear_callbacks()\n\n        mock_tab.disable_network_events.assert_not_called()\n        assert mock_tab.remove_callback.call_count == 2\n        assert request_instance._callback_ids == []\n\n\nclass TestRequestCookieExtraction:\n    \"\"\"Test cookie extraction functionality.\"\"\"\n\n    def test_parse_cookie_line_valid(self, request_instance):\n        \"\"\"Test parsing valid cookie line.\"\"\"\n        line = 'session_id=abc123; Path=/; HttpOnly'\n        \n        result = request_instance._parse_cookie_line(line)\n        \n        assert result is not None\n        assert result['name'] == 'session_id'\n        assert result['value'] == 'abc123'\n\n    def test_parse_cookie_line_invalid(self, request_instance):\n        \"\"\"Test parsing invalid cookie line.\"\"\"\n        line = 'invalid_cookie_without_equals'\n        \n        result = request_instance._parse_cookie_line(line)\n        \n        assert result is None\n\n    def test_parse_cookie_line_with_complex_value(self, request_instance):\n        \"\"\"Test parsing cookie with complex value.\"\"\"\n        line = 'complex=value=with=equals; Secure'\n        \n        result = request_instance._parse_cookie_line(line)\n        \n        assert result is not None\n        assert result['name'] == 'complex'\n        assert result['value'] == 'value=with=equals'\n\n    def test_add_unique_cookies_no_duplicates(self, request_instance):\n        \"\"\"Test adding unique cookies without duplicates.\"\"\"\n        existing_cookies = [CookieParam(name='existing', value='value1')]\n        new_cookies = [\n            CookieParam(name='new', value='value2'),\n            CookieParam(name='existing', value='value1')  # Duplicate\n        ]\n        \n        request_instance._add_unique_cookies(existing_cookies, new_cookies)\n        \n        assert len(existing_cookies) == 2\n        cookie_names = [cookie['name'] for cookie in existing_cookies]\n        assert 'existing' in cookie_names\n        assert 'new' in cookie_names\n\n    def test_parse_set_cookie_header_multiline(self, request_instance):\n        \"\"\"Test parsing multi-line Set-Cookie header.\"\"\"\n        header = 'cookie1=value1; Path=/\\ncookie2=value2; Secure'\n        \n        result = request_instance._parse_set_cookie_header(header)\n        \n        assert len(result) == 2\n        assert result[0]['name'] == 'cookie1'\n        assert result[1]['name'] == 'cookie2'\n\n\nclass TestRequestEdgeCases:\n    \"\"\"Test edge cases and error conditions.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_request_with_empty_url(self, request_instance):\n        \"\"\"Test request with empty URL.\"\"\"\n        with patch.object(request_instance, '_execute_fetch_request') as mock_execute, \\\n             patch.object(request_instance, '_extract_received_headers') as mock_headers, \\\n             patch.object(request_instance, '_extract_sent_headers') as mock_sent, \\\n             patch.object(request_instance, '_extract_set_cookies') as mock_cookies, \\\n             patch.object(request_instance, '_build_response') as mock_build_response, \\\n             patch.object(request_instance, '_clear_callbacks') as mock_clear:\n            \n            mock_execute.return_value = {'result': {'result': {'value': {}}}}\n            mock_headers.return_value = []\n            mock_sent.return_value = []\n            mock_cookies.return_value = []\n            mock_build_response.return_value = Mock()\n            \n            await request_instance.request('GET', '')\n            \n            mock_execute.assert_called_once()\n\n    def test_build_url_with_special_characters(self, request_instance):\n        \"\"\"Test URL building with special characters in parameters.\"\"\"\n        url = 'https://example.com'\n        params = {'q': 'hello world', 'special': 'value&with=chars'}\n        \n        result = request_instance._build_url_with_params(url, params)\n        \n        assert 'hello+world' in result or 'hello%20world' in result\n        assert 'value%26with%3Dchars' in result\n\n    def test_handle_data_options_with_bytes(self, request_instance):\n        \"\"\"Test handling raw bytes data.\"\"\"\n        options = {'headers': {}}\n        data = b'binary data'\n        \n        request_instance._handle_data_options(options, data)\n        \n        assert options['body'] == data\n        assert 'Content-Type' not in options['headers']\n\n    def test_convert_header_entries_empty_list(self, request_instance):\n        \"\"\"Test converting empty header entries list.\"\"\"\n        result = request_instance._convert_header_entries_to_dict([])\n        assert result == {}\n\n    def test_convert_dict_to_header_entries_empty_dict(self, request_instance):\n        \"\"\"Test converting empty dictionary to header entries.\"\"\"\n        result = request_instance._convert_dict_to_header_entries({})\n        assert result == []\n\n\nclass TestRequestHeaderExtraction:\n    \"\"\"Test header extraction methods from network events.\"\"\"\n\n    def test_extract_received_headers(self, request_instance):\n        \"\"\"Test _extract_received_headers method.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock network events with response headers\n        mock_response_event = {\n            'method': NetworkEvent.RESPONSE_RECEIVED,\n            'params': {\n                'response': {\n                    'headers': {\n                        'Content-Type': 'application/json',\n                        'Content-Length': '100',\n                        'Server': 'nginx/1.18.0'\n                    }\n                }\n            }\n        }\n        \n        mock_response_extra_event = {\n            'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n            'params': {\n                'blockedCookies': [],\n                'headers': {\n                    'Set-Cookie': 'session=abc123; Path=/',\n                    'X-Custom-Header': 'custom-value'\n                }\n            }\n        }\n        \n        # Set up mock events\n        request_instance._requests_received = [mock_response_event, mock_response_extra_event]\n        \n        # Extract headers\n        headers = request_instance._extract_received_headers()\n        \n        # Verify headers were extracted\n        assert len(headers) >= 3  # At least Content-Type, Content-Length, Server\n        header_dict = {h['name']: h['value'] for h in headers}\n        \n        assert 'Content-Type' in header_dict\n        assert header_dict['Content-Type'] == 'application/json'\n        assert 'Content-Length' in header_dict\n        assert header_dict['Content-Length'] == '100'\n        assert 'Server' in header_dict\n        assert header_dict['Server'] == 'nginx/1.18.0'\n\n    def test_extract_sent_headers(self, request_instance):\n        \"\"\"Test _extract_sent_headers method.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock network events with request headers\n        mock_request_event = {\n            'method': NetworkEvent.REQUEST_WILL_BE_SENT,\n            'params': {\n                'request': {\n                    'headers': {\n                        'User-Agent': 'PyDoll/1.0',\n                        'Accept': 'application/json',\n                        'Authorization': 'Bearer token123'\n                    }\n                }\n            }\n        }\n        \n        mock_request_extra_event = {\n            'method': NetworkEvent.REQUEST_WILL_BE_SENT_EXTRA_INFO,\n            'params': {\n                'associatedCookies': [],\n                'headers': {\n                    'X-Forwarded-For': '192.168.1.1',\n                    'X-Custom-Request': 'test-value'\n                }\n            }\n        }\n        \n        # Set up mock events\n        request_instance._requests_sent = [mock_request_event, mock_request_extra_event]\n        \n        # Extract headers\n        headers = request_instance._extract_sent_headers()\n        \n        # Verify headers were extracted\n        assert len(headers) >= 3  # At least User-Agent, Accept, Authorization\n        header_dict = {h['name']: h['value'] for h in headers}\n        \n        assert 'User-Agent' in header_dict\n        assert header_dict['User-Agent'] == 'PyDoll/1.0'\n        assert 'Accept' in header_dict\n        assert header_dict['Accept'] == 'application/json'\n        assert 'Authorization' in header_dict\n        assert header_dict['Authorization'] == 'Bearer token123'\n\n    def test_extract_headers_from_events_with_response_events(self, request_instance):\n        \"\"\"Test _extract_headers_from_events with response events.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock response events\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED,\n                'params': {\n                    'response': {\n                        'headers': {\n                            'Content-Type': 'text/html',\n                            'Cache-Control': 'no-cache'\n                        }\n                    }\n                }\n            },\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'blockedCookies': [],\n                    'headers': {\n                        'X-Frame-Options': 'DENY',\n                        'Strict-Transport-Security': 'max-age=31536000'\n                    }\n                }\n            }\n        ]\n        \n        # Define extractors for response events\n        event_extractors = {\n            'response': request_instance._extract_response_received_headers,\n            'blockedCookies': request_instance._extract_response_received_extra_info_headers,\n        }\n        \n        # Extract headers from events\n        headers = request_instance._extract_headers_from_events(events, event_extractors)\n        \n        # Verify headers were extracted and deduplicated\n        assert len(headers) == 4  # Content-Type, Cache-Control, X-Frame-Options, Strict-Transport-Security\n        header_dict = {h['name']: h['value'] for h in headers}\n        \n        assert header_dict['Content-Type'] == 'text/html'\n        assert header_dict['Cache-Control'] == 'no-cache'\n        assert header_dict['X-Frame-Options'] == 'DENY'\n        assert header_dict['Strict-Transport-Security'] == 'max-age=31536000'\n\n    def test_extract_headers_from_events_with_request_events(self, request_instance):\n        \"\"\"Test _extract_headers_from_events with request events.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock request events\n        events = [\n            {\n                'method': NetworkEvent.REQUEST_WILL_BE_SENT,\n                'params': {\n                    'request': {\n                        'headers': {\n                            'Host': 'api.example.com',\n                            'Connection': 'keep-alive'\n                        }\n                    }\n                }\n            },\n            {\n                'method': NetworkEvent.REQUEST_WILL_BE_SENT_EXTRA_INFO,\n                'params': {\n                    'associatedCookies': [],\n                    'headers': {\n                        'Accept-Encoding': 'gzip, deflate',\n                        'Accept-Language': 'en-US,en;q=0.9'\n                    }\n                }\n            }\n        ]\n        \n        # Define extractors for request events\n        event_extractors = {\n            'request': request_instance._extract_request_sent_headers,\n            'associatedCookies': request_instance._extract_request_sent_extra_info_headers,\n        }\n        \n        # Extract headers from events\n        headers = request_instance._extract_headers_from_events(events, event_extractors)\n        \n        # Verify headers were extracted\n        assert len(headers) == 4  # Host, Connection, Accept-Encoding, Accept-Language\n        header_dict = {h['name']: h['value'] for h in headers}\n        \n        assert header_dict['Host'] == 'api.example.com'\n        assert header_dict['Connection'] == 'keep-alive'\n        assert header_dict['Accept-Encoding'] == 'gzip, deflate'\n        assert header_dict['Accept-Language'] == 'en-US,en;q=0.9'\n\n    def test_extract_headers_from_events_deduplication(self, request_instance):\n        \"\"\"Test that _extract_headers_from_events deduplicates headers correctly.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock events with duplicate headers\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED,\n                'params': {\n                    'response': {\n                        'headers': {\n                            'Content-Type': 'application/json',\n                            'Server': 'nginx'\n                        }\n                    }\n                }\n            },\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'blockedCookies': [],\n                    'headers': {\n                        'Content-Type': 'application/json',  # Duplicate\n                        'X-Custom': 'value'\n                    }\n                }\n            }\n        ]\n        \n        event_extractors = {\n            'response': request_instance._extract_response_received_headers,\n            'blockedCookies': request_instance._extract_response_received_extra_info_headers,\n        }\n        \n        # Extract headers\n        headers = request_instance._extract_headers_from_events(events, event_extractors)\n        \n        # Verify deduplication - Content-Type should appear only once\n        header_names = [h['name'] for h in headers]\n        assert header_names.count('Content-Type') == 1\n        assert len(headers) == 3  # Content-Type (deduplicated), Server, X-Custom\n\n    def test_extract_headers_from_events_empty_events(self, request_instance):\n        \"\"\"Test _extract_headers_from_events with empty events list.\"\"\"\n        event_extractors = {\n            'response': request_instance._extract_response_received_headers,\n        }\n        \n        # Extract headers from empty events\n        headers = request_instance._extract_headers_from_events([], event_extractors)\n        \n        # Should return empty list\n        assert headers == []\n\n    def test_extract_headers_from_events_no_matching_keys(self, request_instance):\n        \"\"\"Test _extract_headers_from_events when no event keys match extractors.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock event with keys that don't match extractors\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED,\n                'params': {\n                    'someOtherKey': {\n                        'headers': {\n                            'Content-Type': 'application/json'\n                        }\n                    }\n                }\n            }\n        ]\n        \n        event_extractors = {\n            'response': request_instance._extract_response_received_headers,\n        }\n        \n        # Extract headers\n        headers = request_instance._extract_headers_from_events(events, event_extractors)\n        \n        # Should return empty list since no keys match\n        assert headers == []\n\n    def test_extract_request_sent_headers(self, request_instance):\n        \"\"\"Test _extract_request_sent_headers method.\"\"\"\n        # Mock request params\n        params = {\n            'request': {\n                'headers': {\n                    'User-Agent': 'Mozilla/5.0',\n                    'Accept': '*/*',\n                    'Content-Type': 'application/json',\n                    'Authorization': 'Bearer secret-token'\n                }\n            },\n            'otherData': 'should be ignored'\n        }\n        \n        # Extract headers\n        headers = request_instance._extract_request_sent_headers(params)\n        \n        # Verify headers were extracted correctly\n        assert len(headers) == 4\n        header_dict = {h['name']: h['value'] for h in headers}\n        \n        assert header_dict['User-Agent'] == 'Mozilla/5.0'\n        assert header_dict['Accept'] == '*/*'\n        assert header_dict['Content-Type'] == 'application/json'\n        assert header_dict['Authorization'] == 'Bearer secret-token'\n\n    def test_extract_request_sent_headers_empty_headers(self, request_instance):\n        \"\"\"Test _extract_request_sent_headers with empty headers.\"\"\"\n        params = {\n            'request': {\n                'headers': {}\n            }\n        }\n        \n        headers = request_instance._extract_request_sent_headers(params)\n        assert headers == []\n\n    def test_extract_request_sent_headers_missing_headers_key(self, request_instance):\n        \"\"\"Test _extract_request_sent_headers when headers key is missing.\"\"\"\n        params = {\n            'request': {\n                'url': 'https://example.com',\n                'method': 'GET'\n            }\n        }\n        \n        headers = request_instance._extract_request_sent_headers(params)\n        assert headers == []\n\n    def test_extract_request_sent_extra_info_headers(self, request_instance):\n        \"\"\"Test _extract_request_sent_extra_info_headers method.\"\"\"\n        # Mock extra info params\n        params = {\n            'headers': {\n                'X-Forwarded-For': '10.0.0.1',\n                'X-Real-IP': '192.168.1.100',\n                'X-Custom-Header': 'extra-info-value'\n            },\n            'associatedCookies': [],\n            'otherData': 'should be ignored'\n        }\n        \n        # Extract headers\n        headers = request_instance._extract_request_sent_extra_info_headers(params)\n        \n        # Verify headers were extracted correctly\n        assert len(headers) == 3\n        header_dict = {h['name']: h['value'] for h in headers}\n        \n        assert header_dict['X-Forwarded-For'] == '10.0.0.1'\n        assert header_dict['X-Real-IP'] == '192.168.1.100'\n        assert header_dict['X-Custom-Header'] == 'extra-info-value'\n\n    def test_extract_request_sent_extra_info_headers_empty(self, request_instance):\n        \"\"\"Test _extract_request_sent_extra_info_headers with empty headers.\"\"\"\n        params = {\n            'headers': {},\n            'associatedCookies': []\n        }\n        \n        headers = request_instance._extract_request_sent_extra_info_headers(params)\n        assert headers == []\n\n    def test_extract_request_sent_extra_info_headers_missing_headers(self, request_instance):\n        \"\"\"Test _extract_request_sent_extra_info_headers when headers key is missing.\"\"\"\n        params = {\n            'associatedCookies': [],\n            'otherData': 'value'\n        }\n        \n        headers = request_instance._extract_request_sent_extra_info_headers(params)\n        assert headers == []\n\n    def test_extract_response_received_headers(self, request_instance):\n        \"\"\"Test _extract_response_received_headers method.\"\"\"\n        # Mock response params\n        params = {\n            'response': {\n                'headers': {\n                    'Content-Type': 'text/html; charset=utf-8',\n                    'Content-Length': '1024',\n                    'Last-Modified': 'Wed, 21 Oct 2015 07:28:00 GMT',\n                    'ETag': '\"33a64df551425fcc55e4d42a148795d9f25f89d4\"'\n                }\n            },\n            'otherData': 'should be ignored'\n        }\n        \n        # Extract headers\n        headers = request_instance._extract_response_received_headers(params)\n        \n        # Verify headers were extracted correctly\n        assert len(headers) == 4\n        header_dict = {h['name']: h['value'] for h in headers}\n        \n        assert header_dict['Content-Type'] == 'text/html; charset=utf-8'\n        assert header_dict['Content-Length'] == '1024'\n        assert header_dict['Last-Modified'] == 'Wed, 21 Oct 2015 07:28:00 GMT'\n        assert header_dict['ETag'] == '\"33a64df551425fcc55e4d42a148795d9f25f89d4\"'\n\n    def test_extract_response_received_extra_info_headers(self, request_instance):\n        \"\"\"Test _extract_response_received_extra_info_headers method.\"\"\"\n        # Mock response extra info params\n        params = {\n            'headers': {\n                'Set-Cookie': 'sessionid=abc123; HttpOnly; Secure',\n                'X-Content-Type-Options': 'nosniff',\n                'X-XSS-Protection': '1; mode=block',\n                'Referrer-Policy': 'strict-origin-when-cross-origin'\n            },\n            'blockedCookies': [],\n            'otherData': 'should be ignored'\n        }\n        \n        # Extract headers\n        headers = request_instance._extract_response_received_extra_info_headers(params)\n        \n        # Verify headers were extracted correctly\n        assert len(headers) == 4\n        header_dict = {h['name']: h['value'] for h in headers}\n        \n        assert header_dict['Set-Cookie'] == 'sessionid=abc123; HttpOnly; Secure'\n        assert header_dict['X-Content-Type-Options'] == 'nosniff'\n        assert header_dict['X-XSS-Protection'] == '1; mode=block'\n        assert header_dict['Referrer-Policy'] == 'strict-origin-when-cross-origin'\n\n    def test_header_extraction_with_complex_values(self, request_instance):\n        \"\"\"Test header extraction with complex header values.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock event with complex header values\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED,\n                'params': {\n                    'response': {\n                        'headers': {\n                            'Content-Security-Policy': \"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'\",\n                            'Link': '</css/style.css>; rel=preload; as=style, </js/app.js>; rel=preload; as=script',\n                            'Cache-Control': 'public, max-age=3600, s-maxage=7200, must-revalidate',\n                        }\n                    }\n                }\n            }\n        ]\n        \n        event_extractors = {\n            'response': request_instance._extract_response_received_headers,\n        }\n        \n        # Extract headers\n        headers = request_instance._extract_headers_from_events(events, event_extractors)\n        \n        # Verify complex values are preserved\n        header_dict = {h['name']: h['value'] for h in headers}\n        \n        assert 'Content-Security-Policy' in header_dict\n        assert \"default-src 'self'\" in header_dict['Content-Security-Policy']\n        assert 'Link' in header_dict\n        assert 'rel=preload' in header_dict['Link']\n        assert 'Cache-Control' in header_dict\n        assert 'must-revalidate' in header_dict['Cache-Control']\n\n    def test_header_extraction_integration_flow(self, request_instance):\n        \"\"\"Test complete header extraction flow for both sent and received headers.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Set up complete request/response flow\n        request_instance._requests_sent = [\n            {\n                'method': NetworkEvent.REQUEST_WILL_BE_SENT,\n                'params': {\n                    'request': {\n                        'headers': {\n                            'Host': 'api.example.com',\n                            'User-Agent': 'PyDoll/1.0',\n                            'Accept': 'application/json'\n                        }\n                    }\n                }\n            }\n        ]\n        \n        request_instance._requests_received = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED,\n                'params': {\n                    'response': {\n                        'headers': {\n                            'Content-Type': 'application/json',\n                            'Server': 'nginx/1.18.0',\n                            'Content-Length': '256'\n                        }\n                    }\n                }\n            }\n        ]\n        \n        # Extract both sent and received headers\n        sent_headers = request_instance._extract_sent_headers()\n        received_headers = request_instance._extract_received_headers()\n        \n        # Verify sent headers\n        sent_dict = {h['name']: h['value'] for h in sent_headers}\n        assert sent_dict['Host'] == 'api.example.com'\n        assert sent_dict['User-Agent'] == 'PyDoll/1.0'\n        assert sent_dict['Accept'] == 'application/json'\n        \n        # Verify received headers\n        received_dict = {h['name']: h['value'] for h in received_headers}\n        assert received_dict['Content-Type'] == 'application/json'\n        assert received_dict['Server'] == 'nginx/1.18.0'\n        assert received_dict['Content-Length'] == '256'\n        \n        # Verify they are separate\n        assert len(sent_headers) == 3\n        assert len(received_headers) == 3\n        assert sent_headers != received_headers\n\n    def test_filter_response_extra_info_events(self, request_instance):\n        \"\"\"Test _filter_response_extra_info_events method.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock events with different types\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED,\n                'params': {'response': {'headers': {}}}\n            },\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {'Set-Cookie': 'session=abc123; Path=/'},\n                    'blockedCookies': []\n                }\n            },\n            {\n                'method': NetworkEvent.REQUEST_WILL_BE_SENT,\n                'params': {'request': {'headers': {}}}\n            },\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {'Set-Cookie': 'token=xyz789; Secure'},\n                    'blockedCookies': []\n                }\n            }\n        ]\n        \n        # Set up mock requests_received with the events\n        request_instance._requests_received = events\n        \n        # Filter for response extra info events\n        filtered_events = request_instance._filter_response_extra_info_events()\n        \n        # Should only return RESPONSE_RECEIVED_EXTRA_INFO events\n        assert len(filtered_events) == 2\n        \n        for event in filtered_events:\n            assert event['method'] == NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO\n            assert 'headers' in event['params']\n            assert 'Set-Cookie' in event['params']['headers']\n\n    def test_filter_response_extra_info_events_empty(self, request_instance):\n        \"\"\"Test _filter_response_extra_info_events with no matching events.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock events without RESPONSE_RECEIVED_EXTRA_INFO\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED,\n                'params': {'response': {'headers': {}}}\n            },\n            {\n                'method': NetworkEvent.REQUEST_WILL_BE_SENT,\n                'params': {'request': {'headers': {}}}\n            }\n        ]\n        \n        request_instance._requests_received = events\n        \n        # Filter for response extra info events\n        filtered_events = request_instance._filter_response_extra_info_events()\n        \n        # Should return empty list\n        assert filtered_events == []\n\n    def test_filter_response_extra_info_events_no_events(self, request_instance):\n        \"\"\"Test _filter_response_extra_info_events with empty events list.\"\"\"\n        request_instance._requests_received = []\n        \n        # Filter for response extra info events\n        filtered_events = request_instance._filter_response_extra_info_events()\n        \n        # Should return empty list\n        assert filtered_events == []\n\n    def test_extract_set_cookies_basic(self, request_instance):\n        \"\"\"Test _extract_set_cookies method with basic cookies.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock events with Set-Cookie headers\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {\n                        'Set-Cookie': 'sessionid=abc123; Path=/; HttpOnly',\n                        'Content-Type': 'application/json'\n                    },\n                    'blockedCookies': []\n                }\n            },\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {\n                        'Set-Cookie': 'userid=456; Domain=.example.com; Secure',\n                        'X-Custom': 'value'\n                    },\n                    'blockedCookies': []\n                }\n            }\n        ]\n        \n        request_instance._requests_received = events\n        \n        # Extract cookies\n        cookies = request_instance._extract_set_cookies()\n        \n        # Should have 2 cookies\n        assert len(cookies) == 2\n        \n        # Check first cookie (only name and value are extracted)\n        cookie1 = next(c for c in cookies if c['name'] == 'sessionid')\n        assert cookie1['value'] == 'abc123'\n        \n        # Check second cookie (only name and value are extracted)\n        cookie2 = next(c for c in cookies if c['name'] == 'userid')\n        assert cookie2['value'] == '456'\n\n    def test_extract_set_cookies_multiple_cookies_same_header(self, request_instance):\n        \"\"\"Test _extract_set_cookies with multiple cookies in same Set-Cookie header.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock event with multiple cookies in one header (newline-separated, not comma)\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {\n                        'Set-Cookie': 'cookie1=value1; Path=/\\ncookie2=value2; HttpOnly\\ncookie3=value3; Secure'\n                    },\n                    'blockedCookies': []\n                }\n            }\n        ]\n        \n        request_instance._requests_received = events\n        \n        # Extract cookies\n        cookies = request_instance._extract_set_cookies()\n        \n        # Should have 3 cookies (split by newline)\n        assert len(cookies) == 3\n        \n        cookie_names = [c['name'] for c in cookies]\n        assert 'cookie1' in cookie_names\n        assert 'cookie2' in cookie_names\n        assert 'cookie3' in cookie_names\n        \n        # Check values (attributes are ignored)\n        cookie1 = next(c for c in cookies if c['name'] == 'cookie1')\n        assert cookie1['value'] == 'value1'\n        \n        cookie2 = next(c for c in cookies if c['name'] == 'cookie2')\n        assert cookie2['value'] == 'value2'\n        \n        cookie3 = next(c for c in cookies if c['name'] == 'cookie3')\n        assert cookie3['value'] == 'value3'\n\n    def test_extract_set_cookies_duplicate_names(self, request_instance):\n        \"\"\"Test _extract_set_cookies with duplicate cookie names (should be deduplicated).\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock events with duplicate cookie names\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {\n                        'Set-Cookie': 'sessionid=first_value; Path=/admin'\n                    },\n                    'blockedCookies': []\n                }\n            },\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {\n                        'Set-Cookie': 'sessionid=second_value; Path=/user'\n                    },\n                    'blockedCookies': []\n                }\n            }\n        ]\n        \n        request_instance._requests_received = events\n        \n        # Extract cookies\n        cookies = request_instance._extract_set_cookies()\n        \n        # Should have 2 cookies (different values, so not deduplicated by object equality)\n        assert len(cookies) == 2\n        cookie_names = [c['name'] for c in cookies]\n        assert cookie_names.count('sessionid') == 2\n        \n        # Both cookies should be present with different values\n        values = [c['value'] for c in cookies if c['name'] == 'sessionid']\n        assert 'first_value' in values\n        assert 'second_value' in values\n\n    def test_extract_set_cookies_complex_values(self, request_instance):\n        \"\"\"Test _extract_set_cookies with complex cookie values and attributes.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock event with complex cookie attributes\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {\n                        'Set-Cookie': 'auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9; Domain=api.example.com; Path=/api; Secure; HttpOnly; SameSite=Strict; Max-Age=3600'\n                    },\n                    'blockedCookies': []\n                }\n            }\n        ]\n        \n        request_instance._requests_received = events\n        \n        # Extract cookies\n        cookies = request_instance._extract_set_cookies()\n        \n        # Should have 1 cookie (only name and value extracted)\n        assert len(cookies) == 1\n        cookie = cookies[0]\n        \n        assert cookie['name'] == 'auth_token'\n        assert cookie['value'] == 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'\n        # Attributes like domain, path, secure, etc. are ignored by the implementation\n\n    def test_extract_set_cookies_no_set_cookie_headers(self, request_instance):\n        \"\"\"Test _extract_set_cookies when no Set-Cookie headers are present.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock events without Set-Cookie headers\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {\n                        'Content-Type': 'application/json',\n                        'X-Custom-Header': 'value'\n                    },\n                    'blockedCookies': []\n                }\n            }\n        ]\n        \n        request_instance._requests_received = events\n        \n        # Extract cookies\n        cookies = request_instance._extract_set_cookies()\n        \n        # Should return empty list\n        assert cookies == []\n\n    def test_extract_set_cookies_empty_events(self, request_instance):\n        \"\"\"Test _extract_set_cookies with empty events list.\"\"\"\n        request_instance._requests_received = []\n        \n        # Extract cookies\n        cookies = request_instance._extract_set_cookies()\n        \n        # Should return empty list\n        assert cookies == []\n\n    def test_extract_set_cookies_malformed_cookies(self, request_instance):\n        \"\"\"Test _extract_set_cookies with malformed cookie strings.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock event with malformed cookies (newline-separated to match implementation)\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {\n                        'Set-Cookie': 'valid_cookie=value123; Path=/\\nmalformed_cookie_no_value\\n=empty_name_cookie; HttpOnly\\nanother_valid=test'\n                    },\n                    'blockedCookies': []\n                }\n            }\n        ]\n        \n        request_instance._requests_received = events\n        \n        # Extract cookies\n        cookies = request_instance._extract_set_cookies()\n        \n        # Should only extract valid cookies (2 valid ones - those with non-empty names)\n        # The implementation rejects cookies with empty names\n        assert len(cookies) == 2\n        \n        cookie_names = [c['name'] for c in cookies]\n        assert 'valid_cookie' in cookie_names\n        assert 'another_valid' in cookie_names\n        \n        # Verify values\n        valid_cookie = next(c for c in cookies if c['name'] == 'valid_cookie')\n        assert valid_cookie['value'] == 'value123'\n        \n        another_valid = next(c for c in cookies if c['name'] == 'another_valid')\n        assert another_valid['value'] == 'test'\n\n    def test_extract_set_cookies_edge_case_attributes(self, request_instance):\n        \"\"\"Test _extract_set_cookies with edge case cookie attributes.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock event with edge case attributes\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {\n                        'Set-Cookie': 'test_cookie=value; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Max-Age=0; SameSite=None; Priority=High'\n                    },\n                    'blockedCookies': []\n                }\n            }\n        ]\n        \n        request_instance._requests_received = events\n        \n        # Extract cookies\n        cookies = request_instance._extract_set_cookies()\n        \n        # Should have 1 cookie (only name and value extracted)\n        assert len(cookies) == 1\n        cookie = cookies[0]\n        \n        assert cookie['name'] == 'test_cookie'\n        assert cookie['value'] == 'value'\n        # All attributes like expires, maxAge, sameSite, etc. are ignored by the implementation\n\n    def test_extract_set_cookies_integration_with_filter(self, request_instance):\n        \"\"\"Test integration between _extract_set_cookies and _filter_response_extra_info_events.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock mixed events (some relevant, some not)\n        events = [\n            {\n                'method': NetworkEvent.REQUEST_WILL_BE_SENT,\n                'params': {'request': {'headers': {}}}\n            },\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {'Set-Cookie': 'filtered_cookie=should_be_extracted; Path=/'},\n                    'blockedCookies': []\n                }\n            },\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED,\n                'params': {'response': {'headers': {}}}\n            },\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {'Set-Cookie': 'another_cookie=also_extracted; HttpOnly'},\n                    'blockedCookies': []\n                }\n            }\n        ]\n        \n        request_instance._requests_received = events\n        \n        # Extract cookies (should use filtering internally)\n        cookies = request_instance._extract_set_cookies()\n        \n        # Should have 2 cookies from the 2 RESPONSE_RECEIVED_EXTRA_INFO events\n        assert len(cookies) == 2\n        \n        cookie_names = [c['name'] for c in cookies]\n        assert 'filtered_cookie' in cookie_names\n        assert 'another_cookie' in cookie_names\n\n    def test_extract_set_cookies_empty_name_rejection(self, request_instance):\n        \"\"\"Test that _extract_set_cookies rejects cookies with empty names.\"\"\"\n        from pydoll.protocol.network.events import NetworkEvent\n        \n        # Mock event with various invalid cookie formats\n        events = [\n            {\n                'method': NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO,\n                'params': {\n                    'headers': {\n                        'Set-Cookie': 'valid_cookie=value\\n=empty_name_value\\n =space_only_name_value\\n\\t=tab_only_name_value'\n                    },\n                    'blockedCookies': []\n                }\n            }\n        ]\n        \n        request_instance._requests_received = events\n        \n        # Extract cookies\n        cookies = request_instance._extract_set_cookies()\n        \n        # Should only extract the valid cookie, rejecting all empty/whitespace-only names\n        assert len(cookies) == 1\n        assert cookies[0]['name'] == 'valid_cookie'\n        assert cookies[0]['value'] == 'value'\n\n    def test_parse_cookie_line_empty_name_validation(self, request_instance):\n        \"\"\"Test _parse_cookie_line directly with empty names.\"\"\"\n        # Test various forms of empty names\n        assert request_instance._parse_cookie_line('=value') is None\n        assert request_instance._parse_cookie_line(' =value') is None\n        assert request_instance._parse_cookie_line('\\t=value') is None\n        assert request_instance._parse_cookie_line('  \\t  =value') is None\n        \n        # Test valid names\n        result = request_instance._parse_cookie_line('name=value')\n        assert result is not None\n        assert result['name'] == 'name'\n        assert result['value'] == 'value'\n        \n        # Test whitespace around valid names (should be trimmed)\n        result = request_instance._parse_cookie_line('  name  =  value  ')\n        assert result is not None\n        assert result['name'] == 'name'\n        assert result['value'] == 'value'"
  },
  {
    "path": "tests/test_browser/test_requests_response.py",
    "content": "\"\"\"\nTests for pydoll.browser.requests.response module.\n\"\"\"\n\nimport json\nimport pytest\n\nfrom pydoll.browser.requests.response import Response, STATUS_CODE_RANGE_OK\nfrom pydoll.exceptions import HTTPError\nfrom pydoll.protocol.fetch.types import HeaderEntry\nfrom pydoll.protocol.network.types import CookieParam\n\n\nclass TestResponseInitialization:\n    \"\"\"Test Response class initialization.\"\"\"\n\n    def test_response_initialization_minimal(self):\n        \"\"\"Test Response initialization with minimal parameters.\"\"\"\n        response = Response(status_code=200)\n        \n        assert response.status_code == 200\n        assert response.content == b''\n        assert response.text == ''\n        assert response.headers == []\n        assert response.request_headers == []\n        assert response.cookies == []\n        assert response.ok is True\n\n    def test_response_initialization_full(self):\n        \"\"\"Test Response initialization with all parameters.\"\"\"\n        headers = [HeaderEntry(name='Content-Type', value='application/json')]\n        request_headers = [HeaderEntry(name='User-Agent', value='Test-Agent')]\n        cookies = [CookieParam(name='session', value='abc123')]\n        json_data = {'message': 'success'}\n        \n        response = Response(\n            status_code=201,\n            content=b'{\"message\": \"success\"}',\n            text='{\"message\": \"success\"}',\n            json=json_data,\n            response_headers=headers,\n            request_headers=request_headers,\n            cookies=cookies,\n            url='https://example.com'\n        )\n        \n        assert response.status_code == 201\n        assert response.content == b'{\"message\": \"success\"}'\n        assert response.text == '{\"message\": \"success\"}'\n        assert response.headers == headers\n        assert response.request_headers == request_headers\n        assert response.cookies == cookies\n        assert response.ok is True\n\n    def test_response_initialization_with_none_values(self):\n        \"\"\"Test Response initialization handles None values correctly.\"\"\"\n        response = Response(\n            status_code=200,\n            response_headers=None,\n            request_headers=None,\n            cookies=None\n        )\n        \n        assert response.headers == []\n        assert response.request_headers == []\n        assert response.cookies == []\n\n\nclass TestResponseProperties:\n    \"\"\"Test Response properties.\"\"\"\n\n    def test_ok_property_success_codes(self):\n        \"\"\"Test ok property returns True for success status codes.\"\"\"\n        success_codes = [200, 201, 204, 299, 300, 301, 302, 399]\n        \n        for code in success_codes:\n            response = Response(status_code=code)\n            assert response.ok is True, f\"Status code {code} should be ok\"\n\n    def test_ok_property_error_codes(self):\n        \"\"\"Test ok property returns False for error status codes.\"\"\"\n        error_codes = [400, 401, 403, 404, 500, 502, 503]\n        \n        for code in error_codes:\n            response = Response(status_code=code)\n            assert response.ok is False, f\"Status code {code} should not be ok\"\n\n    def test_status_code_property(self):\n        \"\"\"Test status_code property.\"\"\"\n        response = Response(status_code=404)\n        assert response.status_code == 404\n\n    def test_content_property(self):\n        \"\"\"Test content property returns bytes.\"\"\"\n        content = b'Hello, World!'\n        response = Response(status_code=200, content=content)\n        assert response.content == content\n        assert isinstance(response.content, bytes)\n\n    def test_text_property_provided(self):\n        \"\"\"Test text property when text is provided.\"\"\"\n        text = 'Hello, World!'\n        response = Response(status_code=200, text=text)\n        assert response.text == text\n\n    def test_text_property_decoded_from_content(self):\n        \"\"\"Test text property decodes from content when not provided.\"\"\"\n        content = b'Hello, World!'\n        response = Response(status_code=200, content=content)\n        assert response.text == 'Hello, World!'\n\n    def test_text_property_handles_encoding_errors(self):\n        \"\"\"Test text property handles encoding errors gracefully.\"\"\"\n        # Invalid UTF-8 sequence\n        content = b'\\xff\\xfe\\xfd'\n        response = Response(status_code=200, content=content)\n        \n        # Should not raise exception and should have some text\n        text = response.text\n        assert isinstance(text, str)\n        assert len(text) > 0\n\n    def test_text_property_empty_content(self):\n        \"\"\"Test text property with empty content.\"\"\"\n        response = Response(status_code=200, content=b'')\n        assert response.text == ''\n\n    def test_headers_property(self):\n        \"\"\"Test headers property returns response headers.\"\"\"\n        headers = [\n            HeaderEntry(name='Content-Type', value='application/json'),\n            HeaderEntry(name='Content-Length', value='100')\n        ]\n        response = Response(status_code=200, response_headers=headers)\n        assert response.headers == headers\n\n    def test_request_headers_property(self):\n        \"\"\"Test request_headers property returns request headers.\"\"\"\n        headers = [\n            HeaderEntry(name='User-Agent', value='Test-Agent'),\n            HeaderEntry(name='Accept', value='application/json')\n        ]\n        response = Response(status_code=200, request_headers=headers)\n        assert response.request_headers == headers\n\n    def test_cookies_property(self):\n        \"\"\"Test cookies property returns cookies.\"\"\"\n        cookies = [\n            CookieParam(name='session', value='abc123'),\n            CookieParam(name='csrf', value='token456')\n        ]\n        response = Response(status_code=200, cookies=cookies)\n        assert response.cookies == cookies\n\n    def test_url_property(self):\n        \"\"\"Test url property returns final URL.\"\"\"\n        url = 'https://api.example.com/data'\n        response = Response(status_code=200, url=url)\n        assert response.url == url\n\n    def test_url_property_empty(self):\n        \"\"\"Test url property with empty URL.\"\"\"\n        response = Response(status_code=200, url='')\n        assert response.url == ''\n\n\nclass TestResponseJSONMethod:\n    \"\"\"Test Response json() method.\"\"\"\n\n    def test_json_method_with_provided_json(self):\n        \"\"\"Test json() method when JSON data is provided.\"\"\"\n        json_data = {'message': 'success', 'code': 200}\n        response = Response(status_code=200, json=json_data)\n        \n        result = response.json()\n        assert result == json_data\n\n    def test_json_method_parses_from_text(self):\n        \"\"\"Test json() method parses JSON from text.\"\"\"\n        json_text = '{\"message\": \"success\", \"code\": 200}'\n        response = Response(status_code=200, text=json_text)\n        \n        result = response.json()\n        assert result == {'message': 'success', 'code': 200}\n\n    def test_json_method_caches_result(self):\n        \"\"\"Test json() method caches parsed result.\"\"\"\n        json_text = '{\"message\": \"success\"}'\n        response = Response(status_code=200, text=json_text)\n        \n        # First call should parse\n        result1 = response.json()\n        # Second call should return cached result\n        result2 = response.json()\n        \n        assert result1 == result2\n        assert result1 is result2  # Same object instance\n\n    def test_json_method_invalid_json_raises_error(self):\n        \"\"\"Test json() method raises ValueError for invalid JSON.\"\"\"\n        response = Response(status_code=200, text='invalid json')\n        \n        with pytest.raises(ValueError, match='Response is not valid JSON'):\n            response.json()\n\n    def test_json_method_empty_text(self):\n        \"\"\"Test json() method with empty text.\"\"\"\n        response = Response(status_code=200, text='')\n        \n        with pytest.raises(ValueError, match='Response is not valid JSON'):\n            response.json()\n\n    def test_json_method_with_array(self):\n        \"\"\"Test json() method with JSON array.\"\"\"\n        json_text = '[{\"id\": 1}, {\"id\": 2}]'\n        response = Response(status_code=200, text=json_text)\n        \n        result = response.json()\n        assert result == [{'id': 1}, {'id': 2}]\n\n    def test_json_method_with_primitive_values(self):\n        \"\"\"Test json() method with primitive JSON values.\"\"\"\n        test_cases = [\n            ('true', True),\n            ('false', False),\n            ('null', None),\n            ('42', 42),\n            ('\"string\"', 'string')\n        ]\n        \n        for json_text, expected in test_cases:\n            response = Response(status_code=200, text=json_text)\n            result = response.json()\n            assert result == expected\n\n\nclass TestResponseRaiseForStatus:\n    \"\"\"Test Response raise_for_status() method.\"\"\"\n\n    def test_raise_for_status_success_codes(self):\n        \"\"\"Test raise_for_status() does not raise for success codes.\"\"\"\n        success_codes = [200, 201, 204, 299, 300, 301, 302, 399]\n        \n        for code in success_codes:\n            response = Response(status_code=code, url='https://example.com')\n            # Should not raise any exception\n            response.raise_for_status()\n\n    def test_raise_for_status_client_error(self):\n        \"\"\"Test raise_for_status() raises for client error codes.\"\"\"\n        error_codes = [400, 401, 403, 404, 422, 499]\n        \n        for code in error_codes:\n            response = Response(status_code=code, url='https://example.com')\n            with pytest.raises(HTTPError, match=f'{code} Client Error'):\n                response.raise_for_status()\n\n    def test_raise_for_status_server_error(self):\n        \"\"\"Test raise_for_status() raises for server error codes.\"\"\"\n        error_codes = [500, 502, 503, 504, 599]\n        \n        for code in error_codes:\n            response = Response(status_code=code, url='https://example.com')\n            with pytest.raises(HTTPError, match=f'{code} Client Error'):\n                response.raise_for_status()\n\n    def test_raise_for_status_includes_url(self):\n        \"\"\"Test raise_for_status() includes URL in error message.\"\"\"\n        url = 'https://api.example.com/endpoint'\n        response = Response(status_code=404, url=url)\n        \n        with pytest.raises(HTTPError, match=f'for url {url}'):\n            response.raise_for_status()\n\n    def test_raise_for_status_empty_url(self):\n        \"\"\"Test raise_for_status() works with empty URL.\"\"\"\n        response = Response(status_code=500, url='')\n        \n        with pytest.raises(HTTPError, match='500 Client Error: for url'):\n            response.raise_for_status()\n\n\nclass TestHTTPErrorException:\n    \"\"\"Test HTTPError exception class.\"\"\"\n\n    def test_http_error_creation(self):\n        \"\"\"Test HTTPError can be created with message.\"\"\"\n        error = HTTPError('Test error message')\n        assert str(error) == 'Test error message'\n\n    def test_http_error_inheritance(self):\n        \"\"\"Test HTTPError inherits from Exception.\"\"\"\n        error = HTTPError('Test error')\n        assert isinstance(error, Exception)\n\n    def test_http_error_with_format_string(self):\n        \"\"\"Test HTTPError with formatted message.\"\"\"\n        status_code = 404\n        url = 'https://example.com'\n        error = HTTPError(f'{status_code} Client Error: for url {url}')\n        \n        expected_message = '404 Client Error: for url https://example.com'\n        assert str(error) == expected_message\n\n\nclass TestResponseEdgeCases:\n    \"\"\"Test Response edge cases and unusual scenarios.\"\"\"\n\n    def test_response_with_binary_content(self):\n        \"\"\"Test Response with binary content.\"\"\"\n        binary_data = bytes(range(256))  # All possible byte values\n        response = Response(status_code=200, content=binary_data)\n        \n        assert response.content == binary_data\n        assert isinstance(response.content, bytes)\n\n    def test_response_with_unicode_text(self):\n        \"\"\"Test Response with Unicode text.\"\"\"\n        unicode_text = '🌟 Hello, 世界! 🚀'\n        response = Response(status_code=200, text=unicode_text)\n        \n        assert response.text == unicode_text\n\n    def test_response_text_lazy_decoding(self):\n        \"\"\"Test that text decoding is lazy and cached.\"\"\"\n        content = 'Hello, World!'.encode('utf-8')\n        response = Response(status_code=200, content=content)\n        \n        # Access text multiple times\n        text1 = response.text\n        text2 = response.text\n        \n        assert text1 == text2\n        assert text1 == 'Hello, World!'\n\n    def test_response_with_large_content(self):\n        \"\"\"Test Response with large content.\"\"\"\n        large_content = b'x' * 1000000  # 1MB of data\n        response = Response(status_code=200, content=large_content)\n        \n        assert len(response.content) == 1000000\n        assert response.content == large_content\n\n    def test_response_status_code_boundary_values(self):\n        \"\"\"Test Response with boundary status code values.\"\"\"\n        boundary_codes = [100, 199, 200, 299, 300, 399, 400, 499, 500, 599]\n        \n        for code in boundary_codes:\n            response = Response(status_code=code)\n            assert response.status_code == code\n            \n            # Check ok property boundary\n            if code in STATUS_CODE_RANGE_OK:\n                assert response.ok is True\n            else:\n                assert response.ok is False\n\n    def test_response_with_complex_headers(self):\n        \"\"\"Test Response with complex header scenarios.\"\"\"\n        headers = [\n            HeaderEntry(name='Set-Cookie', value='session=abc; Path=/'),\n            HeaderEntry(name='Set-Cookie', value='csrf=xyz; HttpOnly'),\n            HeaderEntry(name='Content-Type', value='application/json; charset=utf-8'),\n            HeaderEntry(name='X-Custom-Header', value='custom value with spaces')\n        ]\n        \n        response = Response(status_code=200, response_headers=headers)\n        assert len(response.headers) == 4\n        assert response.headers == headers\n\n    def test_response_with_empty_json_object(self):\n        \"\"\"Test Response with empty JSON object.\"\"\"\n        response = Response(status_code=200, text='{}')\n        result = response.json()\n        assert result == {}\n\n    def test_response_with_nested_json(self):\n        \"\"\"Test Response with deeply nested JSON.\"\"\"\n        nested_json = {\n            'level1': {\n                'level2': {\n                    'level3': {\n                        'data': ['item1', 'item2'],\n                        'metadata': {'count': 2, 'type': 'array'}\n                    }\n                }\n            }\n        }\n        \n        response = Response(status_code=200, json=nested_json)\n        result = response.json()\n        assert result == nested_json\n        assert result['level1']['level2']['level3']['data'] == ['item1', 'item2']\n\n\nclass TestResponseIntegration:\n    \"\"\"Test Response integration scenarios.\"\"\"\n\n    def test_complete_response_workflow(self):\n        \"\"\"Test complete response workflow with all components.\"\"\"\n        # Simulate a complete API response\n        headers = [\n            HeaderEntry(name='Content-Type', value='application/json'),\n            HeaderEntry(name='Content-Length', value='45'),\n            HeaderEntry(name='Server', value='nginx/1.18.0')\n        ]\n        \n        request_headers = [\n            HeaderEntry(name='User-Agent', value='PyDoll/1.0'),\n            HeaderEntry(name='Accept', value='application/json'),\n            HeaderEntry(name='Authorization', value='Bearer token123')\n        ]\n        \n        cookies = [\n            CookieParam(name='session_id', value='sess_abc123'),\n            CookieParam(name='preferences', value='theme=dark')\n        ]\n        \n        json_data = {\n            'status': 'success',\n            'data': {'id': 1, 'name': 'Test Item'},\n            'timestamp': '2023-12-01T10:00:00Z'\n        }\n        \n        response = Response(\n            status_code=200,\n            content=json.dumps(json_data).encode('utf-8'),\n            text=json.dumps(json_data),\n            json=json_data,\n            response_headers=headers,\n            request_headers=request_headers,\n            cookies=cookies,\n            url='https://api.example.com/items/1'\n        )\n        \n        # Test all aspects\n        assert response.ok is True\n        assert response.status_code == 200\n        assert response.json() == json_data\n        assert len(response.headers) == 3\n        assert len(response.request_headers) == 3\n        assert len(response.cookies) == 2\n        \n        # Should not raise\n        response.raise_for_status()\n\n    def test_error_response_workflow(self):\n        \"\"\"Test error response workflow.\"\"\"\n        error_json = {\n            'error': 'Not Found',\n            'message': 'The requested resource was not found',\n            'code': 404\n        }\n        \n        response = Response(\n            status_code=404,\n            text=json.dumps(error_json),\n            url='https://api.example.com/items/999'\n        )\n        \n        assert response.ok is False\n        assert response.status_code == 404\n        assert response.json() == error_json\n        \n        with pytest.raises(HTTPError):\n            response.raise_for_status()"
  },
  {
    "path": "tests/test_browser/test_tab_request_integration.py",
    "content": "\"\"\"\nIntegration tests for Tab and Request classes.\n\nThis module tests the integration between the Tab class and the Request class,\nfocusing on the 'request' property and how they work together for HTTP requests.\n\"\"\"\n\nimport pytest\nimport pytest_asyncio\nimport uuid\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nfrom pydoll.browser.tab import Tab\nfrom pydoll.browser.requests.request import Request\nfrom pydoll.browser.requests.response import Response\nfrom pydoll.protocol.fetch.types import HeaderEntry\nfrom pydoll.protocol.network.types import CookieParam\n\n\n@pytest_asyncio.fixture\nasync def mock_connection_handler():\n    \"\"\"Mock connection handler for Tab tests.\"\"\"\n    with patch('pydoll.connection.ConnectionHandler', autospec=True) as mock:\n        handler = mock.return_value\n        handler.execute_command = AsyncMock()\n        handler.register_callback = AsyncMock()\n        handler.remove_callback = AsyncMock()\n        handler.clear_callbacks = AsyncMock()\n        handler.network_logs = []\n        handler.dialog = None\n        yield handler\n\n\n@pytest_asyncio.fixture\nasync def mock_browser():\n    \"\"\"Mock browser instance.\"\"\"\n    browser = MagicMock()\n    browser.close_tab = AsyncMock()\n    return browser\n\n\n@pytest_asyncio.fixture\nasync def tab(mock_browser, mock_connection_handler):\n    \"\"\"Tab fixture with mocked dependencies.\"\"\"\n    # Generate unique target_id for each test to avoid singleton conflicts\n    unique_target_id = f'test-target-{uuid.uuid4().hex[:8]}'\n    \n    with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_connection_handler):\n        tab_instance = Tab(\n            browser=mock_browser,\n            connection_port=9222,\n            target_id=unique_target_id,\n            browser_context_id='test-context-id'\n        )\n        \n        # Mock network events properties\n        tab_instance._network_events_enabled = False\n        tab_instance._page_events_enabled = False\n        tab_instance._dom_events_enabled = False\n        tab_instance._runtime_events_enabled = False\n        tab_instance._fetch_events_enabled = False\n        tab_instance._intercept_file_chooser_dialog_enabled = False\n        \n        yield tab_instance\n\n\n@pytest_asyncio.fixture\ndef cleanup_tab_registry():\n    \"\"\"No-op: singleton removed; keep fixture for compatibility.\"\"\"\n    yield\n\n\nclass TestTabRequestProperty:\n    \"\"\"Test the request property on Tab class.\"\"\"\n\n    def test_request_property_lazy_initialization(self, tab):\n        \"\"\"Test that request property creates Request instance lazily.\"\"\"\n        # Initially _request should be None\n        assert tab._request is None\n        \n        # First access should create the Request instance\n        request_instance = tab.request\n        assert request_instance is not None\n        assert isinstance(request_instance, Request)\n        assert tab._request is request_instance\n        \n        # Second access should return the same instance\n        request_instance2 = tab.request\n        assert request_instance2 is request_instance\n\n    def test_request_property_binds_to_tab(self, tab):\n        \"\"\"Test that Request instance is properly bound to the Tab.\"\"\"\n        request_instance = tab.request\n        \n        # Request should have reference to the tab\n        assert request_instance.tab is tab\n\n    def test_request_property_type_annotation(self, tab):\n        \"\"\"Test that request property returns correct type.\"\"\"\n        request_instance = tab.request\n        assert isinstance(request_instance, Request)\n\n    def test_multiple_tabs_have_separate_requests(self, mock_browser, mock_connection_handler):\n        \"\"\"Test that different Tab instances have separate Request instances.\"\"\"\n        # Create two different tabs\n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_connection_handler):\n            tab1 = Tab(\n                browser=mock_browser,\n                connection_port=9222,\n                target_id=\"test-target-1\",\n                browser_context_id='test-context-1'\n            )\n            \n            tab2 = Tab(\n                browser=mock_browser,\n                connection_port=9222,\n                target_id=\"test-target-2\",\n                browser_context_id='test-context-2'\n            )\n            \n            # Each tab should have its own Request instance\n            request1 = tab1.request\n            request2 = tab2.request\n            \n            assert request1 is not request2\n            assert request1.tab is tab1\n            assert request2.tab is tab2\n\n\nclass TestTabRequestIntegration:\n    \"\"\"Test integration scenarios between Tab and Request.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_request_uses_tab_network_events(self, tab):\n        \"\"\"Test that Request properly uses Tab's network event system.\"\"\"\n        request_instance = tab.request\n        \n        # Mock network events methods\n        tab.enable_network_events = AsyncMock()\n        tab.disable_network_events = AsyncMock()\n        tab.on = AsyncMock(side_effect=lambda *a, **kw: len(tab.on.call_args_list))\n        tab.remove_callback = AsyncMock()\n\n        # Mock tab execute command for HTTP request\n        tab._execute_command = AsyncMock()\n        mock_result = {\n            'result': {\n                'result': {\n                    'value': {\n                        'status': 200,\n                        'content': [72, 101, 108, 108, 111],  # \"Hello\" as bytes\n                        'text': 'Hello',\n                        'json': {'message': 'success'},\n                        'url': 'https://example.com'\n                    }\n                }\n            }\n        }\n        tab._execute_command.return_value = mock_result\n        \n        # Mock helper methods to avoid actual network processing\n        with patch.object(request_instance, '_extract_received_headers') as mock_extract_headers, \\\n             patch.object(request_instance, '_extract_sent_headers') as mock_extract_sent, \\\n             patch.object(request_instance, '_extract_set_cookies') as mock_extract_cookies:\n            \n            mock_extract_headers.return_value = [HeaderEntry(name='Content-Type', value='application/json')]\n            mock_extract_sent.return_value = [HeaderEntry(name='User-Agent', value='Test-Agent')]\n            mock_extract_cookies.return_value = [CookieParam(name='session', value='abc123')]\n            \n            # Make a request\n            response = await request_instance.get('https://example.com')\n            \n            # Verify response\n            assert isinstance(response, Response)\n            assert response.status_code == 200\n            assert response.text == 'Hello'\n            \n            # Verify that tab's execute_command was called\n            tab._execute_command.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_request_enables_network_events_when_needed(self, tab):\n        \"\"\"Test that Request enables network events on tab when not already enabled.\"\"\"\n        request_instance = tab.request\n        \n        # Tab initially has network events disabled\n        tab._network_events_enabled = False\n        tab.enable_network_events = AsyncMock()\n        tab.disable_network_events = AsyncMock()\n        tab.on = AsyncMock(side_effect=lambda *a, **kw: len(tab.on.call_args_list))\n        tab.remove_callback = AsyncMock()\n\n        # Mock tab execute command\n        tab._execute_command = AsyncMock()\n        mock_result = {\n            'result': {\n                'result': {\n                    'value': {\n                        'status': 200,\n                        'content': [],\n                        'text': 'OK',\n                        'json': None,\n                        'url': 'https://example.com'\n                    }\n                }\n            }\n        }\n        tab._execute_command.return_value = mock_result\n\n        # Mock helper methods\n        with patch.object(request_instance, '_extract_received_headers') as mock_extract_headers, \\\n             patch.object(request_instance, '_extract_sent_headers') as mock_extract_sent, \\\n             patch.object(request_instance, '_extract_set_cookies') as mock_extract_cookies:\n\n            mock_extract_headers.return_value = []\n            mock_extract_sent.return_value = []\n            mock_extract_cookies.return_value = []\n\n            # Make a request\n            await request_instance.get('https://example.com')\n\n            # Verify network events were enabled and callbacks were registered\n            tab.enable_network_events.assert_called_once()\n            assert tab.on.call_count == 4  # Four network events should be registered\n\n    @pytest.mark.asyncio\n    async def test_request_clears_callbacks_after_completion(self, tab):\n        \"\"\"Test that Request clears callbacks after request completion.\"\"\"\n        request_instance = tab.request\n        \n        # Mock tab methods\n        tab._network_events_enabled = False\n        tab.enable_network_events = AsyncMock()\n        tab.disable_network_events = AsyncMock()\n        tab.on = AsyncMock(side_effect=lambda *a, **kw: len(tab.on.call_args_list))\n        tab.remove_callback = AsyncMock()\n        tab._execute_command = AsyncMock()\n\n        mock_result = {\n            'result': {\n                'result': {\n                    'value': {\n                        'status': 200,\n                        'content': [],\n                        'text': 'OK',\n                        'json': None,\n                        'url': 'https://example.com'\n                    }\n                }\n            }\n        }\n        tab._execute_command.return_value = mock_result\n\n        # Mock helper methods\n        with patch.object(request_instance, '_extract_received_headers') as mock_extract_headers, \\\n             patch.object(request_instance, '_extract_sent_headers') as mock_extract_sent, \\\n             patch.object(request_instance, '_extract_set_cookies') as mock_extract_cookies:\n\n            mock_extract_headers.return_value = []\n            mock_extract_sent.return_value = []\n            mock_extract_cookies.return_value = []\n\n            # Make a request\n            await request_instance.get('https://example.com')\n\n            # Verify callbacks were removed surgically (4 callbacks registered)\n            assert tab.remove_callback.call_count == 4\n\n    @pytest.mark.asyncio\n    async def test_request_clears_callbacks_on_error(self, tab):\n        \"\"\"Test that Request clears callbacks even when request fails.\"\"\"\n        request_instance = tab.request\n        \n        # Mock tab methods\n        tab._network_events_enabled = False\n        tab.enable_network_events = AsyncMock()\n        tab.disable_network_events = AsyncMock()\n        tab.on = AsyncMock(side_effect=lambda *a, **kw: len(tab.on.call_args_list))\n        tab.remove_callback = AsyncMock()\n\n        # Make tab._execute_command raise an exception\n        tab._execute_command = AsyncMock(side_effect=Exception(\"Network error\"))\n\n        # Make a request that should fail\n        with pytest.raises(Exception):  # Should raise HTTPError wrapping the original exception\n            await request_instance.get('https://example.com')\n\n        # Verify callbacks were still removed despite the error\n        assert tab.remove_callback.call_count == 4\n\n    @pytest.mark.asyncio\n    async def test_request_http_methods_integration(self, tab):\n        \"\"\"Test that all HTTP methods work through the Tab's request property.\"\"\"\n        request_instance = tab.request\n        \n        # Mock tab methods\n        tab._network_events_enabled = False\n        tab.enable_network_events = AsyncMock()\n        tab.disable_network_events = AsyncMock()\n        tab.on = AsyncMock(side_effect=lambda *a, **kw: len(tab.on.call_args_list))\n        tab.remove_callback = AsyncMock()\n        tab._execute_command = AsyncMock()\n\n        mock_result = {\n            'result': {\n                'result': {\n                    'value': {\n                        'status': 200,\n                        'content': [],\n                        'text': 'OK',\n                        'json': None,\n                        'url': 'https://example.com'\n                    }\n                }\n            }\n        }\n        tab._execute_command.return_value = mock_result\n\n        # Mock helper methods\n        with patch.object(request_instance, '_extract_received_headers') as mock_extract_headers, \\\n             patch.object(request_instance, '_extract_sent_headers') as mock_extract_sent, \\\n             patch.object(request_instance, '_extract_set_cookies') as mock_extract_cookies:\n\n            mock_extract_headers.return_value = []\n            mock_extract_sent.return_value = []\n            mock_extract_cookies.return_value = []\n\n            # Test all HTTP methods\n            methods_to_test = [\n                ('get', lambda: request_instance.get('https://example.com')),\n                ('post', lambda: request_instance.post('https://example.com', data={'key': 'value'})),\n                ('put', lambda: request_instance.put('https://example.com', json={'update': True})),\n                ('patch', lambda: request_instance.patch('https://example.com', json={'patch': True})),\n                ('delete', lambda: request_instance.delete('https://example.com')),\n                ('head', lambda: request_instance.head('https://example.com')),\n                ('options', lambda: request_instance.options('https://example.com')),\n            ]\n\n            for method_name, method_call in methods_to_test:\n                # Reset mocks\n                tab._execute_command.reset_mock()\n                tab.remove_callback.reset_mock()\n\n                # Execute method\n                response = await method_call()\n\n                # Verify response\n                assert isinstance(response, Response)\n                assert response.status_code == 200\n\n                # Verify tab's execute_command was called\n                tab._execute_command.assert_called_once()\n                # Verify callbacks were removed surgically (4 per request)\n                assert tab.remove_callback.call_count == 4\n\n    def test_request_property_singleton_behavior(self, tab):\n        \"\"\"Test that request property maintains singleton behavior per tab.\"\"\"\n        # Multiple accesses should return the same instance\n        request1 = tab.request\n        request2 = tab.request\n        request3 = tab.request\n        \n        assert request1 is request2\n        assert request2 is request3\n        assert isinstance(request1, Request)\n\n    @pytest.mark.asyncio\n    async def test_tab_request_maintains_state(self, tab):\n        \"\"\"Test that Tab's request instance maintains its state across calls.\"\"\"\n        request_instance = tab.request\n        \n        # Simulate some state changes in the request instance\n        request_instance._network_events_enabled = True\n        request_instance._requests_sent = ['mock_request']\n        request_instance._requests_received = ['mock_response']\n        \n        # Access request property again\n        same_request = tab.request\n        \n        # Should be the same instance with preserved state\n        assert same_request is request_instance\n        assert same_request._network_events_enabled is True\n        assert same_request._requests_sent == ['mock_request']\n        assert same_request._requests_received == ['mock_response']\n\n\nclass TestTabRequestEdgeCases:\n    \"\"\"Test edge cases for Tab-Request integration.\"\"\"\n\n    def test_request_property_after_tab_reuse(self, mock_browser, mock_connection_handler):\n        \"\"\"Test request property behavior when Tab instances are reused.\"\"\"\n        # Create tab with specific target_id\n        target_id = \"reusable-target-123\"\n        \n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_connection_handler):\n            # First tab instance\n            tab1 = Tab(\n                browser=mock_browser,\n                connection_port=9222,\n                target_id=target_id,\n                browser_context_id='test-context-reuse'\n            )\n            request1 = tab1.request\n            \n            # Second tab instance with same target_id (no singleton anymore)\n            tab2 = Tab(\n                browser=mock_browser,\n                connection_port=9222,\n                target_id=target_id,\n                browser_context_id='test-context-reuse'\n            )\n            # With no singleton, they are different instances, but independent request is allowed\n            assert tab2 is not tab1\n            # Request instances are created per tab; they are distinct here\n            request2 = tab2.request\n            assert request2 is not request1\n\n    @pytest.mark.asyncio\n    async def test_request_property_memory_efficiency(self, tab):\n        \"\"\"Test that request property doesn't create unnecessary instances.\"\"\"\n        import weakref\n        \n        # Get initial request instance\n        request_instance = tab.request\n        weak_ref = weakref.ref(request_instance)\n        \n        # Clear local reference\n        del request_instance\n        \n        # Request instance should still exist because tab holds reference\n        assert weak_ref() is not None\n        \n        # Getting request again should return same instance\n        same_request = tab.request\n        assert weak_ref() is same_request\n\n    def test_request_property_with_different_tab_states(self, mock_browser, mock_connection_handler):\n        \"\"\"Test request property with tabs in different states.\"\"\"\n        # Create tabs with different configurations\n        tab_configurations = [\n            {'target_id': 'tab-1', 'browser_context_id': 'context-1'},\n            {'target_id': 'tab-2', 'browser_context_id': 'context-2'},\n            {'target_id': 'tab-3', 'browser_context_id': 'context-3'},\n        ]\n        \n        tabs_and_requests = []\n        \n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_connection_handler):\n            for config in tab_configurations:\n                tab = Tab(\n                    browser=mock_browser,\n                    connection_port=9222,\n                    **config\n                )\n                request = tab.request\n                tabs_and_requests.append((tab, request))\n        \n        # Each tab should have its own request instance\n        for i, (tab, request) in enumerate(tabs_and_requests):\n            assert isinstance(request, Request)\n            assert request.tab is tab\n            \n            # Compare with other tabs\n            for j, (other_tab, other_request) in enumerate(tabs_and_requests):\n                if i != j:\n                    assert request is not other_request\n                    assert tab is not other_tab"
  },
  {
    "path": "tests/test_click_nested_integration.py",
    "content": "\"\"\"Integration tests for click() on nested elements (shadow DOM, iframes).\"\"\"\n\nimport asyncio\nfrom pathlib import Path\n\nimport pytest\n\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.elements.web_element import WebElement\n\nTEST_PAGE = f'file://{(Path(__file__).parent / \"pages\" / \"test_click_nested.html\").absolute()}'\n\n\nclass TestClickRegularElement:\n    \"\"\"Baseline: click() on a normal page element.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_click_regular_button(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            btn = await tab.find(id='regular-btn')\n            counter = await tab.find(id='regular-btn-count')\n\n            text_before = await counter.text\n            assert text_before == '0'\n\n            await btn.click()\n            await asyncio.sleep(0.2)\n\n            text_after = await counter.text\n            assert text_after == '1'\n\n    @pytest.mark.asyncio\n    async def test_click_regular_button_multiple_times(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            btn = await tab.find(id='regular-btn')\n            counter = await tab.find(id='regular-btn-count')\n\n            for i in range(3):\n                await btn.click()\n                await asyncio.sleep(0.15)\n\n            text = await counter.text\n            assert text == '3'\n\n\nclass TestClickInShadowRoot:\n    \"\"\"click() on elements inside a shadow root.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_click_button_in_shadow_root(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            host = await tab.find(id='shadow-host')\n            shadow = await host.get_shadow_root()\n\n            btn = await shadow.query('#shadow-btn')\n            counter = await shadow.query('#shadow-btn-count')\n\n            text_before = await counter.text\n            assert text_before == '0'\n\n            await btn.click()\n            await asyncio.sleep(0.2)\n\n            text_after = await counter.text\n            assert text_after == '1'\n\n    @pytest.mark.asyncio\n    async def test_find_text_in_shadow_root(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            host = await tab.find(id='shadow-host')\n            shadow = await host.get_shadow_root()\n\n            text_el = await shadow.query('.shadow-text')\n            assert isinstance(text_el, WebElement)\n            text = await text_el.text\n            assert text == 'Content inside shadow root'\n\n\nclass TestClickInNestedShadowRoots:\n    \"\"\"click() on elements inside nested shadow roots (outer open -> inner closed).\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_click_button_in_nested_shadow(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            outer_host = await tab.find(id='nested-shadow-host')\n            outer_shadow = await outer_host.get_shadow_root()\n\n            inner_host = await outer_shadow.query('#inner-shadow-host')\n            inner_shadow = await inner_host.get_shadow_root()\n\n            btn = await inner_shadow.query('#deep-btn')\n            counter = await inner_shadow.query('#deep-btn-count')\n\n            text_before = await counter.text\n            assert text_before == '0'\n\n            await btn.click()\n            await asyncio.sleep(0.2)\n\n            text_after = await counter.text\n            assert text_after == '1'\n\n    @pytest.mark.asyncio\n    async def test_find_text_in_nested_shadow(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            outer_host = await tab.find(id='nested-shadow-host')\n            outer_shadow = await outer_host.get_shadow_root()\n\n            outer_text = await outer_shadow.query('.outer-text')\n            assert 'Outer shadow content' == await outer_text.text\n\n            inner_host = await outer_shadow.query('#inner-shadow-host')\n            inner_shadow = await inner_host.get_shadow_root()\n\n            inner_text = await inner_shadow.query('.inner-text')\n            assert 'Inner shadow content' == await inner_text.text\n\n\nclass TestClickInIframe:\n    \"\"\"click() on elements inside an iframe.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_click_button_in_iframe(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(1)\n\n            iframe = await tab.find(id='test-iframe')\n            assert iframe.is_iframe\n\n            btn = await iframe.find(id='iframe-btn')\n            counter = await iframe.find(id='iframe-btn-count')\n\n            text_before = await counter.text\n            assert text_before == '0'\n\n            await btn.click()\n            await asyncio.sleep(0.3)\n\n            text_after = await counter.text\n            assert text_after == '1'\n\n\nclass TestClickInShadowRootInsideIframe:\n    \"\"\"click() on elements in a shadow root that lives inside an iframe.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_click_shadow_button_inside_iframe(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(1)\n\n            iframe = await tab.find(id='test-iframe')\n            shadow_host = await iframe.find(id='shadow-host-in-iframe')\n            shadow = await shadow_host.get_shadow_root()\n\n            btn = await shadow.query('#shadow-btn-in-iframe')\n            counter = await shadow.query('#shadow-btn-count')\n\n            text_before = await counter.text\n            assert text_before == '0'\n\n            await btn.click()\n            await asyncio.sleep(0.3)\n\n            text_after = await counter.text\n            assert text_after == '1'\n\n    @pytest.mark.asyncio\n    async def test_find_text_in_shadow_inside_iframe(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(1)\n\n            iframe = await tab.find(id='test-iframe')\n            shadow_host = await iframe.find(id='shadow-host-in-iframe')\n            shadow = await shadow_host.get_shadow_root()\n\n            text_el = await shadow.query('.shadow-text')\n            text = await text_el.text\n            assert text == 'Shadow content inside iframe'\n"
  },
  {
    "path": "tests/test_commands/test_browser_commands.py",
    "content": "from pydoll.commands.browser_commands import BrowserCommands\nfrom pydoll.protocol.browser.methods import BrowserMethod\nfrom pydoll.protocol.browser.types import (\n    WindowState, \n    PermissionType, \n    DownloadBehavior,\n    BrowserCommandId,\n    PermissionDescriptor,\n    PermissionSetting,\n    PrivacySandboxAPI\n)\n\n\ndef test_get_version():\n    \"\"\"Test get_version command generation.\"\"\"\n    command = BrowserCommands.get_version()\n    \n    assert command['method'] == BrowserMethod.GET_VERSION\n    assert 'params' not in command\n\n\ndef test_reset_permissions_without_context():\n    \"\"\"Test reset_permissions command without browser context.\"\"\"\n    command = BrowserCommands.reset_permissions()\n    \n    assert command['method'] == BrowserMethod.RESET_PERMISSIONS\n    assert command['params'] == {}\n\n\ndef test_reset_permissions_with_context():\n    \"\"\"Test reset_permissions command with browser context.\"\"\"\n    browser_context_id = \"test-context-123\"\n    command = BrowserCommands.reset_permissions(browser_context_id=browser_context_id)\n    \n    assert command['method'] == BrowserMethod.RESET_PERMISSIONS\n    assert command['params']['browserContextId'] == browser_context_id\n\n\ndef test_cancel_download_minimal():\n    \"\"\"Test cancel_download command with minimal parameters.\"\"\"\n    guid = \"download-guid-123\"\n    command = BrowserCommands.cancel_download(guid=guid)\n    \n    assert command['method'] == BrowserMethod.CANCEL_DOWNLOAD\n    assert command['params']['guid'] == guid\n    assert 'browserContextId' not in command['params']\n\n\ndef test_cancel_download_with_context():\n    \"\"\"Test cancel_download command with browser context.\"\"\"\n    guid = \"download-guid-456\"\n    browser_context_id = \"test-context-456\"\n    command = BrowserCommands.cancel_download(\n        guid=guid, \n        browser_context_id=browser_context_id\n    )\n    \n    assert command['method'] == BrowserMethod.CANCEL_DOWNLOAD\n    assert command['params']['guid'] == guid\n    assert command['params']['browserContextId'] == browser_context_id\n\n\ndef test_crash():\n    \"\"\"Test crash command generation.\"\"\"\n    command = BrowserCommands.crash()\n    \n    assert command['method'] == BrowserMethod.CRASH\n    assert 'params' not in command\n\n\ndef test_crash_gpu_process():\n    \"\"\"Test crash_gpu_process command generation.\"\"\"\n    command = BrowserCommands.crash_gpu_process()\n    \n    assert command['method'] == BrowserMethod.CRASH_GPU_PROCESS\n    assert 'params' not in command\n\n\ndef test_set_download_behavior_minimal():\n    \"\"\"Test set_download_behavior with minimal parameters.\"\"\"\n    behavior = DownloadBehavior.ALLOW\n    command = BrowserCommands.set_download_behavior(\n        behavior=behavior,\n        events_enabled=True,\n    )\n    \n    assert command['method'] == BrowserMethod.SET_DOWNLOAD_BEHAVIOR\n    assert command['params']['behavior'] == behavior\n    assert command['params']['eventsEnabled'] is True\n    assert 'downloadPath' not in command['params']\n    assert 'browserContextId' not in command['params']\n\n\ndef test_set_download_behavior_with_path():\n    \"\"\"Test set_download_behavior with download path.\"\"\"\n    behavior = DownloadBehavior.ALLOW\n    download_path = \"/path/to/downloads\"\n    command = BrowserCommands.set_download_behavior(\n        behavior=behavior,\n        download_path=download_path,\n        events_enabled=True,\n    )\n    \n    assert command['method'] == BrowserMethod.SET_DOWNLOAD_BEHAVIOR\n    assert command['params']['behavior'] == behavior\n    assert command['params']['downloadPath'] == download_path\n    assert command['params']['eventsEnabled'] is True\n\n\ndef test_set_download_behavior_full_params():\n    \"\"\"Test set_download_behavior with all parameters.\"\"\"\n    behavior = DownloadBehavior.ALLOW_AND_NAME\n    download_path = \"/custom/download/path\"\n    browser_context_id = \"context-789\"\n    events_enabled = False\n    \n    command = BrowserCommands.set_download_behavior(\n        behavior=behavior,\n        download_path=download_path,\n        browser_context_id=browser_context_id,\n        events_enabled=events_enabled\n    )\n    \n    assert command['method'] == BrowserMethod.SET_DOWNLOAD_BEHAVIOR\n    assert command['params']['behavior'] == behavior\n    assert command['params']['downloadPath'] == download_path\n    assert command['params']['browserContextId'] == browser_context_id\n\n\ndef test_set_download_behavior_default_behavior():\n    \"\"\"Test set_download_behavior with DEFAULT behavior.\"\"\"\n    behavior = DownloadBehavior.DEFAULT\n    command = BrowserCommands.set_download_behavior(behavior=behavior)\n    \n    assert command['method'] == BrowserMethod.SET_DOWNLOAD_BEHAVIOR\n    assert command['params']['behavior'] == behavior\n\n\ndef test_close():\n    \"\"\"Test close command generation.\"\"\"\n    command = BrowserCommands.close()\n    \n    assert command['method'] == BrowserMethod.CLOSE\n    assert 'params' not in command\n\n\ndef test_get_window_for_target():\n    \"\"\"Test get_window_for_target command generation.\"\"\"\n    target_id = \"target-123\"\n    command = BrowserCommands.get_window_for_target(target_id=target_id)\n    \n    assert command['method'] == BrowserMethod.GET_WINDOW_FOR_TARGET\n    assert command['params']['targetId'] == target_id\n\n\ndef test_set_window_bounds():\n    \"\"\"Test set_window_bounds command generation.\"\"\"\n    window_id = 42\n    bounds = {\n        'width': 1920,\n        'height': 1080,\n        'x': 100,\n        'y': 50,\n        'windowState': WindowState.NORMAL\n    }\n    command = BrowserCommands.set_window_bounds(window_id=window_id, bounds=bounds)\n    \n    assert command['method'] == BrowserMethod.SET_WINDOW_BOUNDS\n    assert command['params']['windowId'] == window_id\n    assert command['params']['bounds'] == bounds\n\n\ndef test_set_window_bounds_minimal():\n    \"\"\"Test set_window_bounds with minimal bounds.\"\"\"\n    window_id = 1\n    bounds = {'windowState': WindowState.MAXIMIZED}\n    command = BrowserCommands.set_window_bounds(window_id=window_id, bounds=bounds)\n    \n    assert command['method'] == BrowserMethod.SET_WINDOW_BOUNDS\n    assert command['params']['windowId'] == window_id\n    assert command['params']['bounds'] == bounds\n\n\ndef test_set_window_maximized():\n    \"\"\"Test set_window_maximized command generation.\"\"\"\n    window_id = 5\n    command = BrowserCommands.set_window_maximized(window_id=window_id)\n    \n    assert command['method'] == BrowserMethod.SET_WINDOW_BOUNDS\n    assert command['params']['windowId'] == window_id\n    assert command['params']['bounds']['windowState'] == WindowState.MAXIMIZED\n\n\ndef test_set_window_minimized():\n    \"\"\"Test set_window_minimized command generation.\"\"\"\n    window_id = 10\n    command = BrowserCommands.set_window_minimized(window_id=window_id)\n    assert command['method'] == BrowserMethod.SET_WINDOW_BOUNDS\n    assert command['params']['windowId'] == window_id\n    assert command['params']['bounds']['windowState'] == WindowState.MINIMIZED\n\n\ndef test_grant_permissions_minimal():\n    \"\"\"Test grant_permissions with minimal parameters.\"\"\"\n    permissions = [PermissionType.GEOLOCATION, PermissionType.NOTIFICATIONS]\n    command = BrowserCommands.grant_permissions(permissions=permissions)\n    \n    assert command['method'] == BrowserMethod.GRANT_PERMISSIONS\n    assert command['params']['permissions'] == permissions\n    assert 'origin' not in command['params']\n    assert 'browserContextId' not in command['params']\n\n\ndef test_grant_permissions_with_origin():\n    \"\"\"Test grant_permissions with origin.\"\"\"\n    permissions = [PermissionType.DISPLAY_CAPTURE]\n    origin = \"https://example.com\"\n    command = BrowserCommands.grant_permissions(\n        permissions=permissions,\n        origin=origin\n    )\n    \n    assert command['method'] == BrowserMethod.GRANT_PERMISSIONS\n    assert command['params']['permissions'] == permissions\n    assert command['params']['origin'] == origin\n    assert 'browserContextId' not in command['params']\n\n\ndef test_grant_permissions_full_params():\n    \"\"\"Test grant_permissions with all parameters.\"\"\"\n    permissions = [PermissionType.MIDI, PermissionType.CLIPBOARD_READ_WRITE]\n    origin = \"https://test.example.com\"\n    browser_context_id = \"context-permissions\"\n    \n    command = BrowserCommands.grant_permissions(\n        permissions=permissions,\n        origin=origin,\n        browser_context_id=browser_context_id\n    )\n    \n    assert command['method'] == BrowserMethod.GRANT_PERMISSIONS\n    assert command['params']['permissions'] == permissions\n    assert command['params']['origin'] == origin\n    assert command['params']['browserContextId'] == browser_context_id\n\n\ndef test_grant_permissions_single_permission():\n    \"\"\"Test grant_permissions with single permission.\"\"\"\n    permissions = [PermissionType.PAYMENT_HANDLER]\n    command = BrowserCommands.grant_permissions(permissions=permissions)\n    \n    assert command['method'] == BrowserMethod.GRANT_PERMISSIONS\n    assert command['params']['permissions'] == permissions\n\n\ndef test_grant_permissions_multiple_permissions():\n    \"\"\"Test grant_permissions with multiple permissions.\"\"\"\n    permissions = [\n        PermissionType.GEOLOCATION,\n        PermissionType.NOTIFICATIONS,\n        PermissionType.MIDI\n    ]\n    command = BrowserCommands.grant_permissions(permissions=permissions)\n    \n    assert command['method'] == BrowserMethod.GRANT_PERMISSIONS\n    assert command['params']['permissions'] == permissions\n\n\ndef test_grant_permissions_empty_list():\n    \"\"\"Test grant_permissions with empty permissions list.\"\"\"\n    permissions = []\n    command = BrowserCommands.grant_permissions(permissions=permissions)\n    \n    assert command['method'] == BrowserMethod.GRANT_PERMISSIONS\n    assert command['params']['permissions'] == permissions\n\n\n# Edge cases and additional coverage tests\n\ndef test_window_bounds_with_all_states():\n    \"\"\"Test window bounds with all possible window states.\"\"\"\n    window_id = 1\n    \n    # Test NORMAL state\n    bounds_normal = {'windowState': WindowState.NORMAL}\n    command_normal = BrowserCommands.set_window_bounds(window_id, bounds_normal)\n    assert command_normal['params']['bounds']['windowState'] == WindowState.NORMAL\n    \n    # Test MAXIMIZED state\n    bounds_max = {'windowState': WindowState.MAXIMIZED}\n    command_max = BrowserCommands.set_window_bounds(window_id, bounds_max)\n    assert command_max['params']['bounds']['windowState'] == WindowState.MAXIMIZED\n    \n    # Test MINIMIZED state\n    bounds_min = {'windowState': WindowState.MINIMIZED}\n    command_min = BrowserCommands.set_window_bounds(window_id, bounds_min)\n    assert command_min['params']['bounds']['windowState'] == WindowState.MINIMIZED\n\n\ndef test_download_behaviors():\n    \"\"\"Test all download behavior types.\"\"\"\n    # Test ALLOW\n    command_allow = BrowserCommands.set_download_behavior(DownloadBehavior.ALLOW)\n    assert command_allow['params']['behavior'] == DownloadBehavior.ALLOW\n    \n    # Test ALLOW_AND_NAME\n    command_allow_name = BrowserCommands.set_download_behavior(DownloadBehavior.ALLOW_AND_NAME)\n    assert command_allow_name['params']['behavior'] == DownloadBehavior.ALLOW_AND_NAME\n    \n    # Test DEFAULT\n    command_default = BrowserCommands.set_download_behavior(DownloadBehavior.DEFAULT)\n    assert command_default['params']['behavior'] == DownloadBehavior.DEFAULT\n\n\ndef test_events_enabled_variations():\n    \"\"\"Test set_download_behavior with different events_enabled values.\"\"\"\n    behavior = DownloadBehavior.ALLOW\n    \n    # Test with events_enabled=True (default)\n    command_true = BrowserCommands.set_download_behavior(behavior, events_enabled=True)\n    assert command_true['params']['eventsEnabled'] is True\n    \n    # Test with events_enabled=False\n    command_false = BrowserCommands.set_download_behavior(behavior, events_enabled=False)\n    assert command_false['params'] == {'behavior': behavior, 'eventsEnabled': False}\n\n\ndef test_various_permission_types():\n    \"\"\"Test grant_permissions with various permission types.\"\"\"\n    # Test web-related permissions\n    web_permissions = [\n        PermissionType.GEOLOCATION,\n        PermissionType.NOTIFICATIONS,\n    ]\n    command_web = BrowserCommands.grant_permissions(web_permissions)\n    assert command_web['params']['permissions'] == web_permissions\n\n    # Test storage permissions\n    storage_permissions = [\n        PermissionType.DURABLE_STORAGE,\n        PermissionType.STORAGE_ACCESS\n    ]\n    command_storage = BrowserCommands.grant_permissions(storage_permissions)\n    assert command_storage['params']['permissions'] == storage_permissions\n\n\n# Tests for new/missing methods\n\ndef test_get_browser_command_line():\n    \"\"\"Test get_browser_command_line command generation.\"\"\"\n    command = BrowserCommands.get_browser_command_line()\n    \n    assert command['method'] == BrowserMethod.GET_BROWSER_COMMAND_LINE\n    assert 'params' not in command\n\n\ndef test_get_histograms_minimal():\n    \"\"\"Test get_histograms with minimal parameters.\"\"\"\n    command = BrowserCommands.get_histograms()\n    \n    assert command['method'] == BrowserMethod.GET_HISTOGRAMS\n    assert command['params'] == {}\n\n\ndef test_get_histograms_with_query():\n    \"\"\"Test get_histograms with query parameter.\"\"\"\n    query = \"Memory\"\n    command = BrowserCommands.get_histograms(query=query)\n    \n    assert command['method'] == BrowserMethod.GET_HISTOGRAMS\n    assert command['params']['query'] == query\n    assert 'delta' not in command['params']\n\n\ndef test_get_histograms_with_delta():\n    \"\"\"Test get_histograms with delta parameter.\"\"\"\n    command = BrowserCommands.get_histograms(delta=True)\n    \n    assert command['method'] == BrowserMethod.GET_HISTOGRAMS\n    assert command['params']['delta'] is True\n    assert 'query' not in command['params']\n\n\ndef test_get_histograms_with_all_params():\n    \"\"\"Test get_histograms with all parameters.\"\"\"\n    query = \"Network\"\n    delta = True\n    command = BrowserCommands.get_histograms(query=query, delta=delta)\n    \n    assert command['method'] == BrowserMethod.GET_HISTOGRAMS\n    assert command['params']['query'] == query\n    assert command['params']['delta'] == delta\n\n\ndef test_get_histogram_minimal():\n    \"\"\"Test get_histogram with minimal parameters.\"\"\"\n    name = \"Memory.Browser.TotalPMF\"\n    command = BrowserCommands.get_histogram(name=name)\n    \n    assert command['method'] == BrowserMethod.GET_HISTOGRAM\n    assert command['params']['name'] == name\n    assert 'delta' not in command['params']\n\n\ndef test_get_histogram_with_delta():\n    \"\"\"Test get_histogram with delta parameter.\"\"\"\n    name = \"PageLoad.Timing.NavigationStart\"\n    delta = True\n    command = BrowserCommands.get_histogram(name=name, delta=delta)\n    \n    assert command['method'] == BrowserMethod.GET_HISTOGRAM\n    assert command['params']['name'] == name\n    assert command['params']['delta'] == delta\n\n\ndef test_get_window_bounds():\n    \"\"\"Test get_window_bounds command generation.\"\"\"\n    window_id = 42\n    command = BrowserCommands.get_window_bounds(window_id=window_id)\n    \n    assert command['method'] == BrowserMethod.GET_WINDOW_BOUNDS\n    assert command['params']['windowId'] == window_id\n\n\ndef test_set_contents_size_with_width_only():\n    \"\"\"Test set_contents_size with width only.\"\"\"\n    window_id = 1\n    width = 1920\n    command = BrowserCommands.set_contents_size(window_id=window_id, width=width)\n    \n    assert command['method'] == BrowserMethod.SET_CONTENTS_SIZE\n    assert command['params']['windowId'] == window_id\n    assert command['params']['width'] == width\n    assert 'height' not in command['params']\n\n\ndef test_set_contents_size_with_height_only():\n    \"\"\"Test set_contents_size with height only.\"\"\"\n    window_id = 2\n    height = 1080\n    command = BrowserCommands.set_contents_size(window_id=window_id, height=height)\n    \n    assert command['method'] == BrowserMethod.SET_CONTENTS_SIZE\n    assert command['params']['windowId'] == window_id\n    assert command['params']['height'] == height\n    assert 'width' not in command['params']\n\n\ndef test_set_contents_size_with_both_dimensions():\n    \"\"\"Test set_contents_size with both width and height.\"\"\"\n    window_id = 3\n    width = 1600\n    height = 900\n    command = BrowserCommands.set_contents_size(\n        window_id=window_id, \n        width=width, \n        height=height\n    )\n    \n    assert command['method'] == BrowserMethod.SET_CONTENTS_SIZE\n    assert command['params']['windowId'] == window_id\n    assert command['params']['width'] == width\n    assert command['params']['height'] == height\n\n\ndef test_set_dock_tile_minimal():\n    \"\"\"Test set_dock_tile with no parameters.\"\"\"\n    command = BrowserCommands.set_dock_tile()\n    \n    assert command['method'] == BrowserMethod.SET_DOCK_TILE\n    assert command['params'] == {}\n\n\ndef test_set_dock_tile_with_badge_label():\n    \"\"\"Test set_dock_tile with badge label.\"\"\"\n    badge_label = \"5\"\n    command = BrowserCommands.set_dock_tile(badge_label=badge_label)\n    \n    assert command['method'] == BrowserMethod.SET_DOCK_TILE\n    assert command['params']['badgeLabel'] == badge_label\n    assert 'image' not in command['params']\n\n\ndef test_set_dock_tile_with_image():\n    \"\"\"Test set_dock_tile with image.\"\"\"\n    image = \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAGAz\"\n    command = BrowserCommands.set_dock_tile(image=image)\n    \n    assert command['method'] == BrowserMethod.SET_DOCK_TILE\n    assert command['params']['image'] == image\n    assert 'badgeLabel' not in command['params']\n\n\ndef test_set_dock_tile_with_all_params():\n    \"\"\"Test set_dock_tile with all parameters.\"\"\"\n    badge_label = \"3\"\n    image = \"base64encodedimage\"\n    command = BrowserCommands.set_dock_tile(badge_label=badge_label, image=image)\n    \n    assert command['method'] == BrowserMethod.SET_DOCK_TILE\n    assert command['params']['badgeLabel'] == badge_label\n    assert command['params']['image'] == image\n\n\ndef test_execute_browser_command():\n    \"\"\"Test execute_browser_command command generation.\"\"\"\n    command_id = BrowserCommandId.OPEN_TAB_SEARCH\n    command = BrowserCommands.execute_browser_command(command_id=command_id)\n    \n    assert command['method'] == BrowserMethod.EXECUTE_BROWSER_COMMAND\n    assert command['params']['commandId'] == command_id\n\n\ndef test_add_privacy_sandbox_enrollment_override():\n    \"\"\"Test add_privacy_sandbox_enrollment_override command generation.\"\"\"\n    url = \"https://example.test\"\n    command = BrowserCommands.add_privacy_sandbox_enrollment_override(url=url)\n    \n    assert command['method'] == BrowserMethod.ADD_PRIVACY_SANDBOX_ENROLLMENT_OVERRIDE\n    assert command['params']['url'] == url\n\n\ndef test_add_privacy_sandbox_coordinator_key_config_minimal():\n    \"\"\"Test add_privacy_sandbox_coordinator_key_config with minimal parameters.\"\"\"\n    api = PrivacySandboxAPI.BIDDING_AND_AUCTION_SERVICES\n    coordinator_origin = \"https://coordinator.test\"\n    key_config = \"test-key-config\"\n    \n    command = BrowserCommands.add_privacy_sandbox_coordinator_key_config(\n        api=api,\n        coordinator_origin=coordinator_origin,\n        key_config=key_config\n    )\n    \n    assert command['method'] == BrowserMethod.ADD_PRIVACY_SANDBOX_COORDINATOR_KEY_CONFIG\n    assert command['params']['api'] == api\n    assert command['params']['coordinatorOrigin'] == coordinator_origin\n    assert command['params']['keyConfig'] == key_config\n    assert 'browserContextId' not in command['params']\n\n\ndef test_add_privacy_sandbox_coordinator_key_config_with_context():\n    \"\"\"Test add_privacy_sandbox_coordinator_key_config with browser context.\"\"\"\n    api = PrivacySandboxAPI.TRUSTED_KEY_VALUE\n    coordinator_origin = \"https://sandbox.test\" \n    key_config = \"config-data\"\n    browser_context_id = \"test-context\"\n    \n    command = BrowserCommands.add_privacy_sandbox_coordinator_key_config(\n        api=api,\n        coordinator_origin=coordinator_origin,\n        key_config=key_config,\n        browser_context_id=browser_context_id\n    )\n    \n    assert command['method'] == BrowserMethod.ADD_PRIVACY_SANDBOX_COORDINATOR_KEY_CONFIG\n    assert command['params']['api'] == api\n    assert command['params']['coordinatorOrigin'] == coordinator_origin\n    assert command['params']['keyConfig'] == key_config\n    assert command['params']['browserContextId'] == browser_context_id\n\n\ndef test_set_permission_minimal():\n    \"\"\"Test set_permission with minimal parameters.\"\"\"\n    permission = PermissionDescriptor(name=PermissionType.GEOLOCATION)\n    setting = PermissionSetting.GRANTED\n    \n    command = BrowserCommands.set_permission(\n        permission=permission,\n        setting=setting\n    )\n    \n    assert command['method'] == BrowserMethod.SET_PERMISSION\n    assert command['params']['permission'] == permission\n    assert command['params']['setting'] == setting\n    assert 'origin' not in command['params']\n    assert 'browserContextId' not in command['params']\n\n\ndef test_set_permission_with_origin():\n    \"\"\"Test set_permission with origin.\"\"\"\n    permission = PermissionDescriptor(name=PermissionType.NOTIFICATIONS)\n    setting = PermissionSetting.DENIED\n    origin = \"https://example.com\"\n    \n    command = BrowserCommands.set_permission(\n        permission=permission,\n        setting=setting,\n        origin=origin\n    )\n    \n    assert command['method'] == BrowserMethod.SET_PERMISSION\n    assert command['params']['permission'] == permission\n    assert command['params']['setting'] == setting\n    assert command['params']['origin'] == origin\n    assert 'browserContextId' not in command['params']\n\n\ndef test_set_permission_with_all_params():\n    \"\"\"Test set_permission with all parameters.\"\"\"\n    permission = PermissionDescriptor(name=PermissionType.MIDI)\n    setting = PermissionSetting.PROMPT\n    origin = \"https://test.example.com\"\n    browser_context_id = \"permission-context\"\n    \n    command = BrowserCommands.set_permission(\n        permission=permission,\n        setting=setting,\n        origin=origin,\n        browser_context_id=browser_context_id\n    )\n    \n    assert command['method'] == BrowserMethod.SET_PERMISSION\n    assert command['params']['permission'] == permission\n    assert command['params']['setting'] == setting\n    assert command['params']['origin'] == origin\n    assert command['params']['browserContextId'] == browser_context_id\n\n\ndef test_set_window_fullscreen():\n    \"\"\"Test set_window_fullscreen command generation.\"\"\"\n    window_id = 7\n    command = BrowserCommands.set_window_fullscreen(window_id=window_id)\n    \n    assert command['method'] == BrowserMethod.SET_WINDOW_BOUNDS\n    assert command['params']['windowId'] == window_id\n    assert command['params']['bounds']['windowState'] == WindowState.FULLSCREEN\n\n\ndef test_set_window_normal():\n    \"\"\"Test set_window_normal command generation.\"\"\"\n    window_id = 8\n    command = BrowserCommands.set_window_normal(window_id=window_id)\n    \n    assert command['method'] == BrowserMethod.SET_WINDOW_BOUNDS\n    assert command['params']['windowId'] == window_id\n    assert command['params']['bounds']['windowState'] == WindowState.NORMAL\n\n\n# Additional edge case and integration tests\n\ndef test_all_window_state_helpers():\n    \"\"\"Test all window state helper methods.\"\"\"\n    window_id = 99\n    \n    # Test all window state helpers return correct commands\n    maximized = BrowserCommands.set_window_maximized(window_id)\n    minimized = BrowserCommands.set_window_minimized(window_id)\n    fullscreen = BrowserCommands.set_window_fullscreen(window_id)\n    normal = BrowserCommands.set_window_normal(window_id)\n    \n    # All should use the same method\n    for command in [maximized, minimized, fullscreen, normal]:\n        assert command['method'] == BrowserMethod.SET_WINDOW_BOUNDS\n        assert command['params']['windowId'] == window_id\n    \n    # Check specific states\n    assert maximized['params']['bounds']['windowState'] == WindowState.MAXIMIZED\n    assert minimized['params']['bounds']['windowState'] == WindowState.MINIMIZED\n    assert fullscreen['params']['bounds']['windowState'] == WindowState.FULLSCREEN\n    assert normal['params']['bounds']['windowState'] == WindowState.NORMAL\n\n\ndef test_privacy_sandbox_apis():\n    \"\"\"Test privacy sandbox with different API types.\"\"\"\n    coordinator_origin = \"https://api.test\"\n    key_config = \"test-config\"\n    \n    # Test BIDDING_AND_AUCTION_SERVICES API\n    bidding_cmd = BrowserCommands.add_privacy_sandbox_coordinator_key_config(\n        api=PrivacySandboxAPI.BIDDING_AND_AUCTION_SERVICES,\n        coordinator_origin=coordinator_origin,\n        key_config=key_config\n    )\n    assert bidding_cmd['params']['api'] == PrivacySandboxAPI.BIDDING_AND_AUCTION_SERVICES\n    \n    # Test TRUSTED_KEY_VALUE API  \n    trusted_key_cmd = BrowserCommands.add_privacy_sandbox_coordinator_key_config(\n        api=PrivacySandboxAPI.TRUSTED_KEY_VALUE,\n        coordinator_origin=coordinator_origin,\n        key_config=key_config\n    )\n    assert trusted_key_cmd['params']['api'] == PrivacySandboxAPI.TRUSTED_KEY_VALUE\n\n\ndef test_permission_settings_variations():\n    \"\"\"Test set_permission with different permission settings.\"\"\"\n    permission = PermissionDescriptor(name=PermissionType.GEOLOCATION)\n    \n    # Test GRANTED setting\n    granted_cmd = BrowserCommands.set_permission(permission, PermissionSetting.GRANTED)\n    assert granted_cmd['params']['setting'] == PermissionSetting.GRANTED\n    \n    # Test DENIED setting\n    denied_cmd = BrowserCommands.set_permission(permission, PermissionSetting.DENIED)\n    assert denied_cmd['params']['setting'] == PermissionSetting.DENIED\n    \n    # Test PROMPT setting\n    prompt_cmd = BrowserCommands.set_permission(permission, PermissionSetting.PROMPT)\n    assert prompt_cmd['params']['setting'] == PermissionSetting.PROMPT\n\n\ndef test_browser_command_ids():\n    \"\"\"Test execute_browser_command with different command IDs.\"\"\"\n    # Test different browser command IDs\n    tab_search_cmd = BrowserCommands.execute_browser_command(BrowserCommandId.OPEN_TAB_SEARCH)\n    assert tab_search_cmd['params']['commandId'] == BrowserCommandId.OPEN_TAB_SEARCH\n"
  },
  {
    "path": "tests/test_commands/test_dom_commands.py",
    "content": "from pydoll.commands.dom_commands import DomCommands\nfrom pydoll.protocol.dom.methods import DomMethod\nfrom pydoll.protocol.dom.types import IncludeWhitespace, LogicalAxes, PhysicalAxes, RelationType\n\nclass TestDomCommands:\n    \"\"\"Tests for the DomCommands class.\"\"\"\n\n    def test_describe_node_with_node_id(self):\n        \"\"\"Test describe_node command with node_id.\"\"\"\n        result = DomCommands.describe_node(node_id=123)\n        \n        assert result['method'] == DomMethod.DESCRIBE_NODE\n        assert result['params']['nodeId'] == 123\n\n    def test_describe_node_with_backend_node_id(self):\n        \"\"\"Test describe_node command with backend_node_id.\"\"\"\n        result = DomCommands.describe_node(backend_node_id=456)\n        \n        assert result['method'] == DomMethod.DESCRIBE_NODE\n        assert result['params']['backendNodeId'] == 456\n\n    def test_describe_node_with_object_id(self):\n        \"\"\"Test describe_node command with object_id.\"\"\"\n        result = DomCommands.describe_node(object_id='obj123')\n        \n        assert result['method'] == DomMethod.DESCRIBE_NODE\n        assert result['params']['objectId'] == 'obj123'\n\n    def test_describe_node_with_all_params(self):\n        \"\"\"Test describe_node command with all parameters.\"\"\"\n        result = DomCommands.describe_node(\n            node_id=123,\n            backend_node_id=456,\n            object_id='obj123',\n            depth=2,\n            pierce=True\n        )\n        \n        assert result['method'] == DomMethod.DESCRIBE_NODE\n        assert result['params']['nodeId'] == 123\n        assert result['params']['backendNodeId'] == 456\n        assert result['params']['objectId'] == 'obj123'\n        assert result['params']['depth'] == 2\n        assert result['params']['pierce'] is True\n\n    def test_disable(self):\n        \"\"\"Test disable command.\"\"\"\n        result = DomCommands.disable()\n        \n        assert result['method'] == DomMethod.DISABLE\n        assert 'params' not in result\n\n    def test_enable_without_params(self):\n        \"\"\"Test enable command without parameters.\"\"\"\n        result = DomCommands.enable()\n        \n        assert result['method'] == DomMethod.ENABLE\n        assert 'params' in result\n\n    def test_enable_with_include_whitespace(self):\n        \"\"\"Test enable command with include_whitespace.\"\"\"\n        result = DomCommands.enable(include_whitespace=IncludeWhitespace.ALL)\n        \n        assert result['method'] == DomMethod.ENABLE\n        assert result['params']['includeWhitespace'] == IncludeWhitespace.ALL\n\n    def test_focus_with_node_id(self):\n        \"\"\"Test focus command with node_id.\"\"\"\n        result = DomCommands.focus(node_id=123)\n        \n        assert result['method'] == DomMethod.FOCUS\n        assert result['params']['nodeId'] == 123\n\n    def test_focus_with_backend_node_id(self):\n        \"\"\"Test focus command with backend_node_id.\"\"\"\n        result = DomCommands.focus(backend_node_id=456)\n        \n        assert result['method'] == DomMethod.FOCUS\n        assert result['params']['backendNodeId'] == 456\n\n    def test_focus_with_object_id(self):\n        \"\"\"Test focus command with object_id.\"\"\"\n        result = DomCommands.focus(object_id='obj123')\n        \n        assert result['method'] == DomMethod.FOCUS\n        assert result['params']['objectId'] == 'obj123'\n\n    def test_get_attributes(self):\n        \"\"\"Test get_attributes command.\"\"\"\n        result = DomCommands.get_attributes(node_id=123)\n        \n        assert result['method'] == DomMethod.GET_ATTRIBUTES\n        assert result['params']['nodeId'] == 123\n\n    def test_get_box_model_with_node_id(self):\n        \"\"\"Test get_box_model command with node_id.\"\"\"\n        result = DomCommands.get_box_model(node_id=123)\n        \n        assert result['method'] == DomMethod.GET_BOX_MODEL\n        assert result['params']['nodeId'] == 123\n\n    def test_get_box_model_with_backend_node_id(self):\n        \"\"\"Test get_box_model command with backend_node_id.\"\"\"\n        result = DomCommands.get_box_model(backend_node_id=456)\n        \n        assert result['method'] == DomMethod.GET_BOX_MODEL\n        assert result['params']['backendNodeId'] == 456\n\n    def test_get_box_model_with_object_id(self):\n        \"\"\"Test get_box_model command with object_id.\"\"\"\n        result = DomCommands.get_box_model(object_id='obj123')\n        \n        assert result['method'] == DomMethod.GET_BOX_MODEL\n        assert result['params']['objectId'] == 'obj123'\n\n    def test_get_document_without_params(self):\n        \"\"\"Test get_document command without parameters.\"\"\"\n        result = DomCommands.get_document()\n        \n        assert result['method'] == DomMethod.GET_DOCUMENT\n        assert 'params' in result\n\n    def test_get_document_with_depth(self):\n        \"\"\"Test get_document command with depth.\"\"\"\n        result = DomCommands.get_document(depth=2)\n        \n        assert result['method'] == DomMethod.GET_DOCUMENT\n        assert result['params']['depth'] == 2\n\n    def test_get_document_with_pierce(self):\n        \"\"\"Test get_document command with pierce.\"\"\"\n        result = DomCommands.get_document(pierce=True)\n        \n        assert result['method'] == DomMethod.GET_DOCUMENT\n        assert result['params']['pierce'] is True\n\n    def test_get_node_for_location(self):\n        \"\"\"Test get_node_for_location command.\"\"\"\n        result = DomCommands.get_node_for_location(x=100, y=200)\n        \n        assert result['method'] == DomMethod.GET_NODE_FOR_LOCATION\n        assert result['params']['x'] == 100\n        assert result['params']['y'] == 200\n\n    def test_get_node_for_location_with_optional_params(self):\n        \"\"\"Test get_node_for_location command with optional parameters.\"\"\"\n        result = DomCommands.get_node_for_location(\n            x=100, \n            y=200,\n            include_user_agent_shadow_dom=True,\n            ignore_pointer_events_none=False\n        )\n        \n        assert result['method'] == DomMethod.GET_NODE_FOR_LOCATION\n        assert result['params']['x'] == 100\n        assert result['params']['y'] == 200\n        assert result['params']['includeUserAgentShadowDOM'] is True\n        assert result['params']['ignorePointerEventsNone'] is False\n\n    def test_get_outer_html_with_node_id(self):\n        \"\"\"Test get_outer_html command with node_id.\"\"\"\n        result = DomCommands.get_outer_html(node_id=123)\n        \n        assert result['method'] == DomMethod.GET_OUTER_HTML\n        assert result['params']['nodeId'] == 123\n\n    def test_get_outer_html_with_backend_node_id(self):\n        \"\"\"Test get_outer_html command with backend_node_id.\"\"\"\n        result = DomCommands.get_outer_html(backend_node_id=456)\n        \n        assert result['method'] == DomMethod.GET_OUTER_HTML\n        assert result['params']['backendNodeId'] == 456\n\n    def test_get_outer_html_with_object_id(self):\n        \"\"\"Test get_outer_html command with object_id.\"\"\"\n        result = DomCommands.get_outer_html(object_id='obj123')\n        \n        assert result['method'] == DomMethod.GET_OUTER_HTML\n        assert result['params']['objectId'] == 'obj123'\n\n    def test_hide_highlight(self):\n        \"\"\"Test hide_highlight command.\"\"\"\n        result = DomCommands.hide_highlight()\n        \n        assert result['method'] == DomMethod.HIDE_HIGHLIGHT\n        assert 'params' not in result\n\n    def test_highlight_node(self):\n        \"\"\"Test highlight_node command.\"\"\"\n        result = DomCommands.highlight_node()\n        \n        assert result['method'] == DomMethod.HIGHLIGHT_NODE\n        assert 'params' not in result\n\n    def test_highlight_rect(self):\n        \"\"\"Test highlight_rect command.\"\"\"\n        result = DomCommands.highlight_rect()\n        \n        assert result['method'] == DomMethod.HIGHLIGHT_RECT\n        assert 'params' not in result\n\n    def test_move_to(self):\n        \"\"\"Test move_to command.\"\"\"\n        result = DomCommands.move_to(node_id=123, target_node_id=456)\n        \n        assert result['method'] == DomMethod.MOVE_TO\n        assert result['params']['nodeId'] == 123\n        assert result['params']['targetNodeId'] == 456\n\n    def test_move_to_with_insert_before(self):\n        \"\"\"Test move_to command with insert_before_node_id.\"\"\"\n        result = DomCommands.move_to(\n            node_id=123, \n            target_node_id=456, \n            insert_before_node_id=789\n        )\n        \n        assert result['method'] == DomMethod.MOVE_TO\n        assert result['params']['nodeId'] == 123\n        assert result['params']['targetNodeId'] == 456\n        assert result['params']['insertBeforeNodeId'] == 789\n\n    def test_query_selector(self):\n        \"\"\"Test query_selector command.\"\"\"\n        result = DomCommands.query_selector(node_id=123, selector='.test-class')\n        \n        assert result['method'] == DomMethod.QUERY_SELECTOR\n        assert result['params']['nodeId'] == 123\n        assert result['params']['selector'] == '.test-class'\n\n    def test_query_selector_all(self):\n        \"\"\"Test query_selector_all command.\"\"\"\n        result = DomCommands.query_selector_all(node_id=123, selector='div')\n        \n        assert result['method'] == DomMethod.QUERY_SELECTOR_ALL\n        assert result['params']['nodeId'] == 123\n        assert result['params']['selector'] == 'div'\n\n    def test_remove_attribute(self):\n        \"\"\"Test remove_attribute command.\"\"\"\n        result = DomCommands.remove_attribute(node_id=123, name='class')\n        \n        assert result['method'] == DomMethod.REMOVE_ATTRIBUTE\n        assert result['params']['nodeId'] == 123\n        assert result['params']['name'] == 'class'\n\n    def test_remove_node(self):\n        \"\"\"Test remove_node command.\"\"\"\n        result = DomCommands.remove_node(node_id=123)\n        \n        assert result['method'] == DomMethod.REMOVE_NODE\n        assert result['params']['nodeId'] == 123\n\n    def test_request_child_nodes(self):\n        \"\"\"Test request_child_nodes command.\"\"\"\n        result = DomCommands.request_child_nodes(node_id=123)\n        \n        assert result['method'] == DomMethod.REQUEST_CHILD_NODES\n        assert result['params']['nodeId'] == 123\n\n    def test_request_child_nodes_with_depth(self):\n        \"\"\"Test request_child_nodes command with depth.\"\"\"\n        result = DomCommands.request_child_nodes(node_id=123, depth=2)\n        \n        assert result['method'] == DomMethod.REQUEST_CHILD_NODES\n        assert result['params']['nodeId'] == 123\n        assert result['params']['depth'] == 2\n\n    def test_request_child_nodes_with_pierce(self):\n        \"\"\"Test request_child_nodes command with pierce.\"\"\"\n        result = DomCommands.request_child_nodes(node_id=123, pierce=True)\n        \n        assert result['method'] == DomMethod.REQUEST_CHILD_NODES\n        assert result['params']['nodeId'] == 123\n        assert result['params']['pierce'] is True\n\n    def test_request_node(self):\n        \"\"\"Test request_node command.\"\"\"\n        result = DomCommands.request_node(object_id='obj123')\n        \n        assert result['method'] == DomMethod.REQUEST_NODE\n        assert result['params']['objectId'] == 'obj123'\n\n    def test_resolve_node_with_node_id(self):\n        \"\"\"Test resolve_node command with node_id.\"\"\"\n        result = DomCommands.resolve_node(node_id=123)\n        \n        assert result['method'] == DomMethod.RESOLVE_NODE\n        assert result['params']['nodeId'] == 123\n\n    def test_resolve_node_with_backend_node_id(self):\n        \"\"\"Test resolve_node command with backend_node_id.\"\"\"\n        result = DomCommands.resolve_node(backend_node_id=456)\n        \n        assert result['method'] == DomMethod.RESOLVE_NODE\n        assert result['params']['backendNodeId'] == 456\n\n    def test_resolve_node_with_all_params(self):\n        \"\"\"Test resolve_node command with all parameters.\"\"\"\n        result = DomCommands.resolve_node(\n            node_id=123,\n            backend_node_id=456,\n            object_group='test-group',\n            execution_context_id=789\n        )\n        \n        assert result['method'] == DomMethod.RESOLVE_NODE\n        assert result['params']['nodeId'] == 123\n        assert result['params']['backendNodeId'] == 456\n        assert result['params']['objectGroup'] == 'test-group'\n        assert result['params']['executionContextId'] == 789\n\n    def test_scroll_into_view_if_needed_with_node_id(self):\n        \"\"\"Test scroll_into_view_if_needed command with node_id.\"\"\"\n        result = DomCommands.scroll_into_view_if_needed(node_id=123)\n        \n        assert result['method'] == DomMethod.SCROLL_INTO_VIEW_IF_NEEDED\n        assert result['params']['nodeId'] == 123\n\n    def test_scroll_into_view_if_needed_with_backend_node_id(self):\n        \"\"\"Test scroll_into_view_if_needed command with backend_node_id.\"\"\"\n        result = DomCommands.scroll_into_view_if_needed(backend_node_id=456)\n        \n        assert result['method'] == DomMethod.SCROLL_INTO_VIEW_IF_NEEDED\n        assert result['params']['backendNodeId'] == 456\n\n    def test_scroll_into_view_if_needed_with_object_id(self):\n        \"\"\"Test scroll_into_view_if_needed command with object_id.\"\"\"\n        result = DomCommands.scroll_into_view_if_needed(object_id='obj123')\n        \n        assert result['method'] == DomMethod.SCROLL_INTO_VIEW_IF_NEEDED\n        assert result['params']['objectId'] == 'obj123'\n\n    def test_set_attributes_as_text(self):\n        \"\"\"Test set_attributes_as_text command.\"\"\"\n        result = DomCommands.set_attributes_as_text(node_id=123, text='class=\"test\"')\n        \n        assert result['method'] == DomMethod.SET_ATTRIBUTES_AS_TEXT\n        assert result['params']['nodeId'] == 123\n        assert result['params']['text'] == 'class=\"test\"'\n\n    def test_set_attributes_as_text_with_name(self):\n        \"\"\"Test set_attributes_as_text command with name.\"\"\"\n        result = DomCommands.set_attributes_as_text(\n            node_id=123, \n            text='test-value', \n            name='class'\n        )\n        \n        assert result['method'] == DomMethod.SET_ATTRIBUTES_AS_TEXT\n        assert result['params']['nodeId'] == 123\n        assert result['params']['text'] == 'test-value'\n        assert result['params']['name'] == 'class'\n\n    def test_set_attribute_value(self):\n        \"\"\"Test set_attribute_value command.\"\"\"\n        result = DomCommands.set_attribute_value(\n            node_id=123, \n            name='class', \n            value='test-class'\n        )\n        \n        assert result['method'] == DomMethod.SET_ATTRIBUTE_VALUE\n        assert result['params']['nodeId'] == 123\n        assert result['params']['name'] == 'class'\n        assert result['params']['value'] == 'test-class'\n\n    def test_set_file_input_files_with_node_id(self):\n        \"\"\"Test set_file_input_files command with node_id.\"\"\"\n        files = ['/path/to/file1.txt', '/path/to/file2.txt']\n        result = DomCommands.set_file_input_files(files=files, node_id=123)\n        \n        assert result['method'] == DomMethod.SET_FILE_INPUT_FILES\n        assert result['params']['files'] == files\n        assert result['params']['nodeId'] == 123\n\n    def test_set_file_input_files_with_backend_node_id(self):\n        \"\"\"Test set_file_input_files command with backend_node_id.\"\"\"\n        files = ['/path/to/file.txt']\n        result = DomCommands.set_file_input_files(files=files, backend_node_id=456)\n        \n        assert result['method'] == DomMethod.SET_FILE_INPUT_FILES\n        assert result['params']['files'] == files\n        assert result['params']['backendNodeId'] == 456\n\n    def test_set_file_input_files_with_object_id(self):\n        \"\"\"Test set_file_input_files command with object_id.\"\"\"\n        files = ['/path/to/file.txt']\n        result = DomCommands.set_file_input_files(files=files, object_id='obj123')\n        \n        assert result['method'] == DomMethod.SET_FILE_INPUT_FILES\n        assert result['params']['files'] == files\n        assert result['params']['objectId'] == 'obj123'\n\n    def test_set_node_name(self):\n        \"\"\"Test set_node_name command.\"\"\"\n        result = DomCommands.set_node_name(node_id=123, name='div')\n        \n        assert result['method'] == DomMethod.SET_NODE_NAME\n        assert result['params']['nodeId'] == 123\n        assert result['params']['name'] == 'div'\n\n    def test_set_node_value(self):\n        \"\"\"Test set_node_value command.\"\"\"\n        result = DomCommands.set_node_value(node_id=123, value='test text')\n        \n        assert result['method'] == DomMethod.SET_NODE_VALUE\n        assert result['params']['nodeId'] == 123\n        assert result['params']['value'] == 'test text'\n\n    def test_set_outer_html(self):\n        \"\"\"Test set_outer_html command.\"\"\"\n        html = '<div class=\"test\">content</div>'\n        result = DomCommands.set_outer_html(node_id=123, outer_html=html)\n        \n        assert result['method'] == DomMethod.SET_OUTER_HTML\n        assert result['params']['nodeId'] == 123\n        assert result['params']['outerHTML'] == html\n\n    def test_collect_class_names_from_subtree(self):\n        \"\"\"Test collect_class_names_from_subtree command.\"\"\"\n        result = DomCommands.collect_class_names_from_subtree(node_id=123)\n        \n        assert result['method'] == DomMethod.COLLECT_CLASS_NAMES_FROM_SUBTREE\n        assert result['params']['nodeId'] == 123\n\n    def test_copy_to(self):\n        \"\"\"Test copy_to command.\"\"\"\n        result = DomCommands.copy_to(node_id=123, target_node_id=456)\n        \n        assert result['method'] == DomMethod.COPY_TO\n        assert result['params']['nodeId'] == 123\n        assert result['params']['targetNodeId'] == 456\n\n    def test_copy_to_with_insert_before(self):\n        \"\"\"Test copy_to command with insert_before_node_id.\"\"\"\n        result = DomCommands.copy_to(\n            node_id=123, \n            target_node_id=456, \n            insert_before_node_id=789\n        )\n        \n        assert result['method'] == DomMethod.COPY_TO\n        assert result['params']['nodeId'] == 123\n        assert result['params']['targetNodeId'] == 456\n        assert result['params']['insertBeforeNodeId'] == 789\n\n    def test_discard_search_results(self):\n        \"\"\"Test discard_search_results command.\"\"\"\n        result = DomCommands.discard_search_results(search_id='search123')\n        \n        assert result['method'] == DomMethod.DISCARD_SEARCH_RESULTS\n        assert result['params']['searchId'] == 'search123'\n\n    def test_get_anchor_element(self):\n        \"\"\"Test get_anchor_element command.\"\"\"\n        result = DomCommands.get_anchor_element(node_id=123)\n        \n        assert result['method'] == DomMethod.GET_ANCHOR_ELEMENT\n        assert result['params']['nodeId'] == 123\n\n    def test_get_anchor_element_with_specifier(self):\n        \"\"\"Test get_anchor_element command with anchor_specifier.\"\"\"\n        result = DomCommands.get_anchor_element(\n            node_id=123, \n            anchor_specifier='href'\n        )\n        \n        assert result['method'] == DomMethod.GET_ANCHOR_ELEMENT\n        assert result['params']['nodeId'] == 123\n        assert result['params']['anchorSpecifier'] == 'href'\n\n    def test_get_container_for_node(self):\n        \"\"\"Test get_container_for_node command.\"\"\"\n        result = DomCommands.get_container_for_node(node_id=123)\n        \n        assert result['method'] == DomMethod.GET_CONTAINER_FOR_NODE\n        assert result['params']['nodeId'] == 123\n\n    def test_get_container_for_node_with_all_params(self):\n        \"\"\"Test get_container_for_node command with all parameters.\"\"\"\n        result = DomCommands.get_container_for_node(\n            node_id=123,\n            container_name='scrollable',\n            physical_axes=PhysicalAxes.HORIZONTAL,\n            logical_axes=LogicalAxes.INLINE,\n            queries_scroll_state=True\n        )\n        \n        assert result['method'] == DomMethod.GET_CONTAINER_FOR_NODE\n        assert result['params']['nodeId'] == 123\n        assert result['params']['containerName'] == 'scrollable'\n        assert result['params']['physicalAxes'] == PhysicalAxes.HORIZONTAL\n        assert result['params']['logicalAxes'] == LogicalAxes.INLINE\n        assert result['params']['queriesScrollState'] is True\n\n    def test_get_content_quads_with_node_id(self):\n        \"\"\"Test get_content_quads command with node_id.\"\"\"\n        result = DomCommands.get_content_quads(node_id=123)\n        \n        assert result['method'] == DomMethod.GET_CONTENT_QUADS\n        assert result['params']['nodeId'] == 123\n\n    def test_get_content_quads_with_backend_node_id(self):\n        \"\"\"Test get_content_quads command with backend_node_id.\"\"\"\n        result = DomCommands.get_content_quads(backend_node_id=456)\n        \n        assert result['method'] == DomMethod.GET_CONTENT_QUADS\n        assert result['params']['backendNodeId'] == 456\n\n    def test_get_content_quads_with_object_id(self):\n        \"\"\"Test get_content_quads command with object_id.\"\"\"\n        result = DomCommands.get_content_quads(object_id='obj123')\n        \n        assert result['method'] == DomMethod.GET_CONTENT_QUADS\n        assert result['params']['objectId'] == 'obj123'\n\n    def test_get_detached_dom_nodes(self):\n        \"\"\"Test get_detached_dom_nodes command.\"\"\"\n        result = DomCommands.get_detached_dom_nodes()\n        \n        assert result['method'] == DomMethod.GET_DETACHED_DOM_NODES\n        assert 'params' not in result\n\n    def test_get_element_by_relation(self):\n        \"\"\"Test get_element_by_relation command.\"\"\"\n        result = DomCommands.get_element_by_relation(\n            node_id=123, \n            relation=RelationType.INTEREST_TARGET\n        )\n        \n        assert result['method'] == DomMethod.GET_ELEMENT_BY_RELATION\n        assert result['params']['nodeId'] == 123\n        assert result['params']['relation'] == RelationType.INTEREST_TARGET\n\n    def test_get_file_info(self):\n        \"\"\"Test get_file_info command.\"\"\"\n        result = DomCommands.get_file_info(object_id='file123')\n        \n        assert result['method'] == DomMethod.GET_FILE_INFO\n        assert result['params']['objectId'] == 'file123'\n\n    def test_get_frame_owner(self):\n        \"\"\"Test get_frame_owner command.\"\"\"\n        result = DomCommands.get_frame_owner(frame_id='frame123')\n        \n        assert result['method'] == DomMethod.GET_FRAME_OWNER\n        assert result['params']['frameId'] == 'frame123'\n\n    def test_get_nodes_for_subtree_by_style(self):\n        \"\"\"Test get_nodes_for_subtree_by_style command.\"\"\"\n        computed_styles = [{'name': 'color', 'value': 'red'}]\n        result = DomCommands.get_nodes_for_subtree_by_style(\n            node_id=123, \n            computed_styles=computed_styles\n        )\n        \n        assert result['method'] == DomMethod.GET_NODES_FOR_SUBTREE_BY_STYLE\n        assert result['params']['nodeId'] == 123\n        assert result['params']['computedStyles'] == computed_styles\n\n    def test_get_nodes_for_subtree_by_style_with_pierce(self):\n        \"\"\"Test get_nodes_for_subtree_by_style command with pierce.\"\"\"\n        computed_styles = [{'name': 'display', 'value': 'block'}]\n        result = DomCommands.get_nodes_for_subtree_by_style(\n            node_id=123, \n            computed_styles=computed_styles,\n            pierce=True\n        )\n        \n        assert result['method'] == DomMethod.GET_NODES_FOR_SUBTREE_BY_STYLE\n        assert result['params']['nodeId'] == 123\n        assert result['params']['computedStyles'] == computed_styles\n        assert result['params']['pierce'] is True\n\n    def test_get_node_stack_traces(self):\n        \"\"\"Test get_node_stack_traces command.\"\"\"\n        result = DomCommands.get_node_stack_traces(node_id=123)\n        \n        assert result['method'] == DomMethod.GET_NODE_STACK_TRACES\n        assert result['params']['nodeId'] == 123\n\n    def test_get_querying_descendants_for_container(self):\n        \"\"\"Test get_querying_descendants_for_container command.\"\"\"\n        result = DomCommands.get_querying_descendants_for_container(node_id=123)\n        \n        assert result['method'] == DomMethod.GET_QUERYING_DESCENDANTS_FOR_CONTAINER\n        assert result['params']['nodeId'] == 123\n\n    def test_get_relayout_boundary(self):\n        \"\"\"Test get_relayout_boundary command.\"\"\"\n        result = DomCommands.get_relayout_boundary(node_id=123)\n        \n        assert result['method'] == DomMethod.GET_RELAYOUT_BOUNDARY\n        assert result['params']['nodeId'] == 123\n\n    def test_get_search_results(self):\n        \"\"\"Test get_search_results command.\"\"\"\n        result = DomCommands.get_search_results(\n            search_id='search123', \n            from_index=0, \n            to_index=10\n        )\n        \n        assert result['method'] == DomMethod.GET_SEARCH_RESULTS\n        assert result['params']['searchId'] == 'search123'\n        assert result['params']['fromIndex'] == 0\n        assert result['params']['toIndex'] == 10\n\n    def test_get_top_layer_elements(self):\n        \"\"\"Test get_top_layer_elements command.\"\"\"\n        result = DomCommands.get_top_layer_elements()\n        \n        assert result['method'] == DomMethod.GET_TOP_LAYER_ELEMENTS\n        assert 'params' not in result\n\n    def test_mark_undoable_state(self):\n        \"\"\"Test mark_undoable_state command.\"\"\"\n        result = DomCommands.mark_undoable_state()\n        \n        assert result['method'] == DomMethod.MARK_UNDOABLE_STATE\n        assert 'params' not in result\n\n    def test_perform_search(self):\n        \"\"\"Test perform_search command.\"\"\"\n        result = DomCommands.perform_search(query='test')\n        \n        assert result['method'] == DomMethod.PERFORM_SEARCH\n        assert result['params']['query'] == 'test'\n\n    def test_perform_search_with_shadow_dom(self):\n        \"\"\"Test perform_search command with include_user_agent_shadow_dom.\"\"\"\n        result = DomCommands.perform_search(\n            query='test', \n            include_user_agent_shadow_dom=True\n        )\n        \n        assert result['method'] == DomMethod.PERFORM_SEARCH\n        assert result['params']['query'] == 'test'\n        assert result['params']['includeUserAgentShadowDOM'] is True\n\n    def test_push_node_by_path_to_frontend(self):\n        \"\"\"Test push_node_by_path_to_frontend command.\"\"\"\n        result = DomCommands.push_node_by_path_to_frontend(path='1,2,3')\n        \n        assert result['method'] == DomMethod.PUSH_NODE_BY_PATH_TO_FRONTEND\n        assert result['params']['path'] == '1,2,3'\n\n    def test_push_nodes_by_backend_ids_to_frontend(self):\n        \"\"\"Test push_nodes_by_backend_ids_to_frontend command.\"\"\"\n        backend_ids = [123, 456, 789]\n        result = DomCommands.push_nodes_by_backend_ids_to_frontend(\n            backend_node_ids=backend_ids\n        )\n        \n        assert result['method'] == DomMethod.PUSH_NODES_BY_BACKEND_IDS_TO_FRONTEND\n        assert result['params']['backendNodeIds'] == backend_ids\n\n    def test_redo(self):\n        \"\"\"Test redo command.\"\"\"\n        result = DomCommands.redo()\n        \n        assert result['method'] == DomMethod.REDO\n        assert 'params' not in result\n\n    def test_set_inspected_node(self):\n        \"\"\"Test set_inspected_node command.\"\"\"\n        result = DomCommands.set_inspected_node(node_id=123)\n        \n        assert result['method'] == DomMethod.SET_INSPECTED_NODE\n        assert result['params']['nodeId'] == 123\n\n    def test_set_node_stack_traces_enabled(self):\n        \"\"\"Test set_node_stack_traces_enabled command.\"\"\"\n        result = DomCommands.set_node_stack_traces_enabled(enable=True)\n        \n        assert result['method'] == DomMethod.SET_NODE_STACK_TRACES_ENABLED\n        assert result['params']['enable'] is True\n\n    def test_undo(self):\n        \"\"\"Test undo command.\"\"\"\n        result = DomCommands.undo()\n        \n        assert result['method'] == DomMethod.UNDO\n        assert 'params' not in result\n"
  },
  {
    "path": "tests/test_commands/test_emulation_commands.py",
    "content": "\"\"\"\nTests for EmulationCommands class.\n\nThis module contains tests for all EmulationCommands methods,\nverifying that they generate the correct CDP commands with proper parameters.\n\"\"\"\n\nfrom pydoll.commands.emulation_commands import EmulationCommands\nfrom pydoll.protocol.emulation.methods import EmulationMethod\nfrom pydoll.protocol.emulation.types import UserAgentBrandVersion, UserAgentMetadata\n\n\ndef test_set_user_agent_override_minimal():\n    \"\"\"Test set_user_agent_override with only required parameter.\"\"\"\n    result = EmulationCommands.set_user_agent_override(user_agent='Test/1.0')\n    assert result['method'] == EmulationMethod.SET_USER_AGENT_OVERRIDE\n    assert result['params']['userAgent'] == 'Test/1.0'\n    assert 'acceptLanguage' not in result['params']\n    assert 'platform' not in result['params']\n    assert 'userAgentMetadata' not in result['params']\n\n\ndef test_set_user_agent_override_with_accept_language():\n    \"\"\"Test set_user_agent_override with acceptLanguage parameter.\"\"\"\n    result = EmulationCommands.set_user_agent_override(\n        user_agent='Test/1.0',\n        accept_language='en-US,en;q=0.9',\n    )\n    assert result['params']['userAgent'] == 'Test/1.0'\n    assert result['params']['acceptLanguage'] == 'en-US,en;q=0.9'\n\n\ndef test_set_user_agent_override_with_platform():\n    \"\"\"Test set_user_agent_override with platform parameter.\"\"\"\n    result = EmulationCommands.set_user_agent_override(\n        user_agent='Test/1.0',\n        platform='Win32',\n    )\n    assert result['params']['userAgent'] == 'Test/1.0'\n    assert result['params']['platform'] == 'Win32'\n\n\ndef test_set_user_agent_override_with_metadata():\n    \"\"\"Test set_user_agent_override with full userAgentMetadata.\"\"\"\n    metadata = UserAgentMetadata(\n        platform='Windows',\n        platformVersion='15.0.0',\n        architecture='x86',\n        model='',\n        mobile=False,\n        brands=[\n            UserAgentBrandVersion(brand='Not/A)Brand', version='20'),\n            UserAgentBrandVersion(brand='Chromium', version='120'),\n            UserAgentBrandVersion(brand='Google Chrome', version='120'),\n        ],\n        fullVersionList=[\n            UserAgentBrandVersion(brand='Not/A)Brand', version='20.0.0.0'),\n            UserAgentBrandVersion(brand='Chromium', version='120.0.6099.109'),\n            UserAgentBrandVersion(brand='Google Chrome', version='120.0.6099.109'),\n        ],\n        bitness='64',\n        wow64=False,\n    )\n    result = EmulationCommands.set_user_agent_override(\n        user_agent='Mozilla/5.0 Chrome/120.0.6099.109',\n        platform='Win32',\n        user_agent_metadata=metadata,\n    )\n    assert result['method'] == EmulationMethod.SET_USER_AGENT_OVERRIDE\n    assert result['params']['userAgent'] == 'Mozilla/5.0 Chrome/120.0.6099.109'\n    assert result['params']['platform'] == 'Win32'\n    assert result['params']['userAgentMetadata']['platform'] == 'Windows'\n    assert result['params']['userAgentMetadata']['mobile'] is False\n    assert len(result['params']['userAgentMetadata']['brands']) == 3\n    assert result['params']['userAgentMetadata']['brands'][1]['brand'] == 'Chromium'\n\n\ndef test_set_user_agent_override_with_all_params():\n    \"\"\"Test set_user_agent_override with all parameters set.\"\"\"\n    metadata = UserAgentMetadata(\n        platform='Android',\n        platformVersion='14.0.0',\n        architecture='arm',\n        model='Pixel 7',\n        mobile=True,\n        bitness='64',\n        wow64=False,\n    )\n    result = EmulationCommands.set_user_agent_override(\n        user_agent='Mozilla/5.0 (Linux; Android 14)',\n        accept_language='pt-BR,pt;q=0.9',\n        platform='Linux armv81',\n        user_agent_metadata=metadata,\n    )\n    assert result['params']['userAgent'] == 'Mozilla/5.0 (Linux; Android 14)'\n    assert result['params']['acceptLanguage'] == 'pt-BR,pt;q=0.9'\n    assert result['params']['platform'] == 'Linux armv81'\n    assert result['params']['userAgentMetadata']['mobile'] is True\n    assert result['params']['userAgentMetadata']['model'] == 'Pixel 7'\n\n\ndef test_set_user_agent_override_none_params_excluded():\n    \"\"\"Test that None parameters are not included in the command.\"\"\"\n    result = EmulationCommands.set_user_agent_override(\n        user_agent='Test/1.0',\n        accept_language=None,\n        platform=None,\n        user_agent_metadata=None,\n    )\n    assert 'acceptLanguage' not in result['params']\n    assert 'platform' not in result['params']\n    assert 'userAgentMetadata' not in result['params']\n"
  },
  {
    "path": "tests/test_commands/test_fetch_commands.py",
    "content": "import pytest\nfrom pydoll.commands.fetch_commands import FetchCommands\nfrom pydoll.protocol.fetch.types import AuthChallengeResponseType, RequestStage\nfrom pydoll.protocol.network.types import RequestMethod, ErrorReason, ResourceType\nfrom pydoll.protocol.fetch.methods import FetchMethod\n\n\nclass TestFetchCommands:\n    \"\"\"Tests for the FetchCommands class.\"\"\"\n\n    def test_continue_request_minimal(self):\n        \"\"\"Test continue_request command with minimal parameters.\"\"\"\n        request_id = 'req123'\n        result = FetchCommands.continue_request(request_id=request_id)\n        \n        assert result['method'] == FetchMethod.CONTINUE_REQUEST\n        assert result['params']['requestId'] == request_id\n\n    def test_continue_request_with_url(self):\n        \"\"\"Test continue_request command with URL.\"\"\"\n        request_id = 'req123'\n        url = 'https://example.com'\n        result = FetchCommands.continue_request(request_id=request_id, url=url)\n        \n        assert result['method'] == FetchMethod.CONTINUE_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['url'] == url\n\n    def test_continue_request_with_method(self):\n        \"\"\"Test continue_request command with HTTP method.\"\"\"\n        request_id = 'req123'\n        method = RequestMethod.POST\n        result = FetchCommands.continue_request(request_id=request_id, method=method)\n        \n        assert result['method'] == FetchMethod.CONTINUE_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['method'] == method\n\n    def test_continue_request_with_post_data(self):\n        \"\"\"Test continue_request command with POST data.\"\"\"\n        request_id = 'req123'\n        post_data = '{\"key\": \"value\"}'\n        result = FetchCommands.continue_request(request_id=request_id, post_data=post_data)\n        \n        assert result['method'] == FetchMethod.CONTINUE_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['postData'] == post_data\n\n    def test_continue_request_with_headers(self):\n        \"\"\"Test continue_request command with headers.\"\"\"\n        request_id = 'req123'\n        headers = [{'name': 'Content-Type', 'value': 'application/json'}]\n        result = FetchCommands.continue_request(request_id=request_id, headers=headers)\n        \n        assert result['method'] == FetchMethod.CONTINUE_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['headers'] == headers\n\n    def test_continue_request_with_intercept_response(self):\n        \"\"\"Test continue_request command with intercept_response.\"\"\"\n        request_id = 'req123'\n        intercept_response = True\n        result = FetchCommands.continue_request(\n            request_id=request_id, \n            intercept_response=intercept_response\n        )\n        \n        assert result['method'] == FetchMethod.CONTINUE_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['interceptResponse'] == intercept_response\n\n    def test_continue_request_with_all_params(self):\n        \"\"\"Test continue_request command with all parameters.\"\"\"\n        request_id = 'req123'\n        url = 'https://example.com'\n        method = RequestMethod.PUT\n        post_data = '{\"data\": \"test\"}'\n        headers = [{'name': 'Authorization', 'value': 'Bearer token'}]\n        intercept_response = False\n        \n        result = FetchCommands.continue_request(\n            request_id=request_id,\n            url=url,\n            method=method,\n            post_data=post_data,\n            headers=headers,\n            intercept_response=intercept_response\n        )\n        \n        assert result['method'] == FetchMethod.CONTINUE_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['url'] == url\n        assert result['params']['method'] == method\n        assert result['params']['postData'] == post_data\n        assert result['params']['headers'] == headers\n        assert result['params']['interceptResponse'] == intercept_response\n\n    def test_continue_request_with_auth_minimal(self):\n        \"\"\"Test continue_request_with_auth command with minimal parameters.\"\"\"\n        request_id = 'req123'\n        auth_response = AuthChallengeResponseType.PROVIDE_CREDENTIALS\n        result = FetchCommands.continue_request_with_auth(\n            request_id=request_id,\n            auth_challenge_response=auth_response\n        )\n        \n        assert result['method'] == FetchMethod.CONTINUE_WITH_AUTH\n        assert result['params']['requestId'] == request_id\n        assert result['params']['authChallengeResponse']['response'] == auth_response\n\n    def test_continue_request_with_auth_credentials(self):\n        \"\"\"Test continue_request_with_auth command with credentials.\"\"\"\n        request_id = 'req123'\n        auth_response = AuthChallengeResponseType.PROVIDE_CREDENTIALS\n        username = 'testuser'\n        password = 'testpass'\n        \n        result = FetchCommands.continue_request_with_auth(\n            request_id=request_id,\n            auth_challenge_response=auth_response,\n            proxy_username=username,\n            proxy_password=password\n        )\n        \n        assert result['method'] == FetchMethod.CONTINUE_WITH_AUTH\n        assert result['params']['requestId'] == request_id\n        assert result['params']['authChallengeResponse']['response'] == auth_response\n        assert result['params']['authChallengeResponse']['username'] == username\n        assert result['params']['authChallengeResponse']['password'] == password\n\n    def test_continue_request_with_auth_cancel(self):\n        \"\"\"Test continue_request_with_auth command with cancel response.\"\"\"\n        request_id = 'req123'\n        auth_response = AuthChallengeResponseType.CANCEL_AUTH\n        \n        result = FetchCommands.continue_request_with_auth(\n            request_id=request_id,\n            auth_challenge_response=auth_response\n        )\n        \n        assert result['method'] == FetchMethod.CONTINUE_WITH_AUTH\n        assert result['params']['requestId'] == request_id\n        assert result['params']['authChallengeResponse']['response'] == auth_response\n\n    def test_disable(self):\n        \"\"\"Test disable command.\"\"\"\n        result = FetchCommands.disable()\n        \n        assert result['method'] == FetchMethod.DISABLE\n        assert 'params' not in result\n\n    def test_enable_minimal(self):\n        \"\"\"Test enable command with minimal parameters.\"\"\"\n        handle_auth = True\n        result = FetchCommands.enable(handle_auth_requests=handle_auth)\n        \n        assert result['method'] == FetchMethod.ENABLE\n        assert result['params']['handleAuthRequests'] == handle_auth\n        assert result['params']['patterns'][0]['urlPattern'] == '*'\n\n    def test_enable_with_url_pattern(self):\n        \"\"\"Test enable command with custom URL pattern.\"\"\"\n        handle_auth = False\n        url_pattern = 'https://api.example.com/*'\n        result = FetchCommands.enable(\n            handle_auth_requests=handle_auth,\n            url_pattern=url_pattern\n        )\n        \n        assert result['method'] == FetchMethod.ENABLE\n        assert result['params']['handleAuthRequests'] == handle_auth\n        assert result['params']['patterns'][0]['urlPattern'] == url_pattern\n\n    def test_enable_with_resource_type(self):\n        \"\"\"Test enable command with resource type.\"\"\"\n        handle_auth = True\n        resource_type = ResourceType.DOCUMENT\n        result = FetchCommands.enable(\n            handle_auth_requests=handle_auth,\n            resource_type=resource_type\n        )\n        \n        assert result['method'] == FetchMethod.ENABLE\n        assert result['params']['handleAuthRequests'] == handle_auth\n        assert result['params']['patterns'][0]['resourceType'] == resource_type\n\n    def test_enable_with_request_stage(self):\n        \"\"\"Test enable command with request stage.\"\"\"\n        handle_auth = True\n        request_stage = RequestStage.REQUEST\n        result = FetchCommands.enable(\n            handle_auth_requests=handle_auth,\n            request_stage=request_stage\n        )\n        \n        assert result['method'] == FetchMethod.ENABLE\n        assert result['params']['handleAuthRequests'] == handle_auth\n        assert result['params']['patterns'][0]['requestStage'] == request_stage\n\n    def test_enable_with_all_params(self):\n        \"\"\"Test enable command with all parameters.\"\"\"\n        handle_auth = True\n        url_pattern = 'https://test.com/*'\n        resource_type = ResourceType.XHR\n        request_stage = RequestStage.RESPONSE\n        \n        result = FetchCommands.enable(\n            handle_auth_requests=handle_auth,\n            url_pattern=url_pattern,\n            resource_type=resource_type,\n            request_stage=request_stage\n        )\n        \n        assert result['method'] == FetchMethod.ENABLE\n        assert result['params']['handleAuthRequests'] == handle_auth\n        assert result['params']['patterns'][0]['urlPattern'] == url_pattern\n        assert result['params']['patterns'][0]['resourceType'] == resource_type\n        assert result['params']['patterns'][0]['requestStage'] == request_stage\n\n    def test_fail_request(self):\n        \"\"\"Test fail_request command.\"\"\"\n        request_id = 'req123'\n        error_reason = ErrorReason.FAILED\n        result = FetchCommands.fail_request(\n            request_id=request_id,\n            error_reason=error_reason\n        )\n        \n        assert result['method'] == FetchMethod.FAIL_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['errorReason'] == error_reason\n\n    def test_fail_request_with_different_error(self):\n        \"\"\"Test fail_request command with different error reason.\"\"\"\n        request_id = 'req123'\n        error_reason = ErrorReason.TIMED_OUT\n        result = FetchCommands.fail_request(\n            request_id=request_id,\n            error_reason=error_reason\n        )\n        \n        assert result['method'] == FetchMethod.FAIL_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['errorReason'] == error_reason\n\n    def test_fulfill_request_minimal(self):\n        \"\"\"Test fulfill_request command with minimal parameters.\"\"\"\n        request_id = 'req123'\n        response_code = 200\n        result = FetchCommands.fulfill_request(\n            request_id=request_id,\n            response_code=response_code\n        )\n        \n        assert result['method'] == FetchMethod.FULFILL_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['responseCode'] == response_code\n\n    def test_fulfill_request_with_headers(self):\n        \"\"\"Test fulfill_request command with response headers.\"\"\"\n        request_id = 'req123'\n        response_code = 201\n        headers = [{'name': 'Content-Type', 'value': 'application/json'}]\n        result = FetchCommands.fulfill_request(\n            request_id=request_id,\n            response_code=response_code,\n            response_headers=headers\n        )\n        \n        assert result['method'] == FetchMethod.FULFILL_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['responseCode'] == response_code\n        assert result['params']['responseHeaders'] == headers\n\n    def test_fulfill_request_with_body(self):\n        \"\"\"Test fulfill_request command with response body.\"\"\"\n        request_id = 'req123'\n        response_code = 200\n        body = {'message': 'success'}\n        result = FetchCommands.fulfill_request(\n            request_id=request_id,\n            response_code=response_code,\n            body=body\n        )\n        \n        assert result['method'] == FetchMethod.FULFILL_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['responseCode'] == response_code\n        assert result['params']['body'] == body\n\n    def test_fulfill_request_with_response_phrase(self):\n        \"\"\"Test fulfill_request command with response phrase.\"\"\"\n        request_id = 'req123'\n        response_code = 404\n        response_phrase = 'Not Found'\n        result = FetchCommands.fulfill_request(\n            request_id=request_id,\n            response_code=response_code,\n            response_phrase=response_phrase\n        )\n        \n        assert result['method'] == FetchMethod.FULFILL_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['responseCode'] == response_code\n        assert result['params']['responsePhrase'] == response_phrase\n\n    def test_fulfill_request_with_all_params(self):\n        \"\"\"Test fulfill_request command with all parameters.\"\"\"\n        request_id = 'req123'\n        response_code = 500\n        headers = [{'name': 'Server', 'value': 'nginx'}]\n        body = {'error': 'Internal Server Error'}\n        response_phrase = 'Internal Server Error'\n        \n        result = FetchCommands.fulfill_request(\n            request_id=request_id,\n            response_code=response_code,\n            response_headers=headers,\n            body=body,\n            response_phrase=response_phrase\n        )\n        \n        assert result['method'] == FetchMethod.FULFILL_REQUEST\n        assert result['params']['requestId'] == request_id\n        assert result['params']['responseCode'] == response_code\n        assert result['params']['responseHeaders'] == headers\n        assert result['params']['body'] == body\n        assert result['params']['responsePhrase'] == response_phrase\n\n    def test_get_response_body(self):\n        \"\"\"Test get_response_body command.\"\"\"\n        request_id = 'req123'\n        result = FetchCommands.get_response_body(request_id=request_id)\n        \n        assert result['method'] == FetchMethod.GET_RESPONSE_BODY\n        assert result['params']['requestId'] == request_id\n\n    def test_continue_response_minimal(self):\n        \"\"\"Test continue_response command with minimal parameters.\"\"\"\n        request_id = 'req123'\n        result = FetchCommands.continue_response(request_id=request_id)\n        \n        assert result['method'] == FetchMethod.CONTINUE_RESPONSE\n        assert result['params']['requestId'] == request_id\n\n    def test_continue_response_with_code(self):\n        \"\"\"Test continue_response command with response code.\"\"\"\n        request_id = 'req123'\n        response_code = 302\n        result = FetchCommands.continue_response(\n            request_id=request_id,\n            response_code=response_code\n        )\n        \n        assert result['method'] == FetchMethod.CONTINUE_RESPONSE\n        assert result['params']['requestId'] == request_id\n        assert result['params']['responseCode'] == response_code\n\n    def test_continue_response_with_headers(self):\n        \"\"\"Test continue_response command with headers.\"\"\"\n        request_id = 'req123'\n        headers = [{'name': 'Location', 'value': 'https://redirect.com'}]\n        result = FetchCommands.continue_response(\n            request_id=request_id,\n            response_headers=headers\n        )\n        \n        assert result['method'] == FetchMethod.CONTINUE_RESPONSE\n        assert result['params']['requestId'] == request_id\n        assert result['params']['responseHeaders'] == headers\n\n    def test_continue_response_with_phrase(self):\n        \"\"\"Test continue_response command with response phrase.\"\"\"\n        request_id = 'req123'\n        response_phrase = 'Found'\n        result = FetchCommands.continue_response(\n            request_id=request_id,\n            response_phrase=response_phrase\n        )\n        \n        assert result['method'] == FetchMethod.CONTINUE_RESPONSE\n        assert result['params']['requestId'] == request_id\n        assert result['params']['responsePhrase'] == response_phrase\n\n    def test_continue_response_with_all_params(self):\n        \"\"\"Test continue_response command with all parameters.\"\"\"\n        request_id = 'req123'\n        response_code = 301\n        headers = [{'name': 'Location', 'value': 'https://new-location.com'}]\n        response_phrase = 'Moved Permanently'\n        \n        result = FetchCommands.continue_response(\n            request_id=request_id,\n            response_code=response_code,\n            response_headers=headers,\n            response_phrase=response_phrase\n        )\n        \n        assert result['method'] == FetchMethod.CONTINUE_RESPONSE\n        assert result['params']['requestId'] == request_id\n        assert result['params']['responseCode'] == response_code\n        assert result['params']['responseHeaders'] == headers\n        assert result['params']['responsePhrase'] == response_phrase\n\n    def test_take_response_body_as_stream(self):\n        \"\"\"Test take_response_body_as_stream command.\"\"\"\n        request_id = 'req123'\n        result = FetchCommands.take_response_body_as_stream(request_id=request_id)\n        \n        assert result['method'] == FetchMethod.TAKE_RESPONSE_BODY_AS_STREAM\n        assert result['params']['requestId'] == request_id\n"
  },
  {
    "path": "tests/test_commands/test_input_commands.py",
    "content": "\"\"\"\nTests for InputCommands class.\n\nThis module contains comprehensive tests for all InputCommands methods,\nverifying that they generate the correct CDP commands with proper parameters.\n\"\"\"\n\nfrom pydoll.commands.input_commands import InputCommands\nfrom pydoll.protocol.input.types import (\n    DragEventType,\n    GestureSourceType,\n    KeyEventType,\n    KeyLocation,\n    KeyModifier,\n    MouseButton,\n    MouseEventType,\n    PointerType,\n    TouchEventType,\n)\nfrom pydoll.protocol.input.methods import InputMethod\n\n\ndef test_cancel_dragging():\n    \"\"\"Test cancel_dragging method generates correct command.\"\"\"\n    expected_command = {\n        'method': InputMethod.CANCEL_DRAGGING,\n    }\n    result = InputCommands.cancel_dragging()\n    assert result['method'] == expected_command['method']\n    assert 'params' not in result\n\n\ndef test_dispatch_key_event_minimal():\n    \"\"\"Test dispatch_key_event with minimal parameters.\"\"\"\n    expected_command = {\n        'method': InputMethod.DISPATCH_KEY_EVENT,\n        'params': {\n            'type': KeyEventType.KEY_DOWN,\n        },\n    }\n    result = InputCommands.dispatch_key_event(type=KeyEventType.KEY_DOWN)\n    assert result['method'] == expected_command['method']\n    assert result['params']['type'] == expected_command['params']['type']\n\n\ndef test_dispatch_key_event_with_modifiers():\n    \"\"\"Test dispatch_key_event with modifiers.\"\"\"\n    result = InputCommands.dispatch_key_event(\n        type=KeyEventType.KEY_DOWN,\n        modifiers=KeyModifier.CTRL | KeyModifier.SHIFT,\n        text='A',\n    )\n    assert result['method'] == InputMethod.DISPATCH_KEY_EVENT\n    assert result['params']['type'] == KeyEventType.KEY_DOWN\n    assert result['params']['modifiers'] == KeyModifier.CTRL | KeyModifier.SHIFT\n    assert result['params']['text'] == 'A'\n\n\ndef test_dispatch_key_event_with_all_params():\n    \"\"\"Test dispatch_key_event with all parameters.\"\"\"\n    result = InputCommands.dispatch_key_event(\n        type=KeyEventType.CHAR,\n        modifiers=KeyModifier.ALT,\n        timestamp=123.456,\n        text='a',\n        unmodified_text='A',\n        key_identifier='U+0041',\n        code='KeyA',\n        key='a',\n        windows_virtual_key_code=65,\n        native_virtual_key_code=65,\n        auto_repeat=True,\n        is_keypad=False,\n        is_system_key=False,\n        location=KeyLocation.LEFT,\n        commands=['selectAll'],\n    )\n    assert result['method'] == InputMethod.DISPATCH_KEY_EVENT\n    assert result['params']['type'] == KeyEventType.CHAR\n    assert result['params']['modifiers'] == KeyModifier.ALT\n    assert result['params']['timestamp'] == 123.456\n    assert result['params']['text'] == 'a'\n    assert result['params']['unmodifiedText'] == 'A'\n    assert result['params']['keyIdentifier'] == 'U+0041'\n    assert result['params']['code'] == 'KeyA'\n    assert result['params']['key'] == 'a'\n    assert result['params']['windowsVirtualKeyCode'] == 65\n    assert result['params']['nativeVirtualKeyCode'] == 65\n    assert result['params']['autoRepeat'] is True\n    assert result['params']['isKeypad'] is False\n    assert result['params']['isSystemKey'] is False\n    assert result['params']['location'] == KeyLocation.LEFT\n    assert result['params']['commands'] == ['selectAll']\n\n\ndef test_dispatch_mouse_event_minimal():\n    \"\"\"Test dispatch_mouse_event with minimal parameters.\"\"\"\n    result = InputCommands.dispatch_mouse_event(\n        type=MouseEventType.MOUSE_PRESSED,\n        x=100,\n        y=200,\n    )\n    assert result['method'] == InputMethod.DISPATCH_MOUSE_EVENT\n    assert result['params']['type'] == MouseEventType.MOUSE_PRESSED\n    assert result['params']['x'] == 100\n    assert result['params']['y'] == 200\n\n\ndef test_dispatch_mouse_event_with_button():\n    \"\"\"Test dispatch_mouse_event with button parameter.\"\"\"\n    result = InputCommands.dispatch_mouse_event(\n        type=MouseEventType.MOUSE_PRESSED,\n        x=100,\n        y=200,\n        button=MouseButton.LEFT,\n        click_count=1,\n    )\n    assert result['method'] == InputMethod.DISPATCH_MOUSE_EVENT\n    assert result['params']['type'] == MouseEventType.MOUSE_PRESSED\n    assert result['params']['x'] == 100\n    assert result['params']['y'] == 200\n    assert result['params']['button'] == MouseButton.LEFT\n    assert result['params']['clickCount'] == 1\n\n\ndef test_dispatch_mouse_event_with_all_params():\n    \"\"\"Test dispatch_mouse_event with all parameters.\"\"\"\n    result = InputCommands.dispatch_mouse_event(\n        type=MouseEventType.MOUSE_MOVED,\n        x=150,\n        y=250,\n        modifiers=KeyModifier.CTRL,\n        timestamp=789.123,\n        button=MouseButton.RIGHT,\n        click_count=2,\n        force=0.5,\n        tangential_pressure=0.3,\n        tilt_x=15.0,\n        tilt_y=20.0,\n        twist=45,\n        delta_x=10.0,\n        delta_y=15.0,\n        pointer_type=PointerType.PEN,\n    )\n    assert result['method'] == InputMethod.DISPATCH_MOUSE_EVENT\n    assert result['params']['type'] == MouseEventType.MOUSE_MOVED\n    assert result['params']['x'] == 150\n    assert result['params']['y'] == 250\n    assert result['params']['modifiers'] == KeyModifier.CTRL\n    assert result['params']['timestamp'] == 789.123\n    assert result['params']['button'] == MouseButton.RIGHT\n    assert result['params']['clickCount'] == 2\n    assert result['params']['force'] == 0.5\n    assert result['params']['tangentialPressure'] == 0.3\n    assert result['params']['tiltX'] == 15.0\n    assert result['params']['tiltY'] == 20.0\n    assert result['params']['twist'] == 45\n    assert result['params']['deltaX'] == 10.0\n    assert result['params']['deltaY'] == 15.0\n    assert result['params']['pointerType'] == PointerType.PEN\n\n\ndef test_dispatch_touch_event_minimal():\n    \"\"\"Test dispatch_touch_event with minimal parameters.\"\"\"\n    result = InputCommands.dispatch_touch_event(\n        type=TouchEventType.TOUCH_START,\n        touch_points=[],\n    )\n    assert result['method'] == InputMethod.DISPATCH_TOUCH_EVENT\n    assert result['params']['type'] == TouchEventType.TOUCH_START\n\n\ndef test_dispatch_touch_event_with_touch_points():\n    \"\"\"Test dispatch_touch_event with touch points.\"\"\"\n    touch_points = [\n        {\n            'x': 100,\n            'y': 200,\n            'radiusX': 10,\n            'radiusY': 10,\n            'rotationAngle': 0,\n            'force': 1.0,\n        }\n    ]\n    result = InputCommands.dispatch_touch_event(\n        type=TouchEventType.TOUCH_START,\n        touch_points=touch_points,\n        modifiers=KeyModifier.SHIFT,\n        timestamp=456.789,\n    )\n    assert result['method'] == InputMethod.DISPATCH_TOUCH_EVENT\n    assert result['params']['type'] == TouchEventType.TOUCH_START\n    assert result['params']['touchPoints'] == touch_points\n    assert result['params']['modifiers'] == KeyModifier.SHIFT\n    assert result['params']['timestamp'] == 456.789\n\n\ndef test_set_ignore_input_events():\n    \"\"\"Test set_ignore_input_events\"\"\"\n    result = InputCommands.set_ignore_input_events(ignore=True)\n    assert result['method'] == InputMethod.SET_IGNORE_INPUT_EVENTS\n    assert result['params']['ignore'] is True\n\n\ndef test_dispatch_drag_event_minimal():\n    \"\"\"Test dispatch_drag_event with minimal parameters.\"\"\"\n    result = InputCommands.dispatch_drag_event(\n        type=DragEventType.DRAG_ENTER,\n        x=100,\n        y=200,\n        data={},\n    )\n    assert result['method'] == InputMethod.DISPATCH_DRAG_EVENT\n    assert result['params']['type'] == DragEventType.DRAG_ENTER\n    assert result['params']['x'] == 100\n    assert result['params']['y'] == 200\n\n\ndef test_dispatch_drag_event_with_data():\n    \"\"\"Test dispatch_drag_event with drag data.\"\"\"\n    drag_data = {\n        'items': [\n            {\n                'mimeType': 'text/plain',\n                'data': 'Hello World',\n            }\n        ],\n        'dragOperationsMask': 1,\n    }\n    result = InputCommands.dispatch_drag_event(\n        type=DragEventType.DROP,\n        x=150,\n        y=250,\n        data=drag_data,\n        modifiers=KeyModifier.ALT,\n    )\n    assert result['method'] == InputMethod.DISPATCH_DRAG_EVENT\n    assert result['params']['type'] == DragEventType.DROP\n    assert result['params']['x'] == 150\n    assert result['params']['y'] == 250\n    assert result['params']['data'] == drag_data\n    assert result['params']['modifiers'] == KeyModifier.ALT\n\n\ndef test_emulate_touch_from_mouse_event_minimal():\n    \"\"\"Test emulate_touch_from_mouse_event with minimal parameters.\"\"\"\n    result = InputCommands.emulate_touch_from_mouse_event(\n        type=MouseEventType.MOUSE_PRESSED,\n        x=100,\n        y=200,\n        button=MouseButton.LEFT,\n    )\n    assert result['method'] == InputMethod.EMULATE_TOUCH_FROM_MOUSE_EVENT\n    assert result['params']['type'] == MouseEventType.MOUSE_PRESSED\n    assert result['params']['x'] == 100\n    assert result['params']['y'] == 200\n    assert result['params']['button'] == MouseButton.LEFT\n\n\ndef test_emulate_touch_from_mouse_event_with_all_params():\n    \"\"\"Test emulate_touch_from_mouse_event with all parameters.\"\"\"\n    result = InputCommands.emulate_touch_from_mouse_event(\n        type=MouseEventType.MOUSE_MOVED,\n        x=150,\n        y=250,\n        button=MouseButton.RIGHT,\n        timestamp=123.456,\n        delta_x=10.0,\n        delta_y=15.0,\n        modifiers=KeyModifier.CTRL | KeyModifier.SHIFT,\n        click_count=2,\n    )\n    assert result['method'] == InputMethod.EMULATE_TOUCH_FROM_MOUSE_EVENT\n    assert result['params']['type'] == MouseEventType.MOUSE_MOVED\n    assert result['params']['x'] == 150\n    assert result['params']['y'] == 250\n    assert result['params']['button'] == MouseButton.RIGHT\n    assert result['params']['timestamp'] == 123.456\n    assert result['params']['deltaX'] == 10.0\n    assert result['params']['deltaY'] == 15.0\n    assert result['params']['modifiers'] == KeyModifier.CTRL | KeyModifier.SHIFT\n    assert result['params']['clickCount'] == 2\n\n\ndef test_ime_set_composition():\n    \"\"\"Test ime_set_composition method.\"\"\"\n    result = InputCommands.ime_set_composition(\n        text='Hello',\n        selection_start=0,\n        selection_end=5,\n    )\n    assert result['method'] == InputMethod.IME_SET_COMPOSITION\n    assert result['params']['text'] == 'Hello'\n    assert result['params']['selectionStart'] == 0\n    assert result['params']['selectionEnd'] == 5\n\n\ndef test_ime_set_composition_with_replacement():\n    \"\"\"Test ime_set_composition with replacement parameters.\"\"\"\n    result = InputCommands.ime_set_composition(\n        text='World',\n        selection_start=0,\n        selection_end=5,\n        replacement_start=0,\n        replacement_end=5,\n    )\n    assert result['method'] == InputMethod.IME_SET_COMPOSITION\n    assert result['params']['text'] == 'World'\n    assert result['params']['selectionStart'] == 0\n    assert result['params']['selectionEnd'] == 5\n    assert result['params']['replacementStart'] == 0\n    assert result['params']['replacementEnd'] == 5\n\n\ndef test_insert_text():\n    \"\"\"Test insert_text method.\"\"\"\n    result = InputCommands.insert_text(text='Hello World')\n    assert result['method'] == InputMethod.INSERT_TEXT\n    assert result['params']['text'] == 'Hello World'\n\n\ndef test_set_intercept_drags_enabled():\n    \"\"\"Test set_intercept_drags with enabled=True.\"\"\"\n    result = InputCommands.set_intercept_drags(enabled=True)\n    assert result['method'] == InputMethod.SET_INTERCEPT_DRAGS\n    assert result['params']['enabled'] is True\n\n\ndef test_set_intercept_drags_disabled():\n    \"\"\"Test set_intercept_drags with enabled=False.\"\"\"\n    result = InputCommands.set_intercept_drags(enabled=False)\n    assert result['method'] == InputMethod.SET_INTERCEPT_DRAGS\n    assert result['params']['enabled'] is False\n\n\ndef test_synthesize_pinch_gesture_minimal():\n    \"\"\"Test synthesize_pinch_gesture with minimal parameters.\"\"\"\n    result = InputCommands.synthesize_pinch_gesture(\n        x=100,\n        y=200,\n        scale_factor=2.0,\n    )\n    assert result['method'] == InputMethod.SYNTHESIZE_PINCH_GESTURE\n    assert result['params']['x'] == 100\n    assert result['params']['y'] == 200\n    assert result['params']['scaleFactor'] == 2.0\n\n\ndef test_synthesize_pinch_gesture_with_all_params():\n    \"\"\"Test synthesize_pinch_gesture with all parameters.\"\"\"\n    result = InputCommands.synthesize_pinch_gesture(\n        x=150,\n        y=250,\n        scale_factor=1.5,\n        relative_speed=100,\n        gesture_source_type=GestureSourceType.TOUCH,\n    )\n    assert result['method'] == InputMethod.SYNTHESIZE_PINCH_GESTURE\n    assert result['params']['x'] == 150\n    assert result['params']['y'] == 250\n    assert result['params']['scaleFactor'] == 1.5\n    assert result['params']['relativeSpeed'] == 100\n    assert result['params']['gestureSourceType'] == GestureSourceType.TOUCH\n\n\ndef test_synthesize_scroll_gesture_minimal():\n    \"\"\"Test synthesize_scroll_gesture with minimal parameters.\"\"\"\n    result = InputCommands.synthesize_scroll_gesture(\n        x=100,\n        y=200,\n    )\n    assert result['method'] == InputMethod.SYNTHESIZE_SCROLL_GESTURE\n    assert result['params']['x'] == 100\n    assert result['params']['y'] == 200\n\n\ndef test_synthesize_scroll_gesture_with_distance():\n    \"\"\"Test synthesize_scroll_gesture with distance parameters.\"\"\"\n    result = InputCommands.synthesize_scroll_gesture(\n        x=100,\n        y=200,\n        x_distance=50.0,\n        y_distance=100.0,\n    )\n    assert result['method'] == InputMethod.SYNTHESIZE_SCROLL_GESTURE\n    assert result['params']['x'] == 100\n    assert result['params']['y'] == 200\n    assert result['params']['xDistance'] == 50.0\n    assert result['params']['yDistance'] == 100.0\n\n\ndef test_synthesize_scroll_gesture_with_all_params():\n    \"\"\"Test synthesize_scroll_gesture with all parameters.\"\"\"\n    result = InputCommands.synthesize_scroll_gesture(\n        x=150,\n        y=250,\n        x_distance=75.0,\n        y_distance=125.0,\n        x_overscroll=10.0,\n        y_overscroll=15.0,\n        prevent_fling=True,\n        speed=500,\n        gesture_source_type=GestureSourceType.MOUSE,\n        repeat_count=3,\n        repeat_delay_ms=100,\n        interaction_marker_name='scroll_test',\n    )\n    assert result['method'] == InputMethod.SYNTHESIZE_SCROLL_GESTURE\n    assert result['params']['x'] == 150\n    assert result['params']['y'] == 250\n    assert result['params']['xDistance'] == 75.0\n    assert result['params']['yDistance'] == 125.0\n    assert result['params']['xOverscroll'] == 10.0\n    assert result['params']['yOverscroll'] == 15.0\n    assert result['params']['preventFling'] is True\n    assert result['params']['speed'] == 500\n    assert result['params']['gestureSourceType'] == GestureSourceType.MOUSE\n    assert result['params']['repeatCount'] == 3\n    assert result['params']['repeatDelayMs'] == 100\n    assert result['params']['interactionMarkerName'] == 'scroll_test'\n\n\ndef test_synthesize_tap_gesture_minimal():\n    \"\"\"Test synthesize_tap_gesture with minimal parameters.\"\"\"\n    result = InputCommands.synthesize_tap_gesture(\n        x=100,\n        y=200,\n    )\n    assert result['method'] == InputMethod.SYNTHESIZE_TAP_GESTURE\n    assert result['params']['x'] == 100\n    assert result['params']['y'] == 200\n\n\ndef test_synthesize_tap_gesture_with_all_params():\n    \"\"\"Test synthesize_tap_gesture with all parameters.\"\"\"\n    result = InputCommands.synthesize_tap_gesture(\n        x=150,\n        y=250,\n        duration=500,\n        tap_count=2,\n        gesture_source_type=GestureSourceType.TOUCH,\n    )\n    assert result['method'] == InputMethod.SYNTHESIZE_TAP_GESTURE\n    assert result['params']['x'] == 150\n    assert result['params']['y'] == 250\n    assert result['params']['duration'] == 500\n    assert result['params']['tapCount'] == 2\n    assert result['params']['gestureSourceType'] == GestureSourceType.TOUCH\n\n\ndef test_mouse_wheel_event():\n    \"\"\"Test mouse wheel event dispatch.\"\"\"\n    result = InputCommands.dispatch_mouse_event(\n        type=MouseEventType.MOUSE_WHEEL,\n        x=100,\n        y=200,\n        delta_x=10.0,\n        delta_y=-20.0,\n    )\n    assert result['method'] == InputMethod.DISPATCH_MOUSE_EVENT\n    assert result['params']['type'] == MouseEventType.MOUSE_WHEEL\n    assert result['params']['x'] == 100\n    assert result['params']['y'] == 200\n    assert result['params']['deltaX'] == 10.0\n    assert result['params']['deltaY'] == -20.0\n\n\ndef test_key_event_with_location():\n    \"\"\"Test key event with location parameter.\"\"\"\n    result = InputCommands.dispatch_key_event(\n        type=KeyEventType.KEY_DOWN,\n        key='Shift',\n        location=KeyLocation.LEFT,\n    )\n    assert result['method'] == InputMethod.DISPATCH_KEY_EVENT\n    assert result['params']['type'] == KeyEventType.KEY_DOWN\n    assert result['params']['key'] == 'Shift'\n    assert result['params']['location'] == KeyLocation.LEFT\n\n\ndef test_touch_event_multiple_points():\n    \"\"\"Test touch event with multiple touch points.\"\"\"\n    touch_points = [\n        {\n            'x': 100,\n            'y': 200,\n            'radiusX': 10,\n            'radiusY': 10,\n            'rotationAngle': 0,\n            'force': 1.0,\n        },\n        {\n            'x': 300,\n            'y': 400,\n            'radiusX': 15,\n            'radiusY': 15,\n            'rotationAngle': 45,\n            'force': 0.8,\n        },\n    ]\n    result = InputCommands.dispatch_touch_event(\n        type=TouchEventType.TOUCH_MOVE,\n        touch_points=touch_points,\n    )\n    assert result['method'] == InputMethod.DISPATCH_TOUCH_EVENT\n    assert result['params']['type'] == TouchEventType.TOUCH_MOVE\n    assert result['params']['touchPoints'] == touch_points\n    assert len(result['params']['touchPoints']) == 2\n\n\ndef test_drag_event_cancel():\n    \"\"\"Test drag cancel event.\"\"\"\n    result = InputCommands.dispatch_drag_event(\n        type=DragEventType.DRAG_CANCEL,\n        x=100,\n        y=200,\n        data={},\n    )\n    assert result['method'] == InputMethod.DISPATCH_DRAG_EVENT\n    assert result['params']['type'] == DragEventType.DRAG_CANCEL\n    assert result['params']['x'] == 100\n    assert result['params']['y'] == 200\n"
  },
  {
    "path": "tests/test_commands/test_network_commands.py",
    "content": "\"\"\"\nTests for NetworkCommands class.\n\nThis module contains comprehensive tests for all NetworkCommands methods,\nverifying that they generate the correct CDP commands with proper parameters.\n\"\"\"\n\nfrom pydoll.commands.network_commands import NetworkCommands\nfrom pydoll.protocol.network.types import ConnectionType, ContentEncoding, CookiePriority, CookieSameSite, CookieSourceScheme\nfrom pydoll.protocol.network.methods import NetworkMethod\n\n\ndef test_clear_browser_cache():\n    \"\"\"Test clear_browser_cache method generates correct command.\"\"\"\n    result = NetworkCommands.clear_browser_cache()\n    assert result['method'] == NetworkMethod.CLEAR_BROWSER_CACHE\n    assert 'params' not in result\n\n\ndef test_clear_browser_cookies():\n    \"\"\"Test clear_browser_cookies method generates correct command.\"\"\"\n    result = NetworkCommands.clear_browser_cookies()\n    assert result['method'] == NetworkMethod.CLEAR_BROWSER_COOKIES\n    assert 'params' not in result\n\n\ndef test_delete_cookies_minimal():\n    \"\"\"Test delete_cookies with minimal parameters.\"\"\"\n    result = NetworkCommands.delete_cookies(name='test_cookie')\n    assert result['method'] == NetworkMethod.DELETE_COOKIES\n    assert result['params']['name'] == 'test_cookie'\n\n\ndef test_delete_cookies_with_url():\n    \"\"\"Test delete_cookies with URL parameter.\"\"\"\n    result = NetworkCommands.delete_cookies(\n        name='test_cookie',\n        url='https://example.com'\n    )\n    assert result['method'] == NetworkMethod.DELETE_COOKIES\n    assert result['params']['name'] == 'test_cookie'\n    assert result['params']['url'] == 'https://example.com'\n\n\ndef test_delete_cookies_with_all_params():\n    \"\"\"Test delete_cookies with all parameters.\"\"\"\n    partition_key = {\n        'topLevelSite': 'https://example.com',\n        'hasCrossSiteAncestor': False\n    }\n    result = NetworkCommands.delete_cookies(\n        name='test_cookie',\n        url='https://example.com',\n        domain='example.com',\n        path='/test',\n        partition_key=partition_key\n    )\n    assert result['method'] == NetworkMethod.DELETE_COOKIES\n    assert result['params']['name'] == 'test_cookie'\n    assert result['params']['url'] == 'https://example.com'\n    assert result['params']['domain'] == 'example.com'\n    assert result['params']['path'] == '/test'\n    assert result['params']['partitionKey'] == partition_key\n\n\ndef test_disable():\n    \"\"\"Test disable method generates correct command.\"\"\"\n    result = NetworkCommands.disable()\n    assert result['method'] == NetworkMethod.DISABLE\n    assert 'params' not in result\n\n\ndef test_enable_minimal():\n    \"\"\"Test enable with minimal parameters.\"\"\"\n    result = NetworkCommands.enable()\n    assert result['method'] == NetworkMethod.ENABLE\n    assert result['params'] == {}\n\n\ndef test_enable_with_buffer_sizes():\n    \"\"\"Test enable with buffer size parameters.\"\"\"\n    result = NetworkCommands.enable(\n        max_total_buffer_size=1024000,\n        max_resource_buffer_size=512000,\n        max_post_data_size=65536\n    )\n    assert result['method'] == NetworkMethod.ENABLE\n    assert result['params']['maxTotalBufferSize'] == 1024000\n    assert result['params']['maxResourceBufferSize'] == 512000\n    assert result['params']['maxPostDataSize'] == 65536\n\n\ndef test_get_cookies_minimal():\n    \"\"\"Test get_cookies with minimal parameters.\"\"\"\n    result = NetworkCommands.get_cookies()\n    assert result['method'] == NetworkMethod.GET_COOKIES\n    assert result['params'] == {}\n\n\ndef test_get_cookies_with_urls():\n    \"\"\"Test get_cookies with URLs parameter.\"\"\"\n    urls = ['https://example.com', 'https://test.com']\n    result = NetworkCommands.get_cookies(urls=urls)\n    assert result['method'] == NetworkMethod.GET_COOKIES\n    assert result['params']['urls'] == urls\n\n\ndef test_get_request_post_data():\n    \"\"\"Test get_request_post_data method.\"\"\"\n    result = NetworkCommands.get_request_post_data(request_id='12345')\n    assert result['method'] == NetworkMethod.GET_REQUEST_POST_DATA\n    assert result['params']['requestId'] == '12345'\n\n\ndef test_get_response_body():\n    \"\"\"Test get_response_body method.\"\"\"\n    result = NetworkCommands.get_response_body(request_id='12345')\n    assert result['method'] == NetworkMethod.GET_RESPONSE_BODY\n    assert result['params']['requestId'] == '12345'\n\n\ndef test_set_cache_disabled_true():\n    \"\"\"Test set_cache_disabled with cache disabled.\"\"\"\n    result = NetworkCommands.set_cache_disabled(cache_disabled=True)\n    assert result['method'] == NetworkMethod.SET_CACHE_DISABLED\n    assert result['params']['cacheDisabled'] is True\n\n\ndef test_set_cache_disabled_false():\n    \"\"\"Test set_cache_disabled with cache enabled.\"\"\"\n    result = NetworkCommands.set_cache_disabled(cache_disabled=False)\n    assert result['method'] == NetworkMethod.SET_CACHE_DISABLED\n    assert result['params']['cacheDisabled'] is False\n\n\ndef test_set_cookie_minimal():\n    \"\"\"Test set_cookie with minimal parameters.\"\"\"\n    result = NetworkCommands.set_cookie(name='test', value='value')\n    assert result['method'] == NetworkMethod.SET_COOKIE\n    assert result['params']['name'] == 'test'\n    assert result['params']['value'] == 'value'\n\n\ndef test_set_cookie_with_url():\n    \"\"\"Test set_cookie with URL parameter.\"\"\"\n    result = NetworkCommands.set_cookie(\n        name='test',\n        value='value',\n        url='https://example.com'\n    )\n    assert result['method'] == NetworkMethod.SET_COOKIE\n    assert result['params']['name'] == 'test'\n    assert result['params']['value'] == 'value'\n    assert result['params']['url'] == 'https://example.com'\n\n\ndef test_set_cookie_with_all_params():\n    \"\"\"Test set_cookie with all parameters.\"\"\"\n    partition_key = {\n        'topLevelSite': 'https://example.com',\n        'hasCrossSiteAncestor': False\n    }\n    result = NetworkCommands.set_cookie(\n        name='test',\n        value='value',\n        url='https://example.com',\n        domain='example.com',\n        path='/test',\n        secure=True,\n        http_only=True,\n        same_site=CookieSameSite.STRICT,\n        expires=1234567890.0,\n        priority=CookiePriority.HIGH,\n        same_party=True,\n        source_scheme=CookieSourceScheme.SECURE,\n        source_port=443,\n        partition_key=partition_key\n    )\n    assert result['method'] == NetworkMethod.SET_COOKIE\n    assert result['params']['name'] == 'test'\n    assert result['params']['value'] == 'value'\n    assert result['params']['url'] == 'https://example.com'\n    assert result['params']['domain'] == 'example.com'\n    assert result['params']['path'] == '/test'\n    assert result['params']['secure'] is True\n    assert result['params']['httpOnly'] is True\n    assert result['params']['sameSite'] == CookieSameSite.STRICT\n    assert result['params']['expires'] == 1234567890.0\n    assert result['params']['priority'] == CookiePriority.HIGH\n    assert result['params']['sameParty'] is True\n    assert result['params']['sourceScheme'] == CookieSourceScheme.SECURE\n    assert result['params']['sourcePort'] == 443\n    assert result['params']['partitionKey'] == partition_key\n\n\ndef test_set_cookies():\n    \"\"\"Test set_cookies method.\"\"\"\n    cookies = [\n        {\n            'name': 'cookie1',\n            'value': 'value1',\n            'url': 'https://example.com'\n        },\n        {\n            'name': 'cookie2',\n            'value': 'value2',\n            'domain': 'example.com'\n        }\n    ]\n    result = NetworkCommands.set_cookies(cookies=cookies)\n    assert result['method'] == NetworkMethod.SET_COOKIES\n    assert result['params']['cookies'] == cookies\n\n\ndef test_set_extra_http_headers():\n    \"\"\"Test set_extra_http_headers method.\"\"\"\n    headers = [\n        {'name': 'Authorization', 'value': 'Bearer token123'},\n        {'name': 'X-Custom-Header', 'value': 'custom-value'}\n    ]\n    result = NetworkCommands.set_extra_http_headers(headers=headers)\n    assert result['method'] == NetworkMethod.SET_EXTRA_HTTP_HEADERS\n    assert result['params']['headers'] == headers\n\n\ndef test_set_useragent_override_minimal():\n    \"\"\"Test set_useragent_override with minimal parameters.\"\"\"\n    user_agent = 'Mozilla/5.0 (Custom Browser)'\n    result = NetworkCommands.set_useragent_override(user_agent=user_agent)\n    assert result['method'] == NetworkMethod.SET_USER_AGENT_OVERRIDE\n    assert result['params']['userAgent'] == user_agent\n\n\ndef test_set_useragent_override_with_all_params():\n    \"\"\"Test set_useragent_override with all parameters.\"\"\"\n    user_agent = 'Mozilla/5.0 (Custom Browser)'\n    accept_language = 'en-US,en;q=0.9'\n    platform = 'Linux x86_64'\n    user_agent_metadata = {\n        'brands': [{'brand': 'Custom', 'version': '1.0'}],\n        'fullVersionList': [{'brand': 'Custom', 'version': '1.0.0'}],\n        'platform': 'Linux',\n        'platformVersion': '5.4.0',\n        'architecture': 'x86',\n        'model': '',\n        'mobile': False,\n        'bitness': '64',\n        'wow64': False\n    }\n    result = NetworkCommands.set_useragent_override(\n        user_agent=user_agent,\n        accept_language=accept_language,\n        platform=platform,\n        user_agent_metadata=user_agent_metadata\n    )\n    assert result['method'] == NetworkMethod.SET_USER_AGENT_OVERRIDE\n    assert result['params']['userAgent'] == user_agent\n    assert result['params']['acceptLanguage'] == accept_language\n    assert result['params']['platform'] == platform\n    assert result['params']['userAgentMetadata'] == user_agent_metadata\n\n\ndef test_clear_accepted_encodings_override():\n    \"\"\"Test clear_accepted_encodings_override method.\"\"\"\n    result = NetworkCommands.clear_accepted_encodings_override()\n    assert result['method'] == NetworkMethod.CLEAR_ACCEPTED_ENCODINGS_OVERRIDE\n    assert 'params' not in result\n\n\ndef test_enable_reporting_api():\n    \"\"\"Test enable_reporting_api method.\"\"\"\n    result = NetworkCommands.enable_reporting_api(enabled=True)\n    assert result['method'] == NetworkMethod.ENABLE_REPORTING_API\n    assert result['params']['enabled'] is True\n\n\ndef test_search_in_response_body_minimal():\n    \"\"\"Test search_in_response_body with minimal parameters.\"\"\"\n    result = NetworkCommands.search_in_response_body(\n        request_id='12345',\n        query='test'\n    )\n    assert result['method'] == NetworkMethod.SEARCH_IN_RESPONSE_BODY\n    assert result['params']['requestId'] == '12345'\n    assert result['params']['query'] == 'test'\n    assert result['params']['caseSensitive'] is False\n    assert result['params']['isRegex'] is False\n\n\ndef test_search_in_response_body_with_options():\n    \"\"\"Test search_in_response_body with all options.\"\"\"\n    result = NetworkCommands.search_in_response_body(\n        request_id='12345',\n        query='test.*pattern',\n        case_sensitive=True,\n        is_regex=True\n    )\n    assert result['method'] == NetworkMethod.SEARCH_IN_RESPONSE_BODY\n    assert result['params']['requestId'] == '12345'\n    assert result['params']['query'] == 'test.*pattern'\n    assert result['params']['caseSensitive'] is True\n    assert result['params']['isRegex'] is True\n\n\ndef test_set_blocked_urls():\n    \"\"\"Test set_blocked_urls method.\"\"\"\n    urls = ['https://ads.example.com', 'https://tracker.com']\n    result = NetworkCommands.set_blocked_urls(urls=urls)\n    assert result['method'] == NetworkMethod.SET_BLOCKED_URLS\n    assert result['params']['urls'] == urls\n\n\ndef test_set_bypass_service_worker():\n    \"\"\"Test set_bypass_service_worker method.\"\"\"\n    result = NetworkCommands.set_bypass_service_worker(bypass=True)\n    assert result['method'] == NetworkMethod.SET_BYPASS_SERVICE_WORKER\n    assert result['params']['bypass'] is True\n\n\ndef test_get_certificate():\n    \"\"\"Test get_certificate method.\"\"\"\n    result = NetworkCommands.get_certificate(origin='https://example.com')\n    assert result['method'] == NetworkMethod.GET_CERTIFICATE\n    assert result['params']['origin'] == 'https://example.com'\n\n\ndef test_get_response_body_for_interception():\n    \"\"\"Test get_response_body_for_interception method.\"\"\"\n    result = NetworkCommands.get_response_body_for_interception(\n        interception_id='interception123'\n    )\n    assert result['method'] == NetworkMethod.GET_RESPONSE_BODY_FOR_INTERCEPTION\n    assert result['params']['interceptionId'] == 'interception123'\n\n\ndef test_set_accepted_encodings():\n    \"\"\"Test set_accepted_encodings method.\"\"\"\n    encodings = [ContentEncoding.GZIP, ContentEncoding.BR]\n    result = NetworkCommands.set_accepted_encodings(encodings=encodings)\n    assert result['method'] == NetworkMethod.SET_ACCEPTED_ENCODINGS\n    assert result['params']['encodings'] == encodings\n\n\ndef test_set_attach_debug_stack():\n    \"\"\"Test set_attach_debug_stack method.\"\"\"\n    result = NetworkCommands.set_attach_debug_stack(enabled=True)\n    assert result['method'] == NetworkMethod.SET_ATTACH_DEBUG_STACK\n    assert result['params']['enabled'] is True\n\n\ndef test_set_cookie_controls_minimal():\n    \"\"\"Test set_cookie_controls with minimal parameters.\"\"\"\n    result = NetworkCommands.set_cookie_controls(\n        enable_third_party_cookie_restriction=True\n    )\n    assert result['method'] == NetworkMethod.SET_COOKIE_CONTROLS\n    assert result['params']['enableThirdPartyCookieRestriction'] is True\n\n\ndef test_set_cookie_controls_with_all_params():\n    \"\"\"Test set_cookie_controls with all parameters.\"\"\"\n    result = NetworkCommands.set_cookie_controls(\n        enable_third_party_cookie_restriction=True,\n        disable_third_party_cookie_metadata=False,\n        disable_third_party_cookie_heuristics=True\n    )\n    assert result['method'] == NetworkMethod.SET_COOKIE_CONTROLS\n    assert result['params']['enableThirdPartyCookieRestriction'] is True\n    assert result['params']['disableThirdPartyCookieMetadata'] is False\n    assert result['params']['disableThirdPartyCookieHeuristics'] is True\n\n\ndef test_stream_resource_content():\n    \"\"\"Test stream_resource_content method.\"\"\"\n    result = NetworkCommands.stream_resource_content(request_id='12345')\n    assert result['method'] == NetworkMethod.STREAM_RESOURCE_CONTENT\n    assert result['params']['requestId'] == '12345'\n\n\ndef test_take_response_body_for_interception_as_stream():\n    \"\"\"Test take_response_body_for_interception_as_stream method.\"\"\"\n    result = NetworkCommands.take_response_body_for_interception_as_stream(\n        interception_id='interception123'\n    )\n    assert result['method'] == NetworkMethod.TAKE_RESPONSE_BODY_FOR_INTERCEPTION_AS_STREAM\n    assert result['params']['interceptionId'] == 'interception123'\n\n\ndef test_emulate_network_conditions_minimal():\n    \"\"\"Test emulate_network_conditions with minimal parameters.\"\"\"\n    result = NetworkCommands.emulate_network_conditions(\n        offline=False,\n        latency=100.0,\n        download_throughput=1000000.0,\n        upload_throughput=500000.0\n    )\n    assert result['method'] == NetworkMethod.EMULATE_NETWORK_CONDITIONS\n    assert result['params']['offline'] is False\n    assert result['params']['latency'] == 100.0\n    assert result['params']['downloadThroughput'] == 1000000.0\n    assert result['params']['uploadThroughput'] == 500000.0\n\n\ndef test_emulate_network_conditions_with_all_params():\n    \"\"\"Test emulate_network_conditions with all parameters.\"\"\"\n    result = NetworkCommands.emulate_network_conditions(\n        offline=False,\n        latency=200.0,\n        download_throughput=2000000.0,\n        upload_throughput=1000000.0,\n        connection_type=ConnectionType.CELLULAR4G,\n        packet_loss=0.1,\n        packet_queue_length=100,\n        packet_reordering=True\n    )\n    assert result['method'] == NetworkMethod.EMULATE_NETWORK_CONDITIONS\n    assert result['params']['offline'] is False\n    assert result['params']['latency'] == 200.0\n    assert result['params']['downloadThroughput'] == 2000000.0\n    assert result['params']['uploadThroughput'] == 1000000.0\n    assert result['params']['connectionType'] == ConnectionType.CELLULAR4G\n    assert result['params']['packetLoss'] == 0.1\n    assert result['params']['packetQueueLength'] == 100\n    assert result['params']['packetReordering'] is True\n\n\ndef test_get_security_isolation_status_minimal():\n    \"\"\"Test get_security_isolation_status with minimal parameters.\"\"\"\n    result = NetworkCommands.get_security_isolation_status()\n    assert result['method'] == NetworkMethod.GET_SECURITY_ISOLATION_STATUS\n    assert result['params'] == {}\n\n\ndef test_get_security_isolation_status_with_frame_id():\n    \"\"\"Test get_security_isolation_status with frame ID.\"\"\"\n    result = NetworkCommands.get_security_isolation_status(frame_id='frame123')\n    assert result['method'] == NetworkMethod.GET_SECURITY_ISOLATION_STATUS\n    assert result['params']['frameId'] == 'frame123'\n\n\ndef test_load_network_resource():\n    \"\"\"Test load_network_resource method.\"\"\"\n    options = {\n        'disableCache': True,\n        'includeCredentials': False\n    }\n    result = NetworkCommands.load_network_resource(\n        url='https://example.com/resource',\n        options=options\n    )\n    assert result['method'] == NetworkMethod.LOAD_NETWORK_RESOURCE\n    assert result['params']['url'] == 'https://example.com/resource'\n    assert result['params']['options'] == options\n\n\ndef test_load_network_resource_with_frame_id():\n    \"\"\"Test load_network_resource with frame ID.\"\"\"\n    options = {\n        'disableCache': False,\n        'includeCredentials': True\n    }\n    result = NetworkCommands.load_network_resource(\n        url='https://example.com/resource',\n        options=options,\n        frame_id='frame123'\n    )\n    assert result['method'] == NetworkMethod.LOAD_NETWORK_RESOURCE\n    assert result['params']['url'] == 'https://example.com/resource'\n    assert result['params']['options'] == options\n    assert result['params']['frameId'] == 'frame123'\n\n\ndef test_replay_xhr():\n    \"\"\"Test replay_xhr method.\"\"\"\n    result = NetworkCommands.replay_xhr(request_id='12345')\n    assert result['method'] == NetworkMethod.REPLAY_XHR\n    assert result['params']['requestId'] == '12345'\n"
  },
  {
    "path": "tests/test_commands/test_page_commands.py",
    "content": "\"\"\"\nTests for PageCommands class.\n\nThis module contains comprehensive tests for all PageCommands methods,\nverifying that they generate the correct CDP commands with proper parameters.\n\"\"\"\n\nfrom pydoll.commands.page_commands import PageCommands\nfrom pydoll.protocol.page.types import (\n    ReferrerPolicy,\n    ScreencastFormat,\n    ScreenshotFormat,\n    TransferMode,\n    TransitionType,\n    WebLifecycleState,\n)\nfrom pydoll.protocol.page.methods import PageMethod\n\n\ndef test_add_script_to_evaluate_on_new_document_minimal():\n    \"\"\"Test add_script_to_evaluate_on_new_document with minimal parameters.\"\"\"\n    result = PageCommands.add_script_to_evaluate_on_new_document(\n        source='console.log(\"Hello World\");'\n    )\n    assert result['method'] == PageMethod.ADD_SCRIPT_TO_EVALUATE_ON_NEW_DOCUMENT\n    assert result['params']['source'] == 'console.log(\"Hello World\");'\n\n\ndef test_add_script_to_evaluate_on_new_document_with_all_params():\n    \"\"\"Test add_script_to_evaluate_on_new_document with all parameters.\"\"\"\n    result = PageCommands.add_script_to_evaluate_on_new_document(\n        source='console.log(\"Test\");',\n        world_name='test_world',\n        include_command_line_api=True,\n        run_immediately=False\n    )\n    assert result['method'] == PageMethod.ADD_SCRIPT_TO_EVALUATE_ON_NEW_DOCUMENT\n    assert result['params']['source'] == 'console.log(\"Test\");'\n    assert result['params']['worldName'] == 'test_world'\n    assert result['params']['includeCommandLineAPI'] is True\n    assert result['params']['runImmediately'] is False\n\n\ndef test_bring_to_front():\n    \"\"\"Test bring_to_front method generates correct command.\"\"\"\n    result = PageCommands.bring_to_front()\n    assert result['method'] == PageMethod.BRING_TO_FRONT\n    assert 'params' not in result\n\n\ndef test_capture_screenshot_minimal():\n    \"\"\"Test capture_screenshot with minimal parameters.\"\"\"\n    result = PageCommands.capture_screenshot()\n    assert result['method'] == PageMethod.CAPTURE_SCREENSHOT\n    assert result['params'] == {}\n\n\ndef test_capture_screenshot_with_format_and_quality():\n    \"\"\"Test capture_screenshot with format and quality.\"\"\"\n    result = PageCommands.capture_screenshot(\n        format=ScreenshotFormat.JPEG,\n        quality=80\n    )\n    assert result['method'] == PageMethod.CAPTURE_SCREENSHOT\n    assert result['params']['format'] == ScreenshotFormat.JPEG\n    assert result['params']['quality'] == 80\n\n\ndef test_capture_screenshot_with_clip():\n    \"\"\"Test capture_screenshot with clip viewport.\"\"\"\n    clip = {\n        'x': 10,\n        'y': 20,\n        'width': 100,\n        'height': 200,\n        'scale': 1.0\n    }\n    result = PageCommands.capture_screenshot(\n        format=ScreenshotFormat.PNG,\n        clip=clip\n    )\n    assert result['method'] == PageMethod.CAPTURE_SCREENSHOT\n    assert result['params']['format'] == ScreenshotFormat.PNG\n    assert result['params']['clip'] == clip\n\n\ndef test_capture_screenshot_with_all_params():\n    \"\"\"Test capture_screenshot with all parameters.\"\"\"\n    clip = {\n        'x': 0,\n        'y': 0,\n        'width': 1920,\n        'height': 1080,\n        'scale': 1.0\n    }\n    result = PageCommands.capture_screenshot(\n        format=ScreenshotFormat.WEBP,\n        quality=90,\n        clip=clip,\n        from_surface=True,\n        capture_beyond_viewport=False,\n        optimize_for_speed=True\n    )\n    assert result['method'] == PageMethod.CAPTURE_SCREENSHOT\n    assert result['params']['format'] == ScreenshotFormat.WEBP\n    assert result['params']['quality'] == 90\n    assert result['params']['clip'] == clip\n    assert result['params']['fromSurface'] is True\n    assert result['params']['captureBeyondViewport'] is False\n    assert result['params']['optimizeForSpeed'] is True\n\n\ndef test_close():\n    \"\"\"Test close method generates correct command.\"\"\"\n    result = PageCommands.close()\n    assert result['method'] == PageMethod.CLOSE\n    assert 'params' not in result\n\n\ndef test_create_isolated_world_minimal():\n    \"\"\"Test create_isolated_world with minimal parameters.\"\"\"\n    result = PageCommands.create_isolated_world(frame_id='frame123')\n    assert result['method'] == PageMethod.CREATE_ISOLATED_WORLD\n    assert result['params']['frameId'] == 'frame123'\n\n\ndef test_create_isolated_world_with_all_params():\n    \"\"\"Test create_isolated_world with all parameters.\"\"\"\n    result = PageCommands.create_isolated_world(\n        frame_id='frame123',\n        world_name='test_world',\n        grant_universal_access=True\n    )\n    assert result['method'] == PageMethod.CREATE_ISOLATED_WORLD\n    assert result['params']['frameId'] == 'frame123'\n    assert result['params']['worldName'] == 'test_world'\n    assert result['params']['grantUniveralAccess'] is True\n\n\ndef test_disable():\n    \"\"\"Test disable method generates correct command.\"\"\"\n    result = PageCommands.disable()\n    assert result['method'] == PageMethod.DISABLE\n    assert 'params' not in result\n\n\ndef test_enable_minimal():\n    \"\"\"Test enable with minimal parameters.\"\"\"\n    result = PageCommands.enable()\n    assert result['method'] == PageMethod.ENABLE\n    assert result['params'] == {}\n\n\ndef test_enable_with_file_chooser():\n    \"\"\"Test enable with file chooser event enabled.\"\"\"\n    result = PageCommands.enable(enable_file_chooser_opened_event=True)\n    assert result['method'] == PageMethod.ENABLE\n    assert result['params']['enableFileChooserOpenedEvent'] is True\n\n\ndef test_get_app_manifest_minimal():\n    \"\"\"Test get_app_manifest with minimal parameters.\"\"\"\n    result = PageCommands.get_app_manifest()\n    assert result['method'] == PageMethod.GET_APP_MANIFEST\n    assert result['params'] == {}\n\n\ndef test_get_app_manifest_with_id():\n    \"\"\"Test get_app_manifest with manifest ID.\"\"\"\n    result = PageCommands.get_app_manifest(manifest_id='manifest123')\n    assert result['method'] == PageMethod.GET_APP_MANIFEST\n    assert result['params']['manifestId'] == 'manifest123'\n\n\ndef test_get_frame_tree():\n    \"\"\"Test get_frame_tree method generates correct command.\"\"\"\n    result = PageCommands.get_frame_tree()\n    assert result['method'] == PageMethod.GET_FRAME_TREE\n    assert 'params' not in result\n\n\ndef test_get_layout_metrics():\n    \"\"\"Test get_layout_metrics method generates correct command.\"\"\"\n    result = PageCommands.get_layout_metrics()\n    assert result['method'] == PageMethod.GET_LAYOUT_METRICS\n    assert 'params' not in result\n\n\ndef test_get_navigation_history():\n    \"\"\"Test get_navigation_history method generates correct command.\"\"\"\n    result = PageCommands.get_navigation_history()\n    assert result['method'] == PageMethod.GET_NAVIGATION_HISTORY\n    assert 'params' not in result\n\n\ndef test_handle_javascript_dialog_accept():\n    \"\"\"Test handle_javascript_dialog with accept.\"\"\"\n    result = PageCommands.handle_javascript_dialog(accept=True)\n    assert result['method'] == PageMethod.HANDLE_JAVASCRIPT_DIALOG\n    assert result['params']['accept'] is True\n\n\ndef test_handle_javascript_dialog_with_prompt():\n    \"\"\"Test handle_javascript_dialog with prompt text.\"\"\"\n    result = PageCommands.handle_javascript_dialog(\n        accept=True,\n        prompt_text='test input'\n    )\n    assert result['method'] == PageMethod.HANDLE_JAVASCRIPT_DIALOG\n    assert result['params']['accept'] is True\n    assert result['params']['promptText'] == 'test input'\n\n\ndef test_navigate_minimal():\n    \"\"\"Test navigate with minimal parameters.\"\"\"\n    result = PageCommands.navigate(url='https://example.com')\n    assert result['method'] == PageMethod.NAVIGATE\n    assert result['params']['url'] == 'https://example.com'\n\n\ndef test_navigate_with_all_params():\n    \"\"\"Test navigate with all parameters.\"\"\"\n    result = PageCommands.navigate(\n        url='https://example.com',\n        referrer='https://google.com',\n        transition_type=TransitionType.LINK,\n        frame_id='frame123',\n        referrer_policy=ReferrerPolicy.STRICT_ORIGIN\n    )\n    assert result['method'] == PageMethod.NAVIGATE\n    assert result['params']['url'] == 'https://example.com'\n    assert result['params']['referrer'] == 'https://google.com'\n    assert result['params']['transitionType'] == TransitionType.LINK\n    assert result['params']['frameId'] == 'frame123'\n    assert result['params']['referrerPolicy'] == ReferrerPolicy.STRICT_ORIGIN\n\n\ndef test_navigate_to_history_entry():\n    \"\"\"Test navigate_to_history_entry method.\"\"\"\n    result = PageCommands.navigate_to_history_entry(entry_id=5)\n    assert result['method'] == PageMethod.NAVIGATE_TO_HISTORY_ENTRY\n    assert result['params']['entryId'] == 5\n\n\ndef test_print_to_pdf_minimal():\n    \"\"\"Test print_to_pdf with minimal parameters.\"\"\"\n    result = PageCommands.print_to_pdf()\n    assert result['method'] == PageMethod.PRINT_TO_PDF\n    assert result['params'] == {}\n\n\ndef test_print_to_pdf_with_basic_params():\n    \"\"\"Test print_to_pdf with basic parameters.\"\"\"\n    result = PageCommands.print_to_pdf(\n        landscape=True,\n        scale=1.5,\n        paper_width=8.5,\n        paper_height=11.0\n    )\n    assert result['method'] == PageMethod.PRINT_TO_PDF\n    assert result['params']['landscape'] is True\n    assert result['params']['scale'] == 1.5\n    assert result['params']['paperWidth'] == 8.5\n    assert result['params']['paperHeight'] == 11.0\n\n\ndef test_print_to_pdf_with_all_params():\n    \"\"\"Test print_to_pdf with all parameters.\"\"\"\n    result = PageCommands.print_to_pdf(\n        landscape=False,\n        display_header_footer=True,\n        print_background=True,\n        scale=1.0,\n        paper_width=8.5,\n        paper_height=11.0,\n        margin_top=0.5,\n        margin_bottom=0.5,\n        margin_left=0.5,\n        margin_right=0.5,\n        page_ranges='1-5',\n        header_template='<div>Header</div>',\n        footer_template='<div>Footer</div>',\n        prefer_css_page_size=True,\n        transfer_mode=TransferMode.RETURN_AS_BASE64,\n        generate_tagged_pdf=True,\n        generate_document_outline=False\n    )\n    assert result['method'] == PageMethod.PRINT_TO_PDF\n    assert result['params']['landscape'] is False\n    assert result['params']['displayHeaderFooter'] is True\n    assert result['params']['printBackground'] is True\n    assert result['params']['scale'] == 1.0\n    assert result['params']['paperWidth'] == 8.5\n    assert result['params']['paperHeight'] == 11.0\n    assert result['params']['marginTop'] == 0.5\n    assert result['params']['marginBottom'] == 0.5\n    assert result['params']['marginLeft'] == 0.5\n    assert result['params']['marginRight'] == 0.5\n    assert result['params']['pageRanges'] == '1-5'\n    assert result['params']['headerTemplate'] == '<div>Header</div>'\n    assert result['params']['footerTemplate'] == '<div>Footer</div>'\n    assert result['params']['preferCSSPageSize'] is True\n    assert result['params']['transferMode'] == TransferMode.RETURN_AS_BASE64\n    assert result['params']['generateTaggedPDF'] is True\n    assert result['params']['generateDocumentOutline'] is False\n\n\ndef test_reload_minimal():\n    \"\"\"Test reload with minimal parameters.\"\"\"\n    result = PageCommands.reload()\n    assert result['method'] == PageMethod.RELOAD\n    assert result['params'] == {}\n\n\ndef test_reload_with_all_params():\n    \"\"\"Test reload with all parameters.\"\"\"\n    result = PageCommands.reload(\n        ignore_cache=True,\n        script_to_evaluate_on_load='console.log(\"reloaded\");',\n        loader_id='loader123'\n    )\n    assert result['method'] == PageMethod.RELOAD\n    assert result['params']['ignoreCache'] is True\n    assert result['params']['scriptToEvaluateOnLoad'] == 'console.log(\"reloaded\");'\n    assert result['params']['loaderId'] == 'loader123'\n\n\ndef test_reset_navigation_history():\n    \"\"\"Test reset_navigation_history method generates correct command.\"\"\"\n    result = PageCommands.reset_navigation_history()\n    assert result['method'] == PageMethod.RESET_NAVIGATION_HISTORY\n    assert 'params' not in result\n\n\ndef test_remove_script_to_evaluate_on_new_document():\n    \"\"\"Test remove_script_to_evaluate_on_new_document method.\"\"\"\n    result = PageCommands.remove_script_to_evaluate_on_new_document(\n        identifier='script123'\n    )\n    assert result['method'] == PageMethod.REMOVE_SCRIPT_TO_EVALUATE_ON_NEW_DOCUMENT\n    assert result['params']['identifier'] == 'script123'\n\n\ndef test_set_bypass_csp():\n    \"\"\"Test set_bypass_csp method.\"\"\"\n    result = PageCommands.set_bypass_csp(enabled=True)\n    assert result['method'] == PageMethod.SET_BYPASS_CSP\n    assert result['params']['enabled'] is True\n\n\ndef test_set_document_content():\n    \"\"\"Test set_document_content method.\"\"\"\n    result = PageCommands.set_document_content(\n        frame_id='frame123',\n        html='<html><body>Test</body></html>'\n    )\n    assert result['method'] == PageMethod.SET_DOCUMENT_CONTENT\n    assert result['params']['frameId'] == 'frame123'\n    assert result['params']['html'] == '<html><body>Test</body></html>'\n\n\ndef test_set_intercept_file_chooser_dialog():\n    \"\"\"Test set_intercept_file_chooser_dialog method.\"\"\"\n    result = PageCommands.set_intercept_file_chooser_dialog(enabled=True)\n    assert result['method'] == PageMethod.SET_INTERCEPT_FILE_CHOOSER_DIALOG\n    assert result['params']['enabled'] is True\n\n\ndef test_set_lifecycle_events_enabled():\n    \"\"\"Test set_lifecycle_events_enabled method.\"\"\"\n    result = PageCommands.set_lifecycle_events_enabled(enabled=True)\n    assert result['method'] == PageMethod.SET_LIFECYCLE_EVENTS_ENABLED\n    assert result['params']['enabled'] is True\n\n\ndef test_stop_loading():\n    \"\"\"Test stop_loading method generates correct command.\"\"\"\n    result = PageCommands.stop_loading()\n    assert result['method'] == PageMethod.STOP_LOADING\n    assert 'params' not in result\n\n\ndef test_add_compilation_cache():\n    \"\"\"Test add_compilation_cache method.\"\"\"\n    result = PageCommands.add_compilation_cache(\n        url='https://example.com/script.js',\n        data='compiled_data_here'\n    )\n    assert result['method'] == PageMethod.ADD_COMPILATION_CACHE\n    assert result['params']['url'] == 'https://example.com/script.js'\n    assert result['params']['data'] == 'compiled_data_here'\n\n\ndef test_capture_snapshot():\n    \"\"\"Test capture_snapshot method.\"\"\"\n    result = PageCommands.capture_snapshot(format='mhtml')\n    assert result['method'] == PageMethod.CAPTURE_SNAPSHOT\n    assert result['params']['format'] == 'mhtml'\n\n\ndef test_clear_compilation_cache():\n    \"\"\"Test clear_compilation_cache method generates correct command.\"\"\"\n    result = PageCommands.clear_compilation_cache()\n    assert result['method'] == PageMethod.CLEAR_COMPILATION_CACHE\n    assert 'params' not in result\n\n\ndef test_crash():\n    \"\"\"Test crash method generates correct command.\"\"\"\n    result = PageCommands.crash()\n    assert result['method'] == PageMethod.CRASH\n    assert 'params' not in result\n\n\ndef test_generate_test_report_minimal():\n    \"\"\"Test generate_test_report with minimal parameters.\"\"\"\n    result = PageCommands.generate_test_report(message='Test message')\n    assert result['method'] == PageMethod.GENERATE_TEST_REPORT\n    assert result['params']['message'] == 'Test message'\n\n\ndef test_generate_test_report_with_group():\n    \"\"\"Test generate_test_report with group parameter.\"\"\"\n    result = PageCommands.generate_test_report(\n        message='Test message',\n        group='test_group'\n    )\n    assert result['method'] == PageMethod.GENERATE_TEST_REPORT\n    assert result['params']['message'] == 'Test message'\n    assert result['params']['group'] == 'test_group'\n\n\ndef test_get_ad_script_ancestry_ids():\n    \"\"\"Test get_ad_script_ancestry_ids method.\"\"\"\n    result = PageCommands.get_ad_script_ancestry_ids(frame_id='frame123')\n    assert result['method'] == PageMethod.GET_AD_SCRIPT_ANCESTRY_IDS\n    assert result['params']['frameId'] == 'frame123'\n\n\ndef test_get_app_id_minimal():\n    \"\"\"Test get_app_id with minimal parameters.\"\"\"\n    result = PageCommands.get_app_id()\n    assert result['method'] == PageMethod.GET_APP_ID\n    assert result['params'] == {}\n\n\ndef test_get_app_id_with_params():\n    \"\"\"Test get_app_id with parameters.\"\"\"\n    result = PageCommands.get_app_id(\n        app_id='app123',\n        recommended_id='rec456'\n    )\n    assert result['method'] == PageMethod.GET_APP_ID\n    assert result['params']['appId'] == 'app123'\n    assert result['params']['recommendedId'] == 'rec456'\n\n\ndef test_get_installability_errors():\n    \"\"\"Test get_installability_errors method generates correct command.\"\"\"\n    result = PageCommands.get_installability_errors()\n    assert result['method'] == PageMethod.GET_INSTALLABILITY_ERRORS\n    assert 'params' not in result\n\n\ndef test_get_origin_trials():\n    \"\"\"Test get_origin_trials method.\"\"\"\n    result = PageCommands.get_origin_trials(frame_id='frame123')\n    assert result['method'] == PageMethod.GET_ORIGIN_TRIALS\n    assert result['params']['frameId'] == 'frame123'\n\n\ndef test_get_permissions_policy_state():\n    \"\"\"Test get_permissions_policy_state method.\"\"\"\n    result = PageCommands.get_permissions_policy_state(frame_id='frame123')\n    assert result['method'] == PageMethod.GET_PERMISSIONS_POLICY_STATE\n    assert result['params']['frameId'] == 'frame123'\n\n\ndef test_get_resource_content():\n    \"\"\"Test get_resource_content method.\"\"\"\n    result = PageCommands.get_resource_content(\n        frame_id='frame123',\n        url='https://example.com/resource.js'\n    )\n    assert result['method'] == PageMethod.GET_RESOURCE_CONTENT\n    assert result['params']['frameId'] == 'frame123'\n    assert result['params']['url'] == 'https://example.com/resource.js'\n\n\ndef test_get_resource_tree():\n    \"\"\"Test get_resource_tree method generates correct command.\"\"\"\n    result = PageCommands.get_resource_tree()\n    assert result['method'] == PageMethod.GET_RESOURCE_TREE\n    assert 'params' not in result\n\n\ndef test_produce_compilation_cache():\n    \"\"\"Test produce_compilation_cache method.\"\"\"\n    scripts = [\n        {'url': 'https://example.com/script1.js', 'eager': True},\n        {'url': 'https://example.com/script2.js', 'eager': False}\n    ]\n    result = PageCommands.produce_compilation_cache(scripts=scripts)\n    assert result['method'] == PageMethod.PRODUCE_COMPILATION_CACHE\n    assert result['params']['scripts'] == scripts\n\n\ndef test_screencast_frame_ack():\n    \"\"\"Test screencast_frame_ack method.\"\"\"\n    result = PageCommands.screencast_frame_ack(session_id='session123')\n    assert result['method'] == PageMethod.SCREENCAST_FRAME_ACK\n    assert result['params']['sessionId'] == 'session123'\n\n\ndef test_search_in_resource_minimal():\n    \"\"\"Test search_in_resource with minimal parameters.\"\"\"\n    result = PageCommands.search_in_resource(\n        frame_id='frame123',\n        url='https://example.com/resource.js',\n        query='function'\n    )\n    assert result['method'] == PageMethod.SEARCH_IN_RESOURCE\n    assert result['params']['frameId'] == 'frame123'\n    assert result['params']['url'] == 'https://example.com/resource.js'\n    assert result['params']['query'] == 'function'\n\n\ndef test_search_in_resource_with_options():\n    \"\"\"Test search_in_resource with all options.\"\"\"\n    result = PageCommands.search_in_resource(\n        frame_id='frame123',\n        url='https://example.com/resource.js',\n        query='function.*test',\n        case_sensitive=True,\n        is_regex=True\n    )\n    assert result['method'] == PageMethod.SEARCH_IN_RESOURCE\n    assert result['params']['frameId'] == 'frame123'\n    assert result['params']['url'] == 'https://example.com/resource.js'\n    assert result['params']['query'] == 'function.*test'\n    assert result['params']['caseSensitive'] is True\n    assert result['params']['isRegex'] is True\n\n\ndef test_set_ad_blocking_enabled():\n    \"\"\"Test set_ad_blocking_enabled method.\"\"\"\n    result = PageCommands.set_ad_blocking_enabled(enabled=True)\n    assert result['method'] == PageMethod.SET_AD_BLOCKING_ENABLED\n    assert result['params']['enabled'] is True\n\n\ndef test_set_font_families():\n    \"\"\"Test set_font_families method.\"\"\"\n    font_families = {\n        'standard': 'Arial',\n        'serif': 'Times New Roman',\n        'sansSerif': 'Helvetica',\n        'cursive': 'Comic Sans MS',\n        'fantasy': 'Impact',\n        'math': 'Latin Modern Math'\n    }\n    for_scripts = [\n        {'script': 'Latn', 'fontFamilies': font_families}\n    ]\n    result = PageCommands.set_font_families(\n        font_families=font_families,\n        for_scripts=for_scripts\n    )\n    assert result['method'] == PageMethod.SET_FONT_FAMILIES\n    assert result['params']['fontFamilies'] == font_families\n    assert result['params']['forScripts'] == for_scripts\n\n\ndef test_set_font_sizes():\n    \"\"\"Test set_font_sizes method.\"\"\"\n    font_sizes = {\n        'standard': 16,\n        'fixed': 14\n    }\n    result = PageCommands.set_font_sizes(font_sizes=font_sizes)\n    assert result['method'] == PageMethod.SET_FONT_SIZES\n    assert result['params']['fontSizes'] == font_sizes\n\n\ndef test_set_prerendering_allowed():\n    \"\"\"Test set_prerendering_allowed method.\"\"\"\n    result = PageCommands.set_prerendering_allowed(is_allowed=True)\n    assert result['method'] == PageMethod.SET_PRERENDERING_ALLOWED\n    assert result['params']['isAllowed'] == True\n\n\ndef test_set_rph_registration_mode():\n    \"\"\"Test set_rph_registration_mode method.\"\"\"\n    from pydoll.protocol.page.methods import AutoResponseMode\n    result = PageCommands.set_rph_registration_mode(mode=AutoResponseMode.AUTO_ACCEPT)\n    assert result['method'] == PageMethod.SET_RPH_REGISTRATION_MODE\n    assert result['params']['mode'] == AutoResponseMode.AUTO_ACCEPT\n\n\ndef test_set_spc_transaction_mode():\n    \"\"\"Test set_spc_transaction_mode method.\"\"\"\n    from pydoll.protocol.page.methods import AutoResponseMode\n    result = PageCommands.set_spc_transaction_mode(mode=AutoResponseMode.AUTO_REJECT)\n    assert result['method'] == PageMethod.SET_SPC_TRANSACTION_MODE\n    assert result['params']['mode'] == AutoResponseMode.AUTO_REJECT\n\n\ndef test_set_web_lifecycle_state():\n    \"\"\"Test set_web_lifecycle_state method.\"\"\"\n    result = PageCommands.set_web_lifecycle_state(state=WebLifecycleState.FROZEN)\n    assert result['method'] == PageMethod.SET_WEB_LIFECYCLE_STATE\n    assert result['params']['state'] == WebLifecycleState.FROZEN\n\n\ndef test_start_screencast_minimal():\n    \"\"\"Test start_screencast with minimal parameters.\"\"\"\n    result = PageCommands.start_screencast(format=ScreencastFormat.JPEG)\n    assert result['method'] == PageMethod.START_SCREENCAST\n    assert result['params']['format'] == ScreencastFormat.JPEG\n\n\ndef test_start_screencast_with_all_params():\n    \"\"\"Test start_screencast with all parameters.\"\"\"\n    result = PageCommands.start_screencast(\n        format=ScreencastFormat.PNG,\n        quality=80,\n        max_width=1920,\n        max_height=1080,\n        every_nth_frame=2\n    )\n    assert result['method'] == PageMethod.START_SCREENCAST\n    assert result['params']['format'] == ScreencastFormat.PNG\n    assert result['params']['quality'] == 80\n    assert result['params']['maxWidth'] == 1920\n    assert result['params']['maxHeight'] == 1080\n    assert result['params']['everyNthFrame'] == 2\n\n\ndef test_stop_screencast():\n    \"\"\"Test stop_screencast method generates correct command.\"\"\"\n    result = PageCommands.stop_screencast()\n    assert result['method'] == PageMethod.STOP_SCREENCAST\n    assert 'params' not in result\n\n\ndef test_wait_for_debugger():\n    \"\"\"Test wait_for_debugger method generates correct command.\"\"\"\n    result = PageCommands.wait_for_debugger()\n    assert result['method'] == PageMethod.WAIT_FOR_DEBUGGER\n    assert 'params' not in result\n"
  },
  {
    "path": "tests/test_commands/test_runtime_commands.py",
    "content": "\"\"\"\nTests for RuntimeCommands class.\n\nThis module contains comprehensive tests for all RuntimeCommands methods,\nverifying that they generate the correct CDP commands with proper parameters.\n\"\"\"\n\nfrom pydoll.commands.runtime_commands import RuntimeCommands\nfrom pydoll.protocol.runtime.methods import RuntimeMethod\n\n\ndef test_add_binding_minimal():\n    \"\"\"Test add_binding with minimal parameters.\"\"\"\n    result = RuntimeCommands.add_binding(name='testBinding')\n    assert result['method'] == RuntimeMethod.ADD_BINDING\n    assert result['params']['name'] == 'testBinding'\n\n\ndef test_add_binding_with_context():\n    \"\"\"Test add_binding with execution context name.\"\"\"\n    result = RuntimeCommands.add_binding(\n        name='testBinding',\n        execution_context_name='main'\n    )\n    assert result['method'] == RuntimeMethod.ADD_BINDING\n    assert result['params']['name'] == 'testBinding'\n    assert result['params']['executionContextName'] == 'main'\n\n\ndef test_await_promise_minimal():\n    \"\"\"Test await_promise with minimal parameters.\"\"\"\n    result = RuntimeCommands.await_promise(promise_object_id='promise123')\n    assert result['method'] == RuntimeMethod.AWAIT_PROMISE\n    assert result['params']['promiseObjectId'] == 'promise123'\n\n\ndef test_await_promise_with_all_params():\n    \"\"\"Test await_promise with all parameters.\"\"\"\n    result = RuntimeCommands.await_promise(\n        promise_object_id='promise123',\n        return_by_value=True,\n        generate_preview=False\n    )\n    assert result['method'] == RuntimeMethod.AWAIT_PROMISE\n    assert result['params']['promiseObjectId'] == 'promise123'\n    assert result['params']['returnByValue'] is True\n    assert result['params']['generatePreview'] is False\n\n\ndef test_call_function_on_minimal():\n    \"\"\"Test call_function_on with minimal parameters.\"\"\"\n    result = RuntimeCommands.call_function_on(\n        function_declaration='function() { return this.value; }'\n    )\n    assert result['method'] == RuntimeMethod.CALL_FUNCTION_ON\n    assert result['params']['functionDeclaration'] == 'function() { return this.value; }'\n\n\ndef test_call_function_on_with_object_id():\n    \"\"\"Test call_function_on with object ID.\"\"\"\n    result = RuntimeCommands.call_function_on(\n        function_declaration='function() { return this.value; }',\n        object_id='obj123'\n    )\n    assert result['method'] == RuntimeMethod.CALL_FUNCTION_ON\n    assert result['params']['functionDeclaration'] == 'function() { return this.value; }'\n    assert result['params']['objectId'] == 'obj123'\n\n\ndef test_call_function_on_with_all_params():\n    \"\"\"Test call_function_on with all parameters.\"\"\"\n    arguments = [\n        {'value': 42},\n        {'value': 'test string'}\n    ]\n    serialization_options = {\n        'serialization': 'deep',\n        'maxDepth': 5\n    }\n    result = RuntimeCommands.call_function_on(\n        function_declaration='function(a, b) { return a + b; }',\n        object_id='obj123',\n        arguments=arguments,\n        silent=True,\n        return_by_value=False,\n        generate_preview=True,\n        user_gesture=False,\n        await_promise=True,\n        execution_context_id='ctx456',\n        object_group='testGroup',\n        throw_on_side_effect=False,\n        unique_context_id='unique789',\n        serialization_options=serialization_options\n    )\n    assert result['method'] == RuntimeMethod.CALL_FUNCTION_ON\n    assert result['params']['functionDeclaration'] == 'function(a, b) { return a + b; }'\n    assert result['params']['objectId'] == 'obj123'\n    assert result['params']['arguments'] == arguments\n    assert result['params']['silent'] is True\n    assert result['params']['returnByValue'] is False\n    assert result['params']['generatePreview'] is True\n    assert result['params']['userGesture'] is False\n    assert result['params']['awaitPromise'] is True\n    assert result['params']['executionContextId'] == 'ctx456'\n    assert result['params']['objectGroup'] == 'testGroup'\n    assert result['params']['throwOnSideEffect'] is False\n    assert result['params']['uniqueContextId'] == 'unique789'\n    assert result['params']['serializationOptions'] == serialization_options\n\n\ndef test_compile_script_minimal():\n    \"\"\"Test compile_script with minimal parameters.\"\"\"\n    result = RuntimeCommands.compile_script(expression='2 + 2', source_url='https://example.com/script.js')\n    assert result['method'] == RuntimeMethod.COMPILE_SCRIPT\n    assert result['params']['expression'] == '2 + 2'\n    assert result['params']['sourceURL'] == 'https://example.com/script.js'\n\n\ndef test_compile_script_with_all_params():\n    \"\"\"Test compile_script with all parameters.\"\"\"\n    result = RuntimeCommands.compile_script(\n        expression='function test() { return 42; }',\n        source_url='https://example.com/script.js',\n        persist_script=True,\n        execution_context_id='ctx123'\n    )\n    assert result['method'] == RuntimeMethod.COMPILE_SCRIPT\n    assert result['params']['expression'] == 'function test() { return 42; }'\n    assert result['params']['sourceURL'] == 'https://example.com/script.js'\n    assert result['params']['persistScript'] is True\n    assert result['params']['executionContextId'] == 'ctx123'\n\n\ndef test_disable():\n    \"\"\"Test disable method generates correct command.\"\"\"\n    result = RuntimeCommands.disable()\n    assert result['method'] == RuntimeMethod.DISABLE\n    assert 'params' not in result\n\n\ndef test_enable():\n    \"\"\"Test enable method generates correct command.\"\"\"\n    result = RuntimeCommands.enable()\n    assert result['method'] == RuntimeMethod.ENABLE\n    assert 'params' not in result\n\n\ndef test_evaluate_minimal():\n    \"\"\"Test evaluate with minimal parameters.\"\"\"\n    result = RuntimeCommands.evaluate(expression='2 + 2')\n    assert result['method'] == RuntimeMethod.EVALUATE\n    assert result['params']['expression'] == '2 + 2'\n\n\ndef test_evaluate_with_basic_params():\n    \"\"\"Test evaluate with basic parameters.\"\"\"\n    result = RuntimeCommands.evaluate(\n        expression='document.title',\n        return_by_value=True,\n        silent=False\n    )\n    assert result['method'] == RuntimeMethod.EVALUATE\n    assert result['params']['expression'] == 'document.title'\n    assert result['params']['returnByValue'] is True\n    assert result['params']['silent'] is False\n\n\ndef test_evaluate_with_all_params():\n    \"\"\"Test evaluate with all parameters.\"\"\"\n    serialization_options = {\n        'serialization': 'json',\n        'maxDepth': 3\n    }\n    result = RuntimeCommands.evaluate(\n        expression='window.location.href',\n        object_group='testGroup',\n        include_command_line_api=True,\n        silent=False,\n        context_id='ctx123',\n        return_by_value=False,\n        generate_preview=True,\n        user_gesture=False,\n        await_promise=True,\n        throw_on_side_effect=False,\n        timeout=5000.0,\n        disable_breaks=True,\n        repl_mode=False,\n        allow_unsafe_eval_blocked_by_csp=False,\n        unique_context_id='unique456',\n        serialization_options=serialization_options\n    )\n    assert result['method'] == RuntimeMethod.EVALUATE\n    assert result['params']['expression'] == 'window.location.href'\n    assert result['params']['objectGroup'] == 'testGroup'\n    assert result['params']['includeCommandLineAPI'] is True\n    assert result['params']['silent'] is False\n    assert result['params']['contextId'] == 'ctx123'\n    assert result['params']['returnByValue'] is False\n    assert result['params']['generatePreview'] is True\n    assert result['params']['userGesture'] is False\n    assert result['params']['awaitPromise'] is True\n    assert result['params']['throwOnSideEffect'] is False\n    assert result['params']['timeout'] == 5000.0\n    assert result['params']['disableBreaks'] is True\n    assert result['params']['replMode'] is False\n    assert result['params']['allowUnsafeEvalBlockedByCSP'] is False\n    assert result['params']['uniqueContextId'] == 'unique456'\n    assert result['params']['serializationOptions'] == serialization_options\n\n\ndef test_get_properties_minimal():\n    \"\"\"Test get_properties with minimal parameters.\"\"\"\n    result = RuntimeCommands.get_properties(object_id='obj123')\n    assert result['method'] == RuntimeMethod.GET_PROPERTIES\n    assert result['params']['objectId'] == 'obj123'\n\n\ndef test_get_properties_with_all_params():\n    \"\"\"Test get_properties with all parameters.\"\"\"\n    result = RuntimeCommands.get_properties(\n        object_id='obj123',\n        own_properties=True,\n        accessor_properties_only=False,\n        generate_preview=True,\n        non_indexed_properties_only=False\n    )\n    assert result['method'] == RuntimeMethod.GET_PROPERTIES\n    assert result['params']['objectId'] == 'obj123'\n    assert result['params']['ownProperties'] is True\n    assert result['params']['accessorPropertiesOnly'] is False\n    assert result['params']['generatePreview'] is True\n    assert result['params']['nonIndexedPropertiesOnly'] is False\n\n\ndef test_global_lexical_scope_names_minimal():\n    \"\"\"Test global_lexical_scope_names with minimal parameters.\"\"\"\n    result = RuntimeCommands.global_lexical_scope_names()\n    assert result['method'] == RuntimeMethod.GLOBAL_LEXICAL_SCOPE_NAMES\n    assert result['params'] == {}\n\n\ndef test_global_lexical_scope_names_with_context():\n    \"\"\"Test global_lexical_scope_names with execution context ID.\"\"\"\n    result = RuntimeCommands.global_lexical_scope_names(\n        execution_context_id='ctx123'\n    )\n    assert result['method'] == RuntimeMethod.GLOBAL_LEXICAL_SCOPE_NAMES\n    assert result['params']['executionContextId'] == 'ctx123'\n\n\ndef test_query_objects_minimal():\n    \"\"\"Test query_objects with minimal parameters.\"\"\"\n    result = RuntimeCommands.query_objects(prototype_object_id='proto123')\n    assert result['method'] == RuntimeMethod.QUERY_OBJECTS\n    assert result['params']['prototypeObjectId'] == 'proto123'\n\n\ndef test_query_objects_with_group():\n    \"\"\"Test query_objects with object group.\"\"\"\n    result = RuntimeCommands.query_objects(\n        prototype_object_id='proto123',\n        object_group='testGroup'\n    )\n    assert result['method'] == RuntimeMethod.QUERY_OBJECTS\n    assert result['params']['prototypeObjectId'] == 'proto123'\n    assert result['params']['objectGroup'] == 'testGroup'\n\n\ndef test_release_object():\n    \"\"\"Test release_object method.\"\"\"\n    result = RuntimeCommands.release_object(object_id='obj123')\n    assert result['method'] == RuntimeMethod.RELEASE_OBJECT\n    assert result['params']['objectId'] == 'obj123'\n\n\ndef test_release_object_group():\n    \"\"\"Test release_object_group method.\"\"\"\n    result = RuntimeCommands.release_object_group(object_group='testGroup')\n    assert result['method'] == RuntimeMethod.RELEASE_OBJECT_GROUP\n    assert result['params']['objectGroup'] == 'testGroup'\n\n\ndef test_remove_binding():\n    \"\"\"Test remove_binding method.\"\"\"\n    result = RuntimeCommands.remove_binding(name='testBinding')\n    assert result['method'] == RuntimeMethod.REMOVE_BINDING\n    assert result['params']['name'] == 'testBinding'\n\n\ndef test_run_script_minimal():\n    \"\"\"Test run_script with minimal parameters.\"\"\"\n    result = RuntimeCommands.run_script(script_id='script123')\n    assert result['method'] == RuntimeMethod.RUN_SCRIPT\n    assert result['params']['scriptId'] == 'script123'\n\n\ndef test_run_script_with_all_params():\n    \"\"\"Test run_script with all parameters.\"\"\"\n    result = RuntimeCommands.run_script(\n        script_id='script123',\n        execution_context_id='ctx456',\n        object_group='testGroup',\n        silent=True,\n        include_command_line_api=False,\n        return_by_value=True,\n        generate_preview=False,\n        await_promise=True\n    )\n    assert result['method'] == RuntimeMethod.RUN_SCRIPT\n    assert result['params']['scriptId'] == 'script123'\n    assert result['params']['executionContextId'] == 'ctx456'\n    assert result['params']['objectGroup'] == 'testGroup'\n    assert result['params']['silent'] is True\n    assert result['params']['includeCommandLineAPI'] is False\n    assert result['params']['returnByValue'] is True\n    assert result['params']['generatePreview'] is False\n    assert result['params']['awaitPromise'] is True\n\n\ndef test_set_async_call_stack_depth():\n    \"\"\"Test set_async_call_stack_depth method.\"\"\"\n    result = RuntimeCommands.set_async_call_stack_depth(max_depth=10)\n    assert result['method'] == RuntimeMethod.SET_ASYNC_CALL_STACK_DEPTH\n    assert result['params']['maxDepth'] == 10\n\n\ndef test_set_custom_object_formatter_enabled():\n    \"\"\"Test set_custom_object_formatter_enabled method.\"\"\"\n    result = RuntimeCommands.set_custom_object_formatter_enabled(enabled=True)\n    assert result['method'] == RuntimeMethod.SET_CUSTOM_OBJECT_FORMATTER_ENABLED\n    assert result['params']['enabled'] is True\n\n\ndef test_set_max_call_stack_size_to_capture():\n    \"\"\"Test set_max_call_stack_size_to_capture method.\"\"\"\n    result = RuntimeCommands.set_max_call_stack_size_to_capture(size=100)\n    assert result['method'] == RuntimeMethod.SET_MAX_CALL_STACK_SIZE_TO_CAPTURE\n    assert result['params']['size'] == 100\n\n\ndef test_evaluate_simple_expression():\n    \"\"\"Test evaluate with a simple mathematical expression.\"\"\"\n    result = RuntimeCommands.evaluate(expression='Math.PI * 2')\n    assert result['method'] == RuntimeMethod.EVALUATE\n    assert result['params']['expression'] == 'Math.PI * 2'\n\n\ndef test_call_function_on_with_arguments():\n    \"\"\"Test call_function_on with function arguments.\"\"\"\n    arguments = [\n        {'value': 10},\n        {'value': 20}\n    ]\n    result = RuntimeCommands.call_function_on(\n        function_declaration='function(a, b) { return a * b; }',\n        object_id='obj123',\n        arguments=arguments,\n        return_by_value=True\n    )\n    assert result['method'] == RuntimeMethod.CALL_FUNCTION_ON\n    assert result['params']['functionDeclaration'] == 'function(a, b) { return a * b; }'\n    assert result['params']['objectId'] == 'obj123'\n    assert result['params']['arguments'] == arguments\n    assert result['params']['returnByValue'] is True\n\n\ndef test_get_properties_own_only():\n    \"\"\"Test get_properties with own properties only.\"\"\"\n    result = RuntimeCommands.get_properties(\n        object_id='obj123',\n        own_properties=True,\n        generate_preview=False\n    )\n    assert result['method'] == RuntimeMethod.GET_PROPERTIES\n    assert result['params']['objectId'] == 'obj123'\n    assert result['params']['ownProperties'] is True\n    assert result['params']['generatePreview'] is False\n\n\ndef test_evaluate_with_context():\n    \"\"\"Test evaluate with specific execution context.\"\"\"\n    result = RuntimeCommands.evaluate(\n        expression='this.document.title',\n        context_id='ctx123',\n        include_command_line_api=True\n    )\n    assert result['method'] == RuntimeMethod.EVALUATE\n    assert result['params']['expression'] == 'this.document.title'\n    assert result['params']['contextId'] == 'ctx123'\n    assert result['params']['includeCommandLineAPI'] is True\n\n\ndef test_compile_script_with_source_url():\n    \"\"\"Test compile_script with source URL.\"\"\"\n    result = RuntimeCommands.compile_script(\n        expression='const x = 42; console.log(x);',\n        source_url='test://script.js',\n        persist_script=False\n    )\n    assert result['method'] == RuntimeMethod.COMPILE_SCRIPT\n    assert result['params']['expression'] == 'const x = 42; console.log(x);'\n    assert result['params']['sourceURL'] == 'test://script.js'\n    assert result['params']['persistScript'] is False\n"
  },
  {
    "path": "tests/test_commands/test_storage_commands.py",
    "content": "\"\"\"\nTests for StorageCommands class.\n\nThis module contains comprehensive tests for all StorageCommands methods,\nverifying that they generate the correct CDP commands with proper parameters.\n\"\"\"\n\nfrom pydoll.commands.storage_commands import StorageCommands\nfrom pydoll.protocol.storage.methods import StorageMethod\n\n\ndef test_clear_cookies_minimal():\n    \"\"\"Test clear_cookies with minimal parameters.\"\"\"\n    result = StorageCommands.clear_cookies()\n    assert result['method'] == StorageMethod.CLEAR_COOKIES\n    assert result['params'] == {}\n\n\ndef test_clear_cookies_with_context():\n    \"\"\"Test clear_cookies with browser context ID.\"\"\"\n    result = StorageCommands.clear_cookies(browser_context_id='context123')\n    assert result['method'] == StorageMethod.CLEAR_COOKIES\n    assert result['params']['browserContextId'] == 'context123'\n\n\ndef test_clear_data_for_origin():\n    \"\"\"Test clear_data_for_origin method.\"\"\"\n    result = StorageCommands.clear_data_for_origin(\n        origin='https://example.com',\n        storage_types='cookies,local_storage'\n    )\n    assert result['method'] == StorageMethod.CLEAR_DATA_FOR_ORIGIN\n    assert result['params']['origin'] == 'https://example.com'\n    assert result['params']['storageTypes'] == 'cookies,local_storage'\n\n\ndef test_clear_data_for_storage_key():\n    \"\"\"Test clear_data_for_storage_key method.\"\"\"\n    result = StorageCommands.clear_data_for_storage_key(\n        storage_key='storage_key_123',\n        storage_types='indexeddb,cache_storage'\n    )\n    assert result['method'] == StorageMethod.CLEAR_DATA_FOR_STORAGE_KEY\n    assert result['params']['storageKey'] == 'storage_key_123'\n    assert result['params']['storageTypes'] == 'indexeddb,cache_storage'\n\n\ndef test_get_cookies_minimal():\n    \"\"\"Test get_cookies with minimal parameters.\"\"\"\n    result = StorageCommands.get_cookies()\n    assert result['method'] == StorageMethod.GET_COOKIES\n    assert result['params'] == {}\n\n\ndef test_get_cookies_with_context():\n    \"\"\"Test get_cookies with browser context ID.\"\"\"\n    result = StorageCommands.get_cookies(browser_context_id='context456')\n    assert result['method'] == StorageMethod.GET_COOKIES\n    assert result['params']['browserContextId'] == 'context456'\n\n\ndef test_get_storage_key_for_frame():\n    \"\"\"Test get_storage_key_for_frame method.\"\"\"\n    result = StorageCommands.get_storage_key_for_frame(frame_id='frame123')\n    assert result['method'] == StorageMethod.GET_STORAGE_KEY_FOR_FRAME\n    assert result['params']['frameId'] == 'frame123'\n\n\ndef test_get_usage_and_quota():\n    \"\"\"Test get_usage_and_quota method.\"\"\"\n    result = StorageCommands.get_usage_and_quota(origin='https://example.com')\n    assert result['method'] == StorageMethod.GET_USAGE_AND_QUOTA\n    assert result['params']['origin'] == 'https://example.com'\n\n\ndef test_set_cookies_minimal():\n    \"\"\"Test set_cookies with minimal parameters.\"\"\"\n    cookies = [\n        {'name': 'cookie1', 'value': 'value1', 'domain': 'example.com'},\n        {'name': 'cookie2', 'value': 'value2', 'domain': 'example.com'}\n    ]\n    result = StorageCommands.set_cookies(cookies=cookies)\n    assert result['method'] == StorageMethod.SET_COOKIES\n    assert result['params']['cookies'] == cookies\n\n\ndef test_set_cookies_with_context():\n    \"\"\"Test set_cookies with browser context ID.\"\"\"\n    cookies = [{'name': 'test', 'value': 'value', 'domain': 'test.com'}]\n    result = StorageCommands.set_cookies(\n        cookies=cookies,\n        browser_context_id='context789'\n    )\n    assert result['method'] == StorageMethod.SET_COOKIES\n    assert result['params']['cookies'] == cookies\n    assert result['params']['browserContextId'] == 'context789'\n\n\ndef test_set_protected_audience_k_anonymity():\n    \"\"\"Test set_protected_audience_k_anonymity method.\"\"\"\n    hashes = ['hash1', 'hash2', 'hash3']\n    result = StorageCommands.set_protected_audience_k_anonymity(\n        owner='https://example.com',\n        name='test_group',\n        hashes=hashes\n    )\n    assert result['method'] == StorageMethod.SET_PROTECTED_AUDIENCE_K_ANONYMITY\n    assert result['params']['owner'] == 'https://example.com'\n    assert result['params']['name'] == 'test_group'\n    assert result['params']['hashes'] == hashes\n\n\ndef test_track_cache_storage_for_origin():\n    \"\"\"Test track_cache_storage_for_origin method.\"\"\"\n    result = StorageCommands.track_cache_storage_for_origin(origin='https://example.com')\n    assert result['method'] == StorageMethod.TRACK_CACHE_STORAGE_FOR_ORIGIN\n    assert result['params']['origin'] == 'https://example.com'\n\n\ndef test_track_cache_storage_for_storage_key():\n    \"\"\"Test track_cache_storage_for_storage_key method.\"\"\"\n    result = StorageCommands.track_cache_storage_for_storage_key(storage_key='key123')\n    assert result['method'] == StorageMethod.TRACK_CACHE_STORAGE_FOR_STORAGE_KEY\n    assert result['params']['storageKey'] == 'key123'\n\n\ndef test_track_indexed_db_for_origin():\n    \"\"\"Test track_indexed_db_for_origin method.\"\"\"\n    result = StorageCommands.track_indexed_db_for_origin(origin='https://test.com')\n    assert result['method'] == StorageMethod.TRACK_INDEXED_DB_FOR_ORIGIN\n    assert result['params']['origin'] == 'https://test.com'\n\n\ndef test_track_indexed_db_for_storage_key():\n    \"\"\"Test track_indexed_db_for_storage_key method.\"\"\"\n    result = StorageCommands.track_indexed_db_for_storage_key(storage_key='key456')\n    assert result['method'] == StorageMethod.TRACK_INDEXED_DB_FOR_STORAGE_KEY\n    assert result['params']['storageKey'] == 'key456'\n\n\ndef test_untrack_cache_storage_for_origin():\n    \"\"\"Test untrack_cache_storage_for_origin method.\"\"\"\n    result = StorageCommands.untrack_cache_storage_for_origin(origin='https://example.org')\n    assert result['method'] == StorageMethod.UNTRACK_CACHE_STORAGE_FOR_ORIGIN\n    assert result['params']['origin'] == 'https://example.org'\n\n\ndef test_untrack_cache_storage_for_storage_key():\n    \"\"\"Test untrack_cache_storage_for_storage_key method.\"\"\"\n    result = StorageCommands.untrack_cache_storage_for_storage_key(storage_key='key789')\n    assert result['method'] == StorageMethod.UNTRACK_CACHE_STORAGE_FOR_STORAGE_KEY\n    assert result['params']['storageKey'] == 'key789'\n\n\ndef test_untrack_indexed_db_for_origin():\n    \"\"\"Test untrack_indexed_db_for_origin method.\"\"\"\n    result = StorageCommands.untrack_indexed_db_for_origin(origin='https://test.org')\n    assert result['method'] == StorageMethod.UNTRACK_INDEXED_DB_FOR_ORIGIN\n    assert result['params']['origin'] == 'https://test.org'\n\n\ndef test_untrack_indexed_db_for_storage_key():\n    \"\"\"Test untrack_indexed_db_for_storage_key method.\"\"\"\n    result = StorageCommands.untrack_indexed_db_for_storage_key(storage_key='key000')\n    assert result['method'] == StorageMethod.UNTRACK_INDEXED_DB_FOR_STORAGE_KEY\n    assert result['params']['storageKey'] == 'key000'\n\n\ndef test_clear_shared_storage_entries():\n    \"\"\"Test clear_shared_storage_entries method.\"\"\"\n    result = StorageCommands.clear_shared_storage_entries(owner_origin='https://owner.com')\n    assert result['method'] == StorageMethod.CLEAR_SHARED_STORAGE_ENTRIES\n    assert result['params']['ownerOrigin'] == 'https://owner.com'\n\n\ndef test_clear_trust_tokens():\n    \"\"\"Test clear_trust_tokens method.\"\"\"\n    result = StorageCommands.clear_trust_tokens(issuer_origin='https://issuer.com')\n    assert result['method'] == StorageMethod.CLEAR_TRUST_TOKENS\n    assert result['params']['issuerOrigin'] == 'https://issuer.com'\n\n\ndef test_delete_shared_storage_entry():\n    \"\"\"Test delete_shared_storage_entry method.\"\"\"\n    result = StorageCommands.delete_shared_storage_entry(\n        owner_origin='https://owner.com',\n        key='test_key'\n    )\n    assert result['method'] == StorageMethod.DELETE_SHARED_STORAGE_ENTRY\n    assert result['params']['ownerOrigin'] == 'https://owner.com'\n    assert result['params']['key'] == 'test_key'\n\n\ndef test_delete_storage_bucket():\n    \"\"\"Test delete_storage_bucket method.\"\"\"\n    bucket = {\n        'storageKey': 'key123',\n        'name': 'test_bucket'\n    }\n    result = StorageCommands.delete_storage_bucket(bucket=bucket)\n    assert result['method'] == StorageMethod.DELETE_STORAGE_BUCKET\n    assert result['params']['bucket'] == bucket\n\n\ndef test_get_affected_urls_for_third_party_cookie_metadata():\n    \"\"\"Test get_affected_urls_for_third_party_cookie_metadata method.\"\"\"\n    third_party_urls = ['https://third1.com', 'https://third2.com']\n    result = StorageCommands.get_affected_urls_for_third_party_cookie_metadata(\n        first_party_url='https://first.com',\n        third_party_urls=third_party_urls\n    )\n    assert result['method'] == StorageMethod.GET_AFFECTED_URLS_FOR_THIRD_PARTY_COOKIE_METADATA\n    assert result['params']['firstPartyUrl'] == 'https://first.com'\n    assert result['params']['thirdPartyUrls'] == third_party_urls\n\n\ndef test_get_interest_group_details():\n    \"\"\"Test get_interest_group_details method.\"\"\"\n    result = StorageCommands.get_interest_group_details(\n        owner_origin='https://owner.com',\n        name='interest_group_1'\n    )\n    assert result['method'] == StorageMethod.GET_INTEREST_GROUP_DETAILS\n    assert result['params']['ownerOrigin'] == 'https://owner.com'\n    assert result['params']['name'] == 'interest_group_1'\n\n\ndef test_get_related_website_sets():\n    \"\"\"Test get_related_website_sets method.\"\"\"\n    result = StorageCommands.get_related_website_sets()\n    assert result['method'] == StorageMethod.GET_RELATED_WEBSITE_SETS\n\n\ndef test_get_shared_storage_entries():\n    \"\"\"Test get_shared_storage_entries method.\"\"\"\n    result = StorageCommands.get_shared_storage_entries(owner_origin='https://shared.com')\n    assert result['method'] == StorageMethod.GET_SHARED_STORAGE_ENTRIES\n    assert result['params']['ownerOrigin'] == 'https://shared.com'\n\n\ndef test_get_shared_storage_metadata():\n    \"\"\"Test get_shared_storage_metadata method.\"\"\"\n    result = StorageCommands.get_shared_storage_metadata(owner_origin='https://metadata.com')\n    assert result['method'] == StorageMethod.GET_SHARED_STORAGE_METADATA\n    assert result['params']['ownerOrigin'] == 'https://metadata.com'\n\n\ndef test_get_trust_tokens():\n    \"\"\"Test get_trust_tokens method.\"\"\"\n    result = StorageCommands.get_trust_tokens()\n    assert result['method'] == StorageMethod.GET_TRUST_TOKENS\n    assert result['params'] == {}\n\n\ndef test_override_quota_for_origin_minimal():\n    \"\"\"Test override_quota_for_origin with minimal parameters.\"\"\"\n    result = StorageCommands.override_quota_for_origin(origin='https://quota.com')\n    assert result['method'] == StorageMethod.OVERRIDE_QUOTA_FOR_ORIGIN\n    assert result['params']['origin'] == 'https://quota.com'\n\n\ndef test_override_quota_for_origin_with_size():\n    \"\"\"Test override_quota_for_origin with quota size.\"\"\"\n    result = StorageCommands.override_quota_for_origin(\n        origin='https://quota.com',\n        quota_size=1024000.0\n    )\n    assert result['method'] == StorageMethod.OVERRIDE_QUOTA_FOR_ORIGIN\n    assert result['params']['origin'] == 'https://quota.com'\n    assert result['params']['quotaSize'] == 1024000.0\n\n\ndef test_reset_shared_storage_budget():\n    \"\"\"Test reset_shared_storage_budget method.\"\"\"\n    result = StorageCommands.reset_shared_storage_budget(owner_origin='https://budget.com')\n    assert result['method'] == StorageMethod.RESET_SHARED_STORAGE_BUDGET\n    assert result['params']['ownerOrigin'] == 'https://budget.com'\n\n\ndef test_run_bounce_tracking_mitigations():\n    \"\"\"Test run_bounce_tracking_mitigations method.\"\"\"\n    result = StorageCommands.run_bounce_tracking_mitigations()\n    assert result['method'] == StorageMethod.RUN_BOUNCE_TRACKING_MITIGATIONS\n    assert result['params'] == {}\n\n\ndef test_send_pending_attribution_reports():\n    \"\"\"Test send_pending_attribution_reports method.\"\"\"\n    result = StorageCommands.send_pending_attribution_reports()\n    assert result['method'] == StorageMethod.SEND_PENDING_ATTRIBUTION_REPORTS\n    assert result['params'] == {}\n\n\ndef test_set_attribution_reporting_local_testing_mode():\n    \"\"\"Test set_attribution_reporting_local_testing_mode method.\"\"\"\n    result = StorageCommands.set_attribution_reporting_local_testing_mode(enabled=True)\n    assert result['method'] == StorageMethod.SET_ATTRIBUTION_REPORTING_LOCAL_TESTING_MODE\n    assert result['params']['enabled'] is True\n\n\ndef test_set_attribution_reporting_tracking():\n    \"\"\"Test set_attribution_reporting_tracking method.\"\"\"\n    result = StorageCommands.set_attribution_reporting_tracking(enable=False)\n    assert result['method'] == StorageMethod.SET_ATTRIBUTION_REPORTING_TRACKING\n    assert result['params']['enable'] is False\n\n\ndef test_set_interest_group_auction_tracking():\n    \"\"\"Test set_interest_group_auction_tracking method.\"\"\"\n    result = StorageCommands.set_interest_group_auction_tracking(enable=True)\n    assert result['method'] == StorageMethod.SET_INTEREST_GROUP_AUCTION_TRACKING\n    assert result['params']['enable'] is True\n\n\ndef test_set_interest_group_tracking():\n    \"\"\"Test set_interest_group_tracking method.\"\"\"\n    result = StorageCommands.set_interest_group_tracking(enable=False)\n    assert result['method'] == StorageMethod.SET_INTEREST_GROUP_TRACKING\n    assert result['params']['enable'] is False\n\n\ndef test_set_shared_storage_entry_minimal():\n    \"\"\"Test set_shared_storage_entry with minimal parameters.\"\"\"\n    result = StorageCommands.set_shared_storage_entry(\n        owner_origin='https://storage.com',\n        key='test_key',\n        value='test_value'\n    )\n    assert result['method'] == StorageMethod.SET_SHARED_STORAGE_ENTRY\n    assert result['params']['ownerOrigin'] == 'https://storage.com'\n    assert result['params']['key'] == 'test_key'\n    assert result['params']['value'] == 'test_value'\n\n\ndef test_set_shared_storage_entry_with_ignore():\n    \"\"\"Test set_shared_storage_entry with ignore_if_present parameter.\"\"\"\n    result = StorageCommands.set_shared_storage_entry(\n        owner_origin='https://storage.com',\n        key='test_key',\n        value='test_value',\n        ignore_if_present=True\n    )\n    assert result['method'] == StorageMethod.SET_SHARED_STORAGE_ENTRY\n    assert result['params']['ownerOrigin'] == 'https://storage.com'\n    assert result['params']['key'] == 'test_key'\n    assert result['params']['value'] == 'test_value'\n    assert result['params']['ignoreIfPresent'] is True\n\n\ndef test_set_shared_storage_tracking():\n    \"\"\"Test set_shared_storage_tracking method.\"\"\"\n    result = StorageCommands.set_shared_storage_tracking(enable=True)\n    assert result['method'] == StorageMethod.SET_SHARED_STORAGE_TRACKING\n    assert result['params']['enable'] is True\n\n\ndef test_set_storage_bucket_tracking():\n    \"\"\"Test set_storage_bucket_tracking method.\"\"\"\n    result = StorageCommands.set_storage_bucket_tracking(\n        storage_key='bucket_key_123',\n        enable=False\n    )\n    assert result['method'] == StorageMethod.SET_STORAGE_BUCKET_TRACKING\n    assert result['params']['storageKey'] == 'bucket_key_123'\n    assert result['params']['enable'] is False\n\n\ndef test_clear_data_for_origin_all_types():\n    \"\"\"Test clear_data_for_origin with all storage types.\"\"\"\n    result = StorageCommands.clear_data_for_origin(\n        origin='https://example.com',\n        storage_types='all'\n    )\n    assert result['method'] == StorageMethod.CLEAR_DATA_FOR_ORIGIN\n    assert result['params']['origin'] == 'https://example.com'\n    assert result['params']['storageTypes'] == 'all'\n\n\ndef test_set_cookies_complex():\n    \"\"\"Test set_cookies with complex cookie parameters.\"\"\"\n    cookies = [\n        {\n            'name': 'session_id',\n            'value': 'abc123',\n            'domain': 'example.com',\n            'path': '/',\n            'secure': True,\n            'httpOnly': True,\n            'sameSite': 'Strict'\n        }\n    ]\n    result = StorageCommands.set_cookies(cookies=cookies)\n    assert result['method'] == StorageMethod.SET_COOKIES\n    assert result['params']['cookies'] == cookies\n"
  },
  {
    "path": "tests/test_commands/test_target_commands.py",
    "content": "\"\"\"\nTests for TargetCommands class.\n\nThis module contains comprehensive tests for all TargetCommands methods,\nverifying that they generate the correct CDP commands with proper parameters.\n\"\"\"\n\nfrom pydoll.commands.target_commands import TargetCommands\nfrom pydoll.protocol.browser.types import WindowState\nfrom pydoll.protocol.target.methods import TargetMethod\n\n\ndef test_activate_target():\n    \"\"\"Test activate_target method.\"\"\"\n    result = TargetCommands.activate_target(target_id='target123')\n    assert result['method'] == TargetMethod.ACTIVATE_TARGET\n    assert result['params']['targetId'] == 'target123'\n\n\ndef test_attach_to_target_minimal():\n    \"\"\"Test attach_to_target with minimal parameters.\"\"\"\n    result = TargetCommands.attach_to_target(target_id='target456')\n    assert result['method'] == TargetMethod.ATTACH_TO_TARGET\n    assert result['params']['targetId'] == 'target456'\n\n\ndef test_attach_to_target_with_flatten():\n    \"\"\"Test attach_to_target with flatten parameter.\"\"\"\n    result = TargetCommands.attach_to_target(target_id='target456', flatten=True)\n    assert result['method'] == TargetMethod.ATTACH_TO_TARGET\n    assert result['params']['targetId'] == 'target456'\n    assert result['params']['flatten'] is True\n\n\ndef test_close_target():\n    \"\"\"Test close_target method.\"\"\"\n    result = TargetCommands.close_target(target_id='target789')\n    assert result['method'] == TargetMethod.CLOSE_TARGET\n    assert result['params']['targetId'] == 'target789'\n\n\ndef test_create_browser_context_minimal():\n    \"\"\"Test create_browser_context with minimal parameters.\"\"\"\n    result = TargetCommands.create_browser_context()\n    assert result['method'] == TargetMethod.CREATE_BROWSER_CONTEXT\n    assert result['params'] == {}\n\n\ndef test_create_browser_context_with_all_params():\n    \"\"\"Test create_browser_context with all parameters.\"\"\"\n    origins = ['https://example.com', 'https://test.com']\n    result = TargetCommands.create_browser_context(\n        dispose_on_detach=True,\n        proxy_server='socks5://192.168.1.100:1080',\n        proxy_bypass_list='*.example.com,localhost',\n        origins_with_universal_network_access=origins\n    )\n    assert result['method'] == TargetMethod.CREATE_BROWSER_CONTEXT\n    assert result['params']['disposeOnDetach'] is True\n    assert result['params']['proxyServer'] == 'socks5://192.168.1.100:1080'\n    assert result['params']['proxyBypassList'] == '*.example.com,localhost'\n    assert result['params']['originsWithUniversalNetworkAccess'] == origins\n\n\ndef test_create_target_minimal():\n    \"\"\"Test create_target with minimal parameters.\"\"\"\n    result = TargetCommands.create_target(url='https://example.com')\n    assert result['method'] == TargetMethod.CREATE_TARGET\n    assert result['params']['url'] == 'https://example.com'\n\n\ndef test_create_target_with_position_and_size():\n    \"\"\"Test create_target with position and size parameters.\"\"\"\n    result = TargetCommands.create_target(\n        url='https://test.com',\n        left=100,\n        top=200,\n        width=800,\n        height=600\n    )\n    assert result['method'] == TargetMethod.CREATE_TARGET\n    assert result['params']['url'] == 'https://test.com'\n    assert result['params']['left'] == 100\n    assert result['params']['top'] == 200\n    assert result['params']['width'] == 800\n    assert result['params']['height'] == 600\n\n\ndef test_create_target_with_window_state():\n    \"\"\"Test create_target with window state.\"\"\"\n    result = TargetCommands.create_target(\n        url='https://example.com',\n        window_state=WindowState.MAXIMIZED\n    )\n    assert result['method'] == TargetMethod.CREATE_TARGET\n    assert result['params']['url'] == 'https://example.com'\n    assert result['params']['windowState'] == WindowState.MAXIMIZED\n\n\ndef test_create_target_with_all_params():\n    \"\"\"Test create_target with all parameters.\"\"\"\n    result = TargetCommands.create_target(\n        url='https://full-test.com',\n        left=50,\n        top=100,\n        width=1200,\n        height=800,\n        window_state=WindowState.NORMAL,\n        browser_context_id='context123',\n        enable_begin_frame_control=True,\n        new_window=False,\n        background=True,\n        for_tab=False,\n        hidden=True\n    )\n    assert result['method'] == TargetMethod.CREATE_TARGET\n    assert result['params']['url'] == 'https://full-test.com'\n    assert result['params']['left'] == 50\n    assert result['params']['top'] == 100\n    assert result['params']['width'] == 1200\n    assert result['params']['height'] == 800\n    assert result['params']['windowState'] == WindowState.NORMAL\n    assert result['params']['browserContextId'] == 'context123'\n    assert result['params']['enableBeginFrameControl'] is True\n    assert result['params']['newWindow'] is False\n    assert result['params']['background'] is True\n    assert result['params']['forTab'] is False\n    assert result['params']['hidden'] is True\n\n\ndef test_detach_from_target_minimal():\n    \"\"\"Test detach_from_target with minimal parameters.\"\"\"\n    result = TargetCommands.detach_from_target()\n    assert result['method'] == TargetMethod.DETACH_FROM_TARGET\n    assert result['params'] == {}\n\n\ndef test_detach_from_target_with_session():\n    \"\"\"Test detach_from_target with session ID.\"\"\"\n    result = TargetCommands.detach_from_target(session_id='session123')\n    assert result['method'] == TargetMethod.DETACH_FROM_TARGET\n    assert result['params']['sessionId'] == 'session123'\n\n\ndef test_dispose_browser_context():\n    \"\"\"Test dispose_browser_context method.\"\"\"\n    result = TargetCommands.dispose_browser_context(browser_context_id='context456')\n    assert result['method'] == TargetMethod.DISPOSE_BROWSER_CONTEXT\n    assert result['params']['browserContextId'] == 'context456'\n\n\ndef test_get_browser_contexts():\n    \"\"\"Test get_browser_contexts method.\"\"\"\n    result = TargetCommands.get_browser_contexts()\n    assert result['method'] == TargetMethod.GET_BROWSER_CONTEXTS\n    assert result['params'] == {}\n\n\ndef test_get_targets_minimal():\n    \"\"\"Test get_targets with minimal parameters.\"\"\"\n    result = TargetCommands.get_targets()\n    assert result['method'] == TargetMethod.GET_TARGETS\n    assert result['params'] == {}\n\n\ndef test_get_targets_with_filter():\n    \"\"\"Test get_targets with filter parameter.\"\"\"\n    filter_list = [{'type': 'page'}, {'type': 'worker'}]\n    result = TargetCommands.get_targets(filter=filter_list)\n    assert result['method'] == TargetMethod.GET_TARGETS\n    assert result['params']['filter'] == filter_list\n\n\ndef test_set_auto_attach_minimal():\n    \"\"\"Test set_auto_attach with minimal parameters.\"\"\"\n    result = TargetCommands.set_auto_attach(auto_attach=True)\n    assert result['method'] == TargetMethod.SET_AUTO_ATTACH\n    assert result['params']['autoAttach'] is True\n\n\ndef test_set_auto_attach_with_all_params():\n    \"\"\"Test set_auto_attach with all parameters.\"\"\"\n    filter_list = [{'type': 'page'}]\n    result = TargetCommands.set_auto_attach(\n        auto_attach=False,\n        wait_for_debugger_on_start=True,\n        flatten=False,\n        filter=filter_list\n    )\n    assert result['method'] == TargetMethod.SET_AUTO_ATTACH\n    assert result['params']['autoAttach'] is False\n    assert result['params']['waitForDebuggerOnStart'] is True\n    assert result['params']['flatten'] is False\n    assert result['params']['filter'] == filter_list\n\n\ndef test_set_discover_targets_minimal():\n    \"\"\"Test set_discover_targets with minimal parameters.\"\"\"\n    result = TargetCommands.set_discover_targets(discover=True)\n    assert result['method'] == TargetMethod.SET_DISCOVER_TARGETS\n    assert result['params']['discover'] is True\n\n\ndef test_set_discover_targets_with_filter():\n    \"\"\"Test set_discover_targets with filter parameter.\"\"\"\n    filter_list = [{'type': 'service_worker'}]\n    result = TargetCommands.set_discover_targets(discover=False, filter=filter_list)\n    assert result['method'] == TargetMethod.SET_DISCOVER_TARGETS\n    assert result['params']['discover'] is False\n    assert result['params']['filter'] == filter_list\n\n\ndef test_attach_to_browser_target():\n    \"\"\"Test attach_to_browser_target method.\"\"\"\n    result = TargetCommands.attach_to_browser_target(session_id='browser_session123')\n    assert result['method'] == TargetMethod.ATTACH_TO_BROWSER_TARGET\n    assert result['params']['sessionId'] == 'browser_session123'\n\n\ndef test_get_target_info():\n    \"\"\"Test get_target_info method.\"\"\"\n    result = TargetCommands.get_target_info(target_id='info_target123')\n    assert result['method'] == TargetMethod.GET_TARGET_INFO\n    assert result['params']['targetId'] == 'info_target123'\n\n\ndef test_set_remote_locations():\n    \"\"\"Test set_remote_locations method.\"\"\"\n    locations = [\n        {\n            'host': 'remote1.example.com',\n            'port': 9222\n        },\n        {\n            'host': 'remote2.example.com',\n            'port': 9223\n        }\n    ]\n    result = TargetCommands.set_remote_locations(locations=locations)\n    assert result['method'] == TargetMethod.SET_REMOTE_LOCATIONS\n    assert result['params']['locations'] == locations\n\n\ndef test_create_target_about_blank():\n    \"\"\"Test create_target with about:blank URL.\"\"\"\n    result = TargetCommands.create_target(url='')\n    assert result['method'] == TargetMethod.CREATE_TARGET\n    assert result['params']['url'] == ''\n\n\ndef test_create_target_new_window():\n    \"\"\"Test create_target with new window option.\"\"\"\n    result = TargetCommands.create_target(\n        url='https://newwindow.com',\n        new_window=True,\n        width=1024,\n        height=768\n    )\n    assert result['method'] == TargetMethod.CREATE_TARGET\n    assert result['params']['url'] == 'https://newwindow.com'\n    assert result['params']['newWindow'] is True\n    assert result['params']['width'] == 1024\n    assert result['params']['height'] == 768\n\n\ndef test_create_target_background():\n    \"\"\"Test create_target with background option.\"\"\"\n    result = TargetCommands.create_target(\n        url='https://background.com',\n        background=True\n    )\n    assert result['method'] == TargetMethod.CREATE_TARGET\n    assert result['params']['url'] == 'https://background.com'\n    assert result['params']['background'] is True\n\n\ndef test_create_target_for_tab():\n    \"\"\"Test create_target with for_tab option.\"\"\"\n    result = TargetCommands.create_target(\n        url='https://tab.com',\n        for_tab=True\n    )\n    assert result['method'] == TargetMethod.CREATE_TARGET\n    assert result['params']['url'] == 'https://tab.com'\n    assert result['params']['forTab'] is True\n\n\ndef test_create_target_hidden():\n    \"\"\"Test create_target with hidden option.\"\"\"\n    result = TargetCommands.create_target(\n        url='https://hidden.com',\n        hidden=True\n    )\n    assert result['method'] == TargetMethod.CREATE_TARGET\n    assert result['params']['url'] == 'https://hidden.com'\n    assert result['params']['hidden'] is True\n\n\ndef test_create_browser_context_with_proxy():\n    \"\"\"Test create_browser_context with proxy configuration.\"\"\"\n    result = TargetCommands.create_browser_context(\n        proxy_server='http://proxy.example.com:8080',\n        proxy_bypass_list='localhost,127.0.0.1'\n    )\n    assert result['method'] == TargetMethod.CREATE_BROWSER_CONTEXT\n    assert result['params']['proxyServer'] == 'http://proxy.example.com:8080'\n    assert result['params']['proxyBypassList'] == 'localhost,127.0.0.1'\n\n\ndef test_create_target_with_context():\n    \"\"\"Test create_target with browser context.\"\"\"\n    result = TargetCommands.create_target(\n        url='https://context-test.com',\n        browser_context_id='isolated_context'\n    )\n    assert result['method'] == TargetMethod.CREATE_TARGET\n    assert result['params']['url'] == 'https://context-test.com'\n    assert result['params']['browserContextId'] == 'isolated_context'\n\n\ndef test_set_auto_attach_disabled():\n    \"\"\"Test set_auto_attach with auto attach disabled.\"\"\"\n    result = TargetCommands.set_auto_attach(\n        auto_attach=False,\n        wait_for_debugger_on_start=False\n    )\n    assert result['method'] == TargetMethod.SET_AUTO_ATTACH\n    assert result['params']['autoAttach'] is False\n    assert result['params']['waitForDebuggerOnStart'] is False\n"
  },
  {
    "path": "tests/test_connection_handler.py",
    "content": "import asyncio\nimport json\nfrom unittest.mock import AsyncMock, MagicMock\n\nimport pytest\nimport pytest_asyncio\nimport websockets\nfrom websockets.protocol import State\n\nfrom pydoll import exceptions\nfrom pydoll.connection import ConnectionHandler\n\n\n@pytest_asyncio.fixture\nasync def connection_handler():\n    handler = ConnectionHandler(connection_port=9222)\n    handler._ws_connection = AsyncMock()\n    handler._ws_connection.state = State.OPEN\n    return handler\n\n\n@pytest_asyncio.fixture\nasync def connection_handler_closed():\n    handler = ConnectionHandler(\n        connection_port=9222,\n        ws_address_resolver=AsyncMock(return_value='ws://localhost:9222'),\n        ws_connector=AsyncMock(),\n    )\n    handler._ws_connection = AsyncMock()\n    handler._ws_connection.state = State.CLOSED\n    return handler\n\n\n@pytest_asyncio.fixture\nasync def connection_handler_with_page_id():\n    handler = ConnectionHandler(\n        page_id='ABCD',\n        connection_port=9222,\n        ws_address_resolver=AsyncMock(return_value='ws://localhost:9222'),\n        ws_connector=AsyncMock(),\n    )\n    handler._ws_connection = AsyncMock()\n    handler._ws_connection.state = State.CLOSED\n    return handler\n\n\n@pytest.mark.asyncio\nasync def test_resolve_ws_address_priority_ws_address_over_port_and_page():\n    handler = ConnectionHandler(\n        connection_port=9333,\n        page_id='SHOULD_NOT_BE_USED',\n        ws_address='ws://host:9999/devtools/page/REAL',\n        ws_address_resolver=AsyncMock(return_value='ws://host:9333/devtools/browser/ALT'),\n        ws_connector=AsyncMock(),\n    )\n    # Should return the explicit ws_address regardless of others\n    resolved = await handler._resolve_ws_address()\n    assert resolved == 'ws://host:9999/devtools/page/REAL'\n\n\n@pytest.mark.asyncio\nasync def test_ping_success(connection_handler):\n    connection_handler._ws_connection.ping = AsyncMock()\n    result = await connection_handler.ping()\n    assert result is True\n\n\n@pytest.mark.asyncio\nasync def test_ping_failure(connection_handler):\n    connection_handler._ws_connection.ping = AsyncMock(\n        side_effect=Exception('Ping failed')\n    )\n    result = await connection_handler.ping()\n    assert result is False\n\n\n@pytest.mark.asyncio\nasync def test_execute_command_success(connection_handler):\n    command = {'id': 1, 'method': 'SomeMethod'}\n    response = json.dumps({'id': 1, 'result': 'success'})\n\n    connection_handler._ws_connection.send = AsyncMock()\n    future = asyncio.Future()\n    future.set_result(response)\n    connection_handler._command_manager.create_command_future = MagicMock(\n        return_value=future\n    )\n    result = await connection_handler.execute_command(command)\n    assert result == {'id': 1, 'result': 'success'}\n\n\n@pytest.mark.asyncio\nasync def test_execute_command_timeout(connection_handler):\n    command = {'id': 2, 'method': 'TimeoutMethod'}\n\n    connection_handler._ws_connection.send = AsyncMock()\n    connection_handler._command_manager.create_command_future = MagicMock(\n        return_value=asyncio.Future()\n    )\n\n    with pytest.raises(exceptions.CommandExecutionTimeout):\n        await connection_handler.execute_command(command, timeout=0.1)\n\n\n@pytest.mark.asyncio\nasync def test_execute_command_connection_closed_exception(connection_handler):\n    connection_handler._ws_connection.send = AsyncMock(\n        side_effect=websockets.ConnectionClosed(\n            1000, 'Normal Closure', rcvd_then_sent=True\n        )\n    )\n    connection_handler._ws_connection.close = AsyncMock()\n    connection_handler._receive_task = AsyncMock(spec=asyncio.Task)\n    connection_handler._receive_task.done = MagicMock(return_value=False)\n    with pytest.raises(exceptions.WebSocketConnectionClosed):\n        await connection_handler.execute_command({\n            'id': 1,\n            'method': 'SomeMethod',\n        })\n\n\n@pytest.mark.asyncio\nasync def test_register_callback(connection_handler):\n    connection_handler._events_handler.register_callback = MagicMock(\n        return_value=123\n    )\n    callback_id = await connection_handler.register_callback(\n        'event', lambda x: x\n    )\n    assert callback_id == 123\n\n\n@pytest.mark.asyncio\nasync def test_remove_callback(connection_handler):\n    connection_handler._events_handler.remove_callback = MagicMock(\n        return_value=True\n    )\n    result = await connection_handler.remove_callback(123)\n    assert result is True\n\n\n@pytest.mark.asyncio\nasync def test_clear_callbacks(connection_handler):\n    connection_handler._events_handler.clear_callbacks = MagicMock(\n        return_value=None\n    )\n    result = await connection_handler.clear_callbacks()\n    connection_handler._events_handler.clear_callbacks.assert_called_once()\n    assert result is None\n\n\n@pytest.mark.asyncio\nasync def test_close(connection_handler):\n    connection_handler._ws_connection.close = AsyncMock()\n    connection_handler.clear_callbacks = AsyncMock()\n\n    await connection_handler.close()\n    connection_handler.clear_callbacks.assert_awaited_once()\n    connection_handler._ws_connection.close.assert_awaited_once()\n\n    connection_handler._ws_connection.close.side_effect = websockets.ConnectionClosed(\n        1000, 'Normal Closure', rcvd_then_sent=True\n    )\n    await connection_handler.close()\n\n\n@pytest.mark.asyncio\nasync def test_execute_command_connection_closed(connection_handler_closed):\n    mock_connector = AsyncMock(\n        return_value=connection_handler_closed._ws_connection\n    )\n    connection_handler_closed._ws_connector = mock_connector\n\n    command = {'id': 1, 'method': 'SomeMethod'}\n    response = json.dumps({'id': 1, 'result': 'success'})\n\n    connection_handler_closed._ws_connection.send = AsyncMock()\n    future = asyncio.Future()\n    future.set_result(response)\n    connection_handler_closed._command_manager.create_command_future = (\n        MagicMock(return_value=future)\n    )\n    result = await connection_handler_closed.execute_command(command)\n    mock_connector.assert_awaited_once()  # Verifica se tentou reconectar\n    connection_handler_closed._ws_connection.send.assert_awaited_once_with(\n        json.dumps(command)\n    )\n    assert result == {'id': 1, 'result': 'success'}\n\n\n@pytest.mark.asyncio\nasync def test__is_command_response_true(connection_handler):\n    command = {'id': 1, 'method': 'SomeMethod'}\n    result = connection_handler._is_command_response(command)\n    assert result is True\n\n\n@pytest.mark.asyncio\nasync def test__is_command_response_false(connection_handler):\n    command = {'id': 'string', 'method': 'SomeMethod'}\n    result = connection_handler._is_command_response(command)\n    assert result is False\n\n\n@pytest.mark.asyncio\nasync def test__resolve_ws_address_with_page_id(\n    connection_handler_with_page_id,\n):\n    result = await connection_handler_with_page_id._resolve_ws_address()\n    assert result == 'ws://localhost:9222/devtools/page/ABCD'\n\n\n@pytest.mark.asyncio\nasync def test__incoming_messages(connection_handler):\n    connection_handler._ws_connection.recv = AsyncMock(\n        return_value='{\"id\": 1, \"method\": \"SomeMethod\"}'\n    )\n    async_generator = connection_handler._incoming_messages()\n    result = await anext(async_generator)\n    assert result == '{\"id\": 1, \"method\": \"SomeMethod\"}'\n\n\n@pytest.mark.asyncio\nasync def test__process_single_message(connection_handler):\n    raw_message = '{\"id\": 1, \"method\": \"SomeMethod\"}'\n    connection_handler._command_manager.resolve_command = MagicMock()\n    await connection_handler._process_single_message(raw_message)\n    connection_handler._command_manager.resolve_command.assert_called_once_with(\n        1, raw_message\n    )\n\n\n@pytest.mark.asyncio\nasync def test__process_single_message_invalid_command(connection_handler):\n    raw_message = 'not a valid JSON'\n    result = await connection_handler._process_single_message(raw_message)\n    assert result is None\n\n\n@pytest.mark.asyncio\nasync def test__process_single_message_event(connection_handler):\n    event = {'method': 'SomeEvent'}\n    connection_handler._events_handler.process_event = AsyncMock()\n    await connection_handler._process_single_message(json.dumps(event))\n    connection_handler._events_handler.process_event.assert_called_once_with(\n        event\n    )\n\n\n@pytest.mark.asyncio\nasync def test__process_single_message_event_with_callback(connection_handler):\n    event = {'method': 'SomeEvent'}\n    callback = MagicMock(return_value=None)\n    await connection_handler.register_callback('SomeEvent', callback)\n    await connection_handler._process_single_message(json.dumps(event))\n    callback.assert_called_once_with(event)\n\n\n@pytest.mark.asyncio\nasync def test__receive_events_flow(connection_handler):\n    async def fake_incoming_messages():\n        yield '{\"id\": 1, \"method\": \"TestCommand\"}'\n        yield '{\"method\": \"TestEvent\"}'\n\n    connection_handler._incoming_messages = fake_incoming_messages\n\n    connection_handler._handle_command_message = AsyncMock()\n    connection_handler._handle_event_message = AsyncMock()\n\n    await connection_handler._receive_events()\n\n    connection_handler._handle_command_message.assert_awaited_once_with({\n        'id': 1,\n        'method': 'TestCommand',\n    })\n    connection_handler._handle_event_message.assert_awaited_once_with({\n        'method': 'TestEvent'\n    })\n\n\n@pytest.mark.asyncio\nasync def test__receive_events_connection_closed(connection_handler):\n    async def fake_incoming_messages_connection_closed():\n        raise websockets.ConnectionClosed(\n            1000, 'Normal Closure', rcvd_then_sent=True\n        )\n        yield  # Garante que seja um async generator\n\n    connection_handler._incoming_messages = (\n        fake_incoming_messages_connection_closed\n    )\n    await connection_handler._receive_events()\n\n\n@pytest.mark.asyncio\nasync def test__receive_events_unexpected_exception(connection_handler):\n    async def fake_incoming_messages_unexpected_error():\n        raise ValueError('Unexpected error in async generator')\n        yield  # Garante que seja um async generator\n\n    connection_handler._incoming_messages = (\n        fake_incoming_messages_unexpected_error\n    )\n\n    with pytest.raises(\n        ValueError, match='Unexpected error in async generator'\n    ):\n        await connection_handler._receive_events()\n\n\n@pytest.mark.asyncio\nasync def test__aenter__(connection_handler):\n    result = await connection_handler.__aenter__()\n    assert result is connection_handler\n\n\n@pytest.mark.asyncio\nasync def test__aexit__(connection_handler):\n    await connection_handler.register_callback('SomeEvent', MagicMock())\n    connection_handler.clear_callbacks = AsyncMock()\n    connection_handler._ws_connection.close = AsyncMock()\n    await connection_handler.__aexit__(None, None, None)\n    connection_handler.clear_callbacks.assert_awaited_once()\n    connection_handler._ws_connection.close.assert_awaited_once()\n\n\ndef test__repr__(connection_handler):\n    result = connection_handler.__repr__()\n    assert result == 'ConnectionHandler(port=9222)'\n\n\ndef test__str__(connection_handler):\n    result = connection_handler.__str__()\n    assert result == 'ConnectionHandler(port=9222)'\n"
  },
  {
    "path": "tests/test_core_integration.py",
    "content": "\"\"\"Integration tests for core WebElement/Tab behaviors (non-iframe).\"\"\"\n\nimport asyncio\nfrom pathlib import Path\n\nimport pytest\n\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.elements.web_element import WebElement\n\n\nclass TestCoreFindQuery:\n    \"\"\"Find and query basics on a simple page.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_find_by_common_selectors(self, ci_chrome_options):\n        test_file = Path(__file__).parent / 'pages' / 'test_core_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(0.5)\n\n            # id\n            heading = await tab.find(id='main-heading')\n            assert heading is not None\n            assert isinstance(heading, WebElement)\n            assert heading.get_attribute('id') == 'main-heading'\n\n            # class_name (first occurrence)\n            first_item = await tab.find(class_name='item')\n            assert first_item is not None\n            assert 'item' in (first_item.get_attribute('class') or '')\n\n            # name\n            name_input = await tab.find(name='username')\n            assert name_input is not None\n            assert name_input.get_attribute('id') == 'text-input'\n\n            # tag_name (first button)\n            button = await tab.find(tag_name='button')\n            assert button is not None\n            assert button.get_attribute('id') == 'btn-1'\n\n    @pytest.mark.asyncio\n    async def test_query_css_and_xpath(self, ci_chrome_options):\n        test_file = Path(__file__).parent / 'pages' / 'test_core_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(0.5)\n\n            # CSS: list items\n            items = await tab.query('.list-item', find_all=True)\n            assert items is not None\n            assert len(items) == 3\n\n            # XPath absolute\n            deep_span = await tab.query('//*[@id=\"deep-section\"]//span[@id=\"deep-span\"]')\n            assert deep_span is not None\n            text = await deep_span.text\n            assert 'Deep nested element' in text\n\n            # XPath relative from container\n            container = await tab.find(id='deep-section')\n            rel_span = await container.find(xpath='.//span[@id=\"deep-span\"]')\n            assert rel_span is not None\n            text2 = await rel_span.text\n            assert 'Deep nested element' in text2\n\n\nclass TestCoreClickAndInput:\n    \"\"\"Click and text insertion behaviors.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_click_increments_counter(self, ci_chrome_options):\n        test_file = Path(__file__).parent / 'pages' / 'test_core_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(0.5)\n\n            button = await tab.find(id='btn-1')\n            counter = await tab.find(id='btn-1-count')\n\n            # before\n            before_text = await counter.text\n            assert before_text.strip() == '0'\n\n            await button.click()\n            await asyncio.sleep(0.2)\n            after_text = await counter.text\n            assert after_text.strip() == '1'\n\n            await button.click()\n            await asyncio.sleep(0.2)\n            after_text2 = await counter.text\n            assert after_text2.strip() == '2'\n\n    @pytest.mark.asyncio\n    async def test_insert_text_input_and_textarea(self, ci_chrome_options):\n        test_file = Path(__file__).parent / 'pages' / 'test_core_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(0.5)\n\n            # input\n            input_el = await tab.find(id='text-input')\n            await input_el.insert_text('Hello')\n            await asyncio.sleep(0.1)\n            assert 'Hello' in (input_el.get_attribute('value') or '')\n\n            # textarea\n            textarea = await tab.find(id='text-area')\n            await textarea.insert_text('World')\n            await asyncio.sleep(0.1)\n            assert 'World' in (textarea.get_attribute('value') or '')\n\n    @pytest.mark.asyncio\n    async def test_clear_input_and_textarea(self, ci_chrome_options):\n        \"\"\"Test clear() removes existing value from input and textarea.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_core_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(0.5)\n\n            # -- input: insert text, clear, verify empty, insert again --\n            input_el = await tab.find(id='text-input')\n            await input_el.insert_text('old value')\n            await asyncio.sleep(0.1)\n\n            await input_el.clear()\n            await asyncio.sleep(0.1)\n            prop = await input_el.execute_script('return this.value', return_by_value=True)\n            assert prop['result']['result']['value'] == ''\n\n            await input_el.insert_text('new value')\n            await asyncio.sleep(0.1)\n            prop = await input_el.execute_script('return this.value', return_by_value=True)\n            assert prop['result']['result']['value'] == 'new value'\n\n            # -- textarea: insert text, clear, verify empty --\n            textarea = await tab.find(id='text-area')\n            await textarea.insert_text('old message')\n            await asyncio.sleep(0.1)\n\n            await textarea.clear()\n            await asyncio.sleep(0.1)\n            prop = await textarea.execute_script('return this.value', return_by_value=True)\n            assert prop['result']['result']['value'] == ''\n\n    @pytest.mark.asyncio\n    async def test_select_option_click(self, ci_chrome_options):\n        test_file = Path(__file__).parent / 'pages' / 'test_core_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(0.5)\n\n            select_el = await tab.find(id='simple-select')\n            assert select_el is not None\n\n            # click on option 'beta'\n            opt_beta = await select_el.find(xpath='.//option[@value=\"beta\"]')\n            await opt_beta.click()\n            await asyncio.sleep(0.2)\n\n            # verify using JS value read\n            prop = await select_el.execute_script('return this.value', return_by_value=True)\n            current_value = prop['result']['result']['value']\n            assert current_value == 'beta'\n\n\nclass TestCoreTypeText:\n    \"\"\"Integration tests for type_text (keyboard-based input).\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_type_text_into_input(self, ci_chrome_options):\n        \"\"\"type_text should insert characters via keyboard events.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_core_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(0.5)\n\n            input_el = await tab.find(id='text-input')\n            await input_el.type_text('hello123')\n            await asyncio.sleep(0.3)\n\n            prop = await input_el.execute_script('return this.value', return_by_value=True)\n            assert prop['result']['result']['value'] == 'hello123'\n\n    @pytest.mark.asyncio\n    async def test_type_text_humanized_into_input(self, ci_chrome_options):\n        \"\"\"type_text with humanize=True should produce the same result.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_core_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(0.5)\n\n            input_el = await tab.find(id='text-input')\n            await input_el.type_text('Test!', humanize=True)\n            await asyncio.sleep(0.3)\n\n            prop = await input_el.execute_script('return this.value', return_by_value=True)\n            value = prop['result']['result']['value']\n            # Humanized typing may introduce and correct typos,\n            # but the final value should be very close to the input.\n            # At minimum, length should be reasonable.\n            assert len(value) >= 3\n\n    @pytest.mark.asyncio\n    async def test_type_text_symbols_and_punctuation(self, ci_chrome_options):\n        \"\"\"type_text should handle symbols, digits, and punctuation.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_core_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(0.5)\n\n            input_el = await tab.find(id='text-input')\n            test_text = 'user@example.com'\n            await input_el.type_text(test_text)\n            await asyncio.sleep(0.3)\n\n            prop = await input_el.execute_script('return this.value', return_by_value=True)\n            assert prop['result']['result']['value'] == test_text\n\n    @pytest.mark.asyncio\n    @pytest.mark.parametrize(\n        'text,label',\n        [\n            ('abcdefghijklmnopqrstuvwxyz', 'lowercase'),\n            ('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'uppercase'),\n            ('0123456789', 'digits'),\n            ('-=[];\\',./', 'punctuation_unshifted'),\n            ('!@#$%^&*()_+{}|:\"<>?~', 'punctuation_shifted'),\n        ],\n    )\n    async def test_type_text_all_character_groups(self, ci_chrome_options, text, label):\n        \"\"\"type_text should correctly type every mapped character group.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_core_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(0.5)\n\n            input_el = await tab.find(id='text-input')\n            await input_el.type_text(text)\n            await asyncio.sleep(0.3)\n\n            prop = await input_el.execute_script('return this.value', return_by_value=True)\n            assert prop['result']['result']['value'] == text, f'Failed for {label}: {text!r}'\n\n\n"
  },
  {
    "path": "tests/test_decorators.py",
    "content": "\"\"\"Tests for the retry decorator.\"\"\"\n\nimport asyncio\nimport pytest\nimport pytest_asyncio\nfrom unittest.mock import AsyncMock, MagicMock, call\n\nfrom pydoll.decorators import retry, RetryConfig\nfrom pydoll.exceptions import (\n    ElementNotFound,\n    WaitElementTimeout,\n    NetworkError,\n    PydollException,\n)\n\n\nclass TestRetryConfigInitialization:\n    \"\"\"Test RetryConfig initialization.\"\"\"\n\n    def test_default_initialization(self):\n        \"\"\"Test RetryConfig is properly initialized with defaults.\"\"\"\n        config = RetryConfig()\n        assert config.max_retries == 5\n        assert config.exceptions == Exception\n        assert config.on_retry is None\n        assert config.delay == 0\n        assert config.exponential_backoff is False\n\n    def test_custom_initialization(self):\n        \"\"\"Test RetryConfig with custom parameters.\"\"\"\n        callback = AsyncMock()\n        config = RetryConfig(\n            max_retries=3,\n            exceptions=[ElementNotFound, NetworkError],\n            on_retry=callback,\n            delay=2.0,\n            exponential_backoff=True,\n        )\n        assert config.max_retries == 3\n        assert config.exceptions == [ElementNotFound, NetworkError]\n        assert config.on_retry == callback\n        assert config.delay == 2.0\n        assert config.exponential_backoff is True\n\n\nclass TestRetryConfigCalculateDelay:\n    \"\"\"Test delay calculation methods.\"\"\"\n\n    def test_no_delay(self):\n        \"\"\"Test calculate_delay with zero delay.\"\"\"\n        config = RetryConfig(delay=0)\n        assert config.calculate_delay(1) == 0\n        assert config.calculate_delay(5) == 0\n\n    def test_constant_delay(self):\n        \"\"\"Test calculate_delay without exponential backoff.\"\"\"\n        config = RetryConfig(delay=2.0, exponential_backoff=False)\n        assert config.calculate_delay(0) == 2.0\n        assert config.calculate_delay(1) == 2.0\n        assert config.calculate_delay(2) == 2.0\n\n    def test_exponential_backoff(self):\n        \"\"\"Test calculate_delay with exponential backoff.\"\"\"\n        config = RetryConfig(delay=1.0, exponential_backoff=True)\n        assert config.calculate_delay(0) == 1.0  # 1 * 2^0 = 1\n        assert config.calculate_delay(1) == 2.0  # 1 * 2^1 = 2\n        assert config.calculate_delay(2) == 4.0  # 1 * 2^2 = 4\n        assert config.calculate_delay(3) == 8.0  # 1 * 2^3 = 8\n\n\nclass TestRetryConfigIsMatchingException:\n    \"\"\"Test exception matching logic.\"\"\"\n\n    def test_single_exception_match(self):\n        \"\"\"Test matching with single exception type.\"\"\"\n        config = RetryConfig(exceptions=ElementNotFound)\n        assert config.is_matching_exception(ElementNotFound(\"test\"))\n        assert not config.is_matching_exception(NetworkError(\"test\"))\n\n    def test_list_exception_match(self):\n        \"\"\"Test matching with list of exception types.\"\"\"\n        config = RetryConfig(exceptions=[ElementNotFound, NetworkError])\n        assert config.is_matching_exception(ElementNotFound(\"test\"))\n        assert config.is_matching_exception(NetworkError(\"test\"))\n        assert not config.is_matching_exception(WaitElementTimeout(\"test\"))\n\n    def test_parent_exception_match(self):\n        \"\"\"Test matching with parent exception class.\"\"\"\n        config = RetryConfig(exceptions=PydollException)\n        assert config.is_matching_exception(ElementNotFound(\"test\"))\n        assert config.is_matching_exception(NetworkError(\"test\"))\n        assert not config.is_matching_exception(ValueError(\"test\"))\n\n\nclass TestRetryConfigCallCallback:\n    \"\"\"Test on_retry callback execution.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_no_callback(self):\n        \"\"\"Test call_callback with no callback set.\"\"\"\n        config = RetryConfig(on_retry=None)\n        # Should not raise any error\n        await config.call_callback(None)\n\n    @pytest.mark.asyncio\n    async def test_callback_with_instance(self):\n        \"\"\"Test callback receiving instance argument.\"\"\"\n        callback = AsyncMock()\n        config = RetryConfig(on_retry=callback)\n        \n        instance = MagicMock()\n        await config.call_callback(instance)\n        \n        callback.assert_called_once_with(instance)\n\n    @pytest.mark.asyncio\n    async def test_callback_without_instance(self):\n        \"\"\"Test callback that doesn't accept instance argument.\"\"\"\n        # Callback that doesn't accept arguments\n        callback_called = False\n        \n        async def simple_callback():\n            nonlocal callback_called\n            callback_called = True\n        \n        config = RetryConfig(on_retry=simple_callback)\n        await config.call_callback(MagicMock())\n        \n        assert callback_called\n\n\nclass TestRetryDecoratorBasic:\n    \"\"\"Test basic retry decorator functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_successful_execution_no_retry(self):\n        \"\"\"Test function succeeds on first try.\"\"\"\n        call_count = 0\n        \n        @retry(max_retries=3, exceptions=[ElementNotFound])\n        async def successful_function():\n            nonlocal call_count\n            call_count += 1\n            return \"success\"\n        \n        result = await successful_function()\n        assert result == \"success\"\n        assert call_count == 1\n\n    @pytest.mark.asyncio\n    async def test_retry_on_matching_exception(self):\n        \"\"\"Test retry occurs when matching exception is raised.\"\"\"\n        call_count = 0\n        \n        @retry(max_retries=2, exceptions=[ElementNotFound], delay=0)\n        async def failing_function():\n            nonlocal call_count\n            call_count += 1\n            if call_count < 3:\n                raise ElementNotFound(\"Element not found\")\n            return \"success\"\n        \n        result = await failing_function()\n        assert result == \"success\"\n        # max_retries=2 means 3 attempts (1 original + 2 retries)\n        assert call_count == 3\n\n    @pytest.mark.asyncio\n    async def test_no_retry_on_non_matching_exception(self):\n        \"\"\"Test no retry when non-matching exception is raised.\"\"\"\n        call_count = 0\n        \n        @retry(max_retries=3, exceptions=[ElementNotFound], delay=0)\n        async def failing_function():\n            nonlocal call_count\n            call_count += 1\n            raise NetworkError(\"Network error\")\n        \n        with pytest.raises(NetworkError):\n            await failing_function()\n        \n        assert call_count == 1\n\n    @pytest.mark.asyncio\n    async def test_exhaust_all_retries(self):\n        \"\"\"Test all retries are exhausted before raising.\"\"\"\n        call_count = 0\n        \n        @retry(max_retries=2, exceptions=[ElementNotFound], delay=0)\n        async def always_failing():\n            nonlocal call_count\n            call_count += 1\n            raise ElementNotFound(\"Always fails\")\n        \n        with pytest.raises(ElementNotFound):\n            await always_failing()\n        \n        # max_retries=2 means 3 attempts (1 original + 2 retries)\n        assert call_count == 3\n\n\nclass TestRetryDecoratorWithMultipleExceptions:\n    \"\"\"Test retry with multiple exception types.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_retry_on_any_listed_exception(self):\n        \"\"\"Test retry occurs for any exception in the list.\"\"\"\n        exceptions_raised = []\n        \n        @retry(\n            max_retries=3,\n            exceptions=[ElementNotFound, NetworkError, WaitElementTimeout],\n            delay=0\n        )\n        async def multi_exception_function():\n            if len(exceptions_raised) == 0:\n                exceptions_raised.append(\"ElementNotFound\")\n                raise ElementNotFound(\"First error\")\n            elif len(exceptions_raised) == 1:\n                exceptions_raised.append(\"NetworkError\")\n                raise NetworkError(\"Second error\")\n            elif len(exceptions_raised) == 2:\n                exceptions_raised.append(\"WaitElementTimeout\")\n                raise WaitElementTimeout(\"Third error\")\n            return \"success\"\n        \n        result = await multi_exception_function()\n        assert result == \"success\"\n        # max_retries=3 means 4 attempts total, success on 4th\n        assert len(exceptions_raised) == 3\n\n\nclass TestRetryDecoratorWithDelay:\n    \"\"\"Test retry with delay between attempts.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_constant_delay(self):\n        \"\"\"Test constant delay between retries.\"\"\"\n        call_times = []\n        \n        @retry(max_retries=2, exceptions=[ElementNotFound], delay=0.1)\n        async def delayed_function():\n            call_times.append(asyncio.get_event_loop().time())\n            if len(call_times) < 3:\n                raise ElementNotFound(\"Retry\")\n            return \"success\"\n        \n        await delayed_function()\n        \n        # max_retries=2 means 3 attempts (1 original + 2 retries)\n        assert len(call_times) == 3\n        # Check delays between calls (should be ~0.1s)\n        # Allow 50ms tolerance for timing\n        assert call_times[1] - call_times[0] >= 0.05\n        assert call_times[2] - call_times[1] >= 0.05\n\n    @pytest.mark.asyncio\n    async def test_exponential_backoff(self):\n        \"\"\"Test exponential backoff increases delay.\"\"\"\n        call_times = []\n        \n        @retry(\n            max_retries=3,\n            exceptions=[ElementNotFound],\n            delay=0.1,\n            exponential_backoff=True\n        )\n        async def exponential_function():\n            call_times.append(asyncio.get_event_loop().time())\n            if len(call_times) < 4:\n                raise ElementNotFound(\"Retry\")\n            return \"success\"\n        \n        await exponential_function()\n        \n        # max_retries=3 means 4 attempts (1 original + 3 retries)\n        assert len(call_times) == 4\n        # First delay: ~0.1s (2^0 * 0.1)\n        # Second delay: ~0.2s (2^1 * 0.1)\n        # Third delay: ~0.4s (2^2 * 0.1)\n        delay1 = call_times[1] - call_times[0]\n        delay2 = call_times[2] - call_times[1]\n        delay3 = call_times[3] - call_times[2]\n        \n        # Each delay should roughly double (with tolerance)\n        assert delay2 > delay1 * 1.5\n        assert delay3 > delay2 * 1.5\n\n\nclass TestRetryDecoratorWithCallback:\n    \"\"\"Test retry with on_retry callback.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_callback_called_on_retry(self):\n        \"\"\"Test callback is called before each retry.\"\"\"\n        callback_count = 0\n        \n        async def retry_callback():\n            nonlocal callback_count\n            callback_count += 1\n        \n        call_count = 0\n        \n        @retry(\n            max_retries=2,\n            exceptions=[ElementNotFound],\n            on_retry=retry_callback,\n            delay=0\n        )\n        async def function_with_callback():\n            nonlocal call_count\n            call_count += 1\n            if call_count < 3:\n                raise ElementNotFound(\"Retry\")\n            return \"success\"\n        \n        await function_with_callback()\n        \n        # max_retries=2 means 3 attempts (1 original + 2 retries)\n        # Function called 3 times, callback called 2 times (before retry 1 and 2)\n        assert call_count == 3\n        assert callback_count == 2\n\n    @pytest.mark.asyncio\n    async def test_callback_receives_instance(self):\n        \"\"\"Test callback receives instance when used with class method.\"\"\"\n        class TestClass:\n            def __init__(self):\n                self.callback_count = 0\n                self.instance_received = None\n                self.call_count = 0\n            \n            async def recovery_callback(self):\n                self.callback_count += 1\n                self.instance_received = self\n            \n            @retry(\n                max_retries=2,\n                exceptions=[ElementNotFound],\n                on_retry=recovery_callback,\n                delay=0\n            )\n            async def method_with_callback(self):\n                self.call_count += 1\n                if self.call_count < 3:\n                    raise ElementNotFound(\"Retry\")\n                return \"success\"\n        \n        instance = TestClass()\n        result = await instance.method_with_callback()\n        \n        assert result == \"success\"\n        # max_retries=2 means 3 attempts, callback called 2 times\n        assert instance.callback_count == 2\n        assert instance.instance_received is instance\n\n\nclass TestRetryDecoratorEdgeCases:\n    \"\"\"Test edge cases and error conditions.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_zero_retries_succeeds(self):\n        \"\"\"Test with max_retries=0 succeeds on first attempt.\"\"\"\n        call_count = 0\n        \n        @retry(max_retries=0, exceptions=[ElementNotFound], delay=0)\n        async def zero_retry_function():\n            nonlocal call_count\n            call_count += 1\n            return \"success\"\n        \n        result = await zero_retry_function()\n        assert result == \"success\"\n        # max_retries=0 means 1 attempt (no retries)\n        assert call_count == 1\n\n    @pytest.mark.asyncio\n    async def test_zero_retries_fails_immediately(self):\n        \"\"\"Test with max_retries=0 fails without retry.\"\"\"\n        call_count = 0\n        \n        @retry(max_retries=0, exceptions=[ElementNotFound], delay=0)\n        async def zero_retry_function():\n            nonlocal call_count\n            call_count += 1\n            raise ElementNotFound(\"Fail\")\n        \n        with pytest.raises(ElementNotFound):\n            await zero_retry_function()\n        \n        # max_retries=0 means 1 attempt, no retries\n        assert call_count == 1\n\n    @pytest.mark.asyncio\n    async def test_one_retry_succeeds_on_second_attempt(self):\n        \"\"\"Test with max_retries=1 succeeds on second attempt.\"\"\"\n        call_count = 0\n        \n        @retry(max_retries=1, exceptions=[ElementNotFound], delay=0)\n        async def one_retry_function():\n            nonlocal call_count\n            call_count += 1\n            if call_count == 1:\n                raise ElementNotFound(\"Fail\")\n            return \"success\"\n        \n        result = await one_retry_function()\n        assert result == \"success\"\n        # max_retries=1 means 2 attempts (1 original + 1 retry)\n        assert call_count == 2\n\n    @pytest.mark.asyncio\n    async def test_exception_to_raise_parameter(self):\n        \"\"\"Test custom exception can be raised instead of original.\"\"\"\n        call_count = 0\n        \n        custom_exception = NetworkError(\"Custom error message\")\n        \n        @retry(\n            max_retries=1,\n            exceptions=[ElementNotFound],\n            delay=0,\n            exception_to_raise=custom_exception\n        )\n        async def function_with_custom_exception():\n            nonlocal call_count\n            call_count += 1\n            raise ElementNotFound(\"Original error\")\n        \n        with pytest.raises(NetworkError) as exc_info:\n            await function_with_custom_exception()\n        \n        assert str(exc_info.value) == \"Custom error message\"\n        # max_retries=1 means 2 attempts (1 original + 1 retry)\n        assert call_count == 2\n\n\nclass TestRetryDecoratorWithClassMethods:\n    \"\"\"Test retry decorator with class methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_instance_method(self):\n        \"\"\"Test decorator on instance method.\"\"\"\n        class Counter:\n            def __init__(self):\n                self.count = 0\n            \n            @retry(max_retries=2, exceptions=[ElementNotFound], delay=0)\n            async def increment(self):\n                self.count += 1\n                if self.count < 3:\n                    raise ElementNotFound(\"Retry\")\n                return self.count\n        \n        counter = Counter()\n        result = await counter.increment()\n        \n        # max_retries=2 means 3 attempts (1 original + 2 retries)\n        assert result == 3\n        assert counter.count == 3\n\n    @pytest.mark.asyncio\n    async def test_method_with_arguments(self):\n        \"\"\"Test decorated method with arguments.\"\"\"\n        class Calculator:\n            @retry(max_retries=3, exceptions=[ValueError], delay=0)\n            async def divide(self, a: int, b: int):\n                if b == 0:\n                    raise ValueError(\"Division by zero\")\n                return a / b\n        \n        calc = Calculator()\n        result = await calc.divide(10, 2)\n        assert result == 5.0\n\n    @pytest.mark.asyncio\n    async def test_method_with_state_restoration(self):\n        \"\"\"Test method that restores state in callback.\"\"\"\n        class StatefulClass:\n            def __init__(self):\n                self.attempts = 0\n                self.state = \"initial\"\n            \n            async def restore_state(self):\n                self.state = \"restored\"\n            \n            @retry(\n                max_retries=2,\n                exceptions=[ElementNotFound],\n                on_retry=restore_state,\n                delay=0\n            )\n            async def process(self):\n                self.attempts += 1\n                if self.attempts < 3:\n                    self.state = \"broken\"\n                    raise ElementNotFound(\"Retry\")\n                return \"success\"\n        \n        obj = StatefulClass()\n        result = await obj.process()\n        \n        assert result == \"success\"\n        # max_retries=2 means 3 attempts (1 original + 2 retries)\n        assert obj.attempts == 3\n        assert obj.state == \"restored\"\n\n\nclass TestRetryConfigHandleDelay:\n    \"\"\"Test handle_delay method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_handle_delay_no_delay(self):\n        \"\"\"Test handle_delay with zero delay.\"\"\"\n        config = RetryConfig(delay=0)\n        \n        start_time = asyncio.get_event_loop().time()\n        await config.handle_delay(1)\n        end_time = asyncio.get_event_loop().time()\n        \n        # Should be nearly instant\n        assert end_time - start_time < 0.01\n\n    @pytest.mark.asyncio\n    async def test_handle_delay_with_delay(self):\n        \"\"\"Test handle_delay waits for specified time.\"\"\"\n        config = RetryConfig(delay=0.1, exponential_backoff=False)\n        \n        start_time = asyncio.get_event_loop().time()\n        await config.handle_delay(1)\n        end_time = asyncio.get_event_loop().time()\n        \n        # Should wait approximately 0.1 seconds\n        assert end_time - start_time >= 0.05\n\n\nclass TestRetryDecoratorRealWorldScenarios:\n    \"\"\"Test real-world usage scenarios.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_network_retry_scenario(self):\n        \"\"\"Simulate network retry scenario.\"\"\"\n        class NetworkClient:\n            def __init__(self):\n                self.attempt_count = 0\n                self.reconnect_count = 0\n            \n            async def reconnect(self):\n                \"\"\"Simulate reconnection logic.\"\"\"\n                await asyncio.sleep(0.01)\n                self.reconnect_count += 1\n            \n            @retry(\n                max_retries=2,\n                exceptions=[NetworkError],\n                on_retry=reconnect,\n                delay=0.05,\n                exponential_backoff=True\n            )\n            async def fetch_data(self, url: str):\n                self.attempt_count += 1\n                # Fail on first 2 attempts, succeed on 3rd\n                if self.attempt_count < 3:\n                    raise NetworkError(f\"Connection failed (attempt {self.attempt_count})\")\n                return f\"Data from {url}\"\n        \n        client = NetworkClient()\n        result = await client.fetch_data(\"https://example.com\")\n        \n        assert result == \"Data from https://example.com\"\n        # max_retries=2 means 3 attempts (1 original + 2 retries)\n        assert client.attempt_count == 3\n        # Callback called 2 times (before retry 1 and retry 2)\n        assert client.reconnect_count == 2\n\n    @pytest.mark.asyncio\n    async def test_element_search_retry_scenario(self):\n        \"\"\"Simulate element search with page refresh.\"\"\"\n        class PageScraper:\n            def __init__(self):\n                self.page_refreshed = False\n                self.search_count = 0\n            \n            async def refresh_page(self):\n                \"\"\"Simulate page refresh.\"\"\"\n                await asyncio.sleep(0.01)\n                self.page_refreshed = True\n            \n            @retry(\n                max_retries=2,\n                exceptions=[ElementNotFound, WaitElementTimeout],\n                on_retry=refresh_page,\n                delay=0.05\n            )\n            async def find_element(self, selector: str):\n                self.search_count += 1\n                if not self.page_refreshed:\n                    raise ElementNotFound(f\"Element '{selector}' not found\")\n                return f\"Element: {selector}\"\n        \n        scraper = PageScraper()\n        result = await scraper.find_element(\"#content\")\n        \n        assert result == \"Element: #content\"\n        # max_retries=2 means up to 3 attempts, succeeds on 2nd\n        assert scraper.search_count == 2\n        assert scraper.page_refreshed is True\n\n"
  },
  {
    "path": "tests/test_events.py",
    "content": "from pydoll.protocol.browser.events import BrowserEvent\nfrom pydoll.protocol.dom.events import DomEvent\nfrom pydoll.protocol.fetch.events import FetchEvent\nfrom pydoll.protocol.input.events import InputEvent\nfrom pydoll.protocol.network.events import NetworkEvent\nfrom pydoll.protocol.page.events import PageEvent\nfrom pydoll.protocol.runtime.events import RuntimeEvent\nfrom pydoll.protocol.storage.events import StorageEvent\nfrom pydoll.protocol.target.events import TargetEvent\n\n\ndef test_browser_events():\n    \"\"\"Test all BrowserEvent enum values.\"\"\"\n    assert BrowserEvent.DOWNLOAD_PROGRESS == 'Browser.downloadProgress'\n    assert BrowserEvent.DOWNLOAD_WILL_BEGIN == 'Browser.downloadWillBegin'\n\n\ndef test_dom_events():\n    \"\"\"Test all DomEvent enum values.\"\"\"\n    assert DomEvent.ATTRIBUTE_MODIFIED == 'DOM.attributeModified'\n    assert DomEvent.ATTRIBUTE_REMOVED == 'DOM.attributeRemoved'\n    assert DomEvent.CHARACTER_DATA_MODIFIED == 'DOM.characterDataModified'\n    assert DomEvent.CHILD_NODE_COUNT_UPDATED == 'DOM.childNodeCountUpdated'\n    assert DomEvent.CHILD_NODE_INSERTED == 'DOM.childNodeInserted'\n    assert DomEvent.CHILD_NODE_REMOVED == 'DOM.childNodeRemoved'\n    assert DomEvent.DOCUMENT_UPDATED == 'DOM.documentUpdated'\n    assert DomEvent.SET_CHILD_NODES == 'DOM.setChildNodes'\n    assert DomEvent.DISTRIBUTED_NODES_UPDATED == 'DOM.distributedNodesUpdated'\n    assert DomEvent.INLINE_STYLE_INVALIDATED == 'DOM.inlineStyleInvalidated'\n    assert DomEvent.PSEUDO_ELEMENT_ADDED == 'DOM.pseudoElementAdded'\n    assert DomEvent.PSEUDO_ELEMENT_REMOVED == 'DOM.pseudoElementRemoved'\n    assert DomEvent.SCROLLABLE_FLAG_UPDATED == 'DOM.scrollableFlagUpdated'\n    assert DomEvent.SHADOW_ROOT_POPPED == 'DOM.shadowRootPopped'\n    assert DomEvent.SHADOW_ROOT_PUSHED == 'DOM.shadowRootPushed'\n    assert DomEvent.TOP_LAYER_ELEMENTS_UPDATED == 'DOM.topLayerElementsUpdated'\n\n\ndef test_fetch_events():\n    \"\"\"Test all FetchEvent enum values.\"\"\"\n    assert FetchEvent.AUTH_REQUIRED == 'Fetch.authRequired'\n    assert FetchEvent.REQUEST_PAUSED == 'Fetch.requestPaused'\n\n\ndef test_input_events():\n    \"\"\"Test all InputEvent enum values.\"\"\"\n    assert InputEvent.DRAG_INTERCEPTED == 'Input.dragIntercepted'\n\n\ndef test_network_events():\n    \"\"\"Test all NetworkEvent enum values.\"\"\"\n    assert NetworkEvent.DATA_RECEIVED == 'Network.dataReceived'\n    assert NetworkEvent.EVENT_SOURCE_MESSAGE_RECEIVED == 'Network.eventSourceMessageReceived'\n    assert NetworkEvent.LOADING_FAILED == 'Network.loadingFailed'\n    assert NetworkEvent.LOADING_FINISHED == 'Network.loadingFinished'\n    assert NetworkEvent.REQUEST_SERVED_FROM_CACHE == 'Network.requestServedFromCache'\n    assert NetworkEvent.REQUEST_WILL_BE_SENT == 'Network.requestWillBeSent'\n    assert NetworkEvent.RESPONSE_RECEIVED == 'Network.responseReceived'\n    assert NetworkEvent.WEBSOCKET_CLOSED == 'Network.webSocketClosed'\n    assert NetworkEvent.WEBSOCKET_CREATED == 'Network.webSocketCreated'\n    assert NetworkEvent.WEBSOCKET_FRAME_ERROR == 'Network.webSocketFrameError'\n    assert NetworkEvent.WEBSOCKET_FRAME_RECEIVED == 'Network.webSocketFrameReceived'\n    assert NetworkEvent.WEBSOCKET_FRAME_SENT == 'Network.webSocketFrameSent'\n    assert NetworkEvent.WEBSOCKET_HANDSHAKE_RESPONSE_RECEIVED == 'Network.webSocketHandshakeResponseReceived'\n    assert NetworkEvent.WEBSOCKET_WILL_SEND_HANDSHAKE_REQUEST == 'Network.webSocketWillSendHandshakeRequest'\n    assert NetworkEvent.WEBTRANSPORT_CLOSED == 'Network.webTransportClosed'\n    assert NetworkEvent.WEBTRANSPORT_CONNECTION_ESTABLISHED == 'Network.webTransportConnectionEstablished'\n    assert NetworkEvent.WEBTRANSPORT_CREATED == 'Network.webTransportCreated'\n    assert NetworkEvent.DIRECT_TCP_SOCKET_ABORTED == 'Network.directTCPSocketAborted'\n    assert NetworkEvent.DIRECT_TCP_SOCKET_CHUNK_RECEIVED == 'Network.directTCPSocketChunkReceived'\n    assert NetworkEvent.DIRECT_TCP_SOCKET_CHUNK_SENT == 'Network.directTCPSocketChunkSent'\n    assert NetworkEvent.DIRECT_TCP_SOCKET_CLOSED == 'Network.directTCPSocketClosed'\n    assert NetworkEvent.DIRECT_TCP_SOCKET_CREATED == 'Network.directTCPSocketCreated'\n    assert NetworkEvent.DIRECT_TCP_SOCKET_OPENED == 'Network.directTCPSocketOpened'\n    assert NetworkEvent.DIRECT_UDP_SOCKET_ABORTED == 'Network.directUDPSocketAborted'\n    assert NetworkEvent.DIRECT_UDP_SOCKET_CHUNK_RECEIVED == 'Network.directUDPSocketChunkReceived'\n    assert NetworkEvent.DIRECT_UDP_SOCKET_CHUNK_SENT == 'Network.directUDPSocketChunkSent'\n    assert NetworkEvent.DIRECT_UDP_SOCKET_CLOSED == 'Network.directUDPSocketClosed'\n    assert NetworkEvent.DIRECT_UDP_SOCKET_CREATED == 'Network.directUDPSocketCreated'\n    assert NetworkEvent.DIRECT_UDP_SOCKET_OPENED == 'Network.directUDPSocketOpened'\n    assert NetworkEvent.POLICY_UPDATED == 'Network.policyUpdated'\n    assert NetworkEvent.REPORTING_API_ENDPOINTS_CHANGED_FOR_ORIGIN == 'Network.reportingApiEndpointsChangedForOrigin'\n    assert NetworkEvent.REPORTING_API_REPORT_ADDED == 'Network.reportingApiReportAdded'\n    assert NetworkEvent.REPORTING_API_REPORT_UPDATED == 'Network.reportingApiReportUpdated'\n    assert NetworkEvent.REQUEST_WILL_BE_SENT_EXTRA_INFO == 'Network.requestWillBeSentExtraInfo'\n    assert NetworkEvent.RESOURCE_CHANGED_PRIORITY == 'Network.resourceChangedPriority'\n    assert NetworkEvent.RESPONSE_RECEIVED_EARLY_HINTS == 'Network.responseReceivedEarlyHints'\n    assert NetworkEvent.RESPONSE_RECEIVED_EXTRA_INFO == 'Network.responseReceivedExtraInfo'\n    assert NetworkEvent.SIGNED_EXCHANGE_RECEIVED == 'Network.signedExchangeReceived'\n    assert NetworkEvent.SUBRESOURCE_WEB_BUNDLE_INNER_RESPONSE_ERROR == 'Network.subresourceWebBundleInnerResponseError'\n    assert NetworkEvent.SUBRESOURCE_WEB_BUNDLE_INNER_RESPONSE_PARSED == 'Network.subresourceWebBundleInnerResponseParsed'\n    assert NetworkEvent.SUBRESOURCE_WEB_BUNDLE_METADATA_ERROR == 'Network.subresourceWebBundleMetadataError'\n    assert NetworkEvent.SUBRESOURCE_WEB_BUNDLE_METADATA_RECEIVED == 'Network.subresourceWebBundleMetadataReceived'\n    assert NetworkEvent.TRUST_TOKEN_OPERATION_DONE == 'Network.trustTokenOperationDone'\n\n\ndef test_page_events():\n    \"\"\"Test all PageEvent enum values.\"\"\"\n    assert PageEvent.DOM_CONTENT_EVENT_FIRED == 'Page.domContentEventFired'\n    assert PageEvent.FILE_CHOOSER_OPENED == 'Page.fileChooserOpened'\n    assert PageEvent.FRAME_ATTACHED == 'Page.frameAttached'\n    assert PageEvent.FRAME_DETACHED == 'Page.frameDetached'\n    assert PageEvent.FRAME_NAVIGATED == 'Page.frameNavigated'\n    assert PageEvent.INTERSTITIAL_HIDDEN == 'Page.interstitialHidden'\n    assert PageEvent.INTERSTITIAL_SHOWN == 'Page.interstitialShown'\n    assert PageEvent.JAVASCRIPT_DIALOG_CLOSED == 'Page.javascriptDialogClosed'\n    assert PageEvent.JAVASCRIPT_DIALOG_OPENING == 'Page.javascriptDialogOpening'\n    assert PageEvent.LIFECYCLE_EVENT == 'Page.lifecycleEvent'\n    assert PageEvent.LOAD_EVENT_FIRED == 'Page.loadEventFired'\n    assert PageEvent.WINDOW_OPEN == 'Page.windowOpen'\n    assert PageEvent.BACK_FORWARD_CACHE_NOT_USED == 'Page.backForwardCacheNotUsed'\n    assert PageEvent.COMPILATION_CACHE_PRODUCED == 'Page.compilationCacheProduced'\n    assert PageEvent.DOCUMENT_OPENED == 'Page.documentOpened'\n    assert PageEvent.FRAME_REQUESTED_NAVIGATION == 'Page.frameRequestedNavigation'\n    assert PageEvent.FRAME_RESIZED == 'Page.frameResized'\n    assert PageEvent.FRAME_STARTED_LOADING == 'Page.frameStartedLoading'\n    assert PageEvent.FRAME_STARTED_NAVIGATING == 'Page.frameStartedNavigating'\n    assert PageEvent.FRAME_STOPPED_LOADING == 'Page.frameStoppedLoading'\n    assert PageEvent.FRAME_SUBTREE_WILL_BE_DETACHED == 'Page.frameSubtreeWillBeDetached'\n    assert PageEvent.NAVIGATED_WITHIN_DOCUMENT == 'Page.navigatedWithinDocument'\n    assert PageEvent.SCREENCAST_FRAME == 'Page.screencastFrame'\n    assert PageEvent.SCREENCAST_VISIBILITY_CHANGED == 'Page.screencastVisibilityChanged'\n\n\ndef test_runtime_events():\n    \"\"\"Test all RuntimeEvent enum values.\"\"\"\n    assert RuntimeEvent.CONSOLE_API_CALLED == 'Runtime.consoleAPICalled'\n    assert RuntimeEvent.EXCEPTION_REVOKED == 'Runtime.exceptionRevoked'\n    assert RuntimeEvent.EXCEPTION_THROWN == 'Runtime.exceptionThrown'\n    assert RuntimeEvent.EXECUTION_CONTEXT_CREATED == 'Runtime.executionContextCreated'\n    assert RuntimeEvent.EXECUTION_CONTEXT_DESTROYED == 'Runtime.executionContextDestroyed'\n    assert RuntimeEvent.EXECUTION_CONTEXTS_CLEARED == 'Runtime.executionContextsCleared'\n    assert RuntimeEvent.INSPECT_REQUESTED == 'Runtime.inspectRequested'\n    assert RuntimeEvent.BINDING_CALLED == 'Runtime.bindingCalled'\n\n\ndef test_storage_events():\n    \"\"\"Test all StorageEvent enum values.\"\"\"\n    assert StorageEvent.CACHE_STORAGE_CONTENT_UPDATED == 'Storage.cacheStorageContentUpdated'\n    assert StorageEvent.CACHE_STORAGE_LIST_UPDATED == 'Storage.cacheStorageListUpdated'\n    assert StorageEvent.INDEXED_DB_CONTENT_UPDATED == 'Storage.indexedDBContentUpdated'\n    assert StorageEvent.INDEXED_DB_LIST_UPDATED == 'Storage.indexedDBListUpdated'\n    assert StorageEvent.INTEREST_GROUP_ACCESSED == 'Storage.interestGroupAccessed'\n    assert StorageEvent.INTEREST_GROUP_AUCTION_EVENT_OCCURRED == 'Storage.interestGroupAuctionEventOccurred'\n    assert StorageEvent.INTEREST_GROUP_AUCTION_NETWORK_REQUEST_CREATED == 'Storage.interestGroupAuctionNetworkRequestCreated'\n    assert StorageEvent.SHARED_STORAGE_ACCESSED == 'Storage.sharedStorageAccessed'\n    assert StorageEvent.SHARED_STORAGE_WORKLET_OPERATION_EXECUTION_FINISHED == 'Storage.sharedStorageWorkletOperationExecutionFinished'\n    assert StorageEvent.STORAGE_BUCKET_CREATED_OR_UPDATED == 'Storage.storageBucketCreatedOrUpdated'\n    assert StorageEvent.STORAGE_BUCKET_DELETED == 'Storage.storageBucketDeleted'\n    assert StorageEvent.ATTRIBUTION_REPORTING_REPORT_SENT == 'Storage.attributionReportingReportSent'\n    assert StorageEvent.ATTRIBUTION_REPORTING_SOURCE_REGISTERED == 'Storage.attributionReportingSourceRegistered'\n    assert StorageEvent.ATTRIBUTION_REPORTING_TRIGGER_REGISTERED == 'Storage.attributionReportingTriggerRegistered'\n\n\ndef test_target_events():\n    \"\"\"Test all TargetEvent enum values.\"\"\"\n    assert TargetEvent.RECEIVED_MESSAGE_FROM_TARGET == 'Target.receivedMessageFromTarget'\n    assert TargetEvent.TARGET_CRASHED == 'Target.targetCrashed'\n    assert TargetEvent.TARGET_CREATED == 'Target.targetCreated'\n    assert TargetEvent.TARGET_DESTROYED == 'Target.targetDestroyed'\n    assert TargetEvent.TARGET_INFO_CHANGED == 'Target.targetInfoChanged'\n    assert TargetEvent.ATTACHED_TO_TARGET == 'Target.attachedToTarget'\n    assert TargetEvent.DETACHED_FROM_TARGET == 'Target.detachedFromTarget'\n\n\ndef test_event_enums_integrity():\n    \"\"\"Test that all event enums are properly structured and have no duplicates.\"\"\"\n    # Test that all enums inherit from str and Enum\n    event_classes = [\n        BrowserEvent, DomEvent, FetchEvent, InputEvent, NetworkEvent,\n        PageEvent, RuntimeEvent, StorageEvent, TargetEvent\n    ]\n    \n    # Map class names to their correct domain prefixes\n    domain_mapping = {\n        'BrowserEvent': 'Browser',\n        'DomEvent': 'DOM',\n        'FetchEvent': 'Fetch',\n        'InputEvent': 'Input',\n        'NetworkEvent': 'Network',\n        'PageEvent': 'Page',\n        'RuntimeEvent': 'Runtime',\n        'StorageEvent': 'Storage',\n        'TargetEvent': 'Target'\n    }\n    \n    for event_class in event_classes:\n        # Check that all values are strings\n        for event in event_class:\n            assert isinstance(event.value, str), f\"{event_class.__name__}.{event.name} should be a string\"\n            \n        # Check that all values start with the correct domain prefix\n        domain_name = domain_mapping[event_class.__name__]\n        for event in event_class:\n            assert event.value.startswith(f'{domain_name}.'), \\\n                f\"{event_class.__name__}.{event.name} should start with '{domain_name}.'\"\n\n\ndef test_no_duplicate_events():\n    \"\"\"Test that there are no duplicate event values across all enums.\"\"\"\n    all_events = []\n\n    event_classes = [\n        BrowserEvent, DomEvent, FetchEvent, InputEvent, NetworkEvent,\n        PageEvent, RuntimeEvent, StorageEvent, TargetEvent\n    ]\n\n    for event_class in event_classes:\n        for event in event_class:\n            all_events.append(event.value)\n\n    assert len(all_events) == len(set(all_events)), \"Found duplicate event values\"\n\n\ndef test_event_enum_completeness():\n    \"\"\"Test that each event enum has at least one event defined.\"\"\"\n    event_classes = [\n        BrowserEvent, DomEvent, FetchEvent, InputEvent, NetworkEvent,\n        PageEvent, RuntimeEvent, StorageEvent, TargetEvent\n    ]\n    \n    for event_class in event_classes:\n        assert len(list(event_class)) > 0, f\"{event_class.__name__} should have at least one event\"\n\n\ndef test_event_naming_convention():\n    \"\"\"Test that all event names follow the correct naming convention.\"\"\"\n    event_classes = [\n        BrowserEvent, DomEvent, FetchEvent, InputEvent, NetworkEvent,\n        PageEvent, RuntimeEvent, StorageEvent, TargetEvent\n    ]\n\n    for event_class in event_classes:\n        for event in event_class:\n            # Event names should be UPPER_CASE\n            assert event.name.isupper(), f\"{event_class.__name__}.{event.name} should be uppercase\"\n            # Event names should not contain lowercase letters\n            assert not any(c.islower() for c in event.name), \\\n                f\"{event_class.__name__}.{event.name} should not contain lowercase letters\"\n\n"
  },
  {
    "path": "tests/test_exceptions.py",
    "content": "import pytest\nfrom pydoll.exceptions import (\n    # Base exceptions\n    PydollException,\n    ConnectionException,\n    BrowserException,\n    ProtocolException,\n    ElementException,\n    TimeoutException,\n    ConfigurationException,\n    DialogException,\n    \n    # Connection exceptions\n    ConnectionFailed,\n    ReconnectionFailed,\n    WebSocketConnectionClosed,\n    NetworkError,\n    \n    # Browser exceptions\n    BrowserNotRunning,\n    FailedToStartBrowser,\n    UnsupportedOS,\n    NoValidTabFound,\n    \n    # Protocol exceptions\n    InvalidCommand,\n    InvalidResponse,\n    ResendCommandFailed,\n    CommandExecutionTimeout,\n    InvalidCallback,\n    EventNotSupported,\n    \n    # Element exceptions\n    ElementNotFound,\n    ElementNotVisible,\n    ElementNotInteractable,\n    ClickIntercepted,\n    ElementNotAFileInput,\n    \n    # Timeout exceptions\n    PageLoadTimeout,\n    WaitElementTimeout,\n    \n    # Configuration exceptions\n    InvalidOptionsObject,\n    InvalidBrowserPath,\n    ArgumentAlreadyExistsInOptions,\n    InvalidFileExtension,\n    \n    # Dialog exceptions\n    NoDialogPresent,\n    \n    # IFrame exceptions\n    NotAnIFrame,\n    InvalidIFrame,\n    IFrameNotFound,\n)\n\n\nclass TestBaseExceptions:\n    \"\"\"Test base exception classes.\"\"\"\n\n    def test_pydoll_exception_default_message(self):\n        \"\"\"Test PydollException with default message.\"\"\"\n        with pytest.raises(PydollException) as exc_info:\n            raise PydollException()\n        assert str(exc_info.value) == 'An error occurred in Pydoll'\n\n    def test_pydoll_exception_custom_message(self):\n        \"\"\"Test PydollException with custom message.\"\"\"\n        custom_message = 'Custom error occurred'\n        with pytest.raises(PydollException) as exc_info:\n            raise PydollException(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_connection_exception_default(self):\n        \"\"\"Test ConnectionException with default message.\"\"\"\n        with pytest.raises(ConnectionException) as exc_info:\n            raise ConnectionException()\n        assert str(exc_info.value) == 'A connection error occurred'\n\n    def test_connection_exception_custom(self):\n        \"\"\"Test ConnectionException with custom message.\"\"\"\n        custom_message = 'Custom connection error'\n        with pytest.raises(ConnectionException) as exc_info:\n            raise ConnectionException(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_browser_exception_default(self):\n        \"\"\"Test BrowserException with default message.\"\"\"\n        with pytest.raises(BrowserException) as exc_info:\n            raise BrowserException()\n        assert str(exc_info.value) == 'A browser error occurred'\n\n    def test_protocol_exception_default(self):\n        \"\"\"Test ProtocolException with default message.\"\"\"\n        with pytest.raises(ProtocolException) as exc_info:\n            raise ProtocolException()\n        assert str(exc_info.value) == 'A protocol error occurred'\n\n    def test_element_exception_default(self):\n        \"\"\"Test ElementException with default message.\"\"\"\n        with pytest.raises(ElementException) as exc_info:\n            raise ElementException()\n        assert str(exc_info.value) == 'An element interaction error occurred'\n\n    def test_timeout_exception_default(self):\n        \"\"\"Test TimeoutException with default message.\"\"\"\n        with pytest.raises(TimeoutException) as exc_info:\n            raise TimeoutException()\n        assert str(exc_info.value) == 'A timeout occurred'\n\n    def test_configuration_exception_default(self):\n        \"\"\"Test ConfigurationException with default message.\"\"\"\n        with pytest.raises(ConfigurationException) as exc_info:\n            raise ConfigurationException()\n        assert str(exc_info.value) == 'A configuration error occurred'\n\n    def test_dialog_exception_default(self):\n        \"\"\"Test DialogException with default message.\"\"\"\n        with pytest.raises(DialogException) as exc_info:\n            raise DialogException()\n        assert str(exc_info.value) == 'A dialog error occurred'\n\n\nclass TestConnectionExceptions:\n    \"\"\"Test connection-related exceptions.\"\"\"\n\n    def test_connection_failed(self):\n        \"\"\"Test ConnectionFailed exception.\"\"\"\n        with pytest.raises(ConnectionFailed) as exc_info:\n            raise ConnectionFailed()\n        assert str(exc_info.value) == 'Failed to connect to the browser'\n\n    def test_reconnection_failed(self):\n        \"\"\"Test ReconnectionFailed exception.\"\"\"\n        with pytest.raises(ReconnectionFailed) as exc_info:\n            raise ReconnectionFailed()\n        assert str(exc_info.value) == 'Failed to reconnect to the browser'\n\n    def test_websocket_connection_closed(self):\n        \"\"\"Test WebSocketConnectionClosed exception.\"\"\"\n        with pytest.raises(WebSocketConnectionClosed) as exc_info:\n            raise WebSocketConnectionClosed()\n        assert str(exc_info.value) == 'The WebSocket connection is closed'\n\n    def test_websocket_connection_closed_custom(self):\n        \"\"\"Test WebSocketConnectionClosed with custom message.\"\"\"\n        custom_message = 'Connection closed unexpectedly'\n        with pytest.raises(WebSocketConnectionClosed) as exc_info:\n            raise WebSocketConnectionClosed(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_network_error(self):\n        \"\"\"Test NetworkError exception.\"\"\"\n        with pytest.raises(NetworkError) as exc_info:\n            raise NetworkError()\n        assert str(exc_info.value) == 'A network error occurred'\n\n\nclass TestBrowserExceptions:\n    \"\"\"Test browser-related exceptions.\"\"\"\n\n    def test_browser_not_running(self):\n        \"\"\"Test BrowserNotRunning exception.\"\"\"\n        with pytest.raises(BrowserNotRunning) as exc_info:\n            raise BrowserNotRunning()\n        assert str(exc_info.value) == 'The browser is not running'\n\n    def test_failed_to_start_browser(self):\n        \"\"\"Test FailedToStartBrowser exception.\"\"\"\n        with pytest.raises(FailedToStartBrowser) as exc_info:\n            raise FailedToStartBrowser()\n        assert str(exc_info.value) == 'Failed to start the browser'\n\n    def test_failed_to_start_browser_custom(self):\n        \"\"\"Test FailedToStartBrowser with custom message.\"\"\"\n        custom_message = 'Browser executable not found'\n        with pytest.raises(FailedToStartBrowser) as exc_info:\n            raise FailedToStartBrowser(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_unsupported_os(self):\n        \"\"\"Test UnsupportedOS exception.\"\"\"\n        with pytest.raises(UnsupportedOS) as exc_info:\n            raise UnsupportedOS()\n        assert str(exc_info.value) == 'Unsupported OS'\n\n    def test_unsupported_os_custom(self):\n        \"\"\"Test UnsupportedOS with custom message.\"\"\"\n        custom_message = 'This OS is not supported: FreeBSD'\n        with pytest.raises(UnsupportedOS) as exc_info:\n            raise UnsupportedOS(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_no_valid_tab_found(self):\n        \"\"\"Test NoValidTabFound exception.\"\"\"\n        with pytest.raises(NoValidTabFound) as exc_info:\n            raise NoValidTabFound()\n        assert str(exc_info.value) == 'No valid attached tab found'\n\n\nclass TestProtocolExceptions:\n    \"\"\"Test protocol-related exceptions.\"\"\"\n\n    def test_invalid_command(self):\n        \"\"\"Test InvalidCommand exception.\"\"\"\n        with pytest.raises(InvalidCommand) as exc_info:\n            raise InvalidCommand()\n        assert str(exc_info.value) == 'The command provided is invalid'\n\n    def test_invalid_response(self):\n        \"\"\"Test InvalidResponse exception.\"\"\"\n        with pytest.raises(InvalidResponse) as exc_info:\n            raise InvalidResponse()\n        assert str(exc_info.value) == 'The response received is invalid'\n\n    def test_resend_command_failed(self):\n        \"\"\"Test ResendCommandFailed exception.\"\"\"\n        with pytest.raises(ResendCommandFailed) as exc_info:\n            raise ResendCommandFailed()\n        assert str(exc_info.value) == 'Failed to resend the command'\n\n    def test_command_execution_timeout(self):\n        \"\"\"Test CommandExecutionTimeout exception.\"\"\"\n        with pytest.raises(CommandExecutionTimeout) as exc_info:\n            raise CommandExecutionTimeout()\n        assert str(exc_info.value) == 'The command execution timed out'\n\n    def test_command_execution_timeout_custom(self):\n        \"\"\"Test CommandExecutionTimeout with custom message.\"\"\"\n        custom_message = 'Command timed out after 30 seconds'\n        with pytest.raises(CommandExecutionTimeout) as exc_info:\n            raise CommandExecutionTimeout(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_invalid_callback(self):\n        \"\"\"Test InvalidCallback exception.\"\"\"\n        with pytest.raises(InvalidCallback) as exc_info:\n            raise InvalidCallback()\n        assert str(exc_info.value) == 'The callback provided is invalid'\n\n    def test_event_not_supported(self):\n        \"\"\"Test EventNotSupported exception.\"\"\"\n        with pytest.raises(EventNotSupported) as exc_info:\n            raise EventNotSupported('Custom error message')\n        assert str(exc_info.value) == 'Custom error message'\n\n        # Testing default message\n        with pytest.raises(EventNotSupported) as exc_info:\n            raise EventNotSupported()\n        assert str(exc_info.value) == 'The event is not supported'\n\n\nclass TestElementExceptions:\n    \"\"\"Test element-related exceptions.\"\"\"\n\n    def test_element_not_found(self):\n        \"\"\"Test ElementNotFound exception.\"\"\"\n        with pytest.raises(ElementNotFound) as exc_info:\n            raise ElementNotFound()\n        assert str(exc_info.value) == 'The specified element was not found'\n\n    def test_element_not_found_custom(self):\n        \"\"\"Test ElementNotFound with custom message.\"\"\"\n        custom_message = 'Button with ID \"submit\" not found'\n        with pytest.raises(ElementNotFound) as exc_info:\n            raise ElementNotFound(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_element_not_visible(self):\n        \"\"\"Test ElementNotVisible exception.\"\"\"\n        with pytest.raises(ElementNotVisible) as exc_info:\n            raise ElementNotVisible()\n        assert str(exc_info.value) == 'The element is not visible'\n\n    def test_element_not_interactable(self):\n        \"\"\"Test ElementNotInteractable exception.\"\"\"\n        with pytest.raises(ElementNotInteractable) as exc_info:\n            raise ElementNotInteractable()\n        assert str(exc_info.value) == 'The element is not interactable'\n\n    def test_click_intercepted(self):\n        \"\"\"Test ClickIntercepted exception.\"\"\"\n        with pytest.raises(ClickIntercepted) as exc_info:\n            raise ClickIntercepted()\n        assert str(exc_info.value) == 'The click was intercepted'\n\n    def test_click_intercepted_custom(self):\n        \"\"\"Test ClickIntercepted with custom message.\"\"\"\n        custom_message = 'Click intercepted by overlay element'\n        with pytest.raises(ClickIntercepted) as exc_info:\n            raise ClickIntercepted(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_element_not_a_file_input(self):\n        \"\"\"Test ElementNotAFileInput exception.\"\"\"\n        with pytest.raises(ElementNotAFileInput) as exc_info:\n            raise ElementNotAFileInput()\n        assert str(exc_info.value) == 'The element is not a file input'\n\n    def test_element_not_a_file_input_custom(self):\n        \"\"\"Test ElementNotAFileInput with custom message.\"\"\"\n        custom_message = 'Expected file input, got text input'\n        with pytest.raises(ElementNotAFileInput) as exc_info:\n            raise ElementNotAFileInput(custom_message)\n        assert str(exc_info.value) == custom_message\n\n\nclass TestTimeoutExceptions:\n    \"\"\"Test timeout-related exceptions.\"\"\"\n\n    def test_page_load_timeout(self):\n        \"\"\"Test PageLoadTimeout exception.\"\"\"\n        with pytest.raises(PageLoadTimeout) as exc_info:\n            raise PageLoadTimeout()\n        assert str(exc_info.value) == 'Page load timed out'\n\n    def test_page_load_timeout_custom(self):\n        \"\"\"Test PageLoadTimeout with custom message.\"\"\"\n        custom_message = 'Page load timed out after 30 seconds'\n        with pytest.raises(PageLoadTimeout) as exc_info:\n            raise PageLoadTimeout(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_wait_element_timeout(self):\n        \"\"\"Test WaitElementTimeout exception.\"\"\"\n        with pytest.raises(WaitElementTimeout) as exc_info:\n            raise WaitElementTimeout()\n        assert str(exc_info.value) == 'Timed out waiting for element to appear'\n\n    def test_wait_element_timeout_custom(self):\n        \"\"\"Test WaitElementTimeout with custom message.\"\"\"\n        custom_message = 'Element with selector \"#button\" did not appear within 10 seconds'\n        with pytest.raises(WaitElementTimeout) as exc_info:\n            raise WaitElementTimeout(custom_message)\n        assert str(exc_info.value) == custom_message\n\n\nclass TestConfigurationExceptions:\n    \"\"\"Test configuration-related exceptions.\"\"\"\n\n    def test_invalid_options_object(self):\n        \"\"\"Test InvalidOptionsObject exception.\"\"\"\n        with pytest.raises(InvalidOptionsObject) as exc_info:\n            raise InvalidOptionsObject()\n        assert str(exc_info.value) == 'The options object provided is invalid'\n\n    def test_invalid_options_object_custom(self):\n        \"\"\"Test InvalidOptionsObject with custom message.\"\"\"\n        custom_message = 'Options must be a dictionary'\n        with pytest.raises(InvalidOptionsObject) as exc_info:\n            raise InvalidOptionsObject(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_invalid_browser_path(self):\n        \"\"\"Test InvalidBrowserPath exception.\"\"\"\n        with pytest.raises(InvalidBrowserPath) as exc_info:\n            raise InvalidBrowserPath()\n        assert str(exc_info.value) == 'The browser path provided is invalid'\n\n    def test_invalid_browser_path_custom(self):\n        \"\"\"Test InvalidBrowserPath with custom message.\"\"\"\n        custom_message = 'Browser not found at /usr/bin/chrome'\n        with pytest.raises(InvalidBrowserPath) as exc_info:\n            raise InvalidBrowserPath(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_argument_already_exists_in_options(self):\n        \"\"\"Test ArgumentAlreadyExistsInOptions exception.\"\"\"\n        with pytest.raises(ArgumentAlreadyExistsInOptions) as exc_info:\n            raise ArgumentAlreadyExistsInOptions()\n        assert str(exc_info.value) == 'The argument already exists in the options'\n\n    def test_argument_already_exists_custom(self):\n        \"\"\"Test ArgumentAlreadyExistsInOptions with custom message.\"\"\"\n        custom_message = 'Argument --headless already exists'\n        with pytest.raises(ArgumentAlreadyExistsInOptions) as exc_info:\n            raise ArgumentAlreadyExistsInOptions(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_invalid_file_extension(self):\n        \"\"\"Test InvalidFileExtension exception.\"\"\"\n        with pytest.raises(InvalidFileExtension) as exc_info:\n            raise InvalidFileExtension()\n        assert str(exc_info.value) == 'The file extension provided is not supported'\n\n\nclass TestDialogExceptions:\n    \"\"\"Test dialog-related exceptions.\"\"\"\n\n    def test_no_dialog_present(self):\n        \"\"\"Test NoDialogPresent exception.\"\"\"\n        with pytest.raises(NoDialogPresent) as exc_info:\n            raise NoDialogPresent()\n        assert str(exc_info.value) == 'No dialog present on the page'\n\n    def test_no_dialog_present_custom(self):\n        \"\"\"Test NoDialogPresent with custom message.\"\"\"\n        custom_message = 'Expected alert dialog but none found'\n        with pytest.raises(NoDialogPresent) as exc_info:\n            raise NoDialogPresent(custom_message)\n        assert str(exc_info.value) == custom_message\n\n\nclass TestIFrameExceptions:\n    \"\"\"Test iframe-related exceptions.\"\"\"\n\n    def test_not_an_iframe(self):\n        \"\"\"Test NotAnIFrame exception.\"\"\"\n        with pytest.raises(NotAnIFrame) as exc_info:\n            raise NotAnIFrame()\n        assert str(exc_info.value) == 'The element is not an iframe'\n\n    def test_not_an_iframe_custom(self):\n        \"\"\"Test NotAnIFrame with custom message.\"\"\"\n        custom_message = 'Expected iframe element, got div'\n        with pytest.raises(NotAnIFrame) as exc_info:\n            raise NotAnIFrame(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_invalid_iframe(self):\n        \"\"\"Test InvalidIFrame exception.\"\"\"\n        with pytest.raises(InvalidIFrame) as exc_info:\n            raise InvalidIFrame()\n        assert str(exc_info.value) == 'The iframe is not valid'\n\n    def test_invalid_iframe_custom(self):\n        \"\"\"Test InvalidIFrame with custom message.\"\"\"\n        custom_message = 'IFrame has no src attribute'\n        with pytest.raises(InvalidIFrame) as exc_info:\n            raise InvalidIFrame(custom_message)\n        assert str(exc_info.value) == custom_message\n\n    def test_iframe_not_found(self):\n        \"\"\"Test IFrameNotFound exception.\"\"\"\n        with pytest.raises(IFrameNotFound) as exc_info:\n            raise IFrameNotFound()\n        assert str(exc_info.value) == 'The iframe was not found'\n\n    def test_iframe_not_found_custom(self):\n        \"\"\"Test IFrameNotFound with custom message.\"\"\"\n        custom_message = 'IFrame with name \"content\" not found'\n        with pytest.raises(IFrameNotFound) as exc_info:\n            raise IFrameNotFound(custom_message)\n        assert str(exc_info.value) == custom_message\n\n\nclass TestExceptionInheritance:\n    \"\"\"Test exception inheritance hierarchy.\"\"\"\n\n    def test_all_exceptions_inherit_from_pydoll_exception(self):\n        \"\"\"Test that all custom exceptions inherit from PydollException.\"\"\"\n        exceptions_to_test = [\n            ConnectionFailed, ReconnectionFailed, WebSocketConnectionClosed, NetworkError,\n            BrowserNotRunning, FailedToStartBrowser, UnsupportedOS, NoValidTabFound,\n            InvalidCommand, InvalidResponse, ResendCommandFailed, CommandExecutionTimeout,\n            InvalidCallback, EventNotSupported, ElementNotFound, ElementNotVisible,\n            ElementNotInteractable, ClickIntercepted, ElementNotAFileInput,\n            PageLoadTimeout, WaitElementTimeout, InvalidOptionsObject, InvalidBrowserPath,\n            ArgumentAlreadyExistsInOptions, InvalidFileExtension, NoDialogPresent,\n            NotAnIFrame, InvalidIFrame, IFrameNotFound\n        ]\n        \n        for exception_class in exceptions_to_test:\n            assert issubclass(exception_class, PydollException), f\"{exception_class.__name__} should inherit from PydollException\"\n\n    def test_base_exception_categories(self):\n        \"\"\"Test that base exception categories inherit from PydollException.\"\"\"\n        base_exceptions = [\n            ConnectionException, BrowserException, ProtocolException,\n            ElementException, TimeoutException, ConfigurationException, DialogException\n        ]\n        \n        for exception_class in base_exceptions:\n            assert issubclass(exception_class, PydollException), f\"{exception_class.__name__} should inherit from PydollException\"\n\n    def test_connection_exceptions_inherit_from_connection_exception(self):\n        \"\"\"Test that connection exceptions inherit from ConnectionException.\"\"\"\n        connection_exceptions = [ConnectionFailed, ReconnectionFailed, WebSocketConnectionClosed, NetworkError]\n        \n        for exception_class in connection_exceptions:\n            assert issubclass(exception_class, ConnectionException), f\"{exception_class.__name__} should inherit from ConnectionException\"\n"
  },
  {
    "path": "tests/test_find_elements_mixin.py",
    "content": "import pytest\nimport re\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nfrom pydoll.elements.mixins.find_elements_mixin import FindElementsMixin\nfrom pydoll.elements.utils import SelectorParser\nfrom pydoll.constants import By\nfrom pydoll.exceptions import ElementNotFound, WaitElementTimeout\n\n\nclass MockFindElementsMixin(FindElementsMixin):\n    \"\"\"Mock implementation of FindElementsMixin for testing.\"\"\"\n    \n    def __init__(self):\n        self._connection_handler = AsyncMock()\n        # Some tests need object_id, others don't\n        self._object_id = None\n\n\nclass TestBuildXPath:\n    \"\"\"Test the _build_xpath static method comprehensively.\"\"\"\n\n    def test_build_xpath_single_id(self):\n        \"\"\"Test XPath building with only ID.\"\"\"\n        xpath = FindElementsMixin._build_xpath(id='test-id')\n        assert xpath == '//*[@id=\"test-id\"]'\n\n    def test_build_xpath_single_class_name(self):\n        \"\"\"Test XPath building with only class name.\"\"\"\n        xpath = FindElementsMixin._build_xpath(class_name='btn-primary')\n        expected = '//*[contains(concat(\" \", normalize-space(@class), \" \"), \" btn-primary \")]'\n        assert xpath == expected\n\n    def test_build_xpath_single_name(self):\n        \"\"\"Test XPath building with only name attribute.\"\"\"\n        xpath = FindElementsMixin._build_xpath(name='username')\n        assert xpath == '//*[@name=\"username\"]'\n\n    def test_build_xpath_single_tag_name(self):\n        \"\"\"Test XPath building with only tag name.\"\"\"\n        xpath = FindElementsMixin._build_xpath(tag_name='button')\n        assert xpath == '//button'\n\n    def test_build_xpath_single_text(self):\n        \"\"\"Test XPath building with only text content.\"\"\"\n        xpath = FindElementsMixin._build_xpath(text='Click me')\n        assert xpath == '//*[contains(text(), \"Click me\")]'\n\n    def test_build_xpath_single_custom_attribute(self):\n        \"\"\"Test XPath building with single custom attribute.\"\"\"\n        xpath = FindElementsMixin._build_xpath(data_testid='submit-btn')\n        assert xpath == '//*[@data-testid=\"submit-btn\"]'\n\n    def test_build_xpath_id_and_class(self):\n        \"\"\"Test XPath building with ID and class name.\"\"\"\n        xpath = FindElementsMixin._build_xpath(id='main-btn', class_name='primary')\n        expected = '//*[@id=\"main-btn\" and contains(concat(\" \", normalize-space(@class), \" \"), \" primary \")]'\n        assert xpath == expected\n\n    def test_build_xpath_tag_and_attributes(self):\n        \"\"\"Test XPath building with tag name and multiple attributes.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            tag_name='input',\n            id='email-field',\n            name='email',\n            type='email'\n        )\n        expected = '//input[@id=\"email-field\" and @name=\"email\" and @type=\"email\"]'\n        assert xpath == expected\n\n    def test_build_xpath_all_parameters(self):\n        \"\"\"Test XPath building with all possible parameters.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            id='complex-element',\n            class_name='form-control',\n            name='user_input',\n            tag_name='input',\n            text='placeholder text',\n            data_role='textbox',\n            aria_label='User input field'\n        )\n        expected = ('//input[@id=\"complex-element\" and '\n                   'contains(concat(\" \", normalize-space(@class), \" \"), \" form-control \") and '\n                   '@name=\"user_input\" and '\n                   'contains(text(), \"placeholder text\") and '\n                   '@data-role=\"textbox\" and '\n                   '@aria-label=\"User input field\"]')\n        assert xpath == expected\n\n    def test_build_xpath_text_with_quotes(self):\n        \"\"\"Test XPath building with text containing quotes.\"\"\"\n        xpath = FindElementsMixin._build_xpath(text='Say \"Hello\"')\n        assert xpath == '//*[contains(text(), \"Say \"Hello\"\")]'\n\n    def test_build_xpath_attribute_with_quotes(self):\n        \"\"\"Test XPath building with attribute value containing quotes.\"\"\"\n        xpath = FindElementsMixin._build_xpath(title='This is a \"quoted\" title')\n        assert xpath == '//*[@title=\"This is a \"quoted\" title\"]'\n\n    def test_build_xpath_empty_values_ignored(self):\n        \"\"\"Test that empty string values are ignored in XPath building.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            id='test-id',\n            class_name='',  # Empty string should be ignored\n            name=None,      # None should be ignored\n            tag_name='div'\n        )\n        assert xpath == '//div[@id=\"test-id\"]'\n\n    def test_build_xpath_class_name_with_spaces(self):\n        \"\"\"Test XPath building with class name that has spaces (edge case).\"\"\"\n        xpath = FindElementsMixin._build_xpath(class_name='btn primary large')\n        expected = '//*[contains(concat(\" \", normalize-space(@class), \" \"), \" btn primary large \")]'\n        assert xpath == expected\n\n    def test_build_xpath_special_characters_in_attributes(self):\n        \"\"\"Test XPath building with special characters in attribute values.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            data_value='test@example.com',\n            aria_describedby='field-help-123'\n        )\n        expected = '//*[@data-value=\"test@example.com\" and @aria-describedby=\"field-help-123\"]'\n        assert xpath == expected\n\n    def test_build_xpath_numeric_attribute_values(self):\n        \"\"\"Test XPath building with numeric attribute values.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            tabindex='0',\n            maxlength='255'\n        )\n        expected = '//*[@tabindex=\"0\" and @maxlength=\"255\"]'\n        assert xpath == expected\n\n    def test_build_xpath_no_parameters(self):\n        \"\"\"Test XPath building with no parameters returns generic selector.\"\"\"\n        xpath = FindElementsMixin._build_xpath()\n        assert xpath == '//*'\n\n    def test_build_xpath_only_tag_name(self):\n        \"\"\"Test XPath building with only tag name.\"\"\"\n        xpath = FindElementsMixin._build_xpath(tag_name='span')\n        assert xpath == '//span'\n\n    def test_build_xpath_hyphenated_attributes(self):\n        \"\"\"Test XPath building with hyphenated attribute names.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            **{'data-test-id': 'submit-button', 'aria-label': 'Submit form'}\n        )\n        expected = '//*[@data-test-id=\"submit-button\" and @aria-label=\"Submit form\"]'\n        assert xpath == expected\n\n\nclass TestGetExpressionType:\n    \"\"\"Test the _get_expression_type static method.\"\"\"\n\n    def test_xpath_double_slash(self):\n        \"\"\"Test XPath detection with double slash.\"\"\"\n        assert FindElementsMixin._get_expression_type('//div') == By.XPATH\n\n    def test_xpath_dot_double_slash(self):\n        \"\"\"Test XPath detection with dot double slash.\"\"\"\n        assert FindElementsMixin._get_expression_type('.//span') == By.XPATH\n\n    def test_xpath_dot_slash(self):\n        \"\"\"Test XPath detection with dot slash.\"\"\"\n        assert FindElementsMixin._get_expression_type('./button') == By.XPATH\n\n    def test_xpath_single_slash(self):\n        \"\"\"Test XPath detection with single slash.\"\"\"\n        assert FindElementsMixin._get_expression_type('/html/body') == By.XPATH\n\n    def test_css_selector_default(self):\n        \"\"\"Test CSS selector as default.\"\"\"\n        assert FindElementsMixin._get_expression_type('div.content > p') == By.CSS_SELECTOR\n\n    def test_css_selector_attribute(self):\n        \"\"\"Test CSS selector with attributes.\"\"\"\n        assert FindElementsMixin._get_expression_type('input[type=\"text\"]') == By.CSS_SELECTOR\n\n    def test_css_selector_pseudo_class(self):\n        \"\"\"Test CSS selector with pseudo-classes.\"\"\"\n        assert FindElementsMixin._get_expression_type('button:hover') == By.CSS_SELECTOR\n    \n    def test_css_selector_not_xpath(self):\n        \"\"\"Test that css selector doesn't conflict with XPath dot slash.\"\"\"\n        assert FindElementsMixin._get_expression_type('.button') == By.CSS_SELECTOR\n        assert FindElementsMixin._get_expression_type('./button') == By.XPATH\n\n    def test_complex_xpath_expressions(self):\n        \"\"\"Test complex XPath expressions are detected correctly.\"\"\"\n        complex_xpaths = [\n            '//div[@class=\"content\"]/p[contains(text(), \"Hello\")]',\n            './/button[position()=1]',\n            './/*[@id=\"test\" and @class=\"active\"]',\n            '/html/body/div[1]/form/input[@type=\"submit\"]'\n        ]\n        for xpath in complex_xpaths:\n            assert FindElementsMixin._get_expression_type(xpath) == By.XPATH\n\n    def test_edge_case_expressions(self):\n        \"\"\"Test edge case expressions.\"\"\"\n        # Empty string should default to CSS\n        assert FindElementsMixin._get_expression_type('') == By.CSS_SELECTOR\n\n    def test_xpath_with_parentheses_and_predicate(self):\n        \"\"\"Test XPath detection with parentheses, e.g. (//div)[last()].\"\"\"\n        expressions = [\n            '(//div)[last()]',\n            '(//span[@class=\"btn\"])[1]',\n            '(/html/body/div)[position()=1]'\n        ]\n        for expr in expressions:\n            assert FindElementsMixin._get_expression_type(expr) == By.XPATH\n\n\nclass TestEnsureRelativeXPath:\n    \"\"\"Test the _ensure_relative_xpath static method.\"\"\"\n\n    def test_absolute_xpath_becomes_relative(self):\n        \"\"\"Test that absolute XPath becomes relative.\"\"\"\n        xpath = '//div[@id=\"test\"]'\n        result = FindElementsMixin._ensure_relative_xpath(xpath)\n        assert result == './/div[@id=\"test\"]'\n\n    def test_already_relative_xpath_unchanged(self):\n        \"\"\"Test that already relative XPath remains unchanged.\"\"\"\n        xpath = './/div[@id=\"test\"]'\n        result = FindElementsMixin._ensure_relative_xpath(xpath)\n        assert result == './/div[@id=\"test\"]'\n\n    def test_dot_slash_xpath_unchanged(self):\n        \"\"\"Test that dot slash XPath remains unchanged.\"\"\"\n        xpath = './button'\n        result = FindElementsMixin._ensure_relative_xpath(xpath)\n        assert result == './button'\n\n    def test_single_slash_xpath_becomes_relative(self):\n        \"\"\"Test that single slash XPath becomes relative.\"\"\"\n        xpath = '/html/body/div'\n        result = FindElementsMixin._ensure_relative_xpath(xpath)\n        assert result == './html/body/div'\n\n    def test_empty_xpath(self):\n        \"\"\"Test empty XPath handling.\"\"\"\n        xpath = ''\n        result = FindElementsMixin._ensure_relative_xpath(xpath)\n        assert result == '.'\n\n    def test_complex_xpath_expressions(self):\n        \"\"\"Test complex XPath expressions.\"\"\"\n        test_cases = [\n            ('//div[contains(@class, \"test\")]', './/div[contains(@class, \"test\")]'),\n            ('.//span[@id=\"existing\"]', './/span[@id=\"existing\"]'),\n            ('//*[@data-test=\"value\"]', './/*[@data-test=\"value\"]'),\n            ('//button[text()=\"Submit\"]', './/button[text()=\"Submit\"]')\n        ]\n        \n        for input_xpath, expected in test_cases:\n            result = FindElementsMixin._ensure_relative_xpath(input_xpath)\n            assert result == expected\n\n\nclass TestGetByAndValue:\n    \"\"\"Test the _get_by_and_value method.\"\"\"\n\n    def setup_method(self):\n        \"\"\"Set up test fixtures.\"\"\"\n        self.mixin = MockFindElementsMixin()\n        self.by_map = {\n            'id': By.ID,\n            'class_name': By.CLASS_NAME,\n            'name': By.NAME,\n            'tag_name': By.TAG_NAME,\n            'xpath': By.XPATH,\n        }\n\n    def test_single_id_selector(self):\n        \"\"\"Test single ID selector returns direct By.ID.\"\"\"\n        by, value = self.mixin._get_by_and_value(self.by_map, id='test-id')\n        assert by == By.ID\n        assert value == 'test-id'\n\n    def test_single_class_name_selector(self):\n        \"\"\"Test single class name selector returns direct By.CLASS_NAME.\"\"\"\n        by, value = self.mixin._get_by_and_value(self.by_map, class_name='btn-primary')\n        assert by == By.CLASS_NAME\n        assert value == 'btn-primary'\n\n    def test_single_name_selector(self):\n        \"\"\"Test single name selector returns direct By.NAME.\"\"\"\n        by, value = self.mixin._get_by_and_value(self.by_map, name='username')\n        assert by == By.NAME\n        assert value == 'username'\n\n    def test_single_tag_name_selector(self):\n        \"\"\"Test single tag name selector returns direct By.TAG_NAME.\"\"\"\n        by, value = self.mixin._get_by_and_value(self.by_map, tag_name='button')\n        assert by == By.TAG_NAME\n        assert value == 'button'\n\n    def test_single_custom_attribute(self):\n        \"\"\"Test single custom attribute builds XPath.\"\"\"\n        by, value = self.mixin._get_by_and_value(self.by_map, data_testid='submit-btn')\n        assert by == By.XPATH\n        assert value == '//*[@data-testid=\"submit-btn\"]'\n\n    def test_multiple_attributes_build_xpath(self):\n        \"\"\"Test multiple attributes build XPath.\"\"\"\n        by, value = self.mixin._get_by_and_value(\n            self.by_map, \n            id='test-id', \n            class_name='btn-primary'\n        )\n        assert by == By.XPATH\n        expected = '//*[@id=\"test-id\" and contains(concat(\" \", normalize-space(@class), \" \"), \" btn-primary \")]'\n        assert value == expected\n\n    def test_text_with_single_attribute_builds_xpath(self):\n        \"\"\"Test that text with any other attribute builds XPath.\"\"\"\n        by, value = self.mixin._get_by_and_value(\n            self.by_map,\n            id='test-id',\n            text='Click me'\n        )\n        assert by == By.XPATH\n        expected = '//*[@id=\"test-id\" and contains(text(), \"Click me\")]'\n        assert value == expected\n\n    def test_text_alone_builds_xpath(self):\n        \"\"\"Test that text alone builds XPath.\"\"\"\n        by, value = self.mixin._get_by_and_value(self.by_map, text='Submit')\n        assert by == By.XPATH\n        assert value == '//*[contains(text(), \"Submit\")]'\n\n    def test_empty_values_ignored(self):\n        \"\"\"Test that empty values are ignored in selector building.\"\"\"\n        by, value = self.mixin._get_by_and_value(\n            self.by_map,\n            id='test-id',\n            class_name='',  # Empty string\n            name=None       # None value\n        )\n        assert by == By.ID\n        assert value == 'test-id'\n\n    def test_all_empty_values_with_custom_attribute(self):\n        \"\"\"Test custom attribute when standard attributes are empty.\"\"\"\n        by, value = self.mixin._get_by_and_value(\n            self.by_map,\n            id='',\n            class_name=None,\n            data_role='button'\n        )\n        assert by == By.XPATH\n        assert value == '//*[@data-role=\"button\"]'\n\n\nclass TestFindElementsMixinEdgeCases:\n    \"\"\"Test edge cases and error conditions in FindElementsMixin.\"\"\"\n\n    def setup_method(self):\n        \"\"\"Set up test fixtures.\"\"\"\n        self.mixin = MockFindElementsMixin()\n\n    @pytest.mark.asyncio\n    async def test_find_no_criteria_raises_error(self):\n        \"\"\"Test that find with no criteria raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match='At least one of the following arguments must be provided'):\n            await self.mixin.find()\n\n    @pytest.mark.asyncio\n    async def test_find_empty_string_criteria_raises_error(self):\n        \"\"\"Test that find with only empty string criteria raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match='At least one of the following arguments must be provided'):\n            await self.mixin.find(id='', class_name='', name='', tag_name='', text='')\n\n    @pytest.mark.asyncio\n    async def test_find_none_criteria_raises_error(self):\n        \"\"\"Test that find with only None criteria raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match='At least one of the following arguments must be provided'):\n            await self.mixin.find(id=None, class_name=None, name=None, tag_name=None, text=None)\n\n    @pytest.mark.asyncio\n    async def test_find_with_custom_attributes_only(self):\n        \"\"\"Test find with only custom attributes works.\"\"\"\n        # Mock the internal methods\n        self.mixin._find_element = AsyncMock(return_value=MagicMock())\n        \n        result = await self.mixin.find(data_testid='submit-button')\n        \n        # Should call _find_element with XPath\n        self.mixin._find_element.assert_called_once()\n        call_args = self.mixin._find_element.call_args[0]\n        assert call_args[0] == By.XPATH\n        assert '@data-testid=\"submit-button\"' in call_args[1]\n\n    @pytest.mark.asyncio\n    async def test_query_empty_expression(self):\n        \"\"\"Test query with empty expression.\"\"\"\n        self.mixin._find_element = AsyncMock(return_value=MagicMock())\n        \n        result = await self.mixin.query('')\n        \n        # Should call _find_element with CSS_SELECTOR (default)\n        self.mixin._find_element.assert_called_once()\n        call_args = self.mixin._find_element.call_args[0]\n        assert call_args[0] == By.CSS_SELECTOR\n        assert call_args[1] == ''\n\n    @pytest.mark.asyncio\n    async def test_find_or_wait_element_timeout_zero(self):\n        \"\"\"Test find_or_wait_element with timeout=0 calls find immediately.\"\"\"\n        self.mixin._find_element = AsyncMock(return_value=MagicMock())\n        \n        result = await self.mixin.find_or_wait_element(By.ID, 'test-id', timeout=0)\n        \n        self.mixin._find_element.assert_called_once_with(By.ID, 'test-id', raise_exc=True)\n\n    @pytest.mark.asyncio\n    async def test_find_or_wait_element_timeout_success_on_retry(self):\n        \"\"\"Test find_or_wait_element succeeds on retry within timeout.\"\"\"\n        # First call returns None, second call returns element\n        mock_element = MagicMock()\n        self.mixin._find_element = AsyncMock(side_effect=[None, mock_element])\n        \n        with patch('asyncio.sleep') as mock_sleep, \\\n             patch('asyncio.get_event_loop') as mock_loop:\n            # Mock time progression\n            mock_loop.return_value.time.side_effect = [0, 0.5, 1.0]\n            \n            result = await self.mixin.find_or_wait_element(\n                By.ID, 'test-id', timeout=2, raise_exc=False\n            )\n        \n        assert result == mock_element\n        assert self.mixin._find_element.call_count == 2\n        mock_sleep.assert_called_once_with(0.5)\n\n    @pytest.mark.asyncio\n    async def test_find_or_wait_element_timeout_failure(self):\n        \"\"\"Test find_or_wait_element raises WaitElementTimeout.\"\"\"\n        self.mixin._find_element = AsyncMock(return_value=None)\n        \n        with patch('asyncio.sleep') as mock_sleep, \\\n             patch('asyncio.get_event_loop') as mock_loop:\n            # Mock time progression that exceeds timeout\n            mock_loop.return_value.time.side_effect = [0, 0.5, 1.0, 1.5, 2.1]\n            \n            with pytest.raises(WaitElementTimeout):\n                await self.mixin.find_or_wait_element(\n                    By.ID, 'test-id', timeout=2, raise_exc=True\n                )\n\n    @pytest.mark.asyncio\n    async def test_find_or_wait_element_timeout_failure_no_exception(self):\n        \"\"\"Test find_or_wait_element returns None when raise_exc=False.\"\"\"\n        self.mixin._find_element = AsyncMock(return_value=None)\n        \n        with patch('asyncio.sleep') as mock_sleep, \\\n             patch('asyncio.get_event_loop') as mock_loop:\n            # Mock time progression that exceeds timeout\n            mock_loop.return_value.time.side_effect = [0, 0.5, 1.0, 1.5, 2.1]\n            \n            result = await self.mixin.find_or_wait_element(\n                By.ID, 'test-id', timeout=2, raise_exc=False\n            )\n        \n        assert result is None\n\n    @pytest.mark.asyncio\n    async def test_find_elements_with_timeout(self):\n        \"\"\"Test find with find_all=True and timeout.\"\"\"\n        mock_elements = [MagicMock(), MagicMock()]\n        self.mixin._find_elements = AsyncMock(return_value=mock_elements)\n        \n        result = await self.mixin.find_or_wait_element(\n            By.CLASS_NAME, 'item', timeout=1, find_all=True\n        )\n        \n        assert result == mock_elements\n        self.mixin._find_elements.assert_called_once()\n\n    def test_regex_pattern_in_get_expression_type(self):\n        \"\"\"Test the regex pattern used in _get_expression_type.\"\"\"\n        xpath_pattern = r'^(//|\\.//|\\.\\/|/)'\n        \n        # Test cases that should match\n        xpath_expressions = [\n            '//div',\n            './/span', \n            './button',\n            '/html/body'\n        ]\n        \n        for expr in xpath_expressions:\n            assert re.match(xpath_pattern, expr), f\"Pattern should match: {expr}\"\n        \n        # Test cases that should not match\n        non_xpath_expressions = [\n            'div.class',\n            '#id',\n            '.class',\n            'input[type=\"text\"]',\n            'button:hover'\n        ]\n        \n        for expr in non_xpath_expressions:\n            assert not re.match(xpath_pattern, expr), f\"Pattern should not match: {expr}\"\n\n    def test_xpath_building_with_boolean_attributes(self):\n        \"\"\"Test XPath building with boolean-like attributes.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            required='true',\n            disabled='false',\n            checked='checked'\n        )\n        expected = '//*[@required=\"true\" and @disabled=\"false\" and @checked=\"checked\"]'\n        assert xpath == expected\n\n    def test_xpath_building_preserves_attribute_order(self):\n        \"\"\"Test that XPath building maintains consistent attribute order.\"\"\"\n        # Test multiple times to ensure consistency\n        for _ in range(5):\n            xpath = FindElementsMixin._build_xpath(\n                id='test',\n                class_name='btn',\n                name='submit',\n                data_role='button'\n            )\n            # The order should be: id, class_name, name, then custom attributes\n            assert '@id=\"test\"' in xpath\n            assert 'contains(concat(\" \", normalize-space(@class), \" \"), \" btn \")' in xpath\n            assert '@name=\"submit\"' in xpath\n            assert '@data-role=\"button\"' in xpath\n\n    def test_xpath_building_with_unicode_characters(self):\n        \"\"\"Test XPath building with Unicode characters.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            text='Olá mundo',\n            title='Título com acentos',\n            placeholder='Escreva aqui...'\n        )\n        expected = '//*[contains(text(), \"Olá mundo\") and @title=\"Título com acentos\" and @placeholder=\"Escreva aqui...\"]'\n        assert xpath == expected\n\n    def test_class_name_xpath_normalization(self):\n        \"\"\"Test that class name XPath uses proper normalization.\"\"\"\n        xpath = FindElementsMixin._build_xpath(class_name='test-class')\n        \n        # Should use normalize-space to handle multiple spaces\n        assert 'normalize-space(@class)' in xpath\n        # Should wrap with spaces to match exact class names\n        assert '\" test-class \"' in xpath\n        # Should use concat to add spaces\n        assert 'concat(\" \"' in xpath\n\n\nclass TestUnderscoreToHyphenConversion:\n    \"\"\"Test automatic conversion of underscores to hyphens in attribute names.\"\"\"\n\n    def test_single_underscore_to_hyphen(self):\n        \"\"\"Test single underscore conversion in attribute name.\"\"\"\n        xpath = FindElementsMixin._build_xpath(data_test='submit-button')\n        assert xpath == '//*[@data-test=\"submit-button\"]'\n\n    def test_multiple_underscores_to_hyphens(self):\n        \"\"\"Test multiple underscores conversion in same attribute.\"\"\"\n        xpath = FindElementsMixin._build_xpath(data_test_id='submit-button')\n        assert xpath == '//*[@data-test-id=\"submit-button\"]'\n\n    def test_aria_attributes_conversion(self):\n        \"\"\"Test aria attributes underscore conversion.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            aria_label='Submit form',\n            aria_describedby='helper-text'\n        )\n        assert '@aria-label=\"Submit form\"' in xpath\n        assert '@aria-describedby=\"helper-text\"' in xpath\n\n    def test_data_attributes_conversion(self):\n        \"\"\"Test data attributes underscore conversion.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            data_testid='main-button',\n            data_value='123',\n            data_action='submit'\n        )\n        assert '@data-testid=\"main-button\"' in xpath\n        assert '@data-value=\"123\"' in xpath\n        assert '@data-action=\"submit\"' in xpath\n\n    def test_mixed_underscore_and_hyphen_attributes(self):\n        \"\"\"Test that attributes already with hyphens are not affected.\"\"\"\n        # Using dict unpacking for attributes with hyphens\n        xpath = FindElementsMixin._build_xpath(\n            data_test='value1',\n            **{'already-hyphenated': 'value2'}\n        )\n        assert '@data-test=\"value1\"' in xpath\n        assert '@already-hyphenated=\"value2\"' in xpath\n\n    def test_combined_standard_and_custom_attributes(self):\n        \"\"\"Test conversion works with combined standard and custom attributes.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            id='main-element',\n            class_name='btn',\n            data_testid='submit-btn',\n            aria_label='Submit button'\n        )\n        assert '@id=\"main-element\"' in xpath\n        assert 'contains(concat(\" \", normalize-space(@class), \" \"), \" btn \")' in xpath\n        assert '@data-testid=\"submit-btn\"' in xpath\n        assert '@aria-label=\"Submit button\"' in xpath\n\n    def test_underscore_in_attribute_value_unchanged(self):\n        \"\"\"Test that underscores in values are not converted.\"\"\"\n        xpath = FindElementsMixin._build_xpath(data_test='some_value_with_underscores')\n        assert xpath == '//*[@data-test=\"some_value_with_underscores\"]'\n        assert 'some_value_with_underscores' in xpath  # Value unchanged\n\n    def test_complex_attribute_names_conversion(self):\n        \"\"\"Test conversion of complex attribute names.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            ng_repeat='item in items',\n            v_model='username',\n            x_bind_value='someValue'\n        )\n        assert '@ng-repeat=\"item in items\"' in xpath\n        assert '@v-model=\"username\"' in xpath\n        assert '@x-bind-value=\"someValue\"' in xpath\n\n    def test_single_character_segments(self):\n        \"\"\"Test attributes with single character segments.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            a_b_c='value1',\n            x_y='value2'\n        )\n        assert '@a-b-c=\"value1\"' in xpath\n        assert '@x-y=\"value2\"' in xpath\n\n    def test_no_underscores_unchanged(self):\n        \"\"\"Test attributes without underscores remain unchanged.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            role='button',\n            type='submit',\n            disabled='true'\n        )\n        assert '@role=\"button\"' in xpath\n        assert '@type=\"submit\"' in xpath\n        assert '@disabled=\"true\"' in xpath\n\n    def test_trailing_and_leading_underscores(self):\n        \"\"\"Test handling of trailing and leading underscores.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            _private='value1',\n            public_='value2',\n            _both_='value3'\n        )\n        # Leading/trailing underscores should also be converted to hyphens\n        assert '@-private=\"value1\"' in xpath\n        assert '@public-=\"value2\"' in xpath\n        assert '@-both-=\"value3\"' in xpath\n\n    def test_conversion_with_text_parameter(self):\n        \"\"\"Test conversion works correctly with text parameter.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            text='Button text',\n            data_testid='submit-btn'\n        )\n        assert 'contains(text(), \"Button text\")' in xpath\n        assert '@data-testid=\"submit-btn\"' in xpath\n\n    def test_conversion_with_tag_name(self):\n        \"\"\"Test conversion works correctly with tag_name parameter.\"\"\"\n        xpath = FindElementsMixin._build_xpath(\n            tag_name='button',\n            data_test='submit',\n            aria_label='Submit form'\n        )\n        assert xpath.startswith('//button')\n        assert '@data-test=\"submit\"' in xpath\n        assert '@aria-label=\"Submit form\"' in xpath\n\n\nclass TestUnderscoreConversionWithGetByAndValue:\n    \"\"\"Test underscore to hyphen conversion in _get_by_and_value method.\"\"\"\n\n    def setup_method(self):\n        \"\"\"Set up test fixtures.\"\"\"\n        self.mixin = MockFindElementsMixin()\n        self.by_map = {\n            'id': By.ID,\n            'class_name': By.CLASS_NAME,\n            'name': By.NAME,\n            'tag_name': By.TAG_NAME,\n            'xpath': By.XPATH,\n        }\n\n    def test_custom_attribute_with_underscore(self):\n        \"\"\"Test custom attribute with underscore converts properly.\"\"\"\n        by, value = self.mixin._get_by_and_value(\n            self.by_map,\n            data_testid='submit-button'\n        )\n        assert by == By.XPATH\n        assert '@data-testid=\"submit-button\"' in value\n\n    def test_multiple_custom_attributes_with_underscores(self):\n        \"\"\"Test multiple custom attributes with underscores.\"\"\"\n        by, value = self.mixin._get_by_and_value(\n            self.by_map,\n            data_test='value1',\n            aria_label='value2',\n            ng_model='value3'\n        )\n        assert by == By.XPATH\n        assert '@data-test=\"value1\"' in value\n        assert '@aria-label=\"value2\"' in value\n        assert '@ng-model=\"value3\"' in value\n\n    def test_standard_and_custom_attributes_mixed(self):\n        \"\"\"Test standard attributes with custom underscore attributes.\"\"\"\n        by, value = self.mixin._get_by_and_value(\n            self.by_map,\n            id='main-btn',\n            data_testid='submit',\n            aria_label='Submit'\n        )\n        assert by == By.XPATH\n        assert '@id=\"main-btn\"' in value\n        assert '@data-testid=\"submit\"' in value\n        assert '@aria-label=\"Submit\"' in value\n\n\nclass TestFindElementsSymbolFiltering:\n    \"\"\"Test that Symbol properties are filtered from element query results.\"\"\"\n\n    def setup_method(self):\n        \"\"\"Set up test fixtures.\"\"\"\n        self.mixin = MockFindElementsMixin()\n\n    @pytest.mark.asyncio\n    async def test_find_elements_filters_symbol_properties(self):\n        \"\"\"Test that Symbol properties are excluded from results.\"\"\"\n        find_response = {'result': {'result': {'objectId': 'arr'}}}\n        properties_response = {\n            'result': {\n                'result': [\n                    {'name': '0', 'value': {'type': 'object', 'objectId': 'el-1'}},\n                    {'name': '1', 'value': {'type': 'object', 'objectId': 'el-2'}},\n                    {'name': 'Symbol(Symbol.unscopables)', 'value': {'type': 'object', 'objectId': 'sym'}},\n                    {'name': 'length', 'value': {'type': 'number', 'value': 2}},\n                ]\n            }\n        }\n        describe_response = {'result': {'node': {'nodeName': 'A', 'attributes': []}}}\n\n        self.mixin._connection_handler.execute_command.side_effect = [\n            find_response,\n            properties_response,\n            describe_response,\n            describe_response,\n        ]\n\n        elements = await self.mixin._find_elements(By.CSS_SELECTOR, 'a')\n\n        assert len(elements) == 2\n\n\nclass TestParseIframeSegmentsXPath:\n    \"\"\"Test _SelectorParser.parse_iframe_segments_xpath static method — pure sync, no mocks.\"\"\"\n\n    @pytest.mark.parametrize(\n        'expression, expected_selectors',\n        [\n            # Basic iframe crossing\n            (\n                '//iframe/body',\n                ['//iframe', '//body'],\n            ),\n            # Iframe with attribute predicate\n            (\n                '//iframe[@src*=\"example.com\"]/body',\n                ['//iframe[@src*=\"example.com\"]', '//body'],\n            ),\n            # Iframe with slashes inside quoted attribute\n            (\n                '//iframe[@src=\"url/with/slashes\"]/body',\n                ['//iframe[@src=\"url/with/slashes\"]', '//body'],\n            ),\n            # Iframe not at root — simple\n            (\n                '//div/iframe/div',\n                ['//div/iframe', '//div'],\n            ),\n            # Iframe not at root — with attributes\n            (\n                '//div[@class=\"wrapper\"]/iframe/body',\n                ['//div[@class=\"wrapper\"]/iframe', '//body'],\n            ),\n            # Case insensitive — uppercase\n            (\n                '//IFRAME/body',\n                ['//IFRAME', '//body'],\n            ),\n            # Case insensitive — mixed case\n            (\n                '//IFrame/body',\n                ['//IFrame', '//body'],\n            ),\n            # Nested iframes\n            (\n                '//iframe/iframe/div',\n                ['//iframe', '//iframe', '//div'],\n            ),\n            # Nested iframes with attributes\n            (\n                '//iframe[@src=\"a\"]/div/iframe[@id=\"inner\"]/span',\n                ['//iframe[@src=\"a\"]', '//div/iframe[@id=\"inner\"]', '//span'],\n            ),\n            # Bracket chars inside quoted attribute\n            (\n                '//iframe[@src=\"a[1]/b\"]/body',\n                ['//iframe[@src=\"a[1]/b\"]', '//body'],\n            ),\n            # Multiple steps after iframe\n            (\n                '//iframe[@src*=\"cloudflare\"]/body/div',\n                ['//iframe[@src*=\"cloudflare\"]', '//body/div'],\n            ),\n            # contains() in predicate with \"iframe\" as string value\n            (\n                '//iframe[contains(@src, \"iframe\")]/body',\n                ['//iframe[contains(@src, \"iframe\")]', '//body'],\n            ),\n            # Multiple predicate conditions\n            (\n                '//iframe[@src=\"a\" and @id=\"b\"]/body',\n                ['//iframe[@src=\"a\" and @id=\"b\"]', '//body'],\n            ),\n            # position() predicate\n            (\n                '//iframe[position()=1]/body',\n                ['//iframe[position()=1]', '//body'],\n            ),\n            # not() predicate\n            (\n                '//iframe[not(@disabled)]/body',\n                ['//iframe[not(@disabled)]', '//body'],\n            ),\n            # Grouped expression\n            (\n                '(//iframe)[1]/body',\n                ['(//iframe)[1]', '//body'],\n            ),\n        ],\n    )\n    def test_splits(self, expression, expected_selectors):\n        segments = SelectorParser.parse_iframe_segments_xpath(expression)\n        assert len(segments) == len(expected_selectors)\n        for (by, sel), expected in zip(segments, expected_selectors):\n            assert by == By.XPATH\n            assert sel == expected\n\n    @pytest.mark.parametrize(\n        'expression',\n        [\n            '//iframe',\n            '//iframe[@src=\"example.com\"]',\n            './/iframe',\n            '//div[contains(@class, \"iframe\")]/p',\n            '//div[@data-iframe=\"true\"]/p',\n            '//body/div/span',\n            '//div[@title=\"This is an iframe container\"]/span',\n        ],\n    )\n    def test_no_split(self, expression):\n        segments = SelectorParser.parse_iframe_segments_xpath(expression)\n        assert len(segments) == 1\n        assert segments[0] == (By.XPATH, expression)\n\n\nclass TestParseIframeSegmentsCSS:\n    \"\"\"Test _SelectorParser.parse_iframe_segments_css static method — pure sync, no mocks.\"\"\"\n\n    @pytest.mark.parametrize(\n        'expression, expected_selectors',\n        [\n            # Basic > combinator\n            (\n                'iframe > body',\n                ['iframe', 'body'],\n            ),\n            # Iframe with attribute\n            (\n                'iframe[src*=\"example\"] > body',\n                ['iframe[src*=\"example\"]', 'body'],\n            ),\n            # Descendant (space) combinator\n            (\n                'iframe body',\n                ['iframe', 'body'],\n            ),\n            # Descendant with attribute\n            (\n                'iframe[src*=\"...\"] body',\n                ['iframe[src*=\"...\"]', 'body'],\n            ),\n            # Case insensitive — uppercase\n            (\n                'IFRAME > body',\n                ['IFRAME', 'body'],\n            ),\n            # Case insensitive — mixed case\n            (\n                'IFrame > body',\n                ['IFrame', 'body'],\n            ),\n            # Nested iframes\n            (\n                'iframe > iframe > div',\n                ['iframe', 'iframe', 'div'],\n            ),\n            # Pseudo-class on iframe\n            (\n                'iframe:nth-child(2) > body',\n                ['iframe:nth-child(2)', 'body'],\n            ),\n            # > inside quoted attribute value\n            (\n                'iframe[src=\"value with > arrow\"] > body',\n                ['iframe[src=\"value with > arrow\"]', 'body'],\n            ),\n            # Attribute selector before iframe\n            (\n                '[data-iframe] iframe > body',\n                ['[data-iframe] iframe', 'body'],\n            ),\n            # Multiple steps after iframe\n            (\n                'iframe[src*=\"cloudflare\"] > body > div.target',\n                ['iframe[src*=\"cloudflare\"]', 'body > div.target'],\n            ),\n            # Simple prefix — div > iframe > div\n            (\n                'div > iframe > div',\n                ['div > iframe', 'div'],\n            ),\n            # Descendant combinator with prefix\n            (\n                'div iframe > body',\n                ['div iframe', 'body'],\n            ),\n            # Child combinator with prefix\n            (\n                'div > iframe > body',\n                ['div > iframe', 'body'],\n            ),\n            # Nested iframes with attributes\n            (\n                'iframe[src*=\"a\"] > div > iframe[id=\"inner\"] > span',\n                ['iframe[src*=\"a\"]', 'div > iframe[id=\"inner\"]', 'span'],\n            ),\n            # Extra spaces around combinator\n            (\n                'iframe  >  body',\n                ['iframe', 'body'],\n            ),\n        ],\n    )\n    def test_splits(self, expression, expected_selectors):\n        segments = SelectorParser.parse_iframe_segments_css(expression)\n        assert len(segments) == len(expected_selectors)\n        for (by, sel), expected in zip(segments, expected_selectors):\n            assert by == By.CSS_SELECTOR\n            assert sel == expected\n\n    @pytest.mark.parametrize(\n        'expression',\n        [\n            '.iframe > body',\n            '#iframe > body',\n            'div.iframe > body',\n            ':not(iframe) > body',\n            ':is(iframe, div) > body',\n            'iframe[src*=\"...\"]',\n            'iframe',\n            'div > span > p',\n            '[data-type=\"iframe\"] > body',\n        ],\n    )\n    def test_no_split(self, expression):\n        segments = SelectorParser.parse_iframe_segments_css(expression)\n        assert len(segments) == 1\n        assert segments[0] == (By.CSS_SELECTOR, expression)\n\n\nclass TestFindAcrossIframes:\n    \"\"\"Test _find_across_iframes and _attempt_find_across_iframes async methods.\"\"\"\n\n    def setup_method(self):\n        self.mixin = MockFindElementsMixin()\n\n    @pytest.mark.asyncio\n    async def test_css_single_iframe_crossing(self):\n        \"\"\"query('iframe > body') — finds iframe, then body inside it.\"\"\"\n        mock_iframe = MagicMock()\n        mock_iframe.is_iframe = True\n        mock_iframe._find_element = AsyncMock(return_value=MagicMock(name='body'))\n\n        self.mixin._find_element = AsyncMock(return_value=mock_iframe)\n\n        result = await self.mixin.find_or_wait_element(\n            By.CSS_SELECTOR, 'iframe > body', timeout=0\n        )\n\n        # First call: find iframe on the page\n        self.mixin._find_element.assert_called_once_with(\n            By.CSS_SELECTOR, 'iframe', raise_exc=False\n        )\n        # Second call: find body inside iframe\n        mock_iframe._find_element.assert_called_once_with(\n            By.CSS_SELECTOR, 'body', raise_exc=False\n        )\n        assert result is not None\n\n    @pytest.mark.asyncio\n    async def test_xpath_single_iframe_crossing(self):\n        \"\"\"query('//iframe/body') — finds iframe, then body inside it.\"\"\"\n        mock_iframe = MagicMock()\n        mock_iframe.is_iframe = True\n        mock_iframe._find_element = AsyncMock(return_value=MagicMock(name='body'))\n\n        self.mixin._find_element = AsyncMock(return_value=mock_iframe)\n\n        result = await self.mixin.find_or_wait_element(\n            By.XPATH, '//iframe/body', timeout=0\n        )\n\n        self.mixin._find_element.assert_called_once_with(\n            By.XPATH, '//iframe', raise_exc=False\n        )\n        mock_iframe._find_element.assert_called_once_with(\n            By.XPATH, '//body', raise_exc=False\n        )\n        assert result is not None\n\n    @pytest.mark.asyncio\n    async def test_nested_iframe_crossing(self):\n        \"\"\"query('iframe > iframe > div') — 3 segments, 3 find calls.\"\"\"\n        mock_inner_iframe = MagicMock()\n        mock_inner_iframe.is_iframe = True\n        mock_div = MagicMock(name='div')\n        mock_inner_iframe._find_element = AsyncMock(return_value=mock_div)\n\n        mock_outer_iframe = MagicMock()\n        mock_outer_iframe.is_iframe = True\n        mock_outer_iframe._find_element = AsyncMock(return_value=mock_inner_iframe)\n\n        self.mixin._find_element = AsyncMock(return_value=mock_outer_iframe)\n\n        result = await self.mixin.find_or_wait_element(\n            By.CSS_SELECTOR, 'iframe > iframe > div', timeout=0\n        )\n\n        self.mixin._find_element.assert_called_once_with(\n            By.CSS_SELECTOR, 'iframe', raise_exc=False\n        )\n        mock_outer_iframe._find_element.assert_called_once_with(\n            By.CSS_SELECTOR, 'iframe', raise_exc=False\n        )\n        mock_inner_iframe._find_element.assert_called_once_with(\n            By.CSS_SELECTOR, 'div', raise_exc=False\n        )\n        assert result is mock_div\n\n    @pytest.mark.asyncio\n    async def test_xpath_iframe_not_at_root(self):\n        \"\"\"query('//div/iframe/div') — iframe is a child of div, not root.\"\"\"\n        mock_iframe = MagicMock()\n        mock_iframe.is_iframe = True\n        mock_div = MagicMock(name='inner_div')\n        mock_iframe._find_element = AsyncMock(return_value=mock_div)\n\n        self.mixin._find_element = AsyncMock(return_value=mock_iframe)\n\n        result = await self.mixin.find_or_wait_element(\n            By.XPATH, '//div/iframe/div', timeout=0\n        )\n\n        # First segment: //div/iframe (finds the iframe inside a div)\n        self.mixin._find_element.assert_called_once_with(\n            By.XPATH, '//div/iframe', raise_exc=False\n        )\n        # Second segment: //div (finds div inside the iframe)\n        mock_iframe._find_element.assert_called_once_with(\n            By.XPATH, '//div', raise_exc=False\n        )\n        assert result is mock_div\n\n    @pytest.mark.asyncio\n    async def test_css_iframe_not_at_root(self):\n        \"\"\"query('div > iframe > div') — iframe is a child of div, not root.\"\"\"\n        mock_iframe = MagicMock()\n        mock_iframe.is_iframe = True\n        mock_div = MagicMock(name='inner_div')\n        mock_iframe._find_element = AsyncMock(return_value=mock_div)\n\n        self.mixin._find_element = AsyncMock(return_value=mock_iframe)\n\n        result = await self.mixin.find_or_wait_element(\n            By.CSS_SELECTOR, 'div > iframe > div', timeout=0\n        )\n\n        # First segment: div > iframe (finds the iframe inside a div)\n        self.mixin._find_element.assert_called_once_with(\n            By.CSS_SELECTOR, 'div > iframe', raise_exc=False\n        )\n        # Second segment: div (finds div inside the iframe)\n        mock_iframe._find_element.assert_called_once_with(\n            By.CSS_SELECTOR, 'div', raise_exc=False\n        )\n        assert result is mock_div\n\n    @pytest.mark.asyncio\n    async def test_find_all_last_segment(self):\n        \"\"\"query('iframe > .item', find_all=True) — _find_elements for last segment.\"\"\"\n        mock_iframe = MagicMock()\n        mock_iframe.is_iframe = True\n        mock_items = [MagicMock(), MagicMock()]\n        mock_iframe._find_elements = AsyncMock(return_value=mock_items)\n\n        self.mixin._find_element = AsyncMock(return_value=mock_iframe)\n\n        result = await self.mixin.find_or_wait_element(\n            By.CSS_SELECTOR, 'iframe > .item', timeout=0, find_all=True\n        )\n\n        self.mixin._find_element.assert_called_once_with(\n            By.CSS_SELECTOR, 'iframe', raise_exc=False\n        )\n        mock_iframe._find_elements.assert_called_once_with(\n            By.CSS_SELECTOR, '.item', raise_exc=False\n        )\n        assert result == mock_items\n\n    @pytest.mark.asyncio\n    async def test_timeout_retry_succeeds(self):\n        \"\"\"First attempt fails (iframe not found), second succeeds.\"\"\"\n        mock_iframe = MagicMock()\n        mock_iframe.is_iframe = True\n        mock_body = MagicMock(name='body')\n        mock_iframe._find_element = AsyncMock(return_value=mock_body)\n\n        # First call: None (not found), second call: found\n        self.mixin._find_element = AsyncMock(side_effect=[None, mock_iframe])\n\n        with patch('asyncio.sleep') as mock_sleep, \\\n             patch('asyncio.get_event_loop') as mock_loop:\n            mock_loop.return_value.time.side_effect = [0, 0.5, 1.0]\n\n            result = await self.mixin.find_or_wait_element(\n                By.CSS_SELECTOR, 'iframe > body', timeout=5\n            )\n\n        assert result is mock_body\n        mock_sleep.assert_called_once_with(0.5)\n\n    @pytest.mark.asyncio\n    async def test_timeout_expires_raises(self):\n        \"\"\"All attempts fail — WaitElementTimeout raised.\"\"\"\n        self.mixin._find_element = AsyncMock(return_value=None)\n\n        with patch('asyncio.sleep') as mock_sleep, \\\n             patch('asyncio.get_event_loop') as mock_loop:\n            mock_loop.return_value.time.side_effect = [0, 0.5, 1.0, 1.5, 2.1]\n\n            with pytest.raises(WaitElementTimeout, match='across iframes'):\n                await self.mixin.find_or_wait_element(\n                    By.CSS_SELECTOR, 'iframe > body', timeout=2\n                )\n\n    @pytest.mark.asyncio\n    async def test_no_timeout_raises_element_not_found(self):\n        \"\"\"timeout=0, iframe not found — ElementNotFound.\"\"\"\n        self.mixin._find_element = AsyncMock(return_value=None)\n\n        with pytest.raises(ElementNotFound, match='across iframes'):\n            await self.mixin.find_or_wait_element(\n                By.CSS_SELECTOR, 'iframe > body', timeout=0\n            )\n\n    @pytest.mark.asyncio\n    async def test_raise_exc_false_returns_none(self):\n        \"\"\"raise_exc=False, not found — returns None.\"\"\"\n        self.mixin._find_element = AsyncMock(return_value=None)\n\n        result = await self.mixin.find_or_wait_element(\n            By.CSS_SELECTOR, 'iframe > body', timeout=0, raise_exc=False\n        )\n        assert result is None\n\n    @pytest.mark.asyncio\n    async def test_raise_exc_false_find_all_returns_empty(self):\n        \"\"\"raise_exc=False, find_all=True — returns [].\"\"\"\n        self.mixin._find_element = AsyncMock(return_value=None)\n\n        result = await self.mixin.find_or_wait_element(\n            By.CSS_SELECTOR, 'iframe > body', timeout=0, find_all=True, raise_exc=False\n        )\n        assert result == []\n\n    @pytest.mark.asyncio\n    async def test_intermediate_not_iframe_returns_none_and_raises(self):\n        \"\"\"Element found but is_iframe=False — treated as not found.\"\"\"\n        mock_element = MagicMock()\n        mock_element.is_iframe = False\n\n        self.mixin._find_element = AsyncMock(return_value=mock_element)\n\n        with pytest.raises(ElementNotFound):\n            await self.mixin.find_or_wait_element(\n                By.CSS_SELECTOR, 'iframe > body', timeout=0\n            )\n\n    @pytest.mark.asyncio\n    async def test_regular_selector_no_iframe_passthrough(self):\n        \"\"\"'div > span' — parser returns 1 segment, uses normal path.\"\"\"\n        mock_element = MagicMock()\n        self.mixin._find_element = AsyncMock(return_value=mock_element)\n\n        result = await self.mixin.find_or_wait_element(\n            By.CSS_SELECTOR, 'div > span', timeout=0\n        )\n\n        # Should call _find_element directly with original selector\n        self.mixin._find_element.assert_called_once_with(\n            By.CSS_SELECTOR, 'div > span', raise_exc=True\n        )\n"
  },
  {
    "path": "tests/test_har_recording_integration.py",
    "content": "\"\"\"Integration tests for HAR recording feature.\n\nThese tests open a real browser, serve a test page with JS-initiated\nfetch requests via a local HTTP server, and verify the recorded HAR entries.\n\"\"\"\n\nimport asyncio\nimport json\nimport socket\nimport threading\nfrom http.server import HTTPServer, BaseHTTPRequestHandler\nfrom pathlib import Path\n\nimport pytest\n\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.browser.requests.har_recorder import HarCapture\n\n\ndef _find_free_port():\n    \"\"\"Find a free port on localhost.\"\"\"\n    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n        s.bind(('127.0.0.1', 0))\n        return s.getsockname()[1]\n\n\nclass _TestAPIHandler(BaseHTTPRequestHandler):\n    \"\"\"Deterministic HTTP handler for HAR integration tests.\"\"\"\n\n    def do_GET(self):\n        if self.path == '/api/users':\n            self._respond(\n                200,\n                'application/json',\n                json.dumps([{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]),\n            )\n        elif self.path == '/api/data':\n            self._respond(200, 'text/plain', 'Hello from the test server')\n        else:\n            self._respond(404, 'text/plain', 'Not Found')\n\n    def do_POST(self):\n        if self.path == '/api/submit':\n            content_length = int(self.headers.get('Content-Length', 0))\n            body = self.rfile.read(content_length)\n            self._respond(\n                201,\n                'application/json',\n                json.dumps({\n                    'status': 'created',\n                    'received': json.loads(body.decode()) if body else None,\n                }),\n            )\n        else:\n            self._respond(404, 'text/plain', 'Not Found')\n\n    def _respond(self, status, content_type, body):\n        self.send_response(status)\n        self.send_header('Content-Type', content_type)\n        self.send_header('Access-Control-Allow-Origin', '*')\n        self.send_header('Access-Control-Allow-Headers', 'Content-Type')\n        self.end_headers()\n        self.wfile.write(body.encode())\n\n    def do_OPTIONS(self):\n        self.send_response(200)\n        self.send_header('Access-Control-Allow-Origin', '*')\n        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')\n        self.send_header('Access-Control-Allow-Headers', 'Content-Type')\n        self.end_headers()\n\n    def log_message(self, format, *args):\n        pass\n\n\n@pytest.fixture(scope='module')\ndef api_server():\n    \"\"\"Start a local HTTP server for the test module.\"\"\"\n    port = _find_free_port()\n    server = HTTPServer(('127.0.0.1', port), _TestAPIHandler)\n    thread = threading.Thread(target=server.serve_forever, daemon=True)\n    thread.start()\n    yield f'http://127.0.0.1:{port}'\n    server.shutdown()\n    server.server_close()\n    thread.join(timeout=5)\n\n\n@pytest.fixture(scope='module')\ndef test_page_path():\n    \"\"\"Path to the HAR recording test HTML page.\"\"\"\n    return Path(__file__).parent / 'pages' / 'test_har_recording.html'\n\n\nasync def _wait_for_requests_done(tab, timeout=15):\n    \"\"\"Poll the page until status shows 'done'.\"\"\"\n    for _ in range(int(timeout / 0.5)):\n        await asyncio.sleep(0.5)\n        status_el = await tab.find(id='status')\n        text = await status_el.text\n        if text == 'done':\n            return True\n    return False\n\n\nclass TestHarRecordIntegration:\n    \"\"\"Integration tests for tab.request.record().\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_record_captures_page_load(self, ci_chrome_options, api_server, test_page_path):\n        \"\"\"Recording captures the document load event.\"\"\"\n        page_url = f'file://{test_page_path.absolute()}?base={api_server}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n\n            async with tab.request.record() as recording:\n                await tab.go_to(page_url)\n                assert await _wait_for_requests_done(tab), 'Page requests did not complete'\n                await asyncio.sleep(1)\n\n            assert isinstance(recording, HarCapture)\n            entries = recording.entries\n            assert len(entries) >= 1\n\n            # First entry should be the document load\n            doc_entries = [\n                e for e in entries if e['request']['url'].startswith('file://')\n            ]\n            assert len(doc_entries) >= 1\n            assert doc_entries[0]['response']['status'] == 200\n            assert doc_entries[0]['response']['content']['mimeType'] == 'text/html'\n\n    @pytest.mark.asyncio\n    async def test_record_captures_fetch_requests(\n        self, ci_chrome_options, api_server, test_page_path\n    ):\n        \"\"\"Recording captures JS fetch() requests with correct URLs and methods.\"\"\"\n        page_url = f'file://{test_page_path.absolute()}?base={api_server}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n\n            async with tab.request.record() as recording:\n                await tab.go_to(page_url)\n                assert await _wait_for_requests_done(tab), 'Page requests did not complete'\n                await asyncio.sleep(1)\n\n            entries = recording.entries\n            api_entries = [e for e in entries if '/api/' in e['request']['url']]\n            # 3 API requests + possible OPTIONS preflight for POST\n            assert len(api_entries) >= 3\n\n            urls = [e['request']['url'] for e in api_entries]\n            assert any('/api/users' in u for u in urls)\n            assert any('/api/data' in u for u in urls)\n            assert any('/api/submit' in u for u in urls)\n\n    @pytest.mark.asyncio\n    async def test_record_captures_response_bodies(\n        self, ci_chrome_options, api_server, test_page_path\n    ):\n        \"\"\"Recording captures response bodies for each request.\"\"\"\n        page_url = f'file://{test_page_path.absolute()}?base={api_server}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n\n            async with tab.request.record() as recording:\n                await tab.go_to(page_url)\n                assert await _wait_for_requests_done(tab), 'Page requests did not complete'\n                await asyncio.sleep(1)\n\n            entries = recording.entries\n            users_entry = next(\n                (e for e in entries if '/api/users' in e['request']['url']), None\n            )\n            assert users_entry is not None\n            body_text = users_entry['response']['content'].get('text', '')\n            assert 'Alice' in body_text\n            assert 'Bob' in body_text\n\n            data_entry = next(\n                (e for e in entries if '/api/data' in e['request']['url']), None\n            )\n            assert data_entry is not None\n            assert 'Hello from the test server' in data_entry['response']['content'].get('text', '')\n\n    @pytest.mark.asyncio\n    async def test_record_captures_post_request(\n        self, ci_chrome_options, api_server, test_page_path\n    ):\n        \"\"\"Recording captures POST requests with body data.\"\"\"\n        page_url = f'file://{test_page_path.absolute()}?base={api_server}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n\n            async with tab.request.record() as recording:\n                await tab.go_to(page_url)\n                assert await _wait_for_requests_done(tab), 'Page requests did not complete'\n                await asyncio.sleep(1)\n\n            entries = recording.entries\n            post_entry = next(\n                (\n                    e\n                    for e in entries\n                    if '/api/submit' in e['request']['url']\n                    and e['request']['method'] == 'POST'\n                ),\n                None,\n            )\n            assert post_entry is not None\n            assert post_entry['response']['status'] == 201\n\n            # POST body should be captured\n            post_data = post_entry['request'].get('postData')\n            assert post_data is not None\n            assert '\"key\"' in post_data['text']\n\n            # Response body should contain what the server echoed back\n            resp_text = post_entry['response']['content'].get('text', '')\n            assert 'created' in resp_text\n\n    @pytest.mark.asyncio\n    async def test_record_correct_status_codes(\n        self, ci_chrome_options, api_server, test_page_path\n    ):\n        \"\"\"Recording captures correct HTTP status codes.\"\"\"\n        page_url = f'file://{test_page_path.absolute()}?base={api_server}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n\n            async with tab.request.record() as recording:\n                await tab.go_to(page_url)\n                assert await _wait_for_requests_done(tab), 'Page requests did not complete'\n                await asyncio.sleep(1)\n\n            entries = recording.entries\n            users_entry = next(\n                (e for e in entries if '/api/users' in e['request']['url']), None\n            )\n            assert users_entry is not None\n            assert users_entry['response']['status'] == 200\n\n            submit_entry = next(\n                (\n                    e\n                    for e in entries\n                    if '/api/submit' in e['request']['url']\n                    and e['request']['method'] == 'POST'\n                ),\n                None,\n            )\n            assert submit_entry is not None\n            assert submit_entry['response']['status'] == 201\n\n    @pytest.mark.asyncio\n    async def test_record_body_sizes(self, ci_chrome_options, api_server, test_page_path):\n        \"\"\"Recording reports correct body sizes from dataReceived events.\"\"\"\n        page_url = f'file://{test_page_path.absolute()}?base={api_server}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n\n            async with tab.request.record() as recording:\n                await tab.go_to(page_url)\n                assert await _wait_for_requests_done(tab), 'Page requests did not complete'\n                await asyncio.sleep(1)\n\n            entries = recording.entries\n            users_entry = next(\n                (e for e in entries if '/api/users' in e['request']['url']), None\n            )\n            assert users_entry is not None\n            # bodySize should be > 0 for successful requests with body\n            assert users_entry['response']['bodySize'] > 0\n            # content.size should match the decoded body length\n            assert users_entry['response']['content']['size'] > 0\n\n\nclass TestHarSaveIntegration:\n    \"\"\"Integration tests for saving and loading HAR files.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_save_produces_valid_har(\n        self, ci_chrome_options, api_server, test_page_path, tmp_path\n    ):\n        \"\"\"Saved HAR file is valid JSON with HAR 1.2 structure.\"\"\"\n        page_url = f'file://{test_page_path.absolute()}?base={api_server}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n\n            async with tab.request.record() as recording:\n                await tab.go_to(page_url)\n                assert await _wait_for_requests_done(tab), 'Page requests did not complete'\n                await asyncio.sleep(1)\n\n            har_path = tmp_path / 'test_output.har'\n            recording.save(har_path)\n\n            assert har_path.exists()\n            with open(har_path, encoding='utf-8') as f:\n                har = json.load(f)\n\n            assert har['log']['version'] == '1.2'\n            assert har['log']['creator']['name'] == 'pydoll'\n            assert isinstance(har['log']['pages'], list)\n            assert isinstance(har['log']['entries'], list)\n            assert len(har['log']['entries']) >= 4\n\n    @pytest.mark.asyncio\n    async def test_save_entries_sorted_by_time(\n        self, ci_chrome_options, api_server, test_page_path, tmp_path\n    ):\n        \"\"\"Saved entries are sorted by startedDateTime.\"\"\"\n        page_url = f'file://{test_page_path.absolute()}?base={api_server}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n\n            async with tab.request.record() as recording:\n                await tab.go_to(page_url)\n                assert await _wait_for_requests_done(tab), 'Page requests did not complete'\n                await asyncio.sleep(1)\n\n            har_path = tmp_path / 'test_sorted.har'\n            recording.save(har_path)\n\n            with open(har_path, encoding='utf-8') as f:\n                har = json.load(f)\n\n            dates = [e['startedDateTime'] for e in har['log']['entries']]\n            assert dates == sorted(dates)\n\n    @pytest.mark.asyncio\n    async def test_save_entries_have_required_fields(\n        self, ci_chrome_options, api_server, test_page_path, tmp_path\n    ):\n        \"\"\"Every entry has required HAR 1.2 fields.\"\"\"\n        page_url = f'file://{test_page_path.absolute()}?base={api_server}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n\n            async with tab.request.record() as recording:\n                await tab.go_to(page_url)\n                assert await _wait_for_requests_done(tab), 'Page requests did not complete'\n                await asyncio.sleep(1)\n\n            har_path = tmp_path / 'test_fields.har'\n            recording.save(har_path)\n\n            with open(har_path, encoding='utf-8') as f:\n                har = json.load(f)\n\n            for entry in har['log']['entries']:\n                # Required entry fields\n                assert 'startedDateTime' in entry\n                assert 'time' in entry\n                assert 'request' in entry\n                assert 'response' in entry\n                assert 'cache' in entry\n                assert 'timings' in entry\n\n                # Required request fields\n                req = entry['request']\n                assert 'method' in req\n                assert 'url' in req\n                assert 'httpVersion' in req\n                assert 'cookies' in req\n                assert 'headers' in req\n                assert 'queryString' in req\n                assert 'headersSize' in req\n                assert 'bodySize' in req\n\n                # Required response fields\n                resp = entry['response']\n                assert 'status' in resp\n                assert 'statusText' in resp\n                assert 'httpVersion' in resp\n                assert 'cookies' in resp\n                assert 'headers' in resp\n                assert 'content' in resp\n                assert 'redirectURL' in resp\n                assert 'headersSize' in resp\n                assert 'bodySize' in resp\n\n                # Required timings fields\n                timings = entry['timings']\n                for field in ('blocked', 'dns', 'connect', 'ssl', 'send', 'wait', 'receive'):\n                    assert field in timings\n"
  },
  {
    "path": "tests/test_iframe_integration.py",
    "content": "\"\"\"Integration tests for iframe functionality in WebElement.\n\nThese tests use real HTML files and Chrome browser to test iframe interactions,\nelement finding, and DOM manipulation within iframes.\n\"\"\"\n\nimport asyncio\nfrom pathlib import Path\n\nimport pytest\n\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.elements.web_element import WebElement\nfrom pydoll.exceptions import ElementNotFound, InvalidIFrame\n\n\nclass TestSimpleIframeIntegration:\n    \"\"\"Integration tests for simple iframe operations.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_find_element_in_iframe_by_id(self, ci_chrome_options):\n        \"\"\"Test finding an element inside an iframe by id.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n\n            # Wait for iframe to load\n            await asyncio.sleep(1)\n\n            # Find the iframe element\n            iframe_element = await tab.find(id='simple-iframe')\n            assert iframe_element is not None\n            assert iframe_element.is_iframe\n\n            # Get iframe context\n            iframe_context = await iframe_element.iframe_context\n            assert iframe_context is not None\n            assert iframe_context.frame_id is not None\n            assert iframe_context.execution_context_id is not None\n\n            # Find element inside iframe\n            heading_in_iframe = await iframe_element.find(id='iframe-heading')\n            assert heading_in_iframe is not None\n            assert isinstance(heading_in_iframe, WebElement)\n\n            # Verify the element text\n            text = await heading_in_iframe.text\n            assert 'Iframe Content' in text\n\n    @pytest.mark.asyncio\n    async def test_find_multiple_elements_in_iframe(self, ci_chrome_options):\n        \"\"\"Test finding multiple elements inside an iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find all links inside iframe\n            links = await iframe_element.query('.iframe-link', find_all=True)\n            assert len(links) == 3\n\n            # Verify each link\n            for i, link in enumerate(links, 1):\n                link_id = link.get_attribute('id')\n                assert link_id == f'iframe-link{i}'\n\n    @pytest.mark.asyncio\n    async def test_find_element_in_iframe_by_css_selector(self, ci_chrome_options):\n        \"\"\"Test finding elements in iframe using CSS selectors.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find by class\n            action_buttons = await iframe_element.query('.action-btn', find_all=True)\n            assert len(action_buttons) >= 2  # At least 2 visible buttons\n\n            # Find by tag\n            inputs = await iframe_element.query('input[type=\"text\"]', find_all=True)\n            assert len(inputs) >= 1\n\n    @pytest.mark.asyncio\n    async def test_find_element_in_iframe_by_xpath(self, ci_chrome_options):\n        \"\"\"Test finding elements in iframe using XPath.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find by XPath\n            paragraph = await iframe_element.find(xpath='//p[@id=\"iframe-paragraph\"]')\n            assert paragraph is not None\n\n            text = await paragraph.text\n            assert 'content inside the iframe' in text\n\n    @pytest.mark.asyncio\n    async def test_insert_text_in_iframe_input(self, ci_chrome_options):\n        \"\"\"Test inserting text into an input field inside an iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find input inside iframe\n            input_element = await iframe_element.find(id='iframe-input')\n            assert input_element is not None\n\n            # Insert text\n            test_text = 'Test User Name'\n            await input_element.insert_text(test_text)\n\n            # Verify text was inserted\n            value = input_element.get_attribute('value')\n            assert test_text in value\n\n    @pytest.mark.asyncio\n    async def test_insert_text_in_iframe_textarea(self, ci_chrome_options):\n        \"\"\"Test inserting text into a textarea inside an iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find textarea inside iframe\n            textarea = await iframe_element.find(id='iframe-textarea')\n            assert textarea is not None\n\n            # Insert new text (textarea initially empty)\n            new_message = 'This is a new test message'\n            await textarea.insert_text(new_message)\n\n            # Verify text was inserted\n            value = textarea.get_attribute('value')\n            assert new_message in value\n\n    @pytest.mark.asyncio\n    async def test_click_button_in_iframe(self, ci_chrome_options):\n        \"\"\"Test clicking a button inside an iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find button inside iframe\n            button = await iframe_element.find(id='iframe-button1')\n            assert button is not None\n\n            # Click the button (should not raise exception)\n            await button.click()\n            await asyncio.sleep(0.5)\n\n    @pytest.mark.asyncio\n    async def test_get_inner_html_of_iframe(self, ci_chrome_options):\n        \"\"\"Test getting inner HTML of an iframe element.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Get inner HTML of the iframe\n            inner_html = await iframe_element.inner_html\n            assert inner_html is not None\n            assert len(inner_html) > 0\n            assert 'iframe-heading' in inner_html\n            assert 'Iframe Content' in inner_html\n\n    @pytest.mark.asyncio\n    async def test_get_inner_html_of_element_in_iframe(self, ci_chrome_options):\n        \"\"\"Test getting inner HTML of an element inside an iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find container inside iframe\n            container = await iframe_element.find(id='iframe-container')\n            assert container is not None\n\n            # Get inner HTML\n            inner_html = await container.inner_html\n            assert inner_html is not None\n            assert 'iframe-paragraph' in inner_html\n            assert 'iframe-form' in inner_html\n\n    @pytest.mark.asyncio\n    async def test_get_children_elements_in_iframe(self, ci_chrome_options):\n        \"\"\"Test getting children elements of an element inside an iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find list inside iframe\n            list_element = await iframe_element.find(id='iframe-list')\n            assert list_element is not None\n\n            # Get list items using tag filter to avoid relying on class attributes\n            list_items = await list_element.get_children_elements(max_depth=2, tag_filter=['li'])\n            assert len(list_items) == 3\n\n    @pytest.mark.asyncio\n    async def test_element_visibility_in_iframe(self, ci_chrome_options):\n        \"\"\"Test checking element visibility inside an iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find visible button\n            visible_button = await iframe_element.find(id='iframe-button1')\n            is_visible = await visible_button.is_visible()\n            assert is_visible is True\n\n            # Find hidden button\n            hidden_button = await iframe_element.find(id='iframe-button3')\n            is_hidden = await hidden_button.is_visible()\n            assert is_hidden is False\n\n\nclass TestNestedIframeIntegration:\n    \"\"\"Integration tests for nested iframe operations.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_find_element_in_parent_iframe(self, ci_chrome_options):\n        \"\"\"Test finding an element in parent iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_nested.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1.5)\n\n            # Find parent iframe\n            parent_iframe = await tab.find(id='parent-iframe')\n            assert parent_iframe is not None\n            assert parent_iframe.is_iframe\n\n            # Find element in parent iframe\n            parent_heading = await parent_iframe.find(id='parent-iframe-heading')\n            assert parent_heading is not None\n\n            text = await parent_heading.text\n            assert 'Parent Iframe Content' in text\n\n    @pytest.mark.asyncio\n    async def test_find_nested_iframe_element(self, ci_chrome_options):\n        \"\"\"Test finding the nested iframe element inside parent iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_nested.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1.5)\n\n            # Find parent iframe\n            parent_iframe = await tab.find(id='parent-iframe')\n\n            # Find nested iframe inside parent iframe\n            nested_iframe = await parent_iframe.find(id='nested-iframe')\n            assert nested_iframe is not None\n            assert nested_iframe.is_iframe\n\n    @pytest.mark.asyncio\n    async def test_find_element_in_nested_iframe(self, ci_chrome_options):\n        \"\"\"Test finding an element in nested iframe (iframe within iframe).\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_nested.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(2)\n\n            # Find parent iframe\n            parent_iframe = await tab.find(id='parent-iframe')\n\n            # Find nested iframe inside parent\n            nested_iframe = await parent_iframe.find(id='nested-iframe')\n            assert nested_iframe is not None\n\n            # Find element in nested iframe\n            nested_heading = await nested_iframe.find(id='nested-iframe-heading')\n            assert nested_heading is not None\n\n            text = await nested_heading.text\n            assert 'Nested Iframe Content' in text\n\n    @pytest.mark.asyncio\n    async def test_insert_text_in_nested_iframe(self, ci_chrome_options):\n        \"\"\"Test inserting text into input field in nested iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_nested.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(2)\n\n            # Navigate to nested iframe\n            parent_iframe = await tab.find(id='parent-iframe')\n            nested_iframe = await parent_iframe.find(id='nested-iframe')\n\n            # Find input in nested iframe\n            nested_input = await nested_iframe.find(id='nested-input')\n            assert nested_input is not None\n\n            # Insert text\n            test_text = 'Nested Input Test'\n            await nested_input.insert_text(test_text)\n\n            # Verify\n            value = nested_input.get_attribute('value')\n            assert test_text in value\n\n    @pytest.mark.asyncio\n    async def test_find_multiple_elements_in_nested_iframe(self, ci_chrome_options):\n        \"\"\"Test finding multiple elements in nested iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_nested.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(2)\n\n            # Navigate to nested iframe\n            parent_iframe = await tab.find(id='parent-iframe')\n            nested_iframe = await parent_iframe.find(id='nested-iframe')\n\n            # Find all links in nested iframe\n            links = await nested_iframe.query('a', find_all=True)\n            assert len(links) == 2\n\n            # Verify link IDs\n            link_ids = [link.get_attribute('id') for link in links]\n            assert 'nested-link1' in link_ids\n            assert 'nested-link2' in link_ids\n\n    @pytest.mark.asyncio\n    async def test_submit_form_in_nested_iframe(self, ci_chrome_options):\n        \"\"\"Test interacting with form elements in nested iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_nested.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(2)\n\n            # Navigate to nested iframe\n            parent_iframe = await tab.find(id='parent-iframe')\n            nested_iframe = await parent_iframe.find(id='nested-iframe')\n\n            # Fill form fields\n            username_input = await nested_iframe.find(id='nested-form-input')\n            await username_input.insert_text('testuser')\n\n            password_input = await nested_iframe.find(id='nested-form-password')\n            await password_input.insert_text('password123')\n\n            # Verify values\n            assert 'testuser' in username_input.get_attribute('value')\n            assert 'password123' in password_input.get_attribute('value')\n\n            # Click submit button\n            submit_button = await nested_iframe.find(id='nested-form-submit')\n            await submit_button.click()\n            await asyncio.sleep(0.5)\n\n\nclass TestIframeElementInteraction:\n    \"\"\"Integration tests for various element interactions within iframes.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_select_option_in_iframe(self, ci_chrome_options):\n        \"\"\"Test selecting an option in a select element inside iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find select element\n            select_element = await iframe_element.find(id='iframe-select')\n            assert select_element is not None\n\n            # Select option2 by clicking the option element\n            option2 = await select_element.find(xpath='.//option[@value=\"option2\"]')\n            await option2.click()\n            await asyncio.sleep(0.2)\n            # Verify via property read (execute_script)\n            prop_val = await select_element.execute_script('return this.value', return_by_value=True)\n            current_value = prop_val['result']['result']['value']\n            assert current_value == 'option2'\n\n            # Select different option (option3) by clicking it\n            option3 = await select_element.find(xpath='.//option[@value=\"option3\"]')\n            await option3.click()\n            await asyncio.sleep(0.2)\n            prop_val2 = await select_element.execute_script('return this.value', return_by_value=True)\n            new_value = prop_val2['result']['result']['value']\n            assert new_value == 'option3'\n\n    @pytest.mark.asyncio\n    async def test_get_attributes_from_iframe_elements(self, ci_chrome_options):\n        \"\"\"Test getting various attributes from elements in iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Get link attributes\n            link = await iframe_element.find(id='iframe-link1')\n            href = link.get_attribute('href')\n            assert href is not None\n            assert '#link1' in href\n\n            link_class = link.get_attribute('class')\n            assert 'iframe-link' in link_class\n\n            # Get input attributes\n            input_elem = await iframe_element.find(id='iframe-input')\n            input_type = input_elem.get_attribute('type')\n            assert input_type == 'text'\n\n            placeholder = input_elem.get_attribute('placeholder')\n            assert 'name' in placeholder.lower()\n\n    @pytest.mark.asyncio\n    async def test_deep_nested_element_search_in_iframe(self, ci_chrome_options):\n        \"\"\"Test finding deeply nested elements inside iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find deeply nested element\n            deep_span = await iframe_element.find(id='deep-span')\n            assert deep_span is not None\n\n            text = await deep_span.text\n            assert 'Deep nested element' in text\n\n\n    @pytest.mark.asyncio\n    async def test_wait_for_element_in_iframe(self, ci_chrome_options):\n        \"\"\"Test waiting for element to appear in iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Wait for element (should already exist)\n            element = await iframe_element.find(\n                id='iframe-paragraph', timeout=5\n            )\n            assert element is not None\n\n            text = await element.text\n            assert 'content inside the iframe' in text\n\n    @pytest.mark.asyncio\n    async def test_element_not_found_in_iframe(self, ci_chrome_options):\n        \"\"\"Test that ElementNotFound is raised for non-existent elements in iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Try to find non-existent element\n            with pytest.raises(ElementNotFound):\n                await iframe_element.find(id='non-existent-element')\n\n    @pytest.mark.asyncio\n    async def test_clear_input_in_iframe(self, ci_chrome_options):\n        \"\"\"Test clearing input field in iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Find input and add text\n            input_elem = await iframe_element.find(id='iframe-input')\n            await input_elem.insert_text('Test text to clear')\n            await asyncio.sleep(0.3)\n\n            await input_elem.insert_text('')\n            await asyncio.sleep(0.3)\n            value = input_elem.get_attribute('value')\n            assert value in ('', None)\n\n    @pytest.mark.asyncio\n    async def test_multiple_iframes_on_same_page(self, ci_chrome_options):\n        \"\"\"Test handling multiple iframes on the same page.\"\"\"\n        # Create a test page with multiple iframes\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            # Find main content (not in iframe)\n            main_heading = await tab.find(id='main-heading')\n            assert main_heading is not None\n            main_text = await main_heading.text\n            assert 'Main Page' in main_text\n\n            # Find content in iframe\n            iframe_element = await tab.find(id='simple-iframe')\n            iframe_heading = await iframe_element.find(id='iframe-heading')\n            iframe_text = await iframe_heading.text\n            assert 'Iframe Content' in iframe_text\n\n            # Verify they are different\n            assert main_text != iframe_text\n\n    @pytest.mark.asyncio\n    async def test_iframe_context_persistence(self, ci_chrome_options):\n        \"\"\"Test that iframe context persists across multiple operations.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Get context first time\n            context1 = await iframe_element.iframe_context\n            assert context1 is not None\n\n            # Perform some operations\n            element1 = await iframe_element.find(id='iframe-heading')\n            await element1.text\n\n            # Get context again\n            context2 = await iframe_element.iframe_context\n            assert context2 is not None\n\n            # Verify contexts are consistent\n            assert context1.frame_id == context2.frame_id\n            assert context1.execution_context_id == context2.execution_context_id\n\n    @pytest.mark.asyncio\n    async def test_get_text_from_multiple_elements_in_iframe(self, ci_chrome_options):\n        \"\"\"Test getting text from multiple elements in iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Get all list items\n            list_items = await iframe_element.query('.list-item', find_all=True)\n            assert len(list_items) == 3\n\n            # Get text from each\n            texts = []\n            for item in list_items:\n                text = await item.text\n                texts.append(text)\n\n            # Verify texts\n            assert 'Item 1' in texts[0]\n            assert 'Item 2' in texts[1]\n            assert 'Item 3' in texts[2]\n\n\nclass TestMultipleIframesSelection:\n    \"\"\"Integration tests for selecting the correct iframe when multiple iframes exist.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_find_specific_iframe_by_id_among_multiple(self, ci_chrome_options):\n        \"\"\"Test finding a specific iframe by ID when multiple iframes exist on the page.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_multiple_iframes.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            # Find all iframes\n            all_iframes = await tab.find(tag_name='iframe', find_all=True)\n            assert len(all_iframes) == 3, \"Should have 3 iframes on the page\"\n\n            # Find specific iframe by ID\n            login_iframe = await tab.find(id='login-iframe')\n            assert login_iframe is not None\n            assert login_iframe.is_iframe\n            assert login_iframe.get_attribute('id') == 'login-iframe'\n\n            # Verify we can access content in the correct iframe\n            iframe_context = await login_iframe.iframe_context\n            assert iframe_context is not None\n            assert iframe_context.frame_id is not None\n\n    @pytest.mark.asyncio\n    async def test_find_elements_in_correct_iframe_among_multiple(self, ci_chrome_options):\n        \"\"\"Test that elements are found in the correct iframe when multiple exist.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_multiple_iframes.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            # Get the login iframe specifically\n            login_iframe = await tab.find(id='login-iframe')\n            \n            # Find elements inside the login iframe\n            heading = await login_iframe.find(id='iframe-heading', timeout=5)\n            assert heading is not None\n            \n            text = await heading.text\n            assert 'Iframe Content' in text\n\n            # Verify we can find multiple elements\n            buttons = await login_iframe.find(class_name='action-btn', find_all=True)\n            assert len(buttons) >= 2\n\n    @pytest.mark.asyncio\n    async def test_different_iframes_have_different_contexts(self, ci_chrome_options):\n        \"\"\"Test that different iframes have distinct frame contexts even with same content.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_multiple_iframes.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            # Get two different iframes\n            cookie_iframe = await tab.find(id='cookie-iframe')\n            login_iframe = await tab.find(id='login-iframe')\n\n            # Both should be iframes\n            assert cookie_iframe.is_iframe\n            assert login_iframe.is_iframe\n\n            # Get their contexts\n            cookie_ctx = await cookie_iframe.iframe_context\n            login_ctx = await login_iframe.iframe_context\n\n            # Frame IDs should be different (distinct iframe contexts)\n            assert cookie_ctx.frame_id != login_ctx.frame_id\n\n            # Both should be able to find elements in their respective content\n            cookie_heading = await cookie_iframe.find(id='iframe-heading')\n            login_heading = await login_iframe.find(id='iframe-heading')\n            \n            assert cookie_heading is not None\n            assert login_heading is not None\n            \n            # The element object IDs should be different (different DOM instances)\n            assert cookie_heading._object_id != login_heading._object_id\n\n    @pytest.mark.asyncio\n    async def test_iframe_selection_by_data_attribute(self, ci_chrome_options):\n        \"\"\"Test selecting iframe by custom data attribute.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_multiple_iframes.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            # Find iframe by data-purpose attribute using xpath\n            login_iframe = await tab.find(xpath='//iframe[@data-purpose=\"login\"]')\n            assert login_iframe is not None\n            assert login_iframe.get_attribute('id') == 'login-iframe'\n\n            # Verify we can interact with it\n            form = await login_iframe.find(id='iframe-form')\n            assert form is not None\n\n    @pytest.mark.asyncio\n    async def test_iterate_over_multiple_iframes(self, ci_chrome_options):\n        \"\"\"Test iterating over multiple iframes and accessing each one's content.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_multiple_iframes.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            # Find all iframes\n            all_iframes = await tab.find(tag_name='iframe', find_all=True)\n            assert len(all_iframes) == 3\n\n            # Each iframe should have accessible content\n            for iframe in all_iframes:\n                assert iframe.is_iframe\n                \n                # Get context for each iframe\n                ctx = await iframe.iframe_context\n                assert ctx is not None\n                assert ctx.frame_id is not None\n                \n                # Each should have an iframe-heading\n                heading = await iframe.find(id='iframe-heading', raise_exc=False)\n                # At least the content iframes should have the heading\n                if heading:\n                    text = await heading.text\n                    assert len(text) > 0\n\n    @pytest.mark.asyncio\n    async def test_find_in_iframe_after_finding_in_another(self, ci_chrome_options):\n        \"\"\"Test finding elements in one iframe after finding in another.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_multiple_iframes.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            # First, find element in cookie iframe\n            cookie_iframe = await tab.find(id='cookie-iframe')\n            cookie_heading = await cookie_iframe.find(id='iframe-heading')\n            cookie_text = await cookie_heading.text\n\n            # Then, find element in login iframe\n            login_iframe = await tab.find(id='login-iframe')\n            login_heading = await login_iframe.find(id='iframe-heading')\n            login_text = await login_heading.text\n\n            # Both should work independently\n            assert 'Iframe Content' in cookie_text\n            assert 'Iframe Content' in login_text\n\n            # Now find in analytics iframe\n            analytics_iframe = await tab.find(id='analytics-iframe')\n            analytics_heading = await analytics_iframe.find(id='iframe-heading')\n            analytics_text = await analytics_heading.text\n\n            assert 'Iframe Content' in analytics_text\n\n\nclass TestIframeEdgeCases:\n    \"\"\"Integration tests for edge cases in iframe handling.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_dynamic_content_in_iframe(self, ci_chrome_options):\n        \"\"\"Test finding dynamically added content in iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n\n            # Add dynamic content via JavaScript\n            iframe_context = await iframe_element.iframe_context\n            await tab.execute_script(\n                \"\"\"\n                const div = document.createElement('div');\n                div.id = 'dynamic-element';\n                div.textContent = 'Dynamic Content';\n                document.body.appendChild(div);\n                \"\"\",\n                context_id=iframe_context.execution_context_id,\n            )\n            await asyncio.sleep(0.5)\n\n            # Find dynamically added element\n            dynamic_element = await iframe_element.find(id='dynamic-element')\n            assert dynamic_element is not None\n\n            text = await dynamic_element.text\n            assert 'Dynamic Content' in text\n\n    @pytest.mark.asyncio\n    async def test_iframe_reload_handling(self, ci_chrome_options):\n        \"\"\"Test that iframe context is properly handled after page reload.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            # Find iframe and element\n            iframe_element = await tab.find(id='simple-iframe')\n            element_before = await iframe_element.find(id='iframe-heading')\n            assert element_before is not None\n\n            # Reload page\n            await tab.refresh()\n            await asyncio.sleep(1)\n\n            # Find iframe again (new instance)\n            iframe_element_after = await tab.find(id='simple-iframe')\n            element_after = await iframe_element_after.find(id='iframe-heading')\n            assert element_after is not None\n\n            # Verify element is accessible\n            text = await element_after.text\n            assert 'Iframe Content' in text\n\n\nclass TestIframeTypeText:\n    \"\"\"Integration tests for type_text inside iframes.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_type_text_in_iframe_input(self, ci_chrome_options):\n        \"\"\"type_text should work inside an iframe input.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n            input_el = await iframe_element.find(id='iframe-input')\n\n            await input_el.type_text('hello')\n            await asyncio.sleep(0.3)\n\n            prop = await input_el.execute_script(\n                'return this.value', return_by_value=True\n            )\n            assert prop['result']['result']['value'] == 'hello'\n\n    @pytest.mark.asyncio\n    async def test_type_text_humanized_in_iframe_input(self, ci_chrome_options):\n        \"\"\"type_text with humanize=True should work inside an iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n            input_el = await iframe_element.find(id='iframe-input')\n\n            await input_el.type_text('Test', humanize=True)\n            await asyncio.sleep(0.3)\n\n            prop = await input_el.execute_script(\n                'return this.value', return_by_value=True\n            )\n            value = prop['result']['result']['value']\n            # Humanized typing may introduce and correct typos,\n            # but the final value should be non-empty.\n            assert len(value) >= 2\n\n    @pytest.mark.asyncio\n    async def test_type_text_email_in_iframe_input(self, ci_chrome_options):\n        \"\"\"type_text should handle symbols like @ and . inside iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_iframe_simple.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            iframe_element = await tab.find(id='simple-iframe')\n            input_el = await iframe_element.find(id='iframe-email')\n\n            test_text = 'user@test.com'\n            await input_el.type_text(test_text)\n            await asyncio.sleep(0.3)\n\n            prop = await input_el.execute_script(\n                'return this.value', return_by_value=True\n            )\n            assert prop['result']['result']['value'] == test_text\n\n\nclass TestFrameElementIntegration:\n    \"\"\"Integration tests for <frame> elements (frameset pages).\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_frame_element_is_iframe(self, ci_chrome_options):\n        \"\"\"Test that a <frame> element is recognized as an iframe.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_frameset.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            frame_element = await tab.find(id='left-frame', timeout=5)\n            assert frame_element is not None\n            assert frame_element.tag_name == 'frame'\n            assert frame_element.is_iframe\n\n    @pytest.mark.asyncio\n    async def test_find_element_inside_frame(self, ci_chrome_options):\n        \"\"\"Test finding an element inside a <frame> element.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_frameset.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            frame_element = await tab.find(id='left-frame', timeout=5)\n            heading = await frame_element.find(id='frame-heading', timeout=5)\n            assert heading is not None\n\n            text = await heading.text\n            assert 'Frame Content' in text\n\n    @pytest.mark.asyncio\n    async def test_frame_context_is_resolved(self, ci_chrome_options):\n        \"\"\"Test that iframe_context works for <frame> elements.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_frameset.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            frame_element = await tab.find(id='left-frame', timeout=5)\n            ctx = await frame_element.iframe_context\n            assert ctx is not None\n            assert ctx.frame_id is not None\n            assert ctx.execution_context_id is not None\n\n    @pytest.mark.asyncio\n    async def test_inner_html_of_frame(self, ci_chrome_options):\n        \"\"\"Test that inner_html works for <frame> elements.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_frameset.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            frame_element = await tab.find(id='left-frame', timeout=5)\n            html = await frame_element.inner_html\n            assert 'frame-heading' in html\n\n    @pytest.mark.asyncio\n    async def test_multiple_frames_in_frameset(self, ci_chrome_options):\n        \"\"\"Test interacting with multiple <frame> elements in a frameset.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_frameset.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            left_frame = await tab.find(id='left-frame', timeout=5)\n            right_frame = await tab.find(id='right-frame', timeout=5)\n\n            assert left_frame.is_iframe\n            assert right_frame.is_iframe\n\n            # Left frame has frame-specific content\n            left_heading = await left_frame.find(id='frame-heading', timeout=5)\n            left_text = await left_heading.text\n            assert 'Frame Content' in left_text\n\n            # Right frame has iframe content (reuses test_iframe_content.html)\n            right_heading = await right_frame.find(id='iframe-heading', timeout=5)\n            right_text = await right_heading.text\n            assert 'Iframe Content' in right_text\n\n    @pytest.mark.asyncio\n    async def test_type_text_in_frame_input(self, ci_chrome_options):\n        \"\"\"Test typing text into an input inside a <frame> element.\"\"\"\n        test_file = Path(__file__).parent / 'pages' / 'test_frameset.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n            await asyncio.sleep(1)\n\n            frame_element = await tab.find(id='left-frame', timeout=5)\n            input_el = await frame_element.find(id='frame-input', timeout=5)\n\n            test_text = 'hello frame'\n            await input_el.type_text(test_text)\n            await asyncio.sleep(0.3)\n\n            prop = await input_el.execute_script(\n                'return this.value', return_by_value=True\n            )\n            assert prop['result']['result']['value'] == test_text\n\n"
  },
  {
    "path": "tests/test_interactions/__init__.py",
    "content": ""
  },
  {
    "path": "tests/test_interactions/test_iframe.py",
    "content": "\"\"\"Unit tests for IFrameContextResolver and IFrameContext.\"\"\"\n\nimport pytest\nimport pytest_asyncio\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nfrom pydoll.interactions.iframe import IFrameContext, IFrameContextResolver\nfrom pydoll.exceptions import InvalidIFrame\n\n\n@pytest_asyncio.fixture\nasync def mock_element():\n    \"\"\"Create a mock WebElement for tests.\"\"\"\n    element = MagicMock()\n    element._object_id = 'mock-object-id'\n    element._connection_handler = MagicMock()\n    element._connection_handler.execute_command = AsyncMock()\n    element._connection_handler._connection_port = 9222\n    element._describe_node = AsyncMock()\n    return element\n\n\n@pytest_asyncio.fixture\nasync def resolver(mock_element):\n    \"\"\"Create IFrameContextResolver with mocked element.\"\"\"\n    return IFrameContextResolver(mock_element)\n\n\nclass TestIFrameContext:\n    \"\"\"Test IFrameContext dataclass.\"\"\"\n\n    def test_default_values(self):\n        \"\"\"Test IFrameContext with only required field.\"\"\"\n        context = IFrameContext(frame_id='test-frame-id')\n\n        assert context.frame_id == 'test-frame-id'\n        assert context.document_url is None\n        assert context.execution_context_id is None\n        assert context.document_object_id is None\n        assert context.session_handler is None\n        assert context.session_id is None\n\n    def test_all_values(self):\n        \"\"\"Test IFrameContext with all fields set.\"\"\"\n        mock_handler = MagicMock()\n\n        context = IFrameContext(\n            frame_id='frame-123',\n            document_url='https://example.com',\n            execution_context_id=42,\n            document_object_id='doc-obj-456',\n            session_handler=mock_handler,\n            session_id='session-789',\n        )\n\n        assert context.frame_id == 'frame-123'\n        assert context.document_url == 'https://example.com'\n        assert context.execution_context_id == 42\n        assert context.document_object_id == 'doc-obj-456'\n        assert context.session_handler is mock_handler\n        assert context.session_id == 'session-789'\n\n\nclass TestIFrameContextResolverInit:\n    \"\"\"Test IFrameContextResolver initialization.\"\"\"\n\n    def test_initialization(self, mock_element):\n        \"\"\"Test resolver stores element reference.\"\"\"\n        resolver = IFrameContextResolver(mock_element)\n\n        assert resolver._element is mock_element\n\n\nclass TestGetBaseSession:\n    \"\"\"Test _get_base_session method.\"\"\"\n\n    def test_get_base_session_default(self, mock_element):\n        \"\"\"Test _get_base_session returns connection handler when no routing session.\"\"\"\n        # Explicitly set routing session to None to simulate no routing\n        mock_element._routing_session_handler = None\n        mock_element._routing_session_id = None\n\n        resolver = IFrameContextResolver(mock_element)\n        handler, session_id = resolver._get_base_session()\n\n        assert handler is mock_element._connection_handler\n        assert session_id is None\n\n    def test_get_base_session_with_routing_session(self, mock_element):\n        \"\"\"Test _get_base_session returns routing session when set.\"\"\"\n        mock_routing_handler = MagicMock()\n        mock_element._routing_session_handler = mock_routing_handler\n        mock_element._routing_session_id = 'routing-session-123'\n\n        resolver = IFrameContextResolver(mock_element)\n        handler, session_id = resolver._get_base_session()\n\n        assert handler is mock_routing_handler\n        assert session_id == 'routing-session-123'\n\n\nclass TestExtractFrameMetadata:\n    \"\"\"Test _extract_frame_metadata static method.\"\"\"\n\n    def test_extract_with_content_document(self):\n        \"\"\"Test extracting metadata when contentDocument is present.\"\"\"\n        node_info = {\n            'contentDocument': {\n                'frameId': 'content-frame-id',\n                'documentURL': 'https://iframe.example.com',\n            },\n            'frameId': 'parent-frame-id',\n            'backendNodeId': 123,\n        }\n\n        frame_id, doc_url, parent_id, backend_id = (\n            IFrameContextResolver._extract_frame_metadata(node_info)\n        )\n\n        assert frame_id == 'content-frame-id'\n        assert doc_url == 'https://iframe.example.com'\n        assert parent_id == 'parent-frame-id'\n        assert backend_id == 123\n\n    def test_extract_without_content_document(self):\n        \"\"\"Test extracting metadata when contentDocument is missing.\"\"\"\n        node_info = {\n            'frameId': 'parent-frame-id',\n            'backendNodeId': 456,\n            'documentURL': 'https://fallback.example.com',\n        }\n\n        frame_id, doc_url, parent_id, backend_id = (\n            IFrameContextResolver._extract_frame_metadata(node_info)\n        )\n\n        assert frame_id is None  # No contentDocument.frameId\n        assert doc_url == 'https://fallback.example.com'\n        assert parent_id == 'parent-frame-id'\n        assert backend_id == 456\n\n    def test_extract_with_base_url_fallback(self):\n        \"\"\"Test documentURL fallback to baseURL.\"\"\"\n        node_info = {\n            'contentDocument': {\n                'baseURL': 'https://base.example.com',\n            },\n            'backendNodeId': 789,\n        }\n\n        frame_id, doc_url, parent_id, backend_id = (\n            IFrameContextResolver._extract_frame_metadata(node_info)\n        )\n\n        assert doc_url == 'https://base.example.com'\n\n    def test_extract_empty_node_info(self):\n        \"\"\"Test extracting from empty node info.\"\"\"\n        node_info = {}\n\n        frame_id, doc_url, parent_id, backend_id = (\n            IFrameContextResolver._extract_frame_metadata(node_info)\n        )\n\n        assert frame_id is None\n        assert doc_url is None\n        assert parent_id is None\n        assert backend_id is None\n\n\nclass TestWalkFrames:\n    \"\"\"Test _walk_frames static method.\"\"\"\n\n    def test_walk_frames_single_frame(self):\n        \"\"\"Test walking a tree with single frame.\"\"\"\n        frame_tree = {\n            'frame': {'id': 'frame-1', 'url': 'https://example.com'},\n            'childFrames': [],\n        }\n\n        frames = list(IFrameContextResolver._walk_frames(frame_tree))\n\n        assert len(frames) == 1\n        assert frames[0]['id'] == 'frame-1'\n\n    def test_walk_frames_with_children(self):\n        \"\"\"Test walking a tree with child frames.\"\"\"\n        frame_tree = {\n            'frame': {'id': 'parent-frame', 'url': 'https://parent.com'},\n            'childFrames': [\n                {\n                    'frame': {'id': 'child-frame-1', 'url': 'https://child1.com'},\n                    'childFrames': [],\n                },\n                {\n                    'frame': {'id': 'child-frame-2', 'url': 'https://child2.com'},\n                    'childFrames': [],\n                },\n            ],\n        }\n\n        frames = list(IFrameContextResolver._walk_frames(frame_tree))\n\n        assert len(frames) == 3\n        frame_ids = [f['id'] for f in frames]\n        assert 'parent-frame' in frame_ids\n        assert 'child-frame-1' in frame_ids\n        assert 'child-frame-2' in frame_ids\n\n    def test_walk_frames_nested_children(self):\n        \"\"\"Test walking deeply nested frame tree.\"\"\"\n        frame_tree = {\n            'frame': {'id': 'level-0'},\n            'childFrames': [\n                {\n                    'frame': {'id': 'level-1'},\n                    'childFrames': [\n                        {\n                            'frame': {'id': 'level-2'},\n                            'childFrames': [],\n                        }\n                    ],\n                }\n            ],\n        }\n\n        frames = list(IFrameContextResolver._walk_frames(frame_tree))\n\n        assert len(frames) == 3\n        frame_ids = [f['id'] for f in frames]\n        assert 'level-0' in frame_ids\n        assert 'level-1' in frame_ids\n        assert 'level-2' in frame_ids\n\n    def test_walk_frames_empty_tree(self):\n        \"\"\"Test walking empty frame tree.\"\"\"\n        frames = list(IFrameContextResolver._walk_frames(None))\n        assert frames == []\n\n    def test_walk_frames_no_child_frames_key(self):\n        \"\"\"Test walking frame tree with no childFrames key.\"\"\"\n        frame_tree = {\n            'frame': {'id': 'single-frame'},\n        }\n\n        frames = list(IFrameContextResolver._walk_frames(frame_tree))\n\n        assert len(frames) == 1\n        assert frames[0]['id'] == 'single-frame'\n\n\nclass TestFindChildByParent:\n    \"\"\"Test _find_child_by_parent static method.\"\"\"\n\n    def test_find_direct_child(self):\n        \"\"\"Test finding direct child by parent ID.\"\"\"\n        frame_tree = {\n            'frame': {'id': 'root'},\n            'childFrames': [\n                {\n                    'frame': {'id': 'child-1', 'parentId': 'target-parent'},\n                    'childFrames': [],\n                },\n            ],\n        }\n\n        result = IFrameContextResolver._find_child_by_parent(frame_tree, 'target-parent')\n\n        assert result == 'child-1'\n\n    def test_find_nested_child(self):\n        \"\"\"Test finding nested child by parent ID.\"\"\"\n        frame_tree = {\n            'frame': {'id': 'root'},\n            'childFrames': [\n                {\n                    'frame': {'id': 'level-1', 'parentId': 'root'},\n                    'childFrames': [\n                        {\n                            'frame': {'id': 'level-2', 'parentId': 'target-parent'},\n                            'childFrames': [],\n                        }\n                    ],\n                }\n            ],\n        }\n\n        result = IFrameContextResolver._find_child_by_parent(frame_tree, 'target-parent')\n\n        assert result == 'level-2'\n\n    def test_find_child_not_found(self):\n        \"\"\"Test when child with matching parent is not found.\"\"\"\n        frame_tree = {\n            'frame': {'id': 'root'},\n            'childFrames': [\n                {\n                    'frame': {'id': 'child', 'parentId': 'other-parent'},\n                    'childFrames': [],\n                }\n            ],\n        }\n\n        result = IFrameContextResolver._find_child_by_parent(frame_tree, 'non-existent')\n\n        assert result is None\n\n    def test_find_child_empty_tree(self):\n        \"\"\"Test finding in empty tree.\"\"\"\n        result = IFrameContextResolver._find_child_by_parent(None, 'any-parent')\n\n        assert result is None\n\n    def test_find_child_no_child_frames(self):\n        \"\"\"Test finding in tree with no child frames.\"\"\"\n        frame_tree = {\n            'frame': {'id': 'root'},\n            'childFrames': [],\n        }\n\n        result = IFrameContextResolver._find_child_by_parent(frame_tree, 'any-parent')\n\n        assert result is None\n\n\nclass TestGetFrameTreeFor:\n    \"\"\"Test _get_frame_tree_for static method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_frame_tree_without_session(self):\n        \"\"\"Test getting frame tree without session ID.\"\"\"\n        mock_handler = MagicMock()\n        mock_handler.execute_command = AsyncMock(\n            return_value={\n                'result': {\n                    'frameTree': {\n                        'frame': {'id': 'main-frame'},\n                        'childFrames': [],\n                    }\n                }\n            }\n        )\n\n        result = await IFrameContextResolver._get_frame_tree_for(mock_handler, None)\n\n        assert result['frame']['id'] == 'main-frame'\n        # Verify command was called\n        mock_handler.execute_command.assert_called_once()\n        call_args = mock_handler.execute_command.call_args[0][0]\n        assert 'sessionId' not in call_args\n\n    @pytest.mark.asyncio\n    async def test_get_frame_tree_with_session(self):\n        \"\"\"Test getting frame tree with session ID.\"\"\"\n        mock_handler = MagicMock()\n        mock_handler.execute_command = AsyncMock(\n            return_value={\n                'result': {\n                    'frameTree': {\n                        'frame': {'id': 'session-frame'},\n                        'childFrames': [],\n                    }\n                }\n            }\n        )\n\n        result = await IFrameContextResolver._get_frame_tree_for(\n            mock_handler, 'session-123'\n        )\n\n        assert result['frame']['id'] == 'session-frame'\n        call_args = mock_handler.execute_command.call_args[0][0]\n        assert call_args['sessionId'] == 'session-123'\n\n\nclass TestOwnerBackendFor:\n    \"\"\"Test _owner_backend_for static method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_owner_backend_without_session(self):\n        \"\"\"Test getting owner backend ID without session.\"\"\"\n        mock_handler = MagicMock()\n        mock_handler.execute_command = AsyncMock(\n            return_value={'result': {'backendNodeId': 456}}\n        )\n\n        result = await IFrameContextResolver._owner_backend_for(\n            mock_handler, None, 'frame-id-123'\n        )\n\n        assert result == 456\n        call_args = mock_handler.execute_command.call_args[0][0]\n        assert 'sessionId' not in call_args\n\n    @pytest.mark.asyncio\n    async def test_owner_backend_with_session(self):\n        \"\"\"Test getting owner backend ID with session.\"\"\"\n        mock_handler = MagicMock()\n        mock_handler.execute_command = AsyncMock(\n            return_value={'result': {'backendNodeId': 789}}\n        )\n\n        result = await IFrameContextResolver._owner_backend_for(\n            mock_handler, 'session-xyz', 'frame-id-456'\n        )\n\n        assert result == 789\n        call_args = mock_handler.execute_command.call_args[0][0]\n        assert call_args['sessionId'] == 'session-xyz'\n\n    @pytest.mark.asyncio\n    async def test_owner_backend_missing_result(self):\n        \"\"\"Test handling missing result.\"\"\"\n        mock_handler = MagicMock()\n        mock_handler.execute_command = AsyncMock(return_value={})\n\n        result = await IFrameContextResolver._owner_backend_for(\n            mock_handler, None, 'frame-id'\n        )\n\n        assert result is None\n\n\nclass TestCreateIsolatedWorldForFrame:\n    \"\"\"Test _create_isolated_world_for_frame static method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_create_isolated_world_success(self):\n        \"\"\"Test successful creation of isolated world.\"\"\"\n        mock_handler = MagicMock()\n        mock_handler.execute_command = AsyncMock(\n            return_value={'result': {'executionContextId': 42}}\n        )\n\n        result = await IFrameContextResolver._create_isolated_world_for_frame(\n            'frame-id-123', mock_handler, None\n        )\n\n        assert result == 42\n        call_args = mock_handler.execute_command.call_args[0][0]\n        assert 'sessionId' not in call_args\n        assert 'pydoll::iframe::frame-id-123' in call_args['params']['worldName']\n\n    @pytest.mark.asyncio\n    async def test_create_isolated_world_with_session(self):\n        \"\"\"Test creation with session ID.\"\"\"\n        mock_handler = MagicMock()\n        mock_handler.execute_command = AsyncMock(\n            return_value={'result': {'executionContextId': 99}}\n        )\n\n        result = await IFrameContextResolver._create_isolated_world_for_frame(\n            'frame-id', mock_handler, 'session-abc'\n        )\n\n        assert result == 99\n        call_args = mock_handler.execute_command.call_args[0][0]\n        assert call_args['sessionId'] == 'session-abc'\n\n    @pytest.mark.asyncio\n    async def test_create_isolated_world_failure(self):\n        \"\"\"Test failure when no execution context ID returned.\"\"\"\n        mock_handler = MagicMock()\n        mock_handler.execute_command = AsyncMock(return_value={'result': {}})\n\n        with pytest.raises(InvalidIFrame, match='Unable to create isolated world'):\n            await IFrameContextResolver._create_isolated_world_for_frame(\n                'frame-id', mock_handler, None\n            )\n\n\nclass TestGetDocumentObjectId:\n    \"\"\"Test _get_document_object_id method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_document_object_id_success(self, resolver, mock_element):\n        \"\"\"Test successful retrieval of document object ID.\"\"\"\n        mock_element._connection_handler.execute_command.return_value = {\n            'result': {'result': {'objectId': 'doc-object-123'}}\n        }\n\n        context = IFrameContext(frame_id='test-frame')\n\n        result = await resolver._get_document_object_id(42, context)\n\n        assert result == 'doc-object-123'\n\n    @pytest.mark.asyncio\n    async def test_get_document_object_id_with_session(self, resolver, mock_element):\n        \"\"\"Test retrieval with session handler.\"\"\"\n        mock_session_handler = MagicMock()\n        mock_session_handler.execute_command = AsyncMock(\n            return_value={'result': {'result': {'objectId': 'session-doc-obj'}}}\n        )\n\n        context = IFrameContext(\n            frame_id='test-frame',\n            session_handler=mock_session_handler,\n            session_id='session-123',\n        )\n\n        result = await resolver._get_document_object_id(99, context)\n\n        assert result == 'session-doc-obj'\n        mock_session_handler.execute_command.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_get_document_object_id_failure(self, resolver, mock_element):\n        \"\"\"Test failure when document object ID not found.\"\"\"\n        mock_element._connection_handler.execute_command.return_value = {\n            'result': {'result': {}}\n        }\n\n        context = IFrameContext(frame_id='test-frame')\n\n        with pytest.raises(InvalidIFrame, match='Unable to obtain document reference'):\n            await resolver._get_document_object_id(42, context)\n\n\nclass TestResolveOopifIfNeeded:\n    \"\"\"Test _resolve_oopif_if_needed method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_returns_early_when_no_parent_frame(self, resolver):\n        \"\"\"Test early return when content_frame_id is None.\"\"\"\n        result = await resolver._resolve_oopif_if_needed(\n            current_frame_id='frame-123',\n            content_frame_id=None,\n            backend_node_id=456,\n            current_document_url='https://example.com',\n        )\n\n        handler, session_id, frame_id, url = result\n        assert handler is None\n        assert session_id is None\n        assert frame_id == 'frame-123'\n        assert url == 'https://example.com'\n\n    @pytest.mark.asyncio\n    async def test_returns_early_when_frame_resolved_without_backend(self, resolver):\n        \"\"\"Test early return when frame is resolved and no backend_node_id.\"\"\"\n        result = await resolver._resolve_oopif_if_needed(\n            current_frame_id='resolved-frame',\n            content_frame_id='parent-123',\n            backend_node_id=None,\n            current_document_url='https://resolved.com',\n        )\n\n        handler, session_id, frame_id, url = result\n        assert handler is None\n        assert session_id is None\n        assert frame_id == 'resolved-frame'\n        assert url == 'https://resolved.com'\n\n\nclass TestResolveFrameByOwner:\n    \"\"\"Test _resolve_frame_by_owner method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_resolve_returns_current_url_on_failure(self, resolver):\n        \"\"\"Test that current URL is preserved when resolution fails.\"\"\"\n        # Mock _find_frame_by_owner to return no match\n        resolver._find_frame_by_owner = AsyncMock(return_value=(None, None))\n\n        mock_handler = MagicMock()\n\n        result = await resolver._resolve_frame_by_owner(\n            mock_handler, None, 123, 'https://current.com'\n        )\n\n        frame_id, url = result\n        assert frame_id is None\n        assert url == 'https://current.com'\n\n    @pytest.mark.asyncio\n    async def test_resolve_returns_found_frame(self, resolver):\n        \"\"\"Test successful frame resolution by owner.\"\"\"\n        resolver._find_frame_by_owner = AsyncMock(\n            return_value=('found-frame-id', 'https://found.com')\n        )\n\n        mock_handler = MagicMock()\n\n        result = await resolver._resolve_frame_by_owner(\n            mock_handler, None, 456, 'https://fallback.com'\n        )\n\n        frame_id, url = result\n        assert frame_id == 'found-frame-id'\n        assert url == 'https://found.com'\n\n    @pytest.mark.asyncio\n    async def test_resolve_uses_fallback_url(self, resolver):\n        \"\"\"Test URL fallback when found frame has no URL.\"\"\"\n        resolver._find_frame_by_owner = AsyncMock(\n            return_value=('frame-id', None)\n        )\n\n        mock_handler = MagicMock()\n\n        result = await resolver._resolve_frame_by_owner(\n            mock_handler, None, 789, 'https://fallback.com'\n        )\n\n        frame_id, url = result\n        assert frame_id == 'frame-id'\n        assert url == 'https://fallback.com'\n"
  },
  {
    "path": "tests/test_interactions/test_keyboard.py",
    "content": "import pytest\nimport pytest_asyncio\nfrom unittest.mock import AsyncMock, MagicMock\n\nfrom pydoll.interactions.keyboard import Keyboard, KeyboardAPI\nfrom pydoll.constants import Key\nfrom pydoll.protocol.input.types import KeyEventType\n\n\n@pytest_asyncio.fixture\nasync def mock_tab():\n    \"\"\"Mock Tab instance for KeyboardAPI tests.\"\"\"\n    tab = MagicMock()\n    tab._execute_command = AsyncMock()\n    tab.focus = AsyncMock()\n    return tab\n\n\n@pytest_asyncio.fixture\nasync def keyboard_api(mock_tab):\n    \"\"\"Create KeyboardAPI instance with mocked tab.\"\"\"\n    return KeyboardAPI(mock_tab)\n\n\nclass TestKeyboardAPIInitialization:\n    \"\"\"Test KeyboardAPI initialization.\"\"\"\n\n    def test_initialization(self, mock_tab):\n        \"\"\"Test KeyboardAPI is properly initialized with executor.\"\"\"\n        keyboard_api = KeyboardAPI(mock_tab)\n        assert keyboard_api._executor == mock_tab\n\n\nclass TestKeyboardAPIDown:\n    \"\"\"Test keyboard.down() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_key_down_without_modifiers(self, keyboard_api, mock_tab):\n        \"\"\"Test pressing key down without modifiers.\"\"\"\n        await keyboard_api.down(Key.A)\n\n        # Verify execute_command was called\n        assert mock_tab._execute_command.called\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n\n        # Verify the command structure\n        assert command['method'] == 'Input.dispatchKeyEvent'\n        assert command['params']['type'] == KeyEventType.KEY_DOWN\n        assert command['params']['key'] == 'A'\n        assert command['params']['windowsVirtualKeyCode'] == 65\n        assert command['params']['nativeVirtualKeyCode'] == 65\n\n    @pytest.mark.asyncio\n    async def test_key_down_with_modifiers(self, keyboard_api, mock_tab):\n        \"\"\"Test pressing key down with modifiers.\"\"\"\n        await keyboard_api.down(Key.C, modifiers=2)  # Ctrl modifier\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n\n        assert command['params']['type'] == KeyEventType.KEY_DOWN\n        assert command['params']['key'] == 'C'\n        assert command['params']['modifiers'] == 2\n\n    @pytest.mark.asyncio\n    async def test_key_down_enter(self, keyboard_api, mock_tab):\n        \"\"\"Test pressing Enter key down.\"\"\"\n        await keyboard_api.down(Key.ENTER)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n\n        assert command['params']['key'] == 'Enter'\n        assert command['params']['windowsVirtualKeyCode'] == 13\n\n\nclass TestKeyboardAPIUp:\n    \"\"\"Test keyboard.up() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_key_up(self, keyboard_api, mock_tab):\n        \"\"\"Test releasing a key.\"\"\"\n        await keyboard_api.up(Key.A)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n\n        assert command['method'] == 'Input.dispatchKeyEvent'\n        assert command['params']['type'] == KeyEventType.KEY_UP\n        assert command['params']['key'] == 'A'\n        assert command['params']['windowsVirtualKeyCode'] == 65\n\n    @pytest.mark.asyncio\n    async def test_key_up_shift(self, keyboard_api, mock_tab):\n        \"\"\"Test releasing Shift key.\"\"\"\n        await keyboard_api.up(Key.SHIFT)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n\n        assert command['params']['key'] == 'Shift'\n        assert command['params']['windowsVirtualKeyCode'] == 16\n\n\nclass TestKeyboardAPIPress:\n    \"\"\"Test keyboard.press() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_press_key(self, keyboard_api, mock_tab):\n        \"\"\"Test pressing and releasing a key.\"\"\"\n        await keyboard_api.press(Key.ENTER)\n\n        # Should call execute_command twice (down + up)\n        assert mock_tab._execute_command.call_count == 2\n\n        # Verify first call is KEY_DOWN\n        first_call = mock_tab._execute_command.call_args_list[0]\n        assert first_call[0][0]['params']['type'] == KeyEventType.KEY_DOWN\n\n        # Verify second call is KEY_UP\n        second_call = mock_tab._execute_command.call_args_list[1]\n        assert second_call[0][0]['params']['type'] == KeyEventType.KEY_UP\n\n    @pytest.mark.asyncio\n    async def test_press_with_modifiers(self, keyboard_api, mock_tab):\n        \"\"\"Test pressing key with modifiers.\"\"\"\n        await keyboard_api.press(Key.S, modifiers=2)  # Ctrl+S\n\n        # Verify KEY_DOWN has modifiers\n        first_call = mock_tab._execute_command.call_args_list[0]\n        assert first_call[0][0]['params']['modifiers'] == 2\n\n    @pytest.mark.asyncio\n    async def test_press_with_custom_interval(self, keyboard_api, mock_tab):\n        \"\"\"Test pressing key with custom hold interval.\"\"\"\n        # Just verify it completes without error\n        await keyboard_api.press(Key.TAB, interval=0.2)\n        assert mock_tab._execute_command.call_count == 2\n\n\nclass TestKeyboardAPIHotkey:\n    \"\"\"Test keyboard.hotkey() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_hotkey_ctrl_c(self, keyboard_api, mock_tab):\n        \"\"\"Test Ctrl+C hotkey.\"\"\"\n        await keyboard_api.hotkey(Key.CONTROL, Key.C)\n\n        # Should call execute_command twice (C down + up)\n        assert mock_tab._execute_command.call_count == 2\n\n        # Verify KEY_DOWN for C with Ctrl modifier\n        first_call = mock_tab._execute_command.call_args_list[0]\n        command = first_call[0][0]\n        assert command['params']['type'] == KeyEventType.KEY_DOWN\n        assert command['params']['key'] == 'C'\n        assert command['params']['modifiers'] == 2  # Ctrl = 2\n\n        # Verify KEY_UP for C\n        second_call = mock_tab._execute_command.call_args_list[1]\n        command = second_call[0][0]\n        assert command['params']['type'] == KeyEventType.KEY_UP\n        assert command['params']['key'] == 'C'\n\n    @pytest.mark.asyncio\n    async def test_hotkey_ctrl_shift_t(self, keyboard_api, mock_tab):\n        \"\"\"Test Ctrl+Shift+T hotkey (3 keys).\"\"\"\n        await keyboard_api.hotkey(Key.CONTROL, Key.SHIFT, Key.T)\n\n        # Should call execute_command twice (T down + up)\n        assert mock_tab._execute_command.call_count == 2\n\n        # Verify KEY_DOWN for T with Ctrl+Shift modifiers\n        first_call = mock_tab._execute_command.call_args_list[0]\n        command = first_call[0][0]\n        assert command['params']['key'] == 'T'\n        assert command['params']['modifiers'] == 10  # Ctrl(2) + Shift(8) = 10\n\n        # Verify KEY_UP for T\n        second_call = mock_tab._execute_command.call_args_list[1]\n        command = second_call[0][0]\n        assert command['params']['type'] == KeyEventType.KEY_UP\n\n    @pytest.mark.asyncio\n    async def test_hotkey_alt_f4(self, keyboard_api, mock_tab):\n        \"\"\"Test Alt+F4 hotkey.\"\"\"\n        await keyboard_api.hotkey(Key.ALT, Key.F4)\n\n        first_call = mock_tab._execute_command.call_args_list[0]\n        command = first_call[0][0]\n        assert command['params']['key'] == 'F4'\n        assert command['params']['modifiers'] == 1  # Alt = 1\n\n    @pytest.mark.asyncio\n    async def test_hotkey_shift_a(self, keyboard_api, mock_tab):\n        \"\"\"Test Shift+A hotkey (uppercase A).\"\"\"\n        await keyboard_api.hotkey(Key.SHIFT, Key.A)\n\n        first_call = mock_tab._execute_command.call_args_list[0]\n        command = first_call[0][0]\n        assert command['params']['key'] == 'A'\n        assert command['params']['modifiers'] == 8  # Shift = 8\n\n\nclass TestKeyboardAPISplitModifiers:\n    \"\"\"Test _split_modifiers_and_keys static method.\"\"\"\n\n    def test_split_single_modifier_and_key(self):\n        \"\"\"Test splitting Ctrl+C.\"\"\"\n        keys = [Key.CONTROL, Key.C]\n        modifiers, non_modifiers = KeyboardAPI._split_modifiers_and_keys(keys)\n\n        assert modifiers == [Key.CONTROL]\n        assert non_modifiers == [Key.C]\n\n    def test_split_multiple_modifiers_and_key(self):\n        \"\"\"Test splitting Ctrl+Shift+T.\"\"\"\n        keys = [Key.CONTROL, Key.SHIFT, Key.T]\n        modifiers, non_modifiers = KeyboardAPI._split_modifiers_and_keys(keys)\n\n        assert set(modifiers) == {Key.CONTROL, Key.SHIFT}\n        assert non_modifiers == [Key.T]\n\n    def test_split_no_modifiers(self):\n        \"\"\"Test splitting when no modifiers present.\"\"\"\n        keys = [Key.A, Key.B]\n        modifiers, non_modifiers = KeyboardAPI._split_modifiers_and_keys(keys)\n\n        assert modifiers == []\n        assert set(non_modifiers) == {Key.A, Key.B}\n\n    def test_split_only_modifiers(self):\n        \"\"\"Test splitting when only modifiers present.\"\"\"\n        keys = [Key.CONTROL, Key.SHIFT]\n        modifiers, non_modifiers = KeyboardAPI._split_modifiers_and_keys(keys)\n\n        assert set(modifiers) == {Key.CONTROL, Key.SHIFT}\n        assert non_modifiers == []\n\n\nclass TestKeyboardAPICalculateModifier:\n    \"\"\"Test _calculate_modifier_value static method.\"\"\"\n\n    def test_calculate_single_modifier_ctrl(self):\n        \"\"\"Test calculating Ctrl modifier value.\"\"\"\n        modifiers = [Key.CONTROL]\n        value = KeyboardAPI._calculate_modifier_value(modifiers)\n        assert value == 2\n\n    def test_calculate_single_modifier_shift(self):\n        \"\"\"Test calculating Shift modifier value.\"\"\"\n        modifiers = [Key.SHIFT]\n        value = KeyboardAPI._calculate_modifier_value(modifiers)\n        assert value == 8\n\n    def test_calculate_single_modifier_alt(self):\n        \"\"\"Test calculating Alt modifier value.\"\"\"\n        modifiers = [Key.ALT]\n        value = KeyboardAPI._calculate_modifier_value(modifiers)\n        assert value == 1\n\n    def test_calculate_single_modifier_meta(self):\n        \"\"\"Test calculating Meta modifier value.\"\"\"\n        modifiers = [Key.META]\n        value = KeyboardAPI._calculate_modifier_value(modifiers)\n        assert value == 4\n\n    def test_calculate_ctrl_shift(self):\n        \"\"\"Test calculating Ctrl+Shift modifier value.\"\"\"\n        modifiers = [Key.CONTROL, Key.SHIFT]\n        value = KeyboardAPI._calculate_modifier_value(modifiers)\n        assert value == 10  # 2 + 8\n\n    def test_calculate_ctrl_alt(self):\n        \"\"\"Test calculating Ctrl+Alt modifier value.\"\"\"\n        modifiers = [Key.CONTROL, Key.ALT]\n        value = KeyboardAPI._calculate_modifier_value(modifiers)\n        assert value == 3  # 2 + 1\n\n    def test_calculate_all_modifiers(self):\n        \"\"\"Test calculating all modifiers together.\"\"\"\n        modifiers = [Key.CONTROL, Key.SHIFT, Key.ALT, Key.META]\n        value = KeyboardAPI._calculate_modifier_value(modifiers)\n        assert value == 15  # 2 + 8 + 1 + 4\n\n    def test_calculate_no_modifiers(self):\n        \"\"\"Test calculating with no modifiers.\"\"\"\n        modifiers = []\n        value = KeyboardAPI._calculate_modifier_value(modifiers)\n        assert value is None\n\n\nclass TestKeyboardAPIIntegrationWithTab:\n    \"\"\"Test KeyboardAPI integration with Tab class.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_keyboard_api_uses_tab_execute_command(self, mock_tab):\n        \"\"\"Test that KeyboardAPI uses tab's _execute_command.\"\"\"\n        keyboard_api = KeyboardAPI(mock_tab)\n        await keyboard_api.down(Key.A)\n\n        # Verify tab's _execute_command was called\n        assert mock_tab._execute_command.called\n\n    def test_keyboard_api_stores_executor_reference(self, mock_tab):\n        \"\"\"Test that KeyboardAPI stores reference to executor.\"\"\"\n        keyboard_api = KeyboardAPI(mock_tab)\n        assert keyboard_api._executor is mock_tab\n\n\nclass TestKeyboardAPIEdgeCases:\n    \"\"\"Test edge cases and special scenarios.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_press_numpad_key(self, keyboard_api, mock_tab):\n        \"\"\"Test pressing numpad keys.\"\"\"\n        await keyboard_api.press(Key.NUMPAD5)\n\n        first_call = mock_tab._execute_command.call_args_list[0]\n        command = first_call[0][0]\n        assert command['params']['key'] == 'Numpad5'\n        assert command['params']['windowsVirtualKeyCode'] == 101\n\n    @pytest.mark.asyncio\n    async def test_press_function_key(self, keyboard_api, mock_tab):\n        \"\"\"Test pressing function keys.\"\"\"\n        await keyboard_api.press(Key.F12)\n\n        first_call = mock_tab._execute_command.call_args_list[0]\n        command = first_call[0][0]\n        assert command['params']['key'] == 'F12'\n        assert command['params']['windowsVirtualKeyCode'] == 123\n\n    @pytest.mark.asyncio\n    async def test_hotkey_with_digit(self, keyboard_api, mock_tab):\n        \"\"\"Test hotkey with digit keys.\"\"\"\n        await keyboard_api.hotkey(Key.CONTROL, Key.DIGIT1)\n\n        first_call = mock_tab._execute_command.call_args_list[0]\n        command = first_call[0][0]\n        assert command['params']['key'] == '1'\n        assert command['params']['modifiers'] == 2\n\n    @pytest.mark.asyncio\n    async def test_sequential_key_presses(self, keyboard_api, mock_tab):\n        \"\"\"Test multiple sequential key presses.\"\"\"\n        await keyboard_api.press(Key.A)\n        await keyboard_api.press(Key.B)\n        await keyboard_api.press(Key.C)\n\n        # Should be called 6 times (3 keys × 2 events each)\n        assert mock_tab._execute_command.call_count == 6\n\n        # Verify sequence: A down, A up, B down, B up, C down, C up\n        calls = mock_tab._execute_command.call_args_list\n        assert calls[0][0][0]['params']['key'] == 'A'\n        assert calls[0][0][0]['params']['type'] == KeyEventType.KEY_DOWN\n        assert calls[1][0][0]['params']['key'] == 'A'\n        assert calls[1][0][0]['params']['type'] == KeyEventType.KEY_UP\n        assert calls[2][0][0]['params']['key'] == 'B'\n        assert calls[4][0][0]['params']['key'] == 'C'\n\n\nclass TestTimingConfig:\n    \"\"\"Test TimingConfig dataclass.\"\"\"\n\n    def test_default_values(self):\n        \"\"\"Test default timing configuration values.\"\"\"\n        from pydoll.interactions.keyboard import TimingConfig\n\n        config = TimingConfig()\n\n        assert config.keystroke_min == 0.03\n        assert config.keystroke_max == 0.12\n        assert config.punctuation_min == 0.08\n        assert config.punctuation_max == 0.18\n        assert config.thinking_probability == 0.02\n        assert config.thinking_min == 0.3\n        assert config.thinking_max == 0.7\n        assert config.distraction_probability == 0.005\n        assert config.distraction_min == 0.5\n        assert config.distraction_max == 1.2\n        assert config.mistake_realize_min == 0.1\n        assert config.mistake_realize_max == 0.25\n        assert config.after_correction_min == 0.03\n        assert config.after_correction_max == 0.08\n        assert config.double_press_min == 0.02\n        assert config.double_press_max == 0.05\n        assert config.hesitation_min == 0.15\n        assert config.hesitation_max == 0.3\n\n    def test_custom_values(self):\n        \"\"\"Test custom timing configuration values.\"\"\"\n        from pydoll.interactions.keyboard import TimingConfig\n\n        config = TimingConfig(\n            keystroke_min=0.05,\n            keystroke_max=0.15,\n            thinking_probability=0.1,\n        )\n\n        assert config.keystroke_min == 0.05\n        assert config.keystroke_max == 0.15\n        assert config.thinking_probability == 0.1\n\n    def test_frozen_dataclass(self):\n        \"\"\"Test that config is immutable (frozen).\"\"\"\n        from pydoll.interactions.keyboard import TimingConfig\n\n        config = TimingConfig()\n\n        with pytest.raises(AttributeError):\n            config.keystroke_min = 1.0\n\n\nclass TestTypoConfig:\n    \"\"\"Test TypoConfig dataclass.\"\"\"\n\n    def test_default_values(self):\n        \"\"\"Test default typo configuration values.\"\"\"\n        from pydoll.interactions.keyboard import TypoConfig\n\n        config = TypoConfig()\n\n        assert config.adjacent_weight == 0.55\n        assert config.transpose_weight == 0.20\n        assert config.double_weight == 0.12\n        assert config.skip_weight == 0.08\n        assert config.missed_space_weight == 0.05\n\n    def test_custom_values(self):\n        \"\"\"Test custom typo configuration values.\"\"\"\n        from pydoll.interactions.keyboard import TypoConfig\n\n        config = TypoConfig(\n            adjacent_weight=0.7,\n            transpose_weight=0.1,\n        )\n\n        assert config.adjacent_weight == 0.7\n        assert config.transpose_weight == 0.1\n\n    def test_frozen_dataclass(self):\n        \"\"\"Test that config is immutable (frozen).\"\"\"\n        from pydoll.interactions.keyboard import TypoConfig\n\n        config = TypoConfig()\n\n        with pytest.raises(AttributeError):\n            config.adjacent_weight = 1.0\n\n\nclass TestTypoResult:\n    \"\"\"Test TypoResult dataclass.\"\"\"\n\n    def test_typo_result_creation(self):\n        \"\"\"Test creating TypoResult.\"\"\"\n        from pydoll.interactions.keyboard import TypoResult\n        from pydoll.constants import TypoType\n\n        result = TypoResult(typo_type=TypoType.ADJACENT, wrong_char='e')\n\n        assert result.typo_type == TypoType.ADJACENT\n        assert result.wrong_char == 'e'\n\n    def test_typo_result_default_wrong_char(self):\n        \"\"\"Test TypoResult with default wrong_char.\"\"\"\n        from pydoll.interactions.keyboard import TypoResult\n        from pydoll.constants import TypoType\n\n        result = TypoResult(typo_type=TypoType.SKIP)\n\n        assert result.typo_type == TypoType.SKIP\n        assert result.wrong_char == ''\n\n\nclass TestKeyboardTypeText:\n    \"\"\"Test keyboard.type_text() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_type_text_basic(self, keyboard_api, mock_tab):\n        \"\"\"Test basic text typing.\"\"\"\n        await keyboard_api.type_text(\"ab\", humanize=False)\n\n        # Should call execute_command for each character (KEY_DOWN + KEY_UP)\n        assert mock_tab._execute_command.call_count == 4\n\n        # Verify characters are typed (checking KEY_DOWN events)\n        # Call 0: 'a' KEY_DOWN\n        first_call = mock_tab._execute_command.call_args_list[0]\n        assert first_call[0][0]['params']['text'] == 'a'\n        assert first_call[0][0]['params']['type'] == KeyEventType.KEY_DOWN\n\n        # Call 1: 'a' KEY_UP\n        second_call = mock_tab._execute_command.call_args_list[1]\n        assert second_call[0][0]['params']['type'] == KeyEventType.KEY_UP\n\n        # Call 2: 'b' KEY_DOWN\n        third_call = mock_tab._execute_command.call_args_list[2]\n        assert third_call[0][0]['params']['text'] == 'b'\n        assert third_call[0][0]['params']['type'] == KeyEventType.KEY_DOWN\n\n    @pytest.mark.asyncio\n    async def test_type_text_with_humanize_calls_humanized_method(self, mock_tab):\n        \"\"\"Test that humanize=True calls _type_text_humanized.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard\n\n        keyboard = Keyboard(mock_tab)\n        keyboard._type_text_humanized = AsyncMock()\n\n        await keyboard.type_text(\"test\", humanize=True)\n\n        keyboard._type_text_humanized.assert_called_once_with(\"test\")\n\n    @pytest.mark.asyncio\n    async def test_type_text_interval_deprecated_warning(self, keyboard_api):\n        \"\"\"Test that interval parameter shows deprecation warning.\"\"\"\n        import warnings\n\n        with warnings.catch_warnings(record=True) as w:\n            warnings.simplefilter(\"always\")\n            await keyboard_api.type_text(\"a\", interval=0.1)\n\n            assert len(w) == 1\n            assert issubclass(w[0].category, DeprecationWarning)\n            assert \"interval\" in str(w[0].message)\n\n    @pytest.mark.asyncio\n    async def test_type_char_calls_focus(self, keyboard_api, mock_tab):\n        \"\"\"_type_char should call focus before dispatching key events.\"\"\"\n        await keyboard_api._type_char(\"x\")\n\n        mock_tab.focus.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_type_backspace_calls_focus(self, keyboard_api, mock_tab):\n        \"\"\"_type_backspace should call focus before dispatching key events.\"\"\"\n        await keyboard_api._type_backspace()\n\n        mock_tab.focus.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_ensure_focus_skipped_without_focus_method(self):\n        \"\"\"Executor without focus method should not trigger focus calls.\"\"\"\n        executor = MagicMock(spec=['_execute_command'])\n        executor._execute_command = AsyncMock()\n        keyboard = Keyboard(executor)\n\n        assert keyboard._has_focus is False\n        await keyboard._type_char(\"a\")\n        # No error and no focus call attempted\n\n    @pytest.mark.asyncio\n    async def test_type_char(self, keyboard_api, mock_tab):\n        \"\"\"Test _type_char sends KEY_DOWN and KEY_UP with key info.\"\"\"\n        await keyboard_api._type_char(\"x\")\n\n        # Should call execute_command twice (down + up)\n        assert mock_tab._execute_command.call_count == 2\n\n        # Verify KEY_DOWN includes key, code and keycode\n        first_call = mock_tab._execute_command.call_args_list[0]\n        command_down = first_call[0][0]\n        assert command_down['method'] == 'Input.dispatchKeyEvent'\n        assert command_down['params']['type'] == KeyEventType.KEY_DOWN\n        assert command_down['params']['text'] == 'x'\n        assert command_down['params']['key'] == 'x'\n        assert command_down['params']['code'] == 'KeyX'\n        assert command_down['params']['windowsVirtualKeyCode'] == 88\n        assert command_down['params']['nativeVirtualKeyCode'] == 88\n\n        # Verify KEY_UP includes key, code and keycode\n        second_call = mock_tab._execute_command.call_args_list[1]\n        command_up = second_call[0][0]\n        assert command_up['method'] == 'Input.dispatchKeyEvent'\n        assert command_up['params']['type'] == KeyEventType.KEY_UP\n        assert command_up['params']['key'] == 'x'\n        assert command_up['params']['code'] == 'KeyX'\n        assert command_up['params']['windowsVirtualKeyCode'] == 88\n\n    @pytest.mark.asyncio\n    async def test_type_char_uppercase(self, keyboard_api, mock_tab):\n        \"\"\"Test _type_char sends correct key info for uppercase letters.\"\"\"\n        await keyboard_api._type_char(\"A\")\n\n        command_down = mock_tab._execute_command.call_args_list[0][0][0]\n        assert command_down['params']['text'] == 'A'\n        assert command_down['params']['key'] == 'A'\n        assert command_down['params']['code'] == 'KeyA'\n        assert command_down['params']['windowsVirtualKeyCode'] == 65\n\n    @pytest.mark.asyncio\n    async def test_type_char_digit(self, keyboard_api, mock_tab):\n        \"\"\"Test _type_char sends correct key info for digits.\"\"\"\n        await keyboard_api._type_char(\"5\")\n\n        command_down = mock_tab._execute_command.call_args_list[0][0][0]\n        assert command_down['params']['text'] == '5'\n        assert command_down['params']['key'] == '5'\n        assert command_down['params']['code'] == 'Digit5'\n        assert command_down['params']['windowsVirtualKeyCode'] == 53\n\n    @pytest.mark.asyncio\n    async def test_type_char_symbol(self, keyboard_api, mock_tab):\n        \"\"\"Test _type_char sends correct key info for symbols.\"\"\"\n        await keyboard_api._type_char(\"@\")\n\n        command_down = mock_tab._execute_command.call_args_list[0][0][0]\n        assert command_down['params']['text'] == '@'\n        assert command_down['params']['key'] == '@'\n        assert command_down['params']['code'] == 'Digit2'\n        assert command_down['params']['windowsVirtualKeyCode'] == 50\n\n    @pytest.mark.asyncio\n    async def test_type_char_unmapped(self, keyboard_api, mock_tab):\n        \"\"\"Test _type_char falls back gracefully for unmapped characters.\"\"\"\n        await keyboard_api._type_char(\"\\u00e9\")  # é (accented)\n\n        command_down = mock_tab._execute_command.call_args_list[0][0][0]\n        assert command_down['params']['text'] == '\\u00e9'\n        assert command_down['params']['key'] == '\\u00e9'\n        assert command_down['params']['windowsVirtualKeyCode'] == 0\n\n    @pytest.mark.asyncio\n    @pytest.mark.parametrize(\n        'char',\n        list('abcdefghijklmnopqrstuvwxyz'\n             'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\n             '0123456789'\n             ' -=[]\\\\;\\',./`'\n             '!@#$%^&*()_+{}|:\"<>?~'\n             '\\n\\t'),\n    )\n    async def test_type_char_all_mapped_characters(self, mock_tab, char):\n        \"\"\"Every character in CHAR_TO_KEY_INFO should produce non-zero keycode.\"\"\"\n        from pydoll.constants import CHAR_TO_KEY_INFO\n\n        keyboard = Keyboard(mock_tab)\n        await keyboard._type_char(char)\n\n        command_down = mock_tab._execute_command.call_args_list[0][0][0]\n        expected_key, expected_code, expected_keycode = CHAR_TO_KEY_INFO[char]\n\n        assert command_down['params']['text'] == char\n        assert command_down['params']['key'] == expected_key\n        assert command_down['params']['code'] == expected_code\n        assert command_down['params']['windowsVirtualKeyCode'] == expected_keycode\n        assert expected_keycode > 0, f'keycode for {char!r} should not be 0'\n\n    @pytest.mark.asyncio\n    async def test_type_backspace(self, keyboard_api, mock_tab):\n        \"\"\"Test _type_backspace sends backspace keys.\"\"\"\n        await keyboard_api._type_backspace()\n\n        # Should call down + up for backspace\n        assert mock_tab._execute_command.call_count == 2\n\n        first_call = mock_tab._execute_command.call_args_list[0]\n        assert first_call[0][0]['params']['key'] == 'Backspace'\n        assert first_call[0][0]['params']['type'] == KeyEventType.KEY_DOWN\n\n        second_call = mock_tab._execute_command.call_args_list[1]\n        assert second_call[0][0]['params']['type'] == KeyEventType.KEY_UP\n\n\nclass TestKeyboardTypoGeneration:\n    \"\"\"Test typo generation methods.\"\"\"\n\n    def test_should_make_typo_returns_boolean(self):\n        \"\"\"Test _should_make_typo returns a boolean.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard\n\n        result = Keyboard._should_make_typo()\n        assert isinstance(result, bool)\n\n    def test_select_typo_type_returns_valid_type(self, keyboard_api):\n        \"\"\"Test _select_typo_type returns valid TypoType.\"\"\"\n        from pydoll.constants import TypoType\n\n        valid_types = {\n            TypoType.ADJACENT,\n            TypoType.TRANSPOSE,\n            TypoType.DOUBLE,\n            TypoType.SKIP,\n            TypoType.MISSED_SPACE,\n        }\n\n        for _ in range(10):\n            result = keyboard_api._select_typo_type()\n            assert result in valid_types\n\n    def test_create_adjacent_typo_returns_adjacent_type(self):\n        \"\"\"Test _create_adjacent_typo returns ADJACENT type for valid char.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard\n        from pydoll.constants import TypoType\n\n        result = Keyboard._create_adjacent_typo('a')\n\n        # 'a' has neighbors, so should return ADJACENT\n        assert result.typo_type == TypoType.ADJACENT\n        assert result.wrong_char != ''\n\n    def test_create_adjacent_typo_fallback_for_unknown_char(self):\n        \"\"\"Test _create_adjacent_typo falls back for unknown chars.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard\n        from pydoll.constants import TypoType\n\n        # Using a character not in QWERTY_NEIGHBORS\n        result = Keyboard._create_adjacent_typo('@')\n\n        # Should fall back to DOUBLE\n        assert result.typo_type == TypoType.DOUBLE\n        assert result.wrong_char == '@'\n\n    def test_create_adjacent_typo_preserves_case(self):\n        \"\"\"Test _create_adjacent_typo preserves character case.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard\n\n        # Uppercase should return uppercase neighbor\n        result_upper = Keyboard._create_adjacent_typo('A')\n        if result_upper.wrong_char:\n            assert result_upper.wrong_char.isupper()\n\n        # Lowercase should return lowercase neighbor\n        result_lower = Keyboard._create_adjacent_typo('a')\n        if result_lower.wrong_char:\n            assert result_lower.wrong_char.islower()\n\n    def test_create_transpose_typo(self, keyboard_api):\n        \"\"\"Test _create_transpose_typo returns TRANSPOSE type.\"\"\"\n        from pydoll.constants import TypoType\n\n        result = keyboard_api._create_transpose_typo('a', 'b')\n\n        assert result.typo_type == TypoType.TRANSPOSE\n        assert result.wrong_char == 'b'\n\n    def test_create_transpose_typo_fallback_no_next_char(self, keyboard_api):\n        \"\"\"Test _create_transpose_typo falls back when no next char.\"\"\"\n        from pydoll.constants import TypoType\n\n        result = keyboard_api._create_transpose_typo('a', None)\n\n        # Should fall back to ADJACENT\n        assert result.typo_type == TypoType.ADJACENT\n\n    def test_create_missed_space_typo(self, keyboard_api):\n        \"\"\"Test _create_missed_space_typo returns MISSED_SPACE type.\"\"\"\n        from pydoll.constants import TypoType\n\n        result = keyboard_api._create_missed_space_typo(' ')\n\n        assert result.typo_type == TypoType.MISSED_SPACE\n\n    def test_create_missed_space_typo_fallback_non_space(self, keyboard_api):\n        \"\"\"Test _create_missed_space_typo falls back for non-space.\"\"\"\n        from pydoll.constants import TypoType\n\n        result = keyboard_api._create_missed_space_typo('a')\n\n        # Should fall back to ADJACENT\n        assert result.typo_type == TypoType.ADJACENT\n\n    def test_generate_typo_returns_typo_result(self, keyboard_api):\n        \"\"\"Test _generate_typo returns TypoResult.\"\"\"\n        from pydoll.interactions.keyboard import TypoResult\n\n        result = keyboard_api._generate_typo('a', 'b')\n\n        assert isinstance(result, TypoResult)\n\n    def test_create_typo_with_all_types(self, keyboard_api):\n        \"\"\"Test _create_typo handles all TypoType values.\"\"\"\n        from pydoll.constants import TypoType\n        from pydoll.interactions.keyboard import TypoResult\n\n        for typo_type in TypoType:\n            result = keyboard_api._create_typo(typo_type, 'a', 'b')\n            assert isinstance(result, TypoResult)\n\n\nclass TestKeyboardTypoHandling:\n    \"\"\"Test typo handling methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_do_adjacent_typo(self, keyboard_api, mock_tab):\n        \"\"\"Test _do_adjacent_typo types wrong char, backspaces, then correct.\"\"\"\n        await keyboard_api._do_adjacent_typo('a', 's')\n\n        # Should type: 's' (wrong), backspace, 'a' (correct)\n        # That's at least 3 key events (char, down, up, char)\n        assert mock_tab._execute_command.call_count >= 3\n\n    @pytest.mark.asyncio\n    async def test_do_transpose_typo(self, keyboard_api, mock_tab):\n        \"\"\"Test _do_transpose_typo types chars in wrong order then fixes.\"\"\"\n        await keyboard_api._do_transpose_typo('a', 'b')\n\n        # Should type: 'b', 'a', backspace×2, 'a', 'b'\n        # Multiple key events expected\n        assert mock_tab._execute_command.call_count >= 4\n\n    @pytest.mark.asyncio\n    async def test_do_double_typo(self, keyboard_api, mock_tab):\n        \"\"\"Test _do_double_typo types char twice then backspaces.\"\"\"\n        await keyboard_api._do_double_typo('a')\n\n        # Should type: 'a', 'a', backspace\n        assert mock_tab._execute_command.call_count >= 3\n\n    @pytest.mark.asyncio\n    async def test_do_skip_typo(self, keyboard_api, mock_tab):\n        \"\"\"Test _do_skip_typo hesitates then types normally.\"\"\"\n        await keyboard_api._do_skip_typo('a')\n\n        # Should just type 'a' (KEY_DOWN + KEY_UP)\n        assert mock_tab._execute_command.call_count == 2\n        \n        # Verify KEY_DOWN\n        first_call = mock_tab._execute_command.call_args_list[0]\n        assert first_call[0][0]['params']['text'] == 'a'\n        assert first_call[0][0]['params']['type'] == KeyEventType.KEY_DOWN\n\n    @pytest.mark.asyncio\n    async def test_do_missed_space_typo(self, keyboard_api, mock_tab):\n        \"\"\"Test _do_missed_space_typo misses space, fixes, types both.\"\"\"\n        await keyboard_api._do_missed_space_typo(' ', 'w')\n\n        # Should type: 'w', backspace, ' ', 'w'\n        assert mock_tab._execute_command.call_count >= 4\n\n    @pytest.mark.asyncio\n    async def test_handle_typo_adjacent(self, keyboard_api, mock_tab):\n        \"\"\"Test _handle_typo with ADJACENT type.\"\"\"\n        from pydoll.interactions.keyboard import TypoResult\n        from pydoll.constants import TypoType\n\n        typo = TypoResult(typo_type=TypoType.ADJACENT, wrong_char='s')\n        result = await keyboard_api._handle_typo('a', 'b', typo)\n\n        assert result is False  # Should not skip next\n        assert mock_tab._execute_command.call_count >= 1\n\n    @pytest.mark.asyncio\n    async def test_handle_typo_transpose_skips_next(self, keyboard_api, mock_tab):\n        \"\"\"Test _handle_typo with TRANSPOSE type returns True.\"\"\"\n        from pydoll.interactions.keyboard import TypoResult\n        from pydoll.constants import TypoType\n\n        typo = TypoResult(typo_type=TypoType.TRANSPOSE, wrong_char='b')\n        result = await keyboard_api._handle_typo('a', 'b', typo)\n\n        assert result is True  # Should skip next\n\n    @pytest.mark.asyncio\n    async def test_handle_typo_double(self, keyboard_api, mock_tab):\n        \"\"\"Test _handle_typo with DOUBLE type.\"\"\"\n        from pydoll.interactions.keyboard import TypoResult\n        from pydoll.constants import TypoType\n\n        typo = TypoResult(typo_type=TypoType.DOUBLE, wrong_char='a')\n        result = await keyboard_api._handle_typo('a', 'b', typo)\n\n        assert result is False\n\n    @pytest.mark.asyncio\n    async def test_handle_typo_skip(self, keyboard_api, mock_tab):\n        \"\"\"Test _handle_typo with SKIP type.\"\"\"\n        from pydoll.interactions.keyboard import TypoResult\n        from pydoll.constants import TypoType\n\n        typo = TypoResult(typo_type=TypoType.SKIP)\n        result = await keyboard_api._handle_typo('a', 'b', typo)\n\n        assert result is False\n\n    @pytest.mark.asyncio\n    async def test_handle_typo_missed_space_skips_next(self, keyboard_api, mock_tab):\n        \"\"\"Test _handle_typo with MISSED_SPACE type returns True.\"\"\"\n        from pydoll.interactions.keyboard import TypoResult\n        from pydoll.constants import TypoType\n\n        typo = TypoResult(typo_type=TypoType.MISSED_SPACE)\n        result = await keyboard_api._handle_typo(' ', 'w', typo)\n\n        assert result is True  # Should skip next\n\n\nclass TestKeyboardRealisticDelay:\n    \"\"\"Test realistic delay application.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_apply_realistic_delay_basic(self, keyboard_api):\n        \"\"\"Test _apply_realistic_delay doesn't raise.\"\"\"\n        # Just ensure it completes without error\n        await keyboard_api._apply_realistic_delay('a')\n\n    @pytest.mark.asyncio\n    async def test_apply_realistic_delay_with_punctuation(self, keyboard_api):\n        \"\"\"Test _apply_realistic_delay adds extra delay for punctuation.\"\"\"\n        # Just ensure it completes without error for punctuation\n        for char in ' .,!?;:\\n':\n            await keyboard_api._apply_realistic_delay(char)\n\n    def test_pause_chars_constant(self):\n        \"\"\"Test PAUSE_CHARS is properly defined.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard\n\n        assert ' ' in Keyboard.PAUSE_CHARS\n        assert '.' in Keyboard.PAUSE_CHARS\n        assert ',' in Keyboard.PAUSE_CHARS\n        assert '!' in Keyboard.PAUSE_CHARS\n        assert '?' in Keyboard.PAUSE_CHARS\n        assert ';' in Keyboard.PAUSE_CHARS\n        assert ':' in Keyboard.PAUSE_CHARS\n        assert '\\n' in Keyboard.PAUSE_CHARS\n\n\nclass TestKeyboardWithCustomConfig:\n    \"\"\"Test Keyboard with custom configurations.\"\"\"\n\n    def test_keyboard_with_custom_timing(self, mock_tab):\n        \"\"\"Test Keyboard accepts custom timing configuration.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard, TimingConfig\n\n        custom_timing = TimingConfig(\n            keystroke_min=0.1,\n            keystroke_max=0.2,\n        )\n\n        keyboard = Keyboard(mock_tab, timing=custom_timing)\n\n        assert keyboard._timing == custom_timing\n        assert keyboard._timing.keystroke_min == 0.1\n\n    def test_keyboard_with_custom_typo_config(self, mock_tab):\n        \"\"\"Test Keyboard accepts custom typo configuration.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard, TypoConfig\n\n        custom_typo = TypoConfig(\n            adjacent_weight=0.9,\n            transpose_weight=0.1,\n        )\n\n        keyboard = Keyboard(mock_tab, typo_config=custom_typo)\n\n        assert keyboard._typo_config == custom_typo\n        assert keyboard._typo_config.adjacent_weight == 0.9\n\n    def test_keyboard_uses_default_configs(self, mock_tab):\n        \"\"\"Test Keyboard uses default configs if none provided.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard\n\n        keyboard = Keyboard(mock_tab)\n\n        assert keyboard._timing.keystroke_min == 0.03\n        assert keyboard._typo_config.adjacent_weight == 0.55\n\n\nclass TestKeyboardProcessCharWithTypo:\n    \"\"\"Test _process_char_with_typo method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_process_char_no_typo(self, mock_tab):\n        \"\"\"Test _process_char_with_typo when no typo occurs.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard\n        from unittest.mock import patch\n\n        keyboard = Keyboard(mock_tab)\n\n        # Patch _should_make_typo to always return False\n        with patch.object(keyboard, '_should_make_typo', return_value=False):\n            result = await keyboard._process_char_with_typo('a', 'b')\n\n        assert result is False  # Should not skip next\n        assert mock_tab._execute_command.call_count == 2\n\n    @pytest.mark.asyncio\n    async def test_process_char_with_typo(self, mock_tab):\n        \"\"\"Test _process_char_with_typo when typo occurs.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard, TypoResult\n        from pydoll.constants import TypoType\n        from unittest.mock import patch\n\n        keyboard = Keyboard(mock_tab)\n\n        # Patch _should_make_typo to always return True\n        # And _generate_typo to return a SKIP typo (simplest case)\n        with patch.object(keyboard, '_should_make_typo', return_value=True):\n            with patch.object(\n                keyboard,\n                '_generate_typo',\n                return_value=TypoResult(typo_type=TypoType.SKIP),\n            ):\n                result = await keyboard._process_char_with_typo('a', 'b')\n\n        assert result is False  # SKIP doesn't skip next\n\n\nclass TestKeyboardAPIBackwardCompatibility:\n    \"\"\"Test backward compatibility alias.\"\"\"\n\n    def test_keyboard_api_alias(self):\n        \"\"\"Test KeyboardAPI is an alias for Keyboard.\"\"\"\n        from pydoll.interactions.keyboard import Keyboard, KeyboardAPI\n\n        assert KeyboardAPI is Keyboard\n"
  },
  {
    "path": "tests/test_interactions/test_mouse.py",
    "content": "import math\n\nimport pytest\nimport pytest_asyncio\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nfrom pydoll.interactions.mouse import Mouse, MouseAPI, MouseTimingConfig\nfrom pydoll.interactions.utils import (\n    bezier_2d,\n    fitts_duration,\n    minimum_jerk,\n    random_control_points,\n)\nfrom pydoll.protocol.input.types import MouseButton, MouseEventType\n\n\n@pytest_asyncio.fixture\nasync def mock_tab():\n    \"\"\"Mock Tab instance for Mouse tests.\"\"\"\n    tab = MagicMock()\n    tab._execute_command = AsyncMock()\n    return tab\n\n\n@pytest_asyncio.fixture\nasync def mouse(mock_tab):\n    \"\"\"Create Mouse instance with mocked tab.\"\"\"\n    return Mouse(mock_tab)\n\n\n# ── MouseTimingConfig ──────────────────────────────────────────────────\n\n\nclass TestMouseTimingConfig:\n    \"\"\"Test MouseTimingConfig dataclass.\"\"\"\n\n    def test_default_values(self):\n        config = MouseTimingConfig()\n        assert config.fitts_a == 0.070\n        assert config.fitts_b == 0.150\n        assert config.frame_interval == 0.012\n        assert config.frame_interval_variance == 0.004\n        assert config.curvature_min == 0.10\n        assert config.curvature_max == 0.30\n        assert config.curvature_asymmetry == 0.6\n        assert config.short_distance_threshold == 50.0\n        assert config.tremor_amplitude == 1.0\n        assert config.overshoot_probability == 0.70\n        assert config.overshoot_distance_min == 0.03\n        assert config.overshoot_distance_max == 0.12\n        assert config.overshoot_speed_threshold == 200.0\n        assert config.pre_click_pause_min == 0.05\n        assert config.pre_click_pause_max == 0.20\n        assert config.click_hold_min == 0.05\n        assert config.click_hold_max == 0.15\n        assert config.double_click_interval_min == 0.05\n        assert config.double_click_interval_max == 0.10\n        assert config.drag_start_pause_min == 0.08\n        assert config.drag_start_pause_max == 0.20\n        assert config.drag_end_pause_min == 0.05\n        assert config.drag_end_pause_max == 0.15\n        assert config.micro_pause_probability == 0.03\n        assert config.micro_pause_min == 0.015\n        assert config.micro_pause_max == 0.04\n        assert config.min_duration == 0.08\n        assert config.max_duration == 2.5\n\n    def test_custom_values(self):\n        config = MouseTimingConfig(fitts_a=0.1, fitts_b=0.2, tremor_amplitude=2.0)\n        assert config.fitts_a == 0.1\n        assert config.fitts_b == 0.2\n        assert config.tremor_amplitude == 2.0\n\n    def test_frozen_dataclass(self):\n        config = MouseTimingConfig()\n        with pytest.raises(AttributeError):\n            config.fitts_a = 1.0\n\n\n# ── Mouse Initialization ──────────────────────────────────────────────\n\n\nclass TestMouseInitialization:\n    \"\"\"Test Mouse initialization.\"\"\"\n\n    def test_initialization(self, mock_tab):\n        mouse = Mouse(mock_tab)\n        assert mouse._tab == mock_tab\n        assert isinstance(mouse._timing, MouseTimingConfig)\n        assert mouse._position == (0.0, 0.0)\n\n    def test_initialization_with_custom_timing(self, mock_tab):\n        config = MouseTimingConfig(fitts_a=0.1)\n        mouse = Mouse(mock_tab, timing=config)\n        assert mouse._timing.fitts_a == 0.1\n\n    def test_timing_property_getter(self, mock_tab):\n        config = MouseTimingConfig(fitts_a=0.2)\n        mouse = Mouse(mock_tab, timing=config)\n        assert mouse.timing is config\n        assert mouse.timing.fitts_a == 0.2\n\n    def test_timing_property_setter(self, mock_tab):\n        mouse = Mouse(mock_tab)\n        default_timing = mouse.timing\n        new_config = MouseTimingConfig(fitts_a=0.5, tremor_amplitude=2.0)\n        mouse.timing = new_config\n        assert mouse.timing is new_config\n        assert mouse.timing is not default_timing\n        assert mouse.timing.fitts_a == 0.5\n        assert mouse.timing.tremor_amplitude == 2.0\n\n    def test_initial_position_is_origin(self, mock_tab):\n        mouse = Mouse(mock_tab)\n        assert mouse._position == (0.0, 0.0)\n\n\n# ── Mouse.move() ──────────────────────────────────────────────────────\n\n\nclass TestMouseMove:\n    \"\"\"Test Mouse.move() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_move_dispatches_mouse_moved(self, mouse, mock_tab):\n        await mouse.move(100, 200, humanize=False)\n\n        assert mock_tab._execute_command.called\n        command = mock_tab._execute_command.call_args[0][0]\n        assert command['method'] == 'Input.dispatchMouseEvent'\n        assert command['params']['type'] == MouseEventType.MOUSE_MOVED\n        assert command['params']['x'] == 100\n        assert command['params']['y'] == 200\n\n    @pytest.mark.asyncio\n    async def test_move_updates_position(self, mouse):\n        await mouse.move(150, 250, humanize=False)\n        assert mouse._position == (150, 250)\n\n    @pytest.mark.asyncio\n    async def test_move_rounds_float_coordinates(self, mouse, mock_tab):\n        await mouse.move(99.7, 200.3, humanize=False)\n\n        command = mock_tab._execute_command.call_args[0][0]\n        assert command['params']['x'] == 100\n        assert command['params']['y'] == 200\n\n    @pytest.mark.asyncio\n    async def test_move_single_event_when_not_humanized(self, mouse, mock_tab):\n        await mouse.move(100, 200, humanize=False)\n        assert mock_tab._execute_command.call_count == 1\n\n    @pytest.mark.asyncio\n    async def test_move_humanize_delegates_to_humanized(self, mouse):\n        with patch.object(mouse, '_move_humanized', new_callable=AsyncMock) as mock_method:\n            await mouse.move(100, 200, humanize=True)\n            mock_method.assert_called_once_with(100, 200)\n\n\n# ── Mouse.click() ─────────────────────────────────────────────────────\n\n\nclass TestMouseClick:\n    \"\"\"Test Mouse.click() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_click_dispatches_move_press_release(self, mouse, mock_tab):\n        await mouse.click(300, 400, humanize=False)\n\n        # 3 calls: move + pressed + released\n        assert mock_tab._execute_command.call_count == 3\n\n        commands = [call[0][0] for call in mock_tab._execute_command.call_args_list]\n        assert commands[0]['params']['type'] == MouseEventType.MOUSE_MOVED\n        assert commands[1]['params']['type'] == MouseEventType.MOUSE_PRESSED\n        assert commands[2]['params']['type'] == MouseEventType.MOUSE_RELEASED\n\n    @pytest.mark.asyncio\n    async def test_click_left_button_default(self, mouse, mock_tab):\n        await mouse.click(300, 400, humanize=False)\n\n        commands = [call[0][0] for call in mock_tab._execute_command.call_args_list]\n        assert commands[1]['params']['button'] == MouseButton.LEFT\n        assert commands[2]['params']['button'] == MouseButton.LEFT\n\n    @pytest.mark.asyncio\n    async def test_click_right_button(self, mouse, mock_tab):\n        await mouse.click(300, 400, button=MouseButton.RIGHT, humanize=False)\n\n        commands = [call[0][0] for call in mock_tab._execute_command.call_args_list]\n        assert commands[1]['params']['button'] == MouseButton.RIGHT\n\n    @pytest.mark.asyncio\n    async def test_click_with_click_count(self, mouse, mock_tab):\n        await mouse.click(300, 400, click_count=2, humanize=False)\n\n        commands = [call[0][0] for call in mock_tab._execute_command.call_args_list]\n        assert commands[1]['params']['clickCount'] == 2\n        assert commands[2]['params']['clickCount'] == 2\n\n    @pytest.mark.asyncio\n    async def test_click_updates_position(self, mouse):\n        await mouse.click(300, 400, humanize=False)\n        assert mouse._position == (300, 400)\n\n    @pytest.mark.asyncio\n    async def test_click_position_in_press_release(self, mouse, mock_tab):\n        await mouse.click(300, 400, humanize=False)\n\n        commands = [call[0][0] for call in mock_tab._execute_command.call_args_list]\n        assert commands[1]['params']['x'] == 300\n        assert commands[1]['params']['y'] == 400\n        assert commands[2]['params']['x'] == 300\n        assert commands[2]['params']['y'] == 400\n\n    @pytest.mark.asyncio\n    async def test_click_humanize_delegates(self, mouse):\n        with patch.object(mouse, '_click_humanized', new_callable=AsyncMock) as mock_method:\n            await mouse.click(300, 400, humanize=True)\n            mock_method.assert_called_once_with(300, 400, MouseButton.LEFT, 1)\n\n\n# ── Mouse.double_click() ──────────────────────────────────────────────\n\n\nclass TestMouseDoubleClick:\n    \"\"\"Test Mouse.double_click() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_double_click_delegates_to_click(self, mouse):\n        with patch.object(mouse, 'click', new_callable=AsyncMock) as mock_click:\n            await mouse.double_click(500, 600)\n            mock_click.assert_called_once_with(\n                500, 600, button=MouseButton.LEFT, click_count=2, humanize=False\n            )\n\n    @pytest.mark.asyncio\n    async def test_double_click_right_button(self, mouse):\n        with patch.object(mouse, 'click', new_callable=AsyncMock) as mock_click:\n            await mouse.double_click(500, 600, button=MouseButton.RIGHT)\n            mock_click.assert_called_once_with(\n                500, 600, button=MouseButton.RIGHT, click_count=2, humanize=False\n            )\n\n    @pytest.mark.asyncio\n    async def test_double_click_humanized(self, mouse):\n        with patch.object(mouse, 'click', new_callable=AsyncMock) as mock_click:\n            await mouse.double_click(500, 600, humanize=True)\n            mock_click.assert_called_once_with(\n                500, 600, button=MouseButton.LEFT, click_count=2, humanize=True\n            )\n\n\n# ── Mouse.down() ──────────────────────────────────────────────────────\n\n\nclass TestMouseDown:\n    \"\"\"Test Mouse.down() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_down_dispatches_mouse_pressed(self, mouse, mock_tab):\n        await mouse.down()\n\n        command = mock_tab._execute_command.call_args[0][0]\n        assert command['params']['type'] == MouseEventType.MOUSE_PRESSED\n        assert command['params']['button'] == MouseButton.LEFT\n\n    @pytest.mark.asyncio\n    async def test_down_at_current_position(self, mouse, mock_tab):\n        mouse._position = (100.0, 200.0)\n        await mouse.down()\n\n        command = mock_tab._execute_command.call_args[0][0]\n        assert command['params']['x'] == 100\n        assert command['params']['y'] == 200\n\n    @pytest.mark.asyncio\n    async def test_down_with_right_button(self, mouse, mock_tab):\n        await mouse.down(button=MouseButton.RIGHT)\n\n        command = mock_tab._execute_command.call_args[0][0]\n        assert command['params']['button'] == MouseButton.RIGHT\n\n\n# ── Mouse.up() ────────────────────────────────────────────────────────\n\n\nclass TestMouseUp:\n    \"\"\"Test Mouse.up() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_up_dispatches_mouse_released(self, mouse, mock_tab):\n        await mouse.up()\n\n        command = mock_tab._execute_command.call_args[0][0]\n        assert command['params']['type'] == MouseEventType.MOUSE_RELEASED\n        assert command['params']['button'] == MouseButton.LEFT\n\n    @pytest.mark.asyncio\n    async def test_up_at_current_position(self, mouse, mock_tab):\n        mouse._position = (100.0, 200.0)\n        await mouse.up()\n\n        command = mock_tab._execute_command.call_args[0][0]\n        assert command['params']['x'] == 100\n        assert command['params']['y'] == 200\n\n    @pytest.mark.asyncio\n    async def test_up_with_right_button(self, mouse, mock_tab):\n        await mouse.up(button=MouseButton.RIGHT)\n\n        command = mock_tab._execute_command.call_args[0][0]\n        assert command['params']['button'] == MouseButton.RIGHT\n\n\n# ── Mouse.drag() ──────────────────────────────────────────────────────\n\n\nclass TestMouseDrag:\n    \"\"\"Test Mouse.drag() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_drag_dispatches_correct_sequence(self, mouse, mock_tab):\n        await mouse.drag(100, 200, 500, 600, humanize=False)\n\n        assert mock_tab._execute_command.call_count == 4\n        commands = [call[0][0] for call in mock_tab._execute_command.call_args_list]\n\n        # move to start, press, move to end, release\n        assert commands[0]['params']['type'] == MouseEventType.MOUSE_MOVED\n        assert commands[0]['params']['x'] == 100\n        assert commands[0]['params']['y'] == 200\n        assert commands[1]['params']['type'] == MouseEventType.MOUSE_PRESSED\n        assert commands[2]['params']['type'] == MouseEventType.MOUSE_MOVED\n        assert commands[2]['params']['x'] == 500\n        assert commands[2]['params']['y'] == 600\n        assert commands[3]['params']['type'] == MouseEventType.MOUSE_RELEASED\n\n    @pytest.mark.asyncio\n    async def test_drag_updates_position_to_end(self, mouse):\n        await mouse.drag(100, 200, 500, 600, humanize=False)\n        assert mouse._position == (500, 600)\n\n    @pytest.mark.asyncio\n    async def test_drag_uses_left_button(self, mouse, mock_tab):\n        await mouse.drag(100, 200, 500, 600, humanize=False)\n\n        commands = [call[0][0] for call in mock_tab._execute_command.call_args_list]\n        assert commands[1]['params']['button'] == MouseButton.LEFT\n        assert commands[3]['params']['button'] == MouseButton.LEFT\n\n    @pytest.mark.asyncio\n    async def test_drag_humanize_delegates(self, mouse):\n        with patch.object(mouse, '_drag_humanized', new_callable=AsyncMock) as mock_method:\n            await mouse.drag(100, 200, 500, 600, humanize=True)\n            mock_method.assert_called_once_with(100, 200, 500, 600)\n\n\n# ── Helper Functions ──────────────────────────────────────────────────\n\n\nclass TestMinimumJerk:\n    \"\"\"Test minimum_jerk function.\"\"\"\n\n    def test_at_zero(self):\n        assert minimum_jerk(0.0) == pytest.approx(0.0)\n\n    def test_at_one(self):\n        assert minimum_jerk(1.0) == pytest.approx(1.0)\n\n    def test_at_half(self):\n        result = minimum_jerk(0.5)\n        assert result == pytest.approx(0.5, abs=0.01)\n\n    def test_monotonic(self):\n        values = [minimum_jerk(t / 100.0) for t in range(101)]\n        for i in range(len(values) - 1):\n            assert values[i + 1] >= values[i]\n\n    def test_stays_in_range(self):\n        for t in [i / 20.0 for i in range(21)]:\n            result = minimum_jerk(t)\n            assert 0.0 <= result <= 1.0\n\n\nclass TestBezier2D:\n    \"\"\"Test bezier_2d function.\"\"\"\n\n    def test_at_t_zero_returns_p0(self):\n        result = bezier_2d(0.0, (0, 0), (1, 1), (2, 2), (3, 3))\n        assert result == pytest.approx((0, 0))\n\n    def test_at_t_one_returns_p3(self):\n        result = bezier_2d(1.0, (0, 0), (1, 1), (2, 2), (3, 3))\n        assert result == pytest.approx((3, 3))\n\n    def test_straight_line_midpoint(self):\n        result = bezier_2d(0.5, (0, 0), (1, 0), (2, 0), (3, 0))\n        assert result[0] == pytest.approx(1.5, abs=0.01)\n        assert result[1] == pytest.approx(0.0, abs=0.01)\n\n    def test_curved_path(self):\n        result = bezier_2d(0.5, (0, 0), (0, 10), (10, 10), (10, 0))\n        assert 0 < result[0] < 10\n        assert result[1] > 0\n\n\nclass TestFittsDuration:\n    \"\"\"Test fitts_duration function.\"\"\"\n\n    def test_zero_distance(self):\n        result = fitts_duration(0, 20, 0.07, 0.15)\n        assert result == 0.07\n\n    def test_negative_distance(self):\n        result = fitts_duration(-10, 20, 0.07, 0.15)\n        assert result == 0.07\n\n    def test_increases_with_distance(self):\n        d1 = fitts_duration(100, 20, 0.07, 0.15)\n        d2 = fitts_duration(500, 20, 0.07, 0.15)\n        assert d2 > d1\n\n    def test_decreases_with_target_width(self):\n        d1 = fitts_duration(200, 10, 0.07, 0.15)\n        d2 = fitts_duration(200, 50, 0.07, 0.15)\n        assert d1 > d2\n\n    def test_known_value(self):\n        # D=400, W=20: log2(400/20 + 1) = log2(21) ≈ 4.39\n        result = fitts_duration(400, 20, 0.07, 0.15)\n        expected = 0.07 + 0.15 * math.log2(21)\n        assert result == pytest.approx(expected)\n\n\nclass TestRandomControlPoints:\n    \"\"\"Test random_control_points function.\"\"\"\n\n    def _call(self, start, end, config=None):\n        config = config or MouseTimingConfig()\n        return random_control_points(\n            start, end,\n            config.curvature_min, config.curvature_max,\n            config.curvature_asymmetry, config.short_distance_threshold,\n        )\n\n    def test_returns_two_points(self):\n        cp1, cp2 = self._call((0, 0), (100, 0))\n        assert len(cp1) == 2\n        assert len(cp2) == 2\n\n    def test_short_distance_returns_start_end(self):\n        result = self._call((0, 0), (0.5, 0))\n        assert result == ((0, 0), (0.5, 0))\n\n    def test_control_points_not_on_line(self):\n        results = []\n        for _ in range(20):\n            cp1, cp2 = self._call((0, 0), (500, 0))\n            results.append(abs(cp1[1]) > 0 or abs(cp2[1]) > 0)\n        assert any(results)\n\n    def test_short_distance_reduced_curvature(self):\n        short_offsets = []\n        long_offsets = []\n        for _ in range(50):\n            cp1_short, _ = self._call((0, 0), (20, 0))\n            cp1_long, _ = self._call((0, 0), (500, 0))\n            short_offsets.append(abs(cp1_short[1]))\n            long_offsets.append(abs(cp1_long[1]))\n        avg_short = sum(short_offsets) / len(short_offsets)\n        avg_long = sum(long_offsets) / len(long_offsets)\n        assert avg_short < avg_long\n\n\n# ── Tremor Computation ────────────────────────────────────────────────\n\n\nclass TestComputeTremorSigma:\n    \"\"\"Test Mouse._compute_tremor_sigma static method.\"\"\"\n\n    def test_zero_dt_returns_full_amplitude(self):\n        config = MouseTimingConfig(tremor_amplitude=2.0)\n        sigma = Mouse._compute_tremor_sigma(10, 20, 1.0, (5, 10, 1.0), config)\n        assert sigma == 2.0\n\n    def test_high_velocity_reduces_tremor(self):\n        config = MouseTimingConfig(tremor_amplitude=1.0)\n        # High velocity: distance=100px in dt=0.01s -> velocity=10000\n        sigma = Mouse._compute_tremor_sigma(100, 0, 1.01, (0, 0, 1.0), config)\n        assert sigma == pytest.approx(0.2)  # min speed_factor\n\n    def test_low_velocity_high_tremor(self):\n        config = MouseTimingConfig(tremor_amplitude=1.0)\n        # Low velocity: distance=1px in dt=0.1s -> velocity=10\n        sigma = Mouse._compute_tremor_sigma(1, 0, 1.1, (0, 0, 1.0), config)\n        assert sigma > 0.9\n\n\n# ── Humanized Move ────────────────────────────────────────────────────\n\n\nclass TestMouseHumanizedMove:\n    \"\"\"Test Mouse._move_humanized method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_short_distance_single_dispatch(self, mouse, mock_tab):\n        mouse._position = (100, 100)\n        await mouse._move_humanized(100.5, 100.5)\n        assert mock_tab._execute_command.call_count == 1\n\n    @pytest.mark.asyncio\n    async def test_dispatches_multiple_events(self, mouse, mock_tab):\n        # Use fast timing to reduce test runtime\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.02,\n            max_duration=0.05,\n            frame_interval=0.005,\n            overshoot_probability=0.0,\n            micro_pause_probability=0.0,\n        )\n        mouse._position = (0, 0)\n        await mouse._move_humanized(500, 500)\n        # Should have dispatched multiple mouseMoved events\n        assert mock_tab._execute_command.call_count > 2\n\n    @pytest.mark.asyncio\n    async def test_final_position_is_target(self, mouse, mock_tab):\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.02,\n            max_duration=0.05,\n            frame_interval=0.005,\n            overshoot_probability=0.0,\n            micro_pause_probability=0.0,\n        )\n        await mouse._move_humanized(300, 400)\n        assert mouse._position == (300, 400)\n\n    @pytest.mark.asyncio\n    async def test_all_events_are_mouse_moved(self, mouse, mock_tab):\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.02,\n            max_duration=0.05,\n            frame_interval=0.005,\n            overshoot_probability=0.0,\n            micro_pause_probability=0.0,\n        )\n        await mouse._move_humanized(200, 200)\n        for call_item in mock_tab._execute_command.call_args_list:\n            command = call_item[0][0]\n            assert command['params']['type'] == MouseEventType.MOUSE_MOVED\n\n    @pytest.mark.asyncio\n    async def test_longer_distance_more_events(self, mouse, mock_tab):\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.02,\n            max_duration=0.5,\n            frame_interval=0.005,\n            overshoot_probability=0.0,\n            micro_pause_probability=0.0,\n        )\n        await mouse._move_humanized(50, 50)\n        short_count = mock_tab._execute_command.call_count\n\n        mock_tab._execute_command.reset_mock()\n        mouse._position = (0, 0)\n        await mouse._move_humanized(800, 800)\n        long_count = mock_tab._execute_command.call_count\n\n        assert long_count > short_count\n\n    @pytest.mark.asyncio\n    async def test_overshoot_moves_past_target(self, mouse, mock_tab):\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.05,\n            max_duration=0.10,\n            frame_interval=0.005,\n            overshoot_probability=1.0,\n            overshoot_speed_threshold=0,\n            overshoot_distance_min=0.10,\n            overshoot_distance_max=0.15,\n            micro_pause_probability=0.0,\n        )\n        await mouse._move_humanized(500, 0)\n\n        x_coords = [\n            call[0][0]['params']['x']\n            for call in mock_tab._execute_command.call_args_list\n        ]\n        assert max(x_coords) > 500\n\n\n# ── Humanized Click ───────────────────────────────────────────────────\n\n\nclass TestMouseHumanizedClick:\n    \"\"\"Test Mouse._click_humanized method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_includes_move_press_release(self, mouse, mock_tab):\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.01,\n            max_duration=0.02,\n            frame_interval=0.005,\n            overshoot_probability=0.0,\n            micro_pause_probability=0.0,\n            pre_click_pause_min=0.001,\n            pre_click_pause_max=0.001,\n            click_hold_min=0.001,\n            click_hold_max=0.001,\n        )\n        await mouse._click_humanized(300, 400, MouseButton.LEFT, 1)\n\n        event_types = [\n            call[0][0]['params']['type']\n            for call in mock_tab._execute_command.call_args_list\n        ]\n        # Should contain: multiple MOUSE_MOVED, then MOUSE_PRESSED, then MOUSE_RELEASED\n        assert MouseEventType.MOUSE_PRESSED in event_types\n        assert MouseEventType.MOUSE_RELEASED in event_types\n        moved_count = event_types.count(MouseEventType.MOUSE_MOVED)\n        assert moved_count >= 1\n\n    @pytest.mark.asyncio\n    async def test_double_click_has_two_press_release_pairs(self, mouse, mock_tab):\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.01,\n            max_duration=0.02,\n            frame_interval=0.005,\n            overshoot_probability=0.0,\n            micro_pause_probability=0.0,\n            pre_click_pause_min=0.001,\n            pre_click_pause_max=0.001,\n            click_hold_min=0.001,\n            click_hold_max=0.001,\n            double_click_interval_min=0.001,\n            double_click_interval_max=0.001,\n        )\n        await mouse._click_humanized(300, 400, MouseButton.LEFT, 2)\n\n        event_types = [\n            call[0][0]['params']['type']\n            for call in mock_tab._execute_command.call_args_list\n        ]\n        assert event_types.count(MouseEventType.MOUSE_PRESSED) == 2\n        assert event_types.count(MouseEventType.MOUSE_RELEASED) == 2\n\n    @pytest.mark.asyncio\n    async def test_click_count_in_commands(self, mouse, mock_tab):\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.01,\n            max_duration=0.02,\n            frame_interval=0.005,\n            overshoot_probability=0.0,\n            micro_pause_probability=0.0,\n            pre_click_pause_min=0.001,\n            pre_click_pause_max=0.001,\n            click_hold_min=0.001,\n            click_hold_max=0.001,\n            double_click_interval_min=0.001,\n            double_click_interval_max=0.001,\n        )\n        await mouse._click_humanized(300, 400, MouseButton.LEFT, 2)\n\n        press_commands = [\n            call[0][0] for call in mock_tab._execute_command.call_args_list\n            if call[0][0]['params']['type'] == MouseEventType.MOUSE_PRESSED\n        ]\n        assert press_commands[0]['params']['clickCount'] == 1\n        assert press_commands[1]['params']['clickCount'] == 2\n\n    @pytest.mark.asyncio\n    async def test_click_lands_at_exact_position(self, mouse, mock_tab):\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.01,\n            max_duration=0.02,\n            frame_interval=0.005,\n            overshoot_probability=0.0,\n            micro_pause_probability=0.0,\n            pre_click_pause_min=0.001,\n            pre_click_pause_max=0.001,\n            click_hold_min=0.001,\n            click_hold_max=0.001,\n        )\n        await mouse._click_humanized(300, 400, MouseButton.LEFT, 1)\n\n        press_commands = [\n            call[0][0] for call in mock_tab._execute_command.call_args_list\n            if call[0][0]['params']['type'] == MouseEventType.MOUSE_PRESSED\n        ]\n        # Click must land at the exact target position\n        assert press_commands[0]['params']['x'] == 300\n        assert press_commands[0]['params']['y'] == 400\n\n\n# ── Humanized Drag ────────────────────────────────────────────────────\n\n\nclass TestMouseHumanizedDrag:\n    \"\"\"Test Mouse._drag_humanized method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_drag_includes_press_and_release(self, mouse, mock_tab):\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.01,\n            max_duration=0.02,\n            frame_interval=0.005,\n            overshoot_probability=0.0,\n            micro_pause_probability=0.0,\n            drag_start_pause_min=0.001,\n            drag_start_pause_max=0.001,\n            drag_end_pause_min=0.001,\n            drag_end_pause_max=0.001,\n        )\n        await mouse._drag_humanized(100, 200, 500, 600)\n\n        event_types = [\n            call[0][0]['params']['type']\n            for call in mock_tab._execute_command.call_args_list\n        ]\n        assert MouseEventType.MOUSE_PRESSED in event_types\n        assert MouseEventType.MOUSE_RELEASED in event_types\n        assert event_types.count(MouseEventType.MOUSE_MOVED) >= 2\n\n    @pytest.mark.asyncio\n    async def test_drag_ends_at_target(self, mouse, mock_tab):\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.01,\n            max_duration=0.02,\n            frame_interval=0.005,\n            overshoot_probability=0.0,\n            micro_pause_probability=0.0,\n            drag_start_pause_min=0.001,\n            drag_start_pause_max=0.001,\n            drag_end_pause_min=0.001,\n            drag_end_pause_max=0.001,\n        )\n        await mouse._drag_humanized(100, 200, 500, 600)\n        assert mouse._position == (500, 600)\n\n    @pytest.mark.asyncio\n    async def test_drag_press_before_release(self, mouse, mock_tab):\n        mouse._timing = MouseTimingConfig(\n            min_duration=0.01,\n            max_duration=0.02,\n            frame_interval=0.005,\n            overshoot_probability=0.0,\n            micro_pause_probability=0.0,\n            drag_start_pause_min=0.001,\n            drag_start_pause_max=0.001,\n            drag_end_pause_min=0.001,\n            drag_end_pause_max=0.001,\n        )\n        await mouse._drag_humanized(100, 200, 500, 600)\n\n        event_types = [\n            call[0][0]['params']['type']\n            for call in mock_tab._execute_command.call_args_list\n        ]\n        press_idx = event_types.index(MouseEventType.MOUSE_PRESSED)\n        release_idx = len(event_types) - 1 - event_types[::-1].index(MouseEventType.MOUSE_RELEASED)\n        assert press_idx < release_idx\n\n\n# ── Dispatch Helpers ──────────────────────────────────────────────────\n\n\nclass TestDispatchMove:\n    \"\"\"Test Mouse._dispatch_move method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_dispatches_correct_command(self, mouse, mock_tab):\n        await mouse._dispatch_move(150.7, 250.3)\n\n        command = mock_tab._execute_command.call_args[0][0]\n        assert command['method'] == 'Input.dispatchMouseEvent'\n        assert command['params']['type'] == MouseEventType.MOUSE_MOVED\n        assert command['params']['x'] == 151\n        assert command['params']['y'] == 250\n\n    @pytest.mark.asyncio\n    async def test_updates_position_with_float(self, mouse):\n        await mouse._dispatch_move(150.7, 250.3)\n        assert mouse._position == (150.7, 250.3)\n\n\nclass TestDispatchButton:\n    \"\"\"Test Mouse._dispatch_button method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_dispatches_pressed(self, mouse, mock_tab):\n        mouse._position = (100.0, 200.0)\n        await mouse._dispatch_button(MouseEventType.MOUSE_PRESSED, MouseButton.LEFT, 1)\n\n        command = mock_tab._execute_command.call_args[0][0]\n        assert command['params']['type'] == MouseEventType.MOUSE_PRESSED\n        assert command['params']['button'] == MouseButton.LEFT\n        assert command['params']['clickCount'] == 1\n        assert command['params']['x'] == 100\n        assert command['params']['y'] == 200\n\n    @pytest.mark.asyncio\n    async def test_dispatches_released(self, mouse, mock_tab):\n        mouse._position = (100.0, 200.0)\n        await mouse._dispatch_button(MouseEventType.MOUSE_RELEASED, MouseButton.LEFT)\n\n        command = mock_tab._execute_command.call_args[0][0]\n        assert command['params']['type'] == MouseEventType.MOUSE_RELEASED\n\n\n# ── Backward Compatibility ────────────────────────────────────────────\n\n\nclass TestMouseAPIAlias:\n    \"\"\"Test MouseAPI backward compatibility alias.\"\"\"\n\n    def test_mouse_api_is_mouse(self):\n        assert MouseAPI is Mouse\n\n\n# ── Tab Integration ───────────────────────────────────────────────────\n\n\nclass TestTabMouseProperty:\n    \"\"\"Test tab.mouse lazy property.\"\"\"\n\n    def test_tab_mouse_property_exists(self):\n        from pydoll.browser.tab import Tab\n        assert hasattr(Tab, 'mouse')\n\n    def test_tab_mouse_returns_mouse_api(self):\n        from pydoll.interactions import MouseAPI\n        tab = MagicMock()\n        tab._execute_command = AsyncMock()\n        tab._mouse = None\n        # Access the property descriptor directly\n        mouse_obj = MouseAPI(tab)\n        assert isinstance(mouse_obj, MouseAPI)\n"
  },
  {
    "path": "tests/test_interactions/test_scroll.py",
    "content": "import pytest\nimport pytest_asyncio\nfrom unittest.mock import AsyncMock, MagicMock, patch, call\n\nfrom pydoll.interactions.scroll import ScrollAPI\nfrom pydoll.constants import ScrollPosition, Scripts\nfrom pydoll.commands import RuntimeCommands\n\n\n@pytest_asyncio.fixture\nasync def mock_tab():\n    \"\"\"Mock Tab instance for ScrollAPI tests.\"\"\"\n    tab = MagicMock()\n    tab._execute_command = AsyncMock()\n    return tab\n\n\n@pytest_asyncio.fixture\nasync def scroll_api(mock_tab):\n    \"\"\"Create ScrollAPI instance with mocked tab.\"\"\"\n    return ScrollAPI(mock_tab)\n\n\nclass TestScrollAPIInitialization:\n    \"\"\"Test ScrollAPI initialization.\"\"\"\n\n    def test_initialization(self, mock_tab):\n        \"\"\"Test ScrollAPI is properly initialized with tab.\"\"\"\n        scroll_api = ScrollAPI(mock_tab)\n        assert scroll_api._tab == mock_tab\n\n\nclass TestScrollAPIBy:\n    \"\"\"Test scroll.by() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_scroll_down_smooth(self, scroll_api, mock_tab):\n        \"\"\"Test scrolling down with smooth animation.\"\"\"\n        await scroll_api.by(ScrollPosition.DOWN, 500, smooth=True, humanize=False)\n\n        # Verify execute_command was called\n        assert mock_tab._execute_command.called\n        call_args = mock_tab._execute_command.call_args\n\n        # Verify the command is RuntimeCommands.evaluate with await_promise=True\n        command = call_args[0][0]\n        assert command['method'] == 'Runtime.evaluate'\n        assert command['params']['awaitPromise'] is True\n\n        # Verify the script contains expected values\n        script = command['params']['expression']\n        assert 'top: 500' in script\n        assert \"behavior: 'smooth'\" in script\n\n    @pytest.mark.asyncio\n    async def test_scroll_up_smooth(self, scroll_api, mock_tab):\n        \"\"\"Test scrolling up with smooth animation.\"\"\"\n        await scroll_api.by(ScrollPosition.UP, 300, smooth=True, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n        script = command['params']['expression']\n\n        assert 'top: -300' in script\n        assert \"behavior: 'smooth'\" in script\n\n    @pytest.mark.asyncio\n    async def test_scroll_right_smooth(self, scroll_api, mock_tab):\n        \"\"\"Test scrolling right with smooth animation.\"\"\"\n        await scroll_api.by(ScrollPosition.RIGHT, 200, smooth=True, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n        script = command['params']['expression']\n\n        assert 'left: 200' in script\n        assert \"behavior: 'smooth'\" in script\n\n    @pytest.mark.asyncio\n    async def test_scroll_left_smooth(self, scroll_api, mock_tab):\n        \"\"\"Test scrolling left with smooth animation.\"\"\"\n        await scroll_api.by(ScrollPosition.LEFT, 150, smooth=True, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n        script = command['params']['expression']\n\n        assert 'left: -150' in script\n        assert \"behavior: 'smooth'\" in script\n\n    @pytest.mark.asyncio\n    async def test_scroll_down_instant(self, scroll_api, mock_tab):\n        \"\"\"Test scrolling down without smooth animation.\"\"\"\n        await scroll_api.by(ScrollPosition.DOWN, 1000, smooth=False, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n        script = command['params']['expression']\n\n        assert 'top: 1000' in script\n        assert \"behavior: 'auto'\" in script\n\n    @pytest.mark.asyncio\n    async def test_scroll_with_float_distance(self, scroll_api, mock_tab):\n        \"\"\"Test scrolling with float distance.\"\"\"\n        await scroll_api.by(ScrollPosition.DOWN, 250.5, smooth=True, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n        script = command['params']['expression']\n\n        assert 'top: 250.5' in script\n\n\nclass TestScrollAPIToTop:\n    \"\"\"Test scroll.to_top() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_scroll_to_top_smooth(self, scroll_api, mock_tab):\n        \"\"\"Test scrolling to top with smooth animation.\"\"\"\n        await scroll_api.to_top(smooth=True, humanize=False)\n\n        assert mock_tab._execute_command.called\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n\n        assert command['method'] == 'Runtime.evaluate'\n        assert command['params']['awaitPromise'] is True\n\n        script = command['params']['expression']\n        assert 'top: 0' in script\n        assert \"behavior: 'smooth'\" in script\n\n    @pytest.mark.asyncio\n    async def test_scroll_to_top_instant(self, scroll_api, mock_tab):\n        \"\"\"Test scrolling to top without smooth animation.\"\"\"\n        await scroll_api.to_top(smooth=False, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n        script = command['params']['expression']\n\n        assert 'top: 0' in script\n        assert \"behavior: 'auto'\" in script\n\n\nclass TestScrollAPIToBottom:\n    \"\"\"Test scroll.to_bottom() method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_scroll_to_bottom_smooth(self, scroll_api, mock_tab):\n        \"\"\"Test scrolling to bottom with smooth animation.\"\"\"\n        await scroll_api.to_bottom(smooth=True, humanize=False)\n\n        assert mock_tab._execute_command.called\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n\n        assert command['method'] == 'Runtime.evaluate'\n        assert command['params']['awaitPromise'] is True\n\n        script = command['params']['expression']\n        assert 'top: document.body.scrollHeight' in script\n        assert \"behavior: 'smooth'\" in script\n\n    @pytest.mark.asyncio\n    async def test_scroll_to_bottom_instant(self, scroll_api, mock_tab):\n        \"\"\"Test scrolling to bottom without smooth animation.\"\"\"\n        await scroll_api.to_bottom(smooth=False, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n        script = command['params']['expression']\n\n        assert 'top: document.body.scrollHeight' in script\n        assert \"behavior: 'auto'\" in script\n\n\nclass TestScrollAPIHelperMethods:\n    \"\"\"Test ScrollAPI private helper methods.\"\"\"\n\n    def test_get_axis_and_distance_down(self, scroll_api):\n        \"\"\"Test _get_axis_and_distance for DOWN direction.\"\"\"\n        axis, distance = scroll_api._get_axis_and_distance(ScrollPosition.DOWN, 100)\n        assert axis == 'top'\n        assert distance == 100\n\n    def test_get_axis_and_distance_up(self, scroll_api):\n        \"\"\"Test _get_axis_and_distance for UP direction.\"\"\"\n        axis, distance = scroll_api._get_axis_and_distance(ScrollPosition.UP, 100)\n        assert axis == 'top'\n        assert distance == -100\n\n    def test_get_axis_and_distance_right(self, scroll_api):\n        \"\"\"Test _get_axis_and_distance for RIGHT direction.\"\"\"\n        axis, distance = scroll_api._get_axis_and_distance(ScrollPosition.RIGHT, 50)\n        assert axis == 'left'\n        assert distance == 50\n\n    def test_get_axis_and_distance_left(self, scroll_api):\n        \"\"\"Test _get_axis_and_distance for LEFT direction.\"\"\"\n        axis, distance = scroll_api._get_axis_and_distance(ScrollPosition.LEFT, 50)\n        assert axis == 'left'\n        assert distance == -50\n\n    def test_get_behavior_smooth(self, scroll_api):\n        \"\"\"Test _get_behavior with smooth=True.\"\"\"\n        behavior = scroll_api._get_behavior(True)\n        assert behavior == 'smooth'\n\n    def test_get_behavior_instant(self, scroll_api):\n        \"\"\"Test _get_behavior with smooth=False.\"\"\"\n        behavior = scroll_api._get_behavior(False)\n        assert behavior == 'auto'\n\n\nclass TestScrollAPIIntegrationWithTab:\n    \"\"\"Test ScrollAPI integration with Tab.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_tab_has_scroll_property(self):\n        \"\"\"Test that Tab has scroll property.\"\"\"\n        with patch('pydoll.connection.ConnectionHandler', autospec=True):\n            from pydoll.browser.tab import Tab\n\n            mock_browser = MagicMock()\n            tab = Tab(mock_browser, target_id='test-id')\n\n            # Access scroll property\n            scroll = tab.scroll\n\n            # Verify it's a ScrollAPI instance\n            assert isinstance(scroll, ScrollAPI)\n            assert scroll._tab == tab\n\n    @pytest.mark.asyncio\n    async def test_tab_scroll_property_is_lazy(self):\n        \"\"\"Test that scroll property is created lazily.\"\"\"\n        with patch('pydoll.connection.ConnectionHandler', autospec=True):\n            from pydoll.browser.tab import Tab\n\n            mock_browser = MagicMock()\n            tab = Tab(mock_browser, target_id='test-id')\n\n            # Initially None\n            assert tab._scroll is None\n\n            # Access creates instance\n            scroll1 = tab.scroll\n            assert tab._scroll is not None\n\n            # Second access returns same instance\n            scroll2 = tab.scroll\n            assert scroll1 is scroll2\n\n    @pytest.mark.asyncio\n    async def test_scroll_execute_command_integration(self):\n        \"\"\"Test that scroll methods properly call tab._execute_command.\"\"\"\n        with patch('pydoll.connection.ConnectionHandler', autospec=True):\n            from pydoll.browser.tab import Tab\n\n            mock_browser = MagicMock()\n            tab = Tab(mock_browser, target_id='test-id')\n            tab._execute_command = AsyncMock()\n\n            # Call scroll method\n            await tab.scroll.by(ScrollPosition.DOWN, 500, smooth=True, humanize=False)\n\n            # Verify _execute_command was called\n            assert tab._execute_command.called\n\n            # Verify command structure\n            call_args = tab._execute_command.call_args\n            command = call_args[0][0]\n            assert command['method'] == 'Runtime.evaluate'\n            assert command['params']['awaitPromise'] is True\n\n\nclass TestScrollAPIScriptGeneration:\n    \"\"\"Test that correct JavaScript scripts are generated.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_scroll_by_script_structure(self, scroll_api, mock_tab):\n        \"\"\"Test that scroll.by generates correct script structure.\"\"\"\n        await scroll_api.by(ScrollPosition.DOWN, 500, smooth=True, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n        script = command['params']['expression']\n\n        # Verify script has Promise structure\n        assert 'new Promise' in script or 'Promise' in script\n        assert 'scrollend' in script or 'scrollBy' in script\n        assert 'resolve' in script\n\n    @pytest.mark.asyncio\n    async def test_to_top_script_structure(self, scroll_api, mock_tab):\n        \"\"\"Test that scroll.to_top generates correct script structure.\"\"\"\n        await scroll_api.to_top(smooth=True, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n        script = command['params']['expression']\n\n        # Verify script has Promise structure and scrollTo\n        assert 'new Promise' in script or 'Promise' in script\n        assert 'scrollTo' in script\n        assert 'top: 0' in script\n\n    @pytest.mark.asyncio\n    async def test_to_bottom_script_structure(self, scroll_api, mock_tab):\n        \"\"\"Test that scroll.to_bottom generates correct script structure.\"\"\"\n        await scroll_api.to_bottom(smooth=True, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n        script = command['params']['expression']\n\n        # Verify script has Promise structure and scrollHeight\n        assert 'new Promise' in script or 'Promise' in script\n        assert 'scrollTo' in script\n        assert 'scrollHeight' in script\n\n\nclass TestScrollAPIAwaitPromise:\n    \"\"\"Test that awaitPromise parameter is correctly set.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_scroll_by_uses_await_promise(self, scroll_api, mock_tab):\n        \"\"\"Test that scroll.by uses awaitPromise parameter.\"\"\"\n        await scroll_api.by(ScrollPosition.DOWN, 100, smooth=True, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n\n        # Verify awaitPromise is True\n        assert command['params']['awaitPromise'] is True\n\n    @pytest.mark.asyncio\n    async def test_to_top_uses_await_promise(self, scroll_api, mock_tab):\n        \"\"\"Test that scroll.to_top uses awaitPromise parameter.\"\"\"\n        await scroll_api.to_top(smooth=True, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n\n        assert command['params']['awaitPromise'] is True\n\n    @pytest.mark.asyncio\n    async def test_to_bottom_uses_await_promise(self, scroll_api, mock_tab):\n        \"\"\"Test that scroll.to_bottom uses awaitPromise parameter.\"\"\"\n        await scroll_api.to_bottom(smooth=True, humanize=False)\n\n        call_args = mock_tab._execute_command.call_args\n        command = call_args[0][0]\n\n        assert command['params']['awaitPromise'] is True\n\n\nclass TestScrollTimingConfig:\n    \"\"\"Test ScrollTimingConfig dataclass.\"\"\"\n\n    def test_default_values(self):\n        \"\"\"Test default configuration values.\"\"\"\n        from pydoll.interactions.scroll import ScrollTimingConfig\n\n        config = ScrollTimingConfig()\n\n        assert config.min_duration == 0.5\n        assert config.max_duration == 1.5\n        assert config.bezier_points == (0.645, 0.045, 0.355, 1.0)\n        assert config.frame_interval == 0.012\n        assert config.delta_jitter == 3\n        assert config.micro_pause_probability == 0.05\n        assert config.micro_pause_min == 0.02\n        assert config.micro_pause_max == 0.05\n        assert config.overshoot_probability == 0.15\n        assert config.overshoot_factor_min == 1.02\n        assert config.overshoot_factor_max == 1.08\n\n    def test_custom_values(self):\n        \"\"\"Test custom configuration values.\"\"\"\n        from pydoll.interactions.scroll import ScrollTimingConfig\n\n        config = ScrollTimingConfig(\n            min_duration=0.3,\n            max_duration=2.0,\n            bezier_points=(0.5, 0.0, 0.5, 1.0),\n            frame_interval=0.016,\n            delta_jitter=5,\n            micro_pause_probability=0.1,\n            overshoot_probability=0.2,\n        )\n\n        assert config.min_duration == 0.3\n        assert config.max_duration == 2.0\n        assert config.bezier_points == (0.5, 0.0, 0.5, 1.0)\n        assert config.frame_interval == 0.016\n        assert config.delta_jitter == 5\n        assert config.micro_pause_probability == 0.1\n        assert config.overshoot_probability == 0.2\n\n    def test_frozen_dataclass(self):\n        \"\"\"Test that config is immutable (frozen).\"\"\"\n        from pydoll.interactions.scroll import ScrollTimingConfig\n\n        config = ScrollTimingConfig()\n\n        with pytest.raises(AttributeError):\n            config.min_duration = 1.0\n\n\nclass TestCubicBezier:\n    \"\"\"Test CubicBezier curve solver.\"\"\"\n\n    def test_initialization(self):\n        \"\"\"Test CubicBezier initialization with control points.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        bezier = CubicBezier(0.25, 0.1, 0.25, 1.0)\n\n        # Verify coefficients are calculated\n        assert hasattr(bezier, 'coefficient_a_x')\n        assert hasattr(bezier, 'coefficient_b_x')\n        assert hasattr(bezier, 'coefficient_c_x')\n        assert hasattr(bezier, 'coefficient_a_y')\n        assert hasattr(bezier, 'coefficient_b_y')\n        assert hasattr(bezier, 'coefficient_c_y')\n\n    def test_sample_curve_x_at_zero(self):\n        \"\"\"Test sample_curve_x returns 0 at t=0.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        bezier = CubicBezier(0.25, 0.1, 0.25, 1.0)\n\n        assert bezier.sample_curve_x(0.0) == 0.0\n\n    def test_sample_curve_x_at_one(self):\n        \"\"\"Test sample_curve_x returns 1 at t=1.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        bezier = CubicBezier(0.25, 0.1, 0.25, 1.0)\n\n        assert abs(bezier.sample_curve_x(1.0) - 1.0) < 1e-10\n\n    def test_sample_curve_y_at_zero(self):\n        \"\"\"Test sample_curve_y returns 0 at t=0.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        bezier = CubicBezier(0.25, 0.1, 0.25, 1.0)\n\n        assert bezier.sample_curve_y(0.0) == 0.0\n\n    def test_sample_curve_y_at_one(self):\n        \"\"\"Test sample_curve_y returns 1 at t=1.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        bezier = CubicBezier(0.25, 0.1, 0.25, 1.0)\n\n        assert abs(bezier.sample_curve_y(1.0) - 1.0) < 1e-10\n\n    def test_sample_curve_derivative_x(self):\n        \"\"\"Test sample_curve_derivative_x returns derivative.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        bezier = CubicBezier(0.25, 0.1, 0.25, 1.0)\n\n        # Derivative at t=0 should equal coefficient_c_x\n        assert bezier.sample_curve_derivative_x(0.0) == bezier.coefficient_c_x\n\n    def test_solve_curve_x_finds_t_for_x(self):\n        \"\"\"Test solve_curve_x finds t value for given x.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        bezier = CubicBezier(0.25, 0.1, 0.25, 1.0)\n\n        # For x=0, t should be 0\n        assert abs(bezier.solve_curve_x(0.0)) < 1e-6\n\n        # For x=1, t should be 1\n        assert abs(bezier.solve_curve_x(1.0) - 1.0) < 1e-6\n\n    def test_solve_returns_y_for_given_x(self):\n        \"\"\"Test solve returns y value for given x (time).\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        bezier = CubicBezier(0.25, 0.1, 0.25, 1.0)\n\n        # At x=0, y should be 0\n        assert abs(bezier.solve(0.0)) < 1e-6\n\n        # At x=1, y should be 1\n        assert abs(bezier.solve(1.0) - 1.0) < 1e-6\n\n    def test_solve_returns_values_between_0_and_1(self):\n        \"\"\"Test solve returns values in valid range for valid inputs.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        bezier = CubicBezier(0.645, 0.045, 0.355, 1.0)\n\n        for x in [0.1, 0.25, 0.5, 0.75, 0.9]:\n            y = bezier.solve(x)\n            assert 0.0 <= y <= 1.0, f\"y={y} out of range for x={x}\"\n\n    def test_solve_curve_x_with_out_of_range_values(self):\n        \"\"\"Test solve_curve_x behavior with out of range values.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        bezier = CubicBezier(0.25, 0.1, 0.25, 1.0)\n\n        # Newton's method will try to find t even for out-of-range x values\n        # Just verify it returns a numeric result without crashing\n        result_negative = bezier.solve_curve_x(-0.5)\n        assert isinstance(result_negative, float)\n\n        result_over_one = bezier.solve_curve_x(1.5)\n        assert isinstance(result_over_one, float)\n\n    def test_ease_in_out_bezier(self):\n        \"\"\"Test standard ease-in-out bezier curve.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        # Standard CSS ease-in-out\n        bezier = CubicBezier(0.42, 0.0, 0.58, 1.0)\n\n        # At midpoint (x=0.5), y should be approximately 0.5\n        y_at_half = bezier.solve(0.5)\n        assert 0.4 <= y_at_half <= 0.6\n\n    def test_linear_bezier(self):\n        \"\"\"Test linear bezier curve (identity).\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        # Linear: control points on the diagonal\n        bezier = CubicBezier(0.0, 0.0, 1.0, 1.0)\n\n        # Should be approximately linear\n        for x in [0.1, 0.3, 0.5, 0.7, 0.9]:\n            y = bezier.solve(x)\n            assert abs(y - x) < 0.1, f\"Expected y≈{x}, got {y}\"\n\n\nclass TestScrollHumanizedMethods:\n    \"\"\"Test humanized scroll methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_scroll_by_with_humanize_true(self, mock_tab):\n        \"\"\"Test scroll.by with humanize=True calls _scroll_humanized.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n\n        scroll = Scroll(mock_tab)\n\n        # Mock _scroll_humanized\n        scroll._scroll_humanized = AsyncMock()\n\n        await scroll.by(ScrollPosition.DOWN, 500, humanize=True)\n\n        scroll._scroll_humanized.assert_called_once_with(ScrollPosition.DOWN, 500)\n\n    @pytest.mark.asyncio\n    async def test_scroll_to_top_with_humanize_true(self, mock_tab):\n        \"\"\"Test scroll.to_top with humanize=True calls _scroll_to_end_humanized.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n\n        scroll = Scroll(mock_tab)\n\n        # Mock _scroll_to_end_humanized\n        scroll._scroll_to_end_humanized = AsyncMock()\n\n        await scroll.to_top(humanize=True)\n\n        scroll._scroll_to_end_humanized.assert_called_once_with(ScrollPosition.UP)\n\n    @pytest.mark.asyncio\n    async def test_scroll_to_bottom_with_humanize_true(self, mock_tab):\n        \"\"\"Test scroll.to_bottom with humanize=True calls _scroll_to_end_humanized.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n\n        scroll = Scroll(mock_tab)\n\n        # Mock _scroll_to_end_humanized\n        scroll._scroll_to_end_humanized = AsyncMock()\n\n        await scroll.to_bottom(humanize=True)\n\n        scroll._scroll_to_end_humanized.assert_called_once_with(ScrollPosition.DOWN)\n\n    @pytest.mark.asyncio\n    async def test_calculate_effective_distance_without_overshoot(self, mock_tab):\n        \"\"\"Test _calculate_effective_distance without overshoot.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n\n        # Config with 0% overshoot probability\n        config = ScrollTimingConfig(overshoot_probability=0.0)\n        scroll = Scroll(mock_tab, timing=config)\n\n        distance = scroll._calculate_effective_distance(100.0)\n\n        assert distance == 100.0\n\n    @pytest.mark.asyncio\n    async def test_calculate_effective_distance_with_overshoot(self, mock_tab):\n        \"\"\"Test _calculate_effective_distance with overshoot.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n\n        # Config with 100% overshoot probability\n        config = ScrollTimingConfig(\n            overshoot_probability=1.0,\n            overshoot_factor_min=1.1,\n            overshoot_factor_max=1.2,\n        )\n        scroll = Scroll(mock_tab, timing=config)\n\n        distance = scroll._calculate_effective_distance(100.0)\n\n        # Should be between 110 and 120\n        assert 110.0 <= distance <= 120.0\n\n    @pytest.mark.asyncio\n    async def test_calculate_duration(self, mock_tab):\n        \"\"\"Test _calculate_duration returns value in expected range.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n\n        config = ScrollTimingConfig(min_duration=0.5, max_duration=1.5)\n        scroll = Scroll(mock_tab, timing=config)\n\n        duration = scroll._calculate_duration(500.0)\n\n        # Should be between min_duration and capped max (3.0)\n        assert 0.5 <= duration <= 3.0\n\n    @pytest.mark.asyncio\n    async def test_calculate_duration_increases_with_distance(self, mock_tab):\n        \"\"\"Test that longer distances result in longer durations.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n        from unittest.mock import patch\n\n        config = ScrollTimingConfig(min_duration=0.5, max_duration=1.5)\n        scroll = Scroll(mock_tab, timing=config)\n\n        # Patch random.uniform to return a constant base duration\n        # This ensures we are testing only the distance scaling logic\n        with patch('random.uniform', return_value=1.0):\n            short_duration = scroll._calculate_duration(100.0)\n            long_duration = scroll._calculate_duration(5000.0)\n\n        # With constant base duration, the formula ensures longer distance -> longer duration\n        # Formula: base_duration * (1 + 0.2 * (distance / 1000))\n        assert long_duration > short_duration\n\n    @pytest.mark.asyncio\n    async def test_get_viewport_center(self, mock_tab):\n        \"\"\"Test _get_viewport_center returns coordinates.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n\n        mock_tab._execute_command.return_value = {\n            'result': {'result': {'value': '[800, 600]'}}\n        }\n\n        scroll = Scroll(mock_tab)\n        result = await scroll._get_viewport_center()\n\n        assert result == (800, 600)\n\n    @pytest.mark.asyncio\n    async def test_get_viewport_center_fallback(self, mock_tab):\n        \"\"\"Test _get_viewport_center returns fallback on error.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n\n        mock_tab._execute_command.return_value = {\n            'result': {'result': {'value': 'invalid'}}\n        }\n\n        scroll = Scroll(mock_tab)\n        result = await scroll._get_viewport_center()\n\n        # Should return fallback values\n        assert result == (400, 300)\n\n    @pytest.mark.asyncio\n    async def test_get_viewport_center_empty_response(self, mock_tab):\n        \"\"\"Test _get_viewport_center handles empty response.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n\n        mock_tab._execute_command.return_value = {}\n\n        scroll = Scroll(mock_tab)\n        result = await scroll._get_viewport_center()\n\n        assert result == (400, 300)\n\n    @pytest.mark.asyncio\n    async def test_get_current_scroll_y(self, mock_tab):\n        \"\"\"Test _get_current_scroll_y returns scroll position.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n\n        mock_tab._execute_command.return_value = {\n            'result': {'result': {'value': 250}}\n        }\n\n        scroll = Scroll(mock_tab)\n        result = await scroll._get_current_scroll_y()\n\n        assert result == 250.0\n\n    @pytest.mark.asyncio\n    async def test_get_current_scroll_y_default(self, mock_tab):\n        \"\"\"Test _get_current_scroll_y returns 0 on missing value.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n\n        mock_tab._execute_command.return_value = {}\n\n        scroll = Scroll(mock_tab)\n        result = await scroll._get_current_scroll_y()\n\n        assert result == 0.0\n\n    @pytest.mark.asyncio\n    async def test_get_remaining_scroll_to_bottom(self, mock_tab):\n        \"\"\"Test _get_remaining_scroll_to_bottom returns remaining distance.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n\n        mock_tab._execute_command.return_value = {\n            'result': {'result': {'value': 1500}}\n        }\n\n        scroll = Scroll(mock_tab)\n        result = await scroll._get_remaining_scroll_to_bottom()\n\n        assert result == 1500.0\n\n    @pytest.mark.asyncio\n    async def test_dispatch_scroll_event(self, mock_tab):\n        \"\"\"Test _dispatch_scroll_event sends mouse wheel event.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n        from pydoll.protocol.input.types import MouseEventType\n\n        mock_tab._execute_command.return_value = {\n            'result': {'result': {'value': '[400, 300]'}}\n        }\n\n        scroll = Scroll(mock_tab)\n        await scroll._dispatch_scroll_event(delta_x=0, delta_y=100)\n\n        # Should have called execute_command twice:\n        # 1. _get_viewport_center\n        # 2. dispatch_mouse_event\n        assert mock_tab._execute_command.call_count == 2\n\n        # Check the second call (dispatch_mouse_event)\n        second_call = mock_tab._execute_command.call_args_list[1]\n        command = second_call[0][0]\n        assert command['method'] == 'Input.dispatchMouseEvent'\n        assert command['params']['type'] == MouseEventType.MOUSE_WHEEL\n        assert command['params']['deltaY'] == 100\n\n\nclass TestScrollWithCustomTiming:\n    \"\"\"Test Scroll with custom timing configuration.\"\"\"\n\n    def test_scroll_with_custom_timing(self, mock_tab):\n        \"\"\"Test Scroll accepts custom timing configuration.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n\n        custom_timing = ScrollTimingConfig(\n            min_duration=1.0,\n            max_duration=2.0,\n        )\n\n        scroll = Scroll(mock_tab, timing=custom_timing)\n\n        assert scroll._timing == custom_timing\n        assert scroll._timing.min_duration == 1.0\n        assert scroll._timing.max_duration == 2.0\n\n    def test_scroll_uses_default_timing(self, mock_tab):\n        \"\"\"Test Scroll uses default timing if none provided.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n\n        scroll = Scroll(mock_tab)\n\n        # Should use default values\n        assert scroll._timing.min_duration == 0.5\n        assert scroll._timing.max_duration == 1.5\n\n\nclass TestCubicBezierBisectionFallback:\n    \"\"\"Test CubicBezier bisection fallback when Newton's method fails.\"\"\"\n\n    def test_bisection_fallback_triggered_by_out_of_range_derivative(self):\n        \"\"\"Test that bisection is used when derivative is out of valid range.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        # Create a bezier with extreme control points\n        # that might cause derivative issues\n        bezier = CubicBezier(0.0, 0.0, 1.0, 1.0)  # Linear\n\n        # Should still work correctly\n        for x in [0.0, 0.25, 0.5, 0.75, 1.0]:\n            result = bezier.solve_curve_x(x)\n            assert isinstance(result, float)\n            # For linear bezier, t should be close to x\n            assert abs(result - x) < 0.1\n\n    def test_bisection_converges_for_edge_cases(self):\n        \"\"\"Test bisection fallback converges for edge case inputs.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        bezier = CubicBezier(0.25, 0.1, 0.25, 1.0)\n\n        # Very small values near 0\n        result_small = bezier.solve_curve_x(0.001)\n        assert isinstance(result_small, float)\n\n        # Values near 1\n        result_near_one = bezier.solve_curve_x(0.999)\n        assert isinstance(result_near_one, float)\n\n    def test_solve_with_zero_derivative_fallback(self):\n        \"\"\"Test bezier handles cases where derivative could be zero.\"\"\"\n        from pydoll.interactions.scroll import CubicBezier\n\n        # Bezier that starts flat (potential zero derivative at start)\n        bezier = CubicBezier(0.0, 0.5, 1.0, 0.5)\n\n        # Should still produce valid results\n        for x in [0.1, 0.5, 0.9]:\n            result = bezier.solve(x)\n            assert 0.0 <= result <= 1.0\n\n\nclass TestScrollHumanized:\n    \"\"\"Test _scroll_humanized method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_scroll_humanized_calls_perform_scroll_loop(self, mock_tab):\n        \"\"\"Test _scroll_humanized delegates to _perform_scroll_loop.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n        from pydoll.constants import ScrollPosition\n        from unittest.mock import patch, AsyncMock\n\n        config = ScrollTimingConfig(overshoot_probability=0.0)  # No overshoot\n        scroll = Scroll(mock_tab, timing=config)\n\n        # Mock the internal method\n        scroll._perform_scroll_loop = AsyncMock(return_value=100.0)\n\n        with patch('asyncio.sleep', new_callable=AsyncMock):\n            await scroll._scroll_humanized(ScrollPosition.DOWN, 100.0)\n\n        scroll._perform_scroll_loop.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_scroll_humanized_with_overshoot_triggers_correction(self, mock_tab):\n        \"\"\"Test _scroll_humanized calls correction when overshoot occurs.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n        from pydoll.constants import ScrollPosition\n        from unittest.mock import patch, AsyncMock\n\n        # Force overshoot\n        config = ScrollTimingConfig(\n            overshoot_probability=1.0,\n            overshoot_factor_min=1.1,\n            overshoot_factor_max=1.2,\n        )\n        scroll = Scroll(mock_tab, timing=config)\n\n        # Mock scroll_loop to return more than target (simulating overshoot)\n        scroll._perform_scroll_loop = AsyncMock(return_value=120.0)\n        scroll._scroll_correction = AsyncMock()\n\n        with patch('asyncio.sleep', new_callable=AsyncMock):\n            await scroll._scroll_humanized(ScrollPosition.DOWN, 100.0)\n\n        # Correction should be called because scrolled (120) > target (100)\n        scroll._scroll_correction.assert_called_once()\n\n\nclass TestScrollToEndHumanized:\n    \"\"\"Test _scroll_to_end_humanized method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_scroll_to_end_down_calls_humanized_scroll(self, mock_tab):\n        \"\"\"Test scrolling to bottom uses _scroll_humanized in loop.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n        from pydoll.constants import ScrollPosition\n        from unittest.mock import patch, AsyncMock\n\n        scroll = Scroll(mock_tab)\n\n        # First call: lots remaining, second call: none remaining\n        call_count = [0]\n        async def mock_remaining():\n            call_count[0] += 1\n            return 500.0 if call_count[0] == 1 else 0.0\n\n        scroll._get_remaining_scroll_to_bottom = mock_remaining\n        scroll._scroll_humanized = AsyncMock()\n\n        with patch('asyncio.sleep', new_callable=AsyncMock):\n            await scroll._scroll_to_end_humanized(ScrollPosition.DOWN)\n\n        # Should have called _scroll_humanized at least once\n        assert scroll._scroll_humanized.call_count >= 1\n\n    @pytest.mark.asyncio\n    async def test_scroll_to_end_up_uses_current_scroll_y(self, mock_tab):\n        \"\"\"Test scrolling to top checks current scroll position.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n        from pydoll.constants import ScrollPosition\n        from unittest.mock import patch, AsyncMock\n\n        scroll = Scroll(mock_tab)\n\n        # First call: has scroll position, second call: at top\n        call_count = [0]\n        async def mock_scroll_y():\n            call_count[0] += 1\n            return 300.0 if call_count[0] == 1 else 0.0\n\n        scroll._get_current_scroll_y = mock_scroll_y\n        scroll._scroll_humanized = AsyncMock()\n\n        with patch('asyncio.sleep', new_callable=AsyncMock):\n            await scroll._scroll_to_end_humanized(ScrollPosition.UP)\n\n        # Should have called _scroll_humanized\n        assert scroll._scroll_humanized.call_count >= 1\n\n    @pytest.mark.asyncio\n    async def test_scroll_to_end_stops_when_threshold_reached(self, mock_tab):\n        \"\"\"Test loop stops when remaining distance is below threshold.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n        from pydoll.constants import ScrollPosition\n        from unittest.mock import patch, AsyncMock\n\n        scroll = Scroll(mock_tab)\n\n        # Return value below threshold (30)\n        scroll._get_remaining_scroll_to_bottom = AsyncMock(return_value=10.0)\n        scroll._scroll_humanized = AsyncMock()\n\n        with patch('asyncio.sleep', new_callable=AsyncMock):\n            await scroll._scroll_to_end_humanized(ScrollPosition.DOWN)\n\n        # Should NOT have called _scroll_humanized (already at end)\n        scroll._scroll_humanized.assert_not_called()\n\n    @pytest.mark.asyncio\n    async def test_scroll_to_end_stops_when_stuck(self, mock_tab):\n        \"\"\"Test loop stops when scroll progress is stuck.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n        from pydoll.constants import ScrollPosition\n        from unittest.mock import patch, AsyncMock\n\n        scroll = Scroll(mock_tab)\n\n        # Always return same remaining distance (stuck)\n        scroll._get_remaining_scroll_to_bottom = AsyncMock(return_value=500.0)\n        scroll._scroll_humanized = AsyncMock()\n\n        with patch('asyncio.sleep', new_callable=AsyncMock):\n            await scroll._scroll_to_end_humanized(ScrollPosition.DOWN)\n\n        # Should have tried a few times then given up\n        # We set max_stuck_attempts = 10, so it should be around 10 calls\n        assert scroll._scroll_humanized.call_count >= 10\n        assert scroll._scroll_humanized.call_count < 20  # Should not loop infinitely\n\n\nclass TestPerformScrollLoop:\n    \"\"\"Test _perform_scroll_loop method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_perform_scroll_loop_dispatches_events(self, mock_tab):\n        \"\"\"Test scroll loop dispatches mouse wheel events.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n        from unittest.mock import patch, AsyncMock, MagicMock\n\n        mock_tab._execute_command.return_value = {\n            'result': {'result': {'value': '[400, 300]'}}\n        }\n\n        config = ScrollTimingConfig(\n            min_duration=0.01,\n            max_duration=0.02,\n            frame_interval=0.001,\n            micro_pause_probability=0.0,\n        )\n        scroll = Scroll(mock_tab, timing=config)\n\n        # Mock time to advance in steps\n        # Start at 0, then 0.005 (halfway), then 0.02 (end)\n        mock_loop = MagicMock()\n        mock_loop.time.side_effect = [0.0, 0.005, 0.02]\n\n        with patch('asyncio.get_running_loop', return_value=mock_loop):\n            with patch('asyncio.sleep', new_callable=AsyncMock):\n                scrolled = await scroll._perform_scroll_loop(\n                    effective_distance=100.0,\n                    duration=0.01,\n                    is_vertical=True,\n                    direction=1,\n                )\n\n        # Should have dispatched at least one event\n        assert mock_tab._execute_command.call_count >= 1\n        assert isinstance(scrolled, float)\n\n    @pytest.mark.asyncio\n    async def test_perform_scroll_loop_horizontal(self, mock_tab):\n        \"\"\"Test scroll loop handles horizontal scrolling.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n        from unittest.mock import patch, AsyncMock\n\n        mock_tab._execute_command.return_value = {\n            'result': {'result': {'value': '[400, 300]'}}\n        }\n\n        config = ScrollTimingConfig(\n            min_duration=0.01,\n            max_duration=0.02,\n            frame_interval=0.001,\n            micro_pause_probability=0.0,\n        )\n        scroll = Scroll(mock_tab, timing=config)\n\n        with patch('asyncio.sleep', new_callable=AsyncMock):\n            scrolled = await scroll._perform_scroll_loop(\n                effective_distance=50.0,\n                duration=0.01,\n                is_vertical=False,  # Horizontal\n                direction=-1,  # Left\n            )\n\n        assert isinstance(scrolled, float)\n\n    @pytest.mark.asyncio\n    async def test_perform_scroll_loop_returns_scrolled_amount(self, mock_tab):\n        \"\"\"Test scroll loop returns total scrolled distance.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n        from unittest.mock import patch, AsyncMock\n\n        mock_tab._execute_command.return_value = {\n            'result': {'result': {'value': '[400, 300]'}}\n        }\n\n        config = ScrollTimingConfig(\n            min_duration=0.05,\n            max_duration=0.05,\n            frame_interval=0.001,\n            micro_pause_probability=0.0,\n            delta_jitter=0,  # No jitter for predictable test\n        )\n        scroll = Scroll(mock_tab, timing=config)\n\n        with patch('asyncio.sleep', new_callable=AsyncMock):\n            scrolled = await scroll._perform_scroll_loop(\n                effective_distance=100.0,\n                duration=0.05,\n                is_vertical=True,\n                direction=1,\n            )\n\n        # Should have scrolled some amount\n        assert scrolled > 0\n\n\n\nclass TestScrollCorrection:\n    \"\"\"Test _scroll_correction method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_scroll_correction_dispatches_scroll_events(self, mock_tab):\n        \"\"\"Test correction dispatches scroll events progressively.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n        from unittest.mock import AsyncMock\n        import asyncio\n\n        config = ScrollTimingConfig(frame_interval=0.001)\n        scroll = Scroll(mock_tab, timing=config)\n\n        # Track calls to _dispatch_scroll_event\n        dispatch_calls = []\n        original_dispatch = scroll._dispatch_scroll_event\n\n        async def tracking_dispatch(delta_x, delta_y):\n            dispatch_calls.append((delta_x, delta_y))\n\n        scroll._dispatch_scroll_event = tracking_dispatch\n\n        # Run with timeout to prevent hanging\n        try:\n            await asyncio.wait_for(\n                scroll._scroll_correction(\n                    is_vertical=True,\n                    direction=-1,\n                    distance=10.0,\n                ),\n                timeout=2.0,\n            )\n        except asyncio.TimeoutError:\n            pass  # Test passed if we got here with some calls\n\n        # Should have dispatched at least one event\n        assert len(dispatch_calls) >= 1\n\n    @pytest.mark.asyncio\n    async def test_scroll_correction_horizontal(self, mock_tab):\n        \"\"\"Test correction works for horizontal scrolling.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n        import asyncio\n\n        # Use larger frame_interval so delta is >= 1\n        config = ScrollTimingConfig(frame_interval=0.01)\n        scroll = Scroll(mock_tab, timing=config)\n\n        dispatch_calls = []\n\n        async def tracking_dispatch(delta_x, delta_y):\n            dispatch_calls.append((delta_x, delta_y))\n\n        scroll._dispatch_scroll_event = tracking_dispatch\n\n        try:\n            await asyncio.wait_for(\n                scroll._scroll_correction(\n                    is_vertical=False,\n                    direction=1,\n                    distance=10.0,\n                ),\n                timeout=2.0,\n            )\n        except asyncio.TimeoutError:\n            pass\n\n        # Should have dispatched at least one event\n        assert len(dispatch_calls) >= 1\n\n    @pytest.mark.asyncio\n    async def test_scroll_correction_velocity_decreases(self, mock_tab):\n        \"\"\"Test correction velocity decreases over time.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollTimingConfig\n        import asyncio\n\n        config = ScrollTimingConfig(frame_interval=0.001)\n        scroll = Scroll(mock_tab, timing=config)\n\n        call_deltas = []\n\n        async def tracking_dispatch(delta_x, delta_y):\n            call_deltas.append(abs(delta_y))\n\n        scroll._dispatch_scroll_event = tracking_dispatch\n\n        try:\n            await asyncio.wait_for(\n                scroll._scroll_correction(\n                    is_vertical=True,\n                    direction=1,\n                    distance=50.0,\n                ),\n                timeout=2.0,\n            )\n        except asyncio.TimeoutError:\n            pass\n\n        # Velocity should be decreasing (later deltas smaller)\n        if len(call_deltas) >= 2:\n            assert call_deltas[0] >= call_deltas[-1]\n\n\nclass TestPublicMethodsWithHumanize:\n    \"\"\"Test that public methods correctly route to humanized methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_by_with_humanize_calls_scroll_humanized(self, mock_tab):\n        \"\"\"Test by() with humanize=True routes to _scroll_humanized.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n        from pydoll.constants import ScrollPosition\n        from unittest.mock import AsyncMock\n\n        scroll = Scroll(mock_tab)\n        scroll._scroll_humanized = AsyncMock()\n\n        await scroll.by(ScrollPosition.DOWN, 100, humanize=True)\n\n        scroll._scroll_humanized.assert_called_once_with(ScrollPosition.DOWN, 100)\n\n    @pytest.mark.asyncio\n    async def test_to_top_with_humanize_calls_scroll_to_end_humanized(self, mock_tab):\n        \"\"\"Test to_top() with humanize=True routes to _scroll_to_end_humanized.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n        from pydoll.constants import ScrollPosition\n        from unittest.mock import AsyncMock\n\n        scroll = Scroll(mock_tab)\n        scroll._scroll_to_end_humanized = AsyncMock()\n\n        await scroll.to_top(humanize=True)\n\n        scroll._scroll_to_end_humanized.assert_called_once_with(ScrollPosition.UP)\n\n    @pytest.mark.asyncio\n    async def test_to_bottom_with_humanize_calls_scroll_to_end_humanized(self, mock_tab):\n        \"\"\"Test to_bottom() with humanize=True routes to _scroll_to_end_humanized.\"\"\"\n        from pydoll.interactions.scroll import Scroll\n        from pydoll.constants import ScrollPosition\n        from unittest.mock import AsyncMock\n\n        scroll = Scroll(mock_tab)\n        scroll._scroll_to_end_humanized = AsyncMock()\n\n        await scroll.to_bottom(humanize=True)\n\n        scroll._scroll_to_end_humanized.assert_called_once_with(ScrollPosition.DOWN)\n\n\nclass TestScrollAPIBackwardCompatibility:\n    \"\"\"Test backward compatibility alias.\"\"\"\n\n    def test_scroll_api_alias(self):\n        \"\"\"Test ScrollAPI is an alias for Scroll.\"\"\"\n        from pydoll.interactions.scroll import Scroll, ScrollAPI\n\n        assert ScrollAPI is Scroll\n"
  },
  {
    "path": "tests/test_managers/test_browser_managers.py",
    "content": "from unittest.mock import MagicMock, Mock, patch, ANY\n\nimport pytest\n\nfrom pydoll.browser.managers import (\n    ChromiumOptionsManager,\n    BrowserProcessManager,\n    ProxyManager,\n    TempDirectoryManager,\n)\nfrom pydoll.browser.options import ChromiumOptions as Options\nfrom pydoll.exceptions import InvalidOptionsObject\n\n\n@pytest.fixture\ndef proxy_options():\n    return Options()\n\n\n@pytest.fixture\ndef temp_manager():\n    mock_dir = MagicMock()\n    mock_dir.name = '/fake/temp/dir'\n    return TempDirectoryManager(temp_dir_factory=lambda: mock_dir)\n\n\n@pytest.fixture\ndef process_manager():\n    mock_creator = Mock(return_value=MagicMock())\n    return BrowserProcessManager(process_creator=mock_creator)\n\n\n@pytest.fixture\ndef chromium_options_manager(proxy_options):\n    options_manager = ChromiumOptionsManager(proxy_options)\n    return options_manager\n\n\ndef test_proxy_manager_no_proxy(proxy_options):\n    manager = ProxyManager(proxy_options)\n    result = manager.get_proxy_credentials()\n\n    assert result[0] is False\n    assert result[1] == (None, None)\n\n\ndef test_proxy_manager_with_credentials(proxy_options):\n    proxy_options.add_argument('--proxy-server=user:pass@example.com')\n    manager = ProxyManager(proxy_options)\n    result = manager.get_proxy_credentials()\n\n    assert result[0] is True\n    assert result[1] == ('user', 'pass')\n    assert proxy_options.arguments == ['--proxy-server=example.com']\n\n\ndef test_proxy_manager_invalid_credentials_format(proxy_options):\n    proxy_options.add_argument('--proxy-server=invalidformat@example.com')\n    manager = ProxyManager(proxy_options)\n    result = manager.get_proxy_credentials()\n\n    assert result[0] is False\n    assert result[1] == (None, None)\n    assert proxy_options.arguments == [\n        '--proxy-server=invalidformat@example.com'\n    ]\n\n\ndef test_proxy_manager_with_scheme_http(proxy_options):\n    proxy_options.add_argument('--proxy-server=http://user:pass@proxy.local:8080')\n    manager = ProxyManager(proxy_options)\n    result = manager.get_proxy_credentials()\n\n    assert result[0] is True\n    assert result[1] == ('user', 'pass')\n    assert proxy_options.arguments == ['--proxy-server=http://proxy.local:8080']\n\n\ndef test_proxy_manager_with_scheme_socks(proxy_options):\n    proxy_options.add_argument('--proxy-server=socks5://alice:pwd@1.2.3.4:1080')\n    manager = ProxyManager(proxy_options)\n    result = manager.get_proxy_credentials()\n\n    assert result[0] is True\n    assert result[1] == ('alice', 'pwd')\n    assert proxy_options.arguments == ['--proxy-server=socks5://1.2.3.4:1080']\n\n\ndef test_proxy_manager_invalid_proxy_format(proxy_options):\n    proxy_options.add_argument('--proxy-server=invalidformat')\n    manager = ProxyManager(proxy_options)\n    result = manager.get_proxy_credentials()\n\n    assert result[0] is False\n    assert result[1] == (None, None)\n\n\ndef test_start_browser_process(process_manager):\n    binary = '/fake/path/browser'\n    port = 9222\n    args = ['--test-arg']\n\n    process_manager.start_browser_process(binary, port, args)\n\n    expected_command = [binary, f'--remote-debugging-port={port}', *args]\n    process_manager._process_creator.assert_called_once_with(expected_command)\n    assert process_manager._process is not None\n\n\ndef test_stop_process(process_manager):\n    mock_process = MagicMock()\n    process_manager._process = mock_process\n\n    process_manager.stop_process()\n\n    mock_process.terminate.assert_called_once()\n\n\ndef test_create_temp_dir(temp_manager):\n    temp_dir = temp_manager.create_temp_dir()\n\n    assert len(temp_manager._temp_dirs) == 1\n    assert temp_dir.name == '/fake/temp/dir'\n\n\ndef test_cleanup_temp_dirs(temp_manager):\n    mock_dir1 = MagicMock()\n    mock_dir2 = MagicMock()\n    temp_manager._temp_dirs = [mock_dir1, mock_dir2]\n\n    with patch('shutil.rmtree') as mock_rmtree:\n        temp_manager.cleanup()\n\n        assert mock_rmtree.call_count == 2\n        mock_rmtree.assert_any_call(mock_dir1.name, onerror=ANY)\n        mock_rmtree.assert_any_call(mock_dir2.name, onerror=ANY)\n\n\ndef test_retry_process_file(temp_manager):\n    mock_func = Mock()\n\n    # retry success\n    success_at = 5\n    mock_func.side_effect = [PermissionError] * (success_at - 1) + [None]\n    temp_manager.retry_process_file(mock_func, \"/test/path\", retry_times=success_at)\n    assert mock_func.call_count == success_at\n    \n    # exceed max retries\n    mock_func.reset_mock()\n    mock_func.side_effect = PermissionError\n    with pytest.raises(PermissionError):\n        temp_manager.retry_process_file(mock_func, \"/test/path\", retry_times=3)\n    assert mock_func.call_count == 3\n\n    # infinite_retries\n    mock_func.reset_mock()\n    mock_func.side_effect = [PermissionError] * 9 + [None]\n    temp_manager.retry_process_file(mock_func, \"/test/path\", retry_times=-1)\n    assert mock_func.call_count == 10\n\n\ndef test_handle_cleanup_error(temp_manager):\n    func_mock = Mock()\n\n    # matched permission error\n    temp_manager.retry_process_file = Mock()\n    path = \"/tmp/CrashpadMetrics-active.pma\"\n\n    temp_manager.handle_cleanup_error(func_mock, path, (PermissionError, PermissionError(), None))\n    temp_manager.retry_process_file.assert_called_once_with(func_mock, path)\n\n    # matched permission error - should not raise, only log and continue\n    temp_manager.retry_process_file = Mock()\n    temp_manager.retry_process_file.side_effect = PermissionError\n    path = \"/tmp/CrashpadMetrics-active.pma\"\n    temp_manager.handle_cleanup_error(func_mock, path, (PermissionError, PermissionError(), None))\n\n    # unmatched permission error\n    temp_manager.retry_process_file = Mock()\n    path = \"/tmp/test.file\"\n    exc = PermissionError(\"Access denied\")\n\n    with pytest.raises(PermissionError) as e:\n        temp_manager.handle_cleanup_error(func_mock, path, (PermissionError, exc, None))\n    assert e.value is exc\n\n    # pass OSError\n    temp_manager.handle_cleanup_error(func_mock, \"/tmp/path\", (OSError, OSError(), None))\n\n    # raise other Exception\n    exc = ValueError(\"Test\")\n    with pytest.raises(ValueError) as e:\n        temp_manager.handle_cleanup_error(func_mock, \"/tmp/path\", (ValueError, exc, None))\n    assert e.value is exc\n\n\ndef test_initialize_options_with_none(chromium_options_manager):\n    result = chromium_options_manager.initialize_options()\n\n    assert isinstance(result, Options)\n    assert result.arguments == ['--no-first-run', '--no-default-browser-check']\n\n\ndef test_initialize_options_with_valid_options():\n    options = Options()\n    options.add_argument('--test')\n    chromium_options_manager = ChromiumOptionsManager(options)\n    result = chromium_options_manager.initialize_options()\n\n    assert result is options\n    assert '--test' in result.arguments\n\n\ndef test_initialize_options_with_invalid_type():\n    chromium_options_manager = ChromiumOptionsManager('invalid options object')\n    with pytest.raises(InvalidOptionsObject):\n        chromium_options_manager.initialize_options()\n\n\ndef test_add_default_arguments():\n    options = Options()\n    chromium_options_manager = ChromiumOptionsManager(options)\n    chromium_options_manager.add_default_arguments()\n\n    assert '--no-first-run' in options.arguments\n    assert '--no-default-browser-check' in options.arguments\n\n\n\ndef test_initialize_options_creates_new_instance():\n    manager = ChromiumOptionsManager(None)\n    result = manager.initialize_options()\n    assert isinstance(result, Options)\n    assert '--no-first-run' in result.arguments\n    assert '--no-default-browser-check' in result.arguments\n\n\ndef test_initialize_options_preserves_custom_arguments():\n    options = Options()\n    options.add_argument('--custom-flag')\n    manager = ChromiumOptionsManager(options)\n    result = manager.initialize_options()\n    assert '--custom-flag' in result.arguments\n    assert '--no-first-run' in result.arguments\n    assert '--no-default-browser-check' in result.arguments\n"
  },
  {
    "path": "tests/test_managers/test_connection_managers.py",
    "content": "import pytest\n\nfrom pydoll import exceptions\nfrom pydoll.connection.managers import CommandsManager, EventsManager\n\n\n@pytest.fixture\ndef commands_manager():\n    \"\"\"Retorna uma instância fresca de CommandManager para os testes.\"\"\"\n    return CommandsManager()\n\n\n@pytest.fixture\ndef events_manager():\n    \"\"\"Retorna uma instância fresca de EventsManager para os testes.\"\"\"\n    return EventsManager()\n\n\ndef test_create_command_future(commands_manager):\n    test_command = {'method': 'TestMethod'}\n    future_result = commands_manager.create_command_future(test_command)\n\n    # Verifica se o ID foi atribuído corretamente\n    assert test_command['id'] == 1, 'The first command ID should be 1'\n    # Verifica se o future foi armazenado no dicionário de pendentes\n    assert 1 in commands_manager._pending_commands\n    assert commands_manager._pending_commands[1] is future_result\n\n    # Cria um segundo comando e verifica o incremento do ID\n    second_command = {'method': 'SecondMethod'}\n    future_second = commands_manager.create_command_future(second_command)\n    assert second_command['id'] == 2, 'The second command ID should be 2'\n    assert 2 in commands_manager._pending_commands\n    assert commands_manager._pending_commands[2] is future_second\n\n\ndef test_resolve_command(commands_manager):\n    test_command = {'method': 'TestMethod'}\n    future_result = commands_manager.create_command_future(test_command)\n    result_payload = '{\"result\": \"success\"}'\n\n    # O future não deve estar concluído antes da resolução\n    assert not future_result.done(), (\n        'The future should not be completed before resolution'\n    )\n\n    # Resolve o comando e verifica o resultado\n    commands_manager.resolve_command(1, result_payload)\n    assert future_result.done(), (\n        'The future should be completed after resolution'\n    )\n    assert future_result.result() == result_payload, (\n        'The future result does not match the expected result'\n    )\n    # O comando pendente deve ser removido\n    assert 1 not in commands_manager._pending_commands\n\n\ndef test_resolve_unknown_command(commands_manager):\n    test_command = {'method': 'TestMethod'}\n    future_result = commands_manager.create_command_future(test_command)\n\n    # Tenta resolver um ID inexistente; o future original deve permanecer pendente\n    commands_manager.resolve_command(999, '{\"result\": \"ignored\"}')\n    assert not future_result.done(), (\n        'The future should not be completed after resolving an unknown command'\n    )\n\n\ndef test_remove_pending_command(commands_manager):\n    test_command = {'method': 'TestMethod'}\n    _ = commands_manager.create_command_future(test_command)\n\n    # Remove o comando pendente e verifica se ele foi removido\n    commands_manager.remove_pending_command(1)\n    assert 1 not in commands_manager._pending_commands, (\n        'The pending command should be removed'\n    )\n    commands_manager.remove_pending_command(1)\n\n\ndef test_register_callback_success(events_manager):\n    dummy_callback = lambda event: event\n    callback_id = events_manager.register_callback('TestEvent', dummy_callback)\n\n    assert callback_id == 1, 'The first callback ID should be 1'\n    assert callback_id in events_manager._event_callbacks, (\n        'The callback must be registered'\n    )\n    callback_info = events_manager._event_callbacks[callback_id]\n    assert callback_info['temporary'] is False, (\n        'The temporary flag should be False by default'\n    )\n\n\ndef test_remove_existing_callback(events_manager):\n    dummy_callback = lambda event: event\n    callback_id = events_manager.register_callback('TestEvent', dummy_callback)\n    removal_result = events_manager.remove_callback(callback_id)\n\n    assert removal_result is True, (\n        'The removal of a existing callback should be successful'\n    )\n    assert callback_id not in events_manager._event_callbacks, (\n        'The callback should be removed'\n    )\n\n\ndef test_remove_nonexistent_callback(events_manager):\n    removal_result = events_manager.remove_callback(999)\n    assert removal_result is False, (\n        'The removal of a nonexistent callback should return False'\n    )\n\n\ndef test_clear_callbacks(events_manager):\n    dummy_callback = lambda event: event\n    events_manager.register_callback('EventA', dummy_callback)\n    events_manager.register_callback('EventB', dummy_callback)\n\n    events_manager.clear_callbacks()\n    assert len(events_manager._event_callbacks) == 0, (\n        'All callbacks should be cleared'\n    )\n\n\n@pytest.mark.asyncio\nasync def test_process_event_updates_network_logs(events_manager):\n    assert events_manager.network_logs == []\n    network_event = {\n        'method': 'Network.requestWillBeSent',\n        'url': 'http://example.com',\n    }\n\n    await events_manager.process_event(network_event)\n\n    assert network_event in events_manager.network_logs, (\n        'The network event should be added to the logs'\n    )\n\n\n@pytest.mark.asyncio\nasync def test_process_event_triggers_callbacks(events_manager):\n    callback_results = []\n\n    def sync_callback(event):\n        callback_results.append(('sync', event.get('value')))\n\n    async def async_callback(event):\n        callback_results.append(('async', event.get('value')))\n\n    sync_callback_id = events_manager.register_callback(\n        'MyCustomEvent', sync_callback, temporary=True\n    )\n    async_callback_id = events_manager.register_callback(\n        'MyCustomEvent', async_callback, temporary=False\n    )\n\n    test_event = {'method': 'MyCustomEvent', 'value': 123}\n    await events_manager.process_event(test_event)\n\n    assert ('sync', 123) in callback_results, (\n        'The synchronous callback was not triggered correctly'\n    )\n    assert ('async', 123) in callback_results, (\n        'The asynchronous callback was not triggered correctly'\n    )\n\n    assert sync_callback_id not in events_manager._event_callbacks, (\n        'The temporary callback should be removed after execution'\n    )\n\n    assert async_callback_id in events_manager._event_callbacks, (\n        'The permanent callback should remain registered'\n    )\n\n\n@pytest.mark.asyncio\nasync def test_trigger_callbacks_error_handling(events_manager, caplog):\n    def faulty_callback(event):\n        raise ValueError('Error in callback')\n\n    faulty_callback_id = events_manager.register_callback(\n        'ErrorEvent', faulty_callback, temporary=True\n    )\n    test_event = {'method': 'ErrorEvent'}\n\n    await events_manager.process_event(test_event)\n    assert faulty_callback_id not in events_manager._event_callbacks, (\n        'The callback with error should be removed after execution'\n    )\n    error_logged = any(\n        'Error in callback' in record.message for record in caplog.records\n    )\n    assert error_logged, 'The error in the callback should be logged'\n"
  },
  {
    "path": "tests/test_nested_oopif_integration.py",
    "content": "\"\"\"Integration tests for nested cross-origin iframe (OOPIF) resolution.\n\nTwo HTTP servers on different ports simulate cross-origin boundaries,\ntriggering Chrome's site isolation (OOPIF) mechanism. Tests verify\nthat Pydoll correctly routes CDP commands through the right session\nhandler when resolving nested iframes inside OOPIFs.\n\"\"\"\n\nimport asyncio\nimport http.server\nimport socket\nimport threading\nfrom pathlib import Path\n\nimport pytest\n\nfrom pydoll.browser.chromium import Chrome\n\nPAGES_DIR = Path(__file__).parent / 'pages' / 'oopif'\n\n\nclass _SilentHandler(http.server.SimpleHTTPRequestHandler):\n    def log_message(self, *args):\n        pass\n\n\ndef _wait_for_server(host: str, port: int, timeout: float = 5.0) -> None:\n    \"\"\"Block until the server at host:port accepts a TCP connection.\"\"\"\n    import time\n\n    deadline = time.monotonic() + timeout\n    while time.monotonic() < deadline:\n        try:\n            with socket.create_connection((host, port), timeout=0.5):\n                return\n        except OSError:\n            time.sleep(0.05)\n    raise RuntimeError(f'Server {host}:{port} not ready within {timeout}s')\n\n\n@pytest.fixture(scope='module')\ndef cross_origin_servers():\n    \"\"\"Two HTTP servers on different ports -> different origins -> OOPIF.\"\"\"\n\n    def _handler():\n        class H(_SilentHandler):\n            def __init__(self, *a, **kw):\n                super().__init__(*a, directory=str(PAGES_DIR), **kw)\n\n        return H\n\n    srv_a = http.server.HTTPServer(('127.0.0.1', 0), _handler())\n    srv_b = http.server.HTTPServer(('127.0.0.1', 0), _handler())\n    port_a = srv_a.server_address[1]\n    port_b = srv_b.server_address[1]\n\n    for srv in (srv_a, srv_b):\n        threading.Thread(target=srv.serve_forever, daemon=True).start()\n\n    _wait_for_server('127.0.0.1', port_a)\n    _wait_for_server('127.0.0.1', port_b)\n\n    yield port_a, port_b\n\n    srv_a.shutdown()\n    srv_b.shutdown()\n\n\nclass TestCrossOriginIframeResolution:\n    \"\"\"Finding elements inside cross-origin (OOPIF) iframes.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_find_element_in_cross_origin_iframe(\n        self, ci_chrome_options, cross_origin_servers\n    ):\n        port_a, port_b = cross_origin_servers\n        url = f'http://127.0.0.1:{port_a}/oopif_main.html?port={port_b}'\n\n        ci_chrome_options.add_argument('--site-per-process')\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(url)\n\n            iframe = await tab.find(id='cross-origin-iframe', timeout=10)\n            assert iframe.is_iframe\n\n            heading = await iframe.find(id='oopif-heading', timeout=10)\n            assert await heading.text == 'Cross-Origin Content'\n\n    @pytest.mark.asyncio\n    async def test_click_button_in_cross_origin_iframe(\n        self, ci_chrome_options, cross_origin_servers\n    ):\n        port_a, port_b = cross_origin_servers\n        url = f'http://127.0.0.1:{port_a}/oopif_main.html?port={port_b}'\n\n        ci_chrome_options.add_argument('--site-per-process')\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(url)\n\n            iframe = await tab.find(id='cross-origin-iframe', timeout=10)\n            btn = await iframe.find(id='oopif-btn', timeout=10)\n            counter = await iframe.find(id='oopif-btn-count', timeout=10)\n\n            assert await counter.text == '0'\n            await btn.click()\n            await asyncio.sleep(0.3)\n            assert await counter.text == '1'\n\n\nclass TestNestedIframeInsideOopif:\n    \"\"\"Finding elements in iframes nested inside cross-origin iframes.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_find_element_in_nested_iframe_inside_oopif(\n        self, ci_chrome_options, cross_origin_servers\n    ):\n        \"\"\"Navigate: main -> OOPIF -> nested iframe -> find element.\"\"\"\n        port_a, port_b = cross_origin_servers\n        url = f'http://127.0.0.1:{port_a}/oopif_main.html?port={port_b}'\n\n        ci_chrome_options.add_argument('--site-per-process')\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(url)\n\n            oopif = await tab.find(id='cross-origin-iframe', timeout=10)\n            nested = await oopif.find(id='nested-iframe', timeout=10)\n            assert nested.is_iframe\n\n            heading = await nested.find(id='nested-heading', timeout=10)\n            assert await heading.text == 'Nested Iframe Content'\n\n    @pytest.mark.asyncio\n    async def test_type_text_in_nested_iframe_inside_oopif(\n        self, ci_chrome_options, cross_origin_servers\n    ):\n        \"\"\"Type text into an input inside a nested iframe within an OOPIF.\"\"\"\n        port_a, port_b = cross_origin_servers\n        url = f'http://127.0.0.1:{port_a}/oopif_main.html?port={port_b}'\n\n        ci_chrome_options.add_argument('--site-per-process')\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(url)\n\n            oopif = await tab.find(id='cross-origin-iframe', timeout=10)\n            nested = await oopif.find(id='nested-iframe', timeout=10)\n\n            input_el = await nested.find(id='nested-input', timeout=10)\n            await input_el.type_text('hello from nested oopif')\n            await asyncio.sleep(0.3)\n            prop = await input_el.execute_script(\n                'return this.value', return_by_value=True\n            )\n            assert prop['result']['result']['value'] == 'hello from nested oopif'\n\n\nclass TestShadowRootInsideOopif:\n    \"\"\"Discovering and interacting with shadow roots inside OOPIFs.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_find_shadow_roots_inside_oopif(\n        self, ci_chrome_options, cross_origin_servers\n    ):\n        \"\"\"find_shadow_roots(True) should discover shadow roots across OOPIFs.\"\"\"\n        port_a, port_b = cross_origin_servers\n        url = f'http://127.0.0.1:{port_a}/oopif_main.html?port={port_b}'\n\n        ci_chrome_options.add_argument('--site-per-process')\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(url)\n\n            shadow_roots = await tab.find_shadow_roots(True, timeout=10)\n            for sr in shadow_roots:\n                html = await sr.inner_html\n                if 'Shadow content inside OOPIF' in html:\n                    text_el = await sr.query('#shadow-text', timeout=10)\n                    assert await text_el.text == 'Shadow content inside OOPIF'\n                    return\n\n            pytest.fail('Shadow root inside OOPIF not found via find_shadow_roots')\n\n    @pytest.mark.asyncio\n    async def test_click_button_in_shadow_root_inside_oopif(\n        self, ci_chrome_options, cross_origin_servers\n    ):\n        port_a, port_b = cross_origin_servers\n        url = f'http://127.0.0.1:{port_a}/oopif_main.html?port={port_b}'\n\n        ci_chrome_options.add_argument('--site-per-process')\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(url)\n\n            shadow_roots = await tab.find_shadow_roots(True, timeout=10)\n            for sr in shadow_roots:\n                html = await sr.inner_html\n                if 'Shadow content inside OOPIF' in html:\n                    btn = await sr.query('#shadow-btn', timeout=10)\n                    counter = await sr.query('#shadow-btn-count', timeout=10)\n                    assert await counter.text == '0'\n\n                    await btn.click()\n                    await asyncio.sleep(0.3)\n                    assert await counter.text == '1'\n                    return\n\n            pytest.fail('Shadow root inside OOPIF not found')\n\n\nclass TestIframeInsideShadowRootInsideOopif:\n    \"\"\"The exact bug scenario: main -> OOPIF -> shadow root -> iframe.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_find_element_in_iframe_inside_shadow_in_oopif(\n        self, ci_chrome_options, cross_origin_servers\n    ):\n        \"\"\"This reproduces the original bug where IFrameContextResolver\n        failed with InvalidIFrame because DOM.getFrameOwner was routed\n        through the wrong session handler.\n        \"\"\"\n        port_a, port_b = cross_origin_servers\n        url = f'http://127.0.0.1:{port_a}/oopif_main.html?port={port_b}'\n\n        ci_chrome_options.add_argument('--site-per-process')\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(url)\n\n            shadow_roots = await tab.find_shadow_roots(True, timeout=10)\n            for sr in shadow_roots:\n                html = await sr.inner_html\n                if 'Shadow content inside OOPIF' in html:\n                    iframe = await sr.query('#shadow-iframe', timeout=10)\n                    assert iframe.is_iframe\n\n                    heading = await iframe.find(\n                        id='shadow-iframe-heading', timeout=10\n                    )\n                    assert await heading.text == 'Shadow Iframe Content'\n                    return\n\n            pytest.fail('Shadow root inside OOPIF not found')\n\n    @pytest.mark.asyncio\n    async def test_type_text_in_iframe_inside_shadow_in_oopif(\n        self, ci_chrome_options, cross_origin_servers\n    ):\n        \"\"\"Type text through: main -> OOPIF -> shadow root -> iframe -> input.\"\"\"\n        port_a, port_b = cross_origin_servers\n        url = f'http://127.0.0.1:{port_a}/oopif_main.html?port={port_b}'\n\n        ci_chrome_options.add_argument('--site-per-process')\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(url)\n\n            shadow_roots = await tab.find_shadow_roots(True, timeout=10)\n            for sr in shadow_roots:\n                html = await sr.inner_html\n                if 'Shadow content inside OOPIF' in html:\n                    iframe = await sr.query('#shadow-iframe', timeout=10)\n                    input_el = await iframe.find(\n                        id='shadow-iframe-input', timeout=10\n                    )\n                    await input_el.type_text('deep nested text')\n                    await asyncio.sleep(0.3)\n                    prop = await input_el.execute_script(\n                        'return this.value', return_by_value=True\n                    )\n                    assert prop['result']['result']['value'] == 'deep nested text'\n                    return\n\n            pytest.fail('Shadow root inside OOPIF not found')\n"
  },
  {
    "path": "tests/test_shadow_root.py",
    "content": "import uuid\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nimport pytest_asyncio\n\nfrom pydoll.browser.tab import Tab\nfrom pydoll.elements.shadow_root import ShadowRoot\nfrom pydoll.elements.web_element import WebElement\nfrom pydoll.exceptions import (\n    CommandExecutionTimeout,\n    ElementNotFound,\n    ShadowRootNotFound,\n    WaitElementTimeout,\n    WebSocketConnectionClosed,\n)\nfrom pydoll.interactions.iframe import IFrameContext\nfrom pydoll.protocol.dom.types import ShadowRootType\n\n\n@pytest_asyncio.fixture\nasync def mock_connection_handler():\n    \"\"\"Mock connection handler for ShadowRoot tests.\"\"\"\n    with patch('pydoll.connection.ConnectionHandler', autospec=True) as mock:\n        handler = mock.return_value\n        handler.execute_command = AsyncMock()\n        yield handler\n\n\n@pytest.fixture\ndef shadow_root(mock_connection_handler):\n    \"\"\"Basic ShadowRoot fixture with open mode.\"\"\"\n    return ShadowRoot(\n        object_id='shadow-root-object-id',\n        connection_handler=mock_connection_handler,\n        mode=ShadowRootType.OPEN,\n    )\n\n\n@pytest.fixture\ndef host_element(mock_connection_handler):\n    \"\"\"WebElement fixture acting as shadow host.\"\"\"\n    return WebElement(\n        object_id='host-object-id',\n        connection_handler=mock_connection_handler,\n        method='css',\n        selector='#host',\n        attributes_list=['id', 'host', 'tag_name', 'div'],\n    )\n\n\nclass TestShadowRootInit:\n    \"\"\"Tests for ShadowRoot initialization.\"\"\"\n\n    def test_init_with_defaults(self, mock_connection_handler):\n        sr = ShadowRoot(\n            object_id='sr-id',\n            connection_handler=mock_connection_handler,\n        )\n        assert sr._object_id == 'sr-id'\n        assert sr._connection_handler is mock_connection_handler\n        assert sr._mode == ShadowRootType.OPEN\n        assert sr._host_element is None\n\n    def test_init_with_all_params(self, mock_connection_handler, host_element):\n        sr = ShadowRoot(\n            object_id='sr-id',\n            connection_handler=mock_connection_handler,\n            mode=ShadowRootType.CLOSED,\n            host_element=host_element,\n        )\n        assert sr._object_id == 'sr-id'\n        assert sr._mode == ShadowRootType.CLOSED\n        assert sr._host_element is host_element\n\n    def test_init_with_user_agent_mode(self, mock_connection_handler):\n        sr = ShadowRoot(\n            object_id='sr-id',\n            connection_handler=mock_connection_handler,\n            mode=ShadowRootType.USER_AGENT,\n        )\n        assert sr._mode == ShadowRootType.USER_AGENT\n\n\nclass TestShadowRootProperties:\n    \"\"\"Tests for ShadowRoot properties.\"\"\"\n\n    def test_mode_property(self, shadow_root):\n        assert shadow_root.mode == ShadowRootType.OPEN\n\n    def test_host_element_none(self, shadow_root):\n        assert shadow_root.host_element is None\n\n    def test_host_element_with_reference(self, mock_connection_handler, host_element):\n        sr = ShadowRoot(\n            object_id='sr-id',\n            connection_handler=mock_connection_handler,\n            host_element=host_element,\n        )\n        assert sr.host_element is host_element\n\n\nclass TestShadowRootInnerHtml:\n    \"\"\"Tests for ShadowRoot.inner_html property.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_inner_html(self, shadow_root):\n        shadow_root._connection_handler.execute_command.return_value = {\n            'result': {'outerHTML': '<div class=\"internal\">Hello</div>'}\n        }\n        html = await shadow_root.inner_html\n        assert html == '<div class=\"internal\">Hello</div>'\n        shadow_root._connection_handler.execute_command.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_inner_html_empty(self, shadow_root):\n        shadow_root._connection_handler.execute_command.return_value = {\n            'result': {'outerHTML': ''}\n        }\n        html = await shadow_root.inner_html\n        assert html == ''\n\n\nclass TestShadowRootRepr:\n    \"\"\"Tests for ShadowRoot string representations.\"\"\"\n\n    def test_repr(self, shadow_root):\n        result = repr(shadow_root)\n        assert 'ShadowRoot' in result\n        assert 'open' in result\n        assert 'shadow-root-object-id' in result\n\n    def test_str(self, shadow_root):\n        result = str(shadow_root)\n        assert result == 'ShadowRoot(open)'\n\n    def test_str_closed(self, mock_connection_handler):\n        sr = ShadowRoot(\n            object_id='sr-id',\n            connection_handler=mock_connection_handler,\n            mode=ShadowRootType.CLOSED,\n        )\n        assert str(sr) == 'ShadowRoot(closed)'\n\n\nclass TestShadowRootFindElements:\n    \"\"\"Tests for element finding within shadow root (CSS only).\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_find_raises_not_implemented(self, shadow_root):\n        \"\"\"find() should raise NotImplementedError on ShadowRoot.\"\"\"\n        with pytest.raises(NotImplementedError, match='find\\\\(\\\\) is not supported on ShadowRoot'):\n            await shadow_root.find(class_name='btn-primary')\n\n    @pytest.mark.asyncio\n    async def test_query_xpath_raises_not_implemented(self, shadow_root):\n        \"\"\"query() with XPath should raise NotImplementedError on ShadowRoot.\"\"\"\n        with pytest.raises(NotImplementedError, match='XPath is not supported on ShadowRoot'):\n            await shadow_root.query('.//button')\n\n    @pytest.mark.asyncio\n    async def test_query_css_in_shadow_root(self, shadow_root):\n        \"\"\"query() should work with CSS selectors inside shadow root.\"\"\"\n        evaluate_response = {\n            'result': {'result': {'objectId': 'queried-element-id'}}\n        }\n        describe_response = {\n            'result': {\n                'node': {\n                    'nodeName': 'INPUT',\n                    'attributes': ['type', 'email', 'name', 'user-email'],\n                }\n            }\n        }\n        shadow_root._connection_handler.execute_command.side_effect = [\n            evaluate_response,\n            describe_response,\n        ]\n\n        element = await shadow_root.query('input[type=\"email\"]')\n\n        assert isinstance(element, WebElement)\n        assert element._object_id == 'queried-element-id'\n\n\nclass TestWebElementGetShadowRoot:\n    \"\"\"Tests for WebElement.get_shadow_root().\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_shadow_root_success(self, host_element):\n        \"\"\"get_shadow_root() should return ShadowRoot when shadow root exists.\"\"\"\n        describe_response = {\n            'result': {\n                'node': {\n                    'nodeName': 'DIV',\n                    'attributes': ['id', 'host'],\n                    'shadowRoots': [\n                        {\n                            'backendNodeId': 42,\n                            'shadowRootType': 'open',\n                        }\n                    ],\n                }\n            }\n        }\n        resolve_response = {\n            'result': {\n                'object': {'objectId': 'shadow-root-resolved-id'}\n            }\n        }\n        host_element._connection_handler.execute_command.side_effect = [\n            describe_response,\n            resolve_response,\n        ]\n\n        shadow_root = await host_element.get_shadow_root()\n\n        assert isinstance(shadow_root, ShadowRoot)\n        assert shadow_root._object_id == 'shadow-root-resolved-id'\n        assert shadow_root.mode == ShadowRootType.OPEN\n        assert shadow_root.host_element is host_element\n\n    @pytest.mark.asyncio\n    async def test_get_shadow_root_closed_mode(self, host_element):\n        \"\"\"get_shadow_root() should handle closed shadow roots.\"\"\"\n        describe_response = {\n            'result': {\n                'node': {\n                    'nodeName': 'DIV',\n                    'attributes': [],\n                    'shadowRoots': [\n                        {\n                            'backendNodeId': 99,\n                            'shadowRootType': 'closed',\n                        }\n                    ],\n                }\n            }\n        }\n        resolve_response = {\n            'result': {\n                'object': {'objectId': 'closed-shadow-id'}\n            }\n        }\n        host_element._connection_handler.execute_command.side_effect = [\n            describe_response,\n            resolve_response,\n        ]\n\n        shadow_root = await host_element.get_shadow_root()\n\n        assert shadow_root.mode == ShadowRootType.CLOSED\n\n    @pytest.mark.asyncio\n    async def test_get_shadow_root_not_found(self, host_element):\n        \"\"\"get_shadow_root() should raise ShadowRootNotFound when no shadow root.\"\"\"\n        describe_response = {\n            'result': {\n                'node': {\n                    'nodeName': 'DIV',\n                    'attributes': ['id', 'no-shadow'],\n                }\n            }\n        }\n        host_element._connection_handler.execute_command.return_value = describe_response\n\n        with pytest.raises(ShadowRootNotFound):\n            await host_element.get_shadow_root()\n\n    @pytest.mark.asyncio\n    async def test_get_shadow_root_empty_shadow_roots_list(self, host_element):\n        \"\"\"get_shadow_root() should raise when shadowRoots is empty list.\"\"\"\n        describe_response = {\n            'result': {\n                'node': {\n                    'nodeName': 'DIV',\n                    'attributes': [],\n                    'shadowRoots': [],\n                }\n            }\n        }\n        host_element._connection_handler.execute_command.return_value = describe_response\n\n        with pytest.raises(ShadowRootNotFound):\n            await host_element.get_shadow_root()\n\n    @pytest.mark.asyncio\n    async def test_get_shadow_root_no_node_id(self, host_element):\n        \"\"\"get_shadow_root() should raise when shadow root has no nodeId.\"\"\"\n        describe_response = {\n            'result': {\n                'node': {\n                    'nodeName': 'DIV',\n                    'attributes': [],\n                    'shadowRoots': [\n                        {\n                            'shadowRootType': 'open',\n                        }\n                    ],\n                }\n            }\n        }\n        host_element._connection_handler.execute_command.return_value = describe_response\n\n        with pytest.raises(ShadowRootNotFound):\n            await host_element.get_shadow_root()\n\n\n# --- Tab.find_shadow_roots() tests ---\n\n\n@pytest_asyncio.fixture\nasync def tab_connection_handler():\n    \"\"\"Mock connection handler for Tab tests.\"\"\"\n    with patch('pydoll.connection.ConnectionHandler', autospec=True) as mock:\n        handler = mock.return_value\n        handler.execute_command = AsyncMock()\n        handler.register_callback = AsyncMock()\n        handler.remove_callback = AsyncMock()\n        handler.clear_callbacks = AsyncMock()\n        handler.network_logs = []\n        handler.dialog = None\n        handler._connection_port = 9222\n        yield handler\n\n\n@pytest_asyncio.fixture\nasync def tab(tab_connection_handler):\n    \"\"\"Tab fixture with mocked dependencies.\"\"\"\n    browser = MagicMock()\n    browser.options = MagicMock()\n    with patch('pydoll.browser.tab.ConnectionHandler', return_value=tab_connection_handler):\n        return Tab(\n            browser=browser,\n            connection_port=9222,\n            target_id=f'test-target-{uuid.uuid4().hex[:8]}',\n        )\n\n\nclass TestTabFindShadowRoots:\n    \"\"\"Tests for Tab.find_shadow_roots().\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_find_shadow_roots_single(self, tab):\n        \"\"\"Should return a single shadow root found in the page.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 2,\n                            'nodeName': 'HTML',\n                            'children': [\n                                {\n                                    'nodeId': 3,\n                                    'backendNodeId': 3,\n                                    'nodeName': 'BODY',\n                                    'children': [\n                                        {\n                                            'nodeId': 4,\n                                            'backendNodeId': 10,\n                                            'nodeName': 'DIV',\n                                            'attributes': ['id', 'host'],\n                                            'shadowRoots': [\n                                                {\n                                                    'nodeId': 5,\n                                                    'backendNodeId': 20,\n                                                    'shadowRootType': 'open',\n                                                    'children': [],\n                                                }\n                                            ],\n                                        }\n                                    ],\n                                }\n                            ],\n                        }\n                    ],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.side_effect = [\n            get_doc_response,\n            {'result': {'object': {'objectId': 'shadow-obj-1'}}},\n            {'result': {'object': {'objectId': 'host-obj-1'}}},\n            {'result': {'node': {'nodeName': 'DIV', 'attributes': ['id', 'host']}}},\n        ]\n\n        result = await tab.find_shadow_roots()\n\n        assert len(result) == 1\n        assert isinstance(result[0], ShadowRoot)\n        assert result[0]._object_id == 'shadow-obj-1'\n        assert result[0].mode == ShadowRootType.OPEN\n        assert result[0].host_element is not None\n        assert result[0].host_element._object_id == 'host-obj-1'\n\n    @pytest.mark.asyncio\n    async def test_find_shadow_roots_none_found(self, tab):\n        \"\"\"Should return empty list when no shadow roots exist.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 2,\n                            'nodeName': 'HTML',\n                            'children': [],\n                        }\n                    ],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.return_value = get_doc_response\n\n        result = await tab.find_shadow_roots()\n\n        assert result == []\n\n    @pytest.mark.asyncio\n    async def test_find_shadow_roots_multiple(self, tab):\n        \"\"\"Should return multiple shadow roots at different depths.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 2,\n                            'nodeName': 'HTML',\n                            'children': [\n                                {\n                                    'nodeId': 3,\n                                    'backendNodeId': 10,\n                                    'nodeName': 'DIV',\n                                    'shadowRoots': [\n                                        {\n                                            'nodeId': 4,\n                                            'backendNodeId': 20,\n                                            'shadowRootType': 'open',\n                                            'children': [],\n                                        }\n                                    ],\n                                },\n                                {\n                                    'nodeId': 5,\n                                    'backendNodeId': 11,\n                                    'nodeName': 'CUSTOM-ELEMENT',\n                                    'shadowRoots': [\n                                        {\n                                            'nodeId': 6,\n                                            'backendNodeId': 30,\n                                            'shadowRootType': 'closed',\n                                            'children': [],\n                                        }\n                                    ],\n                                },\n                            ],\n                        }\n                    ],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.side_effect = [\n            get_doc_response,\n            {'result': {'object': {'objectId': 'shadow-obj-1'}}},\n            {'result': {'object': {'objectId': 'host-obj-1'}}},\n            {'result': {'node': {'nodeName': 'DIV', 'attributes': ['id', 'host1']}}},\n            {'result': {'object': {'objectId': 'shadow-obj-2'}}},\n            {'result': {'object': {'objectId': 'host-obj-2'}}},\n            {'result': {'node': {'nodeName': 'CUSTOM-ELEMENT', 'attributes': []}}},\n        ]\n\n        result = await tab.find_shadow_roots()\n\n        assert len(result) == 2\n        assert result[0].mode == ShadowRootType.OPEN\n        assert result[1].mode == ShadowRootType.CLOSED\n\n    @pytest.mark.asyncio\n    async def test_find_shadow_roots_nested_in_shadow_root(self, tab):\n        \"\"\"Should find shadow roots nested inside other shadow roots.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 10,\n                            'nodeName': 'DIV',\n                            'shadowRoots': [\n                                {\n                                    'nodeId': 3,\n                                    'backendNodeId': 20,\n                                    'shadowRootType': 'open',\n                                    'children': [\n                                        {\n                                            'nodeId': 4,\n                                            'backendNodeId': 11,\n                                            'nodeName': 'INNER-HOST',\n                                            'shadowRoots': [\n                                                {\n                                                    'nodeId': 5,\n                                                    'backendNodeId': 30,\n                                                    'shadowRootType': 'closed',\n                                                    'children': [],\n                                                }\n                                            ],\n                                        }\n                                    ],\n                                }\n                            ],\n                        }\n                    ],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.side_effect = [\n            get_doc_response,\n            {'result': {'object': {'objectId': 'shadow-outer'}}},\n            {'result': {'object': {'objectId': 'host-outer'}}},\n            {'result': {'node': {'nodeName': 'DIV', 'attributes': []}}},\n            {'result': {'object': {'objectId': 'shadow-inner'}}},\n            {'result': {'object': {'objectId': 'host-inner'}}},\n            {'result': {'node': {'nodeName': 'INNER-HOST', 'attributes': []}}},\n        ]\n\n        result = await tab.find_shadow_roots()\n\n        assert len(result) == 2\n        assert result[0]._object_id == 'shadow-outer'\n        assert result[1]._object_id == 'shadow-inner'\n\n    @pytest.mark.asyncio\n    async def test_find_shadow_roots_in_iframe(self, tab):\n        \"\"\"Should find shadow roots inside iframe content documents.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 2,\n                            'nodeName': 'HTML',\n                            'children': [\n                                {\n                                    'nodeId': 3,\n                                    'backendNodeId': 3,\n                                    'nodeName': 'IFRAME',\n                                    'contentDocument': {\n                                        'nodeId': 4,\n                                        'backendNodeId': 4,\n                                        'nodeName': '#document',\n                                        'children': [\n                                            {\n                                                'nodeId': 5,\n                                                'backendNodeId': 15,\n                                                'nodeName': 'BODY',\n                                                'shadowRoots': [\n                                                    {\n                                                        'nodeId': 6,\n                                                        'backendNodeId': 25,\n                                                        'shadowRootType': 'closed',\n                                                        'children': [],\n                                                    }\n                                                ],\n                                            }\n                                        ],\n                                    },\n                                }\n                            ],\n                        }\n                    ],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.side_effect = [\n            get_doc_response,\n            {'result': {'object': {'objectId': 'iframe-shadow-obj'}}},\n            {'result': {'object': {'objectId': 'iframe-host-obj'}}},\n            {'result': {'node': {'nodeName': 'BODY', 'attributes': []}}},\n        ]\n\n        result = await tab.find_shadow_roots()\n\n        assert len(result) == 1\n        assert result[0]._object_id == 'iframe-shadow-obj'\n        assert result[0].mode == ShadowRootType.CLOSED\n\n    @pytest.mark.asyncio\n    async def test_find_shadow_roots_skips_unresolvable(self, tab):\n        \"\"\"Should skip shadow roots that fail to resolve.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 10,\n                            'nodeName': 'DIV',\n                            'shadowRoots': [\n                                {\n                                    'nodeId': 3,\n                                    'backendNodeId': 20,\n                                    'shadowRootType': 'open',\n                                    'children': [],\n                                }\n                            ],\n                        },\n                        {\n                            'nodeId': 4,\n                            'backendNodeId': 11,\n                            'nodeName': 'OTHER',\n                            'shadowRoots': [\n                                {\n                                    'nodeId': 5,\n                                    'backendNodeId': 30,\n                                    'shadowRootType': 'open',\n                                    'children': [],\n                                }\n                            ],\n                        },\n                    ],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.side_effect = [\n            get_doc_response,\n            {'result': {'object': {'objectId': 'shadow-ok'}}},\n            {'result': {'object': {'objectId': 'host-ok'}}},\n            {'result': {'node': {'nodeName': 'DIV', 'attributes': []}}},\n            CommandExecutionTimeout(),\n        ]\n\n        result = await tab.find_shadow_roots()\n\n        assert len(result) == 1\n        assert result[0]._object_id == 'shadow-ok'\n\n    @pytest.mark.asyncio\n    async def test_find_shadow_roots_host_resolution_fails_gracefully(self, tab):\n        \"\"\"Shadow root should still be returned when host resolution fails.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 10,\n                            'nodeName': 'DIV',\n                            'shadowRoots': [\n                                {\n                                    'nodeId': 3,\n                                    'backendNodeId': 20,\n                                    'shadowRootType': 'open',\n                                    'children': [],\n                                }\n                            ],\n                        }\n                    ],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.side_effect = [\n            get_doc_response,\n            {'result': {'object': {'objectId': 'shadow-obj'}}},\n            CommandExecutionTimeout(),\n        ]\n\n        result = await tab.find_shadow_roots()\n\n        assert len(result) == 1\n        assert result[0]._object_id == 'shadow-obj'\n        assert result[0].host_element is None\n\n    @pytest.mark.asyncio\n    async def test_find_shadow_roots_skips_missing_backend_id(self, tab):\n        \"\"\"Should skip shadow roots without backendNodeId.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 10,\n                            'nodeName': 'DIV',\n                            'shadowRoots': [\n                                {\n                                    'shadowRootType': 'open',\n                                    'children': [],\n                                }\n                            ],\n                        }\n                    ],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.return_value = get_doc_response\n\n        result = await tab.find_shadow_roots()\n\n        assert result == []\n\n\nclass TestCollectShadowRootsFromTree:\n    \"\"\"Tests for the _collect_shadow_roots_from_tree static helper.\"\"\"\n\n    def test_empty_tree(self):\n        results = []\n        Tab._collect_shadow_roots_from_tree({}, results)\n        assert results == []\n\n    def test_no_shadow_roots(self):\n        tree = {\n            'nodeId': 1,\n            'backendNodeId': 1,\n            'children': [\n                {'nodeId': 2, 'backendNodeId': 2},\n            ],\n        }\n        results = []\n        Tab._collect_shadow_roots_from_tree(tree, results)\n        assert results == []\n\n    def test_collects_shadow_root(self):\n        shadow = {'backendNodeId': 20, 'shadowRootType': 'open', 'children': []}\n        tree = {\n            'backendNodeId': 10,\n            'shadowRoots': [shadow],\n        }\n        results = []\n        Tab._collect_shadow_roots_from_tree(tree, results)\n        assert len(results) == 1\n        assert results[0] == (shadow, 10)\n\n    def test_collects_from_content_document(self):\n        shadow = {'backendNodeId': 30, 'shadowRootType': 'closed', 'children': []}\n        tree = {\n            'backendNodeId': 1,\n            'children': [\n                {\n                    'backendNodeId': 2,\n                    'nodeName': 'IFRAME',\n                    'contentDocument': {\n                        'backendNodeId': 3,\n                        'children': [\n                            {\n                                'backendNodeId': 15,\n                                'shadowRoots': [shadow],\n                            }\n                        ],\n                    },\n                }\n            ],\n        }\n        results = []\n        Tab._collect_shadow_roots_from_tree(tree, results)\n        assert len(results) == 1\n        assert results[0] == (shadow, 15)\n\n    def test_collects_nested_shadow_roots(self):\n        inner_shadow = {'backendNodeId': 40, 'shadowRootType': 'closed', 'children': []}\n        outer_shadow = {\n            'backendNodeId': 20,\n            'shadowRootType': 'open',\n            'children': [\n                {\n                    'backendNodeId': 30,\n                    'shadowRoots': [inner_shadow],\n                }\n            ],\n        }\n        tree = {\n            'backendNodeId': 10,\n            'shadowRoots': [outer_shadow],\n        }\n        results = []\n        Tab._collect_shadow_roots_from_tree(tree, results)\n        assert len(results) == 2\n        assert results[0] == (outer_shadow, 10)\n        assert results[1] == (inner_shadow, 30)\n\n\nclass TestTabFindShadowRootsDeep:\n    \"\"\"Tests for Tab.find_shadow_roots(deep=True) — OOPIF traversal.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_deep_false_same_as_default(self, tab):\n        \"\"\"deep=False should behave identically to the default (no OOPIF traversal).\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 10,\n                            'nodeName': 'DIV',\n                            'shadowRoots': [\n                                {\n                                    'nodeId': 3,\n                                    'backendNodeId': 20,\n                                    'shadowRootType': 'open',\n                                    'children': [],\n                                }\n                            ],\n                        }\n                    ],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.side_effect = [\n            get_doc_response,\n            {'result': {'object': {'objectId': 'shadow-obj-1'}}},\n            {'result': {'object': {'objectId': 'host-obj-1'}}},\n            {'result': {'node': {'nodeName': 'DIV', 'attributes': ['id', 'host']}}},\n        ]\n\n        result = await tab.find_shadow_roots(deep=False)\n\n        assert len(result) == 1\n        assert result[0]._object_id == 'shadow-obj-1'\n\n    @pytest.mark.asyncio\n    async def test_deep_collects_oopif_shadow_roots(self, tab):\n        \"\"\"deep=True should discover shadow roots inside OOPIF targets.\"\"\"\n        # Main doc has no shadow roots\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.return_value = get_doc_response\n\n        # Mock the browser-level ConnectionHandler used in _collect_oopif_shadow_roots\n        mock_browser_handler = AsyncMock()\n        mock_browser_handler.execute_command = AsyncMock()\n        mock_browser_handler.execute_command.side_effect = [\n            # Target.getTargets\n            {\n                'result': {\n                    'targetInfos': [\n                        {'targetId': 'oopif-1', 'type': 'iframe', 'url': 'https://cf.example.com'},\n                    ]\n                }\n            },\n            # Target.attachToTarget\n            {'result': {'sessionId': 'session-1'}},\n            # DOM.getDocument (in OOPIF)\n            {\n                'result': {\n                    'root': {\n                        'nodeId': 1,\n                        'backendNodeId': 100,\n                        'nodeName': '#document',\n                        'children': [\n                            {\n                                'nodeId': 2,\n                                'backendNodeId': 200,\n                                'nodeName': 'BODY',\n                                'shadowRoots': [\n                                    {\n                                        'nodeId': 3,\n                                        'backendNodeId': 300,\n                                        'shadowRootType': 'closed',\n                                        'children': [],\n                                    }\n                                ],\n                            }\n                        ],\n                    }\n                }\n            },\n            # DOM.resolveNode (shadow root)\n            {'result': {'object': {'objectId': 'oopif-shadow-obj'}}},\n            # DOM.resolveNode (host element)\n            {'result': {'object': {'objectId': 'oopif-host-obj'}}},\n            # DOM.describeNode (host element attrs)\n            {'result': {'node': {'nodeName': 'BODY', 'attributes': []}}},\n        ]\n\n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_browser_handler):\n            result = await tab.find_shadow_roots(deep=True)\n\n        assert len(result) == 1\n        sr = result[0]\n        assert sr._object_id == 'oopif-shadow-obj'\n        assert sr.mode == ShadowRootType.CLOSED\n        assert sr.host_element is not None\n        assert sr.host_element._object_id == 'oopif-host-obj'\n\n    @pytest.mark.asyncio\n    async def test_deep_no_oopif_targets(self, tab):\n        \"\"\"deep=True with no iframe targets returns only main-doc roots.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.return_value = get_doc_response\n\n        mock_browser_handler = AsyncMock()\n        mock_browser_handler.execute_command = AsyncMock()\n        mock_browser_handler.execute_command.return_value = {\n            'result': {'targetInfos': []}\n        }\n\n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_browser_handler):\n            result = await tab.find_shadow_roots(deep=True)\n\n        assert result == []\n\n    @pytest.mark.asyncio\n    async def test_deep_oopif_attachment_fails_gracefully(self, tab):\n        \"\"\"When an OOPIF target fails to attach, others should still be collected.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.return_value = get_doc_response\n\n        mock_browser_handler = AsyncMock()\n        mock_browser_handler.execute_command = AsyncMock()\n        mock_browser_handler.execute_command.side_effect = [\n            # Target.getTargets — two iframe targets\n            {\n                'result': {\n                    'targetInfos': [\n                        {'targetId': 'fail-target', 'type': 'iframe', 'url': 'https://a.com'},\n                        {'targetId': 'ok-target', 'type': 'iframe', 'url': 'https://b.com'},\n                    ]\n                }\n            },\n            # First target: attachment fails\n            WebSocketConnectionClosed(),\n            # Second target: attachment succeeds\n            {'result': {'sessionId': 'session-ok'}},\n            # DOM.getDocument for second target\n            {\n                'result': {\n                    'root': {\n                        'nodeId': 1,\n                        'backendNodeId': 50,\n                        'nodeName': '#document',\n                        'children': [\n                            {\n                                'nodeId': 2,\n                                'backendNodeId': 60,\n                                'nodeName': 'DIV',\n                                'shadowRoots': [\n                                    {\n                                        'nodeId': 3,\n                                        'backendNodeId': 70,\n                                        'shadowRootType': 'open',\n                                        'children': [],\n                                    }\n                                ],\n                            }\n                        ],\n                    }\n                }\n            },\n            # DOM.resolveNode (shadow root)\n            {'result': {'object': {'objectId': 'sr-from-ok-target'}}},\n            # DOM.resolveNode (host element)\n            {'result': {'object': {'objectId': 'host-from-ok-target'}}},\n            # DOM.describeNode (host attrs)\n            {'result': {'node': {'nodeName': 'DIV', 'attributes': ['id', 'widget']}}},\n        ]\n\n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_browser_handler):\n            result = await tab.find_shadow_roots(deep=True)\n\n        assert len(result) == 1\n        assert result[0]._object_id == 'sr-from-ok-target'\n\n    @pytest.mark.asyncio\n    async def test_deep_oopif_shadow_root_has_routing_context(self, tab):\n        \"\"\"ShadowRoot from OOPIF should have _iframe_context with correct routing.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.return_value = get_doc_response\n\n        mock_browser_handler = AsyncMock()\n        mock_browser_handler.execute_command = AsyncMock()\n        mock_browser_handler.execute_command.side_effect = [\n            # Target.getTargets\n            {\n                'result': {\n                    'targetInfos': [\n                        {'targetId': 'oopif-42', 'type': 'iframe', 'url': 'https://cf.example.com'},\n                    ]\n                }\n            },\n            # Target.attachToTarget\n            {'result': {'sessionId': 'sess-42'}},\n            # DOM.getDocument\n            {\n                'result': {\n                    'root': {\n                        'nodeId': 1,\n                        'backendNodeId': 100,\n                        'nodeName': '#document',\n                        'children': [\n                            {\n                                'nodeId': 2,\n                                'backendNodeId': 200,\n                                'nodeName': 'DIV',\n                                'shadowRoots': [\n                                    {\n                                        'nodeId': 3,\n                                        'backendNodeId': 300,\n                                        'shadowRootType': 'closed',\n                                        'children': [],\n                                    }\n                                ],\n                            }\n                        ],\n                    }\n                }\n            },\n            # DOM.resolveNode (shadow root)\n            {'result': {'object': {'objectId': 'sr-obj-42'}}},\n            # DOM.resolveNode (host element)\n            {'result': {'object': {'objectId': 'host-obj-42'}}},\n            # DOM.describeNode (host attrs)\n            {'result': {'node': {'nodeName': 'DIV', 'attributes': ['class', 'turnstile']}}},\n        ]\n\n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_browser_handler):\n            result = await tab.find_shadow_roots(deep=True)\n\n        assert len(result) == 1\n        sr = result[0]\n\n        # Verify the ShadowRoot inherited IFrameContext from host\n        ctx = getattr(sr, '_iframe_context', None)\n        assert ctx is not None\n        assert isinstance(ctx, IFrameContext)\n        assert ctx.frame_id == 'oopif-42'\n        assert ctx.session_id == 'sess-42'\n        assert ctx.session_handler is mock_browser_handler\n\n    @pytest.mark.asyncio\n    async def test_deep_combines_main_and_oopif_roots(self, tab):\n        \"\"\"deep=True should return both main-doc and OOPIF shadow roots.\"\"\"\n        # Main doc has one shadow root\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 10,\n                            'nodeName': 'DIV',\n                            'shadowRoots': [\n                                {\n                                    'nodeId': 3,\n                                    'backendNodeId': 20,\n                                    'shadowRootType': 'open',\n                                    'children': [],\n                                }\n                            ],\n                        }\n                    ],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.side_effect = [\n            get_doc_response,\n            # resolve shadow root\n            {'result': {'object': {'objectId': 'main-shadow'}}},\n            # resolve host\n            {'result': {'object': {'objectId': 'main-host'}}},\n            # describe host\n            {'result': {'node': {'nodeName': 'DIV', 'attributes': ['id', 'main']}}},\n        ]\n\n        mock_browser_handler = AsyncMock()\n        mock_browser_handler.execute_command = AsyncMock()\n        mock_browser_handler.execute_command.side_effect = [\n            # Target.getTargets\n            {\n                'result': {\n                    'targetInfos': [\n                        {'targetId': 'oopif-1', 'type': 'iframe', 'url': 'https://cf.example.com'},\n                    ]\n                }\n            },\n            # Target.attachToTarget\n            {'result': {'sessionId': 'session-1'}},\n            # DOM.getDocument\n            {\n                'result': {\n                    'root': {\n                        'nodeId': 1,\n                        'backendNodeId': 50,\n                        'nodeName': '#document',\n                        'children': [\n                            {\n                                'nodeId': 2,\n                                'backendNodeId': 60,\n                                'nodeName': 'BODY',\n                                'shadowRoots': [\n                                    {\n                                        'nodeId': 3,\n                                        'backendNodeId': 70,\n                                        'shadowRootType': 'closed',\n                                        'children': [],\n                                    }\n                                ],\n                            }\n                        ],\n                    }\n                }\n            },\n            # DOM.resolveNode (shadow root)\n            {'result': {'object': {'objectId': 'oopif-shadow'}}},\n            # DOM.resolveNode (host)\n            {'result': {'object': {'objectId': 'oopif-host'}}},\n            # DOM.describeNode (host attrs)\n            {'result': {'node': {'nodeName': 'BODY', 'attributes': []}}},\n        ]\n\n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_browser_handler):\n            result = await tab.find_shadow_roots(deep=True)\n\n        assert len(result) == 2\n        assert result[0]._object_id == 'main-shadow'\n        assert result[0].mode == ShadowRootType.OPEN\n        assert result[1]._object_id == 'oopif-shadow'\n        assert result[1].mode == ShadowRootType.CLOSED\n\n    @pytest.mark.asyncio\n    async def test_deep_oopif_no_host_sets_iframe_context_on_shadow_root(self, tab):\n        \"\"\"When host resolution fails, IFrameContext should be set directly on ShadowRoot.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.return_value = get_doc_response\n\n        mock_browser_handler = AsyncMock()\n        mock_browser_handler.execute_command = AsyncMock()\n        mock_browser_handler.execute_command.side_effect = [\n            # Target.getTargets\n            {\n                'result': {\n                    'targetInfos': [\n                        {'targetId': 'oopif-99', 'type': 'iframe', 'url': 'https://cf.example.com'},\n                    ]\n                }\n            },\n            # Target.attachToTarget\n            {'result': {'sessionId': 'sess-99'}},\n            # DOM.getDocument\n            {\n                'result': {\n                    'root': {\n                        'nodeId': 1,\n                        'backendNodeId': 100,\n                        'nodeName': '#document',\n                        'children': [\n                            {\n                                'nodeId': 2,\n                                'backendNodeId': 200,\n                                'nodeName': 'DIV',\n                                'shadowRoots': [\n                                    {\n                                        'nodeId': 3,\n                                        'backendNodeId': 300,\n                                        'shadowRootType': 'open',\n                                        'children': [],\n                                    }\n                                ],\n                            }\n                        ],\n                    }\n                }\n            },\n            # DOM.resolveNode (shadow root) - success\n            {'result': {'object': {'objectId': 'sr-obj-99'}}},\n            # DOM.resolveNode (host element) - fails\n            CommandExecutionTimeout(),\n        ]\n\n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_browser_handler):\n            result = await tab.find_shadow_roots(deep=True)\n\n        assert len(result) == 1\n        sr = result[0]\n        assert sr.host_element is None\n        # IFrameContext set directly on ShadowRoot since no host\n        ctx = getattr(sr, '_iframe_context', None)\n        assert ctx is not None\n        assert ctx.frame_id == 'oopif-99'\n        assert ctx.session_id == 'sess-99'\n\n\nclass TestTabFindShadowRootsTimeout:\n    \"\"\"Tests for Tab.find_shadow_roots(timeout=...) — polling behavior.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_timeout_zero_returns_immediately(self, tab):\n        \"\"\"timeout=0 (default) should return immediately without polling.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.return_value = get_doc_response\n\n        result = await tab.find_shadow_roots(timeout=0)\n\n        assert result == []\n\n    @pytest.mark.asyncio\n    async def test_timeout_raises_when_no_shadow_roots_found(self, tab):\n        \"\"\"Should raise WaitElementTimeout when no shadow roots appear within timeout.\"\"\"\n        get_doc_response = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.return_value = get_doc_response\n\n        with pytest.raises(WaitElementTimeout):\n            await tab.find_shadow_roots(timeout=1)\n\n    @pytest.mark.asyncio\n    async def test_timeout_returns_when_shadow_roots_appear(self, tab):\n        \"\"\"Should return shadow roots as soon as they appear during polling.\"\"\"\n        empty_doc = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [],\n                }\n            }\n        }\n        doc_with_shadow = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 10,\n                            'nodeName': 'DIV',\n                            'shadowRoots': [\n                                {\n                                    'nodeId': 3,\n                                    'backendNodeId': 20,\n                                    'shadowRootType': 'open',\n                                    'children': [],\n                                }\n                            ],\n                        }\n                    ],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.side_effect = [\n            # First poll: empty\n            empty_doc,\n            # Second poll: shadow root appears\n            doc_with_shadow,\n            # resolve shadow root\n            {'result': {'object': {'objectId': 'shadow-obj'}}},\n            # resolve host\n            {'result': {'object': {'objectId': 'host-obj'}}},\n            # describe host attrs\n            {'result': {'node': {'nodeName': 'DIV', 'attributes': ['id', 'host']}}},\n        ]\n\n        result = await tab.find_shadow_roots(timeout=5)\n\n        assert len(result) == 1\n        assert result[0]._object_id == 'shadow-obj'\n\n    @pytest.mark.asyncio\n    async def test_timeout_with_deep_waits_for_oopif_roots(self, tab):\n        \"\"\"timeout + deep=True should poll until OOPIF shadow roots appear.\"\"\"\n        empty_doc = {\n            'result': {\n                'root': {\n                    'nodeId': 1,\n                    'backendNodeId': 1,\n                    'nodeName': '#document',\n                    'children': [],\n                }\n            }\n        }\n        tab._connection_handler.execute_command.return_value = empty_doc\n\n        call_count = 0\n\n        async def browser_side_effect(cmd, **kwargs):\n            nonlocal call_count\n            call_count += 1\n            # First cycle: no iframe targets\n            if call_count == 1:\n                return {'result': {'targetInfos': []}}\n            # Second cycle: iframe target appears\n            if call_count == 2:\n                return {\n                    'result': {\n                        'targetInfos': [\n                            {'targetId': 'oopif-1', 'type': 'iframe', 'url': 'https://cf.test'},\n                        ]\n                    }\n                }\n            if call_count == 3:\n                return {'result': {'sessionId': 'sess-1'}}\n            if call_count == 4:\n                return {\n                    'result': {\n                        'root': {\n                            'nodeId': 1,\n                            'backendNodeId': 100,\n                            'nodeName': '#document',\n                            'children': [\n                                {\n                                    'nodeId': 2,\n                                    'backendNodeId': 200,\n                                    'nodeName': 'BODY',\n                                    'shadowRoots': [\n                                        {\n                                            'nodeId': 3,\n                                            'backendNodeId': 300,\n                                            'shadowRootType': 'closed',\n                                            'children': [],\n                                        }\n                                    ],\n                                }\n                            ],\n                        }\n                    }\n                }\n            if call_count == 5:\n                return {'result': {'object': {'objectId': 'oopif-sr-obj'}}}\n            if call_count == 6:\n                return {'result': {'object': {'objectId': 'oopif-host-obj'}}}\n            if call_count == 7:\n                return {'result': {'node': {'nodeName': 'BODY', 'attributes': []}}}\n            return {}\n\n        mock_browser_handler = AsyncMock()\n        mock_browser_handler.execute_command = AsyncMock(side_effect=browser_side_effect)\n\n        with patch('pydoll.browser.tab.ConnectionHandler', return_value=mock_browser_handler):\n            result = await tab.find_shadow_roots(deep=True, timeout=5)\n\n        assert len(result) == 1\n        assert result[0]._object_id == 'oopif-sr-obj'\n        assert result[0].mode == ShadowRootType.CLOSED\n\n\n# --- WebElement.get_shadow_root(timeout=...) tests ---\n\n\nclass TestGetShadowRootTimeout:\n    \"\"\"Tests for WebElement.get_shadow_root(timeout=...) — polling behavior.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_timeout_zero_raises_immediately(self, mock_connection_handler):\n        \"\"\"timeout=0 (default) should raise ShadowRootNotFound immediately.\"\"\"\n        element = WebElement('elem-obj-1', mock_connection_handler)\n        mock_connection_handler.execute_command.return_value = {\n            'result': {\n                'node': {\n                    'nodeId': 1,\n                    'backendNodeId': 10,\n                    'nodeName': 'DIV',\n                }\n            }\n        }\n\n        with pytest.raises(ShadowRootNotFound):\n            await element.get_shadow_root()\n\n    @pytest.mark.asyncio\n    async def test_timeout_raises_wait_element_timeout(self, mock_connection_handler):\n        \"\"\"Should raise WaitElementTimeout when shadow root doesn't appear within timeout.\"\"\"\n        element = WebElement('elem-obj-1', mock_connection_handler)\n        mock_connection_handler.execute_command.return_value = {\n            'result': {\n                'node': {\n                    'nodeId': 1,\n                    'backendNodeId': 10,\n                    'nodeName': 'DIV',\n                }\n            }\n        }\n\n        with pytest.raises(WaitElementTimeout):\n            await element.get_shadow_root(timeout=1)\n\n    @pytest.mark.asyncio\n    async def test_timeout_returns_when_shadow_root_appears(self, mock_connection_handler):\n        \"\"\"Should return the shadow root as soon as it appears during polling.\"\"\"\n        element = WebElement('elem-obj-1', mock_connection_handler)\n        no_shadow = {\n            'result': {\n                'node': {\n                    'nodeId': 1,\n                    'backendNodeId': 10,\n                    'nodeName': 'DIV',\n                }\n            }\n        }\n        with_shadow = {\n            'result': {\n                'node': {\n                    'nodeId': 1,\n                    'backendNodeId': 10,\n                    'nodeName': 'DIV',\n                    'shadowRoots': [\n                        {\n                            'nodeId': 2,\n                            'backendNodeId': 20,\n                            'shadowRootType': 'closed',\n                        }\n                    ],\n                }\n            }\n        }\n        mock_connection_handler.execute_command.side_effect = [\n            # First poll: no shadow root\n            no_shadow,\n            # Second poll: shadow root appears\n            with_shadow,\n            # resolve shadow root\n            {'result': {'object': {'objectId': 'sr-delayed'}}},\n        ]\n\n        result = await element.get_shadow_root(timeout=5)\n\n        assert result._object_id == 'sr-delayed'\n        assert result.mode == ShadowRootType.CLOSED\n        assert result.host_element is element\n"
  },
  {
    "path": "tests/test_shadow_root_integration.py",
    "content": "\"\"\"Integration tests for Shadow DOM support (open, closed, nested).\"\"\"\n\nimport asyncio\nfrom pathlib import Path\n\nimport pytest\n\nfrom pydoll.browser.chromium import Chrome\nfrom pydoll.elements.shadow_root import ShadowRoot\nfrom pydoll.elements.web_element import WebElement\nfrom pydoll.exceptions import ShadowRootNotFound\nfrom pydoll.protocol.dom.types import ShadowRootType\n\nTEST_PAGE = f'file://{(Path(__file__).parent / \"pages\" / \"shadow_dom_test.html\").absolute()}'\n\n\nclass TestOpenShadowRoot:\n    \"\"\"Tests for open shadow root access and element finding.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_shadow_root_open(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            host = await tab.find(id='open-host')\n            shadow = await host.get_shadow_root()\n\n            assert isinstance(shadow, ShadowRoot)\n            assert shadow.mode == ShadowRootType.OPEN\n            assert shadow.host_element is host\n\n    @pytest.mark.asyncio\n    async def test_find_elements_in_open_shadow(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            host = await tab.find(id='open-host')\n            shadow = await host.get_shadow_root()\n\n            text_el = await shadow.query('.open-text')\n            assert isinstance(text_el, WebElement)\n            text = await text_el.text\n            assert text == 'Open shadow content'\n\n            btn = await shadow.query('#open-btn')\n            assert btn is not None\n\n    @pytest.mark.asyncio\n    async def test_query_in_open_shadow(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            host = await tab.find(id='open-host')\n            shadow = await host.get_shadow_root()\n\n            input_el = await shadow.query('input[type=\"email\"]')\n            assert input_el is not None\n\n    @pytest.mark.asyncio\n    async def test_find_all_in_open_shadow(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            host = await tab.find(id='open-host')\n            shadow = await host.get_shadow_root()\n\n            buttons = await shadow.query('.shadow-btn', find_all=True)\n            assert len(buttons) == 1\n\n    @pytest.mark.asyncio\n    async def test_inner_html_open(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            host = await tab.find(id='open-host')\n            shadow = await host.get_shadow_root()\n\n            html = await shadow.inner_html\n            assert 'Open shadow content' in html\n\n\nclass TestClosedShadowRoot:\n    \"\"\"Tests for closed shadow root access via CDP bypass.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_shadow_root_closed(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            host = await tab.find(id='closed-host')\n            shadow = await host.get_shadow_root()\n\n            assert isinstance(shadow, ShadowRoot)\n            assert shadow.mode == ShadowRootType.CLOSED\n\n    @pytest.mark.asyncio\n    async def test_find_elements_in_closed_shadow(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            host = await tab.find(id='closed-host')\n            shadow = await host.get_shadow_root()\n\n            text_el = await shadow.query('.closed-text')\n            assert isinstance(text_el, WebElement)\n            text = await text_el.text\n            assert text == 'Closed shadow content'\n\n    @pytest.mark.asyncio\n    async def test_query_in_closed_shadow(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            host = await tab.find(id='closed-host')\n            shadow = await host.get_shadow_root()\n\n            btn = await shadow.query('#closed-btn')\n            assert btn is not None\n\n            input_el = await shadow.query('input[type=\"password\"]')\n            assert input_el is not None\n\n    @pytest.mark.asyncio\n    async def test_inner_html_closed(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            host = await tab.find(id='closed-host')\n            shadow = await host.get_shadow_root()\n\n            html = await shadow.inner_html\n            assert 'Closed shadow content' in html\n\n\nclass TestNestedShadowRoots:\n    \"\"\"Tests for nested shadow roots (open -> closed).\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_nested_open_then_closed(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            nested_host = await tab.find(id='nested-host')\n            outer_shadow = await nested_host.get_shadow_root()\n            assert outer_shadow.mode == ShadowRootType.OPEN\n\n            outer_text = await outer_shadow.query('.outer-text')\n            text = await outer_text.text\n            assert text == 'Outer shadow'\n\n            inner_host = await outer_shadow.query('#inner-host')\n            inner_shadow = await inner_host.get_shadow_root()\n            assert inner_shadow.mode == ShadowRootType.CLOSED\n\n            inner_text = await inner_shadow.query('.inner-text')\n            text = await inner_text.text\n            assert text == 'Inner closed shadow'\n\n            deep_btn = await inner_shadow.query('#deep-btn')\n            assert deep_btn is not None\n\n\nclass TestShadowRootNotPresent:\n    \"\"\"Tests for elements without shadow roots.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_no_shadow_root_raises(self, ci_chrome_options):\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(TEST_PAGE)\n            await asyncio.sleep(0.5)\n\n            h1 = await tab.find(tag_name='h1')\n            with pytest.raises(ShadowRootNotFound):\n                await h1.get_shadow_root()\n"
  },
  {
    "path": "tests/test_socks5_proxy_forwarder.py",
    "content": "\"\"\"Tests for pydoll.utils.socks5_proxy_forwarder.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\n\nfrom pydoll.utils.socks5_proxy_forwarder import (\n    HANDSHAKE_TIMEOUT,\n    REPLY_ADDRESS_TYPE_NOT_SUPPORTED,\n    REPLY_COMMAND_NOT_SUPPORTED,\n    REPLY_CONNECTION_REFUSED,\n    REPLY_GENERAL_FAILURE,\n    REPLY_SUCCESS,\n    SOCKS5Forwarder,\n    _close_writer,\n    _HandshakeError,\n    _pipe,\n    _read_exact,\n    _skip_bnd_address,\n    _suppress_closed,\n)\n\n\n# ---------------------------------------------------------------------------\n# Fixtures\n# ---------------------------------------------------------------------------\n\n\n@pytest.fixture\ndef forwarder():\n    return SOCKS5Forwarder(\n        remote_host='proxy.example.com',\n        remote_port=1080,\n        username='user',\n        password='pass',\n        local_host='127.0.0.1',\n        local_port=0,\n    )\n\n\n@pytest.fixture\ndef mock_reader():\n    reader = AsyncMock(spec=asyncio.StreamReader)\n    return reader\n\n\n@pytest.fixture\ndef mock_writer():\n    writer = MagicMock(spec=asyncio.StreamWriter)\n    writer.close = MagicMock()\n    writer.write = MagicMock()\n    writer.drain = AsyncMock()\n    writer.wait_closed = AsyncMock()\n    return writer\n\n\n# ---------------------------------------------------------------------------\n# _suppress_closed\n# ---------------------------------------------------------------------------\n\n\nclass TestSuppressClosed:\n    def test_suppresses_os_error(self):\n        with _suppress_closed():\n            raise OSError('transport closed')\n\n    def test_suppresses_connection_reset(self):\n        with _suppress_closed():\n            raise ConnectionResetError('reset')\n\n    def test_does_not_suppress_value_error(self):\n        with pytest.raises(ValueError, match='not an os error'):\n            with _suppress_closed():\n                raise ValueError('not an os error')\n\n    def test_no_error_passes_through(self):\n        with _suppress_closed():\n            pass  # no exception\n\n\n# ---------------------------------------------------------------------------\n# _HandshakeError\n# ---------------------------------------------------------------------------\n\n\nclass TestHandshakeError:\n    def test_default_reply_code(self):\n        exc = _HandshakeError('something failed')\n        assert exc.reply_code == REPLY_GENERAL_FAILURE\n        assert str(exc) == 'something failed'\n\n    def test_custom_reply_code(self):\n        exc = _HandshakeError('cmd not supported', reply_code=REPLY_COMMAND_NOT_SUPPORTED)\n        assert exc.reply_code == REPLY_COMMAND_NOT_SUPPORTED\n\n    def test_connection_refused_reply_code(self):\n        exc = _HandshakeError('refused', reply_code=REPLY_CONNECTION_REFUSED)\n        assert exc.reply_code == REPLY_CONNECTION_REFUSED\n\n    def test_send_reply_defaults_to_true(self):\n        exc = _HandshakeError('fail')\n        assert exc.send_reply is True\n\n    def test_send_reply_false(self):\n        exc = _HandshakeError('no auth', send_reply=False)\n        assert exc.send_reply is False\n\n\n# ---------------------------------------------------------------------------\n# _read_exact\n# ---------------------------------------------------------------------------\n\n\nclass TestReadExact:\n    @pytest.mark.asyncio\n    async def test_returns_exact_bytes(self, mock_reader):\n        mock_reader.readexactly = AsyncMock(return_value=b'\\x05\\x01')\n        result = await _read_exact(mock_reader, 2)\n        assert result == b'\\x05\\x01'\n        mock_reader.readexactly.assert_awaited_once_with(2)\n\n    @pytest.mark.asyncio\n    async def test_incomplete_read_raises_handshake_error(self, mock_reader):\n        mock_reader.readexactly = AsyncMock(\n            side_effect=asyncio.IncompleteReadError(partial=b'\\x05', expected=2)\n        )\n        with pytest.raises(_HandshakeError, match='Connection closed prematurely'):\n            await _read_exact(mock_reader, 2)\n\n    @pytest.mark.asyncio\n    async def test_incomplete_read_has_general_failure_code(self, mock_reader):\n        mock_reader.readexactly = AsyncMock(\n            side_effect=asyncio.IncompleteReadError(partial=b'', expected=4)\n        )\n        with pytest.raises(_HandshakeError) as exc_info:\n            await _read_exact(mock_reader, 4)\n        assert exc_info.value.reply_code == REPLY_GENERAL_FAILURE\n\n    @pytest.mark.asyncio\n    async def test_timeout_raises_handshake_error(self, mock_reader):\n        async def hang_forever(n):\n            await asyncio.sleep(999)\n\n        mock_reader.readexactly = hang_forever\n\n        with patch(\n            'pydoll.utils.socks5_proxy_forwarder.HANDSHAKE_TIMEOUT', 0.01\n        ):\n            with pytest.raises(_HandshakeError, match='Timed out reading'):\n                await _read_exact(mock_reader, 2)\n\n    @pytest.mark.asyncio\n    async def test_timeout_has_general_failure_code(self, mock_reader):\n        async def hang_forever(n):\n            await asyncio.sleep(999)\n\n        mock_reader.readexactly = hang_forever\n\n        with patch(\n            'pydoll.utils.socks5_proxy_forwarder.HANDSHAKE_TIMEOUT', 0.01\n        ):\n            with pytest.raises(_HandshakeError) as exc_info:\n                await _read_exact(mock_reader, 2)\n            assert exc_info.value.reply_code == REPLY_GENERAL_FAILURE\n\n    @pytest.mark.asyncio\n    async def test_peer_label_in_timeout_message(self, mock_reader):\n        async def hang_forever(n):\n            await asyncio.sleep(999)\n\n        mock_reader.readexactly = hang_forever\n\n        with patch(\n            'pydoll.utils.socks5_proxy_forwarder.HANDSHAKE_TIMEOUT', 0.01\n        ):\n            with pytest.raises(_HandshakeError, match='from remote proxy'):\n                await _read_exact(mock_reader, 2, peer='remote proxy')\n\n    @pytest.mark.asyncio\n    async def test_peer_label_in_incomplete_read_message(self, mock_reader):\n        mock_reader.readexactly = AsyncMock(\n            side_effect=asyncio.IncompleteReadError(partial=b'\\x05', expected=4)\n        )\n        with pytest.raises(_HandshakeError, match='from client'):\n            await _read_exact(mock_reader, 4, peer='client')\n\n\n# ---------------------------------------------------------------------------\n# _skip_bnd_address (uses _read_exact, not raw readexactly)\n# ---------------------------------------------------------------------------\n\n\nclass TestSkipBndAddress:\n    @pytest.mark.asyncio\n    async def test_ipv4(self, mock_reader):\n        mock_reader.readexactly = AsyncMock(return_value=b'\\x00' * 6)\n        await _skip_bnd_address(mock_reader, 0x01)\n        mock_reader.readexactly.assert_awaited_once_with(6)\n\n    @pytest.mark.asyncio\n    async def test_domain(self, mock_reader):\n        call_count = 0\n        responses = [b'\\x0b', b'\\x00' * 13]\n\n        async def fake_readexactly(n):\n            nonlocal call_count\n            result = responses[call_count]\n            call_count += 1\n            return result\n\n        mock_reader.readexactly = fake_readexactly\n        await _skip_bnd_address(mock_reader, 0x03)\n        assert call_count == 2\n\n    @pytest.mark.asyncio\n    async def test_ipv6(self, mock_reader):\n        mock_reader.readexactly = AsyncMock(return_value=b'\\x00' * 18)\n        await _skip_bnd_address(mock_reader, 0x04)\n        mock_reader.readexactly.assert_awaited_once_with(18)\n\n    @pytest.mark.asyncio\n    async def test_incomplete_read_propagates(self, mock_reader):\n        mock_reader.readexactly = AsyncMock(\n            side_effect=asyncio.IncompleteReadError(partial=b'', expected=6)\n        )\n        with pytest.raises(_HandshakeError, match='Connection closed prematurely'):\n            await _skip_bnd_address(mock_reader, 0x01)\n\n\n# ---------------------------------------------------------------------------\n# _close_writer\n# ---------------------------------------------------------------------------\n\n\nclass TestCloseWriter:\n    @pytest.mark.asyncio\n    async def test_calls_close_and_wait_closed(self, mock_writer):\n        await _close_writer(mock_writer)\n        mock_writer.close.assert_called_once()\n        mock_writer.wait_closed.assert_awaited_once()\n\n    @pytest.mark.asyncio\n    async def test_suppresses_os_error_on_close(self):\n        writer = MagicMock(spec=asyncio.StreamWriter)\n        writer.close = MagicMock(side_effect=OSError('already closed'))\n        writer.wait_closed = AsyncMock()\n        await _close_writer(writer)  # should not raise\n\n    @pytest.mark.asyncio\n    async def test_suppresses_os_error_on_wait_closed(self):\n        writer = MagicMock(spec=asyncio.StreamWriter)\n        writer.close = MagicMock()\n        writer.wait_closed = AsyncMock(side_effect=OSError('transport closed'))\n        await _close_writer(writer)  # should not raise\n\n\n# ---------------------------------------------------------------------------\n# _pipe\n# ---------------------------------------------------------------------------\n\n\nclass TestPipe:\n    @pytest.mark.asyncio\n    async def test_forwards_data_until_eof(self, mock_reader, mock_writer):\n        mock_reader.read = AsyncMock(side_effect=[b'hello', b'world', b''])\n        await _pipe(mock_reader, mock_writer, 'test')\n        assert mock_writer.write.call_count == 2\n        mock_writer.write.assert_any_call(b'hello')\n        mock_writer.write.assert_any_call(b'world')\n\n    @pytest.mark.asyncio\n    async def test_closes_writer_on_eof(self, mock_reader, mock_writer):\n        mock_reader.read = AsyncMock(return_value=b'')\n        await _pipe(mock_reader, mock_writer, 'test')\n        mock_writer.close.assert_called_once()\n        mock_writer.wait_closed.assert_awaited_once()\n\n    @pytest.mark.asyncio\n    async def test_closes_writer_on_connection_reset(self, mock_reader, mock_writer):\n        mock_reader.read = AsyncMock(side_effect=ConnectionResetError)\n        await _pipe(mock_reader, mock_writer, 'test')\n        mock_writer.close.assert_called_once()\n\n\n# ---------------------------------------------------------------------------\n# SOCKS5Forwarder.__init__ — credential length validation\n# ---------------------------------------------------------------------------\n\n\nclass TestCredentialValidation:\n    def test_valid_credentials(self):\n        fwd = SOCKS5Forwarder(\n            remote_host='host',\n            remote_port=1080,\n            username='user',\n            password='pass',\n        )\n        assert fwd.username == 'user'\n        assert fwd.password == 'pass'\n\n    def test_username_too_long(self):\n        long_user = 'a' * 256\n        with pytest.raises(ValueError, match='username must be at most 255 bytes'):\n            SOCKS5Forwarder(\n                remote_host='host',\n                remote_port=1080,\n                username=long_user,\n                password='pass',\n            )\n\n    def test_password_too_long(self):\n        long_pass = 'b' * 256\n        with pytest.raises(ValueError, match='password must be at most 255 bytes'):\n            SOCKS5Forwarder(\n                remote_host='host',\n                remote_port=1080,\n                username='user',\n                password=long_pass,\n            )\n\n    def test_max_length_credentials_accepted(self):\n        fwd = SOCKS5Forwarder(\n            remote_host='host',\n            remote_port=1080,\n            username='a' * 255,\n            password='b' * 255,\n        )\n        assert len(fwd.username) == 255\n\n    def test_multibyte_username_too_long(self):\n        # Each emoji is 4 bytes in UTF-8; 64 emojis = 256 bytes > 255\n        long_user = '\\U0001f600' * 64\n        with pytest.raises(ValueError, match='username must be at most 255 bytes'):\n            SOCKS5Forwarder(\n                remote_host='host',\n                remote_port=1080,\n                username=long_user,\n                password='pass',\n            )\n\n\n# ---------------------------------------------------------------------------\n# SOCKS5Forwarder.start — non-loopback warning\n# ---------------------------------------------------------------------------\n\n\nclass TestNonLoopbackWarning:\n    @pytest.mark.asyncio\n    async def test_loopback_no_warning(self, forwarder, caplog):\n        mock_server = AsyncMock()\n        mock_sock = MagicMock()\n        mock_sock.getsockname.return_value = ('127.0.0.1', 9999)\n        mock_server.sockets = [mock_sock]\n\n        with patch('asyncio.start_server', return_value=mock_server):\n            with caplog.at_level(logging.WARNING):\n                await forwarder.start()\n            assert 'non-loopback' not in caplog.text\n\n    @pytest.mark.asyncio\n    async def test_non_loopback_warns(self, caplog):\n        fwd = SOCKS5Forwarder(\n            remote_host='proxy.example.com',\n            remote_port=1080,\n            username='user',\n            password='pass',\n            local_host='0.0.0.0',\n        )\n        mock_server = AsyncMock()\n        mock_sock = MagicMock()\n        mock_sock.getsockname.return_value = ('0.0.0.0', 9999)\n        mock_server.sockets = [mock_sock]\n\n        with patch('asyncio.start_server', return_value=mock_server):\n            with caplog.at_level(logging.WARNING):\n                await fwd.start()\n            assert 'non-loopback' in caplog.text\n            assert '0.0.0.0' in caplog.text\n\n    @pytest.mark.asyncio\n    async def test_ipv6_non_loopback_warns(self, caplog):\n        fwd = SOCKS5Forwarder(\n            remote_host='proxy.example.com',\n            remote_port=1080,\n            username='user',\n            password='pass',\n            local_host='::',\n        )\n        mock_server = AsyncMock()\n        mock_sock = MagicMock()\n        mock_sock.getsockname.return_value = ('::', 9999)\n        mock_server.sockets = [mock_sock]\n\n        with patch('asyncio.start_server', return_value=mock_server):\n            with caplog.at_level(logging.WARNING):\n                await fwd.start()\n            assert 'non-loopback' in caplog.text\n\n    @pytest.mark.asyncio\n    async def test_hostname_does_not_crash(self, caplog):\n        \"\"\"local_host='localhost' should not raise ValueError from ip_address().\"\"\"\n        fwd = SOCKS5Forwarder(\n            remote_host='proxy.example.com',\n            remote_port=1080,\n            username='user',\n            password='pass',\n            local_host='localhost',\n        )\n        mock_server = AsyncMock()\n        mock_sock = MagicMock()\n        mock_sock.getsockname.return_value = ('127.0.0.1', 9999)\n        mock_server.sockets = [mock_sock]\n\n        with patch('asyncio.start_server', return_value=mock_server):\n            with caplog.at_level(logging.WARNING):\n                await fwd.start()\n            assert 'non-loopback' not in caplog.text\n\n    @pytest.mark.asyncio\n    async def test_non_localhost_hostname_logs_debug(self, caplog):\n        \"\"\"A non-'localhost' hostname triggers a debug-level message.\"\"\"\n        fwd = SOCKS5Forwarder(\n            remote_host='proxy.example.com',\n            remote_port=1080,\n            username='user',\n            password='pass',\n            local_host='myhost.local',\n        )\n        mock_server = AsyncMock()\n        mock_sock = MagicMock()\n        mock_sock.getsockname.return_value = ('192.168.1.5', 9999)\n        mock_server.sockets = [mock_sock]\n\n        with patch('asyncio.start_server', return_value=mock_server):\n            with caplog.at_level(logging.DEBUG):\n                await fwd.start()\n            assert 'not an IP literal' in caplog.text\n\n    @pytest.mark.asyncio\n    async def test_multi_socket_divergent_ports_raises(self, forwarder):\n        \"\"\"start() raises RuntimeError if sockets have different ports.\"\"\"\n        mock_server = AsyncMock()\n        sock1 = MagicMock()\n        sock1.getsockname.return_value = ('127.0.0.1', 9998)\n        sock2 = MagicMock()\n        sock2.getsockname.return_value = ('::1', 9999)\n        mock_server.sockets = [sock1, sock2]\n        mock_server.close = MagicMock()\n        mock_server.wait_closed = AsyncMock()\n\n        with patch('asyncio.start_server', return_value=mock_server):\n            with pytest.raises(RuntimeError, match='different ports'):\n                await forwarder.start()\n\n    @pytest.mark.asyncio\n    async def test_multi_socket_same_port_ok(self, forwarder):\n        \"\"\"start() succeeds when all sockets share the same port.\"\"\"\n        mock_server = AsyncMock()\n        sock1 = MagicMock()\n        sock1.getsockname.return_value = ('127.0.0.1', 9999)\n        sock2 = MagicMock()\n        sock2.getsockname.return_value = ('::1', 9999)\n        mock_server.sockets = [sock1, sock2]\n\n        with patch('asyncio.start_server', return_value=mock_server):\n            await forwarder.start()\n        assert forwarder.local_port == 9999\n\n\n# ---------------------------------------------------------------------------\n# Credential masking in logs\n# ---------------------------------------------------------------------------\n\n\nclass TestCredentialMasking:\n    @pytest.mark.asyncio\n    async def test_remote_handshake_does_not_log_credentials(self, forwarder, caplog):\n        \"\"\"Verify that _remote_handshake logs ulen/plen instead of raw hex.\"\"\"\n        reader = AsyncMock(spec=asyncio.StreamReader)\n        writer = MagicMock(spec=asyncio.StreamWriter)\n        writer.write = MagicMock()\n        writer.drain = AsyncMock()\n\n        # method selection: server picks username/password auth\n        method_resp = bytes([0x05, 0x02])\n        # auth response: success\n        auth_resp = bytes([0x01, 0x00])\n        # connect reply: success + IPv4\n        connect_reply = bytes([0x05, 0x00, 0x00, 0x01])\n        # BND.ADDR (IPv4) + BND.PORT\n        bnd_addr = b'\\x00\\x00\\x00\\x00'\n        bnd_port = b'\\x00\\x00'\n\n        call_count = 0\n        responses = [method_resp, auth_resp, connect_reply, bnd_addr, bnd_port]\n\n        async def fake_readexactly(n):\n            nonlocal call_count\n            result = responses[call_count]\n            call_count += 1\n            return result\n\n        reader.readexactly = fake_readexactly\n\n        with caplog.at_level(logging.DEBUG):\n            await forwarder._remote_handshake(\n                reader, writer, bytes([0x01, 127, 0, 0, 1]), 80\n            )\n\n        log_text = caplog.text\n        # The raw username/password should NOT appear in hex form\n        username_hex = forwarder.username.encode().hex()\n        password_hex = forwarder.password.encode().hex()\n        assert username_hex not in log_text or 'ulen=' in log_text\n        # But we should see the safe format\n        assert 'ulen=4 plen=4' in log_text\n\n    @pytest.mark.asyncio\n    async def test_remote_connect_failure_propagates_rep_code(self, forwarder):\n        \"\"\"Remote proxy CONNECT failure REP code is forwarded to _HandshakeError.\"\"\"\n        reader = AsyncMock(spec=asyncio.StreamReader)\n        writer = MagicMock(spec=asyncio.StreamWriter)\n        writer.write = MagicMock()\n        writer.drain = AsyncMock()\n\n        # method selection: no-auth\n        method_resp = bytes([0x05, 0x00])\n        # connect reply: 0x05 = connection refused\n        connect_reply = bytes([0x05, 0x05, 0x00, 0x01])\n\n        call_count = 0\n        responses = [method_resp, connect_reply]\n\n        async def fake_readexactly(n):\n            nonlocal call_count\n            result = responses[call_count]\n            call_count += 1\n            return result\n\n        reader.readexactly = fake_readexactly\n        reader.read = AsyncMock(return_value=b'')\n\n        with pytest.raises(_HandshakeError) as exc_info:\n            await forwarder._remote_handshake(\n                reader, writer, bytes([0x01, 127, 0, 0, 1]), 80\n            )\n\n        assert exc_info.value.reply_code == REPLY_CONNECTION_REFUSED\n        assert 'rep=0x05' in str(exc_info.value)\n\n\n# ---------------------------------------------------------------------------\n# _handle_client — reply code routing\n# ---------------------------------------------------------------------------\n\n\nclass TestHandleClientReplyCodes:\n    @pytest.mark.asyncio\n    async def test_handshake_error_uses_exc_reply_code(self, forwarder):\n        \"\"\"_HandshakeError.reply_code flows to _send_reply.\"\"\"\n        client_reader = AsyncMock(spec=asyncio.StreamReader)\n        client_writer = MagicMock(spec=asyncio.StreamWriter)\n        client_writer.write = MagicMock()\n        client_writer.drain = AsyncMock()\n        client_writer.close = MagicMock()\n        client_writer.wait_closed = AsyncMock()\n\n        with patch.object(\n            forwarder,\n            '_accept_local_handshake',\n            side_effect=_HandshakeError('bad cmd', reply_code=REPLY_COMMAND_NOT_SUPPORTED),\n        ), patch.object(forwarder, '_send_reply', new_callable=AsyncMock) as mock_reply:\n            await forwarder._handle_client(client_reader, client_writer)\n            mock_reply.assert_awaited_once_with(client_writer, REPLY_COMMAND_NOT_SUPPORTED)\n\n    @pytest.mark.asyncio\n    async def test_connection_refused_uses_specific_code(self, forwarder):\n        \"\"\"ConnectionRefusedError -> REPLY_CONNECTION_REFUSED.\"\"\"\n        client_reader = AsyncMock(spec=asyncio.StreamReader)\n        client_writer = MagicMock(spec=asyncio.StreamWriter)\n        client_writer.write = MagicMock()\n        client_writer.drain = AsyncMock()\n        client_writer.close = MagicMock()\n        client_writer.wait_closed = AsyncMock()\n\n        with patch.object(\n            forwarder,\n            '_accept_local_handshake',\n            return_value=(b'\\x01\\x7f\\x00\\x00\\x01', 80),\n        ), patch(\n            'asyncio.open_connection',\n            side_effect=ConnectionRefusedError('refused'),\n        ), patch.object(forwarder, '_send_reply', new_callable=AsyncMock) as mock_reply:\n            await forwarder._handle_client(client_reader, client_writer)\n            mock_reply.assert_awaited_once_with(\n                client_writer, REPLY_CONNECTION_REFUSED\n            )\n\n    @pytest.mark.asyncio\n    async def test_generic_os_error_uses_general_failure(self, forwarder):\n        \"\"\"Non-ConnectionRefused OSError -> REPLY_GENERAL_FAILURE.\"\"\"\n        client_reader = AsyncMock(spec=asyncio.StreamReader)\n        client_writer = MagicMock(spec=asyncio.StreamWriter)\n        client_writer.write = MagicMock()\n        client_writer.drain = AsyncMock()\n        client_writer.close = MagicMock()\n        client_writer.wait_closed = AsyncMock()\n\n        with patch.object(\n            forwarder,\n            '_accept_local_handshake',\n            return_value=(b'\\x01\\x7f\\x00\\x00\\x01', 80),\n        ), patch(\n            'asyncio.open_connection',\n            side_effect=OSError('network unreachable'),\n        ), patch.object(forwarder, '_send_reply', new_callable=AsyncMock) as mock_reply:\n            await forwarder._handle_client(client_reader, client_writer)\n            mock_reply.assert_awaited_once_with(client_writer, REPLY_GENERAL_FAILURE)\n\n    @pytest.mark.asyncio\n    async def test_open_connection_timeout_sends_general_failure(self, forwarder):\n        \"\"\"asyncio.TimeoutError from open_connection -> REPLY_GENERAL_FAILURE.\"\"\"\n        client_reader = AsyncMock(spec=asyncio.StreamReader)\n        client_writer = MagicMock(spec=asyncio.StreamWriter)\n        client_writer.write = MagicMock()\n        client_writer.drain = AsyncMock()\n        client_writer.close = MagicMock()\n        client_writer.wait_closed = AsyncMock()\n\n        with patch.object(\n            forwarder,\n            '_accept_local_handshake',\n            return_value=(b'\\x01\\x7f\\x00\\x00\\x01', 80),\n        ), patch(\n            'asyncio.open_connection',\n            new_callable=AsyncMock,\n        ), patch(\n            'asyncio.wait_for',\n            side_effect=asyncio.TimeoutError(),\n        ), patch.object(forwarder, '_send_reply', new_callable=AsyncMock) as mock_reply:\n            await forwarder._handle_client(client_reader, client_writer)\n            mock_reply.assert_awaited_once_with(client_writer, REPLY_GENERAL_FAILURE)\n\n    @pytest.mark.asyncio\n    async def test_send_reply_false_skips_reply(self, forwarder):\n        \"\"\"_HandshakeError with send_reply=False should not call _send_reply.\"\"\"\n        client_reader = AsyncMock(spec=asyncio.StreamReader)\n        client_writer = MagicMock(spec=asyncio.StreamWriter)\n        client_writer.write = MagicMock()\n        client_writer.drain = AsyncMock()\n        client_writer.close = MagicMock()\n        client_writer.wait_closed = AsyncMock()\n\n        with patch.object(\n            forwarder,\n            '_accept_local_handshake',\n            side_effect=_HandshakeError('no auth', send_reply=False),\n        ), patch.object(forwarder, '_send_reply', new_callable=AsyncMock) as mock_reply:\n            await forwarder._handle_client(client_reader, client_writer)\n            mock_reply.assert_not_awaited()\n\n    @pytest.mark.asyncio\n    async def test_handle_client_closes_both_writers(self, forwarder):\n        \"\"\"Both client and remote writers are closed in the finally block.\"\"\"\n        client_reader = AsyncMock(spec=asyncio.StreamReader)\n        client_writer = MagicMock(spec=asyncio.StreamWriter)\n        client_writer.write = MagicMock()\n        client_writer.drain = AsyncMock()\n        client_writer.close = MagicMock()\n        client_writer.wait_closed = AsyncMock()\n\n        remote_writer = MagicMock(spec=asyncio.StreamWriter)\n        remote_writer.write = MagicMock()\n        remote_writer.drain = AsyncMock()\n        remote_writer.close = MagicMock()\n        remote_writer.wait_closed = AsyncMock()\n\n        remote_reader = AsyncMock(spec=asyncio.StreamReader)\n\n        with patch.object(\n            forwarder,\n            '_accept_local_handshake',\n            return_value=(b'\\x01\\x7f\\x00\\x00\\x01', 80),\n        ), patch(\n            'asyncio.open_connection',\n            return_value=(remote_reader, remote_writer),\n        ), patch.object(\n            forwarder,\n            '_remote_handshake',\n            side_effect=_HandshakeError('fail'),\n        ), patch.object(forwarder, '_send_reply', new_callable=AsyncMock):\n            await forwarder._handle_client(client_reader, client_writer)\n\n        client_writer.close.assert_called_once()\n        client_writer.wait_closed.assert_awaited_once()\n        remote_writer.close.assert_called_once()\n        remote_writer.wait_closed.assert_awaited_once()\n\n\n# ---------------------------------------------------------------------------\n# SOCKS5Forwarder._send_reply\n# ---------------------------------------------------------------------------\n\n\nclass TestSendReply:\n    @pytest.mark.asyncio\n    async def test_sends_correct_reply(self, mock_writer):\n        await SOCKS5Forwarder._send_reply(mock_writer, REPLY_SUCCESS)\n        written = mock_writer.write.call_args[0][0]\n        assert written[0] == 0x05  # SOCKS5\n        assert written[1] == REPLY_SUCCESS\n        mock_writer.drain.assert_awaited_once()\n\n    @pytest.mark.asyncio\n    async def test_sends_failure_reply(self, mock_writer):\n        await SOCKS5Forwarder._send_reply(mock_writer, REPLY_CONNECTION_REFUSED)\n        written = mock_writer.write.call_args[0][0]\n        assert written[1] == REPLY_CONNECTION_REFUSED\n\n\n# ---------------------------------------------------------------------------\n# _accept_local_handshake — pre-CONNECT send_reply=False and reply codes\n# ---------------------------------------------------------------------------\n\n\nclass TestAcceptLocalHandshake:\n    @pytest.mark.asyncio\n    async def test_greeting_eof_has_send_reply_false(self, forwarder):\n        \"\"\"If _read_exact fails during greeting, send_reply must be False.\"\"\"\n        reader = AsyncMock(spec=asyncio.StreamReader)\n        writer = MagicMock(spec=asyncio.StreamWriter)\n        writer.write = MagicMock()\n        writer.drain = AsyncMock()\n\n        reader.readexactly = AsyncMock(\n            side_effect=asyncio.IncompleteReadError(partial=b'', expected=2)\n        )\n\n        with pytest.raises(_HandshakeError) as exc_info:\n            await forwarder._accept_local_handshake(reader, writer)\n        assert exc_info.value.send_reply is False\n\n    @pytest.mark.asyncio\n    async def test_unsupported_version_has_send_reply_false(self, forwarder):\n        \"\"\"Bad SOCKS version in greeting → send_reply=False.\"\"\"\n        reader = AsyncMock(spec=asyncio.StreamReader)\n        writer = MagicMock(spec=asyncio.StreamWriter)\n        writer.write = MagicMock()\n        writer.drain = AsyncMock()\n\n        reader.readexactly = AsyncMock(return_value=bytes([0x04, 0x01]))\n\n        with pytest.raises(_HandshakeError, match='Unsupported SOCKS version') as exc_info:\n            await forwarder._accept_local_handshake(reader, writer)\n        assert exc_info.value.send_reply is False\n\n    @pytest.mark.asyncio\n    async def test_unsupported_command_uses_reply_code_0x07(self, forwarder):\n        \"\"\"CMD != CONNECT → REPLY_COMMAND_NOT_SUPPORTED.\"\"\"\n        reader = AsyncMock(spec=asyncio.StreamReader)\n        writer = MagicMock(spec=asyncio.StreamWriter)\n        writer.write = MagicMock()\n        writer.drain = AsyncMock()\n\n        call_count = 0\n        responses = [\n            bytes([0x05, 0x01]),  # VER, NMETHODS\n            bytes([0x00]),  # method: no-auth\n            bytes([0x05, 0x02, 0x00, 0x01]),  # VER, CMD=BIND(0x02), RSV, ATYP\n        ]\n\n        async def fake_readexactly(n):\n            nonlocal call_count\n            result = responses[call_count]\n            call_count += 1\n            return result\n\n        reader.readexactly = fake_readexactly\n\n        with pytest.raises(_HandshakeError) as exc_info:\n            await forwarder._accept_local_handshake(reader, writer)\n        assert exc_info.value.reply_code == REPLY_COMMAND_NOT_SUPPORTED\n\n    @pytest.mark.asyncio\n    async def test_unsupported_address_type_uses_reply_code_0x08(self, forwarder):\n        \"\"\"Unknown ATYP → REPLY_ADDRESS_TYPE_NOT_SUPPORTED.\"\"\"\n        reader = AsyncMock(spec=asyncio.StreamReader)\n        writer = MagicMock(spec=asyncio.StreamWriter)\n        writer.write = MagicMock()\n        writer.drain = AsyncMock()\n\n        call_count = 0\n        responses = [\n            bytes([0x05, 0x01]),  # VER, NMETHODS\n            bytes([0x00]),  # method: no-auth\n            bytes([0x05, 0x01, 0x00, 0xFF]),  # VER, CMD=CONNECT, RSV, ATYP=0xFF (unknown)\n        ]\n\n        async def fake_readexactly(n):\n            nonlocal call_count\n            result = responses[call_count]\n            call_count += 1\n            return result\n\n        reader.readexactly = fake_readexactly\n\n        with pytest.raises(_HandshakeError) as exc_info:\n            await forwarder._accept_local_handshake(reader, writer)\n        assert exc_info.value.reply_code == REPLY_ADDRESS_TYPE_NOT_SUPPORTED\n\n\n# ---------------------------------------------------------------------------\n# Constants\n# ---------------------------------------------------------------------------\n\n\nclass TestConstants:\n    def test_handshake_timeout_is_positive(self):\n        assert HANDSHAKE_TIMEOUT > 0\n\n    def test_reply_codes_are_distinct(self):\n        codes = {\n            REPLY_SUCCESS,\n            REPLY_GENERAL_FAILURE,\n            REPLY_CONNECTION_REFUSED,\n            REPLY_COMMAND_NOT_SUPPORTED,\n            REPLY_ADDRESS_TYPE_NOT_SUPPORTED,\n        }\n        assert len(codes) == 5\n"
  },
  {
    "path": "tests/test_user_agent_parser.py",
    "content": "\"\"\"\nTests for UserAgentParser class.\n\nVerifies that User-Agent strings are parsed into consistent metadata\nfor CDP Emulation.setUserAgentOverride and navigator JS overrides.\n\"\"\"\n\nimport pytest\n\nfrom pydoll.utils.user_agent_parser import UserAgentParser, ParsedUserAgent\n\n\n# --- Chrome on Windows ---\n\nCHROME_WINDOWS_UA = (\n    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '\n    'AppleWebKit/537.36 (KHTML, like Gecko) '\n    'Chrome/120.0.6099.109 Safari/537.36'\n)\n\n\nclass TestChromeWindows:\n    def test_platform(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert result.platform == 'Win32'\n\n    def test_vendor(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert result.vendor == 'Google Inc.'\n\n    def test_app_version(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert result.app_version.startswith('5.0 (Windows NT 10.0')\n\n    def test_metadata_platform(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert result.user_agent_metadata['platform'] == 'Windows'\n\n    def test_metadata_platform_version(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert result.user_agent_metadata['platformVersion'] == '15.0.0'\n\n    def test_metadata_architecture(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert result.user_agent_metadata['architecture'] == 'x86'\n\n    def test_metadata_mobile(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert result.user_agent_metadata['mobile'] is False\n\n    def test_metadata_model_empty(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert result.user_agent_metadata['model'] == ''\n\n    def test_metadata_bitness(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert result.user_agent_metadata['bitness'] == '64'\n\n    def test_metadata_wow64(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert result.user_agent_metadata['wow64'] is False\n\n    def test_brands_contains_chromium(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        brands = result.user_agent_metadata['brands']\n        brand_names = [b['brand'] for b in brands]\n        assert 'Chromium' in brand_names\n\n    def test_brands_contains_chrome(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        brands = result.user_agent_metadata['brands']\n        brand_names = [b['brand'] for b in brands]\n        assert 'Google Chrome' in brand_names\n\n    def test_brands_contains_grease(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        brands = result.user_agent_metadata['brands']\n        assert len(brands) == 3\n        # First brand is GREASE\n        assert brands[0]['brand'] not in {'Chromium', 'Google Chrome', 'Microsoft Edge'}\n\n    def test_brands_major_version(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        brands = result.user_agent_metadata['brands']\n        chromium_brand = next(b for b in brands if b['brand'] == 'Chromium')\n        assert chromium_brand['version'] == '120'\n\n    def test_full_version_list(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        fvl = result.user_agent_metadata['fullVersionList']\n        chromium_fv = next(b for b in fvl if b['brand'] == 'Chromium')\n        assert chromium_fv['version'] == '120.0.6099.109'\n\n    def test_navigator_js_contains_vendor(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert \"Navigator.prototype, 'vendor'\" in result.navigator_override_js\n\n    def test_navigator_js_contains_app_version(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert \"Navigator.prototype, 'appVersion'\" in result.navigator_override_js\n\n\n# --- Chrome on macOS ---\n\nCHROME_MACOS_UA = (\n    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '\n    'AppleWebKit/537.36 (KHTML, like Gecko) '\n    'Chrome/121.0.6167.85 Safari/537.36'\n)\n\n\nclass TestChromeMacOS:\n    def test_platform(self):\n        result = UserAgentParser.parse(CHROME_MACOS_UA)\n        assert result.platform == 'MacIntel'\n\n    def test_metadata_platform(self):\n        result = UserAgentParser.parse(CHROME_MACOS_UA)\n        assert result.user_agent_metadata['platform'] == 'macOS'\n\n    def test_metadata_platform_version(self):\n        result = UserAgentParser.parse(CHROME_MACOS_UA)\n        assert result.user_agent_metadata['platformVersion'] == '10.15.7'\n\n    def test_metadata_architecture(self):\n        result = UserAgentParser.parse(CHROME_MACOS_UA)\n        assert result.user_agent_metadata['architecture'] == 'arm'\n\n    def test_brands_version(self):\n        result = UserAgentParser.parse(CHROME_MACOS_UA)\n        brands = result.user_agent_metadata['brands']\n        chromium_brand = next(b for b in brands if b['brand'] == 'Chromium')\n        assert chromium_brand['version'] == '121'\n\n\n# --- Chrome on Linux ---\n\nCHROME_LINUX_UA = (\n    'Mozilla/5.0 (X11; Linux x86_64) '\n    'AppleWebKit/537.36 (KHTML, like Gecko) '\n    'Chrome/122.0.6261.94 Safari/537.36'\n)\n\n\nclass TestChromeLinux:\n    def test_platform(self):\n        result = UserAgentParser.parse(CHROME_LINUX_UA)\n        assert result.platform == 'Linux x86_64'\n\n    def test_metadata_platform(self):\n        result = UserAgentParser.parse(CHROME_LINUX_UA)\n        assert result.user_agent_metadata['platform'] == 'Linux'\n\n    def test_metadata_platform_version(self):\n        result = UserAgentParser.parse(CHROME_LINUX_UA)\n        assert result.user_agent_metadata['platformVersion'] == '6.1.0'\n\n    def test_metadata_architecture(self):\n        result = UserAgentParser.parse(CHROME_LINUX_UA)\n        assert result.user_agent_metadata['architecture'] == 'x86'\n\n\n# --- Edge on Windows ---\n\nEDGE_WINDOWS_UA = (\n    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '\n    'AppleWebKit/537.36 (KHTML, like Gecko) '\n    'Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.91'\n)\n\n\nclass TestEdgeWindows:\n    def test_platform(self):\n        result = UserAgentParser.parse(EDGE_WINDOWS_UA)\n        assert result.platform == 'Win32'\n\n    def test_brands_contains_edge(self):\n        result = UserAgentParser.parse(EDGE_WINDOWS_UA)\n        brands = result.user_agent_metadata['brands']\n        brand_names = [b['brand'] for b in brands]\n        assert 'Microsoft Edge' in brand_names\n\n    def test_brands_does_not_contain_chrome(self):\n        result = UserAgentParser.parse(EDGE_WINDOWS_UA)\n        brands = result.user_agent_metadata['brands']\n        brand_names = [b['brand'] for b in brands]\n        assert 'Google Chrome' not in brand_names\n\n    def test_brands_chromium_present(self):\n        result = UserAgentParser.parse(EDGE_WINDOWS_UA)\n        brands = result.user_agent_metadata['brands']\n        brand_names = [b['brand'] for b in brands]\n        assert 'Chromium' in brand_names\n\n    def test_full_version_list_edge_version(self):\n        result = UserAgentParser.parse(EDGE_WINDOWS_UA)\n        fvl = result.user_agent_metadata['fullVersionList']\n        edge_fv = next(b for b in fvl if b['brand'] == 'Microsoft Edge')\n        assert edge_fv['version'] == '120.0.2210.91'\n\n\n# --- Android Chrome ---\n\nCHROME_ANDROID_UA = (\n    'Mozilla/5.0 (Linux; Android 14; Pixel 7 Build/AP2A.240805.005) '\n    'AppleWebKit/537.36 (KHTML, like Gecko) '\n    'Chrome/120.0.6099.144 Mobile Safari/537.36'\n)\n\n\nclass TestChromeAndroid:\n    def test_platform(self):\n        result = UserAgentParser.parse(CHROME_ANDROID_UA)\n        assert result.platform == 'Linux armv81'\n\n    def test_metadata_platform(self):\n        result = UserAgentParser.parse(CHROME_ANDROID_UA)\n        assert result.user_agent_metadata['platform'] == 'Android'\n\n    def test_metadata_platform_version(self):\n        result = UserAgentParser.parse(CHROME_ANDROID_UA)\n        assert result.user_agent_metadata['platformVersion'] == '14'\n\n    def test_metadata_mobile(self):\n        result = UserAgentParser.parse(CHROME_ANDROID_UA)\n        assert result.user_agent_metadata['mobile'] is True\n\n    def test_metadata_model(self):\n        result = UserAgentParser.parse(CHROME_ANDROID_UA)\n        assert result.user_agent_metadata['model'] == 'Pixel 7'\n\n    def test_metadata_architecture(self):\n        result = UserAgentParser.parse(CHROME_ANDROID_UA)\n        assert result.user_agent_metadata['architecture'] == 'arm'\n\n\n# --- iPhone Safari-like UA ---\n\nIPHONE_UA = (\n    'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_2 like Mac OS X) '\n    'AppleWebKit/605.1.15 (KHTML, like Gecko) '\n    'CriOS/120.0.6099.119 Mobile/15E148 Safari/604.1'\n)\n\n\nclass TestIPhone:\n    def test_platform(self):\n        result = UserAgentParser.parse(IPHONE_UA)\n        assert result.platform == 'iPhone'\n\n    def test_metadata_platform(self):\n        result = UserAgentParser.parse(IPHONE_UA)\n        assert result.user_agent_metadata['platform'] == 'iOS'\n\n    def test_metadata_platform_version(self):\n        result = UserAgentParser.parse(IPHONE_UA)\n        assert result.user_agent_metadata['platformVersion'] == '17.1.2'\n\n    def test_metadata_mobile(self):\n        result = UserAgentParser.parse(IPHONE_UA)\n        assert result.user_agent_metadata['mobile'] is True\n\n    def test_metadata_architecture(self):\n        result = UserAgentParser.parse(IPHONE_UA)\n        assert result.user_agent_metadata['architecture'] == 'arm'\n\n\n# --- Old Windows versions ---\n\nclass TestWindowsVersionMapping:\n    def test_windows_7(self):\n        ua = (\n            'Mozilla/5.0 (Windows NT 6.1; Win64; x64) '\n            'AppleWebKit/537.36 Chrome/120.0.6099.109 Safari/537.36'\n        )\n        result = UserAgentParser.parse(ua)\n        assert result.user_agent_metadata['platformVersion'] == '0.1.0'\n\n    def test_windows_8(self):\n        ua = (\n            'Mozilla/5.0 (Windows NT 6.2; Win64; x64) '\n            'AppleWebKit/537.36 Chrome/120.0.6099.109 Safari/537.36'\n        )\n        result = UserAgentParser.parse(ua)\n        assert result.user_agent_metadata['platformVersion'] == '0.2.0'\n\n    def test_windows_8_1(self):\n        ua = (\n            'Mozilla/5.0 (Windows NT 6.3; Win64; x64) '\n            'AppleWebKit/537.36 Chrome/120.0.6099.109 Safari/537.36'\n        )\n        result = UserAgentParser.parse(ua)\n        assert result.user_agent_metadata['platformVersion'] == '0.3.0'\n\n\n# --- GREASE brands ---\n\nclass TestGreaseBrands:\n    def test_grease_brand_is_first(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        brands = result.user_agent_metadata['brands']\n        grease_brand = brands[0]['brand']\n        assert grease_brand not in {'Chromium', 'Google Chrome', 'Microsoft Edge'}\n\n    def test_full_version_list_grease_is_first(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        fvl = result.user_agent_metadata['fullVersionList']\n        grease_brand = fvl[0]['brand']\n        assert grease_brand not in {'Chromium', 'Google Chrome', 'Microsoft Edge'}\n\n    def test_grease_version_format_for_brands(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        brands = result.user_agent_metadata['brands']\n        grease_version = brands[0]['version']\n        assert grease_version.isdigit()\n\n    def test_grease_version_format_for_full_version_list(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        fvl = result.user_agent_metadata['fullVersionList']\n        grease_version = fvl[0]['version']\n        assert '.' in grease_version\n\n\n# --- Edge cases ---\n\nclass TestEdgeCases:\n    def test_unknown_browser_defaults_to_chrome(self):\n        ua = 'Some random string without browser info'\n        result = UserAgentParser.parse(ua)\n        brands = result.user_agent_metadata['brands']\n        brand_names = [b['brand'] for b in brands]\n        assert 'Google Chrome' in brand_names\n\n    def test_unknown_os_defaults_to_windows(self):\n        ua = (\n            'Mozilla/5.0 AppleWebKit/537.36 '\n            'Chrome/120.0.6099.109 Safari/537.36'\n        )\n        result = UserAgentParser.parse(ua)\n        assert result.platform == 'Win32'\n        assert result.user_agent_metadata['platform'] == 'Windows'\n\n    def test_returns_parsed_user_agent_type(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert isinstance(result, ParsedUserAgent)\n\n    def test_navigator_js_escapes_single_quotes(self):\n        ua = (\n            \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \"\n            \"AppleWebKit/537.36 Chrome/120.0.6099.109 Safari/537.36\"\n        )\n        result = UserAgentParser.parse(ua)\n        assert \"\\\\'\" not in result.navigator_override_js or \"'\" in result.vendor\n\n    def test_app_version_strips_mozilla_prefix(self):\n        result = UserAgentParser.parse(CHROME_WINDOWS_UA)\n        assert not result.app_version.startswith('Mozilla/')\n        assert result.app_version.startswith('5.0')\n\n    def test_non_mozilla_ua_keeps_full_string(self):\n        ua = 'CustomBot/1.0 Chrome/120.0.6099.109'\n        result = UserAgentParser.parse(ua)\n        assert result.app_version == ua\n\n\n# --- ChromeOS ---\n\nCHROMEOS_UA = (\n    'Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) '\n    'AppleWebKit/537.36 (KHTML, like Gecko) '\n    'Chrome/120.0.6099.109 Safari/537.36'\n)\n\n\nclass TestChromeOS:\n    def test_platform(self):\n        result = UserAgentParser.parse(CHROMEOS_UA)\n        assert result.platform == 'Linux x86_64'\n\n    def test_metadata_platform(self):\n        result = UserAgentParser.parse(CHROMEOS_UA)\n        assert result.user_agent_metadata['platform'] == 'Chrome OS'\n\n    def test_metadata_platform_version(self):\n        result = UserAgentParser.parse(CHROMEOS_UA)\n        assert result.user_agent_metadata['platformVersion'] == '14541.0.0'\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "import aiohttp\nimport pytest\nfrom aioresponses import aioresponses\nimport tempfile\nimport os\nimport sys\nfrom unittest.mock import patch\n\nfrom pydoll import exceptions\nfrom pydoll.utils import (\n    clean_script_for_analysis,\n    decode_base64_to_bytes,\n    get_browser_ws_address,\n    has_return_outside_function,\n    is_script_already_function,\n    validate_browser_paths,\n    extract_text_from_html,\n)\n\n\nclass TestUtils:\n    \"\"\"\n    Test class for utility functions in the pydoll.utils module.\n    Groups tests related to image decoding, browser communication, and path validation.\n    \"\"\"\n\n    def test_decode_image_to_bytes(self):\n        \"\"\"\n        Test the decode_base64_to_bytes function.\n        Verifies that the function correctly decodes a base64 string\n        to its original bytes.\n        \"\"\"\n        base64code = 'aGVsbG8gd29ybGQ='  # 'hello world' in base64\n        assert decode_base64_to_bytes(base64code) == b'hello world'\n\n    def test_decode_image_to_bytes_empty_string(self):\n        \"\"\"\n        Test decode_base64_to_bytes with empty string.\n        Verifies that the function handles empty input correctly.\n        \"\"\"\n        assert decode_base64_to_bytes('') == b''\n\n    def test_decode_image_to_bytes_complex_data(self):\n        \"\"\"\n        Test decode_base64_to_bytes with more complex base64 data.\n        Verifies that the function can handle longer, more complex encoded data.\n        \"\"\"\n        # Base64 for \"The quick brown fox jumps over the lazy dog\"\n        base64code = 'VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw=='\n        expected = b'The quick brown fox jumps over the lazy dog'\n        assert decode_base64_to_bytes(base64code) == expected\n\n    @pytest.mark.asyncio\n    async def test_successful_response(self):\n        \"\"\"\n        Test successful scenario when getting browser WebSocket address.\n        Verifies that the function correctly returns the WebSocket URL when\n        the API response contains the expected field.\n        \"\"\"\n        port = 9222\n        expected_url = 'ws://localhost:9222/devtools/browser/abc123'\n\n        with aioresponses() as mocked:\n            mocked.get(\n                f'http://localhost:{port}/json/version',\n                payload={'webSocketDebuggerUrl': expected_url},\n            )\n            result = await get_browser_ws_address(port)\n            assert result == expected_url\n\n    @pytest.mark.asyncio\n    async def test_network_error(self):\n        \"\"\"\n        Test behavior when a network error occurs.\n        Verifies that the function raises the appropriate NetworkError exception\n        when there's a communication failure with the browser.\n        \"\"\"\n        port = 9222\n\n        with pytest.raises(exceptions.NetworkError):\n            with aioresponses() as mocked:\n                mocked.get(\n                    f'http://localhost:{port}/json/version',\n                    exception=aiohttp.ClientError,\n                )\n                await get_browser_ws_address(port)\n\n    @pytest.mark.asyncio\n    async def test_missing_websocket_url(self):\n        \"\"\"\n        Test behavior when API response doesn't contain WebSocket URL.\n        Verifies that the function raises InvalidResponse exception when the\n        'webSocketDebuggerUrl' field is missing from the response.\n        \"\"\"\n        port = 9222\n\n        with aioresponses() as mocked:\n            mocked.get(\n                f'http://localhost:{port}/json/version',\n                payload={'someOtherKey': 'value'},\n            )\n            with pytest.raises(exceptions.InvalidResponse):\n                await get_browser_ws_address(port)\n\n    @pytest.mark.asyncio\n    async def test_http_error_status(self):\n        \"\"\"\n        Test behavior when HTTP request returns an error status.\n        Verifies that the function raises NetworkError when the server\n        returns an HTTP error status code.\n        \"\"\"\n        port = 9222\n\n        with pytest.raises(exceptions.NetworkError):\n            with aioresponses() as mocked:\n                mocked.get(\n                    f'http://localhost:{port}/json/version',\n                    status=404\n                )\n                await get_browser_ws_address(port)\n\n    @pytest.mark.asyncio\n    async def test_custom_port(self):\n        \"\"\"\n        Test get_browser_ws_address with a custom port.\n        Verifies that the function works correctly with non-default ports.\n        \"\"\"\n        port = 9333\n        expected_url = 'ws://localhost:9333/devtools/browser/xyz789'\n\n        with aioresponses() as mocked:\n            mocked.get(\n                f'http://localhost:{port}/json/version',\n                payload={'webSocketDebuggerUrl': expected_url},\n            )\n            result = await get_browser_ws_address(port)\n            assert result == expected_url\n\n    def test_validate_browser_paths_success(self):\n        \"\"\"\n        Test validate_browser_paths with valid executable path.\n        Verifies that the function returns the first valid path found.\n        \"\"\"\n        with tempfile.TemporaryDirectory() as temp_dir:\n            # Create a temporary executable file\n            valid_path = os.path.join(temp_dir, 'browser')\n            with open(valid_path, 'w') as f:\n                f.write('#!/bin/bash\\necho \"browser\"')\n            os.chmod(valid_path, 0o755)  # Make it executable\n            \n            invalid_path = '/nonexistent/browser'\n            paths = [invalid_path, valid_path]\n            \n            result = validate_browser_paths(paths)\n            assert result == valid_path\n\n    def test_validate_browser_paths_first_valid_wins(self):\n        \"\"\"\n        Test that validate_browser_paths returns the first valid path.\n        Verifies that when multiple valid paths exist, the first one is returned.\n        \"\"\"\n        with tempfile.TemporaryDirectory() as temp_dir:\n            # Create two valid executable files\n            first_valid = os.path.join(temp_dir, 'browser1')\n            second_valid = os.path.join(temp_dir, 'browser2')\n            \n            for path in [first_valid, second_valid]:\n                with open(path, 'w') as f:\n                    f.write('#!/bin/bash\\necho \"browser\"')\n                os.chmod(path, 0o755)\n            \n            paths = [first_valid, second_valid]\n            result = validate_browser_paths(paths)\n            assert result == first_valid\n\n    def test_validate_browser_paths_no_valid_paths(self):\n        \"\"\"\n        Test validate_browser_paths when no valid paths exist.\n        Verifies that InvalidBrowserPath exception is raised when no\n        executable browser is found in the provided paths.\n        \"\"\"\n        invalid_paths = [\n            '/nonexistent/browser1',\n            '/nonexistent/browser2',\n            '/nonexistent/browser3'\n        ]\n        \n        with pytest.raises(exceptions.InvalidBrowserPath) as exc_info:\n            validate_browser_paths(invalid_paths)\n        \n        assert 'No valid browser path found in:' in str(exc_info.value)\n\n    @pytest.mark.skipif(sys.platform.startswith('win'), reason='No executable bit on NTFS on Windows')\n    def test_validate_browser_paths_file_exists_but_not_executable(self):\n        \"\"\"\n        Test validate_browser_paths with non-executable file.\n        Verifies that a file that exists but is not executable is not considered valid.\n        \"\"\"\n        with tempfile.TemporaryDirectory() as temp_dir:\n            # Create a file that exists but is not executable\n            non_executable = os.path.join(temp_dir, 'browser')\n            with open(non_executable, 'w') as f:\n                f.write('not executable')\n            # Don't set executable permissions\n\n            with pytest.raises(exceptions.InvalidBrowserPath):\n                validate_browser_paths([non_executable])\n\n    @pytest.mark.skipif(sys.platform.startswith('win'), reason='No executable bit on NTFS on Windows')\n    def test_validate_browser_paths_directory_instead_of_file(self):\n        \"\"\"\n        Test validate_browser_paths with a directory path.\n        Verifies that directories are not treated as valid executables even if they have execute permission.\n        \"\"\"\n        with tempfile.TemporaryDirectory() as temp_dir:\n            os.chmod(temp_dir, 0o755)\n            with pytest.raises(exceptions.InvalidBrowserPath):\n                validate_browser_paths([temp_dir])\n\n    def test_validate_browser_paths_empty_list(self):\n        \"\"\"\n        Test validate_browser_paths with empty path list.\n        Verifies that InvalidBrowserPath exception is raised when\n        an empty list of paths is provided.\n        \"\"\"\n        with pytest.raises(exceptions.InvalidBrowserPath):\n            validate_browser_paths([])\n\n    def test_validate_browser_paths_mixed_valid_invalid(self):\n        \"\"\"\n        Test validate_browser_paths with mix of valid and invalid paths.\n        Verifies that the function skips invalid paths and returns the first valid one.\n        \"\"\"\n        with tempfile.TemporaryDirectory() as temp_dir:\n            # Create one valid executable\n            valid_path = os.path.join(temp_dir, 'browser')\n            with open(valid_path, 'w') as f:\n                f.write('#!/bin/bash\\necho \"browser\"')\n            os.chmod(valid_path, 0o755)\n            \n            # Mix valid and invalid paths\n            paths = [\n                '/nonexistent/browser1',\n                '/nonexistent/browser2',\n                valid_path,\n                '/nonexistent/browser3'\n            ]\n            \n            result = validate_browser_paths(paths)\n            assert result == valid_path\n\n\nclass TestDecodeBase64ToBytes:\n    \"\"\"Test decode_base64_to_bytes function.\"\"\"\n\n    def test_decode_base64_to_bytes_valid_input(self):\n        \"\"\"Test decoding valid base64 string.\"\"\"\n        base64_string = 'SGVsbG8gV29ybGQ='  # \"Hello World\" in base64\n        result = decode_base64_to_bytes(base64_string)\n        assert result == b'Hello World'\n\n    def test_decode_base64_to_bytes_empty_string(self):\n        \"\"\"Test decoding empty base64 string.\"\"\"\n        result = decode_base64_to_bytes('')\n        assert result == b''\n\n\nclass TestValidateBrowserPaths:\n    \"\"\"Test validate_browser_paths function.\"\"\"\n\n    def test_validate_browser_paths_valid_path(self, tmp_path):\n        \"\"\"Test with valid executable path.\"\"\"\n        # Create a temporary executable file\n        executable = tmp_path / \"browser\"\n        executable.write_text(\"#!/bin/bash\\necho 'browser'\")\n        executable.chmod(0o755)\n        \n        result = validate_browser_paths([str(executable)])\n        assert result == str(executable)\n\n    def test_validate_browser_paths_invalid_paths(self):\n        \"\"\"Test with invalid paths.\"\"\"\n        from pydoll.exceptions import InvalidBrowserPath\n        \n        with pytest.raises(InvalidBrowserPath):\n            validate_browser_paths(['/nonexistent/path', '/another/invalid/path'])\n\n\nclass TestScriptAnalysisFunctions:\n    \"\"\"Test JavaScript script analysis functions.\"\"\"\n\n    def test_clean_script_for_analysis_removes_comments(self):\n        \"\"\"Test that comments are removed from script.\"\"\"\n        script = '''\n        // This is a line comment\n        var x = 5;\n        /* This is a block comment */\n        return x;\n        '''\n        \n        result = clean_script_for_analysis(script)\n        \n        assert '// This is a line comment' not in result\n        assert '/* This is a block comment */' not in result\n        assert 'var x = 5;' in result\n        assert 'return x;' in result\n\n    def test_clean_script_for_analysis_removes_strings(self):\n        \"\"\"Test that string literals are removed from script.\"\"\"\n        script = '''\n        var message = \"This string contains return statement\";\n        var another = 'Another string with return';\n        var template = `Template literal with return`;\n        return \"actual return\";\n        '''\n        \n        result = clean_script_for_analysis(script)\n        \n        assert 'This string contains return statement' not in result\n        assert 'Another string with return' not in result\n        assert 'Template literal with return' not in result\n        assert 'return \"\"' in result  # String replaced with empty quotes\n\n    def test_is_script_already_function_regular_function(self):\n        \"\"\"Test detection of regular function declaration.\"\"\"\n        script = 'function() { console.log(\"test\"); }'\n        assert is_script_already_function(script) is True\n\n    def test_is_script_already_function_arrow_function(self):\n        \"\"\"Test detection of arrow function.\"\"\"\n        script = '() => { console.log(\"test\"); }'\n        assert is_script_already_function(script) is True\n\n    def test_is_script_already_function_with_parameters(self):\n        \"\"\"Test detection of function with parameters.\"\"\"\n        script = 'function(a, b) { return a + b; }'\n        assert is_script_already_function(script) is True\n\n    def test_is_script_already_function_not_function(self):\n        \"\"\"Test detection when script is not a function.\"\"\"\n        script = 'console.log(\"test\"); return \"value\";'\n        assert is_script_already_function(script) is False\n\n    def test_is_script_already_function_with_whitespace(self):\n        \"\"\"Test detection with leading/trailing whitespace.\"\"\"\n        script = '   function() { test(); }   '\n        assert is_script_already_function(script) is True\n\n    def test_has_return_outside_function_simple_return(self):\n        \"\"\"Test detection of simple return statement.\"\"\"\n        script = 'return document.title;'\n        assert has_return_outside_function(script) is True\n\n    def test_has_return_outside_function_no_return(self):\n        \"\"\"Test when script has no return statement.\"\"\"\n        script = 'console.log(\"test\"); var x = 5;'\n        assert has_return_outside_function(script) is False\n\n    def test_has_return_outside_function_return_inside_function(self):\n        \"\"\"Test when return is inside a function.\"\"\"\n        script = '''\n        function getTitle() {\n            return document.title;\n        }\n        getTitle();\n        '''\n        assert has_return_outside_function(script) is False\n\n    def test_has_return_outside_function_mixed_returns(self):\n        \"\"\"Test with both inside and outside returns.\"\"\"\n        script = '''\n        function inner() {\n            return \"inner\";\n        }\n        return \"outer\";\n        '''\n        assert has_return_outside_function(script) is True\n\n    def test_has_return_outside_function_already_function(self):\n        \"\"\"Test when script is already a function.\"\"\"\n        script = 'function() { return \"test\"; }'\n        assert has_return_outside_function(script) is False\n\n    def test_has_return_outside_function_with_comments(self):\n        \"\"\"Test with comments containing 'return'.\"\"\"\n        script = '''\n        // This comment has return in it\n        var message = \"This string has return in it\";\n        /* This block comment also has return */\n        return \"actual return\";\n        '''\n        assert has_return_outside_function(script) is True\n\n    def test_has_return_outside_function_nested_braces(self):\n        \"\"\"Test with nested braces and complex structure.\"\"\"\n        script = '''\n        if (true) {\n            var obj = {\n                method: function() {\n                    return \"nested\";\n                }\n            };\n        }\n        return \"outside\";\n        '''\n        assert has_return_outside_function(script) is True\n\n    def test_has_return_outside_function_arrow_function(self):\n        \"\"\"Test with arrow function containing return.\"\"\"\n        script = '''\n        var func = () => {\n            return \"arrow\";\n        };\n        func();\n        '''\n        assert has_return_outside_function(script) is False\n\n    def test_extract_text_without_strip_without_separator(self):\n        html = ('<div>Hello <span> world </span><script>alert(1)</script><style>body { color: red; }</style>'\n                '<template>hidden</template></div>')\n        result = extract_text_from_html(html)\n        assert result == 'Hello  world '\n\n    def test_extract_text_with_strip_without_separator(self):\n        html = ('<div>Hello <span> world </span><script>alert(1)</script><style>body { color: red; }</style>'\n                '<template>hidden</template></div>')\n        result = extract_text_from_html(html, strip=True)\n        assert result == 'Helloworld'\n\n    def test_extract_text_without_strip_with_separator(self):\n        html = ('<div>Hello <span> world </span><script>alert(1)</script><style>body { color: red; }</style>'\n                '<template>hidden</template></div>')\n        result = extract_text_from_html(html, separator=\"/\")\n        assert result == 'Hello / world '\n\n    def test_extract_text_with_strip_with_separator(self):\n        html = ('<div>Hello <span> world </span><script>alert(1)</script><style>body { color: red; }</style>'\n                '<template>hidden</template></div>')\n        result = extract_text_from_html(html, strip=True, separator=\"/\")\n        assert result == 'Hello/world'\n"
  },
  {
    "path": "tests/test_web_element.py",
    "content": "import asyncio\nimport json\nfrom pathlib import Path\nfrom unittest.mock import AsyncMock, patch\n\nimport pytest\nimport pytest_asyncio\n\nfrom pydoll.browser.options import ChromiumOptions as Options\nfrom pydoll.browser.chromium.chrome import Chrome\nfrom pydoll.commands import DomCommands, RuntimeCommands\nfrom pydoll.constants import Key\nfrom pydoll.elements.web_element import WebElement\nfrom pydoll.exceptions import (\n    ElementNotAFileInput,\n    ElementNotFound,\n    ElementNotInteractable,\n    ElementNotVisible,\n    WaitElementTimeout,\n)\nfrom pydoll.protocol.input.types import KeyModifier\nfrom pydoll.protocol.runtime.types import CallArgument\n\n\n@pytest_asyncio.fixture\nasync def mock_connection_handler():\n    \"\"\"Mock connection handler for WebElement tests.\"\"\"\n    with patch('pydoll.connection.ConnectionHandler', autospec=True) as mock:\n        handler = mock.return_value\n        handler.execute_command = AsyncMock()\n        yield handler\n\n\n@pytest.fixture\ndef web_element(mock_connection_handler):\n    \"\"\"Basic WebElement fixture with common attributes.\"\"\"\n    attributes_list = [\n        'id',\n        'test-id',\n        'class',\n        'test-class',\n        'value',\n        'test-value',\n        'tag_name',\n        'div',\n        'type',\n        'text',\n    ]\n    return WebElement(\n        object_id='test-object-id',\n        connection_handler=mock_connection_handler,\n        method='css',\n        selector='#test',\n        attributes_list=attributes_list,\n    )\n\n\n@pytest.fixture\ndef input_element(mock_connection_handler):\n    \"\"\"Input element fixture for form-related tests.\"\"\"\n    attributes_list = [\n        'id',\n        'input-id',\n        'tag_name',\n        'input',\n        'type',\n        'text',\n        'value',\n        'initial-value',\n    ]\n    return WebElement(\n        object_id='input-object-id',\n        connection_handler=mock_connection_handler,\n        method='css',\n        selector='input[type=\"text\"]',\n        attributes_list=attributes_list,\n    )\n\n\n@pytest.fixture\ndef file_input_element(mock_connection_handler):\n    \"\"\"File input element fixture for file upload tests.\"\"\"\n    attributes_list = ['id', 'file-input-id', 'tag_name', 'input', 'type', 'file']\n    return WebElement(\n        object_id='file-input-object-id',\n        connection_handler=mock_connection_handler,\n        method='css',\n        selector='input[type=\"file\"]',\n        attributes_list=attributes_list,\n    )\n\n\n@pytest.fixture\ndef option_element(mock_connection_handler):\n    \"\"\"Option element fixture for dropdown tests.\"\"\"\n    attributes_list = ['tag_name', 'option', 'value', 'option-value', 'id', 'option-id']\n    return WebElement(\n        object_id='option-object-id',\n        connection_handler=mock_connection_handler,\n        method='css',\n        selector='option[value=\"option-value\"]',\n        attributes_list=attributes_list,\n    )\n\n\n@pytest.fixture\ndef disabled_element(mock_connection_handler):\n    \"\"\"Disabled element fixture for testing enabled/disabled state.\"\"\"\n    attributes_list = ['id', 'disabled-id', 'tag_name', 'button', 'disabled', 'true']\n    return WebElement(\n        object_id='disabled-object-id',\n        connection_handler=mock_connection_handler,\n        method='css',\n        selector='button:disabled',\n        attributes_list=attributes_list,\n    )\n\n\n@pytest.fixture\ndef iframe_element(mock_connection_handler):\n    \"\"\"Iframe element fixture for iframe-related tests.\"\"\"\n    attributes_list = ['id', 'iframe-id', 'tag_name', 'iframe']\n    return WebElement(\n        object_id='iframe-object-id',\n        connection_handler=mock_connection_handler,\n        method='css',\n        selector='iframe#iframe-id',\n        attributes_list=attributes_list,\n    )\n\n\n\n\nclass TestWebElementInitialization:\n    \"\"\"Test WebElement initialization and basic properties.\"\"\"\n\n    def test_web_element_initialization(self, web_element):\n        \"\"\"Test basic WebElement initialization.\"\"\"\n        assert web_element._object_id == 'test-object-id'\n        assert web_element._search_method == 'css'\n        assert web_element._selector == '#test'\n        assert web_element._attributes == {\n            'id': 'test-id',\n            'class_name': 'test-class',\n            'value': 'test-value',\n            'tag_name': 'div',\n            'type': 'text',\n        }\n\n    def test_web_element_initialization_empty_attributes(self, mock_connection_handler):\n        \"\"\"Test WebElement initialization with empty attributes list.\"\"\"\n        element = WebElement(\n            object_id='empty-id', connection_handler=mock_connection_handler, attributes_list=[]\n        )\n        assert element._attributes == {}\n        assert element._search_method is None\n        assert element._selector is None\n\n    def test_web_element_initialization_odd_attributes(self, mock_connection_handler):\n        \"\"\"Test WebElement initialization with odd number of attributes (causes IndexError).\"\"\"\n        attributes_list = ['id', 'test-id', 'class']  # Missing value for 'class'\n\n        # This should raise IndexError because _def_attributes doesn't handle odd lists\n        with pytest.raises(IndexError):\n            WebElement(\n                object_id='odd-id',\n                connection_handler=mock_connection_handler,\n                attributes_list=attributes_list,\n            )\n\n    def test_class_attribute_renamed_to_class_name(self, mock_connection_handler):\n        \"\"\"Test that 'class' attribute is renamed to 'class_name'.\"\"\"\n        attributes_list = ['class', 'my-class', 'id', 'my-id']\n        element = WebElement(\n            object_id='class-test',\n            connection_handler=mock_connection_handler,\n            attributes_list=attributes_list,\n        )\n        assert 'class' not in element._attributes\n        assert element._attributes['class_name'] == 'my-class'\n        assert element._attributes['id'] == 'my-id'\n\n\nclass TestWebElementProperties:\n    \"\"\"Test WebElement properties and getters.\"\"\"\n\n    def test_basic_properties(self, web_element):\n        \"\"\"Test basic property accessors.\"\"\"\n        assert web_element.value == 'test-value'\n        assert web_element.class_name == 'test-class'\n        assert web_element.id == 'test-id'\n        assert web_element.tag_name == 'div'\n\n    def test_is_iframe_property_with_iframe_tag(self, iframe_element):\n        \"\"\"Test is_iframe returns True for iframe tag.\"\"\"\n        assert iframe_element.is_iframe is True\n\n    def test_is_iframe_property_with_frame_tag(self, mock_connection_handler):\n        \"\"\"Test is_iframe returns True for frame tag (frameset frames).\"\"\"\n        element = WebElement(\n            object_id='frame-object-id',\n            connection_handler=mock_connection_handler,\n            attributes_list=['tag_name', 'frame', 'id', 'my-frame'],\n        )\n        assert element.is_iframe is True\n\n    def test_is_iframe_property_with_regular_tag(self, web_element):\n        \"\"\"Test is_iframe returns False for non-frame tags.\"\"\"\n        assert web_element.is_iframe is False\n\n    def test_is_iframe_property_with_no_tag(self, mock_connection_handler):\n        \"\"\"Test is_iframe returns False when tag_name is None.\"\"\"\n        element = WebElement(\n            object_id='no-tag',\n            connection_handler=mock_connection_handler,\n            attributes_list=[],\n        )\n        assert element.is_iframe is False\n\n    def test_is_enabled_property(self, web_element, disabled_element):\n        \"\"\"Test is_enabled property for enabled and disabled elements.\"\"\"\n        assert web_element.is_enabled is True\n        assert disabled_element.is_enabled is False\n\n    def test_properties_with_none_values(self, mock_connection_handler):\n        \"\"\"Test properties when attributes are not present.\"\"\"\n        element = WebElement(\n            object_id='empty-element',\n            connection_handler=mock_connection_handler,\n            attributes_list=[],\n        )\n        assert element.value is None\n        assert element.class_name is None\n        assert element.id is None\n        assert element.tag_name is None\n        assert element.is_enabled is True  # No 'disabled' attribute means enabled\n\n    @pytest.mark.asyncio\n    async def test_text_property(self, web_element):\n        \"\"\"Test text property extraction from HTML.\"\"\"\n        test_html = '<div>Hello <span>World</span></div>'\n        web_element._connection_handler.execute_command.return_value = {\n            'result': {'outerHTML': test_html}\n        }\n\n        text = await web_element.text\n        assert text == 'HelloWorld'  # BeautifulSoup strips spaces between elements\n\n    @pytest.mark.asyncio\n    async def test_text_property_with_nested_elements(self, web_element):\n        \"\"\"Test text property with complex nested HTML.\"\"\"\n        test_html = '<div>Text <b>Bold</b> <i>Italic</i> More text</div>'\n        web_element._connection_handler.execute_command.return_value = {\n            'result': {'outerHTML': test_html}\n        }\n\n        text = await web_element.text\n        assert text == 'TextBoldItalicMore text'  # BeautifulSoup strips spaces between elements\n\n    @pytest.mark.asyncio\n    async def test_bounds_property(self, web_element):\n        \"\"\"Test bounds property returns correct coordinates.\"\"\"\n        expected_bounds = [0, 0, 100, 100, 100, 100, 0, 100]\n        web_element._connection_handler.execute_command.return_value = {\n            'result': {'model': {'content': expected_bounds}}\n        }\n\n        bounds = await web_element.bounds\n        assert bounds == expected_bounds\n\n    @pytest.mark.asyncio\n    async def test_inner_html_property(self, web_element):\n        \"\"\"Test inner_html property returns outer HTML.\"\"\"\n        expected_html = '<div class=\"test\">Content</div>'\n        web_element._connection_handler.execute_command.return_value = {\n            'result': {'outerHTML': expected_html}\n        }\n\n        html = await web_element.inner_html\n        assert html == expected_html\n\n    @pytest.mark.asyncio\n    async def test_iframe_context_non_iframe_returns_none(self, web_element):\n        \"\"\"Non-iframe elements should not produce iframe context.\"\"\"\n        result = await web_element.iframe_context\n        assert result is None\n        web_element._connection_handler.execute_command.assert_not_awaited()\n\n\nclass TestWebElementMethods:\n    \"\"\"Test WebElement methods and interactions.\"\"\"\n\n    def test_get_attribute(self, web_element):\n        \"\"\"Test get_attribute method.\"\"\"\n        assert web_element.get_attribute('id') == 'test-id'\n        assert web_element.get_attribute('class_name') == 'test-class'\n        assert web_element.get_attribute('nonexistent') is None\n\n    @pytest.mark.asyncio\n    async def test_get_bounds_using_js(self, web_element):\n        \"\"\"Test JavaScript-based bounds calculation.\"\"\"\n        expected_bounds = {'x': 10, 'y': 20, 'width': 100, 'height': 50}\n        web_element._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': json.dumps(expected_bounds)}}\n        }\n\n        bounds = await web_element.get_bounds_using_js()\n        assert bounds == expected_bounds\n\n    @pytest.mark.asyncio\n    async def test_scroll_into_view(self, web_element):\n        \"\"\"Test scroll_into_view method.\"\"\"\n        await web_element.scroll_into_view()\n        web_element._connection_handler.execute_command.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_insert_text(self, input_element):\n        \"\"\"Test insert_text method.\"\"\"\n        test_text = 'Hello World'\n        await input_element.insert_text(test_text)\n\n        input_element._connection_handler.execute_command.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_type_text(self, input_element):\n        \"\"\"Test type_text method with character-by-character typing.\"\"\"\n        test_text = 'Hi'\n        input_element.click = AsyncMock()\n        with patch('asyncio.sleep') as mock_sleep:\n            await input_element.type_text(test_text, humanize=False, interval=0.05)\n\n        # Should call execute_command for each character (focus + KEY_DOWN + KEY_UP)\n        assert input_element._connection_handler.execute_command.call_count == len(test_text) * 3\n        assert input_element.click.call_count == 1\n\n        # Verify sleep was called between characters\n        assert mock_sleep.call_count == len(test_text)\n        mock_sleep.assert_called_with(0.05)\n\n    @pytest.mark.asyncio\n    async def test_type_text_default_interval(self, input_element):\n        \"\"\"Test type_text with default interval.\"\"\"\n        test_text = 'A'\n        input_element.click = AsyncMock()\n        with patch('asyncio.sleep') as mock_sleep:\n            await input_element.type_text(test_text, humanize=False)\n\n        mock_sleep.assert_called_with(0.05)  # Default interval\n        assert input_element.click.call_count == 1\n\n    @pytest.mark.asyncio\n    async def test_clear(self, input_element):\n        \"\"\"Test clear method resets element value.\"\"\"\n        input_element._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': True}}\n        }\n\n        await input_element.clear()\n\n        input_element._connection_handler.execute_command.assert_called_once()\n        assert input_element._attributes['value'] == ''\n\n    @pytest.mark.asyncio\n    async def test_clear_not_interactable(self, input_element):\n        \"\"\"Test clear raises ElementNotInteractable for non-editable elements.\"\"\"\n        input_element._connection_handler.execute_command.return_value = {\n            'result': {'result': {'value': False}}\n        }\n\n        with pytest.raises(ElementNotInteractable):\n            await input_element.clear()\n\n\nclass TestWebElementIFrame:\n    \"\"\"Tests for iframe-specific WebElement behaviour.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_iframe_context_initialization(self, iframe_element):\n        \"\"\"Iframe context should be created via CDP commands.\"\"\"\n\n        async def side_effect(command, timeout=60):\n            method = command['method']\n            if method == 'DOM.describeNode':\n                return {\n                    'result': {\n                        'node': {\n                            'frameId': 'frame-123',\n                            'contentDocument': {\n                                'frameId': 'frame-123',\n                                'documentURL': 'https://example.com/frame.html',\n                                'baseURL': 'https://example.com/frame.html',\n                            },\n                        }\n                    }\n                }\n            if method == 'Page.createIsolatedWorld':\n                return {'result': {'executionContextId': 42}}\n            if method == 'Runtime.evaluate':\n                return {\n                    'result': {\n                        'result': {\n                            'type': 'object',\n                            'objectId': 'document-object-id',\n                        }\n                    }\n                }\n            raise AssertionError(f'Unexpected method {method}')\n\n        iframe_element._connection_handler.execute_command.side_effect = side_effect\n\n        ctx = await iframe_element.iframe_context\n        assert ctx is not None\n        assert ctx.frame_id == 'frame-123'\n        assert ctx.document_url == 'https://example.com/frame.html'\n        assert ctx.execution_context_id == 42\n        assert ctx.document_object_id == 'document-object-id'\n\n        # Subsequent access should re-resolve (no caching) to avoid stale contexts\n        ctx2 = await iframe_element.iframe_context\n        assert ctx2 is not ctx\n        assert ctx2.frame_id == ctx.frame_id\n        assert ctx2.execution_context_id == ctx.execution_context_id\n\n    @pytest.mark.asyncio\n    async def test_iframe_inner_html_uses_runtime_evaluate(self, iframe_element):\n        \"\"\"inner_html should read from iframe execution context.\"\"\"\n        async def side_effect(command, timeout=60):\n            method = command['method']\n            if method == 'DOM.describeNode':\n                return {\n                    'result': {\n                        'node': {\n                            'frameId': 'frame-123',\n                            'contentDocument': {\n                                'frameId': 'frame-123',\n                                'documentURL': 'https://example.com/frame.html',\n                                'baseURL': 'https://example.com/frame.html',\n                            },\n                        }\n                    }\n                }\n            if method == 'Page.createIsolatedWorld':\n                return {'result': {'executionContextId': 77}}\n            if method == 'Runtime.evaluate':\n                expression = command['params']['expression']\n                if expression == 'document.documentElement':\n                    return {\n                        'result': {\n                            'result': {\n                                'type': 'object',\n                                'objectId': 'document-object-id',\n                            }\n                        }\n                    }\n                if expression == 'document.documentElement.outerHTML':\n                    assert command['params']['contextId'] == 77\n                    return {\n                        'result': {\n                            'result': {\n                                'type': 'string',\n                                'value': '<html>iframe content</html>',\n                            }\n                        }\n                    }\n            raise AssertionError(f'Unexpected method {method}')\n\n        iframe_element._connection_handler.execute_command.side_effect = side_effect\n\n        html = await iframe_element.inner_html\n        assert html == '<html>iframe content</html>'\n\n        methods = [\n            call.args[0]['method']\n            for call in iframe_element._connection_handler.execute_command.await_args_list\n        ]\n        assert methods.count('DOM.describeNode') == 1\n        assert methods.count('Page.createIsolatedWorld') == 1\n        assert methods.count('Runtime.evaluate') == 2\n\n    @pytest.mark.asyncio\n    async def test_find_within_iframe_uses_document_context(self, iframe_element):\n        \"\"\"find() should query against the iframe's document element.\"\"\"\n\n        async def side_effect(command, timeout=60):\n            method = command['method']\n            if method == 'DOM.describeNode':\n                object_id = command['params'].get('objectId')\n                if object_id == 'iframe-object-id':\n                    return {\n                        'result': {\n                            'node': {\n                                'frameId': 'frame-123',\n                                'contentDocument': {\n                                    'frameId': 'frame-123',\n                                    'documentURL': 'https://example.com/frame.html',\n                                    'baseURL': 'https://example.com/frame.html',\n                                },\n                            }\n                        }\n                    }\n                if object_id == 'element-object-id':\n                    return {\n                        'result': {\n                            'node': {\n                                'nodeName': 'DIV',\n                                'attributes': ['id', 'child', 'data-test', 'value'],\n                            }\n                        }\n                    }\n                raise AssertionError('Unexpected objectId in describeNode')\n            if method == 'Page.createIsolatedWorld':\n                return {'result': {'executionContextId': 88}}\n            if method == 'Runtime.evaluate':\n                expression = command['params']['expression']\n                if expression == 'document.documentElement':\n                    return {\n                        'result': {\n                            'result': {\n                                'type': 'object',\n                                'objectId': 'document-object-id',\n                            }\n                        }\n                    }\n                raise AssertionError(f'Unexpected evaluate expression: {expression}')\n            if method == 'Runtime.callFunctionOn':\n                assert command['params']['objectId'] == 'document-object-id'\n                return {\n                    'result': {\n                        'result': {\n                            'type': 'object',\n                            'objectId': 'element-object-id',\n                        }\n                    }\n                }\n            raise AssertionError(f'Unexpected method {method}')\n\n        iframe_element._connection_handler.execute_command.side_effect = side_effect\n\n        result = await iframe_element.find(tag_name='div')\n\n        assert isinstance(result, WebElement)\n        assert result._object_id == 'element-object-id'\n\n        runtime_calls = [\n            call.args[0]\n            for call in iframe_element._connection_handler.execute_command.await_args_list\n            if call.args[0]['method'] == 'Runtime.callFunctionOn'\n        ]\n        assert runtime_calls, 'Runtime.callFunctionOn should be used for iframe queries'\n        assert runtime_calls[0]['params']['objectId'] == 'document-object-id'\n\n    @pytest.mark.asyncio\n    async def test_get_parent_element_success(self, web_element):\n        \"\"\"Test successful parent element retrieval.\"\"\"\n        script_response = {'result': {'result': {'objectId': 'parent-object-id'}}}\n        describe_response = {\n            'result': {\n                'node': {\n                    'nodeName': 'DIV',\n                    'attributes': ['id', 'parent-container', 'class', 'container'],\n                }\n            }\n        }\n        web_element._connection_handler.execute_command.side_effect = [\n            script_response,  # Script execution\n            describe_response,  # Describe node\n        ]\n\n        parent_element = await web_element.get_parent_element()\n\n        assert isinstance(parent_element, WebElement)\n        assert parent_element._object_id == 'parent-object-id'\n        assert parent_element._attributes == {\n            'id': 'parent-container',\n            'class_name': 'container',\n            'tag_name': 'div',\n        }\n        web_element._connection_handler.execute_command.assert_called()\n\n    @pytest.mark.asyncio\n    async def test_get_parent_element_not_found(self, web_element):\n        \"\"\"Test parent element not found raises ElementNotFound.\"\"\"\n        script_response = {'result': {'result': {}}}  # No objectId\n\n        web_element._connection_handler.execute_command.return_value = script_response\n\n        with pytest.raises(ElementNotFound, match='Parent element not found for element:'):\n            await web_element.get_parent_element()\n\n    @pytest.mark.asyncio\n    async def test_get_parent_element_with_complex_attributes(self, web_element):\n        \"\"\"Test parent element with complex attribute list.\"\"\"\n        script_response = {'result': {'result': {'objectId': 'complex-parent-id'}}}\n\n        describe_response = {\n            'result': {\n                'node': {\n                    'nodeName': 'SECTION',\n                    'attributes': [\n                        'id',\n                        'main-section',\n                        'class',\n                        'content-wrapper',\n                        'data-testid',\n                        'parent-element',\n                        'aria-label',\n                        'Main content area',\n                    ],\n                }\n            }\n        }\n\n        web_element._connection_handler.execute_command.side_effect = [\n            script_response,\n            describe_response,\n        ]\n\n        parent_element = await web_element.get_parent_element()\n\n        assert isinstance(parent_element, WebElement)\n        assert parent_element._object_id == 'complex-parent-id'\n        assert parent_element._attributes == {\n            'id': 'main-section',\n            'class_name': 'content-wrapper',\n            'data-testid': 'parent-element',\n            'aria-label': 'Main content area',\n            'tag_name': 'section',\n        }\n\n    @pytest.mark.asyncio\n    async def test_get_parent_element_root_element(self, web_element):\n        \"\"\"Test getting parent of root element (should return document body).\"\"\"\n        script_response = {'result': {'result': {'objectId': 'body-object-id'}}}\n\n        describe_response = {\n            'result': {'node': {'nodeName': 'BODY', 'attributes': ['class', 'page-body']}}\n        }\n\n        web_element._connection_handler.execute_command.side_effect = [\n            script_response,\n            describe_response,\n        ]\n\n        parent_element = await web_element.get_parent_element()\n\n        assert isinstance(parent_element, WebElement)\n        assert parent_element._object_id == 'body-object-id'\n        assert parent_element._attributes == {'class_name': 'page-body', 'tag_name': 'body'}\n\n\nclass TestWebElementKeyboardInteraction:\n    \"\"\"Test keyboard interaction methods.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_key_down(self, web_element):\n        \"\"\"Test key_down method.\"\"\"\n        key = Key.ENTER\n        modifiers = KeyModifier.CTRL\n\n        await web_element.key_down(key, modifiers)\n\n        web_element._connection_handler.execute_command.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_key_down_without_modifiers(self, web_element):\n        \"\"\"Test key_down without modifiers.\"\"\"\n        key = Key.TAB\n\n        await web_element.key_down(key)\n\n        web_element._connection_handler.execute_command.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_key_up(self, web_element):\n        \"\"\"Test key_up method.\"\"\"\n        key = Key.ESCAPE\n\n        await web_element.key_up(key)\n\n        web_element._connection_handler.execute_command.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_press_keyboard_key(self, web_element):\n        \"\"\"Test press_keyboard_key method (key down + up).\"\"\"\n        key = Key.SPACE\n        modifiers = KeyModifier.SHIFT\n\n        with patch('asyncio.sleep') as mock_sleep:\n            await web_element.press_keyboard_key(key, modifiers, interval=0.05)\n\n        # Should call key_down and key_up\n        assert web_element._connection_handler.execute_command.call_count == 2\n        mock_sleep.assert_called_once_with(0.05)\n\n    @pytest.mark.asyncio\n    async def test_press_keyboard_key_default_interval(self, web_element):\n        \"\"\"Test press_keyboard_key with default interval.\"\"\"\n        key = Key.ENTER\n\n        with patch('asyncio.sleep') as mock_sleep:\n            await web_element.press_keyboard_key(key)\n\n        mock_sleep.assert_called_once_with(0.1)\n\n\nclass TestWebElementClicking:\n    \"\"\"Test clicking methods and behaviors.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_click_using_js_success(self, web_element):\n        \"\"\"Test successful JavaScript click.\"\"\"\n        # Mock element visibility and click success\n        web_element.is_visible = AsyncMock(return_value=True)\n        web_element.scroll_into_view = AsyncMock()\n        web_element.execute_script = AsyncMock(return_value={'result': {'result': {'value': True}}})\n\n        await web_element.click_using_js()\n\n        web_element.scroll_into_view.assert_called_once()\n        web_element.is_visible.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_click_using_js_not_visible(self, web_element):\n        \"\"\"Test JavaScript click when element is not visible.\"\"\"\n        web_element.is_visible = AsyncMock(return_value=False)\n        web_element.scroll_into_view = AsyncMock()\n\n        with pytest.raises(ElementNotVisible):\n            await web_element.click_using_js()\n\n    @pytest.mark.asyncio\n    async def test_click_using_js_not_interactable(self, web_element):\n        \"\"\"Test JavaScript click when element is not interactable.\"\"\"\n        web_element.is_visible = AsyncMock(return_value=True)\n        web_element.scroll_into_view = AsyncMock()\n        web_element.execute_script = AsyncMock(\n            return_value={'result': {'result': {'value': False}}}\n        )\n\n        with pytest.raises(ElementNotInteractable):\n            await web_element.click_using_js()\n\n    @pytest.mark.asyncio\n    async def test_click_using_js_option_element(self, option_element):\n        \"\"\"Test JavaScript click on option element uses specialized method.\"\"\"\n        option_element._click_option_tag = AsyncMock()\n\n        await option_element.click_using_js()\n\n        option_element._click_option_tag.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_click_success(self, web_element):\n        \"\"\"Test successful mouse click.\"\"\"\n        bounds = [0, 0, 100, 0, 100, 100, 0, 100]  # Rectangle coordinates\n        web_element.is_visible = AsyncMock(return_value=True)\n        web_element.scroll_into_view = AsyncMock()\n        web_element._connection_handler.execute_command.side_effect = [\n            {'result': {'model': {'content': bounds}}},  # bounds\n            None,  # mouse press\n            None,  # mouse release\n        ]\n\n        with patch('asyncio.sleep') as mock_sleep:\n            await web_element.click(x_offset=5, y_offset=10, hold_time=0.2)\n\n        # Should call mouse press and release\n        assert web_element._connection_handler.execute_command.call_count == 3\n        mock_sleep.assert_called_once_with(0.2)\n\n    @pytest.mark.asyncio\n    async def test_click_not_visible(self, web_element):\n        \"\"\"Test click when element is not visible.\"\"\"\n        web_element.is_visible = AsyncMock(return_value=False)\n\n        with pytest.raises(ElementNotVisible):\n            await web_element.click()\n\n    @pytest.mark.asyncio\n    async def test_click_option_element(self, option_element):\n        \"\"\"Test click on option element uses specialized method.\"\"\"\n        option_element._click_option_tag = AsyncMock()\n\n        await option_element.click()\n\n        option_element._click_option_tag.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_click_bounds_fallback_to_js(self, web_element):\n        \"\"\"Test click falls back to JS bounds when CDP bounds fail.\"\"\"\n        web_element.is_visible = AsyncMock(return_value=True)\n        web_element.scroll_into_view = AsyncMock()\n\n        # First call (bounds) raises KeyError, second call (JS bounds) succeeds\n        js_bounds = {'x': 10, 'y': 20, 'width': 100, 'height': 50}\n        web_element._connection_handler.execute_command.side_effect = [\n            {'result': {'model': {'invalid_key': []}}},  # bounds with KeyError\n            {'result': {'result': {'value': json.dumps(js_bounds)}}},  # JS bounds\n            None,  # mouse press\n            None,  # mouse release\n        ]\n\n        await web_element.click()\n\n        # Should call bounds, JS bounds, mouse press, and mouse release\n        assert web_element._connection_handler.execute_command.call_count == 4\n\n    @pytest.mark.asyncio\n    async def test_click_option_tag_method(self, option_element):\n        \"\"\"Test _click_option_tag method.\"\"\"\n        await option_element._click_option_tag()\n\n        # Should execute script with option value\n        option_element._connection_handler.execute_command.assert_called_once()\n\n\nclass TestWebElementHumanizedClick:\n    \"\"\"Test WebElement.click() humanized behavior via Mouse API.\"\"\"\n\n    @pytest.fixture\n    def mouse_mock(self):\n        mock = AsyncMock()\n        mock.click = AsyncMock()\n        return mock\n\n    @pytest.fixture\n    def element_with_mouse(self, mock_connection_handler, mouse_mock):\n        attributes_list = ['id', 'btn-1', 'tag_name', 'button']\n        elem = WebElement(\n            object_id='obj-1',\n            connection_handler=mock_connection_handler,\n            method='css',\n            selector='#btn-1',\n            attributes_list=attributes_list,\n            mouse=mouse_mock,\n        )\n        return elem\n\n    @pytest.mark.asyncio\n    async def test_click_humanized_uses_mouse(self, element_with_mouse, mouse_mock):\n        \"\"\"When humanize=True and mouse is set, mouse.click() is called.\"\"\"\n        bounds = [0, 0, 100, 0, 100, 100, 0, 100]\n        element_with_mouse.is_visible = AsyncMock(return_value=True)\n        element_with_mouse.scroll_into_view = AsyncMock()\n        element_with_mouse._connection_handler.execute_command.return_value = {\n            'result': {'model': {'content': bounds}}\n        }\n\n        await element_with_mouse.click(humanize=True)\n\n        mouse_mock.click.assert_called_once_with(50.0, 50.0)\n\n    @pytest.mark.asyncio\n    async def test_click_humanized_with_offset(self, element_with_mouse, mouse_mock):\n        \"\"\"Offset is applied before passing to mouse.click().\"\"\"\n        bounds = [0, 0, 100, 0, 100, 100, 0, 100]\n        element_with_mouse.is_visible = AsyncMock(return_value=True)\n        element_with_mouse.scroll_into_view = AsyncMock()\n        element_with_mouse._connection_handler.execute_command.return_value = {\n            'result': {'model': {'content': bounds}}\n        }\n\n        await element_with_mouse.click(x_offset=10, y_offset=20, humanize=True)\n\n        mouse_mock.click.assert_called_once_with(60.0, 70.0)\n\n    @pytest.mark.asyncio\n    async def test_click_humanize_false_uses_raw_cdp(self, element_with_mouse, mouse_mock):\n        \"\"\"When humanize=False, raw CDP events are used even if mouse is set.\"\"\"\n        bounds = [0, 0, 100, 0, 100, 100, 0, 100]\n        element_with_mouse.is_visible = AsyncMock(return_value=True)\n        element_with_mouse.scroll_into_view = AsyncMock()\n        element_with_mouse._connection_handler.execute_command.side_effect = [\n            {'result': {'model': {'content': bounds}}},\n            None,  # mouse press\n            None,  # mouse release\n        ]\n\n        with patch('asyncio.sleep'):\n            await element_with_mouse.click(humanize=False)\n\n        mouse_mock.click.assert_not_called()\n        assert element_with_mouse._connection_handler.execute_command.call_count == 3\n\n    @pytest.mark.asyncio\n    async def test_click_no_mouse_falls_through(self, web_element):\n        \"\"\"When _mouse is None, raw CDP fallback is used.\"\"\"\n        assert web_element._mouse is None\n        bounds = [0, 0, 100, 0, 100, 100, 0, 100]\n        web_element.is_visible = AsyncMock(return_value=True)\n        web_element.scroll_into_view = AsyncMock()\n        web_element._connection_handler.execute_command.side_effect = [\n            {'result': {'model': {'content': bounds}}},\n            None,  # mouse press\n            None,  # mouse release\n        ]\n\n        with patch('asyncio.sleep'):\n            await web_element.click()\n\n        assert web_element._connection_handler.execute_command.call_count == 3\n\n    @pytest.mark.asyncio\n    async def test_click_humanized_iframe_element_skips_mouse(\n        self, element_with_mouse, mouse_mock\n    ):\n        \"\"\"When element has iframe context, humanized click falls back to CDP.\"\"\"\n        from pydoll.interactions.iframe import IFrameContext\n\n        bounds = [0, 0, 100, 0, 100, 100, 0, 100]\n        element_with_mouse.is_visible = AsyncMock(return_value=True)\n        element_with_mouse.scroll_into_view = AsyncMock()\n        element_with_mouse._iframe_context = IFrameContext(frame_id='frame-123')\n        element_with_mouse._connection_handler.execute_command.side_effect = [\n            {'result': {'model': {'content': bounds}}},\n            None,  # mouse press\n            None,  # mouse release\n        ]\n\n        with patch('asyncio.sleep'):\n            await element_with_mouse.click(humanize=True)\n\n        mouse_mock.click.assert_not_called()\n        assert element_with_mouse._connection_handler.execute_command.call_count == 3\n\n    @pytest.mark.asyncio\n    async def test_click_option_element_skips_mouse(self, mouse_mock, mock_connection_handler):\n        \"\"\"Option elements use JS click path regardless of mouse.\"\"\"\n        attributes_list = ['tag_name', 'option', 'value', 'opt-1']\n        elem = WebElement(\n            object_id='opt-obj',\n            connection_handler=mock_connection_handler,\n            attributes_list=attributes_list,\n            mouse=mouse_mock,\n        )\n        elem._click_option_tag = AsyncMock()\n\n        await elem.click()\n\n        elem._click_option_tag.assert_called_once()\n        mouse_mock.click.assert_not_called()\n\n\nclass TestWebElementFileInput:\n    \"\"\"Test file input specific functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_set_input_files_success(self, file_input_element):\n        \"\"\"Test successful file input setting.\"\"\"\n        files = ['/path/to/file1.txt', '/path/to/file2.pdf']\n\n        await file_input_element.set_input_files(files)\n\n        file_input_element._connection_handler.execute_command.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_set_input_files_not_file_input(self, web_element):\n        \"\"\"Test set_input_files on non-file input element.\"\"\"\n        files = ['/path/to/file.txt']\n\n        with pytest.raises(ElementNotAFileInput):\n            await web_element.set_input_files(files)\n\n    @pytest.mark.asyncio\n    async def test_set_input_files_input_but_wrong_type(self, input_element):\n        \"\"\"Test set_input_files on input element with wrong type.\"\"\"\n        files = ['/path/to/file.txt']\n\n        with pytest.raises(ElementNotAFileInput):\n            await input_element.set_input_files(files)\n\n\nclass TestWebElementScreenshot:\n    \"\"\"Test screenshot functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_take_screenshot_success(self, web_element, tmp_path):\n        \"\"\"Test successful element screenshot.\"\"\"\n        bounds = {'x': 10, 'y': 20, 'width': 100, 'height': 50}\n        screenshot_data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAgAB/edzE+oAAAAASUVORK5CYII='\n\n        web_element._connection_handler.execute_command.side_effect = [\n            {'result': {'result': {'value': json.dumps(bounds)}}},  # get_bounds_using_js\n            {'result': {'data': screenshot_data}},  # capture_screenshot\n        ]\n\n        screenshot_path = tmp_path / 'element.jpeg'\n\n        # Mock aiofiles.open properly for async context manager\n        mock_file = AsyncMock()\n        mock_file.write = AsyncMock()\n\n        with patch('aiofiles.open') as mock_aiofiles_open:\n            mock_aiofiles_open.return_value.__aenter__.return_value = mock_file\n            await web_element.take_screenshot(str(screenshot_path), quality=90)\n\n        # Should call get_bounds_using_js and capture_screenshot\n        assert web_element._connection_handler.execute_command.call_count == 2\n\n    @pytest.mark.asyncio\n    async def test_take_screenshot_default_quality(self, web_element, tmp_path):\n        \"\"\"Test screenshot with default quality.\"\"\"\n        bounds = {'x': 0, 'y': 0, 'width': 50, 'height': 50}\n        screenshot_data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAgAB/edzE+oAAAAASUVORK5CYII='\n        web_element._connection_handler.execute_command.side_effect = [\n            {'result': {'result': {'value': json.dumps(bounds)}}},\n            {'result': {'data': screenshot_data}},\n        ]\n\n        screenshot_path = tmp_path / 'element_default.jpeg'\n\n        # Mock aiofiles.open properly for async context manager\n        mock_file = AsyncMock()\n        mock_file.write = AsyncMock()\n\n        with patch('aiofiles.open') as mock_aiofiles_open:\n            mock_aiofiles_open.return_value.__aenter__.return_value = mock_file\n            await web_element.take_screenshot(str(screenshot_path))\n\n        # Should call get_bounds_using_js and capture_screenshot\n        assert web_element._connection_handler.execute_command.call_count == 2\n\n    @pytest.mark.asyncio\n    async def test_take_screenshot_as_base64(self, web_element):\n        \"\"\"Test screenshot returned as base64 string.\"\"\"\n        bounds = {'x': 10, 'y': 20, 'width': 100, 'height': 50}\n        screenshot_data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAgAB/edzE+oAAAAASUVORK5CYII='\n\n        web_element._connection_handler.execute_command.side_effect = [\n            {'result': {'result': {'value': json.dumps(bounds)}}},  # get_bounds_using_js\n            {'result': {'data': screenshot_data}},  # capture_screenshot\n        ]\n\n        # Take screenshot as base64\n        result = await web_element.take_screenshot(as_base64=True)\n\n        # Should return the base64 data\n        assert result == screenshot_data\n        # Should call get_bounds_using_js and capture_screenshot\n        assert web_element._connection_handler.execute_command.call_count == 2\n\n    @pytest.mark.asyncio\n    async def test_take_screenshot_missing_path_without_base64(self, web_element):\n        \"\"\"Test screenshot raises error when no path and as_base64=False.\"\"\"\n        from pydoll.exceptions import MissingScreenshotPath\n\n        with pytest.raises(MissingScreenshotPath):\n            await web_element.take_screenshot(as_base64=False)\n\n    @pytest.mark.asyncio\n    async def test_take_screenshot_jpg_alias(self, web_element, tmp_path):\n        \"\"\"Test that .jpg extension works as alias for .jpeg.\"\"\"\n        bounds = {'x': 10, 'y': 20, 'width': 100, 'height': 50}\n        screenshot_data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAgAB/edzE+oAAAAASUVORK5CYII='\n\n        web_element._connection_handler.execute_command.side_effect = [\n            {'result': {'result': {'value': json.dumps(bounds)}}},  # get_bounds_using_js\n            {'result': {'data': screenshot_data}},  # capture_screenshot\n        ]\n\n        screenshot_path = tmp_path / 'element.jpg'\n\n        # Mock aiofiles.open properly for async context manager\n        mock_file = AsyncMock()\n        mock_file.write = AsyncMock()\n\n        with patch('aiofiles.open') as mock_aiofiles_open:\n            mock_aiofiles_open.return_value.__aenter__.return_value = mock_file\n            await web_element.take_screenshot(str(screenshot_path), quality=90)\n\n        # Should work without raising InvalidFileExtension\n        assert web_element._connection_handler.execute_command.call_count == 2\n\n\nclass TestWebElementVisibility:\n    \"\"\"Test element visibility and interaction checks.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_is_element_visible_true(self, web_element):\n        \"\"\"Test _is_element_visible returns True.\"\"\"\n        web_element.execute_script = AsyncMock(return_value={'result': {'result': {'value': True}}})\n\n        result = await web_element.is_visible()\n        assert result is True\n\n    @pytest.mark.asyncio\n    async def test_is_element_visible_false(self, web_element):\n        \"\"\"Test _is_element_visible returns False.\"\"\"\n        web_element.execute_script = AsyncMock(\n            return_value={'result': {'result': {'value': False}}}\n        )\n\n        result = await web_element.is_visible()\n        assert result is False\n\n    @pytest.mark.asyncio\n    async def test_is_element_on_top_true(self, web_element):\n        \"\"\"Test _is_element_on_top returns True.\"\"\"\n        web_element.execute_script = AsyncMock(return_value={'result': {'result': {'value': True}}})\n\n        result = await web_element.is_on_top()\n        assert result is True\n\n    @pytest.mark.asyncio\n    async def test_is_element_on_top_false(self, web_element):\n        \"\"\"Test _is_element_on_top returns False.\"\"\"\n        web_element.execute_script = AsyncMock(\n            return_value={'result': {'result': {'value': False}}}\n        )\n\n        result = await web_element.is_on_top()\n        assert result is False\n\n    @pytest.mark.asyncio\n    async def test_is_element_interactable_true(self, web_element):\n        \"\"\"Test _is_element_interactable returns True.\"\"\"\n        web_element.execute_script = AsyncMock(return_value={'result': {'result': {'value': True}}})\n\n        result = await web_element.is_interactable()\n        assert result is True\n\n    @pytest.mark.asyncio\n    async def test_is_element_interactable_false(self, web_element):\n        \"\"\"Test _is_element_interactable returns False.\"\"\"\n        web_element.execute_script = AsyncMock(\n            return_value={'result': {'result': {'value': False}}}\n        )\n\n        result = await web_element.is_interactable()\n        assert result is False\n\n\nclass TestWebElementWaitUntil:\n    \"\"\"Test wait_until method.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_wait_until_visible_success(self, web_element):\n        \"\"\"Test wait_until succeeds when element becomes visible.\"\"\"\n        web_element.is_visible = AsyncMock(side_effect=[False, True])\n\n        with patch('asyncio.sleep') as mock_sleep, patch('asyncio.get_event_loop') as mock_loop:\n            mock_loop.return_value.time.side_effect = [0, 0.5]\n\n            await web_element.wait_until(is_visible=True, timeout=2)\n\n        assert web_element.is_visible.call_count == 2\n        mock_sleep.assert_called_once_with(0.5)\n\n    @pytest.mark.asyncio\n    async def test_wait_until_visible_timeout(self, web_element):\n        \"\"\"Test wait_until raises WaitElementTimeout when visibility not met.\"\"\"\n        web_element.is_visible = AsyncMock(return_value=False)\n\n        with patch('asyncio.sleep') as mock_sleep, patch('asyncio.get_event_loop') as mock_loop:\n            mock_loop.return_value.time.side_effect = [0, 0.5, 1.0, 1.5, 2.1]\n\n            with pytest.raises(WaitElementTimeout, match='element to become visible'):\n                await web_element.wait_until(is_visible=True, timeout=2)\n\n        assert mock_sleep.call_count == 3\n\n    @pytest.mark.asyncio\n    async def test_wait_until_interactable_success(self, web_element):\n        \"\"\"Test wait_until succeeds when element becomes interactable.\"\"\"\n        web_element.is_interactable = AsyncMock(return_value=True)\n\n        await web_element.wait_until(is_interactable=True, timeout=1)\n\n        web_element.is_interactable.assert_called_once()\n\n    @pytest.mark.asyncio\n    async def test_wait_until_interactable_timeout(self, web_element):\n        \"\"\"Test wait_until raises WaitElementTimeout when not interactable.\"\"\"\n        web_element.is_interactable = AsyncMock(return_value=False)\n\n        with patch('asyncio.sleep') as mock_sleep, patch('asyncio.get_event_loop') as mock_loop:\n            mock_loop.return_value.time.side_effect = [0, 0.5, 1.1]\n\n            with pytest.raises(WaitElementTimeout, match='element to become interactable'):\n                await web_element.wait_until(is_interactable=True, timeout=1)\n\n        mock_sleep.assert_called_once_with(0.5)\n\n    @pytest.mark.asyncio\n    async def test_wait_until_visible_and_interactable(self, web_element):\n        \"\"\"Test wait_until requires both conditions when both are True.\"\"\"\n        web_element.is_visible = AsyncMock(side_effect=[False, True])\n        web_element.is_interactable = AsyncMock(side_effect=[False, True])\n\n        with patch('asyncio.sleep') as mock_sleep, patch('asyncio.get_event_loop') as mock_loop:\n            mock_loop.return_value.time.side_effect = [0, 0.5, 1.0]\n\n            await web_element.wait_until(is_visible=True, is_interactable=True, timeout=2)\n\n        assert web_element.is_visible.call_count == 2\n        assert web_element.is_interactable.call_count == 2\n        mock_sleep.assert_called_once_with(0.5)\n\n    @pytest.mark.asyncio\n    async def test_wait_until_no_conditions(self, web_element):\n        \"\"\"Test wait_until raises ValueError when no condition specified.\"\"\"\n        with pytest.raises(ValueError):\n            await web_element.wait_until()\n\n\nclass TestWebElementUtilityMethods:\n    \"\"\"Test utility and helper methods.\"\"\"\n\n    def test_calculate_center(self):\n        \"\"\"Test _calculate_center static method.\"\"\"\n        # Rectangle: (0,0), (100,0), (100,100), (0,100)\n        bounds = [0, 0, 100, 0, 100, 100, 0, 100]\n        x_center, y_center = WebElement._calculate_center(bounds)\n        assert x_center == 50\n        assert y_center == 50\n\n    def test_calculate_center_irregular_shape(self):\n        \"\"\"Test _calculate_center with irregular coordinates.\"\"\"\n        # Triangle-like shape\n        bounds = [0, 0, 50, 0, 25, 50]\n        x_center, y_center = WebElement._calculate_center(bounds)\n        assert x_center == 25  # (0 + 50 + 25) / 3\n        assert y_center == pytest.approx(16.67, rel=1e-2)  # (0 + 0 + 50) / 3\n\n    def test_is_option_tag_true(self, option_element):\n        \"\"\"Test _is_option_tag returns True for option elements.\"\"\"\n        assert option_element._is_option_tag() is True\n\n    def test_is_option_tag_false(self, web_element):\n        \"\"\"Test _is_option_tag returns False for non-option elements.\"\"\"\n        assert web_element._is_option_tag() is False\n\n    def test_def_attributes_empty_list(self, mock_connection_handler):\n        \"\"\"Test _def_attributes with empty list.\"\"\"\n        element = WebElement(\n            object_id='test', connection_handler=mock_connection_handler, attributes_list=[]\n        )\n        assert element._attributes == {}\n\n    def test_def_attributes_class_rename(self, mock_connection_handler):\n        \"\"\"Test _def_attributes renames 'class' to 'class_name'.\"\"\"\n        attributes_list = ['class', 'my-class', 'id', 'my-id']\n        element = WebElement(\n            object_id='test',\n            connection_handler=mock_connection_handler,\n            attributes_list=attributes_list,\n        )\n        assert element._attributes == {'class_name': 'my-class', 'id': 'my-id'}\n\n    @pytest.mark.asyncio\n    async def test_execute_script_basic(self, web_element):\n        \"\"\"Test execute_script basic functionality with return value.\"\"\"\n        script = 'return this.tagName;'\n        expected_response = {'result': {'result': {'value': 'DIV'}}}\n        web_element._connection_handler.execute_command.return_value = expected_response\n\n        result = await web_element.execute_script(script, return_by_value=True)\n\n        assert result == expected_response\n        expected_command = RuntimeCommands.call_function_on(\n            object_id='test-object-id',\n            function_declaration='function(){ return this.tagName; }',\n            return_by_value=True,\n        )\n        web_element._connection_handler.execute_command.assert_called_once_with(\n            expected_command, timeout=60\n        )\n\nclass TestBuildTextExpression:\n    \"\"\"Unit tests for FindElementsMixin._build_text_expression.\"\"\"\n\n    def test_build_text_expression_with_xpath(self):\n        from pydoll.elements.mixins import FindElementsMixin\n        expr = FindElementsMixin._build_text_expression('//p[@id=\"x\"]', 'xpath')\n        assert isinstance(expr, str)\n        assert 'XPathResult.FIRST_ORDERED_NODE_TYPE' in expr\n        assert '@id' in expr\n        assert 'p' in expr\n\n    def test_build_text_expression_with_name(self):\n        from pydoll.elements.mixins import FindElementsMixin\n        expr = FindElementsMixin._build_text_expression('fieldName', 'name')\n        assert isinstance(expr, str)\n        assert '//*[@name=\"fieldName\"]' in expr\n\n    def test_build_text_expression_with_id_css(self):\n        from pydoll.elements.mixins import FindElementsMixin\n        expr = FindElementsMixin._build_text_expression('main', 'id')\n        assert 'document.querySelector' in expr\n        assert '#main' in expr\n\n    def test_build_text_expression_with_class_css(self):\n        from pydoll.elements.mixins import FindElementsMixin\n        expr = FindElementsMixin._build_text_expression('item', 'class_name')\n        assert 'document.querySelector' in expr\n        assert '.item' in expr\n\n    def test_build_text_expression_with_tag_css(self):\n        from pydoll.elements.mixins import FindElementsMixin\n        expr = FindElementsMixin._build_text_expression('button', 'tag_name')\n        assert 'document.querySelector' in expr\n        assert 'button' in expr\n\nclass TestIsOptionElementHeuristics:\n    \"\"\"Unit tests for heuristics inside WebElement._is_option_element.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_is_option_element_by_tag_attribute(self, option_element):\n        assert await option_element._is_option_element() is True\n\n    @pytest.mark.asyncio\n    async def test_is_option_element_by_method_and_selector_tag_name(self, mock_connection_handler):\n        dummy = WebElement('dummy', mock_connection_handler, method='tag_name', selector='option', attributes_list=[])\n        assert await dummy._is_option_element() is True\n\n    @pytest.mark.asyncio\n    async def test_is_option_element_by_xpath_selector_contains_option(self, mock_connection_handler):\n        dummy = WebElement('dummy', mock_connection_handler, method='xpath', selector='//OPTION[@value=\\\"x\\\"]', attributes_list=[])\n        assert await dummy._is_option_element() is True\n    @pytest.mark.asyncio\n    async def test_execute_script_with_this_syntax(self, web_element):\n        \"\"\"Test execute_script method with 'this' syntax.\"\"\"\n        script = 'this.style.border = \"2px solid red\"'\n        expected_response = {'result': {'result': {'value': None}}}\n        web_element._connection_handler.execute_command.return_value = expected_response\n\n        result = await web_element.execute_script(script)\n\n        assert result == expected_response\n        expected_command = RuntimeCommands.call_function_on(\n            object_id='test-object-id',\n            function_declaration='function(){ this.style.border = \"2px solid red\" }',\n        )\n        web_element._connection_handler.execute_command.assert_called_once_with(\n            expected_command, timeout=60\n        )\n\n    @pytest.mark.asyncio\n    async def test_execute_script_already_function(self, web_element):\n        \"\"\"Test execute_script when script is already a function.\"\"\"\n        script = 'function() { this.style.border = \"2px solid red\"; }'\n        expected_response = {'result': {'result': {'value': None}}}\n        web_element._connection_handler.execute_command.return_value = expected_response\n\n        result = await web_element.execute_script(script)\n\n        assert result == expected_response\n        expected_command = RuntimeCommands.call_function_on(\n            object_id='test-object-id',\n            function_declaration='function() { this.style.border = \"2px solid red\"; }',\n        )\n        web_element._connection_handler.execute_command.assert_called_once_with(\n            expected_command, timeout=60\n        )\n\n    @pytest.mark.asyncio\n    async def test_execute_script_with_parameters(self, web_element):\n        \"\"\"Test execute_script with additional parameters.\"\"\"\n        script = 'this.value = \"test\"'\n        expected_response = {'result': {'result': {'value': 'test'}}}\n        web_element._connection_handler.execute_command.return_value = expected_response\n\n        result = await web_element.execute_script(\n            script, \n            return_by_value=True,\n            user_gesture=True\n        )\n\n        assert result == expected_response\n        expected_command = RuntimeCommands.call_function_on(\n            object_id='test-object-id',\n            function_declaration='function(){ this.value = \"test\" }',\n            return_by_value=True,\n            user_gesture=True,\n        )\n        web_element._connection_handler.execute_command.assert_called_once_with(\n            expected_command, timeout=60\n        )\n\n    @pytest.mark.asyncio\n    async def test_execute_script_arrow_function(self, web_element):\n        \"\"\"Test execute_script with arrow function syntax.\"\"\"\n        script = '() => { this.style.color = \"red\"; }'\n        expected_response = {'result': {'result': {'value': None}}}\n        web_element._connection_handler.execute_command.return_value = expected_response\n\n        result = await web_element.execute_script(script)\n\n        assert result == expected_response\n        expected_command = RuntimeCommands.call_function_on(\n            object_id='test-object-id',\n            function_declaration='() => { this.style.color = \"red\"; }',\n        )\n        web_element._connection_handler.execute_command.assert_called_once_with(\n            expected_command, timeout=60\n        )\n\n    @pytest.mark.asyncio\n    async def test_execute_script_multiline(self, web_element):\n        \"\"\"Test execute_script with multiline script.\"\"\"\n        script = '''\n            this.style.padding = \"10px\";\n            this.style.margin = \"5px\";\n            this.style.borderRadius = \"8px\";\n        '''\n        expected_response = {'result': {'result': {'value': None}}}\n        web_element._connection_handler.execute_command.return_value = expected_response\n\n        result = await web_element.execute_script(script)\n\n        assert result == expected_response\n        web_element._connection_handler.execute_command.assert_called_once()\n        call_args = web_element._connection_handler.execute_command.call_args[0][0]\n        \n        assert call_args['method'].value == 'Runtime.callFunctionOn'\n        assert call_args['params']['objectId'] == 'test-object-id'\n        \n        func_decl = call_args['params']['functionDeclaration']\n        assert 'function(){' in func_decl\n        assert 'this.style.padding = \"10px\"' in func_decl\n        assert 'this.style.margin = \"5px\"' in func_decl\n        assert 'this.style.borderRadius = \"8px\"' in func_decl\n\n    @pytest.mark.asyncio\n    async def test_execute_script_with_arguments(self, web_element):\n        \"\"\"Test execute_script with custom arguments.\"\"\"\n        script = 'this.value = arguments[0];'\n        arguments = [CallArgument(value=\"test_value\")]\n        expected_response = {'result': {'result': {'value': None}}}\n        web_element._connection_handler.execute_command.return_value = expected_response\n\n        result = await web_element.execute_script(script, arguments=arguments)\n\n        assert result == expected_response\n        expected_command = RuntimeCommands.call_function_on(\n            object_id='test-object-id',\n            function_declaration='function(){ this.value = arguments[0]; }',\n            arguments=arguments,\n        )\n        web_element._connection_handler.execute_command.assert_called_once_with(\n            expected_command, timeout=60\n        )\n\n    @pytest.mark.asyncio\n    async def test_execute_script_all_parameters(self, web_element):\n        \"\"\"Test execute_script with all optional parameters.\"\"\"\n        script = 'this.click()'\n        expected_response = {'result': {'result': {'value': None}}}\n        web_element._connection_handler.execute_command.return_value = expected_response\n\n        result = await web_element.execute_script(\n            script,\n            silent=True,\n            return_by_value=True,\n            generate_preview=True,\n            user_gesture=True,\n            await_promise=True,\n            execution_context_id=123,\n            object_group=\"test_group\",\n            throw_on_side_effect=True,\n            unique_context_id=\"unique_123\"\n        )\n\n        assert result == expected_response\n        expected_command = RuntimeCommands.call_function_on(\n            object_id='test-object-id',\n            function_declaration='function(){ this.click() }',\n            silent=True,\n            return_by_value=True,\n            generate_preview=True,\n            user_gesture=True,\n            await_promise=True,\n            execution_context_id=123,\n            object_group=\"test_group\",\n            throw_on_side_effect=True,\n            unique_context_id=\"unique_123\",\n        )\n        web_element._connection_handler.execute_command.assert_called_once_with(\n            expected_command, timeout=60\n        )\n\n    def test_repr(self, web_element):\n        \"\"\"Test __repr__ method.\"\"\"\n        repr_str = repr(web_element)\n        assert 'WebElement' in repr_str\n        assert 'test-object-id' in repr_str\n        assert 'id=\\'test-id\\'' in repr_str\n        assert 'class_name=\\'test-class\\'' in repr_str\n\n\nclass TestWebElementFindMethods:\n    \"\"\"Test element finding methods from FindElementsMixin.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_find_element_success(self, web_element):\n        \"\"\"Test successful element finding.\"\"\"\n        node_response = {'result': {'result': {'objectId': 'found-element-id'}}}\n        describe_response = {\n            'result': {'node': {'nodeName': 'BUTTON', 'attributes': ['class', 'btn']}}\n        }\n\n        web_element._connection_handler.execute_command.side_effect = [\n            node_response,\n            describe_response,\n        ]\n\n        element = await web_element.find(id='button-id')\n\n        assert isinstance(element, WebElement)\n        assert element._object_id == 'found-element-id'\n        assert element._attributes['class_name'] == 'btn'\n\n    @pytest.mark.asyncio\n    async def test_find_element_not_found_with_exception(self, web_element):\n        \"\"\"Test element not found raises exception.\"\"\"\n        web_element._connection_handler.execute_command.return_value = {'result': {'result': {}}}\n\n        with pytest.raises(ElementNotFound):\n            await web_element.find(id='nonexistent')\n\n    @pytest.mark.asyncio\n    async def test_find_element_not_found_no_exception(self, web_element):\n        \"\"\"Test element not found returns None when raise_exc=False.\"\"\"\n        web_element._connection_handler.execute_command.return_value = {'result': {'result': {}}}\n\n        result = await web_element.find(id='nonexistent', raise_exc=False)\n        assert result is None\n\n    @pytest.mark.asyncio\n    async def test_find_elements_success(self, web_element):\n        \"\"\"Test successful multiple elements finding.\"\"\"\n        find_response = {'result': {'result': {'objectId': 'parent-id'}}}\n        properties_response = {\n            'result': {\n                'result': [\n                    {'name': '0', 'value': {'type': 'object', 'objectId': 'child-1'}},\n                    {'name': '1', 'value': {'type': 'object', 'objectId': 'child-2'}},\n                ]\n            }\n        }\n        describe_response = {\n            'result': {'node': {'nodeName': 'LI', 'attributes': ['class', 'item']}}\n        }\n\n        web_element._connection_handler.execute_command.side_effect = [\n            find_response,\n            properties_response,\n            describe_response,\n            describe_response,\n        ]\n\n        elements = await web_element.find(class_name='item', find_all=True)\n\n        assert len(elements) == 2\n        assert all(isinstance(elem, WebElement) for elem in elements)\n        assert elements[0]._object_id == 'child-1'\n        assert elements[1]._object_id == 'child-2'\n\n    @pytest.mark.asyncio\n    async def test_find_with_timeout_success(self, web_element):\n        \"\"\"Test find with timeout succeeds on retry.\"\"\"\n        node_response = {'result': {'result': {'objectId': 'delayed-element'}}}\n        describe_response = {'result': {'node': {'nodeName': 'DIV', 'attributes': []}}}\n\n        # First call returns empty, second call succeeds\n        web_element._connection_handler.execute_command.side_effect = [\n            {'result': {'result': {}}},  # First attempt fails\n            node_response,  # Second attempt succeeds\n            describe_response,\n        ]\n\n        with patch('asyncio.sleep') as mock_sleep:\n            element = await web_element.find(id='delayed', timeout=2)\n\n        assert isinstance(element, WebElement)\n        assert element._object_id == 'delayed-element'\n        mock_sleep.assert_called()\n\n    @pytest.mark.asyncio\n    async def test_find_with_timeout_failure(self, web_element):\n        \"\"\"Test find with timeout raises WaitElementTimeout.\"\"\"\n        web_element._connection_handler.execute_command.return_value = {'result': {'result': {}}}\n\n        with patch('asyncio.get_event_loop') as mock_loop:\n            mock_loop.return_value.time.side_effect = [\n                0,\n                0.5,\n                1.0,\n                1.5,\n                2.1,\n            ]  # Simulate time progression\n\n            with pytest.raises(WaitElementTimeout):\n                await web_element.find(id='never-appears', timeout=2)\n\n    @pytest.mark.asyncio\n    async def test_query_css_selector(self, web_element):\n        \"\"\"Test query method with CSS selector.\"\"\"\n        node_response = {'result': {'result': {'objectId': 'queried-element'}}}\n        describe_response = {\n            'result': {'node': {'nodeName': 'A', 'attributes': ['href', 'http://example.com']}}\n        }\n\n        web_element._connection_handler.execute_command.side_effect = [\n            node_response,\n            describe_response,\n        ]\n\n        element = await web_element.query('a[href*=\"example\"]')\n\n        assert isinstance(element, WebElement)\n        assert element._object_id == 'queried-element'\n\n    @pytest.mark.asyncio\n    async def test_query_xpath(self, web_element):\n        \"\"\"Test query method with XPath expression.\"\"\"\n        node_response = {'result': {'result': {'objectId': 'xpath-element'}}}\n        describe_response = {'result': {'node': {'nodeName': 'SPAN', 'attributes': []}}}\n\n        web_element._connection_handler.execute_command.side_effect = [\n            node_response,\n            describe_response,\n        ]\n\n        element = await web_element.query('//span[text()=\"Click me\"]')\n\n        assert isinstance(element, WebElement)\n        assert element._object_id == 'xpath-element'\n\n    def test_find_no_criteria_raises_error(self, web_element):\n        \"\"\"Test find with no search criteria raises ValueError.\"\"\"\n        with pytest.raises(\n            ValueError, match='At least one of the following arguments must be provided'\n        ):\n            asyncio.run(web_element.find())\n\n\nclass TestWebElementEdgeCases:\n    \"\"\"Test edge cases and error conditions.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_bounds_property_with_connection_error(self, web_element):\n        \"\"\"Test bounds property when connection fails.\"\"\"\n        web_element._connection_handler.execute_command.side_effect = Exception(\"Connection failed\")\n\n        with pytest.raises(Exception, match=\"Connection failed\"):\n            await web_element.bounds\n\n    @pytest.mark.asyncio\n    async def test_text_property_with_malformed_html(self, web_element):\n        \"\"\"Test text property with malformed HTML.\"\"\"\n        malformed_html = '<div>Unclosed tag <span>content'\n        web_element._connection_handler.execute_command.return_value = {\n            'result': {'outerHTML': malformed_html}\n        }\n\n        # BeautifulSoup should handle malformed HTML gracefully\n        text = await web_element.text\n        assert 'Unclosed tag' in text\n        assert 'content' in text\n\n    @pytest.mark.asyncio\n    async def test_click_with_zero_hold_time(self, web_element):\n        \"\"\"Test click with zero hold time.\"\"\"\n        bounds = [0, 0, 50, 0, 50, 50, 0, 50]\n        web_element.is_visible = AsyncMock(return_value=True)\n        web_element.scroll_into_view = AsyncMock()\n        web_element._connection_handler.execute_command.side_effect = [\n            {'result': {'model': {'content': bounds}}},\n            None,  # mouse press\n            None,  # mouse release\n        ]\n\n        with patch('asyncio.sleep') as mock_sleep:\n            await web_element.click(hold_time=0)\n\n        mock_sleep.assert_called_once_with(0)\n\n    @pytest.mark.asyncio\n    async def test_type_text_empty_string(self, input_element):\n        \"\"\"Test type_text with empty string.\"\"\"\n        input_element.click = AsyncMock()\n        await input_element.type_text('')\n\n        # Should not call execute_command for empty string\n        input_element._connection_handler.execute_command.assert_not_called()\n        assert input_element.click.call_count == 1\n\n    @pytest.mark.asyncio\n    async def test_set_input_files_empty_list(self, file_input_element):\n        \"\"\"Test set_input_files with empty file list.\"\"\"\n        await file_input_element.set_input_files([])\n\n        expected_command = DomCommands.set_file_input_files(\n            files=[], object_id='file-input-object-id'\n        )\n        file_input_element._connection_handler.execute_command.assert_called_once_with(\n            expected_command, timeout=60\n        )\n\n\nclass TestWebElementGetChildren:\n    \"\"\"Integration tests for WebElement get_children_elements method using real HTML.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_get_children_elements_basic(self, ci_chrome_options):\n        \"\"\"Test get_children_elements with basic depth using real HTML.\"\"\"\n\n        # Get the path to our test HTML file\n        test_file = Path(__file__).parent / 'pages' / 'test_children.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n\n            # Find the parent element\n            parent_element = await tab.find(id='parent-element')\n\n            # Test get_children_elements with depth 3\n            nodes = await parent_element.get_children_elements(3)\n\n            # Verify results - should get all direct children and nested children up to depth 3\n            assert len(nodes) > 0\n            assert all(isinstance(node, WebElement) for node in nodes)\n\n            # Check that we have the expected direct children\n            child_ids = []\n            for node in nodes:\n                node_id = node.get_attribute('id')\n                if node_id:\n                    child_ids.append(node_id)\n\n            # Should include direct children\n            expected_direct_children = [\n                'child1',\n                'child2',\n                'child3',\n                'link1',\n                'link2',\n                'nested-parent',\n            ]\n            for expected_id in expected_direct_children:\n                assert (\n                    expected_id in child_ids\n                ), f\"Expected child {expected_id} not found in {child_ids}\"\n\n            # Should also include nested children (depth 3)\n            expected_nested_children = ['nested-child1', 'nested-child2', 'nested-link']\n            for expected_id in expected_nested_children:\n                assert (\n                    expected_id in child_ids\n                ), f\"Expected nested child {expected_id} not found in {child_ids}\"\n\n    @pytest.mark.asyncio\n    async def test_get_children_elements_with_tag_filter(self, ci_chrome_options):\n        \"\"\"Test get_children_elements with tag filter using real HTML.\"\"\"\n\n        # Get the path to our test HTML file\n        test_file = Path(__file__).parent / 'pages' / 'test_children.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n\n            # Find the parent element\n            parent_element = await tab.find(id='parent-element')\n\n            # Test get_children_elements with tag filter for 'a' tags\n            nodes_filter = await parent_element.get_children_elements(4, ['a'])\n\n            # Verify results - should only get anchor tags\n            assert len(nodes_filter) > 0\n            assert all(isinstance(node, WebElement) for node in nodes_filter)\n\n            # Check that all returned elements are anchor tags\n            for node in nodes_filter:\n                tag_name = node.get_attribute('tag_name')\n                assert tag_name.lower() == 'a', f\"Expected 'a' tag, got '{tag_name}'\"\n\n            # Check that we have the expected anchor elements\n            link_ids = []\n            for node in nodes_filter:\n                node_id = node.get_attribute('id')\n                if node_id:\n                    link_ids.append(node_id)\n\n            # Should include both direct and nested anchor tags\n            expected_links = ['link1', 'link2', 'nested-link']\n            for expected_id in expected_links:\n                assert (\n                    expected_id in link_ids\n                ), f\"Expected link {expected_id} not found in {link_ids}\"\n\n    @pytest.mark.asyncio\n    async def test_get_children_elements_depth_limit(self, ci_chrome_options):\n        \"\"\"Test get_children_elements with depth limit.\"\"\"\n\n        # Get the path to our test HTML file\n        test_file = Path(__file__).parent / 'pages' / 'test_children.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n\n            # Find the parent element\n            parent_element = await tab.find(id='parent-element')\n\n            # Test with depth 1 - should only get direct children\n            nodes_depth_1 = await parent_element.get_children_elements(1)\n\n            # Get IDs of elements found with depth 1\n            depth_1_ids = []\n            for node in nodes_depth_1:\n                node_id = node.get_attribute('id')\n                if node_id:\n                    depth_1_ids.append(node_id)\n\n            # Should include direct children but not nested ones\n            expected_direct = ['child1', 'child2', 'child3', 'link1', 'link2', 'nested-parent']\n            for expected_id in expected_direct:\n                assert expected_id in depth_1_ids, f\"Expected direct child {expected_id} not found\"\n\n            # Should NOT include nested children with depth 1\n            unexpected_nested = ['nested-child1', 'nested-child2', 'nested-link']\n            for unexpected_id in unexpected_nested:\n                assert (\n                    unexpected_id not in depth_1_ids\n                ), f\"Unexpected nested child {unexpected_id} found with depth 1\"\n\n    @pytest.mark.asyncio\n    async def test_get_children_elements_empty_result(self, ci_chrome_options):\n        \"\"\"Test get_children_elements on element with no children.\"\"\"\n\n        # Get the path to our test HTML file\n        test_file = Path(__file__).parent / 'pages' / 'test_children.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n\n            # Find a leaf element (no children)\n            leaf_element = await tab.find(id='child1')\n\n            # Test get_children_elements on element with no children\n            nodes = await leaf_element.get_children_elements(2)\n\n            # Should return empty list\n            assert isinstance(nodes, list)\n            assert len(nodes) == 0\n\n    @pytest.mark.asyncio\n    async def test_get_children_elements_element_not_found_exception(self):\n        \"\"\"Test get_children_elements raises ElementNotFound when script fails.\"\"\"\n        # Create a mock element that will fail the script execution\n        mock_connection_handler = AsyncMock()\n\n        # Mock script result without objectId (simulates script failure)\n        mock_connection_handler.execute_command.return_value = {\n            'result': {'result': {}}  # No objectId key\n        }\n\n        # Create a WebElement with the mock connection\n        element = WebElement(\n            object_id='test-element-id',\n            connection_handler=mock_connection_handler,\n            attributes_list=['id', 'test-element', 'tag_name', 'div'],\n        )\n\n        # Should raise ElementNotFound when script returns no objectId\n        with pytest.raises(ElementNotFound):\n            await element.get_children_elements(1, raise_exc=True)\n\n    @pytest.mark.asyncio\n    async def test_get_siblings_elements_basic(self, ci_chrome_options):\n        \"\"\"Test get_siblings_elements with basic functionality using real HTML.\"\"\"\n\n        # Get the path to our test HTML file\n        test_file = Path(__file__).parent / 'pages' / 'test_children.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n\n            # Find one of the child elements to get its siblings\n            child_element = await tab.find(id='child2')\n\n            # Test get_siblings_elements\n            siblings = await child_element.get_siblings_elements()\n\n            # Verify results - should get all sibling elements\n            assert len(siblings) > 0\n            assert all(isinstance(sibling, WebElement) for sibling in siblings)\n\n            # Check that we have the expected siblings\n            sibling_ids = []\n            for sibling in siblings:\n                sibling_id = sibling.get_attribute('id')\n                if sibling_id:\n                    sibling_ids.append(sibling_id)\n\n            # Should include all siblings of child2 (child1, child3, link1, link2, nested-parent)\n            # but NOT child2 itself\n            expected_siblings = ['child1', 'child3', 'link1', 'link2', 'nested-parent']\n            for expected_id in expected_siblings:\n                assert (\n                    expected_id in sibling_ids\n                ), f\"Expected sibling {expected_id} not found in {sibling_ids}\"\n\n            # Should NOT include the element itself\n            assert 'child2' not in sibling_ids, \"Element should not include itself in siblings\"\n\n    @pytest.mark.asyncio\n    async def test_get_siblings_elements_with_tag_filter(self, ci_chrome_options):\n        \"\"\"Test get_siblings_elements with tag filter.\"\"\"\n\n        # Get the path to our test HTML file\n        test_file = Path(__file__).parent / 'pages' / 'test_children.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n\n            # Find one of the child elements to get its siblings\n            child_element = await tab.find(id='child1')\n\n            # Test get_siblings_elements with tag filter for 'a' tags only\n            siblings_filter = await child_element.get_siblings_elements(tag_filter=['a'])\n\n            # Get IDs of filtered siblings\n            sibling_ids = []\n            for sibling in siblings_filter:\n                sibling_id = sibling.get_attribute('id')\n                if sibling_id:\n                    sibling_ids.append(sibling_id)\n\n            # Should include only anchor tag siblings\n            expected_links = ['link1', 'link2']\n            for expected_id in expected_links:\n                assert (\n                    expected_id in sibling_ids\n                ), f\"Expected link sibling {expected_id} not found in {sibling_ids}\"\n\n            # Should NOT include non-anchor siblings\n            unexpected_siblings = ['child2', 'child3', 'nested-parent']\n            for unexpected_id in unexpected_siblings:\n                assert (\n                    unexpected_id not in sibling_ids\n                ), f\"Unexpected non-anchor sibling {unexpected_id} found with tag filter\"\n\n    @pytest.mark.asyncio\n    async def test_get_siblings_elements_empty_result(self, ci_chrome_options):\n        \"\"\"Test get_siblings_elements on element with no siblings.\"\"\"\n\n        # Get the path to our test HTML file\n        test_file = Path(__file__).parent / 'pages' / 'test_children.html'\n        file_url = f'file://{test_file.absolute()}'\n\n        async with Chrome(options=ci_chrome_options) as browser:\n            tab = await browser.start()\n            await tab.go_to(file_url)\n\n            # Find the parent element which should have no siblings at its level\n            parent_element = await tab.find(id='parent-element')\n\n            # Test get_siblings_elements on element with no siblings\n            siblings = await parent_element.get_siblings_elements()\n\n            # Should return list with only the other parent element as sibling\n            assert isinstance(siblings, list)\n            # Should have at least one sibling (another-parent)\n            sibling_ids = []\n            for sibling in siblings:\n                sibling_id = sibling.get_attribute('id')\n                if sibling_id:\n                    sibling_ids.append(sibling_id)\n\n            # Should include the other parent element\n            assert 'another-parent' in sibling_ids\n\n    @pytest.mark.asyncio\n    async def test_get_siblings_elements_element_not_found_exception(self):\n        \"\"\"Test get_siblings_elements raises ElementNotFound when script fails.\"\"\"\n        # Create a mock element that will fail the script execution\n        mock_connection_handler = AsyncMock()\n\n        # Mock script result without objectId (simulates script failure)\n        mock_connection_handler.execute_command.return_value = {\n            'result': {'result': {}}  # No objectId key\n        }\n\n        # Create a WebElement with the mock connection\n        element = WebElement(\n            object_id='test-element-id',\n            connection_handler=mock_connection_handler,\n            attributes_list=['id', 'test-element', 'tag_name', 'div'],\n        )\n\n        # Should raise ElementNotFound when script returns no objectId\n        with pytest.raises(ElementNotFound):\n            await element.get_siblings_elements(raise_exc=True)\n\n\n\"\"\"\nTests for WebElement iframe edge cases and uncovered code paths.\n\nThis test suite focuses on covering edge cases in iframe resolution and context handling,\nincluding:\n- inner_html edge cases for iframes and iframe context elements\n- Frame tree traversal and owner resolution\n- OOPIF resolution scenarios\n- Isolated world creation failures\n- Document object resolution failures\n\"\"\"\n\nimport pytest\nimport pytest_asyncio\nfrom unittest.mock import AsyncMock, patch\n\nfrom pydoll.elements.web_element import WebElement\nfrom pydoll.interactions.iframe import IFrameContext\nfrom pydoll.connection import ConnectionHandler\nfrom pydoll.exceptions import InvalidIFrame\n\n\n@pytest_asyncio.fixture\nasync def mock_connection_handler():\n    \"\"\"Mock connection handler for WebElement tests.\"\"\"\n    with patch('pydoll.connection.ConnectionHandler', autospec=True) as mock:\n        handler = mock.return_value\n        handler.execute_command = AsyncMock()\n        handler._connection_port = 9222\n        yield handler\n\n\n@pytest.fixture\ndef iframe_element(mock_connection_handler):\n    \"\"\"Iframe element fixture for iframe-related tests.\"\"\"\n    attributes_list = ['id', 'test-iframe', 'tag_name', 'iframe']\n    return WebElement(\n        object_id='iframe-object-id',\n        connection_handler=mock_connection_handler,\n        method='css',\n        selector='iframe#test-iframe',\n        attributes_list=attributes_list,\n    )\n\n\n@pytest.fixture\ndef element_in_iframe(mock_connection_handler):\n    \"\"\"Element inside an iframe (has _iframe_context set).\"\"\"\n    attributes_list = ['id', 'button-in-iframe', 'tag_name', 'button']\n    element = WebElement(\n        object_id='button-object-id',\n        connection_handler=mock_connection_handler,\n        method='css',\n        selector='button',\n        attributes_list=attributes_list,\n    )\n    # Set iframe context to simulate element inside iframe\n    element._iframe_context = IFrameContext(\n        frame_id='frame-123',\n        document_url='https://example.com/iframe.html',\n        execution_context_id=42,\n        document_object_id='doc-obj-id',\n    )\n    return element\n\n\nclass TestInnerHtmlEdgeCases:\n    \"\"\"Test inner_html property edge cases for iframe scenarios.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_inner_html_iframe_element_with_context(self, iframe_element):\n        \"\"\"Test inner_html on iframe element uses Runtime.evaluate in iframe context.\"\"\"\n\n        async def side_effect(command, timeout=60):\n            method = command['method']\n            if method == 'DOM.describeNode':\n                return {\n                    'result': {\n                        'node': {\n                            # Simula um iframe de mesma origem já com frameId\n                            # resolvido; não precisamos de backendNodeId aqui,\n                            # pois não queremos acionar a resolução OOPIF.\n                            'frameId': 'parent-frame',\n                            'contentDocument': {\n                                'frameId': 'iframe-123',\n                                'documentURL': 'https://example.com/frame.html',\n                            },\n                        }\n                    }\n                }\n            if method == 'Page.createIsolatedWorld':\n                return {'result': {'executionContextId': 77}}\n            if method == 'Runtime.evaluate':\n                expression = command['params']['expression']\n                if expression == 'document.documentElement':\n                    return {\n                        'result': {\n                            'result': {\n                                'type': 'object',\n                                'objectId': 'doc-element-id',\n                            }\n                        }\n                    }\n                if expression == 'document.documentElement.outerHTML':\n                    return {\n                        'result': {\n                            'result': {\n                                'type': 'string',\n                                'value': '<html><body>Iframe content</body></html>',\n                            }\n                        }\n                    }\n            raise AssertionError(f'Unexpected method {method}')\n\n        iframe_element._connection_handler.execute_command.side_effect = side_effect\n\n        # Get inner HTML of iframe element\n        html = await iframe_element.inner_html\n\n        # Should return iframe's document HTML\n        assert html == '<html><body>Iframe content</body></html>'\n\n        # Verify Runtime.evaluate was called with correct context\n        evaluate_calls = [\n            call\n            for call in iframe_element._connection_handler.execute_command.await_args_list\n            if call.args[0]['method'] == 'Runtime.evaluate'\n        ]\n        # Should have two calls: one for document.documentElement, one for outerHTML\n        assert len(evaluate_calls) == 2\n        outer_html_call = evaluate_calls[1]\n        assert (\n            outer_html_call.args[0]['params']['expression']\n            == 'document.documentElement.outerHTML'\n        )\n        assert outer_html_call.args[0]['params']['contextId'] == 77\n\n    @pytest.mark.asyncio\n    async def test_inner_html_element_in_iframe_uses_call_function_on(self, element_in_iframe):\n        \"\"\"Test inner_html on element inside iframe uses Runtime.callFunctionOn.\"\"\"\n        element_in_iframe._connection_handler.execute_command.return_value = {\n            'result': {\n                'result': {\n                    'type': 'string',\n                    'value': '<button id=\"button-in-iframe\">Click me</button>',\n                }\n            }\n        }\n\n        html = await element_in_iframe.inner_html\n\n        # Should use callFunctionOn with this.outerHTML\n        assert html == '<button id=\"button-in-iframe\">Click me</button>'\n        element_in_iframe._connection_handler.execute_command.assert_called_once()\n        call_args = element_in_iframe._connection_handler.execute_command.call_args[0][0]\n        assert call_args['method'] == 'Runtime.callFunctionOn'\n        assert call_args['params']['objectId'] == 'button-object-id'\n        assert 'this.outerHTML' in call_args['params']['functionDeclaration']\n\n    @pytest.mark.asyncio\n    async def test_inner_html_element_in_iframe_empty_response(self, element_in_iframe):\n        \"\"\"Test inner_html on element inside iframe when response is empty.\"\"\"\n        element_in_iframe._connection_handler.execute_command.return_value = {\n            'result': {}  # Empty result\n        }\n\n        html = await element_in_iframe.inner_html\n\n        # Should return empty string when result is missing\n        assert html == ''\n\n    @pytest.mark.asyncio\n    async def test_inner_html_regular_element_fallback(self, mock_connection_handler):\n        \"\"\"Test inner_html falls back to DOM.getOuterHTML for regular elements.\"\"\"\n        attributes_list = ['id', 'regular-div', 'tag_name', 'div']\n        element = WebElement(\n            object_id='div-object-id',\n            connection_handler=mock_connection_handler,\n            attributes_list=attributes_list,\n        )\n        mock_connection_handler.execute_command.return_value = {\n            'result': {'outerHTML': '<div id=\"regular-div\">Content</div>'}\n        }\n\n        html = await element.inner_html\n\n        # Should use DOM.getOuterHTML for regular elements\n        assert html == '<div id=\"regular-div\">Content</div>'\n        call_args = mock_connection_handler.execute_command.call_args[0][0]\n        assert call_args['method'] == 'DOM.getOuterHTML'\n"
  }
]