[
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "\r\n# Contributor Covenant Code of Conduct\r\n\r\n## Our Pledge\r\n\r\nWe as members, contributors, and leaders pledge to make participation in our\r\ncommunity a harassment-free experience for everyone, regardless of age, body\r\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\r\nidentity and expression, level of experience, education, socio-economic status,\r\nnationality, personal appearance, race, caste, color, religion, or sexual\r\nidentity and orientation.\r\n\r\nWe pledge to act and interact in ways that contribute to an open, welcoming,\r\ndiverse, inclusive, and healthy community.\r\n\r\n## Our Standards\r\n\r\nExamples of behavior that contributes to a positive environment for our\r\ncommunity include:\r\n\r\n* Demonstrating empathy and kindness toward other people\r\n* Being respectful of differing opinions, viewpoints, and experiences\r\n* Giving and gracefully accepting constructive feedback\r\n* Accepting responsibility and apologizing to those affected by our mistakes,\r\n  and learning from the experience\r\n* Focusing on what is best not just for us as individuals, but for the overall\r\n  community\r\n\r\nExamples of unacceptable behavior include:\r\n\r\n* The use of sexualized language or imagery, and sexual attention or advances of\r\n  any kind\r\n* Trolling, insulting or derogatory comments, and personal or political attacks\r\n* Public or private harassment\r\n* Publishing others' private information, such as a physical or email address,\r\n  without their explicit permission\r\n* Other conduct which could reasonably be considered inappropriate in a\r\n  professional setting\r\n\r\n## Enforcement Responsibilities\r\n\r\nCommunity leaders are responsible for clarifying and enforcing our standards of\r\nacceptable behavior and will take appropriate and fair corrective action in\r\nresponse to any behavior that they deem inappropriate, threatening, offensive,\r\nor harmful.\r\n\r\nCommunity leaders have the right and responsibility to remove, edit, or reject\r\ncomments, commits, code, wiki edits, issues, and other contributions that are\r\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\r\ndecisions when appropriate.\r\n\r\n## Scope\r\n\r\nThis Code of Conduct applies within all community spaces, and also applies when\r\nan individual is officially representing the community in public spaces.\r\nExamples of representing our community include using an official e-mail address,\r\nposting via an official social media account, or acting as an appointed\r\nrepresentative at an online or offline event.\r\n\r\n## Enforcement\r\n\r\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\r\nreported to the community leaders responsible for enforcement at\r\n[karatelabs.io](https://www.karatelabs.io/karate-labs-contact).\r\nAll complaints will be reviewed and investigated promptly and fairly.\r\n\r\nAll community leaders are obligated to respect the privacy and security of the\r\nreporter of any incident.\r\n\r\n## Enforcement Guidelines\r\n\r\nCommunity leaders will follow these Community Impact Guidelines in determining\r\nthe consequences for any action they deem in violation of this Code of Conduct:\r\n\r\n### 1. Correction\r\n\r\n**Community Impact**: Use of inappropriate language or other behavior deemed\r\nunprofessional or unwelcome in the community.\r\n\r\n**Consequence**: A private, written warning from community leaders, providing\r\nclarity around the nature of the violation and an explanation of why the\r\nbehavior was inappropriate. A public apology may be requested.\r\n\r\n### 2. Warning\r\n\r\n**Community Impact**: A violation through a single incident or series of\r\nactions.\r\n\r\n**Consequence**: A warning with consequences for continued behavior. No\r\ninteraction with the people involved, including unsolicited interaction with\r\nthose enforcing the Code of Conduct, for a specified period of time. This\r\nincludes avoiding interactions in community spaces as well as external channels\r\nlike social media. Violating these terms may lead to a temporary or permanent\r\nban.\r\n\r\n### 3. Temporary Ban\r\n\r\n**Community Impact**: A serious violation of community standards, including\r\nsustained inappropriate behavior.\r\n\r\n**Consequence**: A temporary ban from any sort of interaction or public\r\ncommunication with the community for a specified period of time. No public or\r\nprivate interaction with the people involved, including unsolicited interaction\r\nwith those enforcing the Code of Conduct, is allowed during this period.\r\nViolating these terms may lead to a permanent ban.\r\n\r\n### 4. Permanent Ban\r\n\r\n**Community Impact**: Demonstrating a pattern of violation of community\r\nstandards, including sustained inappropriate behavior, harassment of an\r\nindividual, or aggression toward or disparagement of classes of individuals.\r\n\r\n**Consequence**: A permanent ban from any sort of public interaction within the\r\ncommunity.\r\n\r\n## Attribution\r\n\r\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\r\nversion 2.1, available at\r\nhttps://www.contributor-covenant.org/version/2/1/code_of_conduct.html - v2.1\r\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contribution Guidelines\n\nFirst of all, thank you for your interest in contributing to this project !\n\n* Before submitting a [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) (PR), please make sure that you have had a discussion with the project-leads\n* If a [relevant issue](https://github.com/karatelabs/karate/issues) already exists, have a discussion within that issue (by commenting) - and make sure that the project-leads are okay with your approach\n* If no relevant issue exists, please [open a new issue](https://github.com/karatelabs/karate/issues) to start a discussion\n* Please proceed with a PR only *after* the project admins or owners are okay with your approach. We don't want you to spend time and effort working on something - only to find out later that it was not aligned with how the project developers were thinking about it !\n* You can refer to the [Developer Guide](https://github.com/karatelabs/karate/wiki/Developer-Guide) for information on how to build and test the project on your local / developer machine\n* **IMPORTANT**: Submit your PR(s) against the [`develop`](https://github.com/karatelabs/karate/tree/develop) branch of this repository\n\nIf you are interested in project road-map items that you can potentially contribute to, please refer to the [Project Board](https://github.com/karatelabs/karate/projects/3).\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [karatelabs]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "IMPORTANT: If you have a general question please use Stack Overflow instead where Karate has a dedicated \"tag\": https://stackoverflow.com/questions/tagged/karate\n\nIf you are sure you have found a bug, please make sure you follow the instructions here: https://github.com/karatelabs/karate/wiki/How-to-Submit-an-Issue\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "### Description\n\nThanks for contributing this Pull Request. Make sure that you submit this Pull Request against the `develop` branch of this repository, add a brief description, and tag the relevant issue(s) and PR(s) below.\n\n- Relevant Issues : (compulsory)\n- Relevant PRs : (optional)\n- Type of change :\n  - [ ] New feature\n  - [ ] Bug fix for existing feature\n  - [ ] Code quality improvement\n  - [ ] Addition or Improvement of tests\n  - [ ] Addition or Improvement of documentation\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  # Dependencies listed in .github/workflows/*.yml\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ \"develop\", \"master\" ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ \"develop\" ]\n#  schedule:\n#    - cron: '31 14 * * 4'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'java' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Use only 'java' to analyze code written in Java, Kotlin or both\n        # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both\n        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: set up jdk 17\n        uses: actions/setup-java@v5\n        with:\n          distribution: adopt\n          java-version: 17\n\n      # Initializes the CodeQL tools for scanning.\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@v3\n        with:\n          languages: ${{ matrix.language }}\n          # If you wish to specify custom queries, you can do so here or in a config file.\n          # By default, queries listed here will override any specified in a config file.\n          # Prefix the list here with \"+\" to use these queries and those in the config file.\n\n          # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs\n          # queries: security-extended,security-and-quality\n\n\n      # Autobuild attempts to build any compiled languages  (C/C++, C#, Go, or Java).\n      # If this step fails, then you should remove it and run the build manually (see below)\n      - name: Autobuild\n        uses: github/codeql-action/autobuild@v3\n\n      # ℹ️ Command-line programs to run using the OS shell.\n      # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n\n      #   If the Autobuild fails above, remove it and uncomment the following three lines.\n      #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.\n\n      # - run: |\n      #   echo \"Run, Build Application using script\"\n      #   ./location_of_script_within_repo/buildscript.sh\n\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@v3\n        with:\n          category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".github/workflows/delete-workflow-runs.yml",
    "content": "name: delete-workflow-runs\non:\n  workflow_dispatch:\n    inputs:\n      days:\n        description: 'no. of days'\n        required: true\n        default: 30\n      minimum_runs:\n        description: 'minimum runs to keep (per workflow)'\n        required: true\n        default: 6\n        \n#  push:\n#    branches: [ develop ]        \n\njobs:\n  del_runs:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Delete workflow runs\n        uses: Mattraks/delete-workflow-runs@v2\n        with:\n          token: ${{ github.token }}\n          repository: ${{ github.repository }}\n          retain_days: ${{ github.event.inputs.days }}\n          # retain_days: 30\n          keep_minimum_runs: ${{ github.event.inputs.minimum_runs }}\n          # keep_minimum_runs: 6\n"
  },
  {
    "path": ".github/workflows/jdk-compat.yml",
    "content": "name: jdk-compat\n\non:\n  push:\n    branches: [ develop ]\n\njobs:\n  build:\n    timeout-minutes: 10\n    runs-on: ubuntu-latest\n    steps:\n    - name: git checkout\n      uses: actions/checkout@v6\n    - name: get latest jdk ga\n      uses: oracle-actions/setup-java@v1\n      with:\n        website: jdk.java.net\n        release: 25\n    - name: cache maven packages\n      uses: actions/cache@v4\n      with:\n        path: ~/.m2/repository\n        key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}\n        restore-keys: ${{ runner.os }}-maven-\n    - name: build with maven\n      run: mvn -B clean install -P pre-release -Djavacpp.platform=linux-x86_64\n"
  },
  {
    "path": ".github/workflows/maven-build.yml",
    "content": "name: maven-build\n\non:\n  push:\n    branches: [ master, develop ]\n  pull_request:\n    branches: [ master, develop ]\n\njobs:\n  build:\n    timeout-minutes: 20\n    runs-on: ubuntu-latest\n    steps:\n    - name: git checkout\n      uses: actions/checkout@v6\n    - name: set up jdk 17\n      uses: actions/setup-java@v5\n      with:\n        distribution: adopt\n        java-version: 17\n    - name: cache maven packages\n      uses: actions/cache@v4\n      with:\n        path: ~/.m2/repository\n        key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}\n        restore-keys: ${{ runner.os }}-maven-\n    - name: build with maven\n      run: mvn -B clean install -P pre-release -Djavacpp.platform=linux-x86_64\n    - name: build and test with docker\n      run: ./build-docker.sh\n    - name: upload workspace if build fails\n      # if: ${{ failure() }}\n      if: ${{ false }}\n      uses: actions/upload-artifact@v5\n      with:\n        name: build-results\n        path: .\n        retention-days: 5        \n"
  },
  {
    "path": ".github/workflows/maven-release.yml",
    "content": "name: maven-release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: maven and docker release version\n        required: true\n        default: 'X.X.X.RCX'\n      docker:\n        description: push to docker\n        required: true\n        type: choice\n        options:\n        - 'enabled'\n        - 'disabled'\n      maven:\n        description: push to maven central\n        required: true\n        type: choice\n        options:\n          - 'enabled'\n          - 'disabled'\n\njobs:\n  build:\n    timeout-minutes: 20\n    runs-on: ubuntu-latest\n    steps:\n    - name: git checkout\n      uses: actions/checkout@v6\n    - name: set up jdk 17\n      uses: actions/setup-java@v5\n      with:\n        distribution: adopt\n        java-version: 17\n        server-id: central\n        server-username: CENTRAL_TOKEN_USERNAME\n        server-password: CENTRAL_TOKEN_PASSWORD\n        gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }}\n        gpg-passphrase: MAVEN_GPG_PASSPHRASE\n    - name: set maven version\n      run: |\n        mvn versions:set versions:commit -B -ntp -DnewVersion=${{ github.event.inputs.version }}\n    - name: docker build\n      if: ( github.event.inputs.docker == 'enabled' )\n      run: |\n        mvn clean install -B -ntp -DskipTests -P pre-release\n        ./build-docker.sh\n    - name: docker login\n      if: ( github.event.inputs.docker == 'enabled' )\n      uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef\n      with:\n        username: ${{ secrets.DOCKER_USERNAME }}\n        password: ${{ secrets.DOCKER_PASSWORD }}\n    - name: docker push\n      if: ( github.event.inputs.docker == 'enabled' )\n      run: |\n        docker tag karate-chrome karatelabs/karate-chrome:${{ github.event.inputs.version }}\n        docker tag karate-chrome karatelabs/karate-chrome:latest\n        docker push karatelabs/karate-chrome:${{ github.event.inputs.version }}\n        docker push karatelabs/karate-chrome:latest\n    - name: maven deploy to central\n      if: ( github.event.inputs.maven == 'enabled' )\n      env:\n        CENTRAL_TOKEN_USERNAME: ${{ secrets.OSSRH_TOKEN_USER }}\n        CENTRAL_TOKEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}\n        MAVEN_GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }}\n      run: |\n        mvn -B -ntp deploy -DskipTests -P pre-release,release -pl \"!karate-archetype,!karate-demo,!karate-e2e-tests\"\n    - name: maven build binaries\n      run: |\n        mvn package -DskipTests -P fatjar -f karate-core/pom.xml\n        mvn package -DskipTests -P fatjar -f karate-robot/pom.xml\n    - name: upload binaries\n      uses: actions/upload-artifact@v5\n      with: \n        name: karate-release-${{ github.event.inputs.version }}\n        retention-days: 5 \n        path: |\n          karate-core/target/*.jar\n          karate-robot/target/*.jar  \n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\ntarget/\n.idea\n.project\n.settings\n.classpath\n.vscode\n.java-version\n*.iml\n*.tokens\nbuild/\nbin/\n.gradle\ngradle\ngradlew\ngradlew.*\ndependency-reduced-pom.xml\nexamples/zip-release/*.jar\nkarate-demo/activemq-data/\nkarate-demo/*.pem\nkarate-demo/*.jks\nkarate-demo/*.der\nkarate-core/gen\nkarate-core/*.pem\nkarate-core/*.jks\nkarate-core/*.der\nkarate-robot/tessdata\nkarate-junit4/src/test/java/com/intuit/karate/junit4/dev\nkarate-robot/src/test/java/robot/dev\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright 2022 Karate Labs Inc.\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."
  },
  {
    "path": "README.md",
    "content": "# Karate\n\n**The open-source tool that combines API testing, mocks, performance testing, and UI automation into a single, unified framework.**\n\n[![Maven Central](https://img.shields.io/maven-central/v/io.karatelabs/karate-core.svg)](https://central.sonatype.com/namespace/io.karatelabs)\n[![Build Status](https://github.com/karatelabs/karate/actions/workflows/maven-build.yml/badge.svg?branch=develop)](https://github.com/karatelabs/karate/actions?query=workflow%3Amaven-build)\n[![GitHub release](https://img.shields.io/github/release/karatelabs/karate.svg)](https://github.com/karatelabs/karate/releases)\n[![Twitter Follow](https://img.shields.io/twitter/follow/getkarate?style=social)](https://twitter.com/getkarate)\n[![GitHub Stars](https://img.shields.io/github/stars/karatelabs/karate?style=social)](https://github.com/karatelabs/karate/stargazers)\n\n<table>\n<tr>\n<td>\n\n<h2><strong>📖 Documentation:</strong>\n<a href=\"https://docs.karatelabs.io\">docs.karatelabs.io</a></h2>\n\n<details>\n<summary>Looking for the old README?</summary>\n\nThe previous README monolith is preserved at:\n\n**[github.com/karatelabs/karate/blob/v1.5.2](https://github.com/karatelabs/karate/blob/v1.5.2)**\n\nAnchor links (e.g. `#syntax-guide`, `#configuration`) can be appended to navigate directly to specific sections.\n</details>\n\n</td>\n<td width=\"30%\">\n<a href=\"https://karatelabs.io\">\n<picture>\n<source media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/karatelabs/.github/main/profile/karate-labs-wide-black.png\">\n<img src=\"https://raw.githubusercontent.com/karatelabs/.github/main/profile/karate-labs-wide.png\" />\n</picture>\n</a>\n</td>\n</tr>\n</table>\n\n<div> \n  <a href=\"https://github.com/karatelabs/karate/wiki/Support\">\n    <img src=\"https://img.shields.io/badge/Support-Resources-red.svg\"/>\n  </a>\n</div>\n\n\n\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported          |\n| ------- | ------------------ |\n| [latest (non-RC)](https://github.com/karatelabs/karate/releases) | :white_check_mark: |\n| older versions | :x:       |\n\n## Reporting a Vulnerability\n\nPrivate vulnerability reporting is enabled, please [follow these instructions](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability).\n"
  },
  {
    "path": "_config.yml",
    "content": "# Site settings\ntitle: Karate\ndescription: Test Automation Made Simple.\ntwitter_username: getkarate\ngithub_username: ptrthomas\n\n# Theme\nremote_theme: pmarsceill/just-the-docs\n\n# Theme settings\nsearch_enabled: true\naux_links:\n    \"Karate on Github\":\n        - \"//github.com/karatelabs/karate\"\n    \"Karate Labs\":\n        - \"//karatelabs.io\"        \n"
  },
  {
    "path": "_includes/nav.html",
    "content": "<nav>\n  <ul class=\"navigation-list\">\n    {% assign pages_list = site.html_pages | sort:\"nav_order\" %}\n    {% for node in pages_list %}\n      {% unless node.nav_exclude %}\n        {% if node.parent == nil %}\n          <li class=\"navigation-list-item{% if page.url == node.url or page.parent == node.title or page.grand_parent == node.title %} active{% endif %}\">\n            {% if page.parent == node.title or page.grand_parent == node.title %}\n              {% assign first_level_url = node.url | absolute_url %}\n            {% endif %}\n            <a href=\"{{ node.url | absolute_url }}\" class=\"navigation-list-link{% if page.url == node.url %} active{% endif %}\">{{ node.title }}</a>\n            {% if page.url == node.url %}\n              {% include toc.html html=content sanitize=true h_max=2 %}\n            {% endif %}\n            {% if node.has_children %}\n              {% assign children_list = site.html_pages | sort:\"nav_order\" %}\n              <ul class=\"navigation-list-child-list \">\n                {% for child in children_list %}\n                  {% if child.parent == node.title %}\n                    <li class=\"navigation-list-item {% if page.url == child.url or page.parent == child.title %} active{% endif %}\">\n                      {% if page.url == child.url or page.parent == child.title %}\n                        {% assign second_level_url = child.url | absolute_url %}\n                      {% endif %}\n                      <a href=\"{{ child.url | absolute_url }}\" class=\"navigation-list-link{% if page.url == child.url %} active{% endif %}\">{{ child.title }}</a>\n                      {% if child.has_children %}\n                        {% assign grand_children_list = site.html_pages | sort:\"nav_order\" %}\n                        <ul class=\"navigation-list-child-list\">\n                          {% for grand_child in grand_children_list %}\n                            {% if grand_child.parent == child.title %}\n                              <li class=\"navigation-list-item {% if page.url == grand_child.url %} active{% endif %}\">\n                                <a href=\"{{ grand_child.url | absolute_url }}\" class=\"navigation-list-link{% if page.url == grand_child.url %} active{% endif %}\">{{ grand_child.title }}</a>\n                              </li>\n                            {% endif %}\n                          {% endfor %}\n                        </ul>\n                      {% endif %}\n                    </li>\n                  {% endif %}\n                {% endfor %}\n              </ul>\n            {% endif %}\n          </li>\n        {% endif %}\n      {% endunless %}\n    {% endfor %}\n  </ul>\n</nav>\n\n"
  },
  {
    "path": "_includes/toc.html",
    "content": "{% capture tocWorkspace %}\n    {% comment %}\n        Version 1.0.6\n          https://github.com/allejo/jekyll-toc\n\n        \"...like all things liquid - where there's a will, and ~36 hours to spare, there's usually a/some way\" ~jaybe\n\n        Usage:\n            {% include toc.html html=content sanitize=true class=\"inline_toc\" id=\"my_toc\" h_min=2 h_max=3 %}\n\n        Parameters:\n            * html         (string) - the HTML of compiled markdown generated by kramdown in Jekyll\n\n        Optional Parameters:\n            * sanitize     (bool)   : false  - when set to true, the headers will be stripped of any HTML in the TOC\n            * class        (string) :   ''   - a CSS class assigned to the TOC\n            * id           (string) :   ''   - an ID to assigned to the TOC\n            * h_min        (int)    :   1    - the minimum TOC header level to use; any header lower than this value will be ignored\n            * h_max        (int)    :   6    - the maximum TOC header level to use; any header greater than this value will be ignored\n            * ordered      (bool)   : false  - when set to true, an ordered list will be outputted instead of an unordered list\n            * item_class   (string) :   ''   - add custom class(es) for each list item; has support for '%level%' placeholder, which is the current heading level\n            * baseurl      (string) :   ''   - add a base url to the TOC links for when your TOC is on another page than the actual content\n            * anchor_class (string) :   ''   - add custom class(es) for each anchor element\n\n        Output:\n            An ordered or unordered list representing the table of contents of a markdown block. This snippet will only\n            generate the table of contents and will NOT output the markdown given to it\n    {% endcomment %}\n\n    {% capture my_toc %}{% endcapture %}\n    {% assign orderedList = include.ordered | default: false %}\n    {% assign minHeader = include.h_min | default: 1 %}\n    {% assign maxHeader = include.h_max | default: 6 %}\n    {% assign nodes = include.html | split: '<h' %}\n    {% assign firstHeader = true %}\n\n    {% capture listModifier %}{% if orderedList %}1.{% else %}-{% endif %}{% endcapture %}\n\n    {% for node in nodes %}\n        {% if node == \"\" %}\n            {% continue %}\n        {% endif %}\n\n        {% assign headerLevel = node | replace: '\"', '' | slice: 0, 1 | times: 1 %}\n\n        {% if headerLevel < minHeader or headerLevel > maxHeader %}\n            {% continue %}\n        {% endif %}\n\n        {% if firstHeader %}\n            {% assign firstHeader = false %}\n            {% assign minHeader = headerLevel %}\n        {% endif %}\n\n        {% assign indentAmount = headerLevel | minus: minHeader | add: 1 %}\n        {% assign _workspace = node | split: '</h' %}\n\n        {% assign _idWorkspace = _workspace[0] | split: 'id=\"' %}\n        {% assign _idWorkspace = _idWorkspace[1] | split: '\"' %}\n        {% assign html_id = _idWorkspace[0] %}\n\n        {% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %}\n        {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %}\n\n        {% assign space = '' %}\n        {% for i in (1..indentAmount) %}\n            {% assign space = space | prepend: '    ' %}\n        {% endfor %}\n\n        {% unless include.item_class == blank %}\n            {% capture listItemClass %}{:.{{ include.item_class | replace: '%level%', headerLevel }}}{% endcapture %}\n        {% endunless %}\n\n        {% capture my_toc %}{{ my_toc }}\n{{ space }}{{ listModifier }} {{ listItemClass }} [{% if include.sanitize %}{{ header | strip_html }}{% else %}{{ header }}{% endif %}]({% if include.baseurl %}{{ include.baseurl }}{% endif %}#{{ html_id }}){% if include.anchor_class %}{:.{{ include.anchor_class }}}{% endif %}{% endcapture %}\n    {% endfor %}\n\n    {% if include.class %}\n        {% capture my_toc %}{:.{{ include.class }}}\n{{ my_toc | lstrip }}{% endcapture %}\n    {% endif %}\n\n    {% if include.id %}\n        {% capture my_toc %}{: #{{ include.id }}}\n{{ my_toc | lstrip }}{% endcapture %}\n    {% endif %}\n{% endcapture %}{% assign tocWorkspace = '' %}{{ my_toc | markdownify | strip }}\n"
  },
  {
    "path": "build-docker.sh",
    "content": "#!/bin/bash\n#set -x -e\n\n# assume that karate jars are installed in maven local repo\n# mvn clean install -DskipTests -P pre-release\n\nKARATE_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)\n\n# run e2e test that depends on karate-gatling\nmvn versions:set versions:commit -B -ntp -DnewVersion=${KARATE_VERSION} -f examples/gatling/pom.xml\nmvn clean test -B -ntp -f examples/gatling/pom.xml\n\n# copy only karate jars to a place where the docker image build can add from\nKARATE_REPO=karate-docker/karate-chrome/target/repository/io/karatelabs\nmkdir -p \"${KARATE_REPO}\"\ncp -r ~/.m2/repository/io/karatelabs \"${KARATE_REPO}\"\n\n# create / copy the karate fatjar so that the docker image build can add it\nmvn package -B -ntp -P fatjar -DskipTests -f karate-core/pom.xml\ncp \"karate-core/target/karate-${KARATE_VERSION}.jar\" karate-docker/karate-chrome/target/karate.jar\n\n# setup multiplatform build (ignore error if builder doesn't exist)\ndocker buildx rm multiplatform-builder || true\ndocker buildx create --name multiplatform-builder\ndocker buildx use multiplatform-builder\n\n# build karate-chrome docker image that includes karate fatjar + maven jars for convenience\n# Only linux/amd64 is supported because google-chrome-stable is amd64-only\ndocker buildx build \\\n  --platform linux/amd64 \\\n  --cache-from=type=local,src=./target/docker \\\n  --cache-to=type=local,dest=./target/docker \\\n  -t karatelabs/karate-chrome:${{ github.event.inputs.version }} \\\n  -t karatelabs/karate-chrome:latest \\\n  karate-docker/karate-chrome\n\n# Decide platform for local \"load\" build\nPLATFORM_FLAG=\"\"\nif [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n  # On Mac (including M1), force linux/amd64 so Chrome is available\n  PLATFORM_FLAG=\"--platform=linux/amd64\"\nfi\n\n# Build local image (for running tests) and load into Docker\ndocker buildx build \\\n  --load \\\n  ${PLATFORM_FLAG} \\\n  --cache-from=type=local,src=./target/docker \\\n  -t karate-chrome \\\n  karate-docker/karate-chrome\n\n# just in case a previous run had hung (likely only in local dev)\ndocker stop karate || true\n\n# note that this command is run as a background process\ndocker run --name karate --rm --cap-add=SYS_ADMIN \\\n  -v \"$PWD\":/karate \\\n  -v \"$HOME/.m2\":/root/.m2 \\\n  karate-chrome &\n\n# just ensure that the docker container named \"karate\" exists after the above command\n# it does not have to have completed startup, the command / karate test below will wait\nsleep 5\n\n# run a test to check a particular jar packaging issue\ndocker exec -w /karate karate mvn test -B -ntp -f karate-e2e-tests/pom.xml -Dtest=regex.RegexRunner\n\n# run tests against chrome\ndocker exec -w /karate karate mvn test -B -ntp -f karate-e2e-tests/pom.xml -Dtest=driver.DockerRunner\n\ndocker stop karate\nwait\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Karate Examples\n\nKarate Examples are being moved to this url: [Examples](https://github.com/karatelabs/karate-examples/blob/main/README.md). <-- please start here.\n\n> The code in this folder are designed to be stand-alone Maven projects that you can use as a reference or as a starting point."
  },
  {
    "path": "examples/consumer-driven-contracts/.gitignore",
    "content": ".DS_Store\ntarget/\n\n"
  },
  {
    "path": "examples/consumer-driven-contracts/README.md",
    "content": "# Karate Consumer Driven Contracts Demo\n\n## References\nThis is a simplified version of the [example in the Karate test-doubles documentation](https://github.com/karatelabs/karate/tree/master/karate-netty#consumer-provider-example) - with JMS / queues removed and simplified to be a stand-alone maven project.\n\nThese articles are recommended reading:\n* [API Contract Testing - Visual Guide](https://www.linkedin.com/pulse/api-contract-testing-visual-guide-peter-thomas/)\n* [Karate vs Pact](https://stackoverflow.com/a/64218355/143475)\n\n## Instructions\n* clone the project\n* `mvn clean test`\n\n## Main Artifacts\nYou can click on the links to view the source-code.\n| File | Description | Comment |\n| ---- | ----------- | ------- |\n| [PaymentService.java](payment-producer/src/main/java/payment/producer/PaymentService.java) | Producer | A very simple [Spring Boot](https://spring.io/projects/spring-boot) app / REST service |\n| [payment-contract.feature](payment-producer/src/test/java/payment/producer/contract/payment-contract.feature) | Contract + Functional Test | [Karate](https://github.com/karatelabs/karate) API test |\n| [PaymentContractTest.java](payment-producer/src/test/java/payment/producer/contract/PaymentContractTest.java) | Producer Integration Test | JUnit runner for the above |\n| [payment-mock.feature](payment-producer/src/test/java/payment/producer/mock/payment-mock.feature) | Mock / Stub | [Karate mock](https://github.com/karatelabs/karate/tree/master/karate-netty) that *perfectly* simulates the Producer ! | \n| [PaymentContractAgainstMockTest.java](payment-producer/src/test/java/payment/producer/mock/PaymentContractAgainstMockTest.java) | Verify that the Mock is as per Contract | JUnit runner that points `payment-contract.feature` --> `payment-mock.feature` |\n| [Consumer.java](payment-consumer/src/main/java/payment/consumer/Consumer.java) | Consumer | A simple Java app that calls the Producer to do some work |\n| [ConsumerIntegrationTest.java](payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationTest.java) | Consumer Integration Test | A JUnit *full* integration test, using the *real* Consumer and Producer |\n| [ConsumerIntegrationAgainstMockTest.java](payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationAgainstMockTest.java) | Consumer Integration Test but using the Mock | Like the above but using the mock Producer |\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-consumer/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>io.karatelabs.examples</groupId>\n        <artifactId>examples-cdc</artifactId>\n        <version>1.0-SNAPSHOT</version>\n    </parent>\n    \n    <artifactId>examples-cdc-consumer</artifactId>\n    <packaging>jar</packaging>      \n\n    <dependencies>\n        <dependency>\n            <groupId>io.karatelabs.examples</groupId>\n            <artifactId>examples-cdc-producer</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>commons-io</groupId>\n            <artifactId>commons-io</artifactId>\n            <version>2.7</version>\n        </dependency>                                                        \t\t\n    </dependencies>\n    \n</project>\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-consumer/src/main/java/payment/consumer/Consumer.java",
    "content": "package payment.consumer;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport org.apache.commons.io.IOUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport payment.producer.Payment;\n\n/**\n *\n * @author pthomas3\n */\npublic class Consumer {\n\n    private static final Logger logger = LoggerFactory.getLogger(Consumer.class);\n\n    private final String paymentServiceUrl;\n    private final ObjectMapper mapper = new ObjectMapper();\n\n    public Consumer(String paymentServiceUrl) {\n        this.paymentServiceUrl = paymentServiceUrl;\n    }\n\n    private HttpURLConnection getConnection(String path) throws Exception {\n        URL url = new URL(paymentServiceUrl + path);\n        return (HttpURLConnection) url.openConnection();\n    }\n\n    public Payment create(Payment payment) {\n        try {\n            HttpURLConnection con = getConnection(\"/payments\");\n            con.setRequestMethod(\"POST\");\n            con.setDoOutput(true);\n            con.setRequestProperty(\"Content-Type\", \"application/json\");\n            String json = mapper.writeValueAsString(payment);\n            IOUtils.write(json, con.getOutputStream(), \"utf-8\");\n            int status = con.getResponseCode();\n            if (status != 200) {\n                throw new RuntimeException(\"status code was \" + status);\n            }\n            String content = IOUtils.toString(con.getInputStream(), StandardCharsets.UTF_8);\n            return mapper.readValue(content, Payment.class);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-consumer/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit.karate\" level=\"DEBUG\"/>\n    <logger name=\"com.mycompany\" level=\"DEBUG\"/>\n    <logger name=\"payment\" level=\"DEBUG\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationAgainstMockTest.java",
    "content": "package payment.consumer;\n\nimport com.intuit.karate.core.MockServer;\nimport java.io.File;\nimport org.junit.jupiter.api.AfterAll;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport payment.producer.Payment;\n\n/**\n *\n * @author pthomas3\n */\nclass ConsumerIntegrationAgainstMockTest {\n\n    static MockServer server;\n    static Consumer consumer;\n\n    @BeforeAll\n    static void beforeAll() {\n        File file = new File(\"../payment-producer/src/test/java/payment/producer/mock/payment-mock.feature\");\n        server = MockServer.feature(file).http(0).build();\n        String paymentServiceUrl = \"http://localhost:\" + server.getPort();\n        consumer = new Consumer(paymentServiceUrl);\n    }\n\n    @Test\n    void testPaymentCreate() throws Exception {\n        Payment payment = new Payment();\n        payment.setAmount(5.67);\n        payment.setDescription(\"test one\");\n        payment = consumer.create(payment);\n        assertTrue(payment.getId() > 0);\n        assertEquals(payment.getAmount(), 5.67, 0);\n        assertEquals(payment.getDescription(), \"test one\");\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n    }\n\n}\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationTest.java",
    "content": "package payment.consumer;\n\nimport org.junit.jupiter.api.AfterAll;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport payment.producer.Payment;\nimport payment.producer.PaymentService;\n\n/**\n *\n * @author pthomas3\n */\nclass ConsumerIntegrationTest {\n\n    static ConfigurableApplicationContext context;\n    static Consumer consumer;\n\n    @BeforeAll\n    static void beforeAll() {\n        context = PaymentService.start(0);\n        String paymentServiceUrl = \"http://localhost:\" + PaymentService.getPort(context);\n        consumer = new Consumer(paymentServiceUrl);\n    }\n\n    @Test\n    void testPaymentCreate() throws Exception {\n        Payment payment = new Payment();\n        payment.setAmount(5.67);\n        payment.setDescription(\"test one\");\n        payment = consumer.create(payment);\n        assertTrue(payment.getId() > 0);\n        assertEquals(payment.getAmount(), 5.67, 0);\n        assertEquals(payment.getDescription(), \"test one\");\n    }\n\n    @AfterAll\n    static void afterAll() {\n        PaymentService.stop(context);\n    }\n\n}\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-producer/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>io.karatelabs.examples</groupId>\n        <artifactId>examples-cdc</artifactId>\n        <version>1.0-SNAPSHOT</version>\n    </parent>\n    \n    <artifactId>examples-cdc-producer</artifactId>\n    <packaging>jar</packaging>\n\n    <dependencies>        \n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-dependencies</artifactId>\n            <version>${spring.boot.version}</version>\n            <type>pom</type>\n            <scope>import</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n            <version>${spring.boot.version}</version>\n        </dependency>                                                                            \t\t\n    </dependencies>\n    \n</project>\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/Payment.java",
    "content": "package payment.producer;\n\n/**\n *\n * @author pthomas3\n */\npublic class Payment {\n    \n    private int id;\n    private double amount;\n    private String description;\n\n    public int getId() {\n        return id;\n    }\n\n    public void setId(int id) {\n        this.id = id;\n    }\n\n    public double getAmount() {\n        return amount;\n    }\n\n    public void setAmount(double amount) {\n        this.amount = amount;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public void setDescription(String description) {\n        this.description = description;\n    }        \n    \n}\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/PaymentService.java",
    "content": "package payment.producer;\n\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.EnableAutoConfiguration;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.bind.annotation.DeleteMapping;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.PutMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ResponseStatusException;\n\n/**\n *\n * @author pthomas3\n */\n@Configuration\n@EnableAutoConfiguration\npublic class PaymentService {\n\n    @RestController\n    @RequestMapping(\"/payments\")\n    class PaymentController {\n\n        private final AtomicInteger counter = new AtomicInteger();\n        private final Map<Integer, Payment> payments = new ConcurrentHashMap();\n\n        @PostMapping\n        public Payment create(@RequestBody Payment payment) {\n            int id = counter.incrementAndGet();\n            payment.setId(id);\n            payments.put(id, payment);\n            return payment;\n        }\n\n        @PutMapping(\"/{id:.+}\")\n        public Payment update(@PathVariable(\"id\") int id, @RequestBody Payment payment) {\n            payments.put(id, payment);\n            return payment;\n        }\n\n        @GetMapping\n        public Collection<Payment> list() {\n            return payments.values();\n        }\n\n        @GetMapping(\"/{id:.+}\")\n        public Payment get(@PathVariable(\"id\") int id) {\n            Payment payment = payments.get(id);\n            if (payment == null) {\n                throw new ResponseStatusException(HttpStatus.NOT_FOUND);\n            }\n            return payment;\n        }\n\n        @DeleteMapping(\"/{id:.+}\")\n        public void delete(@PathVariable(\"id\") int id) {\n            Payment payment = payments.remove(id);\n            if (payment == null) {\n                throw new RuntimeException(\"payment not found, id: \" + id);\n            }\n        }\n\n    }\n\n    public static ConfigurableApplicationContext start(int port) {\n        return SpringApplication.run(PaymentService.class, new String[]{\"--server.port=\" + port});\n    }\n\n    public static void stop(ConfigurableApplicationContext context) {\n        SpringApplication.exit(context, () -> 0);\n    }\n\n    public static int getPort(ConfigurableApplicationContext context) {\n        ServerStartedInitializingBean ss = context.getBean(ServerStartedInitializingBean.class);\n        return ss.getLocalPort();\n    }\n\n    @Bean\n    public ServerStartedInitializingBean getInitializingBean() {\n        return new ServerStartedInitializingBean();\n    }\n\n    public static void main(String[] args) {\n        start(8090);\n    }\n\n}\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/ServerStartedInitializingBean.java",
    "content": "package payment.producer;\n\nimport java.util.Arrays;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.boot.web.context.WebServerInitializedEvent;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.stereotype.Component;\n\n/**\n *\n * @author pthomas3\n */\n@Component\npublic class ServerStartedInitializingBean implements ApplicationRunner, ApplicationListener<WebServerInitializedEvent> {\n\n    private static final Logger logger = LoggerFactory.getLogger(ServerStartedInitializingBean.class);\n\n    private int localPort;\n\n    public int getLocalPort() {\n        return localPort;\n    }\n\n    @Override\n    public void run(ApplicationArguments aa) throws Exception {\n        logger.info(\"server started with args: {}\", Arrays.toString(aa.getSourceArgs()));\n    }\n\n    @Override\n    public void onApplicationEvent(WebServerInitializedEvent e) {\n        localPort = e.getWebServer().getPort();\n        logger.info(\"after runtime init, local server port: {}\", localPort);\n    }\n\n}\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-producer/src/test/java/karate-config.js",
    "content": "function() {\n  return { paymentServiceUrl: karate.properties['payment.service.url'] }\n}\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-producer/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit.karate\" level=\"DEBUG\"/>\n    <logger name=\"com.mycompany\" level=\"DEBUG\"/>\n    <logger name=\"payment\" level=\"DEBUG\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/contract/PaymentContractTest.java",
    "content": "package payment.producer.contract;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport org.junit.jupiter.api.AfterAll;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport payment.producer.PaymentService;\n\n/**\n *\n * @author pthomas3\n */\nclass PaymentContractTest {\n\n    static ConfigurableApplicationContext context;\n\n    @BeforeAll\n    static void beforeAll() {\n        context = PaymentService.start(0);\n    }\n\n    @Test\n    void testReal() {\n        String paymentServiceUrl = \"http://localhost:\" + PaymentService.getPort(context);\n        Results results = Runner.path(\"classpath:payment/producer/contract/payment-contract.feature\")\n                .systemProperty(\"payment.service.url\", paymentServiceUrl)\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n    @AfterAll\n    static void afterAll() {\n        PaymentService.stop(context);\n    }\n\n}\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/contract/payment-contract.feature",
    "content": "Feature: payment service contract test\n\nBackground:\n* url paymentServiceUrl + '/payments'\n\nScenario: create, get, update, list and delete payments\n    Given request { amount: 5.67, description: 'test one' }\n    When method post\n    Then status 200\n    And match response == { id: '#number', amount: 5.67, description: 'test one' }\n    And def id = response.id\n\n    Given path id\n    When method get\n    Then status 200\n    And match response == { id: '#(id)', amount: 5.67, description: 'test one' }\n\n    Given path id\n    And request { id: '#(id)', amount: 5.67, description: 'test two' }\n    When method put\n    Then status 200\n    And match response == { id: '#(id)', amount: 5.67, description: 'test two' }\n\n    When method get\n    Then status 200\n    And match response contains { id: '#(id)', amount: 5.67, description: 'test two' }\n\n    Given path id\n    When method delete\n    Then status 200\n\n    When method get\n    Then status 200\n    And match response !contains { id: '#(id)', amount: '#number', description: '#string' }\n    "
  },
  {
    "path": "examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/mock/PaymentContractAgainstMockTest.java",
    "content": "package payment.producer.mock;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.MockServer;\nimport org.junit.jupiter.api.AfterAll;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass PaymentContractAgainstMockTest {\n\n    static MockServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        server = MockServer.feature(\"classpath:payment/producer/mock/payment-mock.feature\").http(0).build();\n    }\n\n    @Test\n    void testMock() {\n        String paymentServiceUrl = \"http://localhost:\" + server.getPort();\n        Results results = Runner.path(\"classpath:payment/producer/contract/payment-contract.feature\")\n                .systemProperty(\"payment.service.url\", paymentServiceUrl)\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n    }\n\n}\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/mock/payment-mock.feature",
    "content": "Feature: payment service mock\n\nBackground:\n* def id = 0\n* def payments = {}\n\nScenario: pathMatches('/payments') && methodIs('post')\n    * def payment = request\n    * def id = ~~(id + 1)\n    * payment.id = id\n    * payments[id + ''] = payment\n    * def response = payment \n\nScenario: pathMatches('/payments')\n    * def response = $payments.*\n\nScenario: pathMatches('/payments/{id}') && methodIs('put')\n    * payments[pathParams.id] = request\n    * def response = request\n\nScenario: pathMatches('/payments/{id}') && methodIs('delete')\n    * karate.remove('payments', pathParams.id)\n\nScenario: pathMatches('/payments/{id}')\n    * def response = payments[pathParams.id]\n"
  },
  {
    "path": "examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/mock/test.feature",
    "content": "Feature:\n\nScenario:\n* print 'hello'"
  },
  {
    "path": "examples/consumer-driven-contracts/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n \n    <groupId>io.karatelabs.examples</groupId>\n    <artifactId>examples-cdc</artifactId>\n    <version>1.0-SNAPSHOT</version>\n    <packaging>pom</packaging>\n    \n    <modules>\n        <module>payment-producer</module>\n        <module>payment-consumer</module>\n    </modules>    \n \n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <maven.compiler.version>3.8.1</maven.compiler.version>\n        <maven.surefire.version>2.22.2</maven.surefire.version>\n        <spring.boot.version>3.2.2</spring.boot.version>\n        <junit5.version>5.7.0</junit5.version>\n        <java.version>17</java.version>\n        <karate.version>1.5.2</karate.version>\n    </properties>    \n\n    <dependencies>\n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-core</artifactId>\n            <version>${karate.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <version>${junit5.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-engine</artifactId>\n            <version>${junit5.version}</version>\n            <scope>test</scope>\n        </dependency>                   \n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>        \n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven.compiler.version}</version>\n                <configuration>\n                    <encoding>UTF-8</encoding>\n                    <source>${java.version}</source>\n                    <target>${java.version}</target>\n                    <compilerArgument>-Werror</compilerArgument>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>${maven.surefire.version}</version>\n                <configuration>\n                    <argLine>-Dfile.encoding=UTF-8</argLine>\n                </configuration>\n            </plugin>                              \n        </plugins>        \n    </build>\n    \n</project>"
  },
  {
    "path": "examples/gatling/.gitignore",
    "content": ".DS_Store\ntarget/\n.idea/\n\n"
  },
  {
    "path": "examples/gatling/README.md",
    "content": "# karate-gatling-demo\ndemo sample project for karate [test-doubles](https://github.com/karatelabs/karate/tree/master/karate-netty) and [gatling integration](https://github.com/karatelabs/karate/tree/master/karate-gatling)\n\n> Another example which demos the use of the Java DSL instead of Scala can be found here: [karate-todo](https://github.com/karatelabs/karate-todo).\n\n## Instructions\n\n```\nmvn clean test\n```\n\nThe above works because the `gatling-maven-plugin` has been configured to run as part of the Maven `test` phase automatically in the [`pom.xml`](pom.xml).\n\nThe file location of the Gatling HTML report should appear towards the end of the console log. Copy and paste it into your browser address-bar.\n\nHere's a video of what to expect: https://twitter.com/ptrthomas/status/986463717465391104\n"
  },
  {
    "path": "examples/gatling/build.gradle",
    "content": "plugins {\n    id 'scala'\n}\n\next {\n    karateVersion = '1.5.2'\n}\n\ndependencies {\n    testImplementation \"io.karatelabs:karate-gatling:${karateVersion}\"\n}\n\nrepositories {\n    mavenCentral()\n    // mavenLocal()\n}\n\ntest {\n    systemProperty \"karate.options\", System.properties.getProperty(\"karate.options\")\n    systemProperty \"karate.env\", System.properties.getProperty(\"karate.env\")\n    outputs.upToDateWhen { false }\n}\n\nsourceSets {\n    test {\n        resources {\n            srcDir file('src/test/java')\n            exclude '**/*.java'\n            exclude '**/*.scala'\n        }\n        scala {\n            srcDirs = ['src/test/java']\n        }\n    }\n}\n\n// to run, type: \"gradle gatling\"\ntask gatlingRun(type: JavaExec) {\n    group = 'Web Tests'\n    description = 'Run Gatling Tests'\n    new File(\"${buildDir}/reports/gatling\").mkdirs()\n    classpath = sourceSets.test.runtimeClasspath\n    main = \"io.gatling.app.Gatling\"\n    args = [\n        // change this to suit your simulation entry-point\n        '-s', 'mock.CatsKarateSimulation',\n        '-rf', \"${buildDir}/reports/gatling\"\n    ]\n    systemProperties System.properties\n}\n"
  },
  {
    "path": "examples/gatling/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n \n    <groupId>io.karatelabs.examples</groupId>\n    <artifactId>examples-gatling</artifactId>\n    <version>1.0-SNAPSHOT</version>\n    <packaging>jar</packaging>       \n \n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <java.version>17</java.version>\n        <maven.compiler.version>3.6.0</maven.compiler.version>\n\t<!-- please change this when running locally -->\n        <karate.version>${project.version}</karate.version>\n        <gatling.plugin.version>4.3.4</gatling.plugin.version>\n    </properties>    \n\n    <dependencies>\n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-core</artifactId>\n            <version>${karate.version}</version>\n            <scope>test</scope>\n            <!-- next line is normally not required, but is here for testing karate release process -->\n            <classifier>all</classifier>            \n        </dependency>        \n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-gatling</artifactId>\n            <version>${karate.version}</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven.compiler.version}</version>\n                <configuration>\n                    <encoding>UTF-8</encoding>\n                    <source>${java.version}</source>\n                    <target>${java.version}</target>\n                    <compilerArgument>-Werror</compilerArgument>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>net.alchim31.maven</groupId>\n                <artifactId>scala-maven-plugin</artifactId>\n                <version>4.5.6</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>testCompile</goal>\n                        </goals>\n                        <configuration>\n                            <args>\n                                <arg>-Jbackend:GenBCode</arg>\n                                <arg>-Jdelambdafy:method</arg>\n                                <arg>-release:11</arg>\n                                <arg>-deprecation</arg>\n                                <arg>-feature</arg>\n                                <arg>-unchecked</arg>\n                                <arg>-language:implicitConversions</arg>\n                                <arg>-language:postfixOps</arg>\n                            </args>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>io.gatling</groupId>\n                <artifactId>gatling-maven-plugin</artifactId>\n                <version>${gatling.plugin.version}</version>\n                <configuration>\n                    <simulationsFolder>src/test/java</simulationsFolder>\n                    <includes>\n                        <include>mock.CatsKarateSimulation</include>\n                    </includes>\n                </configuration>\n                <executions>\n                    <execution>\n                        <phase>test</phase>\n                        <goals>\n                            <goal>test</goal>\n                        </goals>\n                    </execution>\n                </executions>                \n            </plugin>            \n        </plugins>        \n    </build>\n    \n</project>"
  },
  {
    "path": "examples/gatling/src/test/java/karate-config.js",
    "content": "function(){\n    return {};\n}"
  },
  {
    "path": "examples/gatling/src/test/java/logback-test.xml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <immediateFlush>false</immediateFlush>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\t\t\t\n        </encoder>\n    </appender>\n\n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <immediateFlush>false</immediateFlush>\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <logger name=\"com.intuit.karate\" level=\"DEBUG\"/>\n\n    <root level=\"WARN\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n\n</configuration>\n"
  },
  {
    "path": "examples/gatling/src/test/java/mock/CatsGatlingSimulation.scala",
    "content": "package mock\n\nimport scala.language.postfixOps\nimport io.gatling.core.Predef._\nimport io.gatling.http.Predef._\n\nimport scala.concurrent.duration._\n\nclass CatsGatlingSimulation extends Simulation {\n\n  MockUtils.startServer()\n\n  val httpConf = http.baseUrl(System.getProperty(\"mock.cats.url\"))\n\n  val create = scenario(\"create\")\n    .pause(25 milliseconds)\n    .exec(http(\"POST /cats\")\n      .post(\"/\")\n      .body(StringBody(\"\"\"{ \"name\": \"Billie\" }\"\"\"))\n      .check(status.is(200))\n      .check(jsonPath(\"$.name\").is(\"Billie\"))\n      .check(jsonPath(\"$.id\")\n        .saveAs(\"id\")))\n\n    .pause(10 milliseconds).exec(\n    http(\"GET /cats/{id}\")\n      .get(\"/${id}\")\n      .check(status.is(200))\n      .check(jsonPath(\"$.id\").is(\"${id}\"))\n      // intentional assertion failure\n      .check(jsonPath(\"$.name\").is(\"Billi\")))\n    .exitHereIfFailed\n    .exec(\n      http(\"PUT /cats/{id}\")\n        .put(\"/${id}\")\n        .body(StringBody(\"\"\"{ \"id\":\"${id}\", \"name\": \"Bob\" }\"\"\"))\n        .check(status.is(200))\n        .check(jsonPath(\"$.id\").is(\"${id}\"))\n        .check(jsonPath(\"$.name\").is(\"Bob\")))\n\n    .pause(10 milliseconds).exec(\n    http(\"GET /cats/{id}\")\n      .get(\"/${id}\")\n      .check(status.is(200)))\n\n  val delete = scenario(\"delete\")\n    .pause(15 milliseconds).exec(\n    http(\"GET /cats\")\n      .get(\"/\")\n      .check(status.is(200))\n      .check(jsonPath(\"$[*].id\").findAll.optional\n        .saveAs(\"ids\")))\n\n    .doIf(_.contains(\"ids\")) {\n      foreach(\"${ids}\", \"id\") {\n        pause(20 milliseconds).exec(\n          http(\"DELETE /cats/{id}\")\n            .delete(\"/${id}\")\n            .check(status.is(200))\n            .check(bodyString.is(\"\")))\n\n          .pause(10 milliseconds).exec(\n          http(\"GET /cats/{id}\")\n            .get(\"/${id}\")\n            .check(status.is(404)))\n      }\n    }\n\n  setUp(\n    create.inject(rampUsers(10) during (5 seconds)).protocols(httpConf),\n    delete.inject(rampUsers(5) during (5 seconds)).protocols(httpConf)\n  )\n\n}\n"
  },
  {
    "path": "examples/gatling/src/test/java/mock/CatsKarateSimulation.scala",
    "content": "package mock\n\nimport scala.language.postfixOps\nimport com.intuit.karate.gatling.PreDef._\nimport io.gatling.core.Predef._\nimport scala.concurrent.duration._\n\nclass CatsKarateSimulation extends Simulation {\n\n  MockUtils.startServer()\n\n  val feeder = Iterator.continually(Map(\"catName\" -> MockUtils.getNextCatName))\n\n  val protocol = karateProtocol(\n    \"/cats/{id}\" -> Nil,\n    \"/cats\" -> pauseFor(\"get\" -> 15, \"post\" -> 25)\n  )\n\n  protocol.nameResolver = (req, ctx) => req.getHeader(\"karate-name\")\n\n  val create = scenario(\"create\").feed(feeder).exec(karateFeature(\"classpath:mock/cats-create.feature\"))\n  val delete = scenario(\"delete\").group(\"delete cats\") {\n    exec(karateFeature(\"classpath:mock/cats-delete.feature@name=delete\"))\n  }\n  val custom = scenario(\"custom\").exec(karateFeature(\"classpath:mock/custom-rpc.feature\"))\n\n  setUp(\n    create.inject(rampUsers(10) during (5 seconds)).protocols(protocol),\n    delete.inject(rampUsers(5) during (5 seconds)).protocols(protocol),\n    custom.inject(rampUsers(10) during (5 seconds)).protocols(protocol)\n  )\n\n}\n"
  },
  {
    "path": "examples/gatling/src/test/java/mock/MockUtils.java",
    "content": "package mock;\n\nimport com.intuit.karate.core.MockServer;\nimport com.intuit.karate.PerfContext;\nimport com.intuit.karate.Runner;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n *\n * @author pthomas3\n */\npublic class MockUtils {\n    \n    public static void startServer() {\n        MockServer server = MockServer.feature(\"classpath:mock/mock.feature\").build();\n        System.setProperty(\"mock.cats.url\", \"http://localhost:\" + server.getPort() + \"/cats\");        \n    }\n\n    private static final List<String> catNames = (List) Runner.runFeature(\"classpath:mock/feeder.feature\", null, false).get(\"names\");\n\n    private static final AtomicInteger counter = new AtomicInteger();\n\n    public static String getNextCatName() {\n        return catNames.get(counter.getAndIncrement() % catNames.size());\n    }\n\n    public static Map<String, Object> myRpc(Map<String, Object> map, PerfContext context) {\n        long startTime = System.currentTimeMillis();\n        // this is just an example, you can put any kind of code here\n        int sleepTime = (Integer) map.get(\"sleep\");\n        try {\n            Thread.sleep(sleepTime);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        long endTime = System.currentTimeMillis();\n        // and here is where you send the performance data to the reporting engine\n        context.capturePerfEvent(\"myRpc-\" + sleepTime, startTime, endTime);\n        return Collections.singletonMap(\"success\", true);\n    }\n    \n}\n"
  },
  {
    "path": "examples/gatling/src/test/java/mock/cats-create.feature",
    "content": "Feature: cats crud\n\n  Background:\n    * url karate.properties['mock.cats.url']\n\n  Scenario: create, get and update cat\n    # example of using the gatling session / feeder data\n    # note how this can still work as a normal test, without gatling\n    * def name = karate.get('__gatling.catName', 'Billie')\n    Given request { name: '#(name)' }\n    When method post\n    Then status 200\n    And match response == { id: '#uuid', name: '#(name)' }\n    * def id = response.id\n\n    Given path id\n    When method get\n    # this step may randomly fail because another thread is doing deletes\n    Then status 200\n    # intentional assertion failure\n    And match response == { id: '#(id)', name: 'Billi' }\n\n    # since we failed above, these lines will not be executed\n    Given path id\n    When request { id: '#(id)', name: 'Bob' }\n    When method put\n    Then status 200\n    And match response == { id: '#(id)', name: 'Bob' }\n\n    When method get\n    Then status 200\n    And match response contains { id: '#(id)', name: 'Bob' }\n"
  },
  {
    "path": "examples/gatling/src/test/java/mock/cats-delete-one.feature",
    "content": "@ignore\nFeature: delete cat by id and verify\n\n  Scenario:\n    Given url karate.properties['mock.cats.url']\n    And path id\n    When method delete\n    Then status 200\n    And match response == ''\n\n    Given path id\n    And header karate-name = 'cats-get-404'\n    When method get\n    Then status 404\n"
  },
  {
    "path": "examples/gatling/src/test/java/mock/cats-delete.feature",
    "content": "Feature: delete all cats found\n\n  Background:\n    * url karate.properties['mock.cats.url']\n\n  Scenario: this scenario will be ignored because the gatling script looks for the tag @name=delete\n    * print 'this should not appear in the logs !'\n    When method get\n    Then status 400\n\n  @name=delete\n  Scenario: get all cats and then delete each by id\n    When method get\n    Then status 200\n\n    * def delete = read('cats-delete-one.feature')\n    * def result = call delete response\n"
  },
  {
    "path": "examples/gatling/src/test/java/mock/custom-rpc.feature",
    "content": "@ignore\nFeature: even java interop performance test reports are possible\n\n  Background:\n    * def Utils = Java.type('mock.MockUtils')\n\n  Scenario: fifty\n    * def payload = { sleep: 50 }\n    * def response = Utils.myRpc(payload, karate)\n    * match response == { success: true }\n\n  Scenario: seventy five\n    * def payload = { sleep: 75 }\n    * def response = Utils.myRpc(payload, karate)\n    # this is deliberately set up to fail\n    * match response == { success: false }\n\n  Scenario: hundred\n    * def payload = { sleep: 100 }\n    * def response = Utils.myRpc(payload, karate)\n    * match response == { success: true }\n"
  },
  {
    "path": "examples/gatling/src/test/java/mock/feeder.feature",
    "content": "Feature: to generate a list of cat names\n\n  Scenario: any variables defined can be retrieved when called via the java api\n\n    * def names = ['Bob', 'Wild', 'Nyan', 'Ceiling']"
  },
  {
    "path": "examples/gatling/src/test/java/mock/mock.feature",
    "content": "Feature: cats stateful crud\n\n  Background:\n    * def uuid = function(){ return java.util.UUID.randomUUID() + '' }\n    * def cats = {}\n\n  Scenario: pathMatches('/cats') && methodIs('post')\n    * def cat = request\n    * def id = uuid()\n    * cat.id = id\n    * cats[id] = cat\n    * def response = cat\n\n  Scenario: pathMatches('/cats')\n    * def response = $cats.*\n\n  Scenario: pathMatches('/cats/{id}') && methodIs('put')\n    * cats[pathParams.id] = request\n    * def response = request\n\n  Scenario: pathMatches('/cats/{id}') && methodIs('delete')\n    * karate.remove('cats', pathParams.id)\n    * def responseDelay = 850\n\n  Scenario: pathMatches('/cats/{id}')\n    * def response = cats[pathParams.id]\n    * def responseStatus = response ? 200 : 404\n"
  },
  {
    "path": "examples/image-comparison/README.md",
    "content": "# Karate Image Comparison\nThis project is designed to demonstrate basic usage of the [Image Comparison](https://github.com/karatelabs/karate/#compare-image) feature. You can also watch a video explanation [here](https://youtu.be/wlvmNBraP60).\n\nA more detailed video deep-dive can be viewed by skipping to 33:30 of this video: [Karate Version 1.3.0 Release Webinar\n](https://youtu.be/oMsKNE_ctaM?t=2009)\n\n## Overview\nThe [Image Comparison](https://github.com/karatelabs/karate/#compare-image) feature was introduced in [Karate 1.3.0](https://github.com/karatelabs/karate/wiki/1.3.0-Upgrade-Guide).\nAs a new feature with a number of options and a new UI component we wanted to provide a simple introduction to help users get started.\n\nThe included features are numbered 1 through 7 and build on each other. \nThey are intended to demonstrate how you might start from scratch without any baseline images on a new project:\n*  `1_establish_baseline.feature` establishes baseline images to use in future test runs\n*  `2_compare_baseline.feature` compares dynamic screenshots against our baseline images\n*  `3_custom_rebase.feature` demonstrates the use of the `onShowRebase` handler to customize the filename when rebasing\n*  `4_generic_rebase.feature` shows a slightly more advanced use of the `onShowRebase` handler that incorporates image comparison configuration options\n*  `5_custom_config.feature` shows a complete scenario that is similar to what you might use in real tests\n*  `6_outline.feature` explores a more complex use case with multiple browsers\n*  `7_api.feature` solves some issues identified in the outline feature above using the JS API\n\nThere is also a [screencast](https://youtu.be/wlvmNBraP60) that demonstrates basic usage of the diff UI in the Karate HTML report.\n\n## Running\nThe `5_custom_config.feature` is a complete [Karate UI test](https://github.com/karatelabs/karate/tree/master/karate-core) that can be executed by running `ImageComparisonRunner` as a JUnit test.\nYou will be able to open the HTML report (the file-name will appear at the end of the console log) and refresh it when re-running the test.\n\nTo manually run the test execute the following commands:\n*  Install maven artifacts from the latest [develop](https://github.com/karatelabs/karate/tree/develop) branch locally\n   ```\n   mvn clean install -P pre-release\n   ```\n*  Run the test from the `examples/image-comparison` directory\n   ```\n    mvn clean test -Dtest=ImageComparisonRunner\n   ```\n\n## Debugging\nYou should be able to use the [Karate extension for Visual Studio Code](https://github.com/karatelabs/karate/wiki/IDE-Support#vs-code-karate-plugin) for stepping-through a test for troubleshooting.\n"
  },
  {
    "path": "examples/image-comparison/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n \n    <groupId>io.karatelabs.examples</groupId>\n    <artifactId>image-comparison-test</artifactId>\n    <version>1.0-SNAPSHOT</version>\n    <packaging>jar</packaging>\n \n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <java.version>17</java.version>\n        <maven.compiler.version>3.8.1</maven.compiler.version>\n        <karate.version>1.5.2</karate.version>\n    </properties>    \n\n    <dependencies>\n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-junit5</artifactId>\n            <version>${karate.version}</version>\n            <scope>test</scope>\n        </dependency>       \t\t\n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven.compiler.version}</version>\n                <configuration>\n                    <encoding>UTF-8</encoding>\n                    <source>${java.version}</source>\n                    <target>${java.version}</target>\n                    <compilerArgument>-Werror</compilerArgument>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>2.22.2</version>\n            </plugin>            \n        </plugins>        \n    </build>       \n    \n</project>"
  },
  {
    "path": "examples/image-comparison/src/test/java/karate-config.js",
    "content": "function fn() {\n    karate.configure('retry', { count: 20, interval: 200 })\n\n    return {\n        baseUrl: karate.properties['web.url.base'] || 'http://localhost:8080/',\n\n        browsers: [\n            {\n                deviceType: 'phone',\n                width: 375,\n                height: 667, \n                userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'\n            },\n            {\n                deviceType: 'tablet',\n                width: 820,\n                height: 1180,\n                useragent: 'Mozilla/5.0 (iPad; CPU OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/87.0.4280.77 Mobile/15E148 Safari/604.1'\n            }\n        ],\n\n        emulateBrowser(deviceType) {\n            const browser = karate.get('browsers').find(browser => browser.deviceType === deviceType)\n            return driver.emulateDevice(browser.width, browser.height, browser.userAgent)\n        }\n    }\n}"
  },
  {
    "path": "examples/image-comparison/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit.karate\" level=\"DEBUG\"/>\n    <logger name=\"ui\" level=\"DEBUG\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/1_establish_baseline.feature",
    "content": "Feature: Image comparison demo\n\nBackground:\n    * configure driver = { type: 'chrome', timeout: 5000, screenshotOnFailure: false }\n    * driver baseUrl + '?r=0.1'\n    * emulateBrowser('phone')\n\nScenario: Landing page\n    * configure imageComparison = { mismatchShouldPass: true }\n    \n    * def loadingScreenshot = screenshot()\n    * compareImage { latest: #(loadingScreenshot) }\n\n    * waitFor('.welcome')\n    * def loadedScreenshot = screenshot()\n    * compareImage { latest: #(loadedScreenshot) }\n"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/2_compare_baseline.feature",
    "content": "Feature: Image comparison demo\n\nBackground:\n    * configure driver = { type: 'chrome', timeout: 5000, screenshotOnFailure: false }\n    * driver baseUrl + '?r=0.75'\n    * emulateBrowser('phone')\n\nScenario: Landing page\n    * configure imageComparison = { mismatchShouldPass: true }\n    \n    * def loadingScreenshot = screenshot()\n    * compareImage { baseline: 'this:screenshots/latest.png', latest: #(loadingScreenshot) }\n\n    * waitFor('.welcome')\n    * def loadedScreenshot = screenshot()\n    * compareImage { baseline: 'this:screenshots/latest.png', latest: #(loadedScreenshot) }"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/3_custom_rebase.feature",
    "content": "Feature: Image comparison demo\n\nBackground:\n    * configure driver = { type: 'chrome', timeout: 5000, screenshotOnFailure: false }\n    * driver baseUrl + '?r=0.75'\n    * emulateBrowser('phone')\n\nScenario: Landing page\n    * def loadingScreenshot = screenshot()\n    * configure imageComparison = { onShowRebase: \"(cfg, saveAs) => saveAs('loading.png')\", mismatchShouldPass: true }\n    * compareImage { baseline: 'this:screenshots/latest.png', latest: #(loadingScreenshot) }\n\n    * waitFor('.welcome')\n    * def loadedScreenshot = screenshot()\n    * configure imageComparison = { onShowRebase: \"(cfg, saveAs) => saveAs('loaded.png')\", mismatchShouldPass: true }\n    * compareImage { baseline: 'this:screenshots/latest.png', latest: #(loadedScreenshot) }"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/4_generic_rebase.feature",
    "content": "Feature: Image comparison demo\n\nBackground:\n    * configure imageComparison = { onShowRebase: '(cfg, saveAs) => saveAs(cfg.name)', mismatchShouldPass: true }\n\n    * configure driver = { type: 'chrome', timeout: 5000, screenshotOnFailure: false }\n    * driver baseUrl + '?r=1.2'\n    * emulateBrowser('phone')\n\nScenario: Landing page\n    * def loadingScreenshot = screenshot()\n    * def loadingOpts =\n      \"\"\"\n      {\n        name: 'loading.png'\n      }\n      \"\"\"\n    * compareImage { baseline: 'this:screenshots/loading.png', latest: #(loadingScreenshot), options: #(loadingOpts) }\n\n    * waitFor('.welcome')\n\n    * def loadedScreenshot = screenshot()\n    * def loadedOpts =\n      \"\"\"\n      {\n        name: 'loaded.png'\n      }\n      \"\"\"\n    * compareImage { baseline: 'this:screenshots/loaded.png', latest: #(loadedScreenshot), options: #(loadedOpts) }"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/5_custom_config.feature",
    "content": "Feature: Image comparison demo\n\nBackground:\n    * configure imageComparison = { onShowRebase: '(cfg, saveAs) => saveAs(cfg.name)' }\n\n    * configure driver = { type: 'chrome', timeout: 5000, screenshotOnFailure: false }\n    * driver baseUrl + '?r=1.2'\n    * emulateBrowser('phone')\n\nScenario: Landing page\n    * def loadingScreenshot = screenshot()\n    * def loadingOpts =\n      \"\"\"\n      {\n        name: 'loading.png',\n        \"ignoredBoxes\": [{\n            \"top\": 278,\n            \"left\": 131,\n            \"bottom\": 391,\n            \"right\": 246\n        }]\n      }\n      \"\"\"\n    * compareImage { baseline: 'this:screenshots/loading.png', latest: #(loadingScreenshot), options: #(loadingOpts) }\n\n    * waitFor('.welcome')\n\n    * def loadedScreenshot = screenshot()\n    * def loadedOpts =\n      \"\"\"\n      {\n        name: 'loaded.png',\n        \"ignoredBoxes\": [{\n            \"top\": 73,\n            \"left\": 17,\n            \"bottom\": 125,\n            \"right\": 188\n        }]\n      }\n      \"\"\"\n    * compareImage { baseline: 'this:screenshots/loaded.png', latest: #(loadedScreenshot), options: #(loadedOpts) }"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/6_outline.feature",
    "content": "Feature: Image comparison demo\n\nBackground:\n    * configure driver = { type: 'chrome', timeout: 5000, screenshotOnFailure: false }\n\n@setup\nScenario:\n    * copy data = browsers\n\nScenario Outline: Landing page - <deviceType>\n    # change to `false` after establishing baseline images... it would be nice if the baseline was created automatically\n    * def firstRun = true\n\n    * configure imageComparison = { onShowRebase: '(cfg, saveAs) => saveAs(cfg.name)', mismatchShouldPass: #(firstRun) }\n    * driver baseUrl + (firstRun ? '?r=0.75' : '?r=1.5')\n    * emulateBrowser('<deviceType>')\n\n    # we have a problem below: the `tablet` device has a different screen size and our `ignoredBoxes` will be off\n    * def loadingScreenshot = screenshot()\n    * def loadingOpts =\n      \"\"\"\n      {\n        name: 'loading_<deviceType>.png',\n        \"ignoredBoxes\": [{\n            \"top\": 278,\n            \"left\": 131,\n            \"bottom\": 391,\n            \"right\": 246\n        }]\n      }\n      \"\"\"\n    * def loadingBaselinePath = firstRun ? null : `this:screenshots/${loadingOpts.name}`\n    * compareImage { baseline: #(loadingBaselinePath), latest: #(loadingScreenshot), options: #(loadingOpts) }\n\n    * waitFor('.welcome')\n\n    * def loadedScreenshot = screenshot()\n    * def loadedOpts =\n      \"\"\"\n      {\n        name: 'loaded_<deviceType>.png',\n        \"ignoredBoxes\": [{\n            \"top\": 73,\n            \"left\": 17,\n            \"bottom\": 125,\n            \"right\": 188\n        }]\n      }\n      \"\"\"\n    * def loadedBaselinePath = firstRun ? null : `this:screenshots/${loadedOpts.name}`\n    * compareImage { baseline: #(loadedBaselinePath), latest: #(loadedScreenshot), options: #(loadedOpts) }\n\n    Examples:\n        | karate.setup().data |"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/7_api.feature",
    "content": "Feature: Image comparison demo\n\nBackground:\n    # `screenGrab` processes a named image comparison by:\n    #   - capturing a screenshot to use as the latest image\n    #   - locating the correct baseline image from the filesystem (if one exists)\n    #   - fetching comparison options from the filesystem (if they exist)\n    #   - automatically copying the latest image to be the new baseline when no baseline exists\n    #   - executing `compareImage` using the named baseline image, the latest screenshot, and dynamically-loaded options\n    * def screenGrab =\n      \"\"\"\n      function (name) {\n        const latestBytes = screenshot(false)\n\n        const File = Java.type('java.io.File')\n        const latestFile = karate.write(latestBytes, `screenshots/${name}.png`)\n        const baselineFile = new File(karate.toAbsolutePath('this:screenshots'), `${name}.png`)\n        const optionsFile = new File(karate.toAbsolutePath('this:screenshots/config'), `${name}.json`)\n\n        // read options from browser-specific config JSON (if exists)\n        const options = optionsFile.exists() ? karate.read('file:' + optionsFile.getPath()) : {}\n\n        options.baselinePath = baselineFile.getPath()\n        options.latestPath = new File(karate.toAbsolutePath('this:' + latestFile.getPath())).getPath()\n        options.optionsPath = optionsFile.getPath()\n\n        let baselinePath = 'file:' + options.baselinePath\n        if (!baselineFile.exists()) {\n          // automatically copy latest image to baseline when no baseline exists\n          java.nio.file.Files.copy(latestFile.toPath(), baselineFile.toPath())\n          baselinePath = null\n        }\n\n        const result = karate.compareImage(baselinePath, latestBytes, options)\n\n        if (result.error && !result.isBaselineMissing) throw new Error(result.error)\n\n        return result\n      }\n      \"\"\"\n\n    # instead of manually downloading / managing rebased images we'll provide a shell command to copy/paste\n    * def rebaseFn = 'cfg => `cp \\\\\\\\\\n  ${cfg.latestPath} \\\\\\\\\\n  ${cfg.baselinePath}`'\n\n    # since we now want the configuration to be stored on the filesystem we'll provide a shell command to copy/paste\n    * def configFn = '(imgConfigStr, cfg) => `cat << EOF > ${cfg.optionsPath}\\n${imgConfigStr}\\nEOF`'\n\n    * configure imageComparison = { mismatchShouldPass: true, onShowRebase: #(rebaseFn), onShowConfig: #(configFn) }\n\n    * configure driver = { type: 'chrome', timeout: 5000, screenshotOnFailure: false }\n    * driver baseUrl\n\n@setup\nScenario:\n    * copy data = browsers\n\nScenario Outline: Landing page - <deviceType>\n    * emulateBrowser('<deviceType>')\n    * screenGrab('loading_<deviceType>')\n    * waitFor('.welcome')\n    * screenGrab('loaded_<deviceType>')\n\n    Examples:\n        | karate.setup().data |"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/ImageComparisonRunner.java",
    "content": "package ui;\n\nimport com.intuit.karate.http.HttpServer;\nimport com.intuit.karate.junit5.Karate;\nimport org.junit.jupiter.api.BeforeAll;\n\nclass ImageComparisonRunner {\n    \n    @BeforeAll\n    public static void beforeAll() {\n        HttpServer server = MockRunner.start(0);\n        System.setProperty(\"web.url.base\", \"http://localhost:\" + server.getPort());        \n    }\n    \n    @Karate.Test\n    Karate testUi() {\n        return Karate.run(\"classpath:ui/7_api.feature\");\n    }\n    \n}\n"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/MockRunner.java",
    "content": "package ui;\n\nimport com.intuit.karate.http.HttpServer;\nimport com.intuit.karate.http.ServerConfig;\nimport org.junit.jupiter.api.Test;\n\n/**\n * run this as a junit test to start an http server at port 8080 the html page\n * can be viewed at http://localhost:8080/ kill / stop this process when done\n */\nclass MockRunner {\n\n    @Test\n    public void testStart() {\n        start(8080).waitSync();\n    }\n\n    public static HttpServer start(int port) {\n        ServerConfig config = new ServerConfig(\"src/test/java/ui/html\")\n                .autoCreateSession(true);\n        return HttpServer.config(config).http(port).build();\n    }\n\n}\n"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/html/index.html",
    "content": "<!doctype html>\n<html>\n<head>\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n    <title>Karate Image Comparison</title>\n    <link href=\"https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400&display=swap\" rel=\"stylesheet\">\n    <style>\n        html, body {\n            font-family: 'Roboto Mono', monospace;\n            text-shadow: 0 0 2px #858585;\n            background-color: black;\n            color: #f9f9f9;\n            width: 100%;\n            height: 100%;\n            margin: 0;\n            padding: 0;\n        }\n        article {\n            flex-direction: column;\n            align-items: stretch;\n            min-height: 100%;\n            display: flex;\n        }\n        main {\n            align-items: center;\n            text-align: center;\n            display: flex;\n            flex-grow: 1;\n        }\n        header, main, footer {\n            flex-shrink: 0;\n        }\n        header {\n            align-items: center;\n            flex-wrap: wrap;\n            display: flex;\n        }\n        header span {\n            text-shadow: 0 0 2px #858585;\n            font-size: 35px;\n            opacity: 0.95;\n        }\n        footer {\n            display: flex;\n        }\n        footer span {\n            opacity: 0.95;\n            margin: 20px;\n        }\n        a, a:visited {\n            color: #f9f9f9;\n        }\n        h2 {\n            font-size: 40px;\n            opacity: 0.95;\n            flex-grow: 1;\n        }\n        @media (max-width:768px) {\n            header span {\n                margin: -40px 0 20px 20px;\n                font-size: 23px;\n            }\n        }\n    </style>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/spin.js/4.1.1/spin.min.css\" integrity=\"sha512-ssYEuK9Epo/48VIlBWTFosf1izrgGZqEMELJP+L7Clh0nvaOSTg87dM+Z8L+KKjrPdMbMvKYOOnzBOkNMhWFsg==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\" />\n    <script type=\"module\">\n        import {Spinner} from 'https://cdnjs.cloudflare.com/ajax/libs/spin.js/4.1.0/spin.min.js';\n        const randomSeed = parseFloat(new URLSearchParams(window.location.search).get('r')) || Math.random()\n        const loadingEl = document.getElementsByTagName('main')[0]\n        new Spinner({\n            lines: 13,\n            length: 38,\n            width: 17,\n            radius: 45,\n            scale: 0.5,\n            corners: 1,\n            speed: 1 + randomSeed,\n            rotate: 0,\n            animation: 'spinner-line-fade-quick',\n            direction: 1,\n            color: '#ffffff',\n            fadeColor: 'transparent',\n            top: '50%',\n            left: '50%',\n            shadow: '0 0 1px transparent',\n            zIndex: 2000000000,\n            className: 'spinner',\n            position: 'absolute',\n        }).spin(loadingEl);\n       setTimeout(() => loadingEl.innerHTML = '<h2 class=\"welcome\">Welcome!</h2>', 1000 + (randomSeed * 1000))\n    </script>\n</head>\n<body>\n    <article>\n        <header>\n            <img src=\"logo.gif\" alt=\"Karate\" id=\"logo\">\n            <span>Image Comparison Demo</span>\n        </header>\n        <main></main>\n        <footer>\n            <span>&copy; <a href=\"https://github.com/karatelabs/karate\">Karate 2022</a></span>\n        </footer>\n    </article>\n</body>\n</html>"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/screenshots/config/loaded_phone.json",
    "content": "{\n  \"ignoredBoxes\": [\n    {\n      \"top\": 75,\n      \"left\": 13,\n      \"bottom\": 128,\n      \"right\": 191\n    }\n  ]\n}\n"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/screenshots/config/loaded_tablet.json",
    "content": "{\n  \"ignoredBoxes\": [\n    {\n      \"top\": 59,\n      \"left\": 10,\n      \"bottom\": 139,\n      \"right\": 195\n    }\n  ]\n}\n"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/screenshots/config/loading_phone.json",
    "content": "{\n  \"ignoredBoxes\": [\n    {\n      \"top\": 262,\n      \"left\": 108,\n      \"bottom\": 396,\n      \"right\": 261\n    }\n  ]\n}\n"
  },
  {
    "path": "examples/image-comparison/src/test/java/ui/screenshots/config/loading_tablet.json",
    "content": "{\n  \"ignoredBoxes\": [\n    {\n      \"top\": 531,\n      \"left\": 344,\n      \"bottom\": 652,\n      \"right\": 471\n    }\n  ]\n}\n"
  },
  {
    "path": "examples/mobile-test/README.md",
    "content": "# Examples - Karate Mobile (Appium)\n> Please consider Mobile support as experimental. But we are very close and there are some teams that use Karate for simple use-cases. Please contribute code if you can.\n\n## Overview\nThis project is to replicate issues with the [Karate UI framework](https://github.com/karatelabs/karate/tree/develop/karate-core) for mobile app testing using appium.\n\n## Running\n> Before running:  \n> * change `karate.version` in [`pom.xml`](pom.xml)\n> * change `platformVersion`,`avd` in [`karate-config.js`](src/test/java/karate-config.js) according to your local android `avd` setup and add/remove the desiredCapabilities according to your requirement.\n> * start appium server manually when `android.feature` has `configure driver` with `start: false`\n> * change `configure driver` to `start: true` if you want karate to start appium driver automatically (this is experimental and needs appium pre-installed via `npm`)\n\n\nThe [`android.feature`](src/test/java/android/android.feature) is a simple [Karate UI test](https://github.com/karatelabs/karate/tree/develop/karate-core), and executing `AndroidRunner` as a JUnit test will run it.\n\nRefer [`Documentation`](https://github.com/karatelabs/karate/tree/develop/karate-core#appium) for currently supported methods\n"
  },
  {
    "path": "examples/mobile-test/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\r\n    <modelVersion>4.0.0</modelVersion>\r\n\r\n    <groupId>io.karatelabs.examples</groupId>\r\n    <artifactId>mobile-test</artifactId>\r\n    <version>1.0-SNAPSHOT</version>\r\n    <packaging>jar</packaging>\r\n\r\n    <properties>\r\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\r\n        <java.version>17</java.version>\r\n        <maven.compiler.version>3.8.1</maven.compiler.version>\r\n        <maven.surefire.version>2.22.2</maven.surefire.version>\r\n        <karate.version>1.5.2</karate.version>\r\n    </properties>\r\n\r\n    <dependencies>\r\n        <dependency>\r\n            <groupId>io.karatelabs</groupId>\r\n            <artifactId>karate-junit5</artifactId>\r\n            <version>${karate.version}</version>\r\n            <scope>test</scope>\r\n        </dependency>\r\n    </dependencies>\r\n\r\n    <build>\r\n        <testResources>\r\n            <testResource>\r\n                <directory>src/test/java</directory>\r\n                <excludes>\r\n                    <exclude>**/*.java</exclude>\r\n                </excludes>\r\n            </testResource>\r\n        </testResources>\r\n        <plugins>\r\n            <plugin>\r\n                <groupId>org.apache.maven.plugins</groupId>\r\n                <artifactId>maven-compiler-plugin</artifactId>\r\n                <version>${maven.compiler.version}</version>\r\n                <configuration>\r\n                    <encoding>UTF-8</encoding>\r\n                    <source>${java.version}</source>\r\n                    <target>${java.version}</target>\r\n                    <compilerArgument>-Werror</compilerArgument>\r\n                </configuration>\r\n            </plugin>\r\n            <plugin>\r\n                <groupId>org.apache.maven.plugins</groupId>\r\n                <artifactId>maven-surefire-plugin</artifactId>\r\n                <version>${maven.surefire.version}</version>\r\n                <configuration>\r\n                    <argLine>-Dfile.encoding=UTF-8</argLine>\r\n                </configuration>\r\n            </plugin>\r\n        </plugins>\r\n    </build>\r\n\r\n</project>"
  },
  {
    "path": "examples/mobile-test/src/test/java/android/AndroidTest.java",
    "content": "package android;\n\nimport com.intuit.karate.junit5.Karate;\n\n/**\n * @author babusekaran\n */\nclass AndroidTest {\n\n    @Karate.Test\n    public Karate test() {\n        return Karate.run(\"classpath:android/android.feature\");\n    }\n\n}\n"
  },
  {
    "path": "examples/mobile-test/src/test/java/android/android.feature",
    "content": "Feature: android test\r\n\r\n  Background: App Preset\r\n    * configure driver = { type: 'android', webDriverPath : \"/wd/hub\", start: false, httpConfig : { readTimeout: 120000 }}\r\n\r\n  Scenario: android mobile app UI tests\r\n    Given driver { webDriverSession: { desiredCapabilities : \"#(android.desiredConfig)\"} }\r\n    And driver.click('#com.bs.droidaction:id/showTextCheckBox')\r\n    And driver.clear('#com.bs.droidaction:id/showTextOnDelay').input(\"10000\")\r\n    And driver.input('#com.bs.droidaction:id/editTextBox', \"KarateDSL\")\r\n    And driver.click('#com.bs.droidaction:id/showTextCheckBox')\r\n    And retry(10, 1000).waitForAny(\"#com.bs.droidaction:id/nameTextView\", \"//android.widget.TextView[@text='KarateDSL']\")\r\n    Then match driver.text('#com.bs.droidaction:id/nameTextView') == 'KarateDSL'\r\n    And driver.click('#com.bs.droidaction:id/showTextCheckBox')\r\n    And assert (optional('#com.bs.droidaction:id/nameTextView').present != true)\r\n"
  },
  {
    "path": "examples/mobile-test/src/test/java/karate-config.js",
    "content": "function fn() {\n  var config = {}\n  var android = {}\n  android[\"desiredConfig\"] = {\n   \"app\" : \"https://github.com/babusekaran/droidAction/raw/main/build/UiDemo.apk\",\n   \"newCommandTimeout\" : 300,\n   \"platformVersion\" : \"9.0\",\n   \"platformName\" : \"Android\",\n   \"connectHardwareKeyboard\" : true,\n   \"deviceName\" : \"emulator-5554\",\n   \"avd\" : \"Pixel2_PIE\",\n   \"automationName\" : \"UiAutomator2\"\n  }\n  config[\"android\"] = android\n  return config;\n}\n"
  },
  {
    "path": "examples/mobile-test/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <logger name=\"com.intuit\" level=\"DEBUG\"/>\n\n    <root level=\"info\">\n        <appender-ref ref=\"STDOUT\"/>\n        <appender-ref ref=\"FILE\"/>\n    </root>\n\n</configuration>"
  },
  {
    "path": "examples/profiling-test/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n \n    <groupId>io.karatelabs.examples</groupId>\n    <artifactId>profiling-test</artifactId>\n    <version>1.0-SNAPSHOT</version>\n    <packaging>jar</packaging>\n \n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <java.version>17</java.version>\n        <maven.compiler.version>3.6.0</maven.compiler.version>\n        <karate.version>1.5.2</karate.version>\n        <gatling.plugin.version>4.1.1</gatling.plugin.version>\n    </properties>    \n\n    <dependencies>\n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-gatling</artifactId>\n            <version>${karate.version}</version>\n            <scope>test</scope>\n        </dependency>\t\t\n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven.compiler.version}</version>\n                <configuration>\n                    <encoding>UTF-8</encoding>\n                    <source>${java.version}</source>\n                    <target>${java.version}</target>\n                    <compilerArgument>-Werror</compilerArgument>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>net.alchim31.maven</groupId>\n                <artifactId>scala-maven-plugin</artifactId>\n                <version>4.5.6</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>testCompile</goal>\n                        </goals>\n                        <configuration>\n                            <args>\n                                <arg>-Jbackend:GenBCode</arg>\n                                <arg>-Jdelambdafy:method</arg>\n                                <arg>-target:jvm-1.8</arg>\n                                <arg>-deprecation</arg>\n                                <arg>-feature</arg>\n                                <arg>-unchecked</arg>\n                                <arg>-language:implicitConversions</arg>\n                                <arg>-language:postfixOps</arg>\n                            </args>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>io.gatling</groupId>\n                <artifactId>gatling-maven-plugin</artifactId>\n                <version>${gatling.plugin.version}</version>\n                <configuration>\n                    <simulationsFolder>src/test/java</simulationsFolder>\n                    <includes>\n                        <include>perf.TestSimulation</include>\n                    </includes>\n                </configuration>\n                <executions>\n                    <execution>\n                        <phase>test</phase>\n                        <goals>\n                            <goal>test</goal>\n                        </goals>\n                    </execution>\n                </executions>                \n            </plugin>            \n        </plugins>        \n    </build>\n    <name>profiling-test</name>\n</project>"
  },
  {
    "path": "examples/profiling-test/src/test/java/karate-config.js",
    "content": "function fn() {\n  var config = karate.call('classpath:perf/called.feature');\n  karate.log('config:', config);\n  return config;\n}"
  },
  {
    "path": "examples/profiling-test/src/test/java/logback-test.xml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <immediateFlush>false</immediateFlush>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\t\t\t\n        </encoder>\n    </appender>\n\n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <immediateFlush>false</immediateFlush>\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <logger name=\"com.intuit.karate\" level=\"WARN\"/>\n\n    <root level=\"WARN\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n\n</configuration>\n"
  },
  {
    "path": "examples/profiling-test/src/test/java/perf/Main.java",
    "content": "package perf;\n\nimport com.intuit.karate.PerfHook;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.FeatureResult;\nimport com.intuit.karate.core.PerfEvent;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.http.HttpRequest;\n\n/**\n *\n * @author pthomas3\n */\npublic class Main {\n\n    public static void main(String[] args) {\n        TestUtils.startServer();\n        int count = 0;\n        PerfHook hook = new PerfHook() {\n            @Override\n            public String getPerfEventName(HttpRequest request, ScenarioRuntime sr) {\n                return request.getMethod() + \" \" + request.getUrl();\n            }\n\n            @Override\n            public void reportPerfEvent(PerfEvent event) {\n\n            }\n\n            @Override\n            public void submit(Runnable runnable) {\n                runnable.run();\n            }\n\n            @Override\n            public void afterFeature(FeatureResult fr) {\n\n            }\n\n            @Override\n            public void pause(Number millis) {\n\n            }\n\n        };\n        Runner.Builder builder = Runner.builder();\n        while (true) {            \n            Runner.callAsync(builder, \"classpath:perf/test.feature\", null, hook);\n            count++;\n            System.out.print(count + \" \");\n            if (count % 100 == 0) {\n                System.out.println(\"\");\n            }            \n        }\n    }\n\n}\n"
  },
  {
    "path": "examples/profiling-test/src/test/java/perf/TestSimulation.scala",
    "content": "package perf\n\nimport com.intuit.karate.gatling.PreDef._\nimport io.gatling.core.Predef._\n\nimport scala.concurrent.duration._\n\nclass TestSimulation extends Simulation {\n\n  val protocol = karateProtocol()\n  TestUtils.startServer()\n\n  val main = scenario(\"main\").exec(karateFeature(\"classpath:perf/main.feature\"))\n  val called = scenario(\"called\").exec(karateFeature(\"classpath:perf/called.feature\"))\n\n  val chained = scenario(\"chained\")\n       .exec(main)\n       .exec(karateSet(\"extraKey\", s => \"extraValue\"))\n       .exec(called)\n\n  setUp(\n    chained.inject(\n      rampUsers(20).during(5.seconds),\n      constantUsersPerSec(20).during(10.minutes)\n    ).protocols(protocol)\n  )\n\n}\n"
  },
  {
    "path": "examples/profiling-test/src/test/java/perf/TestUtils.java",
    "content": "package perf;\n\nimport com.intuit.karate.core.MockServer;\n\n/**\n *\n * @author pthomas3\n */\npublic class TestUtils {\n    \n    public static void startServer() {\n        MockServer server = MockServer.feature(\"classpath:perf/mock.feature\").build();\n        System.setProperty(\"mock.server.url\", \"http://localhost:\" + server.getPort());\n    }\n    \n}\n"
  },
  {
    "path": "examples/profiling-test/src/test/java/perf/called.feature",
    "content": "Feature:\n\n  Scenario:\n    * def var1 = { foo: 'bar' }\n    * def var2 = { baz: { hello: 'world' } }\n    * def var3 = function(name){ return 'hello ' + name }\n    * def extraKey = karate.get('extraKey')\n    # to see the next line in the log, change level to warn\n    * if (extraKey) karate.logger.info('called from gatling:', extraKey)\n"
  },
  {
    "path": "examples/profiling-test/src/test/java/perf/main.feature",
    "content": "Feature:\n\n  Background:\n    * def backgroundData = callonce read('called.feature')\n    * url karate.properties['mock.server.url']\n\n  Scenario:\n    * path 'test'\n    * method get\n    * match response == { success: true }\n    * match backgroundData contains { var1: { foo: 'bar' } }\n    * def scenarioData = call read('called.feature') { callArgData: '#backgroundData' }\n    * match scenarioData contains { var2: { baz: { hello: 'world' } } }\n"
  },
  {
    "path": "examples/profiling-test/src/test/java/perf/mock.feature",
    "content": "Feature:\n\n  Scenario: pathMatches('/test')\n    * def response = { success: true }\n"
  },
  {
    "path": "examples/robot-test/README.md",
    "content": "# Examples - Karate Robot\n\nThis is designed to be a simple Maven project to get started with [`karate-robot`](https://github.com/karatelabs/karate/tree/master/karate-robot).\n\n* [`chrome.feature`](src/test/java/chrome/chrome.feature) - this example will switch to Google Chrome (assumed to be open) and perform a search using only keystrokes and navigation using image-detection"
  },
  {
    "path": "examples/robot-test/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n \n    <groupId>io.karatelabs.examples</groupId>\n    <artifactId>examples-robot-test</artifactId>\n    <version>1.0-SNAPSHOT</version>\n    <packaging>jar</packaging>\n \n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <java.version>17</java.version>\n        <maven.compiler.version>3.8.1</maven.compiler.version>\n        <maven.surefire.version>2.22.2</maven.surefire.version>\n        <karate.version>1.5.2</karate.version>\n    </properties>    \n\n    <dependencies>\n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-robot</artifactId>\n            <version>${karate.version}</version>\n            <scope>test</scope>\n        </dependency>                    \n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-junit5</artifactId>\n            <version>${karate.version}</version>\n            <scope>test</scope>\n        </dependency>       \t\t\n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven.compiler.version}</version>\n                <configuration>\n                    <encoding>UTF-8</encoding>\n                    <source>${java.version}</source>\n                    <target>${java.version}</target>\n                    <compilerArgument>-Werror</compilerArgument>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>${maven.surefire.version}</version>\n                <configuration>\n                    <argLine>-Dfile.encoding=UTF-8</argLine>\n                </configuration>\n            </plugin>            \n        </plugins>        \n    </build>       \n    \n</project>"
  },
  {
    "path": "examples/robot-test/src/test/java/karate-config.js",
    "content": "function fn() {\n  return {};  \n}\n"
  },
  {
    "path": "examples/robot-test/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit\" level=\"DEBUG\"/>\n   \n    <root level=\"info\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>"
  },
  {
    "path": "examples/robot-test/src/test/java/mac/ChromeRunner.java",
    "content": "package mac;\n\nimport com.intuit.karate.junit5.Karate;\n\n/**\n *\n * @author pthomas3\n */\nclass ChromeRunner {\n    \n    @Karate.Test\n    Karate testChrome() {\n        return Karate.run(\"classpath:mac/chrome.feature\");\n    }      \n    \n}\n"
  },
  {
    "path": "examples/robot-test/src/test/java/mac/chrome.feature",
    "content": "Feature: mac - robot and chrome\n\nScenario:\n# * karate.exec('Chrome')\n# or make sure Chrome is open\n* robot { window: '^Chrome', highlight: true, highlightDuration: 500 }\n* input(Key.META + 't')\n* input('karate dsl' + Key.ENTER)\n* waitFor('tams.png').click()\n* delay(2000)\n* screenshot()\n\n"
  },
  {
    "path": "examples/robot-test/src/test/java/win/CalcRunner.java",
    "content": "package win;\n\nimport com.intuit.karate.junit5.Karate;\n\n/**\n *\n * @author pthomas3\n */\nclass CalcRunner {\n    \n    @Karate.Test\n    Karate testCalc() {\n        return Karate.run(\"classpath:win/calc.feature\");\n    }      \n    \n}\n"
  },
  {
    "path": "examples/robot-test/src/test/java/win/calc.feature",
    "content": "Feature: windows calculator\n\nScenario:\n* robot { window: 'Calculator', fork: 'calc', highlight: true, highlightDuration: 500 }\n* click('Clear')\n* click('One')\n* click('Plus')\n* click('Two')\n* click('Equals')\n* match locate('#CalculatorResults').name == 'Display is 3'\n* screenshot()\n* click('Close Calculator')"
  },
  {
    "path": "examples/ui-test/README.md",
    "content": "# Karate UI Test\nThis project is designed to be the simplest way to replicate issues with the [Karate UI framework](https://github.com/karatelabs/karate/tree/master/karate-core) for web-browser testing. It includes an HTTP mock that serves HTML and JavaScript, which you can easily modify to simulate complex situations such as a slow-loading element. To submit an issue after you have a way to replicate the scenario, follow these instructions: [How to Submit an Issue](https://github.com/karatelabs/karate/wiki/How-to-Submit-an-Issue).\n\n## Overview\nTo point to a specifc version of Karate, edit the `pom.xml`. If you are working with the source-code of Karate, follow the [developer guide](https://github.com/karatelabs/karate/wiki/Developer-Guide).\n\nYou can view the HTML source of `page-01.html` to see how it works. It depends on `karate.js` which is very simple, so you can see how to add any JS (if required) along the same lines.\n\nThe code in [`MockRunner.java`](src/test/java/ui/MockRunner.java) starts a Karate HTTP server. Note how it is very simple - but able to serve both HTML and JS. If you need to include navigation to a second page, you can easily add a second HTML file. To manually verify the HTML that will be served, you can start the mock-server by running `MockRunner` as a JUnit test, and then opening [`http://localhost:8080/page-01`](http://localhost:8080/page-01) in a browser. And yes, hot-reloading is possible !\n\n## Running\nThe `test.feature` is a simple [Karate UI test](https://github.com/karatelabs/karate/tree/master/karate-core) that can be executed by running `UiRunner` as a JUnit test. You will be able to open the HTML report (the file-name will appear at the end of the console log) and refresh it when re-running the test. For convenience, this test is a `Scenario Outline` - set up so that you can add multiple browser targets or driver implementations. This makes it easy to validate cross-browser compatibility.\n\n## Debugging\nYou should be able to use the [Karate extension for Visual Studio Code](https://github.com/karatelabs/karate/wiki/IDE-Support#vs-code-karate-plugin) for stepping-through a test for troubleshooting.\n\n## WebDriver Tips\nIf you are targeting a WebDriver implementation, you may need to experiment with HTTP calls. Don't forget that that is Karate's core competency ! So you can use a \"scratchpad\" Karate test on the side, like this, after you have manually started a \"driver executable\", [`chromedriver`](https://chromedriver.chromium.org) in this case:\n\n```cucumber\nFeature:\n\nScenario:\n* url 'http://localhost:9515'\n* path 'session'\n* request {\"capabilities\":{\"browserName\":\"msedge\"}}\n* method post\n```\n\nWithin a test script, as a convenience, the `driver` object exposes an `http` property, which makes it easy to make custom-crafted WebDriver requests using the [`Http` helper / class](../../karate-core/src/main/java/com/intuit/karate/Http.java). Note that this will be available only after the [`driver` keyword](https://github.com/karatelabs/karate/tree/master/karate-core#driver) has been used, and thus a WebDriver session has been initialized.\n\nHere is an example of [getting the page title](https://w3c.github.io/webdriver/#get-title):\n\n```cucumber\n* def temp = driver.http.path('title').get().body().asMap()\n* print 'temp:', temp\n```\n\nWhich results in a `GET` request to: `http://localhost:9515/session/{sessionId}/title` - and the response body will be printed. Now you can easily extract data out of the response JSON.\n\nAnd here is how you can make a `POST` request, to [navigate to a given URL](https://w3c.github.io/webdriver/#navigate-to):\n\n```cucumber\n* driver.http.path('url').post({ url: 'https://github.com' })\n```\n\nAnd note that the [official Karate plugins](https://github.com/karatelabs/karate/wiki/IDE-Support) are really convenient for re-running tests - or you can pause a test using a break-point and [type in interactive commands](https://twitter.com/getkarate/status/1546468347826827264).\n\n## DevTools Protocol Tips\nWhen using the driver type `chrome`, you can call the `send()` method and pass a raw JSON message that will be sent to the Chrome browser using a WebSocket connection. For example here is how to get the [metadata about frames](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-getFrameTree):\n\n```cucumber\n* def temp = driver.send({ method: 'Page.getFrameTree' })\n* print 'temp:', temp\n```\n\nThis will result in the following raw message sent (Karate will supply the `id` automatically):\n\n```\n{\"method\":\"Page.getFrameTree\",\"id\":7}\n```\n\nChrome will respond with something like this, which should be viewable in the log / console:\n\n```\n{\"id\":7,\"result\":{\"frameTree\":{\"frame\":{\"id\":\"11B3A5ABDEE5802201D84389EE0215B8\",\"loaderId\":\"D2241AD7B86ED533F095F907A78A1208\",\"url\":\"http://localhost:52664/page-01\",\"securityOrigin\":\"http://localhost:52664\",\"mimeType\":\"text/html\"}}}}\n```\n\nYou can do more, but this should be sufficient for exploring the possible commands and troubleshooting via trial and error. And then you can suggest / contribute changes to be made to the code, e.g. the [DevToolsDriver](../../karate-core/src/main/java/com/intuit/karate/driver/DevToolsDriver.java)."
  },
  {
    "path": "examples/ui-test/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n \n    <groupId>io.karatelabs.examples</groupId>\n    <artifactId>examples-ui-test</artifactId>\n    <version>1.0-SNAPSHOT</version>\n    <packaging>jar</packaging>\n \n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <java.version>17</java.version>\n        <maven.compiler.version>3.6.0</maven.compiler.version>\n        <karate.version>1.5.2</karate.version>\n    </properties>    \n\n    <dependencies>           \n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-junit5</artifactId>\n            <version>${karate.version}</version>\n            <scope>test</scope>\n        </dependency>       \t\t\n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven.compiler.version}</version>\n                <configuration>\n                    <encoding>UTF-8</encoding>\n                    <source>${java.version}</source>\n                    <target>${java.version}</target>\n                    <compilerArgument>-Werror</compilerArgument>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>2.22.2</version>\n            </plugin>            \n        </plugins>        \n    </build>       \n    \n</project>"
  },
  {
    "path": "examples/ui-test/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit.karate\" level=\"DEBUG\"/>\n    <logger name=\"ui\" level=\"DEBUG\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>"
  },
  {
    "path": "examples/ui-test/src/test/java/ui/MockRunner.java",
    "content": "package ui;\n\nimport com.intuit.karate.http.HttpServer;\nimport com.intuit.karate.http.ServerConfig;\nimport org.junit.jupiter.api.Test;\n\n/**\n * run this as a junit test to start an http server at port 8080 the html page\n * can be viewed at http://localhost:8080/page-01 kill / stop this process when\n * done\n */\nclass MockRunner {\n\n    @Test\n    public void testStart() {\n        start(8080).waitSync();\n    }\n\n    public static HttpServer start(int port) {\n        ServerConfig config = new ServerConfig(\"src/test/java/ui/html\")\n                .autoCreateSession(true);\n        return HttpServer.config(config).port(port).build();\n    }\n\n}\n"
  },
  {
    "path": "examples/ui-test/src/test/java/ui/UiRunner.java",
    "content": "package ui;\n\nimport com.intuit.karate.http.HttpServer;\nimport com.intuit.karate.junit5.Karate;\nimport org.junit.jupiter.api.BeforeAll;\n\nclass UiRunner {\n    \n    @BeforeAll\n    public static void beforeAll() {\n        HttpServer server = MockRunner.start(0);\n        System.setProperty(\"web.url.base\", \"http://localhost:\" + server.getPort());        \n    }\n    \n    @Karate.Test\n    Karate testUi() {\n        return Karate.run(\"classpath:ui/test.feature\");\n    }\n    \n}\n"
  },
  {
    "path": "examples/ui-test/src/test/java/ui/html/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Index</title>\n  </head>\n  <body>\n    <p><a href=\"/page-01\">Page 01</a></p>\n  </body>\n</html>\n\n\n"
  },
  {
    "path": "examples/ui-test/src/test/java/ui/html/karate.js",
    "content": "var karate = {};\nkarate.get = function(id) { return document.getElementById(id) };\nkarate.setHtml = function(id, value) { this.get(id).innerHTML = value };\n"
  },
  {
    "path": "examples/ui-test/src/test/java/ui/html/page-01.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page One</title>\n    <style>\n        .foo { color: blue };\n    </style>\n    <script src=\"karate.js\"></script>\n    <script>       \n        function setPlaceholder(){ karate.setHtml('placeholder', 'After') }\n        // simulate an element that appears in the DOM only after 5 seconds\n        setTimeout(function(){ karate.setHtml('slowparent', '<div id=\"slowelement\">APPEARED!</div>') }, 5000);\n    </script>\n  </head>\n  <body>\n      <button onclick=\"setPlaceholder()\">Click Me</button>\n      <div id=\"placeholder\" class=\"foo\">Before</div>\n      <div id=\"slowparent\">Waiting</div>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/ui-test/src/test/java/ui/test.feature",
    "content": "Feature: ui test\n\nScenario Outline: <type>\n  * def webUrlBase = karate.properties['web.url.base']\n  * configure driver = { type: '#(type)', showDriverLog: true }\n\n  * driver webUrlBase + '/page-01'\n  * match text('#placeholder') == 'Before'\n  * click('{}Click Me')\n  * match text('#placeholder') == 'After'\n\nExamples:\n| type         |\n| chrome       |\n#| chromedriver |\n#| geckodriver  |\n#| safaridriver |\n"
  },
  {
    "path": "examples/zip-release/src/demo/api/httpbin.feature",
    "content": "Feature: simple requests\n\nScenario: simple sequence\nGiven url 'https://httpbin.org/anything'\nAnd request { myKey: 'myValue' }\nWhen method post\nThen status 200\nAnd match response contains { json: { myKey: 'myValue' } }\n\n* path response.json.myKey\n* method get\n* status 200\n"
  },
  {
    "path": "examples/zip-release/src/demo/api/users.feature",
    "content": "Feature: sample karate api test script\n\n  Background:\n    * url 'https://jsonplaceholder.typicode.com'\n\n  Scenario: get all users and then get the first user by id\n    Given path 'users'\n    When method get\n    Then status 200\n\n    * def first = response[0]\n\n    Given path 'users', first.id\n    When method get\n    Then status 200\n\n  Scenario: create a user and then get it by id\n    * def user =\n      \"\"\"\n      {\n        \"name\": \"Test User\",\n        \"username\": \"testuser\",\n        \"email\": \"test@user.com\",\n        \"address\": {\n          \"street\": \"Has No Name\",\n          \"suite\": \"Apt. 123\",\n          \"city\": \"Electri\",\n          \"zipcode\": \"54321-6789\"\n        }\n      }\n      \"\"\"\n\n    Given url 'https://jsonplaceholder.typicode.com/users'\n    And request user\n    When method post\n    Then status 201\n\n    * def id = response.id\n    * print 'created id is: ', id\n\n    Given path id\n    # When method get\n    # Then status 200\n    # And match response contains user\n  "
  },
  {
    "path": "examples/zip-release/src/demo/mock/cats-mock.feature",
    "content": "@ignore\nFeature: stateful mock server\n\nBackground:\n* configure cors = true\n* def uuid = function(){ return java.util.UUID.randomUUID() + '' }\n* def cats = {}\n\nScenario: pathMatches('/cats') && methodIs('post')\n    * def cat = request\n    * def id = uuid()\n    * cat.id = id\n    * cats[id] = cat\n    * def response = cat\n\nScenario: pathMatches('/cats')\n    * def response = $cats.*\n\nScenario: pathMatches('/cats/{id}')\n    * def response = cats[pathParams.id]\n\nScenario: pathMatches('/hardcoded')\n    * def response = { hello: 'world' }\n\nScenario:\n    # catch-all\n    * def responseStatus = 404\n    * def responseHeaders = { 'Content-Type': 'text/html; charset=utf-8' }\n    * def response = <html><body>Not Found</body></html>\n"
  },
  {
    "path": "examples/zip-release/src/demo/mock/cats-test.feature",
    "content": "Feature: integration test for the mock\n\nBackground:\n    * def port = karate.env == 'mock' ? karate.start('cats-mock.feature').port : 8080\n    * url 'http://localhost:' + port + '/cats'\n\nScenario: create cat\n    Given request { name: 'Billie' }\n    When method post\n    Then status 200    \n    And match response == { id: '#uuid', name: 'Billie' }\n    And def id = response.id\n\n    Given path id\n    When method get\n    Then status 200\n    And match response == { id: '#(id)', name: 'Billie' }\n\n    When method get\n    Then status 200\n    And match response contains [{ id: '#(id)', name: 'Billie' }]\n\n    Given request { name: 'Bob' }\n    When method post\n    Then status 200    \n    And match response == { id: '#uuid', name: 'Bob' }\n    And def id = response.id\n\n    Given path id\n    When method get\n    Then status 200\n    And match response == { id: '#(id)', name: 'Bob' }\n\n    When method get\n    Then status 200\n    And match response contains [{ id: '#uuid', name: 'Billie' },{ id: '#(id)', name: 'Bob' }]\n"
  },
  {
    "path": "examples/zip-release/src/demo/mock/cats.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Cats</title>\n    <script src=\"https://code.jquery.com/jquery-3.2.1.min.js\" integrity=\"sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=\" crossorigin=\"anonymous\"></script>\n  </head>\n  <body>\n    <div>\n      <a href=\"http://localhost:8080/cats\">GET /cats</a> | <a href=\"http://localhost:8080/hardcoded\">GET /hardcoded</a>\n    </div>\n    <p/>    \n    <div>\n      <label for=\"catName\">Name: <input id=\"catName\" value=\"Billie\"/></label>\n      <button onclick=\"createCat()\">Create Cat</button>\n    </div>    \n    <p/>\n    <div id=\"response\"></div>\n    <script>\n      var createCat = function() {\n        var name = $('#catName').val();\n        $.ajax({          \n          url: 'http://localhost:8080/cats',\n          data: JSON.stringify({ name: name }),\n          method: 'POST',\n          success: function(result) {\n            $('#response').html('created cat: ' + JSON.stringify(result));\n          }\n        });\n      };\n    </script>    \n  </body>\n</html>\n\n"
  },
  {
    "path": "examples/zip-release/src/demo/robot/calc-old.feature",
    "content": "Feature: windows calculator\n\tdepending on your version of windows, if \"calc.feature\" doesn't work, try this one instead\n\talso refer docs: https://github.com/karatelabs/karate/wiki/ZIP-Release#karate-robot \n\nScenario:\n* robot { window: 'Calculator', fork: 'calc', highlight: true, highlightDuration: 500 }\n* click('Clear')\n* click('1')\n* click('Add')\n* click('2')\n* click('Equals')\n* match locate('//text{3}').name == '3'\n* screenshot()\n"
  },
  {
    "path": "examples/zip-release/src/demo/robot/calc.feature",
    "content": "Feature: windows calculator\n\tdid you know that Karate can do Windows desktop / app automation ?\n\tyou are almost set - just download one more JAR file\n\tinstructions here: https://github.com/karatelabs/karate/wiki/ZIP-Release#karate-robot\n\nScenario:\n* robot { window: 'Calculator', fork: 'calc', highlight: true, highlightDuration: 500 }\n* click('Clear')\n* click('One')\n* click('Plus')\n* click('Two')\n* click('Equals')\n* match locate('#CalculatorResults').name == 'Display is 3'\n* screenshot()\n* click('Close Calculator')"
  },
  {
    "path": "examples/zip-release/src/demo/web/google.feature",
    "content": "Feature: web-browser automation\n\nBackground:\n   * configure driver = { type: 'chrome' }\n\nScenario: try to login to github\n    and then do a google search\n\n  Given driver 'https://github.com/login'\n  And input('#login_field', 'dummy')\n  And input('#password', 'world')\n  When submit().click(\"input[name=commit]\")\n  Then match html('.flash-error') contains 'Incorrect username or password.'\n  \n  Given driver 'https://google.com'\n  And input(\"textarea[name=q]\", 'karate dsl')\n  When submit().click(\"input[name=btnI]\")\n  # this may fail depending on which part of the world you are in !\n  Then waitForUrl('https://github.com/karatelabs/karate')\n"
  },
  {
    "path": "jbang-catalog.json",
    "content": "{\n  \"catalogs\": {},\n  \"aliases\": {\n    \"karate\": {\n      \"script-ref\": \"io.karatelabs:karate-core:1.5.0:all\"\n    }\n  }\n}"
  },
  {
    "path": "karate-archetype/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n \n    <parent>\n        <groupId>io.karatelabs</groupId>\n        <artifactId>karate-parent</artifactId>\n        <version>1.5.2</version>\n    </parent>\n    <artifactId>karate-archetype</artifactId>\n    <packaging>jar</packaging>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.sonatype.central</groupId>\n                <artifactId>central-publishing-maven-plugin</artifactId>\n                <version>${central.publishing.maven.plugin.version}</version>\n                <configuration>\n                    <skipPublishing>true</skipPublishing>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n    \n</project>\n"
  },
  {
    "path": "karate-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml",
    "content": "<archetype-descriptor name=\"karate-archetype\">\n\n    <fileSets>\n        <fileSet filtered=\"false\" packaged=\"false\" encoding=\"UTF-8\">\n            <directory>src</directory>\n            <includes>\n                <include>**/**</include>\n            </includes>\n        </fileSet>\n    </fileSets>\n  \n</archetype-descriptor>"
  },
  {
    "path": "karate-archetype/src/main/resources/archetype-resources/.gitignore",
    "content": ".DS_Store\n.vscode\n.idea\n.project\n.settings\n.classpath\n*.iml\ntarget/\n"
  },
  {
    "path": "karate-archetype/src/main/resources/archetype-resources/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n \n    <groupId>${groupId}</groupId>\n    <artifactId>${artifactId}</artifactId>\n    <version>${version}</version>\n    <packaging>jar</packaging>\n \n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <java.version>17</java.version>\n        <maven.compiler.version>3.11.0</maven.compiler.version>\n        <maven.surefire.version>3.0.0</maven.surefire.version>\n        <karate.version>1.5.2</karate.version>\n    </properties>    \n\n    <dependencies>         \n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-junit5</artifactId>\n            <version>${karate.version}</version>\n            <scope>test</scope>\n        </dependency>\t\t\n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven.compiler.version}</version>\n                <configuration>\n                    <encoding>UTF-8</encoding>\n                    <source>${java.version}</source>\n                    <target>${java.version}</target>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>${maven.surefire.version}</version>\n                <configuration>\n                    <argLine>-Dfile.encoding=UTF-8</argLine>\n                </configuration>\n            </plugin>          \n        </plugins>        \n    </build>       \n    \n</project>"
  },
  {
    "path": "karate-archetype/src/main/resources/archetype-resources/src/test/java/examples/ExamplesTest.java",
    "content": "package examples;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nclass ExamplesTest {\n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:examples\")\n                //.outputCucumberJson(true)\n                .parallel(5);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-archetype/src/main/resources/archetype-resources/src/test/java/examples/users/UsersRunner.java",
    "content": "package examples.users;\n\nimport com.intuit.karate.junit5.Karate;\n\nclass UsersRunner {\n    \n    @Karate.Test\n    Karate testUsers() {\n        return Karate.run(\"users\").relativeTo(getClass());\n    }    \n\n}\n"
  },
  {
    "path": "karate-archetype/src/main/resources/archetype-resources/src/test/java/examples/users/users.feature",
    "content": "Feature: sample karate test script\n  for help, see: https://github.com/karatelabs/karate/wiki/IDE-Support\n\n  Background:\n    * url 'https://jsonplaceholder.typicode.com'\n\n  Scenario: get all users and then get the first user by id\n    Given path 'users'\n    When method get\n    Then status 200\n\n    * def first = response[0]\n\n    Given path 'users', first.id\n    When method get\n    Then status 200\n\n  Scenario: create a user and then get it by id\n    * def user =\n      \"\"\"\n      {\n        \"name\": \"Test User\",\n        \"username\": \"testuser\",\n        \"email\": \"test@user.com\",\n        \"address\": {\n          \"street\": \"Has No Name\",\n          \"suite\": \"Apt. 123\",\n          \"city\": \"Electri\",\n          \"zipcode\": \"54321-6789\"\n        }\n      }\n      \"\"\"\n\n    Given url 'https://jsonplaceholder.typicode.com/users'\n    And request user\n    When method post\n    Then status 201\n\n    * def id = response.id\n    * print 'created id is: ', id\n\n    Given path id\n    # When method get\n    # Then status 200\n    # And match response contains user\n  "
  },
  {
    "path": "karate-archetype/src/main/resources/archetype-resources/src/test/java/karate-config.js",
    "content": "function fn() {\n  var env = karate.env; // get system property 'karate.env'\n  karate.log('karate.env system property was:', env);\n  if (!env) {\n    env = 'dev';\n  }\n  var config = {\n    env: env,\n    myVarName: 'someValue'\n  }\n  if (env == 'dev') {\n    // customize\n    // e.g. config.foo = 'bar';\n  } else if (env == 'e2e') {\n    // customize\n  }\n  return config;\n}"
  },
  {
    "path": "karate-archetype/src/main/resources/archetype-resources/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit\" level=\"DEBUG\"/>\n   \n    <root level=\"info\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>"
  },
  {
    "path": "karate-core/README.md",
    "content": "# Karate UI\n## UI Test Automation Made `Simple.`\n\n### Hello World\n\n<a href=\"https://twitter.com/getkarate/status/1167533484560142336\"><img src=\"src/test/resources/driver-hello-world.jpg\" height=\"230\"/></a>\n\n# Index\n\n<table>\n<tr>\n  <th>Start</th>\n  <td>\n      <a href=\"https://github.com/karatelabs/karate/wiki/IDE-Support\">IDE Support</a>\n    | <a href=\"https://github.com/karatelabs/karate#quickstart\">Maven Quickstart</a>\n    | <a href=\"https://github.com/karatelabs/karate#index\">Karate - Main Index</a>\n  </td>\n</tr>\n<tr>\n  <th>Config</th>\n  <td>\n      <a href=\"#driver\"><code>driver</code></a>\n    | <a href=\"#configure-driver\"><code>configure driver</code></a>\n    | <a href=\"#configure-drivertarget\"><code>configure driverTarget</code></a>\n    | <a href=\"#karate-chrome\">Docker / <code>karate-chrome</code></a>\n    | <a href=\"#driver-types\">Driver Types</a> \n    | <a href=\"#playwright\">Playwright</a> \n    | <a href=\"#playwright-legacy\">Playwright Legacy</a> \n    | <a href=\"#timeout\"><code>timeout()</code></a>\n    | <a href=\"#driversessionid\"><code>driver.sessionId</code></a>\n  </td>\n</tr>\n<tr>\n  <th>Concepts</th>\n  <td>\n      <a href=\"#syntax\">Syntax</a>\n    | <a href=\"#special-keys\">Special Keys</a>\n    | <a href=\"#short-cuts\">Short Cuts</a>\n    | <a href=\"#chaining\">Chaining</a> \n    | <a href=\"#function-composition\">Function Composition</a>\n    | <a href=\"#script\">Browser JavaScript</a>\n    | <a href=\"#looping\">Looping</a>\n    | <a href=\"#drop-downs\">Drop Downs</a>\n    | <a href=\"#debugging\">Debugging</a>\n    | <a href=\"#retry\">Retries</a>\n    | <a href=\"#wait-api\">Waits</a>\n    | <a href=\"#proxy\">Proxy</a>\n    | <a href=\"#intercepting-http-requests\">Intercepting HTTP Requests</a>\n    | <a href=\"#file-upload\">File Upload</a>\n    | <a href=\"#code-reuse\">Code Reuse</a>\n    | <a href=\"#hybrid-tests\">Hybrid Tests</a>\n    | <a href=\"#java-api\">Java API</a>\n    | <a href=\"#visual-testing\">Visual Testing</a>\n  </td>\n</tr>\n<tr>\n  <th>Locators</th>\n  <td>\n      <a href=\"#locators\">Locator Types</a>\n    | <a href=\"#wildcard-locators\">Wildcards</a> \n    | <a href=\"#friendly-locators\">Friendly Locators</a> \n    | <a href=\"#tree-walking\">Tree Walking</a>\n    | <a href=\"#rightof\"><code>rightOf()</code></a>\n    | <a href=\"#leftOf\"><code>leftOf()</code></a>\n    | <a href=\"#above\"><code>above()</code></a>\n    | <a href=\"#below\"><code>below()</code></a>\n    | <a href=\"#near\"><code>near()</code></a>\n    | <a href=\"#locator-lookup\">Locator Lookup</a>\n  </td>\n</tr>\n<tr>\n  <th>Browser</th>\n  <td>\n      <a href=\"#driverurl\"><code>driver.url</code></a>\n    | <a href=\"#driverdimensions\"><code>driver.dimensions</code></a>\n    | <a href=\"#refresh\"><code>refresh()</code></a>\n    | <a href=\"#reload\"><code>reload()</code></a> \n    | <a href=\"#back\"><code>back()</code></a>\n    | <a href=\"#forward\"><code>forward()</code></a>\n    | <a href=\"#maximize\"><code>maximize()</code></a>\n    | <a href=\"#minimize\"><code>minimize()</code></a>\n    | <a href=\"#fullscreen\"><code>fullscreen()</code></a>\n    | <a href=\"#quit\"><code>quit()</code></a>\n  </td>\n</tr>\n<tr>\n  <th>Page</th>\n  <td>\n      <a href=\"#dialog\"><code>dialog()</code></a>    \n    | <a href=\"#switchpage\"><code>switchPage()</code></a>\n    | <a href=\"#switchFrame\"><code>switchFrame()</code></a> \n    | <a href=\"#close\"><code>close()</code></a>    \n    | <a href=\"#drivertitle\"><code>driver.title</code></a>\n    | <a href=\"#screenshot\"><code>screenshot()</code></a>    \n    | <a href=\"#pdf\"><code>pdf()</code></a>    \n  </td>\n</tr>\n<tr>\n  <th>Actions</th>\n  <td>\n      <a href=\"#click\"><code>click()</code></a>\n    | <a href=\"#input\"><code>input()</code></a>\n    | <a href=\"#submit\"><code>submit()</code></a>\n    | <a href=\"#focus\"><code>focus()</code></a>\n    | <a href=\"#clear\"><code>clear()</code></a>\n    | <a href=\"#valueset\"><code>value(set)</code></a>   \n    | <a href=\"#select\"><code>select()</code></a>\n    | <a href=\"#scroll\"><code>scroll()</code></a>\n    | <a href=\"#mouse\"><code>mouse()</code></a>\n    | <a href=\"#highlight\"><code>highlight()</code></a>\n    | <a href=\"#highlightall\"><code>highlightAll()</code></a>\n  </td>\n</tr>\n<tr>\n  <th>State</th>\n  <td>\n      <a href=\"#html\"><code>html()</code></a>\n    | <a href=\"#text\"><code>text()</code></a>\n    | <a href=\"#value\"><code>value()</code></a>\n    | <a href=\"#attribute\"><code>attribute()</code></a>\n    | <a href=\"#enabled\"><code>enabled()</code></a>\n    | <a href=\"#exists\"><code>exists()</code></a>\n    | <a href=\"#optional\"><code>optional()</code></a>    \n    | <a href=\"#locate\"><code>locate()</code></a>    \n    | <a href=\"#locateall\"><code>locateAll()</code></a>\n    | <a href=\"#position\"><code>position()</code></a>\n  </td>\n</tr>\n<tr>\n  <th>Wait / JS</th>\n  <td>\n      <a href=\"#retry\"><code>retry()</code></a>\n    | <a href=\"#waitfor\"><code>waitFor()</code></a>    \n    | <a href=\"#waitforany\"><code>waitForAny()</code></a>\n    | <a href=\"#waitforurl\"><code>waitForUrl()</code></a>\n    | <a href=\"#waitfortext\"><code>waitForText()</code></a>\n    | <a href=\"#waitforenabled\"><code>waitForEnabled()</code></a>\n    | <a href=\"#waitforresultcount\"><code>waitForResultCount()</code></a>  \n    | <a href=\"#waituntil\"><code>waitUntil()</code></a>    \n    | <a href=\"#delay\"><code>delay()</code></a>\n    | <a href=\"#script\"><code>script()</code></a>\n    | <a href=\"#scriptall\"><code>scriptAll()</code></a>\n    | <a href=\"#karate-vs-the-browser\">Karate vs the Browser</a>\n  </td>\n</tr>\n<tr>\n  <th>Cookies</th>\n  <td>\n      <a href=\"#cookie\"><code>cookie()</code></a>\n      <a href=\"#cookieset\"><code>cookie(set)</code></a>\n    | <a href=\"#drivercookies\"><code>driver.cookies</code></a>\n    | <a href=\"#deletecookie\"><code>deleteCookie()</code></a>\n    | <a href=\"#clearcookies\"><code>clearCookies()</code></a>\n  </td>\n</tr>\n<tr>\n  <th>Chrome</th>\n  <td>\n      <a href=\"#java-api\">Java API</a>\n    | <a href=\"#driverpdf\"><code>driver.pdf()</code></a>\n    | <a href=\"#driverscreenshotfull\"><code>driver.screenshotFull()</code></a>\n    | <a href=\"#driverintercept\"><code>driver.intercept()</code></a>\n    | <a href=\"#driverinputfile\"><code>driver.inputFile()</code></a>\n    | <a href=\"#driveremulatedevice\"><code>driver.emulateDevice()</code></a>\n    | <a href=\"#driverscriptawait\"><code>driver.scriptAwait()</code></a> \n  </td> \n</tr>\n<tr>\n  <th>Appium</th>\n  <td>\n      <a href=\"#screen-recording\">Screen Recording</a>\n    | <a href=\"#screen-recording\"><code>driver.startRecordingScreen()</code></a>\n    | <a href=\"#screen-recording\"><code>driver.saveRecordingScreen()</code></a>\n    | <a href=\"#driverhidekeyboard\"><code>driver.hideKeyboard()</code></a>\n  </td> \n</tr>\n</table>\n\n## Capabilities\n\n* Simple, clean syntax that is well suited for people new to programming or test-automation\n* All-in-one framework that includes [parallel-execution](https://github.com/karatelabs/karate#parallel-execution), [HTML reports](https://github.com/karatelabs/karate#junit-html-report), [environment-switching](https://github.com/karatelabs/karate#switching-the-environment), [Visual Testing](#visual-testing), and [CI integration](https://github.com/karatelabs/karate#test-reports)\n* Cross-platform - with even the option to run as a programming-language *neutral* [stand-alone executable](https://github.com/karatelabs/karate/wiki/ZIP-Release)\n* Support for [`iframe`-s](#switchframe), [switching tabs](#switchpage), multiple URL domains, and [uploading files](#driverinputfile)\n* No need to learn complicated programming concepts such as \"callbacks\", \"`async` / `await`\" and \"promises\"\n* Option to use [wildcard](#wildcard-locators) and [\"friendly\" locators](#friendly-locators) without needing to inspect the HTML-page source, CSS, or internal XPath structure\n* Chrome-native automation using the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) (equivalent to [Puppeteer](https://pptr.dev))\n* [W3C WebDriver](https://w3c.github.io/webdriver/) support built-in, which can also use [remote / grid providers](https://twitter.com/ptrthomas/status/1222790566598991873)\n* [Cross-Browser support](https://twitter.com/ptrthomas/status/1048260573513666560) including [Microsoft Edge on Windows](https://twitter.com/ptrthomas/status/1046459965668388866) and [Safari on Mac](https://twitter.com/ptrthomas/status/1047152170468954112)\n* [Playwright](https://playwright.dev) support (experimental) for even more cross-browser and headless options, that can connect to a server or Docker container using the Playwright wire-protocol\n* [Parallel execution on a single node](https://twitter.com/ptrthomas/status/1159295560794308609), cloud-CI environment or [Docker](#configure-drivertarget) - without needing a \"master node\" or \"grid\"\n* Embed [video-recordings of tests](#karate-chrome) into the HTML report from a Docker container\n* [experimental] [Android and iOS mobile support](https://github.com/karatelabs/karate/issues/743) via [Appium](http://appium.io)\n* Seamlessly mix API and UI tests within the same script, for example [sign-in using an API](https://github.com/karatelabs/karate#http-basic-authentication-example) and speed-up your tests\n* [Intercept HTTP requests](#intercepting-http-requests) made by the browser and re-use [Karate mocks](https://github.com/karatelabs/karate/tree/master/karate-netty) to stub / modify server responses and even replace HTML content\n* Use the power of Karate's [`match`](https://github.com/karatelabs/karate#prepare-mutate-assert) assertions and [core capabilities](https://github.com/karatelabs/karate#features) for UI assertions\n* Simple [retry](#retry) and [wait](#wait-api) strategy, no need to graduate from any test-automation university to understand the difference between \"implicit waits\", \"explicit waits\" and \"fluent waits\" :)\n* Simpler, [elegant, and *DRY* alternative](#locator-lookup) to the so-called \"Page Object Model\" pattern\n* Carefully designed [fluent-API](#chaining) to handle common combinations such as a [`submit()` + `click()`](#submit) action\n* Elegant syntax for typical web-automation challenges such as waiting for a [page-load](#waitforurl-instead-of-submit) or [element to appear](#waitfor)\n* Execute JavaScript in the browser with [one-liners](#script) - for example to [get data out of an HTML table](#scriptall)\n* [Compose re-usable functions](#function-composition) based on your specific environment or application needs\n* Comprehensive support for user-input types including [key-combinations](#special-keys) and [`mouse()`](#mouse) actions\n* Step-debug and even *\"go back in time\"* to edit and re-play steps - using the unique, innovative [Karate Extension for Visual Studio Code](https://twitter.com/KarateDSL/status/1167533484560142336)\n* Traceability: detailed [wire-protocol logs](https://twitter.com/ptrthomas/status/1155958170335891467) can be enabled *in-line* with test-steps in the HTML report\n* Convert HTML to PDF and capture the *entire* (scrollable) web-page as an image using the [Chrome Java API](#chrome-java-api)\n\n## Comparison\nTo understand how Karate compares to other UI automation frameworks, this article can be a good starting point: [The world needs an alternative to Selenium - *so we built one*](https://hackernoon.com/the-world-needs-an-alternative-to-selenium-so-we-built-one-zrk3j3nyr).\n\n# Examples\n## Web Browser\n* [Example 1](../karate-demo/src/test/java/driver/demo/demo-01.feature) - simple example that navigates to GitHub and Google Search\n* [Example 2](../karate-demo/src/test/java/driver/demo/demo-02.feature) - simple but *very* relevant and meaty example ([see video](https://twitter.com/ptrthomas/status/1160680240781262851)) that shows how to\n  * wait for [page-navigation](#waitforurl-instead-of-submit)\n  * use a friendly [wildcard locator](#wildcard-locators)\n  * wait for an element to [be ready](#waitfor)\n  * [compose functions](#function-composition) for elegant *custom* \"wait\" logic\n  * assert on tabular [results in the HTML](#scriptall)\n* [Example 3](../karate-e2e-tests/src/test/java/driver/00.feature) - which is a single modular script that exercises *all* capabilities of Karate Driver\n  * a handy reference that can give you ideas on how to structure your tests\n  * run as part of Karate's [regression suite](https://stackoverflow.com/a/66005331/143475) via GitHub Actions\n\n## Mobile / Appium\n> Please consider Mobile support as experimental. But we are very close and there are some teams that use Karate for simple use-cases. Please contribute code if you can.\n* Refer to this [example project](../examples/mobile-test)\n\n## Windows\n* [Example](../karate-demo/src/test/java/driver/windows/calc.feature) - but also see the [`karate-robot`](https://github.com/karatelabs/karate/tree/master/karate-robot) for an alternative approach.\n\n# Driver Configuration\n\n## `configure driver`\n\nThis below declares that the native (direct) Chrome integration should be used, on both Mac OS and Windows - from the default installed location.\n\n```cucumber\n* configure driver = { type: 'chrome' }\n```\n\nIf you want to customize the start-up, you can use a batch-file:\n\n```cucumber\n* configure driver = { type: 'chrome', executable: 'chrome' }\n```\n\nHere a batch-file called `chrome` can be placed in the system [`PATH`](https://www.java.com/en/download/help/path.xml) (and made executable) with the following contents:\n\n```bash\n\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\" $*\n```\n\nFor Windows it would be `chrome.bat` in the system [`PATH`](https://www.java.com/en/download/help/path.xml) as follows:\n\n```bat\n\"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome\" %*\n```\n\nAnother example for WebDriver, again assuming that `chromedriver` is in the [`PATH`](https://www.java.com/en/download/help/path.xml):\n\n```cucumber\n{ type: 'chromedriver', port: 9515, executable: 'chromedriver' }\n```\n\nkey | description\n--- | -----------\n`type` | see [driver types](#driver-types)\n`executable` | if present, Karate will attempt to invoke this, if not in the system [`PATH`](https://www.java.com/en/download/help/path.xml), you can use a full-path instead of just the name of the executable. batch files should also work\n`start` | default `true`, Karate will attempt to start the `executable` - and if the `executable` is not defined, Karate will even try to assume the default for the OS in use\n`stop` | optional, defaults to `true` *very* rarely needed, only in cases where you want the browser to remain open after your tests have completed, typically when you write a custom [`Target`](#custom-target)\n`port` | optional, and Karate would choose the \"traditional\" port for the given `type`\n`host` | optional, will default to `localhost` and you normally never need to change this\n`pollAttempts` | optional, will default to `20`, you normally never need to change this (and changing `pollInterval` is preferred), and this is the number of attempts Karate will make to wait for the `port` to be ready and accepting connections before proceeding\n`pollInterval` | optional, will default to `250` (milliseconds) and you normally never need to change this (see `pollAttempts`) unless the driver `executable` takes a *very* long time to start\n`headless` | [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome) only applies to `{ type: 'chrome' }` for now, also see [`DockerTarget`](#dockertarget) and [`webDriverSession`](#webdriversession)\n`showDriverLog` | default `false`, will include webdriver HTTP traffic in Karate report, useful for troubleshooting or bug reports\n`showProcessLog` | default `false`, will include even executable (webdriver or browser) logs in the Karate report\n`showBrowserLog` | default `true`, only applies to `{ type: 'chrome' }` which shows the browser console log in the HTML report for convenience\n`addOptions` | default `null`, has to be a list / JSON array that will be appended as additional CLI arguments to the `executable`, e.g. `['--no-sandbox', '--windows-size=1920,1080']`\n`beforeStart` | default `null`, an OS command that will be executed before commencing a `Scenario` (and before the `executable` is invoked if applicable) typically used to start video-recording\n`afterStop` | default `null`, an OS command that will be executed after a `Scenario` completes, typically used to stop video-recording and save the video file to an output folder\n`videoFile` | default `null`, the path to the video file that will be added to the end of the test report, if it does not exist, it will be ignored\n`httpConfig` | optional, and typically only used for remote WebDriver usage where the HTTP client [configuration](https://github.com/karatelabs/karate#configure) needs to be tweaked, e.g. `{ readTimeout: 120000 }` (also see `timeout` below)\n`timeout` | default `30000`,  amount of time (in milliseconds) that type `chrome` will wait for an operation that takes time (typically navigating to a new page) - and as a convenience for WebDriver, this will be equivalent to setting the `readTimeout` for the `httpConfig` (see above) - also see [`timeout()`](#timeout)\n`playwrightUrl` | only applies for `{ type: 'playwright', start: false }`, the [Playwright](https://playwright.dev) wire-protocol (websockets) server URL, also see [`playwrightOptions`](#playwrightoptions)\n`playwrightOptions` | optional, see [`playwrightOptions`](#playwrightoptions)\n`webDriverUrl` | see [`webDriverUrl`](#webdriverurl)\n`webDriverSession` | see [`webDriverSession`](#webdriversession)\n`webDriverPath` | optional, and rarely used only in case you need to append a path such as `/wd/hub` - typically needed for Appium (or a Selenium Grid) on `localhost`, where `host`, `port` / `executable` etc. are involved.\n`highlight` | default `false`, useful for demos or for running a test in \"slow motion\" where before each navigation action, the HTML element for the current [locator](#locators) is highlighted for a duration of `highlightDuration`\n`highlightDuration` | default 3000 (milliseconds), duration to apply the `highlight`\n`attach` | optional, only for `type: 'chrome'` and `start: false` when you want to attach to an existing page in a Chrome DevTools session, uses a \"contains\" match against the URL\n`userDataDir` | optional, by default Karate will auto-create a [user dir](https://chromium.googlesource.com/chromium/src.git/+/master/docs/user_data_dir.md) for Chrome and other browsers, but if you want to provide the path to an existing folder (which can reduce disk space usage in some situations), note that for Chrome, this will pass the command line option `--user-data-dir`. if `null`, Chrome will use the system defaults (the `--user-data-dir` command-line option will *not* be passed)\n\nFor more advanced options such as for Docker, CI, headless, cloud-environments or custom needs, see [`configure driverTarget`](#configure-drivertarget).\n\n## webDriverUrl\nKarate implements the [W3C WebDriver spec](https://w3c.github.io/webdriver), which means that you can point Karate to a remote \"grid\" such as [Zalenium](https://opensource.zalando.com/zalenium/) or a SaaS provider such as [the AWS Device Farm](https://docs.aws.amazon.com/devicefarm/latest/testgrid/what-is-testgrid.html). The `webDriverUrl` driver configuration key is optional, but if specified, will be used as the W3C WebDriver remote server. Note that you typically would set `start: false` as well, or use a [Custom `Target`](#custom-target).\n\nFor example, once you run the [couple of Docker commands](https://opensource.zalando.com/zalenium/#try-it) to get Zalenium running, you can do this:\n\n```cucumber\n* configure driver = { type: 'chromedriver', start: false, webDriverUrl: 'http://localhost:4444/wd/hub' }\n```\n\nNote that you can add `showDriverLog: true` to the above for troubleshooting if needed. You should be able to [run tests in parallel](https://github.com/karatelabs/karate#parallel-execution) with ease !\n\n## `webDriverSession`\nWhen targeting a W3C WebDriver implementation, either as a local executable or [Remote WebDriver](https://selenium.dev/documentation/en/remote_webdriver/remote_webdriver_client/), you can specify the JSON that will be passed as the payload to the [Create Session](https://w3c.github.io/webdriver/#new-session) API. The most important part of this payload is the [`capabilities`](https://w3c.github.io/webdriver/#capabilities). It will default to `{ browserName: '<name>' }` for convenience where `<name>` will be `chrome`, `firefox` etc.\n\nSo most of the time this would be sufficient:\n\n```cucumber\n* configure driver = { type: 'chromedriver' }\n```\n\nSince it will result in the following request to the WebDriver `/session`:\n\n```js\n{\"capabilities\":{\"alwaysMatch\":{\"browserName\":\"chrome\"}}}\n```\n\nBut in some cases, especially when you need to talk to remote driver instances, you need to pass specific \"shapes\" of JSON expected by the particular implementation - or you may need to pass custom data or \"extension\" properties. Use the `webDriverSession` property in those cases. \n\n> Note: `desiredCapabilities` has been deprecated and not recommended for use.\n\nFor example:\n\n```cucumber\n* def session = { capabilities: { alwaysMatch: { browserName: 'chrome', 'goog:chromeOptions': { args: [ '--headless', 'window-size=1280,720' ] } } } }\n* configure driver = { type: 'chromedriver', webDriverSession: '#(session)', start: false, webDriverUrl: 'http://localhost:9515/wd/hub' }\n```\n\nAnother example is that for the new Microsoft Edge browser (based on Chromium), the Karate default `alwaysMatch` is not supported, so this is what works:\n\n```cucumber\n* configure driver = { type: 'msedgedriver', webDriverSession: { capabilities: { browserName: 'edge' } } }\n```\n\nHere are some of the things that you can customize, but note that these depend on the driver implementation.\n\n* [`proxy`](#proxy)\n* [`acceptInsecureCerts`](https://w3c.github.io/webdriver/#dfn-insecure-tls-certificates)\n* [`moz:firefoxOptions`](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#firefoxOptions) - e.g. for headless FireFox\n\nNote that some capabilities such as \"headless\" may be possible via the command-line to the local executable, so using [`addOptions`](#configure-driver) may work instead.\n\nAlso see [`driver.sessionId`](#driversessionid).\n\n\n### Playwright\n\nDriver that leverages [Playwright](https://playwright.dev) java native APIs.\n\nTo use it, add the following dependency:\n```xml\n<dependency>\n    <groupId>io.karate</groupId>\n    <artifactId>karate-playwright</artifactId>\n    <scope>test</scope>\n</dependency>\n```\n\nAnd make sure it is declared before `io.karate:karate-core`.\n\nA server will be automatically started and made available to Karate without any extra-script. If you have one pre-started, you can still use the [`playwrightUrl`](#configure-driver) driver config.\n\n### `playwrightOptions`\nWhen using [Playwright](#playwright) you can omit this in which case Karate will default to Chrome (within Playwright) and the default browser window size.\n\nThis can take the following keys:\n* `browserType` - defaults to `chromium`, can be set to the other [types that Playwright supports](https://playwright.dev/docs/core-concepts#browser), e.g. `firefox` and `webkit`\n* `context` - JSON which will be passed as the argument of the Playwright [`browser.newContext()`](https://playwright.dev/docs/api/class-browser#browsernewcontextoptions) call, needed typically to set the page dimensions\n* `channel` - defaults to chrome, for the `chromium` browserType, allows to specify which [flavor](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge) to use\n* `installBrowsers` - defaults to `true`, whether or not all the supported browsers will be downloaded and installed by Playwright (once).  \n\nNote that there is a top-level config flag for `headless` mode. The default is: `* configure driver = { headless: false }`\n\n### Playwright Legacy\nTo use [Playwright](https://playwright.dev), you need to start a Playwright server. If you have one pre-started, you need to use the [`playwrightUrl`](#configure-driver) driver config.\n\nOr you can set up an executable that can do it and log the URL to the console when the server is ready. The websocket URL will look like this: `ws://127.0.0.1:4444/0e0bd1c0bb2d4eb550d02c91046dd6e0`.\n\nHere's a simple recipe to set up this mechanism on your local machine. NodeJS is a pre-requisite and you can choose a folder (e.g. `playwright`) for the \"start scripts\" to live. Within that folder, [you can run](https://playwright.dev/docs/intro#installation):\n\n```\nnpm i -D playwright\n```\n\nNow create a file called `playwright/server.js` with the following code:\n\n```js\nconst playwright = require('playwright');\n\nconst port = process.argv[2] || 4444;\nconst browserType = process.argv[3] || 'chromium';\nconst headless = process.argv[4] == 'true';\nconsole.log('using port:', port, 'browser:', browserType, 'headless:', headless);\n\nconst serverPromise = playwright[browserType].launchServer({ headless: headless, port: port });\nserverPromise.then(bs => console.log(bs.wsEndpoint()));\n```\n\nThe main thing here is that the server URL should be logged to the console when it starts. Karate will scan the log for any string that starts with `ws://` and kick things off from there.\n\nAlso Karate will call the executable with three arguments in this order:\n* `port`\n* `browserType`\n* `headless`\n\nSo this is how you can communicate your cross-browser config from your Karate test to the executable.\n\nThe final piece of the puzzle is to set up a batch file to start the server:\n\n```bash\n#!/bin/bash\nexec node /some/path/playwright/server.js $*\n```\n\nThe [`exec`](http://veithen.io/2014/11/16/sigterm-propagation.html) is important here so that Karate can stop the `node` process cleanly.\n\nNow you can use the path of the batch file in the driver `executable` config.\n\n```cucumber\n* configure driver = { type: 'playwright', executable: 'path/to/start-server' }\n```\n\nFor convenience, Karate assumes by default that the executable name is `playwright` and that it exists in the System [`PATH`](https://www.java.com/en/download/help/path.xml). Make sure that the batch file is made executable depending on your OS.\n\nBased on the above details, you should be able to come up with a custom strategy to connect Karate to Playwright. And you can consider a [`driverTarget`](#custom-target) approach for complex needs such as using a Docker container for CI.\n\n\n## `configure driverTarget`\nThe [`configure driver`](#configure-driver) options are fine for testing on \"`localhost`\" and when not in `headless` mode. But when the time comes for running your web-UI automation tests on a continuous integration server, things get interesting. To support all the various options such as Docker, headless Chrome, cloud-providers etc., Karate introduces the concept of a pluggable [`Target`](src/main/java/com/intuit/karate/driver/Target.java) where you just have to implement two methods:\n\n```java\npublic interface Target {        \n    \n    Map<String, Object> start(com.intuit.karate.core.ScenarioRuntime sr);\n    \n    Map<String, Object> stop(com.intuit.karate.core.ScenarioRuntime sr);\n    \n}\n```\n\n* `start()`: The `Map` returned will be used as the generated [driver configuration](#driver-configuration). And the `start()` method will be invoked as soon as any `Scenario` requests for a web-browser instance (for the first time) via the [`driver`](#driver) keyword.\n\n* `stop()`: Karate will call this method at the end of every top-level `Scenario` (that has not been `call`-ed by another `Scenario`).\n\nIf you use the provided `ScenarioRuntime.logger` instance in your `Target` code, any logging you perform will nicely appear in-line with test-steps in the HTML report, which is great for troubleshooting or debugging tests.\n\nCombined with Docker, headless Chrome and Karate's [parallel-execution capabilities](https://github.com/karatelabs/karate#parallel-execution) - this simple `start()` and `stop()` lifecycle can effectively run web UI automation tests in parallel on a single node.\n\n### `DockerTarget`\nKarate has a built-in implementation for Docker ([`DockerTarget`](src/main/java/com/intuit/karate/driver/DockerTarget.java)) that supports 2 existing Docker images out of the box:\n\n* [`justinribeiro/chrome-headless`](https://hub.docker.com/r/justinribeiro/chrome-headless/) - for Chrome \"native\" in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome)\n* [`karatelabs/karate-chrome`](#karate-chrome) - for Chrome \"native\" but with an option to connect to the container and view via VNC, and with video-recording\n\nTo use either of the above, you do this in a Karate test:\n\n```cucumber\n* configure driverTarget = { docker: 'justinribeiro/chrome-headless', showDriverLog: true }\n```\n\nOr for more flexibility, you could do this in [`karate-config.js`](https://github.com/karatelabs/karate#configuration) and perform conditional logic based on [`karate.env`](https://github.com/karatelabs/karate#switching-the-environment). One very convenient aspect of `configure driverTarget` is that *if* in-scope, it will over-ride any `configure driver` directives that exist. This means that you can have the below snippet activate *only* for your CI build, and you can leave your feature files set to point to what you would use in \"dev-local\" mode.\n\n```javascript\nfunction fn() {\n    var config = {\n        baseUrl: 'https://qa.mycompany.com'\n    };\n    if (karate.env == 'ci') {\n        karate.configure('driverTarget', { docker: 'karatelabs/karate-chrome' });\n    }\n    return config;\n}\n```\n\nTo use the [recommended `--security-opt seccomp=chrome.json` Docker option](https://hub.docker.com/r/justinribeiro/chrome-headless/), add a `secComp` property to the `driverTarget` configuration. And if you need to view the container display via VNC, set the `vncPort` to map the port exposed by Docker.\n\n```javascript\nkarate.configure('driverTarget', { docker: 'karatelabs/karate-chrome', secComp: 'src/test/java/chrome.json', vncPort: 5900 });\n```\n\n### Custom `Target`\nIf you have a custom implementation of a `Target`, you can easily [construct any custom Java class](https://github.com/karatelabs/karate#calling-java) and pass it to `configure driverTarget`. Here below is the equivalent of the above, done the \"hard way\":\n\n```javascript\nvar DockerTarget = Java.type('com.intuit.karate.driver.DockerTarget');\nvar options = { showDriverLog: true };\nvar target = new DockerTarget(options);\ntarget.command = function(port){ return 'docker run -d -p ' \n    + port + ':9222 --security-opt seccomp=./chrome.json justinribeiro/chrome-headless' };\nkarate.configure('driverTarget', target);\n```\n\nThe built-in [`DockerTarget`](src/main/java/com/intuit/karate/driver/DockerTarget.java) is a good example of how to:\n* perform any pre-test set-up actions\n* provision a free port and use it to shape the `start()` command dynamically\n* execute the command to start the target process\n* perform an HTTP health check to wait until we are ready to receive connections\n* and when `stop()` is called, indicate if a video recording is present (after retrieving it from the stopped container)\n\nControlling this flow from Java can take a lot of complexity out your build pipeline and keep things cross-platform. And you don't need to line-up an assortment of shell-scripts to do all these things. You can potentially include the steps of deploying (and un-deploying) the application-under-test using this approach - but probably the top-level [JUnit test-suite](https://github.com/karatelabs/karate#parallel-execution) would be the right place for those.\n\nIf the machine where you are running Karate is not the same as your target host (e.g. a sibling Docker container or a Chrome browser in a different machine) you might need to configure `DockerTarget` with the `remoteHost` and/or `useDockerHost` properties. The `DockerTarget` implementation has an example [and you can find more details here](https://github.com/karatelabs/karate/pull/1603#issuecomment-846420716).\n\nAnother (simple) example of a custom `Target` you can use as a reference is this one: [`karate-devicefarm-demo`](https://github.com/ptrthomas/karate-devicefarm-demo) - which demonstrates how Karate can be used to drive tests on [AWS DeviceFarm](https://docs.aws.amazon.com/devicefarm/latest/testgrid/what-is-testgrid.html). The same approach should apply to any Selenium \"grid\" provider such as [Zalenium](https://opensource.zalando.com/zalenium/).\n\n### `karate-chrome`\nThe [`karate-chrome`](https://hub.docker.com/r/karatelabs/karate-chrome) Docker is an image created from scratch, using a Java / Maven image as a base and with the following features:\n\n* Chrome in \"full\" mode (non-headless)\n* [Chrome DevTools protocol](https://chromedevtools.github.io/devtools-protocol/) exposed on port 9222\n* VNC server exposed on port 5900 so that you can watch the browser in real-time\n* a video of the entire test is saved to `/tmp/karate.mp4`\n* after the test, when `stop()` is called, the [`DockerTarget`](src/main/java/com/intuit/karate/driver/DockerTarget.java) will embed the video into the HTML report (expand the last step in the `Scenario` to view)\n\nTo try this or especially when you need to investigate why a test is not behaving properly when running within Docker, these are the steps:\n\n* start the container:\n  * `docker run --name karate --rm -p 9222:9222 -p 5900:5900 -e KARATE_SOCAT_START=true --cap-add=SYS_ADMIN karatelabs/karate-chrome`\n  * it is recommended to use [`--security-opt seccomp=chrome.json`](https://hub.docker.com/r/justinribeiro/chrome-headless/) instead of `--cap-add=SYS_ADMIN`\n* point your VNC client to `localhost:5900` (password: `karate`)\n  * for example on a Mac you can use this command: `open vnc://localhost:5900`\n* run a test using the following [`driver` configuration](#configure-driver), and this is one of the few times you would ever need to set the [`start` flag](#configure-driver) to `false`\n  * `* configure driver = { type: 'chrome', start: false, showDriverLog: true }`\n* you can even use the [Karate VS Code extension](https://github.com/karatelabs/karate/wiki/IDE-Support#vs-code-karate-plugin) to debug and step-through a test\n* if you omit the `--rm` part in the start command, after stopping the container, you can dump the logs and video recording using this command (here `.` stands for the current working folder, change it if needed):\n  * `docker cp karate:/tmp .`\n  * this would include the `stderr` and `stdout` logs from Chrome, which can be helpful for troubleshooting\n\nFor more information on the Docker containers for Karate and how to use them, refer to the wiki: [Docker](https://github.com/karatelabs/karate/wiki/Docker).\n\n## Driver Types\nThe recommendation is that you prefer `chrome` for development, and once you have the tests running smoothly - you can switch to a different WebDriver implementation.\n\ntype | default port | default executable | description\n---- | ------------ | ------------------ | -----------\n[`chrome`](https://chromedevtools.github.io/devtools-protocol/) | 9222 | mac: `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` <br/>win: `C:/Program Files (x86)/Google/Chrome/Application/chrome.exe` | \"native\" Chrome automation via the [DevTools protocol](https://chromedevtools.github.io/devtools-protocol/)\n[`playwright`](https://playwright.dev) | 4444 | `playwright` | see [`playwrightOptions`](#playwrightoptions) and [Playwright](#playwright)\n[`msedge`](https://docs.microsoft.com/en-us/microsoft-edge/) | 9222 | mac: `/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge` <br/>win: `C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe` | the new Chromium based Microsoft Edge, using the [DevTools protocol](https://docs.microsoft.com/en-us/microsoft-edge/devtools-protocol-chromium)\n[`chromedriver`](https://sites.google.com/a/chromium.org/chromedriver/home) | 9515 | `chromedriver` | W3C Chrome Driver\n[`geckodriver`](https://github.com/mozilla/geckodriver) | 4444 | `geckodriver` | W3C Gecko Driver (Firefox)\n[`safaridriver`](https://webkit.org/blog/6900/webdriver-support-in-safari-10/) | 5555 | `safaridriver` | W3C Safari Driver\n[`msedgedriver`](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/) | 9515 | `msedgedriver` | W3C Microsoft Edge WebDriver (the new one based on Chromium), also see [`webDriverSession`](#webdriversession)\n[`mswebdriver`](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/) | 17556 | `MicrosoftWebDriver` | Microsoft Edge \"Legacy\" WebDriver\n[`iedriver`](https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver) | 5555 | `IEDriverServer` | IE (11 only) Driver\n[`winappdriver`](https://github.com/Microsoft/WinAppDriver) | 4727 | `C:/Program Files (x86)/Windows Application Driver/WinAppDriver` | Windows Desktop automation, similar to Appium\n[`android`](https://github.com/appium/appium/) | 4723 | `appium` | android automation via [Appium](https://github.com/appium/appium/)\n[`ios`](https://github.com/appium/appium/) | 4723 |`appium` | iOS automation via [Appium](https://github.com/appium/appium/)\n\n# Distributed Testing\nKarate can split a test-suite across multiple machines or Docker containers for execution and aggregate the results. Please refer to the wiki: [Distributed Testing](https://github.com/karatelabs/karate/wiki/Distributed-Testing).\n\n# Locators\nThe standard locator syntax is supported. For example for web-automation, a `/` prefix means XPath and else it would be evaluated as a \"CSS selector\".\n\n```cucumber\nAnd input('input[name=someName]', 'test input')\nWhen submit().click(\"//input[@name='commit']\")\n```\n\nplatform | prefix | means | example\n----- | ------ | ----- | -------\nweb | (none) | css selector | `input[name=someName]`\nweb <br/> android <br/> ios | `/` | xpath | `//input[@name='commit']`\nweb | `{}` | [exact text content](#wildcard-locators) | `{a}Click Me`\nweb | `{^}` | [partial text content](#wildcard-locators) | `{^a}Click Me`\nwin <br/> android <br/> ios| (none) | name | `Submit`\nwin <br/> android <br/> ios | `@` | accessibility id | `@CalculatorResults`\nwin <br/> android <br/> ios | `#` | id | `#MyButton`\nios| `:` | -ios predicate string | `:name == 'OK' type == XCUIElementTypeButton`\nios| `^` | -ios class chain | `^**/XCUIElementTypeTable[name == 'dataTable']`\nandroid| `-` | -android uiautomator | `-input[name=someName]`\n\n## Wildcard Locators\nThe \"`{}`\" and \"`{^}`\" locator-prefixes are designed to make finding an HTML element by *text content* super-easy. You will typically also match against a specific HTML tag (which is preferred, and faster at run-time). But even if you use \"`{*}`\" (or \"`{}`\" which is the equivalent short-cut) to match *any* tag, you are selecting based on what the user *sees on the page*.\n\nWhen you use CSS and XPath, you need to understand the internal CSS class-names and XPath structure of the page. But when you use the visible text-content, for example the text within a `<button>` or hyperlink (`<a>`), performing a \"selection\" can be far easier. And this kind of locator is likely to be more stable and resistant to cosmetic changes to the underlying HTML.\n\nYou have the option to adjust the \"scope\" of the match, and here are examples:\n\nLocator | Description\n------- | -----------\n`click('{a}Click Me')` | the first `<a>` where the text-content is *exactly*: `Click Me`\n`click('{}Click Me')` | the first element (*any* tag name) where the text-content is *exactly*: `Click Me`\n`click('{^}Click')` | the first element (*any* tag name) where the text-content *contains*: `Click`\n`click('{^span}Click')` | the first `<span>` where the text-content *contains*: `Click`\n`click('{div:2}Click Me')` | the second `<div>` where the text-content is *exactly*: `Click Me`\n`click('{span/a}Click Me')` | the first `<a>` where a `<span>` is the immediate parent, and where the text-content is *exactly*: `Click Me`\n`click('{^*:4}Me')` | the fourth HTML element (of *any* tag name) where the text-content *contains*: `Me`\n\nNote that \"`{:4}`\" can be used as a short-cut instead of \"`{*:4}`\".\n\nYou can experiment by using XPath snippets like the \"`span/a`\" seen above for even more \"narrowing down\", but try to expand the \"scope modifier\" (the part within curly braces) only when you need to do \"de-duping\" in case the same *user-facing* text appears multiple times on a page.\n\n## Friendly Locators\nThe [\"wildcard\" locators](#wildcard-locators) are great when the human-facing visible text is *within* the HTML element that you want to interact with. But this approach doesn't work when you have to deal with data-entry and `<input>` fields. This is where the \"friendly locators\" come in. You can ask for an element by its *relative position* to another element which is visible - such as a `<span>`, `<div>` or `<label>` and for which the [locator](#locators) is easy to obtain.\n\n> Also see [Tree Walking](#tree-walking).\n\nMethod | Finds Element\n------ | -----------\n[`rightOf()`](#rightof) | to *right* of given locator\n[`leftOf()`](#leftof) | to *left* of given locator\n[`above()`](#above) | *above* given locator\n[`below()`](#below) | *below* given locator\n[`near()`](#near) | *near* given locator in any direction\n\nThe above methods return a [chainable](#chaining) [`Finder`](src/main/java/com/intuit/karate/driver/Finder.java) instance. For example if you have HTML like this:\n\n```html\n<input type=\"checkbox\"><span>Check Three</span>\n```\n\nTo click on the checkbox, you just need to do this:\n\n```cucumber\n* leftOf('{}Check Three').click()\n```\n\nBy default, the HTML tag that will be searched for will be `input`. While rarely needed, you can over-ride this by calling the `find(tagName)` method like this:\n\n```cucumber\n* rightOf('{}Some Text').find('span').click()\n```\n\nOne more variation supported is that instead of an HTML tag name, you can look for the `textContent`:\n\n```cucumber\n* rightOf('{}Some Text').find('{}Click Me').click()\n```\n\nOne thing to watch out for is that the \"origin\" of the search will be the mid-point of the whole HTML element, not just the text. So especially when doing `above()` or `below()`, ensure that the \"search path\" is aligned the way you expect. If you get stuck trying to align the search path, especially if the \"origin\" is a small chunk of text that is aligned right or left - try [`near()`](#near).\n\nIn addition to `<input>` fields, `<select>` boxes are directly supported like this, so internally a `find('select')` is \"[chained](#chaining)\" automatically:\n\n```cucumber\n* below('{}State').select(0)\n```\n\n### `rightOf()`\n```cucumber\n* rightOf('{}Input On Right').input('input right')\n```\n\n### `leftOf()`\n```cucumber\n* leftOf('{}Input On Left').clear().input('input left')\n```\n### `above()`\n```cucumber\n* above('{}Input On Right').click()\n```\n\n### `below()`\n```cucumber\n* below('{}Input On Right').input('input below')\n```\n\n### `near()`\nOne reason why you would need `near()` is because an `<input>` field may either be on the right or below the label depending on whether the \"container\" element had enough width to fit both on the same horizontal line. Of course this can be useful if the element you are seeking is diagonally offset from the [locator](#locators) you have.\n\n```cucumber\n * near('{}Go to Page One').click()\n```\n\n# Visual Testing\nSee [`compareImage`](https://github.com/karatelabs/karate#compare-image).\n\n# Keywords\nOnly one keyword sets up UI automation in Karate, typically by specifying the URL to open in a browser. And then you would use the built-in [`driver`](#syntax) JS object for all other operations, combined with Karate's [`match`](https://github.com/karatelabs/karate#prepare-mutate-assert) syntax for assertions where needed.\n\n## `driver`\nNavigates to a new page / address. If this is the first instance in a test, this step also initializes the [`driver`](#syntax) instance for all subsequent steps - using what is [configured](#configure-driver).\n\n```cucumber\nGiven driver 'https://github.com/login'\n```\n\nAnd yes, you can use [variable expressions](https://github.com/karatelabs/karate#karate-expressions) from [`karate-config.js`](https://github.com/karatelabs/karate#configuration). For example:\n\n```cucumber\n* driver webUrlBase + '/page-01'\n```\n\n> As seen above, you don't have to force all your steps to use the `Given`, `When`, `Then` BDD convention, and you can [just use \"`*`\" instead](https://github.com/karatelabs/karate#given-when-then).\n\n### `driver` JSON\nA variation where the argument is JSON instead of a URL / address-string, used typically if you are testing a desktop (or mobile) application. This example is for Windows, and you can provide the `app`, `appArguments` and other parameters expected by the [WinAppDriver](https://github.com/Microsoft/WinAppDriver) via the [`webDriverSession`](#webdriversession). For example:\n\n```cucumber\n* def session = { desiredCapabilities: { app: 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App' } }\nGiven driver { webDriverSession: '#(session)' }\n```\n\nSo this is just for convenience and readability, using [`configure driver`](#configure-driver) can do the same thing like this:\n\n```cucumber\n* def session = { desiredCapabilities: { app: 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App' } }\n* configure driver = { webDriverSession: '#(session)' }\nGiven driver {}\n```\n\nThis design is so that you can use (and data-drive) all the capabilities supported by the target driver - which can vary a lot depending on whether it is local, remote, for desktop or mobile etc.\n\n# Syntax\nThe built-in `driver` JS object is where you script UI automation. It will be initialized only after the [`driver`](#driver) keyword has been used to navigate to a web-page (or application).\n\nYou can refer to the [Java interface definition](src/main/java/com/intuit/karate/driver/Driver.java) of the `driver` object to better understand what the various operations are. Note that `Map<String, Object>` [translates to JSON](https://github.com/karatelabs/karate#type-conversion), and JavaBean getters and setters translate to JS properties - e.g. `driver.getTitle()` becomes `driver.title`.\n\n## Methods\nAs a convenience, *all* the methods on the `driver` have been injected into the context as special (JavaScript) variables so you can omit the \"`driver.`\" part and save a lot of typing. For example instead of:\n\n```cucumber\nAnd driver.input('#eg02InputId', Key.SHIFT)\nThen match driver.text('#eg02DivId') == '16'\n```\n\nYou can shorten all that to:\n\n```cucumber\nAnd input('#eg02InputId', Key.SHIFT)\nThen match text('#eg02DivId') == '16'\n```\n\nWhen it comes to JavaBean getters and setters, you could call them directly, but the `driver.propertyName` form is much better to read, and you save the trouble of typing out round brackets. So instead of doing this:\n\n```cucumber\nAnd match getUrl() contains 'page-01'\nWhen setUrl(webUrlBase + '/page-02')\n```\n\nYou should prefer this form, which is more readable:\n```cucumber\nAnd match driver.url contains 'page-01'\nWhen driver.url = webUrlBase + '/page-02'\n```\n\nNote that to navigate to a new address you can use [`driver`](#driver) - which is more concise.\n\n### Chaining\nAll the methods that return the following Java object types are \"chain-able\". This means that you can combine them to concisely express certain types of \"intent\" - without having to repeat the [locator](#locators).\n\n* [`Driver`](src/main/java/com/intuit/karate/driver/Driver.java)\n* [`Element`](src/main/java/com/intuit/karate/driver/Element.java) \n* [`Mouse`](src/main/java/com/intuit/karate/driver/Mouse.java)\n* [`Finder`](src/main/java/com/intuit/karate/driver/Finder.java)\n\nFor example, to [`retry()`](#retry) until an HTML element is present and then [`click()`](#click) it:\n\n```cucumber\n# retry returns a \"Driver\" instance\n* retry().click('#someId')\n```\n\nOr to [wait until a button is enabled](#waitForEnabled) using the default retry configuration:\n\n```cucumber\n# waitForEnabled() returns an \"Element\" instance\n* waitForEnabled('#someBtn').click()\n```\n\nOr to temporarily [over-ride the retry configuration](#retry) *and* wait:\n\n```cucumber\n* retry(5, 10000).waitForEnabled('#someBtn').click()\n```\n\nOr to move the [mouse()](#mouse) to a given `[x, y]` co-ordinate *and* perform a click:\n\n```cucumber\n* mouse(100, 200).click()\n```\n\nOr to use [Friendly Locators](#friendly-locators):\n\n```cucumber\n* rightOf('{}Input On Right').input('input right')\n```\n\nAlso see [waits](#wait-api).\n\n# Syntax\n## `driver.url`\nGet the current URL / address for matching. Example:\n\n```cucumber\nThen match driver.url == webUrlBase + '/page-02'\n```\n\nNote that if you do this as soon as you navigate to a new page, there is a chance that this returns the old / stale URL. To avoid \"flaky\" tests, use [`waitForUrl()`](#waitforurl)\n\nThis can also be used as a \"setter\" to navigate to a new URL *during* a test. But always use the [`driver`](#driver) keyword when you *start* a test and you can choose to prefer that shorter form in general.\n\n```cucumber\n* driver.url = 'http://localhost:8080/test'\n```\n\n## `driver.title`\nGet the current page title for matching. Example:\n\n```cucumber\nThen match driver.title == 'Test Page'\n```\n\nNote that if you do this immediately after a page-load, in some cases you need to wait for the page to fully load. You can use a [`waitForUrl()`](#waitforurl) before attempting to access `driver.title` to make sure it works.\n\n## `driver.dimensions`\nSet the size of the browser window:\n```cucumber\n And driver.dimensions = { x: 0, y: 0, width: 300, height: 800 }\n```\n\nThis also works as a \"getter\" to get the current window dimensions.\n```cucumber\n* def dims = driver.dimensions\n```\nThe result JSON will be in the form: `{ x: '#number', y: '#number', width: '#number', height: '#number' }`\n\n## `position()`\nGet the absolute position and size of an element by [locator](#locators) as follows:\n```cucumber\n* def pos = position('#someid')\n```\nThe absolute position returns the coordinate from the top left corner of the page. If you need the position of an element relative to the current viewport, you can pass an extra boolean argument set to 'true' ('false' will return the absolute position) :\n```cucumber\n* def pos = position('#someid', true)\n```\nThe result JSON will be in the form: `{ x: '#number', y: '#number', width: '#number', height: '#number' }`\n\n## `input()`\n2 string arguments: [locator](#locators) and value to enter.\n\n```cucumber\n* input('input[name=someName]', 'test input')\n```\n\nAs a convenience, there is a second form where you can pass an array as the second argument:\n\n```cucumber\n* input('input[name=someName]', ['test', ' input', Key.ENTER])\n```\n\nAnd an extra convenience third argument is a time-delay (in milliseconds) that will be applied before each array value. This is sometimes needed to \"slow down\" keystrokes, especially when there is a lot of JavaScript or security-validation behind the scenes.\n\n```cucumber\n* input('input[name=someName]', ['a', 'b', 'c', Key.ENTER], 200)\n```\n\nAnd for extra convenience, you can pass a string as the second argument above, in which case Karate will split the string and fire the delay before each character:\n\n```cucumber\n* input('input[name=someName]', 'type this slowly', 100)\n```\n\nIf you need to send input to the \"whole page\" (not a specific input field), just use `body` as the selector:\n\n```cucumber\n* input('body', Key.ESCAPE)\n```\n\n### Special Keys\nSpecial keys such as `ENTER`, `TAB` etc. can be specified like this:\n\n```cucumber\n* input('#someInput', 'test input' + Key.ENTER)\n```\n\nA special variable called `Key` will be available and you can see all the possible key codes [here](src/main/java/com/intuit/karate/driver/Key.java).\n\nAlso see [`value(locator, value)`](#valueset) and [`clear()`](#clear)\n\n## `submit()`\nKarate has an [elegant approach](#chaining) to handling any action such as [`click()`](#click) that results in a new page load. You \"signal\" that a submit is expected by calling the `submit()` function (which returns a `Driver` object) and then \"[chaining](#chaining)\" the action that is expected to trigger a page load.\n\n```cucumber\nWhen submit().click('*Page Three')\n```\n\nThe advantage of this approach is that it works with any of the actions. So even if your next step is the [`ENTER` key](#special-keys), you can do this:\n\n```cucumber\nWhen submit().input('#someform', Key.ENTER)\n```\n\nKarate will do the best it can to detect a page change and wait for the load to complete before proceeding to *any* step that follows.\n\nYou can even mix this into [`mouse()`](#mouse) actions.\n\nFor some SPAs (Single Page Applications) the detection of a \"page load\" may be difficult because page-navigation (and the browser history) is taken over by JavaScript. In such cases, you can always fall-back to a [`waitForUrl()`](#waitforurl) or a more generic [`waitFor()`](#waitfor).\n\n### `waitForUrl()` instead of `submit()`\nSometimes, because of an HTTP re-direct, it can be difficult for Karate to detect a page URL change, or it will be detected too soon, causing your test to fail. In such cases, you can use [`waitForUrl()`](#waitforurl). \n\nNote that it uses a string \"contains\" match, so you just need to supply a portion of the URL you are expecting.\n\nSo instead of this, which uses [`submit()`](#submit):\n\n```cucumber\nGiven driver 'https://google.com'\nAnd input('textarea[name=q]', 'karate dsl')\nWhen submit().click('input[name=btnI]')\nThen match driver.url == 'https://github.com/karatelabs/karate'\n```\n\nYou can do this. Note that `waitForUrl()` will also act as an assertion, so you don't have to do an extra [`match`](https://github.com/karatelabs/karate#match).\n\n```cucumber\nGiven driver 'https://google.com'\nAnd input('textarea[name=q]', 'karate dsl')\nWhen click('input[name=btnI]')\nAnd waitForUrl('https://github.com/karatelabs/karate')\n```\n\nAnd you can even [chain](#chaining) a [`retry()`](#retry) before the `waitForUrl()` if you know that it is going to take a long time:\n\n```cucumber\nAnd retry(5, 10000).waitForUrl('https://github.com/karatelabs/karate')\n```\n\n### `waitFor()` instead of `submit()`\nThis is very convenient to use for the *first* element you need to interact with on a freshly-loaded page. It can be used instead of `waitForUrl()` and you can still perform a page URL assertion as seen below.\n\nHere is an example of waiting for a search box to appear after a [`click()`](#click), and note how we re-use the [`Element`](#chaining) reference returned by `waitFor()` to proceed with the flow. We even slip in a page-URL assertion without missing a beat.\n\n```cucumber\nWhen click('{a}Find File')\nAnd def search = waitFor('input[name=query]')\nThen match driver.url == 'https://github.com/karatelabs/karate/find/master'\nGiven search.input('karate-logo.png')\n```\n\nOf course if you did not care about the page URL assertion (you can still do it later), you could do this\n\n```cucumber\nwaitFor('input[name=query]').input('karate-logo.png')\n```\n\n## `delay()`\nOf course, resorting to a \"sleep\" in a UI test is considered a very bad-practice and you should always use [`retry()`](#retry) instead. But sometimes it is un-avoidable, for example to wait for animations to render - before taking a [screenshot](#screenshot). The nice thing here is that it returns a `Driver` instance, so you can [chain](#chaining) any other method and the \"intent\" will be clear. For example:\n\n```cucumber\n* delay(1000).screenshot()\n```\n\nThe other situation where we have found a `delay()` un-avoidable is for some super-secure sign-in forms - where a few milliseconds delay *before* hitting the submit button is needed.\n\n## `click()`\nJust triggers a click event on the DOM element:\n\n```cucumber\n* click('input[name=someName]')\n```\n\nAlso see [`submit()`](#submit) and [`mouse()`](#mouse).\n\n## `select()`\nYou can use this for plain-vanilla `<select>` boxes that have not been overly \"enhanced\" by JavaScript. Nowadays, most \"select\" (or \"multi-select\") user experiences are JavaScript widgets, so you would be needing to fire a [`click()`](#click) or two to get things done. But if you are really dealing with an HTML `<select>`, then read on.\n\nThere are four variations and use the [locator](#locators) prefix conventions for *exact* and *contains* matches against the `<option>` text-content.\n\n```cucumber\n# select by displayed text\nGiven select('select[name=data1]', '{}Option Two')\n\n# select by partial displayed text\nAnd select('select[name=data1]', '{^}Two')\n\n# select by `value`\nGiven select('select[name=data1]', 'option2')\n\n# select by index\nGiven select('select[name=data1]', 2)\n```\n\nIf you have trouble with `<select>` boxes, try using [`script()`](#script) to execute custom JavaScript within the page as a work-around.\n\n## `focus()`\n```cucumber\n* focus('.myClass')\n```\n\n## `clear()`\n```cucumber\n* clear('#myInput')\n```\nIf this does not work, try [`value(selector, value)`](#valueset).\n\n## `scroll()`\nScrolls to the element.\n\n```cucumber\n* scroll('#myInput')\n```\n\nSince a `scroll()` + [`click()`](#click) (or [`input()`](#input)) is a common combination, you can [chain](#chaining) these:\n\n```cucumber\n* scroll('#myBtn').click()\n* scroll('#myTxt').input('hello')\n```\n\n## `mouse()`\nThis returns an instance of [`Mouse` on which you can chain actions](#chaining). A common need is to move (or hover) the mouse, and for this you call the `move()` method.\n\nThe `mouse().move()` method has two forms. You can pass 2 integers as the `x` and `y` co-ordinates or you can pass the [locator](#locators) string of the element to move to. Make sure you call `go()` at the end - if the last method in the chain is not `click()` or `up()`.\n\n```cucumber\n* mouse().move(100, 200).go()\n* mouse().move('#eg02RightDivId').click()\n# this is a \"click and drag\" action\n* mouse().down().move('#eg02LeftDivId').up()\n```\n\nYou can even chain a [`submit()`](#submit) to wait for a page load if needed:\n\n```cucumber\n* mouse().move('#menuItem').submit().click();\n```\n\nSince moving the mouse is a common task, these short-cuts can be used:\n\n```cucumber\n* mouse('#menuItem32').click()\n* mouse(100, 200).go()\n* waitForEnabled('#someBtn').mouse().click()\n```\n\nThese are useful in situations where the \"normal\" [`click()`](#click) does not work - especially when the element you are clicking is not a normal hyperlink (`<a href=\"\">`) or `<button>`.\n\nThe rare need to \"double-click\" is supported as a `doubleClick()` method:\n\n```cucumber\n* mouse('#myBtn').doubleClick()\n```\n\nNote that you can chain `mouse()` off an [`Element`](#chaining) which can be really convenient. Refer to the section on handling [drop downs](#drop-downs) for an example.\n\n## `close()`\nClose the page / tab.\n\n## `quit()`\nCloses the browser. You normally *never* need to use this in a test, Karate will close the browser automatically after a `Scenario` unless the `driver` instance was created before entering the `Scenario`.\n\n## `html()`\nGet the [`outerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML), so will include the markup of the selected element. Useful for [`match contains`](https://github.com/karatelabs/karate#match-contains) assertions. Example:\n\n```cucumber\nAnd match html('#eg01DivId') == '<div id=\"eg01DivId\">this div is outside the iframe</div>'\n```\n\n## `text()`\nGet the [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent). Example:\n```cucumber\nAnd match text('.myClass') == 'Class Locator Test'\n```\n\n## `value()`\nGet the HTML form-element [`value`](https://www.w3schools.com/jsref/prop_text_value.asp). Example:\n```cucumber\nAnd match value('.myClass') == 'some value'\n```\n\n## `value(set)`\nSet the HTML form-element [value](https://www.w3schools.com/jsref/prop_text_value.asp). Example:\n```cucumber\nWhen value('#eg01InputId', 'something more')\n```\n\n## `attribute()`\nGet the HTML element [attribute value by attribute name](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute). Example:\n```cucumber\nAnd match attribute('#eg01SubmitId', 'type') == 'submit'\n```\n\n## `enabled()`\nIf the element is `enabled` and not `disabled`:\n```cucumber\nAnd match enabled('#eg01DisabledId') == false\n```\n\nAlso see [`waitUntil()`](#waituntil) for an example of how to wait *until* an element is \"enabled\" or until any other element property becomes the target value.\n\n## `waitForUrl()`\nVery handy for waiting for an expected URL change *and* asserting if it happened.\n\nFor convenience, it will do a string contains match (not an exact match) so you don't need to worry about `http` vs `https` for example. It will also return a string which is the *actual* URL in case you need to use it for further actions in the test script.\n\n```cucumber\n# note that you don't need the full url\n* waitForUrl('/some/path')\n\n# if you want to get the actual url for later use\n* def actualUrl = waitForUrl('/some/path')\n```\n\nSee [`waitForUrl()` instead of `submit()`](#waitforurl-instead-of-submit). Also see [waits](#wait-api).\n\n## `waitForText()`\nThis is just a convenience short-cut for `waitUntil(locator, \"_.textContent.includes('\" + expected + \"')\")` since it is so frequently needed. Note the use of the JavaScript [`String.includes()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes) function to do a *text contains* match for convenience. The need to \"wait until some text appears\" is so common, and with this - you don't need to worry about dealing with white-space such as line-feeds and invisible tab characters.\n\nOf course, try not to use single-quotes within the string to be matched, or escape them using a back-slash (`\\`) character.\n\n```cucumber\n* waitForText('#eg01WaitId', 'APPEARED')\n```\n\nAnd if you really need to scan the whole page for some text, you can use this, but it is better to be more specific for better performance:\n\n```cucumber\n* waitForText('body', 'APPEARED')\n```\n\n## `waitForEnabled()`\nThis is just a convenience short-cut for `waitUntil(locator, '!_.disabled')` since it is so frequently needed:\n\n```cucumber\nAnd waitForEnabled('#someId').click()\n```\n\nAlso see [waits](#wait-api).\n\n## `waitForResultCount()`\nA very powerful and useful way to wait until the *number* of elements that match a given locator is equal to a given number. This is super-useful when you need to wait for say a table of slow-loading results, and where the table may contain fewer elements at first. There are two variations. The first will simply return a `List` of [`Element`](#chaining) instances.\n\n```cucumber\n  * waitForResultCount('div#eg01 div', 4)  \n```\n\nMost of the time, you just want to wait until a certain number of matching elements, and then move on with your flow, and in that case, the above is sufficient. If you need to actually do something with each returned `Element`, see [`locateAll()`](#locateall) or the option below.\n\nThe second variant takes a third argument, which is going to do the same thing as the [`scriptAll()`](#scriptall) method:\n\n```cucumber\n  When def list = waitForResultCount('div#eg01 div', 4, '_.innerHTML')\n  Then match list == '#[4]'\n  And match each list contains '@@data'\n```\n\nSo in a *single step* we can wait for the number of elements to match *and* extract data as an array.\n\n## `waitFor()`\nThis is typically used for the *first* element you need to interact with on a freshly loaded page. Use this in case a [`submit()`](#submit) for the previous action is un-reliable, see the section on [`waitFor()` instead of `submit()`](#waitfor-instead-of-submit)\n\nThis will wait until the element (by [locator](#locators)) is present in the page and uses the configured [`retry()`](#retry) settings. This will fail the test if the element does not appear after the configured number of re-tries have been attempted.\n\nSince `waitFor()` returns an [`Element`](#chaining) instance on which you can call \"chained\" methods, this can be the pattern you use, which is very convenient and readable:\n\n```cucumber\nAnd waitFor('#eg01WaitId').click()\n```\n\nAlso see [waits](#wait-api).\n\n## `waitForAny()`\nRarely used - but accepts multiple arguments for those tricky situations where a particular element may or may *not* be present in the page. It returns the [`Element`](#chaining) representation of whichever element was found *first*, so that you can perform conditional logic to handle accordingly.\n\nBut since the [`optional()`](#optional) API is designed to handle the case when a given [locator](#locators) does *not* exist, you can write some very concise tests, *without* needing to examine the returned object from `waitForAny()`.\n\nHere is a real-life example combined with the use of [`retry()`](#retry):\n\n```cucumber\n* retry(5, 10000).waitForAny('#nextButton', '#randomButton')\n* optional('#nextButton').click()\n* optional('#randomButton').click()\n```\n\nIf you have more than two locators you need to wait for, use the single-argument-as-array form, like this:\n\n```cucumber\n* waitForAny(['#nextButton', '#randomButton', '#blueMoonButton'])\n```\n\nAlso see [waits](#wait-api).\n\n## `optional()`\nReturns an [`Element`](src/main/java/com/intuit/karate/driver/Element.java) (instead of `exists()` which returns a boolean). What this means is that it can be [chained](#chaining) as you expect. But there is a twist ! If the [locator](#locators) does *not* exist, any attempt to perform actions on it will *not* fail your test - and silently perform a \"no-op\".\n\nThis is designed specifically for the kind of situation described in the example for [`waitForAny()`](#waitforany). If you wanted to check if the `Element` returned exists, you can use the `present` property \"getter\" as follows:\n\n```cucumber\n* assert optional('#someId').present\n```\n\nBut what is most useful is how you can now *click only if element exists*. As you can imagine, this can handle un-predictable dialogs, advertisements and the like.\n\n```cucumber\n* optional('#elusiveButton').click()\n# or if you need to click something else\n* if (exists('#elusivePopup')) click('#elusiveButton')\n```\n\nAnd yes, you *can* use an [`if` statement in Karate](https://github.com/karatelabs/karate#conditional-logic) !\n\nNote that the `optional()`, `exists()` and `locate()` APIs are a little different from the other `Element` actions, because they will *not* honor any intent to [`retry()`](#retry) and *immediately* check the HTML for the given locator. This is important because they are designed to answer the question: \"*does the element exist in the HTML page __right now__ ?*\"\n\nNote that the \"opposite\" of `optional()` is [`locate()`](#locate) which will fail if the element is not present.\n\nIf all you need to do is check whether an element exists and fail the test if it doesn't, see [`exists()`](#exists) below.\n\n## `exists()`\nThis method returns a boolean (`true` or `false`), perfect for asserting if an element exists and giving you the option to perform conditional logic, or manually fail the test.\n\nNote that there is a [`karate.fail()`](https://github.com/karatelabs/karate#karate-fail) API that may be handy when you want to fail a test after advanced / conditional checks.\n\nAnd also note that instead of using the `match` keyword, you can use [`karate.match()`](https://stackoverflow.com/a/50350442/143475) for very advanced conditional checks.\n\n```cucumber\n* var buttonExists = exists('#myButton')\n* var labelExists = exists('#myLabel')\n* if (buttonExists && !labelExists) karate.fail('button exists but label does not')\n```\n\n## `waitUntil()`\nWait for the *browser* JS expression to evaluate to `true`. Will poll using the [retry()](#retry) settings configured.\n\n```cucumber\n* waitUntil(\"document.readyState == 'complete'\")\n```\n\nNote that the JS here has to be a \"raw\" string that is simply sent to the browser as-is and evaluated there. This means that you cannot use any Karate JS objects or API-s such as `karate.get()` or `driver.title`. So trying to use `driver.title == 'My Page'` will *not* work, instead you have to do this:\n\n```cucumber\n* waitUntil(\"document.title == 'My Page'\")\n```\n\nAlso see [Karate vs the Browser](#karate-vs-the-browser).\n\n### `waitUntil(locator,js)`\nA very useful variant that takes a [locator](#locators) parameter is where you supply a JavaScript \"predicate\" function that will be evaluated *on* the element returned  by the locator in the HTML DOM. Most of the time you will prefer the short-cut boolean-expression form that begins with an underscore (or \"`!`\"), and Karate will inject the JavaScript DOM element reference into a variable named \"`_`\".\n\nHere is a real-life example:\n\n> One limitation is that you cannot use double-quotes *within* these expressions, so stick to the pattern seen below.\n\n```cucumber\nAnd waitUntil('.alert-message', \"_.innerHTML.includes('Some Text')\")\n```\n\n### Karate vs the Browser\nOne thing you need to get used to is the \"separation\" between the code that is evaluated by Karate and the JavaScript that is sent to the *browser* (as a raw string) and evaluated. Pay attention to the fact that the [`includes()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes) function you see in the above example - is pure JavaScript.\n\nThe use of `includes()` is needed in this real-life example, because [`innerHTML()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML) can return leading and trailing white-space (such as line-feeds and tabs) - which would cause an exact \"`==`\" comparison in JavaScript to fail.\n\nBut guess what - this example is baked into a Karate API, see [`waitForText()`](#waitfortext).\n\nFor an example of how JavaScript looks like on the \"Karate side\" see [Function Composition](#function-composition).\n\nThis form of `waitUntil()` is very useful for waiting for some HTML element to stop being `disabled`. Note that Karate will fail the test if the `waitUntil()` returned `false` - *even* after the configured number of [re-tries](#retry) were attempted.\n\n```cucumber\nAnd waitUntil('#eg01WaitId', \"function(e){ return e.innerHTML == 'APPEARED!' }\")\n\n# if the expression begins with \"_\" or \"!\", Karate will wrap the function for you !\nAnd waitUntil('#eg01WaitId', \"_.innerHTML == 'APPEARED!'\")\nAnd waitUntil('#eg01WaitId', '!_.disabled')\n```\n\nAlso see [`waitForEnabled()`](#waitforenabled) which is the preferred short-cut for the last example above, also look at the examples for [chaining](#chaining) and then the section on [waits](#wait-api).\n\n### `waitUntil(function)`\nA *very* powerful variation of [`waitUntil()`](#waituntil) takes a full-fledged JavaScript function as the argument. This can loop until *any* user-defined condition and can use any variable (or Karate or [Driver JS API](#syntax)) in scope. The signal to stop the loop is to return any not-null object. And as a convenience, whatever object is returned, can be re-used in future steps.\n\nThis is best explained with an example. Note that [`scriptAll()`](#scriptall) will return an array, as opposed to [`script()`](#script).\n\n```cucumber\nWhen search.input('karate-logo.png')\n\n# note how we return null to keep looping\nAnd def searchFunction =\n  \"\"\"\n  function() {\n    var results = scriptAll('.js-tree-browser-result-path', '_.innerText');\n    return results.size() == 2 ? results : null;\n  }\n  \"\"\"\n\n# note how we returned an array from the above when the condition was met\nAnd def searchResults = waitUntil(searchFunction)\n\n# and now we can use the results like normal\nThen match searchResults contains 'karate-core/src/main/resources/karate-logo.png'\n```\n\nThe above logic can actually be replaced with Karate's built-in short-cut - which is [`waitForResultCount()`](#waitforresultcount) Also see [waits](#wait-api).\n\nAlso see [Loop Until](#loop-until).\n\n## Function Composition\nThe above example can be re-factored in a very elegant way as follows, using Karate's [native support for JavaScript](https://github.com/karatelabs/karate#javascript-functions):\n\n```cucumber\n# this can be a global re-usable function !\nAnd def innerText = function(locator){ return scriptAll(locator, '_.innerText') }\n\n# we compose a function using another function (the one above)\nAnd def searchFunction =\n  \"\"\"\n  function() {\n    var results = innerText('.js-tree-browser-result-path');\n    return results.size() == 2 ? results : null;\n  }\n  \"\"\"\n```\n\nThe great thing here is that the `innnerText()` function can be defined in a [common feature](https://github.com/karatelabs/karate#multiple-functions-in-one-file) which all your scripts can re-use. You can see how it can be re-used anywhere to scrape the contents out of *any* HTML tabular data, and all you need to do is supply the [locator](#locators) that matches the elements you are interested in.\n\nAlso see [Karate vs the Browser](#karate-vs-the-browser).\n\n## `retry()`\nFor tests that need to wait for slow pages or deal with un-predictable element load-times or state / visibility changes, Karate allows you to *temporarily* tweak the internal retry settings. Here are the few things you need to know.\n\n### Retry Defaults\nThe [default retry settings](https://github.com/karatelabs/karate#retry-until) are:\n* `count`: 3, `interval`: 3000 milliseconds (try three times, and wait for 3 seconds before the next re-try attempt)\n* it is recommended that you stick to these defaults, which should suffice for most applications\n* if you really want, you can change this \"globally\" in [`karate-config.js`](https://github.com/karatelabs/karate#configuration) like this:\n  * `configure('retry', { count: 10, interval: 5000 });`\n* or *any time* within a script (`*.feature` file) like this:\n  * `* configure retry = { count: 10, interval: 5000 }`\n\n### Retry Actions\nBy default, all actions such as [`click()`](#click) will *not* be re-tried - and this is what you would stick to most of the time, for tests that run smoothly and *quickly*. But some troublesome parts of your flow *will* require re-tries, and this is where the `retry()` API comes in. There are 3 forms:\n* `retry()` - just signals that the *next* action will be re-tried if it fails, using the [currently configured retry settings](https://github.com/karatelabs/karate#retry-until)\n* `retry(count)` - the next action will *temporarily* use the `count` provided, as the limit for retry-attempts\n* `retry(count, interval)` - *temporarily* change the retry `count` *and* retry `interval` (in milliseconds) for the next action\n\nAnd since you can [chain](#chaining) the `retry()` API, you can have tests that clearly express the \"*intent to wait*\". This results in easily understandable one-liners, *only* at the point of need, and to anyone reading the test - it will be clear as to *where* extra \"waits\" have been applied.\n\nHere are the various combinations for you to compare using [`click()`](#click) as an example.\n\n Script | Description\n-------- | -----------\n`click('#myId')` | Try to stick to this *default* form for 95% of your test. If the element is not found, the test will fail immediately. But your tests will run smoothly and super-fast.\n`waitFor('#myId').click()` | Use [`waitFor()`](#waitfor) for the *first* element on a newly loaded page or any element that takes time to load after the previous action. For the best performance, use this *only if* using [`submit()`](#submit) for the (previous) action (that triggered the page-load) [is not reliable](#waitfor-instead-of-submit). It uses the currently configured [retry settings](#retry-defaults). With the [defaults](#retry-defaults), the test will fail after waiting for 3 x 3000 ms which is 9 seconds. Prefer this instead of any of the options below, or in other words - stick to the defaults as far as possible.\n`retry().click('#myId')` | This happens to be exactly equivalent to the above ! When you request a `retry()`, internally it is just a `waitFor()`. Prefer the above form as it is more readable. The advantage of this form is that it is easy to quickly add (and remove) when working on a test in development mode.\n`retry(5).click('#myId')` | Temporarily use `5` as the max retry attempts to use *and* apply a \"wait\". Since `retry()` expresses an intent to \"wait\", the `waitFor()` can be omitted for the [chained](#chained) action.\n`retry(5, 10000).click('#myId')` | Temporarily use `5` as the max retry attempts *and* 10 seconds as the time to wait before the next retry attempt. Again like the above, the `waitFor()` is implied. The test will fail if the element does not load within 50 seconds.\n\n### Wait API\nThe set of built-in functions that start with \"`wait`\" handle all the cases you would need to typically worry about. Keep in mind that:\n* all of these examples *will* [`retry()`](#retry) internally by default\n* you can prefix a [`retry()`](#retry-actions) *only* if you need to over-ride the settings for *this* \"wait\" - as shown in the second row\n\nScript | Description\n------ | -----------\n[`waitFor('#myId')`](#waitfor) | waits for an element as described above\n`retry(10).waitFor('#myId')` | like the above, but temporarily over-rides the settings to wait for a [longer time](#retry-actions), and this can be done for *all* the below examples as well\n[`waitForUrl('google.com')`](#waitforurl) | for convenience, this uses a string *contains* match - so for example you can omit the `http` or `https` prefix\n[`waitForText('#myId', 'appeared')`](#waitfortext) | frequently needed short-cut for waiting until a string appears - and this uses a \"string contains\" match for convenience\n[`waitForEnabled('#mySubmit')`](#waitforenabled) | frequently needed short-cut for `waitUntil(locator, '!_disabled')`\n[`waitForResultCount('.myClass', 4)`](#waitforresultcount) | wait until a certain number of rows of tabular data is present\n[`waitForAny('#myId', '#maybe')`](#waitforany) | handle if an element may or *may not* appear, and if it does, handle it - for e.g. to get rid of an ad popup or dialog\n[`waitUntil(expression)`](#waituntil) | wait until *any* user defined JavaScript statement to evaluate to `true` in the browser \n[`waitUntil(function)`](#waituntilfunction) | use custom logic to handle *any* kind of situation where you need to wait, *and* use other API calls if needed\n\nAlso see the examples for [chaining](#chaining).\n\n## `script()`\nWill actually attempt to evaluate the given string as JavaScript within the browser.\n\n```cucumber\n* assert 3 == script(\"1 + 2\")\n```\n\nTo avoid problems, stick to the pattern of using double-quotes to \"wrap\" the JavaScript snippet, and you can use single-quotes within.\n\n```cucumber\n* script(\"console.log('hello world')\")\n```\n\nBut note that you can always \"escape\" a quote if needed, using back-slashes:\n\n```cucumber\n* script(\"console.log('I\\\\'ve been logged')\")\n```\n\nA more useful variation is to perform a JavaScript `eval` on a reference to the HTML DOM element retrieved by a [locator](#locators). For example:\n\n```cucumber\nAnd match script('#eg01WaitId', \"function(e){ return e.innerHTML }\") == 'APPEARED!'\n# which can be shortened to:\nAnd match script('#eg01WaitId', '_.innerHTML') == 'APPEARED!'\n```\n\nNormally you would use [`text()`](#text) to do the above, but you get the idea. Expressions follow the same short-cut rules as for [`waitUntil()`](#waituntil).\n\nHere is an interesting example where a JavaScript event can be triggered on a given HTML element:\n\n```cucumber\n* waitFor('#someId').script(\"_.dispatchEvent(new Event('change'))\")\n```\n\nWhen starting with `_`, the ES6 arrow function syntax is also supported. This is more compact, and is especially useful for expressions that do not start with the current DOM element. Here is an example of getting the [\"computed style\"](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle) for a given element:\n\n```cucumber\n* match script('.styled-div', \"function(e){ return getComputedStyle(e)['font-size'] }\") == '30px'\n\n# this shorter version is equivalent to the above\n* match script('.styled-div', \"_ => getComputedStyle(_)['font-size']\") == '30px'\n```\n\nFor an advanced example of simulating a drag and drop operation see [this answer on Stack Overflow](https://stackoverflow.com/a/60800181/143475).\n\nAlso see the plural form [`scriptAll()`](#scriptall).\n\n## `scriptAll()`\nJust like [`script()`](#script), but will perform the script `eval()` on *all* matching elements (not just the first) - and return the results as a JSON array / list. This is very useful for \"bulk-scraping\" data out of the HTML (such as `<table>` rows) - which you can then proceed to use in [`match`](https://github.com/karatelabs/karate#match) assertions:\n\n```cucumber\n# get text for all elements that match css selector\nWhen def list = scriptAll('div div', '_.textContent')\nThen match list == '#[3]'\nAnd match each list contains '@@data'\n```\n\nSee [Function Composition](#function-composition) for another good example. Also see the singular form [`script()`](#script).\n\n### `scriptAll()` with filter\n`scriptAll()` can take a third argument which has to be a JavaScript \"predicate\" function, that returns a boolean `true` or `false`. This is very useful to \"filter\" the results that match a desired condition - typically a text comparison. For example if you want to get only the cells out of a `<table>` that contain the text \"data\" you can do this:\n\n```cucumber\n* def list = scriptAll('div div', '_.textContent', function(x){ return x.contains('data') })\n* match list == ['data1', 'data2']\n```\n\n> Note that the JS in this case is run by *Karate* not the browser, so you use the Java `String.contains()` API not the JavaScript `String.includes()` one.\n\nSee also [`locateAll()` with filter](#locateall-with-filter).\n\n## `driver.scriptAwait()`\nOnly supported for `type: 'chrome'` - this will wait for a JS promise to resolve and then return the result as a JSON object. Here is an [example](../karate-e2e-tests/src/test/java/axe/axe.feature):\n\n```cucumber\n* def axeResponse = driver.scriptAwait('axe.run()')\n```\n\n## `locate()`\nRarely used, but when you want to just instantiate an [`Element`](src/main/java/com/intuit/karate/driver/Element.java) instance, typically when you are writing custom re-usable functions, or using an element as a \"waypoint\" to access other elements in a large, complex \"tree\".\n\n```cucumber\n* def e = locate('.some-div')\n# now you can have multiple steps refer to \"e\"\n* e.locate('.something-else').input('foo')\n* e.locate('.some-button').click()\n```\nNote that `locate()` will fail the test if the element was not found. Think of it as just like [`waitFor()`](#waitfor) but without the \"wait\" part.\n\nSee also [`locateAll()`](#locateall).\n\n## `locateAll()`\nThis will return *all* elements that match the [locator](#locator) as a list of [`Element`](src/main/java/com/intuit/karate/driver/Element.java) instances. You can now use Karate's [core API](https://github.com/karatelabs/karate#the-karate-object) and call [chained](#chaining) methods. Here are some examples:\n\n```cucumber\n# find all elements with the text-content \"Click Me\"\n* def elements = locateAll('{}Click Me')\n* match karate.sizeOf(elements) == 7\n* elements[6].click()\n* match elements[3].script('_.tagName') == 'BUTTON'\n```\n\nTake a look at how to [loop](#looping) and [transform](https://github.com/karatelabs/karate#json-transforms) data for more ideas.\n\n### `locateAll()` with filter\n`locateAll()` can take a second argument which has to be a JavaScript \"predicate\" function, that returns a boolean `true` or `false`. This is very useful to \"filter\" the results that match a desired condition - typically a text comparison.\n\nImagine a situation where you want to get only the element where a certain attribute value *starts with* some text - and then click on it. A plain CSS selector won't work - but you can do this:\n\n```cucumber\n* def filter = function(x){ return x.attribute('data-label').startsWith('somePrefix_') }\n* def list = locateAll('div[data-label]', filter)\n* list[0].click()\n```\n\nThe `filter` function above, will be called for each [`Element`](src/main/java/com/intuit/karate/driver/Element.java) - which means that you can call methods on it such as [`Element.attribute(name)`](#chaining) in this case. Note that the JS function in this case is run by *Karate* not the browser, so you use the Java `String.startsWith()` API.\n\nSince you can call `Element.script()` - *any* kind of filtering will be possible. For example here is the equivalent of the example above. Note the combination of \"Karate JavaScript\" and [\"JS that runs in the browser\"](#karate-vs-the-browser):\n\n```cucumber\n* def filter = function(x){ return x.script(\"_.getAttribute('data-label')\").startsWith('somePrefix_') }\n* def list = locateAll('div[data-label]', filter)\n* list[0].click()\n```\n\nSee also [`scriptAll()` with filter](#scriptall-with-filter).\n\n## `refresh()`\nNormal page reload, does *not* clear cache.\n\n## `reload()`\n*Hard* page reload, which *will* clear the cache.\n\n## `back()`\n\n## `forward()`\n\n## `maximize()`\n\n## `minimize()`\n\n## `fullscreen()`\n\n## `cookie(set)`\nSet a cookie. The method argument is JSON, so that you can pass more data in addition to the `value` such as `domain` and `url`. Most servers expect the `domain` to be set correctly like this:\n\n```cucumber\nGiven def myCookie = { name: 'hello', value: 'world', domain: '.mycompany.com' }\nWhen cookie(myCookie)\nThen match driver.cookies contains '#(^myCookie)'\n```\n\n> Note that you can do the above as a one-liner like this: `* cookie({ name: 'hello', value: 'world' })`, just keep in mind here that then it would follow the rules of [Enclosed JavaScript](https://github.com/karatelabs/karate#enclosed-javascript) (not [Embedded Expressions](https://github.com/karatelabs/karate#embedded-expressions))\n\n### Hybrid Tests\n\nIf you need to set cookies *before* the target URL is loaded, you can start off by navigating to `about:blank` like this:\n\n```cucumber\n# perform some API calls and initialize the value of \"token\"\n* driver 'about:blank'\n* cookie({ name: 'my.auth.token', value: token, domain: '.mycompany.com' })\n# now you can jump straight into your home page and bypass the login screen !\n* driver baseUrl + '/home'\n```\n\nThis is very useful for \"hybrid\" tests. Since Karate combines API testing capabilities, you can sign-in to your SSO store via a REST end-point, and then drop cookies onto the browser so that you can bypass the user log-in experience. This can be a huge time-saver !\n\nNote that the API call (or the routine that gets the required data) can be made to run only once for the whole test-suite using [`karate.callSingle()`](https://github.com/karatelabs/karate#hooks).\n\n## `cookie()`\nGet a cookie by name. Note how Karate's [`match`](https://github.com/karatelabs/karate#match) syntax comes in handy.\n\n```cucumber\n* def cookie1 = { name: 'foo', value: 'bar' }\nAnd match driver.cookies contains '#(^cookie1)'\nAnd match cookie('foo') contains cookie1\n```\n\n## `driver.cookies`\nSee above examples.\n\n## `deleteCookie()`\nDelete a cookie by name:\n\n```cucumber\nWhen deleteCookie('foo')\nThen match driver.cookies !contains '#(^cookie1)'\n```\n\n## `clearCookies()`\nClear all cookies.\n\n```cucumber\nWhen clearCookies()\nThen match driver.cookies == '#[0]'\n```\n\n## `dialog()`\nThere are two forms. The first takes a single boolean argument - whether to \"accept\" or \"cancel\". The second form has an additional string argument which is the text to enter for cases where the dialog is expecting user input.\n\n```cucumber\n# cancel\n* dialog(false)\n\n# enter text and accept\n* dialog(true, 'some text')\n```\n\n## `driver.dialogText`\nAfter using [`dialog()`](#dialog) you can retrieve the text of the currently visible dialog:\n\n```cucumber\n* match driver.dialogText == 'Please enter your name:'\n```\n\n## `switchPage()`\nWhen multiple browser tabs are present, allows you to switch to one based on page title *or* URL.\n\n```cucumber\nWhen switchPage('Page Two')\n```\n\nFor convenience, a string \"contains\" match is used. So you can do this, without needing the `https://` part:\n\n```cucumber\n* switchPage('mydomain.com/dashboard')\n```\n\nYou can also switch by page index if you know it:\n\n```cucumber\n* switchPage(1)\n```\n\n## `switchFrame()`\nThis \"sets context\" to a chosen frame (or `<iframe>`) within the page. There are 2 variants, one that takes an integer as the param, in which case the frame is selected based on the order of appearance in the page:\n\n```cucumber\nWhen switchFrame(0)\n```\n\nOr you use a [locator](#locators) that points to the `<iframe>` element that you need to \"switch to\".\n\n```cucumber\nWhen switchFrame('#frame01')\n```\n\nAfter you have switched, any future actions such as [`click()`](#click) would operate within the \"selected\" `<iframe>`. To \"reset\" so that you are back to the \"root\" page, just switch to `null` (or integer value `-1`):\n\n```cucumber\nWhen switchFrame(null)\n```\n\n## `screenshot()`\nThere are two forms, if a [locator](#locators) is provided - only that HTML element will be captured, else the entire browser viewport will be captured. This method returns a byte array.\n\nThis will also do automatically perform a [`karate.embed()`](https://github.com/karatelabs/karate#karate-embed) - so that the image appears in the HTML report.\n\n```cucumber\n* screenshot()\n# or\n* screenshot('#someDiv')\n```\n\nIf you want to disable the \"auto-embedding\" into the HTML report, pass an additional boolean argument as `false`, e.g: \n\n```cucumber\n* screenshot(false)\n# or\n* screenshot('#someDiv', false)\n```\n\nThe call to `screenshot()` returns a Java byte-array, which is convenient if you want to do something specific such as save it to a file. The usage of [`karate.write()`](https://stackoverflow.com/a/54593057/143475) here is just an example, you can use [JS or Java interop](https://github.com/karatelabs/karate#calling-javascript-functions) as needed.\n\n```cucumber\n* def bytes = screenshot(false)\n* def file = karate.write(bytes, 'test.png')\n* print 'screenshot saved to:', file\n```\n\n## `pdf()`\nTo create paginated pdf document from the page loaded.\n\n```cucumber\n* def pdfDoc = pdf({'orientation': 'landscape'})\n* karate.write(pdfDoc, \"pdfDoc.pdf\")\n```\n\n## `highlight()`\nTo visually highlight an element in the browser, especially useful when working in the [debugger](https://github.com/karatelabs/karate/wiki/IDE-Support#vs-code-karate-plugin). Uses the [configured `highlightDuration`](#configure-driver).\n\n```cucumber\n* highlight('#eg01DivId')\n```\n\n## `highlightAll()`\nPlural form of the above.\n\n```cucumber\n* highlightAll('input')\n```\n\n## `timeout()`\nRarely used, but sometimes for only some parts of your test - you need to tell the browser to wait for a very slow loading page. Behind the scenes, this sets the HTTP communication \"read timeout\". This does the same thing as the `timeout` key in the [driver config](#configure-driver) - but is designed so that you can change this \"on the fly\", *during* the flow of a test.\n\nNote that the duration is in milliseconds. As a convenience, to \"reset\" the value to what was initially set, you can call `timeout()` with no argument:\n\n```cucumber\n# wait 3 minutes if needed for page to load\n* timeout(3 * 60 * 1000)\n* driver 'http://mydomain/some/slow/page'\n# reset to defaults for the rest of the test ...\n* timeout()\n```\n\n## `driver.sessionId`\nOnly applies to WebDriver based driver sessions, and useful in case you need the session id to download any test-reports / video etc.\n\n```cucumber\n* def sessionId = driver.sessionId\n```\n\n## Tree Walking\nThe [Element](#chaining) API has \"getters\" for the following properties:\n* `parent`\n* `children` (returns an array of `Element`-s)\n* `firstChild`\n* `lastChild`\n* `previousSibling`\n* `nextSibling`\n\nThis can be convenient in some cases, for example as an alternative to [Friendly Locators](#friendly-locators). For example, where it is easy (or you already have a reference) to locate some element and you want to use that as a \"base\" to perform something on some other element which may not have a unique `id` or css / XPath locator.\n\n```cucumber\n* locate('#someDiv').parent.firstChild.click()\n* locate('#foo').parent.children[3].click()\n```\n\nAlso note that [`locate()`](#locate) and [`locateAll()`](#locateall) can be called *on* an [`Element`](#chaining), so that the \"search scope\" is limited to that `Element` and it's children.\n\n# Looping\n\nLooping over data is easy in Karate because of the natural way in which you can [loop over JS arrays](https://stackoverflow.com/a/76091034/143475). And the API for UI testing is designed to return arrays, for example [`scriptAll()`](#scriptall) and [`locateAll()`](#locateall) turn out to be very useful.\n\nFor example, if you had a list of rows shown on the screen, and you wanted to click on all of them, you could do this:\n\n```cucumber\n* def rows = locateAll('.my-table tr button')\n* rows.forEach(row => row.click())\n```\n\nIf you wanted to do multiple actions per iteration of the loop, refer to the example for [handling drop downs](#drop-downs).\n\n## Loop Until\nA different kind of loop is when you need to perform an action *until* no more data exists. This is where the [`waitUntil()`](#waituntilfunction) API comes in handy.\n\n```cucumber\n* def delete = \n\"\"\"\nfunction() {\n  if (!exists('.border-bottom div')) {\n    return true;\n  }\n  click('.text-end button');\n}\n\"\"\"\n* waitUntil(delete)\n```\n\nHow this works is as long as the function does not return a value, `waitUntil()` will loop. The `click('.text-end button')` is deleting the first row of records in the HTML. So the code above very neatly performs the loop and also exits the loop when there are no more records, and that is why we have the check for `!exists('.border-bottom div')`.\n\n# Drop Downs\n\nThis section exists here in the documentation because it is a frequently asked question, and most drop-down \"select\" experiences in the wild are powered by JavaScript which makes it harder. These are cases where [`select`](#select) will not work. Instead, most JS-powered drop-down components can be handled by using [`mouse()`](#mouse).\n\nFor example, consider this HTML which is using [Bootstrap](https://getbootstrap.com):\n\n```html\n    <div class=\"dropdown\">\n      <button class=\"btn btn-secondary dropdown-toggle\" type=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n        Dropdown button\n      </button>\n      <ul class=\"dropdown-menu\">\n        <li><a class=\"dropdown-item\" href=\"#\">First</a></li>\n        <li><a class=\"dropdown-item\" href=\"#\">Second</a></li>\n        <li><a class=\"dropdown-item\" href=\"#\">Third</a></li>\n      </ul>\n    </div>\n```\n\nThe way to handle this is in two steps, first to click on the button to show the list of items, and then to click on one of the items:\n\n```cucumber\n* mouse('button').click()\n* mouse('a.dropdown-item').click()\n```\n\n## Looping Over Elements\n\nIn the above example, what if we wanted to [loop over each drop-down item](#looping) and select each one. Here's how we can do it. Note how we can have multiple actions within the loop.\n\n```cucumber\n* def list = locateAll('a.dropdown-item')\n* def fun =\n\"\"\"\nfunction(e) {\n    mouse('button').click();\n    e.mouse().click();\n    delay(2000);\n}\n\"\"\"\n* list.forEach(fun)\n```\n\nNote how we could chain the `mouse()` method off an [`Element`](#chaining) instance, which is really convenient.\n\nFor the full working expanded example that shows all the concepts you need for looping over elements and handling drop-downs, refer to [this example](../karate-e2e-tests/src/test/java/driver/99_bootstrap.feature) and the [corresponding HTML](../karate-e2e-tests/src/test/java/driver/html/99_bootstrap.html).\n\n# Debugging\nYou can use the [Visual Studio Karate entension](https://github.com/karatelabs/karate/wiki/IDE-Support#vs-code-karate-plugin) for stepping through and debugging a test. You can see a [demo video here](https://twitter.com/KarateDSL/status/1167533484560142336). We recommend that you get comfortable with this because it is going to save you lots of time. And creating tests may actually turn out to be fun !\n\nWhen you are in a hurry, you can pause a test in the middle of a flow just to look at the browser developer tools to see what CSS selectors you need to use. For this you can use [`karate.stop()`](../#karate-stop) - but of course, *NEVER* forget to remove this before you move on to something else !\n\n```cucumber\n* karate.stop(9000)\n```\n\nAnd then you would see something like this in the console:\n\n```\n*** waiting for socket, type the command below:\ncurl http://localhost:9000\nin a new terminal (or open the URL in a web-browser) to proceed ...\n```\n\nIn most IDE-s, you would even see the URL above as a clickable hyperlink, so just clicking it would end the `stop()`. This is really convenient in \"dev-local\" mode. The integer port argument is mandatory and you have to choose one that is not being used.\n\n# Code Reuse\nYou will often need to move steps (for e.g. a login flow) into a common feature that can be called from multiple test-scripts. When using a browser-driver, a [`call` in \"shared scope\"](https://github.com/karatelabs/karate#shared-scope) *has* to be used. This means:\n\n* a single driver instance is used for any [`call`-s](https://github.com/karatelabs/karate#call), even if nested\n* even if the driver is instantiated (using the [`driver`](#driver) keyword) within a \"called\" feature - it will remain in the context after the `call` returns\n\nA typical pattern will look like this:\n\n```cucumber\nFeature: main\n\nBackground:\n* call read('login.feature')\n\nScenario:\n* click('#someButton')\n# the rest of your flow\n```\n\nWhere `login.feature` would look something like:\n\n```\nFeature: login\n\nScenario:\n* configure driver = { type: 'chrome' }\n* driver urlBase + '/login'\n* input('#username', 'john')\n* input('#password', 'secret')\n* click('#loginButton')\n* waitForUrl('/dashboard')\n```\n\nThere are many ways to parameterize the driver config or perform environment-switching, read [this](https://stackoverflow.com/a/60581024/143475) for more details.\n\n\nNote [`callonce`](https://github.com/karatelabs/karate#callonce) is not supported for a `driver` instance. Separate `Scenario`-s that can run in parallel are encouraged. If you really want a long-running flow that combines steps from multiple features, you can make a `call` to each of them from the single \"top-level\" `Scenario`.\n\n```cucumber\nFeature: main\n\nBackground:\n* call read('login.feature')\n\nScenario:\n* call read('some.feature')\n* call read('another.feature@someTag')\n```\n\nBest-practice would be to implement [Hybrid Tests](#hybrid-tests) where the values for the auth-cookies are set only once for the whole test-suite using [`karate.callSingle()`](https://github.com/karatelabs/karate#hooks).\n\nAlso see [Looping Over Elements](#looping-over-elements).\n\n## JavaScript Function Reuse\n\nAs described in the section above on [Methods](#methods) - functions on the `driver` object will be auto-injected as variables, but the important thing to note is that this will happen only *after* the driver is instantiated via the [`driver`](#driver) keyword.\n\nIf you need to wrap a sequence of UI actions into a re-usable unit, JavaScript is convenient. A good example of doing this in the section on [Looping Over Elements](#loop-until). Here, the function is declared in-line, and after the `driver` was instantiated.\n\nBut if you wanted to take JS function re-use to the next-level, you may choose to keep them in a separate file and follow the pattern described [here](https://github.com/karatelabs/karate#multiple-functions-in-one-file). If this common feature file is called before the `driver` is initialized, you will need to modify the JS code to:\n* get the `driver` instance using [`karate.get()`](https://github.com/karatelabs/karate#karate-get). \n* and call methods on the `driver` instance (instead of directly)\n\nHere is an example:\n\n```cucumber\n* def deleteFirstRow = \n\"\"\"\nfunction() {\n  var driver = karate.get('driver');\n  if (!driver.exists('.border-bottom div')) {\n    return true;\n  }\n  driver.click('.text-end button');\n}\n\"\"\"\n```\n\nYou can compare the above with the example in the section on [Looping Over Elements](#loop-until) to appreciate the difference.\n\n# Locator Lookup\nOther UI automation frameworks spend a lot of time encouraging you to follow a so-called \"[Page Object Model](https://martinfowler.com/bliki/PageObject.html)\" for your tests. The Karate project team is of the opinion that things can be made simpler.\n\nOne indicator of a *good* automation framework is how much *work* a developer needs to do in order to perform any automation action - such as clicking a button, or retrieving the value of some HTML object / property. In Karate - these are typically *one-liners*. And especially when it comes to test-automation, we have found that attempts to apply patterns in the pursuit of code re-use, more often than not - results in hard-to-maintain code, and severely impacts *readability*.\n\nThat said, there is some benefit to re-use of just [locators](#locators) and Karate's support for [JSON](https://github.com/karatelabs/karate#json) and [reading files](https://github.com/karatelabs/karate#reading-files) turns out to be a great way to achieve [DRY](https://en.wikipedia.org/wiki/Don't_repeat_yourself)-ness in tests. Here is one suggested pattern you can adopt.\n\nFirst, you can maintain a JSON \"map\" of your application locators. It can look something like this. Observe how you can mix different [locator types](#locators), because they are all just string-values that behave differently depending on whether the first character is a \"`/`\" (XPath), \"`{}`\" ([wildcard](#wildcard-locators)), or not (CSS). Also note that this is *pure JSON* which means that you have excellent IDE support for syntax-coloring, formatting, indenting, and ensuring well-formed-ness. And you can have a \"nested\" heirarchy, which means you can neatly \"name-space\" your locator reference look-ups - as you will see later below.\n\n```json\n{\n  \"testAccounts\": {\n    \"numTransactions\": \"input[name=numTransactions]\",\n    \"submit\": \"#submitButton\"\n  },\n  \"leftNav\": {\n    \"home\": \"{}Home\",\n    \"invoices\": \"{a}Invoices\",\n    \"transactions\": \"{^}Transactions\"\n  },\n  \"transactions\": {\n    \"addFirst\": \".transactions .qwl-secondary-button\",\n    \"descriptionInput\": \".description-cell input\",\n    \"description\": \".description-cell .header5\",\n    \"amount\": \".amount-cell input\",\n  }\n}\n```\n\nKarate has [great options for re-usability](https://github.com/karatelabs/karate#calling-other-feature-files), so once the above JSON is saved as `locators.json`, you can do this in a `common.feature`:\n\n```cucumber\n* call read 'locators.json'\n```\n\nThis looks deceptively simple, but what happens is very interesting. It will inject all top-level \"keys\" of the JSON file into the Karate \"context\" as global [variables](https://github.com/karatelabs/karate#def). In normal programming languages, global variables are a *bad thing*, but for test-automation (when you know what you are doing) - this can be *really* convenient.\n\n> For those who are wondering how this works behind the scenes, since `read` refers to the [`read()`](https://github.com/karatelabs/karate#reading-files) function, the behavior of [`call`](https://github.com/karatelabs/karate#calling-javascript-functions) is that it will *invoke* the function *and* use what comes after it as the solitary function argument. And this `call` is using [shared scope](https://github.com/karatelabs/karate#shared-scope).\n\nSo now you have `testAccounts`, `leftNav` and `transactions` as variables, and you have a nice \"name-spacing\" of locators to refer to - within your different feature files:\n\n```cucumber\n* input(testAccounts.numTransactions, '0')\n* click(testAccounts.submit)\n* click(leftNav.transactions)\n\n* retry().click(transactions.addFirst)\n* retry().input(transactions.descriptionInput, 'test')\n```\n\nAnd this is how you can have all your locators defined in one place and re-used across multiple tests. You can experiment for yourself (probably depending on the size of your test-automation team) if this leads to any appreciable benefits, because the down-side is that you need to keep switching between 2 files - when writing and maintaining tests.\n\n# Intercepting HTTP Requests\nYou can selectively re-direct some HTTP requests that the browser makes - into a [Karate test-double](https://github.com/karatelabs/karate/tree/master/karate-netty) ! This gives you some powerful options, for example you can simulate Ajax and [XHR](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) failures, or even replace entire widgets or sections of the page with \"fake\" HTML. The unified use of Karate test-doubles means that you can script dynamic responses and handle incoming URL, query-string and header variations. The following scenario will make this clear.\n\nWe will use this page: [`https://www.seleniumeasy.com/test/dynamic-data-loading-demo.html`](https://www.seleniumeasy.com/test/dynamic-data-loading-demo.html) - as an example. When a button on this page is clicked, a request is made to [`https://api.randomuser.me/?nat=us`](https://api.randomuser.me/?nat=us) - which returns some JSON data. That data is used to make *yet another* request to fetch a JPEG image from e.g. [`https://randomuser.me/api/portraits/women/34.jpg`](https://randomuser.me/api/portraits/women/34.jpg). Finally, the page is updated to display the first-name, last-name and the image.\n\nWhat we will do is intercept any request to a URL pattern `*randomuser.me/*` and \"fake\" a response. We can return JSON and even an image using a mock like this:\n\n```cucumber\n@ignore\nFeature:\n\nBackground:\n* configure cors = true\n* def count = 0\n\nScenario: pathMatches('/api/{img}')\n* def response = read('billie.jpg')\n* def responseHeaders = { 'Content-Type': 'image/jpeg' }\n\nScenario:\n* def count = count + 1\n* def lastName = 'Request #' + count\n* def response = read('response.json')\n```\n\nRefer to the [Karate test-doubles documentation](https://github.com/karatelabs/karate/tree/master/karate-netty) for details. We [configure cors = true](https://github.com/karatelabs/karate/tree/master/karate-netty#configure-cors) to ensure that the browser does not complain about cross-origin requests. If the request is for `/api/*`, the first `Scenario` matches - else the last one is a \"catch all\". Note how we can even serve an image with the right `Content-Type` header. And the returned JSON is dynamic, the `lastName` will modify [`response.json`](../karate-demo/src/test/java/driver/mock/response.json) via an [embedded-expression](https://github.com/karatelabs/karate#embedded-expressions).\n\n## `driver.intercept()`\nAll we need to do now is to tell Chrome to intercept some URL patterns and use the above mock-server feature-file:\n\n```cucumber\nFeature: intercepting browser requests\n\nScenario:\n* configure driver = { type: 'chrome' }\n* driver 'https://www.seleniumeasy.com/test/dynamic-data-loading-demo.html'\n* driver.intercept({ patterns: [{ urlPattern: '*randomuser.me/*' }], mock: 'mock-01.feature' })\n* click('{}Get New User')\n* delay(2000)\n* screenshot()\n```\n\n* `driver.intercept()` is fully supported only for the driver type `chrome`. [`Playwright`](#playwright) supports only [urlPatterns](https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#type-RequestPattern)\n* you can route multiple URL patterns to the same Karate mock-feature, the format of each array-element under `patterns` can be found [here](https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#type-RequestPattern).\n  * the `*` wildcard (most likely what you need) will match any number of characters, e.g. `*myhost/some/path/*`\n  * `?` will match any single character\n  * `\\` can be used as an \"escape\" character\n* `driver.intercept()` can be called only once during a `Scenario`, which means only one mock-feature can be used - but a mock-feature can have any number of `Scenario` \"routes\"\n* the `mock` value supports any [Karate file-reading prefix](https://github.com/karatelabs/karate#reading-files) such as `classpath:` \n* if you need to set up HTTP mocks *before* even loading the first page, you can use `about:blank` for the first URL used for the `driver` init - similar to how you can pre-set a [`cookie()`](#cookieset).\n\nThe entire example can be found [here](../karate-demo/src/test/java/driver/mock/demo-01.feature) - and here is a [video](https://twitter.com/KarateDSL/status/1248996522357739521). Note how the \"fake\" [`response.json`](../karate-demo/src/test/java/driver/mock/response.json) is tiny compared to the \"real\" JSON, because we know that only a few data-elements are needed for the UI to work in this case.\n\nThe Karate [regression test-suite](https://stackoverflow.com/a/66005331/143475) that runs in GitHub actions (effectively our CI) - includes another [example](../karate-e2e-tests/src/test/java/driver/05.feature), and you can find a good explanation [here](https://twitter.com/KarateDSL/status/1350743622312894466).\n\n## Inspecting Intercepted Requests\nThe `driver.intercept()` API returns an object on which you can call `get(variableName)`. This comes in useful if you want to get information on what requests were intercepted. If you really want, you can write a complex mock that calls an external API, saves the response, and returns a modified response to the browser. Since you can store [\"global\" variables in a mock in the `Background`](https://github.com/karatelabs/karate/tree/master/karate-netty#background), you can save any arbitrary data or \"state\", and unpack them from your main test flow. Here is an example.\n\nFirst the mock. Any time we handle an incoming request, we append some JSON data to the `savedRequests` array. Note that [`requestPath`](https://github.com/karatelabs/karate/tree/master/karate-netty#requestpath) and [`requestParams`](https://github.com/karatelabs/karate/tree/master/karate-netty#requestparams) are built-in variables.\n\n```cucumber\nFeature:\n\nBackground:\n* def savedRequests = []\n\nScenario: pathMatches('/api/05')\n* savedRequests.push({ path: requestPath, params: requestParams })\n* print 'saved:', savedRequests\n* def response = { message: 'hello faked' }\n```\n\nAnd in the main UI test, note how we get the value of `savedRequests`, and do a normal [`match`](https://github.com/karatelabs/karate#match) on it !\n\n```cucumber\n* def mock = driver.intercept({ patterns: [{ urlPattern: '*/api/*' }], mock: '05_mock.feature' })\n\n* click('button')\n* waitForText('#containerDiv', 'hello faked')\n\n* def requests = mock.get('savedRequests')\n* match requests == [{ path: '/api/05', params: { foo: ['bar'] } }]\n```\n\n## Intercepting All Requests\nIf you use `*` as the `urlPattern` *every* request can be routed to the mock ! And if you use the following mock, it will actually act as a [\"pass-through\" proxy](https://github.com/karatelabs/karate/tree/master/karate-netty#karateproceed) - but with the advantage that every single request and response will be emitted to `target/karate.log`. You may be able to turn this into a custom \"record-replay\" framework, or do other interesting things. Yes, you can modify the request or response if needed !\n\n```cucumber\n@ignore\nFeature:\n\nScenario:\n* karate.proceed()\n```\n\nAnd here is the [test script](../karate-demo/src/test/java/driver/mock/demo-02.feature):\n\n```cucumber\nFeature: intercepting all requests !\n\nScenario: \n* configure driver = { type: 'chrome' }\n* driver 'about:blank'\n* driver.intercept({ patterns: [{ urlPattern: '*' }], mock: 'mock-02.feature' })\n* driver 'https://github.com/login'\n```\n\n# Emulate Device\n## `driver.emulateDevice()`\nEmulating a device is supported natively only by type: `chrome`. You need to call a method on the `driver` object directly:\n\nThe example below has the `width`, `height` and [`userAgent`](https://wicg.github.io/ua-client-hints/) for an iPhone X.\n\n```cucumber\nAnd driver.emulateDevice(375, 812, 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1')\n```\n\n# File Upload\nThere are multiple options, choose the one that fits you best.\n\n## `driver.inputFile()`\nFile-upload is supported natively only by type: `chrome`. You need to call a method on the `driver` object directly. Here is an [example](../karate-demo/src/test/java/driver/demo/demo-05.feature) that you can try:\n\n```cucumber\n* configure driver = { type: 'chrome' }\n* driver 'http://the-internet.herokuapp.com/upload'\n* driver.inputFile('#file-upload', 'billie.jpg')\n* submit().click('#file-submit')\n* waitForText('#uploaded-files', 'billie.jpg')\n```\n\nThe `driver.inputFile()` can take an array or varargs as the second argument. Note how Karate is able to resolve a [relative path](https://github.com/karatelabs/karate#reading-files) to an actual OS file-path behind the scenes. If you want to point to a real file, use the `file:` prefix.\n\n## Using `multipart file`\nThis is the recommended, browser-agnostic approach that uses Karate's core-competency as an HTTP API client i.e. [`multipart file`](https://github.com/karatelabs/karate#multipart-file).\n\nHere is how the example above looks like:\n\n```cucumber\n* url 'http://the-internet.herokuapp.com/upload'\n* multipart file file = { read: 'billie.jpg', filename: 'billie.jpg', contentType: 'image/jpg' }\n* method post\n```\n\nValidation can be performed if needed on the response to this HTTP `POST` which may be HTML, and the [`karate.extract()`](https://github.com/karatelabs/karate#karate-extract) API may come in useful.\n\nIn real-life flows, you may need to pass cookies from the [browser](#cookie) to the [Karate HTTP client](https://github.com/karatelabs/karate#cookie), so that you can simulate any flows needed after this step.\n\n## Using Karate Robot\n[Karate Robot](https://github.com/karatelabs/karate/tree/master/karate-robot) is designed for desktop application testing, but since you can click on anything in the viewport, you can achieve what you may not be able to with other automation frameworks. [Here](../karate-robot/src/test/java/robot/core/upload.feature) is the same example using this approach, where a couple of images need to be saved as part of the test-script:\n\n```cucumber\n* driver 'http://the-internet.herokuapp.com/upload'\n* robot { window: '^Chrome', highlight: true }\n# since we have the driver active, the \"robot\" namespace is needed\n* robot.waitFor('choose-file.png').click().delay(1000)\n* robot.input('/Users/pthomas3/Desktop' + Key.ENTER)\n* robot.waitFor('file-name.png').click()\n* robot.input(Key.ENTER).delay(1000)\n* submit().click('#file-submit')\n* waitForText('#uploaded-files', 'billie.jpg')\n* screenshot()\n```\n\nA video of the above execution can be viewed [here](https://twitter.com/ptrthomas/status/1253373486384295936).\n\n# Java API\n## Driver Java API\nYou can start a [`Driver`]() instance programmatically and perform actions and assertions like this:\n\n```java\n  Driver driver = Driver.start(\"chrome\");\n  driver.setUrl(serverUrl + \"/05\");  \n  driver.click(\"button\");\n  driver.waitForText(\"#containerDiv\", \"hello world\");\n```\n\nYou can find the complete example [here](../karate-e2e-tests/src/test/java/driver/JavaApiRunner.java). Also see this [explanation](https://twitter.com/KarateDSL/status/1353969718730788865).\n\nAlso see the [Karate Java API](https://github.com/karatelabs/karate#java-api).\n\n## Chrome Java API\nAs a convenience you can use the [`Chrome`](../karate-core/src/main/java/com/intuit/karate/driver/chrome/Chrome.java) concrete implementation of a `Driver` directly, designed for common needs such as converting HTML to PDF - or taking a screenshot of a page. Here is an [example](../karate-demo/src/test/java/driver/screenshot/ChromePdfRunner.java):\n\n```java\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.driver.chrome.Chrome;\nimport java.io.File;\nimport java.util.Collections;\n\npublic class Test {\n\n    public static void main(String[] args) {\n        Chrome chrome = Chrome.startHeadless();\n        chrome.setLocation(\"https://github.com/login\");\n        byte[] bytes = chrome.pdf(Collections.EMPTY_MAP);\n        FileUtils.writeToFile(new File(\"target/github.pdf\"), bytes);\n        bytes = chrome.screenshot();\n        // this will attempt to capture the whole page, not just the visible part\n        // bytes = chrome.screenshotFull();\n        FileUtils.writeToFile(new File(\"target/github.png\"), bytes);\n        chrome.quit();\n    }\n    \n}\n```\n\nNote that in addition to `driver.screenshot()` there is a `driver.screenshotFull()` API that will attempt to capture the whole \"scrollable\" page area, not just the part currently visible in the viewport.\n\nThe parameters that you can optionally customize via the `Map` argument to the `pdf()` method are documented here: [`Page.printToPDF\n`](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF).\n\nIf Chrome is not installed in the default location, you can pass a String argument like this:\n\n```java\nChrome.startHeadless(executable)\n// or\nChrome.start(executable)\n```\n\nFor more control or custom options, the `start()` method takes a `Map<String, Object>` argument where the following keys (all optional) are supported:\n* `executable` - (String) path to the Chrome executable or batch file that starts Chrome\n* `headless` - (Boolean) if headless\n* `maxPayloadSize` - (Integer) defaults to 4194304 (bytes, around 4 MB), but you can override it if you deal with very large output / binary payloads\n* `useFrameAggregation` - (Boolean) defaults to `false`.  Can be enabled to aggregate multiple frames if the server sends multiple frames for a single payload.\n\n## `driver.screenshotFull()`\nOnly supported for driver type [`chrome`](#driver-types). See [Chrome Java API](#chrome-java-api). This will snapshot the entire page, not just what is visible in the viewport.\n\n# Proxy\nFor driver type [`chrome`](#driver-types), you can use the `addOption` key to pass command-line options that [Chrome supports](https://www.linuxbabe.com/desktop-linux/configure-proxy-chromium-google-chrome-command-line):\n\n```cucumber\n* configure driver = { type: 'chrome', addOptions: [ '--proxy-server=\"https://somehost:5000\"' ] }\n```\n\nFor the WebDriver based [driver types](#driver-types) like `chromedriver`, `geckodriver` etc, you can use the [`webDriverSession`](#webdriversession) configuration as per the [W3C WebDriver spec](https://w3c.github.io/webdriver/#proxy):\n\n```cucumber\n* def session = { capabilities: { browserName: 'chrome', proxy: { proxyType: 'manual', httpProxy: 'somehost:5000' } } }\n* configure driver = { type: 'chromedriver', webDriverSession: '#(session)' }\n```\n\n# Appium\n## Screen Recording\nOnly supported for driver type [`android` | `ios`](#driver-types).\n\n```cucumber\n* driver.startRecordingScreen()\n# test\n* driver.saveRecordingScreen(\"invoice.mp4\",true)\n```\nThe above example would save the file and perform \"auto-embedding\" into the HTML report.\n\nYou can also use `driver.startRecordingScreen()` and `driver.stopRecordingScreen()`, and both methods take recording options as JSON input.\n\n## `driver.hideKeyboard()`\nOnly supported for driver type [`android` | `ios`](#driver-types), for hiding the \"soft keyboard\".\n"
  },
  {
    "path": "karate-core/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>io.karatelabs</groupId>\n        <artifactId>karate-parent</artifactId>\n        <version>1.5.2</version>\n    </parent>\n    <artifactId>karate-core</artifactId>\n    <packaging>jar</packaging>\n    <name>${project.artifactId}</name>\n    \n    <properties>\n        <antlr.version>4.13.1</antlr.version>\n        <graal.version>24.0.0</graal.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.graalvm.js</groupId>\n            <artifactId>js-scriptengine</artifactId>\n            <version>${graal.version}</version>\n        </dependency>        \n        <dependency>\n            <groupId>org.graalvm.js</groupId>\n            <artifactId>js-language</artifactId>\n            <version>${graal.version}</version>\n            <scope>runtime</scope>\n        </dependency>  \n        <dependency>\n            <groupId>org.thymeleaf</groupId>\n            <artifactId>thymeleaf</artifactId>\n            <version>3.1.2.RELEASE</version>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.slf4j</groupId>\n                    <artifactId>slf4j-api</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>    \n        <dependency>\n            <groupId>com.linecorp.armeria</groupId>\n            <artifactId>armeria</artifactId>\n            <version>1.33.4</version>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.slf4j</groupId>\n                    <artifactId>slf4j-api</artifactId>\n                </exclusion>\n            </exclusions>            \n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpclient</artifactId>\n            <version>4.5.14</version>\n            <exclusions>\n                <exclusion>\n                    <groupId>commons-logging</groupId>\n                    <artifactId>commons-logging</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <!-- can be removed once httpclient is upgraded -->\n        <dependency>\n            <groupId>commons-codec</groupId>\n            <artifactId>commons-codec</artifactId>\n            <version>1.16.1</version>\n        </dependency>        \n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <version>1.5.20</version>\n        </dependency>         \n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>jcl-over-slf4j</artifactId>\n            <!-- this is tied to logback-classic -->\n            <version>2.0.17</version>\n            <scope>runtime</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <!-- this is tied to logback-classic -->\n            <version>2.0.17</version>\n        </dependency>                               \n        <dependency>\n            <groupId>org.antlr</groupId>\n            <artifactId>antlr4-runtime</artifactId>\n            <version>${antlr.version}</version>\n        </dependency>                                 \n        <dependency>\n            <groupId>com.jayway.jsonpath</groupId>\n            <artifactId>json-path</artifactId>\n            <version>2.9.0</version>\n        </dependency>\n        <dependency>\n            <groupId>net.minidev</groupId>\n            <artifactId>json-smart</artifactId>\n            <version>2.6.0</version>\n        </dependency>                                                                                                                                                           \t\t\t\t\t\t\t\t\t\t\t\t\t                      \n        <dependency>\n            <groupId>org.yaml</groupId>\n            <artifactId>snakeyaml</artifactId>\n            <version>2.3</version>\n        </dependency>\n        <dependency>\n            <groupId>de.siegmar</groupId>\n            <artifactId>fastcsv</artifactId>\n            <version>3.4.0</version>\n        </dependency>        \n        <dependency>\n            <groupId>info.picocli</groupId>\n            <artifactId>picocli</artifactId>\n            <version>4.7.6</version>\n        </dependency>\n        <dependency>\n            <groupId>io.github.classgraph</groupId>\n            <artifactId>classgraph</artifactId>\n            <version>4.8.181</version>\n        </dependency>\n        <dependency>\n            <groupId>io.github.t12y</groupId>\n            <artifactId>resemble</artifactId>\n            <version>1.1.0</version>\n        </dependency>\n        <dependency>\n            <groupId>io.github.t12y</groupId>\n            <artifactId>ssim</artifactId>\n            <version>1.0.0</version>\n        </dependency>\n        <dependency>\n            <groupId>org.brotli</groupId>\n            <artifactId>dec</artifactId>\n            <version>0.1.2</version>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <version>${junit5.version}</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>       \n        <resources>\n            <resource>\n                <directory>src/main/java</directory>\n                <filtering>true</filtering>\n                <includes>\n                    <include>karate-meta.properties</include>\n                </includes>\n            </resource>\n            <resource>\n                <directory>src/main/java</directory>\n                <filtering>false</filtering>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>                \n            </resource>            \n        </resources>        \n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>\n        <plugins>           \n            <plugin>\n                <groupId>org.antlr</groupId>\n                <artifactId>antlr4-maven-plugin</artifactId>\n                <version>${antlr.version}</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>antlr4</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>                   \n        </plugins>               \n    </build>\n    \n    <profiles>\n        <profile> \n            <id>pre-release</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-dependency-plugin</artifactId>\n                        <version>3.6.1</version>\n                        <executions>\n                            <execution>\n                                <id>unpack-dependencies</id>\n                                <phase>validate</phase>\n                                <goals>\n                                    <goal>unpack-dependencies</goal>\n                                </goals>\n                                <configuration>\n                                    <includes>/META-INF/native/*</includes>\n                                    <outputDirectory>target/shade</outputDirectory>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>                                        \n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-shade-plugin</artifactId>\n                        <version>${maven.shade.version}</version>                       \n                        <executions>\n                            <execution>\n                                <phase>package</phase>\n                                <goals>\n                                    <goal>shade</goal>\n                                </goals>\n                                <configuration>\n                                    <shadedArtifactAttached>true</shadedArtifactAttached>\n                                    <shadedClassifierName>all</shadedClassifierName>\n                                    <createDependencyReducedPom>false</createDependencyReducedPom>\n                                    <artifactSet>\n                                        <includes>\n                                            <include>com.linecorp.armeria:*</include>\n                                            <!-- next 3 needed for armeria at runtime -->\n                                            <include>io.micrometer:*</include>\n                                            <include>org.reactivestreams:*</include>\n                                            <include>com.fasterxml.jackson.core:*</include>\n                                            <include>io.netty:*</include>                                       \n                                            <include>org.apache.httpcomponents:*</include>\n                                            <!-- needed for apache httpclient at runtime -->\n                                            <include>commons-codec:*</include>\n                                            <include>org.thymeleaf:*</include>\n                                            <!-- next 2 needed for thymeleaf at runtime -->\n                                            <include>org.attoparser:*</include>\n                                            <include>org.unbescape:*</include>\n                                            <include>org.antlr:*</include>\n                                            <include>io.github.classgraph:*</include>\n                                        </includes>\n                                    </artifactSet>                 \n                                    <relocations>\n                                        <relocation>\n                                            <pattern>com.linecorp.</pattern>\n                                            <shadedPattern>karate.com.linecorp.</shadedPattern>\n                                        </relocation>\n                                        <relocation>\n                                            <pattern>io.micrometer.</pattern>\n                                            <shadedPattern>karate.io.micrometer.</shadedPattern>\n                                        </relocation>\n                                        <relocation>\n                                            <pattern>org.reactivestreams.</pattern>\n                                            <shadedPattern>karate.org.reactivestreams.</shadedPattern>\n                                        </relocation>\n                                        <relocation>\n                                            <pattern>com.fasterxml.jackson.</pattern>\n                                            <shadedPattern>karate.com.fasterxml.jackson.</shadedPattern>\n                                        </relocation>                                                                                                                      \n                                        <relocation>\n                                            <pattern>io.netty.</pattern>\n                                            <shadedPattern>karate.io.netty.</shadedPattern>\n                                        </relocation>                                                                              \n                                        <relocation>\n                                            <pattern>org.apache.http.</pattern>\n                                            <shadedPattern>karate.org.apache.http.</shadedPattern>\n                                        </relocation>\n                                        <relocation>\n                                            <pattern>org.apache.commons.codec.</pattern>\n                                            <shadedPattern>karate.org.apache.commons.codec.</shadedPattern>\n                                        </relocation>                                        \n                                        <relocation>\n                                            <pattern>org.thymeleaf.</pattern>\n                                            <shadedPattern>karate.org.thymeleaf.</shadedPattern>\n                                        </relocation>\n                                        <relocation>\n                                            <pattern>org.attoparser.</pattern>\n                                            <shadedPattern>karate.org.attoparser.</shadedPattern>\n                                        </relocation>\n                                        <relocation>\n                                            <pattern>org.unbescape.</pattern>\n                                            <shadedPattern>karate.org.unbescape.</shadedPattern>\n                                        </relocation>                                                                                                                                                                                                   \n                                        <relocation>\n                                            <pattern>org.antlr.v4.</pattern>\n                                            <shadedPattern>karate.org.antlr.v4.</shadedPattern>\n                                        </relocation>\n                                        <relocation>\n                                            <pattern>io.github.classgraph.</pattern>\n                                            <shadedPattern>karate.io.github.classgraph.</shadedPattern>\n                                        </relocation>\n                                        <relocation>\n                                            <pattern>nonapi.io.github.classgraph.</pattern>\n                                            <shadedPattern>karate.nonapi.io.github.classgraph.</shadedPattern>\n                                        </relocation>                                                                                                                      \n                                    </relocations>\n                                    <filters>                                                                             \n                                        <filter>\n                                            <artifact>io.netty:netty-transport-native-epoll</artifact>\n                                            <excludes>\n                                                <exclude>META-INF/native/libnetty_transport_native_epoll_x86_64.so</exclude>                                                                                                                    \n                                            </excludes>\n                                        </filter>\n                                        <filter>\n                                            <artifact>io.netty:netty-tcnative-boringssl-static</artifact>\n                                            <excludes>\n                                                <exclude>META-INF/native/libnetty_tcnative_linux_x86_64.so</exclude>\n                                                <exclude>META-INF/native/libnetty_tcnative_linux_aarch64.so</exclude>                                               \n                                                <exclude>META-INF/native/libnetty_tcnative_osx_x86_64.jnilib</exclude>\n                                                <exclude>META-INF/native/netty_tcnative_windows_x86_64.dll</exclude>                                                                                                                                               \n                                            </excludes>\n                                        </filter>\n                                    </filters>                                                                                                       \n                                    <transformers>\n                                        <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                            <mainClass>com.intuit.karate.Main</mainClass>\n                                        </transformer>                                                                                \n                                        <transformer implementation=\"org.apache.maven.plugins.shade.resource.IncludeResourceTransformer\">\n                                            <resource>META-INF/native/libkarate_netty_transport_native_epoll_x86_64.so</resource>\n                                            <file>target/shade/META-INF/native/libnetty_transport_native_epoll_x86_64.so</file>\n                                        </transformer>                                        \n                                        <transformer implementation=\"org.apache.maven.plugins.shade.resource.IncludeResourceTransformer\">\n                                            <resource>META-INF/native/libkarate_netty_tcnative_linux_x86_64.so</resource>\n                                            <file>target/shade/META-INF/native/libnetty_tcnative_linux_x86_64.so</file>\n                                        </transformer>\n                                        <transformer implementation=\"org.apache.maven.plugins.shade.resource.IncludeResourceTransformer\">\n                                            <resource>META-INF/native/libkarate_netty_tcnative_linux_aarch64.so</resource>\n                                            <file>target/shade/META-INF/native/libnetty_tcnative_linux_aarch64.so</file>\n                                        </transformer>                                                                                                                       \n                                        <transformer implementation=\"org.apache.maven.plugins.shade.resource.IncludeResourceTransformer\">\n                                            <resource>META-INF/native/libkarate_netty_tcnative_osx_x86_64.jnilib</resource>\n                                            <file>target/shade/META-INF/native/libnetty_tcnative_osx_x86_64.jnilib</file>\n                                        </transformer>\n                                        <transformer implementation=\"org.apache.maven.plugins.shade.resource.IncludeResourceTransformer\">\n                                            <resource>META-INF/native/karate_netty_tcnative_windows_x86_64.dll</resource>\n                                            <file>target/shade/META-INF/native/netty_tcnative_windows_x86_64.dll</file>\n                                        </transformer>                                                                               \n                                    </transformers>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>                                       \n                </plugins>\n            </build>\n        </profile>\n        <profile>\n            <id>fatjar</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-shade-plugin</artifactId>\n                        <version>${maven.shade.version}</version>\n                        <executions>\n                            <execution>\n                                <phase>package</phase>\n                                <goals>\n                                    <goal>shade</goal>\n                                </goals>\n                                <configuration>\n                                    <finalName>karate-${project.version}</finalName>\n                                    <artifactSet>\n                                        <includes>\n                                            <include>*:*</include>                                    \n                                        </includes>\n                                    </artifactSet>\n                                    <transformers>\n                                        <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                            <mainClass>com.intuit.karate.Main</mainClass>\n                                        </transformer>\n                                        <transformer implementation=\"org.apache.maven.plugins.shade.resource.ServicesResourceTransformer\"/>                                       \n                                    </transformers>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>                               \n                </plugins>                 \n            </build>\n        </profile>\n        <profile>\n            <id>smoke</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-surefire-plugin</artifactId>\n                        <version>${maven.surefire.version}</version>\n                        <configuration>\n                            <excludes>\n                                <exclude>**/MockSslTest.java</exclude>\n                                <exclude>**/ProxyServerTest.java</exclude>\n                                <exclude>**/ProxyServerSslTest.java</exclude>\n                            </excludes>\n                        </configuration>\n                    </plugin>\n                </plugins>                \n            </build>\n        </profile>\n    </profiles>                        \n    \n</project>\n"
  },
  {
    "path": "karate-core/src/main/antlr4/com/intuit/karate/core/KarateLexer.g4",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\nlexer grammar KarateLexer;\n\nFEATURE_COMMENT: WSLF* '#' CHAR* NEWLINE -> channel(HIDDEN) ;\nFEATURE_TAGS: WSLF* '@' CHAR+ NEWLINE ;\nFEATURE: WSLF* 'Feature:' WS* -> pushMode(MAIN) ; // we never popMode !\n\nfragment WSLF: [\\r\\n \\t] ;     // White Space or Line Feed\nfragment BOL: [\\r\\n]+ [ \\t]* ; // Beginning Of Line\nfragment WS: [ \\t] ;           // White Space\n\nmode MAIN; // ==================================================================\n\nBACKGROUND: NEWLINE 'Background:' WS* ;\nSCENARIO: NEWLINE 'Scenario:' WS* ;\nSCENARIO_OUTLINE: NEWLINE 'Scenario Outline:' WS* ;\nEXAMPLES: NEWLINE 'Examples:' WS* ;\n\nSTAR: NEWLINE '*' WS+ ;\nGIVEN: NEWLINE 'Given' WS+ ;\nWHEN: NEWLINE 'When' WS+ ;\nTHEN: NEWLINE 'Then' WS+ ;\nAND: NEWLINE 'And' WS+ ;\nBUT: NEWLINE 'But' WS+ ;\n\nCOMMENT: NEWLINE '#' CHAR* -> channel(HIDDEN) ;\nTAGS: NEWLINE '@' CHAR+ ;\nTABLE_ROW: NEWLINE '|' CHAR+ ;\nDOC_STRING: NEWLINE '\"\"\"' .*? '\"\"\"' CHAR* ;\n\nCHAR: ~[\\r\\n] ;\nNEWLINE: BOL+ ;\n"
  },
  {
    "path": "karate-core/src/main/antlr4/com/intuit/karate/core/KarateParser.g4",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\nparser grammar KarateParser ;\n\noptions { tokenVocab=KarateLexer; }\n\nfeature: featureHeader background? ( scenario | scenarioOutline )* NEWLINE? EOF ;\n\nfeatureHeader: featureTags? FEATURE featureDescription ;\n\nfeatureTags: FEATURE_TAGS+ ;\n\nfeatureDescription: ~(BACKGROUND | SCENARIO | SCENARIO_OUTLINE | TAGS)* ;\n\nbackground: BACKGROUND scenarioDescription step* ;\n\nscenario: tags? SCENARIO scenarioDescription step* ;\n\nscenarioDescription: ~(STAR | GIVEN | WHEN | THEN | AND | BUT | SCENARIO | SCENARIO_OUTLINE | TAGS)* ;\n\nscenarioOutline: tags? SCENARIO_OUTLINE scenarioDescription step* examples+ ;\n\nexamples: tags? EXAMPLES exampleDescription table ;\n\nexampleDescription: ~(TABLE_ROW)* ;\n\nstep: prefix line ( docString | table )? ;\n\nprefix: STAR | GIVEN | WHEN | THEN | AND | BUT ;\n\nline: CHAR+ ;\n\ntags: TAGS+ ;\n\ndocString: DOC_STRING ;\n\ntable: TABLE_ROW+ ;\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/Actions.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * @author pthomas3\n */\npublic interface Actions {\n\n    boolean isFailed();\n\n    Throwable getFailedReason();\n\n    boolean isAborted();\n\n    void assertTrue(String exp);\n\n    void call(String line);\n\n    void callonce(String line);\n\n    void csv(String name, String exp);\n\n    void csvDocString(String name, String exp);\n\n    void json(String name, String exp);\n\n    void string(String name, String exp);\n\n    void xml(String name, String exp);\n\n    void xmlstring(String name, String exp);\n\n    void bytes(String name, String exp);\n\n    void configure(String key, String exp);\n\n    void configureDocString(String key, String exp);\n\n    void cookie(String name, String value);\n\n    void cookies(String exp);\n\n    void copy(String name, String exp);\n\n    void def(String name, String exp);\n\n    void defDocString(String name, String exp);\n\n    void eval(String exp);\n\n    void evalAssignDocString(String lhs, String rhs);\n\n    void evalDocString(String exp);\n\n    void eval(String name, String dotOrParen, String exp);\n\n    void evalIf(String exp);\n\n    void evalDelete(String exp);\n\n    void formField(String name, String exp);\n\n    void formFields(String exp);\n\n    void header(String name, String exp);\n\n    void headers(String exp);\n\n    void listen(String exp);\n\n    void match(String exp, String op1, String op2, String rhs);\n\n    void method(String method);\n\n    void retry(String until);\n\n    void multipartEntity(String value);\n\n    void multipartFiles(String exp);\n\n    void multipartField(String name, String value);\n\n    void multipartFields(String exp);\n\n    void multipartFile(String name, String value);\n\n    void param(String name, String exp);\n\n    void params(String exp);\n\n    void path(String exp);\n\n    void print(String exp);\n\n    void remove(String name, String path);\n\n    void replace(String name, List<Map<String, String>> table);\n\n    void replace(String name, String token, String value);\n\n    void request(String body);\n\n    void requestDocString(String body);\n\n    void set(String name, String path, String value);\n\n    void setDocString(String name, String path, String value);\n\n    void set(String name, String path, List<Map<String, String>> table);\n\n    void soapAction(String action);\n\n    void status(int status);\n\n    void table(String name, List<Map<String, String>> table);\n\n    void text(String name, String exp);\n\n    void url(String exp);\n\n    void yaml(String name, String exp);\n\n    void yamlDocString(String name, String exp);\n\n    void doc(String exp);\n\n    void compareImage(String exp);\n\n    //==========================================================================\n    //\n    void driver(String exp);\n\n    void robot(String exp);\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/Constants.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\n/**\n *\n * @author pthomas3\n */\npublic class Constants {\n\n    private Constants() {\n        // only static methods\n    }\n\n    public static final String KARATE_ENV = \"karate.env\";\n    public static final String KARATE_DEBUG_PORT = \"karate.debug.port\";\n    public static final String KARATE_CONFIG_DIR = \"karate.config.dir\";\n    public static final String KARATE_CONFIG_INCL_RESULT_METHOD = \"karate.config.result.result-method.include\";\n    public static final String KARATE_OUTPUT_DIR = \"karate.output.dir\";\n    public static final String KARATE_OPTIONS = \"karate.options\";\n    public static final String KARATE_REPORTS = \"karate-reports\";\n    public static final String KARATE_JSON_SUFFIX = \".karate-json.txt\";\n    \n    public static final byte[] ZERO_BYTES = new byte[0];\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/FileUtils.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.FeatureCall;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Comparator;\nimport java.util.Properties;\nimport java.util.UUID;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class FileUtils {\n\n    private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(FileUtils.class);\n\n    private FileUtils() {\n        // only static methods\n    }\n\n    public static final boolean KARATE_TELEMETRY;\n    public static final String KARATE_VERSION;\n    public static final String KARATE_META;\n    public static final String USER_UUID;\n\n    static {\n        Properties props = new Properties();\n        InputStream stream = FileUtils.class.getResourceAsStream(\"/karate-meta.properties\");\n        String version;\n        try {\n            props.load(stream);\n            stream.close();\n            version = (String) props.get(\"karate.version\");\n        } catch (IOException e) {\n            version = \"(unknown)\";\n        }\n        KARATE_VERSION = version;\n        KARATE_META = System.getenv(\"KARATE_META\");\n        String telemetryEnv = System.getenv(\"KARATE_TELEMETRY\"); // \"true\" / \"false\"\n        KARATE_TELEMETRY = telemetryEnv == null ? true : telemetryEnv.trim().equals(\"true\");\n        String userHome = System.getProperty(\"user.home\", \"\");\n        String uuid;\n        try {\n            File uuidFile = new File(userHome + File.separator + \".karate\" + File.separator + \"uuid.txt\");\n            if (uuidFile.exists()) {\n                uuid = toString(uuidFile);\n            } else {\n                uuid = UUID.randomUUID().toString();\n                writeToFile(uuidFile, uuid);\n            }\n        } catch (Exception e) {\n            uuid = \"unknown\";\n        }\n        USER_UUID = uuid;\n    }\n\n    public static final File WORKING_DIR = new File(\"\").getAbsoluteFile();\n\n    public static StringUtils.Pair parsePathAndTags(String text) {\n        int pos = text.indexOf('@');\n        if (pos == -1) {\n            text = StringUtils.trimToEmpty(text);\n            return new StringUtils.Pair(text, null);\n        } else {\n            String left = StringUtils.trimToEmpty(text.substring(0, pos));\n            String right = StringUtils.trimToEmpty(text.substring(pos));\n            return new StringUtils.Pair(left, right);\n        }\n    }\n\n    public static FeatureCall parseFeatureAndCallTag(String path) {\n        StringUtils.Pair pair = parsePathAndTags(path);\n        Feature feature = Feature.read(pair.left);\n        return new FeatureCall(feature, pair.right, -1, null);\n    }\n\n    public static String toString(File file) {\n        try {\n            return toString(new FileInputStream(file));\n        } catch (FileNotFoundException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static String toString(InputStream is) {\n        try {\n            return toByteStream(is).toString(UTF_8.name());\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static byte[] toBytes(File file) {\n        try {\n            return toBytes(new FileInputStream(file));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static byte[] toBytes(InputStream is) {\n        return toByteStream(is).toByteArray();\n    }\n\n    private static ByteArrayOutputStream toByteStream(InputStream is) {\n        ByteArrayOutputStream result = new ByteArrayOutputStream();\n        byte[] buffer = new byte[1024];\n        int length;\n        try {\n            while ((length = is.read(buffer)) != -1) {\n                result.write(buffer, 0, length);\n            }\n            return result;\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static String toString(byte[] bytes) {\n        if (bytes == null) {\n            return null;\n        }\n        return new String(bytes, UTF_8);\n    }\n\n    public static byte[] toBytes(String string) {\n        if (string == null) {\n            return null;\n        }\n        return string.getBytes(UTF_8);\n    }\n\n    public static void copy(File src, File dest) {\n        try {\n            writeToFile(dest, toBytes(new FileInputStream(src)));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void writeToFile(File file, byte[] data) {\n        try {\n            File parent = file.getAbsoluteFile().getParentFile();\n            if (!parent.exists()) {\n                parent.mkdirs();\n            }\n            // try with resources, so will be closed automatically\n            try (FileOutputStream fos = new FileOutputStream(file)) {\n                fos.write(data);\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void writeToFile(File file, String data) {\n        writeToFile(file, data.getBytes(UTF_8));\n    }\n\n    public static InputStream toInputStream(String text) {\n        return new ByteArrayInputStream(text.getBytes(UTF_8));\n    }\n\n    public static void deleteDirectory(File file) {\n        Path pathToBeDeleted = file.toPath();\n        try {\n            Files.walk(pathToBeDeleted)\n                    .sorted(Comparator.reverseOrder())\n                    .map(Path::toFile)\n                    .forEach(File::delete);\n        } catch (Exception e) {\n            throw new RuntimeException();\n        }\n    }\n\n    public static void renameFileIfZeroBytes(String fileName) {\n        File file = new File(fileName);\n        if (!file.exists()) {\n            LOGGER.warn(\"file not found, previous write operation may have failed: {}\", fileName);\n        } else if (file.length() == 0) {\n            LOGGER.warn(\"file size is zero bytes, previous write operation may have failed: {}\", fileName);\n            try {\n                File dest = new File(fileName + \".fail\");\n                file.renameTo(dest);\n                LOGGER.warn(\"renamed zero length file to: {}\", dest.getName());\n            } catch (Exception e) {\n                LOGGER.warn(\"failed to rename zero length file: {}\", e.getMessage());\n            }\n        }\n    }\n\n    public static String getBuildDir() {\n        String temp = System.getProperty(Constants.KARATE_OUTPUT_DIR);\n        if (temp != null) {\n            return temp;\n        }\n        String command = System.getProperty(\"sun.java.command\", \"\");\n        return command.contains(\"org.gradle.\") ? \"build\" : \"target\";\n    }\n\n    public static enum OsType {\n        WINDOWS,\n        MACOSX,\n        LINUX,\n        UNKNOWN\n    }\n\n    public static boolean isOsWindows() {\n        return getOsType() == OsType.WINDOWS;\n    }\n\n    public static boolean isOsMacOsX() {\n        return getOsType() == OsType.MACOSX;\n    }\n\n    public static String getOsName() {\n        return System.getProperty(\"os.name\");\n    }\n\n    public static OsType getOsType() {\n        return getOsType(getOsName());\n    }\n\n    public static OsType getOsType(String name) {\n        if (name == null) {\n            name = \"unknown\";\n        } else {\n            name = name.toLowerCase();\n        }\n        if (name.contains(\"win\")) {\n            return OsType.WINDOWS;\n        } else if (name.contains(\"mac\")) {\n            return OsType.MACOSX;\n        } else if (name.contains(\"nix\") || name.contains(\"nux\")) {\n            return OsType.LINUX;\n        } else {\n            return OsType.UNKNOWN;\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/Http.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.intuit.karate.core.ScenarioEngine;\nimport com.intuit.karate.core.Variable;\nimport com.intuit.karate.http.HttpClientFactory;\nimport com.intuit.karate.http.HttpRequestBuilder;\nimport com.intuit.karate.http.Response;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class Http {\n\n    public final String urlBase;\n    private final ScenarioEngine engine;\n    private final HttpRequestBuilder builder;\n\n    public static Http to(String url) {\n        return new Http(url);\n    }\n\n    public void setAppender(LogAppender appender) {\n        engine.logger.setAppender(appender);\n    }\n\n    private Http(String urlBase) {\n        this.urlBase = urlBase;\n        engine = ScenarioEngine.forTempUse(HttpClientFactory.DEFAULT);\n        builder = engine.getRequestBuilder();\n        builder.url(urlBase);\n    }\n\n    public Http url(String url) {\n        builder.url(url);\n        return this;\n    }\n    \n    public Http param(String key, String ... values) {\n        builder.param(key, values);\n        return this;\n    }\n\n    public Http path(String... paths) {\n        builder.paths(paths);\n        return this;\n    }\n\n    public Http header(String name, String value) {\n        builder.header(name, value);\n        return this;\n    }\n\n    public Http hook(RuntimeHook hook) {\n        builder.hook(hook);\n        return this;\n    }\n\n    public Response method(String method, Object body) {        \n        if (body != null) {\n            builder.body(body instanceof Json ? ((Json) body).value() : body);\n        }\n        builder.method(method);\n        Response response = engine.httpInvoke();\n        if (response.getStatus() >= 400) {\n            engine.logger.warn(\"http response code: {}, response: {}, request: {}\",\n                    response.getStatus(), response.getBodyAsString(), engine.getHttpRequest());\n        }\n        return response;\n    }\n\n    public Response method(String method) {\n        return method(method, null);\n    }\n    \n    public Response methodJson(String method, String body) {\n        return method(method, Json.of(body));\n    }    \n\n    public Response get() {\n        return method(\"get\");\n    }\n\n    public Response postJson(String body) {\n        return post(Json.of(body));\n    }\n\n    public Response post(Object body) {\n        return method(\"post\", body);\n    }\n    \n    public Response put(Object body) {\n        return method(\"put\", body);\n    }   \n    \n    public Response putJson(String body) {\n        return put(Json.of(body));\n    }    \n\n    public Response delete() {\n        return method(\"delete\");\n    }\n\n    public Http configure(String key, Object value) {\n        engine.configure(key, new Variable(value));\n        return this;\n    }\n\n    public Http configure(Map<String, Object> map) {\n        map.forEach((k, v) -> configure(k, v));\n        return this;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/ImageComparison.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport io.github.t12y.resemble.Resemble;\nimport io.github.t12y.resemble.Result;\nimport io.github.t12y.ssim.models.MSSIMMatrix;\nimport io.github.t12y.ssim.models.Matrix;\nimport io.github.t12y.ssim.models.Options;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.imageio.ImageIO;\nimport javax.imageio.ImageReader;\nimport javax.imageio.stream.ImageInputStream;\nimport java.awt.*;\nimport java.awt.image.BufferedImage;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.*;\n\nimport static io.github.t12y.ssim.SSIM.ssim;\n\npublic class ImageComparison {\n\n    public static final String RESEMBLE = \"resemble\";\n    public static final String SSIM = \"ssim\";\n\n    private static final String[] IGNORED_BOX_KEYS = new String[]{\"left\", \"right\", \"top\", \"bottom\"};\n    private static final String[] IGNORED_COLOR_KEYS = new String[]{\"r\", \"g\", \"b\"};\n\n    static final Logger logger = LoggerFactory.getLogger(ImageComparison.class);\n\n    private double[] baselinePixels;\n    private double[] latestPixels;\n    private int height;\n    private int width;\n    private double stopWhenMismatchIsLessThan;\n    private double failureThreshold;\n    private boolean baselineMissing;\n    private boolean scaleMismatch;\n    private String[] engines;\n    private Map<String, Object> options;\n    private Map<String, Object> result;\n\n    private ImageComparison(byte[] baselineImg, byte[] latestImg, Map<String, Object> options, boolean allowScaling) {\n        this.baselineMissing = baselineImg == null || baselineImg.length == 0;\n\n        BufferedImage baselineImage;\n        BufferedImage latestImage;\n\n        try {\n            latestImage = ImageIO.read(new ByteArrayInputStream(latestImg));\n            baselineImage = baselineMissing ? latestImage : ImageIO.read(new ByteArrayInputStream(baselineImg));\n        } catch (IOException e) {\n            logger.error(\"image comparison failed while reading images: {}\", e.getMessage());\n            return;\n        }\n\n        this.height = baselineImage.getHeight();\n        this.width = baselineImage.getWidth();\n        this.options = options;\n\n        int latestHeight = latestImage.getHeight();\n        int latestWidth = latestImage.getWidth();\n\n        if (width != latestWidth || height != latestHeight) {\n            if (allowScaling) {\n                Image scaledImage = latestImage.getScaledInstance(width, height, Image.SCALE_SMOOTH);\n                latestImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);\n                latestImage.getGraphics().drawImage(scaledImage, 0, 0, null);\n                latestHeight = height;\n                latestWidth = width;\n            } else {\n                scaleMismatch = true;\n            }\n        }\n\n        this.baselinePixels = unpackPixels(baselineImage.getRGB(0, 0, width, height, null, 0, width));\n        this.latestPixels = unpackPixels(latestImage.getRGB(0, 0, latestWidth, latestHeight, null, 0, latestWidth));\n\n        String latestDataUrl = getDataUrl(latestImg);\n        String baselineDataUrl = baselineMissing ? latestDataUrl : getDataUrl(baselineImg);\n\n        this.result = new HashMap<>();\n        result.put(\"baseline\", baselineDataUrl);\n        result.put(\"latest\", latestDataUrl);\n    }\n\n    private static String getDataUrl(byte[] img) {\n        String format = \"png\";\n\n        try {\n            ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(img));\n            Iterator<ImageReader> readers = ImageIO.getImageReaders(input);\n\n            if (readers.hasNext()) {\n                ImageReader reader = readers.next();\n                reader.setInput(input);\n                format = reader.getFormatName();\n            }\n        } catch (Exception e) {\n            logger.error(\"image comparison failed to detect image type: {}\", e.getMessage());\n        }\n\n        return \"data:image/\" + format + \";base64,\" + Base64.getEncoder().encodeToString(img);\n    }\n\n    private void configure(Map<String, Object> defaultOptions) {\n        String defaultEngine = asString(defaultOptions.get(\"engine\"));\n        if (defaultEngine == null) {\n            defaultEngine = RESEMBLE;\n        }\n        result.put(\"defaultEngine\", defaultEngine);\n\n        double defaultFailureThreshold = toDouble(defaultOptions.get(\"failureThreshold\"), 0.0);\n        result.put(\"defaultFailureThreshold\", defaultFailureThreshold);\n\n        failureThreshold = getDouble(\"failureThreshold\", defaultFailureThreshold);\n        result.put(\"failureThreshold\", failureThreshold);\n\n        String engineConfig = getString(\"engine\", defaultEngine).toLowerCase().replaceAll(\"[^a-z,|]\", \"\");\n        result.put(\"engine\", engineConfig);\n\n        if (engineConfig.contains(\"|\")) {\n            stopWhenMismatchIsLessThan = failureThreshold;\n            engines = engineConfig.split(\"\\\\|\");\n        } else {\n            stopWhenMismatchIsLessThan = -1.0; // don't stop\n            engines = engineConfig.split(\",\");\n        }\n    }\n\n    public static Map<String, Object> compare(byte[] baselineImg, byte[] latestImg, Map<String, Object> options,\n                                              Map<String, Object> defaultOptions) throws MismatchException {\n\n        boolean allowScaling = toBool(defaultOptions.get(\"allowScaling\"));\n        ImageComparison imageComparison = new ImageComparison(baselineImg, latestImg, options, allowScaling);\n        imageComparison.configure(defaultOptions);\n\n        if (imageComparison.baselineMissing) {\n            imageComparison.result.put(\"isBaselineMissing\", true);\n            throw new MismatchException(\"baseline image was empty or not found\", imageComparison.result);\n        }\n\n        if (imageComparison.scaleMismatch) {\n            imageComparison.result.put(\"isScaleMismatch\", true);\n            throw new MismatchException(\"latest image dimensions != baseline image dimensions\", imageComparison.result);\n        }\n\n        double mismatchPercentage = 100.0;\n\n        for (String engine : imageComparison.engines) {\n            double currentMismatchPercentage;\n            switch (engine) {\n                case RESEMBLE:\n                    currentMismatchPercentage = imageComparison.execResemble();\n                    break;\n                case SSIM:\n                    currentMismatchPercentage = imageComparison.execSSIM();\n                    break;\n                default:\n                    logger.error(\"skipping unsupported image comparison engine: {}\", engine);\n                    continue;\n            }\n\n            if (currentMismatchPercentage <= mismatchPercentage) {\n                mismatchPercentage = currentMismatchPercentage;\n            }\n\n            if (mismatchPercentage < imageComparison.stopWhenMismatchIsLessThan) {\n                break;\n            }\n        }\n\n        return imageComparison.checkMismatch(mismatchPercentage);\n    }\n\n    private Map<String, Object> checkMismatch(double mismatchPercentage) {\n        result.put(\"mismatchPercentage\", mismatchPercentage);\n\n        if (mismatchPercentage <= 0.0 || mismatchPercentage < failureThreshold) {\n            return result;\n        }\n\n        String msg = \"latest image differed from baseline more than allowable threshold: \"\n                + mismatchPercentage + \"% >= \" + failureThreshold + \"%\";\n\n        result.put(\"isMismatch\", true);\n\n        throw new MismatchException(msg, result);\n    }\n\n    private double execResemble() {\n        Result resembleResult = Resemble.analyzeImages(baselinePixels, latestPixels, resembleOptions());\n        result.put(\"resembleMismatchPercentage\", resembleResult.mismatchedPercent);\n        return resembleResult.mismatchedPercent;\n    }\n\n    private double execSSIM() {\n        MSSIMMatrix ssimResult = ssim(\n                new Matrix(height, width, baselinePixels),\n                new Matrix(height, width, latestPixels),\n                ssimOptions());\n\n        double mismatchPercentage = (1.0 - ssimResult.mssim) * 100.0;\n        result.put(\"ssimMismatchPercentage\", mismatchPercentage);\n\n        return mismatchPercentage;\n    }\n\n    private io.github.t12y.resemble.Options resembleOptions() {\n        io.github.t12y.resemble.Options opts;\n\n        String ignoreOption = getString(\"ignore\", \"less\");\n        switch (ignoreOption.toLowerCase().replaceAll(\"[^a-z]\", \"\")) {\n            case \"nothing\":\n                opts = io.github.t12y.resemble.Options.ignoreNothing();\n                break;\n            case \"less\":\n                opts = io.github.t12y.resemble.Options.ignoreLess();\n                break;\n            case \"antialiasing\":\n                opts = io.github.t12y.resemble.Options.ignoreAntialiasing();\n                break;\n            case \"colors\":\n                opts = io.github.t12y.resemble.Options.ignoreColors();\n                break;\n            case \"alpha\":\n                opts = io.github.t12y.resemble.Options.ignoreAlpha();\n                break;\n            default:\n                logger.error(\"invalid 'ignore' option for resemble engine: {}\", ignoreOption);\n                opts = io.github.t12y.resemble.Options.ignoreNothing();\n        }\n\n        opts.width = width;\n        opts.height = height;\n\n        opts.ignoredBoxes = getIgnoredBoxes();\n        opts.ignoreAreasColoredWith = getIntArray(options.get(\"ignoreAreasColoredWith\"), IGNORED_COLOR_KEYS);\n\n        opts.ignoreColors = getBool(\"ignoreColors\", opts.ignoreColors);\n        opts.ignoreAntialiasing = getBool(\"ignoreAntialiasing\", opts.ignoreAntialiasing);\n\n        Object tolerancesObj = options.get(\"tolerances\");\n        if (tolerancesObj instanceof Map) {\n            Map tolerances = (Map) tolerancesObj;\n            opts.redTolerance = toDouble(tolerances.get(\"red\"), opts.redTolerance);\n            opts.greenTolerance = toDouble(tolerances.get(\"green\"), opts.greenTolerance);\n            opts.blueTolerance = toDouble(tolerances.get(\"blue\"), opts.blueTolerance);\n            opts.alphaTolerance = toDouble(tolerances.get(\"alpha\"), opts.alphaTolerance);\n            opts.minBrightness = toDouble(tolerances.get(\"minBrightness\"), opts.minBrightness);\n            opts.maxBrightness = toDouble(tolerances.get(\"maxBrightness\"), opts.maxBrightness);\n        }\n\n        return opts;\n    }\n\n    private io.github.t12y.ssim.models.Options ssimOptions() {\n        io.github.t12y.ssim.models.Options opts = io.github.t12y.ssim.models.Options.Defaults();\n\n        opts.ssim = Options.SSIMImpl.valueOf(getString(\"ssim\", asString(opts.ssim)));\n        opts.rgb2grayVersion = Options.RGB2Gray.valueOf(getString(\"rgb2grayVersion\", asString(opts.rgb2grayVersion)));\n\n        opts.k1 = getDouble(\"k1\", opts.k1);\n        opts.k2 = getDouble(\"k2\", opts.k2);\n\n        opts.windowSize = getInt(\"windowSize\", opts.windowSize);\n        opts.bitDepth = getInt(\"bitDepth\", opts.bitDepth);\n        opts.maxSize = getInt(\"maxSize\", opts.maxSize);\n\n        opts.ignoredBoxes = getIgnoredBoxes();\n\n        return opts;\n    }\n\n    private boolean getBool(String name, boolean defaultValue) {\n        if (!options.containsKey(name)) {\n            return defaultValue;\n        }\n        return toBool(options.get(name));\n    }\n\n    private static boolean toBool(Object obj) {\n        if (obj == null) {\n            return false;\n        }\n        return Boolean.parseBoolean(asString(obj));\n    }\n\n    private double getDouble(String name, double defaultValue) {\n        return toDouble(options.get(name), defaultValue);\n    }\n\n    private static double toDouble(Object obj, double defaultValue) {\n        if (!(obj instanceof Number)) {\n            return defaultValue;\n        }\n        return ((Number) obj).doubleValue();\n    }\n\n    private int getInt(String name, int defaultValue) {\n        Object val = options.get(name);\n        if (!(val instanceof Number)) {\n            return defaultValue;\n        }\n        return ((Number) val).intValue();\n    }\n\n    private String getString(String name, String defaultValue) {\n        if (!options.containsKey(name)) {\n            return defaultValue;\n        }\n        return asString(options.get(name));\n    }\n\n    private static String asString(Object obj) {\n        if (obj == null) {\n            return null;\n        }\n        return obj.toString();\n    }\n\n    private int[][] getIgnoredBoxes() {\n        Object boxes = options.get(\"ignoredBoxes\");\n        if (!(boxes instanceof Collection)) {\n            return null;\n        }\n\n        List<int[]> ignoredBoxes = new ArrayList<>();\n        for (Object boxObj : (Collection) boxes) {\n            int[] ignoredBox = getIntArray(boxObj, IGNORED_BOX_KEYS);\n            if (ignoredBox != null) {\n                ignoredBoxes.add(ignoredBox);\n            }\n        }\n\n        return ignoredBoxes.toArray(new int[0][]);\n    }\n\n    private static int[] getIntArray(Object obj, String[] keys) {\n        if (!(obj instanceof Map)) {\n            return null;\n        }\n\n        int[] vals = new int[keys.length];\n        Map m = (Map) obj;\n\n        for (int i = 0; i < keys.length; i++) {\n            Object val = m.get(keys[i]);\n            if (val instanceof Number) {\n                vals[i] = ((Number) val).intValue();\n            }\n        }\n\n        return vals;\n    }\n\n    // BufferedImage returns pixels packed into int[] in ARGB order -- we need *unpacked* RGBA to for Resemble / SSIM\n    // see: https://developer.mozilla.org/en-US/docs/Web/API/ImageData\n    private static double[] unpackPixels(int[] packed) {\n        int packedLength = packed.length;\n        double[] unpacked = new double[packedLength * 4];\n        int unpackedIndex;\n        int packedPixel;\n\n        for (int i = 0; i < packedLength; i++) {\n            packedPixel = packed[i];\n            unpackedIndex = i * 4;\n\n            unpacked[unpackedIndex] = (0xff & (packedPixel >> 16));\n            unpacked[unpackedIndex + 1] = (0xff & (packedPixel >> 8));\n            unpacked[unpackedIndex + 2] = (0xff & packedPixel);\n            unpacked[unpackedIndex + 3] = (0xff & (packedPixel >>> 24));\n        }\n\n        return unpacked;\n    }\n\n    public static class MismatchException extends RuntimeException {\n\n        public Map<String, Object> data;\n\n        public MismatchException(String msg, Map<String, Object> data) {\n            super(msg);\n            data.put(\"error\", getMessage());\n            this.data = data;\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/Json.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.jayway.jsonpath.DocumentContext;\nimport com.jayway.jsonpath.JsonPath;\nimport com.jayway.jsonpath.PathNotFoundException;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class Json {\n\n    private static final Logger logger = LoggerFactory.getLogger(Json.class);\n\n    private final DocumentContext doc;\n    private final boolean array;\n    private final String prefix;\n\n    private String prefix(String path) {\n        return path.charAt(0) == '$' ? path : prefix + path;\n    }\n\n    public static Json object() {\n        return Json.of(\"{}\");\n    }\n\n    public static Json array() {\n        return Json.of(\"[]\");\n    }\n\n    public static Json of(Object any) {\n        if (any instanceof String) {\n            return new Json(JsonPath.parse((String) any));\n        } else if (any instanceof List) {\n            return new Json(JsonPath.parse((List) any));\n        } else if (any instanceof Map) {\n            return new Json(JsonPath.parse((Map) any));\n        } else {\n            String json = JsonUtils.toJson(any);\n            return new Json(JsonPath.parse(json));\n        }\n    }\n    \n    public static <T> T parse(String json) {\n        return Json.of(json).value();\n    }\n\n    private Json(DocumentContext doc) {\n        this.doc = doc;\n        array = (doc.json() instanceof List);\n        prefix = array ? \"$\" : \"$.\";\n    }\n\n    public Json getJson(String path) {\n        return Json.of(get(path, Object.class));\n    }\n\n    public <T> List<T> getAll(String prefix, List<String> paths) {\n        List<T> res = new ArrayList();\n        for(String path : paths) {\n            res.add((T) doc.read(prefix(prefix + path)));\n        }\n        return res;\n    }\n\n    public <T> List<T> getAll(List<String> paths) {\n        return Json.this.getAll(\"\", paths);\n    }\n\n    public <T> T get(String path) {\n        return (T) doc.read(prefix(path));\n    }\n\n    public <T> T get(String path, T defaultValue) {\n        return (T) getOptional(path).orElse(defaultValue);\n    }\n\n    public <T> Optional<T> getOptional(String path) {\n        try {\n            return Optional.<T>of(get(path));\n        } catch (Exception e) {\n            return Optional.empty();\n        }\n    }\n\n    public <T> T getFirst(String path) {\n        List<T> list = get(path);\n        if (list == null || list.isEmpty()) {\n            return null;\n        }\n        return list.get(0);\n    }\n\n    public <T> T getAs(String path, Class<T> clazz) {\n        return doc.read(prefix(path), clazz);\n    }\n\n    @Override\n    public String toString() {\n        return doc.jsonString();\n    }\n\n    public String toStringPretty() {\n        return JsonUtils.toJson(value(), true);\n    }\n\n    public boolean isArray() {\n        return array;\n    }\n\n    public <T> T value() {\n        return doc.read(\"$\");\n    }\n\n    public List asList() {\n        return value();\n    }\n\n    public Map<String, Object> asMap() {\n        return value();\n    }\n    \n    public Json set(String path, Object value) {\n        setInternal(path, value);\n        return this;\n    }    \n\n    public Json set(String path, String value) {\n        if (JsonUtils.isJson(value)) {\n            setInternal(path, Json.of(value).value());\n        } else {\n            if (value != null && !value.isEmpty() && value.charAt(0) == '\\\\') {\n                value = value.substring(1);\n            }\n            setInternal(path, value);\n        }\n        return this;\n    }\n    \n    public Json setAsString(String path, String value) {\n        setInternal(path, value);\n        return this;\n    }\n\n    public Json remove(String path) {\n        doc.delete(prefix(path));\n        return this;\n    }\n\n    private boolean isArrayPath(String s) {\n        return s.endsWith(\"]\") && !s.endsWith(\"']\");\n    }\n\n    private String arrayKey(String s) {\n        int pos = s.lastIndexOf('[');\n        return s.substring(0, pos);\n    }\n\n    private int arrayIndex(String s) {\n        int leftPos = s.lastIndexOf('[');\n        if (leftPos == -1) {\n            return -1;\n        }\n        int rightPos = s.indexOf(']', leftPos);\n        if (leftPos == -1) {\n            return -1;\n        }\n        String num = s.substring(leftPos + 1, rightPos);\n        if (num.isEmpty()) {\n            return -1;\n        }\n        try {\n            return Integer.valueOf(num);\n        } catch (NumberFormatException e) {\n            return -1;\n        }\n    }\n\n    private void setInternal(String path, Object o) {\n        path = prefix(path);\n        if (\"$\".equals(path)) {\n            throw new RuntimeException(\"cannot replace root path $\");\n        }\n        boolean forArray = isArrayPath(path);\n        if (!pathExists(path)) {\n            createPath(path, forArray);\n        }\n        StringUtils.Pair pair = toParentAndLeaf(path);\n        if (forArray) {\n            int index = arrayIndex(pair.right);\n            if (index == -1) {\n                doc.add(arrayKey(path), o);\n            } else {\n                doc.set(path, o);\n            }\n        } else {\n            doc.put(pair.left, pair.right, o);\n        }\n    }\n\n    public boolean pathExists(String path) {\n        if (path.endsWith(\"[]\")) {\n            path = path.substring(0, path.length() - 2);\n        }\n        try {\n            Object temp = doc.read(path);\n            return temp != null;\n        } catch (PathNotFoundException pnfe) {\n            return false;\n        }\n    }\n\n    private void createPath(String path, boolean array) {\n        if (isArrayPath(path)) {\n            String parentPath = arrayKey(path);\n            if (!pathExists(parentPath)) {\n                createPath(parentPath, true);\n            }\n            List list = get(parentPath);\n            if (list == null) {\n                list = new ArrayList();\n                set(parentPath, list);\n            }\n            int index = arrayIndex(path);\n            if (list.size() <= index) {\n                for (int i = list.size(); i <= index; i++) {\n                    list.add(null);\n                }\n            }\n        } else {\n            StringUtils.Pair pair = toParentAndLeaf(path);\n            if (!pathExists(pair.left)) {\n                createPath(pair.left, false);\n            }\n            if (isArrayPath(pair.left)) {\n                if (isArrayPath(pair.right)) {\n                    doc.set(pair.left, new ArrayList());\n                } else {\n                    if (!pathExists(pair.left)) { // a necessary repetition\n                        doc.set(pair.left, new LinkedHashMap());\n                    }\n                    doc.put(pair.left, pair.right, array ? new ArrayList() : new LinkedHashMap());\n                }\n            } else {\n                doc.put(pair.left, pair.right, array ? new ArrayList() : new LinkedHashMap());\n            }\n        }\n    }\n\n    public static StringUtils.Pair toParentAndLeaf(String path) {\n        int pos = path.lastIndexOf('.');\n        int temp = path.lastIndexOf(\"['\");\n        if (temp != -1 && temp > pos) {\n            pos = temp - 1;\n        }\n        String right = path.substring(pos + 1);\n        if (right.startsWith(\"[\")) {\n            pos = pos + 1;\n        }\n        String left = path.substring(0, pos == -1 ? 0 : pos);\n        return StringUtils.pair(left, right);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/JsonUtils.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.intuit.karate.http.ResourceType;\nimport com.jayway.jsonpath.Configuration;\nimport com.jayway.jsonpath.Option;\nimport com.jayway.jsonpath.spi.json.JsonProvider;\nimport com.jayway.jsonpath.spi.json.JsonSmartJsonProvider;\nimport com.jayway.jsonpath.spi.mapper.JsonSmartMappingProvider;\nimport com.jayway.jsonpath.spi.mapper.MappingProvider;\nimport de.siegmar.fastcsv.reader.CsvReader;\nimport de.siegmar.fastcsv.reader.CsvRecord;\nimport de.siegmar.fastcsv.writer.CsvWriter;\nimport java.io.StringWriter;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.IdentityHashMap;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport net.minidev.json.JSONStyle;\nimport net.minidev.json.JSONValue;\nimport net.minidev.json.parser.JSONParser;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.yaml.snakeyaml.LoaderOptions;\nimport org.yaml.snakeyaml.Yaml;\nimport org.yaml.snakeyaml.constructor.SafeConstructor;\n\nimport static net.minidev.json.JSONValue.defaultReader;\nimport org.w3c.dom.Node;\n\n/**\n *\n * @author pthomas3\n */\npublic class JsonUtils {\n\n    private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);\n\n    private static final JSONStyle JSON_STYLE = new JSONStyle(JSONStyle.FLAG_PROTECT_4WEB);\n\n    private JsonUtils() {\n        // only static methods\n    }\n\n    static {\n        // ensure that even if jackson (databind?) is on the classpath, don't switch provider\n        Configuration.setDefaults(new Configuration.Defaults() {\n            private final JsonProvider jsonProvider = new JsonSmartJsonProvider();\n            private final MappingProvider mappingProvider = new JsonSmartMappingProvider();\n\n            @Override\n            public JsonProvider jsonProvider() {\n                return jsonProvider;\n            }\n\n            @Override\n            public MappingProvider mappingProvider() {\n                return mappingProvider;\n            }\n\n            @Override\n            public Set<Option> options() {\n                return EnumSet.noneOf(Option.class);\n            }\n        });\n    }\n\n    public static boolean isJson(String s) {\n        if (s == null || s.isEmpty()) {\n            return false;\n        }\n        if (s.charAt(0) == ' ') {\n            s = s.trim();\n            if (s.isEmpty()) {\n                return false;\n            }\n        }\n        return s.charAt(0) == '{' || s.charAt(0) == '[';\n    }\n\n    public static String toStrictJson(String raw) {\n        JSONParser jp = new JSONParser(JSONParser.MODE_PERMISSIVE);\n        try {\n            Object o = jp.parse(raw);\n            return JSONValue.toJSONString(o, JSON_STYLE);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static String toJson(Object o) {\n        return toJson(o, false);\n    }\n\n    public static String toJson(Object o, boolean pretty) {\n        if (!pretty) { // TODO use JSONStyleIdent in json-smart 2.4\n            try {\n                return JSONValue.toJSONString(o, JSON_STYLE);\n            } catch (Throwable t) {\n                logger.warn(\"object to json serialization failure, trying alternate approach: {}\", t.getMessage());\n            }\n        }\n        return JsonUtils.toJsonSafe(o, pretty);\n    }\n\n    public static byte[] toJsonBytes(Object o) {\n        return toJson(o).getBytes(StandardCharsets.UTF_8);\n    }\n\n    public static Object fromJson(String json) {\n        return JSONValue.parseKeepingOrder(json);\n    }\n\n    public static Object fromJsonStrict(String json) {\n        JSONParser parser = new JSONParser(JSONParser.MODE_RFC4627);\n        try {\n            return parser.parse(json.trim(), defaultReader.DEFAULT_ORDERED);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static Object fromJson(String s, String className) {\n        try {\n            Class clazz = Class.forName(className);\n            return JSONValue.parse(s, clazz);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static <T> T fromJson(String s, Class<T> clazz) {\n        return (T) fromJson(s, clazz.getName());\n    }\n\n    public static Object fromYaml(String raw) {\n        Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));\n        return yaml.load(raw);\n    }\n\n    public static List<Map> fromCsv(String raw) {\n        CsvReader<CsvRecord> reader = CsvReader.builder().ofCsvRecord(raw);\n        List<String> header = new ArrayList();\n        List<Map> rows = new ArrayList();\n        try {\n            boolean first = true;\n            for (CsvRecord row : reader) {\n                if (first) {\n                    for (String field : row.getFields()) {\n                        header.add(field.replace(\"\\ufeff\", \"\")); // remove byte order mark\n                    }\n                    first = false;\n                } else {\n                    int count = header.size();\n                    Map<String, Object> map = new LinkedHashMap(count);\n                    for (int i = 0; i < count; i++) {\n                        map.put(header.get(i), row.getField(i));\n                    }\n                    rows.add(map);\n                }\n            }\n            return rows;\n        } catch (Exception e) {\n            logger.warn(\"failed to parse csv: {}\", e.getMessage());\n            return rows;\n        }\n    }\n\n    public static String toCsv(List<Map<String, Object>> list) {\n        StringWriter sw = new StringWriter();\n        CsvWriter writer = CsvWriter.builder().build(sw);\n        // header row\n        if (!list.isEmpty()) {\n            writer.writeRecord(list.get(0).keySet());\n        }\n        for (Map<String, Object> map : list) {\n            List<String> row = new ArrayList(map.size());\n            for (Object value : map.values()) {\n                row.add(value == null ? null : value.toString());\n            }\n            writer.writeRecord(row);\n        }\n        return sw.toString();\n    }\n\n    public static Object shallowCopy(Object o) {\n        if (o instanceof List) {\n            return new ArrayList((List) o);\n        } else if (o instanceof Map) {\n            return new LinkedHashMap((Map) o);\n        } else {\n            return o;\n        }\n    }\n\n    public static Object deepCopy(Object o) {\n        // anti recursion / back-references\n        Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap());\n        return recurseDeepCopy(o, seen);\n    }\n\n    private static Object recurseDeepCopy(Object o, Set<Object> seen) {\n        if (o instanceof List) {\n            List list = (List) o;\n            if (seen.add(o)) {\n                int count = list.size();\n                List listCopy = new ArrayList(count);\n                for (int i = 0; i < count; i++) {\n                    listCopy.add(recurseDeepCopy(list.get(i), seen));\n                }\n                return listCopy;\n            } else {\n                return o;\n            }\n        } else if (o instanceof Map) {\n            if (seen.add(o)) {\n                Map<String, Object> map = (Map<String, Object>) o;\n                Map<String, Object> mapCopy = new LinkedHashMap(map.size());\n                map.forEach((k, v) -> {\n                    mapCopy.put(k, recurseDeepCopy(v, seen));\n                });\n                return mapCopy;\n            } else {\n                return o;\n            }\n        } else {\n            return o;\n        }\n    }\n\n    public static String toJsonSafe(Object o, boolean pretty) {\n        StringBuilder sb = new StringBuilder();\n        // anti recursion / back-references\n        Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap());\n        recurseJsonString(o, pretty, sb, 0, seen);\n        if (pretty) {\n            sb.append('\\n');\n        }\n        return sb.toString();\n    }\n\n    private static void pad(StringBuilder sb, int depth) {\n        for (int i = 0; i < depth; i++) {\n            sb.append(' ').append(' ');\n        }\n    }\n\n    private static void ref(StringBuilder sb, Object o) {\n        sb.append(\"\\\"#ref:\").append(o.getClass().getName()).append('\"');\n    }\n\n    public static String escapeValue(String raw) {\n        return JSONValue.escape(raw, JSONStyle.LT_COMPRESS);\n    }\n\n    private static void recurseJsonString(Object o, boolean pretty, StringBuilder sb, int depth, Set<Object> seen) {\n        if (o == null) {\n            sb.append(\"null\");\n        } else if (o instanceof List) {\n            List list = (List) o;            \n            if (list.isEmpty() || seen.add(o)) {                \n                sb.append('[');\n                if (pretty) {\n                    sb.append('\\n');\n                }\n                Iterator iterator = list.iterator();\n                while (iterator.hasNext()) {\n                    Object child = iterator.next();\n                    if (pretty) {\n                        pad(sb, depth + 1);\n                    }\n                    recurseJsonString(child, pretty, sb, depth + 1, seen);\n                    if (iterator.hasNext()) {\n                        sb.append(',');\n                    }\n                    if (pretty) {\n                        sb.append('\\n');\n                    }\n                }\n                if (pretty) {\n                    pad(sb, depth);\n                }\n                sb.append(']');\n            } else {\n                ref(sb, o);\n            }\n        } else if (o instanceof Map) {\n            Map<String, Object> map = (Map<String, Object>) o;\n            if (map.isEmpty() || seen.add(o)) {\n                sb.append('{');\n                if (pretty) {\n                    sb.append('\\n');\n                }                \n                Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();\n                while (iterator.hasNext()) {\n                    Map.Entry<String, Object> entry = iterator.next();\n                    Object key = entry.getKey(); // found a rare case where this was a boolean\n                    if (pretty) {\n                        pad(sb, depth + 1);\n                    }\n                    sb.append('\"').append(escapeValue(key == null ? null : key.toString())).append('\"').append(':');\n                    if (pretty) {\n                        sb.append(' ');\n                    }\n                    recurseJsonString(entry.getValue(), pretty, sb, depth + 1, seen);\n                    if (iterator.hasNext()) {\n                        sb.append(',');\n                    }\n                    if (pretty) {\n                        sb.append('\\n');\n                    }\n                }\n                if (pretty) {\n                    pad(sb, depth);\n                }\n                sb.append('}');\n            } else {\n                ref(sb, o);\n            }\n        } else if (o instanceof String) {\n            String value = (String) o;\n            sb.append('\"').append(escapeValue(value)).append('\"');\n        } else if (o instanceof Number || o instanceof Boolean) {\n            sb.append(o);\n        } else { // TODO custom writers ?\n            String value = o.toString();\n            sb.append('\"').append(escapeValue(value)).append('\"');\n        }\n    }\n\n    public static void removeKeysWithNullValues(Object o) {\n        if (o instanceof List) {\n            List list = (List) o;\n            for (Object v : list) {\n                removeKeysWithNullValues(v);\n            }\n        } else if (o instanceof Map) {\n            Map<String, Object> map = (Map) o;\n            List<String> toRemove = new ArrayList();\n            for (Map.Entry<String, Object> entry : map.entrySet()) {\n                Object v = entry.getValue();\n                if (v == null) {\n                    toRemove.add(entry.getKey());\n                } else {\n                    removeKeysWithNullValues(v);\n                }\n            }\n            toRemove.forEach(key -> map.remove(key));\n        }\n    }\n\n    public static Map<String, String> simplify(Map<String, List<String>> map) {\n        if (map == null) {\n            return Collections.emptyMap();\n        }\n        Map<String, String> result = new LinkedHashMap(map.size());\n        map.forEach((k, v) -> {\n            if (v instanceof List) {\n                List list = (List) v;\n                if (list.size() > 1) {\n                    result.put(k, StringUtils.join(list, \",\"));\n                } else if (list.size() == 1) {\n                    Object value = list.get(0);\n                    if (value != null) {\n                        result.put(k, value + \"\");\n                    }                    \n                }\n            } else if (v != null) {\n                result.put(k, v + \"\");\n            }\n        });\n        return result;\n    }\n\n    public static String toString(Object o) {\n        return toString(o, false);\n    }\n\n    public static String toString(Object o, boolean pretty) {\n        if (o == null) {\n            return null;\n        }\n        if (o instanceof Map || o instanceof List) {\n            return JsonUtils.toJson(o, pretty);\n        } else if (o instanceof Node) {\n            return XmlUtils.toString((Node) o, pretty);\n        } else if (o instanceof byte[]) {\n            return FileUtils.toString((byte[]) o);\n        } else {\n            return o.toString();\n        }\n    }\n\n    public static byte[] toBytes(Object o) {\n        if (o == null) {\n            return null;\n        }\n        if (o instanceof Map || o instanceof List) {\n            return FileUtils.toBytes(JsonUtils.toJson(o));\n        } else if (o instanceof Node) {\n            return FileUtils.toBytes(XmlUtils.toString((Node) o));\n        } else if (o instanceof byte[]) {\n            return (byte[]) o;\n        } else {\n            return FileUtils.toBytes(o.toString());\n        }\n    }\n\n    public static Object fromBytes(byte[] bytes, boolean strict, ResourceType resourceType) {\n        if (bytes == null) {\n            return null;\n        }\n        String raw = FileUtils.toString(bytes);\n        return fromString(raw, strict, resourceType);\n    }\n\n    public static Object fromString(String raw, boolean strict, ResourceType resourceType) {\n        String trimmed = raw.trim();\n        if (trimmed.isEmpty()) {\n            return raw;\n        }\n        if (resourceType != null && resourceType.isBinary()) {\n            return raw;\n        }\n        switch (trimmed.charAt(0)) {\n            case '{':\n            case '[':\n                if (strict) {\n                    return JsonUtils.fromJsonStrict(raw);\n                }\n                try {\n                    return JsonUtils.fromJson(raw);\n                } catch (Exception e) {\n                    logger.trace(\"failed to parse json: {}\", e.getMessage());\n                    return raw;\n                }\n            case '<':\n                if (resourceType == null || resourceType.isXml()) {\n                    try {\n                        return XmlUtils.toXmlDoc(raw);\n                    } catch (Exception e) {\n                        logger.trace(\"failed to parse xml: {}\", e.getMessage());\n                        if (strict) {\n                            throw e;\n                        }\n                        return raw;\n                    }\n                } else {\n                    return raw;\n                }\n            default:\n                return raw;\n        }\n    }\n\n    public static Object fromStringSafe(String raw) {\n        try {\n            return fromString(raw, false, null);\n        } catch (Exception e) {\n            logger.trace(\"failed to auto convert: {}\", e + \"\");\n            return raw;\n        }\n    }    \n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/KarateException.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\n/**\n *\n * @author pthomas3\n */\npublic class KarateException extends RuntimeException {\n\n    public KarateException(String message) {\n        super(message);\n    }\n\n    public KarateException(String message, Throwable cause) {\n        super(message, cause);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/LogAppender.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\n/**\n *\n * @author pthomas3\n */\npublic interface LogAppender {\n    \n    String getBuffer();\n\n    String collect();\n\n    void append(String text);\n\n    void close();\n\n    public static final LogAppender NO_OP = new LogAppender() {\n        @Override\n        public String getBuffer() {\n            return \"\";\n        }        \n        \n        @Override\n        public String collect() {\n            return \"\";\n        }\n\n        @Override\n        public void append(String text) {\n\n        }\n\n        @Override\n        public void close() {\n\n        }\n    };\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/Logger.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport java.text.DateFormat;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.helpers.FormattingTuple;\nimport org.slf4j.helpers.MessageFormatter;\n\n/**\n * derived from org.slf4j.simple.SimpleLogger\n *\n * @author pthomas3\n */\npublic class Logger {\n\n    private static final String DEFAULT_PACKAGE = \"com.intuit.karate\";\n\n    private final org.slf4j.Logger LOGGER;\n\n    // not static, has to be per thread\n    private final DateFormat dateFormatter = new SimpleDateFormat(\"HH:mm:ss.SSS\");\n\n    private LogAppender appender = LogAppender.NO_OP;\n\n    private boolean appendOnly;\n\n    private boolean logOnly;\n\n    public void setAppender(LogAppender appender) {\n        this.appender = appender;\n    }\n\n    public LogAppender getAppender() {\n        return appender;\n    }\n\n    public boolean isTraceEnabled() {\n        return LOGGER.isTraceEnabled();\n    }\n\n    public void setAppendOnly(boolean appendOnly) {\n        this.appendOnly = appendOnly;\n    }\n\n    public boolean isAppendOnly() {\n        return appendOnly;\n    }\n\n    public void setLogOnly(boolean logOnly) {\n        this.logOnly = logOnly;\n    }\n\n    public boolean isLogOnly() {\n        return logOnly;\n    }\n\n    public Logger(Class clazz) {\n        LOGGER = LoggerFactory.getLogger(clazz);\n    }\n\n    public Logger(String name) {\n        LOGGER = LoggerFactory.getLogger(name);\n    }\n\n    public Logger() {\n        this(DEFAULT_PACKAGE);\n    }\n\n    public void trace(String format, Object... arguments) {\n        if (LOGGER.isTraceEnabled()) {\n            if (!appendOnly) {\n                LOGGER.trace(format, arguments);\n            }\n            if (!logOnly) {\n                formatAndAppend(format, arguments);\n            }\n        }\n    }\n\n    public void debug(String format, Object... arguments) {\n        if (LOGGER.isDebugEnabled()) {\n            if (!appendOnly) {\n                LOGGER.debug(format, arguments);\n            }\n            if (!logOnly) {\n                formatAndAppend(format, arguments);\n            }\n        }\n    }\n\n    public void info(String format, Object... arguments) {\n        if (LOGGER.isInfoEnabled()) {\n            if (!appendOnly) {\n                LOGGER.info(format, arguments);\n            }\n            if (!logOnly) {\n                formatAndAppend(format, arguments);\n            }\n        }\n    }\n\n    public void warn(String format, Object... arguments) {\n        if (LOGGER.isWarnEnabled()) {\n            if (!appendOnly) {\n                LOGGER.warn(format, arguments);\n            }\n            if (!logOnly) {\n                formatAndAppend(format, arguments);\n            }\n        }\n    }\n\n    public void error(String format, Object... arguments) {\n        if (LOGGER.isErrorEnabled()) {\n            if (!appendOnly) {\n                LOGGER.error(format, arguments);\n            }\n            if (!logOnly) {\n                formatAndAppend(format, arguments);\n            }\n        }\n    }\n\n    private String getFormattedDate() {\n        Date now = new Date();\n        String dateText;\n        dateText = dateFormatter.format(now);\n        return dateText;\n    }\n\n    private void formatAndAppend(String format, Object... arguments) {\n        if (appender == null) {\n            return;\n        }\n        FormattingTuple tp = MessageFormatter.arrayFormat(format, arguments);\n        append(tp.getMessage());\n    }\n\n    private void append(String message) {\n        StringBuilder buf = new StringBuilder();\n        buf.append(getFormattedDate()).append(' ').append(message).append('\\n');\n        appender.append(buf.toString());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/Main.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation tests (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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.intuit.karate.core.MockServer;\nimport com.intuit.karate.core.RuntimeHookFactory;\nimport com.intuit.karate.http.HttpServer;\nimport com.intuit.karate.http.RequestHandler;\nimport com.intuit.karate.http.ServerConfig;\nimport com.intuit.karate.http.ServerContext;\nimport com.intuit.karate.http.SslContextFactory;\nimport com.intuit.karate.resource.ResourceUtils;\nimport com.intuit.karate.shell.Command;\nimport java.io.File;\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.Callable;\nimport java.util.stream.Collectors;\nimport org.slf4j.ILoggerFactory;\nimport org.slf4j.LoggerFactory;\nimport picocli.CommandLine;\nimport picocli.CommandLine.Option;\nimport picocli.CommandLine.Parameters;\n\n/**\n *\n * @author pthomas3\n */\npublic class Main implements Callable<Void> {\n\n    private static final String LOGBACK_CONFIG = \"logback.configurationFile\";\n\n    private static org.slf4j.Logger logger;\n\n    @Option(names = {\"-h\", \"--help\"}, usageHelp = true, description = \"display this help message\")\n    boolean help;\n\n    @Parameters(split = \"($|,)\", description = \"one or more tests (features) or search-paths to run\")\n    List<String> paths;\n\n    @Option(names = {\"-m\", \"--mock\", \"--mocks\"}, split = \",\", description = \"one or more mock server files\")\n    List<String> mocks;\n\n    @Option(names = {\"-P\", \"--prefix\"}, description = \"mock server path prefix (context-path)\")\n    String prefix = \"/\";\n\n    @Option(names = {\"-p\", \"--port\"}, description = \"server port (default 8080)\")\n    int port = 8080;\n\n    @Option(names = {\"-W\", \"--watch\"}, description = \"watch (and hot-reload) mock server file for changes\")\n    boolean watch;\n\n    @Option(names = {\"-S\", \"--serve\"}, description = \"app server using --workdir (experimental)\")\n    boolean serve;\n\n    @Option(names = {\"-s\", \"--ssl\"}, description = \"use ssl / https, will use '\"\n            + SslContextFactory.DEFAULT_CERT_NAME + \"' and '\" + SslContextFactory.DEFAULT_KEY_NAME\n            + \"' if they exist in the working directory, or generate them\")\n    boolean ssl;\n\n    @Option(names = {\"-c\", \"--cert\"}, description = \"ssl certificate (default: \" + SslContextFactory.DEFAULT_CERT_NAME + \")\")\n    File cert;\n\n    @Option(names = {\"-k\", \"--key\"}, description = \"ssl private key (default: \" + SslContextFactory.DEFAULT_KEY_NAME + \")\")\n    File key;\n\n    @Option(names = {\"-t\", \"--tags\"}, description = \"cucumber tags - e.g. '@smoke,~@skipme' [@ignore is always skipped by default]\")\n    List<String> tags;\n\n    @Option(names = {\"-T\", \"--threads\"}, description = \"number of threads when running tests\")\n    int threads;\n\n    @Option(names = {\"-o\", \"--output\"}, description = \"directory where logs and report sub-folders are output (default 'target')\")\n    String output = FileUtils.getBuildDir();\n\n    @Option(names = {\"-f\", \"--format\"}, split = \",\", description = \"comma separate report output formats. tilde excludes the output report. html report is included by default unless it's negated.\"\n            + \"e.g. '-f ~html,cucumber:json,junit:xml' - possible values [html: Karate HTML, cucumber:json: Cucumber JSON, junit:xml: JUnit XML]\")\n    List<String> formats;\n\n    @Option(names = {\"-n\", \"--name\"}, description = \"scenario name\")\n    String name;\n\n    @Option(names = {\"-e\", \"--env\"}, description = \"value of 'karate.env'\")\n    String env;\n\n    @Option(names = {\"-w\", \"--workdir\"}, description = \"working directory, defaults to '.'\")\n    File workingDir = FileUtils.WORKING_DIR;\n\n    @Option(names = {\"-g\", \"--configdir\"}, description = \"directory where 'karate-config.js' is expected (default 'classpath:' or <workingdir>)\")\n    String configDir;\n\n    @Option(names = {\"-r\", \"--reportdir\"}, description = \"directory where Karate HTML and JSON reports are placed (default '<output>/karate-reports')\")\n    String reportDir;\n\n    @Option(names = {\"-C\", \"--clean\"}, description = \"clean output directory\")\n    boolean clean;\n\n    @Option(names = {\"-B\", \"--backup-reportdir\"}, defaultValue = \"true\", arity = \"0..1\", fallbackValue = \"true\", description = \"backup report directory before running tests\")\n    boolean backupReportDir = true;\n\n    @Option(names = {\"-d\", \"--debug\"}, arity = \"0..1\", defaultValue = \"-1\", fallbackValue = \"0\",\n            description = \"debug mode (optional port else dynamically chosen)\")\n    int debugPort = -1;\n\n    @Option(names = {\"-D\", \"--dryrun\"}, description = \"dry run, generate html reports only\")\n    boolean dryRun;\n\n    @Option(names = {\"-H\", \"--hook\"}, split = \",\", description = \"class name of a RuntimeHook (or RuntimeHookFactory) to add\")\n    List<String> hookFactoryClassNames;\n\n    @Option(names = {\"--keep-original-headers\"}, description = \"keeping original headers given in the mock scenario or configure\")\n    boolean keepOriginalHeaders;\n\n    //==========================================================================\n    //\n    public void addPath(String path) {\n        if (paths == null) {\n            paths = new ArrayList();\n        }\n        paths.add(path);\n    }\n\n    public void setPaths(List<String> paths) {\n        this.paths = paths;\n    }\n\n    public List<String> getPaths() {\n        return paths;\n    }\n\n    public List<String> getTags() {\n        return tags;\n    }\n\n    public int getThreads() {\n        return threads;\n    }\n\n    public int getDebugPort() {\n        return debugPort;\n    }        \n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public boolean isOutputHtmlReport() {\n        return formats == null ? true : !formats.contains(\"~html\");\n    }\n\n    public boolean isOutputCucumberJson() {\n        return formats == null ? false : formats.contains(\"cucumber:json\");\n    }\n\n    public boolean isOutputJunitXml() {\n        return formats == null ? false : formats.contains(\"junit:xml\");\n    }\n\n    public String getEnv() {\n        return env;\n    }\n\n    public void setEnv(String env) {\n        this.env = env;\n    }\n\n    public String getConfigDir() {\n        return configDir;\n    }\n\n    public void setConfigDir(String configDir) {\n        this.configDir = configDir;\n    }\n\n    public String getReportDir() {\n        return reportDir;\n    }\n\n    public static Main parseKarateOptions(String line) {\n        String[] args = Command.tokenize(line);\n        return CommandLine.populateCommand(new Main(), args);\n    }\n\n    public static Main parseKarateArgs(List<String> args) {\n        return CommandLine.populateCommand(new Main(), args.toArray(new String[args.size()]));\n    }\n\n    public Collection<RuntimeHook> createHooks() {\n        if (this.hookFactoryClassNames != null) {\n            return this.hookFactoryClassNames.stream().map(c -> createHook(c)).collect(Collectors.toList());\n        }\n        return Collections.emptyList();\n    }\n\n    private RuntimeHook createHook(String hookClassName) {\n        if (hookClassName != null) {\n            try {\n                Class clazz = Class.forName(hookClassName);\n                if (RuntimeHookFactory.class.isAssignableFrom(clazz)) {;\n                    return ((RuntimeHookFactory) clazz.getDeclaredConstructor().newInstance()).create();\n                } else if (RuntimeHook.class.isAssignableFrom(clazz)) {\n                    return (RuntimeHook) clazz.getDeclaredConstructor().newInstance();\n                }\n            } catch (Exception e) {\n                logger.error(\"error instantiating RuntimeHook: {}\", hookClassName, e);\n            }\n            logger.error(\"provided hook / class is not a RuntimeHook or RuntimeHookFactory: {}\", hookClassName);\n        }\n        return null;\n    }\n\n    public static void main(String[] args) {\n        boolean isClean = false;\n        boolean isOutputArg = false;\n        String outputDir = FileUtils.getBuildDir();\n        // hack to manually extract the output dir arg to redirect karate.log if needed\n        for (String s : args) {\n            if (isOutputArg) {\n                outputDir = s;\n                isOutputArg = false;\n            }\n            if (s.startsWith(\"-o\") || s.startsWith(\"--output\")) {\n                int pos = s.indexOf('=');\n                if (pos != -1) {\n                    outputDir = s.substring(pos + 1);\n                } else {\n                    isOutputArg = true;\n                }\n            }\n            if (s.startsWith(\"-C\") || s.startsWith(\"--clean\")) {\n                isClean = true;\n            }\n        }\n        if (isClean) {\n            // ensure karate.log is not held open which will prevent\n            // a graceful delete of \"target\" especially on windows\n            System.setProperty(LOGBACK_CONFIG, \"logback-nofile.xml\");\n        } else {\n            System.setProperty(Constants.KARATE_OUTPUT_DIR, outputDir);\n            // ensure we init logback before anything else\n            String logbackConfig = System.getProperty(LOGBACK_CONFIG);\n            if (StringUtils.isBlank(logbackConfig)) {\n                File logbackXml = ResourceUtils.classPathOrFile(\"logback.xml\");\n                File logbackTest = ResourceUtils.classPathOrFile(\"logback-test.xml\");\n                if (logbackTest != null) {\n                    System.setProperty(LOGBACK_CONFIG, \"logback-test.xml\");\n                } else if (logbackXml != null) {\n                    System.setProperty(LOGBACK_CONFIG, \"logback.xml\");\n                } else {\n                    System.setProperty(LOGBACK_CONFIG, \"logback-fatjar.xml\");\n                }\n            }\n        }\n        resetLoggerConfig();\n        logger = LoggerFactory.getLogger(\"com.intuit.karate\");\n        logger.info(\"Karate version: {}\", FileUtils.KARATE_VERSION);\n        CommandLine cmd = new CommandLine(new Main());\n        int returnCode = cmd.execute(args);\n        System.exit(returnCode);\n    }\n\n    private static void resetLoggerConfig() {\n        ILoggerFactory factory = LoggerFactory.getILoggerFactory();\n        try {\n            Method reset = factory.getClass().getDeclaredMethod(\"reset\");\n            reset.invoke(factory);\n            Class clazz = Class.forName(\"ch.qos.logback.classic.util.ContextInitializer\");\n            Object temp = clazz.getDeclaredConstructors()[0].newInstance(factory);\n            Method autoConfig = clazz.getDeclaredMethod(\"autoConfig\");\n            autoConfig.invoke(temp);\n        } catch (Exception e) {\n            // ignore\n        }\n    }\n\n    public static Results startDebugServer(String[] args) {\n        try {\n            Class clazz = Class.forName(\"io.karatelabs.debug.Main\");\n            if (args.length > 1) {\n                Method method = clazz.getMethod(\"run\", String[].class);\n                Object results = method.invoke(null, (Object) args);\n                return (Results) results;\n            } else {\n                Method method = clazz.getMethod(\"main\", String[].class);\n                method.invoke(null, (Object) args);\n                return null;\n            }\n        } catch (Exception e) {\n            String message = \"debug server failed to start\";\n            System.out.println(message);\n            throw new RuntimeException(message);\n        }\n    }\n\n    @Override\n    public Void call() throws Exception {\n        if (clean) {\n            FileUtils.deleteDirectory(new File(output));\n            logger.info(\"deleted directory: {}\", output);\n        }\n        if (debugPort != -1) {\n            startDebugServer(new String[]{debugPort + \"\"});\n            return null;\n        }\n        if (paths != null) {\n            Results results = Runner\n                    .path(paths).tags(tags).scenarioName(name)\n                    .karateEnv(env)\n                    .workingDir(workingDir)\n                    .buildDir(output)\n                    .backupReportDir(backupReportDir)\n                    .configDir(configDir)\n                    .reportDir(reportDir)\n                    .outputHtmlReport(isOutputHtmlReport())\n                    .outputCucumberJson(isOutputCucumberJson())\n                    .outputJunitXml(isOutputJunitXml())\n                    .dryRun(dryRun)\n                    .hooks(createHooks())\n                    .parallel(threads);\n            if (results.getFailCount() > 0) {\n                Exception ke = new KarateException(\"there are test failures !\");\n                StackTraceElement[] newTrace = new StackTraceElement[]{\n                    new StackTraceElement(\".\", \".\", \".\", -1)\n                };\n                ke.setStackTrace(newTrace);\n                throw ke;\n            }\n            return null;\n        }\n        if (clean) {\n            return null;\n        }\n        // these files will not be created, unless ssl or ssl proxying happens\n        // and then they will be lazy-initialized\n        if (cert == null || key == null) {\n            cert = new File(SslContextFactory.DEFAULT_CERT_NAME);\n            key = new File(SslContextFactory.DEFAULT_KEY_NAME);\n        }\n        if (env != null) { // some advanced mocks may want karate.env\n            System.setProperty(Constants.KARATE_ENV, env);\n        }\n        if (serve) {\n            ServerConfig config = new ServerConfig(workingDir.getPath())\n                    .noCache(true)\n                    .devMode(true)\n                    .autoCreateSession(true);\n            RequestHandler handler = new RequestHandler(config);\n            HttpServer.Builder builder = HttpServer\n                    .handler(handler)\n                    .corsEnabled(true)\n                    .keepOriginalHeaders(keepOriginalHeaders);\n            if (ssl) {\n                builder.https(port)\n                        .certFile(cert)\n                        .keyFile(key);\n            } else {\n                builder.http(port);\n            }\n            HttpServer server = builder.build();\n            server.waitSync();\n            return null;\n        }\n        if (mocks == null || mocks.isEmpty()) {\n            CommandLine.usage(this, System.err);\n            return null;\n        }\n        if (mocks.size() == 1 && mocks.get(0).endsWith(\".js\")) { // js mode\n            ServerConfig config = new ServerConfig(workingDir.getPath())\n                    .useGlobalSession(true);\n            config.contextFactory(request -> {\n                ServerContext context = new ServerContext(config, request);\n                context.setApi(true);\n                request.setResourcePath(mocks.get(0));\n                return context;\n            });\n            HttpServer.Builder builder = HttpServer.config(config)\n                    .local(false)\n                    .corsEnabled(true)\n                    .keepOriginalHeaders(keepOriginalHeaders);\n            if (ssl) {\n                builder.https(port);\n            } else {\n                builder.http(port);\n            }\n            HttpServer server = builder.build();\n            server.waitSync();\n            return null;\n        }\n        MockServer.Builder builder = MockServer\n                .featurePaths(mocks)\n                .pathPrefix(prefix)\n                .certFile(cert)\n                .keyFile(key)\n                .watch(watch)\n                .keepOriginalHeaders(keepOriginalHeaders);\n        if (ssl) {\n            builder.https(port);\n        } else {\n            builder.http(port);\n        }\n        MockServer server = builder.build();\n        server.waitSync();\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/Match.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.intuit.karate.MatchOperator.CoreOperator;\nimport com.intuit.karate.graal.JsEngine;\nimport java.lang.reflect.Array;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.function.Function;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.w3c.dom.Node;\n\nimport static com.intuit.karate.Match.MatchOperatorFactory.*;\n\n/**\n *\n * @author pthomas3\n */\npublic class Match {\n\n\n    // Enum constant with that value should never be returned by Match.macroToMatchType.\n    private static final int TYPE_DOES_NOT_SUPPORT_MACRO_SHORTCUT = -1;\n\n    interface MatchOperatorFactory {\n        MatchOperator create(boolean matchEachEmptyAllowed);\n\n        static MatchOperatorFactory not(CoreOperatorFactory delegateFactory, String failureMessage) {\n            return matchEachEmptyAllowed -> new MatchOperator.NotOperator(delegateFactory.create(matchEachEmptyAllowed), failureMessage);\n        }\n\n        static MatchOperatorFactory deep(CoreOperatorFactory delegateFactory) {\n            return matchEachEmptyAllowed -> delegateFactory.create(matchEachEmptyAllowed).deep();\n        }\n\n        static MatchOperatorFactory each(MatchOperatorFactory delegateFactory) {\n            return matchEachEmptyAllowed -> new MatchOperator.EachOperator(delegateFactory.create(matchEachEmptyAllowed), matchEachEmptyAllowed);\n        }\n    }\n\n    interface CoreOperatorFactory extends MatchOperatorFactory {\n        @Override\n        CoreOperator create(boolean matchEachEmptyAllowed);\n    }\n\n    public static enum Type {\n\n        EQUALS(CoreOperator::equalsOperator, 0),\n        NOT_EQUALS(not(CoreOperator::equalsOperator, \"equals\"), 2),\n        CONTAINS(CoreOperator::containsOperator, 1),\n        NOT_CONTAINS(not(CoreOperator::containsOperator, \"actual contains expected\"), 2),\n        CONTAINS_ONLY(CoreOperator::containsOnlyOperator, 2),\n        CONTAINS_ANY(CoreOperator::containsAnyOperator, 2),\n        CONTAINS_DEEP(deep(CoreOperator::containsOperator), 2),\n        CONTAINS_ONLY_DEEP(deep(CoreOperator::containsOnlyOperator), TYPE_DOES_NOT_SUPPORT_MACRO_SHORTCUT),\n        CONTAINS_ANY_DEEP(deep(CoreOperator::containsAnyOperator), TYPE_DOES_NOT_SUPPORT_MACRO_SHORTCUT),\n        EACH_EQUALS(each(EQUALS.operatorFactory), 0),\n        EACH_NOT_EQUALS(each(NOT_EQUALS.operatorFactory), 2),\n        EACH_CONTAINS(each(CONTAINS.operatorFactory), 1),\n        EACH_NOT_CONTAINS(each(NOT_CONTAINS.operatorFactory), 2),\n        EACH_CONTAINS_ONLY(each(CONTAINS_ONLY.operatorFactory), 2),\n        EACH_CONTAINS_ANY(each(CONTAINS_ANY.operatorFactory), 2),\n        EACH_CONTAINS_DEEP(each(CONTAINS_DEEP.operatorFactory), 2);\n\n\n        final MatchOperatorFactory operatorFactory;\n        final int shortcutLength;\n\n        Type(MatchOperatorFactory operatorFactory, int shortcutLength) {\n            this.operatorFactory = operatorFactory;\n            this.shortcutLength = shortcutLength;\n        }\n\n        MatchOperator operator(boolean matchEachEmptyAllowed) {\n            return operatorFactory.create(matchEachEmptyAllowed);\n        }\n    }\n\n    static final Result PASS = new Result(true, null);\n\n    static Result fail(String message) {\n        return new Result(false, message);\n    }\n\n    interface Validator extends Function<Value, Result> {\n        //\n    }\n\n    static class RegexValidator implements Validator {\n\n        private final Pattern pattern;\n\n        public RegexValidator(String regex) {\n            regex = StringUtils.trimToEmpty(regex);\n            pattern = Pattern.compile(regex);\n        }\n\n        @Override\n        public Result apply(Value v) {\n            if (!v.isString()) {\n                return fail(\"not a string\");\n            }\n            String strValue = v.getValue();\n            Matcher matcher = pattern.matcher(strValue);\n            return matcher.matches() ? PASS : fail(\"regex match failed\");\n        }\n\n    }\n\n    static final Map<String, Validator> VALIDATORS = new HashMap<>(11);\n\n    static {\n        VALIDATORS.put(\"array\", v -> v.isList() ? PASS : fail(\"not an array or list\"));\n        VALIDATORS.put(\"boolean\", v -> v.isBoolean() ? PASS : fail(\"not a boolean\"));\n        VALIDATORS.put(\"ignore\", v -> PASS);\n        VALIDATORS.put(\"notnull\", v -> v.isNull() ? fail(\"null\") : PASS);\n        VALIDATORS.put(\"null\", v -> v.isNull() ? PASS : fail(\"not null\"));\n        VALIDATORS.put(\"number\", v -> v.isNumber() ? PASS : fail(\"not a number\"));\n        VALIDATORS.put(\"object\", v -> v.isMap() ? PASS : fail(\"not an object or map\"));\n        VALIDATORS.put(\"present\", v -> v.isNotPresent() ? fail(\"not present\") : PASS);\n        VALIDATORS.put(\"notpresent\", v -> v.isNotPresent() ? PASS : fail(\"present\"));\n        VALIDATORS.put(\"string\", v -> v.isNotPresent() ? fail(\"not present\") : v.isString() ? PASS : fail(\"not a string\"));\n        VALIDATORS.put(\"uuid\", v -> {\n            if (!v.isString()) {\n                return fail(\"not a string\");\n            }\n            try {\n                UUID.fromString(v.getValue());\n                return PASS;\n            } catch (Exception e) {\n                return fail(\"not a valid uuid\");\n            }\n        });\n    }\n\n    public static class Result {\n\n        public final String message;\n        public final boolean pass;\n\n        private Result(boolean pass, String message) {\n            this.pass = pass;\n            this.message = message;\n        }\n\n        @Override\n        public String toString() {\n            return pass ? \"[pass]\" : message;\n        }\n\n        public Map<String, Object> toMap() {\n            Map<String, Object> map = new HashMap<>(2);\n            map.put(\"pass\", pass);\n            map.put(\"message\", message);\n            return map;\n        }\n\n    }\n\n    static class Context {\n\n        final JsEngine JS;\n        final MatchOperation root;\n        final int depth;\n        final boolean xml;\n        final String path;\n        final String name;\n        final int index;\n\n        Context(JsEngine js, MatchOperation root, boolean xml, int depth, String path, String name, int index) {\n            this.JS = js;\n            this.root = root;\n            this.xml = xml;\n            this.depth = depth;\n            this.path = path;\n            this.name = name;\n            this.index = index;\n        }\n\n        Context descend(String name) {\n            if (xml) {\n                String childPath = path.endsWith(\"/@\") ? path + name : (depth == 0 ? \"\" : path) + \"/\" + name;\n                return new Context(JS, root, xml, depth + 1, childPath, name, -1);\n            } else {\n                boolean needsQuotes = name.indexOf('-') != -1 || name.indexOf(' ') != -1 || name.indexOf('.') != -1;\n                String childPath = needsQuotes ? path + \"['\" + name + \"']\" : path + '.' + name;\n                return new Context(JS, root, xml, depth + 1, childPath, name, -1);\n            }\n        }\n\n        Context descend(int index) {\n            if (xml) {\n                return new Context(JS, root, xml, depth + 1, path + \"[\" + (index + 1) + \"]\", name, index);\n            } else {\n                return new Context(JS, root, xml, depth + 1, path + \"[\" + index + \"]\", name, index);\n            }\n        }\n\n    }\n\n    static enum ValueType {\n        NULL,\n        BOOLEAN,\n        NUMBER,\n        STRING,\n        BYTES,\n        LIST,\n        MAP,\n        XML,\n        OTHER\n    }\n\n    public static class Value {\n\n        final ValueType type;\n        final boolean exceptionOnMatchFailure;\n\n        private final Object value;\n\n        Value(Object value) {\n            this(value, false);\n        }\n\n        Value(Object value, boolean exceptionOnMatchFailure) {\n            if (value instanceof Set<?> set) {\n                value = new ArrayList<Object>(set);\n            } else if (value != null && value.getClass().isArray()) {\n                int length = Array.getLength(value);\n                List<Object> list = new ArrayList<>(length);\n                for (int i = 0; i < length; i++) {\n                    list.add(Array.get(value, i));\n                }\n                value = list;\n            }\n            this.value = value;\n            this.exceptionOnMatchFailure = exceptionOnMatchFailure;\n            if (value == null) {\n                type = ValueType.NULL;\n            } else if (value instanceof Node) {\n                type = ValueType.XML;\n            } else if (value instanceof List) {\n                type = ValueType.LIST;\n            } else if (value instanceof Map) {\n                type = ValueType.MAP;\n            } else if (value instanceof String) {\n                type = ValueType.STRING;\n            } else if (Number.class.isAssignableFrom(value.getClass())) {\n                type = ValueType.NUMBER;\n            } else if (Boolean.class.equals(value.getClass())) {\n                type = ValueType.BOOLEAN;\n            } else if (value instanceof byte[]) {\n                type = ValueType.BYTES;\n            } else {\n                type = ValueType.OTHER;\n            }\n        }\n\n        public boolean isBoolean() {\n            return type == ValueType.BOOLEAN;\n        }\n\n        public boolean isNumber() {\n            return type == ValueType.NUMBER;\n        }\n\n        public boolean isString() {\n            return type == ValueType.STRING;\n        }\n\n        public boolean isNull() {\n            return type == ValueType.NULL;\n        }\n\n        public boolean isMap() {\n            return type == ValueType.MAP;\n        }\n\n        public boolean isList() {\n            return type == ValueType.LIST;\n        }\n\n        public boolean isXml() {\n            return type == ValueType.XML;\n        }\n\n        boolean isNotPresent() {\n            return \"#notpresent\".equals(value);\n        }\n\n        boolean isArrayObjectOrReference() {\n            String temp = value.toString();\n            return temp.startsWith(\"#[\")\n                || temp.startsWith(\"##[\")\n                || temp.startsWith(\"#(\")\n                || temp.startsWith(\"##(\")                  \n                || \"#array\".equals(temp)\n                || \"##array\".equals(temp)\n                || \"#object\".equals(temp)\n                || \"##object\".equals(temp);\n        }       \n\n        boolean isMapOrListOrXml() {\n            switch (type) {\n                case MAP:\n                case LIST:\n                case XML:\n                    return true;\n                default:\n                    return false;\n            }\n        }\n\n        public <T> T getValue() {\n            return (T) value;\n        }\n\n        String getWithinSingleQuotesIfString() {\n            if (type == ValueType.STRING) {\n                return \"'\" + value + \"'\";\n            } else {\n                return getAsString();\n            }\n        }\n\n        public String getAsString() {\n            switch (type) {\n                case LIST:\n                case MAP:\n                    return JsonUtils.toJsonSafe(value, false);\n                case XML:\n                    return XmlUtils.toString(getValue());\n                default:\n                    return value + \"\";\n            }\n        }\n\n        String getAsXmlString() {\n            if (type == ValueType.MAP) {\n                Node node = XmlUtils.fromMap(getValue());\n                return XmlUtils.toString(node);\n            } else {\n                return getAsString();\n            }\n        }\n\n        Value getSortedLike(Value other) {\n            if (isMap() && other.isMap()) {\n                Map<String, Object> reference = other.getValue();\n                Map<String, Object> source = getValue();\n                Set<String> remainder = new LinkedHashSet<>(source.keySet());\n                Map<String, Object> result = new LinkedHashMap<>(source.size());\n                reference.keySet().forEach(key -> {\n                    if (source.containsKey(key)) {\n                        result.put(key, source.get(key));\n                        remainder.remove(key);\n                    }\n                });\n                for (String key : remainder) {\n                    result.put(key, source.get(key));\n                }\n                return new Value(result, other.exceptionOnMatchFailure);\n            } else {\n                return this;\n            }\n        }\n\n        @Override\n        public String toString() {\n            return \"[type: \" + type +\n                    \", value: \" + value +\n                    \"]\";\n        }\n\n        public Result is(Type matchType, Object expected) {\n            MatchOperation mo = new MatchOperation(matchType.operator(false), this, new Value(parseIfJsonOrXmlString(expected), exceptionOnMatchFailure));\n            mo.execute();\n            if (mo.pass) {\n                return Match.PASS;\n            } else {\n                if (exceptionOnMatchFailure) {\n                    throw new RuntimeException(mo.getFailureReasons());\n                }\n                return Match.fail(mo.getFailureReasons());\n            }\n        }\n\n        //======================================================================\n        //\n        public Result isEqualTo(Object expected) {\n            return is(Type.EQUALS, expected);\n        }\n\n        public Result contains(Object expected) {\n            return is(Type.CONTAINS, expected);\n        }\n\n        public Result containsDeep(Object expected) {\n            return is(Type.CONTAINS_DEEP, expected);\n        }\n\n        public Result containsOnly(Object expected) {\n            return is(Type.CONTAINS_ONLY, expected);\n        }\n        \n        public Result containsOnlyDeep(Object expected) {\n            return is(Type.CONTAINS_ONLY_DEEP, expected);\n        }        \n\n        public Result containsAny(Object expected) {\n            return is(Type.CONTAINS_ANY, expected);\n        }\n\n        public Result isNotEqualTo(Object expected) {\n            return is(Type.NOT_EQUALS, expected);\n        }\n\n        public Result isNotContaining(Object expected) {\n            return is(Type.NOT_CONTAINS, expected);\n        }\n\n        public Result isEachEqualTo(Object expected) {\n            return is(Type.EACH_EQUALS, expected);\n        }\n\n        public Result isEachNotEqualTo(Object expected) {\n            return is(Type.EACH_NOT_EQUALS, expected);\n        }\n\n        public Result isEachContaining(Object expected) {\n            return is(Type.EACH_CONTAINS, expected);\n        }\n\n        public Result isEachNotContaining(Object expected) {\n            return is(Type.EACH_NOT_CONTAINS, expected);\n        }\n\n        public Result isEachContainingDeep(Object expected) {\n            return is(Type.EACH_CONTAINS_DEEP, expected);\n        }\n\n        public Result isEachContainingOnly(Object expected) {\n            return is(Type.EACH_CONTAINS_ONLY, expected);\n        }\n\n        public Result isEachContainingAny(Object expected) {\n            return is(Type.EACH_CONTAINS_ANY, expected);\n        }\n\n    }\n\n    public static Result execute(JsEngine js, Type matchType, Object actual, Object expected, boolean matchEachEmptyAllowed) {\n        MatchOperation mo = new MatchOperation(js, matchType.operator(matchEachEmptyAllowed), new Value(actual), new Value(expected));\n        mo.execute();\n        if (mo.pass) {\n            return PASS;\n        } else {\n            return fail(mo.getFailureReasons());\n        }\n    }\n\n    public static Object parseIfJsonOrXmlString(Object o) {\n        if (o instanceof String s) {\n            if (s.isEmpty()) {\n                return o;\n            } else if (JsonUtils.isJson(s)) {\n                return Json.of(s).value();\n            } else if (XmlUtils.isXml(s)) {\n                return XmlUtils.toXmlDoc(s);\n            } else {\n                if (s.charAt(0) == '\\\\') {\n                    return s.substring(1);\n                }\n            }\n        }\n        return o;\n    }\n\n    public static Value evaluate(Object actual) {\n        return new Value(parseIfJsonOrXmlString(actual), false);\n    }\n\n    public static Value that(Object actual) {\n        return new Value(parseIfJsonOrXmlString(actual), true);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/MatchOperation.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.intuit.karate.graal.JsEngine;\nimport com.intuit.karate.graal.JsValue;\nimport java.math.BigDecimal;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n *\n * @author pthomas3\n */\npublic class MatchOperation {\n\n    public static final String REGEX = \"regex\";        \n\n    final Match.Context context;\n    final MatchOperator type;\n    final Match.Value actual;\n    final Match.Value expected;\n    final List<MatchOperation> failures;\n\n    boolean pass = true;\n    String failReason;\n\n    MatchOperation(MatchOperator type, Match.Value actual, Match.Value expected) {\n        this(JsEngine.global(), null, type, actual, expected);\n    }\n\n    MatchOperation(JsEngine js, MatchOperator type, Match.Value actual, Match.Value expected) {\n        this(js, null, type, actual, expected);\n    }\n\n    MatchOperation(Match.Context context, MatchOperator type, Match.Value actual, Match.Value expected) {\n        this(null, context, type, actual, expected);\n    }\n\n    private MatchOperation(JsEngine js, Match.Context context, MatchOperator type, Match.Value actual, Match.Value expected) {\n        this.type = type;\n        this.actual = actual;\n        this.expected = expected;\n        if (context == null) {\n            if (js == null) {\n                js = JsEngine.global();\n            }\n            this.failures = new ArrayList();\n            if (actual.isXml()) {\n                this.context = new Match.Context(js, this, true, 0, \"/\", \"\", -1);\n            } else {\n                this.context = new Match.Context(js, this, false, 0, \"$\", \"\", -1);\n            }\n        } else {\n            this.context = context;\n            this.failures = context.root.failures;\n        }        \n    }\n\n    boolean execute() {\n        return type.execute(this);\n    }\n\n    boolean pass() {\n        pass = true;\n        return true;\n    }\n\n    boolean fail(String reason) {\n        pass = false;\n        if (reason == null) {\n            return false;\n        }\n        failReason = failReason == null ? reason : reason + \" | \" + failReason;\n        context.root.failures.add(this);\n        return false;\n    }\n\n    String getFailureReasons() {\n        return collectFailureReasons(this);\n    }\n\n    private boolean isXmlAttributeOrMap() {\n        return context.xml && actual.isMap()\n                && (context.name.equals(\"@\") || actual.<Map>getValue().containsKey(\"_\"));\n    }\n\n    private static String collectFailureReasons(MatchOperation root) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"match failed: \").append(root.type).append('\\n');\n        Collections.reverse(root.failures);\n        Iterator<MatchOperation> iterator = root.failures.iterator();\n        Set previousPaths = new HashSet();\n        int index = 0;\n        int prevDepth = -1;\n        while (iterator.hasNext()) {\n            MatchOperation mo = iterator.next();\n            if (previousPaths.contains(mo.context.path) || mo.isXmlAttributeOrMap()) {\n                continue;\n            }\n            previousPaths.add(mo.context.path);\n            if (mo.context.depth != prevDepth) {\n                prevDepth = mo.context.depth;\n                index++;\n            }\n            String prefix = StringUtils.repeat(' ', index * 2);\n            sb.append(prefix).append(mo.context.path).append(\" | \").append(mo.failReason);\n            sb.append(\" (\").append(mo.actual.type).append(':').append(mo.expected.type).append(\")\");\n            sb.append('\\n');\n            if (mo.context.xml) {\n                sb.append(prefix).append(mo.actual.getAsXmlString()).append('\\n');\n                sb.append(prefix).append(mo.expected.getAsXmlString()).append('\\n');\n            } else {\n                Match.Value expected = mo.expected.getSortedLike(mo.actual);\n                sb.append(prefix).append(mo.actual.getWithinSingleQuotesIfString()).append('\\n');\n                sb.append(prefix).append(expected.getWithinSingleQuotesIfString()).append('\\n');\n            }\n            if (iterator.hasNext()) {\n                sb.append('\\n');\n            }\n        }\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/MatchOperator.java",
    "content": "package com.intuit.karate;\n\nimport com.intuit.karate.graal.JsValue;\n\nimport java.math.BigDecimal;\nimport java.util.*;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport static com.intuit.karate.MatchOperation.REGEX;\n\npublic interface MatchOperator {\n\n    public boolean execute(MatchOperation operation);\n\n    class EachOperator implements MatchOperator {\n        private final MatchOperator delegate;\n        private final boolean matchEachEmptyAllowed;\n\n        EachOperator(MatchOperator delegate, boolean matchEachEmptyAllowed) {\n            this.delegate = delegate;\n            this.matchEachEmptyAllowed = matchEachEmptyAllowed;\n        }\n\n        public String toString() {\n            return \"EACH_\"+delegate;\n        }\n\n        public boolean execute(MatchOperation operation) {\n            Match.Value actual = operation.actual;\n            Match.Context context = operation.context;\n            if (actual.isList()) {\n                List<?> list = actual.getValue();\n                if (list.isEmpty() && !matchEachEmptyAllowed) {\n                    return operation.fail(\"match each failed, empty array / list\");\n                }\n                int count = list.size();\n                for (int i = 0; i < count; i++) {\n                    Object o = list.get(i);\n                    context.JS.put(\"_$\", o);\n                    MatchOperation mo = new MatchOperation(context.descend(i), delegate, new Match.Value(o), operation.expected);\n                    mo.execute();\n                    context.JS.bindings.removeMember(\"_$\");\n                    if (!mo.pass) {\n                        return operation.fail(\"match each failed at index \" + i);\n                    }\n                }\n                // if we reached here all / each LHS items completed successfully\n                return true;\n            } else {\n                return operation.fail(\"actual is not an array or list\");\n            }\n        }\n\n    }\n\n    class NotOperator implements MatchOperator {\n        private final CoreOperator delegate;\n        private final String failureMessage;\n\n        NotOperator(CoreOperator delegate, String failureMessage) {\n            this.delegate = delegate;\n            this.failureMessage = failureMessage;\n        }\n\n        public String toString() {\n            return \"NOT_\"+delegate;\n        }\n\n        public boolean execute(MatchOperation operation) {\n            Match.Value expected = operation.expected;\n            // Pre 2515 would only apply this hack to CONTAINS and not CONTAINS_DEEP\n            if (delegate.isContains() && expected.isMap() && expected.<Map<?, ?>>getValue().isEmpty()) {\n                return true; // hack alert: support for match some_map not contains {}\n            }\n            MatchOperation mo = new MatchOperation(operation.context, delegate, operation.actual, expected);\n            mo.execute();\n            if (!mo.pass) {\n                return operation.pass();\n            }\n            return operation.fail(failureMessage);\n        }\n    }\n\n    class CoreOperator implements MatchOperator {\n\n        private final boolean isEquals;\n        private final boolean isContains;\n        private final boolean isContainsAny;\n        private final boolean isContainsOnly;\n        private final boolean isDeep;\n        private final boolean matchEachEmptyAllowed;\n        // NOt strictly required. We could create a new instance in childOperator but keeping it as an instance field\n        // is a minor optimization.\n        private final CoreOperator equalsOperator;\n\n        private CoreOperator(boolean isEquals, boolean isContains, boolean isContainsAny, boolean isContainsOnly, boolean matchEachEmptyAllowed) {\n            this(isEquals, isContains, isContainsAny, isContainsOnly, false, matchEachEmptyAllowed);\n        }\n\n        private CoreOperator(boolean isEquals, boolean isContains, boolean isContainsAny, boolean isContainsOnly, boolean isDeep, boolean matchEachEmptyAllowed) {\n            this.isEquals = isEquals;\n            this.isContains = isContains;\n            this.isContainsAny = isContainsAny;\n            this.isContainsOnly = isContainsOnly;\n            this.isDeep = isDeep;\n            this.matchEachEmptyAllowed = matchEachEmptyAllowed;\n            this.equalsOperator = isEquals?this:equalsOperator(matchEachEmptyAllowed);\n        }\n\n        public boolean execute(MatchOperation operation) {\n            Match.Value actual = operation.actual;\n            Match.Value expected = operation.expected;\n            Match.Context context = operation.context;\n            if (actual.isNotPresent()) {\n                if (!expected.isString() || !expected.getAsString().startsWith(\"#\")) {\n                    return operation.fail(\"actual path does not exist\");\n                }\n            }\n            boolean isContainsFamily = isContainsFamily();\n            if (actual.type != expected.type) {\n                if (isContainsFamily &&\n                        // don't tamper with strings on the RHS that represent arrays or objects\n                        (!expected.isList() && !(expected.isString() && expected.isArrayObjectOrReference()))) {\n                    MatchOperation mo = new MatchOperation(context, this, actual, new Match.Value(Collections.singletonList(expected.getValue())));\n                    mo.execute();\n                    return mo.pass ? operation.pass() : operation.fail(mo.failReason);\n                }\n                if (expected.isXml() && actual.isMap()) {\n                    // special case, auto-convert rhs\n                    MatchOperation mo = new MatchOperation(context, this, actual, new Match.Value(XmlUtils.toObject(expected.getValue(), true)));\n                    mo.execute();\n                    return mo.pass ? operation.pass() : operation.fail(mo.failReason);\n                }\n                if (expected.isString()) {\n                    String expStr = expected.getValue();\n                    if (!expStr.startsWith(\"#\")) { // edge case if rhs is macro\n                        return operation.fail(\"data types don't match\");\n                    }\n                } else {\n                    return operation.fail(\"data types don't match\");\n                }\n            }\n            if (expected.isString()) {\n                String expStr = expected.getValue();\n                if (expStr.startsWith(\"#\")) {\n                    return macroEqualsExpected(operation, expStr) ? operation.pass() : operation.fail(null);\n                }\n            }\n            if (isEquals()) {\n                return actualEqualsExpected(operation) ? operation.pass() : operation.fail(\"not equal\");\n            } else if (isContainsFamily) {\n                return actualContainsExpected(operation) ? operation.pass() : operation.fail(\"actual does not contain expected\");\n            }\n            throw new RuntimeException(\"unexpected match operator: \" + this);\n        }\n\n\n        private boolean macroEqualsExpected(MatchOperation operation, String expStr) {\n            Match.Value actual = operation.actual;\n            Match.Value expected = operation.expected;\n            Match.Context context = operation.context;\n\n            boolean optional = expStr.startsWith(\"##\");\n            if (optional && actual.isNull()) { // exit early\n                return true;\n            }\n            int minLength = optional ? 3 : 2;\n            if (expStr.length() > minLength) {\n                String macro = expStr.substring(minLength - 1);\n                if (macro.startsWith(\"(\") && macro.endsWith(\")\")) {\n                    macro = macro.substring(1, macro.length() - 1);\n                    Match.Type nestedType = macroToMatchType(false, macro);\n                    int startPos = matchTypeToStartPos(nestedType);\n                    macro = macro.substring(startPos);\n                    MatchOperator nestedOperator = nestedType.operator(isMatchEachEmptyAllowed());\n                    if (isContainsFamily() && actual.isList()) { // special case, look for partial maps within list\n                        nestedOperator = macroOperator(nestedOperator);\n                    }\n                    context.JS.put(\"$\", context.root.actual.getValue());\n                    context.JS.put(\"_\", actual.getValue());\n                    JsValue jv = context.JS.eval(macro);\n                    context.JS.bindings.removeMember(\"$\");\n                    context.JS.bindings.removeMember(\"_\");\n                    MatchOperation mo = new MatchOperation(context, nestedOperator, actual, new Match.Value(jv.getValue()));\n                    return mo.execute();\n                } else if (macro.startsWith(\"[\")) {\n                    int closeBracketPos = macro.indexOf(']');\n                    if (closeBracketPos != -1) { // array, match each\n                        if (!actual.isList()) {\n                            return operation.fail(\"actual is not an array\");\n                        }\n                        if (closeBracketPos > 1) {\n                            String bracketContents = macro.substring(1, closeBracketPos);\n                            List<?> listAct = actual.getValue();\n                            int listSize = listAct.size();\n                            context.JS.put(\"$\", context.root.actual.getValue());\n                            context.JS.put(\"_\", listSize);\n                            String sizeExpr;\n                            if (containsPlaceholderUnderscore(bracketContents)) { // #[_ < 5]\n                                sizeExpr = bracketContents;\n                            } else { // #[5] | #[$.foo]\n                                sizeExpr = bracketContents + \" == _\";\n                            }\n                            JsValue jv = context.JS.eval(sizeExpr);\n                            context.JS.bindings.removeMember(\"$\");\n                            context.JS.bindings.removeMember(\"_\");\n                            if (!jv.isTrue()) {\n                                return operation.fail(\"actual array length is \" + listSize);\n                            }\n                        }\n                        if (macro.length() > closeBracketPos + 1) {\n                            macro = StringUtils.trimToNull(macro.substring(closeBracketPos + 1));\n                            if (macro != null) {\n                                if (macro.startsWith(\"(\") && macro.endsWith(\")\")) {\n                                    macro = macro.substring(1, macro.length() - 1); // strip parens\n                                }\n                                if (macro.startsWith(\"?\")) { // #[]? _.length == 3\n                                    macro = \"#\" + macro;\n                                }\n                                if (macro.startsWith(\"#\")) {\n                                    MatchOperation mo = new MatchOperation(context, Match.Type.EACH_EQUALS.operator(isMatchEachEmptyAllowed()), actual, new Match.Value(macro));\n                                    mo.execute();\n                                    return mo.pass ? operation.pass() : operation.fail(\"all array elements matched\");\n                                } else { // schema reference\n                                    Match.Type nestedType = macroToMatchType(true, macro); // match each\n                                    int startPos = matchTypeToStartPos(nestedType);\n                                    macro = macro.substring(startPos);\n                                    JsValue jv = context.JS.eval(macro);\n                                    MatchOperation mo = new MatchOperation(context, nestedType.operator(isMatchEachEmptyAllowed()), actual, new Match.Value(jv.getValue()));\n                                    return mo.execute();\n                                }\n                            }\n                        }\n                        return true; // expression within square brackets is ok\n                    }\n                } else { // '#? _ != 0' | '#string' | '#number? _ > 0'\n                    int questionPos = macro.indexOf('?');\n                    String validatorName = null;\n                    // in case of regex we don't want to remove the '?'\n                    if (questionPos != -1 && !macro.startsWith(REGEX)) {\n                        validatorName = macro.substring(0, questionPos);\n                        if (macro.length() > questionPos + 1) {\n                            macro = StringUtils.trimToEmpty(macro.substring(questionPos + 1));\n                        } else {\n                            macro = \"\";\n                        }\n                    } else {\n                        validatorName = macro;\n                        macro = \"\";\n                    }\n                    validatorName = StringUtils.trimToNull(validatorName);\n                    if (validatorName != null) {\n                        Match.Validator validator = null;\n                        if (validatorName.startsWith(REGEX)) {\n                            String regex = validatorName.substring(5).trim();\n                            validator = new Match.RegexValidator(regex);\n                        } else {\n                            validator = Match.VALIDATORS.get(validatorName);\n                        }\n                        if (validator != null) {\n                            if (optional && (actual.isNotPresent() || actual.isNull())) {\n                                // pass\n                            } else if (!optional && actual.isNotPresent()) {\n                                // if the element is not present the expected result can only be\n                                // the notpresent keyword, ignored or an optional comparison\n                                return expected.isNotPresent() || \"#ignore\".contentEquals(expected.getAsString());\n                            } else {\n                                Match.Result mr = validator.apply(actual);\n                                if (!mr.pass) {\n                                    return operation.fail(mr.message);\n                                }\n                            }\n                        } else if (!validatorName.startsWith(REGEX)) { // expected is a string that happens to start with \"#\"\n                            String actualValue = actual.getValue();\n                            // Pre 2515 would only apply this hack to CONTAINS and not CONTAINS_DEEP\n                            return isContains()?actualValue.contains(expStr):actualValue.equals(expStr);\n                        }\n\n                    }\n                    macro = StringUtils.trimToNull(macro);\n                    if (macro != null && questionPos != -1) {\n                        context.JS.put(\"$\", context.root.actual.getValue());\n                        context.JS.put(\"_\", actual.getValue());\n                        JsValue jv = context.JS.eval(macro);\n                        context.JS.bindings.removeMember(\"$\");\n                        context.JS.bindings.removeMember(\"_\");\n                        if (!jv.isTrue()) {\n                            return operation.fail(\"evaluated to 'false'\");\n                        }\n                    }\n                }\n            }\n            return true; // all ok\n        }\n\n        private static final Pattern UNDERSCORE_PATTERN = Pattern.compile(\"\\\\W_\\\\W|\\\\W_|_\\\\W\");\n\n        private boolean containsPlaceholderUnderscore(String bracketContents) {\n            Matcher m1 = UNDERSCORE_PATTERN.matcher(bracketContents);\n            while (m1.find()) {\n                return true;\n            }\n            return false;\n        }\n\n\n        private static Match.Type macroToMatchType(boolean each, String macro) {\n            if (macro.startsWith(\"^^\")) {\n                return each ? Match.Type.EACH_CONTAINS_ONLY : Match.Type.CONTAINS_ONLY;\n            } else if (macro.startsWith(\"^+\")) {\n                return each ? Match.Type.EACH_CONTAINS_DEEP : Match.Type.CONTAINS_DEEP;\n            } else if (macro.startsWith(\"^*\")) {\n                return each ? Match.Type.EACH_CONTAINS_ANY : Match.Type.CONTAINS_ANY;\n            } else if (macro.startsWith(\"^\")) {\n                return each ? Match.Type.EACH_CONTAINS : Match.Type.CONTAINS;\n            } else if (macro.startsWith(\"!^\")) {\n                return each ? Match.Type.EACH_NOT_CONTAINS : Match.Type.NOT_CONTAINS;\n            } else if (macro.startsWith(\"!=\")) {\n                return each ? Match.Type.EACH_NOT_EQUALS : Match.Type.NOT_EQUALS;\n            } else {\n                return each ? Match.Type.EACH_EQUALS : Match.Type.EQUALS;\n            }\n        }\n\n        private static int matchTypeToStartPos(Match.Type mt) {\n           return mt.shortcutLength;\n        }\n\n        private static BigDecimal toBigDecimal(Object o) {\n            if (o instanceof BigDecimal) {\n                return (BigDecimal) o;\n            } else if (o instanceof Number n) {\n                return BigDecimal.valueOf(n.doubleValue());\n            } else {\n                throw new RuntimeException(\"expected number instead of: \" + o);\n            }\n        }\n\n        private boolean actualEqualsExpected(MatchOperation operation) {\n            Match.Value actual = operation.actual;\n            Match.Value expected = operation.expected;\n            Match.Context context = operation.context;\n            switch (actual.type) {\n                case NULL:\n                    return true; // both are null\n                case BOOLEAN:\n                    boolean actBoolean = actual.getValue();\n                    boolean expBoolean = expected.getValue();\n                    return actBoolean == expBoolean;\n                case NUMBER:\n                    if (actual.getValue() instanceof BigDecimal || expected.getValue() instanceof BigDecimal) {\n                        BigDecimal actBigDecimal = toBigDecimal(actual.getValue());\n                        BigDecimal expBigDecimal = toBigDecimal(expected.getValue());\n                        return actBigDecimal.compareTo(expBigDecimal) == 0;\n                    } else {\n                        Number actNumber = actual.getValue();\n                        Number expNumber = expected.getValue();\n                        return actNumber.doubleValue() == expNumber.doubleValue();\n                    }\n                case STRING:\n                    return actual.getValue().equals(expected.getValue());\n                case BYTES:\n                    byte[] actBytes = actual.getValue();\n                    byte[] expBytes = expected.getValue();\n                    return Arrays.equals(actBytes, expBytes);\n                case LIST:\n                    List<?> actList = actual.getValue();\n                    List<?> expList = expected.getValue();\n                    int actListCount = actList.size();\n                    int expListCount = expList.size();\n                    if (actListCount != expListCount) {\n                        return operation.fail(\"actual array length is not equal to expected - \" + actListCount + \":\" + expListCount);\n                    }\n                    for (int i = 0; i < actListCount; i++) {\n                        Match.Value actListValue = new Match.Value(actList.get(i));\n                        Match.Value expListValue = new Match.Value(expList.get(i));\n                        MatchOperation mo = new MatchOperation(context.descend(i), equalsOperator, actListValue, expListValue);\n                        mo.execute();\n                        if (!mo.pass) {\n                            return operation.fail(\"array match failed at index \" + i);\n                        }\n                    }\n                    return true;\n                case MAP:\n                    Map<String, Object> actMap = actual.getValue();\n                    Map<String, Object> expMap = expected.getValue();\n                    return matchMapValues(actMap, expMap, operation);\n                case XML:\n                    Map<String, Object> actXml = (Map) XmlUtils.toObject(actual.getValue(), true);\n                    Map<String, Object> expXml = (Map) XmlUtils.toObject(expected.getValue(), true);\n                    return matchMapValues(actXml, expXml, operation);\n                case OTHER:\n                    return actual.getValue().equals(expected.getValue());\n                default:\n                    throw new RuntimeException(\"unexpected type (match equals): \" + actual.type);\n            }\n        }\n\n        private boolean matchMapValues(Map<String, Object> actMap, Map<String, Object> expMap, MatchOperation operation) { // combined logic for equals and contains\n            if (actMap.size() > expMap.size() && (isEquals() || isContainsOnly())) {\n                int sizeDiff = actMap.size() - expMap.size();\n                Map<String, Object> diffMap = new LinkedHashMap<>(actMap);\n                for (String key : expMap.keySet()) {\n                    diffMap.remove(key);\n                }\n                return operation.fail(\"actual has \" + sizeDiff + \" more key(s) than expected - \" + JsonUtils.toJson(diffMap));\n            }\n            Set<String> unMatchedKeysAct = new LinkedHashSet<>(actMap.keySet());\n            Set<String> unMatchedKeysExp = new LinkedHashSet<>(expMap.keySet());\n            for (Map.Entry<String, Object> expEntry : expMap.entrySet()) {\n                String key = expEntry.getKey();\n                Object childExp = expEntry.getValue();\n                if (!actMap.containsKey(key)) {\n                    if (childExp instanceof String childString) {\n                        if (childString.startsWith(\"##\") || childString.equals(\"#ignore\") || childString.equals(\"#notpresent\")) {\n                            if (isContainsAny()) {\n                                return true; // exit early\n                            }\n                            unMatchedKeysExp.remove(key);\n                            if (unMatchedKeysExp.isEmpty()) {\n                                if (isContains()) {\n                                    return true; // all expected keys matched\n                                }\n                            }\n                            continue;\n                        }\n                    }\n                    if (!isContainsAny()) {\n                        return operation.fail(\"actual does not contain key - '\" + key + \"'\");\n                    }\n                }\n                Match.Value childActValue = new Match.Value(actMap.get(key));\n                MatchOperator childMatchType = childOperator(childActValue);\n                MatchOperation mo = new MatchOperation(operation.context.descend(key), childMatchType, childActValue, new Match.Value(childExp));\n                mo.execute();\n                if (mo.pass) {\n                    if (isContainsAny()) {\n                        return true; // exit early\n                    }\n                    unMatchedKeysExp.remove(key);\n                    if (unMatchedKeysExp.isEmpty()) {\n                        if (isContains()) {\n                            return true; // all expected keys matched\n                        }\n                    }\n                    unMatchedKeysAct.remove(key);\n                } else if (isEquals()) {\n                    return operation.fail(\"match failed for name: '\" + key + \"'\");\n                }\n            }\n            if (isContainsAny()) {\n                return unMatchedKeysExp.isEmpty() ? true : operation.fail(\"no key-values matched\");\n            }\n            if (unMatchedKeysExp.isEmpty()) {\n                if (isContains()) {\n                    return true; // all expected keys matched, expMap was empty in the first place\n                }\n                // Special hack in pre 2515 to support match some_map not contains {} is now handled in execute() directly\n            }\n            if (!unMatchedKeysExp.isEmpty()) {\n                return operation.fail(\"all key-values did not match, expected has un-matched keys - \" + unMatchedKeysExp);\n            }\n            if (!unMatchedKeysAct.isEmpty()) {\n                return operation.fail(\"all key-values did not match, actual has un-matched keys - \" + unMatchedKeysAct);\n            }\n            return true;\n        }\n\n        private boolean actualContainsExpected(MatchOperation operation) {\n            Match.Value actual = operation.actual;\n            Match.Value expected = operation.expected;\n            Match.Context context = operation.context;\n            switch (actual.type) {\n                case STRING:\n                    String actString = actual.getValue();\n                    String expString = expected.getValue();\n                    return actString.contains(expString);\n                case LIST:\n                    List actList = actual.getValue();\n                    List expList = expected.getValue();\n                    int actListCount = actList.size();\n                    int expListCount = expList.size();\n                    // visited array used to handle duplicates\n                    boolean[] actVisitedList = new boolean[actListCount];\n                    if (!isContainsAny() && expListCount > actListCount) {\n                        return operation.fail(\"actual array length is less than expected - \" + actListCount + \":\" + expListCount);\n                    }\n                    if (isContainsOnly() && expListCount != actListCount) {\n                        return operation.fail(\"actual array length is not equal to expected - \" + actListCount + \":\" + expListCount);\n                    }\n                    for (Object exp : expList) { // for each item in the expected list\n                        boolean found = false;\n                        Match.Value expListValue = new Match.Value(exp);\n                        for (int i = 0; i < actListCount; i++) {\n                            Match.Value actListValue = new Match.Value(actList.get(i));\n                            MatchOperator childMatchType = childOperator(actListValue);\n                            MatchOperation mo = new MatchOperation(context.descend(i), childMatchType, actListValue, expListValue);\n                            mo.execute();\n                            if (mo.pass) {\n                                if (isContainsAny()) {\n                                    return true; // exit early\n                                }\n                                // contains only : If element is found also check its occurrence in actVisitedList\n                                // Pre 2515 would only apply this hack to CONTAINS_ONLY and not CONTAINS_ONLY_DEEP\n                                else if(isContainsOnly()) {\n                                    // if not yet visited\n                                    if(!actVisitedList[i]) {\n                                        // mark it visited\n                                        actVisitedList[i]  = true;\n                                        found = true;\n                                        break; // next item in expected list\n                                    }\n                                    // else do nothing does not consider it a match\n                                }\n                                else {\n                                    found = true;\n                                    break; // next item in expected list\n                                }\n                            }\n                        }\n                        if (!found && !isContainsAny()) { // if we reached here, all items in the actual list were scanned\n                            return operation.fail(\"actual array does not contain expected item - \" + expListValue.getAsString());\n                        }\n                    }\n                    if (isContainsAny()) {\n                        return operation.fail(\"actual array does not contain any of the expected items\");\n                    }\n                    return true; // if we reached here, all items in the expected list were found\n                case MAP:\n                    Map<String, Object> actMap = actual.getValue();\n                    Map<String, Object> expMap = expected.getValue();\n                    return matchMapValues(actMap, expMap, operation);\n                case XML:\n                    Map<String, Object> actXml = (Map) XmlUtils.toObject(actual.getValue());\n                    Map<String, Object> expXml = (Map) XmlUtils.toObject(expected.getValue());\n                    return matchMapValues(actXml, expXml, operation);\n                default:\n                    throw new RuntimeException(\"unexpected type (match contains): \" + actual.type);\n            }\n        }\n\n        CoreOperator deep() {\n            return new CoreOperator(isEquals, isContains, isContainsAny, isContainsOnly, true, matchEachEmptyAllowed);\n        }\n\n        static CoreOperator equalsOperator(boolean matchEachEmptyAllowed) {\n            return new CoreOperator(true, false, false, false, matchEachEmptyAllowed);\n        }\n\n        static CoreOperator containsOperator(boolean matchEachEmptyAllowed) {\n            return new CoreOperator(false, true, false, false, matchEachEmptyAllowed);\n        }\n\n        static CoreOperator containsAnyOperator(boolean matchEachEmptyAllowed) {\n            return new CoreOperator(false, false, true, false, matchEachEmptyAllowed);\n        }\n\n        static CoreOperator containsOnlyOperator(boolean matchEachEmptyAllowed) {\n            return new CoreOperator(false, false, false, true, matchEachEmptyAllowed);\n        }\n\n        boolean isEquals() {\n            return isEquals;\n        }\n\n        boolean isContains() {\n            return isContains;\n        }\n\n        boolean isContainsAny() {\n            return isContainsAny;\n        }\n\n        boolean isContainsOnly() {\n            return isContainsOnly;\n        }\n\n        boolean isContainsFamily() {\n            return isContains() || isContainsOnly() || isContainsAny();\n        }\n\n        boolean isMatchEachEmptyAllowed() {\n            return matchEachEmptyAllowed;\n        }\n\n        MatchOperator childOperator(Match.Value value) {\n            return isDeep && value.isMapOrListOrXml()?this:equalsOperator;\n        }\n\n        /**\n         * The operator specified by the user (^, ^+, ...) is provided as the {@code specifiedOperator} parameter.\n         * However, when using one of Contains Family operators, it may require some adjustments.\n         *\n         * <p>Example:\n         * <pre>{@code\n         * def actual = [{ a: 1, b: 'x' }, { a: 2, b: 'y' }]\n         * def part = { a: 1 }\n         * match actual contains '#(^part)'\n         * }</pre>\n         *\n         * In this example, {@code specifiedOperator} is {@code Contains}. However:\n         * <ul>\n         *   <li>The specified operator ({@code ^}) is applied when processing the list.</li>\n         *   <li>Child operators are applied when processing objects within the list.</li>\n         * </ul>\n         *\n         * According to {@link #childOperator(Match.Value)}, {@code Contains}' child operator is {@code Equals}.\n         * As a result, the code attempts to match {@code { a: 1, b: 'x' } equals { a: 1 }}, which fails.\n         *\n         * <p>What we actually want is to preserve both {@code Contains} operators:\n         * <ul>\n         *   <li>The one from the match instruction.</li>\n         *   <li>The one implied by the macro logic.</li>\n         * </ul>\n         * This method achieves that by creating a custom operator that effectively applies two {@code Contains} operations.\n         *\n         * <p>Note: If a third level of matching is required (e.g., the objects in {@code actual} contain other objects),\n         * it would fall back to the child operator of the child operator, which is {@code Equals}.\n         * This differs from the legacy implementation, which would enforce a deep {@code Contains},\n         * potentially triggering issue #2515.\n         *\n         * <p>That said, {@code Contains Deep} may still be specified explicitly by the user,\n         * for example, to handle nested structures like objects within objects within lists.\n         */\n        protected MatchOperator macroOperator(MatchOperator specifiedOperator) {\n            if (isContainsFamily()) {\n                return isDeep ? this : new CoreOperator(false, isContains(), isContainsAny(), isContainsOnly(), isMatchEachEmptyAllowed()) {\n                    protected MatchOperator childOperator(Match.Value actual) {\n                        return specifiedOperator;\n                    }\n                };\n            }\n            return specifiedOperator;\n        }\n\n        public String toString() {\n            String operatorString = isEquals?\"EQUALS\":isContains?\"CONTAINS\":isContainsAny?\"CONTAINS_ANY\":\"CONTAINS_ONLY\";\n            return isDeep?operatorString+\"_DEEP\":operatorString;\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/MatchStep.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\n/**\n *\n * @author pthomas3\n */\npublic class MatchStep {\n\n    public final String name;\n    public final String path;\n    public final Match.Type type;\n    public final String expected;\n\n    public MatchStep(String raw) {\n        boolean each = false;\n        raw = raw.trim();\n        if (raw.startsWith(\"each\")) {\n            each = true;\n            raw = raw.substring(4).trim();\n        }\n        boolean contains = false;\n        boolean not = false;\n        boolean only = false;\n        boolean any = false;\n        boolean deep = false;\n        int spacePos = raw.indexOf(' ');\n        int leftParenPos = raw.indexOf('(');\n        int rightParenPos = raw.indexOf(')');\n        int lhsEndPos = raw.indexOf(\" contains\");\n        if (lhsEndPos == -1) {\n            lhsEndPos = raw.indexOf(\" !contains\");\n        }\n        int searchPos = 0;\n        int eqPos = raw.indexOf(\" == \");\n        if (eqPos == -1) {\n            eqPos = raw.indexOf(\" != \");\n        }\n        if (lhsEndPos != -1 && (eqPos == -1 || eqPos > lhsEndPos)) {\n            contains = true;\n            not = raw.charAt(lhsEndPos + 1) == '!';\n            searchPos = lhsEndPos + (not ? 10 : 9);\n            String anyOrOnlyOrDeep = raw.substring(searchPos).trim();\n            if (anyOrOnlyOrDeep.startsWith(\"only deep\")) {\n                int onlyPos = raw.indexOf(\" only deep\", searchPos);\n                only = true;\n                deep = true;\n                searchPos = onlyPos + 10;\n            } else if (anyOrOnlyOrDeep.startsWith(\"only\")) {\n                int onlyPos = raw.indexOf(\" only\", searchPos);\n                only = true;\n                searchPos = onlyPos + 5;\n            } else if (anyOrOnlyOrDeep.startsWith(\"any\")) {\n                int anyPos = raw.indexOf(\" any\", searchPos);\n                any = true;\n                searchPos = anyPos + 4;\n            } else if (anyOrOnlyOrDeep.startsWith(\"deep\")) {\n                int deepPos = raw.indexOf(\" deep\", searchPos);\n                deep = true;\n                searchPos = deepPos + 5;\n            }\n        } else {\n            int equalPos = raw.indexOf(\" ==\", searchPos);\n            int notEqualPos = raw.indexOf(\" !=\", searchPos);\n            if (equalPos == -1 && notEqualPos == -1) {\n                throw new RuntimeException(\"syntax error, expected '==' for match\");\n            }\n            lhsEndPos = min(equalPos, notEqualPos);\n            if (lhsEndPos > spacePos && rightParenPos != -1\n                    && rightParenPos > lhsEndPos && rightParenPos < leftParenPos) {\n                equalPos = raw.indexOf(\" ==\", rightParenPos);\n                notEqualPos = raw.indexOf(\" !=\", rightParenPos);\n                if (equalPos == -1 && notEqualPos == -1) {\n                    throw new RuntimeException(\"syntax error, expected '==' for match\");\n                }\n                lhsEndPos = min(equalPos, notEqualPos);\n            }\n            not = lhsEndPos == notEqualPos;\n            searchPos = lhsEndPos + 3;\n        }\n        String lhs = raw.substring(0, lhsEndPos).trim();\n        if (leftParenPos == -1) {\n            leftParenPos = lhs.indexOf('['); // used later to test for json-path\n            char first = lhs.charAt(0);\n            if (first == '[' || first == '{') { // json array or object\n                spacePos = -1; // just use lhs\n                lhs = \"(\" + lhs + \")\";\n            }\n        }\n        if (spacePos != -1 && (leftParenPos > spacePos || leftParenPos == -1)) {\n            name = lhs.substring(0, spacePos);\n            path = StringUtils.trimToNull(lhs.substring(spacePos));\n        } else {\n            name = lhs;\n            path = null;\n        }\n        expected = StringUtils.trimToNull(raw.substring(searchPos));\n        type = getType(each, not, contains, only, any, deep);\n    }\n\n    private static int min(int a, int b) {\n        if (a == -1) {\n            return b;\n        }\n        if (b == -1) {\n            return a;\n        }\n        return Math.min(a, b);\n    }\n\n    private static Match.Type getType(boolean each, boolean not, boolean contains, boolean only, boolean any, boolean deep) {\n        if (each) {\n            if (contains) {\n                if (only) {\n                    return Match.Type.EACH_CONTAINS_ONLY;\n                }\n                if (any) {\n                    return Match.Type.EACH_CONTAINS_ANY;\n                }\n                if (deep) {\n                    if (not) {\n                        throw new RuntimeException(\"'each !contains deep' is not yet supported, use 'each contains deep' instead\");\n                    }\n                    return Match.Type.EACH_CONTAINS_DEEP;\n                }\n                return not ? Match.Type.EACH_NOT_CONTAINS : Match.Type.EACH_CONTAINS;\n            }\n            return not ? Match.Type.EACH_NOT_EQUALS : Match.Type.EACH_EQUALS;\n        }\n        if (contains) {\n            if (only) {\n                return deep ? Match.Type.CONTAINS_ONLY_DEEP : Match.Type.CONTAINS_ONLY;\n            }\n            if (any) {\n                return Match.Type.CONTAINS_ANY;\n            }\n            if (deep) {\n                if (not) {\n                    throw new RuntimeException(\"'!contains deep' is not yet supported, use 'contains deep' instead\");\n                }\n                return Match.Type.CONTAINS_DEEP;\n            }\n            return not ? Match.Type.NOT_CONTAINS : Match.Type.CONTAINS;\n        }\n        return not ? Match.Type.NOT_EQUALS : Match.Type.EQUALS;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/PerfContext.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\n/**\n *\n * @author pthomas3\n */\npublic interface PerfContext {\n    \n    void capturePerfEvent(String name, long startTime, long endTime);\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/PerfHook.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.intuit.karate.core.FeatureResult;\nimport com.intuit.karate.core.PerfEvent;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.http.HttpRequest;\n\n/**\n *\n * @author pthomas3\n */\npublic interface PerfHook {\n\n    String getPerfEventName(HttpRequest request, ScenarioRuntime sr);\n\n    void reportPerfEvent(PerfEvent event);\n    \n    void submit(Runnable runnable);\n    \n    void afterFeature(FeatureResult fr);\n    \n    void pause(Number millis);\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/Results.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.intuit.karate.core.FeatureResult;\nimport com.intuit.karate.core.ScenarioResult;\nimport com.intuit.karate.core.TagResults;\nimport com.intuit.karate.core.TimelineResults;\nimport com.intuit.karate.report.ReportUtils;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Stream;\n\n/**\n *\n * @author pthomas3\n */\npublic class Results {\n\n    private final Suite suite;\n    private final int featuresPassed;\n    private final int featuresFailed;\n    private final int featuresSkipped;\n    private final int scenariosPassed;\n    private final int scenariosFailed;\n    private final double timeTakenMillis;\n    private final long endTime;\n    private final List<String> errors = new ArrayList<>();\n    private final List<Map<String, Object>> featureSummary = new ArrayList<>();\n\n    public static Results of(Suite suite) {\n        return new Results(suite);\n    }\n\n    private Results(Suite suite) {\n        this.suite = suite;\n        // endTime may not be set for junit\n        endTime = suite.endTime == 0 ? System.currentTimeMillis() : suite.endTime;\n        featuresSkipped = suite.skippedCount;\n        AtomicInteger fp = new AtomicInteger();\n        AtomicInteger ff = new AtomicInteger();\n        AtomicInteger sp = new AtomicInteger();\n        AtomicInteger sf = new AtomicInteger();\n        AtomicInteger time = new AtomicInteger();\n        TimelineResults timeline = new TimelineResults();\n        TagResults tags = new TagResults();\n        suite.getFeatureResults().forEach(fr -> {\n            if (!fr.isEmpty()) {\n                timeline.addFeatureResult(fr);\n                tags.addFeatureResult(fr);\n                if (fr.isFailed()) {\n                    ff.incrementAndGet();\n                } else {\n                    fp.incrementAndGet();\n                }\n                Long duration = Math.round(fr.getDurationMillis());\n                time.addAndGet(duration.intValue());\n                featureSummary.add(fr.toSummaryJson());\n            }\n            sp.addAndGet(fr.getPassedCount());\n            sf.addAndGet(fr.getFailedCount());\n            errors.addAll(fr.getErrors());\n        });\n        featuresPassed = fp.get();\n        featuresFailed = ff.get();\n        scenariosPassed = sp.get();\n        scenariosFailed = sf.get();\n        timeTakenMillis = time.get();\n        saveStatsJson();\n        printStats();\n        if (suite.outputHtmlReport) {\n            String displayEnv = StringUtils.isBlank(suite.env) ? \"\\n\" : \" | env: \" + suite.env + \"\\n\";\n            suite.suiteReports.timelineReport(suite, timeline).render();\n            suite.suiteReports.tagsReport(suite, tags).render();\n            // last so that path can be printed to the console\n            File file = suite.suiteReports.summaryReport(suite, this).render();\n            System.out.println(\"\\nHTML report: (paste into browser to view) | Karate version: \"\n                    + FileUtils.KARATE_VERSION + displayEnv\n                    + file.toPath().toUri()\n                    + \"\\n===================================================================\\n\");\n        }\n    }\n\n    public Stream<FeatureResult> getFeatureResults() {\n        return suite.getFeatureResults();\n    }\n\n    public Stream<ScenarioResult> getScenarioResults() {\n        return suite.getScenarioResults();\n    }\n\n    private void saveStatsJson() {\n        String json = JsonUtils.toJson(toKarateJson());\n        File file = new File(suite.reportDir + File.separator + \"karate-summary-json.txt\");\n        FileUtils.writeToFile(file, json);\n    }\n\n    private void printStats() {\n        String displayEnv = StringUtils.isBlank(suite.env) ? \"\" : \" | env: \" + suite.env;\n        System.out.println(\"Karate version: \" + FileUtils.KARATE_VERSION + displayEnv);\n        System.out.println(\"======================================================\");\n        System.out.println(String.format(\"elapsed: %6.2f | threads: %4d | thread time: %.2f \",\n                getElapsedTime() / 1000, suite.threadCount, timeTakenMillis / 1000));\n        System.out.println(String.format(\"features: %5d | skipped: %4d | efficiency: %.2f\", getFeaturesTotal(), featuresSkipped, getEfficiency()));\n        System.out.println(String.format(\"scenarios: %4d | passed: %5d | failed: %d\",\n                getScenariosTotal(), scenariosPassed, scenariosFailed));\n        System.out.println(\"======================================================\");\n        if (!errors.isEmpty()) {\n            System.out.println(\">>> failed features:\");\n            System.out.println(getErrorMessages());\n            System.out.println(\"<<<\");\n        }\n    }\n\n    public Map<String, Object> toKarateJson() {\n        Map<String, Object> map = new HashMap<>();\n        map.put(\"version\", FileUtils.KARATE_VERSION);\n        map.put(\"env\", suite.env);\n        map.put(\"threads\", suite.threadCount);\n        map.put(\"featuresPassed\", featuresPassed);\n        map.put(\"featuresFailed\", featuresFailed);\n        map.put(\"featuresSkipped\", featuresSkipped);\n        map.put(\"scenariosPassed\", scenariosPassed);\n        map.put(\"scenariosfailed\", errors.size());\n        map.put(\"elapsedTime\", getElapsedTime());\n        map.put(\"totalTime\", getTimeTakenMillis());\n        map.put(\"efficiency\", getEfficiency());\n        map.put(\"resultDate\", ReportUtils.getDateString());\n        map.put(\"featureSummary\", featureSummary);\n        return map;\n    }\n\n    public String getReportDir() {\n        return suite.reportDir;\n    }\n\n    public List<String> getErrors() {\n        return errors;\n    }\n\n    public double getElapsedTime() {\n        return endTime - suite.startTime;\n    }\n\n    public double getEfficiency() {\n        return timeTakenMillis / (getElapsedTime() * suite.threadCount);\n    }\n\n    public int getScenariosPassed() {\n        return scenariosPassed;\n    }\n\n    public int getScenariosFailed() {\n        return scenariosFailed;\n    }\n\n    public int getScenariosTotal() {\n        return scenariosPassed + scenariosFailed;\n    }\n\n    public int getFeaturesTotal() {\n        return featuresPassed + featuresFailed;\n    }\n\n    public int getFeaturesPassed() {\n        return featuresPassed;\n    }\n\n    public int getFeaturesFailed() {\n        return featuresFailed;\n    }\n\n    public int getFailCount() {\n        return errors.size();\n    }\n\n    public double getTimeTakenMillis() {\n        return timeTakenMillis;\n    }\n\n    public long getStartTime() {\n        return suite.startTime;\n    }\n\n    public long getEndTime() {\n        return endTime;\n    }\n\n    public String getErrorMessages() {\n        return StringUtils.join(errors, \"\\n\");\n    }\n\n    public Suite getSuite() {\n        return suite;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/Runner.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.FeatureCall;\nimport com.intuit.karate.core.FeatureResult;\nimport com.intuit.karate.core.FeatureRuntime;\nimport com.intuit.karate.core.RuntimeHookFactory;\nimport com.intuit.karate.core.ScenarioCall;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.driver.DriverRunner;\nimport com.intuit.karate.http.HttpClientFactory;\nimport com.intuit.karate.report.SuiteReports;\nimport com.intuit.karate.resource.ResourceUtils;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.File;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n/**\n *\n * @author pthomas3\n */\npublic class Runner {\n\n    private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(Runner.class);\n\n    public static Map<String, Object> runFeature(FeatureCall feature, Map<String, Object> vars, boolean evalKarateConfig) {\n        Suite suite = new Suite();\n        FeatureRuntime featureRuntime = FeatureRuntime.of(suite, feature, vars);\n        featureRuntime.caller.setKarateConfigDisabled(!evalKarateConfig);\n        featureRuntime.run();\n        FeatureResult result = featureRuntime.result;\n        if (result.isFailed()) {\n            throw result.getErrorMessagesCombined();\n        }\n        return result.getVariables();\n    }\n\n    public static Map<String, Object> runFeature(File file, Map<String, Object> vars, boolean evalKarateConfig) {\n        Feature feature = Feature.read(file);\n        return runFeature(new FeatureCall(feature), vars, evalKarateConfig);\n    }\n\n    public static Map<String, Object> runFeature(Class relativeTo, String path, Map<String, Object> vars, boolean evalKarateConfig) {\n        File file = ResourceUtils.getFileRelativeTo(relativeTo, path);\n        return runFeature(file, vars, evalKarateConfig);\n    }\n\n    public static Map<String, Object> runFeature(String path, Map<String, Object> vars, boolean evalKarateConfig) {\n        FeatureCall feature = FileUtils.parseFeatureAndCallTag(path);\n        return runFeature(feature, vars, evalKarateConfig);\n    }\n\n    // this is called by karate-gatling !\n    public static void callAsync(Runner.Builder builder, String path, Map<String, Object> arg, PerfHook perfHook) {\n        builder.features = Collections.emptyList(); // will skip expensive feature resolution in builder.resolveAll()\n        Suite suite = new Suite(builder);\n        FeatureCall feature = FileUtils.parseFeatureAndCallTag(path);\n        FeatureRuntime featureRuntime = FeatureRuntime.of(suite, feature, arg, perfHook);\n        featureRuntime.setNext(() -> perfHook.afterFeature(featureRuntime.result));\n        perfHook.submit(featureRuntime);\n    }\n\n    //==========================================================================\n    //\n    public static class Builder<T extends Builder> {\n\n        ClassLoader classLoader;\n        Class optionsClass;\n        String env;\n        File workingDir;\n        String buildDir;\n        String configDir;\n        int threadCount;\n        int timeoutMinutes;\n        String reportDir;\n        String scenarioName;\n        List<String> tags;\n        List<String> paths;\n        List<FeatureCall> features;\n        String relativeTo;\n        final Collection<RuntimeHook> hooks = new ArrayList();\n        RuntimeHookFactory hookFactory;\n        HttpClientFactory clientFactory;\n        boolean forTempUse;\n        boolean backupReportDir = true;\n        boolean outputHtmlReport = true;\n        boolean outputJunitXml;\n        boolean outputCucumberJson;\n        boolean dryRun;\n        boolean debugMode;\n        boolean failWhenNoScenariosFound;\n        Map<String, String> systemProperties;\n        Map<String, Object> callSingleCache;\n        Map<String, ScenarioCall.Result> callOnceCache;\n        SuiteReports suiteReports;\n        Map<String, DriverRunner> drivers;\n\n        // synchronize because the main user is karate-gatling\n        public synchronized Builder copy() {\n            Builder b = new Builder();\n            b.classLoader = classLoader;\n            b.optionsClass = optionsClass;\n            b.env = env;\n            b.workingDir = workingDir;\n            b.buildDir = buildDir;\n            b.configDir = configDir;\n            b.threadCount = threadCount;\n            b.timeoutMinutes = timeoutMinutes;\n            b.reportDir = reportDir;\n            b.scenarioName = scenarioName;\n            b.tags = tags;\n            b.paths = paths;\n            b.features = features;\n            b.relativeTo = relativeTo;\n            b.hooks.addAll(hooks); // final\n            b.hookFactory = hookFactory;\n            b.clientFactory = clientFactory;\n            b.forTempUse = forTempUse;\n            b.backupReportDir = backupReportDir;\n            b.outputHtmlReport = outputHtmlReport;\n            b.outputJunitXml = outputJunitXml;\n            b.outputCucumberJson = outputCucumberJson;\n            b.dryRun = dryRun;\n            b.debugMode = debugMode;\n            b.failWhenNoScenariosFound = failWhenNoScenariosFound;\n            b.systemProperties = systemProperties;\n            b.callSingleCache = callSingleCache;\n            b.callOnceCache = callOnceCache;\n            b.suiteReports = suiteReports;\n            b.drivers = drivers;\n            return b;\n        }\n\n        public List<FeatureCall> resolveAll() {\n            if (classLoader == null) {\n                classLoader = Thread.currentThread().getContextClassLoader();\n            }\n            if (clientFactory == null) {\n                clientFactory = HttpClientFactory.DEFAULT;\n            }\n            if (systemProperties == null) {\n                systemProperties = new HashMap(System.getProperties());\n            } else {\n                systemProperties.putAll(new HashMap(System.getProperties()));\n            }\n            // env\n            String tempOptions = StringUtils.trimToNull(systemProperties.get(Constants.KARATE_OPTIONS));\n            if (tempOptions != null) {\n                LOGGER.info(\"using system property '{}': {}\", Constants.KARATE_OPTIONS, tempOptions);\n                Main ko = Main.parseKarateOptions(tempOptions);\n                if (ko.name != null) {\n                    scenarioName = ko.name;\n                }\n                if (ko.configDir != null) {\n                    configDir = ko.configDir;\n                }\n                if (ko.env != null) {\n                    env = ko.env;\n                }\n                if (ko.reportDir != null) {\n                    reportDir = ko.reportDir;\n                }\n                if (ko.tags != null) {\n                    tags = ko.tags;\n                }\n                if (ko.paths != null) {\n                    paths = ko.paths;\n                }\n                if (ko.threads != 0) {\n                    threadCount = ko.threads;\n                }\n                dryRun = ko.dryRun || dryRun;\n            }\n            String tempEnv = StringUtils.trimToNull(systemProperties.get(Constants.KARATE_ENV));\n            if (tempEnv != null) {\n                LOGGER.info(\"using system property '{}': {}\", Constants.KARATE_ENV, tempEnv);\n                env = tempEnv;\n            } else if (env != null) {\n                LOGGER.info(\"karate.env is: '{}'\", env);\n            }\n            // config dir\n            String tempConfig = StringUtils.trimToNull(systemProperties.get(Constants.KARATE_CONFIG_DIR));\n            if (tempConfig != null) {\n                LOGGER.info(\"using system property '{}': {}\", Constants.KARATE_CONFIG_DIR, tempConfig);\n                configDir = tempConfig;\n            }\n            if (workingDir == null) {\n                workingDir = FileUtils.WORKING_DIR;\n            }\n            if (configDir == null) {\n                try {\n                    ResourceUtils.getResource(workingDir, \"classpath:karate-config.js\");\n                    configDir = \"classpath:\"; // default mode\n                } catch (Exception e) {\n                    configDir = workingDir.getPath();\n                }\n            }\n            if (configDir.startsWith(\"file:\") || configDir.startsWith(\"classpath:\")) {\n                // all good\n            } else {\n                configDir = \"file:\" + configDir;\n            }\n            if (configDir.endsWith(\":\") || configDir.endsWith(\"/\") || configDir.endsWith(\"\\\\\")) {\n                // all good\n            } else {\n                configDir = configDir + File.separator;\n            }\n            if (buildDir == null) {\n                buildDir = FileUtils.getBuildDir();\n            }\n            if (reportDir == null) {\n                reportDir = buildDir + File.separator + Constants.KARATE_REPORTS;\n            }\n            // hooks\n            if (hookFactory != null) {\n                hook(hookFactory.create());\n            }\n            // features\n            if (features == null) {\n                if (paths != null && !paths.isEmpty()) {\n                    if (relativeTo != null) {\n                        paths = paths.stream().map(p -> {\n                            if (p.startsWith(\"classpath:\")) {\n                                return p;\n                            }\n                            if (!p.endsWith(\".feature\")) {\n                                p = p + \".feature\";\n                            }\n                            return relativeTo + \"/\" + p;\n                        }).collect(Collectors.toList());\n                    }\n                } else if (relativeTo != null) {\n                    paths = new ArrayList();\n                    paths.add(relativeTo);\n                }\n                features = ResourceUtils.findFeatureFiles(workingDir, paths, scenarioName);\n            }\n            if (callSingleCache == null) {\n                callSingleCache = new HashMap();\n            }\n            if (callOnceCache == null) {\n                callOnceCache = new HashMap();\n            }\n            if (suiteReports == null) {\n                suiteReports = SuiteReports.DEFAULT;\n            }\n            if (drivers != null) {\n                Map<String, DriverRunner> customDrivers = drivers;\n                drivers = DriverOptions.driverRunners();\n                drivers.putAll(customDrivers); // allows override of Karate drivers (e.g. custom 'chrome')\n            } else {\n                drivers = DriverOptions.driverRunners();\n            }\n            if (threadCount < 1) {\n                threadCount = 1;\n            }\n            return features;\n        }\n\n        public T forTempUse() {\n            forTempUse = true;\n            return (T) this;\n        }\n\n        //======================================================================\n        //\n        public T configDir(String dir) {\n            this.configDir = dir;\n            return (T) this;\n        }\n\n        public T karateEnv(String env) {\n            this.env = env;\n            return (T) this;\n        }\n\n        public T systemProperty(String key, String value) {\n            if (systemProperties == null) {\n                systemProperties = new HashMap();\n            }\n            systemProperties.put(key, value);\n            return (T) this;\n        }\n\n        public T workingDir(File value) {\n            if (value != null) {\n                this.workingDir = value;\n            }\n            return (T) this;\n        }\n\n        public T buildDir(String value) {\n            if (value != null) {\n                this.buildDir = value;\n            }\n            return (T) this;\n        }\n\n        public T classLoader(ClassLoader value) {\n            classLoader = value;\n            return (T) this;\n        }\n\n        public T relativeTo(Class clazz) {\n            relativeTo = \"classpath:\" + ResourceUtils.toPathFromClassPathRoot(clazz);\n            return (T) this;\n        }\n\n        public T path(String... value) {\n            path(Arrays.asList(value));\n            return (T) this;\n        }\n\n        public T path(List<String> value) {\n            if (value != null) {\n                if (paths == null) {\n                    paths = new ArrayList();\n                }\n                paths.addAll(value);\n            }\n            return (T) this;\n        }\n\n        public T tags(List<String> value) {\n            if (value != null) {\n                if (tags == null) {\n                    tags = new ArrayList();\n                }\n                tags.addAll(value);\n            }\n            return (T) this;\n        }\n\n        public T tags(String... tags) {\n            tags(Arrays.asList(tags));\n            return (T) this;\n        }\n\n        public T features(Collection<Feature> value) {\n            if (value != null) {\n                if (features == null) {\n                    features = new ArrayList();\n                }\n                features.addAll(value.stream().map(FeatureCall::new).collect(Collectors.toList()));\n            }\n            return (T) this;\n        }\n\n        public T features(Feature... value) {\n            return features(Arrays.asList(value));\n        }\n\n        public T reportDir(String value) {\n            if (value != null) {\n                this.reportDir = value;\n            }\n            return (T) this;\n        }\n\n        public T scenarioName(String name) {\n            this.scenarioName = name;\n            return (T) this;\n        }\n\n        public T timeoutMinutes(int timeoutMinutes) {\n            this.timeoutMinutes = timeoutMinutes;\n            return (T) this;\n        }\n\n        public T hook(RuntimeHook hook) {\n            if (hook != null) {\n                hooks.add(hook);\n            }\n            return (T) this;\n        }\n\n        public T hooks(Collection<RuntimeHook> hooks) {\n            if (hooks != null) {\n                this.hooks.addAll(hooks);\n            }\n            return (T) this;\n        }\n\n        public T hookFactory(RuntimeHookFactory hookFactory) {\n            this.hookFactory = hookFactory;\n            return (T) this;\n        }\n\n        public T clientFactory(HttpClientFactory clientFactory) {\n            this.clientFactory = clientFactory;\n            return (T) this;\n        }\n\n        // don't allow junit 5 builder to run in parallel\n        public Builder threads(int value) {\n            threadCount = value;\n            return this;\n        }\n\n        public T outputHtmlReport(boolean value) {\n            outputHtmlReport = value;\n            return (T) this;\n        }\n\n        public T backupReportDir(boolean value) {\n            backupReportDir = value;\n            return (T) this;\n        }\n\n        public T outputCucumberJson(boolean value) {\n            outputCucumberJson = value;\n            return (T) this;\n        }\n\n        public T outputJunitXml(boolean value) {\n            outputJunitXml = value;\n            return (T) this;\n        }\n\n        public T dryRun(boolean value) {\n            dryRun = value;\n            return (T) this;\n        }\n\n        public T debugMode(boolean value) {\n            debugMode = value;\n            return (T) this;\n        }\n        \n        public T failWhenNoScenariosFound(boolean value) {\n            failWhenNoScenariosFound = value;\n            return (T) this;\n        }\n\n        public T callSingleCache(Map<String, Object> value) {\n            callSingleCache = value;\n            return (T) this;\n        }\n\n        public T callOnceCache(Map<String, ScenarioCall.Result> value) {\n            callOnceCache = value;\n            return (T) this;\n        }\n\n        public T suiteReports(SuiteReports value) {\n            suiteReports = value;\n            return (T) this;\n        }\n\n        public T customDrivers(Map<String, DriverRunner> customDrivers) {\n            drivers = customDrivers;\n            return (T) this;\n        }\n\n        private Integer getDebugPort() {\n            String debugPortString = StringUtils.trimToNull(System.getProperty(Constants.KARATE_DEBUG_PORT));\n            if (debugPortString == null) {\n                return null;\n            }\n            int debugPort = 0;\n            try {\n                debugPort = Integer.valueOf(debugPortString);\n            } catch (Exception e) {\n                // ignore\n            }\n            return debugPort;\n        }\n\n        private String[] getDebugArgs(int debugPort) {\n            List<String> args = new ArrayList();\n            args.add(\"-d\");\n            args.add(debugPort + \"\");\n            if (reportDir != null) {\n                args.add(\"-r\");\n                args.add(reportDir);\n            }\n            if (env != null) {\n                args.add(\"-e\");\n                args.add(env);\n            }\n            if (tags != null) {\n                for (String tag : tags) {\n                    args.add(\"-t\");\n                    args.add(tag);\n                }\n            }\n            if (threadCount != 1) {\n                args.add(\"-T\");\n                args.add(threadCount + \"\");\n            }\n            if (paths != null) {\n                args.addAll(paths);\n            }\n            return args.toArray(new String[]{});\n        }\n\n        public Results parallel(int threadCount) {\n            threads(threadCount);\n            Integer debugPort = getDebugPort();\n            if (debugPort != null && !debugMode) {\n                String[] args = getDebugArgs(debugPort);\n                if (systemProperties != null) {\n                    systemProperties.forEach((k, v) -> System.setProperty(k, v));\n                }\n                return Main.startDebugServer(args);\n            }\n            Suite suite = new Suite(this);\n            suite.run();\n            return suite.buildResults();\n        }\n\n        @Override\n        public String toString() {\n            return paths + \"\";\n        }\n\n    }\n\n    public static Builder path(String... paths) {\n        Builder builder = new Builder();\n        return builder.path(paths);\n    }\n\n    public static Builder path(List<String> paths) {\n        Builder builder = new Builder();\n        return builder.path(paths);\n    }\n\n    public static Builder builder() {\n        return new Runner.Builder();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/RuntimeHook.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.intuit.karate.core.FeatureRuntime;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.core.Step;\nimport com.intuit.karate.core.StepResult;\nimport com.intuit.karate.http.HttpRequest;\nimport com.intuit.karate.http.Response;\n\n/**\n *\n * @author pthomas3\n */\npublic interface RuntimeHook {\n\n    // return false if the scenario / item should be excluded from the test-run\n    // throw RuntimeException (any) to abort    \n    default boolean beforeScenario(ScenarioRuntime sr) {\n        return true;\n    }\n\n    default void afterScenario(ScenarioRuntime sr) {\n\n    }\n\n    default void afterScenarioOutline(ScenarioRuntime sr) {\n\n    }\n\n    default boolean beforeFeature(FeatureRuntime fr) {\n        return true;\n    }\n\n    default void afterFeature(FeatureRuntime fr) {\n\n    }\n\n    default void beforeSuite(Suite suite) {\n\n    }\n\n    default void afterSuite(Suite suite) {\n        \n    }\n\n    default boolean beforeStep(Step step, ScenarioRuntime sr) {\n        return true;\n    }\n\n    default void afterStep(StepResult result, ScenarioRuntime sr) {\n        \n    }\n\n    default void beforeHttpCall(HttpRequest request, ScenarioRuntime sr) {\n        \n    }\n    \n    default void afterHttpCall(HttpRequest request, Response response, ScenarioRuntime sr) {\n        \n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/ScenarioActions.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.intuit.karate.core.AssignType;\nimport com.intuit.karate.core.ScenarioEngine;\nimport com.intuit.karate.core.When;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * @author pthomas3\n */\npublic class ScenarioActions implements Actions {\n\n    private final ScenarioEngine engine;\n\n    public ScenarioActions(ScenarioEngine engine) {\n        this.engine = engine;\n    }\n\n    @Override\n    public boolean isFailed() {\n        return engine.isFailed();\n    }\n\n    @Override\n    public Throwable getFailedReason() {\n        return engine.getFailedReason();\n    }\n\n    @Override\n    public boolean isAborted() {\n        return engine.isAborted();\n    }\n\n    @Override\n    @When(\"^configure ([^\\\\s]+) =$\")\n    public void configureDocString(String key, String exp) {\n        engine.configure(key, exp);\n    }\n\n    @Override\n    @When(\"^configure\\\\h+([^\\\\s]+)\\\\h+= (.+)\")\n    public void configure(String key, String exp) {\n        engine.configure(key, exp);\n    }\n\n    @Override\n    @When(\"^url (.+)\")\n    public void url(String exp) {\n        engine.url(exp);\n    }\n\n    @Override\n    @When(\"^path (.+)\")\n    public void path(String exp) {\n        engine.path(exp);\n    }\n\n    @Override\n    @When(\"^param\\\\h+([^\\\\s]+)\\\\h+= (.+)\")\n    public void param(String name, String exp) {\n        engine.param(name, exp);\n    }\n\n    @Override\n    @When(\"^params (.+)\")\n    public void params(String exp) {\n        engine.params(exp);\n    }\n\n    @Override\n    @When(\"^cookie\\\\h+([^\\\\s]+)\\\\h+= (.+)\")\n    public void cookie(String name, String value) {\n        engine.cookie(name, value);\n    }\n\n    @Override\n    @When(\"^cookies (.+)\")\n    public void cookies(String exp) {\n        engine.cookies(exp);\n    }\n\n    @Override\n    @When(\"^csv (.+) = (.+)\")\n    public void csv(String name, String exp) {\n        engine.assign(AssignType.CSV, name, exp, false);\n    }\n\n    @Override\n    @When(\"^csv (.+) =$\")\n    public void csvDocString(String name, String exp) {\n        engine.assign(AssignType.CSV, name, exp, true);\n    }\n\n    @Override\n    @When(\"^header\\\\h+([^\\\\s]+)\\\\h+= (.+)\")\n    public void header(String name, String exp) {\n        engine.header(name, exp);\n    }\n\n    @Override\n    @When(\"^headers (.+)\")\n    public void headers(String exp) {\n        engine.headers(exp);\n    }\n\n    @Override\n    @When(\"^form field\\\\h+([^\\\\s]+)\\\\h+= (.+)\")\n    public void formField(String name, String exp) {\n        engine.formField(name, exp);\n    }\n\n    @Override\n    @When(\"^form fields (.+)\")\n    public void formFields(String exp) {\n        engine.formFields(exp);\n    }\n\n    @Override\n    @When(\"^request$\")\n    public void requestDocString(String body) {\n        engine.request(body);\n    }\n\n    @Override\n    @When(\"^request (.+)\")\n    public void request(String body) {\n        engine.request(body);\n    }\n\n    @Override\n    @When(\"^table (.+)\")\n    public void table(String name, List<Map<String, String>> table) {\n        engine.table(name, table);\n    }\n\n    @Override\n    @When(\"^replace (\\\\w+)$\")\n    public void replace(String name, List<Map<String, String>> table) {\n        engine.replaceTable(name, table);\n    }\n\n    @Override\n    @When(\"^replace\\\\h+(\\\\w+).([^\\\\s]+)\\\\h+= (.+)\")\n    public void replace(String name, String token, String value) {\n        engine.replace(name, token, value);\n    }\n\n    @Override\n    @When(\"^def\\\\h+(\\\\w+)\\\\h+=\\\\h+(.+)\")\n    public void def(String name, String exp) {\n        engine.assign(AssignType.AUTO, name, exp, false);\n    }\n\n    @Override\n    @When(\"^def (.+) =$\")\n    public void defDocString(String name, String exp) {\n        engine.assign(AssignType.AUTO, name, exp, false); // auto-parse json and xml\n    }\n\n    @Override\n    @When(\"^text (.+) =$\")\n    public void text(String name, String exp) {\n        engine.assign(AssignType.TEXT, name, exp, true); // doc string\n    }\n\n    @Override\n    @When(\"^yaml (.+) = (.+)\")\n    public void yaml(String name, String exp) {\n        engine.assign(AssignType.YAML, name, exp, false);\n    }\n\n    @Override\n    @When(\"^yaml (.+) =$\")\n    public void yamlDocString(String name, String exp) {\n        engine.assign(AssignType.YAML, name, exp, true);\n    }\n\n    @Override\n    @When(\"^copy (.+) = (.+)\")\n    public void copy(String name, String exp) {\n        engine.assign(AssignType.COPY, name, exp, false);\n    }\n\n    @Override\n    @When(\"^json (.+) = (.+)\")\n    public void json(String name, String exp) {\n        engine.assign(AssignType.JSON, name, exp, false);\n    }\n\n    @Override\n    @When(\"^string (.+) = (.+)\")\n    public void string(String name, String exp) {\n        engine.assign(AssignType.STRING, name, exp, false);\n    }\n\n    @Override\n    @When(\"^xml (.+) = (.+)\")\n    public void xml(String name, String exp) {\n        engine.assign(AssignType.XML, name, exp, false);\n    }\n\n    @Override\n    @When(\"^xmlstring (.+) = (.+)\")\n    public void xmlstring(String name, String exp) {\n        engine.assign(AssignType.XML_STRING, name, exp, false);\n    }\n\n    @Override\n    @When(\"^bytes (.+) = (.+)\")\n    public void bytes(String name, String exp) {\n        engine.assign(AssignType.BYTE_ARRAY, name, exp, false);\n    }\n\n    @Override\n    @When(\"^assert (.+)\")\n    public void assertTrue(String exp) {\n        engine.assertTrue(exp);\n    }\n\n    @Override\n    @When(\"^method\\\\h+(\\\\w+)\")\n    public void method(String method) {\n        engine.method(method);\n    }\n\n    @Override\n    @When(\"^retry until (.+)\")\n    public void retry(String until) {\n        engine.retry(until);\n    }\n\n    @Override\n    @When(\"^soap action( .+)?\")\n    public void soapAction(String action) {\n        engine.soapAction(action);\n    }\n\n    @Override\n    @When(\"^multipart entity (.+)\")\n    public void multipartEntity(String value) {\n        engine.multipartField(null, value);\n    }\n\n    @Override\n    @When(\"^multipart field (.+) = (.+)\")\n    public void multipartField(String name, String value) {\n        engine.multipartField(name, value);\n    }\n\n    @Override\n    @When(\"^multipart fields (.+)\")\n    public void multipartFields(String exp) {\n        engine.multipartFields(exp);\n    }\n\n    @Override\n    @When(\"^multipart file (.+) = (.+)\")\n    public void multipartFile(String name, String value) {\n        engine.multipartFile(name, value);\n    }\n\n    @Override\n    @When(\"^multipart files (.+)\")\n    public void multipartFiles(String exp) {\n        engine.multipartFiles(exp);\n    }\n\n    @Override\n    @When(\"^print (.+)\")\n    public void print(String exp) {\n        engine.print(exp);\n    }\n\n    @Override\n    @When(\"^status\\\\h+(\\\\d+)\")\n    public void status(int status) {\n        engine.status(status);\n    }\n\n    @Override\n    @When(\"^match (.+)(=|contains|any|only|deep)(.*)\")\n    public void match(String exp, String op1, String op2, String rhs) {\n        if (op2 == null) {\n            op2 = \"\";\n        }\n        if (rhs == null) {\n            rhs = \"\";\n        } else {\n            rhs = \" \" + rhs;\n        }\n        MatchStep m = new MatchStep(exp + op1 + op2 + rhs);\n        engine.matchResult(m.type, m.name, m.path, m.expected);\n    }\n\n    @Override\n    @When(\"^set\\\\h+([^\\\\s]+)( .+)? =$\")\n    public void setDocString(String name, String path, String value) {\n        engine.set(name, path, value);\n    }\n\n    @Override\n    @When(\"^set\\\\h+([^\\\\s]+)( .+)?\\\\h+= (.+)\")\n    public void set(String name, String path, String value) {\n        engine.set(name, path, value);\n    }\n\n    @Override\n    @When(\"^set ([^\\\\s]+)( [^=]+)?$\")\n    public void set(String name, String path, List<Map<String, String>> table) {\n        engine.setViaTable(name, path, table);\n    }\n\n    @Override\n    @When(\"^remove\\\\h+([^\\\\s]+)( .+)?\")\n    public void remove(String name, String path) {\n        engine.remove(name, path);\n    }\n\n    @Override\n    @When(\"^call (.+)\")\n    public void call(String line) {\n        engine.call(false, line, true);\n    }\n\n    @Override\n    @When(\"^callonce (.+)\")\n    public void callonce(String line) {\n        engine.call(true, line, true);\n    }\n\n    @Override\n    @When(\"^eval (.+)\")\n    public void eval(String exp) {\n        engine.evalJs(exp);\n    }\n\n    @Override\n    @When(\"^eval$\")\n    public void evalDocString(String exp) {\n        engine.evalJs(exp);\n    }\n\n    @Override\n    @When(\"^([\\\\w]+\\\\.[^=]+=$)\")\n    public void evalAssignDocString(String lhs, String rhs) {\n        engine.evalJs(lhs + rhs);\n    }\n\n    @Override\n    @When(\"^([\\\\w]+)([^\\\\s^\\\\w])(.+)\")\n    public void eval(String name, String dotOrParen, String exp) {\n        engine.evalJs(name + dotOrParen + exp);\n    }\n\n    @Override\n    @When(\"^if (.+)\")\n    public void evalIf(String exp) {\n        engine.evalJs(\"if \" + exp);\n    }\n\n    @Override\n    @When(\"^delete (.+)\")\n    public void evalDelete(String exp) {\n        engine.evalJs(\"delete \" + exp);\n    }\n\n    @Override\n    @When(\"^listen (.+)\")\n    public void listen(String body) {\n        engine.listen(body);\n    }\n\n    @Override\n    @When(\"^doc (.+)\")\n    public void doc(String exp) {\n        engine.doc(exp);\n    }\n\n    @Override\n    @When(\"^compareImage (.+)\")\n    public void compareImage(String exp) {\n        engine.compareImage(exp);\n    }\n\n    //==========================================================================\n    //\n    @Override\n    @When(\"^driver (.+)\")\n    public void driver(String exp) {\n        engine.driver(exp);\n    }\n\n    @Override\n    @When(\"^robot (.+)\")\n    public void robot(String exp) {\n        engine.robot(exp);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/StringUtils.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringReader;\nimport java.io.StringWriter;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\n/**\n *\n * @author pthomas3\n */\npublic class StringUtils {\n\n    private StringUtils() {\n        // only static methods\n    }\n\n    public static final String EMPTY = \"\";\n\n    public static class Pair {\n\n        public final String left;\n        public final String right;\n\n        public Pair(String left, String right) {\n            this.left = left;\n            this.right = right;\n        }\n\n        @Override // only needed for unit tests, so no validation and null checks\n        public boolean equals(Object obj) {\n            Pair o = (Pair) obj;\n            return left.equals(o.left) && right.equals(o.right);\n        }\n\n        @Override\n        public String toString() {\n            return left + \":\" + right;\n        }\n\n    }\n\n    public static Pair pair(String left, String right) {\n        return new Pair(left, right);\n    }\n\n    public static String truncate(String s, int length, boolean addDots) {\n        if (s == null) {\n            return EMPTY;\n        }\n        if (s.length() > length) {\n            return addDots ? s.substring(0, length) + \" ...\" : s.substring(0, length);\n        }\n        return s;\n    }\n\n    public static String trimToEmpty(String s) {\n        if (s == null) {\n            return EMPTY;\n        } else {\n            return s.trim();\n        }\n    }\n\n    public static String trimToNull(String s) {\n        if (s == null) {\n            return null;\n        }\n        String temp = trimToEmpty(s);\n        return EMPTY.equals(temp) ? null : temp;\n    }\n\n    public static String repeat(char c, int count) {\n        StringBuilder sb = new StringBuilder();\n        for (int i = 0; i < count; i++) {\n            sb.append(c);\n        }\n        return sb.toString();\n    }\n\n    public static String join(Object[] a, char delimiter) {\n        StringBuilder sb = new StringBuilder();\n        for (int i = 0; i < a.length; i++) {\n            sb.append(a[i]);\n            if (i != a.length - 1) {\n                sb.append(delimiter);\n            }\n        }\n        return sb.toString();\n    }\n\n    public static String join(Collection<String> c, String delimiter) {\n        StringBuilder sb = new StringBuilder();\n        Iterator iterator = c.iterator();\n        while (iterator.hasNext()) {\n            sb.append(iterator.next());\n            if (iterator.hasNext()) {\n                sb.append(delimiter);\n            }\n        }\n        return sb.toString();\n    }\n\n    public static List<String> split(String s, char delimiter, boolean skipBackSlash) {\n        int pos = s.indexOf(delimiter);\n        if (pos == -1) {\n            return Collections.singletonList(s);\n        }\n        List<String> list = new ArrayList();\n        int startPos = 0;\n        int searchPos = 0;\n        while (pos != -1) {\n            if (skipBackSlash && pos > 0 && s.charAt(pos - 1) == '\\\\') {\n                s = s.substring(0, pos - 1) + s.substring(pos);\n                searchPos = pos;\n            } else {\n                String temp = s.substring(startPos, pos);\n                if (!EMPTY.equals(temp)) {\n                    list.add(temp);\n                }\n                startPos = pos + 1;\n                searchPos = startPos;\n            }\n            pos = s.indexOf(delimiter, searchPos);\n        }\n        if (startPos != s.length()) {\n            String temp = s.substring(startPos);\n            if (!EMPTY.equals(temp)) {\n                list.add(temp);\n            }\n        }\n        return list;\n    }\n\n    public static boolean isBlank(String s) {\n        return trimToNull(s) == null;\n    }\n\n    public static String toIdString(String name) {\n        if (name == null) {\n            return \"\";\n        }\n        return name.replaceAll(\"[\\\\s_\\\\\\\\/:<>\\\"\\\\|\\\\?\\\\*]\", \"-\").toLowerCase();\n    }\n\npublic static StringUtils.Pair splitByFirstLineFeed(String text) {\n        String left = \"\";\n        String right = \"\";\n        if (text != null) {\n            int pos = text.indexOf('\\n');\n            if (pos != -1) {\n                left = text.substring(0, pos).trim();\n                right = text.substring(pos).trim();\n            } else {\n                left = text.trim();\n            }\n        }\n        return StringUtils.pair(left, right);\n    }\n\n    public static List<String> toStringLines(String text) {\n        return new BufferedReader(new StringReader(text)).lines().collect(Collectors.toList());\n    }\n\n    public static int countLineFeeds(String text) {\n        int count = 0;\n        for (char c : text.toCharArray()) {\n            if (c == '\\n') {\n                count++;\n            }\n        }\n        return count;\n    }\n\n    public static int wrappedLinesEstimate(String text, int colWidth) {\n        List<String> lines = toStringLines(text);\n        int estimate = 0;\n        for (String s : lines) {\n            int wrapEstimate = (int) Math.ceil(s.length() / colWidth);\n            if (wrapEstimate == 0) {\n                estimate++;\n            } else {\n                estimate += wrapEstimate;\n            }\n        }\n        return estimate;\n    }\n\n    // TODO remove js function utils\n    private static final Pattern FUNCTION_PATTERN = Pattern.compile(\"^function[^(]*\\\\(\");\n\n    public static boolean isJavaScriptFunction(String text) {\n        return FUNCTION_PATTERN.matcher(text).find();\n    }\n\n    public static String fixJavaScriptFunction(String text) {\n        Matcher matcher = FUNCTION_PATTERN.matcher(text);\n        if (matcher.find()) {\n            return matcher.replaceFirst(\"function(\");\n        } else {\n            return text;\n        }\n    }\n\n    public static <T> T getIgnoreKeyCase(Map<String, T> map, String name) {\n        if (map == null || name == null) {\n            return null;\n        }\n        for (Map.Entry<String, T> entry : map.entrySet()) {\n            String key = entry.getKey();\n            if (name.equalsIgnoreCase(key)) {\n                return entry.getValue();\n            }\n        }\n        return null;\n    }\n\n    public static void removeIgnoreKeyCase(Map<String, ?> map, String name) {\n        if (map == null || name == null) {\n            return;\n        }\n        for (String key : map.keySet()) {\n            if (name.equalsIgnoreCase(key)) {\n                map.remove(key);\n                return;\n            }\n        }\n    }\n\n    public static boolean containsIgnoreCase(List<String> list, String str) {\n        for (String i : list) {\n            if (i.equalsIgnoreCase(str)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public static String throwableToString(Throwable t) {\n        try(final StringWriter sw = new StringWriter();\n            final PrintWriter pw = new PrintWriter(sw, true)) {\n            t.printStackTrace(pw);\n            return sw.toString();\n        } catch (IOException e) {\n            return null;\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/Suite.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.intuit.karate.core.FeatureCall;\nimport com.intuit.karate.core.FeatureResult;\nimport com.intuit.karate.core.FeatureRuntime;\nimport com.intuit.karate.core.Scenario;\nimport com.intuit.karate.core.ScenarioCall;\nimport com.intuit.karate.core.ScenarioResult;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.core.Step;\nimport com.intuit.karate.core.SyncExecutorService;\nimport com.intuit.karate.core.Tags;\nimport com.intuit.karate.driver.DriverRunner;\nimport com.intuit.karate.http.HttpClientFactory;\nimport com.intuit.karate.report.ReportUtils;\nimport com.intuit.karate.report.SuiteReports;\nimport com.intuit.karate.resource.Resource;\nimport com.intuit.karate.resource.ResourceUtils;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.File;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static java.util.function.Predicate.not;\n\n/**\n *\n * @author pthomas3\n */\npublic class Suite implements Runnable {\n\n    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Suite.class);\n\n    public final long startTime;\n    protected long endTime;\n    protected int skippedCount;\n    private AtomicBoolean abort = new AtomicBoolean(false);\n\n    public final String env;\n    public final String tagSelector;\n    public final boolean dryRun;\n    public final boolean debugMode;\n    public final boolean failWhenNoScenariosFound;\n    public final File workingDir;\n    public final String buildDir;\n    public final String reportDir;\n    public final ClassLoader classLoader;\n    public final int threadCount;\n    public final int timeoutMinutes;\n    public final int featuresFound;\n    public final List<FeatureCall> features;\n    public final List<CompletableFuture> futures;\n    public final Set<File> featureResultFiles;\n    public final Collection<RuntimeHook> hooks;\n    public final HttpClientFactory clientFactory;\n    public final Map<String, String> systemProperties;\n\n    public final boolean backupReportDir;\n    public final SuiteReports suiteReports;\n\n    public final boolean outputHtmlReport;\n    public final boolean outputCucumberJson;\n    public final boolean outputJunitXml;\n\n    public final boolean parallel;\n    public final ExecutorService scenarioExecutor;\n    public final ExecutorService pendingTasks;\n\n    public final String karateBase;\n    public final String karateConfig;\n    public final String karateConfigEnv;\n\n    public final Map<String, Object> callSingleCache;\n    public final Map<String, ScenarioCall.Result> callOnceCache;\n\n    public final Map<String, DriverRunner> drivers;\n\n    private String read(String name) {\n        try {\n            Resource resource = ResourceUtils.getResource(workingDir, name);\n            logger.debug(\"[config] {}\", resource.getPrefixedPath());\n            return FileUtils.toString(resource.getStream());\n        } catch (Exception e) {\n            logger.trace(\"file not found: {} - {}\", name, e.getMessage());\n            return null;\n        }\n    }\n\n    public static Suite forTempUse(HttpClientFactory hcf) {\n        return new Suite(Runner.builder().clientFactory(hcf).forTempUse());\n    }\n\n    public Suite() {\n        this(Runner.builder());\n    }\n\n    public Suite(Runner.Builder rb) {\n        if (rb.forTempUse) {\n            dryRun = false;\n            debugMode = false;\n            failWhenNoScenariosFound = false;\n            backupReportDir = false;\n            outputHtmlReport = false;\n            outputCucumberJson = false;\n            outputJunitXml = false;\n            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();\n            if (contextClassLoader != null) {\n                classLoader = contextClassLoader;\n            } else {\n                classLoader = getClass().getClassLoader();\n            }\n            clientFactory = rb.clientFactory == null ? HttpClientFactory.DEFAULT : rb.clientFactory;\n            startTime = -1;\n            env = rb.env;\n            systemProperties = null;\n            tagSelector = null;\n            threadCount = -1;\n            timeoutMinutes = -1;\n            hooks = Collections.EMPTY_LIST;\n            features = null;\n            featuresFound = -1;\n            futures = null;\n            featureResultFiles = null;\n            workingDir = FileUtils.WORKING_DIR;\n            buildDir = FileUtils.getBuildDir();\n            reportDir = FileUtils.getBuildDir();\n            karateBase = null;\n            karateConfig = null;\n            karateConfigEnv = null;\n            parallel = false;\n            scenarioExecutor = null;\n            pendingTasks = null;\n            callSingleCache = null;\n            callOnceCache = null;\n            suiteReports = null;\n            drivers = null;\n        } else {\n            startTime = System.currentTimeMillis();\n            rb.resolveAll();\n            backupReportDir = rb.backupReportDir;\n            outputHtmlReport = rb.outputHtmlReport;\n            outputCucumberJson = rb.outputCucumberJson;\n            outputJunitXml = rb.outputJunitXml;\n            dryRun = rb.dryRun;\n            debugMode = rb.debugMode;\n            failWhenNoScenariosFound = rb.failWhenNoScenariosFound;\n            classLoader = rb.classLoader;\n            clientFactory = rb.clientFactory;\n            env = rb.env;\n            systemProperties = rb.systemProperties;\n            tagSelector = Tags.fromKarateOptionsTags(rb.tags);\n            hooks = rb.hooks;\n            features = rb.features;\n            featuresFound = features.size();\n            futures = new ArrayList(featuresFound);\n            callSingleCache = rb.callSingleCache;\n            callOnceCache = rb.callOnceCache;\n            suiteReports = rb.suiteReports;\n            featureResultFiles = new HashSet();\n            workingDir = rb.workingDir;\n            buildDir = rb.buildDir;\n            reportDir = rb.reportDir;\n            karateBase = read(\"classpath:karate-base.js\");\n            karateConfig = read(rb.configDir + \"karate-config.js\");\n            if (env != null) {\n                karateConfigEnv = read(rb.configDir + \"karate-config-\" + env + \".js\");\n            } else {\n                karateConfigEnv = null;\n            }\n            drivers = rb.drivers;\n            threadCount = rb.threadCount;\n            timeoutMinutes = rb.timeoutMinutes;\n            parallel = threadCount > 1;\n            if (parallel) {\n                scenarioExecutor = Executors.newFixedThreadPool(threadCount);\n                pendingTasks = Executors.newSingleThreadExecutor();\n            } else {\n                scenarioExecutor = SyncExecutorService.INSTANCE;\n                pendingTasks = SyncExecutorService.INSTANCE;\n            }\n        }\n    }\n\n    @Override\n    public void run() {\n        try {\n            if (backupReportDir) {\n                backupReportDirIfExists();\n            }\n            hooks.forEach(h -> h.beforeSuite(this));\n            int index = 0;\n            List<FeatureRuntime> featureRuntimes = new ArrayList<>(featuresFound);\n            for (FeatureCall feature : features) {\n                final int featureNum = ++index;\n                FeatureRuntime fr = FeatureRuntime.of(this, feature);\n                final CompletableFuture future = new CompletableFuture();\n                futures.add(future);\n                fr.setNext(() -> {\n                    onFeatureDone(fr.result, featureNum);\n                    future.complete(Boolean.TRUE);\n                });\n                featureRuntimes.add(fr);\n            }\n            if (featuresFound > 1) {\n                logger.debug(\"waiting for {} features to complete\", featuresFound);\n            }\n            featureRuntimes.forEach(pendingTasks::submit);\n            CompletableFuture[] futuresArray = futures.toArray(new CompletableFuture[futures.size()]);\n            if (timeoutMinutes > 0) {\n                CompletableFuture.allOf(futuresArray).get(timeoutMinutes, TimeUnit.MINUTES);\n            } else {\n                CompletableFuture.allOf(futuresArray).join();\n            }\n            endTime = System.currentTimeMillis();\n        } catch (Throwable t) {\n            logger.error(\"runner failed: \" + t);\n        } finally {\n            scenarioExecutor.shutdownNow();\n            pendingTasks.shutdownNow();\n            hooks.forEach(h -> h.afterSuite(this));\n        }\n    }\n\n    public void abort() {\n        abort.set(true);\n    }\n\n    public boolean isAborted() {\n        return abort.get();\n    }\n\n    public void saveFeatureResults(FeatureResult fr) {\n        File file = ReportUtils.saveKarateJson(reportDir, fr, null);\n        synchronized (featureResultFiles) {\n            featureResultFiles.add(file);\n        }\n        if (outputHtmlReport) {\n            suiteReports.featureReport(this, fr).render();\n        }\n        if (outputCucumberJson) {\n            ReportUtils.saveCucumberJson(reportDir, fr, null);\n        }\n        if (outputJunitXml) {\n            ReportUtils.saveJunitXml(reportDir, fr, null);\n        }\n        fr.printStats();\n    }\n\n    private void onFeatureDone(FeatureResult fr, int index) {\n        if (fr.getScenarioCount() > 0) { // possible that zero scenarios matched tags\n            try { // edge case that reports are not writable     \n                saveFeatureResults(fr);\n                String status = fr.isFailed() ? \"fail\" : \"pass\";\n                logger.info(\"<<{}>> feature {} of {} ({} remaining) {}\", status, index, featuresFound, getFeaturesRemaining() - 1, fr.getFeature());\n            } catch (Throwable t) {\n                logger.error(\"<<error>> unable to write report file(s): {} - {}\", fr.getFeature(), t + \"\");\n                fr.printStats();\n                return; // don't attempt to save progress that might fail as well\n            }\n        } else {\n            skippedCount++;\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"<<skip>> feature {} of {}: {}\", index, featuresFound, fr.getFeature());\n            }\n        }\n    }\n\n    static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();\n    static final TypeReference<Map<String, Object>> TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {\n        // empty\n    };\n\n    private static Map toKarateJson(File file) {\n        String text = FileUtils.toString(file);\n        try {\n            return OBJECT_MAPPER.readValue(text, TYPE_REFERENCE);\n        } catch (Exception e) {\n            logger.warn(\"failed to convert json, will re-try: {}\", e.getMessage());\n            return (Map) JsonUtils.fromJson(text);\n        }\n    }\n\n    public Stream<FeatureResult> getFeatureResults() {\n        return featureResultFiles.stream()\n                .sorted()\n                .map(file -> FeatureResult.fromKarateJson(workingDir, toKarateJson(file)));\n    }\n\n    public Stream<ScenarioResult> getScenarioResults() {\n        return getFeatureResults().flatMap(fr -> fr.getScenarioResults().stream());\n    }\n\n    public ScenarioResult retryScenario(Scenario scenario) {\n        // remove any \"fake\" steps that might have been added before retrying\n        scenario.setSteps(scenario.getSteps().stream().filter(not(Step::isFake)).collect(Collectors.toList()));\n        FeatureRuntime fr = FeatureRuntime.of(this, new FeatureCall(scenario.getFeature()));\n        ScenarioRuntime runtime = new ScenarioRuntime(fr, scenario);\n        runtime.run();\n        return runtime.result;\n    }\n\n    public Results updateResults(ScenarioResult sr) {\n        Scenario scenario = sr.getScenario();\n        FeatureResult fr;\n        File file = new File(reportDir + File.separator + scenario.getFeature().getKarateJsonFileName());\n        if (file.exists()) {\n            String json = FileUtils.toString(file);\n            fr = FeatureResult.fromKarateJson(workingDir, Json.of(json).asMap());\n        } else {\n            fr = new FeatureResult(scenario.getFeature());\n        }\n        List<ScenarioResult> scenarioResults = fr.getScenarioResults();\n        int count = scenarioResults.size();\n        int found = -1;\n        for (int i = 0; i < count; i++) {\n            ScenarioResult temp = scenarioResults.get(i);\n            if (temp.getScenario().isEqualTo(scenario)) {\n                found = i;\n                break;\n            }\n        }\n        if (found != -1) {\n            scenarioResults.set(found, sr);\n        } else {\n            scenarioResults.add(sr);\n        }\n        fr.sortScenarioResults();\n        saveFeatureResults(fr);\n        return buildResults();\n    }\n\n    private void backupReportDirIfExists() {\n        File file = new File(reportDir);\n        if (file.exists()) {\n            File dest = new File(reportDir + \"_\" + System.currentTimeMillis());\n            if (file.renameTo(dest)) {\n                logger.info(\"backed up existing '{}' dir to: {}\", reportDir, dest);\n            } else {\n                logger.warn(\"failed to backup existing dir: {}\", file);\n            }\n        }\n    }\n\n    public long getFeaturesRemaining() {\n        return futures.stream().filter(f -> !f.isDone()).count();\n    }\n\n    public Results buildResults() {\n        return Results.of(this);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/XmlUtils.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate;\n\nimport com.jayway.jsonpath.DocumentContext;\nimport com.jayway.jsonpath.JsonPath;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.StringReader;\nimport java.io.StringWriter;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\nimport javax.xml.transform.OutputKeys;\nimport javax.xml.transform.Transformer;\nimport javax.xml.transform.TransformerFactory;\nimport javax.xml.transform.dom.DOMSource;\nimport javax.xml.transform.stream.StreamResult;\nimport javax.xml.xpath.XPath;\nimport javax.xml.xpath.XPathConstants;\nimport javax.xml.xpath.XPathExpression;\nimport javax.xml.xpath.XPathExpressionException;\nimport javax.xml.xpath.XPathFactory;\n\nimport org.w3c.dom.Attr;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.NamedNodeMap;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\nimport org.xml.sax.EntityResolver;\nimport org.xml.sax.InputSource;\nimport org.xml.sax.SAXException;\n\n/**\n * @author pthomas3\n */\npublic class XmlUtils {\n\n    private XmlUtils() {\n        // only static methods\n    }\n\n    public static String toString(Node node) {\n        return toString(node, false);\n    }\n\n    public static String toString(Node node, boolean pretty) {\n        DOMSource domSource = new DOMSource(node);\n        StringWriter writer = new StringWriter();\n        StreamResult result = new StreamResult(writer);\n        TransformerFactory tf = TransformerFactory.newInstance();\n        try {\n            Transformer transformer = tf.newTransformer();\n            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, \"yes\");\n            if (pretty) {\n                transformer.setOutputProperty(OutputKeys.INDENT, \"yes\");\n                transformer.setOutputProperty(\"{http://xml.apache.org/xslt}indent-amount\", \"2\");\n            } else {\n                transformer.setOutputProperty(OutputKeys.INDENT, \"no\");\n            }\n            transformer.transform(domSource, result);\n            return writer.toString();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static class DtdEntityResolver implements EntityResolver {\n\n        protected boolean dtdPresent;\n\n        @Override\n        public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {\n            dtdPresent = true;\n            return new InputSource(new StringReader(\"\"));\n        }\n\n    }\n    \n    public static Document toXmlDoc(String xml) {\n        return toXmlDoc(xml, false);\n    }\n\n    public static Document toXmlDoc(String xml, boolean namespaceAware) {\n        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n        factory.setNamespaceAware(namespaceAware);\n        factory.setIgnoringElementContentWhitespace(false);\n        try {\n            DocumentBuilder builder = factory.newDocumentBuilder();\n            DtdEntityResolver dtdEntityResolver = new DtdEntityResolver();\n            builder.setEntityResolver(dtdEntityResolver);\n            InputStream is = FileUtils.toInputStream(xml);\n            Document doc = builder.parse(is);\n            if (dtdEntityResolver.dtdPresent) { // DOCTYPE present\n                // the XML was not parsed, but I think it hangs at the root as a text node\n                // so conversion to string and back has the effect of discarding the DOCTYPE !\n                return toXmlDoc(toString(doc, false), namespaceAware);\n            } else {\n                return doc;\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static XPathExpression compile(String path) {\n        XPathFactory xPathfactory = XPathFactory.newInstance();\n        XPath xpath = xPathfactory.newXPath();\n        try {\n            return xpath.compile(path);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static NodeList getNodeListByPath(Node node, String path) {\n        XPathExpression expr = compile(path);\n        try {\n            return (NodeList) expr.evaluate(node, XPathConstants.NODESET);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static String stripNameSpacePrefixes(String path) {\n        if (path.indexOf(':') == -1) {\n            return path;\n        }\n        StringBuilder sb = new StringBuilder();\n        for (String s : StringUtils.split(path, '/', false)) {\n            sb.append('/');\n            int pos = s.lastIndexOf(':');\n            if (pos == -1) {\n                sb.append(s);\n            } else {\n                sb.append(s.substring(pos + 1));\n            }\n        }\n        return sb.toString();\n    }\n\n    public static Node getNodeByPath(Node node, String path, boolean create) {\n        String searchPath = create ? stripNameSpacePrefixes(path) : path;\n        XPathExpression expr = compile(searchPath);\n        Node result;\n        try {\n            result = (Node) expr.evaluate(node, XPathConstants.NODE);\n        } catch (XPathExpressionException e) {\n            throw new RuntimeException(e);\n        }\n        if (result == null && create) {\n            Document doc = node.getNodeType() == Node.DOCUMENT_NODE ? (Document) node : node.getOwnerDocument();\n            return createNodeByPath(doc, path);\n        } else {\n            return result;\n        }\n    }\n\n    public static Node createNodeByPath(Document doc, String path) {\n        int pos = path.lastIndexOf('/');\n        if (pos == 0) { // root\n            Node root = doc.getDocumentElement();\n            if (root == null) {\n                root = createElement(doc, path.substring(1), null, null);\n                doc.appendChild(root);\n            }\n            return root;\n        }\n        String left = path.substring(0, pos);\n        Node parent = getNodeByPath(doc, left, true);\n        String right = path.substring(pos + 1);\n        if (right.startsWith(\"@\")) { // attribute\n            Element parentElement = (Element) parent;\n            right = right.substring(1);\n            parentElement.setAttribute(right, \"\");\n            return parentElement.getAttributeNode(right);\n        } else {\n            int bracketPos = right.indexOf('[');\n            if (bracketPos != -1) { // index, we assume it is 1 and still append\n                right = right.substring(0, bracketPos);\n            }\n            Element element = createElement(parent, right, null, null);\n            parent.appendChild(element);\n            return element;\n        }\n    }\n\n    public static String getTextValueByPath(Node node, String path) {\n        XPathExpression expr = compile(path);\n        try {\n            return expr.evaluate(node);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void setByPath(Node doc, String path, String value) {\n        Node node = getNodeByPath(doc, path, true);\n        if (node.getNodeType() == Node.ATTRIBUTE_NODE) {\n            node.setNodeValue(value);\n        } else if (node.hasChildNodes() && node.getFirstChild().getNodeType() == Node.TEXT_NODE) {\n            node.getFirstChild().setTextContent(value);\n        } else if (node.getNodeType() == Node.ELEMENT_NODE) {\n            node.setTextContent(value);\n        }\n    }\n\n    public static void removeByPath(Document doc, String path) {\n        Node node = getNodeByPath(doc, path, false);\n        if (node == null) {\n            return;\n        }\n        if (node.getNodeType() == Node.ATTRIBUTE_NODE) {\n            Element parent = ((Attr) node).getOwnerElement();\n            parent.removeAttribute(node.getNodeName());\n        } else {\n            Node parent = node.getParentNode();\n            if (parent != null) {\n                parent.removeChild(node);\n            }\n        }\n    }\n\n    public static void setByPath(Document doc, String path, Node in) {\n        if (in.getNodeType() == Node.DOCUMENT_NODE) {\n            in = in.getFirstChild();\n        }\n        Node node = getNodeByPath(doc, path, true);\n        if (node == null) {\n            throw new RuntimeException(\"no results for xpath: \" + path);\n        }\n        Node newNode = doc.importNode(in, true);\n        if (node.hasChildNodes() && node.getFirstChild().getNodeType() == Node.TEXT_NODE) {\n            node.replaceChild(newNode, node.getFirstChild());\n        } else {\n            node.appendChild(newNode);\n        }\n    }\n\n    public static DocumentContext toJsonDoc(Node node) {\n        return JsonPath.parse(toObject(node));\n    }\n\n    private static Map<String, Object> getAttributes(Node node) {\n        NamedNodeMap attribs = node.getAttributes();\n        int attribCount = attribs.getLength();\n        Map<String, Object> map = new LinkedHashMap<>(attribCount);\n        for (int j = 0; j < attribCount; j++) {\n            Node attrib = attribs.item(j);\n            map.put(attrib.getNodeName(), attrib.getNodeValue());\n        }\n        return map;\n    }\n\n    public static int getChildElementCount(Node node) {\n        NodeList nodes = node.getChildNodes();\n        int childCount = nodes.getLength();\n        int childElementCount = 0;\n        for (int i = 0; i < childCount; i++) {\n            Node child = nodes.item(i);\n            if (child.getNodeType() == Node.ELEMENT_NODE) {\n                childElementCount++;\n            }\n        }\n        return childElementCount;\n    }\n\n    private static Object getElementAsObject(Node node, boolean removeNamespace) {\n        int childElementCount = getChildElementCount(node);\n        if (childElementCount == 0) {\n            String textContent = node.getTextContent();\n            return StringUtils.isBlank(textContent) ? null:\n                textContent;\n        }\n        Map<String, Object> map = new LinkedHashMap<>(childElementCount);\n        NodeList nodes = node.getChildNodes();\n        int childCount = nodes.getLength();\n        for (int i = 0; i < childCount; i++) {\n            Node child = nodes.item(i);\n            if (child.getNodeType() != Node.ELEMENT_NODE) {\n                continue;\n            }\n            String childName = removeNamespace\n                    ? child.getNodeName().replaceFirst(\"(^.*:)\", \"\") : child.getNodeName();\n            Object childValue = toObject(child, removeNamespace);\n            // auto detect repeating elements\n            if (map.containsKey(childName)) {\n                Object temp = map.get(childName);\n                if (temp instanceof List) {\n                    List list = (List) temp;\n                    list.add(childValue);\n                } else {\n                    List list = new ArrayList(childCount);\n                    map.put(childName, list);\n                    list.add(temp);\n                    list.add(childValue);\n                }\n            } else {\n                map.put(childName, childValue);\n            }\n        }\n        return map;\n    }\n\n    public static Object toObject(Node node) {\n        return toObject(node, false);\n    }\n\n    public static Object toObject(Node node, boolean removeNamespace) {\n        if (node.getNodeType() == Node.DOCUMENT_NODE) {\n            Map<String, Object> map = new LinkedHashMap<>(1);\n            node = node.getFirstChild();\n            if (node == null) {\n                return map;\n            }\n            while (node.getNodeType() != Node.ELEMENT_NODE) { // ignore comments etc\n                node = node.getNextSibling();\n            }\n            String name = removeNamespace\n                    ? node.getNodeName().replaceFirst(\"(^.*:)\", \"\") : node.getNodeName();\n            map.put(name, toObject(node, removeNamespace));\n            return map;\n        }\n        Object value = getElementAsObject(node, removeNamespace);\n        if (node.hasAttributes()) {\n            Map<String, Object> attribs = getAttributes(node);\n            if (removeNamespace) {\n                attribs.keySet().removeIf(key -> \"xmlns\".equals(key) || key.startsWith(\"xmlns:\"));\n            }\n            if (attribs.size() > 0) {\n                Map<String, Object> wrapper = new LinkedHashMap<>(2);\n                wrapper.put(\"_\", value);\n                wrapper.put(\"@\", attribs);\n                return wrapper;\n            } else {\n                //namespaces were the only attributes\n                return value;\n            }\n        } else {\n            return value;\n        }\n    }\n\n    public static Document fromMap(Map<String, Object> map) {\n        Map.Entry<String, Object> first = map.entrySet().iterator().next();\n        return fromObject(first.getKey(), first.getValue());\n    }\n\n    public static Document fromObject(String name, Object o) {\n        Document doc = newDocument();\n        List<Element> list = fromObject(doc, name, o);\n        Element root = list.get(0);\n        doc.appendChild(root);\n        return doc;\n    }\n\n    public static List<Element> fromObject(Document doc, String name, Object o) {\n        if (o instanceof Map) {\n            Map<String, Object> map = (Map) o;\n            Map<String, Object> attribs = (Map) map.get(\"@\");\n            Object value = map.get(\"_\");\n            if (value != null || attribs != null) {\n                List<Element> elements = fromObject(doc, name, value);\n                addAttributes(elements.get(0), attribs);\n                return elements;\n            } else {\n                Element element = createElement(doc, name, null, null);\n                for (Map.Entry<String, Object> entry : map.entrySet()) {\n                    String childName = entry.getKey();\n                    Object childValue = entry.getValue();\n                    List<Element> childNodes = fromObject(doc, childName, childValue);\n                    for (Element e : childNodes) {\n                        element.appendChild(e);\n                    }\n                }\n                return Collections.singletonList(element);\n            }\n        } else if (o instanceof List) {\n            List list = (List) o;\n            List<Element> elements = new ArrayList(list.size());\n            for (Object child : list) {\n                List<Element> childNodes = fromObject(doc, name, child);\n                for (Element e : childNodes) {\n                    elements.add(e);\n                }\n            }\n            return elements;\n        } else {\n            String value = o == null ? null : o.toString();\n            Element element = createElement(doc, name, value, null);\n            return Collections.singletonList(element);\n        }\n    }\n\n    public static Document newDocument() {\n        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n        DocumentBuilder builder;\n        try {\n            builder = factory.newDocumentBuilder();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        return builder.newDocument();\n    }\n\n    public static void addAttributes(Element element, Map<String, Object> map) {\n        if (map != null) {\n            for (Map.Entry<String, Object> entry : map.entrySet()) {\n                Object attrValue = entry.getValue();\n                element.setAttribute(entry.getKey(), attrValue == null ? null : attrValue.toString());\n            }\n        }\n    }\n\n    public static Element createElement(Node node, String name, String value, Map<String, Object> attributes) {\n        Document doc = node.getNodeType() == Node.DOCUMENT_NODE ? (Document) node : node.getOwnerDocument();\n        Element element = doc.createElement(name);\n        element.setTextContent(value);\n        addAttributes(element, attributes);\n        return element;\n    }\n\n    public static Document toNewDocument(Node in) {\n        Document doc = newDocument();\n        Node node = doc.importNode(in, true);\n        doc.appendChild(node);\n        return doc;\n    }\n\n    public static Document fromJavaObject(Object o) {\n        return fromObject(\"root\", Json.of(o).value()); // keep it simple for people to write generic xpath starting with /root\n    }\n\n    public static String toXml(Object o) {\n        return toString(fromJavaObject(o));\n    }\n\n    public static boolean isXml(String s) {\n        if (s == null || s.isEmpty()) {\n            return false;\n        }\n        if (s.charAt(0) == ' ') {\n            s = s.trim();\n            if (s.isEmpty()) {\n                return false;\n            }\n        }\n        return s.charAt(0) == '<';\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Action.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n *\n * @author pthomas3\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.METHOD)\npublic @interface Action {\n\n    String value();\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/AfterHookType.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\n/**\n *\n * @author OwenK2\n */\npublic enum AfterHookType {\n\n    AFTER_SCENARIO(\"afterScenario\"),\n    AFTER_OUTLINE(\"afterScenarioOutline\"),\n    AFTER_FEATURE(\"afterFeature\");\n\n    private String prefix;\n\n    private AfterHookType(String prefix) {\n        this.prefix = prefix;\n    }\n\n    public String getPrefix() {\n        return prefix;\n    }\n}"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/AssignType.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\n/**\n *\n * @author pthomas3\n */\npublic enum AssignType {\n    \n    AUTO,\n    COPY,\n    TEXT,\n    CSV,\n    YAML,\n    JSON,\n    STRING,\n    XML,\n    XML_STRING,\n    BYTE_ARRAY\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/AutoDef.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n *\n * @author pthomas3\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.METHOD)\npublic @interface AutoDef {\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Background.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class Background {\n\n    public static final String TYPE = \"background\";\n    public static final String KEYWORD = \"Background\";\n\n    private int line;\n    private List<Step> steps;\n\n    public int getLine() {\n        return line;\n    }\n\n    public void setLine(int line) {\n        this.line = line;\n    }\n\n    public List<Step> getSteps() {\n        return steps;\n    }\n\n    public void setSteps(List<Step> steps) {\n        this.steps = steps;\n    }\n\n    @Override\n    public String toString() {\n        return steps == null ? null : steps.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Channel.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\n/**\n * @author peter\n */\npublic interface Channel {\n\n    Object init(ScenarioRuntime runtime);\n\n    void afterScenario();\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ChannelFactory.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic interface ChannelFactory {\n    \n    Channel create(ScenarioRuntime runtime, Map<String, Object> options);\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Config.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.driver.DockerTarget;\nimport com.intuit.karate.driver.Target;\nimport com.intuit.karate.http.Cookies;\nimport com.intuit.karate.http.HttpLogModifier;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.lang.reflect.Method;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * @author pthomas3\n */\npublic class Config {\n\n    private static final Logger logger = LoggerFactory.getLogger(Config.class);\n\n    public static final int DEFAULT_RETRY_INTERVAL = 3000;\n    public static final int DEFAULT_RETRY_COUNT = 3;\n    public static final int DEFAULT_TIMEOUT = 30000;\n    public static final int DEFAULT_HIGHLIGHT_DURATION = 3000;\n    \n    public static final String DRIVER = \"driver\";\n    public static final String ROBOT = \"robot\";\n    public static final String KAFKA = \"kafka\";\n    public static final String GRPC = \"grpc\";\n    public static final String WEBSOCKET = \"websocket\";\n    public static final String WEBHOOK = \"webhook\";\n\n    private String url;\n    private boolean sslEnabled = false;\n    private String sslAlgorithm = \"TLS\";\n    private String sslKeyStore;\n    private String sslKeyStorePassword;\n    private String sslKeyStoreType;\n    private String sslTrustStore;\n    private String sslTrustStorePassword;\n    private String sslTrustStoreType;\n    private boolean sslTrustAll = true;\n    private boolean followRedirects = true;\n    private int readTimeout = DEFAULT_TIMEOUT;\n    private int connectTimeout = DEFAULT_TIMEOUT;\n    private Charset charset = StandardCharsets.UTF_8;\n    private String proxyUri;\n    private String proxyUsername;\n    private String proxyPassword;\n    private List<String> nonProxyHosts;\n    private String localAddress;\n    private int responseDelay;\n    private boolean xmlNamespaceAware = false;\n    private boolean lowerCaseResponseHeaders = false;\n    private boolean corsEnabled = false;\n    private boolean logPrettyRequest;\n    private boolean logPrettyResponse;\n    private boolean printEnabled = true;\n    private boolean pauseIfNotPerf = false;\n    private boolean abortedStepsShouldPass = false;\n    private boolean matchEachEmptyAllowed = false;\n    private Target driverTarget;\n    private Map<String, Map<String, Object>> customOptions = new HashMap();\n    private HttpLogModifier logModifier;\n\n    private Variable afterScenario = Variable.NULL;\n    private Variable afterScenarioOutline = Variable.NULL;\n    private Variable afterFeature = Variable.NULL;\n    private Variable headers = Variable.NULL;\n    private Variable cookies = Variable.NULL;\n    private Variable responseHeaders = Variable.NULL;\n    private List<Method> continueOnStepFailureMethods = new ArrayList<>();\n    private boolean continueAfterContinueOnStepFailure;\n    private boolean abortSuiteOnFailure;\n\n    // retry config\n    private int retryInterval = DEFAULT_RETRY_INTERVAL;\n    private int retryCount = DEFAULT_RETRY_COUNT;\n\n    // report config\n    private boolean showLog = true;\n    private boolean showAllSteps = true;\n\n    // call single cache config\n    private int callSingleCacheMinutes = 0;\n    private String callSingleCacheDir = FileUtils.getBuildDir();\n\n    // image comparison config\n    private Map<String, Object> imageComparisonOptions;\n\n    // ntlm authentication\n    private boolean ntlmEnabled = false;\n    private boolean httpRetryEnabled = false;\n    private String ntlmUsername;\n    private String ntlmPassword;\n    private String ntlmDomain;\n    private String ntlmWorkstation;\n\n    public Config() {\n        // zero arg constructor\n    }\n\n    private static <T> T get(Map<String, Object> map, String key, T defaultValue) {\n        Object o = map.get(key);\n        return o == null ? defaultValue : (T) o;\n    }\n\n    public boolean configure(String key, Variable value) { // TODO use enum\n        key = StringUtils.trimToEmpty(key);\n        switch (key) {\n            case \"url\":\n                url = value.getAsString();\n                return false;\n            case \"headers\":\n                headers = value;\n                return false;\n            case \"cookies\":\n                if (!value.isNull()) {\n                    value = new Variable(Cookies.normalize(value.getValue()));\n                }\n                cookies = value;\n                return false;\n            case \"responseHeaders\":\n                responseHeaders = value;\n                return false;\n            case \"responseDelay\":\n                responseDelay = value.isNull() ? 0 : value.getAsInt();\n                return false;\n            case \"xmlNamespaceAware\":\n                xmlNamespaceAware = value.isTrue();\n                return false;\n            case \"lowerCaseResponseHeaders\":\n                lowerCaseResponseHeaders = value.isTrue();\n                return false;\n            case \"cors\":\n                corsEnabled = value.isTrue();\n                return false;\n            case \"logPrettyResponse\":\n                logPrettyResponse = value.isTrue();\n                return false;\n            case \"logPrettyRequest\":\n                logPrettyRequest = value.isTrue();\n                return false;\n            case \"printEnabled\":\n                printEnabled = value.isTrue();\n                return false;\n            case \"afterScenario\":\n                afterScenario = value;\n                return false;\n            case \"afterScenarioOutline\":\n                afterScenarioOutline = value;\n                return false;\n            case \"afterFeature\":\n                afterFeature = value;\n                return false;\n            case \"report\":\n                if (value.isMap()) {\n                    Map<String, Object> map = value.getValue();\n                    showLog = get(map, \"showLog\", showLog);\n                    showAllSteps = get(map, \"showAllSteps\", showAllSteps);\n                } else if (value.isTrue()) {\n                    showLog = true;\n                    showAllSteps = true;\n                } else {\n                    showLog = false;\n                    showAllSteps = false;\n                }\n                return false;\n            case DRIVER:\n            case ROBOT:\n            case KAFKA:\n            case GRPC:\n            case WEBSOCKET:\n            case WEBHOOK:\n                customOptions.put(key, value.getValue());\n                return false;\n            case \"driverTarget\":\n                if (value.isMap()) {\n                    Map<String, Object> map = value.getValue();\n                    if (map.containsKey(\"docker\")) {\n                        // todo add the working dir here\n                        driverTarget = new DockerTarget(map);\n                    } else {\n                        throw new RuntimeException(\"bad driverTarget config, expected key 'docker': \" + map);\n                    }\n                } else {\n                    driverTarget = value.getValue();\n                }\n                return false;\n            case \"retry\":\n                if (value.isMap()) {\n                    Map<String, Object> map = value.getValue();\n                    retryInterval = get(map, \"interval\", retryInterval);\n                    retryCount = get(map, \"count\", retryCount);\n                }\n                return false;\n            case \"pauseIfNotPerf\":\n                pauseIfNotPerf = value.isTrue();\n                return false;\n            case \"abortedStepsShouldPass\":\n                abortedStepsShouldPass = value.isTrue();\n                return false;\n            case \"abortSuiteOnFailure\":\n                abortSuiteOnFailure = value.isTrue();\n                return false;\n            case \"callSingleCache\":\n                if (value.isMap()) {\n                    Map<String, Object> map = value.getValue();\n                    callSingleCacheMinutes = get(map, \"minutes\", callSingleCacheMinutes);\n                    callSingleCacheDir = get(map, \"dir\", callSingleCacheDir);\n                }\n                return false;\n            case \"logModifier\":\n                logModifier = value.getValue();\n                return false;\n            case \"imageComparison\":\n                imageComparisonOptions = value.getValue();\n                return false;\n            case \"matchEachEmptyAllowed\":\n                matchEachEmptyAllowed = value.getValue();\n                return false;\n            case \"continueOnStepFailure\":\n                continueOnStepFailureMethods.clear(); // clears previous configuration - in case someone is trying to chain these and forgets resetting the previous one\n                boolean enableContinueOnStepFailureFeature = false;\n                Boolean continueAfterIgnoredFailure = null;\n                List<String> stepKeywords = null;\n                if (value.isMap()) {\n                    Map<String, Object> map = value.getValue();\n                    stepKeywords = (List<String>) map.get(\"keywords\");\n                    continueAfterIgnoredFailure = (Boolean) map.get(\"continueAfter\");\n                    enableContinueOnStepFailureFeature = map.get(\"enabled\") != null && (Boolean) map.get(\"enabled\");\n                }\n                if (value.isTrue() || enableContinueOnStepFailureFeature) {\n                    continueOnStepFailureMethods.addAll(stepKeywords == null ? StepRuntime.METHOD_MATCH : StepRuntime.findMethodsByKeywords(stepKeywords));\n                } else {\n                    if (stepKeywords == null) {\n                        continueOnStepFailureMethods.clear();\n                    } else {\n                        continueOnStepFailureMethods.removeAll(StepRuntime.findMethodsByKeywords(stepKeywords));\n                    }\n                }\n                if (continueAfterIgnoredFailure != null) {\n                    continueAfterContinueOnStepFailure = continueAfterIgnoredFailure;\n                }\n                return false;\n            // here on the http client has to be re-constructed ================\n            // and we return true instead of false\n            case \"charset\":\n                charset = value.isNull() ? null : Charset.forName(value.getAsString());\n                return true;\n            case \"ssl\":\n                if (value.isString()) {\n                    sslEnabled = true;\n                    sslAlgorithm = value.getAsString();\n                } else if (value.isMap()) {\n                    sslEnabled = true;\n                    Map<String, Object> map = value.getValue();\n                    sslKeyStore = (String) map.get(\"keyStore\");\n                    sslKeyStorePassword = (String) map.get(\"keyStorePassword\");\n                    sslKeyStoreType = (String) map.get(\"keyStoreType\");\n                    sslTrustStore = (String) map.get(\"trustStore\");\n                    sslTrustStorePassword = (String) map.get(\"trustStorePassword\");\n                    sslTrustStoreType = (String) map.get(\"trustStoreType\");\n                    Boolean trustAll = (Boolean) map.get(\"trustAll\");\n                    if (trustAll != null) {\n                        sslTrustAll = trustAll;\n                    }\n                    sslAlgorithm = (String) map.get(\"algorithm\");\n                } else {\n                    sslEnabled = value.isTrue();\n                }\n                return true;\n            case \"followRedirects\":\n                followRedirects = value.isTrue();\n                return true;\n            case \"connectTimeout\":\n                connectTimeout = value.getAsInt();\n                return true;\n            case \"readTimeout\":\n                readTimeout = value.getAsInt();\n                return true;\n            case \"proxy\":\n                if (value.isNull()) {\n                    proxyUri = null;\n                } else if (value.isString()) {\n                    proxyUri = value.getAsString();\n                } else {\n                    Map<String, Object> map = value.getValue();\n                    proxyUri = (String) map.get(\"uri\");\n                    proxyUsername = (String) map.get(\"username\");\n                    proxyPassword = (String) map.get(\"password\");\n                    nonProxyHosts = (List) map.get(\"nonProxyHosts\");\n                }\n                return true;\n            case \"httpRetryEnabled\":\n                httpRetryEnabled = value.isTrue();\n                return true;\n            case \"localAddress\":\n                localAddress = value.getAsString();\n                return true;\n            case \"ntlmAuth\":\n                if (value.isNull()) {\n                    ntlmEnabled = false;\n                } else {\n                    Map<String, Object> map = value.getValue();\n                    ntlmEnabled = true;\n                    ntlmUsername = (String) map.get(\"username\");\n                    ntlmPassword = (String) map.get(\"password\");\n                    ntlmDomain = (String) map.get(\"domain\");\n                    ntlmWorkstation = (String) map.get(\"workstation\");\n                }\n                return true;\n            default:\n                throw new RuntimeException(\"unexpected 'configure' key: '\" + key + \"'\");\n        }\n    }\n\n    public Config(Config parent) {\n        url = parent.url;\n        sslEnabled = parent.sslEnabled;\n        sslAlgorithm = parent.sslAlgorithm;\n        sslTrustStore = parent.sslTrustStore;\n        sslTrustStorePassword = parent.sslTrustStorePassword;\n        sslTrustStoreType = parent.sslTrustStoreType;\n        sslKeyStore = parent.sslKeyStore;\n        sslKeyStorePassword = parent.sslKeyStorePassword;\n        sslKeyStoreType = parent.sslKeyStoreType;\n        sslTrustAll = parent.sslTrustAll;\n        followRedirects = parent.followRedirects;\n        readTimeout = parent.readTimeout;\n        connectTimeout = parent.connectTimeout;\n        charset = parent.charset;\n        proxyUri = parent.proxyUri;\n        proxyUsername = parent.proxyUsername;\n        proxyPassword = parent.proxyPassword;\n        nonProxyHosts = parent.nonProxyHosts;\n        localAddress = parent.localAddress;\n        responseDelay = parent.responseDelay;\n        xmlNamespaceAware = parent.xmlNamespaceAware;\n        lowerCaseResponseHeaders = parent.lowerCaseResponseHeaders;\n        corsEnabled = parent.corsEnabled;\n        logPrettyRequest = parent.logPrettyRequest;\n        logPrettyResponse = parent.logPrettyResponse;\n        printEnabled = parent.printEnabled;\n        driverTarget = parent.driverTarget;\n        customOptions = parent.customOptions;\n        showLog = parent.showLog;\n        showAllSteps = parent.showAllSteps;\n        retryInterval = parent.retryInterval;\n        retryCount = parent.retryCount;\n        pauseIfNotPerf = parent.pauseIfNotPerf;\n        abortedStepsShouldPass = parent.abortedStepsShouldPass;\n        logModifier = parent.logModifier;\n        callSingleCacheMinutes = parent.callSingleCacheMinutes;\n        callSingleCacheDir = parent.callSingleCacheDir;\n        headers = parent.headers;\n        cookies = parent.cookies;\n        responseHeaders = parent.responseHeaders;\n        afterScenario = parent.afterScenario;\n        afterScenarioOutline = parent.afterScenarioOutline;\n        afterFeature = parent.afterFeature;\n        continueOnStepFailureMethods = parent.continueOnStepFailureMethods;\n        continueAfterContinueOnStepFailure = parent.continueAfterContinueOnStepFailure;\n        abortSuiteOnFailure = parent.abortSuiteOnFailure;\n        imageComparisonOptions = parent.imageComparisonOptions;\n        matchEachEmptyAllowed = parent.matchEachEmptyAllowed;\n        ntlmEnabled = parent.ntlmEnabled;\n        httpRetryEnabled = parent.httpRetryEnabled;\n        ntlmUsername = parent.ntlmUsername;\n        ntlmPassword = parent.ntlmPassword;\n        ntlmDomain = parent.ntlmDomain;\n        ntlmWorkstation = parent.ntlmWorkstation;\n    }\n\n    public void setUrl(String url) {\n        this.url = url;\n    }\n\n    public String getUrl() {\n        return url;\n    }        \n\n    public void setCookies(Variable cookies) {\n        this.cookies = cookies;\n    }\n\n    public boolean isSslEnabled() {\n        return sslEnabled;\n    }\n\n    public String getSslAlgorithm() {\n        return sslAlgorithm;\n    }\n\n    public String getSslKeyStore() {\n        return sslKeyStore;\n    }\n\n    public String getSslKeyStorePassword() {\n        return sslKeyStorePassword;\n    }\n\n    public String getSslKeyStoreType() {\n        return sslKeyStoreType;\n    }\n\n    public String getSslTrustStore() {\n        return sslTrustStore;\n    }\n\n    public String getSslTrustStorePassword() {\n        return sslTrustStorePassword;\n    }\n\n    public String getSslTrustStoreType() {\n        return sslTrustStoreType;\n    }\n\n    public boolean isSslTrustAll() {\n        return sslTrustAll;\n    }\n\n    public boolean isFollowRedirects() {\n        return followRedirects;\n    }\n\n    public int getReadTimeout() {\n        return readTimeout;\n    }\n\n    public int getConnectTimeout() {\n        return connectTimeout;\n    }\n\n    public Charset getCharset() {\n        return charset;\n    }\n\n    public String getProxyUri() {\n        return proxyUri;\n    }\n\n    public String getProxyUsername() {\n        return proxyUsername;\n    }\n\n    public String getProxyPassword() {\n        return proxyPassword;\n    }\n\n    public List<String> getNonProxyHosts() {\n        return nonProxyHosts;\n    }\n\n    public String getLocalAddress() {\n        return localAddress;\n    }\n\n    public Variable getHeaders() {\n        return headers;\n    }\n\n    public Variable getCookies() {\n        return cookies;\n    }\n\n    public Variable getResponseHeaders() {\n        return responseHeaders;\n    }\n\n    public int getResponseDelay() {\n        return responseDelay;\n    }\n\n    public boolean isXmlNamespaceAware() {\n        return xmlNamespaceAware;\n    }\n\n    public boolean isLowerCaseResponseHeaders() {\n        return lowerCaseResponseHeaders;\n    }\n\n    public boolean isCorsEnabled() {\n        return corsEnabled;\n    }\n\n    public boolean isLogPrettyRequest() {\n        return logPrettyRequest;\n    }\n\n    public boolean isLogPrettyResponse() {\n        return logPrettyResponse;\n    }\n\n    public boolean isPrintEnabled() {\n        return printEnabled;\n    }\n\n    public boolean isHttpRetryEnabled()\n    {\n        return httpRetryEnabled;\n    }\n\n\n    public Map<String, Map<String, Object>> getCustomOptions() {\n        return customOptions;\n    }    \n\n    public Variable getAfterScenario() {\n        return afterScenario;\n    }\n\n    public void setAfterScenario(Variable afterScenario) {\n        this.afterScenario = afterScenario;\n    }\n\n    public Variable getAfterScenarioOutline() {\n        return afterScenarioOutline;\n    }\n\n    public void setAfterScenarioOutline(Variable afterScenarioOutline) {\n        this.afterScenarioOutline = afterScenarioOutline;\n    }\n\n    public Variable getAfterFeature() {\n        return afterFeature;\n    }\n\n    public void setAfterFeature(Variable afterFeature) {\n        this.afterFeature = afterFeature;\n    }\n\n    public boolean isShowLog() {\n        return showLog;\n    }\n\n    public void setShowLog(boolean showLog) {\n        this.showLog = showLog;\n    }\n\n    public boolean isShowAllSteps() {\n        return showAllSteps;\n    }\n\n    public void setShowAllSteps(boolean showAllSteps) {\n        this.showAllSteps = showAllSteps;\n    }\n\n    public int getRetryInterval() {\n        return retryInterval;\n    }\n\n    public void setRetryInterval(int retryInterval) {\n        this.retryInterval = retryInterval;\n    }\n\n    public int getRetryCount() {\n        return retryCount;\n    }\n\n    public void setRetryCount(int retryCount) {\n        this.retryCount = retryCount;\n    }\n\n    public boolean isPauseIfNotPerf() {\n        return pauseIfNotPerf;\n    }\n\n    public boolean isAbortedStepsShouldPass() {\n        return abortedStepsShouldPass;\n    }\n\n    public Target getDriverTarget() {\n        return driverTarget;\n    }\n\n    public void setDriverTarget(Target driverTarget) {\n        this.driverTarget = driverTarget;\n    }\n\n    public HttpLogModifier getLogModifier() {\n        return logModifier;\n    }\n\n    public String getCallSingleCacheDir() {\n        return callSingleCacheDir;\n    }\n\n    public int getCallSingleCacheMinutes() {\n        return callSingleCacheMinutes;\n    }\n\n    public List<Method> getContinueOnStepFailureMethods() {\n        return continueOnStepFailureMethods;\n    }\n\n    public void setContinueOnStepFailureMethods(List<Method> continueOnStepFailureMethods) {\n        this.continueOnStepFailureMethods = continueOnStepFailureMethods;\n    }\n\n    public boolean isContinueAfterContinueOnStepFailure() {\n        return continueAfterContinueOnStepFailure;\n    }\n\n    public void setContinueAfterContinueOnStepFailure(boolean continueAfterContinueOnStepFailure) {\n        this.continueAfterContinueOnStepFailure = continueAfterContinueOnStepFailure;\n    }\n\n    public void setAbortSuiteOnFailure(boolean abortSuiteOnFailure) {\n        this.abortSuiteOnFailure = abortSuiteOnFailure;\n    }\n\n    public boolean isAbortSuiteOnFailure() {\n        return abortSuiteOnFailure;\n    }\n\n    public Map<String, Object> getImageComparisonOptions() {\n        return imageComparisonOptions;\n    }\n\n    public boolean isMatchEachEmptyAllowed() {\n        return matchEachEmptyAllowed;\n    }        \n\n    public boolean isNtlmEnabled() {\n        return ntlmEnabled;\n    }\n\n    public void setNtlmEnabled(boolean ntlmEnabled) {\n        this.ntlmEnabled = ntlmEnabled;\n    }\n\n    public String getNtlmUsername() {\n        return ntlmUsername;\n    }\n\n    public void setNtlmUsername(String ntlmUsername) {\n        this.ntlmUsername = ntlmUsername;\n    }\n\n    public String getNtlmPassword() {\n        return ntlmPassword;\n    }\n\n    public void setNtlmPassword(String ntlmPassword) {\n        this.ntlmPassword = ntlmPassword;\n    }\n\n    public String getNtlmDomain() {\n        return ntlmDomain;\n    }\n\n    public void setNtlmDomain(String ntlmDomain) {\n        this.ntlmDomain = ntlmDomain;\n    }\n\n    public String getNtlmWorkstation() {\n        return ntlmWorkstation;\n    }\n\n    public void setNtlmWorkstation(String ntlmWorkstation) {\n        this.ntlmWorkstation = ntlmWorkstation;\n    }\n\n    public void setHttpRetryEnabled(boolean httpRetryEnabled)\n    {\n        this.httpRetryEnabled = httpRetryEnabled;\n    }\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Embed.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.http.ResourceType;\nimport java.io.File;\nimport java.util.Base64;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class Embed {\n\n    private final File file;\n    private final ResourceType resourceType;\n\n    public Embed(File file, ResourceType resourceType) {\n        this.file = file;\n        this.resourceType = resourceType;\n    }\n\n    public String getAsHtmlForReport() {\n        if (resourceType.isImage() || resourceType.isVideo() || resourceType.isScript()) {\n            return getAsHtmlTag();\n        } else {\n            return getAsString();\n        }\n    }\n\n    public static Embed fromKarateJson(Map<String, Object> map) {\n        String fileName = (String) map.get(\"file\");\n        String rtName = (String) map.get(\"resourceType\");\n        File file = new File(fileName);\n        ResourceType rt = ResourceType.valueOf(rtName);\n        return new Embed(file, rt);\n    }\n\n    public Map<String, Object> toKarateJson() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"file\", file.getPath());\n        map.put(\"resourceType\", resourceType.name());\n        map.put(\"html\", getAsHtmlForReport()); // not used in fromKarateJson()\n        return map;\n    }\n\n    public File getFile() {\n        return file;\n    }\n\n    public ResourceType getResourceType() {\n        return resourceType;\n    }\n\n    public byte[] getBytes() {\n        return FileUtils.toBytes(file);\n    }\n\n    public String getBase64() {\n        return Base64.getEncoder().encodeToString(getBytes());\n    }\n\n    public String getAsString() {\n        return FileUtils.toString(file);\n    }\n\n    public String getAsHtmlData() {\n        return \"data:\" + resourceType.contentType + \";base64,\" + getBase64();\n    }\n\n    public String getAsHtmlTag() {\n        if (resourceType == ResourceType.MP4) {\n            return \"<video controls=\\\"true\\\" width=\\\"100%\\\"><source src=\\\"\" + file.getName() + \"\\\" type=\\\"video/mp4\\\"/></video>\";\n        } else if (resourceType.isImage()) {\n            return \"<img src=\\\"\" + file.getName() + \"\\\"/>\";\n        } else if (resourceType.isScript()) {\n            return resourceType == ResourceType.DEFERRED_JS ?\n                    \"<div data-deferred=\\\"true\\\" data-src=\\\"\" + file.getName() + \"\\\">Loading...</div>\" :\n                    \"<script type=\\\"text/javascript\\\" src=\\\"\" + file.getName() + \"\\\"></script>\";\n        } else {\n            return \"<a href=\\\"\" + file.getName() + \"\\\">\" + file.getName() + \"</a>\";\n        }\n    }\n\n    public Map toMap() {\n        Map map = new HashMap(2);\n        map.put(\"data\", getBase64());\n        map.put(\"mime_type\", resourceType.contentType);\n        return map;\n    }\n\n    @Override\n    public String toString() {\n        return file.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ExamplesTable.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class ExamplesTable {\n\n    private final ScenarioOutline outline;\n    private final Table table;\n    private List<Tag> tags;\n    \n\n    public ExamplesTable(ScenarioOutline outline, Table table) {\n        this.outline = outline;\n        this.table = table;\n        this.tags = new ArrayList();\n    }\n\n    public ScenarioOutline getOutline() {\n        return outline;\n    }\n\n    public List<Tag> getTags() {\n        return tags;\n    }\n\n    public void setTags(List<Tag> tags) {\n        this.tags = tags;\n    }\n\n    public Table getTable() {\n        return table;\n    }\n\n    public Map<String, Object> toKarateJson() {\n        Map<String, Object> map = new HashMap();\n        List<String> tagStrings = new ArrayList();\n        tags.forEach(tag -> tagStrings.add(tag.toString()));\n        map.put(\"tags\", tagStrings);\n        map.put(\"data\", table.getRowsAsMapsConverted());\n        return map;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Feature.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.Constants;\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.resource.FileResource;\nimport com.intuit.karate.resource.Resource;\nimport com.intuit.karate.resource.ResourceUtils;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class Feature {\n\n    public static final String KEYWORD = \"Feature\";\n\n    private final Resource resource;\n\n    private int line;\n    private List<Tag> tags;\n    private String name;\n    private String description;\n    private Background background;\n    private List<FeatureSection> sections = new ArrayList();\n\n    public static Feature read(String path) {\n        return read(ResourceUtils.getResource(FileUtils.WORKING_DIR, path));\n    }\n\n    public static Feature read(File file) {\n        return read(new FileResource(file));\n    }\n\n    public static Feature read(Resource resource) {\n        Feature feature = new Feature(resource);\n        FeatureParser.parse(feature);\n        return feature;\n    }\n\n    private Feature(Resource resource) {\n        this.resource = resource;\n    }\n\n    public Resource getResource() {\n        return resource;\n    }\n\n    public String getPackageQualifiedName() {\n        return resource.getPackageQualifiedName();\n    }\n\n    public String getKarateJsonFileName() {\n        return getPackageQualifiedName() + Constants.KARATE_JSON_SUFFIX;\n    }\n\n    public boolean isBackgroundPresent() {\n        return background != null && background.getSteps() != null;\n    }\n\n    public String getNameAndDescription() {\n        String temp = \"\";\n        if (!StringUtils.isBlank(name)) {\n            temp = temp + name;\n        }\n        if (!StringUtils.isBlank(description)) {\n            if (!temp.isEmpty()) {\n                temp = temp + \" | \";\n            }\n            temp = temp + description;\n        }\n        return temp;\n    }\n\n    public String getNameForReport() {\n        if (name == null) {\n            return \"[\" + resource.getFileNameWithoutExtension() + \"]\";\n        } else {\n            return \"[\" + resource.getFileNameWithoutExtension() + \"] \" + name;\n        }\n    }\n\n    public Step findStepByLine(int line) {\n        for (FeatureSection section : sections) {\n            List<Step> steps = section.isOutline()\n                    ? section.getScenarioOutline().getSteps() : section.getScenario().getStepsIncludingBackground();\n            for (Step step : steps) {\n                if (step.getLine() == line) {\n                    return step;\n                }\n            }\n        }\n        return null;\n    }\n    \n    public Scenario getSetup(String name) {\n        for (FeatureSection section : sections) {\n            if (section.isOutline()) {\n                continue;\n            }\n            Scenario scenario = section.getScenario();\n            List<Tag> foundTags = scenario.getTags();\n            if (foundTags != null) {\n                for (Tag tag : foundTags) {\n                    if (Tag.SETUP.equals(tag.getName())) {\n                        if (name == null) {\n                            return scenario;\n                        }\n                        if (tag.getValues().contains(name)) {\n                            return scenario;\n                        }\n                    }\n                }\n            }\n        }\n        return null;\n    }\n\n    public void addSection(FeatureSection section) {\n        section.setIndex(sections.size());\n        sections.add(section);\n    }\n\n    public FeatureSection getSection(int sectionIndex) {\n        return sections.get(sectionIndex);\n    }\n\n    public Scenario getScenario(int sectionIndex, int exampleIndex) {\n        FeatureSection section = getSection(sectionIndex);\n        if (exampleIndex == -1) {\n            return section.getScenario();\n        }\n        ScenarioOutline outline = section.getScenarioOutline();\n        return outline.getScenarios().get(exampleIndex);\n    }\n\n    public Step getStep(int sectionIndex, int exampleIndex, int stepIndex) {\n        Scenario scenario = getScenario(sectionIndex, exampleIndex);\n        List<Step> steps = scenario.getSteps();\n        if (stepIndex == -1 || steps.isEmpty() || steps.size() <= stepIndex) {\n            return null;\n        }\n        return steps.get(stepIndex);\n    }\n\n    public int getLine() {\n        return line;\n    }\n\n    public void setLine(int line) {\n        this.line = line;\n    }\n\n    public List<Tag> getTags() {\n        return tags;\n    }\n\n    public void setTags(List<Tag> tags) {\n        this.tags = tags;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public void setDescription(String description) {\n        this.description = description;\n    }\n\n    public Background getBackground() {\n        return background;\n    }\n\n    public void setBackground(Background background) {\n        this.background = background;\n    }\n\n    public List<FeatureSection> getSections() {\n        return sections;\n    }\n\n    public void setSections(List<FeatureSection> sections) {\n        this.sections = sections;\n    }\n\n    @Override\n    public String toString() {\n        return resource.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/FeatureCall.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\n/**\n *\n * @author peter\n */\npublic class FeatureCall {\n    \n    public final Feature feature;\n    public final String callTag;\n    public final int callLine; // -1 if not applicable\n    public final String callName; // scenario name pattern\n    \n    public FeatureCall(Feature feature) {\n        this(feature, null, -1, null);\n    }\n    \n    public FeatureCall(Feature feature, String callTag, int callLine, String callName) {\n        this.feature = feature;\n        this.callTag = callTag;\n        this.callLine = callLine;\n        this.callName = callName;\n    }\n\n    @Override\n    public String toString() {\n        return feature.toString();\n    }        \n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/FeatureParser.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.StringUtils;\nimport org.antlr.v4.runtime.*;\nimport org.antlr.v4.runtime.tree.ParseTreeWalker;\nimport org.antlr.v4.runtime.tree.TerminalNode;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class FeatureParser extends KarateParserBaseListener {\n    \n    private static final Logger logger = LoggerFactory.getLogger(FeatureParser.class);\n    \n    private final ParserErrorListener errorListener = new ParserErrorListener();\n    private final Feature feature;\n    private final CommonTokenStream tokenStream;\n    \n    private FeatureParser(Feature feature, InputStream is) {\n        this.feature = feature;\n        CharStream stream;\n        try {\n            stream = CharStreams.fromStream(is, StandardCharsets.UTF_8);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n        KarateLexer lexer = new KarateLexer(stream);\n        tokenStream = new CommonTokenStream(lexer);\n        KarateParser parser = new KarateParser(tokenStream);\n        parser.addErrorListener(errorListener);\n        RuleContext tree = parser.feature();\n        ParseTreeWalker walker = new ParseTreeWalker();\n        walker.walk(this, tree);\n    }\n    \n    protected static void parse(Feature feature) {\n        FeatureParser fp = new FeatureParser(feature, feature.getResource().getStream());\n        if (fp.errorListener.isFail()) {\n            String errorMessage = fp.errorListener.getMessage();\n            logger.error(\"not a valid feature file: {} - {}\", feature.getResource().getRelativePath(), errorMessage);\n            throw new RuntimeException(errorMessage);\n        }\n    }\n    \n    private static int getActualLine(TerminalNode node) {\n        int count = 0;\n        for (char c : node.getText().toCharArray()) {\n            if (c == '\\n') {\n                count++;\n            } else if (!Character.isWhitespace(c)) {\n                break;\n            }\n        }\n        return node.getSymbol().getLine() + count;\n    }\n    \n    private static List<Tag> toTags(int line, List<TerminalNode> nodes) {\n        List<Tag> tags = new ArrayList();\n        for (TerminalNode node : nodes) {\n            String text = node.getText();\n            if (line == -1) {\n                line = getActualLine(node);\n            }\n            String[] tokens = text.trim().split(\"\\\\s+\"); // handles spaces and tabs also        \n            for (String t : tokens) {\n                tags.add(new Tag(line, t));\n            }\n        }\n        return tags;\n    }\n    \n    private static Table toTable(KarateParser.TableContext ctx) {\n        List<TerminalNode> nodes = ctx.TABLE_ROW();\n        int rowCount = nodes.size();\n        if (rowCount < 1) {\n            // if scenario outline found without examples\n            return null;\n        }\n        List<List<String>> rows = new ArrayList(rowCount);\n        List<Integer> lineNumbers = new ArrayList(rowCount);\n        int prevCount = -1;\n        for (TerminalNode node : nodes) {\n            List<String> tokens = StringUtils.split(node.getText().trim(), '|', true);\n            int count = tokens.size();\n            if (prevCount != -1 && prevCount != count) {                \n                throw new RuntimeException(\"examples column count mismatch at line: \" \n                        + getActualLine(node) \n                        + \"\\n\" + node.getText() + \"\\n\\n\"\n                        + \"rows:\" + nodes);\n            }\n            prevCount = count;\n            for (int i = 0; i < count; i++) {\n                tokens.set(i, tokens.get(i).trim());\n            }\n            rows.add(tokens);\n            lineNumbers.add(getActualLine(node));\n        }\n        return new Table(rows, lineNumbers);\n    }\n    \n    public static final String TRIPLE_QUOTES = \"\\\"\\\"\\\"\";\n    \n    private static int indexOfFirstText(String s) {\n        int pos = 0;\n        for (char c : s.toCharArray()) {\n            if (!Character.isWhitespace(c)) {\n                return pos;\n            }\n            pos++;\n        }\n        return 0; // defensive coding\n    }\n    \n    private static String fixDocString(String temp) {\n        int quotePos = temp.indexOf(TRIPLE_QUOTES);\n        int endPos = temp.lastIndexOf(TRIPLE_QUOTES);\n        String raw = temp.substring(quotePos + 3, endPos).replaceAll(\"\\r\", \"\");\n        List<String> lines = StringUtils.split(raw, '\\n', false);\n        StringBuilder sb = new StringBuilder();\n        int marginPos = -1;\n        Iterator<String> iterator = lines.iterator();\n        while (iterator.hasNext()) {\n            String line = iterator.next();\n            int firstTextPos = indexOfFirstText(line);\n            if (marginPos == -1) {\n                marginPos = firstTextPos;\n            }\n            if (marginPos < line.length() && marginPos <= firstTextPos) {\n                line = line.substring(marginPos);\n            }\n            if (iterator.hasNext()) {\n                sb.append(line).append('\\n');\n            } else {\n                sb.append(line);\n            }\n        }\n        return sb.toString().trim();\n    }\n    \n    private List<String> collectComments(ParserRuleContext prc) {\n        List<Token> tokens = tokenStream.getHiddenTokensToLeft(prc.start.getTokenIndex());\n        if (tokens == null) {\n            return null;\n        }\n        List<String> comments = new ArrayList(tokens.size());\n        for (Token t : tokens) {\n            comments.add(StringUtils.trimToNull(t.getText()));\n        }\n        return comments;\n    }\n    \n    private List<Step> toSteps(Scenario scenario, List<KarateParser.StepContext> list) {\n        List<Step> steps = new ArrayList(list.size());\n        int index = 0;\n        for (KarateParser.StepContext sc : list) {\n            Step step = scenario == null ? new Step(feature, index++) : new Step(scenario, index++);\n            step.setComments(collectComments(sc));\n            steps.add(step);\n            int stepLine = sc.line().getStart().getLine();\n            step.setLine(stepLine);\n            step.setPrefix(sc.prefix().getText().trim());\n            step.setText(sc.line().getText().trim());\n            if (sc.docString() != null) {\n                String raw = sc.docString().getText();\n                step.setDocString(fixDocString(raw));\n                step.setEndLine(stepLine + StringUtils.countLineFeeds(raw));\n            } else if (sc.table() != null) {\n                Table table = toTable(sc.table());\n                step.setTable(table);\n                step.setEndLine(stepLine + StringUtils.countLineFeeds(sc.table().getText()));\n            } else {\n                step.setEndLine(stepLine);\n            }\n        }\n        return steps;\n    }\n    \n    @Override\n    public void enterFeatureHeader(KarateParser.FeatureHeaderContext ctx) {\n        if (ctx.featureTags() != null) {\n            feature.setTags(toTags(ctx.featureTags().getStart().getLine(), ctx.featureTags().FEATURE_TAGS()));\n        }\n        if (ctx.FEATURE() != null) {\n            feature.setLine(ctx.FEATURE().getSymbol().getLine());\n            if (ctx.featureDescription() != null) {\n                StringUtils.Pair pair = StringUtils.splitByFirstLineFeed(ctx.featureDescription().getText());\n                feature.setName(pair.left);\n                feature.setDescription(pair.right);\n            }\n        }\n    }\n    \n    @Override\n    public void enterBackground(KarateParser.BackgroundContext ctx) {\n        Background background = new Background();\n        feature.setBackground(background);\n        background.setLine(getActualLine(ctx.BACKGROUND()));\n        List<Step> steps = toSteps(null, ctx.step());\n        if (!steps.isEmpty()) {\n            background.setSteps(steps);\n        }\n        if (logger.isTraceEnabled()) {\n            logger.trace(\"background steps: {}\", steps);\n        }\n    }\n    \n    @Override\n    public void enterScenario(KarateParser.ScenarioContext ctx) {\n        FeatureSection section = new FeatureSection();\n        Scenario scenario = new Scenario(feature, section, -1);\n        scenario.setLine(getActualLine(ctx.SCENARIO()));\n        section.setScenario(scenario);\n        feature.addSection(section);\n        if (ctx.tags() != null) {\n            scenario.setTags(toTags(-1, ctx.tags().TAGS()));\n        }\n        if (ctx.scenarioDescription() != null) {\n            StringUtils.Pair pair = StringUtils.splitByFirstLineFeed(ctx.scenarioDescription().getText());\n            scenario.setName(pair.left);\n            scenario.setDescription(pair.right);\n        }\n        List<Step> steps = toSteps(scenario, ctx.step());\n        scenario.setSteps(steps);\n    }\n    \n    @Override\n    public void enterScenarioOutline(KarateParser.ScenarioOutlineContext ctx) {\n        FeatureSection section = new FeatureSection();\n        ScenarioOutline outline = new ScenarioOutline(feature, section);\n        outline.setLine(getActualLine(ctx.SCENARIO_OUTLINE()));\n        section.setScenarioOutline(outline);\n        feature.addSection(section);\n        if (ctx.tags() != null) {\n            outline.setTags(toTags(-1, ctx.tags().TAGS()));\n        }\n        if (ctx.scenarioDescription() != null) {\n            outline.setDescription(ctx.scenarioDescription().getText());\n            StringUtils.Pair pair = StringUtils.splitByFirstLineFeed(ctx.scenarioDescription().getText());\n            outline.setName(pair.left);\n            outline.setDescription(pair.right);\n        }\n        List<Step> steps = toSteps(null, ctx.step());\n        outline.setSteps(steps);\n        if (logger.isTraceEnabled()) {\n            logger.trace(\"outline steps: {}\", steps);\n        }\n        List<ExamplesTable> examples = new ArrayList(ctx.examples().size());\n        outline.setExamplesTables(examples);\n        for (KarateParser.ExamplesContext ec : ctx.examples()) {\n            Table table = toTable(ec.table());\n            ExamplesTable example = new ExamplesTable(outline, table);\n            examples.add(example);\n            if (ec.tags() != null) {\n                example.setTags(toTags(-1, ec.tags().TAGS()));\n            }\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"example rows: {}\", table.getRows());\n            }\n        }\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/FeatureResult.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.report.ReportUtils;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.KarateException;\nimport com.intuit.karate.resource.Resource;\nimport com.intuit.karate.resource.ResourceUtils;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class FeatureResult {\n\n    private final Feature feature;\n    private final List<ScenarioResult> scenarioResults = new ArrayList<>();\n\n    private String resultDate;\n    private String displayName; // mutable for users who want to customize\n\n    private Map<String, Object> resultVariables;\n    private Map<String, Object> callArg;\n    private Config config;\n    private int loopIndex = -1;\n    private int callDepth;\n\n    public FeatureResult(Feature feature) {\n        this.feature = feature;\n        displayName = feature.getResource().getRelativePath();\n    }\n\n    public void printStats() {\n        String featureName = feature.getResource().getPrefixedPath();\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"---------------------------------------------------------\\n\");\n        sb.append(\"feature: \").append(featureName).append('\\n');\n        sb.append(String.format(\"scenarios: %2d | passed: %2d | failed: %2d | time: %.4f\\n\", getScenarioCount(), getPassedCount(), getFailedCount(), getDurationMillis() / 1000));\n        sb.append(\"---------------------------------------------------------\\n\");\n        System.out.println(sb);\n    }\n\n    public List<File> getAllEmbedFiles() {\n        List<File> files = new ArrayList<>();\n        for (ScenarioResult sr : scenarioResults) {\n            for (StepResult stepResult : sr.getStepResults()) {\n                if (stepResult.getEmbeds() != null) {\n                    for (Embed embed : stepResult.getEmbeds()) {\n                        files.add(embed.getFile());\n                    }\n                }\n            }\n        }\n        return files;\n    }\n\n    public static FeatureResult fromKarateJson(File workingDir, Map<String, Object> map) {\n        String featurePath = (String) map.get(\"prefixedPath\");\n        Resource resource = ResourceUtils.getResource(workingDir, featurePath);\n        Feature feature = Feature.read(resource);\n        FeatureResult fr = new FeatureResult(feature);\n        fr.callArg = (Map) map.get(\"callArg\");\n        fr.loopIndex = (Integer) map.get(\"loopIndex\");\n        fr.resultDate = (String) map.get(\"resultDate\");\n        fr.callDepth = (Integer) map.get(\"callDepth\");\n        List<Map<String, Object>> list = (List) map.get(\"scenarioResults\");\n        if (list != null) {\n            for (Map<String, Object> srMap : list) {\n                ScenarioResult sr = ScenarioResult.fromKarateJson(workingDir, feature, srMap);\n                if (!sr.getStepResults().isEmpty()) {\n                    fr.addResult(sr);\n                }                \n            }\n        }\n        return fr;\n    }\n\n    public Map<String, Object> toInfoJson() {\n        Map<String, Object> map = new HashMap<>();\n        map.put(\"name\", feature.getName());\n        map.put(\"description\", feature.getDescription());\n        map.put(\"prefixedPath\", feature.getResource().getPrefixedPath());\n        File file = feature.getResource().getFile();\n        if (file != null) {\n            map.put(\"fileName\", file.getName());\n            map.put(\"parentDir\", file.getParent());\n        }\n        return map;\n    }\n\n    public Map<String, Object> toSummaryJson() {\n        Map<String, Object> map = new HashMap<>();\n        map.put(\"failed\", isFailed());\n        map.put(\"name\", feature.getName());\n        map.put(\"description\", feature.getDescription());\n        map.put(\"durationMillis\", getDurationMillis());\n        map.put(\"passedCount\", getPassedCount());\n        map.put(\"failedCount\", getFailedCount());\n        map.put(\"scenarioCount\", getScenarioCount());\n        map.put(\"packageQualifiedName\", feature.getPackageQualifiedName());\n        map.put(\"relativePath\", feature.getResource().getRelativePath());\n        return map;\n    }\n\n    public Map<String, Object> toKarateJson() {\n        Map<String, Object> map = new HashMap<>();\n        // these first few are only for the ease of reports\n        // note that they are not involved in the reverse fromKarateJson()\n        map.put(\"name\", feature.getName());\n        map.put(\"description\", feature.getDescription());\n        map.put(\"durationMillis\", getDurationMillis());\n        map.put(\"passedCount\", getPassedCount());\n        map.put(\"failedCount\", getFailedCount());\n        map.put(\"packageQualifiedName\", feature.getPackageQualifiedName());\n        map.put(\"relativePath\", feature.getResource().getRelativePath());\n        //======================================================================\n        if (resultDate == null) {\n            resultDate = ReportUtils.getDateString();\n        }\n        map.put(\"resultDate\", resultDate);\n        map.put(\"prefixedPath\", feature.getResource().getPrefixedPath());\n        List<Map<String, Object>> list = new ArrayList<>(scenarioResults.size());\n        map.put(\"scenarioResults\", list);\n        for (ScenarioResult sr : scenarioResults) {\n            list.add(sr.toKarateJson());\n        }\n        if (callArg != null) {\n            String json = JsonUtils.toJsonSafe(callArg, false);\n            map.put(\"callArg\", JsonUtils.fromJson(json));\n        }\n        map.put(\"loopIndex\", loopIndex);\n        map.put(\"callDepth\", callDepth);\n        return map;\n    }\n\n    public Map<String, Object> toCucumberJson() {\n        Map<String, Object> map = new HashMap<>();\n        map.put(\"keyword\", Feature.KEYWORD);\n        map.put(\"line\", feature.getLine());\n        map.put(\"uri\", displayName);\n        map.put(\"name\", displayName);\n        map.put(\"id\", StringUtils.toIdString(feature.getName()));\n        String temp = feature.getName() == null ? \"\" : feature.getName();\n        if (feature.getDescription() != null) {\n            temp = temp + \"\\n\" + feature.getDescription();\n        }\n        map.put(\"description\", temp.trim());\n        if (feature.getTags() != null) {\n            map.put(\"tags\", ScenarioResult.tagsToCucumberJson(feature.getTags()));\n        }\n        List<Map<String, Object>> list = new ArrayList<>(scenarioResults.size());\n        map.put(\"elements\", list);\n        for (ScenarioResult sr : scenarioResults) {\n            Map<String, Object> backgroundMap = sr.backgroundToCucumberJson();\n            if (backgroundMap != null) {\n                list.add(backgroundMap);\n            }\n            list.add(sr.toCucumberJson());\n        }\n        return map;\n    }\n\n    public List<StepResult> getAllScenarioStepResultsNotHidden() {\n        List<StepResult> list = new ArrayList<>();\n        for (ScenarioResult sr : scenarioResults) {\n            list.addAll(sr.getStepResultsNotHidden());\n        }\n        return list;\n    }\n\n    public void setDisplayName(String displayName) {\n        this.displayName = displayName;\n    }\n\n    public Feature getFeature() {\n        return feature;\n    }\n\n    public String getDisplayName() {\n        return displayName;\n    }\n\n    public KarateException getErrorMessagesCombined() {\n        List<String> errors = getErrors();\n        if (errors.size() == 1) {\n            return new KarateException(errors.get(0));\n        }\n        return new KarateException(getErrorMessages());\n    }\n\n    public String getErrorMessages() {\n        return StringUtils.join(getErrors(), \"\\n\");\n    }\n\n    public String getCallNameForReport() {\n        String append = loopIndex == -1 ? \"\" : \"[\" + loopIndex + \"] \";\n        return append + displayName;\n    }\n\n    public String getCallArgPretty() {\n        if (callArg == null) {\n            return null;\n        }\n        try {\n            return JsonUtils.toJsonSafe(callArg, true);\n        } catch (Throwable t) {\n            return \"#error: \" + t.getMessage();\n        }\n    }\n\n    public void setCallDepth(int callDepth) {\n        this.callDepth = callDepth;\n    }\n\n    public Map<String, Object> getCallArg() {\n        return callArg;\n    }\n\n    public void setCallArg(Map<String, Object> callArg) {\n        this.callArg = callArg;\n    }\n\n    public int getLoopIndex() {\n        return loopIndex;\n    }\n\n    public void setLoopIndex(int loopIndex) {\n        this.loopIndex = loopIndex;\n    }\n\n    public double getDurationMillis() {\n        long durationNanos = 0;\n        for (ScenarioResult sr : scenarioResults) {\n            durationNanos += sr.getDurationNanos();\n        }\n        return ReportUtils.nanosToMillis(durationNanos);\n    }\n\n    public int getFailedCount() {\n        return getErrors().size();\n    }\n\n    public boolean isEmpty() {\n        return scenarioResults.isEmpty();\n    }\n\n    public int getScenarioCount() {\n        return scenarioResults.size();\n    }\n\n    public int getPassedCount() {\n        return getScenarioCount() - getFailedCount();\n    }\n\n    public boolean isFailed() {\n        return getFailedCount() > 0;\n    }\n\n    public List<String> getErrors() {\n        List<String> errors = new ArrayList<>();\n        for (ScenarioResult sr : scenarioResults) {\n            if (sr.isFailed()) {\n                errors.add(sr.getErrorMessage());\n            }\n        }\n        return errors;\n    }\n\n    public void addResult(ScenarioResult result) {\n        scenarioResults.add(result);\n    }\n\n    public void setVariables(Map<String, Object> resultVariables) {\n        this.resultVariables = resultVariables;\n    }\n\n    public Map<String, Object> getVariables() {\n        // edge case if no scenarios were run\n        return resultVariables == null ? new HashMap<>() : resultVariables;\n    }\n\n    public void setConfig(Config config) {\n        this.config = config;\n    }\n\n    public Config getConfig() {\n        return config;\n    }\n\n    public void sortScenarioResults() {\n        Collections.sort(scenarioResults);\n    }\n\n    public List<ScenarioResult> getScenarioResults() {\n        return scenarioResults;\n    }\n\n    @Override\n    public String toString() {\n        return displayName;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/FeatureRuntime.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.RuntimeHook;\nimport com.intuit.karate.PerfHook;\nimport com.intuit.karate.Suite;\nimport com.intuit.karate.http.HttpClientFactory;\nimport com.intuit.karate.resource.MemoryResource;\nimport com.intuit.karate.resource.Resource;\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.concurrent.CompletableFuture;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class FeatureRuntime implements Runnable {\n\n    protected static final Logger logger = LoggerFactory.getLogger(FeatureRuntime.class);\n\n    public final Suite suite;\n    public final FeatureRuntime rootFeature;\n    public final ScenarioCall caller;\n    public final FeatureCall featureCall;\n    public final Iterator<ScenarioRuntime> scenarios;\n    public final PerfHook perfHook;\n    public final FeatureResult result;\n\n    protected ScenarioResult setupResult;\n\n    private ScenarioEngine mockEngine;\n\n    private final ParallelProcessor<ScenarioRuntime> processor;\n\n    public final Map<String, ScenarioCall.Result> CALLONCE_CACHE = new HashMap();\n    public final Map<String, Map<String, Object>> SETUPONCE_CACHE = new HashMap();\n\n    private Runnable next;\n\n    public Resource resolveFromThis(String path) {\n        return featureCall.feature.getResource().resolve(path);\n    }\n\n    public Resource resolveFromRoot(String path) {\n        return rootFeature.featureCall.feature.getResource().resolve(path);\n    }\n\n    public void setNext(Runnable next) {\n        this.next = next;\n    }\n\n    public void setMockEngine(ScenarioEngine mockEngine) {\n        this.mockEngine = mockEngine;\n    }\n\n    public ScenarioEngine getMockEngine() {\n        return mockEngine;\n    }\n\n    public static FeatureRuntime forTempUse(HttpClientFactory hcf) {\n        Suite sr = Suite.forTempUse(hcf);\n        File workingDir = new File(sr.buildDir).getAbsoluteFile();\n        Resource resource = new MemoryResource(workingDir, \"Feature:\\nScenario:\\n\");\n        Feature feature = Feature.read(resource);\n        return FeatureRuntime.of(sr, new FeatureCall(feature));\n    }\n\n    public static FeatureRuntime of(Feature feature) {\n        return of(new FeatureCall(feature));\n    }\n\n    public static FeatureRuntime of(FeatureCall feature) {\n        return of(new Suite(), feature, null);\n    }\n\n    public static FeatureRuntime of(Suite sr, FeatureCall feature) {\n        return of(sr, feature, null);\n    }\n\n    public static FeatureRuntime of(Suite sr, FeatureCall feature, Map<String, Object> arg) {\n        return new FeatureRuntime(sr, feature, ScenarioCall.none(arg), null);\n    }\n\n    public static FeatureRuntime of(Suite sr, FeatureCall feature, Map<String, Object> arg, PerfHook perfHook) {\n        return new FeatureRuntime(sr, feature, ScenarioCall.none(arg), perfHook);\n    }\n\n    public FeatureRuntime(ScenarioCall call) {\n        this(call.parentRuntime.featureRuntime.suite, call.featureCall, call, call.parentRuntime.featureRuntime.perfHook);\n        result.setLoopIndex(call.getLoopIndex());\n        result.setCallDepth(call.depth);\n        if (call.arg != null && !call.arg.isNull()) {\n            result.setCallArg(call.arg.getValue());\n        }\n    }\n\n    private FeatureRuntime(Suite suite, FeatureCall featureCall, ScenarioCall caller, PerfHook perfHook) {\n        this.suite = suite;\n        this.featureCall = featureCall;\n        this.caller = caller;\n        this.rootFeature = caller.isNone() ? this : caller.parentRuntime.featureRuntime;\n        result = new FeatureResult(featureCall.feature);\n        scenarios = new ScenarioIterator(this).filterSelected().iterator();\n        this.perfHook = perfHook;\n        if (caller.isNone() && suite.parallel && perfHook == null) {\n            processor = new ParallelProcessor<ScenarioRuntime>(\n                    suite.scenarioExecutor,\n                    scenarios,\n                    suite.pendingTasks) {\n\n                @Override\n                public void process(ScenarioRuntime sr) {\n                    processScenario(sr);\n                }\n\n                @Override\n                public void onComplete() {\n                    afterFeature();\n                }\n\n                @Override\n                public boolean shouldRunSynchronously(ScenarioRuntime sr) {\n                    return sr.tags.valuesFor(\"parallel\").isAnyOf(\"false\");\n                }\n\n            };\n        } else {\n            processor = null;\n        }\n    }\n\n    private boolean beforeHookDone;\n    private boolean beforeHookResult = true;\n\n    // logic to run once only if there are runnable scenarios (selected by tag)\n    public boolean beforeHook() {\n        if (beforeHookDone) {\n            return beforeHookResult;\n        }\n        beforeHookDone = true;\n        for (RuntimeHook hook : suite.hooks) {\n            beforeHookResult = beforeHookResult && hook.beforeFeature(this);\n        }\n        return beforeHookResult;\n    }\n\n    @Override\n    public void run() {\n        if (processor != null) {\n            processor.execute();\n        } else {\n            if (!beforeHook()) {\n                logger.info(\"before-feature hook returned [false], aborting: {}\", this);\n            } else {\n                scenarios.forEachRemaining(this::processScenario);\n            }\n            afterFeature();\n        }\n    }\n\n    private ScenarioRuntime lastExecutedScenario;\n\n    private void processScenario(ScenarioRuntime sr) {\n        if (beforeHook()) {\n            lastExecutedScenario = sr;\n            sr.run();\n            // can be empty for distributed / job-server flows\n            if (!sr.result.getStepResults().isEmpty()) {\n                synchronized (result) {\n                    result.addResult(sr.result);\n                    \n                    // Execute afterScenarioOutline if applicable\n                    // NOTE: Needs to be run after adding result, since result count is used to deterime\n                    // if the scenario is the last in the outline\n                    if (!sr.dryRun && isLastScenarioInOutline(sr.scenario)) {\n                        sr.engine.invokeAfterHookIfConfigured(AfterHookType.AFTER_OUTLINE);\n                        suite.hooks.forEach(h -> h.afterScenarioOutline(sr));\n                    }\n                }\n            }\n        }\n    }\n\n    private boolean isLastScenarioInOutline(Scenario scenario) {\n        // Check if scenario is part of an outline\n        if (!scenario.isOutlineExample()) return false;\n\n        // Count the number of completed scenarios with the same section ID (in same outline)\n        int completedScenarios = 0;\n        for (ScenarioResult result : result.getScenarioResults()) {\n            if (result.getScenario().getSection().getIndex() == scenario.getSection().getIndex()) {\n                completedScenarios++;\n            }\n        }\n        return completedScenarios == scenario.getSection().getScenarioOutline().getNumScenarios();\n    }\n\n    // extracted for junit5\n    public synchronized void afterFeature() {\n        result.sortScenarioResults();\n        if (lastExecutedScenario != null) {\n            lastExecutedScenario.engine.invokeAfterHookIfConfigured(AfterHookType.AFTER_FEATURE);\n            result.setVariables(lastExecutedScenario.engine.getAllVariablesAsMap());\n            result.setConfig(lastExecutedScenario.engine.getConfig());\n        }\n        if (!result.isEmpty()) {\n            for (RuntimeHook hook : suite.hooks) {\n                hook.afterFeature(this);\n            }\n        }\n        if (next != null) {\n            next.run();\n        }\n    }\n\n    @Override\n    public String toString() {\n        return featureCall.feature.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/FeatureSection.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\n/**\n *\n * @author pthomas3\n */\npublic class FeatureSection {\n    \n    private int index;\n    private Scenario scenario;\n    private ScenarioOutline scenarioOutline;\n    \n    public int getLine() {\n        if (scenarioOutline != null) {\n            return scenarioOutline.getLine();\n        } else {\n            return scenario.getLine();\n        }\n    }\n\n    public int getIndex() {\n        return index;\n    }\n\n    public void setIndex(int index) {\n        this.index = index;\n    }        \n    \n    public boolean isOutline() {\n        return scenarioOutline != null;\n    }\n\n    public Scenario getScenario() {\n        return scenario;\n    }\n\n    public void setScenario(Scenario scenario) {\n        this.scenario = scenario;\n    }\n\n    public ScenarioOutline getScenarioOutline() {\n        return scenarioOutline;\n    }\n\n    public void setScenarioOutline(ScenarioOutline scenarioOutline) {\n        this.scenarioOutline = scenarioOutline;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/MockHandler.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.ScenarioActions;\nimport com.intuit.karate.Suite;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.Json;\nimport com.intuit.karate.KarateException;\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.http.HttpClientFactory;\nimport com.intuit.karate.http.HttpUtils;\nimport com.intuit.karate.http.Request;\nimport com.intuit.karate.http.ResourceType;\nimport com.intuit.karate.http.Response;\nimport com.intuit.karate.http.ServerHandler;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class MockHandler implements ServerHandler {\n\n    private static final Logger logger = LoggerFactory.getLogger(MockHandler.class);\n\n    private static final String REQUEST_BYTES = \"requestBytes\";\n    private static final String REQUEST_PARAMS = \"requestParams\";\n    private static final String REQUEST_PARTS = \"requestParts\";\n\n    private static final String RESPONSE_DELAY = \"responseDelay\";\n\n    private static final String PATH_MATCHES = \"pathMatches\";\n    private static final String METHOD_IS = \"methodIs\";\n    private static final String TYPE_CONTAINS = \"typeContains\";\n    private static final String ACCEPT_CONTAINS = \"acceptContains\";\n    private static final String HEADER_CONTAINS = \"headerContains\";\n    private static final String PARAM_VALUE = \"paramValue\";\n    private static final String PARAM_EXISTS = \"paramExists\";\n    private static final String PATH_PARAMS = \"pathParams\";\n    private static final String BODY_PATH = \"bodyPath\";\n\n    private final LinkedHashMap<Feature, ScenarioRuntime> scenarioRuntimes = new LinkedHashMap<>(); // feature + holds global config and vars\n    private final Map<String, Variable> globals = new HashMap<>();\n    private boolean corsEnabled;\n\n    protected static final ThreadLocal<Request> LOCAL_REQUEST = new ThreadLocal<>();\n    private final String prefix;\n\n    private final MockInterceptor mockInterceptor;\n\n    public MockHandler(Feature feature) {\n        this(feature, null);\n    }\n\n    public MockHandler(Feature feature, Map<String, Object> args) {\n        this(null, Collections.singletonList(feature), args, null);\n    }\n\n    public MockHandler(List<Feature> features) {\n        this(null, features, null, null);\n    }\n\n    public MockHandler(String prefix, List<Feature> features, Map<String, Object> args, MockInterceptor interceptor) {\n        this.prefix = \"/\".equals(prefix) ? null : prefix;\n        this.mockInterceptor = interceptor;\n        features.forEach(feature -> {\n            ScenarioRuntime runtime = initRuntime(feature, args);\n            corsEnabled = corsEnabled || runtime.engine.getConfig().isCorsEnabled();\n            globals.putAll(runtime.engine.shallowCloneVariables());\n            runtime.logger.info(\"mock server initialized: {}\", feature);\n            scenarioRuntimes.put(feature, runtime);            \n        });\n    }\n    \n    public Object getVariable(String name) {\n        if (globals.containsKey(name)) {\n            Variable v = globals.get(name);\n            if (v != null) {\n                return JsValue.fromJava(v.getValue());\n            }\n        }\n        return null;\n    }\n\n    private ScenarioRuntime initRuntime(Feature feature, Map<String, Object> args) {\n        FeatureRuntime featureRuntime = FeatureRuntime.of(Suite.forTempUse(HttpClientFactory.DEFAULT), new FeatureCall(feature), args);\n        FeatureSection section = new FeatureSection();\n        section.setIndex(-1); // TODO util for creating dummy scenario\n        Scenario dummy = new Scenario(feature, section, -1);\n        section.setScenario(dummy);\n        ScenarioRuntime runtime = new ScenarioRuntime(featureRuntime, dummy);\n        runtime.logger.setLogOnly(true);\n        runtime.engine.setVariable(PATH_MATCHES, (Function<String, Boolean>) this::pathMatches);\n        runtime.engine.setVariable(PARAM_EXISTS, (Function<String, Boolean>) this::paramExists);\n        runtime.engine.setVariable(PARAM_VALUE, (Function<String, String>) this::paramValue);\n        runtime.engine.setVariable(METHOD_IS, (Function<String, Boolean>) this::methodIs);\n        runtime.engine.setVariable(TYPE_CONTAINS, (Function<String, Boolean>) this::typeContains);\n        runtime.engine.setVariable(ACCEPT_CONTAINS, (Function<String, Boolean>) this::acceptContains);\n        runtime.engine.setVariable(HEADER_CONTAINS, (BiFunction<String, String, Boolean>) this::headerContains);\n        runtime.engine.setVariable(BODY_PATH, (Function<String, Object>) this::bodyPath);\n        runtime.engine.init();\n        if (feature.isBackgroundPresent()) {\n            // if we are within a scenario already e.g. karate.start(), preserve context\n            ScenarioEngine prevEngine = ScenarioEngine.get();\n            try {\n                ScenarioEngine.set(runtime.engine);\n                for (Step step : feature.getBackground().getSteps()) {\n                    Result result = StepRuntime.execute(step, runtime.actions);\n                    if (result.isFailed()) {\n                        String message = \"mock-server background failed - \" + feature + \":\" + step.getLine();\n                        runtime.logger.error(message);\n                        throw new KarateException(message, result.getError());\n                    }\n                }\n            } finally {\n                ScenarioEngine.set(prevEngine);\n            }\n        }        \n        return runtime;\n    }\n\n    private static final Result PASSED = Result.passed(0, 0);\n    private static final String ALLOWED_METHODS = \"GET, HEAD, POST, PUT, DELETE, PATCH\";\n\n    @Override\n    public synchronized Response handle(Request req) { // note the [synchronized]\n        if (corsEnabled && \"OPTIONS\".equals(req.getMethod())) {\n            Response response = new Response(200);\n            response.setHeader(\"Allow\", ALLOWED_METHODS);\n            response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n            response.setHeader(\"Access-Control-Allow-Methods\", ALLOWED_METHODS);\n            List<String> requestHeaders = req.getHeaderValues(\"Access-Control-Request-Headers\");\n            if (requestHeaders != null) {\n                response.setHeader(\"Access-Control-Allow-Headers\", requestHeaders);\n            }\n            return response;\n        }\n        if (prefix != null && req.getPath().startsWith(prefix)) {\n            req.setPath(req.getPath().substring(prefix.length()));\n        }\n        // rare case when http-client is active within same jvm\n        // snapshot existing thread-local to restore\n        ScenarioEngine prevEngine = ScenarioEngine.get();\n        for (Map.Entry<Feature, ScenarioRuntime> entry : scenarioRuntimes.entrySet()) {\n            Feature feature = entry.getKey();\n            ScenarioRuntime runtime = entry.getValue();\n            // important for graal to work properly\n            Thread.currentThread().setContextClassLoader(runtime.featureRuntime.suite.classLoader);            \n            LOCAL_REQUEST.set(req);\n            req.processBody();\n            ScenarioEngine engine = initEngine(runtime, globals, req);\n            for (FeatureSection fs : feature.getSections()) {\n                if (fs.isOutline()) {\n                    runtime.logger.warn(\"skipping scenario outline - {}:{}\", feature, fs.getScenarioOutline().getLine());\n                    break;\n                }\n                Scenario scenario = fs.getScenario();\n                if (isMatchingScenario(scenario, engine)) {\n                    Map<String, Object> configureHeaders;\n                    Variable response, responseStatus, responseHeaders, responseDelay;\n                    ScenarioActions actions = new ScenarioActions(engine);\n                    Result result = executeScenarioSteps(feature, runtime, scenario, actions);\n                    engine.mockAfterScenario();\n                    configureHeaders = engine.mockConfigureHeaders();\n                    response = engine.vars.remove(ScenarioEngine.RESPONSE);\n                    responseStatus = engine.vars.remove(ScenarioEngine.RESPONSE_STATUS);\n                    responseHeaders = engine.vars.remove(ScenarioEngine.RESPONSE_HEADERS);\n                    responseDelay = engine.vars.remove(RESPONSE_DELAY);\n                    globals.putAll(engine.shallowCloneVariables());\n                    Response res = new Response(200);\n                    if (result.isFailed()) {\n                        response = new Variable(result.getError().getMessage());\n                        responseStatus = new Variable(500);\n                    } else {\n                        if (corsEnabled) {\n                            res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n                        }\n                        res.setHeaders(configureHeaders);\n                        if (responseHeaders != null && responseHeaders.isMap()) {\n                            res.setHeaders(responseHeaders.getValue());\n                        }\n                        if (responseDelay != null) {\n                            res.setDelay(responseDelay.getAsInt());\n                        }\n                    }\n                    if (response != null && !response.isNull()) {\n                        res.setBody(response.getAsByteArray());\n                        if (res.getContentType() == null) {\n                            ResourceType rt = ResourceType.fromObject(response.getValue());\n                            if (rt != null) {\n                                res.setContentType(rt.contentType);\n                            }\n                        }\n                    }\n                    if (responseStatus != null) {\n                        res.setStatus(responseStatus.getAsInt());\n                    }\n                    if (prevEngine != null) {\n                        ScenarioEngine.set(prevEngine);\n                    }\n                    if (mockInterceptor != null) {\n                        mockInterceptor.intercept(req, res, scenario);\n                    }\n                    return res;\n                }\n            }\n        }\n        logger.warn(\"no scenarios matched, returning 404: {}\", req); // NOTE: not logging with engine.logger\n        if (prevEngine != null) {\n            ScenarioEngine.set(prevEngine);\n        }\n        return new Response(404);\n    }\n    \n    private static ScenarioEngine initEngine(ScenarioRuntime runtime, Map<String, Variable> globals, Request req) {\n        ScenarioEngine engine = new ScenarioEngine(runtime.engine.getConfig(), runtime, new HashMap(globals), runtime.logger);        \n        engine.init();\n        engine.setVariable(ScenarioEngine.REQUEST_URL_BASE, req.getUrlBase());\n        engine.setVariable(ScenarioEngine.REQUEST_PATH, req.getPath());\n        engine.setVariable(ScenarioEngine.REQUEST_URI, req.getPathRaw());\n        engine.setVariable(ScenarioEngine.REQUEST_METHOD, req.getMethod());\n        engine.setVariable(ScenarioEngine.REQUEST_HEADERS, req.getHeaders());\n        engine.setVariable(ScenarioEngine.REQUEST, req.getBodyConverted());\n        engine.setVariable(REQUEST_PARAMS, req.getParams());\n        engine.setVariable(REQUEST_BYTES, req.getBody());\n        engine.setRequest(req);\n        runtime.featureRuntime.setMockEngine(engine);\n        ScenarioEngine.set(engine);\n        Map<String, List<Map<String, Object>>> parts = req.getMultiParts();\n        if (parts != null) {\n            engine.setHiddenVariable(REQUEST_PARTS, parts);\n        }\n        return engine;\n    }\n\n    private Result executeScenarioSteps(Feature feature, ScenarioRuntime runtime, Scenario scenario, ScenarioActions actions) {\n        Result result = PASSED;\n        for (Step step : scenario.getSteps()) {\n            result = StepRuntime.execute(step, actions);\n            if (result.isAborted()) {\n                runtime.logger.debug(\"abort at {}:{}\", feature, step.getLine());\n                break;\n            }\n            if (result.isFailed()) {\n                String message = \"server-side scenario failed, \" + feature + \":\" + step.getLine()\n                        + \"\\n\" + step.toString() + \"\\n\" + result.getError().getMessage();\n                runtime.logger.error(message);\n                break;\n            }\n        }\n        return result;\n    }\n\n    private boolean isMatchingScenario(Scenario scenario, ScenarioEngine engine) {\n        String expression = StringUtils.trimToNull(scenario.getName() + scenario.getDescription());\n        if (expression == null) {\n            engine.logger.debug(\"default scenario matched at line: {} - {}\", scenario.getLine(), engine.getVariable(ScenarioEngine.REQUEST_URI));\n            return true;\n        }\n        try {\n            Variable v = engine.evalJs(expression);\n            if (v.isTrue()) {\n                engine.logger.debug(\"scenario matched at line {}: {}\", scenario.getLine(), expression);\n                return true;\n            } else {\n                engine.logger.trace(\"scenario skipped at line {}: {}\", scenario.getLine(), expression);\n                return false;\n            }\n        } catch (Exception e) {\n            engine.logger.warn(\"scenario match evaluation failed at line {}: {} - {}\", scenario.getLine(), expression, e + \"\");\n            return false;\n        }\n    }\n\n    public boolean pathMatches(String pattern) {\n        String uri = LOCAL_REQUEST.get().getPath();\n        if (uri.equals(pattern)) {\n            return true;\n        }\n        Map<String, String> pathParams = HttpUtils.parseUriPattern(pattern, uri);\n        if (pathParams == null) {\n            return false;\n        } else {\n            ScenarioEngine.get().setVariable(PATH_PARAMS, pathParams);\n            return true;\n        }\n    }\n\n    public boolean paramExists(String name) {\n        Map<String, List<String>> params = LOCAL_REQUEST.get().getParams();\n        return params != null && params.containsKey(name);\n\n    }\n\n    public String paramValue(String name) {\n        return LOCAL_REQUEST.get().getParam(name);\n    }\n\n    public boolean methodIs(String name) { // TODO no more supporting array arg\n        return LOCAL_REQUEST.get().getMethod().equalsIgnoreCase(name);\n    }\n\n    public boolean typeContains(String text) {\n        String contentType = LOCAL_REQUEST.get().getContentType();\n        return contentType != null && contentType.contains(text);\n    }\n\n    public boolean acceptContains(String text) {\n        String acceptHeader = LOCAL_REQUEST.get().getHeader(\"Accept\");\n        return acceptHeader != null && acceptHeader.contains(text);\n    }\n\n    public boolean headerContains(String name, String value) {\n        List<String> values = LOCAL_REQUEST.get().getHeaderValues(name);\n        if (values != null) {\n            for (String v : values) {\n                if (v.contains(value)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    public Object bodyPath(String path) {\n        Object body = LOCAL_REQUEST.get().getBodyConverted();\n        if (body == null) {\n            return null;\n        }\n        if (path.startsWith(\"/\")) {\n            Variable v = ScenarioEngine.evalXmlPath(new Variable(body), path);\n            if (v.isNotPresent()) {\n                return null;\n            } else {\n                return JsValue.fromJava(v.getValue());\n            }\n        } else {\n            Json json = Json.of(body);\n            Object result;\n            try {\n                result = json.get(path);\n            } catch (Exception e) {\n                return null;\n            }\n            return JsValue.fromJava(result);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/MockInterceptor.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.http.Request;\nimport com.intuit.karate.http.Response;\n\n@FunctionalInterface\npublic interface MockInterceptor {\n\n  void intercept(Request req, Response res, Scenario scenario);\n\n}"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/MockServer.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.http.*;\nimport com.linecorp.armeria.server.HttpService;\nimport com.linecorp.armeria.server.Server;\nimport com.linecorp.armeria.server.ServerBuilder;\nimport java.io.File;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\n/**\n *\n * @author pthomas3\n */\npublic class MockServer extends HttpServer {\n\n    private MockServer(ServerBuilder sb) {\n        super(sb);\n    }\n\n    public static class Builder {\n\n        Builder(Feature feature) {\n            this.features = Arrays.asList(feature);\n        }\n\n        Builder(List<Feature> features) {\n            this.features = features;\n        }\n\n        final List<Feature> features;\n        int port;\n        boolean ssl;\n        boolean watch;\n        File certFile;\n        File keyFile;\n        Map<String, Object> args;\n        String prefix = null;\n        MockInterceptor interceptor = null;\n        boolean keepOriginalHeaders;\n\n        public Builder watch(boolean value) {\n            watch = value;\n            return this;\n        }\n\n        public Builder http(int value) {\n            port = value;\n            return this;\n        }\n\n        public Builder https(int value) {\n            ssl = true;\n            port = value;\n            return this;\n        }\n\n        public Builder certFile(File value) {\n            certFile = value;\n            return this;\n        }\n\n        public Builder keyFile(File value) {\n            keyFile = value;\n            return this;\n        }\n\n        public Builder pathPrefix(String prefix) {\n            if (prefix.charAt(0) != '/') {\n                prefix = \"/\" + prefix;\n            }\n            this.prefix = prefix;\n            return this;\n        }\n\n        public Builder args(Map<String, Object> value) {\n            args = value;\n            return this;\n        }\n\n        public Builder arg(String name, Object value) {\n            if (args == null) {\n                args = new HashMap();\n            }\n            args.put(name, value);\n            return this;\n        }\n\n        public Builder interceptor(MockInterceptor value) {\n            interceptor = value;\n            return this;\n        }\n\n        public Builder keepOriginalHeaders(boolean value) {\n            keepOriginalHeaders = value;\n            return this;\n        }\n\n        public MockServer build() {\n            ServerBuilder sb = Server.builder();\n            sb.requestTimeoutMillis(0);\n            if (ssl) {\n                sb.https(port);\n                SslContextFactory factory = new SslContextFactory();\n                factory.setCertFile(certFile);\n                factory.setKeyFile(keyFile);\n                factory.build();\n                sb.tls(factory.getCertFile(), factory.getKeyFile());\n            } else {\n                sb.http(port);\n            }\n\n            ServerHandler handler = watch ? new ReloadingMockHandler(features, args, prefix, interceptor) : new MockHandler(prefix, features, args, interceptor);\n\n            HttpServerHandler.Builder serverHandlerBuilder = HttpServerHandler.Builder.builder();\n            serverHandlerBuilder.handler(handler);\n\n            if (keepOriginalHeaders) {\n                HttpHeaderTracking headerTracking = new GenericHttpHeaderTracking();\n                sb.http1HeaderNaming(http2HeaderName ->\n                        headerTracking.getOriginalHeader(String.valueOf(http2HeaderName)));\n\n                serverHandlerBuilder.httpHeaderTracking(headerTracking);\n            }\n\n            HttpService service = serverHandlerBuilder.build();\n            sb.service(\"prefix:\" + (prefix == null ? \"/\" : prefix), service);\n            return new MockServer(sb);\n        }\n\n    }\n\n    private static class ReloadingMockHandler implements ServerHandler {\n\n        private final Map<String, Object> args;\n        private MockHandler handler;\n        private final LinkedHashMap<File, Long> files = new LinkedHashMap<>();\n        private final String prefix;\n        private final MockInterceptor interceptor;\n\n        public ReloadingMockHandler(List<Feature> features, Map<String, Object> args, String prefix, MockInterceptor interceptor) {\n            this.args = args;\n            this.prefix = prefix;\n            this.interceptor = interceptor;\n            for (Feature f : features) {\n                this.files.put(f.getResource().getFile(), f.getResource().getFile().lastModified());\n            }\n            logger.debug(\"watch mode init - {}\", files);\n            handler = new MockHandler(prefix, features, args, this.interceptor);\n        }\n\n        @Override\n        public Response handle(Request request) {\n            boolean reload = files.entrySet().stream().reduce(false, (modified, entry) -> entry.getKey().lastModified() > entry.getValue(), (a, b) -> a || b);\n            if (reload) {\n                List<Feature> features = files.keySet().stream().map(f -> Feature.read(f)).collect(Collectors.toList());\n                handler = new MockHandler(prefix, features, args, interceptor);\n            }\n            return handler.handle(request);\n        }\n\n    }\n\n    public static Builder feature(String path) {\n        return new Builder(Feature.read(path));\n    }\n\n    public static Builder feature(File file) {\n        return new Builder(Feature.read(file));\n    }\n\n    public static Builder feature(Feature feature) {\n        return new Builder(feature);\n    }\n    \n    public static Builder featurePaths(List<String> paths) {\n        return new Builder(paths.stream().map(p -> Feature.read(p)).collect(Collectors.toList()));\n    }    \n\n    public static Builder featurePaths(String... paths) {\n        return featurePaths(Arrays.asList(paths));\n    }\n\n    public static Builder featureFiles(List<File> features) {\n        return new Builder(features.stream().map(file -> Feature.read(file)).collect(Collectors.toList()));\n    }\n\n    public static Builder features(List<Feature> features) {\n        return new Builder(features);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ParallelProcessor.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ExecutorService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic abstract class ParallelProcessor<T> {\n\n    private static final Logger logger = LoggerFactory.getLogger(ParallelProcessor.class);\n\n    private final ExecutorService executor;\n    private final ExecutorService monitor;\n    private final Iterator<T> publisher;\n    private final List<CompletableFuture> futures = new ArrayList();\n\n    public ParallelProcessor(ExecutorService executor, Iterator<T> publisher, ExecutorService monitor) {\n        this.executor = executor;\n        this.publisher = publisher;\n        this.monitor = monitor;\n    }\n\n    private Runnable toRunnable(final CompletableFuture prevFuture, final T next, final CompletableFuture future) {\n        return () -> {\n            if (prevFuture != null) {\n                prevFuture.join();\n            }\n            try {\n                process(next);\n            } catch (Exception e) {\n                logger.error(\"[parallel] input item failed: {}\", e.getMessage());\n            }\n            future.complete(Boolean.TRUE);\n        };\n    }\n\n    public void execute() {\n        CompletableFuture prevFuture = null;\n        while (publisher.hasNext()) {\n            final CompletableFuture future = new CompletableFuture();\n            futures.add(future);\n            T next = publisher.next();\n            boolean sync = shouldRunSynchronously(next);\n            executor.submit(toRunnable(prevFuture, next, future));\n            prevFuture = sync ? future : null;\n        }\n        final CompletableFuture[] futuresArray = futures.toArray(new CompletableFuture[futures.size()]);\n        monitor.submit(() -> {\n            CompletableFuture.allOf(futuresArray).join();\n            onComplete();\n        });\n    }\n\n    public boolean shouldRunSynchronously(T in) {\n        // parallel by default\n        // but allow a per work-item strategy\n        return false;\n    }\n\n    public abstract void process(T in);\n\n    public abstract void onComplete();\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ParserErrorListener.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.util.BitSet;\nimport org.antlr.v4.runtime.ANTLRErrorListener;\nimport org.antlr.v4.runtime.Parser;\nimport org.antlr.v4.runtime.RecognitionException;\nimport org.antlr.v4.runtime.Recognizer;\nimport org.antlr.v4.runtime.atn.ATNConfigSet;\nimport org.antlr.v4.runtime.dfa.DFA;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ParserErrorListener implements ANTLRErrorListener {\n\n    private static final Logger logger = LoggerFactory.getLogger(FeatureParser.class);\n\n    private String message;\n    private int line = -1;\n    private int position = -1;\n    private Object offendingSymbol;\n\n    public boolean isFail() {\n        return message != null;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public int getLine() {\n        return line;\n    }\n\n    public int getPosition() {\n        return position;\n    }\n\n    public Object offendingSymbol() {\n        return offendingSymbol;\n    }\n\n    @Override\n    public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int position, String message, RecognitionException e) {\n        // logger.error(\"syntax error: {}\", message);\n        this.message = message;\n        this.line = line;\n        this.position = position;\n        this.offendingSymbol = offendingSymbol;\n    }\n\n    @Override\n    public void reportAmbiguity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, boolean exact, BitSet ambigAlts, ATNConfigSet configs) {\n//        if (logger.isTraceEnabled()) {\n//            logger.trace(\"reportAmbiguity: {} {} {} {} {} {} {}\", recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs);\n//        }\n    }\n\n    @Override\n    public void reportAttemptingFullContext(Parser recognizer, DFA dfa, int startIndex, int stopIndex, BitSet conflictingAlts, ATNConfigSet configs) {\n//        if (logger.isTraceEnabled()) {\n//            logger.trace(\"reportAttemptingFullContext: {} {} {} {} {} {}\", recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs);\n//        }\n    }\n\n    @Override\n    public void reportContextSensitivity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, int prediction, ATNConfigSet configs) {\n//        if (logger.isTraceEnabled()) {\n//            logger.trace(\"reportContextSensitivity: {} {} {} {} {} {}\", recognizer, dfa, startIndex, stopIndex, prediction, configs);\n//        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/PerfEvent.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\n/**\n *\n * @author pthomas3\n */\npublic class PerfEvent {\n\n    private final String name;\n    private final long startTime;\n    private final long endTime;\n    private final int statusCode;\n\n    private boolean failed;\n    private String message;\n\n    public PerfEvent(long startTime, long endTime, String name, int statusCode) {\n        this.name = name;\n        this.startTime = startTime;\n        this.endTime = endTime;\n        this.statusCode = statusCode;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public long getStartTime() {\n        return startTime;\n    }\n\n    public long getEndTime() {\n        return endTime;\n    }\n\n    public int getStatusCode() {\n        return statusCode;\n    }\n\n    public boolean isFailed() {\n        return failed;\n    }\n\n    public void setFailed(boolean failed) {\n        this.failed = failed;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void setMessage(String message) {\n        this.message = message;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"[name: \").append(name);\n        sb.append(\", startTime: \").append(startTime);\n        sb.append(\", endTime: \").append(endTime);\n        sb.append(\", statusCode: \").append(statusCode);\n        sb.append(\"]\");\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Plugin.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Plugin {\n\n    void onFailure(StepResult stepResult);\n\n    Map<String, Object> afterScenario();\n\n    default ScenarioRuntime getRuntime() {\n        return ScenarioEngine.get().runtime;\n    }\n\n    List<String> methodNames();\n\n    public static List<String> methodNames(Class clazz) {\n        List<String> list = new ArrayList();\n        for (Method m : clazz.getMethods()) {\n            if (m.getAnnotation(AutoDef.class) != null) {\n                list.add(m.getName());\n            }\n        }\n        return list;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/PluginFactory.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic interface PluginFactory {\n    \n    Plugin create(ScenarioRuntime runtime, Map<String, Object> options);\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Result.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.Constants;\nimport com.intuit.karate.report.ReportUtils;\nimport com.intuit.karate.KarateException;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class Result {\n\n    private static final String PASSED = \"passed\";\n    private static final String FAILED = \"failed\";\n    private static final String SKIPPED = \"skipped\";\n\n    private static final boolean INCLUDE_METHOD_KARATE_JSON = Boolean.parseBoolean(System.getProperty(Constants.KARATE_CONFIG_INCL_RESULT_METHOD));\n\n    private final String status;\n    private final long durationNanos;\n    private final boolean aborted;\n    private final Throwable error;\n    private final boolean skipped;\n    private final StepRuntime.MethodMatch matchingMethod;\n    \n    private final long startTime;\n    private final long endTime;\n\n    public Map<String, Object> toCucumberJson() {\n        Map<String, Object> map = new HashMap(error == null ? 2 : 3);\n        map.put(\"status\", status);\n        map.put(\"duration\", durationNanos);\n        if (error != null) {\n            map.put(\"error_message\", error.getMessage());\n        }\n        return map;\n    }\n\n    public static Result fromKarateJson(Map<String, Object> map) {\n        Long startTime = (Long) map.get(\"startTime\");\n        if (startTime == null) {\n            startTime = System.currentTimeMillis();\n        }\n        String status = (String) map.get(\"status\");\n        Number num = (Number) map.get(\"nanos\");\n        long durationNanos = num == null ? 0 : num.longValue();        \n        String errorMessage = (String) map.get(\"errorMessage\");\n        Throwable error = errorMessage == null ? null : new KarateException(errorMessage);\n        Boolean aborted = (Boolean) map.get(\"aborted\");\n        if (aborted == null) {\n            aborted = false;\n        }\n        StepRuntime.MethodMatch matchingMethod = null;\n        if (INCLUDE_METHOD_KARATE_JSON) {\n            String jsonMatchingMethod = (String) map.get(\"matchingMethod\");\n            if (jsonMatchingMethod != null) {\n                matchingMethod = StepRuntime.MethodMatch.getBySignatureAndArgs(jsonMatchingMethod);\n            }\n        }\n        return new Result(startTime, status, durationNanos, error, aborted, matchingMethod);\n    }\n\n    public Map<String, Object> toKarateJson() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"status\", status);\n        map.put(\"millis\", getDurationMillis()); // not used in fromKarateJson()\n        map.put(\"nanos\", durationNanos);\n        map.put(\"startTime\", startTime);\n        map.put(\"endTime\", endTime);\n        if (error != null) {\n            map.put(\"errorMessage\", error.getMessage());\n        }\n        if (aborted) {\n            map.put(\"aborted\", true);\n        }\n        if (INCLUDE_METHOD_KARATE_JSON && matchingMethod != null) {\n            map.put(\"matchingMethod\", matchingMethod.toString());\n        }\n        return map;\n    }\n\n    private Result(long startTime, String status, long nanos, Throwable error, boolean aborted, StepRuntime.MethodMatch matchingMethod) {\n        this.startTime = startTime;\n        this.status = status;\n        this.durationNanos = nanos;\n        this.endTime = startTime + Math.round(ReportUtils.nanosToMillis(nanos));\n        this.error = error;\n        this.aborted = aborted;\n        this.matchingMethod = matchingMethod;\n        skipped = SKIPPED.equals(status);\n    }\n\n    public boolean isSkipped() {\n        return skipped;\n    }\n\n    public boolean isFailed() {\n        return error != null;\n    }\n\n    public boolean isAborted() {\n        return aborted;\n    }\n\n    public Throwable getError() {\n        return error;\n    }\n    \n    public String getErrorMessage() {\n        return error == null ? null : error.getMessage();\n    }\n\n    public static Result passed(long startTime, long nanos) {\n        return passed(startTime, nanos, null);\n    }\n    public static Result passed(long startTime, long nanos, StepRuntime.MethodMatch matchingMethod) {\n        return new Result(startTime, PASSED, nanos, null, false, matchingMethod);\n    }\n\n    public static Result failed(long startTime, long nanos, Throwable error, Step step) {\n        return failed(startTime, nanos, error, step, null);\n    }\n\n    public static Result failed(long startTime, long nanos, Throwable error, Step step, StepRuntime.MethodMatch matchingMethod) {\n        String message = error.getMessage();\n        if (message == null) {\n            message = error + \"\"; // make sure we show something meaningful\n        }\n        error = new KarateException(message + \"\\n\" + step.getDebugInfo());\n        StackTraceElement[] newTrace = new StackTraceElement[]{\n            new StackTraceElement(\"<feature>\", \": \" + step.getPrefix() + \" \" + step.getText() + \" \", step.getDebugInfo(), step.getLine())\n        };\n        error.setStackTrace(newTrace);\n        return new Result(startTime, FAILED, nanos, error, false, matchingMethod);\n    }\n\n    public static Result skipped(long startTime) {\n        return new Result(startTime, SKIPPED, 0, null, false, null);\n    }\n\n    public static Result aborted(long startTime, long nanos) {\n        return aborted(startTime, nanos, null);\n    }\n\n    public static Result aborted(long startTime, long nanos, StepRuntime.MethodMatch matchingMethod) {\n        return new Result(startTime, PASSED, nanos, null, true, matchingMethod);\n    }\n\n    public String getStatus() {\n        return status;\n    }\n\n    public long getDurationNanos() {\n        return durationNanos;\n    }\n\n    public double getDurationMillis() {\n        return ReportUtils.nanosToMillis(durationNanos);\n    }\n\n    public long getStartTime() {\n        return startTime;\n    }\n\n    public long getEndTime() {\n        return endTime;\n    }    \n\n    public StepRuntime.MethodMatch getMatchingMethod() {\n        return matchingMethod;\n    }\n\n    @Override\n    public String toString() {\n        return status;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/RuntimeHookFactory.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.RuntimeHook;\n\n/**\n *\n * @author pthomas3\n */\npublic interface RuntimeHookFactory {\n    \n    RuntimeHook create();\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Scenario.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * @author pthomas3\n */\npublic class Scenario {\n\n    private final Feature feature;\n    private final FeatureSection section;\n    private final int exampleIndex;\n\n    private int line;\n    private List<Tag> tags;\n    private String name;\n    private String description;\n    private List<Step> steps;\n    private Map<String, Object> exampleData;\n    private String dynamicExpression;\n\n    public Scenario(Feature feature, FeatureSection section, int exampleIndex) {\n        this.feature = feature;\n        this.section = section;\n        this.exampleIndex = exampleIndex;\n    }\n\n    public boolean isEqualTo(Scenario other) {\n        return other.section.getIndex() == section.getIndex() && other.exampleIndex == exampleIndex;\n    }\n\n    public String getNameAndDescription() {\n        String temp = \"\";\n        if (name != null) {\n            temp = temp + name;\n        }\n        if (description != null) {\n            if (!temp.isEmpty()) {\n                temp = temp + \" \";\n            }\n            temp = temp + description;\n        }\n        return temp;\n    }\n\n    public String getRefIdAndName() {\n        if (name == null) {\n            return getRefId();\n        } else {\n            return getRefId() + \" \" + name;\n        }\n    }\n\n    // only called for dynamic scenarios\n    public Scenario copy(int exampleIndex) {\n        Scenario s = new Scenario(feature, section, exampleIndex);\n        s.name = name;\n        s.description = description;\n        s.tags = tags;\n        s.line = line;\n        s.dynamicExpression = dynamicExpression;\n        s.steps = new ArrayList(steps.size());\n        for (Step step : steps) {\n            Step temp = new Step(s, step.getIndex());\n            s.steps.add(temp);\n            temp.setLine(step.getLine());\n            temp.setEndLine(step.getEndLine());\n            temp.setPrefix(step.getPrefix());\n            temp.setText(step.getText());\n            temp.setDocString(step.getDocString());\n            temp.setTable(step.getTable());\n        }\n        return s;\n    }\n\n    public void replace(String token, String value) {\n        if (value == null) {\n            // this can happen for a dynamic scenario outline !\n            // give up trying a cucumber-style placeholder sub\n            // user should be fine with karate-style plain-old variables\n            return;\n        }\n        name = name.replace(token, value);\n        for (Step step : steps) {\n            String text = step.getText();\n            step.setText(text.replace(token, value));\n            String docString = step.getDocString();\n            if (docString != null) {\n                step.setDocString(docString.replace(token, value));\n            }\n            Table table = step.getTable();\n            if (table != null) {\n                step.setTable(table.replace(token, value));\n            }\n        }\n    }\n\n    public Step getStepByLine(int line) {\n        for (Step step : getStepsIncludingBackground()) {\n            if (step.getLine() == line) {\n                return step;\n            }\n        }\n        return null;\n    }\n\n    public String getRefId() {\n        int num = section.getIndex() + 1;\n        String meta = \"[\" + num;\n        if (exampleIndex != -1) {\n            meta = meta + \".\" + (exampleIndex + 1);\n        }\n        return meta + \":\" + line + \"]\";\n    }\n\n    public String getDebugInfo() {\n        return feature + \":\" + line;\n    }\n\n    public String getUniqueId() {\n        String id = feature.getResource().getPackageQualifiedName() + \"_\" + (section.getIndex() + 1);\n        return exampleIndex == -1 ? id : id + \"_\" + (exampleIndex + 1);\n    }\n\n    public List<Step> getBackgroundSteps() {\n        return feature.isBackgroundPresent() ? feature.getBackground().getSteps() : Collections.EMPTY_LIST;\n    }\n\n    public List<Step> getStepsIncludingBackground() {\n        List<Step> background = feature.isBackgroundPresent() ? feature.getBackground().getSteps() : null;\n        int count = background == null ? steps.size() : steps.size() + background.size();\n        List<Step> temp = new ArrayList(count);\n        if (background != null) {\n            temp.addAll(background);\n        }\n        temp.addAll(steps);\n        return temp;\n    }\n\n    private Tags tagsEffective; // cache\n\n    public Tags getTagsEffective() {\n        if (tagsEffective == null) {\n            tagsEffective = Tags.merge(feature.getTags(), tags);\n        }\n        return tagsEffective;\n    }\n\n    public FeatureSection getSection() {\n        return section;\n    }\n\n    public Feature getFeature() {\n        return feature;\n    }\n\n    public int getLine() {\n        return line;\n    }\n\n    public void setLine(int line) {\n        this.line = line;\n    }\n\n    public List<Tag> getTags() {\n        return tags;\n    }\n\n    public void setTags(List<Tag> tags) {\n        this.tags = tags;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public void setDescription(String description) {\n        this.description = description;\n    }\n\n    public List<Step> getSteps() {\n        return steps;\n    }\n\n    public void setSteps(List<Step> steps) {\n        this.steps = steps;\n    }\n\n    public boolean isOutlineExample() {\n        return exampleIndex != -1;\n    }\n\n    public boolean isDynamic() {\n        return dynamicExpression != null;\n    }\n\n    public String getDynamicExpression() {\n        return dynamicExpression;\n    }\n\n    public void setDynamicExpression(String dynamicExpression) {\n        this.dynamicExpression = dynamicExpression;\n    }\n\n    public Map<String, Object> getExampleData() {\n        return exampleData;\n    }\n\n    public void setExampleData(Map<String, Object> exampleData) {\n        this.exampleData = exampleData;\n    }\n\n    public int getExampleIndex() {\n        return exampleIndex;\n    }\n\n    @Override\n    public String toString() {\n        return feature.toString() + getRefId();\n    }\n\n    public URI getUriToLineNumber() {\n        return URI.create(feature.getResource().getUri() + \"?line=\" + line);\n    }\n\n    public boolean isSetup() {\n        return getTags() == null ? false :\n            getTags()\n                .stream()\n                .map(Tag::getName)\n                .anyMatch((t)->t.equals(Tag.SETUP));\n    }\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ScenarioBridge.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.*;\nimport com.intuit.karate.graal.*;\nimport com.intuit.karate.http.*;\nimport com.intuit.karate.shell.Command;\nimport org.graalvm.polyglot.Value;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * @author pthomas3\n */\npublic class ScenarioBridge implements PerfContext {\n\n    private final ScenarioEngine ENGINE;\n\n    protected ScenarioBridge(ScenarioEngine engine) {\n        ENGINE = engine;\n    }\n\n    public void abort() {\n        getEngine().setAborted(true);\n    }\n\n    public Object append(Value... vals) {\n        List list = new ArrayList();\n        JsList jsList = new JsList(list);\n        if (vals.length == 0) {\n            return jsList;\n        }\n        Value val = vals[0];\n        if (val.hasArrayElements()) {\n            list.addAll(val.as(List.class));\n        } else {\n            list.add(val.as(Object.class));\n        }\n        if (vals.length == 1) {\n            return jsList;\n        }\n        for (int i = 1; i < vals.length; i++) {\n            Value v = vals[i];\n            if (v.hasArrayElements()) {\n                list.addAll(v.as(List.class));\n            } else {\n                list.add(v.as(Object.class));\n            }\n        }\n        return jsList;\n    }\n\n    private Object appendToInternal(String varName, Value... vals) {\n        ScenarioEngine engine = getEngine();\n        Variable var = engine.vars.get(varName);\n        if (!var.isList()) {\n            return null;\n        }\n        List list = var.getValue();\n        for (Value v : vals) {\n            if (v.hasArrayElements()) {\n                list.addAll(v.as(List.class));\n            } else {\n                Object temp = v.as(Object.class);\n                list.add(temp);\n            }\n        }\n        engine.setVariable(varName, list);\n        return new JsList(list);\n    }\n\n    public Object appendTo(Value ref, Value... vals) {\n        if (ref.isString()) {\n            return appendToInternal(ref.asString(), vals);\n        }\n        List list;\n        if (ref.hasArrayElements()) {\n            list = new JsValue(ref).getAsList(); // make sure we unwrap the \"original\" list\n        } else {\n            list = new ArrayList();\n        }\n        for (Value v : vals) {\n            if (v.hasArrayElements()) {\n                list.addAll(v.as(List.class));\n            } else {\n                Object temp = v.as(Object.class);\n                list.add(temp);\n            }\n        }\n        return new JsList(list);\n    }\n\n    public Object call(String fileName) {\n        return call(false, fileName, null);\n    }\n\n    public Object call(String fileName, Value arg) {\n        return call(false, fileName, arg);\n    }\n\n    public Object call(boolean sharedScope, String fileName) {\n        return call(sharedScope, fileName, null);\n    }\n\n    public Object call(boolean sharedScope, String fileName, Value arg) {\n        ScenarioEngine engine = getEngine();\n        Variable called = new Variable(engine.fileReader.readFile(fileName));\n        Variable result = engine.call(called, arg == null ? null : new Variable(arg), sharedScope);\n        if (sharedScope) {\n            if (result.isMap()) {\n                engine.setVariables(result.getValue());\n            }\n        }\n        return JsValue.fromJava(result.getValue());\n    }\n\n    private static Object callSingleResult(ScenarioEngine engine, Object o) throws Exception {\n        if (o instanceof Exception) {\n            engine.logger.warn(\"callSingle() cached result is an exception\");\n            throw (Exception) o;\n        }\n        // clone so that threads see the same data snapshot\n        // we also attach js functions\n        o = engine.JS.attachAll(o);\n        return JsValue.fromJava(o);\n    }\n\n    public Object callSingle(String fileName) throws Exception {\n        return callSingle(fileName, null);\n    }\n\n    public Object callSingle(String fileName, Value arg) throws Exception {\n        ScenarioEngine engine = getEngine();\n        final Map<String, Object> CACHE = engine.runtime.featureRuntime.suite.callSingleCache;\n        int minutes = engine.getConfig().getCallSingleCacheMinutes();\n        if ((minutes == 0) && CACHE.containsKey(fileName)) {\n            engine.logger.trace(\"callSingle cache hit: {}\", fileName);\n            return callSingleResult(engine, CACHE.get(fileName));\n        }\n        long startTime = System.currentTimeMillis();\n        engine.logger.trace(\"callSingle waiting for lock: {}\", fileName);\n        synchronized (CACHE) { // lock\n            if ((minutes == 0) && CACHE.containsKey(fileName)) { // retry\n                long endTime = System.currentTimeMillis() - startTime;\n                engine.logger.warn(\"this thread waited {} milliseconds for callSingle lock: {}\", endTime, fileName);\n                return callSingleResult(engine, CACHE.get(fileName));\n            }\n            // this thread is the 'winner'\n            engine.logger.info(\">> lock acquired, begin callSingle: {}\", fileName);\n            Object result = null;\n            File cacheFile = null;\n            if (minutes > 0) {\n                String cleanedName = StringUtils.toIdString(fileName);\n                String cacheFileName = engine.getConfig().getCallSingleCacheDir() + File.separator + cleanedName + \".txt\";\n                cacheFile = new File(cacheFileName);\n                long since = System.currentTimeMillis() - minutes * 60 * 1000;\n                if (cacheFile.exists()) {\n                    long lastModified = cacheFile.lastModified();\n                    if (lastModified > since) {\n                        String json = FileUtils.toString(cacheFile);\n                        result = JsonUtils.fromJson(json);\n                        engine.logger.info(\"callSingleCache hit: {}\", cacheFile);\n                    } else {\n                        engine.logger.info(\"callSingleCache stale, last modified {} - is before {} (minutes: {})\",\n                                lastModified, since, minutes);\n                    }\n                } else {\n                    engine.logger.info(\"callSingleCache file does not exist, will create: {}\", cacheFile);\n                }\n            }\n            if (result == null) {\n                String sFileName = (fileName.contains(\"?\")) ? StringUtils.split(fileName, '?', false).get(0) : fileName;\n                Variable called = new Variable(read(sFileName));\n                Variable argVar;\n                if (arg == null || arg.isNull()) {\n                    argVar = null;\n                } else {\n                    argVar = new Variable(arg);\n                }\n                Variable resultVar;\n                try {\n                    resultVar = engine.call(called, argVar, false);\n                } catch (Exception e) {\n                    // don't retain any vestiges of graal-js\n                    RuntimeException re = new RuntimeException(e.getMessage());\n                    // we do this so that an exception is also \"cached\"\n                    resultVar = new Variable(re); // will be thrown at end\n                    engine.logger.warn(\"callSingle() will cache an exception\");\n                }\n                if (minutes > 0) { // cacheFile will be not null\n                    if (resultVar.isMapOrList()) {\n                        String json = resultVar.getAsString();\n                        FileUtils.writeToFile(cacheFile, json);\n                        engine.logger.info(\"callSingleCache write: {}\", cacheFile);\n                    } else {\n                        engine.logger.warn(\"callSingleCache write failed, not json-like: {}\", resultVar);\n                    }\n                }\n                result = resultVar.getValue();\n            }\n            CACHE.put(fileName, result);\n            engine.logger.info(\"<< lock released, cached callSingle: {}\", fileName);\n            return callSingleResult(engine, result);\n        }\n    }\n\n    public Object callonce(String path) {\n        return callonce(false, path);\n    }\n\n    public Object callonce(boolean sharedScope, String path) {\n        String exp = \"read('\" + path + \"')\";\n        Variable v = getEngine().call(true, exp, sharedScope);\n        return JsValue.fromJava(v.getValue());\n    }\n\n    @Override\n    public void capturePerfEvent(String name, long startTime, long endTime) {\n        PerfEvent event = new PerfEvent(startTime, endTime, name, 200);\n        getEngine().capturePerfEvent(event);\n    }\n\n    public Object channel(String type) {\n        return getEngine().channelSession(type);\n    }\n\n    public Object compareImage(Object baseline, Object latest, Value... optionsVal) {\n        if (optionsVal.length > 0 && !optionsVal[0].hasMembers()) {\n            throw new RuntimeException(\"invalid image comparison options: expected map\");\n        }\n\n        Map<String, Object> options = new HashMap<>();\n        if (optionsVal.length > 0) {\n            for (String k : optionsVal[0].getMemberKeys()) {\n                options.put(k, optionsVal[0].getMember(k).as(Object.class));\n            }\n        }\n\n        Map<String, Object> params = new HashMap<>();\n        params.put(\"baseline\", baseline);\n        params.put(\"latest\", latest);\n        params.put(\"options\", options);\n\n        return JsValue.fromJava(getEngine().compareImageInternal(params));\n    }\n\n    public void configure(String key, Value value) {\n        getEngine().configure(key, new Variable(value));\n    }\n\n    public Object consume(String type) {\n        getEngine().logger.warn(\"karate.consume() is deprecated, use karate.channel() instead\");\n        return channel(type);\n    }\n\n    public Object distinct(Value o) {\n        if (!o.hasArrayElements()) {\n            return JsList.EMPTY;\n        }\n        long count = o.getArraySize();\n        Set<Object> set = new LinkedHashSet();\n        for (int i = 0; i < count; i++) {\n            Object value = JsValue.toJava(o.getArrayElement(i));\n            set.add(value);\n        }\n        return JsValue.fromJava(new ArrayList(set));\n    }\n\n    public String doc(Value v) {\n        Map<String, Object> arg;\n        if (v.isString()) {\n            arg = Collections.singletonMap(\"read\", v.asString());\n        } else if (v.hasMembers()) {\n            arg = new JsValue(v).getAsMap();\n        } else {\n            getEngine().logger.warn(\"doc - unexpected argument: {}\", v);\n            return null;\n        }\n        return getEngine().docInternal(arg);\n    }\n\n    public void embed(Object o, String contentType) {\n        ResourceType resourceType;\n        if (contentType == null) {\n            resourceType = ResourceType.fromObject(o, ResourceType.BINARY);\n        } else {\n            resourceType = ResourceType.fromContentType(contentType);\n        }\n        getEngine().runtime.embed(JsonUtils.toBytes(o), resourceType);\n    }\n\n    public Object eval(String exp) {\n        Variable result = getEngine().evalJs(exp);\n        return JsValue.fromJava(result.getValue());\n    }\n\n    public String exec(Value value) {\n        if (value.isString()) {\n            return execInternal(Collections.singletonMap(\"line\", value.asString()));\n        } else if (value.hasArrayElements()) {\n            List args = new JsValue(value).getAsList();\n            return execInternal(Collections.singletonMap(\"args\", args));\n        } else {\n            return execInternal(new JsValue(value).getAsMap());\n        }\n    }\n\n    private String execInternal(Map<String, Object> options) {\n        Command command = getEngine().fork(false, options);\n        command.waitSync();\n        return command.getAppender().collect();\n    }\n\n    public String extract(String text, String regex, int group) {\n        Pattern pattern = Pattern.compile(regex);\n        Matcher matcher = pattern.matcher(text);\n        if (!matcher.find()) {\n            getEngine().logger.warn(\"failed to find pattern: {}\", regex);\n            return null;\n        }\n        return matcher.group(group);\n    }\n\n    public List<String> extractAll(String text, String regex, int group) {\n        Pattern pattern = Pattern.compile(regex);\n        Matcher matcher = pattern.matcher(text);\n        List<String> list = new ArrayList();\n        while (matcher.find()) {\n            list.add(matcher.group(group));\n        }\n        return list;\n    }\n\n    public void fail(String reason) {\n        getEngine().setFailedReason(new KarateException(reason));\n    }\n\n    public Object filter(Value o, Value f) {\n        if (!o.hasArrayElements()) {\n            return JsList.EMPTY;\n        }\n        assertIfJsFunction(f);\n        long count = o.getArraySize();\n        List list = new ArrayList();\n        for (int i = 0; i < count; i++) {\n            Value v = o.getArrayElement(i);\n            Value res = JsEngine.execute(f, v, i);\n            if (res.isBoolean() && res.asBoolean()) {\n                list.add(new JsValue(v).getValue());\n            }\n        }\n        return new JsList(list);\n    }\n\n    public Object filterKeys(Value o, Value... args) {\n        Variable v = new Variable(o);\n        if (!v.isMap()) {\n            return JsMap.EMPTY;\n        }\n        List<String> keys = new ArrayList();\n        if (args.length == 1) {\n            if (args[0].isString()) {\n                keys.add(args[0].asString());\n            } else if (args[0].hasArrayElements()) {\n                long count = args[0].getArraySize();\n                for (int i = 0; i < count; i++) {\n                    keys.add(args[0].getArrayElement(i).toString());\n                }\n            } else if (args[0].hasMembers()) {\n                for (String s : args[0].getMemberKeys()) {\n                    keys.add(s);\n                }\n            }\n        } else {\n            for (Value key : args) {\n                keys.add(key.toString());\n            }\n        }\n        Map map = v.getValue();\n        Map result = new LinkedHashMap(keys.size());\n        for (String key : keys) {\n            if (key == null) {\n                continue;\n            }\n            if (map.containsKey(key)) {\n                result.put(key, map.get(key));\n            }\n        }\n        return new JsMap(result);\n    }\n\n    public void forEach(Value o, Value f) {\n        assertIfJsFunction(f);\n        if (o.hasArrayElements()) {\n            long count = o.getArraySize();\n            for (int i = 0; i < count; i++) {\n                Value v = o.getArrayElement(i);\n                f.executeVoid(v, i);\n            }\n        } else if (o.hasMembers()) { //map\n            int i = 0;\n            for (String k : o.getMemberKeys()) {\n                Value v = o.getMember(k);\n                f.executeVoid(k, v, i++);\n            }\n        } else {\n            throw new RuntimeException(\"not an array or object: \" + o);\n        }\n    }\n\n    public Command fork(Value value) {\n        if (value.isString()) {\n            return getEngine().fork(true, value.asString());\n        } else if (value.hasArrayElements()) {\n            List args = new JsValue(value).getAsList();\n            return getEngine().fork(true, args);\n        } else {\n            return getEngine().fork(true, new JsValue(value).getAsMap());\n        }\n    }\n\n    // TODO breaking returns actual object not wrapper\n    // and fromObject() has been removed\n    // use new typeOf() method to find type\n    public Object fromString(String exp) {\n        ScenarioEngine engine = getEngine();\n        try {\n            Variable result = engine.evalKarateExpression(exp);\n            return JsValue.fromJava(result.getValue());\n        } catch (Exception e) {\n            engine.setFailedReason(null); // special case\n            engine.logger.warn(\"auto evaluation failed: {}\", e.getMessage());\n            return exp;\n        }\n    }\n\n    public Object get(String exp) {\n        ScenarioEngine engine = getEngine();\n        Variable v;\n        try {\n            v = engine.evalKarateExpression(exp); // even json path expressions will work\n        } catch (Exception e) {\n            engine.logger.trace(\"karate.get failed for expression: '{}': {}\", exp, e.getMessage());\n            engine.setFailedReason(null); // special case !\n            return null;\n        }\n        if (v != null) {\n            return JsValue.fromJava(v.getValue());\n        } else {\n            return null;\n        }\n    }\n\n    public Object get(String exp, Object defaultValue) {\n        Object result = get(exp);\n        return result == null ? defaultValue : result;\n    }\n\n    // getters =================================================================\n    // TODO migrate these to functions not properties\n    //\n    public ScenarioEngine getEngine() {\n        ScenarioEngine engine = ScenarioEngine.get();\n        return engine == null ? ENGINE : engine;\n    }\n\n    public String getEnv() {\n        return getEngine().runtime.featureRuntime.suite.env;\n    }\n\n    public Object getFeature() {\n        return new JsMap(getEngine().runtime.featureRuntime.result.toInfoJson());\n    }\n\n    public Object getInfo() { // TODO deprecate\n        return new JsMap(getEngine().runtime.getScenarioInfo());\n    }\n\n    private LogFacade logFacade;\n\n    public Object getLogger() {\n        if (logFacade == null) {\n            logFacade = new LogFacade();\n        }\n        return logFacade;\n    }\n\n    public Object getOs() {\n        String name = FileUtils.getOsName();\n        String type = FileUtils.getOsType(name).toString().toLowerCase();\n        Map<String, Object> map = new HashMap(2);\n        map.put(\"name\", name);\n        map.put(\"type\", type);\n        return new JsMap(map);\n    }\n\n    public Object getPrevRequest() {\n        HttpRequest hr = getEngine().getHttpRequest();\n        if (hr == null) {\n            return null;\n        }\n        Map<String, Object> map = new HashMap();\n        map.put(\"method\", hr.getMethod());\n        map.put(\"url\", hr.getUrl());\n        map.put(\"headers\", hr.getHeaders());\n        map.put(\"body\", hr.getBody());\n        return JsValue.fromJava(map);\n    }\n\n    public Object getProperties() {\n        return new JsMap(getEngine().runtime.featureRuntime.suite.systemProperties);\n    }\n\n    public Object getResponse() {\n        return getEngine().getResponse();\n    }\n\n    public Object getRequest() {\n        return getEngine().getRequest();\n    }\n\n    public Object getScenario() {\n        return new JsMap(getEngine().runtime.result.toKarateJson());\n    }\n\n    public Object getScenarioOutline() {\n        return new JsMap(getEngine().runtime.outlineResult.toKarateJson());\n    }\n\n    public Object getTags() {\n        return JsValue.fromJava(getEngine().runtime.tags.getTags());\n    }\n\n    public Object getTagValues() {\n        return JsValue.fromJava(getEngine().runtime.tags.getTagValues());\n    }\n\n    //==========================================================================\n    //\n    public HttpRequestBuilder http(String url) {\n        ScenarioEngine engine = getEngine();\n        HttpClient client = engine.runtime.featureRuntime.suite.clientFactory.create(engine);\n        return new HttpRequestBuilder(client).url(url);\n    }\n\n    public Object jsonPath(Object o, String exp) {\n        Json json = Json.of(o);\n        return JsValue.fromJava(json.get(exp));\n    }\n\n    public Object keysOf(Object o) {\n        Variable v = new Variable(o);\n        if (v.isMap()) {\n            return new JsList(v.<Map>getValue().keySet());\n        } else {\n            return JsList.EMPTY;\n        }\n    }\n\n    public void log(Value... values) {\n        ScenarioEngine engine = getEngine();\n        if (engine.getConfig().isPrintEnabled()) {\n            engine.logger.info(\"{}\", new LogWrapper(values));\n        }\n    }\n\n    public Object lowerCase(Object o) {\n        Variable var = new Variable(o);\n        return JsValue.fromJava(var.toLowerCase().getValue());\n    }\n\n    public Object map(Value o, Value f) {\n        if (!o.hasArrayElements()) {\n            return JsList.EMPTY;\n        }\n        assertIfJsFunction(f);\n        long count = o.getArraySize();\n        List list = new ArrayList();\n        for (int i = 0; i < count; i++) {\n            Value v = o.getArrayElement(i);\n            Value res = JsEngine.execute(f, v, i);\n            list.add(new JsValue(res).getValue());\n        }\n        return new JsList(list);\n    }\n\n    public Object mapWithKey(Value v, String key) {\n        if (!v.hasArrayElements()) {\n            return JsList.EMPTY;\n        }\n        long count = v.getArraySize();\n        List list = new ArrayList();\n        for (int i = 0; i < count; i++) {\n            Map map = new LinkedHashMap();\n            Value res = v.getArrayElement(i);\n            map.put(key, res.as(Object.class));\n            list.add(map);\n        }\n        return new JsList(list);\n    }\n\n    public Object match(Value actual, Value expected) {\n        Match.Result mr = getEngine().match(Match.Type.EQUALS, JsValue.toJava(actual), JsValue.toJava(expected));\n        return JsValue.fromJava(mr.toMap());\n    }\n\n    public Object match(String exp) {\n        MatchStep ms = new MatchStep(exp);\n        Match.Result mr = getEngine().match(ms.type, ms.name, ms.path, ms.expected);\n        return JsValue.fromJava(mr.toMap());\n    }\n\n    public Object merge(Value... vals) {\n        if (vals.length == 0) {\n            return null;\n        }\n        if (vals.length == 1) {\n            return vals[0];\n        }\n        Map map = new HashMap(vals[0].as(Map.class));\n        for (int i = 1; i < vals.length; i++) {\n            map.putAll(vals[i].as(Map.class));\n        }\n        return new JsMap(map);\n    }\n\n    public void pause(Value value) {\n        ScenarioEngine engine = getEngine();\n        if (!value.isNumber()) {\n            engine.logger.warn(\"pause argument is not a number:\", value);\n            return;\n        }\n        if (engine.runtime.perfMode) {\n            engine.runtime.featureRuntime.perfHook.pause(value.asInt());\n        } else if (engine.getConfig().isPauseIfNotPerf()) {\n            try {\n                Thread.sleep(value.asInt());\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n\n    public String pretty(Object o) {\n        Variable v = new Variable(o);\n        return v.getAsPrettyString();\n    }\n\n    public String prettyXml(Object o) {\n        Variable v = new Variable(o);\n        return v.getAsPrettyXmlString();\n    }\n\n    public void proceed() {\n        proceed(null);\n    }\n\n    public void proceed(String requestUrlBase) {\n        getEngine().mockProceed(requestUrlBase);\n    }\n\n    public Object range(int start, int end) {\n        return range(start, end, 1);\n    }\n\n    public Object range(int start, int end, int interval) {\n        if (interval <= 0) {\n            throw new RuntimeException(\"interval must be a positive integer\");\n        }\n        List<Integer> list = new ArrayList();\n        if (start <= end) {\n            for (int i = start; i <= end; i += interval) {\n                list.add(i);\n            }\n        } else {\n            for (int i = start; i >= end; i -= interval) {\n                list.add(i);\n            }\n        }\n        return JsValue.fromJava(list);\n    }\n\n    public Object read(String path) {\n        Object result = getEngine().fileReader.readFile(path);\n        return JsValue.fromJava(result);\n    }\n\n    public byte[] readAsBytes(String path) {\n        return getEngine().fileReader.readFileAsBytes(path);\n    }\n\n    public String readAsString(String path) {\n        return getEngine().fileReader.readFileAsString(path);\n    }\n\n    public InputStream readAsStream(String path) {\n        return getEngine().fileReader.readFileAsStream(path);\n    }\n\n    public void remove(String name, String path) {\n        getEngine().remove(name, path);\n    }\n\n    public String render(Value v) {\n        Map<String, Object> arg;\n        if (v.isString()) {\n            arg = Collections.singletonMap(\"read\", v.asString());\n        } else if (v.hasMembers()) {\n            arg = new JsValue(v).getAsMap();\n        } else {\n            getEngine().logger.warn(\"render - unexpected argument: {}\", v);\n            return null;\n        }\n        return getEngine().renderHtml(arg);\n    }\n\n    public Object repeat(int n, Value f) {\n        assertIfJsFunction(f);\n        List list = new ArrayList(n);\n        for (int i = 0; i < n; i++) {\n            Value v = JsEngine.execute(f, i);\n            list.add(new JsValue(v).getValue());\n        }\n        return new JsList(list);\n    }\n\n    // set multiple variables in one shot\n    public void set(Map<String, Object> map) {\n        getEngine().setVariables(map);\n    }\n\n    public void set(String name, Value value) {\n        getEngine().setVariable(name, new Variable(value));\n    }\n\n    // this makes sense mainly for xpath manipulation from within js\n    public void set(String name, String path, Object value) {\n        getEngine().set(name, path, new Variable(value));\n    }\n\n    public Object setup() {\n        return setup(null);\n    }\n\n    public Object setup(String name) {\n        Map<String, Object> result = setupInternal(getEngine(), name);\n        return JsValue.fromJava(result);\n    }\n\n    private static Map<String, Object> setupInternal(ScenarioEngine engine, String name) {\n        Feature feature = engine.runtime.featureRuntime.featureCall.feature;\n        Scenario scenario = feature.getSetup(name);\n        if (scenario == null) {\n            String message = \"no scenario found with @setup tag\";\n            if (name != null) {\n                message = message + \" and name '\" + name + \"'\";\n            }\n            engine.logger.error(message);\n            throw new RuntimeException(message);\n        }\n        ScenarioRuntime sr = new ScenarioRuntime(engine.runtime.featureRuntime, scenario);\n        sr.setSkipBackground(true);\n        sr.run();\n        ScenarioEngine.set(engine);\n        engine.runtime.featureRuntime.setupResult = sr.result; // hack to embed setup into report\n        return sr.engine.getAllVariablesAsMap();\n    }\n\n    public Object setupOnce() {\n        return setupOnce(null);\n    }\n\n    public Object setupOnce(String name) {\n        ScenarioEngine engine = getEngine();\n        final Map<String, Map<String, Object>> CACHE = engine.runtime.featureRuntime.SETUPONCE_CACHE;\n        Map<String, Object> result = CACHE.get(name);\n        if (result != null) {\n            return setupOnceResult(result);\n        }\n        long startTime = System.currentTimeMillis();\n        engine.logger.trace(\"setupOnce waiting for lock: {}\", name);\n        synchronized (CACHE) {\n            result = CACHE.get(name); // retry\n            if (result != null) {\n                long endTime = System.currentTimeMillis() - startTime;\n                engine.logger.warn(\"this thread waited {} milliseconds for setupOnce lock: {}\", endTime, name);\n                return setupOnceResult(result);\n            }\n            result = setupInternal(engine, name);\n            CACHE.put(name, result);\n            return setupOnceResult(result);\n        }\n    }\n\n    private static Object setupOnceResult(Map<String, Object> result) {\n        Map<String, Object> clone = new HashMap(result.size());\n        result.forEach((k, v) -> { // shallow clone\n            Variable variable = new Variable(v);\n            clone.put(k, variable.copy(false).getValue());\n        });\n        return JsValue.fromJava(clone);\n    }\n\n    public void setXml(String name, String xml) {\n        getEngine().setVariable(name, XmlUtils.toXmlDoc(xml));\n    }\n\n    // this makes sense mainly for xpath manipulation from within js\n    public void setXml(String name, String path, String xml) {\n        getEngine().set(name, path, new Variable(XmlUtils.toXmlDoc(xml)));\n    }\n\n    public void signal(Value v) {\n        getEngine().signal(JsValue.toJava(v));\n    }\n\n    public Object sizeOf(Object o) {\n        Variable v = new Variable(o);\n        if (v.isList()) {\n            return v.<List>getValue().size();\n        } else if (v.isMap()) {\n            return v.<Map>getValue().size();\n        } else if (v.isBytes()) {\n            return v.<byte[]>getValue().length;\n        } else {\n            return -1;\n        }\n    }\n\n    static abstract class ValueIndex<T> implements Comparable<ValueIndex<T>> {\n\n        final T object;\n        final long index;\n\n        ValueIndex(T o, long index) {\n            this.object = o;\n            this.index = index;\n        }\n\n    }\n\n    static class StringValueIndex extends ValueIndex<String> {\n\n        public StringValueIndex(String o, long index) {\n            super(o, index);\n        }\n\n        @Override\n        public int compareTo(ValueIndex<String> other) {\n            int result = this.object.compareTo(other.object);\n            return result == 0 ? (int) (this.index - other.index) : result;\n        }\n\n    }\n\n    static class NumberValueIndex extends ValueIndex<Number> {\n\n        public NumberValueIndex(Number o, long index) {\n            super(o, index);\n        }\n\n        @Override\n        public int compareTo(ValueIndex<Number> other) {\n            double result = this.object.doubleValue() - other.object.doubleValue();\n            return result == 0 ? (int) (this.index - other.index) : (int) result;\n        }\n\n    }\n\n    public Object sort(Value o) {\n        return sort(o, getEngine().JS.evalForValue(\"x => x\"));\n    }\n\n    public Object sort(Value o, Value f) {\n        if (!o.hasArrayElements()) {\n            return JsList.EMPTY;\n        }\n        assertIfJsFunction(f);\n        long count = o.getArraySize();\n        List<ValueIndex> pointers = new ArrayList((int) count);\n        List<Object> items = new ArrayList(pointers.size());\n        for (int i = 0; i < count; i++) {\n            Object item = JsValue.toJava(o.getArrayElement(i));\n            items.add(item);\n            Value key = JsEngine.execute(f, item, i);\n            if (key.isNumber()) {\n                pointers.add(new NumberValueIndex(key.as(Number.class), i));\n            } else {\n                pointers.add(new StringValueIndex(key.asString(), i));\n            }\n        }\n        Collections.sort(pointers);\n        List<Object> result = new ArrayList(pointers.size());\n        pointers.forEach(item -> result.add(items.get((int) item.index)));\n        return JsValue.fromJava(result);\n    }\n\n    public MockServer start(Value value) {\n        if (value.isString()) {\n            return startInternal(Collections.singletonMap(\"mock\", value.asString()));\n        } else {\n            return startInternal(new JsValue(value).getAsMap());\n        }\n    }\n\n    private MockServer startInternal(Map<String, Object> config) {\n        String mock = (String) config.get(\"mock\");\n        if (mock == null) {\n            throw new RuntimeException(\"'mock' is missing: \" + config);\n        }\n        File feature = toJavaFile(mock);\n        MockServer.Builder builder = MockServer.feature(feature);\n        String pathPrefix = (String) config.get(\"pathPrefix\");\n        if (pathPrefix != null) {\n            builder.pathPrefix(pathPrefix);\n        }\n        String certFile = (String) config.get(\"cert\");\n        if (certFile != null) {\n            builder.certFile(toJavaFile(certFile));\n        }\n        String keyFile = (String) config.get(\"key\");\n        if (keyFile != null) {\n            builder.keyFile(toJavaFile(keyFile));\n        }\n        Boolean ssl = (Boolean) config.get(\"ssl\");\n        if (ssl == null) {\n            ssl = false;\n        }\n        Integer port = (Integer) config.get(\"port\");\n        if (port == null) {\n            port = 0;\n        }\n        Map<String, Object> arg = (Map) config.get(\"arg\");\n        builder.args(arg);\n        if (ssl) {\n            builder.https(port);\n        } else {\n            builder.http(port);\n        }\n        return builder.build();\n    }\n\n    public void stop(int port) {\n        Command.waitForSocket(port);\n    }\n\n    public String toAbsolutePath(String relativePath) {\n        return getEngine().fileReader.toAbsolutePath(relativePath);\n    }\n\n    public Object toBean(Object o, String className) {\n        Json json = Json.of(o);\n        Object bean = JsonUtils.fromJson(json.toString(), className);\n        return JsValue.fromJava(bean);\n    }\n\n    public String toCsv(Object o) {\n        Variable v = new Variable(o);\n        if (!v.isList()) {\n            throw new RuntimeException(\"not a json array: \" + v);\n        }\n        List<Map<String, Object>> list = v.getValue();\n        return JsonUtils.toCsv(list);\n    }\n\n    public Object toJava(Value value) {\n        return new JsValue(value).getValue();\n    }\n\n    public File toJavaFile(String path) {\n        return getEngine().fileReader.toResource(path).getFile();\n    }\n\n    public Object toJs(Object value) {\n        return JsValue.fromJava(value);\n    }\n\n    public Object toJson(Value value) {\n        return toJson(value, false);\n    }\n\n    public Object toJson(Value value, boolean removeNulls) {\n        JsValue jv = new JsValue(value);\n        String json = JsonUtils.toJson(jv.getValue());\n        Object result = Json.of(json).value();\n        if (removeNulls) {\n            JsonUtils.removeKeysWithNullValues(result);\n        }\n        return JsValue.fromJava(result);\n    }\n\n    // TODO deprecate\n    public Object toList(Value value) {\n        return new JsValue(value).getValue();\n    }\n\n    // TODO deprecate\n    public Object toMap(Value value) {\n        return new JsValue(value).getValue();\n    }\n\n    public String toString(Object o) {\n        Variable v = new Variable(o);\n        return v.getAsString();\n    }\n\n    public String typeOf(Value value) {\n        Variable v = new Variable(value);\n        return v.getTypeString();\n    }\n\n    public String urlEncode(String s) {\n        try {\n            return URLEncoder.encode(s, \"UTF-8\");\n        } catch (Exception e) {\n            getEngine().logger.warn(\"url encode failed: {}\", e.getMessage());\n            return s;\n        }\n    }\n\n    public String urlDecode(String s) {\n        try {\n            return URLDecoder.decode(s, \"UTF-8\");\n        } catch (Exception e) {\n            getEngine().logger.warn(\"url encode failed: {}\", e.getMessage());\n            return s;\n        }\n    }\n\n    public Object valuesOf(Object o) {\n        Variable v = new Variable(o);\n        if (v.isList()) {\n            return new JsList(v.<List>getValue());\n        } else if (v.isMap()) {\n            return new JsList(v.<Map>getValue().values());\n        } else {\n            return null;\n        }\n    }\n\n    public boolean waitForHttp(String url) {\n        return Command.waitForHttp(url);\n    }\n\n    public boolean waitForPort(String host, int port) {\n        return new Command().waitForPort(host, port);\n    }\n\n    public WebSocketClient webSocket(String url) {\n        return webSocket(url, null, null);\n    }\n\n    public WebSocketClient webSocket(String url, Value value) {\n        return webSocket(url, value, null);\n    }\n\n    public WebSocketClient webSocket(String url, Value listener, Value value) {\n        Function<String, Boolean> handler;\n        ScenarioEngine engine = getEngine();\n        if (listener == null || !listener.canExecute()) {\n            handler = m -> true;\n        } else {\n            handler = new JsLambda(listener);\n        }\n        WebSocketOptions options = new WebSocketOptions(url, value == null ? null : new JsValue(value).getValue());\n        options.setTextHandler(handler);\n        return engine.webSocket(options);\n    }\n\n    public WebSocketClient webSocketBinary(String url) {\n        return webSocketBinary(url, null, null);\n    }\n\n    public WebSocketClient webSocketBinary(String url, Value value) {\n        return webSocketBinary(url, value, null);\n    }\n\n    public WebSocketClient webSocketBinary(String url, Value listener, Value value) {\n        Function<byte[], Boolean> handler;\n        ScenarioEngine engine = getEngine();\n        if (listener == null || !listener.canExecute()) {\n            handler = m -> true;\n        } else {\n            handler = new JsLambda(listener);\n        }\n        WebSocketOptions options = new WebSocketOptions(url, value == null ? null : new JsValue(value).getValue());\n        options.setBinaryHandler(handler);\n        return engine.webSocket(options);\n    }\n\n    public Object wrapFunction(Value value) {\n        if (value.isProxyObject()) {\n            Object o = value.asProxyObject();\n            if (o instanceof JsFunction) {\n                JsFunction fun = (JsFunction) o;\n                return JsFunction.wrap(fun.getValue());\n            }\n        }\n        if (value.canExecute()) {\n            return JsFunction.wrap(value);\n        }\n        throw new RuntimeException(\"js function expected\");\n    }\n\n    public File write(Object o, String path) {\n        ScenarioEngine engine = getEngine();\n        path = engine.runtime.featureRuntime.suite.buildDir + File.separator + path;\n        File file = new File(path);\n        FileUtils.writeToFile(file, JsonUtils.toBytes(o));\n        engine.logger.debug(\"write to file: {}\", file);\n        return file;\n    }\n\n    public Object xmlPath(Object o, String path) {\n        Variable var = new Variable(o);\n        Variable res = ScenarioEngine.evalXmlPath(var, path);\n        return JsValue.fromJava(res.getValue());\n    }\n\n    // helpers =================================================================\n    //\n    private static void assertIfJsFunction(Value f) {\n        if (!f.canExecute()) {\n            throw new RuntimeException(\"not a js function: \" + f);\n        }\n    }\n\n    // make sure log() toString() is lazy\n    static class LogWrapper {\n\n        final Value[] values;\n\n        LogWrapper(Value... values) {\n            // sometimes a null array gets passed in, graal weirdness\n            this.values = values == null ? new Value[0] : values;\n        }\n\n        @Override\n        public String toString() {\n            StringBuilder sb = new StringBuilder();\n            for (Value v : values) {\n                Variable var = new Variable(v);\n                sb.append(var.getAsPrettyString()).append(' ');\n            }\n            return sb.toString();\n        }\n\n    }\n\n    public static class LogFacade {\n\n        private static Logger getLogger() {\n            return ScenarioEngine.get().logger;\n        }\n\n        private static String wrap(Value... values) {\n            return new LogWrapper(values).toString();\n        }\n\n        public void debug(Value... values) {\n            getLogger().debug(wrap(values));\n        }\n\n        public void info(Value... values) {\n            getLogger().info(wrap(values));\n        }\n\n        public void trace(Value... values) {\n            getLogger().trace(wrap(values));\n        }\n\n        public void warn(Value... values) {\n            getLogger().warn(wrap(values));\n        }\n\n        public void error(Value... values) {\n            getLogger().error(wrap(values));\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ScenarioCall.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class ScenarioCall {\n\n    public final ScenarioRuntime parentRuntime;\n    public final int depth;\n    public final FeatureCall featureCall;\n    public final Variable arg;\n\n    private boolean callonce;\n\n    private boolean sharedScope;\n    private boolean karateConfigDisabled;\n    private int loopIndex = -1;\n    \n    public Map<String, Variable> getParentVars(boolean shallowCopy) {\n        if (parentRuntime == null) {\n            return new HashMap();\n        }\n        ScenarioEngine mockEngine = parentRuntime.featureRuntime.getMockEngine();\n        if (mockEngine != null) {\n            if (shallowCopy) {\n                return mockEngine.shallowCloneVariables();\n            } else {\n                return mockEngine.vars;\n            }\n        }\n        if (shallowCopy) {\n            return parentRuntime.engine.shallowCloneVariables();\n        } else {\n            return parentRuntime.engine.vars;\n        }\n    }\n    \n    public Config getParentConfig(boolean copy) {\n        if (parentRuntime == null) {\n            return new Config();\n        }\n        Config temp = parentRuntime.engine.getConfig();\n        return copy ? new Config(temp) : temp;\n    }\n\n    public boolean isNone() {\n        return depth == 0;\n    }\n\n    public int getLoopIndex() {\n        return loopIndex;\n    }\n\n    public void setLoopIndex(int loopIndex) {\n        this.loopIndex = loopIndex;\n    }\n\n    public void setSharedScope(boolean sharedScope) {\n        this.sharedScope = sharedScope;\n    }\n\n    public boolean isSharedScope() {\n        return sharedScope;\n    }\n\n    public boolean isCallonce() {\n        return callonce;\n    }\n\n    public void setCallonce(boolean callonce) {\n        this.callonce = callonce;\n    }\n\n    public void setKarateConfigDisabled(boolean karateConfigDisabled) {\n        this.karateConfigDisabled = karateConfigDisabled;\n    }\n\n    public boolean isKarateConfigDisabled() {\n        return karateConfigDisabled;\n    }     \n\n    public static ScenarioCall none(Map<String, Object> arg) {\n        return new ScenarioCall(null, null, arg == null ? null : new Variable(arg));\n    }\n\n    public ScenarioCall(ScenarioRuntime parentRuntime, FeatureCall featureCall, Variable arg) {\n        this.parentRuntime = parentRuntime;\n        this.featureCall = featureCall;\n        if (parentRuntime == null) {\n            depth = 0;\n        } else {\n            depth = parentRuntime.caller.depth + 1;\n        }\n        this.arg = arg;\n    }\n\n    public static class Result {\n\n        public final Variable value;\n        public final Config config;\n        public final Map<String, Variable> vars;\n\n        public Result(Variable value, Config config, Map<String, Variable> vars) {\n            this.value = value;\n            this.config = config;\n            this.vars = vars;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ScenarioEngine.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.*;\nimport com.intuit.karate.driver.Driver;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.driver.Key;\nimport com.intuit.karate.graal.JsEngine;\nimport com.intuit.karate.graal.JsFunction;\nimport com.intuit.karate.graal.JsLambda;\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.http.*;\nimport com.intuit.karate.resource.Resource;\nimport com.intuit.karate.resource.ResourceResolver;\nimport com.intuit.karate.shell.Command;\nimport com.intuit.karate.template.KarateEngineContext;\nimport com.intuit.karate.template.KarateTemplateEngine;\nimport com.intuit.karate.template.TemplateUtils;\nimport com.jayway.jsonpath.PathNotFoundException;\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.ProxyExecutable;\nimport org.slf4j.LoggerFactory;\nimport org.w3c.dom.*;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.security.KeyStore;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.BiConsumer;\nimport java.util.function.Function;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * @author pthomas3\n */\npublic class ScenarioEngine {\n\n    private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(ScenarioEngine.class);\n\n    private static final String KARATE = \"karate\";\n    private static final String READ = \"read\";\n    private static final String KEY = \"Key\";\n\n    public static final String RESPONSE = \"response\";\n    public static final String RESPONSE_HEADERS = \"responseHeaders\";\n    public static final String RESPONSE_STATUS = \"responseStatus\";\n    private static final String RESPONSE_BYTES = \"responseBytes\";\n    private static final String RESPONSE_COOKIES = \"responseCookies\";\n    private static final String RESPONSE_TIME = \"responseTime\";\n    private static final String RESPONSE_TYPE = \"responseType\";\n\n    private static final String LISTEN_RESULT = \"listenResult\";\n\n    public static final String REQUEST = \"request\";\n    public static final String REQUEST_URL_BASE = \"requestUrlBase\";\n    public static final String REQUEST_URI = \"requestUri\";\n    public static final String REQUEST_PATH = \"requestPath\";\n    private static final String REQUEST_PARAMS = \"requestParams\";\n    public static final String REQUEST_METHOD = \"requestMethod\";\n    public static final String REQUEST_HEADERS = \"requestHeaders\";\n    private static final String REQUEST_TIME_STAMP = \"requestTimeStamp\";\n\n    public final ScenarioRuntime runtime;\n    public final ScenarioFileReader fileReader;\n    public final Map<String, Variable> vars;\n    public final Logger logger;\n\n    private final Function<String, Object> readFunction;\n    private final ScenarioBridge bridge;\n    private final Collection<RuntimeHook> hooks;\n\n    private boolean aborted;\n    private Throwable failedReason;\n\n    protected JsEngine JS;\n\n    public ScenarioEngine(Config config, ScenarioRuntime runtime, Map<String, Variable> vars, Logger logger) {\n        this.config = config;\n        this.runtime = runtime;\n        hooks = runtime.featureRuntime.suite.hooks;\n        fileReader = new ScenarioFileReader(this, runtime.featureRuntime);\n        readFunction = s -> JsValue.fromJava(fileReader.readFile(s));\n        bridge = new ScenarioBridge(this);\n        this.vars = vars;\n        this.logger = logger;\n    }\n\n    public static ScenarioEngine forTempUse(HttpClientFactory hcf) {\n        FeatureRuntime fr = FeatureRuntime.forTempUse(hcf);\n        ScenarioRuntime sr = new ScenarioIterator(fr).first();\n        sr.engine.init();\n        return sr.engine;\n    }\n\n    private static final ThreadLocal<ScenarioEngine> THREAD_LOCAL = new ThreadLocal<ScenarioEngine>();\n\n    public static ScenarioEngine get() {\n        return THREAD_LOCAL.get();\n    }\n\n    public static void set(ScenarioEngine se) {\n        THREAD_LOCAL.set(se);\n    }\n\n    protected static void remove() {\n        THREAD_LOCAL.remove();\n    }\n\n    public JsEngine getJsEngine() {\n        return JS;\n    }\n\n    // engine ==================================================================\n    //\n    public boolean isAborted() {\n        return aborted;\n    }\n\n    public void setAborted(boolean aborted) {\n        this.aborted = aborted;\n    }\n\n    public boolean isFailed() {\n        return failedReason != null;\n    }\n\n    public boolean isIgnoringStepErrors() {\n        return !config.getContinueOnStepFailureMethods().isEmpty();\n    }\n\n    public void setFailedReason(Throwable failedReason) {\n        this.failedReason = failedReason;\n    }\n\n    public Throwable getFailedReason() {\n        return failedReason;\n    }\n\n    public void matchResult(Match.Type matchType, String expression, String path, String expected) {\n        Match.Result mr = match(matchType, expression, path, expected);\n        if (!mr.pass) {\n            setFailedReason(new KarateException(mr.message));\n        }\n    }\n\n    public void set(String name, String path, String exp) {\n        set(name, path, exp, false, false);\n    }\n\n    public void remove(String name, String path) {\n        try {\n            set(name, path, null, true, false);\n        } catch (Exception e) {\n            logger.warn(\"remove failed: {}\", e.getMessage());\n        }\n    }\n\n    public void table(String name, List<Map<String, String>> rows) {\n        name = StringUtils.trimToEmpty(name);\n        validateVariableName(name);\n        List<Map<String, Object>> result = new ArrayList<>(rows.size());\n        for (Map<String, String> map : rows) {\n            Map<String, Object> row = new LinkedHashMap<>(map);\n            List<String> toRemove = new ArrayList(map.size());\n            for (Map.Entry<String, Object> entry : row.entrySet()) {\n                String exp = (String) entry.getValue();\n                Variable sv = evalKarateExpression(exp);\n                if (sv.isNull() && !isWithinParentheses(exp)) { // by default empty / null will be stripped, force null like this: '(null)'\n                    toRemove.add(entry.getKey());\n                } else {\n                    if (sv.isString()) {\n                        entry.setValue(sv.getAsString());\n                    } else { // map, list etc\n                        entry.setValue(sv.getValue());\n                    }\n                }\n            }\n            for (String keyToRemove : toRemove) {\n                row.remove(keyToRemove);\n            }\n            result.add(row);\n        }\n        setVariable(name, result);\n    }\n\n    public void replace(String name, String token, String value) {\n        name = name.trim();\n        Variable v = vars.get(name);\n        if (v == null) {\n            throw new RuntimeException(\"no variable found with name: \" + name);\n        }\n        String text = v.getAsString();\n        String replaced = replacePlaceholderText(text, token, value);\n        setVariable(name, replaced);\n    }\n\n    public void assertTrue(String expression) {\n        if (!evalJs(expression).isTrue()) {\n            String message = \"did not evaluate to 'true': \" + expression;\n            setFailedReason(new KarateException(message));\n        }\n    }\n\n    public void print(String exp) {\n        if (!config.isPrintEnabled()) {\n            return;\n        }\n        evalJs(\"karate.log('[print]',\" + exp + \")\");\n    }\n\n    public void invokeAfterHookIfConfigured(AfterHookType hookType) {\n        // Do not call hooks on \"called\" scenarios/features\n        if (runtime.caller.depth > 0) {\n            return;\n        }\n\n        // Get hook variable based on type\n        Variable v;\n        switch (hookType) {\n            case AFTER_SCENARIO: \n                v = config.getAfterScenario();\n                break;\n            case AFTER_OUTLINE: \n                v = config.getAfterScenarioOutline();\n                break;\n            case AFTER_FEATURE: \n                v = config.getAfterFeature();\n                break;\n            default: return;\n        }\n        \n        if (v.isJsOrJavaFunction()) {\n            if (hookType == AfterHookType.AFTER_FEATURE) {\n                ScenarioEngine.set(this); // for any bridge / js to work\n            }\n            try {\n                executeFunction(v);\n            } catch (Exception e) {\n                logger.warn(\"{} hook failed: {}\", hookType.getPrefix(), e + \"\");\n            }\n        }\n    }\n\n    // gatling =================================================================\n    //   \n    private PerfEvent prevPerfEvent;\n\n    public void logLastPerfEvent(String failureMessage) {\n        if (prevPerfEvent != null && runtime.perfMode) {\n            if (failureMessage != null) {\n                prevPerfEvent.setFailed(true);\n                prevPerfEvent.setMessage(failureMessage);\n            }\n            runtime.featureRuntime.perfHook.reportPerfEvent(prevPerfEvent);\n        }\n        prevPerfEvent = null;\n    }\n\n    public void capturePerfEvent(PerfEvent event) {\n        logLastPerfEvent(null);\n        prevPerfEvent = event;\n    }\n\n    // http ====================================================================\n    //\n    protected HttpRequestBuilder requestBuilder; // see init() method\n    private HttpRequest httpRequest;\n    private Request request; // used only for mocks\n    private Response response;\n    private Config config;\n\n    public Config getConfig() {\n        return config;\n    }\n\n    // important: use this to trigger client re-config\n    // callonce routine is one example\n    public void setConfig(Config config) {\n        this.config = config;\n        if (requestBuilder != null) {\n            requestBuilder.client.setConfig(config);\n        }\n    }\n\n    public void setRequest(Request request) {\n        this.request = request;\n    }\n\n    public Request getRequest() {\n        if (request != null) {\n            return request;\n        }\n        if (httpRequest != null) {\n            request = httpRequest.toRequest();\n            request.processBody();\n            return request;\n        }\n        return null;\n    }\n\n    public HttpRequest getHttpRequest() {\n        return httpRequest;\n    }\n\n    public Response getResponse() {\n        return response;\n    }\n\n    public HttpRequestBuilder getRequestBuilder() {\n        return requestBuilder;\n    }\n\n    public void configure(String key, String exp) {\n        Variable v = evalKarateExpression(exp);\n        configure(key, v);\n    }\n\n    public void configure(String key, Variable v) {\n        key = StringUtils.trimToEmpty(key);\n        // if next line returns true, config is http-client related\n        if (config.configure(key, v)) {\n            if (requestBuilder != null) {\n                requestBuilder.client.setConfig(config);\n            }\n        }\n    }\n\n    private void evalAsMap(String exp, BiConsumer<String, List<String>> fun) {\n        Variable var = evalKarateExpression(exp);\n        if (!var.isMap()) {\n            logger.warn(\"did not evaluate to map {}: {}\", exp, var);\n            return;\n        }\n        Map<String, Object> map = var.getValue();\n        map.forEach((k, v) -> {\n            if (v instanceof List) {\n                List list = (List) v;\n                List<String> values = new ArrayList(list.size());\n                for (Object o : list) { // support non-string values, e.g. numbers\n                    if (o != null) {\n                        values.add(o.toString());\n                    }\n                }\n                fun.accept(k, values);\n            } else if (v != null) {\n                fun.accept(k, Collections.singletonList(v.toString()));\n            }\n        });\n    }\n\n    public void url(String exp) {\n        Variable var = evalKarateExpression(exp);\n        requestBuilder.url(var.getAsString());\n    }\n\n    public void path(String exp) {\n        if (exp.contains(\",\")) {\n            exp = \"[\" + exp + \"]\";\n        }\n        Variable v = evalJs(exp);\n        List<?> list;\n        if (v.isList()) {\n            list = v.getValue();\n        } else {\n            list = Collections.singletonList(v.getValue());\n        }\n        for (Object o : list) {\n            if (o != null) {\n                requestBuilder.path(o.toString());\n            }\n        }\n    }\n\n    public void param(String name, String exp) {\n        Variable var = evalKarateExpression(exp);\n        if (var.isList()) {\n            requestBuilder.param(name, var.<List>getValue());\n        } else {\n            requestBuilder.param(name, var.getAsString());\n        }\n    }\n\n    public void params(String expr) {\n        evalAsMap(expr, (k, v) -> requestBuilder.param(k, v));\n    }\n\n    public void header(String name, String exp) {\n        Variable var = evalKarateExpression(exp);\n        if (var.isList()) {\n            requestBuilder.header(name, var.<List>getValue());\n        } else {\n            requestBuilder.header(name, var.getAsString());\n        }\n    }\n\n    public void headers(String expr) {\n        evalAsMap(expr, (k, v) -> requestBuilder.header(k, v));\n    }\n\n    public void cookie(String name, String exp) {\n        Variable var = evalKarateExpression(exp);\n        if (var.isString()) {\n            requestBuilder.cookie(name, var.getAsString());\n        } else if (var.isMap()) {\n            Map<String, Object> map = var.getValue();\n            map.put(\"name\", name);\n            requestBuilder.cookie(map);\n        }\n    }\n\n    public void cookies(String exp) {\n        Variable var = evalKarateExpression(exp);\n        Map<String, Map> cookies = Cookies.normalize(var.getValue());\n        requestBuilder.cookies(cookies.values());\n    }\n\n    private void updateConfigCookies(Map<String, Map> cookies) {\n        if (cookies == null) {\n            return;\n        }\n        if (config.getCookies().isNull()) {\n            config.setCookies(new Variable(cookies));\n        } else {\n            Map<String, Map> map = getOrEvalAsMap(config.getCookies());\n            map.putAll(cookies);\n            config.setCookies(new Variable(map));\n        }\n    }\n\n    public void formField(String name, String exp) {\n        Variable var = evalKarateExpression(exp);\n        if (var.isList()) {\n            requestBuilder.formField(name, var.<List>getValue());\n        } else {\n            requestBuilder.formField(name, var.getAsString());\n        }\n    }\n\n    public void formFields(String exp) {\n        Variable var = evalKarateExpression(exp);\n        if (var.isMap()) {\n            Map<String, Object> map = var.getValue();\n            map.forEach((k, v) -> {\n                requestBuilder.formField(k, v);\n            });\n        } else {\n            logger.warn(\"did not evaluate to map {}: {}\", exp, var);\n        }\n    }\n\n    public void multipartField(String name, String value) {\n        Variable v = evalKarateExpression(value);\n        Map map = new HashMap();\n        map.put(\"value\", v.getValue());\n        multiPartInternal(name, map);\n    }\n\n    public void multipartFields(String exp) {\n        multipartFiles(exp);\n    }\n\n    private void multiPartInternal(String name, Object value) {\n        Map<String, Object> map = new HashMap();\n        if (name != null) {\n            map.put(\"name\", name);\n        }\n        if (value instanceof Number) {\n            value = value.toString();\n        }\n        if (value instanceof Map) {\n            map.putAll((Map) value);\n            String toRead = (String) map.get(\"read\");\n            if (toRead != null) {\n                Resource resource = fileReader.toResource(toRead);\n                if (resource.isFile()) {\n                    File file = resource.getFile();\n                    map.put(\"value\", file);\n                } else {\n                    map.put(\"value\", FileUtils.toBytes(resource.getStream()));\n                }\n            }\n            requestBuilder.multiPart(map);\n        } else if (value instanceof String) {\n            map.put(\"value\", (String) value);\n            multiPartInternal(name, map);\n        } else if (value instanceof List) {\n            List list = (List) value;\n            for (Object o : list) {\n                multiPartInternal(null, o);\n            }\n        } else if (logger.isTraceEnabled()) {\n            logger.trace(\"did not evaluate to string, map or list {}: {}\", name, value);\n        }\n    }\n\n    public void multipartFile(String name, String exp) {\n        Variable var = evalKarateExpression(exp);\n        multiPartInternal(name, var.getValue());\n    }\n\n    public void multipartFiles(String exp) {\n        Variable var = evalKarateExpression(exp);\n        if (var.isMap()) {\n            Map<String, Object> map = var.getValue();\n            map.forEach((k, v) -> multiPartInternal(k, v));\n        } else if (var.isList()) {\n            List<Map> list = var.getValue();\n            for (Map map : list) {\n                multiPartInternal(null, map);\n            }\n        } else {\n            logger.warn(\"did not evaluate to map or list {}: {}\", exp, var);\n        }\n    }\n\n    public void request(String body) {\n        Variable v = evalKarateExpression(body);\n        requestBuilder.body(v.getValue());\n    }\n\n    public void soapAction(String exp) {\n        String action = evalKarateExpression(exp).getAsString();\n        if (action == null) {\n            action = \"\";\n        }\n        requestBuilder.header(\"SOAPAction\", action);\n        requestBuilder.contentType(\"text/xml\");\n        method(\"POST\");\n    }\n\n    public void retry(String condition) {\n        requestBuilder.setRetryUntil(condition);\n    }\n\n    public void method(String method) {\n        if (!HttpConstants.HTTP_METHODS.contains(method.toUpperCase())) { // support expressions also\n            method = evalKarateExpression(method).getAsString();\n        }\n        requestBuilder.method(method);\n        httpInvoke();\n    }\n\n    // extracted for mock proceed()\n    public Response httpInvoke() {\n        if (requestBuilder.isRetry()) {\n            httpInvokeWithRetries();\n        } else {\n            httpInvokeOnce();\n        }\n        requestBuilder.reset();\n        return response;\n    }\n\n    private void httpInvokeOnce() {\n        Map<String, Map> cookies = getOrEvalAsMap(config.getCookies());\n        if (cookies != null) {\n            requestBuilder.cookies(cookies.values());\n        }\n        Map<String, Object> headers;\n        if (config.getHeaders().isJsOrJavaFunction()) {\n            headers = getOrEvalAsMap(config.getHeaders(), requestBuilder.build());\n        } else {\n            headers = getOrEvalAsMap(config.getHeaders()); // avoid an extra http request build\n        }\n        if (headers != null) {\n            requestBuilder.headers(headers);\n        }\n        httpRequest = requestBuilder.build();\n        String perfEventName = null; // acts as a flag to report perf if not null\n        if (runtime.perfMode) {\n            perfEventName = runtime.featureRuntime.perfHook.getPerfEventName(httpRequest, runtime);\n        }\n        long startTime = System.currentTimeMillis();\n        httpRequest.setStartTime(startTime); // this may be fine-adjusted by actual http client\n        Collection<RuntimeHook> allHooks = getRuntimeHooks();\n        allHooks.forEach(h -> h.beforeHttpCall(httpRequest, runtime));\n        try {\n            response = requestBuilder.client.invoke(httpRequest);\n        } catch (Exception e) {\n            long endTime = System.currentTimeMillis();\n            long responseTime = endTime - startTime;\n            String message = \"http call failed after \" + responseTime + \" milliseconds for url: \" + httpRequest.getUrl();\n            logger.error(e.getMessage() + \", \" + message);\n            if (logger.isTraceEnabled()) {\n                String stacktrace = StringUtils.throwableToString(e);\n                if (stacktrace != null) {\n                    logger.trace(stacktrace);\n                }\n            }\n            if (perfEventName != null) {\n                PerfEvent pe = new PerfEvent(startTime, endTime, perfEventName, 0);\n                capturePerfEvent(pe); // failure flag and message should be set by logLastPerfEvent()\n            }\n            throw new KarateException(message + \"\\n\" + e.getMessage(), e);\n        }\n        startTime = httpRequest.getStartTime(); // in case it was re-adjusted by http client\n        final long endTime = httpRequest.getEndTime();\n        final long responseTime = endTime - startTime;\n        response.setResponseTime(responseTime);\n        allHooks.forEach(h -> h.afterHttpCall(httpRequest, response, runtime));\n        byte[] bytes = response.getBody();\n        Object body;\n        String responseType;\n        ResourceType resourceType = response.getResourceType();\n        if (resourceType != null && resourceType.isBinary()) {\n            responseType = \"binary\";\n            body = bytes;\n        } else {\n            try {\n                body = JsonUtils.fromBytes(bytes, true, resourceType);\n            } catch (Exception e) {\n                body = FileUtils.toString(bytes);\n                logger.warn(\"auto-conversion of response failed: {}\", e.getMessage());\n            }\n            if (body instanceof Map || body instanceof List) {\n                responseType = \"json\";\n            } else if (body instanceof Node) {\n                responseType = \"xml\";\n            } else {\n                responseType = \"string\";\n            }\n        }\n        setHiddenVariable(REQUEST_TIME_STAMP, startTime);\n        setVariable(RESPONSE_TIME, responseTime);\n        setVariable(RESPONSE_STATUS, response.getStatus());\n        setVariable(RESPONSE, body);\n        if (config.isLowerCaseResponseHeaders()) {\n            setVariable(RESPONSE_HEADERS, response.getHeadersWithLowerCaseNames());\n        } else {\n            setVariable(RESPONSE_HEADERS, response.getHeaders());\n        }\n        setHiddenVariable(RESPONSE_BYTES, bytes);\n        setHiddenVariable(RESPONSE_TYPE, responseType);\n        cookies = response.getCookies();\n        updateConfigCookies(cookies);\n        setHiddenVariable(RESPONSE_COOKIES, cookies);\n        if (perfEventName != null) {\n            PerfEvent pe = new PerfEvent(startTime, endTime, perfEventName, response.getStatus());\n            capturePerfEvent(pe);\n        }\n    }\n\n    private List<RuntimeHook> getRuntimeHooks() {\n        return Stream.concat(hooks.stream(), Stream.of(requestBuilder.hook()))\n                .filter(Objects::nonNull)\n                .collect(Collectors.toList());\n    }\n\n    private void httpInvokeWithRetries() {\n        int maxRetries = config.getRetryCount();\n        int sleep = config.getRetryInterval();\n        int retryCount = 0;\n        while (true) {\n            if (retryCount == maxRetries) {\n                throw new KarateException(\"too many retry attempts: \" + maxRetries);\n            }\n            if (retryCount > 0) {\n                try {\n                    logger.debug(\"sleeping before retry #{}\", retryCount);\n                    Thread.sleep(sleep);\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n            }\n\n            Variable v;\n            try {\n                httpInvokeOnce();\n                v = evalKarateExpression(requestBuilder.getRetryUntil());\n            } catch (Exception e) {\n                logger.warn(\"retry condition evaluation failed: {}\", e.getMessage());\n                v = Variable.NULL;\n            }\n            if (v.isTrue()) {\n                if (retryCount > 0) {\n                    logger.debug(\"retry condition satisfied\");\n                }\n                break;\n            } else {\n                logger.debug(\"retry condition not satisfied: {}\", requestBuilder.getRetryUntil());\n            }\n            retryCount++;\n        }\n    }\n\n    public void status(int status) {\n        if (status != response.getStatus()) {\n            // make sure log masking is applied\n            String message = HttpLogger.getStatusFailureMessage(status, config, httpRequest, response);\n            setFailedReason(new KarateException(message));\n        }\n    }\n\n    public KeyStore getKeyStore(String trustStoreFile, String password, String type) {\n        if (trustStoreFile == null) {\n            return null;\n        }\n        char[] passwordChars = password == null ? null : password.toCharArray();\n        if (type == null) {\n            type = KeyStore.getDefaultType();\n        }\n        try {\n            KeyStore keyStore = KeyStore.getInstance(type);\n            InputStream is = fileReader.readFileAsStream(trustStoreFile);\n            keyStore.load(is, passwordChars);\n            logger.debug(\"key store key count for {}: {}\", trustStoreFile, keyStore.size());\n            return keyStore;\n        } catch (Exception e) {\n            logger.error(\"key store init failed: {}\", e.getMessage());\n            throw new RuntimeException(e);\n        }\n    }\n\n    // non-http ================================================================\n    //\n    List<Channel> channels;\n\n    private static String getFactory(String channelType) {\n        switch (channelType) {\n            case Config.KAFKA:\n                return \"io.karatelabs.kafka.KafkaChannelFactory\";\n            case Config.GRPC:\n                return \"io.karatelabs.grpc.GrpcChannelFactory\";\n            case Config.WEBSOCKET:\n                return \"io.karatelabs.websocket.WebsocketChannelFactory\";\n            case Config.WEBHOOK:\n                return \"io.karatelabs.webhook.WebhookChannelFactory\";\n            default:\n                throw new RuntimeException(\"unknown channel type: \" + channelType);\n        }\n    }\n\n    protected Object channelSession(String type) {\n        String factoryClass = getFactory(type);\n        try {\n            Class<?> clazz = Class.forName(factoryClass);\n            ChannelFactory factory = (ChannelFactory) clazz.getDeclaredConstructor().newInstance();\n            Map<String, Object> options = config.getCustomOptions().get(type);\n            Channel channel = factory.create(runtime, options);\n            if (channels == null) {\n                channels = new ArrayList<>();\n            }\n            channels.add(channel);\n            return channel.init(this.runtime);\n        } catch (KarateException ke) {\n            throw ke;\n        } catch (Exception e) {\n            String message;\n            if (e instanceof ClassNotFoundException) {\n                message = \"cannot instantiate [\" + type + \"], is 'karate-\" + type + \"' included as a maven / gradle dependency ?\";\n            } else {\n                message = e.getMessage();\n            }\n            logger.error(message);\n            throw new RuntimeException(message, e);\n        }\n    }\n\n    // http mock ===============================================================\n    //\n    public void mockProceed(String requestUrlBase) {\n        String urlBase;\n        if (requestUrlBase == null) {\n            urlBase = vars.get(REQUEST_URL_BASE).getValue();\n        } else {\n            urlBase = requestUrlBase;\n        }\n        requestBuilder.url(urlBase);\n        requestBuilder.path(vars.get(REQUEST_PATH).getValue());\n        requestBuilder.params(vars.get(REQUEST_PARAMS).getValue());\n        requestBuilder.method(vars.get(REQUEST_METHOD).getValue());\n        requestBuilder.headers(vars.get(REQUEST_HEADERS).<Map>getValue());\n        requestBuilder.removeHeader(HttpConstants.HDR_CONTENT_LENGTH);\n        requestBuilder.body(vars.get(REQUEST).getValue());\n        if (requestBuilder.client instanceof ArmeriaHttpClient) {\n            Request mockRequest = MockHandler.LOCAL_REQUEST.get();\n            if (mockRequest != null) {\n                ArmeriaHttpClient client = (ArmeriaHttpClient) requestBuilder.client;\n                client.setRequestContext(mockRequest.getRequestContext());\n            }\n        }\n        httpInvoke();\n    }\n\n    public Map<String, Object> mockConfigureHeaders() {\n        return getOrEvalAsMap(config.getResponseHeaders());\n    }\n\n    public void mockAfterScenario() {\n        if (config.getAfterScenario().isJsOrJavaFunction()) {\n            executeFunction(config.getAfterScenario());\n        }\n    }\n\n    // websocket / async =======================================================\n    //   \n    private List<WebSocketClient> webSocketClients;\n    private CompletableFuture SIGNAL = new CompletableFuture();\n\n    public WebSocketClient webSocket(WebSocketOptions options) {\n        WebSocketClient webSocketClient = new WebSocketClient(options, logger);\n        webSocketClient.setEngine(this);\n        if (webSocketClients == null) {\n            webSocketClients = new ArrayList();\n        }\n        webSocketClients.add(webSocketClient);\n        return webSocketClient;\n    }\n\n    public void signal(Object result) {\n        SIGNAL.complete(result);\n    }\n\n    public void listen(String exp) {\n        Variable v = evalKarateExpression(exp);\n        int timeout = v.getAsInt();\n        logger.debug(\"entered listen state with timeout: {}\", timeout);\n        Object listenResult = null;\n        try {\n            listenResult = SIGNAL.get(timeout, TimeUnit.MILLISECONDS);\n            Thread.sleep(100); // IMPORTANT, else graal js complains\n        } catch (Exception e) {\n            logger.error(\"listen timed out: {}\", e + \"\");\n        }\n        SIGNAL = new CompletableFuture();\n        synchronized (JsFunction.LOCK) {\n            setHiddenVariable(LISTEN_RESULT, listenResult);\n            logger.debug(\"exit listen state with result: {}\", listenResult);\n        }\n    }\n\n    public Command fork(boolean useLineFeed, List<String> args) {\n        return fork(useLineFeed, Collections.singletonMap(\"args\", args));\n    }\n\n    public Command fork(boolean useLineFeed, String line) {\n        return fork(useLineFeed, Collections.singletonMap(\"line\", line));\n    }\n\n    public Command fork(boolean useLineFeed, Map<String, Object> options) {\n        Boolean useShell = (Boolean) options.get(\"useShell\");\n        if (useShell == null) {\n            useShell = false;\n        }\n        List<String> list = (List) options.get(\"args\");\n        String[] args;\n        if (list == null) {\n            String line = (String) options.get(\"line\");\n            if (line == null) {\n                throw new RuntimeException(\"'line' or 'args' is required\");\n            }\n            args = Command.tokenize(line);\n        } else {\n            args = list.toArray(new String[list.size()]);\n        }\n        if (useShell) {\n            args = Command.prefixShellArgs(args);\n        }\n        String workingDir = (String) options.get(\"workingDir\");\n        File workingFile = workingDir == null ? null : new File(workingDir);\n        Command command = new Command(useLineFeed, logger, null, null, workingFile, args);\n        Map env = (Map) options.get(\"env\");\n        if (env != null) {\n            command.setEnvironment(env);\n        }\n        Boolean redirectErrorStream = (Boolean) options.get(\"redirectErrorStream\");\n        if (redirectErrorStream != null) {\n            command.setRedirectErrorStream(redirectErrorStream);\n        }\n        Value funOut = Value.asValue(options.get(\"listener\"));\n        if (funOut.canExecute()) {\n            command.setListener(new JsLambda(funOut));\n        }\n        Value funErr = Value.asValue(options.get(\"errorListener\"));\n        if (funErr.canExecute()) {\n            command.setErrorListener(new JsLambda(funErr));\n        }\n        Boolean start = (Boolean) options.get(\"start\");\n        if (start == null) {\n            start = true;\n        }\n        if (start) {\n            command.start();\n        }\n        return command;\n    }\n\n    // ui driver / robot =======================================================\n    //\n    protected Driver driver;\n    protected Plugin robot;\n\n    private void autoDef(Plugin plugin, String instanceName) {\n        for (String methodName : plugin.methodNames()) {\n            String invoke = instanceName + \".\" + methodName;\n            StringBuilder sb = new StringBuilder();\n            sb.append(\"(function(){ if (arguments.length == 0) return \").append(invoke).append(\"();\")\n                    .append(\" if (arguments.length == 1) return \").append(invoke).append(\"(arguments[0]);\")\n                    .append(\" if (arguments.length == 2) return \").append(invoke).append(\"(arguments[0], arguments[1]);\")\n                    .append(\" return \").append(invoke).append(\"(arguments[0], arguments[1], arguments[2]) })\");\n            setHiddenVariable(methodName, evalJs(sb.toString()));\n        }\n    }\n\n    public void driver(String exp) {\n        Variable v = evalKarateExpression(exp);\n        // re-create driver within a test if needed\n        // but user is expected to call quit() OR use the driver keyword with a JSON argument\n        if (driver == null || driver.isTerminated() || v.isMap()) {\n            Map<String, Object> options = config.getCustomOptions().get(Config.DRIVER);\n            if (options == null) {\n                options = new HashMap();\n            }\n            options.put(\"target\", config.getDriverTarget());\n            if (v.isMap()) {\n                options.putAll(v.getValue());\n            }\n            setDriver(DriverOptions.start(options, runtime));\n        }\n        if (v.isString()) {\n            driver.setUrl(v.getAsString());\n        }\n    }\n\n    public void robot(String exp) {\n        Variable v = evalKarateExpression(exp);\n        if (robot == null) {\n            Map<String, Object> options = config.getCustomOptions().get(Config.ROBOT);\n            if (options == null) {\n                options = new HashMap();\n            }\n            if (v.isMap()) {\n                options.putAll(v.getValue());\n            } else if (v.isString()) {\n                options.put(\"window\", v.getAsString());\n            }\n            try {\n                Class clazz = Class.forName(\"com.intuit.karate.robot.RobotFactory\");\n                PluginFactory factory = (PluginFactory) clazz.getDeclaredConstructor().newInstance();\n                robot = factory.create(runtime, options);\n            } catch (KarateException ke) {\n                throw ke;\n            } catch (Exception e) {\n                String message = \"cannot instantiate robot, is 'karate-robot' included as a maven / gradle dependency ? \" + e.getMessage();\n                logger.error(message);\n                throw new RuntimeException(message, e);\n            }\n            setRobot(robot);\n        }\n    }\n\n    public void setDriverToNull() {\n        this.driver = null;\n    }\n\n    public void setDriver(Driver driver) {\n        this.driver = driver;\n        setHiddenVariable(Config.DRIVER, driver);\n        if (robot != null) {\n            logger.warn(\"'robot' is active, use 'driver.' prefix for driver methods\");\n            return;\n        }\n        autoDef(driver, Config.DRIVER);\n        setHiddenVariable(KEY, Key.INSTANCE);\n    }\n\n    public void setRobot(Plugin robot) { // TODO unify\n        this.robot = robot;\n        // robot.setContext(this);\n        setHiddenVariable(Config.ROBOT, robot);\n        if (driver != null) {\n            logger.warn(\"'driver' is active, use 'robot.' prefix for robot methods\");\n            return;\n        }\n        autoDef(robot, Config.ROBOT);\n        setHiddenVariable(KEY, Key.INSTANCE);\n    }\n\n    public void stop(StepResult lastStepResult) {\n        if (runtime.caller.isSharedScope()) {\n            // TODO life-cycle this hand off\n            ScenarioEngine caller = runtime.caller.parentRuntime.engine;\n            if (driver != null) { // a called feature inited the driver\n                caller.setDriver(driver);\n            }\n            if (robot != null) {\n                caller.setRobot(robot);\n            }\n            caller.webSocketClients = webSocketClients;\n            // return, don't kill driver just yet\n        } else if (runtime.caller.depth == 0) { // end of top-level scenario (no caller)\n            if (webSocketClients != null) {\n                webSocketClients.forEach(WebSocketClient::close);\n            }\n            if (driver != null) { // TODO move this to Plugin.afterScenario()                \n                DriverOptions options = driver.getOptions();\n                if (options.stop) {\n                    driver.quit();\n                }\n                if (options.target != null) {\n                    logger.debug(\"custom target configured, attempting stop()\");\n                    Map<String, Object> map = options.target.stop(runtime);\n                    String video = (String) map.get(\"video\");\n                    embedVideo(video);\n                } else {\n                    if (options.afterStop != null) {\n                        Command.execLine(null, options.afterStop);\n                    }\n                    embedVideo(options.videoFile);\n                }\n            }\n            if (robot != null) {\n                robot.afterScenario();\n            }\n            if (channels != null) {\n                for (Channel channel : channels) {\n                    channel.afterScenario();\n                }\n            }\n        }\n    }\n\n    private void embedVideo(String path) {\n        if (path != null) {\n            File videoFile = new File(path);\n            if (videoFile.exists()) {\n                Embed embed = runtime.embedVideo(videoFile);\n                logger.debug(\"appended video to report: {}\", embed);\n            }\n        }\n    }\n\n    // doc =====================================================================\n    //    \n    private KarateTemplateEngine templateEngine;\n\n    private ResourceResolver resourceResolver;\n\n    public void setResourceResolver(ResourceResolver resourceResolver) {\n        this.resourceResolver = resourceResolver;\n    }\n\n    private ResourceResolver getResourceResolver() {\n        if (resourceResolver != null) {\n            return resourceResolver;\n        }\n        String prefixedPath = runtime.featureRuntime.rootFeature.featureCall.feature.getResource().getPrefixedParentPath();\n        return new ResourceResolver(prefixedPath);\n    }\n\n    public String renderHtml(Map<String, Object> options) {\n        String path = (String) options.get(\"read\");\n        String html;\n        if (path == null) {\n            html = (String) options.get(\"html\");\n            if (html == null) {\n                logger.warn(\"'read' or 'html' property is mandatory: {}\", options);\n                return null;\n            } else { // TODO do we cache this\n                KarateTemplateEngine stringEngine = TemplateUtils.forStrings(JS, getResourceResolver());\n                return stringEngine.process(html);\n            }\n        }\n        if (templateEngine == null) {\n            templateEngine = TemplateUtils.forResourceResolver(JS, getResourceResolver());\n        }\n        KarateEngineContext old = KarateEngineContext.get();\n        try {\n            return templateEngine.process(path);\n        } finally {\n            KarateEngineContext.set(old);\n        }\n    }\n\n    public void doc(String exp) {\n        Variable v = evalKarateExpression(exp);\n        if (v.isString()) {\n            docInternal(Collections.singletonMap(\"read\", v.getAsString()));\n        } else if (v.isMap()) {\n            Map<String, Object> map = v.getValue();\n            docInternal(map);\n        } else {\n            logger.warn(\"doc is not string or json: {}\", v);\n        }\n    }\n\n    protected String docInternal(Map<String, Object> options) {\n        String html = renderHtml(options);\n        if (html != null && !runtime.reportDisabled) {\n            runtime.embed(FileUtils.toBytes(html), ResourceType.HTML);\n        }\n        return html;\n    }\n\n    // compareImage =====================================================================\n    //\n    public void compareImage(String exp) {\n        Variable v = evalKarateExpression(exp);\n        if (!v.isMap()) {\n            throw new RuntimeException(\"invalid image comparison params: expected map\");\n        }\n\n        compareImageInternal(v.getValue());\n    }\n\n    protected Map<String, Object> compareImageInternal(Map<String, Object> params) {\n        Map<String, Object> options = getImageOptions(params.get(\"options\"), \"options\");\n        byte[] baselineImg = getImageBytes(params, \"baseline\");\n        byte[] latestImg = getImageBytes(params, \"latest\");\n\n        Map<String, Object> defaultOptions = getImageOptions(config.getImageComparisonOptions(), \"defaultOptions\");\n        boolean embedUI = !Boolean.TRUE.equals(defaultOptions.get(\"hideUiOnSuccess\"));\n\n        Map<String, Object> result = null;\n        try {\n            result = ImageComparison.compare(baselineImg, latestImg, options, defaultOptions);\n        } catch (ImageComparison.MismatchException e) {\n            logger.error(\"image comparison failed: {}\", e.getMessage());\n            embedUI = true;\n            result = e.data;\n            if (!Boolean.TRUE.equals(defaultOptions.get(\"mismatchShouldPass\"))) {\n                throw e;\n            }\n        } finally {\n            if (embedUI) {\n                String diffJS = \"newDiffUI(document.currentScript,\"\n                        + JsonUtils.toJson(result) + \",\"\n                        + JsonUtils.toJson(options) + \",\"\n                        + getImageHookFunction(options, defaultOptions, \"onShowRebase\") + \",\"\n                        + getImageHookFunction(options, defaultOptions, \"onShowConfig\")\n                        + \")\";\n\n                runtime.embed(JsonUtils.toBytes(diffJS), ResourceType.DEFERRED_JS);\n            }\n        }\n\n        return result;\n    }\n\n    private byte[] getImageBytes(Map<String, Object> params, String paramName) {\n        Object img = params.get(paramName);\n        if (img == null) {\n            return null;\n        }\n\n        if (img instanceof String) {\n            return fileReader.readFileAsBytes((String) img);\n        }\n\n        if (img instanceof byte[]) {\n            return (byte[]) img;\n        }\n\n        throw new RuntimeException(\n                \"invalid image comparison options: expected \" + paramName + \" to be one of string|byte[]\");\n    }\n\n    private Map<String, Object> getImageOptions(Object obj, String objName) {\n        if (obj == null) {\n            return new HashMap<>();\n        }\n\n        if (obj instanceof Map) {\n            return (Map<String, Object>) obj;\n        }\n\n        throw new RuntimeException(\"invalid image comparison \" + objName + \": expected map\");\n    }\n\n    private String getImageHookFunction(Map<String, Object> options, Map<String, Object> defaultOptions, String name) {\n        Object fn = options.containsKey(name) ? options.get(name) : defaultOptions.get(name);\n        return fn == null ? null : fn.toString();\n    }\n\n    //==========================================================================        \n    //       \n    public void init() { // not in constructor because it has to be on Runnable.run() thread \n        JS = JsEngine.local();\n        logger.trace(\"js context: {}\", JS);\n        runtime.magicVariables.forEach((k, v) -> JS.put(k, v));\n        vars.forEach((k, v) -> JS.put(k, v.getValue()));\n        if (runtime.caller.arg != null && runtime.caller.arg.isMap()) {\n            // add the call arg as separate \"over ride\" variables\n            Map<String, Object> arg = runtime.caller.arg.getValue();\n            setVariables(arg);\n        }\n        JS.put(KARATE, bridge);\n        JS.put(READ, readFunction);\n        // edge case: can be left as-is because a callonce triggered init()\n        if (requestBuilder == null) {\n            // note that the http builder is always reset when a \"call\" occurs\n            HttpClient client = runtime.featureRuntime.suite.clientFactory.create(this);\n            requestBuilder = new HttpRequestBuilder(client);\n        }\n        // TODO improve life cycle and concept of shared objects\n        if (!runtime.caller.isNone()) {\n            ScenarioEngine caller = runtime.caller.parentRuntime.engine;\n            if (caller.driver != null) {\n                setDriver(caller.driver);\n            }\n            if (caller.robot != null) {\n                setRobot(caller.robot);\n            }\n        }\n    }\n\n    protected Map<String, Variable> shallowCloneVariables() {\n        Map<String, Variable> copy = new HashMap(vars.size());\n        vars.forEach((k, v) -> copy.put(k, v.copy(false))); // shallow clone\n        return copy;\n    }\n\n    protected <T> Map<String, T> getOrEvalAsMap(Variable var, Object... args) {\n        if (var.isJsOrJavaFunction()) {\n            Variable res = executeFunction(var, args);\n            return res.isMap() ? res.getValue() : null;\n        } else {\n            return var.isMap() ? var.getValue() : null;\n        }\n    }\n\n    public Variable executeFunction(Variable var, Object... args) {\n        switch (var.type) {\n            case JS_FUNCTION:\n                ProxyExecutable pe = var.getValue();\n                Object result = JsEngine.execute(pe, args);\n                return new Variable(result);\n            case JAVA_FUNCTION:  // definitely a \"call\" with a single argument\n                Function javaFunction = var.getValue();\n                Object arg = args.length == 0 ? null : args[0];\n                Object javaResult = javaFunction.apply(arg);\n                return new Variable(JsValue.unWrap(javaResult));\n            default:\n                throw new RuntimeException(\"expected function, but was: \" + var);\n        }\n    }\n\n    public Variable evalJs(String js) {\n        try {\n            return new Variable(JS.eval(js));\n        } catch (Exception e) {\n            KarateException ke = JsEngine.fromJsEvalException(js, e, null);\n            setFailedReason(ke);\n            throw ke;\n        }\n    }\n\n    public void setHiddenVariable(String key, Object value) {\n        if (value instanceof Variable) {\n            value = ((Variable) value).getValue();\n        }\n        JS.put(key, value);\n    }\n\n    public Object getVariable(String key) {\n        return JS.get(key).getValue();\n    }\n\n    public boolean hasVariable(String key) {\n        return JS.bindings.hasMember(key);\n    }\n\n    public void setVariable(String key, Object value) {\n        Variable v;\n        Object o;\n        if (value instanceof Variable) {\n            v = (Variable) value;\n            o = v.getValue();\n        } else {\n            o = value;\n            v = new Variable(value);\n        }\n        vars.put(key, v);\n        if (JS != null) {\n            JS.put(key, o);\n        }\n    }\n\n    public void setVariables(Map<String, Object> map) {\n        if (map == null) {\n            return;\n        }\n        map.forEach((k, v) -> setVariable(k, v));\n    }\n\n    public Map<String, Object> getAllVariablesAsMap() {\n        Map<String, Object> map = new HashMap(vars.size());\n        vars.forEach((k, v) -> map.put(k, v == null ? null : v.getValue()));\n        return map;\n    }\n\n    private static void validateVariableName(String name) {\n        if (!isValidVariableName(name)) {\n            throw new RuntimeException(\"invalid variable name: \" + name);\n        }\n        if (KARATE.equals(name)) {\n            throw new RuntimeException(\"'karate' is a reserved name\");\n        }\n        if (REQUEST.equals(name) || \"url\".equals(name)) {\n            throw new RuntimeException(\"'\" + name + \"' is a reserved name, also use the form '* \" + name + \" <expression>' instead\");\n        }\n    }\n\n    private Variable evalAndCastTo(AssignType assignType, String exp, boolean docString) {\n        Variable v = docString ? new Variable(exp) : evalKarateExpression(exp);\n        switch (assignType) {\n            case BYTE_ARRAY:\n                return new Variable(v.getAsByteArray());\n            case STRING:\n                return new Variable(v.getAsString());\n            case XML:\n                return new Variable(v.getAsXml());\n            case XML_STRING:\n                String xml = XmlUtils.toString(v.getAsXml());\n                return new Variable(xml);\n            case JSON:\n                return new Variable(v.getValueAndForceParsingAsJson());\n            case YAML:\n                return new Variable(JsonUtils.fromYaml(v.getAsString()));\n            case CSV:\n                return new Variable(JsonUtils.fromCsv(v.getAsString()));\n            case COPY:\n                return v.copy(true);\n            default: // TEXT will be docstring, AUTO (def) will auto-parse JSON or XML\n                return v; // as is\n        }\n    }\n\n    public void assign(AssignType assignType, String name, String exp, boolean docString) {\n        name = StringUtils.trimToEmpty(name);\n        validateVariableName(name); // always validate when gherkin\n        if (vars.containsKey(name)) {\n            LOGGER.debug(\"over-writing existing variable '{}' with new value: {}\", name, exp);\n        }\n        setVariable(name, evalAndCastTo(assignType, exp, docString));\n    }\n\n    private static boolean isEmbeddedExpression(String text) {\n        return text != null && (text.startsWith(\"#(\") || text.startsWith(\"##(\")) && text.endsWith(\")\");\n    }\n\n    private Map<String, Object> Map(Object callResult) {\n        throw new UnsupportedOperationException(\"Not supported yet.\"); //To change body of generated methods, choose Tools | Templates.\n    }\n\n    private static class EmbedAction {\n\n        final boolean remove;\n        final Object value;\n\n        private EmbedAction(boolean remove, Object value) {\n            this.remove = remove;\n            this.value = value;\n        }\n\n        static EmbedAction remove() {\n            return new EmbedAction(true, null);\n        }\n\n        static EmbedAction update(Object value) {\n            return new EmbedAction(false, value);\n        }\n\n    }\n\n    public Variable evalEmbeddedExpressions(Variable value, boolean forMatch) {\n        switch (value.type) {\n            case STRING:\n            case MAP:\n            case LIST:\n                EmbedAction ea = recurseEmbeddedExpressions(value, forMatch);\n                if (ea != null) {\n                    return ea.remove ? Variable.NULL : new Variable(ea.value);\n                } else {\n                    return value;\n                }\n            case XML:\n                recurseXmlEmbeddedExpressions(value.getValue(), forMatch);\n            default:\n                return value;\n        }\n    }\n\n    private EmbedAction recurseEmbeddedExpressions(Variable node, boolean forMatch) {\n        switch (node.type) {\n            case LIST:\n                List list = node.getValue();\n                Set<Integer> indexesToRemove = new HashSet();\n                int count = list.size();\n                for (int i = 0; i < count; i++) {\n                    EmbedAction ea = recurseEmbeddedExpressions(new Variable(list.get(i)), forMatch);\n                    if (ea != null) {\n                        if (ea.remove) {\n                            indexesToRemove.add(i);\n                        } else {\n                            list.set(i, ea.value);\n                        }\n                    }\n                }\n                if (!indexesToRemove.isEmpty()) {\n                    List copy = new ArrayList(count - indexesToRemove.size());\n                    for (int i = 0; i < count; i++) {\n                        if (!indexesToRemove.contains(i)) {\n                            copy.add(list.get(i));\n                        }\n                    }\n                    return EmbedAction.update(copy);\n                } else {\n                    return null;\n                }\n            case MAP:\n                Map<String, Object> map = node.getValue();\n                List<String> keysToRemove = new ArrayList();\n                map.forEach((k, v) -> {\n                    EmbedAction ea = recurseEmbeddedExpressions(new Variable(v), forMatch);\n                    if (ea != null) {\n                        if (ea.remove) {\n                            keysToRemove.add(k);\n                        } else {\n                            map.put(k, ea.value);\n                        }\n                    }\n                });\n                for (String key : keysToRemove) {\n                    map.remove(key);\n                }\n                return null;\n            case XML:\n                return null;\n            case STRING:\n                String value = StringUtils.trimToNull(node.getValue());\n                if (!isEmbeddedExpression(value)) {\n                    return null;\n                }\n                boolean optional = value.charAt(1) == '#';\n                value = value.substring(optional ? 2 : 1);\n                try {\n                    JsValue result = JS.eval(value);\n                    if (optional) {\n                        if (result.isNull()) {\n                            return EmbedAction.remove();\n                        }\n                        if (forMatch && (result.isObject() || result.isArray())) {\n                            // preserve optional JSON chunk schema-like references as-is, they are needed for future match attempts\n                            return null;\n                        }\n                    }\n                    return EmbedAction.update(result.getValue());\n                } catch (Exception e) {\n                    logger.trace(\"embedded expression failed {}: {}\", value, e.getMessage());\n                    return null;\n                }\n            default:\n                // do nothing\n                return null;\n        }\n    }\n\n    private void recurseXmlEmbeddedExpressions(Node node, boolean forMatch) {\n        if (node.getNodeType() == Node.DOCUMENT_NODE) {\n            node = node.getFirstChild();\n        }\n        NamedNodeMap attribs = node.getAttributes();\n        int attribCount = attribs == null ? 0 : attribs.getLength();\n        Set<Attr> attributesToRemove = new HashSet(attribCount);\n        for (int i = 0; i < attribCount; i++) {\n            Attr attrib = (Attr) attribs.item(i);\n            String value = attrib.getValue();\n            value = StringUtils.trimToNull(value);\n            if (isEmbeddedExpression(value)) {\n                boolean optional = value.charAt(1) == '#';\n                value = value.substring(optional ? 2 : 1);\n                try {\n                    JsValue jv = JS.eval(value);\n                    if (optional && jv.isNull()) {\n                        attributesToRemove.add(attrib);\n                    } else {\n                        attrib.setValue(jv.getAsString());\n                    }\n                } catch (Exception e) {\n                    logger.trace(\"xml-attribute embedded expression failed, {}: {}\", attrib.getName(), e.getMessage());\n                }\n            }\n        }\n        for (Attr toRemove : attributesToRemove) {\n            attribs.removeNamedItem(toRemove.getName());\n        }\n        NodeList nodeList = node.getChildNodes();\n        int childCount = nodeList.getLength();\n        List<Node> nodes = new ArrayList(childCount);\n        for (int i = 0; i < childCount; i++) {\n            nodes.add(nodeList.item(i));\n        }\n        Set<Node> elementsToRemove = new HashSet(childCount);\n        for (Node child : nodes) {\n            String value = child.getNodeValue();\n            if (value != null) {\n                value = StringUtils.trimToEmpty(value);\n                if (isEmbeddedExpression(value)) {\n                    boolean optional = value.charAt(1) == '#';\n                    value = value.substring(optional ? 2 : 1);\n                    try {\n                        JsValue jv = JS.eval(value);\n                        if (optional) {\n                            if (jv.isNull()) {\n                                elementsToRemove.add(child);\n                            } else if (forMatch && (jv.isXml() || jv.isObject())) {\n                                // preserve optional XML chunk schema-like references as-is, they are needed for future match attempts\n                            } else {\n                                child.setNodeValue(jv.getAsString());\n                            }\n                        } else {\n                            if (jv.isXml() || jv.isObject()) {\n                                Node evalNode = jv.isXml() ? jv.getValue() : XmlUtils.fromMap(jv.getValue());\n                                if (evalNode.getNodeType() == Node.DOCUMENT_NODE) {\n                                    evalNode = evalNode.getFirstChild();\n                                }\n                                if (child.getNodeType() == Node.CDATA_SECTION_NODE) {\n                                    child.setNodeValue(XmlUtils.toString(evalNode));\n                                } else {\n                                    evalNode = node.getOwnerDocument().importNode(evalNode, true);\n                                    child.getParentNode().replaceChild(evalNode, child);\n                                }\n                            } else {\n                                child.setNodeValue(jv.getAsString());\n                            }\n                        }\n                    } catch (Exception e) {\n                        logger.trace(\"xml embedded expression failed, {}: {}\", child.getNodeName(), e.getMessage());\n                    }\n                }\n            } else if (child.hasChildNodes() || child.hasAttributes()) {\n                recurseXmlEmbeddedExpressions(child, forMatch);\n            }\n        }\n        for (Node toRemove : elementsToRemove) { // because of how the above routine works, these are always of type TEXT_NODE\n            Node parent = toRemove.getParentNode(); // element containing the text-node\n            Node grandParent = parent.getParentNode(); // parent element\n            grandParent.removeChild(parent);\n        }\n    }\n\n    public String replacePlaceholderText(String text, String token, String replaceWith) {\n        if (text == null) {\n            return null;\n        }\n        replaceWith = StringUtils.trimToNull(replaceWith);\n        if (replaceWith == null) {\n            return text;\n        }\n        try {\n            Variable v = evalKarateExpression(replaceWith);\n            replaceWith = v.getAsString();\n        } catch (Exception e) {\n            throw new RuntimeException(\"expression error (replace string values need to be within quotes): \" + e.getMessage());\n        }\n        if (replaceWith == null) { // ignore if eval result is null\n            return text;\n        }\n        token = StringUtils.trimToNull(token);\n        if (token == null) {\n            return text;\n        }\n        char firstChar = token.charAt(0);\n        if (Character.isLetterOrDigit(firstChar)) {\n            token = '<' + token + '>';\n        }\n        return text.replace(token, replaceWith);\n    }\n\n    private static final String TOKEN = \"token\";\n\n    public void replaceTable(String text, List<Map<String, String>> list) {\n        if (text == null) {\n            return;\n        }\n        if (list == null) {\n            return;\n        }\n        for (Map<String, String> map : list) {\n            String token = map.get(TOKEN);\n            if (token == null) {\n                continue;\n            }\n            // the verbosity below is to be lenient with table second column name\n            List<String> keys = new ArrayList(map.keySet());\n            keys.remove(TOKEN);\n            Iterator<String> iterator = keys.iterator();\n            if (iterator.hasNext()) {\n                String key = keys.iterator().next();\n                String value = map.get(key);\n                replace(text, token, value);\n            }\n        }\n\n    }\n\n    public void set(String name, String path, Variable value) {\n        set(name, path, false, value, false, false);\n    }\n\n    private void set(String name, String path, String exp, boolean delete, boolean viaTable) {\n        set(name, path, isWithinParentheses(exp), evalKarateExpression(exp), delete, viaTable);\n    }\n\n    private void set(String name, String path, boolean isWithinParentheses, Variable value, boolean delete, boolean viaTable) {\n        name = StringUtils.trimToEmpty(name);\n        path = StringUtils.trimToNull(path);\n        if (viaTable && value.isNull() && !isWithinParentheses) {\n            // by default, skip any expression that evaluates to null unless the user expressed\n            // intent to over-ride by enclosing the expression in parentheses\n            return;\n        }\n        if (path == null) {\n            StringUtils.Pair nameAndPath = parseVariableAndPath(name);\n            name = nameAndPath.left;\n            path = nameAndPath.right;\n        }\n        Variable target = JS.bindings.hasMember(name) ? new Variable(JS.get(name)) : null; // should work in called features\n        if (isXmlPath(path)) {\n            if (target == null || target.isNull()) {\n                if (viaTable) { // auto create if using set via cucumber table as a convenience\n                    Document empty = XmlUtils.newDocument();\n                    target = new Variable(empty);\n                    setVariable(name, target);\n                } else {\n                    throw new RuntimeException(\"variable is null or not set '\" + name + \"'\");\n                }\n            }\n            Document doc = target.getValue();\n            if (delete) {\n                XmlUtils.removeByPath(doc, path);\n            } else if (value.isXml()) {\n                Node node = value.getValue();\n                XmlUtils.setByPath(doc, path, node);\n            } else if (value.isMap()) { // cast to xml\n                Node node = XmlUtils.fromMap(value.getValue());\n                XmlUtils.setByPath(doc, path, node);\n            } else {\n                XmlUtils.setByPath(doc, path, value.getAsString());\n            }\n            setVariable(name, new Variable(doc));\n        } else { // assume json-path\n            if (target == null || target.isNull()) {\n                if (viaTable) { // auto create if using set via cucumber table as a convenience\n                    Json json;\n                    if (path.startsWith(\"$[\") && !path.startsWith(\"$['\")) {\n                        json = Json.of(\"[]\");\n                    } else {\n                        json = Json.of(\"{}\");\n                    }\n                    target = new Variable(json.value());\n                    setVariable(name, target);\n                } else {\n                    throw new RuntimeException(\"variable is null or not set '\" + name + \"'\");\n                }\n            }\n            Json json;\n            if (target.isMapOrList()) {\n                json = Json.of(target.<Object>getValue());\n            } else {\n                throw new RuntimeException(\"cannot set json path on type: \" + target);\n            }\n            if (delete) {\n                json.remove(path);\n            } else {\n                json.set(path, value.<Object>getValue());\n            }\n        }\n    }\n\n    private static final String PATH = \"path\";\n\n    public void setViaTable(String name, String path, List<Map<String, String>> list) {\n        name = StringUtils.trimToEmpty(name);\n        path = StringUtils.trimToNull(path);\n        if (path == null) {\n            StringUtils.Pair nameAndPath = parseVariableAndPath(name);\n            name = nameAndPath.left;\n            path = nameAndPath.right;\n        }\n        for (Map<String, String> map : list) {\n            String append = (String) map.get(PATH);\n            if (append == null) {\n                continue;\n            }\n            List<String> keys = new ArrayList(map.keySet());\n            keys.remove(PATH);\n            int columnCount = keys.size();\n            for (int i = 0; i < columnCount; i++) {\n                String key = keys.get(i);\n                String expression = StringUtils.trimToNull(map.get(key));\n                if (expression == null) { // cucumber cell was left blank\n                    continue; // skip\n                    // default behavior is to skip nulls when the expression evaluates \n                    // this is driven by the routine in setValueByPath\n                    // and users can over-ride this by simply enclosing the expression in parentheses\n                }\n                String suffix;\n                try {\n                    int arrayIndex = Integer.valueOf(key);\n                    suffix = \"[\" + arrayIndex + \"]\";\n                } catch (NumberFormatException e) { // default to the column position as the index\n                    suffix = columnCount > 1 ? \"[\" + i + \"]\" : \"\";\n                }\n                String finalPath;\n                if (append.startsWith(\"/\") || (path != null && path.startsWith(\"/\"))) { // XML\n                    if (path == null) {\n                        finalPath = append + suffix;\n                    } else {\n                        finalPath = path + suffix + '/' + append;\n                    }\n                } else {\n                    if (path == null) {\n                        path = \"$\";\n                    }\n                    finalPath = path + suffix + '.' + append;\n                }\n                set(name, finalPath, expression, false, true);\n            }\n        }\n    }\n\n    public static StringUtils.Pair parseVariableAndPath(String text) {\n        Matcher matcher = VAR_AND_PATH_PATTERN.matcher(text);\n        matcher.find();\n        String name = text.substring(0, matcher.end());\n        String path;\n        if (matcher.end() == text.length()) {\n            path = \"\";\n        } else {\n            path = text.substring(matcher.end()).trim();\n        }\n        if (isXmlPath(path) || isXmlPathFunction(path)) {\n            // xml, don't prefix for json\n        } else {\n            path = \"$\" + path;\n        }\n        return StringUtils.pair(name, path);\n    }\n\n    public Match.Result match(Match.Type matchType, String expression, String path, String rhs) {\n        String name = StringUtils.trimToEmpty(expression);\n        if (isDollarPrefixedJsonPath(name) || isXmlPath(name)) { // \n            path = name;\n            name = RESPONSE;\n        }\n        if (name.startsWith(\"$\")) { // in case someone used the dollar prefix by mistake on the LHS\n            name = name.substring(1);\n        }\n        path = StringUtils.trimToNull(path);\n        if (path == null) {\n            if (name.startsWith(\"(\")) { // edge case, eval entire LHS\n                path = \"$\";\n            } else {\n                StringUtils.Pair pair = parseVariableAndPath(name);\n                name = pair.left;\n                path = pair.right;\n            }\n        }\n        if (\"header\".equals(name)) { // convenience shortcut for asserting against response header\n            return matchHeader(matchType, path, rhs);\n        }\n        Variable actual;\n        // karate started out by \"defaulting\" to JsonPath on the LHS of a match so we have this kludge\n        // but we now handle JS expressions of almost any shape on the LHS, if in doubt, wrap in parentheses\n        // actually it is not too bad - the XPath function check is the only odd one out\n        // rules:\n        // if not XPath function, wrapped in parentheses, involves function call\n        //      [then] JS eval\n        // else if XPath, JsonPath, JsonPath wildcard \"..\" or \"*\" or \"[?\"\n        //      [then] eval name, and do a JsonPath or XPath using the parsed path\n        if (isXmlPathFunction(path)\n                || (!name.startsWith(\"(\") && !path.endsWith(\")\") && !path.contains(\").\"))\n                && (isDollarPrefixed(path) || isJsonPath(path) || isXmlPath(path))) {\n            actual = evalKarateExpression(name);\n            // edge case: java property getter, e.g. \"driver.cookies\"\n            if (!actual.isMap() && !actual.isList() && !isXmlPath(path) && !isXmlPathFunction(path)) {\n                actual = evalKarateExpression(expression); // fall back to JS eval of entire LHS\n                path = \"$\";\n            }\n        } else {\n            actual = evalKarateExpression(expression); // JS eval of entire LHS\n            path = \"$\";\n        }\n        if (\"$\".equals(path) || \"/\".equals(path)) {\n            // we have eval-ed the entire LHS, so proceed to match RHS to \"$\"\n        } else {\n            if (isDollarPrefixed(path)) { // json-path\n                actual = evalJsonPath(actual, path);\n            } else { // xpath\n                actual = evalXmlPath(actual, path);\n            }\n        }\n        Variable expected = evalKarateExpression(rhs, true);\n        return match(matchType, actual.getValue(), expected.getValue());\n    }\n\n    private Match.Result matchHeader(Match.Type matchType, String name, String exp) {\n        Variable expected = evalKarateExpression(exp, true);\n        String actual = response.getHeader(name);\n        return match(matchType, actual, expected.getValue());\n    }\n\n    public Match.Result match(Match.Type matchType, Object actual, Object expected) {\n        return Match.execute(JS, matchType, actual, expected, config.isMatchEachEmptyAllowed());\n    }\n\n    private static final Pattern VAR_AND_PATH_PATTERN = Pattern.compile(\"\\\\w+\");\n    private static final String VARIABLE_PATTERN_STRING = \"[a-zA-Z][\\\\w]*\";\n    private static final Pattern VARIABLE_PATTERN = Pattern.compile(VARIABLE_PATTERN_STRING);\n    private static final Pattern FUNCTION_PATTERN = Pattern.compile(\"^function[^(]*\\\\(\");\n    private static final Pattern JS_PLACEHODER = Pattern.compile(\"\\\\$\\\\{.*?\\\\}\");\n\n    public static boolean isJavaScriptFunction(String text) {\n        return FUNCTION_PATTERN.matcher(text).find();\n    }\n\n    public static boolean isValidVariableName(String name) {\n        return VARIABLE_PATTERN.matcher(name).matches();\n    }\n\n    public static boolean hasJavaScriptPlacehoder(String exp) {\n        return JS_PLACEHODER.matcher(exp).find();\n    }\n\n    public static final boolean isVariableAndSpaceAndPath(String text) {\n        return text.matches(\"^\" + VARIABLE_PATTERN_STRING + \"\\\\s+.+\");\n    }\n\n    public static final boolean isVariable(String text) {\n        return VARIABLE_PATTERN.matcher(text).matches();\n    }\n\n    public static final boolean isWithinParentheses(String text) {\n        return text != null && text.startsWith(\"(\") && text.endsWith(\")\");\n    }\n\n    public static final boolean isCallSyntax(String text) {\n        return text.startsWith(\"call \");\n    }\n\n    public static final boolean isCallOnceSyntax(String text) {\n        return text.startsWith(\"callonce \");\n    }\n\n    public static final boolean isGetSyntax(String text) {\n        return text.startsWith(\"get \") || text.startsWith(\"get[\");\n    }\n\n    public static final boolean isJson(String text) {\n        return text.startsWith(\"{\") || text.startsWith(\"[\");\n    }\n\n    public static final boolean isXml(String text) {\n        return text.startsWith(\"<\");\n    }\n\n    public static boolean isXmlPath(String text) {\n        return text.startsWith(\"/\");\n    }\n\n    public static boolean isXmlPathFunction(String text) {\n        return text.matches(\"^[a-z-]+\\\\(.+\");\n    }\n\n    public static final boolean isJsonPath(String text) {\n        return text.indexOf('*') != -1 || text.contains(\"..\") || text.contains(\"[?\");\n    }\n\n    public static final boolean isDollarPrefixed(String text) {\n        return text.startsWith(\"$\");\n    }\n\n    public static final boolean isDollarPrefixedJsonPath(String text) {\n        return text.startsWith(\"$.\") || text.startsWith(\"$[\") || text.equals(\"$\");\n    }\n\n    public static StringUtils.Pair parseCallArgs(String line) {\n        int pos = line.indexOf(\"read(\");\n        if (pos != -1) {\n            pos = line.indexOf(')');\n            if (pos == -1) {\n                throw new RuntimeException(\"failed to parse call arguments: \" + line);\n            }\n            return new StringUtils.Pair(line.substring(0, pos + 1), StringUtils.trimToNull(line.substring(pos + 1)));\n        }\n        pos = line.indexOf(' ');\n        if (pos == -1) {\n            return new StringUtils.Pair(line, null);\n        }\n        return new StringUtils.Pair(line.substring(0, pos), StringUtils.trimToNull(line.substring(pos)));\n    }\n\n    public Variable call(Variable called, Variable arg, boolean sharedScope) {\n        switch (called.type) {\n            case JS_FUNCTION:\n            case JAVA_FUNCTION:\n                return arg == null ? executeFunction(called) : executeFunction(called, new Object[]{arg.getValue()});\n            case FEATURE:\n                // call result will be always a map or a list of maps (loop call result)\n                Object callResult = callFeature(called.getValue(), arg, -1, sharedScope);\n                return new Variable(callResult);\n            default:\n                throw new RuntimeException(\"not a callable feature or js function: \" + called);\n        }\n    }\n\n    public Variable call(boolean callOnce, String exp, boolean sharedScope) {\n        StringUtils.Pair pair = parseCallArgs(exp);\n        Variable called = evalKarateExpression(pair.left);\n        Variable arg = pair.right == null ? null : evalKarateExpression(pair.right);\n        Variable result;\n        if (callOnce) {\n            result = callOnce(exp, called, arg, sharedScope);\n        } else {\n            result = call(called, arg, sharedScope);\n        }\n        // attach js functions from a different graal context\n        result = new Variable(JS.attachAll(result.getValue()));\n        if (sharedScope && result.isMap()) {\n            setVariables(result.getValue());\n        }\n        return result;\n    }\n\n    private Variable callOnceResult(ScenarioCall.Result result, boolean sharedScope) {\n        if (sharedScope) { // if shared scope\n            vars.clear(); // clean slate            \n            if (result.vars != null) {\n                // shallow clone maps and lists so that subsequent steps don't modify data / references being passed around\n                result.vars.forEach((k, v) -> vars.put(k, v.copy(false)));\n            } else if (result.value != null) {\n                if (result.value.isMap()) {\n                    Map<String, Object> map = result.value.getValue();\n                    // shallow clone newly added variables\n                    map.forEach((k, v) -> vars.put(k, new Variable(JsonUtils.shallowCopy(v))));\n                } else {\n                    logger.warn(\"callonce: ignoring non-map value from result.value: {}\", result.value);\n                }\n            }\n            init(); // this will insert magic variables\n            // re-apply config from time of snapshot\n            setConfig(new Config(result.config));\n            return Variable.NULL; // since we already reset the vars above we return null\n            // else the call() routine would try to do it again\n            // note that shared scope means a return value is meaningless\n        } else {\n            // shallow clone for the same reasons mentioned above\n            return result.value.copy(false);\n        }\n    }\n\n    private Variable callOnce(String cacheKey, Variable called, Variable arg, boolean sharedScope) {\n        final Map<String, ScenarioCall.Result> CACHE;\n        if (runtime.perfMode) { // use suite-wide cache for gatling\n            CACHE = runtime.featureRuntime.suite.callOnceCache;\n        } else {\n            CACHE = runtime.featureRuntime.CALLONCE_CACHE;\n        }\n        ScenarioCall.Result result = CACHE.get(cacheKey);\n        if (result != null) {\n            logger.trace(\"callonce cache hit for: {}\", cacheKey);\n            return callOnceResult(result, sharedScope);\n        }\n        long startTime = System.currentTimeMillis();\n        logger.trace(\"callonce waiting for lock: {}\", cacheKey);\n        synchronized (CACHE) {\n            result = CACHE.get(cacheKey); // retry\n            if (result != null) {\n                long endTime = System.currentTimeMillis() - startTime;\n                logger.warn(\"this thread waited {} milliseconds for callonce lock: {}\", endTime, cacheKey);\n                return callOnceResult(result, sharedScope);\n            }\n            // this thread is the 'winner'\n            logger.info(\">> lock acquired, begin callonce: {}\", cacheKey);\n            Variable callResult = call(called, arg, sharedScope);\n            // we clone result (and config) here, to snapshot state at the point the callonce was invoked\n            Map<String, Variable> clonedVars = called.isFeature() && sharedScope ? shallowCloneVariables() : null;\n            result = new ScenarioCall.Result(callResult.copy(false), new Config(config), clonedVars);\n            CACHE.put(cacheKey, result);\n            logger.info(\"<< lock released, cached callonce: {}\", cacheKey);\n            // another routine will apply globally if needed\n            return callOnceResult(result, sharedScope);\n        }\n    }\n\n    public Object callFeature(FeatureCall featureCall, Variable arg, int index, boolean sharedScope) {\n        if (arg == null || arg.isMap()) {\n            ScenarioCall call = new ScenarioCall(runtime, featureCall, arg);\n            call.setLoopIndex(index);\n            call.setSharedScope(sharedScope);\n            FeatureRuntime fr = new FeatureRuntime(call);\n            fr.run();\n            // VERY IMPORTANT ! switch back from called feature js context\n            THREAD_LOCAL.set(this);\n            FeatureResult result = fr.result;\n            runtime.addCallResult(result);\n            if (result.isFailed()) {\n                KarateException ke = result.getErrorMessagesCombined();\n                throw ke;\n            } else {\n                return result.getVariables();\n            }\n        } else if (arg.isList() || arg.isJsOrJavaFunction()) {\n            List result = new ArrayList();\n            List<String> errors = new ArrayList();\n            int loopIndex = 0;\n            boolean isList = arg.isList();\n            Iterator iterator = isList ? arg.<List>getValue().iterator() : null;\n            while (true) {\n                Variable loopArg;\n                if (isList) {\n                    loopArg = iterator.hasNext() ? new Variable(iterator.next()) : Variable.NULL;\n                } else { // function\n                    loopArg = executeFunction(arg, new Object[]{loopIndex});\n                }\n                if (!loopArg.isMap()) {\n                    if (!isList) {\n                        logger.info(\"feature call loop function ended at index {}, returned: {}\", loopIndex, loopArg);\n                    }\n                    break;\n                }\n                try {\n                    Object loopResult = callFeature(featureCall, loopArg, loopIndex, sharedScope);\n                    result.add(loopResult);\n                } catch (Exception e) {\n                    String message = \"feature call loop failed at index: \" + loopIndex + \", \" + e.getMessage();\n                    errors.add(message);\n                    runtime.logError(message);\n                    if (!isList) { // this is a generator function, abort infinite loop !\n                        break;\n                    }\n                }\n                loopIndex++;\n            }\n            if (errors.isEmpty()) {\n                return result;\n            } else {\n                String errorMessage = StringUtils.join(errors, \"\\n\");\n                throw new KarateException(errorMessage);\n            }\n        } else {\n            throw new RuntimeException(\"feature call argument is not a json object or array: \" + arg);\n        }\n    }\n\n    public Variable evalJsonPath(Variable v, String path) {\n        Json json = Json.of(v.getValueAndForceParsingAsJson());\n        try {\n            return new Variable(json.get(path));\n        } catch (PathNotFoundException e) {\n            return Variable.NOT_PRESENT;\n        }\n    }\n\n    public static Variable evalXmlPath(Variable xml, String path) {\n        NodeList nodeList;\n        Node doc = xml.getAsXml();\n        try {\n            nodeList = XmlUtils.getNodeListByPath(doc, path);\n        } catch (Exception e) {\n            // hack, this happens for xpath functions that don't return nodes (e.g. count)\n            String strValue = XmlUtils.getTextValueByPath(doc, path);\n            Variable v = new Variable(strValue);\n            if (path.startsWith(\"count\")) { // special case\n                return new Variable(v.getAsInt());\n            } else {\n                return v;\n            }\n        }\n        int count = nodeList.getLength();\n        if (count == 0) { // xpath / node does not exist !\n            return Variable.NOT_PRESENT;\n        }\n        if (count == 1) {\n            return nodeToValue(nodeList.item(0));\n        }\n        List list = new ArrayList();\n        for (int i = 0; i < count; i++) {\n            Variable v = nodeToValue(nodeList.item(i));\n            list.add(v.getValue());\n        }\n        return new Variable(list);\n    }\n\n    private static Variable nodeToValue(Node node) {\n        int childElementCount = XmlUtils.getChildElementCount(node);\n        if (childElementCount == 0) {\n            // hack assuming this is the most common \"intent\"\n            return new Variable(node.getTextContent());\n        }\n        if (node.getNodeType() == Node.DOCUMENT_NODE) {\n            return new Variable(node);\n        } else { // make sure we create a fresh doc else future xpath would run against original root\n            return new Variable(XmlUtils.toNewDocument(node));\n        }\n    }\n\n    public Variable evalJsonPathOnVariableByName(String name, String path) {\n        Variable v = new Variable(JS.get(name)); // should work in called features\n        return evalJsonPath(v, path);\n    }\n\n    public Variable evalXmlPathOnVariableByName(String name, String path) {\n        Variable v = new Variable(JS.get(name)); // should work in called features\n        return evalXmlPath(v, path);\n    }\n\n    public Variable evalKarateExpression(String text) {\n        return evalKarateExpression(text, false);\n    }\n\n    public Variable evalKarateExpression(String text, boolean forMatch) {\n        text = StringUtils.trimToNull(text);\n        if (text == null) {\n            return Variable.NULL;\n        }\n        // don't re-evaluate if this is clearly a direct reference to a variable\n        // this avoids un-necessary conversion of xml into a map in some cases\n        // e.g. 'Given request foo' - where foo is a Variable of type XML      \n        if (JS.bindings.hasMember(text)) {\n            return new Variable(JS.get(text));\n        }\n        boolean callOnce = isCallOnceSyntax(text);\n        if (callOnce || isCallSyntax(text)) { // special case in form \"callBegin foo arg\"\n            if (callOnce) {\n                text = text.substring(9);\n            } else {\n                text = text.substring(5);\n            }\n            return call(callOnce, text, false);\n        } else if (isDollarPrefixedJsonPath(text)) {\n            return evalJsonPathOnVariableByName(RESPONSE, text);\n        } else if (isGetSyntax(text) || isDollarPrefixed(text)) { // special case in form\n            // get json[*].path\n            // $json[*].path\n            // get /xml/path\n            // get xpath-function(expression)\n            int index = -1;\n            if (text.startsWith(\"$\")) {\n                text = text.substring(1);\n            } else if (text.startsWith(\"get[\")) {\n                int pos = text.indexOf(']');\n                index = Integer.valueOf(text.substring(4, pos));\n                text = text.substring(pos + 2);\n            } else {\n                text = text.substring(4);\n            }\n            String left;\n            String right;\n            if (isDollarPrefixedJsonPath(text)) { // edge case get[0] $..foo\n                left = RESPONSE;\n                right = text;\n            } else if (isVariableAndSpaceAndPath(text)) {\n                int pos = text.indexOf(' ');\n                right = text.substring(pos + 1);\n                left = text.substring(0, pos);\n            } else {\n                StringUtils.Pair pair = parseVariableAndPath(text);\n                left = pair.left;\n                right = pair.right;\n            }\n            Variable sv;\n            if (isXmlPath(right) || isXmlPathFunction(right)) {\n                sv = evalXmlPathOnVariableByName(left, right);\n            } else {\n                sv = evalJsonPathOnVariableByName(left, right);\n            }\n            if (index != -1 && sv.isList()) {\n                List list = sv.getValue();\n                if (!list.isEmpty()) {\n                    return new Variable(list.get(index));\n                }\n            }\n            return sv;\n        } else if (isJson(text)) {\n            Json json = Json.of(text);\n            return evalEmbeddedExpressions(new Variable(json.value()), forMatch);\n        } else if (isXml(text)) {\n            Document doc = XmlUtils.toXmlDoc(text, config.isXmlNamespaceAware());\n            return evalEmbeddedExpressions(new Variable(doc), forMatch);\n        } else if (isXmlPath(text)) {\n            return evalXmlPathOnVariableByName(RESPONSE, text);\n        } else {\n            // old school function declarations e.g. function() { } need wrapping in graal\n            if (isJavaScriptFunction(text)) {\n                text = \"(\" + text + \")\";\n            }\n            // js expressions e.g. foo, foo(bar), foo.bar, foo + bar, foo + '', 5, true\n            // including arrow functions e.g. x => x + 1\n            return evalJs(text);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ScenarioFileReader.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.resource.Resource;\nimport com.intuit.karate.resource.ResourceUtils;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n *\n * @author pthomas3\n */\npublic class ScenarioFileReader {\n\n    private final ScenarioEngine engine;\n    private final FeatureRuntime featureRuntime;\n\n    public ScenarioFileReader(ScenarioEngine engine, FeatureRuntime featureRuntime) {\n        this.engine = engine;\n        this.featureRuntime = featureRuntime;\n    }\n\n    public Object readFile(String text) {\n        StringUtils.Pair pair = parsePathAndTags(text);\n        text = pair.left;\n        if (text == null && pair.right != null && pair.right.startsWith(\"@\")) {\n            return new FeatureCall(this.featureRuntime.featureCall.feature, pair.right, -1, null);\n        } else if (isJsonFile(text) || isXmlFile(text)) {\n            String contents = readFileAsString(text);\n            Variable temp = engine.evalKarateExpression(contents);\n            return temp.getValue();\n        } else if (isJavaScriptFile(text)) {\n            String contents = readFileAsString(text);\n            Variable temp = engine.evalJs(\"(\" + contents + \")\");\n            return temp.getValue();\n        } else if (isTextFile(text) || isGraphQlFile(text)) {\n            return readFileAsString(text);\n        } else if (isFeatureFile(text)) {\n            Resource fr = toResource(text);\n            Feature feature = Feature.read(fr);\n            return new FeatureCall(feature, pair.right, -1, null);\n        } else if (isCsvFile(text)) {\n            String contents = readFileAsString(text);\n            return JsonUtils.fromCsv(contents);\n        } else if (isYamlFile(text)) {\n            String contents = readFileAsString(text);\n            Object asJson = JsonUtils.fromYaml(contents);\n            Variable temp = engine.evalKarateExpression(JsonUtils.toJson(asJson));\n            return temp.getValue();\n        } else {\n            InputStream is = readFileAsStream(text);\n            return FileUtils.toBytes(is); // TODO stream\n        }\n    }\n\n    public String toAbsolutePath(String relativePath) {\n        Resource resource = toResource(relativePath);\n        try {\n            return resource.getFile().getCanonicalPath();\n        } catch (IOException e) {\n            return resource.getFile().getAbsolutePath();\n        }\n    }\n\n    public byte[] readFileAsBytes(String path) {\n        return FileUtils.toBytes(readFileAsStream(path));\n    }\n\n    public String readFileAsString(String path) {\n        return FileUtils.toString(readFileAsStream(path));\n    }\n\n    public InputStream readFileAsStream(String path) {\n        return toResource(path).getStream();\n    }\n\n    private static String removePrefix(String text) {\n        if (text == null) {\n            return null;\n        }\n        int pos = text.indexOf(':');\n        return pos == -1 ? text : text.substring(pos + 1);\n    }\n\n    private static StringUtils.Pair parsePathAndTags(String text) {\n        if (text.startsWith(\"@\")) {\n            return new StringUtils.Pair(null, StringUtils.trimToEmpty(text));\n        }\n        int pos = text.indexOf(\".feature@\");\n        if (pos == -1) {\n            pos = text.indexOf(\".feature?\");\n            if (pos != -1) {\n                text = text.substring(0, pos + 8);\n            }\n            text = StringUtils.trimToEmpty(text);\n            return new StringUtils.Pair(text, null);\n        } else {\n            return new StringUtils.Pair(StringUtils.trimToEmpty(text.substring(0, pos + 8)), StringUtils.trimToEmpty(text.substring(pos + 9)));\n        }\n    }\n\n    public Resource toResource(String path) {\n        if (isClassPath(path)) {\n            return ResourceUtils.getResource(featureRuntime.suite.workingDir, path);\n        } else if (isFilePath(path)) {\n            return ResourceUtils.getResource(featureRuntime.suite.workingDir, removePrefix(path));\n        } else if (isThisPath(path)) {\n            return featureRuntime.resolveFromThis(removePrefix(path));\n        } else {\n            return featureRuntime.resolveFromRoot(path);\n        }\n    }\n\n    private static boolean isClassPath(String text) {\n        return text.startsWith(\"classpath:\");\n    }\n\n    private static boolean isFilePath(String text) {\n        return text.startsWith(\"file:\");\n    }\n\n    private static boolean isThisPath(String text) {\n        return text.startsWith(\"this:\");\n    }\n\n    private static boolean isJsonFile(String text) {\n        return text.endsWith(\".json\");\n    }\n\n    private static boolean isJavaScriptFile(String text) {\n        return text.endsWith(\".js\");\n    }\n\n    private static boolean isYamlFile(String text) {\n        return text.endsWith(\".yaml\") || text.endsWith(\".yml\");\n    }\n\n    private static boolean isXmlFile(String text) {\n        return text.endsWith(\".xml\");\n    }\n\n    private static boolean isTextFile(String text) {\n        return text.endsWith(\".txt\");\n    }\n\n    private static boolean isCsvFile(String text) {\n        return text.endsWith(\".csv\");\n    }\n\n    private static boolean isGraphQlFile(String text) {\n        return text.endsWith(\".graphql\") || text.endsWith(\".gql\");\n    }\n\n    private static boolean isFeatureFile(String text) {\n        return text.endsWith(\".feature\");\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ScenarioIterator.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Spliterator;\nimport java.util.function.Consumer;\nimport java.util.stream.Stream;\nimport java.util.stream.StreamSupport;\nimport org.slf4j.Logger;\n\n/**\n *\n * @author pthomas3\n */\npublic class ScenarioIterator implements Spliterator<ScenarioRuntime> {\n\n    private final FeatureRuntime featureRuntime;\n    private final Iterator<FeatureSection> sections;\n\n    // state\n    private Iterator<Scenario> scenarios;\n    private Scenario currentScenario;\n\n    // dynamic\n    private ScenarioRuntime dynamicRuntime;\n    private Variable expressionValue;\n    private int index;\n\n    public ScenarioIterator(FeatureRuntime featureRuntime) {\n        this.featureRuntime = featureRuntime;\n        this.sections = featureRuntime.featureCall.feature.getSections().iterator();\n    }\n\n    public Stream<ScenarioRuntime> filterSelected() {\n        return StreamSupport.stream(this, false).filter(sr -> sr.selectedForExecution);\n    }\n\n    public ScenarioRuntime first() {\n        return filterSelected().findFirst().get();\n    }\n\n    @Override\n    public boolean tryAdvance(Consumer<? super ScenarioRuntime> action) {\n        if (currentScenario == null) {\n            if (scenarios == null) {\n                if (sections.hasNext()) {\n                    FeatureSection section = sections.next();\n                    if (section.isOutline()) {\n                        scenarios = section.getScenarioOutline().getScenarios(featureRuntime).iterator();\n                    } else {\n                        scenarios = Collections.singletonList(section.getScenario()).iterator();\n                    }\n                } else {\n                    return false;\n                }\n            }\n            if (scenarios.hasNext()) {\n                currentScenario = scenarios.next();\n                index = 0;\n                expressionValue = null;\n            } else {\n                scenarios = null;\n                return tryAdvance(action);\n            }\n        }\n        if (currentScenario.isDynamic()) {\n            Logger logger = FeatureRuntime.logger;\n            if (expressionValue == null) {                \n                dynamicRuntime = new ScenarioRuntime(featureRuntime, currentScenario);\n                if (!dynamicRuntime.selectedForExecution) { // may be un-selected by tag\n                    currentScenario = null;\n                    return tryAdvance(action);                    \n                }\n                String expression = currentScenario.getDynamicExpression();\n                ScenarioEngine prevEngine = ScenarioEngine.get();\n                try {\n                    ScenarioEngine.set(dynamicRuntime.engine);\n                    dynamicRuntime.engine.init();\n                    expressionValue = dynamicRuntime.engine.evalJs(expression);\n                    if (expressionValue.isList() || expressionValue.isJsOrJavaFunction()) {\n                        // all good\n                    } else {\n                        throw new RuntimeException(\"result is neither list nor function: \" + expressionValue);\n                    }\n                } catch (Exception e) {\n                    String message = currentScenario + \" dynamic expression evaluation failed: \" + expression;\n                    logger.error(message);\n                    dynamicRuntime.result.addFakeStepResult(message, e);\n                    currentScenario = null;\n                    action.accept(dynamicRuntime);\n                    return true; // exit early\n                } finally {\n                    ScenarioEngine.set(prevEngine);\n                }\n            }\n            final int rowIndex = index++;\n            Variable rowValue;\n            if (expressionValue.isJsOrJavaFunction()) {\n                ScenarioEngine prevEngine = ScenarioEngine.get();\n                try {\n                    ScenarioEngine.set(dynamicRuntime.engine);\n                    rowValue = dynamicRuntime.engine.executeFunction(expressionValue, rowIndex);\n                } catch (Exception e) {\n                    String message = currentScenario + \" dynamic function expression evaluation failed at index \" + rowIndex + \": \" + e.getMessage();\n                    logger.error(message);\n                    dynamicRuntime.result.addFakeStepResult(message, e);\n                    currentScenario = null;\n                    action.accept(dynamicRuntime);\n                    return true; // exit early                    \n                } finally {\n                    ScenarioEngine.set(prevEngine);\n                }\n            } else { // is list\n                List list = expressionValue.getValue();\n                if (rowIndex >= list.size()) {\n                    currentScenario = null;\n                    return tryAdvance(action);\n                }\n                rowValue = new Variable(list.get(rowIndex));\n            }\n            if (rowValue.isMap()) {\n                Scenario dynamic = currentScenario.copy(rowIndex); // this will set exampleIndex\n                Map<String, Object> map = rowValue.getValue();\n                dynamic.setExampleData(map); // and here we set exampleData\n                map.forEach((k, v) -> {\n                    Variable var = new Variable(v);\n                    dynamic.replace(\"<\" + k + \">\", var.getAsString());\n                });\n                action.accept(new ScenarioRuntime(featureRuntime, dynamic));\n                return true;\n            } else { // assume that this is signal to stop the dynamic scenario outline\n                dynamicRuntime.logger.info(\"dynamic expression complete at index: {}, not map-like: {}\", rowIndex, rowValue);\n                currentScenario = null;\n                return tryAdvance(action);\n            }\n        } else {\n            action.accept(new ScenarioRuntime(featureRuntime, currentScenario));\n            currentScenario = null;\n            return true;\n        }\n    }\n\n    @Override\n    public Spliterator<ScenarioRuntime> trySplit() {\n        return null;\n    }\n\n    @Override\n    public long estimateSize() {\n        return 0;\n    }\n\n    @Override\n    public int characteristics() {\n        return 0;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ScenarioOutline.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * @author pthomas3\n */\npublic class ScenarioOutline {\n    \n    private final Feature feature;\n    private final FeatureSection section;\n    \n    private int line;\n    private List<Tag> tags;\n    private String name;\n    private String description;\n    private List<Step> steps;\n    private List<ExamplesTable> examplesTables;\n    private int numScenarios = 0;\n    \n    public ScenarioOutline(Feature feature, FeatureSection section) {\n        this.feature = feature;\n        this.section = section;\n    }\n    \n    public Scenario toScenario(String dynamicExpression, int exampleIndex, int updateLine, List<Tag> tagsForExamples) {\n        Scenario s = new Scenario(feature, section, exampleIndex);\n        s.setName(name);\n        s.setDescription(description);\n        s.setLine(updateLine);\n        s.setDynamicExpression(dynamicExpression);\n        if (tags != null || tagsForExamples != null) {\n            List<Tag> temp = new ArrayList();\n            if (tags != null) {\n                temp.addAll(tags);\n            }\n            if (tagsForExamples != null) {\n                temp.addAll(tagsForExamples);\n            }\n            s.setTags(temp);\n        }\n        List<Step> temp = new ArrayList(steps.size());\n        s.setSteps(temp);\n        for (Step original : steps) {\n            Step step = new Step(s, original.getIndex());\n            temp.add(step);\n            step.setLine(original.getLine());\n            step.setEndLine(original.getEndLine());\n            step.setPrefix(original.getPrefix());\n            step.setText(original.getText());\n            step.setDocString(original.getDocString());\n            step.setTable(original.getTable());\n            step.setComments(original.getComments());\n        }\n        numScenarios++;\n        return s;\n    }\n    \n    public List<Scenario> getScenarios() {\n        return this.getScenarios(null);\n    }\n    \n    public List<Scenario> getScenarios(FeatureRuntime fr) {\n        List<Scenario> list = new ArrayList();\n        boolean examplesHaveTags = examplesTables.stream().anyMatch(t -> !t.getTags().isEmpty());\n        for (ExamplesTable examples : examplesTables) {\n            boolean selectedForExecution = false;\n            if (fr != null && examplesHaveTags && fr.caller.isNone()) {\n                // getting examples in the context of an execution\n                // if the examples do not have any tagged example, do not worry about selecting\n                Tags tableTags = Tags.merge(fr.featureCall.feature.getTags(), tags, examples.getTags());\n                boolean executeForTable = tableTags.evaluate(fr.suite.tagSelector, fr.suite.env);\n                if (executeForTable) {\n                    selectedForExecution = true;\n                }\n            } else {\n                selectedForExecution = true;\n            }\n            if (selectedForExecution) {\n                Table table = examples.getTable();\n                if (table.isDynamic()) {\n                    Scenario scenario = toScenario(table.getDynamicExpression(), -1, table.getLineNumberForRow(0), examples.getTags());\n                    list.add(scenario);\n                } else {\n                    int rowCount = table.getRows().size();\n                    for (int i = 1; i < rowCount; i++) { // don't include header row\n                        int exampleIndex = i - 1; // next line will set exampleIndex on scenario\n                        Scenario scenario = toScenario(null, exampleIndex, table.getLineNumberForRow(i), examples.getTags());\n                        scenario.setExampleData(table.getExampleData(exampleIndex)); // and we set exampleData here\n                        list.add(scenario);\n                        for (String key : table.getKeys()) {\n                            scenario.replace(\"<\" + key + \">\", table.getValueAsString(key, i));\n                        }\n                    }\n                }\n            }\n        }\n        return list;\n    }\n    \n    public FeatureSection getSection() {\n        return section;\n    }\n    \n    public int getLine() {\n        return line;\n    }\n    \n    public void setLine(int line) {\n        this.line = line;\n    }\n    \n    public List<Tag> getTags() {\n        return tags;\n    }\n    \n    public void setTags(List<Tag> tags) {\n        this.tags = tags;\n    }\n    \n    public String getName() {\n        return name;\n    }\n    \n    public void setName(String name) {\n        this.name = name;\n    }\n    \n    public String getDescription() {\n        return description;\n    }\n    \n    public void setDescription(String description) {\n        this.description = description;\n    }\n    \n    public List<Step> getSteps() {\n        return steps;\n    }\n    \n    public void setSteps(List<Step> steps) {\n        this.steps = steps;\n    }\n    \n    public List<ExamplesTable> getExamplesTables() {\n        return examplesTables;\n    }\n\n    public int getNumExampleTables() {\n        return examplesTables.size();\n    }\n\n    public List<Map<String, Object>> getAllExampleData() {\n        List<Map<String, Object>> exampleData = new ArrayList();\n        examplesTables.forEach(table -> exampleData.add(table.toKarateJson()));\n        return exampleData;\n    }\n    \n    public void setExamplesTables(List<ExamplesTable> examplesTables) {\n        this.examplesTables = examplesTables;\n    }\n\n    public int getNumScenarios() {\n        return numScenarios;\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ScenarioOutlineResult.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author OwenK2\n */\npublic class ScenarioOutlineResult {\n\n    final private ScenarioOutline scenarioOutline;\n    final private ScenarioRuntime runtime;\n\n    public ScenarioOutlineResult(ScenarioOutline scenarioOutline, ScenarioRuntime runtime) {\n        // NOTE: this value can be null, in which case the scenario is not from an outline\n        this.scenarioOutline = scenarioOutline;\n        this.runtime = runtime;\n    }\n\n    public Map<String, Object> toKarateJson() {\n        if (scenarioOutline == null) return null;\n        Map<String, Object> map = new HashMap();\n        map.put(\"name\", scenarioOutline.getName());\n        map.put(\"description\", scenarioOutline.getDescription());\n        map.put(\"line\", scenarioOutline.getLine());\n        map.put(\"sectionIndex\", scenarioOutline.getSection().getIndex());\n        map.put(\"exampleTableCount\", scenarioOutline.getNumExampleTables());\n        map.put(\"exampleTables\", scenarioOutline.getAllExampleData());\n        map.put(\"numScenariosToExecute\", scenarioOutline.getNumScenarios());\n        \n        // Get results of other examples in this outline\n        List<Map<String, Object>> scenarioResults = new ArrayList();\n        if (runtime.featureRuntime != null && runtime.featureRuntime.result != null) {\n            // Add all past results\n            boolean needToAddRecent = runtime.result != null;\n            for(ScenarioResult result : runtime.featureRuntime.result.getScenarioResults()) {\n                if (result.getScenario().getSection().getIndex() == scenarioOutline.getSection().getIndex()) {\n                    scenarioResults.add(result.toInfoJson());\n                    if(result.equals(runtime.result)) {\n                        needToAddRecent = false;\n                    }\n                }\n            }\n\n            // Add most recent result if we haven't already (and it's not null)\n            if (needToAddRecent) {\n                scenarioResults.add(runtime.result.toInfoJson());\n            }\n        }\n        map.put(\"scenarioResults\", scenarioResults);\n        map.put(\"numScenariosExecuted\", scenarioResults.size());\n\n        return map;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ScenarioResult.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.report.ReportUtils;\nimport com.intuit.karate.StringUtils;\nimport java.io.File;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class ScenarioResult implements Comparable<ScenarioResult> {\n\n    private final List<StepResult> stepResults = new ArrayList<>();\n    private final Scenario scenario;\n\n    private StepResult failedStep;\n\n    private String executorName;\n    private long startTime;\n    private long endTime;\n    private long durationNanos;\n\n    @Override\n    public int compareTo(ScenarioResult sr) {\n        if (sr == null) {\n            return 1;\n        }\n        int delta = scenario.getLine() - sr.scenario.getLine();\n        if (delta != 0) {\n            return delta;\n        }\n        return scenario.getExampleIndex() - sr.scenario.getExampleIndex();\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj) {\n            return true;\n        }\n        if (obj == null) {\n            return false;\n        }\n        if (getClass() != obj.getClass()) {\n            return false;\n        }\n        return compareTo((ScenarioResult)obj) == 0;\n    }\n\n    public String getFailureMessageForDisplay() {\n        if (failedStep == null) {\n            return null;\n        }        \n        Step step = failedStep.getStep();\n        String featureName = scenario.getFeature().getResource().getRelativePath();\n        return featureName + \":\" + step.getLine() + \" \" + step.getText();\n    }\n\n    public StepResult addFakeStepResult(String message, Throwable error) {\n        Step step = new Step(scenario, -1);\n        step.setLine(scenario.getLine());\n        step.setPrefix(\"*\");\n        step.setText(message);\n        Result result = error == null ? Result.passed(System.currentTimeMillis(), 0) : Result.failed(System.currentTimeMillis(), 0, error, step);\n        StepResult sr = new StepResult(step, result);\n        if (error != null) {\n            sr.setStepLog(error.getMessage());\n        }\n        addStepResult(sr);\n        return sr;\n    }\n\n    public void addStepResults(List<StepResult> value) {\n        if (value != null) {\n            value.forEach(this::addStepResult);\n        }\n    }\n\n    public void addStepResult(StepResult stepResult) {\n        stepResults.add(stepResult);\n        Result result = stepResult.getResult();\n        durationNanos += result.getDurationNanos();\n        if (result.isFailed()) {\n            failedStep = stepResult;\n        }\n    }\n\n    private static void recurse(List<Map> list, StepResult stepResult, int depth) {\n        if (stepResult.getCallResults() != null) {\n            for (FeatureResult fr : stepResult.getCallResults()) {\n                Step call = new Step(stepResult.getStep().getFeature(), -1);\n                call.setLine(stepResult.getStep().getLine());\n                call.setPrefix(StringUtils.repeat('>', depth));\n                call.setText(fr.getCallNameForReport());\n                // call.setDocString(fr.getCallArgPretty());                \n                StepResult callResult = new StepResult(call, Result.passed(stepResult.getResult().getStartTime(), 0));\n                callResult.setHidden(stepResult.isHidden());\n                list.add(callResult.toCucumberJson());\n                for (StepResult sr : fr.getAllScenarioStepResultsNotHidden()) {\n                    Map<String, Object> map = sr.toCucumberJson();\n                    String temp = (String) map.get(\"keyword\");\n                    map.put(\"keyword\", StringUtils.repeat('>', depth + 1) + ' ' + temp);\n                    list.add(map);\n                    recurse(list, sr, depth + 1);\n                }\n            }\n        }\n    }\n\n    private List<Map> getStepResults(boolean background) {\n        List<Map> list = new ArrayList(stepResults.size());\n        for (StepResult stepResult : stepResults) {\n            if (stepResult.isHidden()) {\n                continue;\n            }\n            if (background == stepResult.getStep().isBackground()) {\n                list.add(stepResult.toCucumberJson());\n                recurse(list, stepResult, 0);\n            }\n        }\n        return list;\n    }\n\n    public static ScenarioResult fromKarateJson(File workingDir, Feature feature, Map<String, Object> map) {\n        int sectionIndex = (Integer) map.get(\"sectionIndex\");\n        int exampleIndex = (Integer) map.get(\"exampleIndex\");\n        FeatureSection section = feature.getSection(sectionIndex);\n        Scenario scenario = new Scenario(feature, section, exampleIndex);\n        if (section.isOutline()) {\n            scenario.setTags(section.getScenarioOutline().getTags());\n            scenario.setDescription(section.getScenarioOutline().getDescription());\n        } else {\n            scenario.setTags(section.getScenario().getTags());\n            scenario.setDescription(section.getScenario().getDescription());\n        }\n        scenario.setName((String) map.get(\"name\"));\n        scenario.setDescription((String) map.get(\"description\"));\n        scenario.setLine((Integer) map.get(\"line\"));\n        scenario.setExampleData((Map) map.get(\"exampleData\"));\n        ScenarioResult sr = new ScenarioResult(scenario);\n        String executorName = (String) map.get(\"executorName\");\n        Number startTime = (Number) map.get(\"startTime\");\n        Number endTime = (Number) map.get(\"endTime\");\n        sr.setExecutorName(executorName);\n        if (startTime != null) {\n            sr.setStartTime(startTime.longValue());\n        }\n        if (endTime != null) {\n            sr.setEndTime(endTime.longValue());\n        }\n        List<Map<String, Object>> list = (List) map.get(\"stepResults\");\n        if (list != null) {\n            List<Step> steps = new ArrayList(list.size());\n            for (Map<String, Object> stepResultMap : list) {\n                StepResult stepResult = StepResult.fromKarateJson(workingDir, scenario, stepResultMap);\n                sr.addStepResult(stepResult);\n                Step step = stepResult.getStep();\n                if (!step.isBackground() && step.getLine() != -1) {\n                    steps.add(step);\n                }\n            }\n            scenario.setSteps(steps);\n        }\n        \n\t\tif (scenario.getTagsEffective().contains(Tag.FAIL) && sr.isFailed()) {\n\t\t\tif (!sr.getErrorMessage().startsWith(ScenarioRuntime.EXPECT_TEST_TO_FAIL_BECAUSE_OF_FAIL_TAG)) {\n\t\t\t\tsr.ignoreFailedStep();\n\t\t\t}\n\n\t\t}\n        return sr;\n    }\n\n    public Map<String, Object> toKarateJson() {\n        Map<String, Object> map = new HashMap();\n        // these first few are only for the ease of reports\n        // note that they are not involved in the reverse fromKarateJson()\n        map.put(\"durationMillis\", getDurationMillis());\n        List<String> tags = scenario.getTagsEffective().getTags();\n        if (tags != null && !tags.isEmpty()) {\n            map.put(\"tags\", tags);\n        }\n        map.put(\"failed\", isFailed());\n        map.put(\"refId\", scenario.getRefId());\n        if (isFailed()) {\n            map.put(\"error\", getErrorMessage());\n        }\n        //======================================================================\n        map.put(\"sectionIndex\", scenario.getSection().getIndex());\n        map.put(\"exampleIndex\", scenario.getExampleIndex());\n        Map<String, Object> exampleData = scenario.getExampleData();\n        if (exampleData != null) {\n            map.put(\"exampleData\", exampleData);\n        }\n        map.put(\"name\", scenario.getName());\n        map.put(\"description\", scenario.getDescription());\n        map.put(\"line\", scenario.getLine());\n        map.put(\"executorName\", executorName);\n        map.put(\"startTime\", startTime);\n        map.put(\"endTime\", endTime);\n        List<Map<String, Object>> list = new ArrayList(stepResults.size());\n        map.put(\"stepResults\", list);\n        for (StepResult sr : stepResults) {\n            list.add(sr.toKarateJson());\n        }\n        return map;\n    }\n\n    // Paired down information for use in karate.scenarioOutline\n    public Map<String, Object> toInfoJson() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"durationMillis\", getDurationMillis());\n        List<String> tags = scenario.getTagsEffective().getTags();\n        if (tags != null && !tags.isEmpty()) {\n            map.put(\"tags\", tags);\n        }\n        map.put(\"failed\", isFailed());\n        map.put(\"refId\", scenario.getRefId());\n        map.put(\"sectionIndex\", scenario.getSection().getIndex());\n        map.put(\"exampleIndex\", scenario.getExampleIndex());\n        map.put(\"name\", scenario.getName());\n        map.put(\"description\", scenario.getDescription());\n        map.put(\"line\", scenario.getLine());\n        map.put(\"executorName\", executorName);\n        map.put(\"startTime\", startTime);\n        map.put(\"endTime\", endTime);\n        return map;\n    }\n\n    public Map<String, Object> toCucumberJson() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"name\", scenario.getName());\n        map.put(\"steps\", getStepResults(false));\n        map.put(\"line\", scenario.getLine());\n        map.put(\"id\", StringUtils.toIdString(scenario.getName()));\n        map.put(\"description\", scenario.getDescription());\n        map.put(\"type\", \"scenario\");\n        map.put(\"keyword\", scenario.isOutlineExample() ? \"Scenario Outline\" : \"Scenario\");\n        map.put(\"tags\", tagsToCucumberJson(scenario.getTagsEffective().getOriginal()));\n        map.put(\"start_timestamp\", Instant.ofEpochMilli(startTime).toString());\n        return map;\n    }\n\n    public static List<Map> tagsToCucumberJson(Collection<Tag> tags) {\n        List<Map> list = new ArrayList(tags.size());\n        for (Tag tag : tags) {\n            Map<String, Object> tagMap = new HashMap(2);\n            tagMap.put(\"line\", tag.getLine());\n            tagMap.put(\"name\", '@' + tag.getText());\n            list.add(tagMap);\n        }\n        return list;\n    }\n\n    public Map<String, Object> backgroundToCucumberJson() {\n        if (!scenario.getFeature().isBackgroundPresent()) {\n            return null;\n        }\n        Map<String, Object> map = new HashMap();\n        map.put(\"name\", \"\");\n        map.put(\"steps\", getStepResults(true));\n        map.put(\"line\", scenario.getFeature().getBackground().getLine());\n        map.put(\"description\", \"\");\n        map.put(\"type\", Background.TYPE);\n        map.put(\"keyword\", Background.KEYWORD);\n        return map;\n    }\n\n    public ScenarioResult(Scenario scenario) {\n        this.scenario = scenario;\n    }\n\n    public Scenario getScenario() {\n        return scenario;\n    }\n\n    public List<StepResult> getStepResults() {\n        return stepResults;\n    }\n\n    public List<StepResult> getStepResultsNotHidden() {\n        List<StepResult> list = new ArrayList(stepResults.size());\n        for (StepResult sr : stepResults) {\n            if (sr.isHidden()) {\n                continue;\n            }\n            list.add(sr);\n        }\n        return list;\n    }\n\n    public boolean isFailed() {\n        return failedStep != null;\n    }\n\n    public StepResult getFailedStep() {\n        return failedStep;\n    }\n\n    public Throwable getError() {\n        return failedStep == null ? null : failedStep.getResult().getError();\n    }\n\n    public String getErrorMessage() {\n        return failedStep == null ? null : failedStep.getResult().getErrorMessage();\n    }\n\n    public long getDurationNanos() {\n        return durationNanos;\n    }\n\n    public double getDurationMillis() {\n        return ReportUtils.nanosToMillis(durationNanos);\n    }\n\n    public String getExecutorName() {\n        return executorName;\n    }\n\n    public void setExecutorName(String executorName) {\n        this.executorName = executorName;\n    }\n\n    public long getStartTime() {\n        return startTime;\n    }\n\n    public void setStartTime(long startTime) {\n        this.startTime = startTime;\n    }\n\n    public long getEndTime() {\n        return endTime;\n    }\n\n    public void setEndTime(long endTime) {\n        this.endTime = endTime;\n    }\n\n    @Override\n    public String toString() {\n        return failedStep == null ? scenario.toString() : failedStep + \"\";\n    }\n\n    public void ignoreFailedStep() {\n        failedStep = null;\n    }\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/ScenarioRuntime.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.LogAppender;\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.RuntimeHook;\nimport com.intuit.karate.ScenarioActions;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.graal.JsEngine;\nimport com.intuit.karate.http.ResourceType;\nimport com.intuit.karate.shell.StringLogAppender;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * @author pthomas3\n */\npublic class ScenarioRuntime implements Runnable {\n\n    public static final String EXPECT_TEST_TO_FAIL_BECAUSE_OF_FAIL_TAG = \"Expect test to fail because of @fail tag\";\n    public final Logger logger;\n    public final FeatureRuntime featureRuntime;\n    public final ScenarioCall caller;\n    public final Scenario scenario;\n    public final Tags tags;\n    public final ScenarioActions actions;\n    public final ScenarioResult result;\n    public final ScenarioOutlineResult outlineResult;\n    public final ScenarioEngine engine;\n    public final boolean reportDisabled;\n    public final Map<String, Object> magicVariables;\n    public final boolean selectedForExecution;\n    public final boolean perfMode;\n    public final boolean dryRun;\n    public final LogAppender logAppender;\n\n    private boolean skipBackground;\n    private boolean ignoringFailureSteps;\n\n    public ScenarioRuntime(FeatureRuntime featureRuntime, Scenario scenario) {\n        logger = new Logger();\n        this.featureRuntime = featureRuntime;\n        this.caller = featureRuntime.caller;\n        perfMode = featureRuntime.perfHook != null;\n        if (caller.isNone()) {\n            logAppender = new StringLogAppender(false);\n            engine = new ScenarioEngine(caller.getParentConfig(false), this, new HashMap<>(), logger);\n        } else if (caller.isSharedScope()) {\n            logAppender = caller.parentRuntime.logAppender;\n            engine = new ScenarioEngine(caller.getParentConfig(false), this, caller.getParentVars(false), logger);\n        } else { // new, but clone and copy data\n            logAppender = caller.parentRuntime.logAppender;\n            // in this case, parent variables are set via magic variables - see initMagicVariables()\n            engine = new ScenarioEngine(caller.getParentConfig(true), this, new HashMap<>(), logger);\n        }\n        logger.setAppender(logAppender);\n        actions = new ScenarioActions(engine);\n        this.scenario = scenario;\n        if (scenario.isDynamic() && !scenario.isOutlineExample()) { // from dynamic scenario iterator\n            steps = Collections.emptyList();\n            skipped = true; // ensures run() is a no-op\n            magicVariables = Collections.emptyMap();\n        } else {\n            magicVariables = initMagicVariables();\n        }\n        result = new ScenarioResult(scenario);\n        outlineResult = new ScenarioOutlineResult(scenario.getSection().getScenarioOutline(), this);\n        if (featureRuntime.setupResult != null) {\n            // TODO improve this and simplify report rendering code in report/karate-feature.html\n            StepResult sr = result.addFakeStepResult(\"@setup\", null);\n            List<FeatureResult> list = new ArrayList<>(1);\n            FeatureResult fr = new FeatureResult(featureRuntime.featureCall.feature);\n            fr.setCallDepth(1);\n            fr.addResult(featureRuntime.setupResult);\n            list.add(fr);\n            sr.addCallResults(list);\n            featureRuntime.setupResult = null;\n        }\n        dryRun = featureRuntime.suite.dryRun;\n        tags = scenario.getTagsEffective();\n        reportDisabled = perfMode ? true : tags.valuesFor(\"report\").isAnyOf(\"false\");\n        selectedForExecution = isSelectedForExecution(featureRuntime, scenario, tags);\n    }\n\n    private Map<String, Object> initMagicVariables() {\n        // magic variables are only in the JS engine - [ see ScenarioEngine.init() ]\n        // and not \"visible\" and tracked in ScenarioEngine.vars\n        // one consequence is that they won't show up in the debug variables view\n        // but more importantly don't get passed back to caller and float around, bloating memory        \n        Map<String, Object> map = new HashMap<>();\n        if (!caller.isNone()) {\n            // karate principle: parent variables are always \"visible\"\n            // so we inject the parent variables\n            // but they will be over-written by what is local to this scenario\n            if (!caller.isSharedScope()) {\n                // shallow clone variables if not shared scope\n                Map<String, Variable> copy = caller.getParentVars(true);\n                copy.forEach((k, v) -> map.put(k, v.getValue()));\n            }\n            map.putAll(caller.parentRuntime.magicVariables);\n            map.put(\"__arg\", caller.arg == null ? null : caller.arg.getValue());\n            map.put(\"__loop\", caller.getLoopIndex());\n        }\n        if (scenario.isOutlineExample()) { // init examples row magic variables            \n            Map<String, Object> exampleData = scenario.getExampleData();\n            map.putAll(exampleData);\n            map.put(\"__row\", exampleData);\n            map.put(\"__num\", scenario.getExampleIndex());\n        }\n        return map;\n    }\n\n    public boolean isFailed() {\n        return error != null || result.isFailed();\n    }\n\n    public Step getCurrentStep() {\n        return currentStep;\n    }\n\n    public boolean isStopped() {\n        return stopped;\n    }\n\n    public boolean isSkipBackground() {\n        return this.skipBackground;\n    }\n\n    public void setSkipBackground(boolean skipBackground) {\n        this.skipBackground = skipBackground;\n    }\n\n    public String getEmbedFileName(ResourceType resourceType) {\n        String extension = resourceType == null ? null : resourceType.getExtension();\n        return scenario.getUniqueId() + \"_\" + System.currentTimeMillis() + (extension == null ? \"\" : \".\" + extension);\n    }\n\n    private Embed saveToFileAndCreateEmbed(byte[] bytes, ResourceType resourceType) {\n        File file = new File(featureRuntime.suite.reportDir + File.separator + getEmbedFileName(resourceType));\n        FileUtils.writeToFile(file, bytes);\n        return new Embed(file, resourceType);\n    }\n\n    public Embed embed(byte[] bytes, ResourceType resourceType) {\n        if (embeds == null) {\n            embeds = new ArrayList<>();\n        }\n        Embed embed = saveToFileAndCreateEmbed(bytes, resourceType);\n        embeds.add(embed);\n        return embed;\n    }\n\n    public Embed embedVideo(File file) {\n        StepResult stepResult = result.addFakeStepResult(\"[video]\", null);\n        Embed embed = saveToFileAndCreateEmbed(FileUtils.toBytes(file), ResourceType.MP4);\n        stepResult.addEmbed(embed);\n        return embed;\n    }\n\n    private List<FeatureResult> callResults;\n\n    public void addCallResult(FeatureResult fr) {\n        if (callResults == null) {\n            callResults = new ArrayList<>();\n        }\n        callResults.add(fr);\n    }\n\n    public LogAppender getLogAppender() {\n        return logAppender;\n    }\n\n    private List<Step> steps;\n    private List<Embed> embeds;\n    private StepResult currentStepResult;\n    private Step currentStep;\n    private Throwable error;\n    private boolean configFailed;\n    private boolean skipped; // beforeScenario hook only\n    private boolean stopped;\n    private boolean aborted;\n    private int stepIndex;\n\n    public void stepBack() {\n        stopped = false;\n        stepIndex -= 2;\n        if (stepIndex < 0) {\n            stepIndex = 0;\n        }\n    }\n\n    public void stepReset() {\n        stopped = false;\n        stepIndex--;\n        if (stepIndex < 0) { // maybe not required\n            stepIndex = 0;\n        }\n    }\n\n    public void stepProceed() {\n        stopped = false;\n    }\n\n    private int nextStepIndex() {\n        return stepIndex++;\n    }\n\n    public Result evalAsStep(String expression) {\n        Step evalStep = new Step(scenario, -1);\n        try {\n            evalStep.parseAndUpdateFrom(expression);\n        } catch (Exception e) {\n            return Result.failed(System.currentTimeMillis(), 0, e, evalStep);\n        }\n        return StepRuntime.execute(evalStep, actions);\n    }\n\n    public boolean hotReload() {\n        boolean success = false;\n        Feature feature = scenario.getFeature();\n        feature = Feature.read(feature.getResource());\n        for (Step oldStep : steps) {\n            Step newStep = feature.findStepByLine(oldStep.getLine());\n            if (newStep == null) {\n                continue;\n            }\n            String oldText = oldStep.getText();\n            String newText = newStep.getText();\n            if (!oldText.equals(newText)) {\n                try {\n                    oldStep.parseAndUpdateFrom(newStep.getText());\n                    logger.info(\"hot reloaded line: {} - {}\", newStep.getLine(), newStep.getText());\n                    success = true;\n                } catch (Exception e) {\n                    logger.warn(\"failed to hot reload step: {}\", e.getMessage());\n                }\n            }\n        }\n        return success;\n    }\n\n    public Map<String, Object> getScenarioInfo() {\n        Map<String, Object> info = new HashMap<>(5);\n        File featureFile = featureRuntime.featureCall.feature.getResource().getFile();\n        if (featureFile != null) {\n            info.put(\"featureDir\", featureFile.getParent());\n            info.put(\"featureFileName\", featureFile.getName());\n        }\n        info.put(\"scenarioName\", scenario.getName());\n        info.put(\"scenarioDescription\", scenario.getDescription());\n        String errorMessage = error == null ? null : error.getMessage();\n        info.put(\"errorMessage\", errorMessage);\n        return info;\n    }\n\n    protected void logError(String message) {\n        if (currentStep != null) {\n            message = currentStep.getDebugInfo()\n                    + \"\\n\" + currentStep.toString()\n                    + \"\\n\" + message;\n        }\n        logger.error(\"{}\", message);\n    }\n\n    private void evalConfigJs(String js, String displayName) {\n        if (js == null || configFailed) {\n            return;\n        }\n        try {\n            Variable fun = engine.evalJs(\"(\" + js + \")\");\n            if (!fun.isJsFunction()) {\n                logger.warn(\"not a valid js function: {}\", displayName);\n                return;\n            }\n            Map<String, Object> map = engine.getOrEvalAsMap(fun);\n            engine.setVariables(map);\n        } catch (Exception e) {\n            String message = \">> \" + scenario.getDebugInfo() + \"\\n>> \" + displayName + \" failed\\n>> \" + e.getMessage();\n            error = JsEngine.fromJsEvalException(js, e, message);\n            stopped = true;\n            configFailed = true;\n        }\n    }\n\n    private static boolean isSelectedForExecution(FeatureRuntime fr, Scenario scenario, Tags tags) {\n        org.slf4j.Logger logger = FeatureRuntime.logger;\n        int callLine = fr.featureCall.callLine;\n        if (callLine != -1) {\n            int sectionLine = scenario.getSection().getLine();\n            int scenarioLine = scenario.getLine();\n            if (callLine == sectionLine || callLine == scenarioLine) {\n                logger.info(\"found scenario at line: {}\", callLine);\n                return true;\n            }\n            logger.trace(\"skipping scenario at line: {}, needed: {}\", scenario.getLine(), callLine);\n            return false;\n        }\n        String callName = fr.featureCall.callName;\n        if (callName != null) {\n            if (scenario.getName().matches(callName)) {\n                logger.info(\"found scenario at line: {} - {}\", scenario.getLine(), callName);\n                return true;\n            }\n            logger.trace(\"skipping scenario at line: {} - {}, needed: {}\", scenario.getLine(), scenario.getName(), callName);\n            return false;\n        }\n        String callTag = fr.featureCall.callTag;\n        if (callTag != null) {\n            if (tags.contains(callTag)) {\n                logger.info(\"{} - call by tag at line {}: {}\", fr, scenario.getLine(), callTag);\n                return true;\n            } else {\n                logger.trace(\"skipping scenario at line: {} with call by tag effective: {}\", scenario.getLine(), callTag);\n                return false;\n            }\n        }\n        if (fr.caller.isNone()) {\n            if (tags.evaluate(fr.suite.tagSelector, fr.suite.env)) {\n                logger.trace(\"matched scenario at line: {} with tags effective: {}\", scenario.getLine(), tags.getTags());\n                return true;\n            }\n            logger.trace(\"skipping scenario at line: {} with tags effective: {}\", scenario.getLine(), tags.getTags());\n            return false;\n        } else {\n            return true; // when called, tags are ignored, all scenarios will be run\n        }\n    }\n\n    //==========================================================================\n    //\n    public void beforeRun() {\n        if (featureRuntime.caller.isNone() && featureRuntime.suite.isAborted()) {\n            skipped = true;\n            return;\n        }\n        steps = skipBackground ? scenario.getSteps() : scenario.getStepsIncludingBackground();\n        ScenarioEngine.set(engine);\n        engine.init();\n        result.setExecutorName(Thread.currentThread().getName());\n        result.setStartTime(System.currentTimeMillis());\n        if (!dryRun) {\n            if (caller.isNone() && !caller.isKarateConfigDisabled()) {\n                // evaluate config js, variables above will apply !\n                evalConfigJs(featureRuntime.suite.karateBase, \"karate-base.js\");\n                evalConfigJs(featureRuntime.suite.karateConfig, \"karate-config.js\");\n                evalConfigJs(featureRuntime.suite.karateConfigEnv, \"karate-config-\" + featureRuntime.suite.env + \".js\");\n            }\n            skipped = !featureRuntime.suite.hooks.stream()\n                    .map(h -> h.beforeScenario(this))\n                    .reduce(Boolean.TRUE, Boolean::logicalAnd);\n            if (skipped) {\n                logger.debug(\"beforeScenario hook returned false, will skip scenario: {}\", scenario);\n            } else {\n                evaluateScenarioName();\n            }\n        }\n    }\n\n    @Override\n    public void run() {\n        try { // make sure we call afterRun() even on crashes\n            // and operate countdown latches, else we may hang the parallel runner\n            if (steps == null) {\n                beforeRun();\n            }\n            if (skipped) {\n                return;\n            }\n            int count = steps.size();\n            int index = 0;\n            while ((index = nextStepIndex()) < count) {\n                currentStep = steps.get(index);\n                execute(currentStep);\n                if (currentStepResult != null) { // can be null if debug step-back or hook skip\n                    result.addStepResult(currentStepResult);\n                }\n            }\n        } catch (Exception e) {\n            if (currentStepResult != null) {\n                result.addStepResult(currentStepResult);\n            }\n            logError(\"scenario [run] failed\\n\" + StringUtils.throwableToString(e));\n            currentStepResult = result.addFakeStepResult(\"scenario [run] failed\", e);\n        } finally {\n            if (!skipped) {\n                afterRun();\n                if (isFailed() && engine.getConfig().isAbortSuiteOnFailure()) {\n                    featureRuntime.suite.abort();\n                }\n            }\n            if (caller.isNone()) {\n                logAppender.close(); // reclaim memory\n            }\n        }\n    }\n\n    public StepResult execute(Step step) {\n        if (!stopped && !dryRun) {\n            boolean shouldExecute = true;\n            for (RuntimeHook hook : featureRuntime.suite.hooks) {\n                if (!hook.beforeStep(step, this)) {\n                    shouldExecute = false;\n                }\n            }\n            if (!shouldExecute) {\n                return null;\n            }\n        }\n        Result stepResult;\n        final boolean executed = !stopped;\n        if (stopped) {\n            if (aborted && engine.getConfig().isAbortedStepsShouldPass()) {\n                stepResult = Result.passed(System.currentTimeMillis(), 0);\n            } else if (configFailed) {\n                stepResult = Result.failed(System.currentTimeMillis(), 0, error, step);\n            } else {\n                stepResult = Result.skipped(System.currentTimeMillis());\n            }\n        } else if (dryRun && !step.isSetup()) {\n            stepResult = Result.passed(System.currentTimeMillis(), 0);\n        } else {\n            stepResult = StepRuntime.execute(step, actions);\n        }\n        currentStepResult = new StepResult(step, stepResult);\n        if (stepResult.isAborted()) { // we log only aborts for visibility\n            aborted = true;\n            stopped = true;\n            logger.debug(\"abort at {}\", step.getDebugInfo());\n        } else if (stepResult.isFailed()) {\n            if (stepResult.getMatchingMethod() != null && engine.getConfig().getContinueOnStepFailureMethods().contains(stepResult.getMatchingMethod().method)) {\n                stopped = false;\n                ignoringFailureSteps = true;\n                currentStepResult.setErrorIgnored(true);\n            } else {\n                stopped = true;\n            }\n            if (stopped && (!this.engine.getConfig().isContinueAfterContinueOnStepFailure() || !engine.isIgnoringStepErrors())) {\n                error = stepResult.getError();\n                logError(error.getMessage());\n            }\n            if (engine.driver != null) {\n                engine.driver.onFailure(currentStepResult);\n            }\n            if (engine.robot != null) {\n                engine.robot.onFailure(currentStepResult);\n            }\n        } else {\n            boolean hidden = reportDisabled || (step.isPrefixStar() && !step.isPrint() && !engine.getConfig().isShowAllSteps());\n            currentStepResult.setHidden(hidden);\n        }\n        addStepLogEmbedsAndCallResults();\n        if (currentStepResult.isErrorIgnored()) {\n            engine.setFailedReason(null);\n        }\n        if (!engine.isIgnoringStepErrors() && ignoringFailureSteps) {\n            if (engine.getConfig().isContinueAfterContinueOnStepFailure()) {\n                // continue execution and reset failed reason for engine to null\n                engine.setFailedReason(null);\n                ignoringFailureSteps = false;\n            } else {\n                // stop execution\n                // keep failed reason for scenario as the last failed step that was ignored\n                stopped = true;\n            }\n        }\n        if (executed && !dryRun) {\n            featureRuntime.suite.hooks.forEach(h -> h.afterStep(currentStepResult, this));\n        }\n        return currentStepResult;\n    }\n\n    public void afterRun() {\n        try {\n            result.setEndTime(System.currentTimeMillis());\n            engine.logLastPerfEvent(result.getFailureMessageForDisplay());\n            if (currentStepResult == null) {\n                currentStepResult = result.addFakeStepResult(\"no steps executed\", null);\n            }\n            if (!dryRun) {\n                engine.invokeAfterHookIfConfigured(AfterHookType.AFTER_SCENARIO);\n                featureRuntime.suite.hooks.forEach(h -> h.afterScenario(this));\n                engine.stop(currentStepResult);\n            }\n            addStepLogEmbedsAndCallResults();\n            if (tags.contains(Tag.FAIL)) {\n                if (result.isFailed()) {\n                    result.ignoreFailedStep();\n                    result.addFakeStepResult(EXPECT_TEST_TO_FAIL_BECAUSE_OF_FAIL_TAG, null);\n                } else {\n                    result.addFakeStepResult(EXPECT_TEST_TO_FAIL_BECAUSE_OF_FAIL_TAG,\n                            new Throwable(EXPECT_TEST_TO_FAIL_BECAUSE_OF_FAIL_TAG));\n                }\n            }\n        } catch (Exception e) {\n            error = e;\n            logError(\"scenario [cleanup] failed\\n\" + e.getMessage());\n            currentStepResult = result.addFakeStepResult(\"scenario [cleanup] failed\", e);\n        }\n    }\n\n    private void addStepLogEmbedsAndCallResults() {\n        boolean showLog = !reportDisabled && engine.getConfig().isShowLog();\n        String stepLog = logAppender.collect();\n        if (showLog) {\n            currentStepResult.appendToStepLog(stepLog);\n            if (currentStepResult.isErrorIgnored()) {\n                currentStepResult.appendToStepLog(currentStepResult.getErrorMessage());\n            }\n        }\n        if (callResults != null) {\n            currentStepResult.addCallResults(callResults);\n            callResults = null;\n        }\n        if (embeds != null) {\n            currentStepResult.addEmbeds(embeds);\n            embeds = null;\n        }\n    }\n\n    @Override\n    public String toString() {\n        return scenario.toString();\n    }\n\n    public void evaluateScenarioName() {\n        String scenarioName = scenario.getName();\n        boolean wrappedByBackTick = scenarioName != null\n                && scenarioName.length() > 1\n                && '`' == scenarioName.charAt(0)\n                && '`' == scenarioName.charAt((scenarioName.length() - 1));\n        boolean hasJavascriptPlaceholder = ScenarioEngine.hasJavaScriptPlacehoder(scenarioName);\n        if (wrappedByBackTick || hasJavascriptPlaceholder) {\n            String eval = scenarioName;\n            if (!wrappedByBackTick) {\n                eval = '`' + eval + '`';\n            }\n            String evaluatedScenarioName = engine.evalJs(eval).getAsString();\n            scenario.setName(evaluatedScenarioName);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Step.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.KarateException;\nimport com.intuit.karate.resource.MemoryResource;\nimport com.intuit.karate.resource.Resource;\n\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * @author pthomas3\n */\npublic class Step {\n\n    private final Feature feature;\n    private final Scenario scenario; // can be  null for background !!\n    private final int index;\n\n    private int line;\n    private int endLine;\n    private List<String> comments;\n    private String prefix;\n    private String text;\n    private String docString;\n    private Table table;\n\n    public static final List<String> PREFIXES = Arrays.asList(\"*\", \"Given\", \"When\", \"Then\", \"And\", \"But\");\n\n    public void parseAndUpdateFrom(String text) {\n        final String stepText = text.trim();\n        boolean hasPrefix = PREFIXES.stream().anyMatch(prefixValue -> stepText.startsWith(prefixValue));\n        // to avoid parser considering text without prefix as scenario comments / doc-string\n        if (!hasPrefix) {\n            text = \"* \" + stepText;\n        }\n        Resource resource = new MemoryResource(scenario.getFeature().getResource().getFile(), \"Feature:\\nScenario:\\n\" + text);\n        Feature tempFeature = Feature.read(resource);\n        Step tempStep = tempFeature.getStep(0, -1, 0);\n        if (tempStep == null) {\n            throw new KarateException(\"invalid expression: \" + text);\n        }\n        this.prefix = tempStep.prefix;\n        this.text = tempStep.text;\n        this.docString = tempStep.docString;\n        this.table = tempStep.table;\n    }\n\n    public String getDebugInfo() {\n        return feature + \":\" + line;\n    }\n\n    public boolean isPrint() {\n        return text != null && text.startsWith(\"print\");\n    }\n\n    public boolean isPrefixStar() {\n        return \"*\".equals(prefix);\n    }\n\n    public Feature getFeature() {\n        return feature;\n    }\n\n    public Step(Feature feature, int index) {\n        this.feature = feature;\n        this.scenario = null;\n        this.index = index;\n    }\n\n    public Step(Scenario scenario, int index) {\n        this.scenario = scenario;\n        this.feature = scenario.getFeature();\n        this.index = index;\n    }\n\n    public static Step fromKarateJson(Scenario scenario, Map<String, Object> map) {\n        int index = (Integer) map.get(\"index\");\n        Boolean background = (Boolean) map.get(\"background\");\n        if (background == null) {\n            background = false;\n        }\n        Step step = background ? new Step(scenario.getFeature(), index) : new Step(scenario, index);\n        int line = (Integer) map.get(\"line\");\n        step.setLine(line);\n        Integer endLine = (Integer) map.get(\"endLine\");\n        if (endLine == null) {\n            endLine = line;\n        }\n        step.setEndLine(endLine);\n        if(map.get(\"comments\") instanceof List) {\n            step.setComments((List) map.get(\"comments\"));\n        }\n        step.setPrefix((String) map.get(\"prefix\"));\n        step.setText((String) map.get(\"text\"));\n        step.setDocString((String) map.get(\"docString\"));\n        if(map.get(\"table\") instanceof List) {\n            List<Map<String, Object>> table = (List) map.get(\"table\");\n            if (table != null) {\n                step.setTable(Table.fromKarateJson(table));\n            }\n        }\n        return step;\n    }\n\n    public Map<String, Object> toKarateJson() {\n        Map<String, Object> map = new HashMap();\n        if (isBackground()) {\n            map.put(\"background\", true);\n        }\n        map.put(\"index\", index);\n        map.put(\"line\", line);\n        if (endLine != line) {\n            map.put(\"endLine\", endLine);\n        }\n        if (comments != null && !comments.isEmpty()) {\n            map.put(\"comments\", comments);\n        }\n        map.put(\"prefix\", prefix);\n        map.put(\"text\", text);\n        if (docString != null) {\n            map.put(\"docString\", docString);\n        }\n        if (table != null) {\n            map.put(\"table\", table.toKarateJson());\n        }\n        return map;\n    }\n\n    public boolean isBackground() {\n        return scenario == null;\n    }\n\n    public boolean isOutline() {\n        return scenario != null && scenario.isOutlineExample();\n    }\n\n    public int getIndex() {\n        return index;\n    }\n\n    public int getLine() {\n        return line;\n    }\n\n    public void setLine(int line) {\n        this.line = line;\n    }\n\n    public int getLineCount() {\n        return endLine - line + 1;\n    }\n\n    public int getEndLine() {\n        return endLine;\n    }\n\n    public void setEndLine(int endLine) {\n        this.endLine = endLine;\n    }\n\n    public String getPrefix() {\n        return prefix;\n    }\n\n    public void setPrefix(String prefix) {\n        this.prefix = prefix;\n    }\n\n    public String getText() {\n        return text;\n    }\n\n    public void setText(String text) {\n        this.text = text;\n    }\n\n    public String getDocString() {\n        return docString;\n    }\n\n    public void setDocString(String docString) {\n        this.docString = docString;\n    }\n\n    public Table getTable() {\n        return table;\n    }\n\n    public void setTable(Table table) {\n        this.table = table;\n    }\n\n    public List<String> getComments() {\n        return comments;\n    }\n\n    public void setComments(List<String> comments) {\n        this.comments = comments;\n    }\n\n    public boolean isFake() {\n        return getIndex() == -1;\n    }\n\n    public boolean isSetup() {\n        return scenario !=null && scenario.isSetup();\n    }\n\n    @Override\n    public String toString() {\n        String temp = prefix + \" \" + text;\n        if (docString != null) {\n            temp = temp + \"\\n\\\"\\\"\\\"\\n\" + docString + \"\\n\\\"\\\"\\\"\";\n        }\n        if (table != null) {\n            temp = temp + \" \" + table.toString();\n        }\n        return temp;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/StepResult.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.Json;\nimport com.intuit.karate.StringUtils;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class StepResult {\n\n    private static final Map<String, Object> DUMMY_MATCH;\n\n    private final Step step;\n    private final Result result;\n\n    private boolean hidden;\n    private List<Embed> embeds;\n    private List<FeatureResult> callResults;\n    private String stepLog;\n    private boolean errorIgnored = false;\n    private Throwable failedReason;\n\n    public String getErrorMessage() {\n        if (result == null) {\n            return null;\n        }\n        Throwable error = result.getError();\n        return error == null ? null : error.getMessage();\n    }\n\n    public void appendToStepLog(String log) {\n        if (log == null) {\n            return;\n        }\n        if (stepLog == null) {\n            stepLog = \"\";\n        }\n        stepLog = stepLog + log;\n    }\n\n    public void setStepLog(String stepLog) {\n        this.stepLog = stepLog;\n    }\n\n    public void setCallResults(List<FeatureResult> callResults) {\n        this.callResults = callResults;\n    }\n\n    public void addEmbeds(List<Embed> value) {\n        if (value != null) {\n            if (embeds == null) {\n                embeds = new ArrayList();\n            }\n            embeds.addAll(value);\n        }\n    }\n\n    public void setCallResultsFromKarateJson(File workingDir, List<Map<String, Object>> list) {\n        if (list != null) {\n            callResults = new ArrayList(list.size());\n            for (Map<String, Object> map : list) {\n                FeatureResult fr = FeatureResult.fromKarateJson(workingDir, map);\n                callResults.add(fr);\n            }\n        }\n    }\n\n    public static StepResult fromKarateJson(File workingDir, Scenario scenario, Map<String, Object> map) {\n        Map<String, Object> stepMap = (Map) map.get(\"step\");\n        Step step = Step.fromKarateJson(scenario, stepMap);\n        Result result = Result.fromKarateJson((Map) map.get(\"result\"));\n        StepResult sr = new StepResult(step, result);\n        Boolean hidden = (Boolean) map.get(\"hidden\");\n        if (hidden != null) {\n            sr.setHidden(hidden);\n        }\n        String stepLog = (String) map.get(\"stepLog\");\n        sr.setStepLog(stepLog);\n        List<Map<String, Object>> embedsList = (List) map.get(\"embeds\");\n        if (embedsList != null) {\n            List<Embed> embeds = new ArrayList(embedsList.size());\n            for (Map<String, Object> embedMap : embedsList) {\n                Embed embed = Embed.fromKarateJson(embedMap);\n                embeds.add(embed);\n            }\n            sr.addEmbeds(embeds);\n        }\n        sr.setCallResultsFromKarateJson(workingDir, (List) map.get(\"callResults\"));\n        return sr;\n    }\n\n    public Map<String, Object> toKarateJson() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"step\", step.toKarateJson());\n        map.put(\"result\", result.toKarateJson());\n        if (hidden) {\n            map.put(\"hidden\", hidden);\n        }\n        if (!StringUtils.isBlank(stepLog)) {\n            map.put(\"stepLog\", stepLog);\n        }\n        if (embeds != null && !embeds.isEmpty()) {\n            List<Map<String, Object>> list = new ArrayList(embeds.size());\n            map.put(\"embeds\", list);\n            for (Embed embed : embeds) {\n                list.add(embed.toKarateJson());\n            }\n        }\n        if (callResults != null && !callResults.isEmpty()) {\n            List<Map<String, Object>> list = new ArrayList(callResults.size());\n            map.put(\"callResults\", list);\n            for (FeatureResult fr : callResults) {\n                list.add(Json.of(fr.toKarateJson()).asMap());\n            }\n        }\n        return map;\n    }\n\n    private static List<Map> tableToCucumberJson(Table table) {\n        List<List<String>> rows = table.getRows();\n        List<Map> list = new ArrayList(rows.size());\n        int count = rows.size();\n        for (int i = 0; i < count; i++) {\n            List<String> row = rows.get(i);\n            Map<String, Object> map = new HashMap(2);\n            map.put(\"cells\", row);\n            map.put(\"line\", table.getLineNumberForRow(i));\n            list.add(map);\n        }\n        return list;\n    }\n\n    public Map<String, Object> toCucumberJson() {\n        Map<String, Object> map = new HashMap(8);\n        map.put(\"line\", step.getLine());\n        map.put(\"keyword\", step.getPrefix());\n        map.put(\"name\", step.getText());\n        map.put(\"result\", result.toCucumberJson());\n        map.put(\"match\", DUMMY_MATCH);\n        StringBuilder sb = new StringBuilder();\n        if (step.getDocString() != null) {\n            sb.append(step.getDocString());\n        }\n        if (stepLog != null) {\n            sb.append(stepLog);\n        }\n        if (sb.length() > 0) {\n            map.put(\"doc_string\", docStringToCucumberJson(step.getLine(), sb.toString()));\n        }\n        if (step.getTable() != null) {\n            map.put(\"rows\", tableToCucumberJson(step.getTable()));\n        }\n        if (embeds != null) {\n            List<Map> embedList = new ArrayList(embeds.size());\n            for (Embed embed : embeds) {\n                embedList.add(embed.toMap());\n            }\n            map.put(\"embeddings\", embedList);\n        }\n        if (step.getComments() != null && !step.getComments().isEmpty()) {\n            map.put(\"comments\", step.getComments());\n        }\n        return map;\n    }\n\n    static {\n        DUMMY_MATCH = new HashMap(2);\n        DUMMY_MATCH.put(\"location\", \"karate\");\n        DUMMY_MATCH.put(\"arguments\", Collections.EMPTY_LIST);\n    }\n\n    private static Map<String, Object> docStringToCucumberJson(int line, String text) {\n        Map<String, Object> map = new HashMap(3);\n        map.put(\"content_type\", \"\");\n        map.put(\"line\", line);\n        map.put(\"value\", text);\n        return map;\n    }\n\n    public void setHidden(boolean hidden) {\n        this.hidden = hidden;\n    }\n\n    public boolean isHidden() {\n        return hidden;\n    }\n\n    public boolean isWithCallResults() {\n        return callResults != null && !callResults.isEmpty();\n    }\n\n    public boolean isStopped() {\n        return result.isFailed() || result.isAborted();\n    }\n    \n    public boolean isFailed() {\n        return result.isFailed();\n    }\n\n    public StepResult(Step step, Result result) {\n        this.step = step;\n        this.result = result;\n    }\n\n    public Step getStep() {\n        return step;\n    }\n\n    public Result getResult() {\n        return result;\n    }\n\n    public String getStepLog() {\n        return stepLog;\n    }\n\n    public List<Embed> getEmbeds() {\n        return embeds;\n    }\n\n    public void addEmbed(Embed embed) {\n        if (embeds == null) {\n            embeds = new ArrayList();\n        }\n        embeds.add(embed);\n    }\n\n    public void addCallResults(List<FeatureResult> values) {\n        if (callResults == null) {\n            callResults = new ArrayList();\n        }\n        callResults.addAll(values);\n    }\n\n    public List<FeatureResult> getCallResults() {\n        return callResults;\n    }\n\n    public boolean isErrorIgnored() {\n        return errorIgnored;\n    }\n\n    public void setErrorIgnored(boolean errorIgnored) {\n        this.errorIgnored = errorIgnored;\n    }\n\n    public Throwable getFailedReason() {\n        return failedReason;\n    }\n\n    public void setFailedReason(Throwable failedReason) {\n        this.failedReason = failedReason;\n    }        \n\n    @Override\n    public String toString() {\n        return \"[\" + result + \"] \" + step;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/StepRuntime.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.Actions;\nimport com.intuit.karate.Json;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.KarateException;\nimport com.intuit.karate.ScenarioActions;\nimport com.intuit.karate.StringUtils;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.util.*;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.slf4j.LoggerFactory;\n\n/**\n * @author pthomas3\n */\npublic class StepRuntime {\n\n    private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(StepRuntime.class);\n\n    private StepRuntime() {\n        // only static methods\n    }\n\n    static class MethodPattern {\n\n        final String regex;\n        final Method method;\n        final Pattern pattern;\n        final String keyword;\n\n        MethodPattern(Method method, String regex) {\n            String keyword1;\n            this.regex = regex;\n            this.method = method;\n            try {\n                pattern = Pattern.compile(regex);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n\n            // assuming all @When or @Action start with a ^, get the first word\n            keyword1 = regex.substring(1).split(\" |\\\\\\\\h|\\\\\\\\s\")[0];\n            if (keyword1.endsWith(\"$\")) { // if ends with $ most likely it's a doc string (i.e. the \"\"\" for multiline strings)\n                keyword1 = keyword1.substring(0, keyword1.length() - 1);\n            }\n            keyword = keyword1;\n        }\n\n        List<String> match(String text) {\n            Matcher matcher = pattern.matcher(text);\n            if (matcher.lookingAt()) {\n                List<String> args = new ArrayList(matcher.groupCount());\n                for (int i = 1; i <= matcher.groupCount(); i++) {\n                    int startIndex = matcher.start(i);\n                    args.add(startIndex == -1 ? null : matcher.group(i));\n                }\n                return args;\n            } else {\n                return null;\n            }\n        }\n\n        @Override\n        public String toString() {\n            return \"\\n\" + pattern + \" \" + method.toGenericString();\n        }\n\n    }\n\n    public static class MethodMatch {\n\n        private static final Pattern METHOD_REGEX_PATTERN = Pattern.compile(\"([a-zA-Z_$][a-zA-Z\\\\d_$\\\\.]*)*\\\\.([a-zA-Z_$][a-zA-Z\\\\d_$]*?)\\\\((.*)\\\\)\");\n\n        final Method method;\n        final List<String> args;\n\n        MethodMatch(Method method, List<String> args) {\n            this.method = method;\n            this.args = args;\n        }\n\n        Object[] convertArgs(Object last) {\n            Class[] types = method.getParameterTypes();\n            Object[] result = new Object[types.length];\n            int i = 0;\n            for (String arg : args) {\n                Class type = types[i];\n                if (List.class.isAssignableFrom(type)) {\n                    result[i] = StringUtils.split(arg, ',', false);\n                } else if (int.class.isAssignableFrom(type)) {\n                    result[i] = Integer.valueOf(arg);\n                } else { // string\n                    result[i] = arg;\n                }\n                i++;\n            }\n            if (last != null) {\n                result[i] = last;\n            }\n            return result;\n        }\n\n        public static MethodMatch getBySignatureAndArgs(String methodReference) {\n\n            String methodSignature = methodReference.substring(0, methodReference.indexOf(' '));\n            String referenceArgs = methodReference.substring(methodReference.indexOf(' ') + 1);\n            Matcher methodMatch = METHOD_REGEX_PATTERN.matcher(methodSignature);\n\n            Method method = null;\n            if (methodMatch.find()) {\n                try {\n                    String className = methodMatch.group(1);\n                    String methodName = methodMatch.group(2);\n                    String params = methodMatch.group(3);\n                    List<String> paramList = Arrays.asList(params.split(\",\"));\n                    method = Class.forName(className).getMethod(methodName, paramList.stream().map(param -> {\n                        try {\n                            return Class.forName(param);\n                        } catch (ClassNotFoundException e) {\n                            return null;\n                        }\n                    }).filter(Objects::nonNull).toArray(Class<?>[]::new));\n                } catch (ClassNotFoundException | NoSuchMethodException e) {\n                    return null;\n                }\n            }\n\n            List<String> args = \"null\".equalsIgnoreCase(referenceArgs) ? null : Json.of(JsonUtils.fromJson(referenceArgs)).asList();\n            return new MethodMatch(method, args);\n        }\n\n        public Method getMethod() {\n            return method;\n        }\n\n        public List<String> getArgs() {\n            return args;\n        }\n\n        @Override\n        public String toString() {\n            StringBuilder sb = new StringBuilder();\n            sb.append(method.getDeclaringClass().getName());\n            sb.append(\".\");\n            sb.append(method.getName());\n            sb.append(\"(\");\n            StringJoiner sj = new StringJoiner(\",\");\n            for (Class<?> parameterType : method.getParameterTypes()) {\n                sj.add(parameterType.getTypeName());\n            }\n            sb.append(sj);\n            sb.append(\")\");\n\n            return sb.toString() + \" \" + (args == null || args.isEmpty() ? \"null\" : JsonUtils.toJson(args));\n        }\n\n    }\n\n    private static final Collection<MethodPattern> PATTERNS;\n    private static final Map<String, Collection<Method>> KEYWORDS_METHODS;\n    public static final Collection<Method> METHOD_MATCH;\n\n    static {\n        Map<String, MethodPattern> temp = new HashMap();\n        List<MethodPattern> overwrite = new ArrayList();\n        KEYWORDS_METHODS = new HashMap();\n        for (Method method : ScenarioActions.class.getMethods()) {\n            When when = method.getDeclaredAnnotation(When.class);\n            if (when != null) {\n                String regex = when.value();\n                MethodPattern methodPattern = new MethodPattern(method, regex);\n                temp.put(regex, methodPattern);\n                // edge case for eval() method it's mean to match anything in a line e.g. waitFor('#stuff')\n                // regex is ([\\w]+)([^\\s^\\w])(.+)\n                String keyword = method.getName().equalsIgnoreCase(\"eval\") ? \"eval\" : methodPattern.keyword;\n                Collection<Method> keywordMethods = KEYWORDS_METHODS.computeIfAbsent(keyword, k -> new HashSet<>());\n                keywordMethods.add(methodPattern.method);\n            }\n        }\n        for (MethodPattern mp : overwrite) {\n            temp.put(mp.regex, mp);\n\n            String keyword = mp.method.getName().equalsIgnoreCase(\"eval\") ? \"eval\" : mp.keyword;\n            Collection<Method> keywordMethods = KEYWORDS_METHODS.computeIfAbsent(keyword, k -> new HashSet<>());\n            keywordMethods.add(mp.method);\n        }\n        PATTERNS = temp.values();\n        METHOD_MATCH = findMethodsByKeyword(\"match\");\n    }\n\n    private static List<MethodMatch> findMethodsMatching(String text) {\n        List<MethodMatch> matches = new ArrayList(1);\n        for (MethodPattern pattern : PATTERNS) {\n            List<String> args = pattern.match(text);\n            if (args != null) {\n                matches.add(new MethodMatch(pattern.method, args));\n            }\n        }\n        return matches;\n    }\n\n    public static Collection<Method> findMethodsByKeywords(List<String> text) {\n        Collection<Method> methods = new HashSet();\n        text.forEach(m -> {\n            methods.addAll(findMethodsByKeyword(m));\n        });\n        return methods;\n    }\n\n    public static Collection<Method> findMethodsByKeyword(String text) {\n        if (KEYWORDS_METHODS.get(text) != null) {\n            return KEYWORDS_METHODS.get(text);\n        } else {\n            LOGGER.warn(\"No keyword found for {}. Potential unexpected behavior.\", text);\n            return new HashSet<>();\n        }\n    }\n\n    private static long getElapsedTimeNanos(long startTime) {\n        return System.nanoTime() - startTime;\n    }\n\n    public static Result execute(Step step, Actions actions) {\n        String text = step.getText();\n        List<MethodMatch> matches = findMethodsMatching(text);\n        if (matches.isEmpty()) {\n            KarateException e = new KarateException(\"no step-definition method match found for: \" + text);\n            return Result.failed(System.currentTimeMillis(), 0, e, step);\n        } else if (matches.size() > 1) {\n            boolean evalAssign = false; // special case to support foo.bar = (docstring) in cucumber syntax\n            for (MethodMatch m : matches) {\n                if (m.getMethod().getName().equalsIgnoreCase(\"evalAssignDocString\")) {\n                    evalAssign = true;\n                    matches = Collections.singletonList(m);\n                    break;\n                }\n            }\n            if (!evalAssign) {\n                KarateException e = new KarateException(\"more than one step-definition method matched: \" + text + \" - \" + matches);\n                return Result.failed(System.currentTimeMillis(), 0, e, step);\n            }\n        }\n        MethodMatch match = matches.get(0);\n        Object last;\n        if (step.getDocString() != null) {\n            last = step.getDocString();\n        } else if (step.getTable() != null) {\n            last = step.getTable().getRowsAsMaps();\n        } else {\n            last = null;\n        }\n        Object[] args;\n        try {\n            args = match.convertArgs(last);\n        } catch (Exception ignored) { // edge case where user error causes [request =] to match [request docstring]\n            KarateException e = new KarateException(\"no step-definition method match found for: \" + text);\n            return Result.failed(System.currentTimeMillis(), 0, e, step);\n        }\n        final long startTime = System.currentTimeMillis();\n        final long startTimeNanos = System.nanoTime();\n        try {\n            match.method.invoke(actions, args);\n            final long elapsedTimeNanos = getElapsedTimeNanos(startTimeNanos);\n            if (actions.isAborted()) {\n                return Result.aborted(startTime, elapsedTimeNanos, match);\n            } else if (actions.isFailed()) {\n                return Result.failed(startTime, elapsedTimeNanos, actions.getFailedReason(), step, match);\n            } else {\n                return Result.passed(startTime, elapsedTimeNanos, match);\n            }\n        } catch (InvocationTargetException e) {\n            return Result.failed(startTime, getElapsedTimeNanos(startTimeNanos), e.getTargetException(), step, match);\n        } catch (Exception e) {\n            return Result.failed(startTime, getElapsedTimeNanos(startTimeNanos), e, step, match);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/SyncExecutorService.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.AbstractExecutorService;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\n/**\n *\n * @author pthomas3\n */\npublic class SyncExecutorService extends AbstractExecutorService {\n    \n    public static final SyncExecutorService INSTANCE = new SyncExecutorService();\n\n    private final AtomicBoolean terminated = new AtomicBoolean();\n\n    @Override\n    public void shutdown() {\n        terminated.set(true);\n    }\n\n    @Override\n    public List<Runnable> shutdownNow() {\n        return Collections.EMPTY_LIST;\n    }\n\n    @Override\n    public boolean isShutdown() {\n        return terminated.get();\n    }\n\n    @Override\n    public boolean isTerminated() {\n        return terminated.get();\n    }\n\n    @Override\n    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {\n        shutdown();\n        return true;\n    }\n\n    @Override\n    public void execute(Runnable command) {\n        command.run();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Table.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.graal.JsEngine;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class Table {\n\n    private static final Logger logger = LoggerFactory.getLogger(Table.class);\n\n    enum ColumnType {\n        STRING,\n        EVALUATED\n    }\n\n    class Column {\n\n        final String key;\n        final int index;\n        final ColumnType type;\n\n        Column(String key, int index, ColumnType type) {\n            this.key = key;\n            this.index = index;\n            this.type = type;\n        }\n\n    }\n\n    private final List<List<String>> rows;\n    private final Map<String, Column> colMap;\n    private final List<Column> cols;\n    private final List<Integer> lineNumbers;\n    private final String dynamicExpression;\n\n    public String getDynamicExpression() {\n        return dynamicExpression;\n    }\n\n    public boolean isDynamic() {\n        return dynamicExpression != null;\n    }\n\n    public Collection<String> getKeys() {\n        return colMap.keySet();\n    }\n\n    public int getLineNumberForRow(int i) {\n        return lineNumbers.get(i);\n    }\n\n    public Table replace(String token, String value) {\n        int rowCount = rows.size();\n        List<String> keys = rows.get(0);\n        int colCount = keys.size();\n        List<List<String>> list = new ArrayList(rowCount);\n        list.add(keys); // header row\n        for (int i = 1; i < rowCount; i++) { // don't include header row    \n            List<String> row = rows.get(i);\n            List<String> replaced = new ArrayList(colCount);\n            list.add(replaced);\n            for (int j = 0; j < colCount; j++) {\n                String text = row.get(j).replace(token, value);\n                replaced.add(text);\n            }\n        }\n        return new Table(list, lineNumbers);\n    }\n\n    public String getValueAsString(String key, int row) {\n        Column col = colMap.get(key);\n        if (col == null) {\n            return null;\n        }\n        return rows.get(row).get(col.index);\n    }\n\n    public static Table fromKarateJson(List<Map<String, Object>> tableRows) {\n        List<List<String>> rows = new ArrayList(tableRows.size());\n        List<Integer> lines = new ArrayList(tableRows.size());\n        for (Map<String, Object> row : tableRows) {\n            rows.add((List) row.get(\"row\"));\n            lines.add((Integer) row.get(\"line\"));\n        }\n        return new Table(rows, lines);\n    }\n    \n    public List<Map<String, Object>> toKarateJson() {\n        int count = rows.size();\n        List<Map<String, Object>> list = new ArrayList(count);\n        for (int i = 0; i < count; i++) {\n            Map<String, Object> map = new HashMap(2);\n            list.add(map);\n            map.put(\"row\", rows.get(i));\n            map.put(\"line\", lineNumbers.get(i));            \n        }\n        return list;\n    }\n\n    public Table(List<List<String>> rows, List<Integer> lineNumbers) {\n        this.rows = rows;\n        this.lineNumbers = lineNumbers;\n        List<String> keys = rows.get(0);\n        int colCount = keys.size();\n        colMap = new LinkedHashMap(colCount);\n        for (int i = 0; i < colCount; i++) {\n            String key = keys.get(i);\n            ColumnType type;\n            if (key.endsWith(\"!\")) {\n                key = key.substring(0, key.length() - 1);\n                type = ColumnType.EVALUATED;\n            } else {\n                type = ColumnType.STRING;\n            }\n            colMap.put(key, new Column(key, i, type));\n        }\n        if (colCount == 1 && rows.size() == 1) {\n            dynamicExpression = keys.get(0);\n        } else {\n            dynamicExpression = null;\n        }\n        cols = new ArrayList(colMap.values()); // just to be able to call cols.get(index)\n    }\n\n    public List<List<String>> getRows() {\n        return rows;\n    }\n\n    public List<Map<String, String>> getRowsAsMaps() {\n        int rowCount = rows.size();\n        List<Map<String, String>> list = new ArrayList(rowCount - 1);\n        for (int i = 1; i < rowCount; i++) { // don't include header row    \n            Map<String, String> map = new LinkedHashMap(cols.size());\n            list.add(map);\n            List<String> row = rows.get(i);\n            for (Column col : cols) {\n                map.put(col.key, row.get(col.index));\n            }\n        }\n        return list;\n    }\n\n    public List<Map<String, Object>> getRowsAsMapsConverted() {\n        int rowCount = rows.size();\n        List<Map<String, Object>> list = new ArrayList(rowCount - 1);\n        for (int i = 1; i < rowCount; i++) { // don't include header row    \n            Map<String, Object> map = new LinkedHashMap(cols.size());\n            list.add(map);\n            List<String> row = rows.get(i);\n            for (Column col : cols) {\n                map.put(col.key, convert(row.get(col.index), col));\n            }\n        }\n        return list;\n    }\n\n    private static Object convert(String raw, Column col) {\n        try {\n            switch (col.type) {\n                case EVALUATED:\n                    if (JsonUtils.isJson(raw)) {\n                        raw = '(' + raw + ')';\n                    }\n                    return JsEngine.evalGlobal(raw).getValue();\n                default:\n                    if (StringUtils.isBlank(raw)) {\n                        return null;\n                    } else {\n                        return raw;\n                    }\n            }\n        } catch (Exception e) {\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"type conversion failed for column: {}, type: {} - {}\", col.key, col.type, e.getMessage());\n            }\n            return raw;\n        }\n    }\n\n    public Map<String, Object> getExampleData(int exampleIndex) {\n        List<String> row = rows.get(exampleIndex + 1);\n        Map<String, Object> map = new LinkedHashMap(cols.size());\n        for (Column col : cols) {\n            String raw = row.get(col.index);\n            map.put(col.key, convert(raw, col));\n        }\n        return map;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append('\\n');\n        for (List<String> row : rows) {\n            sb.append('|').append('\\t');\n            for (String s : row) {\n                sb.append(s).append('\\t').append('|');\n            }\n            sb.append('\\n');\n        }\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Tag.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.StringUtils;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class Tag {\n\n    public static final String IGNORE = \"ignore\";\n    public static final String ENV = \"env\";\n    public static final String ENVNOT = \"envnot\";\n    public static final String SETUP = \"setup\";\n    public static final String FAIL = \"fail\";\n\n    private final int line;\n    private final String text;\n    private final String name;\n    private final List<String> values;\n\n    public Tag(int line, String text) {\n        this.line = line;\n        this.text = text.substring(1);\n        int pos = text.indexOf('=');\n        if (pos != -1) {\n            name = text.substring(1, pos);\n            String temp = text.substring(pos + 1);\n            if (temp.isEmpty()) { // edge case '@id='\n                values = Collections.EMPTY_LIST;\n            } else {\n                values = StringUtils.split(temp, ',', false);\n            }\n        } else {\n            name = this.text;\n            values = Collections.EMPTY_LIST;\n        }\n    }\n\n    public int getLine() {\n        return line;\n    }\n\n    public String getText() {\n        return text;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public List<String> getValues() {\n        return values;\n    }\n\n    @Override\n    public String toString() {\n        return '@' + text;\n    }\n\n    @Override\n    public int hashCode() {\n        return text.hashCode();\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj) {\n            return true;\n        }\n        if (obj == null) {\n            return false;\n        }\n        if (getClass() != obj.getClass()) {\n            return false;\n        }\n        final Tag other = (Tag) obj;\n        return text.equals(other.text);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/TagResults.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.report.ReportUtils;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.TreeSet;\n\n/**\n *\n * @author pthomas3\n */\npublic class TagResults {\n\n    private final Set<String> allTagKeys = new TreeSet();\n    private final Set<String> failedTagKeys = new TreeSet();\n    private final List<Map<String, Object>> featureTagsList = new ArrayList();\n\n    public void addFeatureResult(FeatureResult fr) {\n        Map<String, Object> featureTags = new HashMap();\n        featureTagsList.add(featureTags);\n        featureTags.put(\"featureSummary\", fr.toSummaryJson());\n        Set<String> tagKeysSet = new TreeSet();\n        featureTags.put(\"tagKeys\", tagKeysSet);\n        Set<String> failedTagKeysSet = new TreeSet();\n        featureTags.put(\"failedTagKeys\", failedTagKeysSet);        \n        for (ScenarioResult sr : fr.getScenarioResults()) {\n            Tags tags = sr.getScenario().getTagsEffective();\n            allTagKeys.addAll(tags.getTagKeys());\n            tagKeysSet.addAll(tags.getTagKeys());\n            if (sr.isFailed()) {\n                failedTagKeys.addAll(tags.getTagKeys());\n                failedTagKeysSet.addAll(tags.getTagKeys());\n            }\n        }\n    }\n    \n    public Map<String, Object> toKarateJson() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"tagKeysPassed\", allTagKeys.size() - failedTagKeys.size());\n        map.put(\"tagKeysFailed\", failedTagKeys.size());\n        map.put(\"resultDate\", ReportUtils.getDateString());\n        map.put(\"tagKeys\", allTagKeys);\n        map.put(\"failedTagKeys\", failedTagKeys);\n        map.put(\"featureTags\", featureTagsList);\n        return map;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Tags.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.graal.JsEngine;\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.graal.Methods;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Function;\nimport org.graalvm.polyglot.Value;\n\n/**\n *\n * @author pthomas3\n */\npublic class Tags implements Iterable<Tag> {\n\n    public static final Tags EMPTY = new Tags(Collections.EMPTY_LIST);\n\n    private final Collection<Tag> original;\n    private final List<String> tags;\n    private Map<String, List<String>> tagValues;\n\n    @Override\n    public Iterator<Tag> iterator() {\n        return original.iterator();\n    }\n\n    public static class Values {\n\n        public final List<String> values;\n        public final boolean isPresent;\n\n        public Values(List<String> values) {\n            this.values = values == null ? Collections.EMPTY_LIST : values;\n            isPresent = !this.values.isEmpty();\n        }\n\n        public boolean isPresent() {\n            return isPresent;\n        }\n\n        public boolean isAnyOf(Object... args) {\n            for (Object o : args) {\n                if (values.contains(o.toString())) {\n                    return true;\n                }\n            }\n            return false;\n        }\n\n        public boolean isAllOf(Object... args) {\n            List list = new ArrayList(args.length);\n            for (Object o : args) {\n                list.add(o.toString());\n            }\n            return values.containsAll(list);\n        }\n\n        public boolean isOnly(Object... args) {\n            return isAllOf(args) && args.length == values.size();\n        }\n\n        public boolean isEach(Value v) {\n            if (!v.canExecute()) {\n                return false;\n            }\n            for (String s : values) {                \n                JsValue jv = new JsValue(JsEngine.execute(v, s));\n                if (!jv.isTrue()) {\n                    return false;\n                }\n            }\n            return true;\n        }\n\n    }\n\n    public static Tags merge(List<Tag>... lists) {\n        Set<Tag> tags = new HashSet();\n        for (List<Tag> list : lists) {\n            if (list != null) {\n                tags.addAll(list);\n            }\n        }\n        return new Tags(tags);\n    }\n\n    public Tags(Collection<Tag> in) {\n        if (in == null) {\n            original = Collections.EMPTY_LIST;\n            tags = Collections.EMPTY_LIST;\n        } else {\n            original = in;\n            tags = new ArrayList(in.size());\n            tagValues = new HashMap(in.size());\n            for (Tag tag : in) {\n                tags.add(tag.getText());\n                tagValues.put(tag.getName(), tag.getValues());\n            }\n        }\n    }\n\n    public boolean evaluate(String tagSelector, String karateEnv) {\n        if (StringUtils.containsIgnoreCase(tags, Tag.IGNORE)) {\n            return false;\n        }\n        if (tagValues.containsKey(Tag.SETUP)) {\n            return false;\n        }\n        Values envValues = valuesFor(Tag.ENV);\n        if (envValues.isPresent) {\n            if (karateEnv == null) {\n                return false;\n            }\n            if (!envValues.isAnyOf(karateEnv)) {\n                return false;\n            }\n        }\n        if (karateEnv != null && valuesFor(Tag.ENVNOT).isAnyOf(karateEnv)) {\n            return false;\n        }\n        if (tagSelector == null) {\n            return true;\n        }\n        JsEngine je = JsEngine.global();\n        je.put(\"anyOf\", (Methods.FunVar) this::anyOf);\n        je.put(\"allOf\", (Methods.FunVar) this::allOf);\n        je.put(\"not\", (Methods.FunVar) this::not);\n        je.put(\"valuesFor\", (Function<String, Values>) this::valuesFor);\n        JsValue jv = je.eval(tagSelector);\n        return jv.isTrue();\n    }\n\n    public boolean anyOf(Object... values) {\n        for (String s : removeTagPrefixes(values)) {\n            if (tags.contains(s)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public boolean allOf(Object... values) {\n        return tags.containsAll(removeTagPrefixes(values));\n    }\n\n    public boolean not(Object... values) {\n        return !anyOf(values);\n    }\n\n    public Values valuesFor(String name) {\n        List<String> list = tagValues.get(removeTagPrefix(name));\n        return new Values(list);\n    }\n\n    public boolean contains(String tagText) {\n        return tags.contains(removeTagPrefix(tagText));\n    }\n\n    public List<String> getTags() {\n        return tags;\n    }\n\n    public Collection<String> getTagKeys() {\n        return tagValues.keySet();\n    }\n\n    public Map<String, List<String>> getTagValues() {\n        return tagValues;\n    }\n\n    public Collection<Tag> getOriginal() {\n        return original;\n    }\n\n    private static String removeTagPrefix(String s) {\n        if (s.charAt(0) == '@') {\n            return s.substring(1);\n        } else {\n            return s;\n        }\n    }\n\n    private static Collection<String> removeTagPrefixes(Object... values) {\n        List<String> list = new ArrayList(values.length);\n        for (Object o : values) {\n            list.add(removeTagPrefix(o.toString()));\n        }\n        return list;\n    }\n\n    public static String fromKarateOptionsTags(List<String> tags) {\n        if (tags == null || tags.isEmpty()) {\n            return null;\n        }\n        return fromKarateOptionsTags(tags.toArray(new String[]{}));\n    }\n\n    public static String fromKarateOptionsTags(String... tags) {\n        if (tags == null || tags.length == 0) {\n            return null;\n        }\n        for (String s : tags) {\n            if (s.indexOf('(') != -1) { // new enhanced tag expression detected !\n                return s;\n            }\n        }\n        StringBuilder sb = new StringBuilder();\n        for (int i = 0; i < tags.length; i++) {\n            String and = StringUtils.trimToEmpty(tags[i]);\n            if (and.startsWith(\"~\")) {\n                sb.append(\"not('\").append(and.substring(1)).append(\"')\");\n            } else {\n                sb.append(\"anyOf(\");\n                List<String> or = StringUtils.split(and, ',', false);\n                for (String tag : or) {\n                    sb.append('\\'').append(tag.trim()).append('\\'').append(',');\n                }\n                sb.setLength(sb.length() - 1);\n                sb.append(')');\n            }\n            if (i < (tags.length - 1)) {\n                sb.append(\" && \");\n            }\n        }\n        return sb.toString();\n    }\n\n    @Override\n    public String toString() {\n        return tags.toString();\n    }        \n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/TimelineResults.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.StringUtils;\nimport java.text.DateFormat;\nimport java.text.SimpleDateFormat;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class TimelineResults {\n\n    private final Map<String, Integer> groupsMap = new LinkedHashMap();\n    private final List<Map> items = new ArrayList();\n    private final DateFormat dateFormat = new SimpleDateFormat(\"HH:mm:ss.SSS\");\n    private int id;\n\n    public void addFeatureResult(FeatureResult fr) {\n        fr.getScenarioResults().stream().forEach(sr -> {\n            String threadName = sr.getExecutorName();\n            Integer groupId = groupsMap.get(threadName);\n            if (groupId == null) {\n                groupId = groupsMap.size() + 1;\n                groupsMap.put(threadName, groupId);\n            }\n            Map<String, Object> item = new LinkedHashMap(10);\n            items.add(item);\n            item.put(\"id\", ++id);\n            item.put(\"group\", groupId);\n            Scenario s = sr.getScenario();\n            String featureName = s.getFeature().getResource().getFileNameWithoutExtension();\n            String content = featureName + s.getRefId();\n            item.put(\"content\", content);\n            long startTime = sr.getStartTime();\n            item.put(\"start\", startTime);\n            long endTime = sr.getEndTime() - 1; // avoid overlap when rendering\n            item.put(\"end\", endTime);\n            String startTimeString = dateFormat.format(new Date(startTime));\n            String endTimeString = dateFormat.format(new Date(endTime));\n            content = content + \" \" + startTimeString + \"-\" + endTimeString;\n            String scenarioTitle = StringUtils.trimToEmpty(s.getName());\n            if (!scenarioTitle.isEmpty()) {\n                content = content + \" \" + scenarioTitle;\n            }\n            item.put(\"title\", content);\n            if (sr.isFailed()) {\n                item.put(\"className\", \"failed\");\n            }\n        });\n    }\n\n    public Map<String, Object> toKarateJson() {\n        List<Map> groups = new ArrayList(groupsMap.size());\n        groupsMap.forEach((k, v) -> {\n            Map<String, Object> group = new HashMap(2);\n            groups.add(group);\n            group.put(\"id\", v);\n            group.put(\"content\", k);\n        });\n        StringBuilder sb = new StringBuilder();\n        sb.append('\\n');\n        sb.append(\"var groups = \").append(JsonUtils.toJson(groups)).append(';');\n        sb.append('\\n');\n        sb.append(\"var items = \").append(JsonUtils.toJson(items)).append(';');\n        sb.append('\\n');\n        return Collections.singletonMap(\"data\", sb.toString());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/Variable.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.XmlUtils;\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.Json;\nimport com.intuit.karate.JsonUtils;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.ProxyExecutable;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.w3c.dom.Node;\n\n/**\n *\n * @author pthomas3\n */\npublic class Variable {\n    \n    private static final Logger logger = LoggerFactory.getLogger(Variable.class);\n    \n    public static enum Type {\n        NULL,\n        BOOLEAN,\n        NUMBER,\n        STRING,\n        BYTES,\n        LIST,\n        MAP,\n        XML,\n        JS_FUNCTION,\n        JAVA_FUNCTION,\n        FEATURE,\n        OTHER\n    }\n    \n    public static final Variable NULL = new Variable(null);\n    public static final Variable NOT_PRESENT = new Variable(\"#notpresent\");\n    \n    public final Type type;\n    private final Object value;\n    \n    public Variable(Object o) {\n        if (o instanceof Value) {\n            o = new JsValue((Value) o).getValue();\n        } else if (o instanceof JsValue) {\n            o = ((JsValue) o).getValue();\n        }\n        if (o == null) {\n            type = Type.NULL;\n        } else if (o instanceof ProxyExecutable) {\n            type = Type.JS_FUNCTION;\n        } else if (o instanceof Value) {\n            type = Type.OTHER; // java.lang.Class            \n        } else if (o instanceof Function) {\n            type = Type.JAVA_FUNCTION;\n        } else if (o instanceof Node) {\n            type = Type.XML;\n        } else if (o instanceof List) {\n            type = Type.LIST;\n        } else if (o instanceof Map) {\n            type = Type.MAP;\n        } else if (o instanceof String) {\n            type = Type.STRING;\n        } else if (Number.class.isAssignableFrom(o.getClass())) {\n            type = Type.NUMBER;\n        } else if (Boolean.class.equals(o.getClass())) {\n            type = Type.BOOLEAN;\n        } else if (o instanceof byte[]) {\n            type = Type.BYTES;\n        } else if (o instanceof FeatureCall) {\n            type = Type.FEATURE;\n        } else {\n            type = Type.OTHER;\n        }\n        value = o;\n    }\n    \n    public <T> T getValue() {\n        return (T) value;\n    }\n    \n    public boolean isJsOrJavaFunction() {\n        return type == Type.JS_FUNCTION || type == Type.JAVA_FUNCTION;\n    }\n    \n    public boolean isJavaFunction() {\n        return type == Type.JAVA_FUNCTION;\n    }\n    \n    public boolean isJsFunction() {\n        return type == Type.JS_FUNCTION;\n    }\n    \n    public boolean isBytes() {\n        return type == Type.BYTES;\n    }\n    \n    public boolean isString() {\n        return type == Type.STRING;\n    }\n    \n    public boolean isList() {\n        return type == Type.LIST;\n    }\n    \n    public boolean isMap() {\n        return type == Type.MAP;\n    }\n    \n    public boolean isMapOrList() {\n        return type == Type.MAP || type == Type.LIST;\n    }\n    \n    public boolean isXml() {\n        return type == Type.XML;\n    }\n    \n    public boolean isNumber() {\n        return type == Type.NUMBER;\n    }\n    \n    public boolean isNull() {\n        return type == Type.NULL;\n    }\n    \n    public boolean isOther() {\n        return type == Type.OTHER;\n    }\n    \n    public boolean isFeature() {\n        return type == Type.FEATURE;\n    }\n    \n    public boolean isBoolean() {\n        return type == Type.BOOLEAN;\n    }\n    \n    public boolean isTrue() {\n        return type == Type.BOOLEAN && ((Boolean) value);\n    }\n    \n    public String getTypeString() {\n        return type.name().toLowerCase();\n    }\n    \n    public Node getAsXml() {\n        switch (type) {\n            case XML:\n                return getValue();\n            case MAP:\n                return XmlUtils.fromMap(getValue());\n            case STRING:\n            case BYTES:\n                String xml = getAsString();\n                return XmlUtils.toXmlDoc(xml);\n            case OTHER: // POJO\n                return XmlUtils.fromJavaObject(value);\n            default:\n                throw new RuntimeException(\"cannot convert to xml:\" + this);\n        }\n    }\n    \n    public Object getValueAndConvertIfXmlToMap() {\n        return isXml() ? XmlUtils.toObject(getValue()) : value;\n    }\n    \n    public Object getValueAndForceParsingAsJson() {\n        switch (type) {\n            case LIST:\n            case MAP:\n                return value;\n            case STRING:\n            case BYTES:\n                return JsonUtils.fromJson(getAsString());\n            case XML:\n                return XmlUtils.toObject(getValue());\n            case OTHER: // pojo\n                return Json.of(value).value();\n            default:\n                throw new RuntimeException(\"cannot convert to json: \" + this);\n        }\n        \n    }\n    \n    public byte[] getAsByteArray() {\n        if (type == Type.BYTES) {\n            return getValue();\n        } else {\n            return FileUtils.toBytes(getAsString());\n        }\n    }\n    \n    public String getAsString() {\n        switch (type) {\n            case NULL:\n                return null;\n            case BYTES:\n                return FileUtils.toString((byte[]) value);\n            case LIST:\n            case MAP:\n                try {\n                    return JsonUtils.toJson(value);\n                } catch (Throwable t) {\n                    logger.warn(\"conversion to json string failed, will attempt to use fall-back approach: {}\", t.getMessage());\n                    return JsonUtils.toJsonSafe(value, false);\n                }\n            case XML:\n                return XmlUtils.toString(getValue());\n            default:\n                return value.toString();\n        }\n    }\n    \n    public String getAsPrettyString() {\n        switch (type) {\n            case LIST:\n            case MAP:\n                return JsonUtils.toJsonSafe(value, true);\n            case XML:\n                return getAsPrettyXmlString();\n            default:\n                return getAsString();\n        }\n    }\n    \n    public String getAsPrettyXmlString() {\n        return XmlUtils.toString(getAsXml(), true);\n    }\n    \n    public int getAsInt() {\n        if (isNumber()) {\n            return ((Number) value).intValue();\n        } else {\n            return Integer.valueOf(getAsString());\n        }\n    }\n    \n    public Variable copy(boolean deep) {\n        switch (type) {\n            case LIST:\n                return deep ? new Variable(JsonUtils.deepCopy(value)) : new Variable(new ArrayList((List) value));\n            case MAP:\n                return deep ? new Variable(JsonUtils.deepCopy(value)) : new Variable(new LinkedHashMap((Map) value));\n            case XML:\n                return new Variable(XmlUtils.toXmlDoc(getAsString()));\n            default:\n                return this;\n        }\n    }\n    \n    public Variable toLowerCase() {\n        switch (type) {\n            case STRING:\n                return new Variable(getAsString().toLowerCase());\n            case LIST:\n            case MAP:\n                String json = getAsString().toLowerCase();\n                return new Variable(JsonUtils.fromJson(json));\n            case XML:\n                String xml = getAsString().toLowerCase();\n                return new Variable(XmlUtils.toXmlDoc(xml));\n            default:\n                return this;\n        }\n    }\n    \n    public boolean isNotPresent() {\n        return \"#notpresent\".equals(value);\n    }    \n    \n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"[type: \").append(type);\n        sb.append(\", value: \").append(value);\n        sb.append(\"]\");\n        return sb.toString();\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/core/When.java",
    "content": "package com.intuit.karate.core;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * replaces cucumber-jvm code\n * \n * @author pthomas3\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.METHOD)\npublic @interface When {\n\n    String value();\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/DevToolsDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport com.intuit.karate.Constants;\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Json;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.FeatureCall;\nimport com.intuit.karate.core.MockHandler;\nimport com.intuit.karate.core.ScenarioEngine;\nimport com.intuit.karate.core.Variable;\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.http.HttpRequest;\nimport com.intuit.karate.http.ResourceType;\nimport com.intuit.karate.http.Response;\nimport com.intuit.karate.http.WebSocketClient;\nimport com.intuit.karate.http.WebSocketOptions;\nimport com.intuit.karate.shell.Command;\nimport java.util.ArrayList;\nimport java.util.Base64;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Predicate;\n\nimport com.jayway.jsonpath.PathNotFoundException;\nimport org.graalvm.polyglot.Value;\n\n/**\n *\n * @author pthomas3\n */\npublic abstract class DevToolsDriver implements Driver {\n\n    protected final DriverOptions options;\n    protected final Command command;\n    protected final WebSocketClient client;\n    private boolean terminated;\n\n    private final DevToolsWait wait;\n    protected final String rootFrameId;\n\n    private Integer windowId;\n    private String windowState;\n    protected String sessionId;\n    protected String mainFrameId;\n\n    // iframe support\n    private Frame frame;\n    private final Map<String, Integer> frameContexts = new HashMap();\n    private final Map<String, String> frameSessions = new HashMap();\n\n    protected boolean domContentEventFired;\n    protected final Set<String> framesStillLoading = new HashSet();\n    private boolean submit;\n\n    protected String currentDialogText;\n\n    private int nextId;\n\n    public int nextId() {\n        return ++nextId;\n    }\n\n    private MockHandler mockHandler;\n\n    protected final Logger logger;\n\n    protected DevToolsDriver(DriverOptions options, Command command, String webSocketUrl) {\n        logger = options.driverLogger;\n        this.options = options;\n        this.command = command;\n\n        if (options.isRemoteHost()) {\n            String host = options.host;\n            Integer port = options.port;\n            webSocketUrl = webSocketUrl.replace(\"ws://localhost/\", \"ws://\" + host + \":\" + port + \"/\");\n        }\n\n        this.wait = new DevToolsWait(this, options);\n        int pos = webSocketUrl.lastIndexOf('/');\n        rootFrameId = webSocketUrl.substring(pos + 1);\n        mainFrameId = rootFrameId;\n        logger.debug(\"root frame id: {}\", rootFrameId);\n        WebSocketOptions wsOptions = new WebSocketOptions(webSocketUrl);\n        wsOptions.setMaxPayloadSize(options.maxPayloadSize);\n        wsOptions.setTextConsumer(text -> {\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"<< {}\", text);\n            } else {\n                // to avoid swamping the console when large base64 encoded binary responses happen\n                logger.debug(\"<< {}\", StringUtils.truncate(text, 1024, true));\n            }\n            Map<String, Object> map = Json.of(text).value();\n            DevToolsMessage dtm = new DevToolsMessage(this, map);\n            receive(dtm);\n        });\n        client = new WebSocketClient(wsOptions, logger);\n    }\n\n    @Override\n    public Driver timeout(Integer millis) {\n        options.setTimeout(millis);\n        return this;\n    }\n\n    @Override\n    public Driver timeout() {\n        return timeout(null);\n    }\n\n    public DevToolsMessage method(String method) {\n        return new DevToolsMessage(this, method);\n    }\n\n    // this can be used for exploring / sending any raw message !\n    public Map<String, Object> send(Map<String, Object> map) {\n        DevToolsMessage dtm = new DevToolsMessage(this, map);\n        dtm.setId(nextId());\n        return sendAndWait(dtm, null).toMap();\n    }\n\n    public void send(DevToolsMessage dtm) {\n        String json = JsonUtils.toJson(dtm.toMap());\n        logger.debug(\">> {}\", json);\n        client.send(json);\n    }\n\n    public DevToolsMessage sendAndWait(DevToolsMessage dtm, Predicate<DevToolsMessage> condition) {\n        boolean wasSubmit = submit;\n        if (condition == null && submit) {\n            submit = false;\n            condition = DevToolsWait.ALL_FRAMES_LOADED;\n        }\n        // do stuff inside wait to avoid missing messages\n        DevToolsMessage result = wait.send(dtm, condition);\n        if (result == null && !wasSubmit) {\n            if (condition == DevToolsWait.ALL_FRAMES_LOADED) {\n                logger.error(\"failed to get reply for :\" + dtm + \". Will try to check by running a script.\");\n                boolean readyState = (Boolean) this.script(\"document.readyState === 'complete'\");\n                if (!readyState) {\n                    throw new RuntimeException(\"failed to get reply for: \" + dtm);\n                } else {\n                    logger.warn(\"document is ready, but no reply for: \" + dtm + \" with a ready event received. Will proceed.\");\n                }\n            } else {\n                throw new RuntimeException(\"failed to get reply for: \" + dtm);\n            }\n        }\n        return result;\n    }\n\n    public void receive(DevToolsMessage dtm) {\n        if (dtm.methodIs(\"Page.domContentEventFired\")) {\n            domContentEventFired = true;\n            logger.trace(\"** set dom ready flag to true\");\n        }\n        if (dtm.methodIs(\"Page.javascriptDialogOpening\")) {\n            currentDialogText = dtm.getParam(\"message\");\n            // this will stop waiting NOW\n            wait.setCondition(DevToolsWait.DIALOG_OPENING);\n        }\n        if (dtm.methodIs(\"Page.frameStartedLoading\")) {\n            String frameLoadingId = dtm.getParam(\"frameId\");\n            if (rootFrameId.equals(frameLoadingId)) { // root page is loading\n                domContentEventFired = false;\n                framesStillLoading.clear();\n                logger.trace(\"** root frame started loading, cleared all page state: {}\", frameLoadingId);\n            } else {\n                framesStillLoading.add(frameLoadingId);\n                logger.trace(\"** frame started loading, added to in-progress list: {}\", framesStillLoading);\n            }\n        }\n        if (dtm.methodIs(\"Page.frameStoppedLoading\")) {\n            String frameLoadedId = dtm.getParam(\"frameId\");\n            framesStillLoading.remove(frameLoadedId);\n            logger.trace(\"** frame stopped loading: {}, remaining in-progress: {}\", frameLoadedId, framesStillLoading);\n        }\n        if (dtm.methodIs(\"Page.frameNavigated\")) {\n            Frame newFrame = new Frame(dtm.getParam(\"frame.id\"), dtm.getParam(\"frame.url\"), dtm.getParam(\"frame.name\"));\n            logger.trace(\"** detected new frame: {}\", newFrame);\n            if (frame != null && (frame.name.equals(newFrame.name) || frame.url.equals(newFrame.url))) {\n                logger.trace(\"** auto switching frame: {} -> {}\", frame, newFrame);\n                frame = newFrame;\n            }\n        }\n        if (dtm.methodIs(\"Runtime.executionContextCreated\")) {\n            String newFrameId = dtm.getParam(\"context.auxData.frameId\");\n            Integer contextId = dtm.getParam(\"context.id\");\n            frameContexts.put(newFrameId, contextId);\n            logger.trace(\"** new frame execution context: {} - {}\", newFrameId, contextId);\n        }\n        if (dtm.methodIs(\"Runtime.executionContextsCleared\")) {\n            frame = null;\n            frameContexts.clear();\n            framesStillLoading.clear();\n        }\n        if (dtm.methodIs(\"Runtime.consoleAPICalled\") && options.showBrowserLog) {\n            List<Object> values = dtm.getParam(\"args[*].value\");\n            for (Object value : values) {\n                logger.debug(\"[console] {}\", value);\n            }\n        }\n        if (dtm.methodIs(\"Fetch.requestPaused\")) {\n            handleInterceptedRequest(dtm);\n        }\n        if (dtm.methodIs(\"Target.targetInfoChanged\")) {\n            String frameId = dtm.getParam(\"targetInfo.targetId\");\n            Integer frameContextId = frameContexts.get(frameId);\n            if (frameContextId != null) {\n                logger.trace(\"** removed stale execution context: {} - {}\", frameId, frameContextId);\n                frameContexts.remove(frameId);\n            }\n        }\n        if (dtm.methodIs(\"Target.attachedToTarget\")) {\n            String targetType = dtm.getParam(\"targetInfo.type\");\n            if (\"iframe\".equals(targetType) || \"frame\".equals(targetType)) {\n                String targetSessionId = dtm.getParam(\"sessionId\");\n                String targetFrameId = dtm.getParam(\"targetInfo.targetId\");\n                frameSessions.put(targetFrameId, targetSessionId);\n                logger.trace(\"** new frame session: {} - {}\", targetFrameId, targetSessionId);\n            }\n        }\n        // all needed state is set above before we get into conditional checks\n        wait.receive(dtm);\n    }\n\n    private void handleInterceptedRequest(DevToolsMessage dtm) {\n        String requestId = dtm.getParam(\"requestId\");\n        String requestUrl = dtm.getParam(\"request.url\");\n        if (mockHandler != null) {\n            String method = dtm.getParam(\"request.method\");\n            Map<String, String> headers = dtm.getParam(\"request.headers\");\n            String postData = dtm.getParam(\"request.postData\");\n            logger.debug(\"intercepting request for url: {}\", requestUrl);\n            HttpRequest request = new HttpRequest();\n            request.setUrl(requestUrl);\n            request.setMethod(method);\n            headers.forEach((k, v) -> request.putHeader(k, v));\n            if (postData != null) {\n                request.setBody(FileUtils.toBytes(postData));\n            } else {\n                request.setBody(Constants.ZERO_BYTES);\n            }\n            Response response = mockHandler.handle(request.toRequest());\n            String responseBody = response.getBody() == null ? \"\" : Base64.getEncoder().encodeToString(response.getBody());\n            List<Map> responseHeaders = new ArrayList();\n            Map<String, List<String>> map = response.getHeaders();\n            if (map != null) {\n                map.forEach((k, v) -> {\n                    if (v != null && !v.isEmpty()) {\n                        Map<String, Object> nv = new HashMap(2);\n                        nv.put(\"name\", k);\n                        nv.put(\"value\", v.get(0));\n                        responseHeaders.add(nv);\n                    }\n                });\n            }\n            method(\"Fetch.fulfillRequest\")\n                    .param(\"requestId\", requestId)\n                    .param(\"responseCode\", response.getStatus())\n                    .param(\"responseHeaders\", responseHeaders)\n                    .param(\"body\", responseBody).sendWithoutWaiting();\n        } else {\n            logger.warn(\"no mock server, continuing paused request to url: {}\", requestUrl);\n            method(\"Fetch.continueRequest\").param(\"requestId\", requestId).sendWithoutWaiting();\n        }\n    }\n\n    //==========================================================================\n    //\n    private DevToolsMessage evalOnce(String expression, boolean quickly, boolean fireAndForget, boolean returnByValue) {\n        DevToolsMessage toSend = method(\"Runtime.evaluate\")\n                .param(\"expression\", expression);\n        if (returnByValue) {\n            toSend.param(\"returnByValue\", true);\n        }\n        Integer contextId = getFrameContext();\n        if (contextId != null) {\n            toSend.param(\"contextId\", contextId);\n        }\n        if (quickly) {\n            toSend.setTimeout(options.getRetryInterval());\n        }\n        if (fireAndForget) {\n            toSend.sendWithoutWaiting();\n            return null;\n        }\n        return toSend.send();\n    }\n\n    protected DevToolsMessage eval(String expression) {\n        return evalInternal(expression, false, true);\n    }\n\n    protected DevToolsMessage evalQuickly(String expression) {\n        return evalInternal(expression, true, true);\n    }\n\n    protected String evalForObjectId(String expression) {\n        return options.retry(() -> {\n            DevToolsMessage dtm = evalInternal(expression, true, false);\n            return dtm.getResult(\"objectId\");\n        }, returned -> returned != null, \"eval for object id: \" + expression, true);\n    }\n\n    private DevToolsMessage evalInternal(String expression, boolean quickly, boolean returnByValue) {\n        DevToolsMessage dtm = evalOnce(expression, quickly, false, returnByValue);\n        if (dtm.isResultError()) {\n            Map<String, Object> error = dtm.getError();\n            if (error != null) {\n                Object errorCode = error.get(\"code\");\n                if (errorCode instanceof Integer) {\n                    if ((Integer) errorCode == -32000) { // Object reference chain is too long\n                        dtm.setResult(Variable.NULL);\n                        return dtm;\n                    }\n                }\n            }\n            String message = \"js eval failed once:\" + expression\n                    + \", error: \" + dtm.getResult();\n            logger.warn(message);\n            options.sleep();\n            dtm = evalOnce(expression, quickly, false, returnByValue); // no wait condition for the re-try\n            if (dtm.isResultError()) {\n                message = \"js eval failed twice:\" + expression\n                        + \", error: \" + dtm.getResult();\n                logger.error(message);\n                throw new RuntimeException(message);\n            }\n        }\n        return dtm;\n    }\n\n    protected void retryIfEnabled(String locator) {\n        if (options.isRetryEnabled()) {\n            waitFor(locator); // will throw exception if not found\n        }\n        if (options.highlight) {\n            // highlight(locator, options.highlightDuration); // instead of this\n            String highlightJs = options.highlight(locator, options.highlightDuration);\n            evalOnce(highlightJs, true, true, true); // do it safely, i.e. fire and forget\n        }\n    }\n\n    protected int getRootNodeId() {\n        return method(\"DOM.getDocument\").param(\"depth\", 0).send().getResult(\"root.nodeId\");\n    }\n\n    @Override\n    public String elementId(String locator) {\n        return evalForObjectId(DriverOptions.selector(locator));\n    }\n\n    @Override\n    public List elementIds(String locator) {\n        List<Element> elements = locateAll(locator);\n        List<String> objectIds = new ArrayList(elements.size());\n        for (Element e : elements) {\n            String objectId = evalForObjectId(e.getLocator());\n            objectIds.add(objectId);\n        }\n        return objectIds;\n    }\n\n    @Override\n    public DriverOptions getOptions() {\n        return options;\n    }\n\n    private void attachAndActivate(String targetId, boolean isFrame) {\n        DevToolsMessage dtm = method(\"Target.attachToTarget\").param(\"targetId\", targetId).param(\"flatten\", true).send();\n        sessionId = dtm.getResult(\"sessionId\");\n        frameSessions.put(targetId, sessionId);\n        if (!isFrame) {\n            mainFrameId = targetId;\n        }\n        method(\"Target.activateTarget\").param(\"targetId\", targetId).send();\n        method(\"Target.setDiscoverTargets\").param(\"discover\", true).send();\n    }\n\n    @Override\n    public void activate() {\n        attachAndActivate(rootFrameId, false);\n    }\n\n    protected void initWindowIdAndState() {\n        DevToolsMessage dtm = method(\"Browser.getWindowForTarget\").param(\"targetId\", rootFrameId).send();\n        if (!dtm.isResultError()) {\n            windowId = dtm.getResultVariable(\"windowId\").getValue();\n            windowState = (String) dtm.getResultVariable(\"bounds\").<Map>getValue().get(\"windowState\");\n        }\n    }\n\n    @Override\n    public Map<String, Object> getDimensions() {\n        DevToolsMessage dtm = method(\"Browser.getWindowForTarget\").param(\"targetId\", rootFrameId).send();\n        Map<String, Object> map = dtm.getResultVariable(\"bounds\").getValue();\n        Integer x = (Integer) map.remove(\"left\");\n        Integer y = (Integer) map.remove(\"top\");\n        map.put(\"x\", x);\n        map.put(\"y\", y);\n        return map;\n    }\n\n    @Override\n    public void setDimensions(Map<String, Object> map) {\n        Integer left = (Integer) map.remove(\"x\");\n        Integer top = (Integer) map.remove(\"y\");\n        map.put(\"left\", left);\n        map.put(\"top\", top);\n        Map temp = getDimensions();\n        temp.putAll(map);\n        temp.remove(\"windowState\");\n        method(\"Browser.setWindowBounds\")\n                .param(\"windowId\", windowId)\n                .param(\"bounds\", temp).send();\n    }\n\n    public void emulateDevice(int width, int height, String userAgent) {\n        method(\"Network.setUserAgentOverride\").param(\"userAgent\", userAgent).send();\n        method(\"Emulation.setDeviceMetricsOverride\")\n                .param(\"width\", width)\n                .param(\"height\", height)\n                .param(\"deviceScaleFactor\", 1)\n                .param(\"mobile\", true)\n                .send();\n    }\n\n    @Override\n    public void close() {\n        method(\"Page.close\").sendWithoutWaiting();\n        sessionId = frameSessions.get(rootFrameId);\n    }\n\n    @Override\n    public void quit() {\n        if (terminated) {\n            return;\n        }\n        terminated = true;\n        // don't wait, may fail and hang\n        method(\"Target.closeTarget\").param(\"targetId\", rootFrameId).sendWithoutWaiting();\n        // method(\"Browser.close\").send();\n        client.close();\n        if (command != null) {\n            command.close(true);\n        }\n        getRuntime().engine.setDriverToNull();\n    }\n\n    @Override\n    public boolean isTerminated() {\n        return terminated;\n    }\n\n    @Override\n    public void setUrl(String url) {\n        method(\"Page.navigate\").param(\"url\", url)\n                .send(DevToolsWait.ALL_FRAMES_LOADED);\n    }\n\n    @Override\n    public void refresh() {\n        method(\"Page.reload\").send(DevToolsWait.ALL_FRAMES_LOADED);\n    }\n\n    @Override\n    public void reload() {\n        method(\"Page.reload\").param(\"ignoreCache\", true).send();\n    }\n\n    private void history(int delta) {\n        DevToolsMessage dtm = method(\"Page.getNavigationHistory\").send();\n        int currentIndex = dtm.getResultVariable(\"currentIndex\").getValue();\n        List<Map> list = dtm.getResultVariable(\"entries\").getValue();\n        int targetIndex = currentIndex + delta;\n        if (targetIndex < 0 || targetIndex == list.size()) {\n            return;\n        }\n        Map<String, Object> entry = list.get(targetIndex);\n        Integer id = (Integer) entry.get(\"id\");\n        method(\"Page.navigateToHistoryEntry\").param(\"entryId\", id).send(DevToolsWait.ALL_FRAMES_LOADED);\n    }\n\n    @Override\n    public void back() {\n        history(-1);\n    }\n\n    @Override\n    public void forward() {\n        history(1);\n    }\n\n    private void setWindowState(String state) {\n        if (!\"normal\".equals(windowState)) {\n            method(\"Browser.setWindowBounds\")\n                    .param(\"windowId\", windowId)\n                    .param(\"bounds\", Collections.singletonMap(\"windowState\", \"normal\"))\n                    .send();\n            windowState = \"normal\";\n        }\n        if (!state.equals(windowState)) {\n            method(\"Browser.setWindowBounds\")\n                    .param(\"windowId\", windowId)\n                    .param(\"bounds\", Collections.singletonMap(\"windowState\", state))\n                    .send();\n            windowState = state;\n        }\n    }\n\n    @Override\n    public void maximize() {\n        setWindowState(\"maximized\");\n    }\n\n    @Override\n    public void minimize() {\n        setWindowState(\"minimized\");\n    }\n\n    @Override\n    public void fullscreen() {\n        setWindowState(\"fullscreen\");\n    }\n\n    @Override\n    public Element click(String locator) {\n        retryIfEnabled(locator);\n        eval(DriverOptions.selector(locator) + \".click()\");\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public Element select(String locator, String text) {\n        retryIfEnabled(locator);\n        eval(options.optionSelector(locator, text));\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public Element select(String locator, int index) {\n        retryIfEnabled(locator);\n        eval(options.optionSelector(locator, index));\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public Driver submit() {\n        submit = true;\n        return this;\n    }\n\n    @Override\n    public Element focus(String locator) {\n        retryIfEnabled(locator);\n        eval(options.focusJs(locator));\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public Element clear(String locator) {\n        eval(DriverOptions.selector(locator) + \".value = ''\");\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    private void sendKey(Character c, int modifiers, String type, Integer keyCode) {\n        DevToolsMessage dtm = method(\"Input.dispatchKeyEvent\")\n                .param(\"modifiers\", modifiers)\n                .param(\"type\", type);\n        if (keyCode == null) {\n            if (c != null) {\n                dtm.param(\"text\", c.toString());\n            }\n        } else {\n            switch (keyCode) {\n                case 9: // tab\n                    if (\"char\".equals(type)) {\n                        return; // special case\n                    }\n                    dtm.param(\"text\", \"\");\n                    break;\n                case 13: // enter\n                    dtm.param(\"text\", \"\\r\"); // important ! \\n does NOT work for chrome\n                    break;\n                case 46: // dot\n                    if (\"rawKeyDown\".equals(type)) {\n                        dtm.param(\"type\", \"keyDown\"); // special case\n                    }\n                    dtm.param(\"text\", \".\");\n                    break;\n                default:\n                    if (c != null) {\n                        dtm.param(\"text\", c.toString());\n                    }\n            }\n            dtm.param(\"windowsVirtualKeyCode\", keyCode);\n        }\n        dtm.send();\n    }\n\n    @Override\n    public Element input(String locator, String value) {\n        retryIfEnabled(locator);\n        // focus\n        eval(options.focusJs(locator));\n        Input input = new Input(value);\n        while (input.hasNext()) {\n            char c = input.next();\n            int modifiers = input.getModifierFlags();\n            Integer keyCode = Keys.code(c);\n            if (keyCode != null) {\n                switch (keyCode) {\n                    case Keys.CODE_SHIFT:\n                    case Keys.CODE_CONTROL:\n                    case Keys.CODE_ALT:\n                    case Keys.CODE_META:\n                        if (input.release) {\n                            sendKey(null, modifiers, \"keyUp\", keyCode);\n                        } else {\n                            sendKey(null, modifiers, \"rawKeyDown\", keyCode);\n                        }\n                        break;\n                    default:\n                        sendKey(c, modifiers, \"rawKeyDown\", keyCode);\n                        sendKey(c, modifiers, \"char\", keyCode);\n                        sendKey(c, modifiers, \"keyUp\", keyCode);\n                }\n            } else {\n                logger.warn(\"unknown character / key code: {}\", c);\n                sendKey(c, modifiers, \"char\", null);\n            }\n        }\n        for (int keyCode : input.getKeyCodesToRelease()) {\n            sendKey(null, 0, \"keyUp\", keyCode);\n        }\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    protected int currentMouseXpos;\n    protected int currentMouseYpos;\n\n    @Override\n    public void actions(List<Map<String, Object>> sequence) {\n        boolean submitRequested = submit;\n        submit = false; // make sure only LAST action is handled as a submit()\n        for (Map<String, Object> map : sequence) {\n            List<Map<String, Object>> actions = (List) map.get(\"actions\");\n            if (actions == null) {\n                logger.warn(\"no actions property found: {}\", sequence);\n                return;\n            }\n            Iterator<Map<String, Object>> iterator = actions.iterator();\n            while (iterator.hasNext()) {\n                Map<String, Object> action = iterator.next();\n                String type = (String) action.get(\"type\");\n                if (type == null) {\n                    logger.warn(\"no type property found: {}\", action);\n                    continue;\n                }\n                String chromeType;\n                switch (type) {\n                    case \"pointerMove\":\n                        chromeType = \"mouseMoved\";\n                        break;\n                    case \"pointerDown\":\n                        chromeType = \"mousePressed\";\n                        break;\n                    case \"pointerUp\":\n                        chromeType = \"mouseReleased\";\n                        break;\n                    default:\n                        logger.warn(\"unexpected action type: {}\", action);\n                        continue;\n                }\n                Integer x = (Integer) action.get(\"x\");\n                Integer y = (Integer) action.get(\"y\");\n                if (x != null) {\n                    currentMouseXpos = x;\n                }\n                if (y != null) {\n                    currentMouseYpos = y;\n                }\n                Integer duration = (Integer) action.get(\"duration\");\n                DevToolsMessage toSend = method(\"Input.dispatchMouseEvent\")\n                        .param(\"type\", chromeType)\n                        .param(\"x\", currentMouseXpos).param(\"y\", currentMouseYpos);\n                if (\"mousePressed\".equals(chromeType) || \"mouseReleased\".equals(chromeType)) {\n                    toSend.param(\"button\", \"left\").param(\"clickCount\", 1);\n                }\n                if (!iterator.hasNext() && submitRequested) {\n                    submit = true;\n                }\n                toSend.send();\n                if (duration != null) {\n                    options.sleep(duration);\n                }\n            }\n        }\n    }\n\n    @Override\n    public String text(String id) {\n        return property(id, \"textContent\");\n    }\n\n    @Override\n    public String html(String id) {\n        return property(id, \"outerHTML\");\n    }\n\n    @Override\n    public String value(String locator) {\n        return property(locator, \"value\");\n    }\n\n    @Override\n    public Element value(String locator, String value) {\n        retryIfEnabled(locator);\n        eval(DriverOptions.selector(locator) + \".value = '\" + value + \"'\");\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public String attribute(String id, String name) {\n        retryIfEnabled(id);\n        DevToolsMessage dtm = eval(DriverOptions.selector(id) + \".getAttribute('\" + name + \"')\");\n        return dtm.getResult().getAsString();\n    }\n\n    @Override\n    public String property(String id, String name) {\n        retryIfEnabled(id);\n        DevToolsMessage dtm = eval(DriverOptions.selector(id) + \"['\" + name + \"']\");\n        return dtm.getResult().getAsString();\n    }\n\n    @Override\n    public boolean enabled(String id) {\n        retryIfEnabled(id);\n        DevToolsMessage dtm = eval(DriverOptions.selector(id) + \".disabled\");\n        return !dtm.getResult().isTrue();\n    }\n\n    @Override\n    public boolean waitUntil(String expression) {\n        return options.retry(() -> {\n            try {\n                return evalQuickly(expression).getResult().isTrue();\n            } catch (Exception e) {\n                logger.warn(\"waitUntil evaluate failed: {}\", e.getMessage());\n                return false;\n            }\n        }, b -> b, \"waitUntil (js)\", true);\n    }\n\n    @Override\n    public Object script(String expression) {\n        return eval(expression).getResult().getValue();\n    }\n\n    @Override\n    public String getTitle() {\n        return eval(\"document.title\").getResult().getAsString();\n    }\n\n    @Override\n    public String getUrl() {\n        return eval(\"document.location.href\").getResult().getAsString();\n    }\n\n    @Override\n    public List<Map> getCookies() {\n        DevToolsMessage dtm = method(\"Network.getAllCookies\").send();\n        return dtm.getResultVariable(\"cookies\").getValue();\n    }\n\n    @Override\n    public Map<String, Object> cookie(String name) {\n        List<Map> list = getCookies();\n        if (list == null) {\n            return null;\n        }\n        for (Map<String, Object> map : list) {\n            if (map != null && name.equals(map.get(\"name\"))) {\n                return map;\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public void cookie(Map<String, Object> cookie) {\n        if (cookie.get(\"url\") == null && cookie.get(\"domain\") == null) {\n            cookie = new HashMap(cookie); // don't mutate test\n            cookie.put(\"url\", getUrl());\n        }\n        method(\"Network.setCookie\").params(cookie).send();\n    }\n\n    @Override\n    public void deleteCookie(String name) {\n        method(\"Network.deleteCookies\").param(\"name\", name).param(\"url\", getUrl()).send();\n    }\n\n    @Override\n    public void clearCookies() {\n        method(\"Network.clearBrowserCookies\").send();\n    }\n\n    @Override\n    public void dialog(boolean accept) {\n        dialog(accept, null);\n    }\n\n    @Override\n    public void dialog(boolean accept, String text) {\n        DevToolsMessage temp = method(\"Page.handleJavaScriptDialog\").param(\"accept\", accept);\n        if (text == null) {\n            temp.send();\n        } else {\n            temp.param(\"promptText\", text).send();\n        }\n    }\n\n    @Override\n    public String getDialogText() {\n        return currentDialogText;\n    }\n\n    @Override\n    public byte[] pdf(Map<String, Object> options) {\n        DevToolsMessage dtm = method(\"Page.printToPDF\").params(options).send();\n        String temp = dtm.getResultVariable(\"data\").getAsString();\n        return Base64.getDecoder().decode(temp);\n    }\n\n    @Override\n    public byte[] screenshot(boolean embed) {\n        return screenshot(null, embed);\n    }\n\n    @Override\n    public Map<String, Object> position(String locator) {\n        return position(locator, false);\n    }\n\n    @Override\n    public Map<String, Object> position(String locator, boolean relative) {\n        boolean submitTemp = submit; // in case we are prepping for a submit().mouse(locator).click()\n        submit = false;\n        retryIfEnabled(locator);\n        Map<String, Object> map = eval(relative ? DriverOptions.getRelativePositionJs(locator) : DriverOptions.getPositionJs(locator)).getResult().getValue();\n        submit = submitTemp;\n        return map;\n    }\n\n    @Override\n    public byte[] screenshot(String id, boolean embed) {\n        DevToolsMessage dtm;\n        try {\n            if (id == null) {\n                dtm = method(\"Page.captureScreenshot\").send();\n            } else {\n                Map<String, Object> map = position(id);\n                map.put(\"scale\", 1);\n                dtm = method(\"Page.captureScreenshot\").param(\"clip\", map).send();\n            }\n            if (dtm == null) {\n                logger.error(\"unable to capture screenshot - no data returned\");\n                return Constants.ZERO_BYTES;\n            }\n            String temp = dtm.getResultVariable(\"data\").getAsString();\n            byte[] bytes = Base64.getDecoder().decode(temp);\n            if (embed) {\n                getRuntime().embed(bytes, ResourceType.PNG);\n            }\n            return bytes;\n        } catch (Exception e) { // rare case where message does not get a reply\n            logger.error(\"screenshot failed: {}\", e.getMessage());\n            return Constants.ZERO_BYTES;\n        }\n    }\n\n    // chrome only\n    public byte[] screenshotFull() {\n        DevToolsMessage layout = method(\"Page.getLayoutMetrics\").send();\n        Map<String, Object> size = layout.getResultVariable(\"cssContentSize\").getValue();\n        Map<String, Object> map = options.newMapWithSelectedKeys(size, \"height\", \"width\");\n        map.put(\"x\", 0);\n        map.put(\"y\", 0);\n        map.put(\"scale\", 1);\n        DevToolsMessage dtm = method(\"Page.captureScreenshot\").param(\"clip\", map).param(\"captureBeyondViewport\", true).send();\n        if (dtm.isResultError()) {\n            logger.error(\"unable to capture screenshot: {}\", dtm);\n            return new byte[0];\n        }\n        String temp = dtm.getResultVariable(\"data\").getAsString();\n        return Base64.getDecoder().decode(temp);\n    }\n\n    @Override\n    public List<String> getPages() {\n        DevToolsMessage dtm = method(\"Target.getTargets\").send();\n        return dtm.getResultVariable(\"targetInfos.targetId\").getValue();\n    }\n\n    @Override\n    public void switchPage(String titleOrUrl) {\n        if (titleOrUrl == null) {\n            return;\n        }\n        String targetId = options.retry(() -> {\n            DevToolsMessage dtm = method(\"Target.getTargets\").send();\n            List<Map> targets = dtm.getResultVariable(\"targetInfos\").getValue();\n            for (Map map : targets) {\n                String title = (String) map.get(\"title\");\n                String url = (String) map.get(\"url\");\n                if ((title != null && title.contains(titleOrUrl))\n                        || (url != null && url.contains(titleOrUrl))) {\n                    return (String) map.get(\"targetId\");\n                }\n            }\n            return null;\n        }, returned -> returned != null, \"waiting to switch to tab: \" + titleOrUrl, true);\n        attachAndActivate(targetId, false);\n    }\n\n    @Override\n    public void switchPage(int index) {\n        if (index == -1) {\n            return;\n        }\n        DevToolsMessage dtm = method(\"Target.getTargets\").send();\n        List<Map> targets = dtm.getResultVariable(\"targetInfos\").getValue();\n        if (index < targets.size()) {\n            Map target = targets.get(index);\n            String targetId = (String) target.get(\"targetId\");\n            attachAndActivate(targetId, false);\n        } else {\n            logger.warn(\"failed to switch frame by index: {}\", index);\n        }\n    }\n\n    @Override\n    public void switchFrame(int index) {\n        if (index == -1) {\n            frame = null;\n            sessionId = frameSessions.get(mainFrameId);\n            return;\n        }\n        List<String> objectIds = elementIds(\"iframe,frame\");\n        if (index < objectIds.size()) {\n            String objectId = objectIds.get(index);\n            if (!setExecutionContext(objectId)) {\n                logger.warn(\"failed to switch frame by index: {}\", index);\n            }\n        } else {\n            logger.warn(\"unable to switch frame by index: {}\", index);\n        }\n    }\n\n    @Override\n    public void switchFrame(String locator) {\n        if (locator == null) {\n            frame = null;\n            sessionId = frameSessions.get(mainFrameId);\n            return;\n        }\n        retryIfEnabled(locator);\n        String objectId = evalForObjectId(DriverOptions.selector(locator));\n        if (!setExecutionContext(objectId)) {\n            logger.warn(\"failed to switch frame by locator: {}\", locator);\n        }\n    }\n\n    private Integer getFrameContext() {\n        if (frame == null) {\n            return null;\n        }\n        Integer result = frameContexts.get(frame.id);\n        logger.trace(\"** get frame context: {} - {}\", frame, result);\n        return result;\n    }\n\n    private boolean setExecutionContext(String objectId) { // locator just for logging      \n        DevToolsMessage dtm = method(\"DOM.describeNode\")\n                .param(\"objectId\", objectId)\n                .param(\"depth\", 0)\n                .send();\n        String frameId = dtm.getResult(\"node.frameId\");\n        if (frameId == null) {\n            return false;\n        }\n        dtm = method(\"Page.getFrameTree\").send();\n        frame = null;\n        try {\n            List<Map> childFrames = dtm.getResult(\"frameTree.childFrames[*]\");\n            List<Map> flattenFrameTree = getFrameTree(childFrames);\n            for (Map<String, Object> frameMap : flattenFrameTree) {\n                String frameMapTemp = (String) frameMap.get(\"id\");\n                if (frameId.equals(frameMapTemp)) {\n                    String frameUrl = (String) frameMap.get(\"url\");\n                    String frameName = (String) frameMap.get(\"name\");\n                    frame = new Frame(frameId, frameUrl, frameName);\n                    logger.trace(\"** switched to frame: {}\", frame);\n                    break;\n                }\n            }\n        } catch (PathNotFoundException e) {\n            logger.trace(\"** childFrames not found. Will try to change to a different Target in Chrome.\");\n        }\n\n        if (frame == null) {\n            // for some reason need to trigger Target.getTargets before attaching\n            dtm = method(\"Target.getTargets\").send();\n            if (frameSessions.get(frameId) == null) {\n                // attempt to force attach (see: https://github.com/karatelabs/karate/pull/1944#issuecomment-1070793461)\n                attachAndActivate(frameId, true);\n            }\n\n            List<Map<String, Object>> targetInfos = dtm.getResult(\"targetInfos\");\n            for (Map<String, Object> targetInfo : targetInfos) {\n                String temp = (String) targetInfo.get(\"targetId\");\n                String tempType = (String) targetInfo.get(\"type\");\n                if (frameId.equals(temp) && (\"iframe\".equals(tempType) || \"frame\".equals(tempType))) {\n                    String frameUrl = (String) targetInfo.get(\"url\");\n                    String frameName = (String) targetInfo.get(\"title\");\n                    frame = new Frame(frameId, frameUrl, frameName);\n                    logger.trace(\"** switched to frame: {}\", frame);\n                }\n            }\n        }\n\n        if (frame == null) {\n            return false;\n        }\n\n        if (frameSessions.get(frameId) != null) {\n            sessionId = frameSessions.get(frameId);\n        } else {\n            // attach to frame / target / process with the frame\n            attachAndActivate(frameId, true);\n\n            // a null sessionId indicates that we failed to attach directly to the frame\n            // this occurs on local frames that are already being debugged with the main frame\n            if (sessionId == null) {\n                sessionId = frameSessions.get(mainFrameId);\n            }\n        }\n\n        Integer contextId = getFrameContext();\n        if (contextId != null) {\n            return true;\n        }\n        dtm = method(\"Page.createIsolatedWorld\").param(\"frameId\", frameId).send();\n        contextId = dtm.getResult(\"executionContextId\");\n        frameContexts.put(frameId, contextId);\n        return true;\n    }\n\n    private List<Map> getFrameTree(List<Map> frames) {\n        List<Map> resultFrames = new ArrayList<>();\n        for (Map frame : frames) {\n            Map currFrame = (Map) frame.get(\"frame\");\n            List<Map> childFrames = (List<Map>) frame.get(\"childFrames\");\n            if (currFrame != null) {\n                resultFrames.add((Map) frame.get(\"frame\"));\n            }\n\n            if (childFrames != null) {\n                resultFrames.addAll(getFrameTree(childFrames));\n            }\n        }\n        return resultFrames;\n    }\n\n    public void enableNetworkEvents() {\n        method(\"Network.enable\").send();\n    }\n\n    public void enablePageEvents() {\n        method(\"Page.enable\").send();\n    }\n\n    public void enableRuntimeEvents() {\n        method(\"Runtime.enable\").send();\n    }\n\n    public DevToolsMock intercept(Value value) {\n        Map<String, Object> config = (Map) JsValue.toJava(value);\n        config = new Variable(config).getValue();\n        return intercept(config);\n    }\n\n    public DevToolsMock intercept(Map<String, Object> config) {\n        List<String> patterns = (List) config.get(\"patterns\");\n        if (patterns == null) {\n            throw new RuntimeException(\"missing array argument 'patterns': \" + config);\n        }\n        if (mockHandler != null) {\n            throw new RuntimeException(\"'intercept()' can be called only once\");\n        }\n        String mock = (String) config.get(\"mock\");\n        if (mock == null) {\n            throw new RuntimeException(\"missing argument 'mock': \" + config);\n        }\n        Object o = getRuntime().engine.fileReader.readFile(mock);\n        if (!(o instanceof FeatureCall)) {\n            throw new RuntimeException(\"'mock' is not a feature file: \" + config + \", \" + mock);\n        }\n        FeatureCall fc = (FeatureCall) o;\n        mockHandler = new MockHandler(fc.feature);\n        method(\"Fetch.enable\").param(\"patterns\", patterns).send();\n        return new DevToolsMock(mockHandler);\n    }\n\n    public void inputFile(String locator, String... relativePaths) {\n        retryIfEnabled(locator);\n        List<String> files = new ArrayList(relativePaths.length);\n        ScenarioEngine engine = ScenarioEngine.get();\n        for (String p : relativePaths) {\n            files.add(engine.fileReader.toAbsolutePath(p));\n        }\n        String objectId = evalForObjectId(DriverOptions.selector(locator));\n        method(\"DOM.setFileInputFiles\").param(\"files\", files).param(\"objectId\", objectId).send();\n    }\n\n    public Object scriptAwait(String expression) {\n        DevToolsMessage toSend = method(\"Runtime.evaluate\")\n                .param(\"expression\", expression)\n                .param(\"returnByValue\", true)\n                .param(\"awaitPromise\", true);\n        Integer contextId = getFrameContext();\n        if (contextId != null) {\n            toSend.param(\"contextId\", contextId);\n        }\n        return toSend.send().getResult().getValue();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/DevToolsMessage.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport com.intuit.karate.Json;\nimport com.intuit.karate.core.Variable;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class DevToolsMessage {\n\n    private static final Logger logger = LoggerFactory.getLogger(DevToolsMessage.class);\n\n    protected final DevToolsDriver driver;\n\n    private final String sessionId;\n    private final String method;\n\n    private Integer id;\n    private Json params;\n    private Map<String, Object> error;\n    private Variable result;\n    private Integer timeout;        \n\n    public Integer getId() {\n        return id;\n    }\n\n    public void setId(Integer id) {\n        this.id = id;\n    }\n\n    public Integer getTimeout() {\n        return timeout;\n    }\n\n    public void setTimeout(Integer timeout) {\n        this.timeout = timeout;\n    }\n\n    public String getMethod() {\n        return method;\n    }\n\n    public boolean methodIs(String method) {\n        return method.equals(this.method);\n    }\n\n    public <T> T getParam(String path) {\n        if (params == null) {\n            return null;\n        }\n        try {\n            return params.get(path);\n        } catch (Exception e) {\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"get param - json path failed: {} - {}\", path, params);\n            }\n            return null;\n        }\n    }\n\n    public Variable getResult() {\n        return result == null ? Variable.NULL : result;\n    }\n\n    public <T> T getResult(String path) {\n        if (result == null || result.isNull()) {\n            return null;\n        }\n        Json json = Json.of(result.getValue());\n        return json.get(path);\n    }\n\n    public void setResult(Variable result) {\n        this.result = result;\n    }\n\n    private static Map<String, Object> toMap(List<Map<String, Object>> list) {\n        Map<String, Object> res = new HashMap();\n        for (Map<String, Object> map : list) {\n            String key = (String) map.get(\"name\");\n            Map<String, Object> valMap = (Map) map.get(\"value\");\n            res.put(key, valMap == null ? null : valMap.get(\"value\"));\n        }\n        return res;\n    }\n\n    public boolean isResultError() {\n        if (error != null) {\n            return true;\n        }\n        if (result == null || !result.isMap()) {\n            return false;\n        }\n        String resultError = (String) result.<Map>getValue().get(\"subtype\");\n        return \"error\".equals(resultError);\n    }\n\n    public Variable getResultVariable(String key) {\n        if (result == null || !result.isMap()) {\n            return null;\n        }\n        return new Variable(result.<Map>getValue().get(key));\n    }\n\n    public Map<String, Object> getError() {\n        return error;\n    }        \n\n    public DevToolsMessage(DevToolsDriver driver, String method) {\n        this.driver = driver;\n        id = driver.nextId();\n        this.method = method;\n        sessionId = driver.sessionId;\n    }\n\n    public DevToolsMessage(DevToolsDriver driver, Map<String, Object> map) {\n        this.driver = driver;\n        sessionId = (String) map.get(\"sessionId\");;\n        id = (Integer) map.get(\"id\");\n        method = (String) map.get(\"method\");\n        Map temp = (Map) map.get(\"params\");\n        if (temp != null) {\n            params = Json.of(temp);\n        }\n        temp = (Map) map.get(\"result\");\n        if (temp != null) {\n            if (temp.containsKey(\"result\")) {\n                Object inner = temp.get(\"result\");\n                if (inner instanceof List) {\n                    result = new Variable(toMap((List) inner));\n                } else {\n                    Map innerMap = (Map) inner;\n                    String subtype = (String) innerMap.get(\"subtype\");\n                    if (\"error\".equals(subtype) || innerMap.containsKey(\"objectId\")) {\n                        result = new Variable(innerMap);\n                    } else { // Runtime.evaluate \"returnByValue\" is true\n                        result = new Variable(innerMap.get(\"value\"));\n                    }\n                }\n            } else {\n                result = new Variable(temp);\n            }\n        }\n        error = (Map) map.get(\"error\");\n    }\n\n    public DevToolsMessage param(String path, Object value) {\n        if (params == null) {\n            params = Json.object();\n        }\n        params.set(path, value);\n        return this;\n    }\n\n    public DevToolsMessage params(Map<String, Object> map) {\n        this.params = Json.of(map);\n        return this;\n    }\n\n    public Map<String, Object> toMap() {\n        Map<String, Object> map = new LinkedHashMap(4);\n        map.put(\"id\", id);\n        if (sessionId != null) {\n            map.put(\"sessionId\", sessionId);\n        }\n        map.put(\"method\", method);\n        if (params != null) {\n            map.put(\"params\", params.value());\n        }\n        if (result != null) {\n            map.put(\"result\", result.getValue());\n        }\n        return map;\n    }\n\n    public void sendWithoutWaiting() {\n        driver.send(this);\n    }\n\n    public DevToolsMessage send() {\n        return send(null);\n    }\n\n    public DevToolsMessage send(Predicate<DevToolsMessage> condition) {\n        return driver.sendAndWait(this, condition);\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"[id: \").append(id);\n        if (sessionId != null) {\n            sb.append(\", sessionId: \").append(sessionId);\n        }\n        if (method != null) {\n            sb.append(\", method: \").append(method);\n        }\n        if (params != null) {\n            sb.append(\", params: \").append(params);\n        }\n        if (result != null) {\n            sb.append(\", result: \").append(result);\n        }\n        if (error != null) {\n            sb.append(\", error: \").append(error);\n        }\n        sb.append(\"]\");\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/DevToolsMock.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport com.intuit.karate.core.MockHandler;\n\n/**\n *\n * @author peter\n */\npublic class DevToolsMock {\n    \n    private final MockHandler mock;\n    \n    public DevToolsMock(MockHandler mock) {\n        this.mock = mock;\n    }\n    \n    public Object get(String name) {\n        return mock.getVariable(name);\n    }    \n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/DevToolsWait.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport com.intuit.karate.Logger;\nimport java.util.function.Predicate;\n\n/**\n *\n * @author pthomas3\n */\npublic class DevToolsWait {\n\n    private final DriverOptions options;\n    private final DevToolsDriver driver;\n\n    private DevToolsMessage lastSent;\n    private Predicate<DevToolsMessage> condition;\n    private DevToolsMessage lastReceived;\n\n    private final Predicate<DevToolsMessage> DEFAULT = m -> lastSent.getId().equals(m.getId());\n    \n    public static final Predicate<DevToolsMessage> FRAME_RESIZED = forEvent(\"Page.frameResized\");\n    public static final Predicate<DevToolsMessage> INSPECTOR_DETACHED = forEvent(\"Inspector.detached\");\n    public static final Predicate<DevToolsMessage> DIALOG_OPENING = forEvent(\"Page.javascriptDialogOpening\");\n    \n    public static final Predicate<DevToolsMessage> ALL_FRAMES_LOADED = m -> {\n        // page is considered ready only when the dom is ready\n        // AND all child frames that STARTED loading BEFORE the dom became ready\n        if (m.methodIs(\"Page.domContentEventFired\")) {\n            if (m.driver.framesStillLoading.isEmpty()) {\n                m.driver.logger.trace(\"** dom ready, and no frames loading, wait done\");\n                return true;\n            } else {\n                m.driver.logger.trace(\"** dom ready, but frames still loading, will wait: {}\", m.driver.framesStillLoading);\n                return false;\n            }\n        }\n        if (m.methodIs(\"Page.frameStoppedLoading\")) {\n            if (!m.driver.domContentEventFired) {\n                m.driver.logger.trace(\"** dom not ready, will wait, and frames loading: {}\", m.driver.framesStillLoading);\n                return false;\n            }\n            if (m.driver.framesStillLoading.isEmpty()) {\n                m.driver.logger.trace(\"** dom ready, and no frames loading, wait done\");\n                return true;\n            } else {\n                m.driver.logger.trace(\"** dom ready, but frames still loading, will wait: {}\", m.driver.framesStillLoading);\n            }\n        }\n        return false;\n    };\n\n    public static Predicate<DevToolsMessage> forEvent(String name) {\n        return m -> name.equals(m.getMethod());\n    }\n\n    public DevToolsWait(DevToolsDriver driver, DriverOptions options) {\n        this.driver = driver;\n        this.options = options;\n        logger = options.driverLogger;\n    }\n\n    // mutable when driver logger is swapped\n    private Logger logger;\n\n    public void setLogger(Logger logger) {\n        this.logger = logger;\n    }\n\n    public void setCondition(Predicate<DevToolsMessage> condition) {\n        this.condition = condition;\n    }\n\n    public DevToolsMessage send(DevToolsMessage dtm, Predicate<DevToolsMessage> condition) {\n        lastReceived = null;\n        lastSent = dtm;\n        this.condition = condition == null ? DEFAULT : condition;        \n        long timeout = dtm.getTimeout() == null ? options.getTimeout() : dtm.getTimeout();\n        synchronized (this) {\n            logger.trace(\">> wait: {}\", dtm);\n            try {\n                driver.send(dtm);\n                wait(timeout);\n            } catch (InterruptedException e) {\n                logger.error(\"interrupted: {} wait: {}\", e.getMessage(), dtm);\n            }\n        }\n        if (lastReceived != null) {\n            logger.trace(\"<< notified: {}\", dtm);\n        } else {\n            logger.error(\"<< timed out after milliseconds: {} - {}\", timeout, dtm);\n            return null;\n        }\n        return lastReceived;\n    }\n\n    public void receive(DevToolsMessage dtm) {\n        synchronized (this) {\n            if (condition.test(dtm)) {\n                if (dtm.isResultError()) {\n                    logger.warn(\"devtools error: {}\", dtm);\n                } else {\n                    logger.trace(\"<< notify: {}\", dtm);\n                }                \n                lastReceived = dtm;\n                notify();\n            } else {\n                logger.trace(\"<< ignore: {}\", dtm);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/DockerTarget.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.KarateException;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.shell.Command;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.File;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Function;\n\n/**\n *\n * @author pthomas3\n */\npublic class DockerTarget implements Target {\n\n    private final String imageId;\n    private String containerId;\n    private Function<Integer, String> command;\n    private final Map<String, Object> options;\n    private boolean pull = false;\n    private boolean karateChrome = false;\n\n    static final Logger logger = LoggerFactory.getLogger(DockerTarget.class);\n\n\n    public DockerTarget(String dockerImage) {\n        this(Collections.singletonMap(\"docker\", dockerImage));\n    }\n\n    public DockerTarget(Map<String, Object> options) {\n        this.options = options;\n        if (options != null) {\n            imageId = (String) options.get(\"docker\");\n            Integer vncPort = (Integer) options.get(\"vncPort\");\n            String secComp = (String) options.get(\"secComp\");\n            Boolean temp = (Boolean) options.get(\"pull\");\n            pull = temp == null ? false : temp;\n            StringBuilder sb = new StringBuilder();\n            sb.append(\"docker run -d -e KARATE_SOCAT_START=true\");\n            if (secComp == null) {\n                sb.append(\" --cap-add=SYS_ADMIN\");\n            } else {\n                sb.append(\" --security-opt seccomp=\").append(secComp);\n            }\n            if (vncPort != null) {\n                sb.append(\" -p \").append(vncPort).append(\":5900\");\n            }\n            if (imageId != null) {\n                if (imageId.contains(\"/chrome-headless\")) {\n                    command = p -> sb.toString() + \" -p 9222 \" + imageId;\n                } else if (imageId.contains(\"/karate-chrome\")) {\n                    karateChrome = true;\n                    command = p -> sb.toString() + \" -p 9222 \" + imageId;\n                }\n            }\n        } else {\n            imageId = null;\n        }\n    }\n\n    public void setCommand(Function<Integer, String> command) {\n        this.command = command;\n    }\n\n    public Function<Integer, String> getCommand() {\n        return command;\n    }\n\n    @Override\n    public Map<String, Object> start(ScenarioRuntime sr) {\n        if (command == null) {\n            throw new RuntimeException(\"docker target command (function) not set\");\n        }\n        if (imageId != null && pull) {\n            sr.logger.debug(\"attempting to pull docker image: {}\", imageId);\n            Command.execLine(null, \"docker pull \" + imageId);\n        }\n        containerId = Command.execLine(null, command.apply(null));\n        int port = this.getContainerPort(containerId);\n        Map<String, Object> map = new HashMap();\n        if (options != null) {\n            map.putAll(options);\n        }\n\n        boolean remoteHost = options != null && options.get(\"remoteHost\") != null && (Boolean) options.get(\"remoteHost\");\n        boolean useDockerHost = options != null && options.get(\"useDockerHost\") != null && (Boolean) options.get(\"useDockerHost\");\n        String host = \"127.0.0.1\";\n\n        if (remoteHost) {\n            String gateway = Command.execLine(null, \"docker inspect -f '{{.NetworkSettings.Gateway}}' \" + containerId);\n            host = gateway.replaceAll(\"'\", \"\"); // Some responses are wrapped in single quotes.\n        } else if (useDockerHost) {\n            host = \"host.docker.internal\";\n        }\n\n        map.put(\"start\", false);\n        map.put(\"host\", host);\n        map.put(\"port\", port);\n        map.put(\"type\", \"chrome\");\n        Command.waitForHttp(\"http://\" + host + \":\" + port + \"/json\");\n        return map;\n    }\n\n\n    @Override\n    public Map<String, Object> stop(ScenarioRuntime sr) {\n        Command.execLine(null, \"docker stop \" + containerId);\n        if (!karateChrome) { // no video\n            Command.execLine(null, \"docker rm \" + containerId);\n            return Collections.EMPTY_MAP;\n        }\n        String shortName = containerId.contains(\"_\") ? containerId : StringUtils.truncate(containerId, 12, false);\n        String dirName = \"karate-chrome_\" + shortName;\n        String buildDir = sr.featureRuntime.suite.buildDir;\n        String resultsDir = buildDir + File.separator + dirName;\n        Command.execLine(null, \"docker cp \" + containerId + \":/tmp \" + resultsDir);\n        Command.execLine(null, \"docker rm \" + containerId);\n        String video = resultsDir + File.separator + \"karate.mp4\";\n        File file = new File(video);\n        if (!file.exists()) {\n            sr.logger.warn(\"video file missing: {}\", file);\n            return Collections.EMPTY_MAP;\n        }\n        File copy = new File(buildDir + File.separator + dirName + \".mp4\");\n        FileUtils.copy(file, copy);\n        return Collections.singletonMap(\"video\", copy.getPath());\n    }\n\n    private int getContainerPort(String containerId) {\n        // 9222/tcp is the default port for chrome headless\n        String format = \"--format='{{(index (index .NetworkSettings.Ports \\\"9222/tcp\\\") 0).HostPort}}'\";\n\n        if (FileUtils.isOsWindows()) {\n            format = format.replace(\"\\\"\", \"\\\\\\\"\");\n        }\n\n        logger.debug(\"cmd: docker format {} {}\", format, containerId);\n        String dockerInspect = Command.exec(false, (File) null,\n                \"docker\", \"inspect\", format, containerId\n        );\n\n        // Certain OS responses come back with single quotes with Command.exec\n        dockerInspect = dockerInspect.replaceAll(\"[^\\\\d]\", \"\");\n        logger.debug(\"docker inspect command output: {}\", dockerInspect);\n        try {\n            return Integer.parseInt(dockerInspect);\n        } catch (NumberFormatException e) {\n            throw new KarateException(\"Error fetching port from started docker container\", e);\n        }\n    }\n\n    public String getContainerId() {\n        return this.containerId;\n    }\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/Driver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport com.intuit.karate.core.AutoDef;\nimport com.intuit.karate.core.Plugin;\nimport com.intuit.karate.core.Config;\nimport com.intuit.karate.core.FeatureRuntime;\nimport com.intuit.karate.core.ScenarioEngine;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.core.StepResult;\nimport com.intuit.karate.http.HttpClientFactory;\nimport com.intuit.karate.http.ResourceType;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Driver extends Plugin {\n\n    public static Driver start(String browserType) {\n        return start(Collections.singletonMap(\"type\", browserType));\n    }\n\n    public static Driver start(Map<String, Object> options) {\n        ScenarioRuntime runtime = FeatureRuntime.forTempUse(HttpClientFactory.DEFAULT).scenarios.next();\n        ScenarioEngine.set(runtime.engine);\n        return DriverOptions.start(options, runtime);\n    }\n\n    @AutoDef\n    void activate();\n\n    @AutoDef\n    void refresh();\n\n    @AutoDef\n    void reload();\n\n    @AutoDef\n    void back();\n\n    @AutoDef\n    void forward();\n\n    @AutoDef\n    void maximize();\n\n    @AutoDef\n    void minimize();\n\n    @AutoDef\n    void fullscreen();\n\n    @AutoDef\n    void close();\n\n    @AutoDef\n    void quit();\n\n    @AutoDef\n    void switchPage(String titleOrUrl);\n\n    @AutoDef\n    void switchPage(int index);\n\n    @AutoDef\n    void switchFrame(int index);\n\n    @AutoDef\n    void switchFrame(String locator);\n\n    String getUrl(); // getter\n\n    void setUrl(String url); // setter    \n\n    Map<String, Object> getDimensions(); // getter\n\n    void setDimensions(Map<String, Object> map); // setter\n\n    String getTitle(); // getter\n\n    List<String> getPages(); // getter\n\n    String getDialogText(); // getter\n\n    @AutoDef\n    byte[] screenshot(boolean embed);\n\n    @AutoDef\n    default byte[] screenshot() {\n        return screenshot(true);\n    }\n\n    @AutoDef\n    Map<String, Object> cookie(String name);\n\n    @AutoDef\n    default void setCookies(List<Map<String, Object>> cookies) {\n        System.out.println(\"got this cookie: \" + cookies);\n        cookies.forEach(c -> cookie(c));\n    }\n\n    @AutoDef\n    void cookie(Map<String, Object> cookie);\n\n    @AutoDef\n    void deleteCookie(String name);\n\n    @AutoDef\n    void clearCookies();\n\n    List<Map> getCookies(); // getter    \n\n    @AutoDef\n    void dialog(boolean accept);\n\n    @AutoDef\n    void dialog(boolean accept, String input);\n\n    @AutoDef\n    Object script(String expression);\n\n    @AutoDef\n    boolean waitUntil(String expression);\n\n    @AutoDef\n    Driver submit();\n\n    @AutoDef\n    default Driver retry() {\n        return retry(null, null);\n    }\n\n    @AutoDef\n    default Driver retry(int count) {\n        return retry(count, null);\n    }\n\n    @AutoDef\n    default Driver retry(Integer count, Integer interval) {\n        getOptions().enableRetry(count, interval);\n        return this;\n    }\n\n    @AutoDef\n    default Driver delay(int millis) {\n        getOptions().sleep(millis);\n        return this;\n    }\n\n    @AutoDef\n    Driver timeout(Integer millis);\n\n    @AutoDef\n    Driver timeout();\n\n    // element actions =========================================================\n    //\n    @AutoDef\n    Element focus(String locator);\n\n    @AutoDef\n    Element clear(String locator);\n\n    @AutoDef\n    Element click(String locator);\n\n    @AutoDef\n    Element input(String locator, String value);\n\n    @AutoDef\n    default Element input(String locator, String[] values) {\n        return input(locator, values, 0);\n    }\n\n    @AutoDef\n    default Element input(String locator, String chars, int delay) {\n        String[] array = new String[chars.length()];\n        for (int i = 0; i < array.length; i++) {\n            array[i] = Character.toString(chars.charAt(i));\n        }\n        return input(locator, array, delay);\n    }\n\n    @AutoDef\n    default Element input(String locator, String[] values, int delay) {\n        Element element = DriverElement.locatorUnknown(this, locator);\n        for (String value : values) {\n            if (delay > 0) {\n                delay(delay);\n            }\n            element = input(locator, value);\n        }\n        return element;\n    }\n\n    @AutoDef\n    Element select(String locator, String text);\n\n    @AutoDef\n    Element select(String locator, int index);\n\n    @AutoDef\n    Element value(String locator, String value);\n\n    @AutoDef\n    default Element waitFor(String locator) {\n        return getOptions().waitForAny(this, locator);\n    }\n\n    @AutoDef\n    default String waitForUrl(String expected) {\n        return getOptions().waitForUrl(this, expected);\n    }\n\n    @AutoDef\n    default Element waitForText(String locator, String expected) {\n        return waitUntil(locator, \"_.textContent.includes('\" + expected + \"')\");\n    }\n\n    @AutoDef\n    default Element waitForEnabled(String locator) {\n        return waitUntil(locator, \"!_.disabled\");\n    }\n\n    @AutoDef\n    default List<Element> waitForResultCount(String locator, int count) {\n        return (List) waitUntil(() -> {\n            List<Element> list = locateAll(locator);\n            return list.size() == count ? list : null;\n        });\n    }\n\n    @AutoDef\n    default List waitForResultCount(String locator, int count, String expression) {\n        return (List) waitUntil(() -> {\n            List list = scriptAll(locator, expression);\n            return list.size() == count ? list : null;\n        });\n    }\n\n    @AutoDef\n    default Element waitForAny(String locator1, String locator2) {\n        return getOptions().waitForAny(this, new String[]{locator1, locator2});\n    }\n\n    @AutoDef\n    default Element waitForAny(String[] locators) {\n        return getOptions().waitForAny(this, locators);\n    }\n\n    @AutoDef\n    default Element waitUntil(String locator, String expression) {\n        return getOptions().waitUntil(this, locator, expression);\n    }\n\n    @AutoDef\n    default Object waitUntil(Supplier<Object> condition) {\n        return getOptions().retry(() -> condition.get(), o -> o != null, \"waitUntil (function)\", true);\n    }\n\n    @AutoDef\n    default Element locate(String locator) {\n        Element e = DriverElement.locatorUnknown(this, locator);\n        if (e.isPresent()) {\n            return e;\n        }\n        throw new RuntimeException(\"cannot find locator: \" + locator);\n    }\n\n    @AutoDef\n    default List<Element> locateAll(String locator) {\n        return getOptions().findAll(this, locator);\n    }\n\n    @AutoDef\n    default List<Element> locateAll(String locator, Predicate predicate) {\n        List before = locateAll(locator);\n        List after = new ArrayList(before.size());\n        for (Object o : before) {\n            if (predicate.test(o)) {\n                after.add(o);\n            }\n        }\n        return after;\n    }\n\n    @AutoDef\n    default Element scroll(String locator) {\n        script(locator, DriverOptions.SCROLL_JS_FUNCTION);\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @AutoDef\n    default Element highlight(String locator) {\n        return highlight(locator, Config.DEFAULT_HIGHLIGHT_DURATION);\n    }\n\n    default Element highlight(String locator, int millis) {\n        script(getOptions().highlight(locator, millis));\n        delay(millis);\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @AutoDef\n    default void highlightAll(String locator) {\n        highlightAll(locator, Config.DEFAULT_HIGHLIGHT_DURATION);\n    }\n\n    default void highlightAll(String locator, int millis) {\n        script(getOptions().highlightAll(locator, millis));\n        delay(millis);\n    }\n\n    // friendly locators =======================================================\n    //\n    @AutoDef\n    default Finder rightOf(String locator) {\n        return new ElementFinder(this, locator, ElementFinder.Type.RIGHT);\n    }\n\n    @AutoDef\n    default Finder leftOf(String locator) {\n        return new ElementFinder(this, locator, ElementFinder.Type.LEFT);\n    }\n\n    @AutoDef\n    default Finder above(String locator) {\n        return new ElementFinder(this, locator, ElementFinder.Type.ABOVE);\n    }\n\n    @AutoDef\n    default Finder below(String locator) {\n        return new ElementFinder(this, locator, ElementFinder.Type.BELOW);\n    }\n\n    @AutoDef\n    default Finder near(String locator) {\n        return new ElementFinder(this, locator, ElementFinder.Type.NEAR);\n    }\n\n    // mouse and keys ==========================================================\n    //\n    @AutoDef\n    default Mouse mouse() {\n        return new DriverMouse(this);\n    }\n\n    @AutoDef\n    default Mouse mouse(String locator) {\n        return new DriverMouse(this).move(locator);\n    }\n\n    @AutoDef\n    default Mouse mouse(Number x, Number y) {\n        return new DriverMouse(this).move(x, y);\n    }\n\n    @AutoDef\n    default Keys keys() {\n        return new Keys(this);\n    }\n\n    @AutoDef\n    void actions(List<Map<String, Object>> actions);\n\n    // element state ===========================================================\n    //\n    @AutoDef\n    String html(String locator);\n\n    @AutoDef\n    String text(String locator);\n\n    @AutoDef\n    String value(String locator);\n\n    @AutoDef\n    String attribute(String locator, String name);\n\n    @AutoDef\n    String property(String locator, String name);\n\n    @AutoDef\n    boolean enabled(String locator);\n\n    @AutoDef\n    default boolean exists(String locator) {\n        return getOptions().optional(this, locator).isPresent();\n    }\n\n    @AutoDef\n    default Element optional(String locator) {\n        return getOptions().optional(this, locator);\n    }\n\n    @AutoDef\n    Map<String, Object> position(String locator);\n\n    @AutoDef\n    Map<String, Object> position(String locator, boolean relative);\n\n    @AutoDef\n    byte[] screenshot(String locator, boolean embed);\n\n    @AutoDef\n    default byte[] screenshot(String locator) {\n        return screenshot(locator, true);\n    }\n\n    @AutoDef\n    default Object script(String locator, String expression) {\n        String js = getOptions().scriptSelector(locator, expression);\n        return script(js);\n    }\n\n    @AutoDef\n    default List scriptAll(String locator, String expression) {\n        String js = getOptions().scriptAllSelector(locator, expression);\n        return (List) script(js);\n    }\n\n    @AutoDef\n    default List scriptAll(String locator, String expression, Predicate predicate) {\n        List before = scriptAll(locator, expression);\n        List after = new ArrayList(before.size());\n        for (Object o : before) {\n            if (predicate.test(o)) {\n                after.add(o);\n            }\n        }\n        return after;\n    }\n\n    @AutoDef\n    public byte[] pdf(Map<String, Object> options);\n\n    // for internal use ========================================================\n    //        \n    boolean isTerminated();\n\n    DriverOptions getOptions();\n\n    Object elementId(String locator);\n\n    List elementIds(String locator);\n\n    static final List<String> METHOD_NAMES = Plugin.methodNames(Driver.class);\n\n    @Override\n    default List<String> methodNames() {\n        return METHOD_NAMES;\n    }\n\n    @Override\n    default Map<String, Object> afterScenario() {\n        return Collections.EMPTY_MAP; // TODO\n    }\n\n    @Override\n    public default void onFailure(StepResult stepResult) {\n        if (getOptions().screenshotOnFailure && !stepResult.isWithCallResults()) {\n            byte[] bytes = screenshot(false);\n            getRuntime().embed(bytes, ResourceType.PNG);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/DriverElement.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * TODO make this convert-able to JSON\n *\n * @author pthomas3\n */\npublic class DriverElement implements Element {\n\n    private final Driver driver;\n    private final String locator;\n\n    private Boolean exists;\n\n    private DriverElement(Driver driver, String locator, Boolean exists) {\n        this.driver = driver;\n        this.locator = locator;\n        this.exists = exists;\n    }\n\n    public static Element locatorExists(Driver driver, String locator) {\n        return new DriverElement(driver, locator, true);\n    }\n\n    public static Element locatorUnknown(Driver driver, String locator) {\n        return new DriverElement(driver, locator, null); // exists flag set to null\n    }\n\n    @Override\n    public String getLocator() {\n        return locator;\n    }\n\n    @Override\n    public boolean isPresent() {\n        if (exists == null) {\n            exists = driver.optional(locator).isPresent();\n        }\n        return exists;\n    }\n\n    public void setExists(Boolean exists) {\n        this.exists = exists;\n    }\n\n    @Override\n    public Map<String, Object> getPosition() {\n        return driver.position(locator);\n    }\n\n    @Override\n    public byte[] screenshot() {\n        return driver.screenshot();\n    }\n\n    @Override\n    public boolean isEnabled() {\n        return driver.enabled(locator);\n    }\n\n    @Override\n    public Element highlight() {\n        return driver.highlight(locator);\n    }\n\n    @Override\n    public Element focus() {\n        return driver.focus(locator);\n    }\n\n    @Override\n    public Element clear() {\n        return driver.clear(locator);\n    }\n\n    @Override\n    public Element click() {\n        return driver.click(locator);\n    }\n\n    @Override\n    public Element submit() {\n        driver.submit();\n        return this;\n    }\n\n    @Override\n    public Element scroll() {\n        driver.scroll(locator);\n        return this;\n    }        \n\n    @Override\n    public Mouse mouse() {\n        return driver.mouse(locator);\n    }\n\n    @Override\n    public Element input(String value) {\n        return driver.input(locator, value);\n    }\n\n    @Override\n    public Element input(String[] values) {\n        return driver.input(locator, values);\n    }\n\n    @Override\n    public Element input(String[] values, int delay) {\n        return driver.input(locator, values, delay);\n    }\n\n    @Override\n    public Element select(String text) {\n        return driver.select(locator, text);\n    }\n\n    @Override\n    public Element select(int index) {\n        return driver.select(locator, index);\n    }\n\n    @Override\n    public Element switchFrame() {\n        driver.switchFrame(locator);\n        return this;\n    }\n\n    @Override\n    public Element delay(int millis) {\n        driver.delay(millis);\n        return this;\n    }\n\n    @Override\n    public Element retry() {\n        driver.retry();\n        return this;\n    }\n\n    @Override\n    public Element retry(int count) {\n        driver.retry(count);\n        return this;\n    }\n\n    @Override\n    public Element retry(Integer count, Integer interval) {\n        driver.retry(count, interval);\n        return this;\n    }\n\n    @Override\n    public Element waitFor() {\n        driver.waitFor(locator); // will throw exception if not found\n        return this;\n    }\n\n    @Override\n    public Element waitForText(String text) {\n        return driver.waitForText(locator, text);\n    }\n\n    @Override\n    public Element waitUntil(String expression) {\n        return driver.waitUntil(locator, expression); // will throw exception if not found\n    }\n\n    @Override\n    public Object script(String expression) {\n        return driver.script(locator, expression);\n    }\n    \n    @Override\n    public Object scriptAll(String relative, String expression) {\n        String script = driver.getOptions().scriptAllSelector(relative, expression, thisLocator());       \n        return driver.script(script);\n    }    \n\n    private String thisLocator() {\n        String thisRef = (String) driver.script(locator, DriverOptions.KARATE_REF_GENERATOR);\n        return DriverOptions.karateLocator(thisRef);\n    }\n\n    @Override\n    public Element optional(String locator) {\n        String childRefScript = driver.getOptions().scriptSelector(locator, DriverOptions.KARATE_REF_GENERATOR, thisLocator());\n        try {\n            String childRef = (String) driver.script(childRefScript);\n            return DriverElement.locatorExists(driver, DriverOptions.karateLocator(childRef));\n        } catch (Exception e) {\n            return new MissingElement(driver, locator);\n        }\n    }\n\n    @Override\n    public boolean exists(String locator) {\n        return optional(locator).isPresent();\n    }\n\n    @Override\n    public Element locate(String locator) {\n        Element e = optional(locator);\n        if (e.isPresent()) {\n            return e;\n        }\n        throw new RuntimeException(\"cannot find locator: \" + locator);\n    }\n\n    @Override\n    public List<Element> locateAll(String locator) {\n        String childRefScript = driver.getOptions().scriptAllSelector(locator, DriverOptions.KARATE_REF_GENERATOR, thisLocator());\n        List<String> childRefs = (List) driver.script(childRefScript);\n        return refsToElements(childRefs);\n    }\n    \n    private List<Element> refsToElements(List<String> refs) {\n        List<Element> elements = new ArrayList(refs.size());\n        for (String ref : refs) {\n            String karateLocator = DriverOptions.karateLocator(ref);\n            elements.add(DriverElement.locatorExists(driver, karateLocator));\n        }\n        return elements;        \n    }\n\n    @Override\n    public String attribute(String name) {\n        return driver.attribute(locator, name);\n    }\n\n    @Override\n    public String property(String name) {\n        return driver.property(locator, name);\n    }\n\n    //java bean naming conventions =============================================        \n    //        \n    @Override\n    public String getHtml() {\n        return driver.html(locator);\n    }\n\n    @Override\n    public void setHtml(String html) {\n        driver.script(locator, \"_.outerHTML = '\" + html + \"'\");\n    }\n\n    @Override\n    public String getText() {\n        return driver.text(locator);\n    }\n\n    @Override\n    public void setText(String text) {\n        driver.script(locator, \"_.innerHTML = '\" + text + \"'\");\n    }\n\n    @Override\n    public String getValue() {\n        return driver.value(locator);\n    }\n\n    @Override\n    public void setValue(String value) {\n        driver.value(locator, value);\n    }\n    \n    private Element relationLocator(String relation) {\n        String js = \"var gen = \" + DriverOptions.KARATE_REF_GENERATOR + \"; var e = \" \n                + DriverOptions.selector(locator) + \"; return gen(e.\" + relation + \")\";\n        String karateRef = (String) driver.script(DriverOptions.wrapInFunctionInvoke(js));\n        return DriverElement.locatorExists(driver, DriverOptions.karateLocator(karateRef));        \n    }\n\n    @Override\n    public Element getParent() {\n        return relationLocator(\"parentElement\");\n    }\n\n    @Override\n    public List<Element> getChildren() {\n        String js = \"var gen = \" + DriverOptions.KARATE_REF_GENERATOR + \"; var es = \" \n                + DriverOptions.selector(locator) + \".children; var res = []; var i;\"\n                + \" for(i = 0; i < es.length; i++) res.push(gen(es[i])); return res\";\n        List<String> childRefs = (List) driver.script(DriverOptions.wrapInFunctionInvoke(js));\n        return refsToElements(childRefs);\n    }        \n\n    @Override\n    public Element getFirstChild() {\n        return relationLocator(\"firstElementChild\");\n    }  \n\n    @Override\n    public Element getLastChild() {\n        return relationLocator(\"lastElementChild\");\n    }     \n\n    @Override\n    public Element getPreviousSibling() {\n        return relationLocator(\"previousElementSibling\");\n    }  \n\n    @Override\n    public Element getNextSibling() {\n        return relationLocator(\"nextElementSibling\");\n    }        \n\n    @Override\n    public Finder rightOf() {\n        return driver.rightOf(locator);\n    }\n\n    @Override\n    public Finder leftOf() {\n        return driver.leftOf(locator);\n    }\n\n    @Override\n    public Finder above() {\n        return driver.above(locator);\n    }\n\n    @Override\n    public Finder below() {\n        return driver.below(locator);\n    }\n\n    @Override\n    public Finder near() {\n        return driver.near(locator);\n    }\n\n    @Override\n    public String toString() {\n        return locator;\n    }        \n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/DriverMouse.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class DriverMouse implements Mouse {\n\n    private final Driver driver;\n\n    public DriverMouse(Driver driver) {\n        this.driver = driver;\n    }\n\n    private Integer duration;\n    private final List<Map<String, Object>> actions = new ArrayList();\n    private Number x, y;\n\n    private Map<String, Object> moveAction(int x, int y) {\n        // {\"type\":\"pointer\",\"id\":\"1\",\"actions\":[{\"type\":\"pointerMove\",\"x\":250,\"y\":250}]}        \n        Map<String, Object> map = new HashMap();\n        map.put(\"type\", \"pointerMove\");\n        map.put(\"x\", x);\n        map.put(\"y\", y);\n        if (duration != null) {\n            map.put(\"duration\", duration);\n        }\n        return map;\n    }\n\n    @Override\n    public DriverMouse duration(Integer duration) {\n        this.duration = duration;\n        return this;\n    }\n\n    @Override\n    public DriverMouse pause(Integer duration) {\n        Map<String, Object> map = new HashMap();\n        map.put(\"type\", \"pause\");\n        map.put(\"duration\", duration);\n        actions.add(map);\n        return this;\n    }\n\n    @Override\n    public DriverMouse move(String locator) {\n        Map<String, Object> map = driver.position(locator);\n        Number x = (Number) map.get(\"x\");\n        Number y = (Number) map.get(\"y\");\n        Number width = (Number) map.get(\"width\");\n        Number height = (Number) map.get(\"height\");\n        return move(x.intValue() + width.intValue() / 2, y.intValue() + height.intValue() / 2);\n    }\n\n    @Override\n    public DriverMouse move(Number x, Number y) {\n        this.x = x == null ? 0 : x;\n        this.y = y == null ? 0 : y;\n        Map<String, Object> action = moveAction(this.x.intValue(), this.y.intValue());\n        actions.add(action);\n        return this;\n    }\n    \n    @Override\n    public DriverMouse offset(Number x, Number y) {\n       if (x == null) {\n           x = 0;\n       }\n       if (y == null) {\n           y = 0;           \n       }\n       if (this.x == null) {\n           this.x = 0;\n       }\n       if (this.y == null) {\n           this.y = 0;\n       }\n        Map<String, Object> action = moveAction(this.x.intValue() + x.intValue(), this.y.intValue() + y.intValue());\n        actions.add(action);\n        return this;\n    }    \n\n    @Override\n    public DriverMouse down() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"type\", \"pointerDown\");\n        map.put(\"button\", 0);\n        actions.add(map);\n        return this;\n    }\n\n    @Override\n    public DriverMouse up() {\n        Map<String, Object> up = new HashMap();\n        up.put(\"type\", \"pointerUp\");\n        up.put(\"button\", 0);\n        actions.add(up);\n        return go();\n    }\n\n    @Override\n    public DriverMouse submit() {\n        driver.submit();\n        return this;\n    }\n\n    @Override\n    public DriverMouse click() {\n        return down().up();\n    }\n\n    @Override\n    public DriverMouse doubleClick() {\n        String js = \"document.elementFromPoint(\" + x + \",\" + y + \").dispatchEvent(new MouseEvent('dblclick'))\";\n        driver.script(js);\n        return this;\n    }\n\n    @Override\n    public DriverMouse go() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"type\", \"pointer\");\n        map.put(\"id\", \"1\");\n        map.put(\"actions\", actions);\n        driver.actions(Collections.singletonList(map));\n        actions.clear();\n        return this;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/DriverOptions.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport com.intuit.karate.Http;\nimport com.intuit.karate.KarateException;\nimport com.intuit.karate.LogAppender;\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.driver.appium.AndroidDriver;\nimport com.intuit.karate.driver.chrome.Chrome;\nimport com.intuit.karate.driver.chrome.ChromeWebDriver;\nimport com.intuit.karate.driver.microsoft.EdgeChromium;\nimport com.intuit.karate.driver.microsoft.IeWebDriver;\nimport com.intuit.karate.driver.microsoft.MsWebDriver;\nimport com.intuit.karate.driver.firefox.GeckoWebDriver;\nimport com.intuit.karate.driver.appium.IosDriver;\nimport com.intuit.karate.driver.microsoft.MsEdgeDriver;\nimport com.intuit.karate.driver.safari.SafariWebDriver;\nimport com.intuit.karate.driver.microsoft.WinAppDriver;\nimport com.intuit.karate.driver.playwright.PlaywrightDriver;\nimport com.intuit.karate.core.Config;\nimport com.intuit.karate.core.ScenarioEngine;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.shell.Command;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\n/**\n *\n * @author pthomas3\n */\npublic class DriverOptions {\n\n    public final Map<String, Object> options;\n    public final int timeout;\n    public final boolean start;\n    public final boolean stop;\n    public final String executable;\n    public final String type;\n    public final int port;\n    public final String host;\n    public final int pollAttempts;\n    public final int pollInterval;\n    public final boolean headless;\n    public final boolean showProcessLog;\n    public final boolean showDriverLog;\n    public final boolean showBrowserLog;\n    public final Logger logger;\n    public final LogAppender appender;\n    public final Logger processLogger;\n    public final Logger driverLogger;\n    public final String uniqueName;\n    public final File workingDir;\n    public final String userAgent;\n    public final String userDataDir;\n    public final String processLogFile;\n    public final int maxPayloadSize;\n    public final List<String> addOptions;\n    public final List<String> args = new ArrayList<>();\n    public final String webDriverUrl;\n    public final String webDriverPath;\n    public final Map<String, Object> webDriverSession;\n    public final Map<String, Object> httpConfig;\n    public final boolean remoteHost;\n    public final boolean useDockerHost;\n    public final Target target;\n    public final String beforeStart;\n    public final String afterStop;\n    public final String videoFile;\n    public final boolean highlight;\n    public final int highlightDuration;\n    public final String attach;\n    public final boolean screenshotOnFailure;\n    public final String playwrightUrl;\n    public final Map<String, Object> playwrightOptions;\n\n    // mutable during a test\n    private boolean retryEnabled;\n    private Integer retryInterval = null;\n    private Integer retryCount = null;\n    private String preSubmitHash = null;\n\n    private Integer timeoutOverride;\n\n    public static final String SCROLL_JS_FUNCTION = \"function(e){ var d = window.getComputedStyle(e).display;\"\n            + \" while(d == 'none'){ e = e.parentElement; d = window.getComputedStyle(e).display }\"\n            + \" e.scrollIntoView({block: 'center'}) }\";\n\n    public static final String KARATE_REF_GENERATOR = \"function(e){\"\n            + \" if (!document._karate) document._karate = { seq: (new Date()).getTime() };\"\n            + \" var ref = 'ref' + document._karate.seq++; document._karate[ref] = e; return ref }\";\n\n    public boolean isRetryEnabled() {\n        return retryEnabled;\n    }\n\n    public String getPreSubmitHash() {\n        return preSubmitHash;\n    }\n\n    public boolean isRemoteHost() {\n        return remoteHost;\n    }\n\n    public boolean isHostDockerHost() {\n        return host.equalsIgnoreCase(\"host.docker.internal\");\n    }\n\n    public void setPreSubmitHash(String preSubmitHash) {\n        this.preSubmitHash = preSubmitHash;\n    }\n\n    private <T> T get(String key, T defaultValue) {\n        T temp = (T) options.get(key);\n        return temp == null ? defaultValue : temp;\n    }\n\n    public DriverOptions(Map<String, Object> options, ScenarioRuntime sr, int defaultPort, String defaultExecutable) {\n        this.options = options;\n        this.appender = sr.logAppender;\n        logger = new Logger(getClass());\n        logger.setAppender(appender);\n        timeout = get(\"timeout\", Config.DEFAULT_TIMEOUT);\n        type = get(\"type\", null);\n        start = get(\"start\", true);\n        stop = get(\"stop\", true);\n        executable = get(\"executable\", defaultExecutable);\n        headless = get(\"headless\", false);\n        showProcessLog = get(\"showProcessLog\", false);\n        showBrowserLog = get(\"showBrowserLog\", true);\n        addOptions = get(\"addOptions\", null);\n        uniqueName = type + \"_\" + System.currentTimeMillis();\n        String packageName = getClass().getPackage().getName();\n        processLogger = showProcessLog ? logger : new Logger(packageName + \".\" + uniqueName);\n        showDriverLog = get(\"showDriverLog\", false);\n        driverLogger = showDriverLog ? logger : new Logger(packageName + \".\" + uniqueName);\n        if (executable != null) {\n            if (executable.startsWith(\".\")) { // honor path even when we set working dir\n                args.add(new File(executable).getAbsolutePath());\n            } else {\n                args.add(executable);\n            }\n        }\n        userAgent = get(\"userAgent\", null);\n        if (options.containsKey(\"userDataDir\")) {\n            String temp = get(\"userDataDir\", null);\n            if (temp != null) {\n                workingDir = new File(temp);\n                userDataDir = workingDir.getAbsolutePath();\n            } else { // special case allow user-specified null\n                userDataDir = null;\n                workingDir = null;\n            }\n        } else {\n            workingDir = new File(sr.featureRuntime.suite.buildDir + File.separator + uniqueName);\n            userDataDir = workingDir.getAbsolutePath();\n        }\n        if (workingDir == null) {\n            processLogFile = sr.featureRuntime.suite.buildDir + File.separator + uniqueName + \".log\";\n        } else {\n            processLogFile = workingDir.getPath() + File.separator + type + \".log\";\n        }\n        maxPayloadSize = get(\"maxPayloadSize\", 4194304);\n        target = get(\"target\", null);\n        host = get(\"host\", \"localhost\");\n        webDriverUrl = get(\"webDriverUrl\", null);\n        webDriverPath = get(\"webDriverPath\", null);\n        webDriverSession = get(\"webDriverSession\", null);\n        httpConfig = get(\"httpConfig\", null);\n        remoteHost = get(\"remoteHost\", false);\n        useDockerHost = get(\"useDockerHost\", false);\n        beforeStart = get(\"beforeStart\", null);\n        afterStop = get(\"afterStop\", null);\n        videoFile = get(\"videoFile\", null);\n        pollAttempts = get(\"pollAttempts\", 20);\n        pollInterval = get(\"pollInterval\", 250);\n        highlight = get(\"highlight\", false);\n        highlightDuration = get(\"highlightDuration\", Config.DEFAULT_HIGHLIGHT_DURATION);\n        attach = get(\"attach\", null);\n        screenshotOnFailure = get(\"screenshotOnFailure\", true);\n        playwrightUrl = get(\"playwrightUrl\", null);\n        playwrightOptions = get(\"playwrightOptions\", null);\n        // do this last to ensure things like logger, start-flag, webDriverUrl etc. are set\n        port = resolvePort(defaultPort);\n    }\n\n    private int resolvePort(int defaultPort) {\n        if (webDriverUrl != null) {\n            return 0;\n        }\n        int preferredPort = get(\"port\", defaultPort);\n        if (start) {\n            int freePort = Command.getFreePort(preferredPort);\n            if (freePort != preferredPort) {\n                logger.warn(\"preferred port {} not available, will use: {}\", preferredPort, freePort);\n            }\n            return freePort;\n        }\n        return preferredPort;\n    }\n\n    public Http getHttp() {\n        Http http = Http.to(getUrlBase());\n        if (this.isRemoteHost()) {\n            http.header(\"HOST\", \"localhost\");\n        }\n        http.setAppender(driverLogger.getAppender());\n        if (httpConfig != null) {\n            http.configure(httpConfig);\n        }\n        return http;\n    }\n\n    private String getUrlBase() {\n        if (webDriverUrl != null) {\n            return webDriverUrl;\n        }\n        String urlBase = \"http://\" + host + \":\" + port;\n        if (webDriverPath != null) {\n            return urlBase + webDriverPath;\n        }\n        return urlBase;\n    }\n\n    public void arg(String arg) {\n        args.add(arg);\n    }\n\n    public Command startProcess() {\n        return startProcess(null);\n    }\n\n    public Command startProcess(Consumer<String> listener) {\n        if (beforeStart != null) {\n            Command.execLine(null, beforeStart);\n        }\n        Command command;\n        if (target != null || !start) {\n            command = null;\n        } else {\n            if (addOptions != null) {\n                args.addAll(addOptions);\n            }\n            command = new Command(false, processLogger, uniqueName, processLogFile, workingDir, args.toArray(new String[args.size()]));\n            if (listener != null) {\n                command.setListener(listener);\n            }\n            command.setPollAttempts(pollAttempts);\n            command.setPollInterval(pollInterval);\n            command.start();\n        }\n        if (command != null) { // wait for a slow booting browser / driver process\n            command.waitForPort(host, port);\n            if (command.isFailed()) {\n                throw new KarateException(\"start failed\", command.getFailureReason());\n            }\n        }\n        return command;\n    }\n\n    public static Driver start(Map<String, Object> options, ScenarioRuntime sr) { // TODO unify logger\n        Target target = (Target) options.get(\"target\");\n        if (target != null) {\n            sr.logger.debug(\"custom target configured, calling start()\");\n            Map<String, Object> map = target.start(sr);\n            sr.logger.trace(\"custom target returned options: {}\", map);\n            options.putAll(map);\n        }\n        String type = (String) options.get(\"type\");\n        if (type == null) {\n            sr.logger.warn(\"type was null, defaulting to 'chrome'\");\n            type = Chrome.DRIVER_TYPE;\n            options.put(\"type\", type);\n        }\n        try { // to make troubleshooting errors easier\n            Map<String, DriverRunner> drivers = sr.featureRuntime.suite.drivers;\n            if (drivers == null) { // java api, so dummy suite\n                drivers = DriverOptions.driverRunners();\n            }\n            DriverRunner driverRunner = drivers.get(type);\n            if (driverRunner == null) {\n                sr.logger.warn(\"unknown driver type: {}, defaulting to 'chrome'\", type);\n                options.put(\"type\", Chrome.DRIVER_TYPE);\n                driverRunner = sr.featureRuntime.suite.drivers.get(Chrome.DRIVER_TYPE);\n            }\n            return driverRunner.start(options, sr);\n        } catch (Exception e) {\n            String message = \"driver config / start failed: \" + e.getMessage() + \", options: \" + options;\n            sr.logger.error(message, e);\n            if (target != null) {\n                target.stop(sr);\n            }\n            throw new RuntimeException(message, e);\n        }\n    }\n\n    public static Map<String, DriverRunner> driverRunners() {\n        Map<String, DriverRunner> driverRunners = new HashMap<>();\n        driverRunners.put(Chrome.DRIVER_TYPE, Chrome::start);\n        driverRunners.put(EdgeChromium.DRIVER_TYPE, EdgeChromium::start);\n        driverRunners.put(ChromeWebDriver.DRIVER_TYPE, ChromeWebDriver::start);\n        driverRunners.put(GeckoWebDriver.DRIVER_TYPE, GeckoWebDriver::start);\n        driverRunners.put(SafariWebDriver.DRIVER_TYPE, SafariWebDriver::start);\n        driverRunners.put(MsEdgeDriver.DRIVER_TYPE, MsEdgeDriver::start);\n        driverRunners.put(MsWebDriver.DRIVER_TYPE, MsWebDriver::start);\n        driverRunners.put(IeWebDriver.DRIVER_TYPE, IeWebDriver::start);\n        driverRunners.put(WinAppDriver.DRIVER_TYPE, WinAppDriver::start);\n        driverRunners.put(AndroidDriver.DRIVER_TYPE, AndroidDriver::start);\n        driverRunners.put(IosDriver.DRIVER_TYPE, IosDriver::start);\n        driverRunners.put(PlaywrightDriver.DRIVER_TYPE, PlaywrightDriver::start);\n        return driverRunners;\n    }\n\n    private Map<String, Object> getSession(String browserName) {\n        Map<String, Object> session = webDriverSession;\n        if (session == null) {\n            session = new HashMap();\n        }\n        Map<String, Object> capabilities = (Map) session.get(\"capabilities\");\n        if (capabilities == null) {\n            capabilities = (Map) session.get(\"desiredCapabilities\");\n        }\n        if (capabilities == null) {\n            capabilities = new HashMap();\n            session.put(\"capabilities\", capabilities);\n            Map<String, Object> alwaysMatch = new HashMap();\n            capabilities.put(\"alwaysMatch\", alwaysMatch);\n            alwaysMatch.put(\"browserName\", browserName);\n        }\n        return session;\n    }\n\n    // TODO abstract as method per implementation\n    public Map<String, Object> getWebDriverSessionPayload() {\n        switch (type) {\n            case ChromeWebDriver.DRIVER_TYPE:\n                return getSession(\"chrome\");\n            case GeckoWebDriver.DRIVER_TYPE:\n                return getSession(\"firefox\");\n            case SafariWebDriver.DRIVER_TYPE:\n                return getSession(\"safari\");\n            case MsEdgeDriver.DRIVER_TYPE:\n            case MsWebDriver.DRIVER_TYPE:\n                return getSession(\"edge\");\n            case IeWebDriver.DRIVER_TYPE:\n                return getSession(\"internet explorer\");\n            default:\n                // else user has to specify full payload via webDriverSession\n                return getSession(type);\n        }\n    }\n\n    public static String preProcessWildCard(String locator) {\n        boolean contains;\n        String tag, prefix, text;\n        int index;\n        int pos = locator.indexOf('}');\n        if (pos == -1) {\n            throw new RuntimeException(\"bad locator prefix: \" + locator);\n        }\n        if (locator.charAt(1) == '^') {\n            contains = true;\n            prefix = locator.substring(2, pos);\n        } else {\n            contains = false;\n            prefix = locator.substring(1, pos);\n        }\n        text = locator.substring(pos + 1);\n        pos = prefix.indexOf(':');\n        if (pos != -1) {\n            String tagTemp = prefix.substring(0, pos);\n            tag = tagTemp.isEmpty() ? \"*\" : tagTemp;\n            String indexTemp = prefix.substring(pos + 1);\n            if (indexTemp.isEmpty()) {\n                index = 0;\n            } else {\n                try {\n                    index = Integer.valueOf(indexTemp);\n                } catch (Exception e) {\n                    throw new RuntimeException(\"bad locator prefix: \" + locator + \", \" + e.getMessage());\n                }\n            }\n        } else {\n            tag = prefix.isEmpty() ? \"*\" : prefix;\n            index = 0;\n        }\n        if (!tag.startsWith(\"/\")) {\n            tag = \"//\" + tag;\n        }\n        String xpath;\n        if (contains) {\n            xpath = tag + \"[contains(normalize-space(text()),'\" + text + \"')]\";\n        } else {\n            xpath = tag + \"[normalize-space(text())='\" + text + \"']\";\n        }\n        if (index == 0) {\n            return xpath;\n        }\n        return \"/(\" + xpath + \")[\" + index + \"]\";\n    }\n\n    private static final String DOCUMENT = \"document\";\n\n    public static String selector(String locator) {\n        return selector(locator, DOCUMENT);\n    }\n\n    public static String selector(String locator, String contextNode) {\n        if (locator.startsWith(\"(\") && !locator.startsWith(\"(//\")) {\n            return locator; // pure js !\n        }\n        if (locator.startsWith(\"{\")) {\n            locator = preProcessWildCard(locator);\n        }\n        if (locator.startsWith(\"/\") || locator.startsWith(\"./\") || locator.startsWith(\"../\") || locator.startsWith(\"(//\"))  { // XPathResult.FIRST_ORDERED_NODE_TYPE = 9\n            if (locator.startsWith(\"/(\")) { // hack for wildcard with index (see preProcessWildCard last line)\n                if (DOCUMENT.equals(contextNode)) {\n                    locator = locator.substring(1);\n                } else {\n                    locator = \"(.\" + locator.substring(2);\n                }\n            } else if (!DOCUMENT.equals(contextNode) && !locator.startsWith(\".\")) {\n                locator = \".\" + locator; // evaluate relative to this node not root\n            }\n            return \"document.evaluate(\\\"\" + locator + \"\\\", \" + contextNode + \", null, 9, null).singleNodeValue\";\n        }\n        return contextNode + \".querySelector(\\\"\" + locator + \"\\\")\";\n    }\n\n    public void setTimeout(Integer timeout) {\n        this.timeoutOverride = timeout;\n    }\n\n    public int getTimeout() {\n        if (timeoutOverride != null) {\n            return timeoutOverride;\n        }\n        return timeout;\n    }\n\n    public void setRetryInterval(Integer retryInterval) {\n        this.retryInterval = retryInterval;\n    }\n\n    public int getRetryInterval() {\n        if (retryInterval != null) {\n            return retryInterval;\n        }\n        ScenarioEngine engine = ScenarioEngine.get();\n        if (engine == null) {\n            return Config.DEFAULT_RETRY_INTERVAL;\n        } else {\n            return engine.getConfig().getRetryInterval();\n        }\n    }\n\n    public int getRetryCount() {\n        if (retryCount != null) {\n            return retryCount;\n        }\n        ScenarioEngine engine = ScenarioEngine.get();\n        if (engine == null) {\n            return Config.DEFAULT_RETRY_COUNT;\n        } else {\n            return ScenarioEngine.get().getConfig().getRetryCount();\n        }\n    }\n\n    public <T> T retry(Supplier<T> action, Predicate<T> condition, String logDescription, boolean failWithException) {\n        long startTime = System.currentTimeMillis();\n        int count = 0, max = getRetryCount();\n        T result;\n        boolean success;\n        do {\n            if (count > 0) {\n                logger.debug(\"{} - retry #{}\", logDescription, count);\n                sleep();\n            }\n            result = action.get();\n            success = condition.test(result);\n        } while (!success && count++ < max);\n        if (!success) {\n            long elapsedTime = System.currentTimeMillis() - startTime;\n            String message = logDescription + \": failed after \" + (count - 1) + \" retries and \" + elapsedTime + \" milliseconds\";\n            logger.warn(message);\n            if (failWithException) {\n                throw new RuntimeException(message);\n            }\n        }\n        return result;\n    }\n\n    public static String wrapInFunctionInvoke(String text) {\n        return \"(function(){ \" + text + \" })()\";\n    }\n\n    private static final String HIGHLIGHT_FN = \"function(e){ var old = e.getAttribute('style');\"\n            + \" e.setAttribute('style', 'background: yellow; border: 2px solid red;');\"\n            + \" setTimeout(function(){ e.setAttribute('style', old) }, %d) }\";\n\n    private static String highlightFn(int millis) {\n        return String.format(HIGHLIGHT_FN, millis);\n    }\n\n    public String highlight(String locator, int millis) {\n        String e = selector(locator);\n        String temp = \"var e = \" + e + \"; var fun = \" + highlightFn(millis) + \"; fun(e)\";\n        return wrapInFunctionInvoke(temp);\n    }\n\n    public String highlightAll(String locator, int millis) {\n        return scriptAllSelector(locator, highlightFn(millis));\n    }\n\n    public String optionSelector(String locator, String text) {\n        boolean textEquals = text.startsWith(\"{}\");\n        boolean textContains = text.startsWith(\"{^}\");\n        String condition;\n        if (textEquals || textContains) {\n            text = text.substring(text.indexOf('}') + 1);\n            condition = textContains ? \"e.options[i].text.indexOf(t) !== -1\" : \"e.options[i].text === t\";\n        } else {\n            condition = \"e.options[i].value === t\";\n        }\n        String e = selector(locator);\n        String temp = \"var e = \" + e + \"; var t = \\\"\" + text + \"\\\";\"\n                + \" for (var i = 0; i < e.options.length; ++i)\"\n                + \" if (\" + condition + \") { e.options[i].selected = true; e.dispatchEvent(new Event('change')) }\";\n        return wrapInFunctionInvoke(temp);\n    }\n\n    public String optionSelector(String id, int index) {\n        String e = selector(id);\n        String temp = \"var e = \" + e + \"; var t = \" + index + \";\"\n                + \" for (var i = 0; i < e.options.length; ++i)\"\n                + \" if (i === t) { e.options[i].selected = true; e.dispatchEvent(new Event('change')) }\";\n        return wrapInFunctionInvoke(temp);\n    }\n\n    private String fun(String expression) {\n        char first = expression.charAt(0);\n        switch (first) {\n            case '_':\n                if (expression.contains(\"=>\")) {\n                    return expression;\n                }\n            case '!':\n                return \"function(_){ return \" + expression + \" }\";\n            default:\n                return expression;\n        }\n    }\n\n    public String scriptSelector(String locator, String expression) {\n        return scriptSelector(locator, expression, DOCUMENT);\n    }\n\n    public String scriptSelector(String locator, String expression, String contextNode) {\n        String temp = \"var fun = \" + fun(expression) + \"; var e = \" + selector(locator, contextNode) + \"; return fun(e)\";\n        return wrapInFunctionInvoke(temp);\n    }\n\n    public String scriptAllSelector(String locator, String expression) {\n        return scriptAllSelector(locator, expression, DOCUMENT);\n    }\n\n    // the difference here from selector() is the use of querySelectorAll()\n    // how the loop for XPath results has to be handled\n    public String scriptAllSelector(String locator, String expression, String contextNode) {\n        if (locator.startsWith(\"{\")) {\n            locator = preProcessWildCard(locator);\n        }\n        boolean isXpath = locator.startsWith(\"/\") || locator.startsWith(\"./\") || locator.startsWith(\"../\") || locator.startsWith(\"(//\");\n        String selector;\n        if (isXpath) { // XPathResult.ORDERED_NODE_ITERATOR_TYPE = 5\n            selector = \"document.evaluate(\\\"\" + locator + \"\\\", \" + contextNode + \", null, 5, null)\";\n        } else {\n            selector = contextNode + \".querySelectorAll(\\\"\" + locator + \"\\\")\";\n        }\n        String temp = \"var res = []; var fun = \" + fun(expression) + \"; var es = \" + selector + \"; \";\n        if (isXpath) {\n            temp = temp + \"var e = null; while(e = es.iterateNext()) res.push(fun(e)); return res\";\n        } else {\n            temp = temp + \"es.forEach(function(e){ res.push(fun(e)) }); return res\";\n        }\n        return wrapInFunctionInvoke(temp);\n    }\n\n    public void sleep() {\n        sleep(getRetryInterval());\n    }\n\n    public void sleep(int millis) {\n        if (millis == 0) {\n            return;\n        }\n        try {\n            processLogger.trace(\"sleeping for millis: {}\", millis);\n            Thread.sleep(millis);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static String getRelativePositionJs(String locator) {\n        String temp = \"var r = \" + selector(locator, DOCUMENT)\n                + \".getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height }\";\n        return wrapInFunctionInvoke(temp);\n    }\n\n    public static String getPositionJs(String locator) {\n        String temp = \"var r = \" + selector(locator, DOCUMENT) + \".getBoundingClientRect();\"\n                + \" var dx = window.scrollX; var dy = window.scrollY;\"\n                + \" return { x: r.x + dx, y: r.y + dy, width: r.width + dx, height: r.height + dy }\";\n        return wrapInFunctionInvoke(temp);\n    }\n\n    public Map<String, Object> newMapWithSelectedKeys(Map<String, Object> map, String... keys) {\n        Map<String, Object> out = new HashMap(keys.length);\n        for (String key : keys) {\n            Object o = map.get(key);\n            if (o != null) {\n                out.put(key, o);\n            }\n        }\n        return out;\n    }\n\n    public void disableRetry() {\n        retryEnabled = false;\n        retryCount = null;\n        retryInterval = null;\n    }\n\n    public void enableRetry(Integer count, Integer interval) {\n        retryEnabled = true;\n        retryCount = count; // can be null\n        retryInterval = interval; // can be null\n    }\n\n    public Element waitUntil(Driver driver, String locator, String expression) {\n        long startTime = System.currentTimeMillis();\n        String js = scriptSelector(locator, expression);\n        boolean found = driver.waitUntil(js);\n        if (!found) {\n            long elapsedTime = System.currentTimeMillis() - startTime;\n            throw new RuntimeException(\"wait failed for: \" + locator\n                    + \" and condition: \" + expression + \" after \" + elapsedTime + \" milliseconds\");\n        }\n        return DriverElement.locatorExists(driver, locator);\n    }\n\n    public String waitForUrl(Driver driver, String expected) {\n        return retry(() -> driver.getUrl(), url -> url.contains(expected), \"waitForUrl\", true);\n    }\n\n    public Element waitForAny(Driver driver, String... locators) {\n        long startTime = System.currentTimeMillis();\n        List<String> list = Arrays.asList(locators);\n        Iterator<String> iterator = list.iterator();\n        StringBuilder sb = new StringBuilder();\n        while (iterator.hasNext()) {\n            String locator = iterator.next();\n            String js = selector(locator);\n            sb.append(\"(\").append(js).append(\" != null)\");\n            if (iterator.hasNext()) {\n                sb.append(\" || \");\n            }\n        }\n        boolean found = driver.waitUntil(sb.toString());\n        // important: un-set the retry flag        \n        disableRetry();\n        if (!found) {\n            long elapsedTime = System.currentTimeMillis() - startTime;\n            throw new RuntimeException(\"wait failed for: \" + list + \" after \" + elapsedTime + \" milliseconds\");\n        }\n        if (locators.length == 1) {\n            return DriverElement.locatorExists(driver, locators[0]);\n        }\n        for (String locator : locators) {\n            Element temp = driver.optional(locator);\n            if (temp.isPresent()) {\n                return temp;\n            }\n        }\n        // this should never happen\n        throw new RuntimeException(\"unexpected wait failure for locators: \" + list);\n    }\n\n    public Element optional(Driver driver, String locator) {\n        String js = selector(locator);\n        String evalJs = js + \" != null\";\n        Object o = driver.script(evalJs);\n        if (o instanceof Boolean && (Boolean) o) {\n            return DriverElement.locatorExists(driver, locator);\n        } else {\n            return new MissingElement(driver, locator);\n        }\n    }\n\n    public static String karateLocator(String karateRef) {\n        return \"(document._karate.\" + karateRef + \")\";\n    }\n\n    public String focusJs(String locator) {\n        return \"var e = \" + selector(locator) + \"; e.focus(); try { e.selectionStart = e.selectionEnd = e.value.length } catch(x) {}\";\n    }\n\n    public List<Element> findAll(Driver driver, String locator) {\n        List<String> list = driver.scriptAll(locator, DriverOptions.KARATE_REF_GENERATOR);\n        List<Element> elements = new ArrayList(list.size());\n        for (String karateRef : list) {\n            String karateLocator = karateLocator(karateRef);\n            elements.add(DriverElement.locatorExists(driver, karateLocator));\n        }\n        return elements;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/DriverRunner.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport com.intuit.karate.core.ScenarioRuntime;\n\nimport java.util.Map;\n\n@FunctionalInterface\npublic interface DriverRunner<D extends Driver> {\n    \n    D start(Map<String, Object> options, ScenarioRuntime sr);\n    \n}"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/Element.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Element {\n\n    String getLocator(); // getter\n\n    boolean isPresent(); // getter\n\n    boolean isEnabled(); // getter\n    \n    Map<String, Object> getPosition(); // getter\n    \n    byte[] screenshot();\n\n    Element highlight();\n\n    Element focus();\n\n    Element clear();\n\n    Element click();\n\n    Element submit();\n    \n    Element scroll();\n\n    Mouse mouse();\n\n    Element input(String value);\n\n    Element input(String[] values);\n\n    Element input(String[] values, int delay);\n\n    Element select(String text);\n\n    Element select(int index);\n\n    Element switchFrame();\n\n    Element delay(int millis);\n\n    Element retry();\n\n    Element retry(int count);\n\n    Element retry(Integer count, Integer interval);\n\n    Element waitFor();\n\n    Element waitUntil(String expression);\n\n    Element waitForText(String text);\n\n    Object script(String expression);\n    \n    Object scriptAll(String locator, String expression);\n    \n    Element optional(String locator);\n    \n    boolean exists(String locator);\n    \n    Element locate(String locator);\n    \n    List<Element> locateAll(String locator);\n\n    String getHtml(); // getter\n\n    void setHtml(String html); // setter\n\n    String getText(); // getter\n\n    void setText(String text); // setter    \n\n    String getValue(); // getter\n\n    void setValue(String value); // setter\n    \n    String attribute(String name);\n    \n    String property(String name);\n    \n    Element getParent(); // getter\n    \n    Element getFirstChild(); // getter\n            \n    Element getLastChild(); // getter\n    \n    Element getPreviousSibling(); // getter\n    \n    Element getNextSibling(); // getter\n    \n    List<Element> getChildren();\n    \n    Finder rightOf();\n    \n    Finder leftOf();\n    \n    Finder above();\n    \n    Finder below();\n    \n    Finder near();\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/ElementFinder.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport com.intuit.karate.StringUtils;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class ElementFinder implements Finder {\n\n    public static enum Type {\n        RIGHT,\n        LEFT,\n        ABOVE,\n        BELOW,\n        NEAR\n    }\n\n    private final Driver driver;\n    private final String fromLocator;\n    private final Type type;\n\n    private String tag = \"INPUT\";\n\n    public ElementFinder(Driver driver, String fromLocator, Type type) {\n        this.driver = driver;\n        this.fromLocator = fromLocator;\n        this.type = type;\n    }\n\n    private static String forLoopChunk(ElementFinder.Type type) {\n        switch (type) {\n            case RIGHT:\n                return \"x += s;\";\n            case BELOW:\n                return \"y += s;\";\n            case LEFT:\n                return \"x -= s;\";\n            case ABOVE:\n                return \"y -= s;\";\n            default: // NEAR\n                return \" var a = 0.381966 * i; var x = (s + a) * Math.cos(a); var y = (s + a) * Math.sin(a);\";\n        }\n    }\n\n    public static String exitCondition(String findTag) {\n        int pos = findTag.indexOf('}');\n        if (pos == -1) {\n            return \"e.tagName == '\" + findTag.toUpperCase() + \"'\";\n        }\n        int caretPos = findTag.indexOf('^');\n        boolean contains = caretPos != -1 && caretPos < pos;\n        if (!contains) {\n            caretPos = 0;\n        }\n        String tagName = StringUtils.trimToNull(findTag.substring(caretPos + 1, pos));\n        String suffix = tagName == null ? \"\" : \" && e.tagName == '\" + tagName.toUpperCase() + \"'\";\n        String findText = findTag.substring(pos + 1);\n        if (contains) {\n            return \"e.textContent.trim().includes('\" + findText + \"')\" + suffix;\n        } else {\n            return \"e.textContent.trim() == '\" + findText + \"'\" + suffix;\n        }\n    }\n\n    private static String findScript(Driver driver, String locator, ElementFinder.Type type, String findTag) {\n        Map<String, Object> pos = driver.position(locator, true);\n        Number xNum = (Number) pos.get(\"x\");\n        Number yNum = (Number) pos.get(\"y\");\n        Number width = (Number) pos.get(\"width\");\n        Number height = (Number) pos.get(\"height\");\n        // get center point\n        int x = xNum.intValue() + width.intValue() / 2;\n        int y = yNum.intValue() + height.intValue() / 2;\n        // o: origin, a: angle, s: step\n        String fun = \"var gen = \" + DriverOptions.KARATE_REF_GENERATOR + \";\"\n                + \" var o = { x: \" + x + \", y: \" + y + \"}; var s = 10; var x = 0; var y = 0;\"\n                + \" for (var i = 0; i < 200; i++) {\"\n                + forLoopChunk(type)\n                + \" var e = document.elementFromPoint(o.x + x, o.y + y);\"\n                // + \" console.log(o.x +':' + o.y + ' ' + x + ':' + y + ' ' + e.tagName + ':' + e.textContent);\"\n                + \" if (e && \" + exitCondition(findTag) + \") return gen(e); \"\n                + \" } return null\";\n        return DriverOptions.wrapInFunctionInvoke(fun);\n    }\n\n    private String getDebugString() {\n        return fromLocator + \", \" + type + \", \" + tag;\n    }\n\n    @Override\n    public Element find() {\n        String js = findScript(driver, fromLocator, type, tag);\n        String karateRef = (String) driver.script(js);\n        if (karateRef == null) {\n            throw new RuntimeException(\"unable to find: \" + getDebugString());\n        }\n        return DriverElement.locatorExists(driver, DriverOptions.karateLocator(karateRef));\n    }\n\n    @Override\n    public Element find(String tag) {\n        this.tag = tag;\n        return find();\n    }\n\n    @Override\n    public String getValue() {\n        return find().getValue();\n    }\n\n    @Override\n    public Element clear() {\n        return find().clear();\n    }\n\n    @Override\n    public Element input(String value) {\n        return find().input(value);\n    }\n\n    @Override\n    public Element select(String value) {\n        return find(\"select\").select(value);\n    }\n\n    @Override\n    public Element select(int index) {\n        return find(\"select\").select(index);\n    }\n\n    @Override\n    public Element click() {\n        return find().click();\n    }\n\n    @Override\n    public Element highlight() {\n        return find().highlight();\n    }\n\n    @Override\n    public Element retry() {\n        return find().retry();\n    }\n\n    @Override\n    public Element retry(int count) {\n        return find().retry(count);\n    }\n\n    @Override\n    public Element retry(Integer count, Integer interval) {\n        return find().retry(count, interval);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/Finder.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Finder {\n\n    Element input(String value);\n    \n    Element select(String value);\n    \n    Element select(int index);\n\n    Element click();\n    \n    String getValue();\n\n    Element clear();\n\n    Element find();\n\n    Element find(String tag);\n    \n    Element highlight();\n    \n    Element retry();\n    \n    Element retry(int count);\n    \n    Element retry(Integer count, Integer interval);\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/Frame.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\n/**\n *\n * @author pthomas3\n */\npublic class Frame {\n\n    public final String id;\n    public final String url;\n    public final String name;\n\n    public Frame(String id, String url, String name) {\n        this.id = id;\n        this.url = url;\n        this.name = name;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"[id: \").append(id);\n        sb.append(\", url: \").append(url);\n        sb.append(\", name: \").append(name);\n        sb.append(\"]\");\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/Input.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class Input {\n\n    protected boolean control;\n    protected boolean alt;\n    protected boolean shift;\n    protected boolean meta;\n\n    protected boolean release;\n\n    private int pos = 0;\n\n    public final char[] chars;\n\n    public Input(String chars) {\n        this.chars = chars.toCharArray();\n    }\n\n    public boolean hasNext() {\n        return pos < chars.length;\n    }\n\n    public List<Integer> getKeyCodesToRelease() {\n        if (control || alt || shift || meta) {\n            List<Integer> list = new ArrayList();\n            if (shift) {\n                list.add(Keys.CODE_SHIFT);\n            }\n            if (control) {\n                list.add(Keys.CODE_CONTROL);\n            }\n            if (alt) {\n                list.add(Keys.CODE_ALT);\n            }\n            if (meta) {\n                list.add(Keys.CODE_META);\n            }\n            return list;\n        } else {\n            return Collections.emptyList();\n        }\n    }\n\n    private void updateModifiers(char c) {\n        switch (c) {\n            case Keys.CONTROL:\n                release = control;\n                control = !control;\n                break;\n            case Keys.ALT:\n                release = alt;\n                alt = !alt;\n                break;\n            case Keys.SHIFT:\n                release = shift;\n                shift = !shift;\n                break;\n            case Keys.META:\n                release = meta;\n                meta = !meta;\n                break;\n            default:\n                break;\n        }\n    }\n\n    public char next() {\n        char c = chars[pos++];\n        updateModifiers(c);\n        return c;\n    }\n\n    public int getModifierFlags() {\n        int modifier = 0;\n        if (control) {\n            modifier += 2;\n        }\n        if (alt) {\n            modifier += 1;\n        }\n        if (shift) {\n            modifier += 8;\n        }\n        if (meta) {\n            modifier += 4;\n        }\n        return modifier;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/Key.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\n/**\n *\n * @author pthomas3\n */\npublic class Key {\n\n    public static final Key INSTANCE = new Key();\n\n    public final char NULL = Keys.NULL;\n    public final char CANCEL = Keys.CANCEL;\n    public final char HELP = Keys.HELP;\n    public final char BACK_SPACE = Keys.BACK_SPACE;\n    public final char TAB = Keys.TAB;\n    public final char CLEAR = Keys.CLEAR;\n    public final char RETURN = Keys.RETURN;\n    public final char ENTER = Keys.ENTER;\n    public final char SHIFT = Keys.SHIFT;\n    public final char CONTROL = Keys.CONTROL;\n    public final char ALT = Keys.ALT;\n    public final char PAUSE = Keys.PAUSE;\n    public final char ESCAPE = Keys.ESCAPE;\n    public final char SPACE = Keys.SPACE;\n    public final char PAGE_UP = Keys.PAGE_UP;\n    public final char PAGE_DOWN = Keys.PAGE_DOWN;\n    public final char END = Keys.END;\n    public final char HOME = Keys.HOME;\n    public final char LEFT = Keys.LEFT;\n    public final char UP = Keys.UP;\n    public final char RIGHT = Keys.RIGHT;\n    public final char DOWN = Keys.DOWN;\n    public final char INSERT = Keys.INSERT;\n    public final char DELETE = Keys.DELETE;\n    public final char SEMICOLON = Keys.SEMICOLON;\n    public final char EQUALS = Keys.EQUALS;\n\n    // numpad keys\n    public final char NUMPAD0 = Keys.NUMPAD0;\n    public final char NUMPAD1 = Keys.NUMPAD1;\n    public final char NUMPAD2 = Keys.NUMPAD2;\n    public final char NUMPAD3 = Keys.NUMPAD3;\n    public final char NUMPAD4 = Keys.NUMPAD4;\n    public final char NUMPAD5 = Keys.NUMPAD5;\n    public final char NUMPAD6 = Keys.NUMPAD6;\n    public final char NUMPAD7 = Keys.NUMPAD7;\n    public final char NUMPAD8 = Keys.NUMPAD8;\n    public final char NUMPAD9 = Keys.NUMPAD9;\n    public final char MULTIPLY = Keys.MULTIPLY;\n    public final char ADD = Keys.ADD;\n    public final char SEPARATOR = Keys.SEPARATOR;\n    public final char SUBTRACT = Keys.SUBTRACT;\n    public final char DECIMAL = Keys.DECIMAL;\n    public final char DIVIDE = Keys.DIVIDE;\n\n    // function keys\n    public final char F1 = Keys.F1;\n    public final char F2 = Keys.F2;\n    public final char F3 = Keys.F3;\n    public final char F4 = Keys.F4;\n    public final char F5 = Keys.F5;\n    public final char F6 = Keys.F6;\n    public final char F7 = Keys.F7;\n    public final char F8 = Keys.F8;\n    public final char F9 = Keys.F9;\n    public final char F10 = Keys.F10;\n    public final char F11 = Keys.F11;\n    public final char F12 = Keys.F12;\n    public final char META = Keys.META;\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/Keys.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class Keys {\n\n    private final Driver driver;\n\n    public Keys(Driver driver) {\n        this.driver = driver;\n    }\n\n    public static Integer code(char c) {\n        return CODES.get(c);\n    }\n    \n    public static String keyValue(char c) {\n        return VALUES.get(c);\n    }\n \n    public static boolean isNormal(char c) {\n        return c < NULL;\n    }\n    \n    public static boolean isModifier(char c) {\n        switch(c) {\n            case CONTROL:\n            case ALT:\n            case SHIFT:\n            case META:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    public static String keyIdentifier(char c) {\n        return \"\\\\u\" + Integer.toHexString(c | 0x10000).substring(1);\n    }\n\n    public static final char NULL = '\\uE000';\n    public static final char CANCEL = '\\uE001';\n    public static final char HELP = '\\uE002';\n    public static final char BACK_SPACE = '\\uE003';\n    public static final char TAB = '\\uE004';\n    public static final char CLEAR = '\\uE005';\n    public static final char RETURN = '\\uE006';\n    public static final char ENTER = '\\uE007';\n    public static final char SHIFT = '\\uE008';\n    public static final char CONTROL = '\\uE009';\n    public static final char ALT = '\\uE00A';\n    public static final char PAUSE = '\\uE00B';\n    public static final char ESCAPE = '\\uE00C';\n    public static final char SPACE = '\\uE00D';\n    public static final char PAGE_UP = '\\uE00E';\n    public static final char PAGE_DOWN = '\\uE00F';\n    public static final char END = '\\uE010';\n    public static final char HOME = '\\uE011';\n    public static final char LEFT = '\\uE012';\n    public static final char UP = '\\uE013';\n    public static final char RIGHT = '\\uE014';\n    public static final char DOWN = '\\uE015';\n    public static final char INSERT = '\\uE016';\n    public static final char DELETE = '\\uE017';\n    public static final char SEMICOLON = '\\uE018';\n    public static final char EQUALS = '\\uE019';\n\n    // numpad keys\n    public static final char NUMPAD0 = '\\uE01A';\n    public static final char NUMPAD1 = '\\uE01B';\n    public static final char NUMPAD2 = '\\uE01C';\n    public static final char NUMPAD3 = '\\uE01D';\n    public static final char NUMPAD4 = '\\uE01E';\n    public static final char NUMPAD5 = '\\uE01F';\n    public static final char NUMPAD6 = '\\uE020';\n    public static final char NUMPAD7 = '\\uE021';\n    public static final char NUMPAD8 = '\\uE022';\n    public static final char NUMPAD9 = '\\uE023';\n    public static final char MULTIPLY = '\\uE024';\n    public static final char ADD = '\\uE025';\n    public static final char SEPARATOR = '\\uE026';\n    public static final char SUBTRACT = '\\uE027';\n    public static final char DECIMAL = '\\uE028';\n    public static final char DIVIDE = '\\uE029';\n\n    // function keys\n    public static final char F1 = '\\uE031';\n    public static final char F2 = '\\uE032';\n    public static final char F3 = '\\uE033';\n    public static final char F4 = '\\uE034';\n    public static final char F5 = '\\uE035';\n    public static final char F6 = '\\uE036';\n    public static final char F7 = '\\uE037';\n    public static final char F8 = '\\uE038';\n    public static final char F9 = '\\uE039';\n    public static final char F10 = '\\uE03A';\n    public static final char F11 = '\\uE03B';\n    public static final char F12 = '\\uE03C';\n    public static final char META = '\\uE03D';\n\n    private static final Map<Character, Integer> CODES = new HashMap();\n    private static final Map<Character, String> VALUES = new HashMap();\n    \n    private static void put(char c, int code, String value) {\n        CODES.put(c, code);\n        VALUES.put(c, value);\n    }\n    \n    public static final int CODE_SHIFT = 16;\n    public static final int CODE_CONTROL = 17;\n    public static final int CODE_ALT = 18;\n    public static final int CODE_META = 91; // left command key on mac, right is 93\n\n    static {\n        put(CANCEL, 3, \"Cancel\");\n        put(BACK_SPACE, 8, \"Backspace\");\n        put(TAB, 9, \"Tab\");\n        put(CLEAR, 12, \"Clear\");\n        put(NULL, 12, \"Clear\"); // same as clear\n        put(RETURN, 13, \"Enter\"); // same as enter\n        put(ENTER, 13, \"Enter\");\n        put(SHIFT, CODE_SHIFT, \"Shift\");\n        put(CONTROL, CODE_CONTROL, \"Control\");\n        put(ALT, CODE_ALT, \"Alt\");\n        put(PAUSE, 19, \"Pause\");\n        put(ESCAPE, 27, \"Escape\");\n        put(SPACE, 32, \" \");\n        put(PAGE_UP, 33, \"PageUp\");\n        put(PAGE_DOWN, 34, \"PageDown\");\n        put(END, 35, \"End\");\n        put(HOME, 36, \"Home\");\n        put(LEFT, 37, \"ArrowLeft\");\n        put(UP, 38, \"ArrowUp\");\n        put(RIGHT, 39, \"ArrowRight\");\n        put(DOWN, 40, \"ArrowDown\");\n        put(NUMPAD0, 96, \"0\");\n        put(NUMPAD1, 97, \"1\");\n        put(NUMPAD2, 98, \"2\");\n        put(NUMPAD3, 99, \"3\");\n        put(NUMPAD4, 100, \"4\");\n        put(NUMPAD5, 101, \"5\");\n        put(NUMPAD6, 102, \"6\");\n        put(NUMPAD7, 103, \"7\");\n        put(NUMPAD8, 104, \"8\");\n        put(NUMPAD9, 105, \"9\");\n        put(MULTIPLY, 106, \"Multiply\");\n        put(ADD, 107, \"Add\");\n        put(SEPARATOR, 108, \"Separator\");\n        put(SUBTRACT, 109, \"Subtract\");\n        put(DECIMAL, 110, \"Decimal\");\n        put(DIVIDE, 111, \"Divide\");\n        put(F1, 112, \"F1\");\n        put(F2, 113, \"F2\");\n        put(F3, 114, \"F3\");\n        put(F4, 115, \"F4\");\n        put(F5, 116, \"F5\");\n        put(F6, 117, \"F6\");\n        put(F7, 118, \"F7\");\n        put(F8, 119, \"F8\");\n        put(F9, 120, \"F9\");\n        put(F10, 121, \"F10\");\n        put(F11, 122, \"F11\");\n        put(F12, 123, \"F12\");\n        put(DELETE, 127, \"Delete\");\n        put(INSERT, 155, \"Insert\");\n        put(HELP, 156, \"Help\");\n        put(META, CODE_META, \"Meta\");\n        //======================================================================\n        CODES.put(' ', 32);\n        CODES.put(',', 44);\n        CODES.put('-', 45);\n        CODES.put('.', 46);\n        CODES.put('/', 47);\n        CODES.put('0', 48);\n        CODES.put('1', 49);\n        CODES.put('2', 50);\n        CODES.put('3', 51);\n        CODES.put('4', 52);\n        CODES.put('5', 53);\n        CODES.put('6', 54);\n        CODES.put('7', 55);\n        CODES.put('8', 56);\n        CODES.put('9', 57);\n        CODES.put(';', 59);\n        CODES.put('=', 61);\n        CODES.put('A', 65);\n        CODES.put('B', 66);\n        CODES.put('C', 67);\n        CODES.put('D', 68);\n        CODES.put('E', 69);\n        CODES.put('F', 70);\n        CODES.put('G', 71);\n        CODES.put('H', 72);\n        CODES.put('I', 73);\n        CODES.put('J', 74);\n        CODES.put('K', 75);\n        CODES.put('L', 76);\n        CODES.put('M', 77);\n        CODES.put('N', 78);\n        CODES.put('O', 79);\n        CODES.put('P', 80);\n        CODES.put('Q', 81);\n        CODES.put('R', 82);\n        CODES.put('S', 83);\n        CODES.put('T', 84);\n        CODES.put('U', 85);\n        CODES.put('V', 86);\n        CODES.put('W', 87);\n        CODES.put('X', 88);\n        CODES.put('Y', 89);\n        CODES.put('Z', 90);\n        CODES.put('a', 97);\n        CODES.put('b', 98);\n        CODES.put('c', 99);\n        CODES.put('d', 100);\n        CODES.put('e', 101);\n        CODES.put('f', 102);\n        CODES.put('g', 103);\n        CODES.put('h', 104);\n        CODES.put('i', 105);\n        CODES.put('j', 106);\n        CODES.put('k', 107);\n        CODES.put('l', 108);\n        CODES.put('m', 109);\n        CODES.put('n', 110);\n        CODES.put('o', 111);\n        CODES.put('p', 112);\n        CODES.put('q', 113);\n        CODES.put('r', 114);\n        CODES.put('s', 115);\n        CODES.put('t', 116);\n        CODES.put('u', 117);\n        CODES.put('v', 118);\n        CODES.put('w', 119);\n        CODES.put('x', 120);\n        CODES.put('y', 121);\n        CODES.put('z', 122);\n        CODES.put('&', 150);\n        CODES.put('*', 151);\n        CODES.put('\"', 152);\n        CODES.put('<', 153);\n        CODES.put('>', 160);\n        CODES.put('{', 161);\n        CODES.put('}', 162);\n        CODES.put('~', 171);\n        CODES.put('|', 172);\n        CODES.put('%', 175);\n        CODES.put('^', 176);\n        CODES.put('?', 191);\n        CODES.put('`', 192);\n        CODES.put('[', 219);\n        CODES.put('\\\\', 220);\n        CODES.put(']', 221);\n        CODES.put('\\'', 222);\n        CODES.put('@', 512);\n        CODES.put(':', 513);\n        CODES.put('$', 515);\n        CODES.put('!', 517);\n        CODES.put('(', 519);\n        CODES.put('#', 520);\n        CODES.put('+', 521);\n        CODES.put(')', 522);\n        CODES.put('_', 523);\n        \n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/MissingElement.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class MissingElement implements Element {\n\n    private final Driver driver;\n    private final String locator;\n\n    public MissingElement(Driver driver, String locator) {\n        this.driver = driver;\n        this.locator = locator;\n    }\n\n    @Override\n    public String getLocator() {\n        return locator;\n    }\n\n    @Override\n    public boolean isPresent() {\n        return false;\n    }\n\n    @Override\n    public boolean isEnabled() {\n        return false;\n    }\n\n    @Override\n    public Map<String, Object> getPosition() {\n        return null;\n    }\n\n    @Override\n    public byte[] screenshot() {\n        return null;\n    }\n\n    @Override\n    public Element highlight() {\n        return this;\n    }\n\n    @Override\n    public Element focus() {\n        return this;\n    }\n\n    @Override\n    public Element clear() {\n        return this;\n    }\n\n    @Override\n    public Element click() {\n        return this;\n    }\n\n    @Override\n    public Element submit() {\n        return this;\n    }\n\n    @Override\n    public Element scroll() {\n        return this;\n    }        \n\n    @Override\n    public Mouse mouse() {\n        return null;\n    }\n\n    @Override\n    public Element input(String text) {\n        return this;\n    }\n\n    @Override\n    public Element input(String[] values) {\n        return this;\n    }\n\n    @Override\n    public Element input(String[] values, int delay) {\n        return this;\n    }\n\n    @Override\n    public Element select(String text) {\n        return this;\n    }\n\n    @Override\n    public Element select(int index) {\n        return this;\n    }\n\n    @Override\n    public Element switchFrame() {\n        return this;\n    }\n\n    @Override\n    public Element delay(int millis) {\n        driver.delay(millis);\n        return this;\n    }\n\n    @Override\n    public Element retry() {\n        return this;\n    }\n\n    @Override\n    public Element retry(int count) {\n        return this;\n    }\n\n    @Override\n    public Element retry(Integer count, Integer interval) {\n        return this;\n    }\n\n    @Override\n    public Element waitFor() {\n        return this;\n    }\n\n    @Override\n    public Element waitForText(String text) {\n        return this;\n    }\n\n    @Override\n    public Element waitUntil(String expression) {\n        return this;\n    }\n\n    @Override\n    public Object script(String expression) {\n        return null;\n    }\n    \n    @Override\n    public Object scriptAll(String locator, String expression) {\n        return null;\n    }    \n\n    @Override\n    public Element optional(String locator) {\n        return new MissingElement(driver, locator);\n    }\n\n    @Override\n    public boolean exists(String locator) {\n        return false;\n    }\n\n    @Override\n    public Element locate(String locator) {\n        return new MissingElement(driver, locator);\n    }\n\n    @Override\n    public List<Element> locateAll(String locator) {\n        return Collections.EMPTY_LIST;\n    }\n\n    @Override\n    public String attribute(String name) {\n        return null;\n    }\n\n    @Override\n    public String property(String name) {\n        return null;\n    }\n\n    @Override\n    public String getHtml() {\n        return null;\n    }\n\n    @Override\n    public void setHtml(String html) {\n\n    }\n\n    @Override\n    public String getText() {\n        return null;\n    }\n\n    @Override\n    public void setText(String text) {\n\n    }\n\n    @Override\n    public String getValue() {\n        return null;\n    }\n\n    @Override\n    public void setValue(String value) {\n\n    }\n\n    @Override\n    public Element getParent() {\n        return this;\n    }\n\n    @Override\n    public List<Element> getChildren() {\n        return Collections.EMPTY_LIST;\n    }\n\n    @Override\n    public Element getFirstChild() {\n        return this;\n    }\n\n    @Override\n    public Element getLastChild() {\n        return this;\n    }\n\n    @Override\n    public Element getPreviousSibling() {\n        return this;\n    }\n\n    @Override\n    public Element getNextSibling() {\n        return this;\n    }\n\n    @Override\n    public Finder rightOf() {\n        return new MissingFinder(this);\n    }\n\n    @Override\n    public Finder leftOf() {\n        return new MissingFinder(this);\n    }\n\n    @Override\n    public Finder above() {\n        return new MissingFinder(this);\n    }\n\n    @Override\n    public Finder below() {\n        return new MissingFinder(this);\n    }\n\n    @Override\n    public Finder near() {\n        return new MissingFinder(this);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/MissingFinder.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\n/**\n *\n * @author pthomas3\n */\npublic class MissingFinder implements Finder {\n    \n    private final MissingElement element;\n    \n    public MissingFinder(MissingElement element) {\n        this.element = element;\n    }\n\n    @Override\n    public Element input(String value) {\n        return element;\n    }\n\n    @Override\n    public Element select(String value) {\n        return element;\n    }\n\n    @Override\n    public Element select(int index) {\n        return element;\n    }\n\n    @Override\n    public Element click() {\n        return element;\n    }\n\n    @Override\n    public String getValue() {\n        return element.getValue();\n    }        \n\n    @Override\n    public Element clear() {\n        return element;\n    }\n\n    @Override\n    public Element find() {\n        return element;\n    }\n\n    @Override\n    public Element find(String tag) {\n        return element;\n    }\n\n    @Override\n    public Element highlight() {\n        return element;\n    }\n\n    @Override\n    public Element retry() {\n        return element;\n    }\n\n    @Override\n    public Element retry(int count) {\n        return element;\n    }\n\n    @Override\n    public Element retry(Integer count, Integer interval) {\n        return element;\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/Mouse.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Mouse {        \n    \n    Mouse move(String locator);\n    \n    Mouse move(Number x, Number y);\n    \n    Mouse offset(Number x, Number y);\n    \n    Mouse down();\n    \n    Mouse up();\n    \n    Mouse submit();\n    \n    Mouse click();\n    \n    Mouse doubleClick();\n    \n    Mouse go();\n    \n    Mouse duration(Integer duration);\n\n    Mouse pause(Integer duration);\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/Target.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport com.intuit.karate.core.ScenarioRuntime;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Target {        \n    \n    Map<String, Object> start(ScenarioRuntime sr);\n    \n    Map<String, Object> stop(ScenarioRuntime sr);\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/WebDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver;\n\nimport com.intuit.karate.Http;\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.Json;\nimport com.intuit.karate.core.Variable;\nimport com.intuit.karate.http.ResourceType;\nimport com.intuit.karate.http.Response;\nimport com.intuit.karate.shell.Command;\n\nimport java.util.*;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\n/**\n * @author pthomas3\n */\npublic abstract class WebDriver implements Driver {\n\n    protected final DriverOptions options;\n    protected final Command command;\n    protected final Http http;\n    private final String sessionId;\n    private boolean terminated;\n    //private final String windowId;\n\n    protected boolean open = true;\n    protected Boolean specCompliant;\n\n    protected final Logger logger;\n\n    protected WebDriver(DriverOptions options) {\n        this.options = options;\n        this.logger = options.driverLogger;\n        command = options.startProcess();\n        http = options.getHttp();\n        Response response = http.path(\"session\").post(options.getWebDriverSessionPayload());\n        if (response.getStatus() != 200) {\n            String message = \"webdriver session create status \" + response.getStatus() + \", \" + response.getBodyAsString();\n            logger.error(message);\n            if (command != null) {\n                command.close(true);\n            }\n            throw new RuntimeException(message);\n        }\n        sessionId = response.json().getFirst(\"$..sessionId\");\n        logger.debug(\"init session id: {}\", sessionId);\n        http.url(http.urlBase + \"/session/\" + sessionId);\n        if (options.start) {\n            activate();\n        }\n    }\n\n    @Override\n    public Driver timeout(Integer millis) {\n        options.setTimeout(millis);\n        // this will \"reset\" to default if null was set above\n        http.configure(\"readTimeout\", options.getTimeout() + \"\");\n        return this;\n    }\n\n    @Override\n    public Driver timeout() {\n        return timeout(null);\n    }\n\n    public String getSessionId() {\n        return sessionId;\n    }\n\n    // can be used directly if you know what you are doing !\n    public Http getHttp() {\n        return http;\n    }\n\n    private String getSubmitHash() {\n        return getUrl() + elementId(\"html\");\n    }\n\n    protected <T> T retryIfEnabled(String locator, Supplier<T> action) {\n        if (options.isRetryEnabled()) {\n            waitFor(locator); // will throw exception if not found\n        }\n        if (options.highlight) {\n            highlight(locator, options.highlightDuration);\n        }\n        String before = options.getPreSubmitHash();\n        if (before != null) {\n            logger.trace(\"submit requested, will wait for page load after next action on : {}\", locator);\n            options.setPreSubmitHash(null); // clear the submit flag\n            T result = action.get();\n            Integer retryInterval = options.getRetryInterval();\n            options.setRetryInterval(500); // reduce retry interval for this special case\n            options.retry(() -> getSubmitHash(), hash -> !before.equals(hash), \"waiting for document to change\", false);\n            options.setRetryInterval(retryInterval); // restore\n            return result;\n        } else {\n            return action.get();\n        }\n    }\n\n    protected boolean isJavaScriptError(Response res) {\n        return res.getStatus() != 200\n                && !res.json().<String>get(\"value\").contains(\"unexpected alert open\");\n    }\n\n    protected boolean isLocatorError(Response res) {\n        return res.getStatus() != 200;\n    }\n\n    protected boolean isCookieError(Response res) {\n        return res.getStatus() != 200;\n    }\n\n    private Element evalLocator(String locator, String dotExpression) {\n        eval(prefixReturn(DriverOptions.selector(locator) + \".\" + dotExpression));\n        // if the js above did not throw an exception, the element exists\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    private Element evalFocus(String locator) {\n        eval(options.focusJs(locator));\n        // if the js above did not throw an exception, the element exists\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    protected Variable eval(String expression, List args) {\n        Json json = Json.object().set(\"script\", expression).set(\"args\", (args == null) ? Collections.EMPTY_LIST : args);\n        Response res = http.path(\"execute\", \"sync\").post(json);\n        if (isJavaScriptError(res)) {\n            logger.warn(\"javascript failed, will retry once: {}\", res.getBodyAsString());\n            options.sleep();\n            res = http.path(\"execute\", \"sync\").post(json);\n            if (isJavaScriptError(res)) {\n                String message = \"javascript failed twice: \" + res.getBodyAsString();\n                logger.error(message);\n                throw new RuntimeException(message);\n            }\n        }\n        return new Variable(res.json().get(\"value\"));\n    }\n\n    protected Variable eval(String expression) {\n        return eval(expression, null);\n    }\n\n    protected List<String> getElementKeys() {\n        // \"element-6066-11e4-a52e-4f735466cecf\" is the key to element in the W3C WebDriver standard\n        // \"ELEMENT\" is a deviation from the W3C standard\n        // explanation can be found here: https://github.com/karatelabs/karate/issues/1840#issuecomment-974688715\n        return Arrays.asList(\"element-6066-11e4-a52e-4f735466cecf\", \"ELEMENT\");\n    }\n\n    protected String getJsonForInput(String text) {\n        return Json.object().set(\"text\", text).toString();\n    }\n\n    protected String getJsonForLegacyInput(String text) {\n        return Json.of(\"{ value: [ '\" + text + \"' ] }\").toString();\n    }\n\n    protected String getJsonForHandle(String text) {\n        return Json.object().set(\"handle\", text).toString();\n    }\n\n    protected String getJsonForFrame(String text) {\n        return Json.object().set(\"id\", text).toString();\n    }\n\n    protected String selectorPayload(String locator) {\n        if (locator.startsWith(\"{\")) {\n            locator = DriverOptions.preProcessWildCard(locator);\n        }\n        Json json = Json.object();\n        if (locator.startsWith(\"/\")) {\n            json.set(\"using\", \"xpath\").set(\"value\", locator);\n        } else {\n            json.set(\"using\", \"css selector\").set(\"value\", locator);\n        }\n        return json.toString();\n    }\n\n    @Override\n    public String elementId(String locator) {\n        String json = selectorPayload(locator);\n        Response res = http.path(\"element\").postJson(json);\n        if (isLocatorError(res)) {\n            logger.warn(\"locator failed, will retry once: {}\", res.getBodyAsString());\n            options.sleep();\n            res = http.path(\"element\").postJson(json);\n            if (isLocatorError(res)) {\n                String message = \"locator failed twice: \" + res.getBodyAsString();\n                logger.error(message);\n                throw new RuntimeException(message);\n            }\n        }\n        List<String> resultElements = res.json().<List<String>>getAll(\"$..\", getElementKeys()).stream()\n                .flatMap(List::stream)\n                .collect(Collectors.toList());\n        String resultElement = resultElements != null && !resultElements.isEmpty() ? resultElements.get(0) : null;\n        if (resultElement == null) {\n            String message = \"locator failed to retrieve element returned by target driver: \" + res.getBodyAsString();\n            logger.error(message);\n            throw new RuntimeException(message);\n        }\n        return resultElement;\n    }\n\n    @Override\n    public List<String> elementIds(String locator) {\n        return http.path(\"elements\")\n                .postJson(selectorPayload(locator)).json()\n                .<List<String>>getAll(\"$..\", getElementKeys()).stream()\n                .flatMap(List::stream)\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public DriverOptions getOptions() {\n        return options;\n    }\n\n    @Override\n    public void setUrl(String url) {\n        Json json = Json.object().set(\"url\", url);\n        http.path(\"url\").post(json);\n    }\n\n    @Override\n    public Map<String, Object> getDimensions() {\n        return http.path(\"window\", \"rect\").get().json().get(\"value\");\n    }\n\n    @Override\n    public void setDimensions(Map<String, Object> map) {\n        http.path(\"window\", \"rect\").post(map);\n    }\n\n    @Override\n    public void refresh() {\n        http.path(\"refresh\").postJson(\"{}\");\n    }\n\n    @Override\n    public void reload() {\n        // not supported by webdriver\n        refresh();\n    }\n\n    @Override\n    public void back() {\n        http.path(\"back\").postJson(\"{}\");\n    }\n\n    @Override\n    public void forward() {\n        http.path(\"forward\").postJson(\"{}\");\n    }\n\n    @Override\n    public void maximize() {\n        http.path(\"window\", \"maximize\").postJson(\"{}\");\n    }\n\n    @Override\n    public void minimize() {\n        http.path(\"window\", \"minimize\").postJson(\"{}\");\n    }\n\n    @Override\n    public void fullscreen() {\n        http.path(\"window\", \"fullscreen\").postJson(\"{}\");\n    }\n\n    @Override\n    public Element focus(String locator) {\n        return retryIfEnabled(locator, () -> evalFocus(locator));\n    }\n\n    @Override\n    public Element clear(String locator) {\n        return retryIfEnabled(locator, () -> evalLocator(locator, \"value = ''\"));\n    }\n\n    @Override\n    public Element input(String locator, String value) {\n        return retryIfEnabled(locator, () -> {\n            String elementId;\n            if (locator.startsWith(\"(\")) {\n                evalFocus(locator);\n                List<String> elements = http.path(\"element\", \"active\").get()\n                        .json().<List<String>>getAll(\"$..\", getElementKeys()).stream()\n                        .flatMap(List::stream)\n                        .collect(Collectors.toList());;\n                elementId = elements != null && !elements.isEmpty() ? elements.get(0) : null;\n            } else {\n                elementId = elementId(locator);\n            }\n            Response response = http.path(\"element\", elementId, \"value\").postJson(isSpecCompliant() ? getJsonForInput(value) : getJsonForLegacyInput(value));\n            if (checkForSpecCompliance()) {\n                if (response.json().get(\"$.value\") != null) {\n                    String responseMessage = response.json().get(\"$.value.message\");\n                    if (responseMessage.contains(\"invalid argument: 'value' must be a list\")) {\n                        http.path(\"element\", elementId, \"value\").postJson(getJsonForLegacyInput(value));\n                        specCompliant = false;\n                    } else {\n                        specCompliant = true;\n                    }\n                } else {\n                    // did not complain that value should be a list so assume W3C WebDriver compliant moving forward\n                    specCompliant = true;\n                }\n            }\n            return DriverElement.locatorExists(this, locator);\n        });\n    }\n\n    @Override\n    public Element click(String locator) {\n        return retryIfEnabled(locator, () -> evalLocator(locator, \"click()\"));\n    }\n\n    @Override\n    public Driver submit() {\n        options.setPreSubmitHash(getSubmitHash());\n        return this;\n    }\n\n    @Override\n    public Element select(String locator, String text) {\n        return retryIfEnabled(locator, () -> {\n            eval(options.optionSelector(locator, text));\n            // if the js above did not throw an exception, the element exists\n            return DriverElement.locatorExists(this, locator);\n        });\n    }\n\n    @Override\n    public Element select(String locator, int index) {\n        return retryIfEnabled(locator, () -> {\n            eval(options.optionSelector(locator, index));\n            // if the js above did not throw an exception, the element exists\n            return DriverElement.locatorExists(this, locator);\n        });\n    }\n\n    @Override\n    public void actions(List<Map<String, Object>> actions) {\n        http.path(\"actions\").post(Collections.singletonMap(\"actions\", actions));\n    }\n\n    @Override\n    public void close() {\n        http.path(\"window\").delete();\n        open = false;\n    }\n\n    @Override\n    public boolean isTerminated() {\n        return terminated;\n    }\n\n    public boolean isSpecCompliant() {\n        return specCompliant == null || specCompliant;\n    }\n\n    public boolean checkForSpecCompliance() {\n        return specCompliant == null;\n    }\n\n    @Override\n    public void quit() {\n        if (terminated) {\n            return;\n        }\n        terminated = true;\n        if (open) {\n            close();\n        }\n        // delete session\n        try {\n            http.delete();\n        } catch (Exception e) {\n            logger.warn(\"session delete failed: {}\", e.getMessage());\n        }\n        if (command != null) {\n            command.close(true);\n        }\n    }\n\n    @Override\n    public String getUrl() {\n        return http.path(\"url\").get().json().get(\"value\");\n    }\n\n    private String evalReturn(String locator, String dotExpression) {\n        return eval(\"return \" + DriverOptions.selector(locator) + \".\" + dotExpression).getAsString();\n    }\n\n    @Override\n    public String html(String locator) {\n        return retryIfEnabled(locator, () -> evalReturn(locator, \"outerHTML\"));\n    }\n\n    @Override\n    public String text(String locator) {\n        return retryIfEnabled(locator, () -> evalReturn(locator, \"textContent\"));\n    }\n\n    @Override\n    public String value(String locator) {\n        return retryIfEnabled(locator, () -> evalReturn(locator, \"value\"));\n    }\n\n    @Override\n    public Element value(String locator, String value) {\n        return retryIfEnabled(locator, () -> evalLocator(locator, \"value = '\" + value + \"'\"));\n    }\n\n    @Override\n    public String attribute(String locator, String name) {\n        return retryIfEnabled(locator, () -> evalReturn(locator, \"getAttribute('\" + name + \"')\"));\n    }\n\n    @Override\n    public String property(String locator, String name) {\n        return retryIfEnabled(locator, () -> evalReturn(locator, name));\n    }\n\n    @Override\n    public Map<String, Object> position(String locator) {\n        return position(locator, false);\n    }\n\n    @Override\n    public Map<String, Object> position(String locator, boolean relative) {\n        return retryIfEnabled(locator, ()\n                -> eval(\"return \" + DriverOptions.selector(locator) + \".getBoundingClientRect()\").getValue());\n    }\n\n    @Override\n    public boolean enabled(String locator) {\n        return retryIfEnabled(locator, ()\n                -> eval(\"return !\" + DriverOptions.selector(locator) + \".disabled\").isTrue());\n    }\n\n    private String prefixReturn(String expression) {\n        return expression.startsWith(\"return \") ? expression : \"return \" + expression;\n    }\n\n    @Override\n    public boolean waitUntil(String expression) {\n        return options.retry(() -> {\n            try {\n                return eval(prefixReturn(expression)).isTrue();\n            } catch (Exception e) {\n                logger.warn(\"waitUntil evaluate failed: {}\", e.getMessage());\n                return false;\n            }\n        }, b -> b, \"waitUntil (js)\", true);\n    }\n\n    @Override\n    public Object script(String expression) {\n        expression = prefixReturn(expression);\n        return eval(expression).getValue();\n    }\n\n    @Override\n    public String getTitle() {\n        return http.path(\"title\").get().json().get(\"value\");\n    }\n\n    @Override\n    public List<Map> getCookies() {\n        return http.path(\"cookie\").get().json().get(\"value\");\n    }\n\n    @Override\n    public Map<String, Object> cookie(String name) {\n        return http.path(\"cookie\", name).get().json().get(\"value\");\n    }\n\n    @Override\n    public void cookie(Map<String, Object> cookie) {\n        Response res = http.path(\"cookie\").post(Collections.singletonMap(\"cookie\", cookie));\n        if (isCookieError(res)) {\n            throw new RuntimeException(\"set-cookie failed: \" + res.getBodyAsString());\n        }\n    }\n\n    @Override\n    public void deleteCookie(String name) {\n        http.path(\"cookie\", name).delete();\n    }\n\n    @Override\n    public void clearCookies() {\n        http.path(\"cookie\").delete();\n    }\n\n    @Override\n    public void dialog(boolean accept) {\n        dialog(accept, null);\n    }\n\n    @Override\n    public String getDialogText() {\n        return http.path(\"alert\", \"text\").get().json().get(\"value\");\n    }\n\n    @Override\n    public void dialog(boolean accept, String text) {\n        if (text == null) {\n            http.path(\"alert\", accept ? \"accept\" : \"dismiss\").postJson(\"{}\");\n        } else {\n            http.path(\"alert\", \"text\").post(Collections.singletonMap(\"text\", text));\n            http.path(\"alert\", \"accept\").postJson(\"{}\");\n        }\n    }\n\n    @Override\n    public byte[] screenshot(boolean embed) {\n        return screenshot(null, embed);\n    }\n\n    @Override\n    public byte[] screenshot(String locator, boolean embed) {\n        String temp;\n        if (locator == null) {\n            temp = http.path(\"screenshot\").get().json().get(\"value\");\n        } else {\n            temp = retryIfEnabled(locator, () -> {\n                String id = elementId(locator);\n                return http.path(\"element\", id, \"screenshot\").get().json().get(\"value\");\n            });\n        }\n        byte[] bytes = getDecoder().decode(temp);\n        if (embed) {\n            getRuntime().embed(bytes, ResourceType.PNG);\n        }\n        return bytes;\n    }\n\n    @Override\n    public List<String> getPages() {\n        return http.path(\"window\", \"handles\").get().json().get(\"value\");\n    }\n\n    @Override\n    public void switchPage(String titleOrUrl) {\n        if (titleOrUrl == null) {\n            return;\n        }\n        options.retry(() -> {\n            for (String handle : getPages()) {\n                http.path(\"window\").postJson(getJsonForHandle(handle));\n                String title = getTitle();\n                if (title != null && title.contains(titleOrUrl)) {\n                    return true;\n                }\n                String url = getUrl();\n                if (url != null && url.contains(titleOrUrl)) {\n                    return true;\n                }\n            }\n            return false;\n        }, returned -> returned, \"waiting to switch to tab: \" + titleOrUrl, true);\n    }\n\n    @Override\n    public void switchPage(int index) {\n        if (index == -1) {\n            return;\n        }\n        String json = Json.object().set(\"id\", index).toString();\n        http.path(\"window\").postJson(json);\n    }\n\n    @Override\n    public void switchFrame(int index) {\n        if (index == -1) {\n            http.path(\"frame\", \"parent\").postJson(\"{}\");\n            return;\n        }\n        String json = Json.object().set(\"id\", index).toString();\n        http.path(\"frame\").postJson(json);\n    }\n\n    @Override\n    public void switchFrame(String locator) {\n        if (locator == null) { // reset to parent frame\n            http.path(\"frame\", \"parent\").postJson(\"{}\");\n            return;\n        }\n        retryIfEnabled(locator, () -> {\n            String frameId = elementId(locator);\n            if (frameId == null) {\n                return null;\n            }\n            List<String> ids = elementIds(\"iframe,frame\");\n            for (int i = 0; i < ids.size(); i++) {\n                String id = ids.get(i);\n                if (frameId.equals(id)) {\n                    switchFrame(i);\n                    break;\n                }\n            }\n            return null;\n        });\n    }\n\n    protected Base64.Decoder getDecoder() {\n        return Base64.getDecoder();\n    }\n\n    @Override\n    public byte[] pdf(Map<String, Object> printOptions) {\n        String temp = http.path(\"print\").post(printOptions).json().get(\"value\");\n        return Base64.getDecoder().decode(temp);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/appium/AndroidDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.appium;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport java.util.Map;\n\n/**\n * @author babusekaran\n */\npublic class AndroidDriver extends AppiumDriver {\n\n    public static final String DRIVER_TYPE = \"android\";\n\n    protected AndroidDriver(MobileDriverOptions options) {\n        super(options);\n    }\n\n    public static AndroidDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        MobileDriverOptions options = new MobileDriverOptions(map, sr, 4723, FileUtils.isOsWindows() ? \"cmd.exe\" : \"appium\");\n        // additional commands needed to start appium on windows\n        if (FileUtils.isOsWindows()){\n            options.arg(\"/C\");\n            options.arg(\"cmd.exe\");\n            options.arg(\"/K\");\n            options.arg(\"appium\");\n        }\n        options.arg(\"--port=\" + options.port);\n        return new AndroidDriver(options);\n    }\n\n    @Override\n    public void activate() {\n        super.setContext(\"NATIVE_APP\");\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/appium/AppiumDriver.java",
    "content": "/*\r\n * The MIT License\r\n *\r\n * Copyright 2022 Karate Labs Inc.\r\n *\r\n * Permission is hereby granted, free of charge, to any person obtaining a copy\r\n * of this software and associated documentation files (the \"Software\"), to deal\r\n * in the Software without restriction, including without limitation the rights\r\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\n * copies of the Software, and to permit persons to whom the Software is\r\n * furnished to do so, subject to the following conditions:\r\n *\r\n * The above copyright notice and this permission notice shall be included in\r\n * all copies or substantial portions of the Software.\r\n *\r\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r\n * THE SOFTWARE.\r\n */\r\npackage com.intuit.karate.driver.appium;\r\n\r\nimport com.intuit.karate.Json;\r\nimport com.intuit.karate.driver.DriverElement;\r\nimport com.intuit.karate.driver.DriverOptions;\r\nimport com.intuit.karate.driver.Element;\r\nimport com.intuit.karate.driver.WebDriver;\r\nimport com.intuit.karate.http.ResourceType;\r\nimport com.intuit.karate.http.Response;\r\n\r\nimport java.io.File;\r\nimport java.io.FileOutputStream;\r\nimport java.util.Base64;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\nimport java.util.List;\r\nimport java.util.ArrayList;\r\nimport java.util.function.Supplier;\r\n\r\n/**\r\n * @author babusekaran\r\n */\r\npublic abstract class AppiumDriver extends WebDriver {\r\n\r\n    private boolean isWebSession;\r\n\r\n    protected AppiumDriver(MobileDriverOptions options) {\r\n        super(options);\r\n        // flag to know if driver runs for browser on mobile\r\n        Map<String, Object> sessionPayload = (Map<String, Object>) options.getWebDriverSessionPayload();\r\n        isWebSession = options.isWebSession();\r\n    }\r\n\r\n    @Override\r\n    public String attribute(String locator, String name) {\r\n        String id = elementId(locator);\r\n        return http.path(\"element\", id, \"attribute\", name).get().json().get(\"value\");\r\n    }\r\n\r\n    @Override\r\n    protected String selectorPayload(String id) {\r\n        if (isWebSession) { // use WebDriver selector strategies for mobile browser\r\n            return super.selectorPayload(id);\r\n        }\r\n        Json json = Json.object();\r\n        if (id.startsWith(\"/\")) {\r\n            json.set(\"using\", \"xpath\").set(\"value\", id);\r\n        } else if (id.startsWith(\"@\")) {\r\n            json.set(\"using\", \"accessibility id\").set(\"value\", id.substring(1));\r\n        } else if (id.startsWith(\"#\")) {\r\n            json.set(\"using\", \"id\").set(\"value\", id.substring(1));\r\n        } else if (id.startsWith(\":\")) {\r\n            json.set(\"using\", \"-ios predicate string\").set(\"value\", id.substring(1));\r\n        } else if (id.startsWith(\"^\")) {\r\n            json.set(\"using\", \"-ios class chain\").set(\"value\", id.substring(1));\r\n        } else if (id.startsWith(\"-\")) {\r\n            json.set(\"using\", \"-android uiautomator\").set(\"value\", id.substring(1));\r\n        } else {\r\n            json.set(\"using\", \"name\").set(\"value\", id);\r\n        }\r\n        return json.toString();\r\n    }\r\n\r\n    @Override\r\n    public Element click(String locator) {\r\n        String id = elementId(locator);\r\n        http.path(\"element\", id, \"click\").postJson(\"{}\");\r\n        return DriverElement.locatorExists(this, locator);\r\n    }\r\n\r\n    public void setContext(String context) {\r\n        Json contextBody = Json.object();\r\n        contextBody.set(\"name\", context);\r\n        http.path(\"context\").post(contextBody);\r\n    }\r\n\r\n    public void hideKeyboard() {\r\n        http.path(\"appium\", \"device\", \"hide_keyboard\").postJson(\"{}\");\r\n    }\r\n\r\n    public String startRecordingScreen() {\r\n        return http.path(\"appium\", \"start_recording_screen\").postJson(\"{}\").json().get(\"value\");\r\n    }\r\n\r\n    public String startRecordingScreen(Map<String, Object> payload) {\r\n        Map<String, Object> options = new HashMap<>();\r\n        options.put(\"options\", payload);\r\n        return http.path(\"appium\", \"start_recording_screen\").post(options).json().get(\"value\");\r\n    }\r\n\r\n    public String stopRecordingScreen() {\r\n        return http.path(\"appium\", \"stop_recording_screen\").postJson(\"{}\").json().get(\"value\");\r\n    }\r\n\r\n    public String stopRecordingScreen(Map<String, Object> payload) {\r\n        Map<String, Object> options = new HashMap<>();\r\n        options.put(\"options\", payload);\r\n        return http.path(\"appium\", \"stop_recording_screen\").post(options).json().get(\"value\");\r\n    }\r\n\r\n    public void saveRecordingScreen(String fileName, boolean embed) {\r\n        String videoTemp = stopRecordingScreen();\r\n        byte[] bytes = Base64.getDecoder().decode(videoTemp);\r\n        File src = new File(fileName);\r\n        try (FileOutputStream fileOutputStream = new FileOutputStream(src.getAbsolutePath())) {\r\n            fileOutputStream.write(bytes);\r\n        } catch (Exception e) {\r\n            logger.error(\"error while saveRecordingScreen {}\", e.getMessage());\r\n        }\r\n        if (embed) {\r\n            if (src.exists()) {\r\n                getRuntime().embed(bytes, ResourceType.MP4);\r\n            }\r\n        }\r\n    }\r\n\r\n    public void saveRecordingScreen(String fileName) {\r\n        saveRecordingScreen(fileName, false);\r\n    }\r\n\r\n    @Override\r\n    public String text(String locator) {\r\n        String id = elementId(locator);\r\n        return http.path(\"element\", id, \"text\").get().json().get(\"value\");\r\n    }\r\n\r\n    @Override\r\n    protected Base64.Decoder getDecoder() {\r\n        return Base64.getMimeDecoder();\r\n    }\r\n\r\n    @Override\r\n    public void close() {\r\n        // TODO\r\n    }\r\n\r\n    @Override\r\n    public Object script(String expression) {\r\n        if (isWebSession) { // use WebDriver script for mobile browser\r\n            return super.script(expression);\r\n        }\r\n        return eval(expression).getValue();\r\n    }\r\n\r\n    public Object script(String expression, List<Map<String, Object>> args) {\r\n        return eval(expression, args).getValue();\r\n    }\r\n\r\n    public Object script(String expression, Map<String, Object> args) {\r\n        List<Map<String, Object>> scriptArgs = new ArrayList<>(1);\r\n        scriptArgs.add(args);\r\n        return eval(expression, scriptArgs).getValue();\r\n    }\r\n\r\n    @Override\r\n    protected <T> T retryIfEnabled(String locator, Supplier<T> action) {\r\n        if (isWebSession) {\r\n            return super.retryIfEnabled(locator, action);\r\n        }\r\n        if (options.isRetryEnabled()) {\r\n            waitFor(locator); // will throw exception if not found\r\n        }\r\n        return action.get();\r\n    }\r\n\r\n    @Override\r\n    public DriverOptions getOptions() {\r\n        if (isWebSession) {\r\n            return super.getOptions();\r\n        }\r\n        return (MobileDriverOptions)options;\r\n    }\r\n\r\n    @Override\r\n    public Element waitForText(String locator, String expected) {\r\n        if (isWebSession) {\r\n            return super.waitForText(locator, expected);\r\n        }\r\n        return (Element) waitUntil(() -> {\r\n            String text = optional(locator).getText();\r\n            if (!expected.equals(text)) {\r\n                return null;\r\n            }\r\n            return DriverElement.locatorExists(this, locator);\r\n        });\r\n    }\r\n\r\n    @Override\r\n    public Element clear(String locator) {\r\n        if (isWebSession) {\r\n            return super.clear(locator);\r\n        }\r\n        String id = elementId(locator);\r\n        http.path(\"element\", id, \"clear\").postJson(\"{}\");\r\n        return DriverElement.locatorExists(this, locator);\r\n    }\r\n\r\n\r\n}\r\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/appium/IosDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.appium;\n\nimport com.intuit.karate.core.ScenarioRuntime;\nimport java.util.Map;\n\n/**\n * @author babusekaran\n */\npublic class IosDriver extends AppiumDriver {\n\n    public static final String DRIVER_TYPE = \"ios\";\n\n    public IosDriver(MobileDriverOptions options) {\n        super(options);\n    }\n\n    public static IosDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        MobileDriverOptions options = new MobileDriverOptions(map, sr, 4723, \"appium\");\n        options.arg(\"--port=\" + options.port);\n        return new IosDriver(options);\n    }\n\n    @Override\n    public void activate() {\n        super.setContext(\"NATIVE_APP\");\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/appium/MobileDriverOptions.java",
    "content": "/*\r\n * The MIT License\r\n *\r\n * Copyright 2022 Karate Labs Inc.\r\n *\r\n * Permission is hereby granted, free of charge, to any person obtaining a copy\r\n * of this software and associated documentation files (the \"Software\"), to deal\r\n * in the Software without restriction, including without limitation the rights\r\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\n * copies of the Software, and to permit persons to whom the Software is\r\n * furnished to do so, subject to the following conditions:\r\n *\r\n * The above copyright notice and this permission notice shall be included in\r\n * all copies or substantial portions of the Software.\r\n *\r\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r\n * THE SOFTWARE.\r\n */\r\npackage com.intuit.karate.driver.appium;\r\n\r\nimport com.intuit.karate.core.ScenarioRuntime;\r\nimport com.intuit.karate.driver.DriverOptions;\r\nimport com.intuit.karate.driver.Driver;\r\nimport com.intuit.karate.driver.Element;\r\nimport com.intuit.karate.driver.DriverElement;\r\nimport com.intuit.karate.driver.MissingElement;\r\n\r\nimport java.util.Arrays;\r\nimport java.util.List;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author babusekaran\r\n */\r\npublic class MobileDriverOptions extends DriverOptions {\r\n\r\n    public MobileDriverOptions(Map<String, Object> options, ScenarioRuntime sr, int defaultPort, String defaultExecutable) {\r\n        super(options, sr, defaultPort, defaultExecutable);\r\n    }\r\n\r\n    public boolean isWebSession() {\r\n        // flag to know if driver runs for browser on mobile\r\n        Map<String, Object> sessionPayload = super.getWebDriverSessionPayload();\r\n        return getBrowserName(sessionPayload) != null;\r\n    }\r\n\r\n    @Override\r\n    public Element waitForAny(Driver driver, String... locators) {\r\n        if (isWebSession()) {\r\n            return super.waitForAny(driver, locators);\r\n        }\r\n        long startTime = System.currentTimeMillis();\r\n        List<String> list = Arrays.asList(locators);\r\n        boolean found = (boolean)driver.waitUntil(() -> {\r\n            for (String locator: list) {\r\n                try {\r\n                    ((AppiumDriver)driver).elementId(locator);\r\n                    return true;\r\n                }\r\n                catch (RuntimeException re){\r\n                    logger.debug(\"failed to locate : {}\", locator);\r\n                }\r\n            }\r\n            return null;\r\n        });\r\n        // important: un-set the retry flag\r\n        disableRetry();\r\n        if (!found) {\r\n            long elapsedTime = System.currentTimeMillis() - startTime;\r\n            throw new RuntimeException(\"wait failed for: \" + list + \" after \" + elapsedTime + \" milliseconds\");\r\n        }\r\n        if (locators.length == 1) {\r\n            return DriverElement.locatorExists(driver, locators[0]);\r\n        }\r\n        for (String locator : locators) {\r\n            Element temp = driver.optional(locator);\r\n            if (temp.isPresent()) {\r\n                return temp;\r\n            }\r\n        }\r\n        // this should never happen\r\n        throw new RuntimeException(\"unexpected wait failure for locators: \" + list);\r\n\r\n    }\r\n\r\n    @Override\r\n    public Element optional(Driver driver, String locator) {\r\n        if (isWebSession()) {\r\n            return super.optional(driver, locator);\r\n        }\r\n        try{\r\n            retry(() -> {\r\n                try {\r\n                    ((AppiumDriver)driver).elementId(locator);\r\n                    return true;\r\n                } catch (RuntimeException re) {\r\n                    return false;\r\n                }\r\n            }, b -> b, \"optional (locator)\", true);\r\n            // the element exists, if the above function did not throw an exception\r\n            return DriverElement.locatorExists(driver, locator);\r\n        }\r\n        catch (RuntimeException re) {\r\n            return new MissingElement(driver, locator);\r\n        }\r\n    }\r\n\r\n    protected static String getBrowserName(Map<String, Object> sessionPayload) {\r\n        // get browserName from capabilities or desiredCapabilities node\r\n        Map<String, Object> capabilities = (Map<String, Object>) sessionPayload.get(\"capabilities\");\r\n        Map<String, Object> desiredCapabilities = (Map<String, Object>) sessionPayload.get(\"desiredCapabilities\");\r\n\r\n        if (capabilities != null) {\r\n            if (capabilities.containsKey(\"firstMatch\")) {\r\n                // sauce labs uses the firstMatch node for some reason\r\n                // see https://support.saucelabs.com/hc/en-us/articles/4412359870231-Migrating-Appium-Real-Device-Tests-to-W3C\r\n                List<Map<String, Object>> firstMatch = (List<Map<String, Object>>) capabilities.get(\"firstMatch\");\r\n                if (firstMatch.size() == 0) {\r\n                    throw new RuntimeException(\"firstMatch node in webdriver session is empty\");\r\n                }\r\n                return (String) firstMatch.get(0).get(\"browserName\");\r\n            } else {\r\n                return (String) capabilities.get(\"browserName\");\r\n            }\r\n        } else if (desiredCapabilities != null) {\r\n            return (String) desiredCapabilities.get(\"browserName\");\r\n        } else {\r\n            // no browserName found\r\n            return null;\r\n        }\r\n\r\n    }\r\n}"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/chrome/Chrome.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.chrome;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Http;\nimport com.intuit.karate.core.FeatureRuntime;\nimport com.intuit.karate.core.ScenarioEngine;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.shell.Command;\nimport com.intuit.karate.driver.DevToolsDriver;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.http.HttpClientFactory;\nimport com.intuit.karate.http.Response;\n\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class Chrome extends DevToolsDriver {\n    \n    public static final String DRIVER_TYPE = \"chrome\";\n\n    public static final String DEFAULT_PATH_MAC = \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\";\n    public static final String DEFAULT_PATH_WIN32 = \"C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\";\n    public static final String DEFAULT_PATH_WIN64 = \"C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\";\n    public static final String DEFAULT_PATH_WIN = Files.isRegularFile(Paths.get(DEFAULT_PATH_WIN64)) && Files.isReadable(Paths.get(DEFAULT_PATH_WIN64)) ? DEFAULT_PATH_WIN64 : DEFAULT_PATH_WIN32;\n    public static final String DEFAULT_PATH_LINUX = \"/usr/bin/google-chrome\";\n\n    public Chrome(DriverOptions options, Command command, String webSocketUrl) {\n        super(options, command, webSocketUrl);\n    }\n\n    public static Chrome start(Map<String, Object> map, ScenarioRuntime sr) {\n        DriverOptions options = new DriverOptions(map, sr, 9222,\n                FileUtils.isOsWindows() ? DEFAULT_PATH_WIN : FileUtils.isOsMacOsX() ? DEFAULT_PATH_MAC : DEFAULT_PATH_LINUX);\n        options.arg(\"--remote-debugging-port=\" + options.port);\n        options.arg(\"--remote-allow-origins=*\");\n        options.arg(\"--no-first-run\");\n        if (options.userDataDir != null) {\n            options.arg(\"--user-data-dir=\" + options.userDataDir);\n        }\n        options.arg(\"--disable-popup-blocking\");\n        if (options.headless) {\n            options.arg(\"--headless=new\");\n        }\n        Command command = options.startProcess();\n        Http http = options.getHttp();        \n        Command.waitForHttp(http.urlBase + \"/json\", r -> r.getStatus() == 200 && !r.json().asList().isEmpty());\n        Response res = http.path(\"json\").get();\n        if (res.json().asList().isEmpty()) {\n            if (command != null) {\n                command.close(true);\n            }\n            throw new RuntimeException(\"chrome server returned empty list from \" + http.urlBase);\n        }\n        String webSocketUrl = null;\n        List<Map<String, Object>> targets = res.json().asList();\n        for (Map<String, Object> target : targets) {\n            String targetUrl = (String) target.get(\"url\");\n            if (targetUrl == null || targetUrl.startsWith(\"chrome-\")) {\n                continue;\n            }\n            String targetType = (String) target.get(\"type\");\n            if (!\"page\".equals(targetType)) {\n                continue;\n            }\n            webSocketUrl = (String) target.get(\"webSocketDebuggerUrl\");\n            if (options.attach == null) { // take the first                \n                break;\n            }\n            if (targetUrl.contains(options.attach)) {\n                break;\n            }\n        }\n        if (webSocketUrl == null) {\n            throw new RuntimeException(\"failed to attach to chrome debug server\");\n        }\n        Chrome chrome = new Chrome(options, command, webSocketUrl);\n        chrome.activate();\n        chrome.enablePageEvents();\n        chrome.enableRuntimeEvents();\n        if (!options.headless) {\n            chrome.initWindowIdAndState();\n        }\n        return chrome;\n    }\n\n    public static Chrome start(String chromeExecutablePath, boolean headless) {\n        Map<String, Object> options = new HashMap();\n        options.put(\"executable\", chromeExecutablePath);\n        options.put(\"headless\", headless);\n        return Chrome.start(options);\n    }\n\n    public static Chrome start(Map<String, Object> options) {\n        if (options == null) {\n            options = new HashMap();\n        }\n        options.putIfAbsent(\"type\", DRIVER_TYPE);\n        ScenarioRuntime runtime = FeatureRuntime.forTempUse(HttpClientFactory.DEFAULT).scenarios.next();\n        ScenarioEngine.set(runtime.engine);\n        return Chrome.start(options, runtime);\n    }\n\n    public static Chrome start() {\n        return start(null);\n    }\n\n    public static Chrome startHeadless() {\n        return start(Collections.singletonMap(\"headless\", true));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/chrome/ChromeWebDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.chrome;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.driver.WebDriver;\nimport com.intuit.karate.http.Response;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class ChromeWebDriver extends WebDriver {\n\n    public static final String DRIVER_TYPE = \"chromedriver\";\n\n    public ChromeWebDriver(DriverOptions options) {\n        super(options);\n    }\n\n    public static ChromeWebDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        DriverOptions options = new DriverOptions(map, sr, 9515, \"chromedriver\");\n        options.arg(\"--port=\" + options.port);\n        if (options.userDataDir != null) {\n            options.arg(\"--user-data-dir=\" + options.userDataDir);\n        }\n        return new ChromeWebDriver(options);\n    }\n\n    @Override\n    public void activate() {\n        if (!options.headless) {\n            try {\n                switch (FileUtils.getOsType()) {\n                    case MACOSX:\n                        Runtime.getRuntime().exec(new String[]{\"osascript\", \"-e\", \"tell app \\\"Chrome\\\" to activate\"});\n                        break;\n                    default:\n\n                }\n            } catch (Exception e) {\n                logger.warn(\"native window switch failed: {}\", e.getMessage());\n            }\n        }\n    }\n\n    @Override\n    protected boolean isJavaScriptError(Response res) {\n        Object value = res.json().get(\"value\");\n        return value != null && value.toString().contains(\"javascript error\");\n    }\n\n    @Override\n    protected boolean isLocatorError(Response res) {\n        Object value = res.json().get(\"value\");\n        return value.toString().contains(\"no such element\");\n    }\n\n    @Override\n    protected boolean isCookieError(Response res) {\n        Object value = res.json().get(\"value\");\n        return value != null && value.toString().contains(\"unable to set cookie\");\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/firefox/GeckoWebDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.firefox;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Json;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.driver.WebDriver;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class GeckoWebDriver extends WebDriver {\n\n    public static final String DRIVER_TYPE = \"geckodriver\";\n\n    public GeckoWebDriver(DriverOptions options) {\n        super(options);\n    }\n\n    public static GeckoWebDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        DriverOptions options = new DriverOptions(map, sr, 4444, \"geckodriver\");\n        options.arg(\"--port=\" + options.port);\n        return new GeckoWebDriver(options);\n    }\n    \n    @Override\n    protected String getJsonForFrame(String text) {\n        return Json.object().set(\"frameId\", text).toString();\n    }    \n\n    @Override\n    public void activate() {\n        if (!options.headless) {\n            try {\n                switch (FileUtils.getOsType()) {\n                    case MACOSX:\n                        Runtime.getRuntime().exec(new String[]{\"osascript\", \"-e\", \"tell app \\\"Firefox\\\" to activate\"});\n                        break;\n                    default:\n\n                }\n            } catch (Exception e) {\n                logger.warn(\"native window switch failed: {}\", e.getMessage());\n            }\n        }\n    }\n\n    @Override\n    public void quit() {\n        // geckodriver already closes all windows on delete session\n        open = false;\n        super.quit();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/microsoft/EdgeChromium.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.microsoft;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Http;\nimport com.intuit.karate.core.FeatureRuntime;\nimport com.intuit.karate.core.ScenarioEngine;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.DevToolsDriver;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.http.HttpClientFactory;\nimport com.intuit.karate.http.Response;\nimport com.intuit.karate.shell.Command;\n\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author sixdouglas\n */\npublic class EdgeChromium extends DevToolsDriver {\n    \n    public static final String DRIVER_TYPE = \"msedge\";\n\n    public static final String DEFAULT_PATH_MAC = \"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge\";\n    public static final String DEFAULT_PATH_WIN32 = \"C:\\\\Program Files (x86)\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe\";\n    public static final String DEFAULT_PATH_WIN64 = \"C:\\\\Program Files\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe\";\n    public static final String DEFAULT_PATH_WIN = Files.isRegularFile(Paths.get(DEFAULT_PATH_WIN64)) && Files.isReadable(Paths.get(DEFAULT_PATH_WIN64)) ? DEFAULT_PATH_WIN64 : DEFAULT_PATH_WIN32;\n    public static final String DEFAULT_PATH_LINUX = \"/dev/null\";\n\n    public EdgeChromium(DriverOptions options, Command command, String webSocketUrl) {\n        super(options, command, webSocketUrl);\n    }\n\n    public static EdgeChromium start(Map<String, Object> map, ScenarioRuntime sr) {\n        if (!FileUtils.isOsWindows() && !FileUtils.isOsMacOsX()) {\n            throw new UnsupportedOperationException(\"edge browser is not yet available on linux!\");\n        }\n        DriverOptions options = new DriverOptions(map, sr, 9222,\n                FileUtils.isOsWindows() ? DEFAULT_PATH_WIN : FileUtils.isOsMacOsX() ? DEFAULT_PATH_MAC : DEFAULT_PATH_LINUX);\n        options.arg(\"--remote-debugging-port=\" + options.port);\n        options.arg(\"--no-first-run\");\n        if (options.userDataDir != null) {\n            options.arg(\"--user-data-dir=\" + options.userDataDir);\n        }\n        options.arg(\"--disable-popup-blocking\");\n        if (options.headless) {\n            options.arg(\"--headless=new\");\n        }\n        Command command = options.startProcess();\n        Http http = options.getHttp();\n        Command.waitForHttp(http.urlBase);\n        Response res = http.path(\"json\").get();\n        if (res.json().asList().isEmpty()) {\n            if (command != null) {\n                command.close(true);\n            }\n            throw new RuntimeException(\"edge server returned empty list from \" + http.urlBase);\n        }\n        String webSocketUrl = null;\n        List<Map<String, Object>> targets = res.json().asList();\n        for (Map<String, Object> target : targets) {\n            String targetUrl = (String) target.get(\"url\");\n            if (targetUrl == null || targetUrl.startsWith(\"edge-\")) {\n                continue;\n            }\n            String targetType = (String) target.get(\"type\");\n            if (!\"page\".equals(targetType)) {\n                continue;\n            }\n            webSocketUrl = (String) target.get(\"webSocketDebuggerUrl\");\n            if (options.attach == null) { // take the first                \n                break;\n            }\n            if (targetUrl.contains(options.attach)) {\n                break;\n            }\n        }\n        if (webSocketUrl == null) {\n            throw new RuntimeException(\"failed to attach to Edge-Chromium debug server\");\n        }\n        EdgeChromium edgeChromium = new EdgeChromium(options, command, webSocketUrl);\n        edgeChromium.activate();\n        edgeChromium.enablePageEvents();\n        edgeChromium.enableRuntimeEvents();\n        if (!options.headless) {\n            edgeChromium.initWindowIdAndState();\n        }\n        return edgeChromium;\n    }\n\n    public static EdgeChromium start(String chromeExecutablePath, boolean headless) {\n        Map<String, Object> options = new HashMap();\n        options.put(\"executable\", chromeExecutablePath);\n        options.put(\"headless\", headless);\n        return EdgeChromium.start(options);\n    }\n\n    public static EdgeChromium start(Map<String, Object> options) {\n        if (options == null) {\n            options = new HashMap();\n        }\n        options.putIfAbsent(\"type\", DRIVER_TYPE);\n        ScenarioRuntime runtime = FeatureRuntime.forTempUse(HttpClientFactory.DEFAULT).scenarios.next();\n        ScenarioEngine.set(runtime.engine);        \n        return EdgeChromium.start(options, runtime);\n    }\n\n    public static EdgeChromium start() {\n        return start(null);\n    }\n\n    public static EdgeChromium startHeadless() {\n        return start(Collections.singletonMap(\"headless\", true));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/microsoft/IeWebDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.microsoft;\n\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.driver.WebDriver;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class IeWebDriver extends WebDriver {\n\n    public static final String DRIVER_TYPE = \"iedriver\";\n    \n    public IeWebDriver(DriverOptions options) {\n        super(options);\n    }\n\n    public static IeWebDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        DriverOptions options = new DriverOptions(map, sr, 5555, \"IEDriverServer\");\n        options.arg(\"port=\" + options.port);\n        return new IeWebDriver(options);\n    }\n    \n    @Override\n    public void activate() {\n        logger.warn(\"activate not implemented for iewebdriver\");\n    }    \n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/microsoft/MsEdgeDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.microsoft;\n\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.driver.WebDriver;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class MsEdgeDriver extends WebDriver {\n\n    public static final String DRIVER_TYPE = \"msedgedriver\";\n\n    public MsEdgeDriver(DriverOptions options) {\n        super(options);\n    }\n\n    public static MsEdgeDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        DriverOptions options = new DriverOptions(map, sr, 9515, \"msedgedriver\");\n        options.arg(\"--port=\" + options.port);\n        return new MsEdgeDriver(options);\n    }\n\n    @Override\n    public void activate() {\n        logger.warn(\"activate not implemented for mswebdriver\");\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/microsoft/MsWebDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.microsoft;\n\nimport com.intuit.karate.Json;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.driver.WebDriver;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class MsWebDriver extends WebDriver {\n\n    public static final String DRIVER_TYPE = \"mswebdriver\";\n\n    public MsWebDriver(DriverOptions options) {\n        super(options);\n    }\n\n    public static MsWebDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        DriverOptions options = new DriverOptions(map, sr, 17556, \"MicrosoftWebDriver\");\n        options.arg(\"--port=\" + options.port);\n        return new MsWebDriver(options);\n    }\n\n    @Override\n    protected String getJsonForInput(String text) {\n        return Json.object().set(\"keysToSend[0]\", text).toString();\n    }\n\n    @Override\n    public void activate() {\n        logger.warn(\"activate not implemented for mswebdriver\");\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/microsoft/WinAppDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.microsoft;\n\nimport com.intuit.karate.Json;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.DriverElement;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.driver.Element;\nimport com.intuit.karate.driver.WebDriver;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class WinAppDriver extends WebDriver {\n\n    public static final String DRIVER_TYPE = \"winappdriver\";\n\n    public WinAppDriver(DriverOptions options) {\n        super(options);\n    }\n\n    public static WinAppDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        DriverOptions options = new DriverOptions(map, sr, 4727, \n                \"C:/Program Files (x86)/Windows Application Driver/WinAppDriver\");\n        options.arg(options.port + \"\");\n        return new WinAppDriver(options);\n    }\n\n    @Override\n    public void activate() {\n        // TODO\n    }\n    \n    private String getElementSelector(String id) {\n        Json json = Json.object();\n        if (id.startsWith(\"/\")) {\n            json.set(\"using\", \"xpath\").set(\"value\", id);\n        } else if (id.startsWith(\"@\")){\n            json.set(\"using\", \"accessibility id\").set(\"value\", id.substring(1));\n        } else if (id.startsWith(\"#\")){\n            json.set(\"using\", \"id\").set(\"value\", id.substring(1));\n        } else {\n            json.set(\"using\", \"name\").set(\"value\", id);\n        }\n        return json.toString();\n    }\n\n    @Override\n    public String elementId(String id) {\n        String body = getElementSelector(id);\n        return http.path(\"element\").postJson(body).json().getFirst(\"$..ELEMENT\");\n    }\n\n    @Override\n    public Element click(String locator) {\n        String id = elementId(locator);\n        http.path(\"element\", id, \"click\").postJson(\"{}\");\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public String text(String locator) {\n        String id = elementId(locator);\n        return http.path(\"element\", id, \"text\").get().json().get(\"value\");\n    }\n\n    @Override\n    protected String getJsonForInput(String text) {\n        return Json.object().set(\"value[0]\", text).toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/playwright/PlaywrightDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.playwright;\n\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.Json;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.Driver;\nimport com.intuit.karate.driver.DriverElement;\n\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.driver.Element;\nimport com.intuit.karate.driver.Input;\nimport com.intuit.karate.driver.Keys;\nimport com.intuit.karate.http.ResourceType;\nimport com.intuit.karate.http.WebSocketClient;\nimport com.intuit.karate.http.WebSocketOptions;\nimport com.intuit.karate.shell.Command;\nimport java.util.ArrayList;\nimport java.util.Base64;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.Predicate;\n\n/**\n *\n * @author pthomas3\n */\npublic class PlaywrightDriver implements Driver {\n\n    public static final String DRIVER_TYPE = \"playwright\";\n\n    private final DriverOptions options;\n    private final Command command;\n    private final WebSocketClient client;\n    private final PlaywrightWait wait;\n    private final Logger logger;\n\n    private boolean submit;\n    private boolean initialized;\n    private boolean terminated;\n\n    private String browserGuid;\n    private String browserContextGuid;\n\n    private final Object LOCK = new Object();\n\n    private void lockAndWait() {\n        synchronized (LOCK) {\n            try {\n                LOCK.wait();\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n\n    protected void unlockAndProceed() {\n        initialized = true;\n        synchronized (LOCK) {\n            LOCK.notify();\n        }\n    }\n\n    private int nextId;\n\n    public int nextId() {\n        return ++nextId;\n    }\n\n    public void waitSync() {\n        client.waitSync();\n    }\n\n    public static PlaywrightDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        DriverOptions options = new DriverOptions(map, sr, 4444, \"playwright\");\n        String playwrightUrl;\n        Command command;\n        if (options.start) {\n            Map<String, Object> pwOptions = options.playwrightOptions == null ? Collections.EMPTY_MAP : options.playwrightOptions;\n            options.arg(options.port + \"\");\n            String browserType = (String) pwOptions.get(\"browserType\");\n            if (browserType == null) {\n                browserType = \"chromium\";\n            }\n            options.arg(browserType);\n            if (options.headless) {\n                options.arg(\"true\");\n            }\n            CompletableFuture<String> future = new CompletableFuture();\n            command = options.startProcess(s -> {\n                int pos = s.indexOf(\"ws://\");\n                if (pos != -1) {\n                    s = s.substring(pos).trim();\n                    pos = s.indexOf(' ');\n                    if (pos != -1) {\n                        s = s.substring(0, pos);\n                    }\n                    future.complete(s);\n                }\n            });\n            try {\n                playwrightUrl = future.get();\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n            options.processLogger.debug(\"playwright server url ready: {}\", playwrightUrl);\n        } else {\n            command = null;\n            playwrightUrl = options.playwrightUrl;\n            if (playwrightUrl == null) {\n                throw new RuntimeException(\"playwrightUrl is mandatory if start == false\");\n            }\n        }\n        return new PlaywrightDriver(options, command, playwrightUrl);\n    }\n\n    public PlaywrightDriver(DriverOptions options, Command command, String webSocketUrl) {\n        this.options = options;\n        logger = options.driverLogger;\n        this.command = command;\n        wait = new PlaywrightWait(this, options);\n        WebSocketOptions wsOptions = new WebSocketOptions(webSocketUrl);\n        wsOptions.setMaxPayloadSize(options.maxPayloadSize);\n        wsOptions.setTextConsumer(text -> {\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"<< {}\", text);\n            } else {\n                // to avoid swamping the console when large base64 encoded binary responses happen\n                logger.debug(\"<< {}\", StringUtils.truncate(text, 1024, true));\n            }\n            Map<String, Object> map = Json.of(text).value();\n            PlaywrightMessage pwm = new PlaywrightMessage(this, map);\n            receive(pwm);\n        });\n        client = new WebSocketClient(wsOptions, logger);\n        lockAndWait();\n        logger.debug(\"contexts ready, frame: {}, page: {}, browser-context: {}, browser: {}\",\n                currentFrame, currentPage, browserContextGuid, browserGuid);\n    }\n\n    private PlaywrightMessage method(String method, String guid) {\n        return new PlaywrightMessage(this, method, guid);\n    }\n\n    public void send(PlaywrightMessage pwm) {\n        String json = JsonUtils.toJson(pwm.toMap());\n        logger.debug(\">> {}\", json);\n        client.send(json);\n    }\n\n    private String currentDialog;\n    private String currentDialogText;\n    private String currentDialogType;\n    private boolean dialogAccept = true;\n    private String dialogInput = \"\";\n\n    private String currentFrame;\n    private String currentPage;\n    private final Map<String, Set<String>> pageFrames = new LinkedHashMap();\n    private final Map<String, Frame> frameInfo = new HashMap();\n\n    private PlaywrightMessage page(String method) {\n        return method(method, currentPage);\n    }\n\n    private PlaywrightMessage frame(String method) {\n        return method(method, currentFrame);\n    }\n\n    private static class Frame {\n\n        final String frameGuid;\n        final String url;\n        final String name;\n\n        Frame(String frameGuid, String url, String name) {\n            this.frameGuid = frameGuid;\n            this.url = url;\n            this.name = name;\n        }\n\n    }\n\n    public void receive(PlaywrightMessage pwm) {\n        if (pwm.methodIs(\"frameAttached\")) {\n            String pageGuid = pwm.getGuid();\n            String frameGuid = pwm.getParam(\"frame.guid\");\n            Set<String> frames = pageFrames.get(pageGuid);\n            if (frames == null) {\n                frames = new LinkedHashSet(); // order important !!\n                pageFrames.put(pageGuid, frames);\n            }\n            frames.add(frameGuid);\n        } else if (pwm.methodIs(\"frameDetached\")) {\n            String pageGuid = pwm.getGuid();\n            String frameGuid = pwm.getParam(\"frame.guid\");\n            frameInfo.remove(frameGuid);\n            Set<String> frames = pageFrames.get(pageGuid);\n            frames.remove(frameGuid);\n        } else if (pwm.methodIs(\"navigated\")) {\n            String frameGuid = pwm.getGuid();\n            String url = pwm.getParam(\"url\");\n            String name = pwm.getParam(\"name\");\n            frameInfo.put(frameGuid, new Frame(frameGuid, url, name));\n        } else if (pwm.methodIs(\"__create__\")) {\n            if (pwm.paramHas(\"type\", \"Page\")) {\n                String pageGuid = pwm.getParam(\"guid\");\n                String frameGuid = pwm.getParam(\"initializer.mainFrame.guid\");\n                Set<String> frames = pageFrames.get(pageGuid);\n                if (frames == null) {\n                    frames = new LinkedHashSet(); // order important !!\n                    pageFrames.put(pageGuid, frames);\n                }\n                frames.add(frameGuid);\n                if (!initialized) {\n                    currentPage = pageGuid;\n                    currentFrame = frameGuid;\n                    unlockAndProceed();\n                }\n            } else if (pwm.paramHas(\"type\", \"Dialog\")) {\n                currentDialog = pwm.getParam(\"guid\");\n                currentDialogText = pwm.getParam(\"initializer.message\");\n                currentDialogType = pwm.getParam(\"initializer.type\");\n                if (\"alert\".equals(currentDialogType)) {\n                    method(\"dismiss\", currentDialog).sendWithoutWaiting();\n                } else {\n                    if (dialogInput == null) {\n                        dialogInput = \"\";\n                    }\n                    method(dialogAccept ? \"accept\" : \"dismiss\", currentDialog)\n                            .param(\"promptText\", dialogInput).sendWithoutWaiting();\n                }\n            } else if (pwm.paramHas(\"type\", \"Browser\")) {\n                browserGuid = pwm.getParam(\"guid\");\n                Map<String, Object> map = new HashMap();\n                map.put(\"sdkLanguage\", \"javascript\");\n                if (!options.headless) {\n                    map.put(\"noDefaultViewport\", false);\n                }\n                if (options.playwrightOptions != null) {\n                    Map<String, Object> temp = (Map) options.playwrightOptions.get(\"context\");\n                    if (temp != null) {\n                        map.putAll(temp);\n                    }\n                }\n                method(\"newContext\", browserGuid).params(map).sendWithoutWaiting();\n            } else if (pwm.paramHas(\"type\", \"BrowserContext\")) {\n                browserContextGuid = pwm.getParam(\"guid\");\n                method(\"newPage\", browserContextGuid).sendWithoutWaiting();\n            } else {\n                logger.trace(\"ignoring __create__: {}\", pwm);\n            }\n        } else {\n            wait.receive(pwm);\n        }\n    }\n\n    public PlaywrightMessage sendAndWait(PlaywrightMessage pwm, Predicate<PlaywrightMessage> condition) {\n        boolean wasSubmit = submit;\n        if (condition == null && submit) {\n            submit = false;\n            condition = PlaywrightWait.DOM_CONTENT_LOADED;\n        }\n        // do stuff inside wait to avoid missing messages\n        PlaywrightMessage result = wait.send(pwm, condition);\n        if (result == null && !wasSubmit) {\n            throw new RuntimeException(\"failed to get reply for: \" + pwm);\n        }\n        return result;\n    }\n\n    @Override\n    public DriverOptions getOptions() {\n        return options;\n    }\n\n    @Override\n    public Driver timeout(Integer millis) {\n        options.setTimeout(millis);\n        return this;\n    }\n\n    @Override\n    public Driver timeout() {\n        return timeout(null);\n    }\n\n    private static final Map<String, Object> NO_ARGS = Json.of(\"{ value: { v: 'undefined' }, handles: [] }\").value();\n\n    private PlaywrightMessage evalOnce(String expression, boolean quickly, boolean fireAndForget) {\n        PlaywrightMessage toSend = frame(\"evaluateExpression\")\n                .param(\"expression\", expression)\n                .param(\"isFunction\", false)\n                .param(\"arg\", NO_ARGS);\n        if (quickly) {\n            toSend.setTimeout(options.getRetryInterval());\n        }\n        if (fireAndForget) {\n            toSend.sendWithoutWaiting();\n            return null;\n        }\n        return toSend.send();\n    }\n\n    private PlaywrightMessage eval(String expression) {\n        return eval(expression, false);\n    }\n\n    private PlaywrightMessage eval(String expression, boolean quickly) {\n        PlaywrightMessage pwm = evalOnce(expression, quickly, false);\n        if (pwm.isError()) {\n            String message = \"js eval failed once:\" + expression\n                    + \", error: \" + pwm.getResult();\n            logger.warn(message);\n            options.sleep();\n            pwm = evalOnce(expression, quickly, false); // no wait condition for the re-try\n            if (pwm.isError()) {\n                message = \"js eval failed twice:\" + expression\n                        + \", error: \" + pwm.getResult();\n                logger.error(message);\n                throw new RuntimeException(message);\n            }\n        }\n        return pwm;\n    }\n\n    @Override\n    public Object script(String expression) {\n        return eval(expression).getResultValue();\n    }\n\n    @Override\n    public String elementId(String locator) {\n        return frame(\"querySelector\").param(\"selector\", locator).send().getResult(\"element.guid\");\n    }\n\n    @Override\n    public List<String> elementIds(String locator) {\n        throw new UnsupportedOperationException(\"Not supported yet.\"); //To change body of generated methods, choose Tools | Templates.\n    }\n\n    private void retryIfEnabled(String locator) {\n        if (options.isRetryEnabled()) {\n            waitFor(locator); // will throw exception if not found\n        }\n        if (options.highlight) {\n            // highlight(locator, options.highlightDuration); // instead of this\n            String highlightJs = options.highlight(locator, options.highlightDuration);\n            evalOnce(highlightJs, true, true); // do it safely, i.e. fire and forget\n        }\n    }\n\n    @Override\n    public void setUrl(String url) {\n        frame(\"goto\").param(\"url\", url).param(\"waitUntil\", \"load\").send();\n    }\n\n    @Override\n    public void activate() {\n        page(\"bringToFront\").send();\n    }\n\n    @Override\n    public void refresh() {\n        page(\"reload\").param(\"waitUntil\", \"load\").send();\n    }\n\n    @Override\n    public void reload() {\n        refresh(); // TODO ignore cache ?\n    }\n\n    @Override\n    public void back() {\n        page(\"goBack\").param(\"waitUntil\", \"load\").send();\n    }\n\n    @Override\n    public void forward() {\n        page(\"goForward\").param(\"waitUntil\", \"load\").send();\n    }\n\n    @Override\n    public void maximize() {\n        // https://github.com/microsoft/playwright/issues/1086\n    }\n\n    @Override\n    public void minimize() {\n        // see maximize()\n    }\n\n    @Override\n    public void fullscreen() {\n        // TODO JS\n    }\n\n    @Override\n    public void close() {\n        page(\"close\").send();\n    }\n\n    @Override\n    public void quit() {\n        if (terminated) {\n            return;\n        }\n        terminated = true;\n        method(\"close\", browserGuid).sendWithoutWaiting();\n        client.close();\n        if (command != null) {\n            // cannot force else node process does not terminate gracefully\n            command.close(false);\n        }\n    }\n\n    @Override\n    public String property(String id, String name) {\n        retryIfEnabled(id);\n        return eval(DriverOptions.selector(id) + \"['\" + name + \"']\").getResultValue();\n    }\n\n    @Override\n    public String html(String id) {\n        return property(id, \"outerHTML\");\n    }\n\n    @Override\n    public String text(String id) {\n        return property(id, \"textContent\");\n    }\n\n    @Override\n    public String value(String locator) {\n        return property(locator, \"value\");\n    }\n\n    @Override\n    public String getUrl() {\n        return eval(\"document.location.href\").getResultValue();\n    }\n\n    @Override\n    public void setDimensions(Map<String, Object> map) {\n        // todo\n    }\n\n    @Override\n    public String getTitle() {\n        return eval(\"document.title\").getResultValue();\n    }\n\n    @Override\n    public Element click(String locator) {\n        retryIfEnabled(locator);\n        eval(DriverOptions.selector(locator) + \".click()\");\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public Element value(String locator, String value) {\n        retryIfEnabled(locator);\n        eval(DriverOptions.selector(locator) + \".value = '\" + value + \"'\");\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public String attribute(String id, String name) {\n        retryIfEnabled(id);\n        return eval(DriverOptions.selector(id) + \".getAttribute('\" + name + \"')\").getResultValue();\n    }\n\n    @Override\n    public boolean enabled(String id) {\n        retryIfEnabled(id);\n        PlaywrightMessage pwm = eval(DriverOptions.selector(id) + \".disabled\");\n        Boolean disabled = pwm.getResultValue();\n        return !disabled;\n    }\n\n    @Override\n    public boolean waitUntil(String expression) {\n        return options.retry(() -> {\n            try {\n                return eval(expression, true).getResultValue();\n            } catch (Exception e) {\n                logger.warn(\"waitUntil evaluate failed: {}\", e.getMessage());\n                return false;\n            }\n        }, b -> b, \"waitUntil (js)\", true);\n    }\n\n    @Override\n    public Driver submit() {\n        submit = true;\n        return this;\n    }\n\n    @Override\n    public Element focus(String locator) {\n        retryIfEnabled(locator);\n        eval(options.focusJs(locator));\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public Element clear(String locator) {\n        eval(DriverOptions.selector(locator) + \".value = ''\");\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public Map<String, Object> position(String locator) {\n        return position(locator, false);\n    }\n\n    @Override\n    public Map<String, Object> position(String locator, boolean relative) {\n        boolean submitTemp = submit; // in case we are prepping for a submit().mouse(locator).click()\n        submit = false;\n        retryIfEnabled(locator);\n        Map<String, Object> map = eval(relative ? DriverOptions.getRelativePositionJs(locator) : DriverOptions.getPositionJs(locator)).getResultValue();\n        submit = submitTemp;\n        return map;\n    }\n\n    private PlaywrightMessage evalFrame(String frameGuid, String expression) {\n        return method(\"evaluateExpression\", frameGuid)\n                .param(\"expression\", expression)\n                .param(\"isFunction\", false)\n                .param(\"arg\", NO_ARGS).send();\n    }\n\n    @Override\n    public void switchPage(String titleOrUrl) {\n        if (titleOrUrl == null) {\n            return;\n        }\n        for (Map.Entry<String, Set<String>> entry : pageFrames.entrySet()) {\n            String pageGuid = entry.getKey();\n            String frameGuid = entry.getValue().iterator().next();\n            String title = evalFrame(frameGuid, \"document.title\").getResultValue();\n            if (title != null && title.contains(titleOrUrl)) {\n                currentPage = pageGuid;\n                currentFrame = frameGuid;\n                activate();\n                return;\n            }\n            String url = evalFrame(frameGuid, \"document.location.href\").getResultValue();\n            if (url != null && url.contains(titleOrUrl)) {\n                currentPage = pageGuid;\n                currentFrame = frameGuid;\n                activate();\n                return;\n            }\n        }\n        logger.warn(\"failed to find page by title / url: {}\", titleOrUrl);\n    }\n\n    @Override\n    public void switchPage(int index) {\n        if (index == -1 || index >= pageFrames.size()) {\n            logger.warn(\"not switching page for size {}: {}\", pageFrames.size(), index);\n            return;\n        }\n        List<String> temp = getPages();\n        currentPage = temp.get(index);\n        currentFrame = pageFrames.get(currentPage).iterator().next();\n        activate();\n    }\n\n    private void waitForFrame(String previousFrame) {\n        String previousFrameUrl = frameInfo.get(previousFrame).url;\n        logger.debug(\"waiting for frame url to switch from: {} - {}\", previousFrame, previousFrameUrl);\n        Integer retryInterval = options.getRetryInterval();\n        options.setRetryInterval(1000); // reduce retry interval for this special case\n        options.retry(() -> evalFrame(currentFrame, \"document.location.href\"),\n                pwm -> !pwm.isError() && !pwm.getResultValue().equals(previousFrameUrl), \"waiting for frame context\", false);\n        options.setRetryInterval(retryInterval); // restore\n    }\n\n    @Override\n    public void switchFrame(int index) {\n        String previousFrame = currentFrame;\n        List<String> temp = new ArrayList(pageFrames.get(currentPage));\n        index = index + 1; // the root frame is always zero, api here is consistent with webdriver etc\n        if (index < temp.size()) {\n            currentFrame = temp.get(index);\n            logger.debug(\"switched to frame: {} - pages: {}\", currentFrame, pageFrames);\n            waitForFrame(previousFrame);\n        } else {\n            logger.warn(\"not switching frame for size {}: {}\", temp.size(), index);\n        }\n    }\n\n    @Override\n    public void switchFrame(String locator) {\n        String previousFrame = currentFrame;\n        if (locator == null) {\n            switchFrame(-1);\n        } else {\n            if (locator.startsWith(\"#\")) { // TODO get reference to frame element via locator\n                locator = locator.substring(1);\n            }\n            for (Frame frame : frameInfo.values()) {\n                if (frame.url.contains(locator) || frame.name.contains(locator)) {\n                    currentFrame = frame.frameGuid;\n                    logger.debug(\"switched to frame: {} - pages: {}\", currentFrame, pageFrames);\n                    waitForFrame(previousFrame);\n                    return;\n                }\n            }\n        }\n    }\n\n    @Override\n    public Map<String, Object> getDimensions() {\n        logger.warn(\"getDimensions() not supported\");\n        return Collections.EMPTY_MAP;\n    }\n\n    @Override\n    public List<String> getPages() {\n        return new ArrayList(pageFrames.keySet());\n    }\n\n    @Override\n    public String getDialogText() {\n        return currentDialogText;\n    }\n\n    @Override\n    public byte[] screenshot(boolean embed) {\n        return screenshot(null, embed);\n    }\n\n    @Override\n    public Map<String, Object> cookie(String name) {\n        List<Map> list = getCookies();\n        if (list == null) {\n            return null;\n        }\n        for (Map<String, Object> map : list) {\n            if (map != null && name.equals(map.get(\"name\"))) {\n                return map;\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public void cookie(Map<String, Object> cookie) {\n        if (cookie.get(\"url\") == null && cookie.get(\"domain\") == null) {\n            cookie = new HashMap(cookie); // don't mutate test\n            cookie.put(\"url\", getUrl());\n        }\n        method(\"addCookies\", browserContextGuid).param(\"cookies\", Collections.singletonList(cookie)).send();\n    }\n\n    @Override\n    public void deleteCookie(String name) {\n        List<Map> cookies = getCookies();\n        List<Map> filtered = new ArrayList(cookies.size());\n        for (Map m : cookies) {\n            if (!name.equals(m.get(\"name\"))) {\n                filtered.add(m);\n            }\n        }\n        clearCookies();\n        method(\"addCookies\", browserContextGuid).param(\"cookies\", filtered).send();\n    }\n\n    @Override\n    public void clearCookies() {\n        method(\"clearCookies\", browserContextGuid).send();\n    }\n\n    @Override\n    public List<Map> getCookies() {\n        return method(\"cookies\", browserContextGuid).param(\"urls\", Collections.EMPTY_LIST).send().getResult(\"cookies\");\n    }\n\n    @Override\n    public void dialog(boolean accept) {\n        dialog(accept, null);\n    }\n\n    @Override\n    public void dialog(boolean accept, String input) {\n        this.dialogAccept = accept;\n        this.dialogInput = input;\n    }\n\n    @Override\n    public Element input(String locator, String value) {\n        retryIfEnabled(locator);\n        // focus\n        eval(options.focusJs(locator));\n        Input input = new Input(value);\n        Set<String> pressed = new HashSet();\n        while (input.hasNext()) {\n            char c = input.next();\n            String keyValue = Keys.keyValue(c);\n            if (keyValue != null) {\n                if (Keys.isModifier(c)) {\n                    pressed.add(keyValue);\n                    page(\"keyboardDown\").param(\"key\", keyValue).send();\n                } else {\n                    page(\"keyboardPress\").param(\"key\", keyValue).send();\n                }\n            } else {\n                page(\"keyboardType\").param(\"text\", c + \"\").send();\n            }\n        }\n        for (String keyValue : pressed) {\n            page(\"keyboardUp\").param(\"key\", keyValue).send();\n        }\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    protected int currentMouseXpos;\n    protected int currentMouseYpos;\n\n    @Override\n    public void actions(List<Map<String, Object>> sequence) {\n        boolean submitRequested = submit;\n        submit = false; // make sure only LAST action is handled as a submit()\n        for (Map<String, Object> map : sequence) {\n            List<Map<String, Object>> actions = (List) map.get(\"actions\");\n            if (actions == null) {\n                logger.warn(\"no actions property found: {}\", sequence);\n                return;\n            }\n            Iterator<Map<String, Object>> iterator = actions.iterator();\n            while (iterator.hasNext()) {\n                Map<String, Object> action = iterator.next();\n                String type = (String) action.get(\"type\");\n                if (type == null) {\n                    logger.warn(\"no type property found: {}\", action);\n                    continue;\n                }\n                String pageAction;\n                switch (type) {\n                    case \"pointerMove\":\n                        pageAction = \"mouseMove\";\n                        break;\n                    case \"pointerDown\":\n                        pageAction = \"mouseDown\";\n                        break;\n                    case \"pointerUp\":\n                        pageAction = \"mouseUp\";\n                        break;\n                    default:\n                        logger.warn(\"unexpected action type: {}\", action);\n                        continue;\n\n                }\n                Integer x = (Integer) action.get(\"x\");\n                Integer y = (Integer) action.get(\"y\");\n                if (x != null) {\n                    currentMouseXpos = x;\n                }\n                if (y != null) {\n                    currentMouseYpos = y;\n                }\n                Integer duration = (Integer) action.get(\"duration\");\n                PlaywrightMessage toSend = page(pageAction);\n                if (\"mouseMove\".equals(pageAction) && x != null && y != null) {\n                    toSend.param(\"x\", x).param(\"y\", y);\n                } else {\n                    toSend.params(Collections.EMPTY_MAP);\n                }\n                if (!iterator.hasNext() && submitRequested) {\n                    submit = true;\n                }\n                toSend.send();\n                if (duration != null) {\n                    options.sleep(duration);\n                }\n            }\n        }\n    }\n\n    @Override\n    public Element select(String locator, String text) {\n        retryIfEnabled(locator);\n        eval(options.optionSelector(locator, text));\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public Element select(String locator, int index) {\n        retryIfEnabled(locator);\n        eval(options.optionSelector(locator, index));\n        return DriverElement.locatorExists(this, locator);\n    }\n\n    @Override\n    public byte[] screenshot(String locator, boolean embed) {\n        PlaywrightMessage toSend = page(\"screenshot\").param(\"type\", \"png\");\n        if (locator != null) {\n            toSend.param(\"clip\", position(locator));\n        }\n        PlaywrightMessage pwm = toSend.send();\n        String data = pwm.getResult(\"binary\");\n        byte[] bytes = Base64.getDecoder().decode(data);\n        if (embed) {\n            getRuntime().embed(bytes, ResourceType.PNG);\n        }\n        return bytes;\n    }\n\n    @Override\n    public byte[] pdf(Map<String, Object> options) {\n        if (options == null) {\n            options = Collections.EMPTY_MAP;\n        }\n        PlaywrightMessage pwm = page(\"pdf\").params(options).send();\n        String temp = pwm.getResult(\"pdf\");\n        return Base64.getDecoder().decode(temp);\n    }\n\n    @Override\n    public boolean isTerminated() {\n        return terminated;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/playwright/PlaywrightMessage.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.playwright;\n\nimport com.intuit.karate.Json;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class PlaywrightMessage {\n\n    private static final Logger logger = LoggerFactory.getLogger(PlaywrightMessage.class);\n\n    protected final PlaywrightDriver driver;\n\n    private Integer id;\n    private final String guid;\n    private final String method;\n    private Json params;\n    private Json result;\n    private Json error;\n    private Integer timeout;\n\n    public void sendWithoutWaiting() {\n        driver.send(this);\n    }\n\n    public PlaywrightMessage send() {\n        return send(null);\n    }\n\n    public PlaywrightMessage send(Predicate<PlaywrightMessage> condition) {\n        return driver.sendAndWait(this, condition);\n    }\n\n    public boolean methodIs(String method) {\n        return method.equals(this.method);\n    }\n\n    public Integer getTimeout() {\n        return timeout;\n    }\n\n    public void setTimeout(Integer timeout) {\n        this.timeout = timeout;\n    }\n\n    public PlaywrightMessage param(String path, Object value) {\n        if (params == null) {\n            params = Json.object();\n        }\n        params.set(path, value);\n        return this;\n    }\n\n    public PlaywrightMessage params(Json json) {\n        params = json;\n        return this;\n    }\n\n    public PlaywrightMessage params(Map<String, Object> map) {\n        params = Json.of(map);\n        return this;\n    }\n\n    public <T> T getParam(String path) {\n        if (params == null) {\n            return null;\n        }\n        return params.get(path);\n    }\n\n    public <T> boolean paramHas(String path, T expected) {\n        Object actual = getParam(path);\n        if (actual == null) {\n            return expected == null;\n        }\n        return actual.equals(expected);\n    }\n\n    public Json getResult() {\n        return result;\n    }\n\n    public <T> T getResult(String path) {\n        if (result == null) {\n            return null;\n        }\n        return result.get(path);\n    }\n\n    public <T> T getResultValue() {\n        if (result == null) {\n            return null;\n        }\n        Map<String, Object> map = result.get(\"value\");\n        return (T) recurse(map);\n    }\n\n    private static Object recurse(Map<String, Object> raw) {\n        if (raw == null || raw.isEmpty()) {\n            return null;\n        }\n        String key = raw.keySet().iterator().next();\n        Object val = raw.get(key);\n        switch (key) {\n            case \"o\":\n                List<Map<String, Object>> objectItems = (List) val;\n                Map<String, Object> map = new HashMap(objectItems.size());\n                for (Map<String, Object> entry : objectItems) {\n                    String entryKey = (String) entry.get(\"k\");\n                    Map<String, Object> entryValue = (Map) entry.get(\"v\");\n                    map.put(entryKey, recurse(entryValue));\n                }\n                return map;\n            case \"a\":\n                List<Map<String, Object>> arrayItems = (List) val;\n                List<Object> list = new ArrayList(arrayItems.size());\n                for (Map<String, Object> entry : arrayItems) {\n                    list.add(recurse(entry));\n                }\n                return list;\n        default: // s: string, n: number, b: boolean\n                return val;\n        }\n    }    \n\n    public boolean isError() {\n        return error != null;\n    }\n\n    public Json getError() {\n        return error;\n    }\n\n    public PlaywrightMessage(PlaywrightDriver driver, String method, String guid) {\n        this.driver = driver;\n        this.method = method;\n        this.guid = guid;\n        this.result = null;\n        id = driver.nextId();\n    }\n\n    public PlaywrightMessage(PlaywrightDriver driver, Map<String, Object> map) {\n        this.driver = driver;\n        id = (Integer) map.get(\"id\");\n        guid = (String) map.get(\"guid\");\n        method = (String) map.get(\"method\");\n        Map temp = (Map) map.get(\"params\");\n        if (temp != null) {\n            params = Json.of(temp);\n        }\n        temp = (Map) map.get(\"result\");\n        if (temp != null) {\n            result = Json.of(temp);\n        }\n        temp = (Map) map.get(\"error\");\n        if (temp != null) {\n            if (temp.containsKey(\"error\")) {\n                temp = (Map) temp.get(\"error\");\n            }\n            error = Json.of(temp);\n        }\n    }\n\n    public Map<String, Object> toMap() {\n        Map<String, Object> map = new LinkedHashMap(4);\n        if (id != null) {\n            map.put(\"id\", id);\n        }\n        map.put(\"guid\", guid);\n        map.put(\"method\", method);\n        if (params != null) {\n            map.put(\"params\", params.value());\n        }\n        return map;\n    }\n\n    public Integer getId() {\n        return id;\n    }\n\n    public void setId(Integer id) {\n        this.id = id;\n    }\n\n    public String getGuid() {\n        return guid;\n    }\n\n    public String getMethod() {\n        return method;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        if (id == null) {\n            sb.append(\"[guid: \").append(guid);\n        } else {\n            sb.append(\"[id: \").append(id);\n            sb.append(\", guid: \").append(guid);\n        }\n        if (method != null) {\n            sb.append(\", method: \").append(method);\n        }\n        if (params != null) {\n            sb.append(\", params: \").append(params);\n        }\n        if (result != null) {\n            sb.append(\", results: \").append(result);\n        }\n        if (error != null) {\n            sb.append(\", error: \").append(error);\n        }\n        sb.append(\"]\");\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/playwright/PlaywrightWait.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.playwright;\n\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.driver.DriverOptions;\nimport java.util.function.Predicate;\n\n/**\n *\n * @author pthomas3\n */\npublic class PlaywrightWait {\n\n    private final DriverOptions options;\n    private final PlaywrightDriver driver;\n\n    private PlaywrightMessage lastSent;\n    private Predicate<PlaywrightMessage> condition;\n    private PlaywrightMessage lastReceived;\n\n    private final Predicate<PlaywrightMessage> DEFAULT = m -> lastSent.getId().equals(m.getId());\n    \n    public static final Predicate<PlaywrightMessage> DOM_CONTENT_LOADED = m -> m.methodIs(\"domcontentloaded\");\n\n    public PlaywrightWait(PlaywrightDriver driver, DriverOptions options) {\n        this.driver = driver;\n        this.options = options;\n        logger = options.driverLogger;        \n    }\n\n    // mutable when driver logger is swapped\n    private Logger logger;\n\n    public void setLogger(Logger logger) {\n        this.logger = logger;\n    }\n    \n    public PlaywrightMessage send(PlaywrightMessage pwm, Predicate<PlaywrightMessage> condition) {\n        lastReceived = null;\n        lastSent = pwm;\n        this.condition = condition == null ? DEFAULT : condition;        \n        long timeout = pwm.getTimeout() == null ? options.getTimeout() : pwm.getTimeout();\n        synchronized (this) {\n            logger.trace(\">> wait: {}\", pwm);\n            try {\n                driver.send(pwm);\n                wait(timeout);\n            } catch (InterruptedException e) {\n                logger.error(\"interrupted: {} wait: {}\", e.getMessage(), pwm);\n            }\n        }\n        if (lastReceived != null) {\n            logger.trace(\"<< notified: {}\", pwm);\n        } else {\n            logger.error(\"<< timed out after milliseconds: {} - {}\", timeout, pwm);\n            return null;\n        }\n        return lastReceived;\n    }\n\n    public void receive(PlaywrightMessage pwm) {\n        if (condition == null) {\n            return;\n        }\n        synchronized (this) {\n            if (condition.test(pwm)) {   \n                if (pwm.isError()) {\n                    logger.warn(\"playwright error: {}\", pwm);\n                } else {\n                    logger.trace(\"<< notify: {}\", pwm);\n                }\n                lastReceived = pwm;\n                notify();\n            } else {\n                logger.trace(\"<< ignore: {}\", pwm);\n            }\n        }\n    }    \n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/driver/safari/SafariWebDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.safari;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.driver.WebDriver;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class SafariWebDriver extends WebDriver {\n\n    public static final String DRIVER_TYPE = \"safaridriver\";\n\n    public SafariWebDriver(DriverOptions options) {\n        super(options);\n    }\n\n    public static SafariWebDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        DriverOptions options = new DriverOptions(map, sr, 5555, \"safaridriver\");\n        options.arg(\"--port=\" + options.port);\n        return new SafariWebDriver(options);\n    }\n\n    @Override\n    public void setDimensions(Map<String, Object> map) {\n        Integer x = (Integer) map.remove(\"left\");\n        Integer y = (Integer) map.remove(\"top\");\n        // webdriver bug where 0 or 1 is mis-interpreted as boolean !\n        if (x != null) {\n            map.put(\"x\", x < 2 ? 2 : x);\n        }\n        if (y != null) {\n            map.put(\"y\", y < 2 ? 2 : y);\n        }\n        String json = JsonUtils.toJson(map);\n        http.path(\"window\", \"rect\").postJson(json);\n    }    \n    \n    @Override\n    public void activate() {\n        if (!options.headless) {\n            try {\n                switch (FileUtils.getOsType()) {\n                    case MACOSX:\n                        Runtime.getRuntime().exec(new String[]{\"osascript\", \"-e\", \"tell app \\\"Safari\\\" to activate\"});\n                        break;\n                    default:\n\n                }\n            } catch (Exception e) {\n                logger.warn(\"native window switch failed: {}\", e.getMessage());\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/graal/JsArray.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.graal;\n\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.ProxyArray;\n\n/**\n *\n * @author pthomas3\n */\npublic class JsArray implements ProxyArray {\n\n    private final Object[] array;\n\n    public JsArray(Object[] array) {\n        this.array = array;\n    }\n\n    @Override\n    public Object get(long index) {\n        return array[(int) index];\n    }\n\n    @Override\n    public void set(long index, Value value) {\n        throw new UnsupportedOperationException(\"set by index not supported\");\n    }\n\n    @Override\n    public long getSize() {\n        return array.length;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/graal/JsEngine.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.graal;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.KarateException;\nimport com.intuit.karate.StringUtils;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Function;\nimport org.graalvm.polyglot.Context;\nimport org.graalvm.polyglot.Engine;\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.ProxyExecutable;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class JsEngine {\n\n    private static final Logger logger = LoggerFactory.getLogger(JsEngine.class);\n\n    private static final String JS = \"js\";\n    private static final String JS_FOREIGN_OBJECT_PROTOTYPE = \"js.foreign-object-prototype\";\n    private static final String JS_NASHORN_COMPAT = \"js.nashorn-compat\";\n    private static final String JS_ECMASCRIPT_VERSION = \"js.ecmascript-version\";\n    private static final String ENGINE_WARN_INTERPRETER_ONLY = \"engine.WarnInterpreterOnly\";\n    private static final String V_2021 = \"2021\";\n    private static final String TRUE = \"true\";\n    private static final String FALSE = \"false\";\n\n    private static final ThreadLocal<JsEngine> GLOBAL_JS_ENGINE = new ThreadLocal<JsEngine>() {\n        @Override\n        protected JsEngine initialValue() {\n            return new JsEngine(createContext(null));\n        }\n    };\n\n    private static Context createContext(Engine engine) {\n        if (engine == null) {\n            engine = Engine.newBuilder()\n                    .option(ENGINE_WARN_INTERPRETER_ONLY, FALSE)\n                    .build();\n        }\n        return Context.newBuilder(JS)\n                .allowExperimentalOptions(true)\n                .allowAllAccess(true)\n                .option(JS_NASHORN_COMPAT, TRUE)\n                .option(JS_ECMASCRIPT_VERSION, V_2021)\n                .option(JS_FOREIGN_OBJECT_PROTOTYPE, TRUE)\n                .engine(engine).build();\n    }\n\n    public static JsValue evalGlobal(String src) {\n        return global().eval(src);\n    }\n\n    public static JsValue evalGlobal(InputStream is) {\n        return global().eval(is);\n    }\n\n    public static JsEngine global() {\n        return GLOBAL_JS_ENGINE.get();\n    }\n\n    public static void remove() {\n        GLOBAL_JS_ENGINE.remove();\n    }\n\n    public static JsEngine local() {\n        Engine engine = GLOBAL_JS_ENGINE.get().context.getEngine();\n        return new JsEngine(createContext(engine));\n    }\n\n    //==========================================================================\n    //\n    public final Context context;\n    public final Value bindings;\n\n    private JsEngine(Context context) {\n        this.context = context;\n        bindings = context.getBindings(JS);\n    }\n\n    public JsEngine copy() {\n        JsEngine temp = local();\n        for (String key : bindings.getMemberKeys()) {\n            Value v = bindings.getMember(key);\n            if (v.isHostObject()) {\n                temp.bindings.putMember(key, v);\n            } else {\n                temp.bindings.putMember(key, JsValue.toJava(v));\n            }\n        }\n        return temp;\n    }\n\n    public Value attach(Value value) {\n        try {\n            return context.asValue(value);\n        } catch (Exception e) {\n            logger.trace(\"context switch: {}\", e.getMessage());\n            CharSequence source = value.getSourceLocation().getCharacters();\n            return evalForValue(\"(\" + source + \")\");\n        }\n    }\n\n    public Object attachAll(Object o) {\n        if (o instanceof List) {\n            List list = (List) o;\n            List result = new ArrayList(list.size());\n            list.forEach(v -> result.add(attachAll(v)));\n            return result;\n        } else if (o instanceof Map) {\n            Map map = (Map) o;\n            Map result = new LinkedHashMap(map.size());\n            map.forEach((k, v) -> result.put(k, attachAll(v)));\n            return result;\n        } else if (o instanceof Value) {\n            return attach((Value) o);\n        } else {\n            return o;\n        }\n    }\n\n    public JsValue eval(InputStream is) {\n        return eval(FileUtils.toString(is));\n    }\n\n    public JsValue eval(File file) {\n        return eval(FileUtils.toString(file));\n    }\n\n    public JsValue eval(String exp) {\n        return new JsValue(evalForValue(exp));\n    }\n\n    public Value evalForValue(String exp) {\n        return context.eval(JS, exp);\n    }\n\n    public void put(String key, Object value) {\n        bindings.putMember(key, JsValue.fromJava(value));\n    }\n\n    public void remove(String key) {\n        bindings.removeMember(key);\n    }\n\n    public void putAll(Map<String, Object> map) {\n        map.forEach((k, v) -> put(k, v));\n    }\n\n    public JsValue get(String key) {\n        if (bindings.hasMember(key)) {\n            return new JsValue(bindings.getMember(key));\n        }\n        throw new RuntimeException(\"no such variable: \" + key);\n    }\n\n    public static Object execute(ProxyExecutable function, Object... args) {\n        Value[] values = new Value[args.length];\n        for (int i = 0; i < args.length; i++) {\n            values[i] = Value.asValue(args[i]);\n        }\n        return function.execute(values);\n    }\n\n    public static Value execute(Value function, Object... args) {\n        for (int i = 0; i < args.length; i++) {\n            args[i] = JsValue.fromJava(args[i]);\n        }\n        return function.execute(args);\n    }\n\n    public Value evalWith(Value value, String src, boolean returnValue) {\n        return evalWith(value.getMemberKeys(), value::getMember, src, returnValue);\n    }\n\n    public Value evalWith(Map<String, Object> variables, String src, boolean returnValue) {\n        return evalWith(variables.keySet(), variables::get, src, returnValue);\n    }\n\n    public Value evalWith(Set<String> names, Function<String, Object> getVariable, String src, boolean returnValue) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"(function($){ \");\n        Map<String, Object> arg = new HashMap(names.size());\n        for (String name : names) {\n            sb.append(\"let \").append(name).append(\" = $.\").append(name).append(\"; \");\n            arg.put(name, getVariable.apply(name));\n        }\n        if (returnValue) {\n            sb.append(\"return \");\n        }\n        sb.append(src).append(\" })\");\n        Value function = evalForValue(sb.toString());\n        return function.execute(JsValue.fromJava(arg));\n    }\n\n    public static KarateException fromJsEvalException(String js, Exception e, String message) {\n        // do our best to make js error traces informative, else thrown exception seems to\n        // get swallowed by the java reflection based method invoke flow\n        StackTraceElement[] stack = e.getStackTrace();\n        StringBuilder sb = new StringBuilder();\n        if (message != null) {\n            sb.append(message).append('\\n');\n        }\n        sb.append(\"js failed:\\n>>>>\\n\");\n        List<String> lines = StringUtils.toStringLines(js);\n        int index = 0;\n        for (String line : lines) {\n            sb.append(String.format(\"%02d\", ++index)).append(\": \").append(line).append('\\n');\n        }\n        sb.append(\"<<<<\\n\");\n        sb.append(e.toString()).append('\\n');\n        for (int i = 0; i < stack.length; i++) {\n            String line = stack[i].toString();\n            sb.append(\"- \").append(line).append('\\n');\n            if (line.startsWith(\"<js>\") || i > 5) {\n                break;\n            }\n        }\n        return new KarateException(sb.toString());\n    }\n\n    @Override\n    public String toString() {\n        return context.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/graal/JsFunction.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.graal;\n\nimport com.intuit.karate.core.ScenarioEngine;\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.ProxyExecutable;\nimport org.graalvm.polyglot.proxy.ProxyInstantiable;\nimport org.graalvm.polyglot.proxy.ProxyObject;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author peter\n */\npublic abstract class JsFunction implements ProxyObject {\n\n    protected static final Logger logger = LoggerFactory.getLogger(JsFunction.class);\n\n    public static final Object LOCK = new Object();\n\n    protected final Value value;\n\n    protected JsFunction(Value v) {\n        this.value = v;\n    }\n\n    public static ProxyExecutable wrap(Value value) {\n        return new Executable(value, true);\n    }\n\n    public Value getValue() {\n        return value;\n    }\n\n    @Override\n    public void putMember(String key, Value value) {\n        this.value.putMember(key, new JsValue(value).value);\n    }\n\n    @Override\n    public boolean hasMember(String key) {\n        return value.hasMember(key);\n    }\n\n    @Override\n    public Object getMemberKeys() {\n        return value.getMemberKeys().toArray(new String[0]);\n    }\n\n    @Override\n    public Object getMember(String key) {\n        return new JsValue(value.getMember(key)).value;\n    }\n\n    @Override\n    public boolean removeMember(String key) {\n        return value.removeMember(key);\n    }\n\n    public static class Executable extends JsFunction implements ProxyExecutable {\n\n        private final boolean lock;\n        private final String source;\n\n        protected Executable(Value value) {\n            this(value, false);\n        }\n\n        protected Executable(Value value, boolean lock) {\n            super(value);\n            this.lock = lock;\n            source = \"(\" + value.getSourceLocation().getCharacters() + \")\";\n        }\n\n        public Object execute(JsEngine je, Object... args) {\n            Object[] newArgs = new Object[args.length];\n            for (int i = 0; i < newArgs.length; i++) {\n                newArgs[i] = JsValue.fromJava(args[i]);\n            }\n            Value attached = je.evalForValue(source);\n            return new JsValue(attached.execute(newArgs)).value;\n        }\n\n        @Override\n        public Object execute(Value... args) {\n            Object[] newArgs = new Object[args.length];\n            for (int i = 0; i < newArgs.length; i++) {\n                newArgs[i] = JsValue.fromJava(args[i]);\n            }\n            if (lock) {\n                synchronized (LOCK) {\n                    return new JsValue(value.execute(newArgs)).value;\n                }\n            }\n            ScenarioEngine se = ScenarioEngine.get();\n            JsEngine je = se == null ? null : se.getJsEngine();\n            if (je == null || je.context.equals(value.getContext())) {\n                return new JsValue(value.execute(newArgs)).value;\n            }\n            Value attached = je.evalForValue(source);\n            return new JsValue(attached.execute(newArgs)).value;\n        }\n\n    }\n\n    protected static class Instantiable extends Executable implements ProxyInstantiable {\n\n        protected Instantiable(Value value) {\n            super(value);\n        }\n\n        @Override\n        public Object newInstance(Value... args) {\n            Object[] newArgs = new Object[args.length];\n            for (int i = 0; i < newArgs.length; i++) {\n                newArgs[i] = JsValue.fromJava(args[i]);\n            }\n            JsEngine je = ScenarioEngine.get().getJsEngine();\n            Value wrappedWithParentheses = je.evalForValue(\n                    \"(\"\n                    + value.getSourceLocation().getCharacters()\n                    + \")\"); // Evaluate '(function)' to get function value\n            // execute the function with arguments\n            return new JsValue(wrappedWithParentheses.execute(newArgs)).value;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/graal/JsLambda.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.graal;\n\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport org.graalvm.polyglot.Value;\n\n/**\n *\n * @author pthomas3\n */\npublic class JsLambda extends JsFunction.Instantiable implements Consumer, Function, Runnable {    \n\n    public JsLambda(Value v) {\n        super(v);\n    }\n\n    @Override\n    public void accept(Object arg) {\n        JsEngine.execute(this, arg);\n    }\n\n    @Override\n    public Object apply(Object arg) {\n        return JsEngine.execute(this, arg);\n    }\n\n    @Override\n    public void run() {\n        JsEngine.execute(this);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/graal/JsList.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.graal;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.ListIterator;\nimport java.util.Set;\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.ProxyArray;\n\n/**\n *\n * @author pthomas3\n */\npublic class JsList implements ProxyArray, List {\n\n    public static final JsList EMPTY = new JsList(Collections.EMPTY_LIST);\n\n    private final List list;\n\n    public JsList(Collection collection) {\n        this(new ArrayList(collection));\n    }\n\n    public JsList(List list) {\n        this.list = list;\n    }\n\n    public List getList() {\n        return list;\n    }\n\n    @Override\n    public Object get(long index) {\n        return JsValue.fromJava(list.get((int) index));\n    }\n\n    @Override\n    public void set(long index, Value value) {\n        if (index >= list.size()) {\n            list.add(null); // support js push()\n        }\n        list.set((int) index, JsValue.toJava(value));\n    }\n\n    @Override\n    public long getSize() {\n        return list.size();\n    }\n\n    @Override\n    public boolean remove(long index) {\n        list.remove((int) index);\n        return true;\n    }\n\n    @Override\n    public String toString() {\n        return list.toString();\n    }\n    \n    //==========================================================================\n    //\n    @Override\n    public int size() {\n        return list.size();\n    }\n\n    @Override\n    public boolean isEmpty() {\n        return list.isEmpty();\n    }\n\n    @Override\n    public boolean contains(Object o) {\n        return list.contains(o);\n    }\n\n    @Override\n    public Iterator iterator() {\n        return list.iterator();\n    }\n\n    @Override\n    public Object[] toArray() {\n        return list.toArray();\n    }\n\n    @Override\n    public Object[] toArray(Object[] a) {\n        return list.toArray(a);\n    }\n\n    @Override\n    public boolean add(Object e) {\n        return list.add(e);\n    }\n\n    @Override\n    public boolean remove(Object o) {\n        return list.remove(o);\n    }\n\n    @Override\n    public boolean containsAll(Collection c) {\n        return list.containsAll(c);\n    }\n\n    @Override\n    public boolean addAll(Collection c) {\n        return list.addAll(c);\n    }\n\n    @Override\n    public boolean addAll(int index, Collection c) {\n        return list.addAll(index, c);\n    }\n\n    @Override\n    public boolean removeAll(Collection c) {\n        return list.removeAll(c);\n    }\n\n    @Override\n    public boolean retainAll(Collection c) {\n        return list.retainAll(c);\n    }\n\n    @Override\n    public void clear() {\n        list.clear();\n    }\n\n    @Override\n    public Object get(int index) {\n        return get(index);\n    }\n\n    @Override\n    public Object set(int index, Object element) {\n        return list.set(index, element);\n    }\n\n    @Override\n    public void add(int index, Object element) {\n        list.add(index, element);\n    }\n\n    @Override\n    public Object remove(int index) {\n        return list.remove(index);\n    }\n\n    @Override\n    public int indexOf(Object o) {\n        return list.indexOf(o);\n    }\n\n    @Override\n    public int lastIndexOf(Object o) {\n        return list.lastIndexOf(o);\n    }\n\n    @Override\n    public ListIterator listIterator() {\n        return list.listIterator();\n    }\n\n    @Override\n    public ListIterator listIterator(int index) {\n        return list.listIterator(index);\n    }\n\n    @Override\n    public List subList(int fromIndex, int toIndex) {\n        return list.subList(fromIndex, toIndex);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/graal/JsMap.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.graal;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Set;\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.ProxyObject;\n\n/**\n *\n * @author pthomas3\n */\npublic class JsMap implements ProxyObject, Map {\n\n    public static final JsMap EMPTY = new JsMap(Collections.EMPTY_MAP);\n\n    private final Map map;\n\n    public JsMap(Map map) {\n        this.map = map;\n    }\n\n    public Map getMap() {\n        return map;\n    }\n\n    @Override\n    public Object getMember(String key) {\n        return JsValue.fromJava(map.get(key));\n    }\n\n    @Override\n    public Object getMemberKeys() {\n        return new JsArray(map.keySet().toArray());\n    }\n\n    @Override\n    public boolean hasMember(String key) {\n        return map.containsKey(key);\n    }\n\n    @Override\n    public void putMember(String key, Value value) {\n        map.put(key, JsValue.toJava(value));\n    }\n\n    @Override\n    public boolean removeMember(String key) { // not supported by graal\n        return map.remove(key) != null;\n    }\n\n    @Override\n    public String toString() {\n        return map.toString();\n    }\n\n    //==========================================================================\n    //\n    @Override\n    public int size() {\n        return map.size();\n    }\n\n    @Override\n    public boolean isEmpty() {\n        return map.isEmpty();\n    }\n\n    @Override\n    public boolean containsKey(Object key) {\n        return map.containsKey(key);\n    }\n\n    @Override\n    public boolean containsValue(Object value) {\n        return map.containsValue(value);\n    }\n\n    @Override\n    public Object get(Object key) {\n        return map.get(key);\n    }\n\n    @Override\n    public Object put(Object key, Object value) {\n        return map.put(key, value);\n    }\n\n    @Override\n    public Object remove(Object key) {\n        return map.remove(key);\n    }\n\n    @Override\n    public void putAll(Map m) {\n        map.putAll(m);\n    }\n\n    @Override\n    public void clear() {\n        map.clear();\n    }\n\n    @Override\n    public Set keySet() {\n        return map.keySet();\n    }\n\n    @Override\n    public Collection values() {\n        return map.values();\n    }\n\n    @Override\n    public Set entrySet() {\n        return map.entrySet();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/graal/JsValue.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.graal;\n\nimport com.intuit.karate.JsonUtils;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Function;\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.Proxy;\nimport org.graalvm.polyglot.proxy.ProxyExecutable;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.w3c.dom.Node;\n\n/**\n *\n * @author pthomas3\n */\npublic class JsValue {\n\n    private static final Logger logger = LoggerFactory.getLogger(JsValue.class);\n\n    public static enum Type {\n        OBJECT,\n        ARRAY,\n        FUNCTION,\n        XML,\n        NULL,\n        OTHER\n    }\n\n    public static final JsValue NULL = new JsValue(Value.asValue(null));\n\n    private final Value original;\n    protected final Object value;\n    public final Type type;\n\n    public JsValue(Value v) {\n        if (v == null) {\n            throw new RuntimeException(\"JsValue() constructor argument has to be not-null\");\n        }\n        this.original = v;\n        try {\n            if (v.isNull()) {\n                value = null;\n                type = Type.NULL;\n            } else if (v.isHostObject()) {\n                if (v.isMetaObject()) { // java.lang.Class !\n                    value = v; // special case, keep around as graal value\n                } else {\n                    value = v.asHostObject();\n                }\n                type = Type.OTHER;\n            } else if (v.isProxyObject()) {\n                Object o = v.asProxyObject();\n                if (o instanceof JsXml) {\n                    value = ((JsXml) o).getNode();\n                    type = Type.XML;\n                } else if (o instanceof JsMap) {\n                    value = ((JsMap) o).getMap();\n                    type = Type.OBJECT;\n                } else if (o instanceof JsList) {\n                    value = ((JsList) o).getList();\n                    type = Type.ARRAY;\n                } else if (o instanceof ProxyExecutable) {\n                    value = o;\n                    type = Type.FUNCTION;\n                } else { // e.g. custom bridge, e.g. Request\n                    value = v.as(Object.class);\n                    type = Type.OTHER;\n                }\n            } else if (v.hasArrayElements()) {\n                int size = (int) v.getArraySize();\n                List list = new ArrayList(size);\n                for (int i = 0; i < size; i++) {\n                    Value child = v.getArrayElement(i);\n                    list.add(new JsValue(child).value);\n                }\n                value = list;\n                type = Type.ARRAY;\n            } else if (v.hasMembers()) {\n                if (v.canExecute()) {\n                    if (v.canInstantiate()) {\n                        // js functions have members, can be executed and are instantiable\n                        value = new JsFunction.Instantiable(v);\n                    } else {\n                        // js, but anonymous / arrow function\n                        value = new JsFunction.Executable(v);\n                    }\n                    type = Type.FUNCTION;\n                } else {\n                    Set<String> keys = v.getMemberKeys();\n                    Map<String, Object> map = new LinkedHashMap(keys.size());\n                    for (String key : keys) {\n                        Value child = v.getMember(key);\n                        map.put(key, new JsValue(child).value);\n                    }\n                    value = map;\n                    type = Type.OBJECT;\n                }\n            } else if (v.isNumber()) {\n                value = v.as(Number.class);\n                type = Type.OTHER;\n            } else if (v.isBoolean()) {\n                value = v.asBoolean();\n                type = Type.OTHER;\n            } else if (v.isString()) {\n                value = v.asString();\n                type = Type.OTHER;\n            } else {\n                value = v.as(Object.class);\n                if (value instanceof Function) {\n                    type = Type.FUNCTION;\n                } else {\n                    type = Type.OTHER;\n                }\n            }\n        } catch (Exception e) {\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"js conversion failed\", e);\n            }\n            throw e;\n        }\n    }\n\n    public <T> T getValue() {\n        return (T) value;\n    }\n\n    public Map<String, Object> getAsMap() {\n        return (Map) value;\n    }\n\n    public List getAsList() {\n        return (List) value;\n    }\n\n    public Value getOriginal() {\n        return original;\n    }\n\n    public boolean isXml() {\n        return type == Type.XML;\n    }\n\n    public boolean isNull() {\n        return type == Type.NULL;\n    }\n\n    public boolean isObject() {\n        return type == Type.OBJECT;\n    }\n\n    public boolean isArray() {\n        return type == Type.ARRAY;\n    }\n\n    public boolean isTrue() {\n        if (type != Type.OTHER || !Boolean.class.equals(value.getClass())) {\n            return false;\n        }\n        return (Boolean) value;\n    }\n\n    public boolean isFunction() {\n        return type == Type.FUNCTION;\n    }\n\n    public boolean isOther() {\n        return type == Type.OTHER;\n    }\n\n    @Override\n    public String toString() {\n        return original.toString();\n    }\n\n    public String toJsonOrXmlString(boolean pretty) {\n        return JsonUtils.toString(value, pretty);\n    }\n\n    public String getAsString() {\n        return JsonUtils.toString(value);\n    }\n\n    public static Object fromJava(Object o) {\n        if (o instanceof Function || o instanceof Proxy) {\n            return o;\n        } else if (o instanceof List) {\n            return new JsList((List) o);\n        } else if (o instanceof Map) {\n            return new JsMap((Map) o);\n        } else if (o instanceof Node) {\n            return new JsXml((Node) o);\n        } else {\n            return o;\n        }\n    }\n\n    public static Object toJava(Value v) {\n        return new JsValue(v).getValue();\n    }\n\n    public static Object unWrap(Object o) {\n        if (o instanceof JsXml) {\n            return ((JsXml) o).getNode();\n        } else if (o instanceof JsMap) {\n            return ((JsMap) o).getMap();\n        } else if (o instanceof JsList) {\n            return ((JsList) o).getList();\n        } else {\n            return o;\n        }\n    }\n\n    public static byte[] toBytes(Value v) {\n        return JsonUtils.toBytes(toJava(v));\n    }\n\n    public static boolean isTruthy(Object o) {\n        if (o == null) {\n            return false;\n        }\n        if (o instanceof Boolean) {\n            return ((Boolean) o);\n        }\n        if (o instanceof Number) {\n            return ((Number) o).doubleValue() != 0.0;\n        }\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/graal/JsXml.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.graal;\n\nimport com.intuit.karate.XmlUtils;\nimport java.util.Map;\nimport org.w3c.dom.Node;\n\n/**\n * used to detect xml within the js bridge / log-pretty routine\n * @author pthomas3\n */\npublic class JsXml extends JsMap { \n    \n    private final Node node;\n    \n    public JsXml(Node node) {\n        super((Map) XmlUtils.toObject(node));\n        this.node = node;\n    }\n\n    public Node getNode() {\n        return node;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/graal/Methods.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.graal;\n\n/**\n *\n * @author pthomas3\n */\npublic class Methods {\n\n    private Methods() {\n        // only static methods\n    }\n\n    @FunctionalInterface\n    public interface FunVar<T, U> {\n\n        U call(T... args);\n\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ApacheHttpClient.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.Constants;\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.core.Config;\nimport com.intuit.karate.core.ScenarioEngine;\nimport io.netty.handler.codec.http.cookie.ClientCookieDecoder;\nimport io.netty.handler.codec.http.cookie.ServerCookieEncoder;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.InetAddress;\nimport java.net.InetSocketAddress;\nimport java.net.Proxy;\nimport java.net.ProxySelector;\nimport java.net.SocketAddress;\nimport java.net.URI;\nimport java.security.KeyStore;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport javax.net.ssl.SSLContext;\nimport org.apache.http.Header;\nimport org.apache.http.HttpEntity;\nimport org.apache.http.HttpException;\nimport org.apache.http.HttpHost;\nimport org.apache.http.HttpMessage;\nimport org.apache.http.HttpRequestInterceptor;\nimport org.apache.http.auth.AuthScope;\nimport org.apache.http.auth.NTCredentials;\nimport org.apache.http.auth.UsernamePasswordCredentials;\nimport org.apache.http.client.ClientProtocolException;\nimport org.apache.http.client.CookieStore;\nimport org.apache.http.client.CredentialsProvider;\nimport org.apache.http.client.config.AuthSchemes;\nimport org.apache.http.client.config.RequestConfig;\nimport org.apache.http.client.entity.EntityBuilder;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.RequestBuilder;\nimport org.apache.http.client.utils.URIBuilder;\nimport org.apache.http.config.Registry;\nimport org.apache.http.config.RegistryBuilder;\nimport org.apache.http.config.SocketConfig;\nimport nodebug.io.karatelabs.LenientSslConnectionSocketFactory;\nimport org.apache.http.conn.ssl.NoopHostnameVerifier;\nimport org.apache.http.conn.ssl.SSLConnectionSocketFactory;\nimport org.apache.http.conn.ssl.TrustAllStrategy;\nimport org.apache.http.conn.ssl.TrustSelfSignedStrategy;\nimport org.apache.http.cookie.Cookie;\nimport org.apache.http.cookie.CookieOrigin;\nimport org.apache.http.cookie.CookieSpecProvider;\nimport org.apache.http.cookie.MalformedCookieException;\nimport org.apache.http.impl.client.BasicCookieStore;\nimport org.apache.http.impl.client.BasicCredentialsProvider;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClientBuilder;\nimport org.apache.http.impl.client.LaxRedirectStrategy;\nimport org.apache.http.impl.conn.SystemDefaultRoutePlanner;\nimport org.apache.http.impl.cookie.DefaultCookieSpec;\nimport org.apache.http.protocol.HttpContext;\nimport org.apache.http.ssl.SSLContextBuilder;\nimport org.apache.http.ssl.SSLContexts;\nimport org.brotli.dec.BrotliInputStream;\n\n/**\n *\n * @author pthomas3\n */\npublic class ApacheHttpClient implements HttpClient, HttpRequestInterceptor {\n\n    private final ScenarioEngine engine;\n    private final Logger logger;\n    private final HttpLogger httpLogger;\n\n    private HttpClientBuilder clientBuilder;\n    private CookieStore cookieStore;\n\n    public static class LenientCookieSpec extends DefaultCookieSpec {\n\n        static final String KARATE = \"karate\";\n\n        public LenientCookieSpec() {\n            super(new String[]{\"EEE, dd-MMM-yy HH:mm:ss z\", \"EEE, dd MMM yyyy HH:mm:ss Z\"}, false);\n        }\n\n        @Override\n        public boolean match(Cookie cookie, CookieOrigin origin) {\n            return true;\n        }\n\n        @Override\n        public void validate(Cookie cookie, CookieOrigin origin) throws MalformedCookieException {\n            // do nothing\n        }\n\n        public static Registry<CookieSpecProvider> registry() {\n            CookieSpecProvider specProvider = (HttpContext hc) -> new LenientCookieSpec();\n            return RegistryBuilder.<CookieSpecProvider>create()\n                    .register(KARATE, specProvider).build();\n        }\n\n    }\n\n    public ApacheHttpClient(ScenarioEngine engine) {\n        this.engine = engine;\n        logger = engine.logger;\n        httpLogger = new HttpLogger(logger);\n        configure(engine.getConfig());\n    }\n\n    private void configure(Config config) {\n        clientBuilder = HttpClientBuilder.create();\n        if (config.isHttpRetryEnabled()) {\n            clientBuilder.setRetryHandler(new CustomHttpRequestRetryHandler(logger));\n        } else {\n            clientBuilder.disableAutomaticRetries();\n        }\n\n        if (!config.isFollowRedirects()) {\n            clientBuilder.disableRedirectHandling();\n        } else { // support redirect on POST by default\n            clientBuilder.setRedirectStrategy(LaxRedirectStrategy.INSTANCE);\n        }\n        cookieStore = new BasicCookieStore();\n        clientBuilder.setDefaultCookieStore(cookieStore);\n        clientBuilder.setDefaultCookieSpecRegistry(LenientCookieSpec.registry());\n        clientBuilder.useSystemProperties();\n        if (config.isSslEnabled()) {\n            // System.setProperty(\"jsse.enableSNIExtension\", \"false\");\n            String algorithm = config.getSslAlgorithm(); // could be null\n            KeyStore trustStore = engine.getKeyStore(config.getSslTrustStore(), config.getSslTrustStorePassword(), config.getSslTrustStoreType());\n            KeyStore keyStore = engine.getKeyStore(config.getSslKeyStore(), config.getSslKeyStorePassword(), config.getSslKeyStoreType());\n            SSLContext sslContext;\n            try {\n                SSLContextBuilder builder = SSLContexts.custom()\n                        .setProtocol(algorithm); // will default to TLS if null\n                if (trustStore == null && config.isSslTrustAll()) {\n                    builder = builder.loadTrustMaterial(new TrustAllStrategy());\n                } else {\n                    if (config.isSslTrustAll()) {\n                        builder = builder.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy());\n                    } else {\n                        builder = builder.loadTrustMaterial(trustStore, null); // will use system / java default\n                    }\n                }\n                if (keyStore != null) {\n                    char[] keyPassword = config.getSslKeyStorePassword() == null ? null : config.getSslKeyStorePassword().toCharArray();\n                    builder = builder.loadKeyMaterial(keyStore, keyPassword);\n                }\n                sslContext = builder.build();\n                SSLConnectionSocketFactory socketFactory;\n                if (keyStore != null) {\n                    socketFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());\n                } else {\n                    socketFactory = new LenientSslConnectionSocketFactory(sslContext, new NoopHostnameVerifier());\n                }\n                clientBuilder.setSSLSocketFactory(socketFactory);\n            } catch (Exception e) {\n                logger.error(\"ssl context init failed: {}\", e.getMessage());\n                throw new RuntimeException(e);\n            }\n        }\n        RequestConfig.Builder configBuilder = RequestConfig.custom()\n                .setCookieSpec(LenientCookieSpec.KARATE)\n                .setConnectTimeout(config.getConnectTimeout())\n                .setSocketTimeout(config.getReadTimeout());\n        if (config.getLocalAddress() != null) {\n            try {\n                InetAddress localAddress = InetAddress.getByName(config.getLocalAddress());\n                configBuilder.setLocalAddress(localAddress);\n            } catch (Exception e) {\n                logger.warn(\"failed to resolve local address: {} - {}\", config.getLocalAddress(), e.getMessage());\n            }\n        }\n        if (config.isNtlmEnabled()) {\n            List<String> authSchemes = new ArrayList<>();\n            authSchemes.add(AuthSchemes.NTLM);\n            CredentialsProvider credentialsProvider = new BasicCredentialsProvider();\n            NTCredentials ntCredentials = new NTCredentials(\n                config.getNtlmUsername(), config.getNtlmPassword(), config.getNtlmWorkstation(), config.getNtlmDomain());\n            credentialsProvider.setCredentials(AuthScope.ANY, ntCredentials);\n            clientBuilder.setDefaultCredentialsProvider(credentialsProvider);\n            configBuilder.setTargetPreferredAuthSchemes(authSchemes);\n        }\n        clientBuilder.setDefaultRequestConfig(configBuilder.build());\n        SocketConfig.Builder socketBuilder = SocketConfig.custom().setSoTimeout(config.getConnectTimeout());\n        clientBuilder.setDefaultSocketConfig(socketBuilder.build());\n        if (config.getProxyUri() != null) {\n            try {\n                URI proxyUri = new URIBuilder(config.getProxyUri()).build();\n                clientBuilder.setProxy(new HttpHost(proxyUri.getHost(), proxyUri.getPort(), proxyUri.getScheme()));\n                if (config.getProxyUsername() != null && config.getProxyPassword() != null) {\n                    CredentialsProvider credsProvider = new BasicCredentialsProvider();\n                    credsProvider.setCredentials(\n                            new AuthScope(proxyUri.getHost(), proxyUri.getPort()),\n                            new UsernamePasswordCredentials(config.getProxyUsername(), config.getProxyPassword()));\n                    clientBuilder.setDefaultCredentialsProvider(credsProvider);\n                }\n                if (config.getNonProxyHosts() != null) {\n                    ProxySelector proxySelector = new ProxySelector() {\n                        private final List<String> proxyExceptions = config.getNonProxyHosts();\n\n                        @Override\n                        public List<Proxy> select(URI uri) {\n                            return Collections.singletonList(proxyExceptions.contains(uri.getHost())\n                                    ? Proxy.NO_PROXY\n                                    : new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyUri.getHost(), proxyUri.getPort())));\n                        }\n\n                        @Override\n                        public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {\n                            logger.info(\"connect failed to uri: {}\", uri, ioe);\n                        }\n                    };\n                    clientBuilder.setRoutePlanner(new SystemDefaultRoutePlanner(proxySelector));\n                }\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n        clientBuilder.addInterceptorLast(this);\n    }\n\n    @Override\n    public void setConfig(Config config) {\n        configure(config);\n    }\n\n    @Override\n    public Config getConfig() {\n        return engine.getConfig();\n    }\n\n    private HttpRequest request;\n\n    @Override\n    public Response invoke(HttpRequest request) {\n        this.request = request;\n        RequestBuilder requestBuilder = RequestBuilder.create(request.getMethod()).setUri(request.getUrl());\n        if (request.getBody() != null) {\n            EntityBuilder entityBuilder = EntityBuilder.create().setBinary(request.getBody());\n            List<String> transferEncoding = request.getHeaderValues(HttpConstants.HDR_TRANSFER_ENCODING);\n            if (transferEncoding != null) {\n                for (String te : transferEncoding) {\n                    if (te == null) {\n                        continue;\n                    }\n                    if (te.contains(\"chunked\")) { // can be comma delimited as per spec\n                        entityBuilder.chunked();\n                    }\n                    if (te.contains(\"gzip\")) {\n                        entityBuilder.gzipCompress();\n                    }\n                }\n                request.removeHeader(HttpConstants.HDR_TRANSFER_ENCODING);\n            }\n            requestBuilder.setEntity(entityBuilder.build());\n        }\n        if (request.getHeaders() != null) {\n            request.getHeaders().forEach((k, vals) -> vals.forEach(v -> requestBuilder.addHeader(k, v)));\n        }        \n        CloseableHttpResponse httpResponse;\n        byte[] bytes;\n        try (CloseableHttpClient client = clientBuilder.build()) {\n            httpResponse = client.execute(requestBuilder.build());\n            HttpEntity responseEntity = httpResponse.getEntity();\n            if (responseEntity == null || responseEntity.getContent() == null) {\n                bytes = Constants.ZERO_BYTES;\n            } else {\n                InputStream is = responseEntity.getContent();\n                Header contentEncoding = httpResponse.getFirstHeader(HttpConstants.HDR_CONTENT_ENCODING);\n                if (contentEncoding != null) {\n                    String encoding = contentEncoding.getValue();\n                    if (\"br\".equalsIgnoreCase(encoding)) {\n                        is = new BrotliInputStream(is);\n                    }\n                }\n                bytes = FileUtils.toBytes(is);\n            }\n            request.setEndTime(System.currentTimeMillis());\n            httpResponse.close();\n        } catch (Exception e) {\n            if (e instanceof ClientProtocolException && e.getCause() != null) { // better error message                \n                throw new RuntimeException(e.getCause());\n            } else {\n                throw new RuntimeException(e);\n            }\n        }\n        int statusCode = httpResponse.getStatusLine().getStatusCode();\n        Map<String, List<String>> headers = toHeaders(httpResponse);\n        List<Cookie> storedCookies = cookieStore.getCookies();\n        Header[] requestCookieHeaders = httpResponse.getHeaders(HttpConstants.HDR_SET_COOKIE);\n        // edge case where the apache client\n        // auto-followed a redirect where cookies were involved\n        List<String> mergedCookieValues = new ArrayList(requestCookieHeaders.length);\n        Set<String> alreadyMerged = new HashSet(requestCookieHeaders.length);\n        for (Header ch : requestCookieHeaders) {\n            String requestCookieValue = ch.getValue();\n            io.netty.handler.codec.http.cookie.Cookie c = ClientCookieDecoder.LAX.decode(requestCookieValue);\n            if (c != null) {\n                mergedCookieValues.add(requestCookieValue);\n                alreadyMerged.add(c.name());\n            }\n        }        \n        for (Cookie c : storedCookies) {\n            if (c.getValue() != null) {\n                String name = c.getName();\n                if (alreadyMerged.contains(name)) {\n                    continue;\n                }                \n                Map<String, Object> map = new HashMap();\n                map.put(Cookies.NAME, name);\n                map.put(Cookies.VALUE, c.getValue());\n                map.put(Cookies.DOMAIN, c.getDomain());\n                if (c.getExpiryDate() != null) {\n                    map.put(Cookies.MAX_AGE, c.getExpiryDate().getTime());\n                }\n                map.put(Cookies.SECURE, c.isSecure());\n                io.netty.handler.codec.http.cookie.Cookie nettyCookie = Cookies.fromMap(map);\n                String cookieValue = ServerCookieEncoder.LAX.encode(nettyCookie);\n                mergedCookieValues.add(cookieValue);\n            }\n        }\n        headers.put(HttpConstants.HDR_SET_COOKIE, mergedCookieValues);\n        cookieStore.clear();\n        Response response = new Response(statusCode, headers, bytes);\n        httpLogger.logResponse(getConfig(), request, response);\n        return response;\n    }\n\n    @Override\n    public void process(org.apache.http.HttpRequest hr, HttpContext hc) throws HttpException, IOException {\n        request.setHeaders(toHeaders(hr));\n        httpLogger.logRequest(getConfig(), request);\n        request.setStartTime(System.currentTimeMillis());\n    }\n\n    private static Map<String, List<String>> toHeaders(HttpMessage msg) {\n        Header[] headers = msg.getAllHeaders();\n        Map<String, List<String>> map = new LinkedHashMap(headers.length);\n        for (Header outer : headers) {\n            String name = outer.getName();\n            Header[] inner = msg.getHeaders(name);\n            List<String> list = new ArrayList(inner.length);\n            for (Header h : inner) {\n                list.add(h.getValue());\n            }\n            map.put(name, list);\n        }\n        return map;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ArmeriaHttpClient.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.Constants;\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.core.Config;\nimport com.linecorp.armeria.client.ClientRequestContext;\nimport com.linecorp.armeria.client.DecoratingHttpClientFunction;\nimport com.linecorp.armeria.client.WebClient;\nimport com.linecorp.armeria.common.AggregatedHttpResponse;\nimport com.linecorp.armeria.common.HttpHeaderNames;\nimport com.linecorp.armeria.common.HttpMethod;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.common.RequestContext;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport com.linecorp.armeria.common.RequestHeadersBuilder;\nimport com.linecorp.armeria.common.ResponseHeaders;\nimport com.linecorp.armeria.common.logging.RequestLogProperty;\nimport com.linecorp.armeria.server.ServiceRequestContext;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.Future;\n\n/**\n *\n * @author pthomas3\n */\npublic class ArmeriaHttpClient implements HttpClient, DecoratingHttpClientFunction {\n\n    private final Logger logger;\n    private final HttpLogger httpLogger;\n\n    private Config config;\n    private HttpRequest request;\n    private RequestContext requestContext;     \n\n    public void setRequestContext(RequestContext requestContext) {\n        this.requestContext = requestContext;\n    }\n\n    public ArmeriaHttpClient(Config config, Logger logger) {\n        this.config = config;\n        this.logger = logger;\n        httpLogger = new HttpLogger(logger);\n    }\n\n    @Override\n    public Response invoke(HttpRequest request) {\n        this.request = request;\n        HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod());\n        StringUtils.Pair urlAndPath = HttpUtils.parseUriIntoUrlBaseAndPath(request.getUrl());\n        WebClient webClient = WebClient.builder(urlAndPath.left).decorator(this).build();\n        RequestHeadersBuilder rhb = RequestHeaders.builder(httpMethod, urlAndPath.right);\n        Map<String, List<String>> headers = request.getHeaders();\n        if (headers != null) {\n            headers.forEach((k, v) -> rhb.add(k, v));\n        }\n        final byte[] body = request.getBody() == null ? Constants.ZERO_BYTES : request.getBody();\n        AggregatedHttpResponse ahr;\n        Callable<AggregatedHttpResponse> callable = () -> webClient.execute(rhb.build(), body).aggregate().join();\n        ServiceRequestContext src = requestContext == null ? null : requestContext.root();\n        try {\n            if (src == null) {\n                ahr = callable.call();\n            } else {\n                Future<AggregatedHttpResponse> future = src.blockingTaskExecutor().submit(callable);\n                ahr = future.get();\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        ResponseHeaders rh = ahr.headers();\n        Map<String, List<String>> responseHeaders = new LinkedHashMap(rh.size());\n        for (CharSequence name : rh.names()) {\n            if (!HttpHeaderNames.STATUS.equals(name)) {\n                responseHeaders.put(name.toString(), rh.getAll(name));\n            }\n        }\n        byte[] responseBody = ahr.content().isEmpty() ? Constants.ZERO_BYTES : ahr.content().array();\n        Response response = new Response(ahr.status().code(), responseHeaders, responseBody);\n        httpLogger.logResponse(config, request, response);\n        return response;\n    }\n\n    @Override\n    public void setConfig(Config config) {\n        this.config = config;\n    }\n\n    @Override\n    public Config getConfig() {\n        return config;\n    }\n\n    @Override\n    public HttpResponse execute(com.linecorp.armeria.client.HttpClient delegate, ClientRequestContext ctx,\n            com.linecorp.armeria.common.HttpRequest req) throws Exception {\n        ctx.log().whenAvailable(RequestLogProperty.REQUEST_HEADERS).thenAccept(log -> {\n            request.setStartTime(log.requestStartTimeMillis());\n            RequestHeaders rh = log.requestHeaders();\n            for (CharSequence name : rh.names()) {\n                if (name.charAt(0) != ':') {\n                    request.putHeader(name.toString(), rh.getAll(name));\n                }\n            }\n            httpLogger.logRequest(config, request);\n        });\n        ctx.log().whenAvailable(RequestLogProperty.RESPONSE_START_TIME).thenAccept(log -> request.setEndTime(log.responseStartTimeMillis()));\n        return delegate.execute(ctx, req);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/AwsLambdaHandler.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.StringUtils;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.util.Base64;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport net.minidev.json.JSONValue;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class AwsLambdaHandler {\n\n    protected static final Logger logger = LoggerFactory.getLogger(AwsLambdaHandler.class);\n\n    private static final String REQUEST_CONTEXT = \"requestContext\";\n    private static final String DOMAIN_NAME = \"domainName\";\n    private static final String HTTP = \"http\";\n    private static final String HTTPS_PREFIX = \"https://\";\n    private static final String METHOD = \"method\";\n    private static final String RAW_PATH = \"rawPath\";\n    private static final String QUERY_STRING_PARAMETERS = \"queryStringParameters\";\n    private static final String HEADERS = \"headers\";\n    private static final String COOKIES = \"cookies\";\n    private static final String BODY = \"body\";\n    private static final String IS_BASE64_ENCODED = \"isBase64Encoded\";\n    private static final String STATUS_CODE = \"statusCode\";\n\n    private final ServerHandler handler;\n\n    public AwsLambdaHandler(ServerHandler handler) {\n        this.handler = handler;\n    }\n\n    public void handle(InputStream in, OutputStream out) throws IOException {\n        Map<String, Object> req = (Map) JSONValue.parse(in);\n        if (logger.isTraceEnabled()) {\n            logger.trace(\"request: {}\", req);\n        }\n        Map<String, Object> ctx = (Map) req.get(REQUEST_CONTEXT);\n        String domainName = (String) ctx.get(DOMAIN_NAME);\n        Map<String, Object> http = (Map) ctx.get(HTTP);\n        String method = (String) http.get(METHOD);\n        String path = (String) req.get(RAW_PATH);\n        Map<String, Object> rawParams = (Map) req.get(QUERY_STRING_PARAMETERS);\n        Map<String, Object> rawHeaders = (Map) req.get(HEADERS);\n        List<String> rawCookies = (List) req.get(COOKIES);\n        String body = (String) req.get(BODY);\n        Boolean isBase64Encoded = (Boolean) req.get(IS_BASE64_ENCODED);\n        Request request = new Request();\n        request.setUrlBase(HTTPS_PREFIX + domainName);\n        request.setMethod(method);\n        request.setPath(path);\n        if (rawParams != null) {\n            rawParams.forEach((k, v) -> request.setParamCommaDelimited(k, (String) v));\n        }\n        if (rawHeaders != null) {\n            rawHeaders.forEach((k, v) -> request.setHeaderCommaDelimited(k, (String) v));\n        }\n        if (rawCookies != null) {\n            request.setCookiesRaw(rawCookies);\n        }\n        if (body != null) {\n            if (isBase64Encoded) {\n                request.setBody(Base64.getDecoder().decode(body));\n            } else {\n                request.setBody(FileUtils.toBytes(body));\n            }\n        }\n        Response response = handler.handle(request);\n        Map<String, Object> res = new HashMap(4);\n        res.put(STATUS_CODE, response.getStatus());\n        Map<String, List<String>> responseHeaders = response.getHeaders();\n        if (responseHeaders != null) {\n            Map<String, String> temp = new HashMap(responseHeaders.size());\n            responseHeaders.forEach((k, v) -> temp.put(k, StringUtils.join(v, \",\")));\n            res.put(HEADERS, temp);\n        }\n        boolean isBinary = response.isBinary();\n        res.put(IS_BASE64_ENCODED, isBinary);\n        byte[] responseBody = response.getBody();\n        if (responseBody == null) {\n            body = null;\n        } else if (isBinary) {\n            body = Base64.getEncoder().encodeToString(responseBody);\n        } else {\n            body = FileUtils.toString(responseBody);\n        }\n        res.put(BODY, body);\n        if (logger.isTraceEnabled()) {\n            logger.trace(\"response: {}\", res);\n        }\n        out.write(JsonUtils.toJsonBytes(res));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/Cookies.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport io.netty.handler.codec.http.cookie.ClientCookieEncoder;\nimport io.netty.handler.codec.http.cookie.Cookie;\nimport io.netty.handler.codec.http.cookie.CookieHeaderNames;\nimport io.netty.handler.codec.http.cookie.DefaultCookie;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class Cookies {\n\n    private static final Logger logger = LoggerFactory.getLogger(Cookies.class);\n\n    private Cookies() {\n        // only static methods               \n    }\n\n    public static final String NAME = \"name\";\n    public static final String VALUE = \"value\";\n    public static final String WRAP = \"wrap\";\n    public static final String DOMAIN = \"domain\";\n    public static final String PATH = \"path\";\n    public static final String MAX_AGE = \"max-age\"; // only one with a hyphen\n    public static final String SECURE = \"secure\";\n    public static final String HTTP_ONLY = \"httponly\";\n    public static final String SAME_SITE = \"samesite\";\n\n    public static Map<String, Object> toMap(Cookie cookie) {\n        Map<String, Object> map = new HashMap();\n        map.put(NAME, cookie.name());\n        map.put(VALUE, cookie.value());\n        map.put(WRAP, cookie.wrap());\n        map.put(DOMAIN, cookie.domain());\n        map.put(PATH, cookie.path());\n        map.put(MAX_AGE, cookie.maxAge());\n        map.put(SECURE, cookie.isSecure());\n        map.put(HTTP_ONLY, cookie.isHttpOnly());\n        if (cookie instanceof DefaultCookie) {\n            DefaultCookie dc = (DefaultCookie) cookie;\n            if (dc.sameSite() != null) {\n                map.put(SAME_SITE, dc.sameSite().name());\n            }\n        }\n        return map;\n    }\n\n    public static Cookie fromMap(Map<String, Object> map) {\n        String name = (String) map.get(NAME);\n        String value = (String) map.get(VALUE);\n        DefaultCookie cookie = new DefaultCookie(name, value);\n        Boolean wrap = (Boolean) map.get(WRAP);\n        if (wrap != null) {\n            cookie.setWrap(wrap);\n        }\n        String domain = (String) map.get(DOMAIN);\n        if (domain != null) {\n            cookie.setDomain(domain);\n        }\n        String path = (String) map.get(PATH);\n        if (path != null) {\n            cookie.setPath(path);\n        }\n        Object maxAge = map.get(MAX_AGE);\n        if (maxAge != null) {\n            cookie.setMaxAge(Long.parseLong(maxAge + \"\"));\n        }\n        Boolean secure = (Boolean) map.get(SECURE);\n        if (secure != null) {\n            cookie.setSecure(secure);\n        }\n        Boolean httpOnly = (Boolean) map.get(HTTP_ONLY);\n        if (httpOnly != null) {\n            cookie.setHttpOnly(httpOnly);\n        }\n        String sameSite = (String) map.get(SAME_SITE);\n        if (sameSite != null) {\n            cookie.setSameSite(CookieHeaderNames.SameSite.valueOf(sameSite));\n        }\n        return cookie;\n    }\n\n    public static Map<String, Map> normalize(Object mapOrList) {\n        Map<String, Map> cookies = new HashMap();\n        if (mapOrList instanceof Map) {\n            Map<String, Object> map = (Map) mapOrList;\n            map.forEach((k, v) -> {\n                if (v instanceof String) {\n                    Map<String, Object> cookie = new HashMap(2);\n                    cookie.put(\"name\", k);\n                    cookie.put(\"value\", v);\n                    cookies.put(k, cookie);\n                } else if (v instanceof Map) {\n                    Map<String, Object> cookie = (Map) v;\n                    cookie.put(\"name\", k);\n                    cookies.put(k, cookie);\n                }\n            });\n        } else if (mapOrList instanceof List) {\n            List<Map> list = (List) mapOrList;\n            list.forEach(map -> cookies.put((String) map.get(\"name\"), map));\n        }\n        return cookies;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/CustomHttpRequestRetryHandler.java",
    "content": "package com.intuit.karate.http;\n\nimport java.io.IOException;\n\nimport org.apache.http.NoHttpResponseException;\nimport org.apache.http.client.HttpRequestRetryHandler;\nimport org.apache.http.protocol.HttpContext;\n\nimport com.intuit.karate.Logger;\n\n/**\n * Calls will retry the call when the client throws a NoHttpResponseException.\n * This is usually the case when there is steal connection. The retry cause that\n * the connection is renewed and the second call will succeed.\n */\npublic class CustomHttpRequestRetryHandler implements HttpRequestRetryHandler\n{\n    private final Logger logger;\n\n    public CustomHttpRequestRetryHandler(Logger logger)\n    {\n        this.logger = logger;\n    }\n\n    @Override\n    public boolean retryRequest(IOException exception, int executionCount, HttpContext context)\n    {\n        if (exception instanceof NoHttpResponseException && executionCount < 1)\n        {\n            logger.error(\"Thrown an NoHttpResponseException retry...\");\n            return true;\n        }\n        else\n        {\n            logger.error(\"Thrown an exception {}\", exception.getMessage());\n            return false;\n        }\n    }\n}"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/GenericHttpHeaderTracking.java",
    "content": "package com.intuit.karate.http;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class GenericHttpHeaderTracking implements HttpHeaderTracking {\n\n    private final Map<String, String> httpHeaderReference = new HashMap<>();\n\n    @Override\n    public void putHeaderReference(String originalHeader) {\n        if (originalHeader == null) {\n            return;\n        }\n\n        httpHeaderReference.put(originalHeader.toLowerCase(), originalHeader);\n    }\n\n    @Override\n    public String getOriginalHeader(String headerReference) {\n        if (headerReference == null) {\n            return null;\n        }\n\n        return httpHeaderReference.getOrDefault(headerReference.toLowerCase(), headerReference);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/HttpClient.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.core.Config;\n\n/**\n *\n * @author pthomas3\n */\npublic interface HttpClient {\n\n    void setConfig(Config config);\n\n    Config getConfig();\n\n    Response invoke(HttpRequest request);\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/HttpClientFactory.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.core.ScenarioEngine;\n\n/**\n *\n * @author pthomas3\n */\n@FunctionalInterface\npublic interface HttpClientFactory {\n\n    HttpClient create(ScenarioEngine engine);\n\n    HttpClientFactory DEFAULT = ApacheHttpClient::new;\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/HttpConstants.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n *\n * @author pthomas3\n */\npublic class HttpConstants {\n\n    private HttpConstants() {\n        // only static methods\n    }\n\n    public static final Set<String> HTTP_METHODS\n            = Stream.of(\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\", \"HEAD\", \"CONNECT\", \"TRACE\")\n                    .collect(Collectors.toSet());\n\n    public static final String HDR_COOKIE = \"Cookie\";\n    public static final String HDR_SET_COOKIE = \"Set-Cookie\";\n    public static final String HDR_CONTENT_TYPE = \"Content-Type\";\n    public static final String HDR_LOCATION = \"Location\";\n    public static final String HDR_CONTENT_LENGTH = \"Content-Length\";\n    public static final String HDR_TRANSFER_ENCODING = \"Transfer-Encoding\";\n    public static final String HDR_CONTENT_ENCODING = \"Content-Encoding\";\n    public static final String HDR_ACCEPT = \"Accept\";\n    public static final String HDR_ALLOW = \"Allow\";\n    public static final String HDR_CACHE_CONTROL = \"Cache-Control\";\n\n    public static final String HDR_HX_REQUEST = \"HX-Request\";\n    public static final String HDR_HX_REDIRECT = \"HX-Redirect\";\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/HttpHeaderTracking.java",
    "content": "package com.intuit.karate.http;\n\npublic interface HttpHeaderTracking {\n\n    void putHeaderReference(String originalHeader);\n\n    String getOriginalHeader(String headerReference);\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/HttpLogModifier.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\n/**\n *\n * @author pthomas3\n */\npublic interface HttpLogModifier {\n    \n    boolean enableForUri(String uri); // TODO rename url ?\n    \n    String uri(String uri);\n    \n    String header(String header, String value);\n    \n    String request(String uri, String request);\n    \n    String response(String uri, String response);\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/HttpLogger.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.core.Config;\nimport com.intuit.karate.core.Variable;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class HttpLogger {\n\n    private int requestCount;\n    private final Logger logger;\n\n    public HttpLogger(Logger logger) {\n        this.logger = logger;\n    }\n\n    private static void logHeaders(int num, String prefix, StringBuilder sb,\n            HttpLogModifier modifier, Map<String, List<String>> headers) {\n        if (headers == null || headers.isEmpty()) {\n            return;\n        }\n        sb.append('\\n');\n        headers.forEach((k, v) -> {\n            for (String value : v) {\n                sb.append(num).append(prefix).append(k).append(\": \");\n                if (modifier == null) {\n                    sb.append(value);\n                } else {\n                    sb.append(modifier.header(k, value));\n                }\n                sb.append('\\n');\n            }\n        });\n    }\n\n    private static void logBody(Config config, HttpLogModifier logModifier,\n            StringBuilder sb, String uri, byte[] body, boolean request, ResourceType rt) {\n        if (body == null) {\n            return;\n        }\n        String text;\n        if (config != null && needsPrettyLogging(config, request)) {\n            Object converted = JsonUtils.fromBytes(body, false, rt);\n            Variable v = new Variable(converted);\n            text = v.getAsPrettyString();\n        } else {\n            text = FileUtils.toString(body);\n        }\n        if (logModifier != null) {\n            text = request ? logModifier.request(uri, text) : logModifier.response(uri, text);\n        }\n        sb.append(text);\n    }\n\n    private static boolean needsPrettyLogging(Config config, boolean request) {\n        return logPrettyRequest(config, request) || logPrettyResponse(config, request);\n    }\n\n    private static boolean logPrettyResponse(Config config, boolean request) {\n        return !request && config.isLogPrettyResponse();\n    }\n\n    private static boolean logPrettyRequest(Config config, boolean request) {\n        return request && config.isLogPrettyRequest();\n    }\n\n    private static HttpLogModifier logModifier(Config config, String uri) {\n        HttpLogModifier logModifier = config.getLogModifier();\n        return logModifier == null ? null : logModifier.enableForUri(uri) ? logModifier : null;\n    }\n\n    public static String getStatusFailureMessage(int expected, Config config, HttpRequest request, Response response) {\n        String url = request.getUrl();\n        HttpLogModifier logModifier = logModifier(config, url);\n        String maskedUrl = logModifier == null ? url : logModifier.uri(url);\n        String rawResponse = response.getBodyAsString();\n        if (rawResponse != null && logModifier != null) {\n            rawResponse = logModifier.response(url, rawResponse);\n        }\n        long responseTime = request.getEndTime() - request.getStartTime();\n        return \"status code was: \" + response.getStatus() + \", expected: \" + expected\n                + \", response time in milliseconds: \" + responseTime + \", url: \" + maskedUrl\n                + \", response: \\n\" + rawResponse;\n    }\n\n    public void logRequest(Config config, HttpRequest request) {\n        requestCount++;\n        String uri = request.getUrl();\n        HttpLogModifier requestModifier = logModifier(config, uri);\n        String maskedUri = requestModifier == null ? uri : requestModifier.uri(uri);\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"request:\\n\").append(requestCount).append(\" > \")\n                .append(request.getMethod()).append(' ').append(maskedUri);\n        logHeaders(requestCount, \" > \", sb, requestModifier, request.getHeaders());\n        ResourceType rt = ResourceType.fromContentType(request.getContentType());\n        if (rt == null || rt.isBinary()) {\n            // don't log body\n        } else {\n            byte[] body;\n            if (rt == ResourceType.MULTIPART) {\n                body = request.getBodyForDisplay() == null ? null : request.getBodyForDisplay().getBytes();\n            } else {\n                body = request.getBody();\n            }\n            logBody(config, requestModifier, sb, uri, body, true, rt);\n        }\n        sb.append('\\n');\n        logger.debug(\"{}\", sb);\n    }\n\n    public void logResponse(Config config, HttpRequest request, Response response) {\n        long startTime = request.getStartTime();\n        long elapsedTime = request.getEndTime() - startTime;\n        response.setResponseTime(elapsedTime);\n        StringBuilder sb = new StringBuilder();\n        String uri = request.getUrl();\n        HttpLogModifier responseModifier = logModifier(config, uri);\n        sb.append(\"response time in milliseconds: \").append(elapsedTime).append('\\n');\n        sb.append(requestCount).append(\" < \").append(response.getStatus());\n        logHeaders(requestCount, \" < \", sb, responseModifier, response.getHeaders());\n        ResourceType rt = response.getResourceType();\n        if (rt == null || rt.isBinary()) {\n            // don't log body\n        } else {\n            logBody(config, responseModifier, sb, uri, response.getBody(), false, rt);\n        }\n        sb.append('\\n');\n        logger.debug(\"{}\", sb);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/HttpRequest.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.StringUtils;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class HttpRequest {\n\n    private long startTime;\n    private long endTime;\n    private String url;\n    private String method;\n    private Map<String, List<String>> headers;\n    private byte[] body;\n    private String bodyForDisplay;\n\n    public void putHeader(String name, String... values) {\n        putHeader(name, Arrays.asList(values));\n    }\n\n    public void putHeader(String name, List<String> values) {\n        if (headers == null) {\n            headers = new HashMap();\n        }\n        for (String key : headers.keySet()) {\n            if (key.equalsIgnoreCase(name)) {\n                name = key;\n                break;\n            }\n        }\n        headers.put(name, values);\n    }\n\n    public long getStartTime() {\n        return startTime;\n    }\n\n    public void setStartTime(long startTime) {\n        this.startTime = startTime;\n    }\n\n    public long getEndTime() {\n        return endTime;\n    }\n\n    public void setEndTime(long endTime) {\n        this.endTime = endTime;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public void setUrl(String url) {\n        this.url = url;\n    }\n\n    public String getMethod() {\n        return method;\n    }\n\n    public void setMethod(String method) {\n        this.method = method;\n    }\n\n    public Map<String, List<String>> getHeaders() {\n        return headers;\n    }\n\n    public void setHeaders(Map<String, List<String>> headers) {\n        this.headers = headers;\n    }\n\n    public byte[] getBody() {\n        return body;\n    }\n\n    public String getBodyAsString() {\n        return FileUtils.toString(body);\n    }\n\n    public void setBody(byte[] body) {\n        this.body = body;\n    }\n\n    public String getBodyForDisplay() {\n        return bodyForDisplay;\n    }\n\n    public void setBodyForDisplay(String bodyForDisplay) {\n        this.bodyForDisplay = bodyForDisplay;\n    }\n\n    public List<String> getHeaderValues(String name) { // TOTO optimize\n        return StringUtils.getIgnoreKeyCase(headers, name);\n    }\n\n    public void removeHeader(String name) {\n        if (headers == null) {\n            return;\n        }\n        for (String key : headers.keySet()) {\n            if (key.equalsIgnoreCase(name)) {\n                name = key;\n                break;\n            }\n        }\n        headers.remove(name);\n    }\n\n    public String getHeader(String name) {\n        List<String> values = getHeaderValues(name);\n        return values == null || values.isEmpty() ? null : values.get(0);\n    }\n\n    public String getContentType() {\n        return getHeader(HttpConstants.HDR_CONTENT_TYPE);\n    }\n\n    public void setContentType(String contentType) {\n        putHeader(HttpConstants.HDR_CONTENT_TYPE, contentType);\n    }\n\n    public Request toRequest() {\n        Request request = new Request();\n        request.setStartTime(startTime);\n        request.setEndTime(endTime);\n        request.setMethod(method);\n        request.setUrl(url);\n        request.setHeaders(headers);\n        request.setBody(body);\n        return request;\n    }\n\n    @Override\n    public String toString() {\n        return method + \" \" + url;\n    }        \n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/HttpRequestBuilder.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.Json;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.RuntimeHook;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.graal.JsArray;\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.graal.Methods;\nimport io.netty.handler.codec.http.cookie.ClientCookieEncoder;\nimport io.netty.handler.codec.http.cookie.Cookie;\nimport io.netty.handler.codec.http.cookie.DefaultCookie;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\nimport org.apache.http.client.utils.URIBuilder;\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.ProxyObject;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class HttpRequestBuilder implements ProxyObject {\n\n    private static final Logger logger = LoggerFactory.getLogger(HttpRequestBuilder.class);\n\n    private static final String URL = \"url\";\n    private static final String METHOD = \"method\";\n    private static final String PATH = \"path\";\n    private static final String PARAM = \"param\";\n    private static final String PARAMS = \"params\";\n    private static final String HEADER = \"header\";\n    private static final String HEADERS = \"headers\";\n    private static final String BODY = \"body\";\n    private static final String INVOKE = \"invoke\";\n    private static final String GET = \"get\";\n    private static final String POST = \"post\";\n    private static final String PUT = \"put\";\n    private static final String DELETE = \"delete\";\n    private static final String PATCH = \"patch\";\n    private static final String HEAD = \"head\";\n    private static final String CONNECT = \"connect\";\n    private static final String OPTIONS = \"options\";\n    private static final String TRACE = \"trace\";\n    private static final String MULTI_PART = \"multiPart\";\n\n    private static final String[] KEYS = new String[]{\n        URL, METHOD, PATH, PARAM, PARAMS, HEADER, HEADERS, BODY, INVOKE,\n        GET, POST, PUT, DELETE, PATCH, HEAD, CONNECT, OPTIONS, TRACE, MULTI_PART\n    };\n    private static final Set<String> KEY_SET = new HashSet<>(Arrays.asList(KEYS));\n    private static final JsArray KEY_ARRAY = new JsArray(KEYS);\n\n    private String url;\n    private String method;\n    private List<String> paths;\n    private Map<String, List<String>> params;\n    private Map<String, List<String>> headers;\n    private MultiPartBuilder multiPart;\n    private Object body;\n    private Set<Cookie> cookies;\n    private String retryUntil;\n    private RuntimeHook hook;\n    public final HttpClient client;\n\n    public HttpRequestBuilder(HttpClient client) {\n        this.client = client;\n    }\n\n    public HttpRequestBuilder reset() {\n        // url will be retained\n        method = null;\n        paths = null;\n        params = null;\n        headers = null;\n        multiPart = null;\n        body = null;\n        cookies = null;\n        retryUntil = null;\n        return this;\n    }\n\n    public HttpRequestBuilder copy(HttpClient newClient) {\n        HttpRequestBuilder hrb = new HttpRequestBuilder(newClient == null ? client : newClient);\n        hrb.url = url;\n        hrb.method = method;\n        hrb.paths = paths;\n        hrb.params = params;\n        hrb.headers = headers;\n        hrb.multiPart = multiPart;\n        hrb.body = body;\n        hrb.cookies = cookies;\n        hrb.retryUntil = retryUntil;\n        return hrb;\n    }\n\n    public Response invoke(String method) {\n        this.method = method;\n        return invoke();\n    }\n\n    public Response invoke(String method, Object body) {\n        this.method = method;\n        this.body = body;\n        return invoke();\n    }\n\n    public HttpRequest build() {\n        buildInternal();\n        HttpRequest request = new HttpRequest();\n        request.setMethod(method);\n        request.setUrl(getUri());\n        if (multiPart != null) {\n            request.setBodyForDisplay(multiPart.getBodyForDisplay());\n        }\n        if (body != null) {\n            request.setBody(JsonUtils.toBytes(body));\n        }\n        request.setHeaders(headers);\n        return request;\n    }\n\n    private void buildInternal() {\n        if (url == null) {\n            url = client.getConfig().getUrl();\n            if (url == null) {\n                throw new RuntimeException(\"incomplete http request, 'url' not set\");\n            }\n        }\n        if (method == null) {\n            if (multiPart != null && multiPart.isMultipart()) {\n                method = \"POST\";\n            } else {\n                method = \"GET\";\n            }\n        }\n        method = method.toUpperCase();\n        if (\"GET\".equals(method) && multiPart != null) {\n            Map<String, Object> parts = multiPart.getFormFields();\n            if (parts != null) {\n                parts.forEach((k, v) -> param(k, (String) v));\n            }\n            multiPart = null;\n        }\n        if (multiPart != null) {\n            if (body == null) { // this is not-null only for a re-try, don't rebuild multi-part\n                body = multiPart.build();\n                String userContentType = getHeader(HttpConstants.HDR_CONTENT_TYPE);\n                if (userContentType != null) {\n                    String boundary = multiPart.getBoundary();\n                    if (boundary != null) {\n                        contentType(userContentType + \"; boundary=\" + boundary);\n                    }\n                } else {\n                    contentType(multiPart.getContentTypeHeader());\n                }\n            }\n        }\n        if (cookies != null && !cookies.isEmpty()) {\n            List<String> cookieValues = new ArrayList<>(cookies.size());\n            for (Cookie c : cookies) {\n                String cookieValue = ClientCookieEncoder.LAX.encode(c);\n                cookieValues.add(cookieValue);\n            }\n            header(HttpConstants.HDR_COOKIE, StringUtils.join(cookieValues, \"; \"));\n        }\n        if (body != null) {\n            if (multiPart == null) {\n                String contentType = getContentType();\n                if (contentType == null) {\n                    ResourceType rt = ResourceType.fromObject(body);\n                    if (rt != null) {\n                        contentType = rt.contentType;\n                    }\n                }\n                Charset charset = contentType == null ? null : HttpUtils.parseContentTypeCharset(contentType);\n                if (charset == null) {\n                    // client can be null when not in karate scenario, and mock clients can have nulls\n                    charset = client == null ? null : client.getConfig() == null ? null : client.getConfig().getCharset();\n                    if (charset != null) {\n                        // edge case, support setting content type to an empty string\n                        contentType = StringUtils.trimToNull(contentType);\n                        if (contentType != null) {\n                            contentType = contentType + \"; charset=\" + charset;\n                        }\n                    }\n                }\n                contentType(contentType);\n            }\n        }\n    }\n\n    public Response invoke() {\n        return client.invoke(build());\n    }\n\n    public boolean isRetry() {\n        return retryUntil != null;\n    }\n\n    public String getRetryUntil() {\n        return retryUntil;\n    }\n\n    public void setRetryUntil(String retryUntil) {\n        this.retryUntil = retryUntil;\n    }\n\n    public HttpRequestBuilder url(String value) {\n        url = value;\n        return this;\n    }\n\n    public HttpRequestBuilder method(String method) {\n        this.method = method;\n        return this;\n    }\n\n    public HttpRequestBuilder paths(String... paths) {\n        for (String path : paths) {\n            path(path);\n        }\n        return this;\n    }\n\n    public HttpRequestBuilder path(String path) {\n        if (path == null) {\n            return this;\n        }\n        if (paths == null) {\n            paths = new ArrayList<>();\n        }\n        paths.add(path);\n        return this;\n    }\n\n    public List<String> getPaths() {\n        return paths;\n    }\n\n    public Object getBody() {\n        return body;\n    }\n\n    public Map<String, String> getHeaders() {\n        if (headers == null) {\n            return new LinkedHashMap(0);\n        }\n        Map<String, String> map = new LinkedHashMap(headers.size());\n        headers.forEach((k, v) -> {\n            if (v != null && !v.isEmpty()) {\n                Object value = v.get(0);\n                if (value != null) {\n                    map.put(k, value.toString());\n                }\n            }\n        });\n        return map;\n    }\n\n    public String getUri() {\n        try {\n            URIBuilder builder;\n            if (url == null) {\n                builder = new URIBuilder();\n            } else {\n                builder = new URIBuilder(url);\n            }\n            if (params != null) {\n                params.forEach((key, values) -> values.forEach(value -> builder.addParameter(key, value)));\n            }\n            if (paths != null) {\n                List<String> segments = new ArrayList();\n                for (String item : builder.getPathSegments()) {\n                    if (!item.isEmpty()) {\n                        segments.add(item);\n                    }\n                }\n                Iterator<String> pathIterator = paths.iterator();\n                while (pathIterator.hasNext()) {\n                    String item = pathIterator.next();\n                    if (!pathIterator.hasNext() && \"/\".equals(item)) { // preserve trailing slash\n                        segments.add(\"\");\n                    } else {\n                        for (String s : StringUtils.split(item, '/', true)) {\n                            segments.add(s);\n                        }\n                    }\n                }\n                builder.setPathSegments(segments);\n            }\n            URI uri = builder.build();\n            return uri.toASCIIString();\n        } catch (URISyntaxException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public HttpRequestBuilder body(Object body) {\n        this.body = body;\n        return this;\n    }\n\n    public HttpRequestBuilder bodyJson(String json) {\n        this.body = Json.of(json).value();\n        return this;\n    }\n\n    public List<String> getHeaderValues(String name) {\n        return StringUtils.getIgnoreKeyCase(headers, name); // TODO optimize\n    }\n\n    public String getHeader(String name) {\n        List<String> list = getHeaderValues(name);\n        if (list == null || list.isEmpty()) {\n            return null;\n        } else {\n            return list.get(0);\n        }\n    }\n\n    public String getContentType() {\n        return getHeader(HttpConstants.HDR_CONTENT_TYPE);\n    }\n\n    public HttpRequestBuilder removeHeader(String name) {\n        if (headers != null) {\n            StringUtils.removeIgnoreKeyCase(headers, name);\n        }\n        return this;\n    }\n\n    public HttpRequestBuilder header(String name, String... values) {\n        return header(name, Arrays.asList(values));\n    }\n\n    public HttpRequestBuilder header(String name, List<String> values) {\n        if (headers == null) {\n            headers = new LinkedHashMap<>();\n        }\n        for (String key : headers.keySet()) {\n            if (key.equalsIgnoreCase(name)) {\n                name = key;\n                break;\n            }\n        }\n        headers.put(name, values);\n        return this;\n    }\n\n    public HttpRequestBuilder header(String name, String value) {\n        return header(name, Collections.singletonList(value));\n    }\n\n    public HttpRequestBuilder headers(Map<String, Object> map) {\n        map.forEach((k, v) -> {\n            if (!k.startsWith(\":\")) { // strip (armeria) special headers\n                if (v instanceof List) {\n                    header(k, (List) v);\n                } else if (v != null) {\n                    header(k, v.toString());\n                }\n            }\n        });\n        return this;\n    }\n\n    public HttpRequestBuilder headers(Value value) {\n        JsValue jv = new JsValue(value);\n        if (jv.isObject()) {\n            headers(jv.getAsMap());\n        } else {\n            logger.warn(\"unexpected headers() argument: {}\", value);\n        }\n        return this;\n    }\n\n    public HttpRequestBuilder contentType(String contentType) {\n        if (contentType != null) {\n            header(HttpConstants.HDR_CONTENT_TYPE, contentType);\n        }\n        return this;\n    }\n\n    public List<String> getParam(String name) {\n        if (params == null || name == null) {\n            return null;\n        }\n        return params.get(name);\n    }\n\n    public HttpRequestBuilder param(String name, String... values) {\n        return param(name, Arrays.asList(values));\n    }\n\n    public HttpRequestBuilder param(String name, List<String> values) {\n        if (params == null) {\n            params = new LinkedHashMap<>();\n        }\n        List<String> notNullValues = values.stream().filter(v -> v != null).collect(Collectors.toList());\n        if (!notNullValues.isEmpty()) {\n            params.put(name, notNullValues);\n        }\n        return this;\n    }\n\n    public HttpRequestBuilder params(Map<String, List<String>> params) {\n        this.params = params;\n        return this;\n    }\n\n    public HttpRequestBuilder cookies(Collection<Map> cookies) {\n        for (Map<String, Object> map : cookies) {\n            cookie(map);\n        }\n        return this;\n    }\n\n    public HttpRequestBuilder cookie(Map<String, Object> map) {\n        return cookie(Cookies.fromMap(map));\n    }\n\n    public HttpRequestBuilder cookie(Cookie cookie) {\n        if (cookies == null) {\n            cookies = new HashSet<>();\n        }\n        cookies.add(cookie);\n        return this;\n    }\n\n    public HttpRequestBuilder cookie(String name, String value) {\n        return cookie(new DefaultCookie(name, value));\n    }\n\n    public HttpRequestBuilder formField(String name, Object value) {\n        if (multiPart == null) {\n            multiPart = new MultiPartBuilder(false, client);\n        }\n        multiPart.part(name, value);\n        return this;\n    }\n\n    public HttpRequestBuilder multiPartJson(String json) {\n        return multiPart(Json.of(json).value());\n    }\n\n    public HttpRequestBuilder multiPart(Map<String, Object> map) {\n        if (multiPart == null) {\n            multiPart = new MultiPartBuilder(true, client);\n        }\n        multiPart.part(map);\n        return this;\n    }\n\n    public HttpRequestBuilder hook(RuntimeHook hook) {\n        this.hook = hook;\n        return this;\n    }\n\n    public RuntimeHook hook() {\n        return hook;\n    }\n\n    //==========================================================================\n    //\n    private final Methods.FunVar PATH_FUNCTION = args -> {\n        if (args.length == 0) {\n            return getUri();\n        } else {\n            for (Object o : args) {\n                if (o != null) {\n                    path(o.toString());\n                }\n            }\n            return this;\n        }\n    };\n\n    private static String toString(Object o) {\n        return o == null ? null : o.toString();\n    }\n\n    private final Methods.FunVar PARAM_FUNCTION = args -> {\n        if (args.length == 1) {\n            List<String> list = getParam(toString(args[0]));\n            if (list == null || list.isEmpty()) {\n                return null;\n            }\n            return list.get(0);\n        } else {\n            param(toString(args[0]), toString(args[1]));\n            return this;\n        }\n    };\n\n    private final Methods.FunVar HEADER_FUNCTION = args -> {\n        if (args.length == 1) {\n            return getHeader(toString(args[0]));\n        } else {\n            header(toString(args[0]), toString(args[1]));\n            return this;\n        }\n    };\n\n    private final Methods.FunVar INVOKE_FUNCTION = args -> {\n        switch (args.length) {\n            case 0:\n                return invoke();\n            case 1:\n                return invoke(args[0].toString());\n            default:\n                return invoke(args[0].toString(), args[1]);\n        }\n    };\n\n    private final Methods.FunVar METHOD_FUNCTION = args -> {\n        if (args.length > 0) {\n            return method((String) args[0]);\n        } else {\n            return method;\n        }\n    };\n\n    private final Methods.FunVar BODY_FUNCTION = args -> {\n        if (args == null) { // can be null\n            return this;\n        }\n        if (args.length > 0) {\n            return body(args[0]);\n        } else {\n            return JsValue.fromJava(body);\n        }\n    };\n\n    private final Supplier GET_FUNCTION = () -> invoke(GET);\n    private final Function POST_FUNCTION = o -> invoke(POST, o);\n    private final Function PUT_FUNCTION = o -> invoke(PUT, o);\n    private final Function PATCH_FUNCTION = o -> invoke(PATCH, o);\n    private final Supplier DELETE_FUNCTION = () -> invoke(DELETE);\n\n    @Override\n    public Object getMember(String key) {\n        switch (key) {\n            case METHOD:\n                return METHOD_FUNCTION;\n            case PATH:\n                return PATH_FUNCTION;\n            case HEADER:\n                return HEADER_FUNCTION;\n            case HEADERS:\n                return JsValue.fromJava(headers);\n            case PARAM:\n                return PARAM_FUNCTION;\n            case PARAMS:\n                return JsValue.fromJava(params);\n            case BODY:\n                return BODY_FUNCTION;\n            case INVOKE:\n                return INVOKE_FUNCTION;\n            case GET:\n                return GET_FUNCTION;\n            case POST:\n                return POST_FUNCTION;\n            case PUT:\n                return PUT_FUNCTION;\n            case PATCH:\n                return PATCH_FUNCTION;\n            case DELETE:\n                return DELETE_FUNCTION;\n            case URL:\n                return (Function<String, Object>) this::url;\n            case MULTI_PART:\n                return (Function<Map<String, Object>, Object>) this::multiPart;\n            default:\n                logger.warn(\"no such property on http object: {}\", key);\n                return null;\n        }\n    }\n\n    @Override\n    public void putMember(String key, Value value) {\n        switch (key) {\n            case METHOD:\n                method = value.asString();\n                break;\n            case BODY:\n                body = JsValue.toJava(value);\n                break;\n            case HEADERS:\n                headers(value);\n                break;\n            case PARAMS:\n                params = (Map) JsValue.toJava(value);\n                break;\n            case URL:\n                url = value.asString();\n                break;\n            default:\n                logger.warn(\"put not supported on http object: {} - {}\", key, value);\n        }\n    }\n\n    @Override\n    public Object getMemberKeys() {\n        return KEY_ARRAY;\n    }\n\n    @Override\n    public boolean hasMember(String key) {\n        return KEY_SET.contains(key);\n    }\n\n    @Override\n    public String toString() {\n        return getUri();\n    }\n\n    public String toCurlCommand() {\n        buildInternal();\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"curl \");\n        sb.append(\"-X \").append(method).append(' ');\n        String url = getUri();\n        if (!StringUtils.isBlank(url)) {\n            sb.append(getUri()).append(' ');\n        }\n        if (multiPart != null) {\n            sb.append(\"\\\\\\n\");\n            sb.append(multiPart.toCurlCommand());\n        } else if (body != null) {\n            sb.append(\"\\\\\\n\");\n            String raw = JsonUtils.toString(body);\n            sb.append(\"-d '\").append(raw).append(\"'\");\n        }\n        return sb.toString();\n    }\n\n    public Map<String, Object> toMap() {\n        buildInternal();\n        Map<String, Object> map = new HashMap();\n        map.put(\"url\", getUri());\n        map.put(\"method\", method);\n        if (headers != null) {\n            List<Map> list = new ArrayList(headers.size());\n            map.put(\"headers\", list);\n            headers.forEach((k, v) -> {\n                if (v != null) {\n                    v.forEach(value -> {\n                        if (value != null) {\n                            Map<String, Object> header = new HashMap();\n                            header.put(\"name\", k);\n                            header.put(\"value\", value);\n                            list.add(header);\n                        }\n                    });\n                }\n            });\n        }\n        if (params != null) {\n            List<Map> list = new ArrayList(params.size());\n            map.put(\"params\", list);\n            params.forEach((k, v) -> {\n                if (v != null) {\n                    v.forEach(value -> {\n                        if (value != null) {\n                            Map<String, Object> header = new HashMap();\n                            header.put(\"name\", k);\n                            header.put(\"value\", value);\n                            list.add(header);\n                        }\n                    });\n                }\n            });\n        }\n        if (body != null) {\n            map.put(\"body\", body);\n        }\n        return map;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/HttpServer.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.common.HttpStatus;\nimport com.linecorp.armeria.common.SessionProtocol;\nimport com.linecorp.armeria.server.HttpService;\nimport com.linecorp.armeria.server.Server;\nimport com.linecorp.armeria.server.ServerBuilder;\nimport com.linecorp.armeria.server.cors.CorsService;\nimport java.io.File;\nimport java.util.concurrent.CompletableFuture;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class HttpServer {\n\n    protected static final Logger logger = LoggerFactory.getLogger(HttpServer.class);\n\n    private final Server server;\n    private final CompletableFuture<Void> future;\n    private final int port;\n\n    public static class Builder { // TODO fix code duplication with MockServer\n\n        int port;\n        boolean ssl;\n        boolean local = true;\n        File certFile;\n        File keyFile;\n        boolean corsEnabled;\n        ServerHandler handler;\n        boolean keepOriginalHeaders;\n\n        public Builder local(boolean value) {\n            local = value;\n            return this;\n        }\n\n        public Builder http(int value) {\n            port = value;\n            return this;\n        }\n\n        public Builder https(int value) {\n            ssl = true;\n            port = value;\n            return this;\n        }\n\n        public Builder certFile(File value) {\n            certFile = value;\n            return this;\n        }\n\n        public Builder keyFile(File value) {\n            keyFile = value;\n            return this;\n        }\n\n        public Builder corsEnabled(boolean value) {\n            corsEnabled = value;\n            return this;\n        }\n\n        public Builder handler(ServerHandler value) {\n            handler = value;\n            return this;\n        }\n\n        public Builder keepOriginalHeaders(boolean value) {\n            keepOriginalHeaders = value;\n            return this;\n        }\n\n        public HttpServer build() {\n            ServerBuilder sb = Server.builder();\n            sb.requestTimeoutMillis(0);\n            sb.maxRequestLength(0);\n            if (ssl) {\n                if (local) {\n                    sb.localPort(port, SessionProtocol.HTTPS);\n                } else {\n                    sb.https(port);\n                }\n                SslContextFactory factory = new SslContextFactory();\n                factory.setCertFile(certFile);\n                factory.setKeyFile(keyFile);\n                factory.build();\n                sb.tls(factory.getCertFile(), factory.getKeyFile());\n            } else {\n                if (local) {\n                    sb.localPort(port, SessionProtocol.HTTP);\n                } else {\n                    sb.http(port);\n                }\n            }\n\n            HttpServerHandler.Builder serverHandlerBuilder = HttpServerHandler.Builder.builder();\n            serverHandlerBuilder.handler(handler);\n\n            if (keepOriginalHeaders) {\n                HttpHeaderTracking headerTracking = new GenericHttpHeaderTracking();\n                sb.http1HeaderNaming(http2HeaderName ->\n                        headerTracking.getOriginalHeader(String.valueOf(http2HeaderName)));\n\n                serverHandlerBuilder.httpHeaderTracking(headerTracking);\n            }\n\n            HttpService service = serverHandlerBuilder.build();\n            if (corsEnabled) {\n                service = service.decorate(CorsService.builderForAnyOrigin().allowAllRequestHeaders(true).newDecorator());\n            }\n            sb.service(\"prefix:/\", service);\n            return new HttpServer(sb);\n        }\n\n    }\n\n    public void waitSync() {\n        try {\n            Thread.currentThread().join();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public int getPort() {\n        return port;\n    }\n\n    public CompletableFuture stop() {\n        return server.stop();\n    }\n\n    public static Builder handler(ServerHandler handler) {\n        return new Builder().handler(handler);\n    }\n\n    public static Builder root(String root) {\n        return config(new ServerConfig(root));\n    }\n\n    public static Builder config(ServerConfig config) {\n        return handler(new RequestHandler(config));\n    }\n\n    public HttpServer(ServerBuilder sb) {\n        HttpService httpStopService = (ctx, req) -> {\n            logger.debug(\"received command to stop server: {}\", req.path());\n            this.stop();\n            return HttpResponse.of(HttpStatus.ACCEPTED);\n        };\n        sb.service(\"/__admin/stop\", httpStopService);\n        server = sb.build();\n        future = server.start();\n        future.join();\n        this.port = server.activePort().localAddress().getPort();\n        logger.debug(\"server started: {}:{}\", server.defaultHostname(), this.port);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/HttpServerHandler.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.Constants;\nimport com.linecorp.armeria.common.AggregatedHttpRequest;\nimport com.linecorp.armeria.common.HttpData;\nimport com.linecorp.armeria.common.HttpRequest;\nimport com.linecorp.armeria.common.HttpResponse;\nimport com.linecorp.armeria.common.RequestHeaders;\nimport com.linecorp.armeria.common.ResponseHeaders;\nimport com.linecorp.armeria.common.ResponseHeadersBuilder;\nimport com.linecorp.armeria.server.HttpService;\nimport com.linecorp.armeria.server.ServiceRequestContext;\nimport io.netty.util.AsciiString;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n *\n * @author pthomas3\n */\npublic class HttpServerHandler implements HttpService {\n\n    private final ServerHandler handler;\n\n    private final HttpHeaderTracking httpHeaderTracking;\n\n    public HttpServerHandler(ServerHandler handler, HttpHeaderTracking httpHeaderTracking) {\n        this.handler = handler;\n        this.httpHeaderTracking = httpHeaderTracking;\n    }\n\n    @Override\n    public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {\n        return HttpResponse.from(req.aggregate().thenApply(ahr -> {\n            Request request = toRequest(ctx, ahr);\n            Response response = handler.handle(request);\n            return toResponse(ctx, response);\n        }));\n    }\n\n    private Request toRequest(ServiceRequestContext ctx, AggregatedHttpRequest req) {\n        Request request = new Request();\n        request.setRequestContext(ctx);\n        request.setUrl(req.path());\n        request.setUrlBase(req.scheme() + \"://\" + req.authority());\n        request.setMethod(req.method().name());\n        RequestHeaders rh = req.headers();\n        if (rh != null) {\n            Set<AsciiString> names = rh.names();\n            Map<String, List<String>> headers = new HashMap(names.size());\n            request.setHeaders(headers);\n            for (AsciiString name : names) {\n                if (name.charAt(0) == ':') {\n                    continue;\n                }\n                headers.put(name.toString(), rh.getAll(name));\n            }\n        }\n        if (!req.content().isEmpty()) {\n            byte[] bytes = req.content().array();\n            request.setBody(bytes);\n        }\n        return request;\n    }\n\n    private HttpResponse toResponse(ServiceRequestContext ctx, Response response) {\n        byte[] body = response.getBody();\n        if (body == null) {\n            body = Constants.ZERO_BYTES;\n        }\n        ResponseHeadersBuilder rhb = ResponseHeaders.builder(response.getStatus());\n        Map<String, List<String>> headers = response.getHeaders();\n        if (headers != null) {\n            headers.forEach((k, v) -> {\n                rhb.add(k, v);\n                if (httpHeaderTracking != null) {\n                    httpHeaderTracking.putHeaderReference(k);\n                }\n            });\n        }\n        HttpResponse hr = HttpResponse.of(rhb.build(), HttpData.wrap(body));\n        if (response.getDelay() > 0) {\n            return HttpResponse.delayed(hr, Duration.ofMillis(response.getDelay()), ctx.eventLoop());\n        } else {\n            return hr;\n        }\n    }\n\n    public static class Builder {\n\n        private ServerHandler handler;\n\n        private HttpHeaderTracking httpHeaderTracking;\n\n        public static Builder builder() {\n            return new Builder();\n        }\n\n        public Builder handler(ServerHandler value) {\n            handler = value;\n            return this;\n        }\n\n        public Builder httpHeaderTracking(HttpHeaderTracking value) {\n            httpHeaderTracking = value;\n            return this;\n        }\n\n        public HttpServerHandler build() {\n            return new HttpServerHandler(handler, httpHeaderTracking);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/HttpUtils.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.http.cert.SelfSignedCertGenerator;\nimport com.intuit.karate.shell.Command;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.handler.codec.http.DefaultFullHttpResponse;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpMessage;\nimport io.netty.handler.codec.http.HttpRequest;\nimport io.netty.handler.codec.http.HttpResponseStatus;\nimport io.netty.handler.codec.http.HttpVersion;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.net.URI;\n\nimport java.nio.charset.Charset;\nimport java.security.KeyStore;\nimport java.security.Security;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.net.ssl.KeyManager;\nimport javax.net.ssl.KeyManagerFactory;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.TrustManager;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class HttpUtils {\n\n    private static final Logger logger = LoggerFactory.getLogger(HttpUtils.class);\n\n    private HttpUtils() {\n        // only static methods\n    }\n\n    public static Charset parseContentTypeCharset(String mimeType) {\n        Map<String, String> map = parseContentTypeParams(mimeType);\n        if (map == null) {\n            return null;\n        }\n        String cs = map.get(\"charset\");\n        if (cs == null) {\n            return null;\n        }\n        return Charset.forName(cs);\n    }\n\n    public static Map<String, String> parseContentTypeParams(String mimeType) {\n        List<String> items = StringUtils.split(mimeType, ';', false);\n        int count = items.size();\n        if (count <= 1) {\n            return null;\n        }\n        Map<String, String> map = new LinkedHashMap<>(count - 1);\n        for (int i = 1; i < count; i++) {\n            String item = items.get(i);\n            int pos = item.indexOf('=');\n            if (pos == -1) {\n                continue;\n            }\n            String key = item.substring(0, pos).trim();\n            String val = item.substring(pos + 1).trim();\n            map.put(key, val);\n        }\n        return map;\n    }\n\n    public static Map<String, String> parseUriPattern(String pattern, String url) {\n        int qpos = url.indexOf('?');\n        if (qpos != -1) {\n            url = url.substring(0, qpos);\n        }\n        List<String> leftList = StringUtils.split(pattern, '/', false);\n        List<String> rightList = StringUtils.split(url, '/', false);\n        int leftSize = leftList.size();\n        int rightSize = rightList.size();\n        if (rightSize != leftSize) {\n            return null;\n        }\n        Map<String, String> map = new LinkedHashMap<>(leftSize);\n        for (int i = 0; i < leftSize; i++) {\n            String left = leftList.get(i);\n            String right = rightList.get(i);\n            if (left.equals(right)) {\n                continue;\n            }\n            if (left.startsWith(\"{\") && left.endsWith(\"}\")) {\n                left = left.substring(1, left.length() - 1);\n                map.put(left, right);\n            } else {\n                return null; // match failed\n            }\n        }\n        return map;\n    }\n\n    public static String normaliseUriPath(String uri) {\n        uri = uri.indexOf('?') == -1 ? uri : uri.substring(0, uri.indexOf('?'));\n        if (uri.endsWith(\"/\")) {\n            uri = uri.substring(0, uri.length() - 1);\n        }\n        if (!uri.startsWith(\"/\")) {\n            uri = \"/\" + uri;\n        }\n        return uri;\n    }\n\n    public static StringUtils.Pair parseUriIntoUrlBaseAndPath(String rawUri) {\n        int pos = rawUri.indexOf('/');\n        if (pos == -1) {\n            return StringUtils.pair(null, \"\");\n        }\n        URI uri;\n        try {\n            uri = new URI(rawUri);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        if (uri.getHost() == null) {\n            return StringUtils.pair(null, rawUri);\n        }\n        String path = uri.getRawPath();\n        pos = rawUri.lastIndexOf(path); // edge case that path is just \"/\"\n        String urlBase = rawUri.substring(0, pos);\n        return StringUtils.pair(urlBase, rawUri.substring(pos));\n    }\n\n    //==========================================================================\n    //\n    public static void flushAndClose(Channel ch) {\n        if (ch != null && ch.isActive()) {\n            ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);\n        }\n    }\n\n    public static void createSelfSignedCertificate(File cert, File key) {\n        try {\n            SelfSignedCertGenerator ssc = new SelfSignedCertGenerator();\n            FileUtils.copy(ssc.getCertificate(), cert);\n            FileUtils.copy(ssc.getPrivateKey(), key);\n        } catch (Exception e) {\n            throw new RuntimeException();\n        }\n    }\n    \n    private static final String PROXY_ALIAS = \"karate-proxy\";\n    private static final String KEYSTORE_PASSWORD = \"karate-secret\";\n    private static final String KEYSTORE_FILENAME = PROXY_ALIAS + \".jks\";    \n\n    public static SSLContext getSslContext(File keyStoreFile) {\n        keyStoreFile = initKeyStore(keyStoreFile);\n        String algorithm = Security.getProperty(\"ssl.KeyManagerFactory.algorithm\");\n        if (algorithm == null) {\n            algorithm = \"SunX509\";\n        }\n        try {\n            KeyStore ks = KeyStore.getInstance(\"JKS\");\n            ks.load(new FileInputStream(keyStoreFile), KEYSTORE_PASSWORD.toCharArray());\n            TrustManager[] trustManagers = new TrustManager[]{LenientTrustManager.INSTANCE};\n            KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);\n            kmf.init(ks, KEYSTORE_PASSWORD.toCharArray());\n            KeyManager[] keyManagers = kmf.getKeyManagers();\n            SSLContext ctx = SSLContext.getInstance(\"TLS\");\n            ctx.init(keyManagers, trustManagers, null);\n            return ctx;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static File initKeyStore(File keyStoreFile) {\n        if (keyStoreFile == null) {\n            keyStoreFile = new File(KEYSTORE_FILENAME);\n        }\n        if (keyStoreFile.exists()) {\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"keystore file already exists: {}\", keyStoreFile);\n            }\n            return keyStoreFile;\n        }\n        File parentFile = keyStoreFile.getParentFile();\n        if (parentFile != null) {\n            parentFile.mkdirs();\n        }\n        Command.exec(false, parentFile, \"keytool\", \"-genkey\", \"-alias\", PROXY_ALIAS, \"-keysize\",\n                \"4096\", \"-validity\", \"36500\", \"-keyalg\", \"RSA\", \"-dname\",\n                \"CN=\" + PROXY_ALIAS, \"-keypass\", KEYSTORE_PASSWORD, \"-storepass\",\n                KEYSTORE_PASSWORD, \"-keystore\", keyStoreFile.getName());\n        Command.exec(false, parentFile, \"keytool\", \"-exportcert\", \"-alias\", PROXY_ALIAS, \"-keystore\",\n                keyStoreFile.getName(), \"-storepass\", KEYSTORE_PASSWORD, \"-file\", keyStoreFile.getName() + \".der\");\n        return keyStoreFile;\n    }\n\n    public static FullHttpResponse createResponse(int status, String body) {\n        return createResponse(HttpResponseStatus.valueOf(status), body);\n    }\n\n    public static FullHttpResponse createResponse(HttpResponseStatus status, String body) {\n        byte[] bytes = FileUtils.toBytes(body);\n        ByteBuf bodyBuf = Unpooled.copiedBuffer(bytes);\n        DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, bodyBuf);\n        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, bytes.length);\n        return response;\n    }\n\n    public static FullHttpResponse transform(FullHttpResponse original, String body) {\n        FullHttpResponse response = createResponse(original.status(), body);\n        response.headers().set(original.headers());\n        return response;\n    }\n\n    private static final HttpResponseStatus CONNECTION_ESTABLISHED = new HttpResponseStatus(200, \"Connection established\");\n\n    public static FullHttpResponse connectionEstablished() {\n        return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, CONNECTION_ESTABLISHED);\n    }\n\n    public static void fixHeadersForProxy(HttpRequest request) {\n        String adjustedUri = ProxyContext.removeHostColonPort(request.uri());\n        request.setUri(adjustedUri);\n        request.headers().remove(HttpHeaderNames.CONNECTION);\n        // addViaHeader(request, PROXY_ALIAS);\n    }\n\n    public static void addViaHeader(HttpMessage msg, String alias) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(msg.protocolVersion().majorVersion()).append('.');\n        sb.append(msg.protocolVersion().minorVersion()).append(' ');\n        sb.append(alias);\n        List<String> list;\n        if (msg.headers().contains(HttpHeaderNames.VIA)) {\n            List<String> existing = msg.headers().getAll(HttpHeaderNames.VIA);\n            list = new ArrayList<>(existing);\n            list.add(sb.toString());\n        } else {\n            list = Collections.singletonList(sb.toString());\n        }\n        msg.headers().set(HttpHeaderNames.VIA, list);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/JvmSessionStore.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicLong;\n\n/**\n *\n * @author pthomas3\n */\npublic class JvmSessionStore implements SessionStore {\n\n    public static final SessionStore INSTANCE = new JvmSessionStore();\n\n    private static final AtomicLong COUNTER = new AtomicLong();\n\n    private final Map<String, Session> sessions = new ConcurrentHashMap();\n\n    private JvmSessionStore() {\n        // singleton\n    }\n\n    @Override\n    public Session create(long now, long expires) {\n        String id = COUNTER.incrementAndGet() + \"-\" + System.currentTimeMillis();\n        return new Session(id, new HashMap(), now, now, expires);\n    }\n\n    @Override\n    public Session get(String id) {\n        return sessions.get(id);\n    }\n\n    @Override\n    public void save(Session session) {\n        sessions.put(session.getId(), session);\n    }\n\n    @Override\n    public void delete(String id) {\n        sessions.remove(id);\n    }        \n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/LenientTrustManager.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport java.net.Socket;\nimport java.security.cert.CertificateException;\nimport java.security.cert.X509Certificate;\nimport javax.net.ssl.SSLEngine;\nimport javax.net.ssl.X509ExtendedTrustManager;\n\n/**\n *\n * @author pthomas3\n */\npublic class LenientTrustManager extends X509ExtendedTrustManager {\n    \n    public static final LenientTrustManager INSTANCE = new LenientTrustManager();\n\n    @Override\n    public void checkClientTrusted(X509Certificate[] xcs, String string, Socket socket) throws CertificateException {\n\n    }\n\n    @Override\n    public void checkServerTrusted(X509Certificate[] xcs, String string, Socket socket) throws CertificateException {\n\n    }\n\n    @Override\n    public void checkClientTrusted(X509Certificate[] xcs, String string, SSLEngine ssle) throws CertificateException {\n\n    }\n\n    @Override\n    public void checkServerTrusted(X509Certificate[] xcs, String string, SSLEngine ssle) throws CertificateException {\n\n    }\n\n    @Override\n    public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {\n\n    }\n\n    @Override\n    public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {\n\n    }\n\n    @Override\n    public X509Certificate[] getAcceptedIssuers() {\n        return new X509Certificate[0];\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/MultiPartBuilder.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.Constants;\nimport com.intuit.karate.JsonUtils;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.ByteBufAllocator;\nimport io.netty.buffer.Unpooled;\nimport io.netty.handler.codec.http.DefaultFullHttpRequest;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.HttpContent;\nimport io.netty.handler.codec.http.HttpMethod;\nimport io.netty.handler.codec.http.HttpVersion;\nimport io.netty.handler.codec.http.multipart.Attribute;\nimport io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;\nimport io.netty.handler.codec.http.multipart.HttpPostRequestEncoder;\nimport io.netty.handler.codec.http.multipart.HttpPostRequestEncoder.EncoderMode;\nimport io.netty.util.CharsetUtil;\nimport io.netty.handler.codec.http.multipart.InterfaceHttpData;\nimport io.netty.handler.codec.http.multipart.MemoryFileUpload;\nimport java.io.File;\nimport java.nio.charset.Charset;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class MultiPartBuilder {\n\n    private static final Logger logger = LoggerFactory.getLogger(MultiPartBuilder.class);\n\n    private final HttpClient client;\n    private final boolean multipart;\n    private final HttpPostRequestEncoder encoder;\n    private Map<String, Object> formFields; // only for the edge case of GET\n    private StringBuilder bodyForDisplay = new StringBuilder();\n\n    private String contentTypeHeader;\n\n    public String getBoundary() {\n        if (contentTypeHeader == null) {\n            return null;\n        }\n        int pos = contentTypeHeader.lastIndexOf('=');\n        return pos == -1 ? null : contentTypeHeader.substring(pos + 1);\n    }\n\n    public Map<String, Object> getFormFields() {\n        return formFields;\n    }\n\n    public String getContentTypeHeader() {\n        return contentTypeHeader;\n    }\n\n    public boolean isMultipart() {\n        return multipart;\n    }\n\n    public String getBodyForDisplay() {\n        return bodyForDisplay.toString();\n    }\n\n    public MultiPartBuilder(boolean multipart, HttpClient client) {\n        this.client = client;\n        this.multipart = multipart;\n        DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(\"POST\"), \"/\");\n        try {\n            encoder = new HttpPostRequestEncoder(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, multipart, CharsetUtil.UTF_8, EncoderMode.HTML5);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private void addFile(String name, File file, Map<String, Object> map) {\n        String filename = (String) map.get(\"filename\");\n        if (filename == null) {\n            filename = file.getName();\n        }\n        String contentType = (String) map.get(\"contentType\");\n        ResourceType resourceType;\n        if (contentType == null) {\n            resourceType = ResourceType.fromFileExtension(filename);\n        } else {\n            resourceType = ResourceType.fromContentType(contentType);\n        }\n        if (resourceType == null) {\n            resourceType = ResourceType.BINARY;\n        }\n        if (contentType == null) {\n            contentType = resourceType.contentType;\n        }\n        try {\n            encoder.addBodyFileUpload(name, filename, file, contentType, !resourceType.isBinary());\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public MultiPartBuilder part(Map<String, Object> map) {\n        String name = (String) map.get(\"name\");\n        Object value = map.get(\"value\");\n        if (!multipart) {\n            List<String> list;\n            if (value instanceof List) {\n                list = (List) value;\n            } else {\n                if (value == null) {\n                    list = Collections.emptyList();\n                } else {\n                    list = Collections.singletonList(value.toString());\n                }\n            }\n            if (formFields == null) {\n                formFields = new HashMap();\n            }\n            for (String s : list) {\n                formFields.put(name, s);\n                try {\n                    encoder.addBodyAttribute(name, s);\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n            }\n        } else {\n            if (value instanceof File) {\n                File file = (File) value;\n                addFile(name, file, map);\n            } else if (value instanceof List) { // recurse, hope that adding to array of fields is supported\n                List list = (List) value;\n                for (Object o : list) {\n                    Map<String, Object> temp = new HashMap();\n                    temp.put(\"name\", name);\n                    temp.put(\"value\", o);\n                    part(temp);\n                }\n            } else {\n                String contentType = (String) map.get(\"contentType\");\n                ResourceType resourceType;\n                if (contentType == null) {\n                    resourceType = ResourceType.fromObject(value);\n                } else {\n                    resourceType = ResourceType.fromContentType(contentType);\n                }\n                if (resourceType == null) {\n                    resourceType = ResourceType.BINARY;\n                }\n                if (contentType == null) {\n                    contentType = resourceType.contentType;\n                }\n                Charset cs = null;\n                if (!resourceType.isBinary()) {\n                    String charset = (String) map.get(\"charset\");\n                    if (charset == null && client != null && client.getConfig() != null) { // mock clients can have nulls\n                        cs = client.getConfig().getCharset();\n                    } else if (charset != null) {\n                        cs = Charset.forName(charset);\n                    }\n                }\n                byte[] encoded = value == null ? Constants.ZERO_BYTES : JsonUtils.toBytes(value);\n                String filename = (String) map.get(\"filename\");\n                if (filename == null) {\n                    filename = \"\"; // will be treated as an inline value, behaves like null\n                }\n                String transferEncoding = (String) map.get(\"transferEncoding\");\n                final Charset nullable = cs;\n                MemoryFileUpload item = new MemoryFileUpload(name, filename, contentType, transferEncoding, cs, encoded.length) {\n                    @Override\n                    public Charset getCharset() {\n                        return nullable; // workaround for netty api strictness\n                    }\n                };\n                try {\n                    item.setContent(Unpooled.wrappedBuffer(encoded));\n                    encoder.addBodyHttpData(item);\n                    logger.debug(\"multipart: {}\", item);\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n            }\n        }\n        return this;\n    }\n\n    public MultiPartBuilder part(String name, Object value) {\n        Map<String, Object> map = new HashMap();\n        map.put(\"name\", name);\n        map.put(\"value\", value);\n        return part(map);\n    }\n\n    public String toCurlCommand() {\n        StringBuilder sb = new StringBuilder();\n        Iterator<InterfaceHttpData> parts = encoder.getBodyListAttributes().iterator();\n        while (parts.hasNext()) {\n            InterfaceHttpData part = parts.next();\n            if (part instanceof Attribute) {\n                Attribute attr = (Attribute) part;\n                String value;\n                try {\n                    value = attr.getValue();\n                } catch (Exception e) {\n                    value = null;\n                    logger.error(\"failed to get multipart value: {}\", e.getMessage());\n                }\n                sb.append(\"-d \")\n                        .append(part.getName())\n                        .append(\"=\")\n                        .append(value);\n                if (parts.hasNext()) {\n                    sb.append(\" \\\\\\n\");\n                }\n            }\n        }\n        return sb.toString();\n    }\n\n    public byte[] build() {\n        // TODO move this to getter if possible\n        for (InterfaceHttpData part : encoder.getBodyListAttributes()) {\n            bodyForDisplay.append('\\n').append(part.toString()).append('\\n');\n        }\n        try {\n            io.netty.handler.codec.http.HttpRequest request = encoder.finalizeRequest();\n            contentTypeHeader = request.headers().get(HttpConstants.HDR_CONTENT_TYPE);\n            // logger.debug(\"content type header: {}\", contentTypeHeader);\n            ByteBuf content;\n            if (request instanceof FullHttpRequest) {\n                FullHttpRequest fullRequest = (FullHttpRequest) request;\n                content = fullRequest.content();\n            } else {\n                content = Unpooled.buffer();\n                HttpContent data;\n                while ((data = encoder.readChunk(ByteBufAllocator.DEFAULT)) != null) {\n                    content.writeBytes(data.content());\n                }\n            }\n            byte[] bytes = new byte[content.readableBytes()];\n            content.readBytes(bytes);\n            return bytes;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ProxyClientHandler.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport io.netty.bootstrap.Bootstrap;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.nio.NioSocketChannel;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.HttpClientCodec;\nimport io.netty.handler.codec.http.HttpContentDecompressor;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpHeaderValues;\nimport io.netty.handler.codec.http.HttpMethod;\nimport io.netty.handler.codec.http.HttpObjectAggregator;\nimport io.netty.handler.codec.http.HttpResponse;\nimport io.netty.handler.ssl.SslHandler;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.SSLEngine;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ProxyClientHandler extends SimpleChannelInboundHandler<FullHttpRequest> {\n\n    private static final Logger logger = LoggerFactory.getLogger(ProxyClientHandler.class);\n\n    protected final RequestFilter requestFilter;\n    protected final ResponseFilter responseFilter;\n    private final Map<String, ProxyRemoteHandler> REMOTE_HANDLERS = new ConcurrentHashMap();\n    private final Object LOCK = new Object();\n    \n    private ProxyRemoteHandler remoteHandler;\n    protected Channel clientChannel;\n\n    public ProxyClientHandler(RequestFilter requestFilter, ResponseFilter responseFilter) {\n        this.requestFilter = requestFilter;\n        this.responseFilter = responseFilter;\n    }\n    \n    @Override\n    public void channelActive(ChannelHandlerContext ctx) {\n        clientChannel = ctx.channel();\n    }\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {\n        boolean isConnect = HttpMethod.CONNECT.equals(request.method());\n        ProxyContext pc = new ProxyContext(request, isConnect);\n        // if ssl CONNECT, always create new remote pipeline\n        if (remoteHandler == null && !isConnect) {\n            remoteHandler = REMOTE_HANDLERS.get(pc.hostColonPort);\n        }\n        if (remoteHandler != null) {\n            remoteHandler.send(request);\n            return;\n        }\n        if (logger.isTraceEnabled()) {\n            logger.trace(\">> init: {} - {}\", pc, request);\n        }\n        Bootstrap b = new Bootstrap();\n        b.group(new NioEventLoopGroup(4));\n        b.channel(NioSocketChannel.class);\n        b.handler(new ChannelInitializer() {\n            @Override\n            protected void initChannel(Channel remoteChannel) throws Exception {\n                ChannelPipeline p = remoteChannel.pipeline();\n                if (isConnect) {\n                    SSLContext sslContext = HttpUtils.getSslContext(null);\n                    SSLEngine remoteSslEngine = sslContext.createSSLEngine(pc.host, pc.port);\n                    remoteSslEngine.setUseClientMode(true);\n                    remoteSslEngine.setNeedClientAuth(false);\n                    SslHandler remoteSslHandler = new SslHandler(remoteSslEngine);\n                    p.addLast(remoteSslHandler);\n                    remoteSslHandler.handshakeFuture().addListener(rhf -> {\n                        if (logger.isTraceEnabled()) {\n                            logger.trace(\"** ssl: server handshake done: {}\", remoteChannel);\n                        }\n                        SSLEngine clientSslEngine = sslContext.createSSLEngine();\n                        clientSslEngine.setUseClientMode(false);\n                        clientSslEngine.setNeedClientAuth(false);\n                        SslHandler clientSslHandler = new SslHandler(clientSslEngine);\n                        HttpResponse response = HttpUtils.connectionEstablished();\n                        response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);\n                        clientChannel.eventLoop().execute(() -> {\n                            clientChannel.writeAndFlush(response);\n                            clientChannel.pipeline().addFirst(clientSslHandler);\n                        });\n                        clientSslHandler.handshakeFuture().addListener(chf -> {\n                            if (logger.isTraceEnabled()) {\n                                logger.trace(\"** ssl: client handshake done: {}\", clientChannel);\n                            }\n                            unlockAndProceed();\n                        });\n                        lockAndWait();\n                    });\n                }\n                p.addLast(new HttpClientCodec());\n                p.addLast(new HttpContentDecompressor());\n                p.addLast(new HttpObjectAggregator(1048576));                 \n                remoteHandler = new ProxyRemoteHandler(pc, ProxyClientHandler.this, isConnect ? null : request);\n                REMOTE_HANDLERS.put(pc.hostColonPort, remoteHandler);\n                p.addLast(remoteHandler);\n                if (logger.isTraceEnabled()) {\n                    logger.trace(\"updated remote handlers: {}\", REMOTE_HANDLERS);\n                }\n            }\n        });\n        ChannelFuture cf = b.connect(pc.host, pc.port);\n        cf.addListener((ChannelFutureListener) future -> {\n            if (future.isSuccess()) {\n                if (logger.isTraceEnabled()) {\n                    logger.trace(\"** ready: {} - {}\", pc, cf.channel());\n                }\n            } else {\n                HttpUtils.flushAndClose(clientChannel);\n            }\n        });\n        if (!isConnect) {\n            lockAndWait();\n        }\n    }\n\n    private void lockAndWait() throws Exception {\n        synchronized (LOCK) {\n            LOCK.wait();\n        }\n    }\n\n    protected void unlockAndProceed() {\n        synchronized (LOCK) {\n            LOCK.notify();\n        }\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        if (cause.getMessage() == null) {\n            cause.printStackTrace();\n        } else {\n            logger.error(\"closing proxy inbound connection: {}\", cause.getMessage());\n        }\n        ctx.close();\n        HttpUtils.flushAndClose(remoteHandler.remoteChannel);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ProxyContext.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.StringUtils;\nimport io.netty.handler.codec.http.HttpHeaderNames;\nimport io.netty.handler.codec.http.HttpRequest;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class ProxyContext {\n\n    public final String host;\n    public final int port;\n    public final boolean ssl;\n    public final String hostColonPort;\n\n    private static final int HTTPS_PORT = 443;\n    private static final int HTTP_PORT = 80;\n\n    public ProxyContext(HttpRequest request, boolean ssl) {\n        this(getHostColonPortFromHeader(request), ssl);\n    }\n\n    public ProxyContext(String raw, boolean ssl) {\n        this.ssl = ssl;\n        raw = extractHostColonPort(raw);\n        int pos = raw.indexOf(':');\n        if (pos != -1) {\n            host = raw.substring(0, pos);\n            port = parsePort(raw.substring(pos + 1), ssl);\n        } else {\n            host = raw;\n            port = ssl ? HTTPS_PORT : HTTP_PORT;\n        }\n        hostColonPort = host + ':' + port;\n    }\n\n    @Override\n    public String toString() {\n        return (ssl ? \"https\" : \"http\") + \"://\" + host + \":\" + port;\n    }    \n    \n    private static int parsePort(String raw, boolean ssl) {\n        try {\n            return Integer.valueOf(raw);\n        } catch (Exception e) {\n            return ssl ? HTTPS_PORT : HTTP_PORT;\n        }\n    }\n\n    private static String getHostColonPortFromHeader(HttpRequest request) {\n        String hostColonPort = extractHostColonPort(request.uri());\n        if (StringUtils.isBlank(hostColonPort)) {\n            List<String> hosts = request.headers().getAll(HttpHeaderNames.HOST);\n            if (hosts != null && !hosts.isEmpty()) {\n                hostColonPort = hosts.get(0);\n            }\n        }\n        return hostColonPort;\n    }\n\n    private static String extractHostColonPort(String uri) {\n        int pos = uri.indexOf('/');\n        if (pos == -1) {\n            return uri;\n        }\n        if (uri.startsWith(\"http\") && pos < 7) {\n            uri = uri.substring(pos + 2);\n        }\n        pos = uri.indexOf('/');\n        if (pos == -1) {\n            return uri; \n        }\n        return uri.substring(0, pos);\n    }\n\n    public static String removeHostColonPort(String uri) {\n        if (!uri.startsWith(\"http\")) {\n            return uri;\n        }\n        uri = uri.substring(uri.indexOf('/') + 2);\n        int pos = uri.indexOf(\"/\");\n        if (pos == -1) {\n            return \"/\";\n        }\n        return uri.substring(pos);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ProxyRemoteHandler.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.util.ReferenceCountUtil;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ProxyRemoteHandler extends SimpleChannelInboundHandler<FullHttpResponse> {\n\n    private static final Logger logger = LoggerFactory.getLogger(ProxyRemoteHandler.class);\n\n    private final ProxyContext proxyContext;\n    private final ProxyClientHandler clientHandler;\n    private final RequestFilter requestFilter;\n    private final ResponseFilter responseFilter;\n    private final Channel clientChannel;\n    private final FullHttpRequest initialRequest;\n\n    protected Channel remoteChannel;\n    protected FullHttpRequest currentRequest;\n\n    public ProxyRemoteHandler(ProxyContext proxyContext, ProxyClientHandler clientHandler, FullHttpRequest initialRequest) {\n        this.proxyContext = proxyContext;\n        this.clientHandler = clientHandler;\n        this.clientChannel = clientHandler.clientChannel;\n        this.requestFilter = clientHandler.requestFilter;\n        this.responseFilter = clientHandler.responseFilter;\n        this.initialRequest = initialRequest;\n    }\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse response) throws Exception {\n        if (logger.isTraceEnabled()) {\n            logger.debug(\"<< {}\", response);\n        }\n        ProxyResponse filtered = responseFilter == null ? null : responseFilter.apply(proxyContext, currentRequest, response);\n        if (filtered == null || filtered.response == null) {\n            ReferenceCountUtil.retain(response);\n        } else {\n            response = filtered.response;\n            if (logger.isTraceEnabled()) {\n                logger.debug(\"<<<< {}\", response);\n            }            \n        }        \n        clientChannel.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);\n    }\n\n    protected void send(FullHttpRequest request) {\n        currentRequest = request;\n        FullHttpRequest filtered;\n        if (requestFilter != null) {\n            ProxyResponse pr = requestFilter.apply(proxyContext, request);\n            if (pr != null && pr.response != null) { // short circuit\n                clientChannel.writeAndFlush(pr.response);\n                return;\n            }\n            filtered = pr == null ? null : pr.request; // if not null, is transformed\n        } else {\n            filtered = null;\n        }\n        if (logger.isTraceEnabled()) {\n            logger.trace(\">> before: {}\", request);\n        }\n        if (filtered == null) {\n            ReferenceCountUtil.retain(request);\n            filtered = request;\n        } else {\n            if (logger.isTraceEnabled()) {\n                logger.trace(\">>>> after: {}\", filtered);\n            }\n        }\n        HttpUtils.fixHeadersForProxy(filtered);\n        remoteChannel.writeAndFlush(filtered);\n    }\n\n    @Override\n    public void channelActive(ChannelHandlerContext ctx) {\n        remoteChannel = ctx.channel();\n        if (initialRequest != null) { // only if not ssl\n            send(initialRequest);\n            clientHandler.unlockAndProceed();\n        }\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        if (cause.getMessage() == null) {\n            cause.printStackTrace();\n        } else {\n            logger.error(\"closing proxy outbound connection: {}\", cause.getMessage());\n        }\n        ctx.close();\n        HttpUtils.flushAndClose(clientChannel);\n    }\n\n    @Override\n    public String toString() {\n        return remoteChannel + \"\";\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ProxyRequest.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.FullHttpResponse;\n\n/**\n *\n * @author pthomas3\n */\npublic class ProxyRequest {\n    \n    public final ProxyContext context; \n    public final FullHttpRequest request;\n    \n    public String uri() {\n        return request.uri();\n    }\n    \n    public ProxyRequest(ProxyContext context, FullHttpRequest request) {\n        this.context = context;\n        this.request = request;\n    }\n    \n    public ProxyResponse fake(int status, String body) {\n        FullHttpResponse response = HttpUtils.createResponse(status, body);\n        return new ProxyResponse(null, null, response);\n    }    \n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ProxyResponse.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.FullHttpResponse;\n\n/**\n *\n * @author pthomas3\n */\npublic class ProxyResponse {\n\n    public final ProxyContext context;\n    public final FullHttpRequest request;\n    public final FullHttpResponse response;\n    \n    public String uri() {\n        return request.uri();\n    }    \n\n    public ProxyResponse transform(String body) {\n        return new ProxyResponse(context, request, HttpUtils.transform(response, body));\n    }\n    \n    public ProxyResponse fake(int status, String body) {\n        return new ProxyResponse(context, request, HttpUtils.createResponse(status, body));\n    }\n\n    public ProxyResponse(ProxyContext context, FullHttpRequest request, FullHttpResponse response) {\n        this.context = context;\n        this.request = request;\n        this.response = response;\n    }\n\n    public ProxyResponse header(String key, Object value) {\n        if (response != null) {\n            response.headers().add(key, value);\n        }\n        return this;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ProxyServer.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport io.netty.bootstrap.ServerBootstrap;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.EventLoopGroup;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.nio.NioServerSocketChannel;\nimport io.netty.handler.codec.http.HttpObjectAggregator;\nimport io.netty.handler.codec.http.HttpServerCodec;\nimport java.net.InetSocketAddress;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ProxyServer {\n\n    private static final Logger logger = LoggerFactory.getLogger(ProxyServer.class);\n\n    private final Channel channel;\n    private final int port;\n    private final EventLoopGroup bossGroup;\n    private final EventLoopGroup workerGroup;\n\n    public int getPort() {\n        return port;\n    }\n\n    public void waitSync() {\n        try {\n            channel.closeFuture().sync();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void stop() {\n        logger.info(\"stop: shutting down\");\n        bossGroup.shutdownGracefully();\n        workerGroup.shutdownGracefully();\n        logger.info(\"stop: shutdown complete\");\n    }\n\n    public ProxyServer(int requestedPort, RequestFilter requestFilter, ResponseFilter responseFilter) {\n        bossGroup = new NioEventLoopGroup(1);\n        workerGroup = new NioEventLoopGroup(8);\n        try {\n            ServerBootstrap b = new ServerBootstrap();\n            b.group(bossGroup, workerGroup)\n                    .channel(NioServerSocketChannel.class)\n                    .childHandler(new ChannelInitializer() {\n                        @Override\n                        protected void initChannel(Channel c) {\n                            ChannelPipeline p = c.pipeline();\n                            p.addLast(new HttpServerCodec());\n                            p.addLast(new HttpObjectAggregator(1048576));\n                            p.addLast(new ProxyClientHandler(requestFilter, responseFilter));\n                        }\n                    });\n            channel = b.bind(requestedPort).sync().channel();\n            InetSocketAddress isa = (InetSocketAddress) channel.localAddress();\n            String host = \"127.0.0.1\"; //isa.getHostString();\n            port = isa.getPort();\n            logger.info(\"proxy server started - http://{}:{}\", host, port);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/RedirectException.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\n/**\n *\n * @author pthomas3\n */\npublic class RedirectException extends RuntimeException {\n\n    private final String template;\n\n    public String getTemplate() {\n        return template;\n    }\n\n    public RedirectException(String template) {\n        super(\"redirect requested to: \" + template);\n        this.template = template;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/Request.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.graal.JsArray;\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.graal.Methods;\nimport com.linecorp.armeria.common.RequestContext;\nimport io.netty.buffer.Unpooled;\nimport io.netty.handler.codec.http.DefaultFullHttpRequest;\nimport io.netty.handler.codec.http.HttpMethod;\nimport io.netty.handler.codec.http.HttpVersion;\nimport io.netty.handler.codec.http.QueryStringDecoder;\nimport io.netty.handler.codec.http.cookie.ClientCookieDecoder;\nimport io.netty.handler.codec.http.cookie.Cookie;\nimport io.netty.handler.codec.http.multipart.Attribute;\nimport io.netty.handler.codec.http.multipart.FileUpload;\nimport io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder;\nimport io.netty.handler.codec.http.multipart.HttpPostStandardRequestDecoder;\nimport io.netty.handler.codec.http.multipart.InterfaceHttpData;\nimport io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Function;\nimport static java.util.stream.Collectors.toList;\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.ProxyObject;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class Request implements ProxyObject {\n\n    private static final Logger logger = LoggerFactory.getLogger(Request.class);\n\n    private static final String PATH = \"path\";\n    private static final String METHOD = \"method\";\n    private static final String PARAM = \"param\";\n    private static final String PARAM_INT = \"paramInt\";\n    private static final String PARAM_BOOL = \"paramBool\";\n    private static final String PARAM_JSON = \"paramJson\";\n    private static final String PARAM_EXISTS = \"paramExists\";\n    private static final String PARAMS = \"params\";\n    private static final String HEADER = \"header\";\n    private static final String HEADERS = \"headers\";\n    private static final String HEADER_VALUES = \"headerValues\";\n    private static final String PATH_PARAM = \"pathParam\";\n    private static final String PATH_PARAMS = \"pathParams\";\n    private static final String PATH_MATCHES = \"pathMatches\";\n    private static final String PATH_PATTERN = \"pathPattern\";\n    private static final String BODY = \"body\";\n    private static final String BODY_STRING = \"bodyString\";\n    private static final String BODY_BYTES = \"bodyBytes\";\n    private static final String MULTI_PART = \"multiPart\";\n    private static final String MULTI_PARTS = \"multiParts\";\n    private static final String GET = \"get\";\n    private static final String POST = \"post\";\n    private static final String PUT = \"put\";\n    private static final String DELETE = \"delete\";\n    private static final String PATCH = \"patch\";\n    private static final String HEAD = \"head\";\n    private static final String CONNECT = \"connect\";\n    private static final String OPTIONS = \"options\";\n    private static final String TRACE = \"trace\";\n    private static final String URL_BASE = \"urlBase\";\n    private static final String URL = \"url\";\n    private static final String PATH_RAW = \"pathRaw\";\n    private static final String START_TIME = \"startTime\";\n    private static final String END_TIME = \"endTime\";\n\n    private static final String[] KEYS = new String[]{\n        PATH, METHOD, PARAM, PARAM_INT, PARAM_BOOL, PARAM_JSON, PARAM_EXISTS, PARAMS,\n        HEADER, HEADERS, HEADER_VALUES, PATH_PARAM, PATH_PARAMS, PATH_MATCHES, PATH_PATTERN,\n        BODY, BODY_STRING, BODY_BYTES, MULTI_PART, MULTI_PARTS,\n        GET, POST, PUT, DELETE, PATCH, HEAD, CONNECT, OPTIONS, TRACE, URL_BASE, URL, PATH_RAW, START_TIME, END_TIME\n    };\n    private static final Set<String> KEY_SET = new HashSet<>(Arrays.asList(KEYS));\n    private static final JsArray KEY_ARRAY = new JsArray(KEYS);\n\n    private long startTime = System.currentTimeMillis();\n    private long endTime;\n    private String urlAndPath;\n    private String urlBase;\n    private String pathOriginal;\n    private String path;\n    private String method;\n    private Map<String, List<String>> params;\n    private Map<String, List<String>> headers;\n    private byte[] body;\n    private Map<String, List<Map<String, Object>>> multiParts;\n    private ResourceType resourceType;\n    private String resourcePath;\n    private Map<String, String> pathParams = Collections.emptyMap();\n    private String pathPattern;\n    private RequestContext requestContext;\n\n    public RequestContext getRequestContext() {\n        return requestContext;\n    }\n\n    public void setRequestContext(RequestContext requestContext) {\n        this.requestContext = requestContext;\n    }\n\n    public boolean isAjax() {\n        return getHeader(HttpConstants.HDR_HX_REQUEST) != null;\n    }\n\n    public boolean isMultiPart() {\n        return multiParts != null;\n    }\n\n    public Map<String, List<Map<String, Object>>> getMultiParts() {\n        return multiParts;\n    }\n\n    public List<String> getHeaderValues(String name) {\n        return StringUtils.getIgnoreKeyCase(headers, name); // TODO optimize\n    }\n\n    public String getHeader(String name) {\n        List<String> list = getHeaderValues(name);\n        if (list == null || list.isEmpty()) {\n            return null;\n        } else {\n            return list.get(0);\n        }\n    }\n\n    public String getContentType() {\n        return getHeader(HttpConstants.HDR_CONTENT_TYPE);\n    }\n\n    public List<Cookie> getCookies() {\n        List<String> cookieValues = getHeaderValues(HttpConstants.HDR_COOKIE);\n        if (cookieValues == null) {\n            return Collections.emptyList();\n        }\n        return cookieValues.stream().map(ClientCookieDecoder.STRICT::decode).collect(toList());\n    }\n\n    public int getParamInt(String name) {\n        String value = getParam(name);\n        try {\n            return value == null ? -1 : Integer.valueOf(value);\n        } catch (Exception e) {\n            return -1;\n        }\n    }\n\n    public boolean getParamBool(String name) {\n        String value = getParam(name);\n        try {\n            return value == null ? false : Boolean.valueOf(value);\n        } catch (Exception e) {\n            return false;\n        }\n    }\n\n    public String getParam(String name) {\n        List<String> values = getParamValues(name);\n        if (values == null || values.isEmpty()) {\n            return null;\n        }\n        return values.get(0);\n    }\n\n    public Object getParam(String name, Object value) {\n        String temp = getParam(name);\n        return StringUtils.isBlank(temp) ? value : temp;\n    }\n    \n    private final Methods.FunVar PARAM_FUNCTION = args -> {    \n        if (args.length == 0 || args[0] == null) {\n            return null;\n        }\n        String name = args[0].toString();\n        if (args.length > 1) {\n            return getParam(name, args[1]);\n        } else {\n            return getParam(name);\n        }\n    };\n\n    public List<String> getParamValues(String name) {\n        if (params == null) {\n            return null;\n        }\n        return params.get(name);\n    }\n    \n    public boolean getParamExists(String name) {\n        if (params == null) {\n            return false;\n        }\n        return params.containsKey(name);\n    }    \n\n    public String getPath() {\n        return path;\n    }\n\n    public String getPathRaw() {\n        if (urlBase != null && urlAndPath != null) {\n            if (urlAndPath.charAt(0) == '/') {\n                return urlAndPath;\n            } else {\n                return urlAndPath.substring(urlBase.length());\n            }\n        } else {\n            return path;\n        }\n    }\n\n    public void setUrl(String url) {\n        urlAndPath = url;\n        StringUtils.Pair pair = HttpUtils.parseUriIntoUrlBaseAndPath(url);\n        urlBase = pair.left;\n        QueryStringDecoder qsd = new QueryStringDecoder(pair.right);\n        setPath(qsd.path());\n        setParams(qsd.parameters());\n    }\n\n    public void setStartTime(long startTime) {\n        this.startTime = startTime;\n    }        \n\n    public long getStartTime() {\n        return startTime;\n    }\n\n    public void setEndTime(long endTime) {\n        this.endTime = endTime;\n    }\n\n    public long getEndTime() {\n        return endTime;\n    }        \n\n    public String getUrlAndPath() {\n        return urlAndPath != null ? urlAndPath : (urlBase != null ? urlBase : \"\") + path;\n    }\n\n    public String getUrlBase() {\n        return urlBase;\n    }\n\n    public void setUrlBase(String urlBase) {\n        this.urlBase = urlBase;\n    }\n\n    public void setPath(String path) {\n        if (path == null || path.isEmpty()) {\n            path = \"/\";\n        }\n        if (path.charAt(0) != '/') { // mocks and synthetic situations\n            path = \"/\" + path;\n        }\n        this.path = path;\n        if (pathOriginal == null) {\n            pathOriginal = path;\n        }\n    }\n\n    public String getPathOriginal() {\n        return pathOriginal;\n    }\n\n    public void setResourceType(ResourceType resourceType) {\n        this.resourceType = resourceType;\n    }\n\n    public String getResourcePath() {\n        return resourcePath;\n    }\n\n    public void setResourcePath(String resourcePath) {\n        this.resourcePath = resourcePath;\n    }\n\n    public String getMethod() {\n        return method;\n    }\n\n    public void setMethod(String method) {\n        this.method = method;\n    }\n\n    public Map<String, List<String>> getParams() {\n        return params == null ? Collections.emptyMap() : params;\n    }\n\n    public void setParams(Map<String, List<String>> params) {\n        this.params = params;\n    }\n\n    public boolean pathMatches(String pattern) {\n        Map<String, String> temp = HttpUtils.parseUriPattern(pattern, path);\n        if (temp == null) {\n            return false;\n        }\n        pathParams = temp;\n        pathPattern = pattern;\n        return true;\n    }\n\n    public void setParamCommaDelimited(String name, String value) {\n        if (value == null) {\n            return;\n        }\n        setParam(name, StringUtils.split(value, ',', false));\n    }\n\n    public void setParam(String name, Object value) {\n        if (params == null) {\n            params = new HashMap();\n        }\n        if (value == null) {\n            params.put(name, null);\n        } else if (value instanceof List) {\n            List list = (List) value;\n            List<String> values = new ArrayList(list.size());\n            for (Object o : list) {\n                values.add(o == null ? null : o.toString());\n            }\n            params.put(name, values);\n        } else {\n            params.put(name, Collections.singletonList(value.toString()));\n        }\n    }\n\n    public Object getPathParam() {\n        if (pathParams.isEmpty()) {\n            return null;\n        }\n        return pathParams.values().iterator().next();\n    }\n\n    public Map<String, String> getPathParams() {\n        return pathParams;\n    }\n\n    public void setPathParams(Map<String, String> pathParams) {\n        this.pathParams = pathParams;\n    }\n\n    public Map<String, List<String>> getHeaders() {\n        return headers == null ? Collections.emptyMap() : headers;\n    }\n\n    public void setHeaders(Map<String, List<String>> headers) {\n        this.headers = headers;\n    }\n    \n    public void setCookiesRaw(List<String> values) {\n        if (values == null) {\n            return;\n        }\n        if (headers == null) {\n            headers = new HashMap();\n        }\n        headers.put(HttpConstants.HDR_COOKIE, values);\n    }\n\n    public void setHeaderCommaDelimited(String name, String value) {\n        if (value == null) {\n            return;\n        }\n        if (headers == null) {\n            headers = new HashMap();\n        }\n        headers.put(name, StringUtils.split(value, ',', false));\n    }\n\n    public byte[] getBody() {\n        return body;\n    }\n\n    public void setBody(byte[] body) {\n        this.body = body;\n    }\n\n    public String getBodyAsString() {\n        return body == null ? null : FileUtils.toString(body);\n    }\n\n    public Object getBodyConverted() {\n        ResourceType rt = getResourceType(); // derive if needed\n        if (rt != null && rt.isBinary()) {\n            return body;\n        }\n        return JsonUtils.fromBytes(body, false, rt);\n    }\n\n    public boolean isHttpGetForStaticResource() {\n        if (!\"GET\".equals(method)) {\n            return false;\n        }\n        ResourceType rt = getResourceType();\n        return rt != null && !rt.isUrlEncodedOrMultipart();\n    }\n\n    public ResourceType getResourceType() {\n        if (resourceType == null) {\n            String contentType = getContentType();\n            if (contentType != null) {\n                resourceType = ResourceType.fromContentType(contentType);\n            }\n        }\n        return resourceType;\n    }\n\n    public Object getParamJson(String name) {\n        String value = getParam(name);\n        if (StringUtils.isBlank(value)) {\n            return null;\n        }\n        try {\n            return JsValue.fromJava(JsonUtils.fromJson(value));\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    public Map<String, Object> getMultiPart(String name) {\n        if (multiParts == null) {\n            return null;\n        }\n        List<Map<String, Object>> parts = multiParts.get(name);\n        if (parts == null || parts.isEmpty()) {\n            return null;\n        }\n        return parts.get(0);\n    }\n\n    public Object getMultiPartAsJsValue(String name) {\n        return JsValue.fromJava(getMultiPart(name));\n    }\n\n    public void processBody() {\n        if (body == null) {\n            return;\n        }\n        String contentType = getContentType();\n        if (contentType == null) {\n            return;\n        }\n        boolean multipart;\n        if (contentType.startsWith(\"multipart\")) {\n            multipart = true;\n            multiParts = new HashMap<>();\n        } else if (contentType.contains(\"form-urlencoded\")) {\n            multipart = false;\n        } else {\n            return;\n        }\n        logger.trace(\"decoding content-type: {}\", contentType);\n        params = (params == null || params.isEmpty()) ? new HashMap<>() : new HashMap<>(params); // since it may be immutable\n        DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), path, Unpooled.wrappedBuffer(body));\n        request.headers().add(HttpConstants.HDR_CONTENT_TYPE, contentType);\n        InterfaceHttpPostRequestDecoder decoder = multipart ? new HttpPostMultipartRequestDecoder(request) : new HttpPostStandardRequestDecoder(request);\n        try {\n            for (InterfaceHttpData part : decoder.getBodyHttpDatas()) {\n                String name = part.getName();\n                if (multipart && part instanceof FileUpload) {\n                    List<Map<String, Object>> list = multiParts.computeIfAbsent(name, k -> new ArrayList<>());\n                    Map<String, Object> map = new HashMap<>();\n                    list.add(map);\n                    FileUpload fup = (FileUpload) part;\n                    map.put(\"name\", name);\n                    map.put(\"filename\", fup.getFilename());\n                    Charset charset = fup.getCharset();\n                    if (charset != null) {\n                        map.put(\"charset\", charset.name());\n                    }\n                    String ct = fup.getContentType();\n                    map.put(\"contentType\", ct);\n                    map.put(\"value\", fup.get()); // bytes\n                    String transferEncoding = fup.getContentTransferEncoding();\n                    if (transferEncoding != null) {\n                        map.put(\"transferEncoding\", transferEncoding);\n                    }\n                } else { // form-field, url-encoded if not multipart\n                    Attribute attribute = (Attribute) part;\n                    List<String> list = params.computeIfAbsent(name, k -> new ArrayList<>());\n                    list.add(attribute.getValue());\n                }\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        } finally {\n            decoder.destroy();\n        }\n    }\n\n    @Override\n    public Object getMember(String key) {\n        switch (key) {\n            case METHOD:\n                return method;\n            case BODY:\n                return JsValue.fromJava(getBodyConverted());\n            case BODY_STRING:\n                return getBodyAsString();\n            case BODY_BYTES:\n                return body;\n            case PARAM:\n                return PARAM_FUNCTION;\n            case PARAM_INT:\n                return (Function<String, Integer>) this::getParamInt;\n            case PARAM_BOOL:\n                return (Function<String, Boolean>) this::getParamBool;\n            case PARAM_JSON:\n                return (Function<String, Object>) this::getParamJson;\n            case PARAM_EXISTS:\n                return (Function<String, Boolean>) this::getParamExists;    \n            case PATH:\n                return path;\n            case PATH_RAW:\n                return getPathRaw();\n            case URL_BASE:\n                return urlBase;\n            case URL:\n                return urlAndPath;\n            case PARAMS:\n                return JsValue.fromJava(params);\n            case PATH_PARAM:\n                return getPathParam();\n            case PATH_PARAMS:\n                return JsValue.fromJava(pathParams);\n            case PATH_MATCHES:\n                return (Function<String, Object>) this::pathMatches;\n            case PATH_PATTERN:\n                return pathPattern;\n            case HEADER:\n                return (Function<String, String>) this::getHeader;\n            case HEADERS:\n                return JsValue.fromJava(JsonUtils.simplify(headers));\n            case HEADER_VALUES:\n                return (Function<String, List<String>>) this::getHeaderValues;\n            case MULTI_PART:\n                return (Function<String, Object>) this::getMultiPartAsJsValue;\n            case MULTI_PARTS:\n                return JsValue.fromJava(multiParts);\n            case GET:\n            case POST:\n            case PUT:\n            case DELETE:\n            case PATCH:\n            case HEAD:\n            case CONNECT:\n            case OPTIONS:\n            case TRACE:\n                return method.toLowerCase().equals(key);\n            case START_TIME:\n                return startTime;\n            case END_TIME:\n                return endTime;\n            default:\n                logger.warn(\"no such property on request object: {}\", key);\n                return null;\n        }\n    }\n\n    public Map<String, Object> toMap() {\n        Map<String, Object> map = new HashMap();\n        map.put(URL, urlAndPath);\n        map.put(URL_BASE, urlBase);\n        map.put(PATH, path);        \n        map.put(PATH_RAW, getPathRaw());\n        map.put(METHOD, method);\n        map.put(HEADERS, JsonUtils.simplify(headers));\n        map.put(PARAMS, JsonUtils.simplify(params));\n        map.put(BODY, getBodyConverted());\n        return map;\n    }\n\n    @Override\n    public Object getMemberKeys() {\n        return KEY_ARRAY;\n    }\n\n    @Override\n    public boolean hasMember(String key) {\n        return KEY_SET.contains(key);\n    }\n\n    @Override\n    public void putMember(String key, Value value) {\n        logger.warn(\"put not supported on request object: {} - {}\", key, value);\n    }\n\n    @Override\n    public String toString() {\n        return method + \" \" + pathOriginal;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/RequestCycle.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.graal.JsEngine;\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.resource.ResourceResolver;\nimport com.intuit.karate.template.KarateTemplateEngine;\nimport java.io.InputStream;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Function;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class RequestCycle {\n\n    private static final Logger logger = LoggerFactory.getLogger(RequestCycle.class);\n\n    public static final String CONTEXT = \"context\";\n    private static final String REQUEST = \"request\";\n    protected static final String SESSION = \"session\";\n    private static final String RESPONSE = \"response\";\n\n    private static final ThreadLocal<RequestCycle> THREAD_LOCAL = new ThreadLocal();\n\n    public static RequestCycle get() {\n        return THREAD_LOCAL.get();\n    }\n\n    protected static RequestCycle init(KarateTemplateEngine te, ServerContext context) {\n        RequestCycle rc = new RequestCycle(JsEngine.global(), te, context);\n        THREAD_LOCAL.set(rc);\n        return rc;\n    }\n\n    private final JsEngine engine;\n    private final KarateTemplateEngine templateEngine;\n    private final Request request;\n    private final Response response;\n    private final ServerContext context;\n    private final ServerConfig config;\n    private final Function<ServerContext, Boolean> requestValidator;\n\n    private String switchTemplate;\n    private Map<String, Object> switchParams;\n\n    private RequestCycle(JsEngine engine, KarateTemplateEngine templateEngine, ServerContext context) {\n        this.engine = engine;\n        this.templateEngine = templateEngine;\n        this.requestValidator = context.getRequestValidator();\n        this.context = context;\n        config = context.getConfig();\n        Session session = context.getSession();\n        if (session != null && !session.isTemporary()) {\n            engine.put(SESSION, session.getData());\n        } else {\n            // easier for users to write code such as\n            // if (session.foo) {}\n            engine.put(SESSION, Collections.emptyMap());\n        }\n        // this has to be after the session init\n        Map<String, Object> variables = context.getVariables();\n        if (variables != null) {\n            engine.putAll(variables);\n        }\n        request = context.getRequest();\n        request.processBody();\n        engine.put(REQUEST, request);\n        response = new Response(200);\n        engine.put(RESPONSE, response);\n        engine.put(CONTEXT, context);\n    }\n\n    public RequestCycle copy(Request request, Map<String, Object> variables) {\n        ServerContext temp = new ServerContext(config, request, variables);\n        temp.setSession(context.getSession());\n        return new RequestCycle(JsEngine.local(), templateEngine, temp);\n    }\n\n    public JsEngine getEngine() {\n        return engine;\n    }\n\n    public KarateTemplateEngine getTemplateEngine() {\n        return templateEngine;\n    }\n\n    public ResourceResolver getResourceResolver() {\n        return config.getResourceResolver();\n    }\n\n    private void close() {\n        Session session = context.getSession();\n        if (session != null && !session.isTemporary()) {\n            if (context.isClosed()) {\n                // note that session cookie is deleted in response-builder\n                context.getConfig().getSessionStore().delete(session.getId());\n                session.getData().clear();\n                logger.debug(\"session deleted: {}\", session.getId());\n            } else {\n                JsValue sessionValue = engine.get(SESSION);\n                if (sessionValue.isObject()) {\n                    session.getData().putAll(sessionValue.getAsMap());\n                    context.getConfig().getSessionStore().save(session);\n                } else {\n                    logger.error(\"invalid session, not map-like: {}\", sessionValue);\n                }\n            }\n        }\n        JsEngine.remove();\n        THREAD_LOCAL.remove();\n    }\n\n    public Session getSession() {\n        return context.getSession();\n    }\n\n    public Request getRequest() {\n        return request;\n    }\n\n    public Response getResponse() {\n        return response;\n    }\n\n    public ServerContext getContext() {\n        return context;\n    }\n\n    public void setSwitchTemplate(String switchTemplate) {\n        this.switchTemplate = switchTemplate;\n    }\n\n    public String getSwitchTemplate() {\n        return switchTemplate;\n    }\n\n    public void setSwitchParams(Map<String, Object> switchParams) {\n        // clone to allow context.switch('pagename', request.params);\n        // else the htmlResponse() routine will clear \"self\"\n        this.switchParams = new HashMap(switchParams);\n    }\n\n    protected Response handle() {\n        try {\n            if (requestValidator != null) {\n                Boolean valid = requestValidator.apply(context);\n                if (valid == null || !valid) {\n                    logger.error(\"unauthorized request: {}\", request);\n                    response.setStatus(401); // just for logging in finally block\n                    return response().buildWithStatus(401);\n                }\n            }\n            if (context.isApi()) {\n                InputStream is = apiResource();\n                if (context.isLockNeeded()) {\n                    synchronized (config) {\n                        engine.eval(is);\n                    }\n                } else {\n                    engine.eval(is);\n                }\n                return response().build();\n            } else {\n                return htmlResponse();\n            }\n        } catch (Exception e) {\n            logger.error(\"handle failed: {}\", e.getMessage());\n            response.setStatus(500); // just for logging in finally block\n            return response().buildWithStatus(500);\n        } finally {\n            close();\n            if (logger.isDebugEnabled()) {\n                logger.debug(\"{} {} [{} ms]\", request, response.getStatus(), System.currentTimeMillis() - request.getStartTime());\n            }\n        }\n    }\n\n    private Response htmlResponse() {\n        String html;\n        try {\n            html = templateEngine.process(request.getPath());\n        } catch (Exception e) {\n            if (context.isSwitched()) {\n                if (switchTemplate == null) {\n                    logger.debug(\"abort template requested\");\n                    html = null;\n                } else {\n                    logger.debug(\"switch template requested: {}\", switchTemplate);\n                    request.getParams().clear();\n                    if (switchParams != null) {\n                        switchParams.forEach((k, v) -> request.setParam(k, v));\n                    }\n                    html = templateEngine.process(switchTemplate);\n                }\n            } else {\n                throw e;\n            }\n        }\n        return response().html(html).build();\n    }\n\n    private static final String DOT_JS = \".js\";\n\n    private InputStream apiResource() {\n        String resourcePath = request.getResourcePath();\n        String jsPath = resourcePath == null ? request.getPathOriginal() + DOT_JS : resourcePath;\n        try {\n            return config.getResourceResolver().resolve(jsPath).getStream();\n        } catch (Exception e) {\n            throw new RuntimeException(\"failed to resolve api resource: \" + resourcePath + \", \" + request + \", \" + e.getMessage());\n        }\n    }\n\n    public ResponseBuilder response() {\n        return new ResponseBuilder(config, this).session(context.getSession(), context.isNewSession());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/RequestFilter.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport java.util.function.Function;\n\n/**\n *\n * @author pthomas3\n */\npublic interface RequestFilter extends Function<ProxyRequest, ProxyResponse> {\n\n    default ProxyResponse apply(ProxyContext context, FullHttpRequest request) {\n        return apply(new ProxyRequest(context, request));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/RequestHandler.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.template.KarateTemplateEngine;\nimport com.intuit.karate.template.TemplateUtils;\nimport java.time.Instant;\nimport java.util.function.Function;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class RequestHandler implements ServerHandler {\n\n    private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class);\n\n    private static final String SLASH = \"/\";\n\n    private final SessionStore sessionStore;\n    private final KarateTemplateEngine templateEngine;\n    private final ServerConfig config;\n    private final Function<Request, ServerContext> contextFactory;\n    private final String stripHostContextPath;\n\n    public RequestHandler(ServerConfig config) {\n        this.config = config;\n        contextFactory = config.getContextFactory();\n        templateEngine = TemplateUtils.forServer(config);\n        sessionStore = config.getSessionStore();\n        stripHostContextPath = config.isStripContextPathFromRequest() ? config.getHostContextPath() : null;\n    }\n\n    @Override\n    public Response handle(Request request) {\n        if (stripHostContextPath != null) {\n            if (request.getPath().startsWith(stripHostContextPath)) {\n                request.setPath(request.getPath().substring(stripHostContextPath.length()));\n            }\n        }\n        if (SLASH.equals(request.getPath())) {\n            request.setPath(config.getHomePagePath());\n        }\n        ServerContext context = contextFactory.apply(request);\n        if (request.getResourceType() == null) { // can be set by context factory\n            request.setResourceType(ResourceType.fromFileExtension(request.getPath()));\n        }\n        if (!context.isApi() && request.isHttpGetForStaticResource() && context.isHttpGetAllowed()) {\n            if (request.getResourcePath() == null) { // can be set by context factory\n                request.setResourcePath(request.getPath()); // static resource\n            }\n            try {\n                return response().buildStatic(request);\n            } finally {\n                if (logger.isDebugEnabled()) {\n                    logger.debug(\"{} {} [{} ms]\", request, 200, System.currentTimeMillis() - request.getStartTime());\n                }\n            }\n        }\n        Session session = context.getSession(); // can be pre-resolved by context-factory\n        if (session == null && !context.isStateless()) {\n            String sessionId = context.getSessionCookieValue();\n            if (sessionId != null) {\n                session = sessionStore.get(sessionId);\n                if (session != null && isExpired(session)) {\n                    logger.debug(\"session expired: {}\", session);\n                    sessionStore.delete(sessionId);\n                    session = null;\n                }\n            }\n            if (session == null) {\n                if (config.isUseGlobalSession()) {\n                    session = ServerConfig.GLOBAL_SESSION;\n                } else {\n                    if (config.isAutoCreateSession()) {\n                        context.init();\n                        session = context.getSession();\n                        logger.debug(\"auto-created session: {} - {}\", request, session);\n                    } else if (config.getSigninPagePath().equals(request.getPath())\n                            || config.getSignoutPagePath().equals(request.getPath())) {\n                        session = Session.TEMPORARY;\n                        logger.debug(\"auth flow: {}\", request);\n                    } else {\n                        logger.warn(\"session not found: {}\", request);\n                        ResponseBuilder rb = response();\n                        if (sessionId != null) {\n                            rb.deleteSessionCookie(sessionId);\n                        }\n                        if (request.isAjax()) {\n                            rb.ajaxRedirect(signInPath());\n                        } else {\n                            rb.locationHeader(signInPath());\n                        }\n                        return rb.buildWithStatus(302);\n                    }\n                }\n            }\n            context.setSession(session);\n        }\n        RequestCycle rc = RequestCycle.init(templateEngine, context);\n        return rc.handle();\n    }\n\n    private String signInPath() {\n        String path = config.getSigninPagePath();\n        String contextPath = config.getHostContextPath();\n        return contextPath == null ? path : contextPath + path.substring(1);\n    }\n\n    private boolean isExpired(Session session) {\n        int configExpirySeconds = config.getSessionExpirySeconds();\n        if (configExpirySeconds == -1) {\n            return false;\n        }\n        long now = Instant.now().getEpochSecond();\n        long expires = session.getUpdated() + configExpirySeconds;\n        if (now > expires) {\n            return true;\n        }\n        session.setUpdated(now);\n        session.setExpires(expires);\n        return false;\n    }\n\n    private ResponseBuilder response() {\n        return new ResponseBuilder(config, null);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ResourceType.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.w3c.dom.Node;\n\n/**\n *\n * @author pthomas3\n */\npublic enum ResourceType {\n\n    DEFERRED_JS(\"text/javascript\", vals(\"javascript\"), vals(\"js\")),\n    JS(\"text/javascript\", vals(\"javascript\"), vals(\"js\")),\n    JSON(\"application/json\", vals(\"json\"), vals(\"json\")),\n    CSS(\"text/css\", vals(\"css\"), vals(\"css\")),\n    MAP(\"application/json\", vals(), vals(\"map\")),\n    ICO(\"image/x-icon\", vals(\"x-icon\"), vals(\"ico\")),\n    PNG(\"image/png\", vals(\"png\"), vals(\"png\")),\n    GIF(\"image/gif\", vals(\"gif\"), vals(\"gif\")),\n    JPG(\"image/jpeg\", vals(\"jpeg\", \"jpg\"), vals(\"jpg\", \"jpeg\")),\n    SVG(\"image/svg+xml\", vals(\"svg\"), vals(\"svg\")),\n    MP4(\"video/mp4\", vals(\"mp4\"), vals(\"mp4\")),\n    PDF(\"application/pdf\", vals(\"pdf\"), vals(\"pdf\")),\n    HTML(\"text/html\", vals(\"html\"), vals(\"html\", \"htm\")),\n    XML(\"application/xml\", vals(\"xml\"), vals(\"xml\")),\n    TEXT(\"text/plain\", vals(\"plain\"), vals(\"txt\")),\n    WOFF2(\"application/font-woff2\", vals(\"woff2\"), vals(\"woff2\")),\n    MULTIPART(\"multipart/form-data\", vals(\"multipart\"), vals()),    \n    URLENCODED(\"application/x-www-form-urlencoded\", vals(\"urlencoded\"), vals()),\n    BINARY(\"application/octet-stream\", vals(\"octet\"), vals()),\n    RDFXML(\"application/rdf+xml\", vals(\"rdf\", \"rdf+xml\"), vals(\"rdf\")),\n    NTRIPLES(\"application/n-triples\", vals(\"triples\"), vals(\"nt\")),\n    TURTLE(\"text/turtle\", vals(\"turtle\"), vals(\"ttl\")),\n    NQUADS(\"application/n-quads\", vals(\"quads\"), vals(\"nq\")),\n    TRIG(\"application/trig\", vals(\"trig\"), vals(\"trig\")),\n    N3(\"text/n3\", vals(\"n3\"), vals(\"n3\")),\n    JSONLD(\"application/ld+json\", vals(\"ld+json\"), vals(\"jsonld\"));\n\n    private static String[] vals(String... values) {\n        return values;\n    }\n\n    public final String contentType;\n    public final String[] contentLike;\n    public final String[] extensions;\n\n    ResourceType(String contentType, String[] contentLike, String[] extensions) {\n        this.contentType = contentType;\n        this.contentLike = contentLike;\n        this.extensions = extensions;\n    }\n\n    private static final Map<String, ResourceType> EXTENSION_MAP = new HashMap();\n\n    static {\n        for (ResourceType rt : ResourceType.values()) {\n            for (String ext : rt.extensions) {\n                EXTENSION_MAP.put(ext, rt);\n            }\n        }\n    }\n\n    public static ResourceType fromFileExtension(String path) {\n        if (path == null) {\n            return null;\n        }\n        int pos = path.lastIndexOf('.');\n        if (pos == -1 || pos == path.length() - 1) {\n            return null;\n        }\n        String extension = path.substring(pos + 1).trim().toLowerCase();\n        return EXTENSION_MAP.get(extension);\n    }\n\n    public String getExtension() {\n        return extensions.length == 0 ? null : extensions[0];\n    }\n\n    public boolean isVideo() {\n        switch (this) {\n            case MP4:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    public boolean isImage() {\n        switch (this) {\n            case BINARY:\n            case ICO:\n            case PNG:\n            case GIF:\n            case JPG:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    public boolean isUrlEncodedOrMultipart() {\n        switch (this) {\n            case URLENCODED:\n            case MULTIPART:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    public boolean isHtml() {\n        return this == HTML;\n    }\n\n    public boolean isJson() {\n        switch (this) {\n            case JSON:\n            case JSONLD:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    public boolean isXml() {\n        switch (this) {\n            case XML:\n            case RDFXML:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    public boolean isText() {\n        return this == TEXT;\n    }\n\n    public boolean isBinary() {\n        switch (this) {\n            case BINARY:\n            case ICO:\n            case PNG:\n            case GIF:\n            case JPG:\n            case PDF:\n            case MP4:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    public boolean isScript() {\n        switch (this){\n            case JS:\n            case DEFERRED_JS:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    public static ResourceType fromContentType(String ct) {\n        if (ct == null) {\n            return null;\n        }\n        ct = ct.toLowerCase();\n        for (ResourceType rt : ResourceType.values()) {\n            if (ct.equals(rt.contentType)) {\n                return rt;\n            }\n            for (String like : rt.contentLike) {\n                if (ct.contains(like)) {\n                    return rt;\n                }\n            }\n        }\n        return null;\n    }\n\n    public static ResourceType fromObject(Object o) {\n        return fromObject(o, null);\n    }\n\n    public static ResourceType fromObject(Object o, ResourceType defaultType) {\n        if (o instanceof List || o instanceof Map) {\n            return JSON;\n        } else if (o instanceof String) {\n            return TEXT;\n        } else if (o instanceof Node) {\n            return XML;\n        } else if (o instanceof byte[]) {\n            return BINARY;\n        } else {\n            return defaultType;\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/Response.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.Json;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.graal.JsArray;\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.graal.Methods;\nimport io.netty.handler.codec.http.cookie.ClientCookieDecoder;\nimport io.netty.handler.codec.http.cookie.Cookie;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Function;\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.ProxyObject;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class Response implements ProxyObject {\n\n    private static final Logger logger = LoggerFactory.getLogger(Response.class);\n\n    public static final Response OK = new Response(200);\n\n    private static final String BODY = \"body\";\n    private static final String BODY_BYTES = \"bodyBytes\";\n    private static final String STATUS = \"status\";\n    private static final String HEADER = \"header\";\n    private static final String HEADERS = \"headers\";\n    private static final String HEADER_VALUES = \"headerValues\";\n    private static final String DATA_TYPE = \"dataType\";\n    private static final String RESPONSE_TIME = \"responseTime\";\n\n    private static final String[] KEYS = new String[]{STATUS, HEADER, HEADERS, HEADER_VALUES, BODY, DATA_TYPE, BODY_BYTES, RESPONSE_TIME};\n    private static final Set<String> KEY_SET = new HashSet(Arrays.asList(KEYS));\n    private static final JsArray KEY_ARRAY = new JsArray(KEYS);\n\n    private int status;\n    private Map<String, List<String>> headers;\n    private Object body;\n\n    private ResourceType resourceType;\n    private int delay;\n    private long responseTime;\n\n    public Response(int status) {\n        this.status = status;\n    }\n\n    public Response(int status, Map<String, List<String>> headers, byte[] body) {\n        this(status, headers, body, null);\n    }\n\n    public Response(int status, Map<String, List<String>> headers, byte[] body, ResourceType resourceType) {\n        this.status = status;\n        this.headers = headers;\n        this.body = body;\n        this.resourceType = resourceType;\n    }\n\n    public int getStatus() {\n        return status;\n    }\n\n    public void setStatus(int status) {\n        this.status = status;\n    }\n\n    public int getDelay() {\n        return delay;\n    }\n\n    public void setDelay(int delay) {\n        this.delay = delay;\n    }\n\n    public void setResponseTime(long responseTime) {\n        this.responseTime = responseTime;\n    }\n\n    public long getResponseTime() {\n        return responseTime;\n    }    \n       \n\n    public Map<String, List<String>> getHeaders() {\n        return headers;\n    }\n\n    public Map<String, List<String>> getHeadersWithLowerCaseNames() {\n        Map<String, List<String>> map = new HashMap(headers.size());\n        headers.forEach((k, v) -> map.put(k.toLowerCase(), v));\n        return map;\n    }\n\n    public Map<String, Map> getCookies() {\n        List<String> values = getHeaderValues(HttpConstants.HDR_SET_COOKIE);\n        if (values == null) {\n            return null;\n        }\n        Map<String, Map> map = new HashMap();\n        for (String value : values) {\n            Cookie cookie = ClientCookieDecoder.LAX.decode(value);\n            if (cookie != null) { // can be null if cookie contains invalid characters\n                map.put(cookie.name(), Cookies.toMap(cookie));\n            }\n        }\n        return map;\n    }\n\n    public byte[] getBody() {\n        if (body instanceof byte[]) {\n            return (byte[]) body;\n        }\n        return JsonUtils.toBytes(body);\n    }\n\n    public void setBody(byte[] body) {\n        this.body = body;\n    }\n\n    public void setBody(String value) {\n        body = FileUtils.toBytes(value);\n    }\n\n    public String getBodyAsString() {\n        return body == null ? null : FileUtils.toString(getBody());\n    }\n\n    public Object getBodyConverted() {\n        if (body instanceof byte[]) {\n            ResourceType rt = getResourceType(); // derive if needed\n            if (rt != null && rt.isBinary()) {\n                return body;\n            }\n            return JsonUtils.fromBytes((byte[]) body, false, rt);\n        } else {\n            return body;\n        }\n    }\n\n    public Json json() {\n        return body == null ? null : Json.of(getBodyConverted());\n    }\n\n    public boolean isBinary() {\n        ResourceType rt = getResourceType();\n        return rt == null ? false : rt.isBinary();\n    }\n\n    public ResourceType getResourceType() {\n        if (resourceType == null) {\n            String contentType = getContentType();\n            if (contentType != null) {\n                resourceType = ResourceType.fromContentType(contentType);\n            }\n        }\n        return resourceType;\n    }\n\n    public void setResourceType(ResourceType resourceType) {\n        this.resourceType = resourceType;\n    }\n\n    public List<String> getHeaderValues(String name) { // TOTO optimize\n        return StringUtils.getIgnoreKeyCase(headers, name);\n    }\n\n    public String getHeader(String name) {\n        List<String> values = getHeaderValues(name);\n        return values == null || values.isEmpty() ? null : values.get(0);\n    }\n\n    public String getContentType() {\n        return getHeader(HttpConstants.HDR_CONTENT_TYPE);\n    }\n\n    public void setContentType(String contentType) {\n        setHeader(HttpConstants.HDR_CONTENT_TYPE, contentType);\n    }\n\n    public void setHeader(String name, List<String> values) {\n        if (headers == null) {\n            headers = new HashMap();\n        }\n        headers.put(name, values);\n    }\n\n    public void setHeader(String name, String... values) {\n        setHeader(name, Arrays.asList(values));\n    }\n\n    public void setHeaders(Map<String, Object> map) {\n        if (map == null) {\n            return;\n        }\n        map.forEach((k, v) -> {\n            if (v instanceof List) {\n                setHeader(k, (List) v);\n            } else if (v != null) {\n                setHeader(k, v.toString());\n            }\n        });\n    }\n\n    private static String toString(Object o) {\n        return o == null ? null : o.toString();\n    }\n\n    private final Methods.FunVar HEADER_FUNCTION = args -> {\n        if (args.length == 1) {\n            return getHeader(toString(args[0]));\n        } else {\n            setHeader(toString(args[0]), toString(args[1]));\n            return Response.this;\n        }\n    };\n\n    @Override\n    public Object getMember(String key) {\n        switch (key) {\n            case STATUS:\n                return status;\n            case HEADER:\n                return HEADER_FUNCTION;\n            case HEADERS:\n                return JsValue.fromJava(JsonUtils.simplify(headers));\n            case BODY:\n                if (body instanceof byte[]) {\n                    return JsValue.fromJava(getBodyConverted());\n                } else {\n                    return JsValue.fromJava(body);\n                }\n            case DATA_TYPE:\n                ResourceType rt = getResourceType();\n                if (rt == null || rt == ResourceType.BINARY) {\n                    return null;\n                }\n                return rt.name().toLowerCase();\n            case HEADER_VALUES:\n                return (Function<String, List<String>>) this::getHeaderValues;\n            case BODY_BYTES:\n                return getBody();\n            case RESPONSE_TIME:\n                return responseTime;\n            default:\n                logger.warn(\"no such property on response object: {}\", key);\n                return null;\n        }\n    }\n\n    public Map<String, Object> toMap() {\n        Map<String, Object> map = new HashMap();\n        map.put(STATUS, status);\n        map.put(HEADERS, JsonUtils.simplify(headers));\n        map.put(BODY, getBodyConverted());\n        map.put(RESPONSE_TIME, responseTime);\n        return map;\n    }\n\n    @Override\n    public Object getMemberKeys() {\n        return KEY_ARRAY;\n    }\n\n    @Override\n    public boolean hasMember(String key) {\n        return KEY_SET.contains(key);\n    }\n\n    @Override\n    public void putMember(String key, Value value) {\n        switch (key) {\n            case BODY:\n                body = JsValue.toJava(value);\n                break;\n            case STATUS:\n                status = value.asInt();\n                break;\n            case HEADERS:\n                setHeaders((Map) JsValue.toJava(value));\n                break;\n            default:\n                logger.warn(\"put not supported on response object: {} - {}\", key, value);\n        }\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"[status: \").append(status);\n        sb.append(\", responseTime: \").append(responseTime);\n        if (resourceType != null && resourceType != ResourceType.BINARY) {\n            sb.append(\", type: \").append(resourceType);\n        }\n        if (body instanceof byte[]) {\n            sb.append(\", length: \").append(((byte[]) body).length);\n        }\n        if (headers != null) {\n            sb.append(\", headers: \").append(headers);\n        }\n        sb.append(']');\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ResponseBuilder.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.resource.ResourceResolver;\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.StringUtils;\nimport io.netty.handler.codec.http.cookie.Cookie;\nimport io.netty.handler.codec.http.cookie.DefaultCookie;\nimport io.netty.handler.codec.http.cookie.ServerCookieEncoder;\nimport java.io.InputStream;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ResponseBuilder {\n    \n    private static final Logger logger = LoggerFactory.getLogger(ResponseBuilder.class);\n    \n    private byte[] body;\n    private Set<Cookie> cookies;\n    private Map<String, List<String>> headers;\n    private ResourceType resourceType;\n    private final ServerConfig config;\n    private final ResourceResolver resourceResolver;\n    private final RequestCycle requestCycle;\n    \n    public ResponseBuilder(ServerConfig config, RequestCycle requestCycle) {\n        this.config = config;\n        resourceResolver = config.getResourceResolver();\n        this.requestCycle = requestCycle;\n        if (requestCycle != null) {\n            headers = requestCycle.getResponse().getHeaders();\n        }\n    }\n    \n    public ResponseBuilder body(String body) {\n        this.body = FileUtils.toBytes(body);\n        return this;\n    }\n    \n    public ResponseBuilder html(String body) {\n        body(body);\n        contentTypeHtml();\n        return this;\n    }\n    \n    public ResponseBuilder body(InputStream body) {\n        this.body = FileUtils.toBytes(body);\n        return this;\n    }\n    \n    public ResponseBuilder locationHeader(String url) {\n        return header(HttpConstants.HDR_LOCATION, url);\n    }\n    \n    public ResponseBuilder contentTypeHtml() {\n        resourceType = ResourceType.HTML;\n        contentType(resourceType.contentType);\n        return this;\n    }\n    \n    public ResponseBuilder contentType(String contentType) {\n        if (contentType != null) {\n            header(HttpConstants.HDR_CONTENT_TYPE, contentType);\n        }\n        return this;\n    }\n    \n    public ResponseBuilder cookie(String name, String value) {\n        return cookie(name, value, false);\n    }\n    \n    public ResponseBuilder sessionCookie(String value) {\n        return cookie(config.getSessionCookieName(), value);\n    }\n    \n    public ResponseBuilder deleteSessionCookie(String value) {\n        return cookie(config.getSessionCookieName(), value, true);\n    }\n    \n    private ResponseBuilder cookie(String name, String value, boolean delete) {\n        DefaultCookie cookie = new DefaultCookie(name, value);\n        cookie.setHttpOnly(true);\n        cookie.setSecure(true);\n        if (delete) {\n            cookie.setMaxAge(0);\n        }\n        if (cookies == null) {\n            cookies = new HashSet();\n        }\n        cookies.add(cookie);\n        return this;\n    }\n    \n    public ResponseBuilder header(String name, String value) {\n        if (headers == null) {\n            headers = new LinkedHashMap();\n        }\n        headers.put(name, Collections.singletonList(value));\n        return this;\n    }\n    \n    public ResponseBuilder ajaxRedirect(String url) {\n        header(HttpConstants.HDR_HX_REDIRECT, url);\n        return this;\n    }\n    \n    public ResponseBuilder session(Session session, boolean newSession) {\n        if (session != null && newSession) {\n            sessionCookie(session.getId());\n        }\n        return this;\n    }\n    \n    public Response build() {\n        Response response = requestCycle.getResponse();\n        ServerContext context = requestCycle.getContext();\n        if (context.isClosed()) {\n            Session session = requestCycle.getSession();\n            if (session != null && !session.isTemporary()) {\n                deleteSessionCookie(session.getId());\n            }\n        }\n        if (cookies != null) {\n            cookies.forEach(c -> header(HttpConstants.HDR_SET_COOKIE, ServerCookieEncoder.LAX.encode(c)));\n        }\n        if (resourceType != null && resourceType.isHtml()) {\n            if (context.getBodyAppends() != null) {\n                String appends = StringUtils.join(context.getBodyAppends(), \"\\n\");\n                body = merge(body, FileUtils.toBytes(appends));\n            }\n        }\n        if (context.getRedirectPath() != null) {\n            locationHeader(context.getRedirectPath());\n            response.setStatus(302);\n        }        \n        if (context.isApi()) {\n            body = response.getBody();\n            if (resourceType != null) {\n                contentType(resourceType.contentType);\n            } else if (body != null) {\n                contentType(ResourceType.JSON.contentType);  // default, which can be over-ridden\n            }\n            Map<String, List<String>> apiHeaders = response.getHeaders();\n            if (apiHeaders != null) {\n                if (headers == null) {\n                    headers = apiHeaders;\n                } else {\n                    headers.putAll(apiHeaders);\n                }\n            }\n        }\n        return buildWithStatus(response.getStatus());\n    }\n    \n    private static byte[] merge(byte[] body, byte[] extra) {\n        if (body == null) {\n            body = new byte[0];\n        }\n        byte[] merged = new byte[body.length + extra.length];\n        System.arraycopy(body, 0, merged, 0, body.length);\n        System.arraycopy(extra, 0, merged, body.length, extra.length);\n        return merged;\n    }\n    \n    public Response buildStatic(Request request) { // TODO ETag header handling\n        resourceType = request.getResourceType();\n        if (resourceType == null) {\n            resourceType = ResourceType.BINARY;\n        }\n        contentType(resourceType.contentType);\n        try {\n            InputStream is = resourceResolver.resolve(request.getResourcePath()).getStream();\n            body(is);\n            if (config.isNoCache()) {\n                header(HttpConstants.HDR_CACHE_CONTROL, \"max-age=0\");\n            } else {\n                header(HttpConstants.HDR_CACHE_CONTROL, \"max-age=31536000\");\n            }\n        } catch (Exception e) {\n            logger.error(\"local resource failed: {} - {}\", request, e.toString());\n        }\n        return buildWithStatus(200);\n    }\n    \n    public Response buildWithStatus(int status) {\n        return new Response(status, headers, status == 204 ? null : body, resourceType);\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ResponseFilter.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport java.util.function.UnaryOperator;\n\n/**\n *\n * @author pthomas3\n */\npublic interface ResponseFilter extends UnaryOperator<ProxyResponse> {\n\n    default ProxyResponse apply(ProxyContext context, FullHttpRequest request, FullHttpResponse response) {\n        return apply(new ProxyResponse(context, request, response));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ServerConfig.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.core.Config;\nimport com.intuit.karate.resource.ResourceResolver;\nimport com.linecorp.armeria.common.RequestContext;\nimport java.util.HashMap;\nimport java.util.function.Function;\n\n/**\n *\n * @author pthomas3\n */\npublic class ServerConfig {\n\n    private final ResourceResolver resourceResolver;\n\n    private String hostContextPath = null;\n    private String homePagePath = \"/index\";\n    private String signinPagePath = \"/signin\";\n    private String signoutPagePath = \"/signout\";\n    private String sessionCookieName = \"karate.sid\";\n    private boolean stripContextPathFromRequest;\n    private boolean useGlobalSession;\n    private boolean autoCreateSession;\n    private boolean noCache;\n    private boolean devMode;\n    private SessionStore sessionStore = JvmSessionStore.INSTANCE;\n    private int sessionExpirySeconds = 60 * 10;\n\n    public static final Session GLOBAL_SESSION = new Session(\"-1\", new HashMap(), -1, -1, -1);\n\n    private Function<Request, ServerContext> contextFactory = request -> {\n        ServerContext context = new ServerContext(this, request);\n        if (context.setApiIfPathStartsWith(\"/api/\")) {\n            context.setLockNeeded(true);\n        } else {\n            context.setHttpGetAllowed(true);\n        }\n        return context;\n    };\n\n    private Config httpClientConfig = new Config(); // TODO decouple http config\n    private Logger logger = new Logger();\n\n    private Function<Request, HttpClient> httpClientFactory = request -> {\n        RequestContext context = request == null ? null : request.getRequestContext();\n        ArmeriaHttpClient client = new ArmeriaHttpClient(httpClientConfig, logger);\n        client.setRequestContext(context);\n        return client;\n    };\n\n    public ServerConfig(ResourceResolver resourceResolver) {\n        this.resourceResolver = resourceResolver;\n    }\n\n    public ServerConfig(String root) {\n        this(new ResourceResolver(root));\n    }\n\n    public ResourceResolver getResourceResolver() {\n        return resourceResolver;\n    }\n\n    public String getHostContextPath() {\n        return hostContextPath;\n    }\n\n    public String getHomePagePath() {\n        return homePagePath;\n    }\n\n    public String getSigninPagePath() {\n        return signinPagePath;\n    }\n\n    public String getSignoutPagePath() {\n        return signoutPagePath;\n    }\n\n    public String getSessionCookieName() {\n        return sessionCookieName;\n    }\n\n    public boolean isStripContextPathFromRequest() {\n        return stripContextPathFromRequest;\n    }\n\n    public boolean isUseGlobalSession() {\n        return useGlobalSession;\n    }\n\n    public boolean isAutoCreateSession() {\n        return autoCreateSession;\n    }\n\n    public boolean isNoCache() {\n        return noCache;\n    }\n\n    public boolean isDevMode() {\n        return devMode;\n    }\n\n    public int getSessionExpirySeconds() {\n        return sessionExpirySeconds;\n    }\n\n    public SessionStore getSessionStore() {\n        return sessionStore;\n    }\n\n    public Function<Request, ServerContext> getContextFactory() {\n        return contextFactory;\n    }\n\n    public Function<Request, HttpClient> getHttpClientFactory() {\n        return httpClientFactory;\n    }\n\n    public ServerConfig hostContextPath(String value) {\n        if (value.charAt(0) != '/') {\n            value = \"/\" + value;\n        }\n        if (!value.endsWith(\"/\")) {\n            value = value + \"/\";\n        }\n        hostContextPath = value;\n        return this;\n    }\n\n    public ServerConfig homePagePath(String value) {\n        homePagePath = value;\n        return this;\n    }\n\n    public ServerConfig signinPagePath(String value) {\n        signinPagePath = value;\n        return this;\n    }\n\n    public ServerConfig signoutPagePath(String value) {\n        signoutPagePath = value;\n        return this;\n    }\n\n    public ServerConfig sessionCookieName(String value) {\n        sessionCookieName = value;\n        return this;\n    }\n\n    public ServerConfig stripContextPathFromRequest(boolean value) {\n        stripContextPathFromRequest = value;\n        return this;\n    }\n\n    public ServerConfig useGlobalSession(boolean value) {\n        useGlobalSession = value;\n        return this;\n    }\n\n    public ServerConfig autoCreateSession(boolean value) {\n        autoCreateSession = value;\n        return this;\n    }\n\n    public ServerConfig noCache(boolean value) {\n        noCache = value;\n        return this;\n    }\n\n    public ServerConfig devMode(boolean value) {\n        devMode = value;\n        return this;\n    }\n\n    public ServerConfig sessionStore(SessionStore value) {\n        sessionStore = value;\n        return this;\n    }\n\n    public ServerConfig sessionExpirySeconds(int value) {\n        sessionExpirySeconds = value;\n        return this;\n    }\n\n    public ServerConfig contextFactory(Function<Request, ServerContext> value) {\n        contextFactory = value;\n        return this;\n    }\n\n    public ServerConfig httpClientFactory(Function<Request, HttpClient> value) {\n        httpClientFactory = value;\n        return this;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ServerContext.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.Match;\nimport com.intuit.karate.core.Variable;\nimport com.intuit.karate.graal.JsArray;\nimport com.intuit.karate.graal.JsEngine;\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.graal.Methods;\nimport com.intuit.karate.resource.Resource;\nimport com.intuit.karate.template.KarateEngineContext;\nimport com.intuit.karate.template.TemplateUtils;\nimport io.netty.handler.codec.http.cookie.Cookie;\nimport io.netty.handler.codec.http.cookie.ServerCookieDecoder;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.BiFunction;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport org.graalvm.polyglot.Value;\nimport org.graalvm.polyglot.proxy.ProxyObject;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ServerContext implements ProxyObject {\n\n    private static final Logger logger = LoggerFactory.getLogger(ServerContext.class);\n\n    private static final String READ = \"read\";\n    private static final String RESOLVER = \"resolver\";\n    private static final String READ_AS_STRING = \"readAsString\";\n    private static final String EVAL = \"eval\";\n    private static final String EVAL_WITH = \"evalWith\";\n    private static final String GET = \"get\";\n    private static final String SET = \"set\";\n    private static final String LOG = \"log\";\n    private static final String UUID = \"uuid\";\n    private static final String REMOVE = \"remove\";\n    private static final String REDIRECT = \"redirect\";\n    private static final String SWITCH = \"switch\";\n    private static final String SWITCHED = \"switched\";\n    private static final String AJAX = \"ajax\";\n    private static final String HTTP = \"http\";\n    private static final String NEXT_ID = \"nextId\";\n    private static final String SESSION_ID = \"sessionId\";\n    private static final String INIT = \"init\";\n    private static final String CLOSE = \"close\";\n    private static final String CLOSED = \"closed\";\n    private static final String RENDER = \"render\";\n    private static final String BODY_APPEND = \"bodyAppend\";\n    private static final String COPY = \"copy\";\n    private static final String DELAY = \"delay\";\n    private static final String TO_STRING = \"toString\";\n    private static final String TO_JSON = \"toJson\";\n    private static final String TO_JS = \"toJs\";\n    private static final String TO_JSON_PRETTY = \"toJsonPretty\";\n    private static final String FROM_JSON = \"fromJson\";\n    private static final String CALLER = \"caller\";\n    private static final String TEMPLATE = \"template\";\n    private static final String TYPE_OF = \"typeOf\";\n    private static final String IS_PRIMITIVE = \"isPrimitive\";\n    private static final String IS_JSON = \"isJson\";\n    private static final String MATCH = \"match\";\n    private static final String JOIN_PATHS = \"joinPaths\";\n    private static final String FLASH = \"flash\";\n\n    private static final String[] KEYS = new String[]{\n        READ, RESOLVER, READ_AS_STRING, EVAL, EVAL_WITH, GET, SET, LOG, UUID, REMOVE, REDIRECT, SWITCH, SWITCHED, AJAX, HTTP, NEXT_ID, SESSION_ID,\n        INIT, CLOSE, CLOSED, RENDER, BODY_APPEND, COPY, DELAY, TO_STRING, TO_JSON, TO_JS, TO_JSON_PRETTY, FROM_JSON,\n        CALLER, TEMPLATE, TYPE_OF, IS_PRIMITIVE, IS_JSON, MATCH, JOIN_PATHS, FLASH};\n    private static final Set<String> KEY_SET = new HashSet(Arrays.asList(KEYS));\n    private static final JsArray KEY_ARRAY = new JsArray(KEYS);\n\n    private final ServerConfig config;\n    private final Request request;\n\n    private boolean stateless;\n    private boolean api;\n    private boolean httpGetAllowed;\n    private boolean lockNeeded;\n    private boolean newSession;\n    private Session session; // can be pre-resolved, else will be set by RequestCycle.init()\n    private boolean switched;\n    private boolean closed;\n    private int nextId;\n\n    private final Map<String, Object> variables;\n    private Object flash;\n    private String redirectPath;\n    private List<String> bodyAppends;\n    private Function<ServerContext, Boolean> requestValidator;\n\n    public ServerContext(ServerConfig config, Request request) {\n        this(config, request, null);\n    }\n\n    public ServerContext(ServerConfig config, Request request, Map<String, Object> variables) {\n        this.config = config;\n        this.request = request;\n        this.variables = variables;\n        HTTP_FUNCTION = args -> {\n            HttpClient client = config.getHttpClientFactory().apply(request);\n            HttpRequestBuilder http = new HttpRequestBuilder(client);\n            if (args.length > 0) {\n                http.url((String) args[0]);\n            }\n            return http;\n        };\n        RENDER_FUNCTION = o -> {\n            if (o instanceof String) {\n                return TemplateUtils.renderHtmlResource((String) o, getEngine(), config.getResourceResolver(), config.isDevMode());\n            }\n            Map<String, Object> map;\n            if (o instanceof Map) {\n                map = (Map) o;\n            } else {\n                logger.warn(\"invalid argument to render: {}\", o);\n                return null;\n            }\n            Map<String, Object> vars = (Map) map.get(\"vars\");\n            String path = (String) map.get(\"path\");\n            String html = (String) map.get(\"html\");\n            Boolean fork = (Boolean) map.get(\"fork\");\n            Boolean append = (Boolean) map.get(\"append\");\n            if (path == null && html == null) {\n                logger.warn(\"invalid argument to render, 'path' or 'html' needed: {}\", map);\n                return null;\n            }\n            JsEngine je;\n            if (fork != null && fork) {\n                je = JsEngine.local();\n            } else {\n                je = getEngine().copy();\n            }\n            if (vars != null) {\n                je.putAll(vars);\n            }\n            String body;\n            if (path != null) {\n                body = TemplateUtils.renderHtmlResource(path, je, config.getResourceResolver(), config.isDevMode());\n            } else {\n                body = TemplateUtils.renderHtmlString(html, je, config.getResourceResolver());\n            }\n            if (append != null && append) {\n                bodyAppend(body);\n            }\n            return body;\n        };\n    }\n\n    public boolean setApiIfPathStartsWith(String prefix) {\n        String path = request.getPath();\n        if (path.startsWith(prefix)) {\n            api = true;\n            int length = prefix.length();\n            int pos = path.indexOf('/', length);\n            if (pos != -1) {\n                request.setResourcePath(path.substring(0, pos) + \".js\");\n            } else {\n                request.setResourcePath(path + \".js\");\n            }\n            request.setPath(path.substring(length - 1));\n            return true;\n        }\n        return false;\n    }\n\n    public String getSessionCookieValue() {\n        List<String> values = request.getHeaderValues(HttpConstants.HDR_COOKIE);\n        if (values == null) {\n            return null;\n        }\n        for (String value : values) {\n            Set<Cookie> cookies = ServerCookieDecoder.STRICT.decode(value);\n            for (Cookie c : cookies) {\n                if (config.getSessionCookieName().equals(c.name())) {\n                    return c.value();\n                }\n            }\n        }\n        return null;\n    }\n\n    public String readAsString(String resource) {\n        if (resource.startsWith(Resource.THIS_COLON)) {\n            resource = resource.substring(Resource.THIS_COLON.length());\n            if (resource.charAt(0) != '/') {\n                resource = \"/\" + resource;\n            }\n            String path = request.getResourcePath();\n            int pos = path.lastIndexOf('/');\n            if (pos != -1) {\n                resource = path.substring(0, pos) + resource;\n            }\n        }\n        InputStream is = config.getResourceResolver().resolve(resource).getStream();\n        return FileUtils.toString(is);\n    }\n\n    public Object read(String resource) {\n        String raw = readAsString(resource);\n        ResourceType resourceType = ResourceType.fromFileExtension(resource);\n        if (resourceType == ResourceType.JS) {\n            return eval(raw);\n        } else {\n            return JsValue.fromJava(JsonUtils.fromString(raw, false, resourceType));\n        }\n    }\n\n    private JsEngine getEngine() {\n        KarateEngineContext kec = KarateEngineContext.get();\n        return kec == null ? RequestCycle.get().getEngine() : kec.getJsEngine();\n    }\n\n    public Object eval(String source) {\n        return getEngine().evalForValue(source);\n    }\n\n    public Object evalWith(Object o, String source) {\n        Value value = Value.asValue(o);\n        return getEngine().evalWith(value, source, true);\n    }\n\n    public String toJson(Object o) {\n        Value value = Value.asValue(o);\n        return new JsValue(value).toJsonOrXmlString(false);\n    }\n\n    public Object toJs(Object o) {\n        return JsValue.fromJava(o);\n    }\n\n    public String toJsonPretty(Object o) {\n        Value value = Value.asValue(o);\n        String pretty = new JsValue(value).toJsonOrXmlString(true);\n        return pretty == null ? null : pretty.trim();\n    }\n\n    public ServerConfig getConfig() {\n        return config;\n    }\n\n    public Request getRequest() {\n        return request;\n    }\n\n    public Map<String, Object> getVariables() {\n        return variables;\n    }\n\n    public boolean isNewSession() {\n        return newSession;\n    }\n\n    public void init() {\n        long now = Instant.now().getEpochSecond();\n        long expires = now + config.getSessionExpirySeconds();\n        session = config.getSessionStore().create(now, expires);\n        newSession = true;\n    }\n\n    public Session getSession() {\n        return session;\n    }\n\n    public void setSession(Session session) {\n        this.session = session;\n    }\n\n    public boolean isLockNeeded() {\n        return lockNeeded;\n    }\n\n    public void setLockNeeded(boolean lockNeeded) {\n        this.lockNeeded = lockNeeded;\n    }\n\n    public boolean isStateless() {\n        return stateless;\n    }\n\n    public void setStateless(boolean stateless) {\n        this.stateless = stateless;\n    }\n\n    public boolean isAjax() {\n        return request.isAjax();\n    }\n\n    public boolean isApi() {\n        return api;\n    }\n\n    public void setApi(boolean api) {\n        this.api = api;\n    }\n\n    public boolean isClosed() {\n        return closed;\n    }\n\n    public boolean isHttpGetAllowed() {\n        return httpGetAllowed;\n    }\n\n    public void setHttpGetAllowed(boolean httpGetAllowed) {\n        this.httpGetAllowed = httpGetAllowed;\n    }\n\n    public void setRequestValidator(Function<ServerContext, Boolean> requestValidator) {\n        this.requestValidator = requestValidator;\n    }\n\n    public Function<ServerContext, Boolean> getRequestValidator() {\n        return requestValidator;\n    }    \n\n    public boolean isSwitched() {\n        return switched;\n    }\n\n    public String getRedirectPath() {\n        return redirectPath;\n    }\n\n    public List<String> getBodyAppends() {\n        return bodyAppends;\n    }\n\n    public void bodyAppend(String body) {\n        if (bodyAppends == null) {\n            bodyAppends = new ArrayList();\n        }\n        bodyAppends.add(body);\n    }\n\n    public void log(Object... args) {\n        String log = new LogWrapper(args).toString();\n        logger.info(log);\n    }\n\n    private final Methods.FunVar GET_FUNCTION = args -> {\n        if (args.length == 0 || args[0] == null) {\n            return null;\n        }\n        String name = args[0].toString();\n        KarateEngineContext kec = KarateEngineContext.get();\n        Object value;\n        if (kec != null && kec.containsVariable(name)) {\n            value = kec.getVariable(name);\n        } else {\n            JsEngine je = getEngine();\n            if (je.bindings.hasMember(name)) {\n                value = je.get(name).getValue();\n            } else if (args.length > 1) {\n                value = args[1];\n            } else {\n                value = null;\n            }\n        }\n        return value;\n    };\n\n    private Void setVariable(String name, Object value) {\n        getEngine().put(name, value);\n        return null;\n    }\n\n    private static final Supplier<String> UUID_FUNCTION = () -> java.util.UUID.randomUUID().toString();\n    private static final Function<String, Object> FROM_JSON_FUNCTION = s -> JsonUtils.fromString(s, false, null);\n\n    private final Methods.FunVar HTTP_FUNCTION; // set in constructor\n    private final Function<Object, String> RENDER_FUNCTION; // set in constructor  \n\n    private final Methods.FunVar LOG_FUNCTION = args -> {\n        log(args);\n        return null;\n    };\n\n    private final Function<Object, Object> COPY_FUNCTION = o -> {\n        return JsValue.fromJava(JsonUtils.deepCopy(o));\n    };\n\n    private final Consumer<Number> DELAY_FUNCTION = v -> {\n        try {\n            Thread.sleep(v.longValue());\n        } catch (Exception e) {\n            logger.error(\"delay failed: {}\", e.getMessage());\n        }\n    };\n\n    private final Function<Object, Object> TO_STRING_FUNCTION = o -> {\n        Variable v = new Variable(o);\n        return v.getAsString();\n    };\n\n    private final Methods.FunVar SWITCH_FUNCTION = args -> {\n        if (switched) {\n            logger.warn(\"context.switch() can be called only once during a request, ignoring: {}\", args[0]);\n        } else {\n            switched = true; // flag for request cycle render\n            KarateEngineContext.get().setRedirect(true); // flag for template engine\n            RequestCycle rc = RequestCycle.get();\n            if (args.length > 1) {\n                Value value = Value.asValue(args[1]);\n                if (value.hasMembers()) {\n                    JsValue jv = new JsValue(value);\n                    rc.setSwitchParams(jv.getAsMap());\n                }\n            }\n            String template;\n            if (args.length > 0) {\n                template = args[0].toString();\n                rc.setSwitchTemplate(template);\n            } else {\n                template = null;\n            }\n            throw new RedirectException(template);\n        }\n        return null;\n    };\n\n    private final Supplier<String> CLOSE_FUNCTION = () -> {\n        closed = true;\n        return null;\n    };\n\n    private final Supplier<Object> INIT_FUNCTION = () -> {\n        init();\n        getEngine().put(RequestCycle.SESSION, session.getData());\n        logger.debug(\"init session: {}\", session);\n        return null;\n    };\n\n    private final Function<String, Object> REDIRECT_FUNCTION = (path) -> {\n        redirectPath = path;\n        logger.debug(\"redirect requested to: {}\", redirectPath);\n        return null;\n    };\n\n    private static final BiFunction<Object, Object, Object> REMOVE_FUNCTION = (o, k) -> {\n        if (o instanceof Map && k != null) {\n            Map in = (Map) o;\n            Map out = new HashMap(in);\n            Object removed = out.remove(k.toString());\n            if (removed == null) {\n                logger.warn(\"nothing removed, key not present: {}\", k);\n                return o;\n            } else {\n                return JsValue.fromJava(out);\n            }\n        } else if (o != null) {\n            logger.warn(\"unable to cast to map: {} - {}\", o.getClass(), o);\n        }\n        return o;\n    };\n\n    private final Supplier<String> NEXT_ID_FUNCTION = () -> ++nextId + \"-\" + System.currentTimeMillis();\n\n    private final Function<String, Object> TYPE_OF_FUNCTION = o -> new Variable(o).getTypeString();\n\n    private final Function<Object, Object> IS_PRIMITIVE_FUNCTION = o -> !new Variable(o).isMapOrList();\n\n    private final Function<Object, Object> IS_JSON_FUNCTION = o -> new Variable(o).isMapOrList();\n\n    private final Methods.FunVar MATCH_FUNCTION = args -> {\n        if (args.length > 2 && args[0] != null) {\n            String type = args[0].toString();\n            Match.Type matchType = Match.Type.valueOf(type.toUpperCase());\n            return JsValue.fromJava(Match.execute(getEngine(), matchType, args[1], args[2], false));\n        } else if (args.length == 2) {\n            return JsValue.fromJava(Match.execute(getEngine(), Match.Type.EQUALS, args[0], args[1], false));\n        } else {\n            logger.warn(\"at least two arguments needed for match\");\n            return null;\n        }\n    };\n\n    private final Methods.FunVar JOIN_PATHS_FUNCTION = args -> {\n        List<String> temp = Arrays.asList(args).stream().filter(x -> x != null).map(Object::toString).collect(Collectors.toList());\n        return String.join(File.separator, temp);\n    };\n\n    @Override\n    public Object getMember(String key) {\n        switch (key) {\n            case READ:\n                return (Function<String, Object>) this::read;\n            case READ_AS_STRING:\n                return (Function<String, String>) this::readAsString;\n            case EVAL:\n                return (Function<String, Object>) this::eval;\n            case EVAL_WITH:\n                return (BiFunction<Object, String, Object>) this::evalWith;\n            case GET:\n                return GET_FUNCTION;\n            case SET:\n                return (BiFunction<String, Object, Void>) this::setVariable;\n            case LOG:\n                return LOG_FUNCTION;\n            case UUID:\n                return UUID_FUNCTION;\n            case COPY:\n                return COPY_FUNCTION;\n            case DELAY:\n                return DELAY_FUNCTION;\n            case TO_STRING:\n                return TO_STRING_FUNCTION;\n            case TO_JSON:\n                return (Function<Object, String>) this::toJson;\n            case TO_JS:\n                return (Function<Object, Object>) this::toJs;\n            case TO_JSON_PRETTY:\n                return (Function<Object, String>) this::toJsonPretty;\n            case FROM_JSON:\n                return FROM_JSON_FUNCTION;\n            case REMOVE:\n                return REMOVE_FUNCTION;\n            case REDIRECT:\n                return REDIRECT_FUNCTION;\n            case SWITCH:\n                return SWITCH_FUNCTION;\n            case SWITCHED:\n                return switched;\n            case AJAX:\n                return isAjax();\n            case HTTP:\n                return HTTP_FUNCTION;\n            case NEXT_ID:\n                return NEXT_ID_FUNCTION;\n            case SESSION_ID:\n                return session == null ? null : session.getId();\n            case INIT:\n                return INIT_FUNCTION;\n            case CLOSE:\n                return CLOSE_FUNCTION;\n            case CLOSED:\n                return closed || session == null || session.isTemporary();\n            case RENDER:\n                return RENDER_FUNCTION;\n            case BODY_APPEND:\n                return (Consumer<String>) this::bodyAppend;\n            case RESOLVER:\n                return config.getResourceResolver();\n            case CALLER:\n                return KarateEngineContext.get().getCallerTemplateName();\n            case TEMPLATE:\n                return KarateEngineContext.get().getTemplateName();\n            case TYPE_OF:\n                return TYPE_OF_FUNCTION;\n            case IS_PRIMITIVE:\n                return IS_PRIMITIVE_FUNCTION;\n            case IS_JSON:\n                return IS_JSON_FUNCTION;\n            case MATCH:\n                return MATCH_FUNCTION;\n            case JOIN_PATHS:\n                return JOIN_PATHS_FUNCTION;\n            case FLASH:\n                return flash;\n            default:\n                logger.warn(\"no such property on context object: {}\", key);\n                return null;\n        }\n    }\n\n    @Override\n    public Object getMemberKeys() {\n        return KEY_ARRAY;\n    }\n\n    @Override\n    public boolean hasMember(String key) {\n        return KEY_SET.contains(key);\n    }\n\n    @Override\n    public void putMember(String key, Value value) {\n        switch (key) {\n            case FLASH:\n                flash = JsValue.toJava(value);\n                break;\n            default:\n                logger.warn(\"put not supported on context object: {} - {}\", key, value);\n        }\n    }\n\n    static class LogWrapper { // TODO code duplication with ScenarioBridge\n\n        final Object[] values;\n\n        LogWrapper(Object... values) {\n            // sometimes a null array gets passed in, graal weirdness\n            this.values = values == null ? new Value[0] : values;\n        }\n\n        @Override\n        public String toString() {\n            StringBuilder sb = new StringBuilder();\n            for (Object v : values) {\n                Variable var = new Variable(v);\n                sb.append(var.getAsPrettyString()).append(' ');\n            }\n            return sb.toString();\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/ServerHandler.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\n/**\n *\n * @author pthomas3\n */\npublic interface ServerHandler {\n    \n    Response handle(Request request);\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/Session.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class Session {\n\n    private final String id;\n    private final Map<String, Object> data;\n    private final long created;\n    private long updated;\n    private long expires;\n\n    public static final Session TEMPORARY = new Session(null, null, -1, -1, -1);\n\n    public Session(String id, Map<String, Object> data, long created, long updated, long expires) {\n        this.id = id;\n        this.data = data;\n        this.created = created;\n        this.updated = updated;\n        this.expires = expires;\n    }\n\n    public boolean isTemporary() {\n        return id == null;\n    }\n\n    public Session copy() { // TODO deep-clone ?\n        return new Session(id, new HashMap(data), created, updated, expires);\n    }\n\n    public void setUpdated(long updated) {\n        this.updated = updated;\n    }\n\n    public void setExpires(long expires) {\n        this.expires = expires;\n    }\n\n    public String getId() {\n        return id;\n    }\n\n    public Map<String, Object> getData() {\n        return data;\n    }\n\n    public long getCreated() {\n        return created;\n    }\n\n    public long getUpdated() {\n        return updated;\n    }\n\n    public long getExpires() {\n        return expires;\n    }\n\n    @Override\n    public String toString() {\n        return id;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/SessionStore.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\n/**\n *\n * @author pthomas3\n */\npublic interface SessionStore {\n\n    Session create(long now, long expires);\n\n    Session get(String id);\n    \n    void save(Session session);\n    \n    void delete(String id);\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/SslContextFactory.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.FileUtils;\nimport java.io.File;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class SslContextFactory {\n\n    private static final Logger logger = LoggerFactory.getLogger(SslContextFactory.class);\n\n    public static final String DEFAULT_CERT_NAME = \"cert.pem\";\n    public static final String DEFAULT_KEY_NAME = \"key.pem\";\n\n    private String buildDir;\n    private File certFile;\n    private File keyFile;\n\n    public void setBuildDir(String buildDir) {\n        this.buildDir = buildDir;\n    }\n\n    public void setCertFile(File certFile) {\n        this.certFile = certFile;\n    }\n\n    public void setKeyFile(File keyFile) {\n        this.keyFile = keyFile;\n    }\n\n    public File getCertFile() {\n        return certFile;\n    }\n\n    public File getKeyFile() {\n        return keyFile;\n    }\n\n    public void build() {\n        if (buildDir == null) {\n            buildDir = FileUtils.getBuildDir();\n        }\n        try {\n            if (certFile == null || keyFile == null) {\n                // attempt to re-use as far as possible\n                certFile = new File(buildDir + File.separator + DEFAULT_CERT_NAME);\n                keyFile = new File(buildDir + File.separator + DEFAULT_KEY_NAME);\n            }\n            if (!certFile.exists() || !keyFile.exists()) {\n                logger.warn(\"ssl - \" + certFile + \" and / or \" + keyFile + \" not found, will create\");\n                HttpUtils.createSelfSignedCertificate(certFile, keyFile);\n            } else {\n                logger.info(\"ssl - re-using existing files: {} and {}\", certFile, keyFile);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/WebSocketClient.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.core.ScenarioEngine;\nimport io.netty.bootstrap.Bootstrap;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.EventLoopGroup;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.nio.NioSocketChannel;\nimport io.netty.handler.codec.http.DefaultHttpHeaders;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.HttpClientCodec;\nimport io.netty.handler.codec.http.HttpHeaders;\nimport io.netty.handler.codec.http.HttpObjectAggregator;\nimport io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.PingWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.TextWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;\nimport io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;\nimport io.netty.handler.codec.http.websocketx.WebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;\nimport io.netty.handler.codec.http.websocketx.WebSocketVersion;\nimport io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;\nimport io.netty.handler.ssl.SslContext;\nimport io.netty.handler.ssl.SslContextBuilder;\nimport io.netty.handler.ssl.util.InsecureTrustManagerFactory;\nimport java.net.URI;\nimport java.util.Map;\nimport java.util.function.Function;\nimport javax.net.ssl.SSLException;\n\n/**\n *\n * @author pthomas3\n */\npublic class WebSocketClient implements WebSocketListener {\n\n    // mutable\n    private Logger logger;\n\n    private final Channel channel;\n    private final EventLoopGroup group;\n\n    private final URI uri;\n    private final int port;\n    private final SslContext sslContext;\n    private final WebSocketClientHandshaker handShaker;\n    private final WebSocketClientHandler handler;\n\n    private Function<String, Boolean> textHandler;\n    private Function<byte[], Boolean> binaryHandler;\n    \n    private ScenarioEngine engine;\n\n    public void setEngine(ScenarioEngine engine) {\n        this.engine = engine;\n    }    \n\n    @Override\n    public void onMessage(String text) {\n        if (textHandler != null) {\n            if (textHandler.apply(text)) {\n                if (engine != null) {\n                    engine.signal(text);\n                }\n            }\n        }\n    }\n\n    @Override\n    public void onMessage(byte[] bytes) {\n        if (binaryHandler != null) {\n            if (binaryHandler.apply(bytes)) {\n                if (engine != null) {\n                    engine.signal(bytes);\n                }\n            }\n        }\n    }\n\n    public void setLogger(Logger logger) {\n        this.logger = logger;\n    }\n    \n    public WebSocketClient(WebSocketOptions options, Logger logger) {\n        this.logger = logger;\n        textHandler = options.getTextHandler();\n        binaryHandler = options.getBinaryHandler();\n        uri = options.getUri();\n        port = options.getPort();\n        group = new NioEventLoopGroup();\n        if (options.isSsl()) {\n            try {\n                sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();\n            } catch (SSLException e) {\n                throw new RuntimeException(e);\n            }\n        } else {\n            sslContext = null;\n        }\n        HttpHeaders nettyHeaders = new DefaultHttpHeaders();\n        Map<String, Object> headers = options.getHeaders();\n        if (headers != null) {\n            headers.forEach((k, v) -> nettyHeaders.add(k, v));\n        }\n        handShaker = WebSocketClientHandshakerFactory.newHandshaker(\n                uri, WebSocketVersion.V13, options.getSubProtocol(), true, nettyHeaders, options.getMaxPayloadSize());\n        handler = new WebSocketClientHandler(handShaker, this);\n        try {\n            Bootstrap b = new Bootstrap();\n            b.group(group)\n                    .channel(NioSocketChannel.class)\n                    .handler(new ChannelInitializer() {\n                        @Override\n                        protected void initChannel(Channel c) {\n                            ChannelPipeline p = c.pipeline();\n                            if (sslContext != null) {\n                                p.addLast(sslContext.newHandler(c.alloc(), uri.getHost(), port));\n                            }\n                            p.addLast(new HttpClientCodec());\n                            p.addLast(new HttpObjectAggregator(8192));\n                            p.addLast(WebSocketClientCompressionHandler.INSTANCE);\n                            if (options.getUseFrameAggregation()) {\n                                p.addLast(new WebSocketFrameAggregator(options.getMaxPayloadSize()));\n                            }\n                            p.addLast(handler);\n                        }\n                    });\n            channel = b.connect(options.getUri().getHost(), options.getPort()).sync().channel();\n            handler.handshakeFuture().sync();\n        } catch (Exception e) {\n            logger.error(\"websocket client init failed: {}\", e.getMessage());\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void setBinaryHandler(Function<byte[], Boolean> binaryHandler) {\n        this.binaryHandler = binaryHandler;\n    }\n\n    public void setTextHandler(Function<String, Boolean> textHandler) {\n        this.textHandler = textHandler;\n    }\n\n    private boolean waiting;\n\n    public void waitSync() {\n        if (waiting) {\n            return;\n        }\n        try {\n            waiting = true;\n            channel.closeFuture().sync();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void close() {\n        channel.writeAndFlush(new CloseWebSocketFrame());\n        waitSync();\n        group.shutdownGracefully();\n    }\n\n    public void ping() {\n        WebSocketFrame frame = new PingWebSocketFrame(Unpooled.wrappedBuffer(new byte[]{8, 1, 8, 1}));\n        channel.writeAndFlush(frame);\n    }\n\n    public void send(String msg) {\n        WebSocketFrame frame = new TextWebSocketFrame(msg);\n        channel.writeAndFlush(frame);\n        if (logger.isTraceEnabled()) {\n            logger.trace(\"sent: {}\", msg);\n        }\n    }\n    \n    public void sendHttpRequest(FullHttpRequest request) {        \n        channel.writeAndFlush(request);\n    }\n\n    public void sendBytes(byte[] msg) {\n        ByteBuf byteBuf = Unpooled.copiedBuffer(msg);\n        BinaryWebSocketFrame frame = new BinaryWebSocketFrame(byteBuf);\n        channel.writeAndFlush(frame);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/WebSocketClientHandler.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport io.netty.buffer.ByteBuf;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelPromise;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.handler.codec.http.FullHttpResponse;\nimport io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.PongWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.TextWebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;\nimport io.netty.handler.codec.http.websocketx.WebSocketFrame;\nimport io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;\nimport io.netty.util.CharsetUtil;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {\n\n    private static final Logger logger = LoggerFactory.getLogger(WebSocketClientHandler.class);\n\n    private final WebSocketClientHandshaker handshaker;\n    private final WebSocketListener listener;\n    private ChannelPromise handshakeFuture;\n\n    public WebSocketClientHandler(WebSocketClientHandshaker handshaker, WebSocketListener listener) {\n        this.handshaker = handshaker;\n        this.listener = listener;\n    }\n\n    public ChannelFuture handshakeFuture() {\n        return handshakeFuture;\n    }\n\n    @Override\n    public void handlerAdded(ChannelHandlerContext ctx) {\n        handshakeFuture = ctx.newPromise();\n    }\n\n    @Override\n    public void channelActive(ChannelHandlerContext ctx) {\n        handshaker.handshake(ctx.channel());\n    }\n\n    @Override\n    public void channelInactive(ChannelHandlerContext ctx) {\n        logger.debug(\"websocket client disconnected\");\n    }\n\n    @Override\n    public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {\n        Channel ch = ctx.channel();\n        if (!handshaker.isHandshakeComplete()) {\n            try {\n                handshaker.finishHandshake(ch, (FullHttpResponse) msg);\n                logger.debug(\"websocket client connected\");\n                handshakeFuture.setSuccess();\n            } catch (WebSocketHandshakeException e) {\n                logger.debug(\"websocket client connect failed: {}\", e.getMessage());\n                handshakeFuture.setFailure(e);\n            }\n            return;\n        }\n        if (msg instanceof FullHttpResponse) {\n            FullHttpResponse response = (FullHttpResponse) msg;\n            throw new IllegalStateException(\n                    \"unexpected FullHttpResponse (getStatus=\" + response.status()\n                    + \", content=\" + response.content().toString(CharsetUtil.UTF_8) + ')');\n        }\n        WebSocketFrame frame = (WebSocketFrame) msg;\n        if (frame instanceof TextWebSocketFrame) {\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"websocket received text\");\n            }\n            TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;\n            listener.onMessage(textFrame.text());\n        } else if (frame instanceof PongWebSocketFrame) {\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"websocket received pong\");\n            }\n        } else if (frame instanceof CloseWebSocketFrame) {\n            logger.debug(\"websocket closing\");\n            ch.close();\n        } else if (frame instanceof BinaryWebSocketFrame) {\n            logger.debug(\"websocket received binary\");\n            BinaryWebSocketFrame binaryFrame = (BinaryWebSocketFrame) frame;\n            ByteBuf buf = binaryFrame.content();\n            byte[] bytes = new byte[buf.readableBytes()];\n            buf.readBytes(bytes);\n            listener.onMessage(bytes);\n        }\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        if (cause.getMessage() != null) {\n            logger.error(\"websocket exception: {}\", cause.getMessage());\n        } else {\n            cause.printStackTrace();\n        }\n        if (!handshakeFuture.isDone()) {\n            handshakeFuture.setFailure(cause);\n        }\n        ctx.close();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/WebSocketListener.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\n/**\n *\n * @author pthomas3\n */\npublic interface WebSocketListener {\n    \n    void onMessage(String text);\n    \n    void onMessage(byte[] bytes);\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/WebSocketOptions.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport java.net.URI;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n/**\n *\n * @author pthomas3\n */\npublic class WebSocketOptions {\n\n    private final URI uri;\n    private String subProtocol;\n    private final int port;\n    private final boolean ssl;\n    private Function<String, Boolean> textHandler;\n    private Function<byte[], Boolean> binaryHandler;\n    private Map<String, Object> headers;\n    private int maxPayloadSize = 4194304;\n    private boolean useFrameAggregation = false;\n\n    public WebSocketOptions(String url) {\n        this(url, null);\n    }\n    \n    public WebSocketOptions(String url, Map<String, Object> options) {\n        this.uri = URI.create(url);\n        ssl = \"wss\".equalsIgnoreCase(uri.getScheme());\n        port = uri.getPort() == -1 ? (ssl ? 443 : 80) : uri.getPort();\n        if (options != null) {\n            subProtocol = (String) options.get(\"subProtocol\");\n            Integer temp = (Integer) options.get(\"maxPayloadSize\");\n            if (temp != null) {\n                maxPayloadSize = temp;\n            }\n\n            Boolean tempUseFrameAggregation = (Boolean) options.get(\"useFrameAggregation\");\n            if (tempUseFrameAggregation != null) {\n                useFrameAggregation = tempUseFrameAggregation;\n            }\n\n            headers = (Map) options.get(\"headers\");\n        }\n    }\n\n    public void setTextConsumer(Consumer<String> consumer) {\n        textHandler = t -> {\n            consumer.accept(t);\n            return false; // no async signalling, for normal use, e.g. chrome developer tools\n        };\n    }\n    \n    public URI getUri() {\n        return uri;\n    }\n\n    public int getPort() {\n        return port;\n    }\n\n    public boolean isSsl() {\n        return ssl;\n    }        \n\n    public String getSubProtocol() {\n        return subProtocol;\n    }      \n\n    public void setSubProtocol(String subProtocol) {\n        this.subProtocol = subProtocol;\n    }\n\n    public Function<String, Boolean> getTextHandler() {\n        return textHandler;\n    }        \n\n    public void setTextHandler(Function<String, Boolean> textHandler) {\n        this.textHandler = textHandler;\n    }\n\n    public Function<byte[], Boolean> getBinaryHandler() {\n        return binaryHandler;\n    }\n\n    public void setBinaryHandler(Function<byte[], Boolean> binaryHandler) {\n        this.binaryHandler = binaryHandler;\n    }\n\n    public Map<String, Object> getHeaders() {\n        return headers;\n    }\n\n    public void setHeaders(Map<String, Object> headers) {\n        this.headers = headers;\n    }\n\n    public int getMaxPayloadSize() {\n        return maxPayloadSize;\n    }\n\n    public void setMaxPayloadSize(int maxPayloadSize) {\n        this.maxPayloadSize = maxPayloadSize;\n    }\n\n    public boolean getUseFrameAggregation() { return this.useFrameAggregation; }\n\n    public void setUseFrameAggregation(boolean useFrameAggregation) { this.useFrameAggregation = useFrameAggregation; }\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/WebSocketProxyHandler.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.handler.codec.http.FullHttpRequest;\nimport io.netty.handler.codec.http.websocketx.TextWebSocketFrame;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class WebSocketProxyHandler extends SimpleChannelInboundHandler<Object> {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketProxyHandler.class);\n\n    private final com.intuit.karate.Logger logger = new com.intuit.karate.Logger();\n    private WebSocketClient client;\n    private final WebSocketOptions options;\n\n    public WebSocketProxyHandler(WebSocketOptions options) {\n        this.options = options;\n    }\n\n    private Channel channel;\n\n    private void initClient() {\n        client = new WebSocketClient(options, logger) {\n            @Override\n            public void onMessage(String text) {\n                LOGGER.debug(\"<< {}\", text);\n                channel.eventLoop().submit(() -> {\n                    channel.writeAndFlush(new TextWebSocketFrame(text));\n                });\n            }\n        };\n    }\n\n    @Override\n    public void channelActive(ChannelHandlerContext ctx) throws Exception {\n        channel = ctx.channel();\n        initClient();\n    }\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {\n        if (msg instanceof TextWebSocketFrame) {\n            String request = ((TextWebSocketFrame) msg).text();\n            LOGGER.debug(\">> {}\", request);\n            client.send(request);\n        } else if (msg instanceof FullHttpRequest) {\n            client.ping();\n//            initClient();\n//            FullHttpRequest request = (FullHttpRequest) msg;\n//            request.retain();\n//            client.sendHttpRequest(request);\n        } else {\n            String message = \"unsupported frame type: \" + msg.getClass().getName();\n            LOGGER.warn(message);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/WebSocketProxyServer.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\n/**\n *\n * @author pthomas3\n */\npublic class WebSocketProxyServer extends WebSocketServerBase {\n\n    public WebSocketProxyServer(int port, String url, String path) {\n        super(port, path, handler(url));\n    }\n\n    private static WebSocketProxyHandler handler(String url) {\n        WebSocketOptions options = new WebSocketOptions(url, null);\n        return new WebSocketProxyHandler(options);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/WebSocketServerBase.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.http;\n\nimport io.netty.bootstrap.ServerBootstrap;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.ChannelPipeline;\nimport io.netty.channel.EventLoopGroup;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.nio.NioServerSocketChannel;\nimport io.netty.handler.codec.http.HttpObjectAggregator;\nimport io.netty.handler.codec.http.HttpServerCodec;\nimport io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig;\nimport io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;\nimport io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;\nimport java.net.InetSocketAddress;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class WebSocketServerBase {\n\n    private static final Logger logger = LoggerFactory.getLogger(WebSocketServerBase.class);\n\n    private final Channel channel;\n    private final int port;\n    private final EventLoopGroup bossGroup;\n    private final EventLoopGroup workerGroup;\n\n    public int getPort() {\n        return port;\n    }\n\n    public void waitSync() {\n        try {\n            channel.closeFuture().sync();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void stop() {\n        logger.info(\"stop: shutting down\");\n        bossGroup.shutdownGracefully();\n        workerGroup.shutdownGracefully();\n        logger.info(\"stop: shutdown complete\");\n    }\n\n    public WebSocketServerBase(int port, String path, SimpleChannelInboundHandler handler) {\n        this.port = port;\n        bossGroup = new NioEventLoopGroup(1);\n        workerGroup = new NioEventLoopGroup(8);\n        WebSocketServerProtocolConfig config = WebSocketServerProtocolConfig.newBuilder()\n                .websocketPath(path)\n                .allowExtensions(true)\n                .checkStartsWith(true).build();\n        try {\n            ServerBootstrap b = new ServerBootstrap();\n            b.group(bossGroup, workerGroup)\n                    .channel(NioServerSocketChannel.class)\n                    .childHandler(new ChannelInitializer() {\n                        @Override\n                        protected void initChannel(Channel c) {\n                            ChannelPipeline p = c.pipeline();\n                            p.addLast(new HttpServerCodec());                            \n                            p.addLast(new HttpObjectAggregator(65536));\n                            p.addLast(new WebSocketServerCompressionHandler());                            \n                            p.addLast(new WebSocketServerProtocolHandler(config));                            \n                            p.addLast(handler);\n                        }\n                    });\n            channel = b.bind(port).sync().channel();\n            InetSocketAddress isa = (InetSocketAddress) channel.localAddress();\n            String host = \"127.0.0.1\"; //isa.getHostString();\n            port = isa.getPort();\n            logger.info(\"proxy server started - ws://{}:{}\", host, port);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/cert/SelfSignedCertGenerator.java",
    "content": "package com.intuit.karate.http.cert;\n\nimport com.intuit.karate.Logger;\nimport com.linecorp.armeria.internal.shaded.bouncycastle.asn1.x500.X500Name;\nimport com.linecorp.armeria.internal.shaded.bouncycastle.cert.X509CertificateHolder;\nimport com.linecorp.armeria.internal.shaded.bouncycastle.cert.X509v3CertificateBuilder;\nimport com.linecorp.armeria.internal.shaded.bouncycastle.cert.jcajce.JcaX509CertificateConverter;\nimport com.linecorp.armeria.internal.shaded.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;\nimport com.linecorp.armeria.internal.shaded.bouncycastle.operator.ContentSigner;\nimport com.linecorp.armeria.internal.shaded.bouncycastle.operator.jcajce.JcaContentSignerBuilder;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport io.netty.handler.codec.base64.Base64;\nimport io.netty.util.CharsetUtil;\nimport io.netty.util.internal.PlatformDependent;\nimport io.netty.util.internal.SystemPropertyUtil;\nimport io.netty.util.internal.ThrowableUtil;\n\nimport java.io.*;\nimport java.math.BigInteger;\nimport java.security.*;\nimport java.security.cert.CertificateEncodingException;\nimport java.security.cert.CertificateException;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\nimport java.util.Date;\n\npublic final class SelfSignedCertGenerator {\n    private static final Logger logger = new Logger();\n    private static final Provider PROVIDER = Security.getProvider(\"SUN\");\n    private static final String DEFAULT_FQDN = \"localhost\";\n    private static final Date DEFAULT_NOT_BEFORE = new Date(SystemPropertyUtil.getLong(\"io.netty.selfSignedCertificate.defaultNotBefore\", System.currentTimeMillis() - 31536000000L));\n    private static final Date DEFAULT_NOT_AFTER = new Date(SystemPropertyUtil.getLong(\"io.netty.selfSignedCertificate.defaultNotAfter\", 253402300799000L));\n    private static final String ALGORITHM = \"RSA\";\n    private static final int DEFAULT_KEY_LENGTH_BITS = SystemPropertyUtil.getInt(\"io.netty.handler.ssl.util.selfSignedKeyStrength\", 2048);\n\n    private File certificate;\n    private File privateKey;\n    private X509Certificate cert;\n    private PrivateKey key;\n\n    public SelfSignedCertGenerator() throws CertificateException {\n        KeyPair keypair;\n        SecureRandom random = ThreadLocalInsecureRandom.current();\n        try {\n            KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);\n            keyGen.initialize(DEFAULT_KEY_LENGTH_BITS, random);\n            keypair = keyGen.generateKeyPair();\n        } catch (NoSuchAlgorithmException var24) {\n            throw new Error(var24);\n        }\n\n        String[] paths;\n        try {\n            paths = generate(DEFAULT_FQDN, keypair, random, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);\n        } catch (Throwable var23) {\n            logger.debug(\"Failed to generate a self-signed X.509 certificate:\", var23);\n            CertificateException certificateException = new CertificateException(\"No provider succeeded to generate a self-signed certificate. See debug log for the root cause.\", var23);\n            ThrowableUtil.addSuppressed(certificateException, var23);\n            throw certificateException;\n        }\n\n        this.certificate = new File(paths[0]);\n        this.privateKey = new File(paths[1]);\n        this.key = keypair.getPrivate();\n        FileInputStream certificateInput = null;\n\n        try {\n            certificateInput = new FileInputStream(this.certificate);\n            this.cert = (X509Certificate) CertificateFactory.getInstance(\"X509\").generateCertificate(certificateInput);\n        } catch (Exception var21) {\n            throw new CertificateEncodingException(var21);\n        } finally {\n            if (certificateInput != null) {\n                try {\n                    certificateInput.close();\n                } catch (IOException var25) {\n                    logger.warn(\"Failed to close a file: \" + this.certificate, var25);\n                }\n            }\n        }\n    }\n\n    public File getCertificate() {\n        return certificate;\n    }\n\n    public File getPrivateKey() {\n        return privateKey;\n    }\n\n    private String[] generate(String fqdn, KeyPair keypair, SecureRandom random, Date notBefore, Date notAfter) throws Exception {\n        PrivateKey key = keypair.getPrivate();\n        X500Name owner = new X500Name(\"CN=\" + fqdn);\n        X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(owner, new BigInteger(64, random), notBefore, notAfter, owner, keypair.getPublic());\n        ContentSigner signer = (new JcaContentSignerBuilder(\"SHA256WithRSAEncryption\")).build(key);\n        X509CertificateHolder certHolder = builder.build(signer);\n        X509Certificate cert = new JcaX509CertificateConverter().setProvider(PROVIDER).getCertificate(certHolder);\n        cert.verify(keypair.getPublic());\n        return newSelfSignedCertificate(fqdn, key, cert);\n    }\n\n    private String[] newSelfSignedCertificate(String fqdn, PrivateKey key, X509Certificate cert) throws IOException, CertificateEncodingException {\n        ByteBuf wrappedBuf = Unpooled.wrappedBuffer(key.getEncoded());\n        ByteBuf encodedBuf;\n        String keyText;\n        try {\n            encodedBuf = Base64.encode(wrappedBuf, true);\n            try {\n                keyText = \"-----BEGIN PRIVATE KEY-----\\n\" + encodedBuf.toString(CharsetUtil.US_ASCII) + \"\\n-----END PRIVATE KEY-----\\n\";\n            } finally {\n                encodedBuf.release();\n            }\n        } finally {\n            wrappedBuf.release();\n        }\n        fqdn = fqdn.replaceAll(\"[^\\\\w.-]\", \"x\");\n        File keyFile = PlatformDependent.createTempFile(\"keyutil_\" + fqdn + '_', \".key\", (File)null);\n        keyFile.deleteOnExit();\n        OutputStream keyOut = new FileOutputStream(keyFile);\n        try {\n            keyOut.write(keyText.getBytes(CharsetUtil.US_ASCII));\n            keyOut.close();\n            keyOut = null;\n        } finally {\n            if (keyOut != null) {\n                safeClose(keyFile, keyOut);\n                safeDelete(keyFile);\n            }\n        }\n        wrappedBuf = Unpooled.wrappedBuffer(cert.getEncoded());\n        String certText;\n        try {\n            encodedBuf = Base64.encode(wrappedBuf, true);\n            try {\n                certText = \"-----BEGIN CERTIFICATE-----\\n\" + encodedBuf.toString(CharsetUtil.US_ASCII) + \"\\n-----END CERTIFICATE-----\\n\";\n            } finally {\n                encodedBuf.release();\n            }\n        } finally {\n            wrappedBuf.release();\n        }\n        File certFile = PlatformDependent.createTempFile(\"keyutil_\" + fqdn + '_', \".crt\", (File)null);\n        certFile.deleteOnExit();\n        OutputStream certOut = new FileOutputStream(certFile);\n        try {\n            certOut.write(certText.getBytes(CharsetUtil.US_ASCII));\n            certOut.close();\n            certOut = null;\n        } finally {\n            if (certOut != null) {\n                safeClose(certFile, certOut);\n                safeDelete(certFile);\n                safeDelete(keyFile);\n            }\n        }\n        return new String[]{certFile.getPath(), keyFile.getPath()};\n    }\n\n    private static void safeDelete(File certFile) {\n        if (!certFile.delete()) {\n            logger.warn(\"Failed to delete a file: \" + certFile);\n        }\n    }\n\n    private static void safeClose(File keyFile, OutputStream keyOut) {\n        try {\n            keyOut.close();\n        } catch (IOException var3) {\n            logger.warn(\"Failed to close a file: \" + keyFile, var3);\n        }\n    }\n}"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/http/cert/ThreadLocalInsecureRandom.java",
    "content": "package com.intuit.karate.http.cert;\n\nimport io.netty.util.internal.PlatformDependent;\nimport java.security.SecureRandom;\nimport java.util.Random;\n\nfinal class ThreadLocalInsecureRandom extends SecureRandom {\n    private static final long serialVersionUID = -8209473337192526191L;\n    private static final SecureRandom INSTANCE = new ThreadLocalInsecureRandom();\n\n    static SecureRandom current() {\n        return INSTANCE;\n    }\n\n    private ThreadLocalInsecureRandom() {\n    }\n\n    public String getAlgorithm() {\n        return \"insecure\";\n    }\n\n    public void setSeed(byte[] seed) {\n    }\n\n    public void setSeed(long seed) {\n    }\n\n    public void nextBytes(byte[] bytes) {\n        random().nextBytes(bytes);\n    }\n\n    public byte[] generateSeed(int numBytes) {\n        byte[] seed = new byte[numBytes];\n        random().nextBytes(seed);\n        return seed;\n    }\n\n    public int nextInt() {\n        return random().nextInt();\n    }\n\n    public int nextInt(int n) {\n        return random().nextInt(n);\n    }\n\n    public boolean nextBoolean() {\n        return random().nextBoolean();\n    }\n\n    public long nextLong() {\n        return random().nextLong();\n    }\n\n    public float nextFloat() {\n        return random().nextFloat();\n    }\n\n    public double nextDouble() {\n        return random().nextDouble();\n    }\n\n    public double nextGaussian() {\n        return random().nextGaussian();\n    }\n\n    private static Random random() {\n        return PlatformDependent.threadLocalRandom();\n    }\n}"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/Report.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.report;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.graal.JsEngine;\nimport com.intuit.karate.template.KarateTemplateEngine;\nimport com.intuit.karate.template.TemplateUtils;\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Report {\n\n    JsEngine getJsEngine();\n\n    String getResourceRoot();\n\n    String getTemplate();\n\n    String getReportDir();\n\n    String getReportFileName();\n    \n    default File render() {\n        return render(null);\n    }\n\n    default File render(String reportDir) {\n        JsEngine je = getJsEngine();\n        KarateTemplateEngine engine = TemplateUtils.forResourceRoot(je, getResourceRoot());\n        String html = engine.process(getTemplate());\n        if (reportDir == null) {\n            reportDir = getReportDir();\n        }\n        ReportUtils.initStaticResources(reportDir);\n        File file = new File(reportDir + File.separator + getReportFileName());\n        FileUtils.writeToFile(file, html);\n        return file;\n    }\n\n    public static class Builder {\n\n        private JsEngine je;\n        private String resourceRoot = \"classpath:com/intuit/karate/report\";\n        private String template;\n        private String reportDir;\n        private String reportFileName;\n        private final Map<String, Object> variables = new HashMap();\n\n        public Builder resourceRoot(String value) {\n            resourceRoot = value;\n            return this;\n        }\n\n        public Builder template(String value) {\n            template = value;\n            return this;\n        }\n\n        public Builder jsEngine(JsEngine value) {\n            je = value;\n            return this;\n        }\n\n        public Builder variable(String name, Object value) {\n            variables.put(name, value);\n            return this;\n        }\n\n        public Builder variables(Map<String, Object> value) {\n            variables.putAll(value);\n            return this;\n        }\n\n        public Builder reportDir(String value) {\n            reportDir = value;\n            return this;\n        }\n        \n        public Builder reportFileName(String value) {\n            reportFileName = value;\n            return this;\n        }\n\n        public Report build() {\n            if (template == null) {\n                throw new RuntimeException(\"template name is mandatory\");\n            }\n            if (reportDir == null) {\n                throw new RuntimeException(\"report dir is mandatory\");\n            }\n            if (reportFileName == null) {\n                reportFileName = template;\n            }\n            if (je == null) {\n                je = JsEngine.local();\n            }\n            je.putAll(variables);\n            return new Report() {\n\n                @Override\n                public JsEngine getJsEngine() {\n                    return je;\n                }\n\n                @Override\n                public String getResourceRoot() {\n                    return resourceRoot;\n                }\n\n                @Override\n                public String getTemplate() {\n                    return template;\n                }\n\n                @Override\n                public String getReportDir() {\n                    return reportDir;\n                }\n\n                @Override\n                public String getReportFileName() {\n                    return reportFileName;\n                }\n\n            };\n        }\n\n    }\n\n    public static Builder template(String value) {\n        return new Builder().template(value);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/ReportUtils.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.report;\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.XmlUtils;\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.core.FeatureResult;\nimport com.intuit.karate.core.ScenarioResult;\nimport com.intuit.karate.core.StepResult;\nimport com.intuit.karate.resource.ResourceUtils;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.text.DecimalFormat;\nimport java.text.NumberFormat;\nimport java.text.SimpleDateFormat;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\n\n/**\n *\n * @author pthomas3\n */\npublic class ReportUtils {\n\n    private ReportUtils() {\n        // only static methods\n    }\n\n    private static final String[] STATIC_RESOURCES = new String[]{\n        \"favicon.ico\",\n        \"karate-logo.png\",\n        \"karate-logo.svg\",\n        \"karate-labs-logo-ring.svg\",\n        \"com/intuit/karate/report/bootstrap.min.css\",\n        \"com/intuit/karate/report/bootstrap.min.js\",\n        \"com/intuit/karate/report/jquery.min.js\",\n        \"com/intuit/karate/report/jquery.tablesorter.min.js\",\n        \"com/intuit/karate/report/jquery-ui.min.js\",\n        \"com/intuit/karate/report/vis.min.css\",\n        \"com/intuit/karate/report/vis.min.js\",\n        \"com/intuit/karate/report/karate-report.css\",\n        \"com/intuit/karate/report/karate-report.js\",\n        \"com/intuit/karate/report/Resemble.js\"\n    };\n    \n    public static Map<String, Object> commonVars() {\n        Map<String, Object> map = new HashMap(5);\n        map.put(\"userUuid\", FileUtils.USER_UUID);\n        map.put(\"karateVersion\", FileUtils.KARATE_VERSION);\n        map.put(\"karateMeta\", FileUtils.KARATE_META);\n        map.put(\"karateTelemetry\", FileUtils.KARATE_TELEMETRY);\n        return map;\n    }    \n\n    public static String getDateString() {\n        SimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd hh:mm:ss a\");\n        return sdf.format(new Date());\n    }\n\n    private static void copyToFile(String classPath, String destPath) {\n        InputStream is = ResourceUtils.classPathResourceToStream(classPath);\n        byte[] bytes = FileUtils.toBytes(is);\n        FileUtils.writeToFile(new File(destPath), bytes);\n    }\n\n    public static void initStaticResources(String targetDir) {\n        String resPath = targetDir + File.separator + \"res\" + File.separator;\n        File resFile = new File(resPath);\n        if (resFile.exists()) {\n            return;\n        }\n        for (String path : STATIC_RESOURCES) {\n            int pos = path.lastIndexOf('/');\n            if (pos == -1) {\n                copyToFile(path, resFile.getParent() + File.separator + path);\n            } else {\n                copyToFile(path, resPath + path.substring(pos + 1));\n            }\n        }\n    }\n\n    private static final double MILLION = 1000000;\n    private static final double BILLION = 1000000000;\n\n    public static double nanosToSeconds(long nanos) {\n        return (double) nanos / BILLION;\n    }\n\n    public static double nanosToMillis(long nanos) {\n        return (double) nanos / MILLION;\n    }\n\n    public static File saveKarateJson(String targetDir, FeatureResult result, String fileName) {\n        if (fileName == null) {\n            fileName = result.getFeature().getKarateJsonFileName();\n        }\n        File file = new File(targetDir + File.separator + fileName);\n        FileUtils.writeToFile(file, JsonUtils.toJson(result.toKarateJson()));\n        return file;\n    }\n\n    public static File saveCucumberJson(String targetDir, FeatureResult result, String fileName) {\n        if (fileName == null) {\n            fileName = result.getFeature().getPackageQualifiedName() + \".json\";\n        }\n        File file = new File(targetDir + File.separator + fileName);\n        String json = JsonUtils.toJson(Collections.singletonList(result.toCucumberJson()));\n        FileUtils.writeToFile(file, json);\n        return file;\n    }\n\n    private static Throwable appendSteps(List<StepResult> steps, StringBuilder sb) {\n        Throwable error = null;\n        for (StepResult sr : steps) {\n            int length = sb.length();\n            sb.append(sr.getStep().getPrefix());\n            sb.append(' ');\n            sb.append(sr.getStep().getText());\n            sb.append(' ');\n            do {\n                sb.append('.');\n            } while (sb.length() - length < 75);\n            sb.append(' ');\n            sb.append(sr.getResult().getStatus());\n            sb.append('\\n');\n            if (sr.getResult().isFailed()) {\n                sb.append(\"\\nStack Trace:\\n\");\n                error = sr.getResult().getError();\n                sb.append(StringUtils.throwableToString(error));\n                sb.append('\\n');\n            }\n        }\n        return error;\n    }\n\n    private static Element addCustomTags(Element testCase, Document doc, ScenarioResult sr){\n        //Adding requirement and test tags\n        Element properties = null;   \n\n        if (sr.getScenario() != null){\n            List<String> tags = sr.getScenario().getTagsEffective().getTags();\n            if (tags.size() > 0 ){\n                properties = doc.createElement(\"properties\");\n                \n                for (String tag : tags) {                        \n                    String[] innerTags = tag.split(\"=\");\n                    int size = innerTags.length;\n                    Element requirement = doc.createElement(\"property\");\n                    \n                    if(size > 1){\n                        requirement.setAttribute(\"name\", innerTags[0]);     \n                        requirement.setAttribute(\"value\", innerTags[1]);\n                        properties.appendChild(requirement);\n                    }\n                }\n            }\n        }\n\n        return properties;\n    }\n\n    public static File saveJunitXml(String targetDir, FeatureResult result, String fileName) {\n        DecimalFormat formatter = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US);\n        formatter.applyPattern(\"0.######\");\n        Document doc = XmlUtils.newDocument();\n        Element root = doc.createElement(\"testsuite\");\n        doc.appendChild(root);\n        root.setAttribute(\"tests\", result.getScenarioCount() + \"\");\n        root.setAttribute(\"failures\", result.getFailedCount() + \"\");\n        root.setAttribute(\"time\", formatter.format(result.getDurationMillis() / 1000));\n        root.setAttribute(\"name\", result.getDisplayName()); // will be uri\n        root.setAttribute(\"skipped\", \"0\");\n        StringBuilder xmlString = new StringBuilder();\n        xmlString.append(XmlUtils.toString(doc, false).replace(\"/>\", \">\"));\n        String baseName = result.getFeature().getPackageQualifiedName();\n        Iterator<ScenarioResult> iterator = result.getScenarioResults().iterator();\n        while (iterator.hasNext()) {\n            ScenarioResult sr = iterator.next();\n            Element testCase = doc.createElement(\"testcase\");\n            testCase.setAttribute(\"classname\", baseName);\n            StringBuilder sb = new StringBuilder();\n            Throwable error = appendSteps(sr.getStepResults(), sb);\n            String name = sr.getScenario().getName();\n            if (StringUtils.isBlank(name)) {\n                name = sr.getScenario().getUniqueId();\n            }\n            testCase.setAttribute(\"name\", name);\n            testCase.setAttribute(\"time\", formatter.format(sr.getDurationMillis() / 1000));\n            Element stepsHolder;\n            if (error != null && sr.isFailed()) {\n                stepsHolder = doc.createElement(\"failure\");\n                stepsHolder.setAttribute(\"message\", error.getMessage());\n            } else {\n                stepsHolder = doc.createElement(\"system-out\");\n            }\n\n            Element properties = null;\n            properties = addCustomTags(testCase, doc, sr);\n            if(properties != null && properties.getChildNodes().getLength() > 0){\n                testCase.appendChild(properties);\n            }\n            \n            testCase.appendChild(stepsHolder);\n            stepsHolder.setTextContent(sb.toString());\n            xmlString.append(XmlUtils.toString(testCase)).append('\\n');\n        }\n        xmlString.append(\"</testsuite>\");\n        if (fileName == null) {\n            fileName = baseName + \".xml\";\n        }\n        File file = new File(targetDir + File.separator + fileName);\n        FileUtils.writeToFile(file, xmlString.toString());\n        return file;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/Resemble.js",
    "content": "/*\nJames Cryer / Huddle\nURL: https://github.com/Huddle/Resemble.js\n*/\n\nvar naiveFallback = function () {\n    // ISC (c) 2011-2019 https://github.com/medikoo/es5-ext/blob/master/global.js\n    if (typeof self === \"object\" && self) {\n        return self;\n    }\n    if (typeof window === \"object\" && window) {\n        return window;\n    }\n    throw new Error(\"Unable to resolve global `this`\");\n};\n\nvar getGlobalThis = function () {\n    // ISC (c) 2011-2019 https://github.com/medikoo/es5-ext/blob/master/global.js\n    // Fallback to standard globalThis if available\n    if (typeof globalThis === \"object\" && globalThis) {\n        return globalThis;\n    }\n\n    try {\n        Object.defineProperty(Object.prototype, \"__global__\", {\n            get: function () {\n                return this;\n            },\n            configurable: true\n        });\n    } catch (error) {\n        return naiveFallback();\n    }\n    try {\n        // eslint-disable-next-line no-undef\n        if (!__global__) {\n            return naiveFallback();\n        }\n        return __global__; // eslint-disable-line no-undef\n    } finally {\n        delete Object.prototype.__global__;\n    }\n};\n\nvar isNode = function () {\n    const globalPolyfill = getGlobalThis();\n    return typeof globalPolyfill.process !== \"undefined\" && globalPolyfill.process.versions && globalPolyfill.process.versions.node;\n};\n\n(function (root, factory) {\n    \"use strict\";\n    if (typeof define === \"function\" && define.amd) {\n        define([], factory);\n    } else if (typeof module === \"object\" && module.exports) {\n        module.exports = factory();\n    } else {\n        root.resemble = factory();\n    }\n})(this /* eslint-disable-line no-invalid-this*/, function () {\n    \"use strict\";\n\n    var Img;\n    var Canvas;\n    var loadNodeCanvasImage;\n\n    if (isNode()) {\n        Canvas = require(\"canvas\"); // eslint-disable-line global-require\n        Img = Canvas.Image;\n        loadNodeCanvasImage = Canvas.loadImage;\n    } else {\n        Img = Image;\n    }\n\n    function createCanvas(width, height) {\n        if (isNode()) {\n            return Canvas.createCanvas(width, height);\n        }\n\n        var cnvs = document.createElement(\"canvas\");\n        cnvs.width = width;\n        cnvs.height = height;\n        return cnvs;\n    }\n\n    var oldGlobalSettings = {};\n    var globalOutputSettings = oldGlobalSettings;\n\n    var resemble = function (fileData) {\n        var pixelTransparency = 1;\n\n        var errorPixelColor = {\n            // Color for Error Pixels. Between 0 and 255.\n            red: 255,\n            green: 0,\n            blue: 255,\n            alpha: 255\n        };\n\n        var targetPix = { r: 0, g: 0, b: 0, a: 0 }; // isAntialiased\n\n        var errorPixelTransform = {\n            flat: function (px, offset) {\n                px[offset] = errorPixelColor.red;\n                px[offset + 1] = errorPixelColor.green;\n                px[offset + 2] = errorPixelColor.blue;\n                px[offset + 3] = errorPixelColor.alpha;\n            },\n            movement: function (px, offset, d1, d2) {\n                px[offset] = (d2.r * (errorPixelColor.red / 255) + errorPixelColor.red) / 2;\n                px[offset + 1] = (d2.g * (errorPixelColor.green / 255) + errorPixelColor.green) / 2;\n                px[offset + 2] = (d2.b * (errorPixelColor.blue / 255) + errorPixelColor.blue) / 2;\n                px[offset + 3] = d2.a;\n            },\n            flatDifferenceIntensity: function (px, offset, d1, d2) {\n                px[offset] = errorPixelColor.red;\n                px[offset + 1] = errorPixelColor.green;\n                px[offset + 2] = errorPixelColor.blue;\n                px[offset + 3] = colorsDistance(d1, d2);\n            },\n            movementDifferenceIntensity: function (px, offset, d1, d2) {\n                var ratio = (colorsDistance(d1, d2) / 255) * 0.8;\n\n                px[offset] = (1 - ratio) * (d2.r * (errorPixelColor.red / 255)) + ratio * errorPixelColor.red;\n                px[offset + 1] = (1 - ratio) * (d2.g * (errorPixelColor.green / 255)) + ratio * errorPixelColor.green;\n                px[offset + 2] = (1 - ratio) * (d2.b * (errorPixelColor.blue / 255)) + ratio * errorPixelColor.blue;\n                px[offset + 3] = d2.a;\n            },\n            diffOnly: function (px, offset, d1, d2) {\n                px[offset] = d2.r;\n                px[offset + 1] = d2.g;\n                px[offset + 2] = d2.b;\n                px[offset + 3] = d2.a;\n            }\n        };\n\n        var errorPixel = errorPixelTransform.flat;\n        var errorType;\n        var boundingBoxes;\n        var ignoredBoxes;\n        var ignoreAreasColoredWith;\n        var largeImageThreshold = 1200;\n        var useCrossOrigin = true;\n        var data = {};\n        var images = [];\n        var updateCallbackArray = [];\n\n        var tolerance = {\n            // between 0 and 255\n            red: 16,\n            green: 16,\n            blue: 16,\n            alpha: 16,\n            minBrightness: 16,\n            maxBrightness: 240\n        };\n\n        var ignoreAntialiasing = false;\n        var ignoreColors = false;\n        var scaleToSameSize = false;\n        var compareOnly = false;\n        var returnEarlyThreshold;\n\n        function colorsDistance(c1, c2) {\n            return (Math.abs(c1.r - c2.r) + Math.abs(c1.g - c2.g) + Math.abs(c1.b - c2.b)) / 3;\n        }\n\n        function withinBoundingBox(x, y, width, height, box) {\n            // [JK 2022.10.24] we allow boxes to reach edge of images (small deviation from main Resemble.js)\n            return x >= (box.left || 0) && x <= (box.right || width) && y >= (box.top || 0) && y <= (box.bottom || height);\n        }\n\n        function withinComparedArea(x, y, width, height, pixel2) {\n            var isIncluded = true;\n            var i;\n            var boundingBox;\n            var ignoredBox;\n            var selected;\n            var ignored;\n\n            if (boundingBoxes instanceof Array) {\n                selected = false;\n                for (i = 0; i < boundingBoxes.length; i++) {\n                    boundingBox = boundingBoxes[i];\n                    if (withinBoundingBox(x, y, width, height, boundingBox)) {\n                        selected = true;\n                        break;\n                    }\n                }\n            }\n            if (ignoredBoxes instanceof Array) {\n                ignored = true;\n                for (i = 0; i < ignoredBoxes.length; i++) {\n                    ignoredBox = ignoredBoxes[i];\n                    if (withinBoundingBox(x, y, width, height, ignoredBox)) {\n                        ignored = false;\n                        break;\n                    }\n                }\n            }\n\n            if (ignoreAreasColoredWith) {\n                return colorsDistance(pixel2, ignoreAreasColoredWith) !== 0;\n            }\n\n            if (selected === undefined && ignored === undefined) {\n                return true;\n            }\n            if (selected === false && ignored === true) {\n                return false;\n            }\n            if (selected === true || ignored === true) {\n                isIncluded = true;\n            }\n            if (selected === false || ignored === false) {\n                isIncluded = false;\n            }\n            return isIncluded;\n        }\n\n        function triggerDataUpdate() {\n            var len = updateCallbackArray.length;\n            var i;\n            for (i = 0; i < len; i++) {\n                if (typeof updateCallbackArray[i] === \"function\") {\n                    updateCallbackArray[i](data);\n                }\n            }\n        }\n\n        function loop(w, h, callback) {\n            var x;\n            var y;\n\n            for (x = 0; x < w; x++) {\n                for (y = 0; y < h; y++) {\n                    callback(x, y);\n                }\n            }\n        }\n\n        function parseImage(sourceImageData, width, height) {\n            var pixelCount = 0;\n            var redTotal = 0;\n            var greenTotal = 0;\n            var blueTotal = 0;\n            var alphaTotal = 0;\n            var brightnessTotal = 0;\n            var whiteTotal = 0;\n            var blackTotal = 0;\n\n            loop(width, height, function (horizontalPos, verticalPos) {\n                var offset = (verticalPos * width + horizontalPos) * 4;\n                var red = sourceImageData[offset];\n                var green = sourceImageData[offset + 1];\n                var blue = sourceImageData[offset + 2];\n                var alpha = sourceImageData[offset + 3];\n                var brightness = getBrightness(red, green, blue);\n\n                if (red === green && red === blue && alpha) {\n                    if (red === 0) {\n                        blackTotal++;\n                    } else if (red === 255) {\n                        whiteTotal++;\n                    }\n                }\n\n                pixelCount++;\n\n                redTotal += (red / 255) * 100;\n                greenTotal += (green / 255) * 100;\n                blueTotal += (blue / 255) * 100;\n                alphaTotal += ((255 - alpha) / 255) * 100;\n                brightnessTotal += (brightness / 255) * 100;\n            });\n\n            data.red = Math.floor(redTotal / pixelCount);\n            data.green = Math.floor(greenTotal / pixelCount);\n            data.blue = Math.floor(blueTotal / pixelCount);\n            data.alpha = Math.floor(alphaTotal / pixelCount);\n            data.brightness = Math.floor(brightnessTotal / pixelCount);\n            data.white = Math.floor((whiteTotal / pixelCount) * 100);\n            data.black = Math.floor((blackTotal / pixelCount) * 100);\n\n            triggerDataUpdate();\n        }\n\n        function onLoadImage(hiddenImage, callback) {\n            // don't assign to hiddenImage, see https://github.com/Huddle/Resemble.js/pull/87/commits/300d43352a2845aad289b254bfbdc7cd6a37e2d7\n            var width = hiddenImage.width;\n            var height = hiddenImage.height;\n\n            if (scaleToSameSize && images.length === 1) {\n                width = images[0].width;\n                height = images[0].height;\n            }\n\n            var hiddenCanvas = createCanvas(width, height);\n            var imageData;\n\n            hiddenCanvas.getContext(\"2d\").drawImage(hiddenImage, 0, 0, width, height);\n            imageData = hiddenCanvas.getContext(\"2d\").getImageData(0, 0, width, height);\n\n            images.push(imageData);\n\n            callback(imageData, width, height);\n        }\n\n        function loadImageData(fileDataForImage, callback) {\n            var fileReader;\n            var hiddenImage = new Img();\n\n            if (!hiddenImage.setAttribute) {\n                hiddenImage.setAttribute = function setAttribute() {};\n            }\n\n            if (useCrossOrigin) {\n                hiddenImage.setAttribute(\"crossorigin\", \"anonymous\");\n            }\n\n            hiddenImage.onerror = function (event) {\n                hiddenImage.onload = null;\n                hiddenImage.onerror = null; // fixes pollution between calls\n                const error = event ? event + \"\" : \"Unknown error\";\n                images.push({ error: `Failed to load image '${fileDataForImage}'. ${error}` });\n                callback();\n            };\n\n            hiddenImage.onload = function () {\n                hiddenImage.onload = null; // fixes pollution between calls\n                hiddenImage.onerror = null;\n                onLoadImage(hiddenImage, callback);\n            };\n\n            if (typeof fileDataForImage === \"string\") {\n                hiddenImage.src = fileDataForImage;\n                if (!isNode() && hiddenImage.complete && hiddenImage.naturalWidth > 0) {\n                    hiddenImage.onload();\n                }\n            } else if (\n                typeof fileDataForImage.data !== \"undefined\" &&\n                typeof fileDataForImage.width === \"number\" &&\n                typeof fileDataForImage.height === \"number\"\n            ) {\n                images.push(fileDataForImage);\n\n                callback(fileDataForImage, fileDataForImage.width, fileDataForImage.height);\n            } else if (typeof Buffer !== \"undefined\" && fileDataForImage instanceof Buffer) {\n                // If we have Buffer, assume we're on Node+Canvas and its supported\n                // hiddenImage.src = fileDataForImage;\n\n                loadNodeCanvasImage(fileDataForImage)\n                    .then(function (image) {\n                        hiddenImage.onload = null; // fixes pollution between calls\n                        hiddenImage.onerror = null;\n                        onLoadImage(image, callback);\n                    })\n                    .catch(function (err) {\n                        images.push({\n                            error: err ? err + \"\" : \"Image load error.\"\n                        });\n                        callback();\n                    });\n            } else {\n                fileReader = new FileReader();\n                fileReader.onload = function (event) {\n                    hiddenImage.src = event.target.result;\n                };\n                fileReader.readAsDataURL(fileDataForImage);\n            }\n        }\n\n        function isColorSimilar(a, b, color) {\n            var absDiff = Math.abs(a - b);\n\n            if (typeof a === \"undefined\") {\n                return false;\n            }\n            if (typeof b === \"undefined\") {\n                return false;\n            }\n\n            if (a === b) {\n                return true;\n            } else if (absDiff < tolerance[color]) {\n                return true;\n            }\n            return false;\n        }\n\n        function isPixelBrightnessSimilar(d1, d2) {\n            var alpha = isColorSimilar(d1.a, d2.a, \"alpha\");\n            var brightness = isColorSimilar(d1.brightness, d2.brightness, \"minBrightness\");\n            return brightness && alpha;\n        }\n\n        function getBrightness(r, g, b) {\n            return 0.3 * r + 0.59 * g + 0.11 * b;\n        }\n\n        function isRGBSame(d1, d2) {\n            var red = d1.r === d2.r;\n            var green = d1.g === d2.g;\n            var blue = d1.b === d2.b;\n            return red && green && blue;\n        }\n\n        function isRGBSimilar(d1, d2) {\n            var red = isColorSimilar(d1.r, d2.r, \"red\");\n            var green = isColorSimilar(d1.g, d2.g, \"green\");\n            var blue = isColorSimilar(d1.b, d2.b, \"blue\");\n            var alpha = isColorSimilar(d1.a, d2.a, \"alpha\");\n\n            return red && green && blue && alpha;\n        }\n\n        function isContrasting(d1, d2) {\n            return Math.abs(d1.brightness - d2.brightness) > tolerance.maxBrightness;\n        }\n\n        function getHue(red, green, blue) {\n            var r = red / 255;\n            var g = green / 255;\n            var b = blue / 255;\n            var max = Math.max(r, g, b);\n            var min = Math.min(r, g, b);\n            var h;\n            var d;\n\n            if (max === min) {\n                h = 0; // achromatic\n            } else {\n                d = max - min;\n                switch (max) {\n                    case r:\n                        h = (g - b) / d + (g < b ? 6 : 0);\n                        break;\n                    case g:\n                        h = (b - r) / d + 2;\n                        break;\n                    case b:\n                        h = (r - g) / d + 4;\n                        break;\n                    default:\n                        h /= 6;\n                }\n            }\n\n            return h;\n        }\n\n        function isAntialiased(sourcePix, pix, cacheSet, verticalPos, horizontalPos, width) {\n            var offset;\n            var distance = 1;\n            var i;\n            var j;\n            var hasHighContrastSibling = 0;\n            var hasSiblingWithDifferentHue = 0;\n            var hasEquivalentSibling = 0;\n\n            addHueInfo(sourcePix);\n\n            for (i = distance * -1; i <= distance; i++) {\n                for (j = distance * -1; j <= distance; j++) {\n                    if (i === 0 && j === 0) {\n                        // ignore source pixel\n                    } else {\n                        offset = ((verticalPos + j) * width + (horizontalPos + i)) * 4;\n\n                        if (!getPixelInfo(targetPix, pix, offset, cacheSet)) {\n                            continue;\n                        }\n\n                        addBrightnessInfo(targetPix);\n                        addHueInfo(targetPix);\n\n                        if (isContrasting(sourcePix, targetPix)) {\n                            hasHighContrastSibling++;\n                        }\n\n                        if (isRGBSame(sourcePix, targetPix)) {\n                            hasEquivalentSibling++;\n                        }\n\n                        if (Math.abs(targetPix.h - sourcePix.h) > 0.3) {\n                            hasSiblingWithDifferentHue++;\n                        }\n\n                        if (hasSiblingWithDifferentHue > 1 || hasHighContrastSibling > 1) {\n                            return true;\n                        }\n                    }\n                }\n            }\n\n            if (hasEquivalentSibling < 2) {\n                return true;\n            }\n\n            return false;\n        }\n\n        function copyPixel(px, offset, pix) {\n            if (errorType === \"diffOnly\") {\n                return;\n            }\n\n            px[offset] = pix.r; // r\n            px[offset + 1] = pix.g; // g\n            px[offset + 2] = pix.b; // b\n            px[offset + 3] = pix.a * pixelTransparency; // a\n        }\n\n        function copyGrayScalePixel(px, offset, pix) {\n            if (errorType === \"diffOnly\") {\n                return;\n            }\n\n            px[offset] = pix.brightness; // r\n            px[offset + 1] = pix.brightness; // g\n            px[offset + 2] = pix.brightness; // b\n            px[offset + 3] = pix.a * pixelTransparency; // a\n        }\n\n        function getPixelInfo(dst, pix, offset) {\n            if (pix.length > offset) {\n                dst.r = pix[offset];\n                dst.g = pix[offset + 1];\n                dst.b = pix[offset + 2];\n                dst.a = pix[offset + 3];\n\n                return true;\n            }\n\n            return false;\n        }\n\n        function addBrightnessInfo(pix) {\n            pix.brightness = getBrightness(pix.r, pix.g, pix.b); // 'corrected' lightness\n        }\n\n        function addHueInfo(pix) {\n            pix.h = getHue(pix.r, pix.g, pix.b);\n        }\n\n        function analyseImages(img1, img2, width, height) {\n            var data1 = img1.data;\n            var data2 = img2.data;\n            var hiddenCanvas;\n            var context;\n            var imgd;\n            var pix;\n\n            if (!compareOnly) {\n                hiddenCanvas = createCanvas(width, height);\n\n                context = hiddenCanvas.getContext(\"2d\");\n                imgd = context.createImageData(width, height);\n                pix = imgd.data;\n            }\n\n            var mismatchCount = 0;\n            var diffBounds = {\n                top: height,\n                left: width,\n                bottom: 0,\n                right: 0\n            };\n            var updateBounds = function (x, y) {\n                diffBounds.left = Math.min(x, diffBounds.left);\n                diffBounds.right = Math.max(x, diffBounds.right);\n                diffBounds.top = Math.min(y, diffBounds.top);\n                diffBounds.bottom = Math.max(y, diffBounds.bottom);\n            };\n\n            var time = Date.now();\n\n            var skip;\n\n            if (!!largeImageThreshold && ignoreAntialiasing && (width > largeImageThreshold || height > largeImageThreshold)) {\n                skip = 6;\n            }\n\n            var pixel1 = { r: 0, g: 0, b: 0, a: 0 };\n            var pixel2 = { r: 0, g: 0, b: 0, a: 0 };\n\n            var skipTheRest = false;\n\n            loop(width, height, function (horizontalPos, verticalPos) {\n                if (skipTheRest) {\n                    return;\n                }\n\n                if (skip) {\n                    // only skip if the image isn't small\n                    if (verticalPos % skip === 0 || horizontalPos % skip === 0) {\n                        return;\n                    }\n                }\n\n                var offset = (verticalPos * width + horizontalPos) * 4;\n                if (!getPixelInfo(pixel1, data1, offset, 1) || !getPixelInfo(pixel2, data2, offset, 2)) {\n                    return;\n                }\n\n                var isWithinComparedArea = withinComparedArea(horizontalPos, verticalPos, width, height, pixel2);\n\n                if (ignoreColors) {\n                    addBrightnessInfo(pixel1);\n                    addBrightnessInfo(pixel2);\n\n                    if (isPixelBrightnessSimilar(pixel1, pixel2) || !isWithinComparedArea) {\n                        if (!compareOnly) {\n                            copyGrayScalePixel(pix, offset, pixel2);\n                        }\n                    } else {\n                        if (!compareOnly) {\n                            errorPixel(pix, offset, pixel1, pixel2);\n                        }\n\n                        mismatchCount++;\n                        updateBounds(horizontalPos, verticalPos);\n                    }\n                    return;\n                }\n\n                if (isRGBSimilar(pixel1, pixel2) || !isWithinComparedArea) {\n                    if (!compareOnly) {\n                        copyPixel(pix, offset, pixel1);\n                    }\n                } else if (\n                    ignoreAntialiasing &&\n                    (addBrightnessInfo(pixel1), // jit pixel info augmentation looks a little weird, sorry.\n                        addBrightnessInfo(pixel2),\n                    isAntialiased(pixel1, data1, 1, verticalPos, horizontalPos, width) || isAntialiased(pixel2, data2, 2, verticalPos, horizontalPos, width))\n                ) {\n                    if (isPixelBrightnessSimilar(pixel1, pixel2) || !isWithinComparedArea) {\n                        if (!compareOnly) {\n                            copyGrayScalePixel(pix, offset, pixel2);\n                        }\n                    } else {\n                        if (!compareOnly) {\n                            errorPixel(pix, offset, pixel1, pixel2);\n                        }\n\n                        mismatchCount++;\n                        updateBounds(horizontalPos, verticalPos);\n                    }\n                } else {\n                    if (!compareOnly) {\n                        errorPixel(pix, offset, pixel1, pixel2);\n                    }\n\n                    mismatchCount++;\n                    updateBounds(horizontalPos, verticalPos);\n                }\n\n                if (compareOnly) {\n                    var currentMisMatchPercent = (mismatchCount / (height * width)) * 100;\n\n                    if (currentMisMatchPercent > returnEarlyThreshold) {\n                        skipTheRest = true;\n                    }\n                }\n            });\n\n            data.rawMisMatchPercentage = (mismatchCount / (height * width)) * 100;\n            data.misMatchPercentage = data.rawMisMatchPercentage.toFixed(2);\n            data.diffBounds = diffBounds;\n            data.analysisTime = Date.now() - time;\n\n            data.getImageDataUrl = function (text) {\n                if (compareOnly) {\n                    throw Error(\"No diff image available - ran in compareOnly mode\");\n                }\n\n                var barHeight = 0;\n\n                if (text) {\n                    barHeight = addLabel(text, context, hiddenCanvas);\n                }\n\n                context.putImageData(imgd, 0, barHeight);\n\n                return hiddenCanvas.toDataURL(\"image/png\");\n            };\n\n            if (!compareOnly && hiddenCanvas.toBuffer) {\n                data.getBuffer = function (includeOriginal) {\n                    if (includeOriginal) {\n                        var imageWidth = hiddenCanvas.width + 2;\n                        hiddenCanvas.width = imageWidth * 3;\n                        context.putImageData(img1, 0, 0);\n                        context.putImageData(img2, imageWidth, 0);\n                        context.putImageData(imgd, imageWidth * 2, 0);\n                    } else {\n                        context.putImageData(imgd, 0, 0);\n                    }\n                    return hiddenCanvas.toBuffer();\n                };\n            }\n        }\n\n        function addLabel(text, context, hiddenCanvas) {\n            var textPadding = 2;\n\n            context.font = \"12px sans-serif\";\n\n            var textWidth = context.measureText(text).width + textPadding * 2;\n            var barHeight = 22;\n\n            if (textWidth > hiddenCanvas.width) {\n                hiddenCanvas.width = textWidth;\n            }\n\n            hiddenCanvas.height += barHeight;\n\n            context.fillStyle = \"#666\";\n            context.fillRect(0, 0, hiddenCanvas.width, barHeight - 4);\n            context.fillStyle = \"#fff\";\n            context.fillRect(0, barHeight - 4, hiddenCanvas.width, 4);\n\n            context.fillStyle = \"#fff\";\n            context.textBaseline = \"top\";\n            context.font = \"12px sans-serif\";\n            context.fillText(text, textPadding, 1);\n\n            return barHeight;\n        }\n\n        function normalise(img, w, h) {\n            var c;\n            var context;\n\n            if (img.height < h || img.width < w) {\n                c = createCanvas(w, h);\n                context = c.getContext(\"2d\");\n                context.putImageData(img, 0, 0);\n                return context.getImageData(0, 0, w, h);\n            }\n\n            return img;\n        }\n\n        function outputSettings(options) {\n            var key;\n\n            if (options.errorColor) {\n                for (key in options.errorColor) {\n                    if (options.errorColor.hasOwnProperty(key)) {\n                        errorPixelColor[key] = options.errorColor[key] === void 0 ? errorPixelColor[key] : options.errorColor[key];\n                    }\n                }\n            }\n\n            if (options.errorType && errorPixelTransform[options.errorType]) {\n                errorPixel = errorPixelTransform[options.errorType];\n                errorType = options.errorType;\n            }\n\n            if (options.errorPixel && typeof options.errorPixel === \"function\") {\n                errorPixel = options.errorPixel;\n            }\n\n            pixelTransparency = isNaN(Number(options.transparency)) ? pixelTransparency : options.transparency;\n\n            if (options.largeImageThreshold !== undefined) {\n                largeImageThreshold = options.largeImageThreshold;\n            }\n\n            if (options.useCrossOrigin !== undefined) {\n                useCrossOrigin = options.useCrossOrigin;\n            }\n\n            if (options.boundingBox !== undefined) {\n                boundingBoxes = [options.boundingBox];\n            }\n\n            if (options.ignoredBox !== undefined) {\n                ignoredBoxes = [options.ignoredBox];\n            }\n\n            if (options.boundingBoxes !== undefined) {\n                boundingBoxes = options.boundingBoxes;\n            }\n\n            if (options.ignoredBoxes !== undefined) {\n                ignoredBoxes = options.ignoredBoxes;\n            }\n\n            if (options.ignoreAreasColoredWith !== undefined) {\n                ignoreAreasColoredWith = options.ignoreAreasColoredWith;\n            }\n        }\n\n        function compare(one, two) {\n            if (globalOutputSettings !== oldGlobalSettings) {\n                outputSettings(globalOutputSettings);\n            }\n\n            function onceWeHaveBoth() {\n                var width;\n                var height;\n                if (images.length === 2) {\n                    if (images[0].error || images[1].error) {\n                        data = {};\n                        data.error = images[0].error ? images[0].error : images[1].error;\n                        triggerDataUpdate();\n                        return;\n                    }\n                    width = images[0].width > images[1].width ? images[0].width : images[1].width;\n                    height = images[0].height > images[1].height ? images[0].height : images[1].height;\n\n                    if (images[0].width === images[1].width && images[0].height === images[1].height) {\n                        data.isSameDimensions = true;\n                    } else {\n                        data.isSameDimensions = false;\n                    }\n\n                    data.dimensionDifference = {\n                        width: images[0].width - images[1].width,\n                        height: images[0].height - images[1].height\n                    };\n\n                    analyseImages(normalise(images[0], width, height), normalise(images[1], width, height), width, height);\n\n                    triggerDataUpdate();\n                }\n            }\n\n            images = [];\n            loadImageData(one, onceWeHaveBoth);\n            loadImageData(two, onceWeHaveBoth);\n        }\n\n        function getCompareApi(param) {\n            var secondFileData;\n            var hasMethod = typeof param === \"function\";\n\n            if (!hasMethod) {\n                // assume it's file data\n                secondFileData = param;\n            }\n\n            var self = {\n                setReturnEarlyThreshold: function (threshold) {\n                    if (threshold) {\n                        compareOnly = true;\n                        returnEarlyThreshold = threshold;\n                    }\n                    return self;\n                },\n                scaleToSameSize: function () {\n                    scaleToSameSize = true;\n\n                    if (hasMethod) {\n                        param();\n                    }\n                    return self;\n                },\n                useOriginalSize: function () {\n                    scaleToSameSize = false;\n\n                    if (hasMethod) {\n                        param();\n                    }\n                    return self;\n                },\n                ignoreNothing: function () {\n                    tolerance.red = 0;\n                    tolerance.green = 0;\n                    tolerance.blue = 0;\n                    tolerance.alpha = 0;\n                    tolerance.minBrightness = 0;\n                    tolerance.maxBrightness = 255;\n\n                    ignoreAntialiasing = false;\n                    ignoreColors = false;\n\n                    if (hasMethod) {\n                        param();\n                    }\n                    return self;\n                },\n                ignoreLess: function () {\n                    tolerance.red = 16;\n                    tolerance.green = 16;\n                    tolerance.blue = 16;\n                    tolerance.alpha = 16;\n                    tolerance.minBrightness = 16;\n                    tolerance.maxBrightness = 240;\n\n                    ignoreAntialiasing = false;\n                    ignoreColors = false;\n\n                    if (hasMethod) {\n                        param();\n                    }\n                    return self;\n                },\n                ignoreAntialiasing: function () {\n                    tolerance.red = 32;\n                    tolerance.green = 32;\n                    tolerance.blue = 32;\n                    tolerance.alpha = 32;\n                    tolerance.minBrightness = 64;\n                    tolerance.maxBrightness = 96;\n\n                    ignoreAntialiasing = true;\n                    ignoreColors = false;\n\n                    if (hasMethod) {\n                        param();\n                    }\n                    return self;\n                },\n                ignoreColors: function () {\n                    tolerance.alpha = 16;\n                    tolerance.minBrightness = 16;\n                    tolerance.maxBrightness = 240;\n\n                    ignoreAntialiasing = false;\n                    ignoreColors = true;\n\n                    if (hasMethod) {\n                        param();\n                    }\n                    return self;\n                },\n                ignoreAlpha: function () {\n                    tolerance.red = 16;\n                    tolerance.green = 16;\n                    tolerance.blue = 16;\n                    tolerance.alpha = 255;\n                    tolerance.minBrightness = 16;\n                    tolerance.maxBrightness = 240;\n\n                    ignoreAntialiasing = false;\n                    ignoreColors = false;\n\n                    if (hasMethod) {\n                        param();\n                    }\n                    return self;\n                },\n                repaint: function () {\n                    if (hasMethod) {\n                        param();\n                    }\n                    return self;\n                },\n                outputSettings: function (options) {\n                    outputSettings(options);\n                    return self;\n                },\n                onComplete: function (callback) {\n                    updateCallbackArray.push(callback);\n\n                    var wrapper = function () {\n                        compare(fileData, secondFileData);\n                    };\n\n                    wrapper();\n\n                    return getCompareApi(wrapper);\n                },\n                setupCustomTolerance: function (customSettings) {\n                    for (var property in tolerance) {\n                        if (!customSettings.hasOwnProperty(property)) {\n                            continue;\n                        }\n\n                        tolerance[property] = customSettings[property];\n                    }\n                }\n            };\n\n            return self;\n        }\n\n        var rootSelf = {\n            onComplete: function (callback) {\n                updateCallbackArray.push(callback);\n                loadImageData(fileData, function (imageData, width, height) {\n                    parseImage(imageData.data, width, height);\n                });\n            },\n            compareTo: function (secondFileData) {\n                return getCompareApi(secondFileData);\n            },\n            outputSettings: function (options) {\n                outputSettings(options);\n                return rootSelf;\n            }\n        };\n\n        return rootSelf;\n    };\n\n    function setGlobalOutputSettings(settings) {\n        globalOutputSettings = settings;\n        return resemble;\n    }\n\n    function applyIgnore(api, ignore, customTolerance) {\n        switch (ignore) {\n            case \"nothing\":\n                api.ignoreNothing();\n                break;\n            case \"less\":\n                api.ignoreLess();\n                break;\n            case \"antialiasing\":\n                api.ignoreAntialiasing();\n                break;\n            case \"colors\":\n                api.ignoreColors();\n                break;\n            case \"alpha\":\n                api.ignoreAlpha();\n                break;\n            default:\n                throw new Error(\"Invalid ignore: \" + ignore);\n        }\n\n        api.setupCustomTolerance(customTolerance);\n    }\n\n    resemble.compare = function (image1, image2, options, cb) {\n        var callback;\n        var opt;\n\n        if (typeof options === \"function\") {\n            callback = options;\n            opt = {};\n        } else {\n            callback = cb;\n            opt = options || {};\n        }\n\n        var res = resemble(image1);\n        var compare;\n\n        if (opt.output) {\n            res.outputSettings(opt.output);\n        }\n\n        compare = res.compareTo(image2);\n\n        if (opt.returnEarlyThreshold) {\n            compare.setReturnEarlyThreshold(opt.returnEarlyThreshold);\n        }\n\n        if (opt.scaleToSameSize) {\n            compare.scaleToSameSize();\n        }\n\n        var toleranceSettings = opt.tolerance || {};\n        if (typeof opt.ignore === \"string\") {\n            applyIgnore(compare, opt.ignore, toleranceSettings);\n        } else if (opt.ignore && opt.ignore.forEach) {\n            opt.ignore.forEach(function (v) {\n                applyIgnore(compare, v, toleranceSettings);\n            });\n        }\n\n        compare.onComplete(function (data) {\n            if (data.error) {\n                callback(data.error);\n            } else {\n                callback(null, data);\n            }\n        });\n    };\n\n    resemble.outputSettings = setGlobalOutputSettings;\n    return resemble;\n});"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/SuiteReports.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.report;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Suite;\nimport com.intuit.karate.core.FeatureResult;\nimport com.intuit.karate.core.TagResults;\nimport com.intuit.karate.core.TimelineResults;\n\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic interface SuiteReports {\n\n\n\n    default Report featureReport(Suite suite, FeatureResult featureResult) {\n        Map<String, Object> map = featureResult.toKarateJson();\n\n        map.put(\"env\", suite.env);\n        return Report.template(\"karate-feature.html\")\n                .reportDir(suite.reportDir)\n                .reportFileName(featureResult.getFeature().getPackageQualifiedName() + \".html\")\n                .variable(\"results\", map)\n                .variables(ReportUtils.commonVars())\n                .build();\n    }\n\n    default Report tagsReport(Suite suite, TagResults tagResults) {\n        Map<String, Object>     map = tagResults.toKarateJson();\n\n        map.put(\"env\", suite.env);\n        return Report.template(\"karate-tags.html\")\n                .reportDir(suite.reportDir)\n                .variable(\"results\", map)\n                .build();\n    }\n\n    default Report timelineReport(Suite suite, TimelineResults timelineResults) {\n        return Report.template(\"karate-timeline.html\")\n                .reportDir(suite.reportDir)\n                .variable(\"results\", timelineResults.toKarateJson())\n                .build();\n    }\n\n    default Report summaryReport(Suite suite, Results results) {\n        Map<String, Object>     map = results.toKarateJson();\n\n        map.put(\"env\", suite.env);\n        return Report.template(\"karate-summary.html\")\n                .reportDir(suite.reportDir)\n                .variable(\"results\", map)\n                .variables(ReportUtils.commonVars())\n                .build();\n    }\n\n    public static final SuiteReports DEFAULT = new SuiteReports() {\n        // defaults\n    };\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/karate-feature.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, shrink-to-fit=no\"/>\n    <link rel=\"stylesheet\" href=\"res/bootstrap.min.css\" type=\"text/css\"/>\n    <link rel=\"stylesheet\" href=\"res/karate-report.css\" type=\"text/css\"/>\n    <link rel=\"shortcut icon\" href=\"favicon.ico\"/>\n    <script type=\"text/javascript\" src=\"res/jquery.min.js\"></script>\n    <script type=\"text/javascript\" src=\"res/jquery.tablesorter.min.js\"></script>\n    <script type=\"text/javascript\" src=\"res/jquery-ui.min.js\"></script>\n    <script type=\"text/javascript\" src=\"res/bootstrap.min.js\"></script>\n    <script type=\"text/javascript\" src=\"res/karate-report.js\"></script>\n    <script type=\"text/javascript\" src=\"res/Resemble.js\"></script>\n    <th:block th:insert=\"~{karate-posthog.html}\" th:with=\"karateEvent: 'report_feature'\"></th:block>\n    <title th:text=\"results.packageQualifiedName\"></title>\n  </head>\n  <body>\n    <div id=\"root-container\">\n      <div id=\"leftnav\">\n        <script ka:scope=\"global\">\n          var reportMeta = {\n            passedCount: results.passedCount,\n            failedCount: results.failedCount,\n            env: results.env,\n            reportType: 'Scenarios',\n            reportDate: results.resultDate\n          };\n          var repeat = n => Array.from({length: n}, (v, k) => k);\n        </script>\n        <div th:replace=\"~{karate-leftnav.html}\"></div>\n        <div class=\"nav-container\">\n          <div class=\"nav-item\" th:each=\"scenario: results.scenarioResults\" th:classappend=\"scenario.failed ? 'failed' : 'passed'\">\n            <a th:href=\"'#' + scenario.refId\"><span th:text=\"scenario.refId\"></span> <span th:text=\"scenario.name\"></span></a>\n          </div>\n        </div>\n      </div>\n      <div id=\"content\">\n        <div class=\"page-heading alert alert-primary\">\n          <a href=\"karate-summary.html\">Summary</a><span class=\"feature-label\">|</span>\n          <a href=\"karate-tags.html\">Tags</a><span class=\"feature-label\">|</span>\n          <span class=\"feature-label\">Feature:</span>\n          <span class=\"feature-path\" th:text=\"results.relativePath\"></span><span class=\"feature-label\">|</span>\n          <span class=\"feature-name\" th:text=\"results.name\"></span>\n          <span class=\"feature-description\" th:text=\"results.description\"></span>\n        </div>\n        <div th:each=\"scenario: results.scenarioResults\" class=\"scenario\">\n          <div class=\"scenario-heading\" th:id=\"scenario.refId\">\n            <div class=\"heading-container\">\n              <div class=\"scenario-tags\">\n                <a class=\"badge badge-primary\" href=\"karate-tags.html\" th:each=\"tag: scenario.tags\" th:text=\"tag\"></a>\n              </div>\n              <div class=\"scenario-keyword\">\n                Scenario: <span th:text=\"scenario.refId\"></span>\n                <span class=\"scenario-name\" th:text=\"scenario.name\"></span>\n                <span th:text=\"scenario.description\"></span>\n              </div>\n            </div>\n            <div class=\"scenario-time\" th:classappend=\"scenario.failed ? 'failed' : 'passed'\">\n              <span>ms:</span>&nbsp;<span th:text=\"Math.round(scenario.durationMillis)\"></span>\n            </div>\n          </div>\n          <div th:if=\"(scenario.stepResults.length > 0 && scenario.stepResults[0].step.background) || (scenario.stepResults.length > 1 && scenario.stepResults[1].step.background)\" class=\"step-row\">\n            <div class=\"step-container\" th:id=\"scenario.refId + 'bg'\">\n              <div class=\"step-ref bg-step\">&gt;&gt;</div>\n              <div class=\"step-cell passed\">Background:</div>\n            </div>\n            <div class=\"time-cell passed\"></div>\n          </div>\n          <div th:each=\"sr: scenario.stepResults\"\n               th:unless=\"sr.hidden\"\n               th:attr=\"'data-parent': sr.step.background ? scenario.refId + 'bg' : null\"\n               th:with=\"callDepth: 0, parentId: null, isBackground: sr.step.background\"\n               th:insert=\"~{karate-step.html}\">\n          </div>\n        </div>\n      </div>\n    </div>\n  </body>\n</html>\n\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/karate-leftnav.html",
    "content": "<div>\n  <div class=\"nav-logo\">\n    <a href=\"https://karatelabs.io\">\n      <img src=\"karate-labs-logo-ring.svg\" alt=\"Karate Labs\"/>\n    </a>\n    <div class=\"nav-count\">\n      <div id=\"nav-pass\" class=\"bg-success\" th:text=\"reportMeta.passedCount\">XX</div>\n      <div id=\"nav-fail\" class=\"bg-danger\" th:text=\"reportMeta.failedCount\">YY</div>\n    </div>\n  </div>\n  <div id=\"nav-env\">\n    <div th:text=\"reportMeta.env\">ZZZZ</div>\n  </div>\n  <div id=\"nav-type\">\n    <div th:text=\"reportMeta.reportType\">XXXX</div>\n  </div>\n  <div id=\"nav-date\">\n    <div th:text=\"reportMeta.reportDate\">YYYY</div>\n  </div>\n</div>\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/karate-posthog.html",
    "content": "<script th:inline=\"javascript\" th:if=\"karateTelemetry\">\n!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(\".\");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement(\"script\")).type=\"text/javascript\",p.async=!0,p.src=s.api_host+\"/static/array.js\",(r=t.getElementsByTagName(\"script\")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a=\"posthog\",u.people=u.people||[],u.toString=function(t){var e=\"posthog\";return\"posthog\"!==a&&(e+=\".\"+a),t||(e+=\" (stub)\"),e},u.people.toString=function(){return u.toString(1)+\".people (stub)\"},o=\"capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags\".split(\" \"),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);\nposthog.init('phc_M6fKk3dSX9HjglkqPSybauGas45NINb7EgsPGU8N9g1',{\n  api_host:'https://app.posthog.com',\n  persistence:'localStorage',\n  autocapture:false,\n  capture_pageview:false,\n  api_transport:'sendBeacon',\n  loaded: function(posthog) { \n    posthog.capture(/*[[${karateEvent}]]*/'',{distinct_id:/*[[${userUuid}]]*/'',karate_version:/*[[${karateVersion}]]*/'',karate_meta:/*[[${karateMeta}]]*/''});\n  }\n});\n</script>\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/karate-report.css",
    "content": "#root-container { display: flex; align-items: stretch; }\n#leftnav { min-width: 250px; max-width: 250px; min-height: 100vh; background: #000; color: #fff; position: fixed; }\n\n.nav-logo { display: flex; }\n.nav-logo img { width: 120px; padding: 0.3em; }\n.nav-count { margin-top: 0.5em; margin-right: 1em; width: 100%; }\n#nav-pass, #nav-fail { font-size: xx-large; text-align: center; width: 100%; display: block; margin-top: 0.1em; }\n#nav-type, #nav-date, #nav-env { margin: 0.5em 1em; font-size: large; text-align: center; }\n\n.nav-container { display: flex; flex-direction: column; overflow-y: scroll; height: 80vh; }\n.nav-item { margin: 0.5em 1em; padding: 0.2em; }\n.nav-item a { color: #000; display: block; }\n\n#content { padding: 0.5em; margin-left: 250px; width: 100%; }\n\n.features-table, .tags-table { border: 1px solid gray; }\n.tags-table th, .tags-table td { text-align: center; }\n.tags-table .feature-cell { text-align: left; }\n.features-table .passed a, .tags-table .passed a { color: #006600; }\n.features-table .failed a, .tags-table .failed a { color: #990000; }\n.features-table th, .tags-table th { border: 1px solid gray; background-color: #f2f2f2; }\n.features-table td, .tags-table td { border: 1px solid gray; }\n.num { text-align: right; }\n\n.page-heading  { margin-bottom: 0.5em; }\n.feature-label { padding: 0 0.5em; }\n.feature-path { font-weight: bold; font-style: italic; }\n.feature-name { font-weight: bold; }\n.feature-description { font-style: italic; padding-left: 1em; }\n\n.scenario { border: 1px solid gray; margin-bottom: 1em;  }\n.scenario-heading { border-bottom: 1px solid gray; margin-bottom: 2px; display: flex; }\n.heading-container { padding: 0.2em 0.5em; background-color: #fff3cd; width: 100%; margin: 0 0 0 2px; }\n.scenario-tags .badge { margin-right: 0.5em; }\n.scenario-name { font-weight: bold; padding-left: 0.2em; }\n.scenario-time { width: 90px; padding: 0.2em 0.6em; text-align: right; margin: 0 2px 0 2px }\n\n.step-row { display: flex; margin: 0 2px 2px 2px; }\n.step-container { width: 100%; display: flex; }\n.step-row a { width: 100%; }\na .step-container { width: 100%; color: #000; }\n.callarg-container { display: flex; }\n.step-indent { display: inline-block; width: 15px; background-color: #d9d9d9; margin-left: 2px; }\n.step-ref { width: 50px; text-align: right; padding: 0 0.2em 0 0.2em; }\n.bg-step { background-color: #d9d9d9; }\n.comment { background-color: #d9d9d9; }\na .step-ref { font-weight: bold; color: blue; }\n.step-cell { display: flex; width: 100%; padding: 0 0.2em 0 0.2em; margin-left: 2px; }\n.time-cell { width: 90px; margin-left: 2px; padding-right: 0.5em; text-align: right; }\n\n.scenario table { margin: 0.5em; }\n.scenario table td { border: 1px solid gray; padding: 0.1em 0.2em; }\n.preformatted { white-space: pre-wrap; font-family: monospace; padding: 0 0.3em 0.2em 0.3em; overflow: auto; font-size: 13px; }\n.embed img { width: 100%; }\n\n.passed { background-color: #bff2bf; }\n.failed { background-color: #ffcccc; }\n.skipped { background-color: #ffdd99; }\n\n/***********/\n/* diff ui */\n/***********/\n.diff-ui > div {\n    text-align: center;\n    vertical-align: top;\n}\n.diff-ui .hidden {\n    visibility: hidden;\n}\n.diff-ui-screenshots {\n    display: flex;\n    flex-direction: row;\n}\n.diff-ui-screenshots img {\n    cursor: zoom-in;\n}\n.diff-ui-screenshots > div {\n    width: 32%;\n    margin: 10px 1%;\n}\n.diff-ui .image-diff {\n    position: relative;\n}\n.diff-ui .img-container img {\n    user-select: none;\n}\n.diff-ui-inset {\n    margin: 20px;\n}\n.diff-ui .diff-results {\n    width: 100%;\n}\n.diff-ui .form-group {\n    text-align: left;\n}\n.diff-ui-controls {\n    margin: 20px auto;\n    max-width: 720px;\n}\n.diff-ui-controls select {\n    -webkit-appearance: none;\n    -moz-appearance: none;\n    background: transparent;\n    background-image: url(\"data:image/svg+xml;utf8,<svg fill='black' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>\");\n    background-repeat: no-repeat;\n    background-position-x: 100%;\n    background-position-y: 2px;\n    color: black;\n    border: 1px solid rgb(128, 128, 128);\n    border-radius: .2rem;\n    margin-right: 2rem;\n    padding-right: 2rem;\n}\n.diff-ui-controls .btn.rebase {\n    color: black;\n    background-color: #bff2bf;\n    border: 1px solid #add1ad;\n}\n.diff-ui-controls .btn.rebase:hover {\n    background-color: #add1ad;\n}\n.diff-ui-controls .btn-group button {\n    color: rgb(128, 128, 128);\n    background-color: lightgray;\n    border: 1px solid rgb(128, 128, 128);\n}\n.diff-ui-controls .btn-group button.active {\n    color: black;\n    background-color: white;\n}\n.diff-ui-controls button.active.yellow {\n    color: black;\n    background-color: rgb(255, 255, 0);\n}\n.diff-ui-controls button.active.pink {\n    color: white;\n    background-color: rgb(255, 0, 255);\n}\n/*********/\n/* modal */\n/*********/\n.diff-ui .modal {\n    width: 100%;\n    display: none;\n}\n.diff-ui .modal pre {\n    text-align: left;\n}\n.diff-ui .fit-content .modal-dialog {\n    max-width: fit-content;\n}\n.diff-ui .full-screen .modal-dialog {\n    max-width: 95%;\n}\n.diff-ui .full-screen img {\n    cursor: zoom-in;\n    width: auto;\n    max-width: 100%;\n}\n.diff-ui .full-screen .zoomed img {\n    cursor: zoom-out;\n    width: 100%\n}\n\n/*********************/\n/* comparison slider */\n/*********************/\n.diff-ui .compareContainer {\n    position: relative;\n    display: inline-block;\n}\n.diff-ui .zoomed .compareContainer {\n    position: relative;\n    display: block;\n}\n.diff-ui .compareContainer > div {\n    position: absolute;\n    top: 0;\n    left: 0;\n    bottom: 0;\n    right: 0;\n    background-size: cover;\n}\n.diff-ui .compareContainer .slider {\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    left: calc(50% - 4px);\n    width: 8px;\n    cursor: col-resize;\n    background-color: #4c4c4c;\n}\n.diff-ui .slider > div {\n    width: 1px;\n    height: 100%;\n    margin: 0 auto;\n    display: inline-block;\n    background-color: black;\n}\n\n/******************/\n/* ignored box UI */\n/******************/\n.diff-ui .ignored-box-ui {\n    position: absolute;\n    background-color: rgba(155, 155, 155, 0.5);\n    outline: 1px dashed #0000bb;\n    box-sizing: border-box;\n    z-index: 999;\n}\n.diff-ui .ignored-box-ui.active {\n    outline: 1px dashed yellow;\n}\n.diff-ui .ui-resizable-handle {\n    position: absolute;\n    font-size: 0.1px;\n    display: block;\n    -ms-touch-action: none;\n    touch-action: none;\n}\n.diff-ui .ui-resizable-n {\n    cursor: n-resize;\n    height: 7px;\n    width: 100%;\n    top: -5px;\n    left: 0;\n}\n.diff-ui .ui-resizable-s {\n    cursor: s-resize;\n    height: 7px;\n    width: 100%;\n    bottom: -5px;\n    left: 0;\n}\n.diff-ui .ui-resizable-e {\n    cursor: e-resize;\n    width: 7px;\n    right: -5px;\n    top: 0;\n    height: 100%;\n}\n.diff-ui .ui-resizable-w {\n    cursor: w-resize;\n    width: 7px;\n    left: -5px;\n    top: 0;\n    height: 100%;\n}\n.diff-ui .ui-resizable-se {\n    cursor: se-resize;\n    width: 12px;\n    height: 12px;\n    right: -5px;\n    bottom: -5px;\n}\n.diff-ui .ui-resizable-sw {\n    cursor: sw-resize;\n    width: 9px;\n    height: 9px;\n    left: -5px;\n    bottom: -5px;\n}\n.diff-ui .ui-resizable-nw {\n    cursor: nw-resize;\n    width: 9px;\n    height: 9px;\n    left: -5px;\n    top: -5px;\n}\n.diff-ui .ui-resizable-ne {\n    cursor: ne-resize;\n    width: 9px;\n    height: 9px;\n    right: -5px;\n    top: -5px;\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/karate-report.js",
    "content": "window.onload = function () {\n  const parentIds = {}\n\n  document.querySelectorAll('[data-parent]').forEach(child => {\n    parentIds[child.dataset.parent] = true\n    child.style.display = 'none'\n  })\n\n  document.querySelectorAll('div.step-container').forEach(parent => {\n    if (!parentIds[parent.id]) return\n    const link = document.createElement('a')\n    link.dataset.stepId = parent.id\n    link.href = 'javascript:void(0)'\n    parent.parentNode.insertBefore(link, parent)\n    link.appendChild(parent)\n  })\n\n  $('#content').on('click', 'a[data-step-id]', function () {\n    const parentId = $(this).data('step-id')\n    $(`[data-parent='${parentId}']`).toggle('fast')\n    $(`[data-parent='${parentId}'] [data-deferred]`).each((i, deferred) => {\n      const script = document.createElement('script')\n      script.type = 'text/javascript'\n      script.src = deferred.dataset.src\n      deferred.parentNode.replaceChild(script, deferred)\n    })\n  })\n\n  $(\"table.features-table\").tablesorter();\n  $(\"table.tags-table\").tablesorter();\n};\n\nfunction newDiffUI(targetElement, diffResult, diffConfig, onShowRebase, onShowConfig) {\n  diffConfig = diffConfig || {}\n  diffConfig.tolerances = diffConfig.tolerances || {}\n\n  // setup common vars\n  const $el = $('<div class=\"diff-ui\"></div>').appendTo($(targetElement).parent());\n  const ignoredBoxes = (diffConfig.ignoredBoxes || []).map((ignoredBox, i) => Object.assign({ id: i }, ignoredBox))\n  const tolerances = ['red', 'green', 'blue', 'alpha', 'minBrightness', 'maxBrightness'].reduce((tols, toleranceName) => {\n    if (diffConfig.tolerances[toleranceName] !== undefined) tols[toleranceName] = diffConfig.tolerances[optionName]\n    return tols\n  }, {})\n  let firstPaintComplete = false\n  let resembleControl\n\n  // add UI dynamically to avoid repeating html on-disk for each screenshot\n  createHtml()\n\n  // create baseline and latest images\n  $el.find('.baseline').append($('<img/>').attr('src', diffResult.baseline))\n  $el.find('.img-container').append($('<img class=\"hidden\"/>').attr('src', diffResult.baseline))\n  $el.find('.latest').append($('<img/>').attr('src', diffResult.latest))\n  $el.find('.compareContainer').prepend($('<img class=\"hidden\"/>').attr('src', diffResult.baseline))\n  $el.find('.baselineImgContainer').css('background-image', `url(${diffResult.baseline})`)\n  $el.find('.latestImgContainer').css('background-image', `url(${diffResult.latest})`)\n\n  // bind dropdown config options\n  $el.find('.ignore-config').val(diffConfig.ignore || 'less')\n  $el.find('.resemble-select').change(function() {\n    const val = this.value\n    if (val === 'nothing') {\n      resembleControl.ignoreNothing()\n    } else if (val === 'less') {\n      resembleControl.ignoreLess()\n    } else if (val === 'colors') {\n      resembleControl.ignoreColors()\n    } else if (val === 'antialiasing') {\n      resembleControl.ignoreAntialiasing()\n    } else if (val === 'alpha') {\n      resembleControl.ignoreAlpha()\n    }\n\n    if (['flat', 'movement', 'flatDifferenceIntensity', 'movementDifferenceIntensity', 'diffOnly'].includes(val)) {\n      resembleControl.outputSettings({ errorType: val }).repaint()\n    }\n  })\n\n  // bind button toggle config options\n  $el.find('.btn').click((e) => {\n    const $this = $(e.currentTarget)\n\n    $this.parent().find('button').removeClass('active')\n    $this.addClass('active')\n\n    if ($this.hasClass('pink')) {\n      resembleControl\n        .outputSettings({\n          errorColor: {\n            red: 255,\n            green: 0,\n            blue: 255\n          }\n        })\n        .repaint()\n    } else if ($this.hasClass('yellow')) {\n      resembleControl\n        .outputSettings({\n          errorColor: {\n            red: 255,\n            green: 255,\n            blue: 0\n          }\n        })\n        .repaint()\n    } else if ($this.hasClass('opaque')) {\n      resembleControl.outputSettings({ transparency: 1 }).repaint()\n    } else if ($this.hasClass('transparent')) {\n      resembleControl.outputSettings({ transparency: 0.3 }).repaint()\n    }\n  })\n\n  // bind ignored box actions\n  $el.on('click', '.ignored-box-ui:not(.active)', (e) => activateIgnoredBox(parseInt($(e.currentTarget).data('box-id'), 10)))\n  $el.on('click', '.ignored-box-ui.active', (e) => {\n    const $this = $(e.currentTarget)\n    // ignore click events that bubble up while we're resizing / dragging an ignore box\n    if ($this.hasClass('ui-resizable-resizing') || $this.hasClass('ui-draggable-dragging')) return\n    updateIgnoredBox(parseInt($(e.currentTarget).data('box-id'), 10))\n  })\n  $el.on('contextmenu', '.ignored-box-ui', removeIgnoredBox)\n  $el.on('click', '.removeIgnoredBox', removeIgnoredBox)\n\n  // bind 'show config' button click\n  $el.find('.showConfig').click(() => {\n    const diffOptions = {}\n\n    if (diffResult.engine !== diffResult.defaultEngine) {\n      diffOptions.engine = diffResult.engine\n    }\n\n    if (diffResult.failureThreshold !== diffResult.defaultFailureThreshold) {\n      diffOptions.failureThreshold = diffResult.failureThreshold\n    }\n\n    const ignoreOption = $el.find('.ignore-config').val()\n    if (ignoreOption !== 'less') diffOptions.ignore = ignoreOption\n\n    if (ignoreOption === diffConfig.ignore && Object.keys(tolerances).length > 0) {\n      diffOptions.tolerances = tolerances\n    }\n\n    if (diffConfig.ignoreAreasColoredWith) {\n      diffOptions.ignoreAreasColoredWith = diffConfig.ignoreAreasColoredWith\n    }\n\n    const boxes = ignoredBoxes.map((ignoredBox) => {\n      return {\n        top: ignoredBox.top,\n        left: ignoredBox.left,\n        bottom: ignoredBox.bottom,\n        right: ignoredBox.right\n      }\n    })\n\n    if (boxes.length) {\n      diffOptions.ignoredBoxes = boxes\n    }\n\n    const formatFn = onShowConfig || ((x) => x);\n\n    $el.find('.configModal pre').text(formatFn(JSON.stringify(diffOptions, null, 2), diffConfig))\n    $el.find('.configModal .copyConfig').addClass('btn-light').removeClass('btn-success')\n    $el.find('.configModal').modal({ keyboard: true })\n  })\n\n  // bind 'rebase' button click\n  $el.find('.rebase').click(() => {\n    if (!onShowRebase) return downloadLatest()\n\n    const txt = onShowRebase(diffConfig, downloadLatest)\n    if (!txt) return\n\n    $el.find('.rebaseModal pre').text(txt)\n    $el.find('.rebaseModal .copyCmd').addClass('btn-light').removeClass('btn-success')\n    $el.find('.rebaseModal').modal({ keyboard: true })\n  })\n\n  // bind 'copy' button click for modals\n  $el.on('click', '.copy', (e) => {\n    const $this = $(e.currentTarget)\n    const $tmpTextArea = $('<textarea/>')\n    $('body').append($tmpTextArea);\n    $tmpTextArea.val($this.closest('.modal').find('pre').text()).select()\n    try { document.execCommand('copy') } catch (err) {}\n    try { navigator.clipboard.writeText($tmpTextArea.val()) } catch (err) {}\n    $tmpTextArea.remove()\n    $this.removeClass('btn-light').addClass('btn-success')\n  })\n\n  // bind baseline / latest image click events\n  const $slider = $el.find('.compareModal .slider')\n  $el.find('.baseline img, .latest img').click(() => {\n    $slider.css('left', 'calc(50% - 4px)')\n    $el.find('.compareModal .modal-body').removeClass('zoomed')\n    $el.find('.compareModal').modal({ keyboard: true })\n  })\n  $el.find('.compareContainer').on('click', (e) => $(e.currentTarget).closest('.modal-body').toggleClass('zoomed'))\n\n  // bind comparison slider to mouse movement\n  const $baselineImgContainer = $el.find('.baselineImgContainer')\n  $el.find('.compareContainer').on('mousemove', function (e) {\n    const $this = $(this)\n    const maxWidth = $this.find('img:first')[0].clientWidth - 4\n    const offsetX = e.pageX - $this.offset().left\n\n    let sliderX = offsetX <= 4 ? 0 : offsetX - 4\n    if (sliderX > maxWidth) sliderX = maxWidth\n\n    $slider.css('left', sliderX)\n    $baselineImgContainer.css('right', maxWidth - sliderX)\n  })\n\n  // redraw ignore boxes on window resize\n  let windowResizeThrottle = { timer: null, isPending: false }\n  $(window).resize((e) => {\n    if (e.target !== window) return\n\n    if (windowResizeThrottle.timer) {\n      windowResizeThrottle.isPending = true\n      return\n    }\n\n    redrawIgnoreBoxes()\n\n    windowResizeThrottle.timer = setInterval(() => {\n      if (!windowResizeThrottle.isPending) {\n        clearInterval(windowResizeThrottle.timer)\n        windowResizeThrottle.timer = null\n        return\n      }\n\n      windowResizeThrottle.isPending = false\n      redrawIgnoreBoxes()\n    }, 50)\n  })\n\n  // wait for step contents animation to complete and then execute the diff\n  setTimeout(compareImg, 300)\n\n\n  // -- begin helper function definitions -- \\\\\n\n  function downloadLatest(filename) {\n    const format = diffResult.latest.substring(11, diffResult.latest.indexOf(';'))\n    const a = document.createElement('a');\n    a.href = diffResult.latest;\n    a.download = filename || ('latest.' + format);\n    document.body.appendChild(a);\n    a.click();\n    document.body.removeChild(a);\n  }\n\n  function addIgnoredBox(id, isActive) {\n    $el.find('.image-diff').append(`<div data-box-id=\"${id}\" class=\"ignored-box-ui ignored-box-ui-${id}\"></div>`)\n\n    updateIgnoredBox(id, isActive)\n\n    if (isActive) activateIgnoredBox(id)\n  }\n\n  function removeIgnoredBox(e) {\n    e.preventDefault()\n\n    const boxId = parseInt($(e.currentTarget).data('box-id'), 10)\n    $el.find(`.ignored-box-ui-${boxId}`).remove()\n\n    const boxIndex = ignoredBoxes.findIndex((ignoredBox) => ignoredBox.id === boxId)\n    ignoredBoxes.splice(boxIndex, 1)\n\n    resembleControl.outputSettings({ ignoredBoxes }).repaint()\n  }\n\n  function redrawIgnoreBoxes () {\n    $el.find('.ignored-box-ui').each(function () {\n      const boxId = parseInt($(this).data('box-id'), 10)\n      updateIgnoredBox(boxId, true)\n    })\n  }\n\n  function updateIgnoredBox(e, suppressRepaint) {\n    const boxId = $.isNumeric(e) ? e : parseInt($(e.currentTarget).data('box-id'), 10)\n    const ignoredBox = ignoredBoxes.find((ignoredBox) => ignoredBox.id === boxId)\n    const { scale, maxWidth, maxHeight } = calcScale()\n\n    ignoredBox.left = greaterOf(0, ignoredBox.left)\n    ignoredBox.top = greaterOf(0, ignoredBox.top)\n    ignoredBox.right = lessOf(maxWidth, ignoredBox.right)\n    ignoredBox.bottom = lessOf(maxHeight, ignoredBox.bottom)\n\n    // force sane values\n    if (ignoredBox.left >= ignoredBox.right) {\n      ignoredBox.right = lessOf(maxWidth, ignoredBox.left + 5)\n      ignoredBox.left = greaterOf(0, ignoredBox.right - 5)\n    }\n\n    if (ignoredBox.top >= ignoredBox.bottom) {\n      ignoredBox.bottom = lessOf(maxHeight, ignoredBox.top + 5)\n      ignoredBox.top = greaterOf(0, ignoredBox.bottom - 5)\n    }\n\n    $el.find(`.ignored-box-ui-${boxId}`).css({\n      width: (ignoredBox.right - ignoredBox.left) / scale,\n      height: (ignoredBox.bottom - ignoredBox.top) / scale,\n      top: ignoredBox.top / scale,\n      left: ignoredBox.left / scale\n    })\n\n    if (!suppressRepaint) {\n      resembleControl.outputSettings({ ignoredBoxes }).repaint()\n      activateIgnoredBox(null)\n    }\n  }\n\n  function activateIgnoredBox(id) {\n    $el.find('.ignored-box-ui.active').removeClass('active').each(function () {\n      $(this).resizable('destroy').draggable('destroy')\n    })\n\n    if (id === null) return\n\n    $el.find(`.ignored-box-ui-${id}`)\n      .addClass('active')\n      .resizable({\n        handles: 'all',\n        containment: 'parent',\n        minHeight: 10,\n        minWidth: 10,\n        resize(e, ui) {\n          const ignoredBox = ignoredBoxes.find((ignoredBox) => ignoredBox.id === id)\n          const { scale, maxWidth, maxHeight } = calcScale()\n\n          ignoredBox.left = greaterOf(0, ui.position.left * scale)\n          ignoredBox.top = greaterOf(0, ui.position.top * scale)\n          ignoredBox.right = lessOf(maxWidth, (ui.position.left + ui.size.width) * scale)\n          ignoredBox.bottom = lessOf(maxHeight, (ui.position.top + ui.size.height) * scale)\n        }\n      })\n      .draggable({\n        containment: 'parent',\n        drag(e, ui) {\n          const ignoredBox = ignoredBoxes.find((ignoredBox) => ignoredBox.id === id)\n          const { scale, maxWidth, maxHeight } = calcScale()\n          const width = (parseInt(ignoredBox.right, 10) - parseInt(ignoredBox.left, 10))\n          const height = (parseInt(ignoredBox.bottom, 10) - parseInt(ignoredBox.top, 10))\n\n          if (width === maxWidth) {\n            ignoredBox.left = 0\n            ignoredBox.right = width\n          } else {\n            const scaledWidth = width / scale\n            ignoredBox.left = greaterOf(0, ui.position.left * scale)\n            ignoredBox.right = lessOf(maxWidth, (ui.position.left + scaledWidth) * scale)\n          }\n\n          if (height === maxHeight) {\n            ignoredBox.top = 0\n            ignoredBox.bottom = height\n          } else {\n            const scaledHeight = height / scale\n            ignoredBox.top = greaterOf(0, ui.position.top * scale)\n            ignoredBox.bottom = lessOf(maxHeight, (ui.position.top + scaledHeight) * scale)\n          }\n        }\n      })\n  }\n\n  function lessOf(a, b) {\n    a = parseInt(a, 10)\n    b = parseInt(b, 10)\n    return a < b ? a : b\n  }\n\n  function greaterOf(a, b) {\n    a = parseInt(a, 10)\n    b = parseInt(b, 10)\n    return a > b ? a : b\n  }\n\n  function calcScale() {\n    const diffImg = $el.find('.img-container img')[0]\n    return {\n      scale: diffImg.naturalWidth / diffImg.clientWidth,\n      maxHeight: diffImg.naturalHeight,\n      maxWidth: diffImg.naturalWidth\n    }\n  }\n\n  function compareImg() {\n    const outputSettings = { ignoredBoxes, largeImageThreshold: 0}\n    if (diffConfig.ignoreAreasColoredWith) outputSettings.ignoreAreasColoredWith = diffConfig.ignoreAreasColoredWith\n\n    resembleControl = resemble(diffResult.baseline)\n        .compareTo(diffResult.latest)\n        .outputSettings(outputSettings)\n\n    switch ($el.find('.ignore-config').val()) {\n      case 'nothing':\n        resembleControl.ignoreNothing()\n        break\n      case 'antialiasing':\n        resembleControl.ignoreAntialiasing()\n        break\n      case 'colors':\n        resembleControl.ignoreColors()\n        break\n      case 'alpha':\n        resembleControl.ignoreAlpha()\n        break\n      default:\n        resembleControl.ignoreLess()\n    }\n\n    resembleControl.setupCustomTolerance(tolerances)\n\n    resembleControl = resembleControl.onComplete((data) => {\n      const outputImage = new Image()\n      if (!firstPaintComplete) {\n        firstPaintComplete = true\n        outputImage.onload = () => {\n          ignoredBoxes.forEach((ignoredBox) => addIgnoredBox(ignoredBox.id, false))\n        }\n      }\n\n      outputImage.src = data.getImageDataUrl()\n\n      $el.find('.img-container').html(outputImage)\n\n      // open full-size diff image in modal\n      $(outputImage).click(() => {\n        $el.find('.fullScreenModal .modal-body').html($('<img/>').attr('src', outputImage.src))\n        $el.find('.fullScreenModal .modal-body img').click((e) => $(e.currentTarget).closest('.modal-body').toggleClass('zoomed'))\n        $el.find('.fullScreenModal .modal-body').removeClass('zoomed')\n        $el.find('.fullScreenModal').modal({ keyboard: true })\n      })\n\n      // right-click adds new ignore box\n      $(outputImage).contextmenu(function (e) {\n        e.preventDefault()\n\n        const $this = $(this)\n        const { scale } = calcScale()\n        const offsetX = e.pageX - $this.offset().left - 50\n        const offsetY = e.pageY - $this.offset().top - 50\n        const boxId = ignoredBoxes.length\n\n        ignoredBoxes.push({\n          id: boxId,\n          left: offsetX * scale,\n          top: offsetY * scale,\n          right: (offsetX + 100) * scale,\n          bottom: (offsetY + 100) * scale\n        })\n\n        addIgnoredBox(boxId, true)\n      })\n\n      $el.find('.mismatch').text(data.misMatchPercentage)\n    })\n  }\n\n  function createHtml() {\n    $el.html(`\n    <div class=\"diff-ui-screenshots\">\n      <div class=\"baseline\">\n        <h3>Baseline</h3>\n      </div>\n      <div class=\"diff\">\n        <h3>Diff</h3>\n        <div class=\"image-diff\">\n          <div class=\"img-container\"></div>\n        </div>\n      </div>\n      <div class=\"latest\">\n        <h3>Latest</h3>\n      </div>\n    </div>\n    <div class=\"diff-ui-inset\">\n      <div class=\"diff-results\">\n        <div>\n          <strong>\n            The second image is <span class=\"mismatch\">0.00</span>% different compared to the first.\n          </strong>\n          <em>\n            ${diffResult.ssimMismatchPercentage === undefined ? '' : '(SSIM reported ' + diffResult.ssimMismatchPercentage.toFixed(2) + '% difference)'}\n          </em>\n        </div>\n      </div>\n      <div class=\"diff-ui-controls\">\n        <div class=\"form-row\">\n          <div class=\"form-group col-sm-4\">\n            <select class=\"form-control form-control-sm resemble-select ignore-config\">\n              <option value=\"less\">Ignore less</option>\n              <option value=\"nothing\">Ignore nothing</option>\n              <option value=\"colors\">Ignore colors</option>\n              <option value=\"antialiasing\">Ignore antialiasing</option>\n              <option value=\"alpha\">Ignore alpha</option>\n            </select>\n          </div>\n          <div class=\"form-group col-sm-4\">\n            <select class=\"form-control form-control-sm resemble-select\">\n              <option value=\"flat\" selected>Flat</option>\n              <option value=\"movement\">Movement</option>\n              <option value=\"flatDifferenceIntensity\">Flat with diff intensity</option>\n              <option value=\"movementDifferenceIntensity\">Movement with diff intensity</option>\n              <option value=\"diffOnly\">Diff portion from the input</option>\n            </select>\n          </div>\n          <div class=\"form-group col-sm-4\">\n            <button class=\"btn btn-sm btn-secondary form-control form-control-sm rebase\">Rebase</button>\n          </div>\n        </div>\n        <div class=\"form-row\">\n          <div class=\"form-group col-sm-4 btn-group\" role=\"group\">\n            <button class=\"btn btn-sm active pink\">Pink</button>\n            <button class=\"btn btn-sm light yellow\">Yellow</button>\n          </div>\n          <div class=\"form-group col-sm-4 btn-group\" role=\"group\">\n            <button class=\"btn btn-sm active opaque\">Opaque</button>\n            <button class=\"btn btn-sm light transparent\">Transparent</button>\n          </div>\n          <div class=\"form-group col-sm-4\">\n            <button class=\"btn btn-sm btn-info form-control form-control-sm showConfig\">Show config</button>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div class=\"modal modal-dialog-centered fit-content configModal\" tabindex=\"-1\" role=\"dialog\">\n      <div class=\"modal-dialog\" role=\"document\">\n        <div class=\"modal-content\">\n          <div class=\"modal-header\">\n            <h5 class=\"modal-title\">Image Comparison Config</h5>\n            <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n              <span aria-hidden=\"true\">&times;</span>\n            </button>\n          </div>\n          <div class=\"modal-body\"><pre></pre></div>\n          <div class=\"modal-footer\">\n            <button type=\"button\" class=\"btn btn-secondary\" data-dismiss=\"modal\">Close</button>\n            <button type=\"button\" class=\"btn btn-light copy\">Copy</button>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div class=\"modal modal-dialog-centered fit-content rebaseModal\" tabindex=\"-1\" role=\"dialog\">\n      <div class=\"modal-dialog\" role=\"document\">\n        <div class=\"modal-content\">\n          <div class=\"modal-header\">\n            <h5 class=\"modal-title\">Rebase</h5>\n            <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n              <span aria-hidden=\"true\">&times;</span>\n            </button>\n          </div>\n          <div class=\"modal-body\">\n            <pre></pre>\n          </div>\n          <div class=\"modal-footer\">\n            <button type=\"button\" class=\"btn btn-secondary\" data-dismiss=\"modal\">Close</button>\n            <button type=\"button\" class=\"btn btn-light copy\">Copy</button>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div class=\"modal modal-dialog-centered full-screen fullScreenModal\" tabindex=\"-1\" role=\"dialog\">\n      <div class=\"modal-dialog\" role=\"document\">\n        <div class=\"modal-content\">\n          <div class=\"modal-header\">\n            <h5 class=\"modal-title\">Diff</h5>\n            <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n              <span aria-hidden=\"true\">&times;</span>\n            </button>\n          </div>\n          <div class=\"modal-body\"></div>\n          <div class=\"modal-footer\">\n            <button type=\"button\" class=\"btn btn-secondary\" data-dismiss=\"modal\">Close</button>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div class=\"modal modal-dialog-centered full-screen compareModal\" tabindex=\"-1\" role=\"dialog\">\n      <div class=\"modal-dialog\" role=\"document\">\n        <div class=\"modal-content\">\n          <div class=\"modal-header\">\n            <h5 class=\"modal-title\">Diff</h5>\n            <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\">\n              <span aria-hidden=\"true\">&times;</span>\n            </button>\n          </div>\n          <div class=\"modal-body\">\n            <div class=\"compareContainer\">\n              <div class=\"latestImgContainer\"></div>\n              <div class=\"baselineImgContainer\"></div>\n              <div class=\"slider\"><div></div></div>\n            </div>\n          </div>\n          <div class=\"modal-footer\">\n            <button type=\"button\" class=\"btn btn-secondary\" data-dismiss=\"modal\">Close</button>\n          </div>\n        </div>\n      </div>\n    </div>`)\n  }\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/karate-step.html",
    "content": "<div>\n  <script ka:scope=\"local\">\n    _.stepId = (scenario.exampleIndex + 1) + '.' + sr.step.line;\n    if (parentId) {\n      _.stepId = parentId + '.' + _.stepId;\n    }\n    _.stepStatus = sr.result.status;    \n  </script>\n\n  <div th:each=\"comment: sr.step.comments\" class=\"step-row\" th:attr=\"'data-parent': parentId\">\n    <div class=\"step-container\">\n      <div class=\"step-ref passed\"></div>\n      <div th:each=\"indent: repeat(callDepth)\" class=\"step-indent\"></div>\n      <div class=\"step-cell comment\" th:text=\"comment\"></div>\n    </div>\n    <div class=\"time-cell comment\"></div>            \n  </div>\n\n  <div class=\"step-row\" th:attr=\"'data-parent': parentId\">\n    <div class=\"step-container\" th:id=\"stepId\">\n      <div class=\"step-ref\" th:classappend=\"isBackground ? 'bg-step' : stepStatus\" th:text=\"sr.step.line\"></div>\n      <div th:each=\"indent: repeat(callDepth)\" class=\"step-indent\"></div>\n      <div class=\"step-cell\" th:classappend=\"stepStatus\">\n        <span th:text=\"sr.step.prefix\"></span>&nbsp;<span th:text=\"sr.step.text\"></span>\n      </div>\n    </div>\n    <div class=\"time-cell\" th:classappend=\"stepStatus\" th:text=\"Math.round(sr.result.millis)\"></div>\n  </div>\n\n  <table th:if=\"sr.step.table\">\n    <tr th:each=\"tableRow: sr.step.table\" class=\"tr\">      \n      <td th:each=\"cell: tableRow.row\" class=\"td\" th:text=\"cell\"></td>\n    </tr>     \n  </table> \n  \n  <div class=\"preformatted\" th:attr=\"'data-parent': stepId\" th:if=\"sr.step.docString\" th:text=\"sr.step.docString\"></div>\n\n  <div th:each=\"embed: sr.embeds\" class=\"embed\" th:attr=\"'data-parent': stepId\" th:utext=\"embed.html\"></div>\n\n  <div class=\"preformatted\" th:attr=\"'data-parent': stepId\" th:if=\"sr.stepLog\" th:text=\"sr.stepLog\"></div>   \n\n  <div th:each=\"cr: sr.callResults\">\n    <script ka:scope=\"local\">\n      _.callStatus = cr.failed ? 'failed' : 'passed';\n      _.callName = cr.packageQualifiedName;\n      if (cr.loopIndex !== -1) {\n        _.callName = '[' + cr.loopIndex + ']' + _.callName;\n      }\n      _.callerId = stepId + '.' + (cr.loopIndex + 1) + '.call';\n    </script>    \n    <div class=\"step-row\" th:attr=\"'data-parent': parentId\">\n      <div class=\"step-container\" th:id=\"callerId\">\n        <div class=\"step-ref\" th:classappend=\"isBackground ? 'bg-step' : callStatus\">&gt;&gt;</div>\n        <div th:each=\"indent: repeat(cr.callDepth)\" class=\"step-indent\"></div>\n        <div class=\"step-cell\" th:classappend=\"callStatus\" th:text=\"callName\"></div>\n      </div>\n      <div class=\"time-cell\" th:classappend=\"callStatus\" th:text=\"Math.round(cr.durationMillis)\"></div>\n    </div>\n    <div th:each=\"scenario: cr.scenarioResults\">\n      <div th:each=\"sr: scenario.stepResults\"\n           th:unless=\"sr.hidden\"\n           th:with=\"callDepth: cr.callDepth, parentId: callerId, isBackground: isBackground\"\n           th:insert=\"~{karate-step.html}\">\n      </div>\n    </div>    \n  </div>\n\n</div>\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/karate-summary.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, shrink-to-fit=no\"/>\n    <link rel=\"stylesheet\" href=\"res/bootstrap.min.css\" type=\"text/css\"/>\n    <link rel=\"stylesheet\" href=\"res/karate-report.css\" type=\"text/css\"/>\n    <link rel=\"shortcut icon\" href=\"favicon.ico\"/>\n    <script type=\"text/javascript\" src=\"res/jquery.min.js\"></script>\n    <script type=\"text/javascript\" src=\"res/jquery.tablesorter.min.js\"></script>\n    <script type=\"text/javascript\" src=\"res/bootstrap.min.js\"></script>\n    <script type=\"text/javascript\" src=\"res/karate-report.js\"></script>    \n    <title>Karate Summary Report</title>\n  </head>\n  <body>\n    <div id=\"root-container\">\n      <div id=\"leftnav\">\n        <script ka:scope=\"global\">\n          var reportMeta = {\n            passedCount: results.featuresPassed,\n            failedCount: results.featuresFailed,\n            env: results.env,\n            reportType: 'Features',\n            reportDate: results.resultDate\n          };\n        </script>\n        <div th:replace=\"~{karate-leftnav.html}\"></div>\n      </div>\n      <div id=\"content\">\n        <div class=\"page-heading alert alert-primary\">\n          <a href=\"karate-tags.html\">Tags</a><span class=\"feature-label\">|</span>\n          <a href=\"karate-timeline.html\">Timeline</a>\n        </div>\n        <table class=\"features-table table table-sm\">\n          <thead>\n            <tr>\n              <th>Feature</th>\n              <th>Title</th>\n              <th class=\"num\">Passed</th>\n              <th class=\"num\">Failed</th>\n              <th class=\"num\">Scenarios</th>\n              <th class=\"num\">Time (ms)</th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr th:each=\"fr: results.featureSummary\" th:classappend=\"fr.failed ? 'failed' : 'passed'\">\n              <td>\n                <a th:href=\"fr.packageQualifiedName + '.html'\" th:text=\"fr.relativePath\"></a>\n              </td>\n              <td th:title=\"fr.description\" th:text=\"fr.name\"></td>\n              <td class=\"num\" th:text=\"fr.passedCount\"></td>\n              <td class=\"num\" th:text=\"fr.failedCount\"></td>\n              <td class=\"num\" th:text=\"fr.scenarioCount\"></td>\n              <td class=\"num\" th:text=\"Math.round(fr.durationMillis)\"></td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </div>\n  </body>\n</html>\n\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/karate-tags.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, shrink-to-fit=no\"/>\n    <link rel=\"stylesheet\" href=\"res/bootstrap.min.css\" type=\"text/css\"/>\n    <link rel=\"stylesheet\" href=\"res/karate-report.css\" type=\"text/css\"/>\n    <link rel=\"shortcut icon\" href=\"favicon.ico\"/>\n    <script type=\"text/javascript\" src=\"res/jquery.min.js\"></script>\n    <script type=\"text/javascript\" src=\"res/jquery.tablesorter.min.js\"></script>\n    <script type=\"text/javascript\" src=\"res/bootstrap.min.js\"></script>\n    <script type=\"text/javascript\" src=\"res/karate-report.js\"></script>\n    <title>Karate Tags Report</title>\n  </head>\n  <body>\n    <div id=\"root-container\">\n      <div id=\"leftnav\">\n        <script ka:scope=\"global\">\n          var reportMeta = {\n            passedCount: results.tagKeysPassed,\n            failedCount: results.tagKeysFailed,\n            env: results.env,\n            reportType: 'Tags',\n            reportDate: results.resultDate\n          };\n        </script>\n        <div th:replace=\"~{karate-leftnav.html}\"></div>\n      </div>\n      <div id=\"content\">\n        <div class=\"page-heading alert alert-primary\">\n          <a href=\"karate-summary.html\">Summary</a>\n        </div>\n        <table class=\"tags-table table table-sm \">\n          <thead>\n            <tr>\n              <th class=\"feature-cell\">Feature</th>\n              <th th:each=\"tagKey: results.tagKeys\" th:text=\"tagKey\" th:classapend=\"results.failedTagKeys.contains(tagKey) ? 'failed' : 'passed'\"></th>\n            </tr>\n          </thead>\n          <tbody>\n            <tr th:each=\"ft: results.featureTags\" th:classappend=\"ft.featureSummary.failed ? 'failed' : 'passed'\">\n              <td class=\"feature-cell\">\n                <a th:href=\"ft.featureSummary.packageQualifiedName + '.html'\" th:text=\"ft.featureSummary.relativePath\"></a>\n              </td>\n              <td th:each=\"tagKey: results.tagKeys\"\n                  th:text=\"ft.tagKeys.contains(tagKey) ? 'X' : ''\"\n                  th:classapend=\"ft.failedTagKeys.contains(tagKey) ? 'failed' : 'passed'\"></td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </div>\n  </body>\n</html>\n\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/report/karate-timeline.html",
    "content": "<!DOCTYPE HTML>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <title>Karate Timeline</title>\n    <style>\n      body, html { font-family: arial, sans-serif; font-size: 11pt; }\n      #visualization { box-sizing: border-box; width: 100%; height: 300px; }\n      .vis-item.failed { background-color: #F2928C; }\n      .page-heading { padding: 0.5em; margin-bottom: 0.5em; background-color: lightcyan; border: 1px solid gray; }\n    </style>\n    <script src=\"res/vis.min.js\"></script>\n    <link href=\"res/vis.min.css\" rel=\"stylesheet\" type=\"text/css\" />\n  </head>\n  <body>\n    <div class=\"page-heading\">\n      <a href=\"karate-summary.html\">Summary</a>\n    </div>     \n    <div id=\"visualization\"></div>\n    <script th:utext=\"results.data\">\n      var groups = [];\n      var items = [];\n    </script>\n    <script>\n      var container = document.getElementById('visualization');\n      var timeline = new vis.Timeline(container);\n      timeline.setOptions({groupOrder: 'content'});\n      timeline.setGroups(new vis.DataSet(groups));\n      timeline.setItems(new vis.DataSet(items));\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/resource/FileResource.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.resource;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.net.URI;\n\n/**\n *\n * @author pthomas3\n */\npublic class FileResource implements Resource {\n\n    private final File file;\n    private final boolean classPath;\n    private final String relativePath;\n\n    public FileResource(File file) {\n        this(file, false);\n    }\n\n    private FileResource(File file, boolean classpath) {\n        this(file, classpath, file.getPath());\n    }\n\n    public FileResource(File file, boolean classPath, String relativePath) {\n        this.file = file;\n        this.classPath = classPath;\n        this.relativePath = relativePath.replace('\\\\', '/');\n    }\n\n    @Override\n    public boolean isFile() {\n        return true;\n    }\n\n    @Override\n    public File getFile() {\n        return file;\n    }\n\n    @Override\n    public URI getUri() {\n        return file.toPath().toUri();\n    }\n\n    @Override\n    public boolean isClassPath() {\n        return classPath;\n    }\n\n    @Override\n    public String getRelativePath() {\n        return relativePath;\n    }\n\n    @Override\n    public Resource resolve(String path) {\n        int pos = relativePath.lastIndexOf('/');\n        String childPath;\n        if (pos == -1) {\n            childPath = path;\n        } else {\n            childPath = relativePath.substring(0, pos) + File.separator + path;\n        }\n        File child = new File(file.getAbsoluteFile().getParent() + File.separator + path);\n        return new FileResource(child, classPath, childPath);\n    }\n\n    @Override\n    public InputStream getStream() {\n        try {\n            return new FileInputStream(file);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        return file.equals(obj);\n    }\n\n    @Override\n    public int hashCode() {\n        return file.hashCode();\n    }\n\n    @Override\n    public String toString() {\n        return getPrefixedPath();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/resource/JarResource.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.resource;\n\nimport com.intuit.karate.FileUtils;\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.net.URI;\n\n/**\n *\n * @author pthomas3\n */\npublic class JarResource implements Resource {\n\n    private final byte[] bytes;\n    private final String relativePath;\n    private final URI uri;\n\n    public JarResource(byte[] bytes, String relativePath, URI uri) {\n        this.bytes = bytes;\n        this.relativePath = relativePath;\n        this.uri = uri;\n    }\n\n    @Override\n    public boolean isFile() {\n        return false;\n    }\n\n    @Override\n    public boolean isClassPath() {\n        return true;\n    }\n\n    @Override\n    public File getFile() {\n        return null;\n    }\n\n    @Override\n    public URI getUri() {\n        return uri;\n    }        \n\n    @Override\n    public String getRelativePath() {\n        return relativePath;\n    }      \n    \n    @Override\n    public Resource resolve(String path) {\n        int pos = relativePath.lastIndexOf('/');\n        String parentPath = pos == -1 ? \"\" : relativePath.substring(0, pos);\n        return ResourceUtils.getResource(FileUtils.WORKING_DIR, \"classpath:\" + parentPath + \"/\" + path);\n    }    \n\n    @Override\n    public InputStream getStream() {\n        return new ByteArrayInputStream(bytes);\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        return relativePath.equals(obj);\n    }        \n\n    @Override\n    public int hashCode() {\n        return relativePath.hashCode();\n    }        \n    \n    @Override\n    public String toString() {\n        return getPrefixedPath();\n    }    \n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/resource/MemoryResource.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.resource;\n\nimport com.intuit.karate.FileUtils;\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.net.URI;\n\n/**\n *\n * @author pthomas3\n */\npublic class MemoryResource implements Resource {\n\n    private final File file;\n    private final byte[] bytes;\n\n    public MemoryResource(File file, String text) {\n        this(file, FileUtils.toBytes(text));\n    }\n\n    public MemoryResource(File file, byte[] bytes) {\n        this.file = file;\n        this.bytes = bytes;\n    }\n\n    @Override\n    public boolean isFile() {\n        return true;\n    }\n\n    @Override\n    public boolean isClassPath() {\n        return false;\n    }\n\n    @Override\n    public File getFile() {\n        return file;\n    }\n\n    @Override\n    public URI getUri() {\n        return file.toURI();\n    }\n\n    @Override\n    public String getRelativePath() {\n        return file.getPath().replace('\\\\', '/');\n    }\n\n    @Override\n    public Resource resolve(String path) {\n        return new FileResource(new File(file.getParent() + File.separator + path));\n    }\n\n    @Override\n    public InputStream getStream() {\n        return new ByteArrayInputStream(bytes);\n    }\n\n    @Override\n    public String toString() {\n        return getPrefixedPath();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/resource/Resource.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.resource;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.net.URI;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Resource {\n\n    public static final String CLASSPATH_COLON = \"classpath:\";\n    public static final String FILE_COLON = \"file:\";\n    public static final String THIS_COLON = \"this:\"; // used only in html templating\n\n    boolean isFile();\n\n    boolean isClassPath();\n\n    File getFile();\n\n    URI getUri();\n\n    String getRelativePath();\n\n    Resource resolve(String path);\n\n    default String getPrefixedPath() {\n        return isClassPath() ? CLASSPATH_COLON + getRelativePath() : getRelativePath();\n    }\n\n    default String getPrefixedParentPath() {\n        return ResourceUtils.getParentPath(getPrefixedPath());\n    }\n\n    default String getPackageQualifiedName() {\n        String path = getRelativePath();\n        if (path.endsWith(\".feature\")) {\n            path = path.substring(0, path.length() - 8);\n        }\n        if (path.charAt(0) == '/') {\n            path = path.substring(1);\n        }\n        return path.replace('/', '.').replaceAll(\"\\\\.[.]+\", \".\");\n    }\n\n    default String getFileNameWithoutExtension() {\n        String path = getRelativePath();\n        int pos = path.lastIndexOf('.');\n        if (pos == -1) {\n            return path;\n        } else {\n            return path.substring(0, pos);\n        }\n    }\n\n    InputStream getStream();\n\n    default long getLastModified() {\n        if (isFile()) {\n            return getFile().lastModified();\n        }\n        try {\n            return getUri().toURL().openConnection().getLastModified();\n        } catch (Exception e) {\n            return 0;\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/resource/ResourceResolver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.resource;\n\nimport com.intuit.karate.FileUtils;\n\n/**\n *\n * @author pthomas3\n */\npublic class ResourceResolver {\n\n    public final boolean classpath;\n    public final String root;\n\n    private static final String EMPTY = \"\";\n    private static final String SLASH = \"/\";\n\n    public ResourceResolver(String root) {\n        if (root == null) {\n            root = EMPTY;\n        }\n        classpath = root.startsWith(Resource.CLASSPATH_COLON);\n        root = ResourceUtils.removePrefix(root);\n        if (!root.isEmpty() && !root.endsWith(SLASH)) {\n            root = root + SLASH;\n        }\n        this.root = root;\n    }\n\n    public Resource resolve(String path) {\n        return resolve(null, path);\n    }\n\n    public Resource resolve(String caller, String path) {\n        if (path.startsWith(Resource.CLASSPATH_COLON)) {\n            return get(path);\n        }\n        String prefix = classpath ? Resource.CLASSPATH_COLON + root : root;\n        if (path.startsWith(Resource.THIS_COLON) && caller != null) {\n            return get(prefix + ResourceUtils.getParentPath(caller) + path.substring(5));\n        }\n        return get(prefix + (path.charAt(0) == '/' ? path.substring(1) : path));\n    }\n\n    private Resource get(String path) {\n        return ResourceUtils.getResource(FileUtils.WORKING_DIR, path);\n    }\n\n    @Override\n    public String toString() {\n        return classpath ? Resource.CLASSPATH_COLON + root : root;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/resource/ResourceUtils.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.resource;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.FeatureCall;\nimport io.github.classgraph.ClassGraph;\nimport io.github.classgraph.ResourceList;\nimport io.github.classgraph.ScanResult;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ResourceUtils {\n    \n    private static final Logger logger = LoggerFactory.getLogger(ResourceUtils.class);\n    \n    private ResourceUtils() {\n        // only static methods\n    }\n    \n    public static List<FeatureCall> findFeatureFiles(File workingDir, List<String> paths, String scenarioName) {\n        List<FeatureCall> features = new ArrayList();\n        if (paths == null || paths.isEmpty()) {\n            return features;\n        }\n        if (paths.size() == 1) { // TODO handle multiple paths with tag or line suffixes\n            String path = paths.get(0);\n            int pos = path.indexOf(\".feature:\");\n            int line;\n            String callTag;\n            if (pos != -1) { // line number has been appended\n                line = Integer.valueOf(path.substring(pos + 9));\n                path = path.substring(0, pos + 8);\n            } else {\n                line = -1;\n            }\n            pos = path.indexOf('@');\n            if (pos != -1) { // call by tag\n                callTag = path.substring(pos);\n                path = path.substring(0, pos);\n            } else {\n                callTag = null;\n            }\n            if (path.endsWith(\".feature\")) {\n                Resource resource = getResource(workingDir, path);\n                Feature feature = Feature.read(resource);\n                features.add(new FeatureCall(feature, callTag, line, scenarioName));\n                return features;\n            }\n        }\n        Collection<Resource> resources = findResourcesByExtension(workingDir, \"feature\", paths);\n        for (Resource resource : resources) {\n            features.add(new FeatureCall(Feature.read(resource), null, -1, scenarioName)); // TODO smart find of scenario name\n        }\n        return features;\n    }\n    \n    private static final ScanResult SCAN_RESULT = new ClassGraph().acceptPaths(\"/\").scan(1);\n    \n    public static Resource getResource(File workingDir, String path) {\n        if (path.startsWith(Resource.CLASSPATH_COLON)) {\n            path = removePrefix(path);\n            File file = classPathToFile(path);\n            if (file != null) {\n                return new FileResource(file, true, path);\n            }\n            List<Resource> resources = new ArrayList<>();\n            synchronized (SCAN_RESULT) {\n                ResourceList rl = SCAN_RESULT.getResourcesWithPath(path);\n                if (rl == null) {\n                    rl = ResourceList.emptyList();\n                }\n                rl.forEachByteArrayIgnoringIOException((res, bytes) -> {\n                    URI uri = res.getURI();\n                    if (\"file\".equals(uri.getScheme())) {\n                        File found = Paths.get(uri).toFile();\n                        resources.add(new FileResource(found, true, res.getPath()));\n                    } else {\n                        resources.add(new JarResource(bytes, res.getPath(), uri));\n                    }\n                });\n            }\n            if (resources.isEmpty()) {\n                throw new RuntimeException(\"not found: \" + path);\n            }\n            return resources.get(0);\n        } else {\n            path = path.replace('\\\\', '/'); // windows fix\n            File file = new File(removePrefix(path));\n            if (!file.exists()) {\n                throw new RuntimeException(\"not found: \" + path);\n            }\n            Path relativePath = workingDir.toPath().relativize(file.getAbsoluteFile().toPath());\n            return new FileResource(file, false, relativePath.toString());\n        }\n    }\n    \n    public static Collection<Resource> findResourcesByExtension(File workingDir, String extension, String path) {\n        return findResourcesByExtension(workingDir, extension, Collections.singletonList(path));\n    }\n    \n    public static List<Resource> findResourcesByExtension(File workingDir, String extension, List<String> paths) {\n        List<Resource> results = new ArrayList();\n        List<File> fileRoots = new ArrayList();\n        List<String> pathRoots = new ArrayList();\n        for (String path : paths) {\n            if (path.endsWith(\".\" + extension)) {\n                results.add(getResource(workingDir, path));\n            } else if (path.startsWith(Resource.CLASSPATH_COLON)) {\n                pathRoots.add(removePrefix(path));\n            } else {\n                fileRoots.add(new File(removePrefix(path)));\n            }\n        }\n        if (!fileRoots.isEmpty()) {\n            results.addAll(findFilesByExtension(workingDir, extension, fileRoots));\n        } else if (results.isEmpty() && !pathRoots.isEmpty()) {\n            String[] searchPaths = pathRoots.toArray(new String[pathRoots.size()]);\n            try (ScanResult scanResult = new ClassGraph().acceptPaths(searchPaths).scan(1)) {\n                ResourceList rl = scanResult.getResourcesWithExtension(extension);\n                rl.forEachByteArrayIgnoringIOException((res, bytes) -> {\n                    URI uri = res.getURI();\n                    if (\"file\".equals(uri.getScheme())) {\n                        File file = Paths.get(uri).toFile();\n                        results.add(new FileResource(file, true, res.getPath()));\n                    } else {\n                        results.add(new JarResource(bytes, res.getPath(), uri));\n                    }\n                });\n            }\n        }\n        return results;\n    }\n    \n    private static List<Resource> findFilesByExtension(File workingDir, String extension, List<File> files) {\n        List<File> results = new ArrayList();\n        for (File base : files) {\n            Path searchPath = base.toPath();\n            Stream<Path> stream;\n            try {\n                stream = Files.walk(searchPath);\n                for (Iterator<Path> paths = stream.iterator(); paths.hasNext();) {\n                    Path path = paths.next();\n                    String fileName = path.getFileName().toString();\n                    if (fileName.endsWith(\".\" + extension)) {\n                        results.add(path.toFile());\n                    }\n                }\n            } catch (IOException e) { // NoSuchFileException  \n                logger.trace(\"unable to walk path: {} - {}\", searchPath, e.getMessage());\n            }\n        }\n        return results.stream()\n                .map(f -> {\n                    Path relativePath = workingDir.toPath().relativize(f.getAbsoluteFile().toPath());\n                    return new FileResource(f, false, relativePath.toString());\n                })\n                .collect(Collectors.toList());\n    }\n    \n    public static File getFileRelativeTo(Class clazz, String path) {\n        Path dirPath = getPathContaining(clazz);\n        File file = new File(dirPath + File.separator + path);\n        if (file.exists()) {\n            return file;\n        }\n        try {\n            URL relativePath = clazz.getClassLoader().getResource(toPathFromClassPathRoot(clazz) + File.separator + path);\n            return Paths.get(relativePath.toURI()).toFile();\n        } catch (Exception e) {\n            throw new RuntimeException(\"cannot find \" + path + \" relative to \" + clazz + \", \" + e.getMessage());\n        }\n    }\n    \n    public static Path getPathContaining(Class clazz) {\n        String relative = toPathFromClassPathRoot(clazz);\n        URL url = clazz.getClassLoader().getResource(relative);\n        try {\n            return Paths.get(url.toURI());\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n    \n    public static File getDirContaining(Class clazz) {\n        Path path = getPathContaining(clazz);\n        return path.toFile();\n    }\n    \n    public static String toPathFromClassPathRoot(Class clazz) {\n        Package p = clazz.getPackage();\n        String relative = \"\";\n        if (p != null) {\n            relative = p.getName().replace('.', '/');\n        }\n        return relative;\n    }\n    \n    protected static String removePrefix(String text) {\n        if (text.startsWith(Resource.CLASSPATH_COLON) || text.startsWith(Resource.FILE_COLON)) {\n            return text.substring(text.indexOf(':') + 1);\n        } else {\n            return text;\n        }\n    }\n    \n    public static String getParentPath(String relativePath) {\n        int pos = relativePath.lastIndexOf('/');\n        return pos == -1 ? \"\" : relativePath.substring(0, pos + 1);        \n    }\n    \n    private static final ClassLoader CLASS_LOADER = ResourceUtils.class.getClassLoader();\n    \n    public static InputStream classPathResourceToStream(String path) {\n        return CLASS_LOADER.getResourceAsStream(path);\n    }\n    \n    public static String classPathResourceToString(String path) {\n        return FileUtils.toString(classPathResourceToStream(path));\n    }\n    \n    public static File classPathToFile(String path) {\n        URL url = CLASS_LOADER.getResource(path);\n        if (url == null || !\"file\".equals(url.getProtocol())) {\n            return null;\n        }\n        try {\n            return Paths.get(url.toURI()).toFile();\n        } catch (URISyntaxException e) {\n            return null;\n        }\n    }\n    \n    public static File classPathOrFile(String path) {\n        File temp = classPathToFile(path);\n        if (temp != null) {\n            return temp;\n        }\n        temp = new File(path);\n        return temp.exists() ? temp : null;\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/shell/Command.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.shell;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Http;\nimport com.intuit.karate.LogAppender;\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.http.Response;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.File;\nimport java.net.InetSocketAddress;\nimport java.net.ServerSocket;\nimport java.net.SocketAddress;\nimport java.nio.channels.SocketChannel;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class Command extends Thread {\n\n    protected static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(Command.class);\n\n    private final boolean useLineFeed;\n    private final File workingDir;\n    private final String uniqueName;\n    private final Logger logger;\n    private final String[] args;\n    private final List argList; // just for logging\n    private final boolean sharedAppender;\n    private final LogAppender appender;\n\n    private Map<String, String> environment;\n    private Consumer<String> listener;\n    private Consumer<String> errorListener;\n    private boolean redirectErrorStream = true;\n    private Console sysOut;\n    private Console sysErr;\n    private Process process;\n    private int exitCode = -1;\n    private Exception failureReason;\n\n    private int pollAttempts = 30;\n    private int pollInterval = 250;\n\n    public void setPollAttempts(int pollAttempts) {\n        this.pollAttempts = pollAttempts;\n    }\n\n    public void setPollInterval(int pollInterval) {\n        this.pollInterval = pollInterval;\n    }\n\n    public synchronized boolean isFailed() {\n        return failureReason != null;\n    }\n\n    public Exception getFailureReason() {\n        return failureReason;\n    }\n\n    public void setEnvironment(Map<String, String> environment) {\n        this.environment = environment;\n    }\n\n    public void setListener(Consumer<String> listener) {\n        this.listener = listener;\n    }\n\n    public void setErrorListener(Consumer<String> errorListener) {\n        this.errorListener = errorListener;\n    }\n\n    public void setRedirectErrorStream(boolean redirectErrorStream) {\n        this.redirectErrorStream = redirectErrorStream;\n    }\n\n    public String getSysOut() {\n        return sysOut == null ? null : sysOut.getBuffer();\n    }\n\n    public String getSysErr() {\n        return sysErr == null ? null : sysErr.getBuffer();\n    }\n\n    public static String exec(boolean useLineFeed, File workingDir, String... args) {\n        Command command = new Command(useLineFeed, workingDir, args);\n        command.start();\n        command.waitSync();\n        return command.getSysOut();\n    }\n\n    private static final Pattern CLI_ARG = Pattern.compile(\"'([^']*)'[^\\\\S]|\\\"([^\\\"]*)\\\"[^\\\\S]|(\\\\S+)\");\n\n    public static String[] tokenize(String command) {\n        List<String> args = new ArrayList();\n        Matcher m = CLI_ARG.matcher(command + \" \");\n        while (m.find()) {\n            if (m.group(1) != null) {\n                args.add(m.group(1));\n            } else if (m.group(2) != null) {\n                args.add(m.group(2));\n            } else {\n                args.add(m.group(3));\n            }\n        }\n        return args.toArray(new String[args.size()]);\n    }\n\n    public static String execLine(File workingDir, String command) {\n        return exec(false, workingDir, tokenize(command));\n    }\n\n    public static String[] prefixShellArgs(String[] args) {\n        List<String> list = new ArrayList();\n        switch (FileUtils.getOsType()) {\n            case WINDOWS:\n                list.add(\"cmd\");\n                list.add(\"/c\");\n                break;\n            default:\n                list.add(\"sh\");\n                list.add(\"-c\");\n        }\n        list.add(StringUtils.join(args, ' '));\n        return list.toArray(new String[list.size()]);\n    }\n\n    private static final Set<Integer> PORTS_IN_USE = ConcurrentHashMap.newKeySet();\n\n    public static synchronized int getFreePort(int preferred) {\n        if (preferred != 0 && PORTS_IN_USE.contains(preferred)) {\n            LOGGER.trace(\"preferred port {} in use (karate), will attempt to find free port ...\", preferred);\n            preferred = 0;\n        }\n        try {\n            ServerSocket s = new ServerSocket(preferred);\n            int port = s.getLocalPort();\n            LOGGER.debug(\"found / verified free local port: {}\", port);\n            s.close();\n            PORTS_IN_USE.add(port);\n            return port;\n        } catch (Exception e) {\n            if (preferred > 0) {\n                LOGGER.trace(\"preferred port {} in use (system), re-trying ...\", preferred);\n                PORTS_IN_USE.add(preferred);\n                return getFreePort(0);\n            }\n            LOGGER.error(\"failed to find free port: {}\", e.getMessage());\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static void sleep(int millis) {\n        try {\n            LOGGER.trace(\"sleeping for millis: {}\", millis);\n            Thread.sleep(millis);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public boolean waitForPort(String host, int port) {\n        int attempts = 0;\n        do {\n            SocketAddress address = new InetSocketAddress(host, port);\n            try {\n                if (isFailed()) {\n                    throw failureReason;\n                }\n                logger.debug(\"poll attempt #{} for port to be ready - {}:{}\", attempts, host, port);\n                SocketChannel sock = SocketChannel.open(address);\n                sock.close();\n                return true;\n            } catch (Exception e) {\n                sleep(pollInterval);\n            }\n        } while (attempts++ < pollAttempts);\n        return false;\n    }\n\n    private static final int SLEEP_TIME = 2000;\n    private static final int POLL_ATTEMPTS_MAX = 30;\n    \n    public static boolean waitForHttp(String url) {\n        return waitForHttp(url, r -> r.getStatus() == 200);\n    }\n\n    public static boolean waitForHttp(String url, Predicate<Response> condition) {\n        int attempts = 0;\n        long startTime = System.currentTimeMillis();\n        Http http = Http.to(url);\n        do {\n            if (attempts > 0) {\n                LOGGER.debug(\"attempt #{} waiting for http to be ready at: {}\", attempts, url);\n            }\n            try {\n                Response response = http.get();\n                if (condition.test(response)) {\n                    long elapsedTime = System.currentTimeMillis() - startTime;\n                    LOGGER.debug(\"ready to accept http connections after {} ms - {}\", elapsedTime, url);\n                    return true;\n                } else {\n                    LOGGER.warn(\"not ready / http get returned status: {} - {}\", response.getStatus(), url);\n                }\n            } catch (Exception e) {\n                sleep(SLEEP_TIME);\n            }\n        } while (attempts++ < POLL_ATTEMPTS_MAX);\n        return false;\n    }\n\n    public static boolean waitForSocket(int port) {\n        StopListenerThread waiter = new StopListenerThread(port, () -> {\n            LOGGER.info(\"*** exited socket wait succesfully\");\n        });\n        waiter.start();\n        port = waiter.getPort();\n        System.out.println(\"*** waiting for socket, type the command below:\\ncurl http://localhost:\"\n                + port + \"\\nin a new terminal (or open the URL in a web-browser) to proceed ...\");\n        try {\n            waiter.join();\n            return true;\n        } catch (Exception e) {\n            LOGGER.warn(\"*** wait thread failed: {}\", e.getMessage());\n            return false;\n        }\n    }\n\n    public Command(String... args) {\n        this(false, null, null, null, null, args);\n    }\n\n    public Command(boolean useLineFeed, File workingDir, String... args) {\n        this(useLineFeed, null, null, null, workingDir, args);\n    }\n\n    public Command(boolean useLineFeed, Logger logger, String uniqueName, String logFile, File workingDir, String... args) {\n        this.useLineFeed = useLineFeed;\n        setDaemon(true);\n        this.uniqueName = uniqueName == null ? System.currentTimeMillis() + \"\" : uniqueName;\n        setName(this.uniqueName);\n        this.logger = logger == null ? new Logger() : logger;\n        this.workingDir = workingDir;\n        this.args = args;\n        if (workingDir != null) {\n            workingDir.mkdirs();\n        }\n        argList = Arrays.asList(args);\n        if (logFile == null) {\n            appender = new StringLogAppender(useLineFeed);\n            sharedAppender = false;\n        } else { // don't create new file if re-using an existing appender\n            LogAppender temp = this.logger.getAppender();\n            sharedAppender = temp != null;\n            if (sharedAppender) {\n                appender = temp;\n            } else {\n                appender = new FileLogAppender(new File(logFile));\n                this.logger.setAppender(appender);\n            }\n        }\n    }\n\n    public Map<String, String> getEnvironment() {\n        return environment;\n    }\n\n    public File getWorkingDir() {\n        return workingDir;\n    }\n\n    public List getArgList() {\n        return argList;\n    }\n\n    public Logger getLogger() {\n        return logger;\n    }\n\n    public LogAppender getAppender() {\n        return appender;\n    }\n\n    public String getUniqueName() {\n        return uniqueName;\n    }\n\n    public int getExitCode() {\n        return exitCode;\n    }\n\n    public int waitSync() {\n        try {\n            join();\n            return exitCode;\n        } catch (InterruptedException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void close(boolean force) {\n        LOGGER.debug(\"closing command: {}\", uniqueName);\n        if (force) {\n            process.destroyForcibly();\n        } else {\n            process.destroy();\n        }\n    }\n\n    @Override\n    public void run() {\n        try {\n            logger.debug(\"command: {}, working dir: {}\", argList, workingDir);\n            ProcessBuilder pb = new ProcessBuilder(args);\n            if (environment != null) {\n                pb.environment().putAll(environment);\n                environment = pb.environment();\n            }\n            logger.trace(\"env PATH: {}\", pb.environment().get(\"PATH\"));\n            if (workingDir != null) {\n                pb.directory(workingDir);\n            }\n            pb.redirectErrorStream(redirectErrorStream);\n            process = pb.start();\n            sysOut = new Console(uniqueName + \"-out\", useLineFeed, process.getInputStream(), logger, appender, listener);\n            sysOut.start();\n            sysErr = new Console(uniqueName + \"-err\", useLineFeed, process.getErrorStream(), logger, appender, errorListener);\n            sysErr.start();\n            exitCode = process.waitFor();\n            if (exitCode == 0) {\n                LOGGER.debug(\"command complete, exit code: {} - {}\", exitCode, argList);\n            } else {\n                LOGGER.warn(\"exit code was non-zero: {} - {} working dir: {}\", exitCode, argList, workingDir);\n            }\n            // the consoles actually can take more time to flush even after the process has exited\n            sysErr.join();\n            sysOut.join();\n            LOGGER.trace(\"console readers complete\");\n            if (!sharedAppender) {\n                appender.close();\n            }\n        } catch (Exception e) {\n            failureReason = e;\n            LOGGER.error(\"command error: {} - {}\", argList, e.getMessage());\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/shell/Console.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.shell;\n\nimport com.intuit.karate.LogAppender;\nimport com.intuit.karate.Logger;\nimport java.io.BufferedReader;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.util.function.Consumer;\n\n/**\n *\n * @author pthomas3\n */\npublic class Console extends Thread {\n\n    private final boolean useLineFeed;\n    private final InputStream is;\n    private final BufferedReader reader;\n    private final Logger logger;\n    private final LogAppender appender;\n    private final StringBuilder buffer;\n    private final Consumer<String> listener;\n    \n    public String getBuffer() {\n        return buffer.toString();\n    }\n\n    public Console(String name, boolean useLineFeed, InputStream is, Logger logger, LogAppender appender, Consumer<String> listener) {\n        super(name);\n        this.useLineFeed = useLineFeed;\n        this.is = is;\n        this.buffer = new StringBuilder();\n        reader = new BufferedReader(new InputStreamReader(is));\n        this.logger = logger;\n        this.appender = appender;\n        this.listener = listener;\n    }\n\n    @Override\n    public void run() {\n        String line;\n        try {\n            while ((line = reader.readLine()) != null) {\n                appender.append(line);\n                buffer.append(line);\n                logger.debug(\"{}\", line);\n                if (useLineFeed) {\n                    buffer.append('\\n');\n                }\n                if (listener != null) {\n                    listener.accept(line);\n                }\n            }\n        } catch (Exception e) {\n            logger.error(\"console reader error: {}\", e.getMessage());\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/shell/FileLogAppender.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.shell;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.LogAppender;\nimport java.io.File;\nimport java.io.RandomAccessFile;\nimport java.nio.Buffer;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.FileChannel;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class FileLogAppender implements LogAppender {\n    \n    private static final Logger LOGGER = LoggerFactory.getLogger(FileLogAppender.class);\n\n    private final RandomAccessFile file;\n    private final FileChannel channel;\n    private int prevPos;\n    private boolean closed;\n\n    public FileLogAppender(File in) {\n        try {\n            if (in == null) {\n                in = File.createTempFile(\"karate\", \"tmp\");\n            } else {\n                if (!in.getParentFile().exists()) {\n                    in.getParentFile().mkdirs();\n                }\n            }\n            file = new RandomAccessFile(in, \"rw\");\n            channel = file.getChannel();            \n            prevPos = (int) channel.position();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n    \n    private String getBuffer(boolean resetAndClear) {\n        try {\n            int pos = (int) channel.position();\n            ByteBuffer buf = ByteBuffer.allocate(pos - prevPos);\n            channel.read(buf, prevPos);\n            if (resetAndClear) {\n                prevPos = pos;\n            }\n            ((Buffer) buf).flip(); // java 8 to 9 fix\n            return FileUtils.toString(buf.array());\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }         \n    }\n\n    @Override\n    public String getBuffer() {\n        return getBuffer(false);\n    }   \n    \n    @Override\n    public String collect() {\n        return getBuffer(true);\n    }\n\n    @Override\n    public void append(String text) {\n        if (closed) {\n            return;\n        }\n        try {\n            channel.write(ByteBuffer.wrap(FileUtils.toBytes(text)));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void close() {\n        try {\n            file.close();\n            closed = true;\n        } catch (Exception e) {\n            LOGGER.warn(\"log appender close failed: {}\", e.getMessage());\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/shell/StopListenerThread.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.shell;\n\nimport java.io.BufferedReader;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.net.InetAddress;\nimport java.net.ServerSocket;\nimport java.net.Socket;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class StopListenerThread extends Thread {\n    \n    private static final Logger logger = LoggerFactory.getLogger(StopListenerThread.class);\n    \n    private Stoppable stoppable;\n    private ServerSocket socket;\n\n    public StopListenerThread(int port, Stoppable stoppable) {\n        this.stoppable = stoppable;\n        setDaemon(true);\n        setName(\"stop-listener-\" + port);\n        try {\n            socket = new ServerSocket(port, 1, InetAddress.getByName(\"127.0.0.1\"));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n    \n    public int getPort() {\n        return socket.getLocalPort();\n    }\n\n    @Override\n    public void run() {\n        logger.info(\"starting thread: {}\", getName());\n        Socket accept;\n        try {\n            accept = socket.accept();\n            BufferedReader reader = new BufferedReader(new InputStreamReader(accept.getInputStream()));\n            reader.readLine();\n            logger.info(\"shutting down thread: {}\", getName());\n            stoppable.stop();\n            accept.close();\n            socket.close();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }   \n    \n    public static void stop(int port) {\n         try {\n            Socket s = new Socket(InetAddress.getByName(\"127.0.0.1\"), port);\n            OutputStream out = s.getOutputStream();\n            logger.info(\"sending stop request to port: {}\", port);\n            out.write((\"\\r\\n\").getBytes());\n            out.flush();\n            s.close();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }       \n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/shell/Stoppable.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.shell;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Stoppable {\n    \n    void stop();\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/shell/StringLogAppender.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.shell;\n\nimport com.intuit.karate.LogAppender;\n\n/**\n *\n * @author pthomas3\n */\npublic class StringLogAppender implements LogAppender {\n    \n    private final StringBuilder sb = new StringBuilder();\n    \n    private final boolean useLineFeed;\n    \n    public StringLogAppender(boolean useLineFeed) {\n        this.useLineFeed = useLineFeed;\n    }\n    \n    @Override\n    public String getBuffer() {\n        return sb.toString();\n    }    \n    \n    @Override\n    public String collect() {\n        String temp = sb.toString();\n        sb.setLength(0);\n        return temp;\n    }\n    \n    @Override\n    public void append(String text) {\n        sb.append(text);\n        if (useLineFeed) {\n            sb.append('\\n');\n        }\n    }\n    \n    @Override\n    public void close() {\n        // don't dispose of buffer it can be collected later\n    }    \n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KaHxAnyAttrProcessor.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.AttributeName;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.AbstractAttributeTagProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n *\n * @author pthomas3\n */\npublic class KaHxAnyAttrProcessor extends AbstractAttributeTagProcessor {\n\n    private static final Logger logger = LoggerFactory.getLogger(KaHxAnyAttrProcessor.class);\n    \n    private final String attributeName;\n\n    public KaHxAnyAttrProcessor(String dialectPrefix, String attributeName) {\n        super(TemplateMode.HTML, dialectPrefix, null, false, attributeName, true, 1000, true);\n        this.attributeName = attributeName;\n    }\n\n    @Override\n    protected void doProcess(ITemplateContext ctx, IProcessableElementTag tag, AttributeName an, String av, IElementTagStructureHandler sh) {\n        sh.setAttribute(\"hx-\" + attributeName, av);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KaHxMethodAttrProcessor.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.http.ServerConfig;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.AttributeName;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.AbstractAttributeTagProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n *\n * @author pthomas3\n */\npublic class KaHxMethodAttrProcessor extends AbstractAttributeTagProcessor {\n\n    private static final Logger logger = LoggerFactory.getLogger(KaHxMethodAttrProcessor.class);\n\n    private final String attributeName;\n    private final String hostContextPath;\n\n    public KaHxMethodAttrProcessor(String dialectPrefix, String attributeName, ServerConfig config) {\n        super(TemplateMode.HTML, dialectPrefix, null, false, attributeName, true, 1000, true);\n        this.attributeName = attributeName;\n        hostContextPath = config.getHostContextPath();\n    }\n\n    @Override\n    protected void doProcess(ITemplateContext ctx, IProcessableElementTag tag, AttributeName an, String av, IElementTagStructureHandler sh) {\n        if (\"this\".equals(av)) {\n            av = ctx.getTemplateData().getTemplate();\n        } else if (av.startsWith(\"${\")) {\n            av = KarateEngineContext.get().evalLocal(\"`\" + av + \"`\", true).getValue();\n        }\n        if (hostContextPath != null) {\n            av = hostContextPath + av;\n        }\n        sh.setAttribute(\"hx-\" + attributeName, av);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KaHxValsAttrProcessor.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.graal.JsValue;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.AttributeName;\nimport org.thymeleaf.model.AttributeValueQuotes;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.AbstractAttributeTagProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n *\n * @author pthomas3\n */\npublic class KaHxValsAttrProcessor extends AbstractAttributeTagProcessor {\n\n    private static final Logger logger = LoggerFactory.getLogger(KaHxValsAttrProcessor.class);\n\n    public KaHxValsAttrProcessor(String dialectPrefix) {\n        super(TemplateMode.HTML, dialectPrefix, null, false, \"vals\", true, 1000, true);\n    }\n\n    @Override\n    protected void doProcess(ITemplateContext ctx, IProcessableElementTag tag, AttributeName an, String av, IElementTagStructureHandler sh) {\n        JsValue jv = KarateEngineContext.get().evalLocalAsObject(av);\n        if (!jv.isObject()) {\n            logger.warn(\"value did not evaluate to json: {}\", av);\n        } else {\n            sh.setAttribute(\"hx-vals\", jv.toJsonOrXmlString(false), AttributeValueQuotes.SINGLE);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KaLinkAttrProcessor.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.http.ServerConfig;\nimport com.intuit.karate.resource.Resource;\nimport com.intuit.karate.resource.ResourceResolver;\nimport java.io.File;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.AttributeName;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.AbstractAttributeTagProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n *\n * @author pthomas3\n */\npublic class KaLinkAttrProcessor extends AbstractAttributeTagProcessor {\n\n    private static final Logger logger = LoggerFactory.getLogger(KaLinkAttrProcessor.class);\n    \n    private static final String HREF = \"href\";\n\n    private final ResourceResolver resourceResolver;\n    private final String hostContextPath;\n\n    public KaLinkAttrProcessor(String dialectPrefix, ServerConfig config) {\n        super(TemplateMode.HTML, dialectPrefix, \"link\", false, HREF, false, 1000, false);\n        hostContextPath = config.getHostContextPath();\n        resourceResolver = config.getResourceResolver();\n    }\n\n    @Override\n    protected void doProcess(ITemplateContext ctx, IProcessableElementTag tag, AttributeName an, String av, IElementTagStructureHandler sh) {\n        String href = hostContextPath == null ? av : hostContextPath + av;\n        String noCache = tag.getAttributeValue(getDialectPrefix(), KaScriptElemProcessor.NOCACHE);\n        if (noCache != null) {\n            try {\n                Resource resource = resourceResolver.resolve(href);\n                href = href + \"?ts=\" + resource.getLastModified();\n            } catch (Exception e) {\n                logger.warn(\"nocache failed: {}\", e.getMessage());\n            }\n            sh.removeAttribute(getDialectPrefix(), KaScriptElemProcessor.NOCACHE);\n        }\n        sh.setAttribute(HREF, href);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KaScriptAttrProcessor.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.http.ServerConfig;\nimport com.intuit.karate.resource.Resource;\nimport com.intuit.karate.resource.ResourceResolver;\nimport java.io.File;\nimport java.io.InputStream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.AttributeName;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.AbstractAttributeTagProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n *\n * @author pthomas3\n */\npublic class KaScriptAttrProcessor extends AbstractAttributeTagProcessor {\n\n    private static final Logger logger = LoggerFactory.getLogger(KaScriptAttrProcessor.class);\n\n    private static final String SRC = \"src\";\n\n    private final String hostContextPath;\n    private final ResourceResolver resourceResolver;\n\n    public KaScriptAttrProcessor(String dialectPrefix, ServerConfig config) {\n        super(TemplateMode.HTML, dialectPrefix, null, false, SRC, false, 1000, false);\n        resourceResolver = config.getResourceResolver();\n        hostContextPath = config.getHostContextPath();\n    }\n\n    @Override\n    protected void doProcess(ITemplateContext ctx, IProcessableElementTag tag, AttributeName an, String src, IElementTagStructureHandler sh) {\n        String scope = tag.getAttributeValue(getDialectPrefix(), KaScriptElemProcessor.SCOPE);\n        if (scope == null) {\n            if (hostContextPath != null) {\n                src = hostContextPath + src;\n            }\n            String noCache = tag.getAttributeValue(getDialectPrefix(), KaScriptElemProcessor.NOCACHE);\n            if (noCache != null) {\n                try {\n                    Resource resource = resourceResolver.resolve(src);\n                    src = src + \"?ts=\" + resource.getLastModified();\n                } catch (Exception e) {\n                    logger.warn(\"nocache failed: {}\", e.getMessage());\n                }\n                sh.removeAttribute(getDialectPrefix(), KaScriptElemProcessor.NOCACHE);\n            }\n            sh.setAttribute(SRC, src);\n            return;\n        }\n        InputStream is = resourceResolver.resolve(src).getStream();\n        String js = FileUtils.toString(is);\n        if (KaScriptElemProcessor.LOCAL.equals(scope)) {\n            KarateEngineContext.get().evalLocal(js, false);\n        } else {\n            KarateEngineContext.get().evalGlobal(js);\n        }\n        sh.removeElement();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KaScriptElemProcessor.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.model.ITemplateEvent;\nimport org.thymeleaf.model.IText;\nimport org.thymeleaf.processor.element.AbstractElementModelProcessor;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n *\n * @author pthomas3\n */\npublic class KaScriptElemProcessor extends AbstractElementModelProcessor {\n\n    private static final Logger logger = LoggerFactory.getLogger(KaScriptElemProcessor.class);\n    \n    protected static final String SCOPE = \"scope\";\n    protected static final String LOCAL = \"local\";\n    protected static final String HEAD = \"head\";\n    protected static final String NOCACHE = \"nocache\";\n\n    public KaScriptElemProcessor(String dialectPrefix) {\n        super(TemplateMode.HTML, dialectPrefix, \"script\", false, SCOPE, true, 1000);\n    }\n\n    @Override\n    protected void doProcess(ITemplateContext ctx, IModel model, IElementModelStructureHandler sh) {        \n        int depth = ctx.getElementStack().size();\n        IProcessableElementTag tag = ctx.getElementStack().get(depth - 1);\n        String scope = tag.getAttributeValue(getDialectPrefix(), SCOPE);\n        int n = model.size();\n        while (n-- != 0) {\n            final ITemplateEvent event = model.get(n);\n            if (event instanceof IText) {\n                String text = StringUtils.trimToNull(((IText) event).getText());\n                if (text != null) {\n                    if (LOCAL.equals(scope)) {\n                        KarateEngineContext.get().evalLocal(text, false);\n                    } else {\n                        KarateEngineContext.get().evalGlobal(text);\n                    }\n                }\n            }\n        }\n        model.reset();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KaSetElemProcessor.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.model.ITemplateEvent;\nimport org.thymeleaf.model.IText;\nimport org.thymeleaf.processor.element.AbstractElementModelProcessor;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n *\n * @author pthomas3\n */\npublic class KaSetElemProcessor extends AbstractElementModelProcessor {\n\n    private static final Logger logger = LoggerFactory.getLogger(KaSetElemProcessor.class);\n\n    protected static final String SET = \"set\";\n\n    public KaSetElemProcessor(String dialectPrefix) {\n        super(TemplateMode.HTML, dialectPrefix, null, false, SET, true, 1000);\n    }\n\n    @Override\n    protected void doProcess(ITemplateContext ctx, IModel model, IElementModelStructureHandler sh) {\n        int depth = ctx.getElementStack().size();\n        IProcessableElementTag tag = ctx.getElementStack().get(depth - 1);\n        String name = tag.getAttributeValue(getDialectPrefix(), SET);\n        int n = model.size();\n        StringBuilder sb = new StringBuilder();\n        while (n-- != 0) {\n            final ITemplateEvent event = model.get(n);\n            if (event instanceof IText) {\n                sb.append(((IText) event).getText());\n            }\n        }\n        KarateEngineContext.get().setLocal(name, sb.toString().trim());\n        model.reset();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KarateAttributeTagProcessor.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.graal.JsValue;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.AttributeName;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.AbstractAttributeTagProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.standard.processor.StandardConditionalFixedValueTagProcessor;\nimport org.thymeleaf.templatemode.TemplateMode;\nimport org.thymeleaf.util.ArrayUtils;\nimport org.thymeleaf.util.EscapedAttributeUtils;\n\n/**\n * derived from\n * org.thymeleaf.standard.processor.AbstractStandardMultipleAttributeModifierTagProcessor\n *\n * @author pthomas3\n */\nabstract class KarateAttributeTagProcessor extends AbstractAttributeTagProcessor {\n\n    private static final Logger logger = LoggerFactory.getLogger(KarateAttributeTagProcessor.class);\n\n    protected enum ModificationType {\n        SUBSTITUTION, APPEND, PREPEND, APPEND_WITH_SPACE, PREPEND_WITH_SPACE\n    }\n\n    private final ModificationType modificationType;\n\n    protected KarateAttributeTagProcessor(\n            final TemplateMode templateMode, final String dialectPrefix,\n            final String attrName, final int precedence,\n            final ModificationType modificationType) {\n        super(templateMode, dialectPrefix, null, false, attrName, true, precedence, true);\n        this.modificationType = modificationType;\n    }\n\n    @Override\n    protected final void doProcess(\n            final ITemplateContext context,\n            final IProcessableElementTag tag,\n            final AttributeName attributeName, final String av,\n            final IElementTagStructureHandler structureHandler) {\n        JsValue jv = KarateEngineContext.get().evalLocalAsObject(av);\n        if (!jv.isObject()) {\n            logger.warn(\"value did not evaluate to json: {}\", av);\n            return;\n        }\n        Map<String, Object> map = jv.getAsMap();\n        map.forEach((k, v) -> {\n            if (getTemplateMode() == TemplateMode.HTML\n                    && this.modificationType == ModificationType.SUBSTITUTION\n                    && ArrayUtils.contains(StandardConditionalFixedValueTagProcessor.ATTR_NAMES, k)) {\n                // is a fixed-value conditional one, like \"selected\", which can only\n                // appear as selected=\"selected\" or not appear at all.\n                if (JsValue.isTruthy(v)) {\n                    structureHandler.setAttribute(k, k);\n                } else {\n                    structureHandler.removeAttribute(k);\n                }\n            } else {\n                // is a \"normal\" attribute, not a fixed-value conditional one - or we are not just replacing\n                final String newAttributeValue\n                        = EscapedAttributeUtils.escapeAttribute(getTemplateMode(), v == null ? null : v.toString());\n                if (newAttributeValue == null || newAttributeValue.length() == 0) {\n                    if (this.modificationType == ModificationType.SUBSTITUTION) {\n                        // equivalent to simply removing\n                        structureHandler.removeAttribute(k);\n                    }\n                    // prepend and append simply ignored in this case\n                } else {\n                    if (this.modificationType == ModificationType.SUBSTITUTION\n                            || !tag.hasAttribute(k)\n                            || tag.getAttributeValue(k).length() == 0) {\n                        // normal value replace\n                        structureHandler.setAttribute(k, newAttributeValue);\n                    } else {\n                        String currentValue = tag.getAttributeValue(k);\n                        if (this.modificationType == ModificationType.APPEND) {\n                            structureHandler.setAttribute(k, currentValue + newAttributeValue);\n                        } else if (this.modificationType == ModificationType.APPEND_WITH_SPACE) {\n                            structureHandler.setAttribute(k, currentValue + ' ' + newAttributeValue);\n                        } else if (this.modificationType == ModificationType.PREPEND) {\n                            structureHandler.setAttribute(k, newAttributeValue + currentValue);\n                        } else { // modification type is PREPEND_WITH_SPACE\n                            structureHandler.setAttribute(k, newAttributeValue + ' ' + currentValue);\n                        }\n                    }\n\n                }\n            }\n        });\n    }\n\n    public static class KarateAttrTagProcessor extends KarateAttributeTagProcessor {\n\n        public KarateAttrTagProcessor(final TemplateMode templateMode, final String dialectPrefix) {\n            super(templateMode, dialectPrefix, \"attr\", 700, ModificationType.SUBSTITUTION);\n        }\n\n    }\n\n    public static class KarateAttrappendTagProcessor extends KarateAttributeTagProcessor {\n\n        public KarateAttrappendTagProcessor(final TemplateMode templateMode, final String dialectPrefix) {\n            super(templateMode, dialectPrefix, \"attrappend\", 900, ModificationType.APPEND);\n        }\n\n    }\n\n    public static class KarateAttrprependTagProcessor extends KarateAttributeTagProcessor {\n\n        public KarateAttrprependTagProcessor(final TemplateMode templateMode, final String dialectPrefix) {\n            super(templateMode, dialectPrefix, \"attrprepend\", 900, ModificationType.PREPEND);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KarateEachTagProcessor.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.AttributeName;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.AbstractAttributeTagProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n *\n * @author pthomas3\n */\npublic class KarateEachTagProcessor extends AbstractAttributeTagProcessor {\n\n    private static final Logger logger = LoggerFactory.getLogger(KarateEachTagProcessor.class);\n\n    public static final int PRECEDENCE = 200;\n    public static final String ATTR_NAME = \"each\";\n\n    public KarateEachTagProcessor(final TemplateMode templateMode, final String dialectPrefix) {\n        super(templateMode, dialectPrefix, null, false, ATTR_NAME, true, PRECEDENCE, true);\n    }\n\n    @Override\n    protected void doProcess(\n            final ITemplateContext context,\n            final IProcessableElementTag tag,\n            final AttributeName attributeName, String av,\n            final IElementTagStructureHandler structureHandler) {\n        int pos = av.indexOf(':');\n        String iterVarName;\n        if (pos == -1) {\n            iterVarName = \"_\";\n        } else {\n            iterVarName = av.substring(0, pos).trim();\n            av = av.substring(pos + 1);\n        }\n        Object value = KarateEngineContext.get().evalLocal(av, true).getValue();\n        structureHandler.iterateElement(iterVarName, null, value);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KarateEngineContext.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.graal.JsEngine;\nimport com.intuit.karate.graal.JsValue;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Set;\nimport org.graalvm.polyglot.Value;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.context.IEngineContext;\nimport org.thymeleaf.context.IdentifierSequences;\nimport org.thymeleaf.engine.TemplateData;\nimport org.thymeleaf.expression.IExpressionObjects;\nimport org.thymeleaf.inline.IInliner;\nimport org.thymeleaf.model.IModelFactory;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.templatemode.TemplateMode;\nimport org.thymeleaf.templateresource.ITemplateResource;\n\n/**\n *\n * @author pthomas3\n */\npublic class KarateEngineContext implements IEngineContext {\n\n    private static final Logger logger = LoggerFactory.getLogger(KarateEngineContext.class);\n\n    private static final ThreadLocal<KarateEngineContext> THREAD_LOCAL = new ThreadLocal();\n\n    private final IEngineContext wrapped;\n    private final JsEngine jsEngine;\n    private final Map<String, Object> context = new HashMap();\n    private boolean redirect;\n\n    public static KarateEngineContext initThreadLocal(IEngineContext wrapped, JsEngine engine) {\n        KarateEngineContext tec = new KarateEngineContext(wrapped, engine);\n        THREAD_LOCAL.set(tec);\n        return tec;\n    }\n\n    private KarateEngineContext(IEngineContext wrapped, JsEngine jsEngine) {\n        this.wrapped = wrapped;\n        this.jsEngine = jsEngine;\n        jsEngine.put(\"_\", context);\n    }\n\n    public static KarateEngineContext get() {\n        return THREAD_LOCAL.get();\n    }\n\n    public static void set(KarateEngineContext kec) {\n        THREAD_LOCAL.set(kec);\n    }\n\n    public JsEngine getJsEngine() {\n        return jsEngine;\n    }\n    \n    public String getCallerTemplateName() {\n        TemplateData td = wrapped.getTemplateData();\n        ITemplateResource tr = td.getTemplateResource();\n        if (tr instanceof KarateTemplateResource) {\n            KarateTemplateResource ktr = (KarateTemplateResource) tr;\n            return ktr.getCaller();\n        }\n        return null;\n    }\n\n    public String getTemplateName() {\n        String name = wrapped.getTemplateData().getTemplate();\n        return name.startsWith(\"/\") ? name.substring(1) : name;\n    }\n\n    public void setRedirect(boolean redirect) {\n        this.redirect = redirect;\n    }\n\n    public boolean isRedirect() {\n        return redirect;\n    }\n\n    public JsValue evalGlobal(String src) {\n        getVariableNames().forEach(name -> jsEngine.put(name, getVariable(name)));\n        try {\n            return jsEngine.eval(src);\n        } catch (Exception e) {\n            throw JsEngine.fromJsEvalException(src, e, null);\n        }\n    }\n\n    public JsValue evalLocalAsObject(String src) {\n        String temp;\n        if (src.startsWith(\"${\")) {\n            temp = \"`\" + src + \"`\";\n        } else {\n            temp = \"({\" + src + \"})\";\n        }\n        return evalLocal(temp, true);\n    }\n\n    public JsValue evalLocal(String src, boolean returnValue) {\n        try {\n            Value value = jsEngine.evalWith(getVariableNames(), this::getVariable, src, returnValue);\n            return new JsValue(value);\n        } catch (Exception e) {\n            throw JsEngine.fromJsEvalException(src, e, null);\n        }\n    }\n\n    public void setLocal(String name, Object value) {\n        context.put(name, value);\n    }\n\n    @Override\n    public void increaseLevel() {\n        if (!context.isEmpty()) {\n            setVariables(context);\n            context.clear();\n        }\n        wrapped.increaseLevel();\n    }\n\n    @Override\n    public void setVariable(String name, Object value) {\n        wrapped.setVariable(name, value);\n    }\n\n    @Override\n    public void setVariables(Map<String, Object> variables) {\n        wrapped.setVariables(variables);\n    }\n\n    @Override\n    public void removeVariable(String name) {\n        wrapped.removeVariable(name);\n    }\n\n    @Override\n    public void setTemplateData(TemplateData template) {\n        wrapped.setTemplateData(template);\n    }\n\n    @Override\n    public void decreaseLevel() {\n        wrapped.decreaseLevel();\n    }\n\n    @Override\n    public boolean containsVariable(String name) {\n        return wrapped.containsVariable(name);\n    }\n\n    @Override\n    public Set<String> getVariableNames() {\n        return wrapped.getVariableNames();\n    }\n\n    @Override\n    public Object getVariable(String name) {\n        return wrapped.getVariable(name);\n    }\n\n    @Override\n    public boolean isVariableLocal(String name) {\n        return wrapped.isVariableLocal(name);\n    }\n\n    @Override\n    public void setSelectionTarget(Object selectionTarget) {\n        wrapped.setSelectionTarget(selectionTarget);\n    }\n\n    @Override\n    public void setInliner(IInliner inliner) {\n        wrapped.setInliner(inliner);\n    }\n\n    @Override\n    public void setElementTag(IProcessableElementTag elementTag) {\n        wrapped.setElementTag(elementTag);\n    }\n\n    @Override\n    public List<IProcessableElementTag> getElementStackAbove(int contextLevel) {\n        return wrapped.getElementStackAbove(contextLevel);\n    }\n\n    @Override\n    public int level() {\n        return wrapped.level();\n    }\n\n    @Override\n    public TemplateData getTemplateData() {\n        return wrapped.getTemplateData();\n    }\n\n    @Override\n    public TemplateMode getTemplateMode() {\n        return wrapped.getTemplateMode();\n    }\n\n    @Override\n    public List<TemplateData> getTemplateStack() {\n        return wrapped.getTemplateStack();\n    }\n\n    @Override\n    public List<IProcessableElementTag> getElementStack() {\n        return wrapped.getElementStack();\n    }\n\n    @Override\n    public Map<String, Object> getTemplateResolutionAttributes() {\n        return wrapped.getTemplateResolutionAttributes();\n    }\n\n    @Override\n    public IModelFactory getModelFactory() {\n        return wrapped.getModelFactory();\n    }\n\n    @Override\n    public boolean hasSelectionTarget() {\n        return wrapped.hasSelectionTarget();\n    }\n\n    @Override\n    public Object getSelectionTarget() {\n        return wrapped.getSelectionTarget();\n    }\n\n    @Override\n    public IInliner getInliner() {\n        return wrapped.getInliner();\n    }\n\n    @Override\n    public String getMessage(Class<?> origin, String key, Object[] messageParameters, boolean useAbsent) {\n        return wrapped.getMessage(origin, key, messageParameters, useAbsent);\n    }\n\n    @Override\n    public String buildLink(String base, Map<String, Object> parameters) {\n        return wrapped.buildLink(base, parameters);\n    }\n\n    @Override\n    public IdentifierSequences getIdentifierSequences() {\n        return wrapped.getIdentifierSequences();\n    }\n\n    @Override\n    public IEngineConfiguration getConfiguration() {\n        return wrapped.getConfiguration();\n    }\n\n    @Override\n    public IExpressionObjects getExpressionObjects() {\n        return wrapped.getExpressionObjects();\n    }\n\n    @Override\n    public Locale getLocale() {\n        return wrapped.getLocale();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KarateExpression.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.IExpressionContext;\nimport org.thymeleaf.standard.expression.IStandardExpression;\nimport org.thymeleaf.standard.expression.StandardExpressionExecutionContext;\n\n/**\n *\n * @author pthomas3\n */\npublic class KarateExpression implements IStandardExpression {\n\n    private static final Logger logger = LoggerFactory.getLogger(KarateExpression.class);\n\n    private final String input;\n\n    public KarateExpression(String input) {\n        if (input.startsWith(\"${\")) {\n            input = input.substring(2, input.length() - 1);\n        }\n        this.input = input;\n    }\n\n    @Override\n    public String getStringRepresentation() {\n        return input;\n    }\n\n    @Override\n    public Object execute(IExpressionContext context) {\n        return KarateEngineContext.get().evalLocal(input, true).getValue();\n    }\n\n    @Override\n    public Object execute(IExpressionContext context, StandardExpressionExecutionContext expContext) {\n        return execute(context);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KarateScriptDialect.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.http.ServerConfig;\nimport java.util.HashSet;\nimport java.util.Set;\nimport org.thymeleaf.dialect.AbstractProcessorDialect;\nimport org.thymeleaf.processor.IProcessor;\n\n/**\n *\n * @author pthomas3\n */\npublic class KarateScriptDialect extends AbstractProcessorDialect {\n\n    private final ServerConfig config;\n\n    public KarateScriptDialect(ServerConfig config) {\n        super(\"karate\", \"ka\", 2000); // has to be processed after standard (default) dialect which is 1000\n        this.config = config;\n    }\n\n    @Override\n    public Set<IProcessor> getProcessors(String dialectPrefix) {\n        Set<IProcessor> ps = new HashSet();\n        ps.add(new KaScriptAttrProcessor(dialectPrefix, config));\n        ps.add(new KaScriptElemProcessor(dialectPrefix));\n        ps.add(new KaSetElemProcessor(dialectPrefix));\n        return ps;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KarateServerDialect.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.http.ServerConfig;\nimport java.util.HashSet;\nimport java.util.Set;\nimport org.thymeleaf.dialect.AbstractProcessorDialect;\nimport org.thymeleaf.processor.IProcessor;\n\n/**\n *\n * @author pthomas3\n */\npublic class KarateServerDialect extends AbstractProcessorDialect {\n\n    private final ServerConfig config;\n\n    public KarateServerDialect(ServerConfig config) {\n        super(\"karate\", \"ka\", 2000); // has to be processed after standard (default) dialect which is 1000\n        this.config = config;\n    }\n\n    @Override\n    public Set<IProcessor> getProcessors(String dialectPrefix) {\n        Set<IProcessor> ps = new HashSet();\n        ps.add(new KaScriptAttrProcessor(dialectPrefix, config));\n        ps.add(new KaScriptElemProcessor(dialectPrefix));\n        ps.add(new KaSetElemProcessor(dialectPrefix));\n        ps.add(new KaLinkAttrProcessor(dialectPrefix, config));\n        ps.add(new KaHxMethodAttrProcessor(dialectPrefix, \"get\", config));\n        ps.add(new KaHxMethodAttrProcessor(dialectPrefix, \"post\", config));\n        ps.add(new KaHxMethodAttrProcessor(dialectPrefix, \"put\", config));\n        ps.add(new KaHxMethodAttrProcessor(dialectPrefix, \"patch\", config));\n        ps.add(new KaHxMethodAttrProcessor(dialectPrefix, \"delete\", config));\n        ps.add(new KaHxValsAttrProcessor(dialectPrefix));\n        return ps;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KarateStandardDialect.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.IExpressionContext;\nimport org.thymeleaf.processor.IProcessor;\nimport org.thymeleaf.standard.StandardDialect;\nimport org.thymeleaf.standard.expression.IStandardExpression;\nimport org.thymeleaf.standard.expression.IStandardExpressionParser;\nimport org.thymeleaf.standard.expression.IStandardVariableExpression;\nimport org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator;\nimport org.thymeleaf.standard.expression.StandardExpressionExecutionContext;\nimport org.thymeleaf.standard.expression.StandardExpressionParser;\nimport org.thymeleaf.standard.processor.StandardAttrTagProcessor;\nimport org.thymeleaf.standard.processor.StandardAttrappendTagProcessor;\nimport org.thymeleaf.standard.processor.StandardAttrprependTagProcessor;\nimport org.thymeleaf.standard.processor.StandardEachTagProcessor;\nimport org.thymeleaf.standard.processor.StandardWithTagProcessor;\n\n/**\n *\n * @author pthomas3\n */\npublic class KarateStandardDialect extends StandardDialect implements IStandardVariableExpressionEvaluator, IStandardExpressionParser {\n\n    private static final Logger logger = LoggerFactory.getLogger(KarateStandardDialect.class);\n\n    private final StandardExpressionParser expressionParser = new StandardExpressionParser();\n\n    @Override\n    public IStandardVariableExpressionEvaluator getVariableExpressionEvaluator() {\n        return this;\n    }\n\n    @Override\n    public IStandardExpressionParser getExpressionParser() {\n        return this;\n    }\n\n    @Override\n    public Set<IProcessor> getProcessors(String dialectPrefix) {\n        Set<IProcessor> processors = StandardDialect.createStandardProcessorsSet(dialectPrefix);\n        Set<IProcessor> patched = new HashSet(processors.size());\n        for (IProcessor p : processors) {\n            if (p instanceof StandardEachTagProcessor) {\n                p = new KarateEachTagProcessor(p.getTemplateMode(), dialectPrefix);\n            }\n            if (p instanceof StandardWithTagProcessor) {\n                p = new KarateWithTagProcessor(p.getTemplateMode(), dialectPrefix);\n            }\n            if (p instanceof StandardAttrTagProcessor) {\n                p = new KarateAttributeTagProcessor.KarateAttrTagProcessor(p.getTemplateMode(), dialectPrefix);\n            }\n            if (p instanceof StandardAttrappendTagProcessor) {\n                p = new KarateAttributeTagProcessor.KarateAttrappendTagProcessor(p.getTemplateMode(), dialectPrefix);\n            }\n            if (p instanceof StandardAttrprependTagProcessor) {\n                p = new KarateAttributeTagProcessor.KarateAttrprependTagProcessor(p.getTemplateMode(), dialectPrefix);\n            }            \n            patched.add(p);\n        }\n        return patched;\n    }\n\n    @Override\n    public Object evaluate(IExpressionContext ctx, IStandardVariableExpression ve, StandardExpressionExecutionContext ec) {\n        // found to be used for th:attrappend=\"data-parent=${expression}\"\n        KarateExpression ke = new KarateExpression(ve.getExpression());\n        return ke.execute(ctx);\n    }\n\n    @Override\n    public IStandardExpression parseExpression(IExpressionContext context, String input) {\n        if (input.charAt(0) == '~') { // template\n            return expressionParser.parseExpression(context, input);\n        }\n        return new KarateExpression(input);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KarateTemplateEngine.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.graal.JsEngine;\nimport java.io.IOException;\nimport java.io.Writer;\nimport java.util.Map;\nimport java.util.function.Supplier;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.TemplateEngine;\nimport org.thymeleaf.TemplateSpec;\nimport org.thymeleaf.context.IContext;\nimport org.thymeleaf.context.IEngineContext;\nimport org.thymeleaf.context.StandardEngineContextFactory;\nimport org.thymeleaf.dialect.IDialect;\nimport org.thymeleaf.engine.TemplateData;\nimport org.thymeleaf.engine.TemplateManager;\nimport org.thymeleaf.exceptions.TemplateOutputException;\nimport org.thymeleaf.exceptions.TemplateProcessingException;\nimport org.thymeleaf.templatemode.TemplateMode;\nimport org.thymeleaf.templateresolver.ITemplateResolver;\nimport org.thymeleaf.util.FastStringWriter;\n\n/**\n *\n * @author pthomas3\n */\npublic class KarateTemplateEngine {\n\n    private static final Logger logger = LoggerFactory.getLogger(KarateTemplateEngine.class);\n\n    private final StandardEngineContextFactory standardFactory;\n    private final TemplateEngine wrapped;\n\n    public KarateTemplateEngine(Supplier<JsEngine> jsEngine, IDialect... dialects) {\n        standardFactory = new StandardEngineContextFactory();\n        wrapped = new TemplateEngine();\n        wrapped.setEngineContextFactory((IEngineConfiguration ec, TemplateData data, Map<String, Object> attrs, IContext context) -> {\n            IEngineContext engineContext = standardFactory.createEngineContext(ec, data, attrs, context);\n            return KarateEngineContext.initThreadLocal(engineContext, jsEngine.get());\n        });\n        // the next line is a set which clears and replaces all existing / default\n        wrapped.setDialect(new KarateStandardDialect());\n        for (IDialect dialect : dialects) {\n            wrapped.addDialect(dialect);\n        }\n    }\n\n    public void addTemplateResolver(ITemplateResolver templateResolver) {\n        wrapped.addTemplateResolver(templateResolver);\n    }\n\n    public void setTemplateResolver(ITemplateResolver templateResolver) {\n        wrapped.setTemplateResolver(templateResolver);\n    }\n\n    public String process(String template) {\n        return process(template, TemplateContext.LOCALE_US);\n    }\n\n    public String process(String template, IContext context) {\n        TemplateSpec templateSpec = new TemplateSpec(template, TemplateMode.HTML);\n        Writer stringWriter = new FastStringWriter(100);\n        process(templateSpec, context, stringWriter);\n        return stringWriter.toString();\n    }\n\n    public void process(TemplateSpec templateSpec, IContext context, Writer writer) {\n        try {\n            TemplateManager templateManager = wrapped.getConfiguration().getTemplateManager();\n            templateManager.parseAndProcess(templateSpec, context, writer);\n            try {\n                writer.flush();\n            } catch (IOException e) {\n                throw new TemplateOutputException(\"error flushing output writer\", templateSpec.getTemplate(), -1, -1, e);\n            }\n        } catch (Exception e) {\n            KarateEngineContext kec = KarateEngineContext.get();\n            if (kec != null && !kec.isRedirect()) { // don't log redirects\n                // make thymeleaf errors easier to troubleshoot from the logs\n                while (e.getCause() instanceof Exception) {\n                    e = (Exception) e.getCause();\n                    if (e instanceof TemplateProcessingException) {\n                        logger.error(\"{}\", e.getMessage()); // will print line and col numbers\n                        if (e.getCause() != null) { // typically the js error\n                            String message = e.getCause().getMessage();\n                            logger.error(\"{}\", message);\n                        }\n                        break;\n                    }\n                }\n            }\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"{}\", StringUtils.throwableToString(e));\n            }\n            throw new RuntimeException(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KarateTemplateResource.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.resource.Resource;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.io.Reader;\nimport org.thymeleaf.templateresource.ITemplateResource;\n\n/**\n *\n * @author pthomas3\n */\npublic class KarateTemplateResource implements ITemplateResource {\n\n    private final String caller;\n    private final Resource resource;\n\n    public KarateTemplateResource(String caller, Resource resource) {\n        this.caller = caller;\n        this.resource = resource;\n    }\n\n    @Override\n    public String getDescription() {\n        return resource.getRelativePath();\n    }\n\n    @Override\n    public String getBaseName() {\n        return resource.getRelativePath();\n    }\n\n    public String getCaller() {\n        return caller;\n    }        \n\n    @Override\n    public boolean exists() {\n        return true;\n    }\n\n    @Override\n    public Reader reader() throws IOException {\n        return new InputStreamReader(resource.getStream());\n    }\n\n    @Override\n    public ITemplateResource relative(String relativeLocation) {\n        return new KarateTemplateResource(relativeLocation, resource.resolve(relativeLocation));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/KarateWithTagProcessor.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.graal.JsValue;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.IEngineContext;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.AttributeName;\nimport org.thymeleaf.model.AttributeValueQuotes;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.AbstractAttributeTagProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n *\n * @author pthomas3\n */\npublic class KarateWithTagProcessor extends AbstractAttributeTagProcessor {\n\n    private static final Logger logger = LoggerFactory.getLogger(KarateWithTagProcessor.class);\n\n    public static final int PRECEDENCE = 600;\n    public static final String ATTR_NAME = \"with\";\n\n    public KarateWithTagProcessor(final TemplateMode templateMode, final String dialectPrefix) {\n        super(templateMode, dialectPrefix, null, false, ATTR_NAME, true, PRECEDENCE, true);\n    }\n\n    @Override\n    protected void doProcess(\n            final ITemplateContext context,\n            final IProcessableElementTag tag,\n            final AttributeName attributeName, String av,\n            final IElementTagStructureHandler structureHandler) {\n        JsValue jv = KarateEngineContext.get().evalLocalAsObject(av);\n        if (!jv.isObject()) {\n            logger.warn(\"value did not evaluate to json: {}\", av);\n            return;\n        }\n        Map<String, Object> map = jv.getAsMap();\n        final IEngineContext engineContext;\n        if (context instanceof IEngineContext) {\n            engineContext = (IEngineContext) context;\n        } else {\n            engineContext = null;\n        }\n        map.forEach((k, v) -> {\n            if (engineContext != null) {\n                engineContext.setVariable(k, v);\n            } else {\n                structureHandler.setLocalVariable(k, v);\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/ResourceHtmlTemplateResolver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.resource.Resource;\nimport com.intuit.karate.resource.ResourceResolver;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.cache.AlwaysValidCacheEntryValidity;\nimport org.thymeleaf.templatemode.TemplateMode;\nimport org.thymeleaf.templateresolver.ITemplateResolver;\nimport org.thymeleaf.templateresolver.TemplateResolution;\n\n/**\n *\n * @author pthomas3\n */\npublic class ResourceHtmlTemplateResolver implements ITemplateResolver {\n\n    private static final Logger logger = LoggerFactory.getLogger(ResourceHtmlTemplateResolver.class);\n\n    private final ResourceResolver resourceResolver;\n\n    public ResourceHtmlTemplateResolver(ResourceResolver resourceResolver) {\n        this.resourceResolver = resourceResolver;\n    }\n\n    @Override\n    public String getName() {\n        return getClass().getName();\n    }\n\n    @Override\n    public Integer getOrder() {\n        return 1;\n    }\n\n    @Override\n    public TemplateResolution resolveTemplate(IEngineConfiguration ec, String ownerTemplate, String name, Map<String, Object> templateResolutionAttributes) {\n        if (name.startsWith(\"${\")) {\n            JsValue jv = KarateEngineContext.get().evalLocal(\"`\" + name + \"`\", true);\n            name = jv.getAsString();\n        }        \n        if (!name.endsWith(\".html\")) {\n            name = name + \".html\";\n        }\n        Resource resource = resourceResolver.resolve(ownerTemplate, name);\n        KarateTemplateResource templateResource = new KarateTemplateResource(ownerTemplate, resource);\n        return new TemplateResolution(templateResource, TemplateMode.HTML, AlwaysValidCacheEntryValidity.INSTANCE);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/ServerHtmlTemplateResolver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.resource.Resource;\nimport com.intuit.karate.resource.ResourceResolver;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.cache.AlwaysValidCacheEntryValidity;\nimport org.thymeleaf.cache.NonCacheableCacheEntryValidity;\nimport org.thymeleaf.templatemode.TemplateMode;\nimport org.thymeleaf.templateresolver.ITemplateResolver;\nimport org.thymeleaf.templateresolver.TemplateResolution;\n\n/**\n *\n * @author pthomas3\n */\npublic class ServerHtmlTemplateResolver implements ITemplateResolver {\n\n    private static final Logger logger = LoggerFactory.getLogger(ServerHtmlTemplateResolver.class);\n\n    private final ResourceResolver resourceResolver;\n    private final boolean devMode;\n\n    public ServerHtmlTemplateResolver(ResourceResolver resourceResolver, boolean devMode) {\n        this.resourceResolver = resourceResolver;\n        this.devMode = devMode;\n    }\n\n    @Override\n    public String getName() {\n        return getClass().getName();\n    }\n\n    @Override\n    public Integer getOrder() {\n        return 0;\n    }\n\n    @Override\n    public TemplateResolution resolveTemplate(IEngineConfiguration ec, String ownerTemplate, String name, Map<String, Object> templateResolutionAttributes) {\n        if (name.startsWith(\"${\")) {\n            JsValue jv = KarateEngineContext.get().evalLocal(\"`\" + name + \"`\", true);\n            name = jv.getAsString();\n        }\n        if (!name.endsWith(\".html\")) {\n            name = name + \".html\";\n        }        \n        Resource resource = resourceResolver.resolve(ownerTemplate, name);\n        KarateTemplateResource templateResource = new KarateTemplateResource(ownerTemplate, resource);\n        return new TemplateResolution(templateResource, TemplateMode.HTML,\n                devMode ? NonCacheableCacheEntryValidity.INSTANCE : AlwaysValidCacheEntryValidity.INSTANCE);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/StringHtmlTemplateResolver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.cache.NonCacheableCacheEntryValidity;\nimport org.thymeleaf.templatemode.TemplateMode;\nimport org.thymeleaf.templateresolver.ITemplateResolver;\nimport org.thymeleaf.templateresolver.TemplateResolution;\n\n/**\n *\n * @author pthomas3\n */\npublic class StringHtmlTemplateResolver implements ITemplateResolver {\n    \n    private static final Logger logger = LoggerFactory.getLogger(StringHtmlTemplateResolver.class);\n    \n    public static final StringHtmlTemplateResolver INSTANCE = new StringHtmlTemplateResolver();\n\n    @Override\n    public String getName() {\n        return getClass().getName();\n    }\n\n    @Override\n    public Integer getOrder() {\n        return 0;\n    }\n\n    @Override\n    public TemplateResolution resolveTemplate(IEngineConfiguration configuration, String ownerTemplate, String template, Map<String, Object> templateResolutionAttributes) {\n        if (ownerTemplate != null) {\n            return null;\n        }\n        StringTemplateResource resource = new StringTemplateResource(template);\n        return new TemplateResolution(resource, TemplateMode.HTML, NonCacheableCacheEntryValidity.INSTANCE);\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/StringTemplateResource.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.io.StringReader;\nimport org.thymeleaf.templateresource.ITemplateResource;\n\n/**\n *\n * @author pthomas3\n */\npublic class StringTemplateResource implements ITemplateResource {    \n    \n    private final String text;\n    \n    public StringTemplateResource(String text) {\n        this.text = text;\n    }\n\n    @Override\n    public String getDescription() {\n        return getBaseName();\n    }\n\n    @Override\n    public String getBaseName() {\n        return getClass().getName();\n    }\n\n    @Override\n    public boolean exists() {\n        return true;\n    }\n\n    @Override\n    public Reader reader() throws IOException {\n        return new StringReader(text);\n    }\n\n    @Override\n    public ITemplateResource relative(String relativeLocation) {\n        throw new UnsupportedOperationException(\"relative: \" + relativeLocation + \" - not implemented\");\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/TemplateContext.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport java.util.Collections;\nimport java.util.Locale;\nimport java.util.Set;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.thymeleaf.context.IContext;\n\n/**\n *\n * @author pthomas3\n */\npublic class TemplateContext implements IContext {\n\n    private static final Logger logger = LoggerFactory.getLogger(TemplateContext.class);\n    \n    public static final TemplateContext LOCALE_US = new TemplateContext(Locale.US);\n\n    private final Locale locale;\n\n    public TemplateContext(Locale locale) {\n        this.locale = locale;\n    }\n\n    @Override\n    public Locale getLocale() {\n        return locale;\n    }\n\n    @Override\n    public boolean containsVariable(String name) {\n        return false;\n    }\n\n    @Override\n    public Set<String> getVariableNames() {\n        return Collections.EMPTY_SET;\n    }\n\n    @Override\n    public Object getVariable(String name) {\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/com/intuit/karate/template/TemplateUtils.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.template;\n\nimport com.intuit.karate.graal.JsEngine;\nimport com.intuit.karate.http.RequestCycle;\nimport com.intuit.karate.http.ServerConfig;\nimport com.intuit.karate.http.ServerContext;\nimport com.intuit.karate.resource.ResourceResolver;\n\n/**\n *\n * @author pthomas3\n */\npublic class TemplateUtils {\n\n    private TemplateUtils() {\n        // only static methods\n    }\n\n    private static KarateTemplateEngine initEngine(JsEngine je, ResourceResolver resolver, boolean server) {\n        ServerConfig config = new ServerConfig(resolver);\n        ServerContext sc = new ServerContext(config, null);\n        je.put(RequestCycle.CONTEXT, sc); // TODO improve\n        return new KarateTemplateEngine(() -> je, server ? new KarateServerDialect(config) : new KarateScriptDialect(config));\n    }\n\n    public static KarateTemplateEngine forServer(ServerConfig config) {\n        KarateTemplateEngine engine = new KarateTemplateEngine(() -> RequestCycle.get().getEngine(), new KarateServerDialect(config));\n        engine.setTemplateResolver(new ServerHtmlTemplateResolver(config.getResourceResolver(), config.isDevMode()));\n        return engine;\n    }\n\n    public static KarateTemplateEngine forStrings(JsEngine je, ResourceResolver resourceResolver) {\n        KarateTemplateEngine engine = initEngine(je, resourceResolver, false);\n        engine.setTemplateResolver(StringHtmlTemplateResolver.INSTANCE);\n        engine.addTemplateResolver(new ResourceHtmlTemplateResolver(resourceResolver));\n        return engine;\n    }\n\n    public static KarateTemplateEngine forResourceResolver(JsEngine je, ResourceResolver resourceResolver) {\n        KarateTemplateEngine engine = initEngine(je, resourceResolver, false);\n        engine.setTemplateResolver(new ResourceHtmlTemplateResolver(resourceResolver));\n        return engine;\n    }\n\n    public static KarateTemplateEngine forServerResolver(JsEngine je, ResourceResolver resourceResolver, boolean devMode) {\n        KarateTemplateEngine engine = initEngine(je, resourceResolver, true);\n        engine.setTemplateResolver(new ServerHtmlTemplateResolver(resourceResolver, devMode));\n        return engine;\n    }\n\n    public static KarateTemplateEngine forResourceRoot(JsEngine je, String root) {\n        return forResourceResolver(je, new ResourceResolver(root));\n    }\n\n    public static String renderHtmlResource(String path, JsEngine je, ResourceResolver resourceResolver, boolean devMode) {\n        KarateEngineContext old = KarateEngineContext.get();\n        try {\n            KarateTemplateEngine kte = forServerResolver(je, resourceResolver, devMode);\n            return kte.process(path);\n        } finally {\n            KarateEngineContext.set(old);\n        }\n    }\n\n    public static String renderHtmlString(String html, JsEngine je, ResourceResolver resourceResolver) {\n        KarateEngineContext old = KarateEngineContext.get();\n        try {\n            KarateTemplateEngine kte = forStrings(je, resourceResolver);\n            return kte.process(html);\n        } finally {\n            KarateEngineContext.set(old);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/main/java/karate-meta.properties",
    "content": "karate.version=${project.version}"
  },
  {
    "path": "karate-core/src/main/java/logback-fatjar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %mdc{karateRequestId} %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>${karate.output.dir}/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %mdc{karateRequestId} %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit.karate\" level=\"DEBUG\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>\n"
  },
  {
    "path": "karate-core/src/main/java/logback-nofile.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %mdc{karateRequestId} %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender> \n   \n    <logger name=\"com.intuit.karate\" level=\"DEBUG\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n    </root>\n  \n</configuration>\n"
  },
  {
    "path": "karate-core/src/main/java/nodebug/io/karatelabs/LenientSslConnectionSocketFactory.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage nodebug.io.karatelabs;\n\nimport java.io.IOException;\nimport java.net.Socket;\nimport javax.net.ssl.HostnameVerifier;\nimport javax.net.ssl.SSLContext;\n\nimport org.apache.http.conn.ssl.SSLConnectionSocketFactory;\nimport org.apache.http.protocol.HttpContext;\n\n/**\n * in a package that is NOT in the karate package, else it will add un-necessary debug logging\n * the parent class is third-party code that unfortunately calls getClass() for logger name\n * \n * @author pthomas3\n */\npublic class LenientSslConnectionSocketFactory extends SSLConnectionSocketFactory {\n    \n    public LenientSslConnectionSocketFactory(SSLContext sslContext, HostnameVerifier hostnameVerifier) {\n        super(sslContext, hostnameVerifier);\n    }\n\n    @Override\n    public Socket createLayeredSocket(Socket socket, String target, int port, HttpContext context) throws IOException {\n        return super.createLayeredSocket(socket, \"\", port, context);\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/ComplexPojo.java",
    "content": "package com.intuit.karate;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class ComplexPojo {\n    \n    private String foo;\n    private int bar;\n    private Map<String, String> baz;\n    private List<ComplexPojo> ban;\n\n    public String getFoo() {\n        return foo;\n    }\n\n    public void setFoo(String foo) {\n        this.foo = foo;\n    }\n\n    public int getBar() {\n        return bar;\n    }\n\n    public void setBar(int bar) {\n        this.bar = bar;\n    }\n\n    public Map<String, String> getBaz() {\n        return baz;\n    }\n\n    public void setBaz(Map<String, String> baz) {\n        this.baz = baz;\n    } \n\n    public List<ComplexPojo> getBan() {\n        return ban;\n    }\n\n    public void setBan(List<ComplexPojo> ban) {\n        this.ban = ban;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/FileUtilsTest.java",
    "content": "package com.intuit.karate;\n\nimport com.intuit.karate.core.Feature;\nimport java.io.File;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass FileUtilsTest {\n    \n    static final Logger logger = LoggerFactory.getLogger(FileUtilsTest.class);\n    \n    @Test\n    void testToStringBytes() {\n        final byte[] bytes = {102, 111, 111, 98, 97, 114};\n        assertEquals(\"foobar\", FileUtils.toString(bytes));\n        assertNull(FileUtils.toString((byte[]) null));\n    }\n    \n    @Test\n    void testToBytesString() {\n        final byte[] bytes = {102, 111, 111, 98, 97, 114};\n        assertArrayEquals(bytes, FileUtils.toBytes(\"foobar\"));\n        assertNull(FileUtils.toBytes((String) null));\n    }\n    \n    @Test\n    void testRenameZeroLengthFile() {\n        long time = System.currentTimeMillis();\n        String name = \"target/\" + time + \".json\";\n        FileUtils.writeToFile(new File(name), \"\");\n        FileUtils.renameFileIfZeroBytes(name);\n        File file = new File(name + \".fail\");\n        assertTrue(file.exists());\n    }\n    \n    @Test\n    void testUsingBadPath() {\n        String relativePath = \"/foo/bar/feeder.feature\";\n        try {\n            Feature.read(relativePath);\n            fail(\"we should not have reached here\");\n        } catch (Exception e) {\n            assertEquals(e.getMessage(), \"not found: /foo/bar/feeder.feature\");\n        }\n    }\n    \n    @Test\n    void testDeleteDirectory() throws Exception {\n        new File(\"target/foo/bar\").mkdirs();\n        FileUtils.writeToFile(new File(\"target/foo/hello.txt\"), \"hello world\");\n        FileUtils.writeToFile(new File(\"target/foo/bar/world.txt\"), \"hello again\");\n        assertTrue(new File(\"target/foo/hello.txt\").exists());\n        assertTrue(new File(\"target/foo/bar/world.txt\").exists());\n        FileUtils.deleteDirectory(new File(\"target/foo\"));\n        assertFalse(new File(\"target/foo/hello.txt\").exists());\n        assertFalse(new File(\"target/foo/bar/world.txt\").exists());        \n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/ImageComparisonTest.java",
    "content": "package com.intuit.karate;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.ValueSource;\n\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ImageComparisonTest {\n    private static final byte[] BG_3x3_IMG = Base64.getDecoder().decode(\"iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAFUlEQVR4AWPgSj4BQSBWQAoXlAVBAJZuCmeg2F+9AAAAAElFTkSuQmCC\");\n    private static final byte[] B_3x3_IMG = Base64.getDecoder().decode(\"iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAD0lEQVR4AWPgSj4BQdhYAJxjCt5/e6nZAAAAAElFTkSuQmCC\");\n    private static final String R_1x1_BASE64 = \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/AAAZ4gk3AAAACklEQVR4AWNiAAAABgADDe3ZwQAAAABJRU5ErkJggg==\";\n    private static final byte[] R_1x1_IMG = Base64.getDecoder().decode(R_1x1_BASE64);\n    private static final byte[] R_2x2_IMG = Base64.getDecoder().decode(\"iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAADklEQVR4AWP4zwBCUAoAH+4D/cQhcooAAAAASUVORK5CYII=\");\n\n\n    private static Map<String, Object> opts(Object... params) {\n        Map<String, Object> options = new HashMap<>();\n        for (int i=0; i<params.length; i+=2) {\n            options.put(params[i].toString(), params[i+1]);\n        }\n        return options;\n    }\n\n    private static double round(double d) {\n        return Math.round(d * 100.0) / 100.0;\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\"resemble\", \"ssim\"})\n    void testIgnoredBoxes(String engine) {\n        Map<String, Integer> box = new HashMap<>();\n        box.put(\"left\", 1);\n        box.put(\"right\", 2);\n        box.put(\"top\", 1);\n        box.put(\"bottom\", 2);\n\n        Map<String, Object> result = ImageComparison.compare(\n                B_3x3_IMG,\n                BG_3x3_IMG,\n                opts(\"ignoredBoxes\", Collections.singletonList(box), \"windowSize\", 1),\n                opts(\"engine\", engine));\n\n        assertEquals(0.0, result.get(\"mismatchPercentage\"));\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\"resemble\", \"ssim\"})\n    void testIgnoreColors(String engine) {\n        Map<String, Object> result = ImageComparison.compare(\n                B_3x3_IMG,\n                BG_3x3_IMG,\n                opts(\"ignoreColors\", true, \"windowSize\", 1),\n                opts(\"engine\", engine));\n\n        assertEquals(0.0, result.get(\"mismatchPercentage\"));\n    }\n\n    @Test\n    void testIgnoreAreasColoredWith() {\n        Map<String, Integer> darkGreen = new HashMap<>();\n        darkGreen.put(\"r\", 80);\n        darkGreen.put(\"g\", 100);\n        darkGreen.put(\"b\", 10);\n\n        Map<String, Object> result = ImageComparison.compare(\n                B_3x3_IMG,\n                BG_3x3_IMG,\n                opts(\"ignoreAreasColoredWith\", darkGreen),\n                opts());\n\n        assertEquals(0.0, result.get(\"mismatchPercentage\"));\n    }\n\n    @Test\n    void testFailureThresholdTriggered() {\n        ImageComparison.MismatchException exception = assertThrows(ImageComparison.MismatchException.class, () ->\n                ImageComparison.compare(B_3x3_IMG, BG_3x3_IMG, opts(), opts()));\n\n        double mismatchPercentage = (double)exception.data.get(\"mismatchPercentage\");\n\n        // 3x3 = 9 pixels, 1 is different => 1/9 = 0.111111... => ~11.11%\n        assertEquals(11.11, round(mismatchPercentage));\n    }\n\n    @ParameterizedTest\n    @CsvSource({\n            \"12, 0\", // set options only\n            \"0, 12\", // set config only\n            \"12, 1\"  // set both and ensure we prefer options\n    })\n    void testSetFailureThreshold(double optionFailureThreshold, double configFailureThreshold) {\n        Map<String, Object> result = ImageComparison.compare(\n                B_3x3_IMG,\n                BG_3x3_IMG,\n                optionFailureThreshold == 0.0 ? opts() : opts(\"failureThreshold\", optionFailureThreshold),\n                configFailureThreshold == 0.0 ? opts() : opts(\"failureThreshold\", configFailureThreshold));\n\n        double mismatchPercentage = (double)result.get(\"mismatchPercentage\");\n\n        // 3x3 = 9 pixels, 1 is different => 1/9 = 0.111111... => ~11.11%\n        assertEquals(11.11, round(mismatchPercentage));\n    }\n\n    @Test\n    void testDataUrl() {\n        Map<String, Object> result = ImageComparison.compare(R_1x1_IMG, R_1x1_IMG, opts(), opts());\n        String dataUrl = \"data:image/png;base64,\" + R_1x1_BASE64;\n\n        assertEquals(dataUrl, result.get(\"baseline\"));\n        assertEquals(dataUrl, result.get(\"latest\"));\n    }\n\n    @Test\n    void testMissingBaseline() {\n        ImageComparison.MismatchException exception = assertThrows(ImageComparison.MismatchException.class, () ->\n                ImageComparison.compare(null, R_1x1_IMG, opts(), opts()));\n\n        assertTrue(exception.getMessage().contains(\"baseline image was empty or not found\"));\n        assertEquals(Boolean.TRUE, exception.data.get(\"isBaselineMissing\"));\n    }\n\n    @Test\n    void testScale() {\n        Map<String, Object> result = ImageComparison.compare(R_1x1_IMG, R_2x2_IMG, opts(), opts(\"allowScaling\", true));\n        assertEquals(0.0, result.get(\"mismatchPercentage\"));\n    }\n\n    @Test\n    void testScaleMismatch() {\n        ImageComparison.MismatchException exception = assertThrows(ImageComparison.MismatchException.class, () ->\n                ImageComparison.compare(R_1x1_IMG, R_2x2_IMG, opts(), opts()));\n\n        assertTrue(exception.getMessage().contains(\"latest image dimensions != baseline image dimensions\"));\n        assertEquals(Boolean.TRUE, exception.data.get(\"isScaleMismatch\"));\n    }\n\n    @Test\n    void testInvalidEngine() {\n        ImageComparison.MismatchException exception = assertThrows(ImageComparison.MismatchException.class, () ->\n                ImageComparison.compare(R_1x1_IMG, R_1x1_IMG, opts(\"engine\", \"ng\"), opts()));\n\n        assertTrue(exception.getMessage().contains(\"latest image differed from baseline more than allowable threshold\"));\n        assertEquals(Boolean.TRUE, exception.data.get(\"isMismatch\"));\n        assertEquals(100.0, exception.data.get(\"mismatchPercentage\"));\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/JsonTest.java",
    "content": "package com.intuit.karate;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.NoSuchElementException;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass JsonTest {\n\n    static final Logger logger = LoggerFactory.getLogger(JsonTest.class);\n\n    private void match(Json json, String expected) {\n        Match.Result mr = Match.evaluate(json.value()).isEqualTo(expected);\n        assertTrue(mr.pass, mr.message);\n    }\n\n    @Test\n    void testParsingParentAndLeafName() {\n        assertEquals(StringUtils.pair(\"\", \"$\"), Json.toParentAndLeaf(\"$\"));\n        assertEquals(StringUtils.pair(\"$\", \"foo\"), Json.toParentAndLeaf(\"$.foo\"));\n        assertEquals(StringUtils.pair(\"$\", \"['foo']\"), Json.toParentAndLeaf(\"$['foo']\"));\n        assertEquals(StringUtils.pair(\"$.foo\", \"bar\"), Json.toParentAndLeaf(\"$.foo.bar\"));\n        assertEquals(StringUtils.pair(\"$.foo\", \"['bar']\"), Json.toParentAndLeaf(\"$.foo['bar']\"));\n        assertEquals(StringUtils.pair(\"$.foo\", \"bar[0]\"), Json.toParentAndLeaf(\"$.foo.bar[0]\"));\n        assertEquals(StringUtils.pair(\"$.foo\", \"['bar'][0]\"), Json.toParentAndLeaf(\"$.foo['bar'][0]\"));\n        assertEquals(StringUtils.pair(\"$.foo[2]\", \"bar[0]\"), Json.toParentAndLeaf(\"$.foo[2].bar[0]\"));\n        assertEquals(StringUtils.pair(\"$.foo[2]\", \"['bar'][0]\"), Json.toParentAndLeaf(\"$.foo[2]['bar'][0]\"));\n        assertEquals(StringUtils.pair(\"$.foo[2]\", \"bar\"), Json.toParentAndLeaf(\"$.foo[2].bar\"));\n        assertEquals(StringUtils.pair(\"$.foo[2]\", \"['bar']\"), Json.toParentAndLeaf(\"$.foo[2]['bar']\"));\n    }\n\n    @Test\n    void testSetAndRemove() {\n        Json json = Json.object();\n        match(json, \"{}\");\n        assertTrue(json.pathExists(\"$\"));\n        assertFalse(json.pathExists(\"$.foo\"));\n        json.set(\"foo\", \"bar\");\n        match(json, \"{ foo: 'bar' }\");\n        assertTrue(json.pathExists(\"$.foo\"));\n        assertFalse(json.pathExists(\"bar.baz\"));\n        assertFalse(json.pathExists(\"bar\"));\n        json.set(\"bar.baz\", \"ban\");\n        match(json, \"{ foo: 'bar', bar: { baz: 'ban' } }\");\n        json.remove(\"foo\");\n        match(json, \"{ bar: { baz: 'ban' } }\");\n        json.remove(\"bar.baz\");\n        match(json, \"{ bar: { } }\");\n        json.remove(\"bar\");\n        match(json, \"{}\");\n        json.set(\"foo.bar\", \"[1, 2]\");\n        match(json, \"{ foo: { bar: [1, 2] } }\");\n        json.set(\"foo.bar[0]\", 9);\n        match(json, \"{ foo: { bar: [9, 2] } }\");\n        json.set(\"foo.bar[]\", 8);\n        match(json, \"{ foo: { bar: [9, 2, 8] } }\");\n        json.remove(\"foo.bar[0]\");\n        match(json, \"{ foo: { bar: [2, 8] } }\");\n        json.remove(\"foo.bar[1]\");\n        match(json, \"{ foo: { bar: [2] } }\");\n        json.remove(\"foo.bar\");\n        match(json, \"{ foo: {} }\");\n        json.remove(\"foo\");\n        match(json, \"{}\");\n        json = Json.of(\"[]\");\n        match(json, \"[]\");\n        json.set(\"$[0].foo\", \"[1, 2]\");\n        match(json, \"[{ foo: [1, 2] }]\");\n        json.set(\"$[1].foo\", \"[3, 4]\");\n        match(json, \"[{ foo: [1, 2] }, { foo: [3, 4] }]\");\n        json.remove(\"$[0]\");\n        match(json, \"[{ foo: [3, 4] }]\");\n        json = Json.object().set(\"$.foo[]\", \"a\");\n        match(json, \"{ foo: ['a'] }\");\n    }\n\n    @Test\n    void testSetNestedObject() {\n        Json json = Json.of(\"{ name: 'Wild', kitten: null }\");\n        json.set(\"$.kitten.name\", \"Bob\");\n        match(json, \"{ name: 'Wild', kitten: { name: 'Bob' } }\");\n    }\n\n    @Test\n    void testSetNestedArray() {\n        Json json = Json.of(\"[]\");\n        json.set(\"$[0].name.first\", \"Bob\");\n        match(json, \"[{ name: { first: 'Bob' } }]\");\n    }\n\n    @Test\n    void testSetMultipleNestedArray(){\n        Json json = Json.object();\n        json.set(\"first.second[0].third[0].fourth[0]\",\"hello\");\n        match(json, \"{ first :{ second :[{ third :[{ fourth :[ hello ]}]}]}}\");\n        json.set(\"first.fifth[0].sixth[1].seventh[2]\",\"hello\");\n        match(json,\"{ first :{ second :[{ third :[{ fourth :[ 'hello' ]}]}], fifth :[{ sixth :[null,{ seventh :[null,null, hello ]}]}]}}\");\n    }\n    \n    @Test\n    void testJsonApi() {\n        Json json = Json.of(\"{ a: 1, b: { c: 2 } }\");\n        assertEquals(2, (int) json.get(\"b.c\"));\n        assertEquals(2, (int) json.getOptional(\"b.c\").get());\n        assertNull(json.get(\"b.d\", null));\n        assertEquals(3, (int) json.get(\"b.d\", 3));\n//        try {\n//            json.getOptional(\"b.d\").get();\n//            fail(\"expected exception\");\n//        } catch (Exception e) {\n//            assertTrue(e instanceof NoSuchElementException);\n//        }\n    }\n    \n    @Test\n    void testGetAsJson() {\n        Json json = Json.of(\"{ a: { b: [1, 2], c: { d: 1, e: 2 } } }\");       \n        Json child1 = json.getJson(\"a.b\");\n        assertTrue(child1.isArray());\n        assertEquals(Arrays.asList(1, 2), child1.asList());\n        Json child2 = json.getJson(\"a.c\");\n        assertFalse(child2.isArray());\n        Map expected = Json.of(\"{ d: 1, e: 2 }\").asMap();\n        assertEquals(expected, child2.asMap());        \n    }\n    \n    @Test\n    void testGetAsJava() {\n        Map map = Json.parse(\"{ a: 1 }\");\n        assertEquals(map, Collections.singletonMap(\"a\", 1));\n        List list = Json.parse(\"[ 1, 2 ]\");\n        assertEquals(list, Arrays.asList(1, 2));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/JsonUtilsTest.java",
    "content": "package com.intuit.karate;\n\nimport com.intuit.karate.core.ComplexPojo;\nimport com.intuit.karate.core.SimplePojo;\nimport com.intuit.karate.core.Variable;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass JsonUtilsTest {\n\n    static final Logger logger = LoggerFactory.getLogger(JsonUtilsTest.class);\n\n    @Test\n    void testParse() {\n        String temp = JsonUtils.toStrictJson(\"{redirect:{url:'/index'}}\");\n        assertEquals(\"{\\\"redirect\\\":{\\\"url\\\":\\\"/index\\\"}}\", temp);\n    }\n\n    @Test\n    void testDetect() {\n        assertTrue(JsonUtils.isJson(\"{}\"));\n        assertTrue(JsonUtils.isJson(\"[]\"));\n        assertTrue(JsonUtils.isJson(\" {}\"));\n        assertTrue(JsonUtils.isJson(\" []\"));\n        assertFalse(JsonUtils.isJson(null));\n        assertFalse(JsonUtils.isJson(\"\"));\n    }\n\n    @Test\n    void testBeanConversion() {\n        SimplePojo pojo = new SimplePojo();\n        String s = JsonUtils.toJson(pojo);\n        Match.that(s).isEqualTo(\"{\\\"bar\\\":0,\\\"foo\\\":null}\");\n        Map<String, Object> map = Json.of(pojo).asMap();\n        Match.that(map).isEqualTo(\"{ foo: null, bar: 0 }\");\n    }\n\n    @Test\n    public void testPojoConversion() {\n        ComplexPojo pojo = new ComplexPojo();\n        pojo.setFoo(\"testFoo\");\n        pojo.setBar(1);\n        ComplexPojo p1 = new ComplexPojo();\n        p1.setFoo(\"p1\");\n        ComplexPojo p2 = new ComplexPojo();\n        p2.setFoo(\"p2\");\n        pojo.setBan(Arrays.asList(p1, p2));\n        String s = JsonUtils.toJson(pojo);\n        Match.that(s).isEqualTo(\"{\\\"bar\\\":1,\\\"foo\\\":\\\"testFoo\\\",\\\"baz\\\":null,\\\"ban\\\":[{\\\"bar\\\":0,\\\"foo\\\":\\\"p1\\\",\\\"baz\\\":null,\\\"ban\\\":null},{\\\"bar\\\":0,\\\"foo\\\":\\\"p2\\\",\\\"baz\\\":null,\\\"ban\\\":null}]}\");\n        ComplexPojo temp = (ComplexPojo) JsonUtils.fromJson(s, ComplexPojo.class.getName());\n        assertEquals(temp.getFoo(), \"testFoo\");\n        assertEquals(2, temp.getBan().size());\n        temp = JsonUtils.fromJson(s, ComplexPojo.class);\n        assertEquals(temp.getFoo(), \"testFoo\");\n        assertEquals(2, temp.getBan().size());\n        s = XmlUtils.toXml(pojo);\n        Match.that(s).isEqualTo(\"<root><bar>1</bar><foo>testFoo</foo><baz/><ban><bar>0</bar><foo>p1</foo><baz/><ban/></ban><ban><bar>0</bar><foo>p2</foo><baz/><ban/></ban></root>\");\n    }\n\n    @Test\n    void testDeepCopy() {\n        Map<String, Object> one = new HashMap();\n        Map<String, Object> two = new HashMap();\n        two.put(\"one\", one);\n        one.put(\"two\", two);\n        Object temp = JsonUtils.deepCopy(one);\n        assertEquals(temp, one);\n        assertFalse(temp == one);\n        String json = JsonUtils.toJsonSafe(temp, false);\n        assertEquals(\"{\\\"two\\\":{\\\"one\\\":{\\\"two\\\":{\\\"one\\\":\\\"#ref:java.util.HashMap\\\"}}}}\", json);\n    }\n\n    @Test\n    void testMalformed() {\n        String text = FileUtils.toString(getClass().getResourceAsStream(\"malformed.txt\"));\n        try {\n            Object o = JsonUtils.fromJsonStrict(text);\n            fail(\"we should not have reached here\");\n        } catch (Exception e) {\n            assertTrue(e.getCause() instanceof net.minidev.json.parser.ParseException);\n        }\n    }\n\n    @Test\n    void fromJsonStrictRetainsKeyOrder() {\n        String originalString = \"{\\\"tango\\\":\\\"Alice\\\",\\\"foxtrot\\\":\\\"0.0.0.0\\\",\\\"sierra\\\":\\\"Bob\\\"}\";\n        Object obj = JsonUtils.fromJsonStrict(originalString);\n                Variable var = new Variable(obj);\n        String serialized = var.getAsString();\n        assertEquals(originalString, serialized);\n    }\n\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/MainTest.java",
    "content": "package com.intuit.karate;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author peter\n */\nclass MainTest {\n    \n    @Test\n    void testNoDebug() {\n        Main options = Main.parseKarateArgs(List.of());\n        assertEquals(-1, options.debugPort);\n    }    \n    \n    @Test\n    void testDebug() {\n        Main options = Main.parseKarateArgs(List.of(\"-d\"));\n        assertEquals(0, options.debugPort);\n    }\n\n    @Test\n    void testKeepOriginalHeaders() {\n        Main options = Main.parseKarateArgs(List.of(\"--keep-original-headers\"));\n        assertEquals(true, options.keepOriginalHeaders);\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/MatchStepTest.java",
    "content": "package com.intuit.karate;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static com.intuit.karate.Match.Type.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass MatchStepTest {\n\n    private void test(String raw, Match.Type type, String name, String path, String expected) {\n        MatchStep step = new MatchStep(raw);\n        assertEquals(type, step.type);\n        assertEquals(name, step.name);\n        assertEquals(path, step.path);\n        assertEquals(expected, step.expected);\n    }\n\n    @Test\n    void testMatchStep() {\n        test(\"aXml //active == '#regex (false|true)'\", EQUALS, \"aXml\", \"//active\", \"'#regex (false|true)'\");\n        test(\"hello ==\", EQUALS, \"hello\", null, null);\n        test(\"hello world == foo\", EQUALS, \"hello\", \"world\", \"foo\");\n        test(\"hello world contains only deep foo\", CONTAINS_ONLY_DEEP, \"hello\", \"world\", \"foo\");\n        test(\"each hello world == foo\", EACH_EQUALS, \"hello\", \"world\", \"foo\");\n        test(\"each hello world contains deep foo\", EACH_CONTAINS_DEEP, \"hello\", \"world\", \"foo\");\n        test(\"{\\\"a\\\":1,\\\"b\\\":2} == '#object'\", EQUALS, \"({\\\"a\\\":1,\\\"b\\\":2})\", null, \"'#object'\");\n        test(\"hello.foo(bar) != blah\", NOT_EQUALS, \"hello.foo(bar)\", null, \"blah\");\n        test(\"foo count(/records//record) contains any blah\", CONTAINS_ANY, \"foo\", \"count(/records//record)\", \"blah\");\n        test(\"__arg == karate.get('foos[' + __loop + ']')\", EQUALS, \"__arg\", null, \"karate.get('foos[' + __loop + ']')\");\n        test(\"response $[?(@.b=='ab')] == '#[1]'\", EQUALS, \"response\", \"$[?(@.b=='ab')]\", \"'#[1]'\");\n        test(\"test != '#? _.length == 2'\", NOT_EQUALS, \"test\", null, \"'#? _.length == 2'\");\n        test(\"actual[0] !contains badSchema\", NOT_CONTAINS, \"actual[0]\", null, \"badSchema\");\n        test(\"actual[0] contains badSchema\", CONTAINS, \"actual[0]\", null, \"badSchema\");\n        test(\"driver.eval('{ foo: \\\"bar\\\" }') == { hello: 'world' }\", EQUALS, \"driver.eval('{ foo: \\\"bar\\\" }')\", null, \"{ hello: 'world' }\");\n        test(\"response.integration.serviceData['Usage Data'][0].Stage ==\", EQUALS, \"response.integration.serviceData['Usage Data'][0].Stage\", null, null);\n        test(\"response contains { foo: 'a any b' }\", CONTAINS, \"response\", null, \"{ foo: 'a any b' }\");\n        test(\"response.foo == 'a contains b'\", EQUALS, \"response.foo\", null, \"'a contains b'\");\n        test(\"$.addOns[?(@.entitlementStateChangeReason=='RESUBSCRIBE')].addOnOfferID contains only toAddOnOfferIDs\", CONTAINS_ONLY, \"$.addOns[?(@.entitlementStateChangeReason=='RESUBSCRIBE')].addOnOfferID\", null, \"toAddOnOfferIDs\");\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/MatchTest.java",
    "content": "package com.intuit.karate;\n\nimport static com.intuit.karate.Match.Type.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport com.intuit.karate.Match;\nimport com.intuit.karate.Match.Type;\nimport com.intuit.karate.graal.JsEngine;\nimport java.math.BigDecimal;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\nimport com.intuit.karate.MatchOperation;\nimport net.minidev.json.JSONArray;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * @author pthomas3\n */\nclass MatchTest {\n\n    static final Logger logger = LoggerFactory.getLogger(MatchTest.class);\n\n    private static final boolean FAILS = true;\n\n    private void match(Object actual, String mt, Object expected) {\n        match(actual, Match.Type.valueOf(mt), expected, false);\n    }\n\n    private void match(Object actual, Match.Type mt, Object expected) {\n        match(actual, mt, expected, false);\n    }\n\n    String message;\n\n    private void message(String expected) {\n        assertEquals(expected, message);\n    }\n\n    private void log() {\n        logger.debug(\"{}\", message);\n    }\n\n    private void match(Object actual, String mt, Object expected, boolean fails) {\n        match(actual, Type.valueOf(mt), expected, fails);\n    }\n\n    private void match(Object actual, Match.Type mt, Object expected, boolean fails) {\n        Match.Result mr = Match.evaluate(actual).is(mt, expected);\n        message = mr.message;\n        if (!fails) {\n            assertTrue(mr.pass, mr.message);\n        } else {\n            assertFalse(mr.pass);\n        }\n    }\n\n    @Test\n    void testApi() {\n        assertTrue(Match.that(null).isEqualTo(null).pass);\n    }\n\n    @Test\n    void testNull() {\n        match(null, EQUALS, null);\n        match(\"\", EQUALS, null, FAILS);\n        message(\"\"\"\n                match failed: EQUALS\n                  $ | data types don't match (STRING:NULL)\n                  ''\n                  null\n                \"\"\");\n        match(\"\", NOT_EQUALS, null);\n        match(null, NOT_EQUALS, null, FAILS);\n    }\n\n    @Test\n    void testBoolean() {\n        match(true, EQUALS, true);\n        match(false, EQUALS, false);\n        match(true, EQUALS, false, FAILS);\n        match(true, NOT_EQUALS, false);\n        match(true, NOT_EQUALS, true, FAILS);\n    }\n\n    @Test\n    void testNumber() {\n        match(1, EQUALS, 1);\n        match(0.1, EQUALS, .100);\n        match(100, EQUALS, 200, FAILS);\n        match(100, NOT_EQUALS, 2000);\n        match(300, NOT_EQUALS, 300, FAILS);\n    }\n\n    @Test\n    void testBigDecimal() {\n        match(new BigDecimal(\"1000\"), EQUALS, 1000);\n        match(300, NOT_EQUALS, new BigDecimal(\"300\"), FAILS);\n    }\n\n    @Test\n    void testString() {\n        match(\"hello\", EQUALS, \"hello\");\n        match(\"foo\", EQUALS, \"bar\", FAILS);\n    }\n\n    @Test\n    void testStringContains() {\n        match(\"hello\", CONTAINS, \"hello\");\n        match(\"hello\", NOT_CONTAINS, \"hello\", FAILS);\n        match(\"foobar\", CONTAINS, \"bar\");\n        match(\"foobar\", CONTAINS, \"baz\", FAILS);\n        match(\"foobar\", NOT_CONTAINS, \"baz\");\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\"EQUALS\", \"CONTAINS\", \"CONTAINS_DEEP\"})\n    void testStringStartingWithHash(String matchType) {\n        match(\"#bob\", matchType, \"#bob\");\n    }\n\n    @Test\n    void testBytes() {\n        match(\"hello\".getBytes(), EQUALS, \"hello\".getBytes());\n        match(\"hello\".getBytes(), NOT_EQUALS, \"helloo\".getBytes());\n        match(\"hello\".getBytes(), NOT_EQUALS, \"hello\".getBytes(), FAILS);\n    }\n\n    @Test\n    void testJavaSet() {\n        Set<String> set = new HashSet();\n        set.add(\"foo\");\n        set.add(\"bar\");\n        Match.that(set).containsOnly(\"['foo', 'bar']\");\n    }\n\n    @Test\n    void testJavaArray() {\n        String[] strArray = new String[]{\"foo\", \"bar\"};\n        Match.that(strArray).containsOnly(\"['foo', 'bar']\");\n        int[] intArray = new int[]{1, 2, 3};\n        Match.that(intArray).isEqualTo(\"[1, 2, 3]\");\n    }\n\n    @Test\n    void testNotEquals() {\n        match(\"[1, 2]\", NOT_EQUALS, \"#[1]\");\n        match(\"[1, 2]\", NOT_EQUALS, \"#[]? _ > 2\");\n        log();\n    }\n\n    @Test\n    void testList() {\n        match(\"[1, 2, 3]\", EQUALS, \"[1, 2, 3]\");\n        match(\"[1, 2, 3]\", NOT_EQUALS, \"[1, 2, 4]\");\n        match(\"[1, 2]\", EQUALS, \"[1, 2, 4]\", FAILS);\n        match(\"[1, 2, 3]\", CONTAINS, \"[1, 2, 3]\");\n        match(\"[1, 2, 3]\", CONTAINS_ONLY, \"[1, 2, 3]\");\n        match(\"[1, 2, 3]\", CONTAINS_ONLY, \"[3, 2, 1]\");\n        match(\"[1, 5, 10]\", CONTAINS_ONLY, \"[10, 5, 1]\");\n        match(\"[4, 2]\", CONTAINS_ONLY, \"[2, 4]\");\n        match(\"[5]\", CONTAINS_ONLY, \"[5]\");\n        match(\"[4, 4]\", CONTAINS_ONLY, \"[4, 4]\");\n        match(\"[1, 2, 2]\", CONTAINS_ONLY, \"[2, 2, 1]\");\n        match(\"[1, 2, 3]\", CONTAINS_ONLY, \"[2, 2, 3]\", FAILS);\n        match(\"[2, 3, 2]\", CONTAINS_ONLY, \"[2, 2, 3]\");\n        match(\"[2, 2, 3]\", CONTAINS_ONLY, \"[1, 2, 3]\", FAILS);\n        match(\"[1, 4, 7]\", CONTAINS_ONLY, \"[4, 7]\", FAILS);\n        match(\"[1, 2, 3]\", CONTAINS, \"[1, 2, 4]\", FAILS);\n        match(\"[1, 2, 3]\", NOT_CONTAINS, \"[1, 2, 4]\");\n        match(\"[1, 2, 3]\", CONTAINS_ANY, \"[1, 2, 4]\");\n        match(\"[1, 2, 3]\", CONTAINS_ANY, \"[1, 2, 4, 5]\");\n        match(\"[{ a: 1 }, { b: 2 }, { c: 3 }]\", EQUALS, \"[{ a: 1 }, { b: 2 }, { c: 3 }]\");\n        match(\"[{ a: 1 }, { b: 2 }, { c: 3 }]\", EQUALS, \"[{ a: 1 }, { b: 2 }, { c: 4 }]\", FAILS);\n        match(\"[{ a: 1 }, { b: 2 }, { c: 3 }]\", CONTAINS, \"[{ a: 1 }, { b: 2 }, { c: 3 }]\");\n        match(\"[{ a: 1 }, { b: 2 }, { c: 3 }]\", CONTAINS_ONLY, \"[{ a: 1 }, { b: 2 }, { c: 3 }]\");\n        match(\"[{ a: 1 }, { b: 2 }, { c: 3 }]\", CONTAINS, \"[{ a: 1 }, { c: 3 }]\");\n        match(\"[{ a: 1 }, { b: 2 }, { c: 3 }]\", CONTAINS_ANY, \"[{ a: 9 }, { c: 3 }]\");\n        match(\"[{ a: 1 }, { b: 2 }, { c: 3 }]\", CONTAINS_ANY, \"[{ a: 9 }, { c: 9 }]\", FAILS);\n        match(\"[{ a: 1 }, { b: 2 }, { c: 3 }]\", CONTAINS_DEEP, \"[{ a: 1 }, { c: 3 }]\");\n        match(\"[{ a: 1 }, { b: [1, 2, 3] }]\", CONTAINS_DEEP, \"[{ b: [2] }]\");\n        match(\"{ a: { foo: 'bar' } }\", CONTAINS_DEEP, \"{ a: '#object' }\");\n    }\n\n    @Test\n    void testContainsOnlyDeep() {\n        match(\"{ a: [1, 2, 3] }\", CONTAINS_ONLY_DEEP, \"{ a: [1, 2, 3], b: 4 }\", FAILS);\n        match(\"{ a: [1, 2, 3] }\", CONTAINS_ONLY_DEEP, \"{ a: [3, 2, 1] }\");\n        match(\"[{a: [1, 2, 3]}]\", CONTAINS_ONLY_DEEP, \"[{a: [3, 2, 1]}]\");\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\"CONTAINS\", \"CONTAINS_DEEP\"})\n    void testListContains(String containsType) {\n        match(\"['foo', 'bar']\", containsType, \"baz\", FAILS);\n        message(\"\"\"\n                match failed: %s\n                  $ | actual does not contain expected | actual array does not contain expected item - baz (LIST:STRING)\n                  [\"foo\",\"bar\"]\n                  'baz'\n                \n                    $[1] | not equal (STRING:STRING)\n                    'bar'\n                    'baz'\n                \n                    $[0] | not equal (STRING:STRING)\n                    'foo'\n                    'baz'\n                \"\"\".formatted(containsType));\n        match(\"['foo', 'bar']\", containsType, \"['baz']\", FAILS);\n        message(\"\"\"\n                match failed: %s\n                  $ | actual does not contain expected | actual array does not contain expected item - baz (LIST:LIST)\n                  [\"foo\",\"bar\"]\n                  [\"baz\"]\n                \n                    $[1] | not equal (STRING:STRING)\n                    'bar'\n                    'baz'\n                \n                    $[0] | not equal (STRING:STRING)\n                    'foo'\n                    'baz'\n                \"\"\".formatted(containsType));\n    }\n\n    @Test\n    void testListContainsRegex() {\n        match(\"['foo', 'bar']\", CONTAINS, \"#regex .{3}\");\n        match(\"['foo', 'bar']\", CONTAINS_DEEP, \"#regex .{3}\");\n        match(\"['foo', 'bar']\", CONTAINS_ANY, \"#regex .{3}\");\n        match(\"['foo', 'bar']\", CONTAINS_ANY_DEEP, \"#regex .{3}\");\n        match(\"{ array: ['foo', 'bar'] }\", EQUALS, \"{ array: '#[] #regex .{3}' }\");\n        match(\"{ array: ['foo', 'bar'] }\", CONTAINS, \"{ array: '#[] #regex .{3}' }\");\n        match(\"{ array: ['foo', 'bar'] }\", CONTAINS_DEEP, \"{ array: '#[] #regex .{3}' }\");\n        match(\"{ array: ['foo', 'bar'] }\", CONTAINS_DEEP, \"{ array: '#array' }\");\n        match(\"{ array: ['foo', 'bar'] }\", CONTAINS_ANY, \"{ array: '#[] #regex .{3}' }\");\n        match(\"{ array: ['foo', 'bar'] }\", CONTAINS_ANY_DEEP, \"{ array: '#[] #regex .{3}' }\");\n\n        match(\"['foo', 'barr']\", CONTAINS, \"#regex .{4}\");\n        match(\"['foo', 'barr']\", CONTAINS_ANY, \"#regex .{4}\");\n\n        match(\"['foo', 'bar']\", CONTAINS, \"#regex .{4}\", FAILS);\n        message(\"\"\"\n                match failed: CONTAINS\n                  $ | actual does not contain expected | actual array does not contain expected item - #regex .{4} (LIST:STRING)\n                  [\"foo\",\"bar\"]\n                  '#regex .{4}'\n                \n                    $[1] | regex match failed (STRING:STRING)\n                    'bar'\n                    '#regex .{4}'\n                \n                    $[0] | regex match failed (STRING:STRING)\n                    'foo'\n                    '#regex .{4}'\n                \"\"\");\n\n    }\n\n    @Test\n    void testListNotContains() {\n        match(\"['foo', 'bar']\", NOT_CONTAINS, \"baz\");\n        match(\"['foo', 'bar']\", NOT_CONTAINS, \"bar\", FAILS);\n        message(\"\"\"\n                match failed: NOT_CONTAINS\n                  $ | actual contains expected (LIST:STRING)\n                  [\"foo\",\"bar\"]\n                  'bar'\n                \n                    $[0] | not equal (STRING:STRING)\n                    'foo'\n                    'bar'\n                \"\"\");\n        match(\n                \"[{ foo: 1 }, { foo: 2 }, { foo: 3 }]\",\n                CONTAINS,\n                \"[{ foo: 0 }, { foo: 2 }, { foo: 3 }]\",\n                FAILS);\n        message(\"\"\"\n                match failed: CONTAINS\n                  $ | actual does not contain expected | actual array does not contain expected item - {\"foo\":0} (LIST:LIST)\n                  [{\"foo\":1},{\"foo\":2},{\"foo\":3}]\n                  [{\"foo\":0},{\"foo\":2},{\"foo\":3}]\n                \n                    $[2] | not equal | match failed for name: 'foo' (MAP:MAP)\n                    {\"foo\":3}\n                    {\"foo\":0}\n                \n                      $[2].foo | not equal (NUMBER:NUMBER)\n                      3\n                      0\n                \n                        $[1] | not equal | match failed for name: 'foo' (MAP:MAP)\n                        {\"foo\":2}\n                        {\"foo\":0}\n                \n                          $[1].foo | not equal (NUMBER:NUMBER)\n                          2\n                          0\n                \n                            $[0] | not equal | match failed for name: 'foo' (MAP:MAP)\n                            {\"foo\":1}\n                            {\"foo\":0}\n                \n                              $[0].foo | not equal (NUMBER:NUMBER)\n                              1\n                              0\n                \"\"\"); // TODO improve error message for this case\n    }\n\n    @Test\n    void testEach() {\n        match(\"[1, 2, 3]\", EACH_EQUALS, \"#number\");\n        match(\"[1, 2, 3]\", EACH_EQUALS, \"#number? _ > 0\");\n        match(\"[1, 2, 3]\", EACH_EQUALS, \"#number? _ < 2\", FAILS);\n        String expected = \"\"\"\n                match failed: EACH_EQUALS\n                  $ | match each failed at index 1 (LIST:STRING)\n                  [1,2,3]\n                  '#number? _ < 2'\n                \n                    $[1] | evaluated to 'false' (NUMBER:STRING)\n                    2\n                    '#number? _ < 2'\n                \"\"\";\n        message(expected);\n        match(\"[1, 'a', 3]\", EACH_EQUALS, \"#number\", FAILS);\n        expected = \"\"\"\n                match failed: EACH_EQUALS\n                  $ | match each failed at index 1 (LIST:STRING)\n                  [1,\"a\",3]\n                  '#number'\n                \n                    $[1] | not a number (STRING:STRING)\n                    'a'\n                    '#number'\n                \"\"\";\n        message(expected);\n        match(\"[{ a: 1 }, { a: 2 }]\", EACH_EQUALS, \"#object\");\n        match(\"[{ a: 1 }, { a: 2 }]\", EACH_EQUALS, \"{ a: '#number' }\");\n    }\n\n    @Test\n    void testEachEmpty() {\n        match(\"[]\", EACH_EQUALS, \"#number\", FAILS);\n        message(\"\"\"\n                match failed: EACH_EQUALS\n                  $ | match each failed, empty array / list (LIST:STRING)\n                  []\n                  '#number'\n                \"\"\");\n    }\n\n    @Test\n    void testEachWithMagicVariables() {\n        match(\"[{a: 1, b: 2}, {a: 2, b: 4}]\", EACH_EQUALS, \"{ a: '#number', b: '#(_$.a * 2)' }\");\n        match(\"[{a: 1, b: 2}, {a: 2, b: 4}]\", EACH_EQUALS, \"{ a: '#number', b: '#? _ == _$.a * 2' }\");\n        match(\"[{a: 1, b: 2}, {a: 2, b: 4}]\", EACH_CONTAINS, \"{ b: '#(_$.a * 2)' }\");\n        match(\"[{a: 1, b: 2}, {a: 2, b: 4}]\", EACH_CONTAINS, \"{ b: '#? _ == _$.a * 2' }\");\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\"EQUALS\", \"CONTAINS\", \"CONTAINS_DEEP\"})\n    void testArray(String matchType) {\n        match(\"[{ a: 1 }, { a: 2 }]\", matchType, \"#[2]\");\n        match(\"[{ a: 1 }, { a: 2 }]\", matchType, \"#[] #object\");\n    }\n\n    @Test\n    void testIssue2515() {\n\n        Json cat = Json.of(\n                \"\"\"\n                        {\n                          name: 'Billie',\n                          kittens: [\n                            { id: 23, name: 'Bob', bla: [{ b: '1'}] },\n                            { id: 42, name: 'Wild' }\n                          ]\n                        }\n                        \"\"\");\n        Json expectedKittens1 = Json.of(\"[{ id: 42, name: 'Wild' }, { id: 23, name: 'Bob', bla: [{ b: '1'}]}]\");\n        JsEngine.global().put(\"expectedKittens1\", expectedKittens1.asList());\n        match(cat.asMap(), EQUALS, Json.of(\"{ name: 'Billie', kittens: '#(^^expectedKittens1)' }\").asMap());\n\n        Json expectedKittens2 = Json.of(\"[{ id: 42, name: 'Wild' }, { id: 23, name: 'Bob', bla: { b: '1'}}]\");\n        JsEngine.global().put(\"expectedKittens2\", expectedKittens2.asList());\n        match(cat.asMap(), EQUALS, Json.of(\"{ name: 'Billie', kittens: '#(^^expectedKittens2)' }\").asMap(), true);\n    }\n\n    @ParameterizedTest\n    @CsvSource(value = {\n            \"EQUALS;EACH_EQUALS\",\n            \"CONTAINS;EACH_CONTAINS\",\n            \"CONTAINS_DEEP;EACH_CONTAINS_DEEP\"}, delimiter = ';')\n    void testSchema(String matchType, String matchEachType) {\n        Json json = Json.of(\"{ a: '#number' }\");\n        Map map = json.asMap();\n        match(\"[{ a: 1 }, { a: 2 }]\", matchEachType, map);\n        JsEngine.global().put(\"schema\", map);\n        match(\"[{ a: 1 }, { a: 2 }]\", matchType, \"#[] schema\");\n        match(\"{ a: 'x', b: { c: 'y' } }\", matchType, \"{ a: '#string', b: { c: '#string' } }\");\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\"EQUALS\", \"CONTAINS\", \"CONTAINS_DEEP\"})\n    void testSchemaOptionalObject(String matchType) {\n        Json part = Json.of(\"{ bar: '#string' }\");\n        JsEngine.global().put(\"part\", part.asMap());\n        match(\"{ foo: null }\", matchType, \"{ foo: '##(bar)' }\");\n    }\n\n\n    @Test\n    void testMap() {\n        match(\"{ a: 1, b: 2, c: 3 }\", CONTAINS, \"{}\");\n        match(\"{ a: 1, b: 2, c: 3 }\", EQUALS, \"{ b: 2, c: 3, a: 1 }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", CONTAINS, \"{ b: 2, c: 3, a: 1 }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", CONTAINS_ONLY, \"{ b: 2, c: 3, a: 1 }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", CONTAINS_DEEP, \"{ c: 3, a: 1 }\");\n        match(\"{ a: 1, b: 2, c: [1, 2] }\", CONTAINS_DEEP, \"{ a: 1, c: [2] }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", CONTAINS, \"{ b: 2 }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", CONTAINS, \"{ }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", CONTAINS_ANY, \"{ }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", CONTAINS_DEEP, \"{ }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", CONTAINS_ANY, \"{ z: 9, b: 2 }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", CONTAINS, \"{ z: 9, x: 2 }\", FAILS);\n\n        message(\"\"\"\n                match failed: CONTAINS\n                  $ | actual does not contain expected | actual does not contain key - 'z' (MAP:MAP)\n                  {\"a\":1,\"b\":2,\"c\":3}\n                  {\"z\":9,\"x\":2}\n                \n                \"\"\");\n        match(\"{ a: 1, b: 2, c: 3 }\", CONTAINS_ANY, \"{ z: 9, x: 2 }\", FAILS);\n        message(\"\"\"\n                match failed: CONTAINS_ANY\n                  $ | actual does not contain expected | no key-values matched (MAP:MAP)\n                  {\"a\":1,\"b\":2,\"c\":3}\n                  {\"z\":9,\"x\":2}\n                \n                    $.x | data types don't match (NULL:NUMBER)\n                    null\n                    2\n                \n                    $.z | data types don't match (NULL:NUMBER)\n                    null\n                    9\n                \"\"\");\n        match(\"{ a: 1, b: 2, c: 3 }\", NOT_CONTAINS, \"{ a: 1 }\", FAILS);\n        message(\"\"\"\n                match failed: NOT_CONTAINS\n                  $ | actual contains expected (MAP:MAP)\n                  {\"a\":1,\"b\":2,\"c\":3}\n                  {\"a\":1}\n                \"\"\");\n        match(\"{ a: 1, b: 2, c: 3 }\", NOT_CONTAINS, \"{}\");\n\n    }\n\n    @Test\n    void testJsonFailureMessages() {\n        match(\"{ a: 1, b: 2, c: 3 }\", EQUALS, \"{ a: 1, b: 9, c: 3 }\", FAILS);\n        String expected = \"\"\"\n                match failed: EQUALS\n                  $ | not equal | match failed for name: 'b' (MAP:MAP)\n                  {\"a\":1,\"b\":2,\"c\":3}\n                  {\"a\":1,\"b\":9,\"c\":3}\n                \n                    $.b | not equal (NUMBER:NUMBER)\n                    2\n                    9\n                \"\"\";\n        message(expected);\n        match(\"{ a: { b: { c: 1 } } }\", EQUALS, \"{ a: { b: { c: 2 } } }\", FAILS);\n        expected = \"\"\"\n                match failed: EQUALS\n                  $ | not equal | match failed for name: 'a' (MAP:MAP)\n                  {\"a\":{\"b\":{\"c\":1}}}\n                  {\"a\":{\"b\":{\"c\":2}}}\n                \n                    $.a | not equal | match failed for name: 'b' (MAP:MAP)\n                    {\"b\":{\"c\":1}}\n                    {\"b\":{\"c\":2}}\n                \n                      $.a.b | not equal | match failed for name: 'c' (MAP:MAP)\n                      {\"c\":1}\n                      {\"c\":2}\n                \n                        $.a.b.c | not equal (NUMBER:NUMBER)\n                        1\n                        2\n                \"\"\";\n        message(expected);\n    }\n\n    @Test\n    void testXmlFailureMessages() {\n        match(\"<a><b><c>1</c></b></a>\", EQUALS, \"<a><b><c>2</c></b></a>\", FAILS);\n        String expected = \"\"\"\n                match failed: EQUALS\n                  / | not equal | match failed for name: 'a' (XML:XML)\n                  <a><b><c>1</c></b></a>\n                  <a><b><c>2</c></b></a>\n                \n                    /a | not equal | match failed for name: 'b' (MAP:MAP)\n                    <b><c>1</c></b>\n                    <b><c>2</c></b>\n                \n                      /a/b | not equal | match failed for name: 'c' (MAP:MAP)\n                      <c>1</c>\n                      <c>2</c>\n                \n                        /a/b/c | not equal (STRING:STRING)\n                        1\n                        2\n                \"\"\";\n        message(expected);\n        match(\"<hello foo=\\\"bar\\\">world</hello>\", EQUALS, \"<hello foo=\\\"baz\\\">world</hello>\", FAILS);\n        expected = \"\"\"\n                match failed: EQUALS\n                  / | not equal | match failed for name: 'hello' (XML:XML)\n                  <hello foo=\"bar\">world</hello>\n                  <hello foo=\"baz\">world</hello>\n                \n                    /hello/@foo | not equal (STRING:STRING)\n                    bar\n                    baz\n                \"\"\";\n        message(expected);\n    }\n\n    @Test\n    void testMapFuzzyIgnores() {\n        match(\"{ a: 1, b: 2, c: 3 }\", EQUALS, \"{ b: 2, c: 3, z: '#ignore', a: 1 }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", EQUALS, \"{ b: 2, c: 3, z: '#notpresent', a: 1 }\");\n        match(\n                \"{ a: 1, b: 2, c: 3 }\",\n                EQUALS,\n                \"{ b: 2, c: 3, z: '##anything', a: 1 }\"); // not really correct, TODO !\n    }\n\n    @Test\n    void testMapFuzzy() {\n        match(\"{ a: 1, b: 2, c: 3 }\", EQUALS, \"{ b: 2, c: '#number', a: 1 }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", EQUALS, \"{ b: 2, c: '#present', a: 1 }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", EQUALS, \"{ b: 2, c: '#notnull', a: 1 }\");\n        match(\"{ a: 1, b: 2, c: 3 }\", EQUALS, \"{ b: 2, c: '#null', a: 1 }\", FAILS);\n        String expected = \"\"\"\n                match failed: EQUALS\n                  $ | not equal | match failed for name: 'c' (MAP:MAP)\n                  {\"a\":1,\"b\":2,\"c\":3}\n                  {\"a\":1,\"b\":2,\"c\":\"#null\"}\n                \n                    $.c | not null (NUMBER:STRING)\n                    3\n                    '#null'\n                \"\"\";\n        message(expected);\n        match(\"{ a: 1, b: 2, c: 3 }\", EQUALS, \"{ b: 2, c: '#string', a: 1 }\", FAILS);\n        expected = \"\"\"\n                match failed: EQUALS\n                  $ | not equal | match failed for name: 'c' (MAP:MAP)\n                  {\"a\":1,\"b\":2,\"c\":3}\n                  {\"a\":1,\"b\":2,\"c\":\"#string\"}\n                \n                    $.c | not a string (NUMBER:STRING)\n                    3\n                    '#string'\n                \"\"\";\n        message(expected);\n        match(\"{ a: 1, b: 2, c: 3 }\", EQUALS, \"{ b: 2, c: '#notpresent', a: 1 }\", FAILS);\n        expected = \"\"\"\n                match failed: EQUALS\n                  $ | not equal | match failed for name: 'c' (MAP:MAP)\n                  {\"a\":1,\"b\":2,\"c\":3}\n                  {\"a\":1,\"b\":2,\"c\":\"#notpresent\"}\n                \n                    $.c | present (NUMBER:STRING)\n                    3\n                    '#notpresent'\n                \"\"\";\n        message(expected);\n        match(\"{ a: 1, b: 'foo', c: 2 }\", EQUALS, \"{ b: '#regex foo', c: 2, a: 1 }\");\n        match(\"{ a: 1, b: 'foo', c: 2 }\", EQUALS, \"{ b: '#regex .+', c: 2, a: 1 }\");\n        match(\"{ a: 1, b: 'foo', c: 2 }\", EQUALS, \"{ b: '#regex .{3}', c: 2, a: 1 }\");\n        match(\"{ a: 1, b: 'foo', c: 2 }\", EQUALS, \"{ b: '#regex .{2}', c: 2, a: 1 }\", FAILS);\n        expected = \"\"\"\n                match failed: EQUALS\n                  $ | not equal | match failed for name: 'b' (MAP:MAP)\n                  {\"a\":1,\"b\":\"foo\",\"c\":2}\n                  {\"a\":1,\"b\":\"#regex .{2}\",\"c\":2}\n                \n                    $.b | regex match failed (STRING:STRING)\n                    'foo'\n                    '#regex .{2}'\n                \"\"\";\n        message(expected);\n    }\n\n    @Test\n    void testNotPresentOnLhs() {\n        match(\"#notpresent\", EQUALS, 2, FAILS);\n        message(\"\"\"\n                match failed: EQUALS\n                  $ | actual path does not exist (STRING:NUMBER)\n                  '#notpresent'\n                  2\n                \"\"\");\n        match(\"#notpresent\", EQUALS, \"foo\", FAILS);\n        message(\"\"\"\n                match failed: EQUALS\n                  $ | actual path does not exist (STRING:STRING)\n                  '#notpresent'\n                  'foo'\n                \"\"\");\n    }\n\n    @Test\n    void testXml() {\n        match(\"<root>foo</root>\", EQUALS, \"<root>foo</root>\");\n        match(\"<root>foo</root>\", CONTAINS, \"<root>foo</root>\");\n        match(\"<root>foo</root>\", EQUALS, \"<root>bar</root>\", FAILS);\n        match(\"<root>foo</root>\", CONTAINS, \"<root>bar</root>\", FAILS);\n        match(\"<root><a>1</a><b>2</b></root>\", EQUALS, \"<root><a>1</a><b>2</b></root>\");\n        match(\"<root><a>1</a><b>2</b></root>\", EQUALS, \"<root><b>2</b><a>1</a></root>\");\n        match(\"<root><a>1</a><b>2</b></root>\", CONTAINS, \"<root><b>2</b><a>1</a></root>\");\n        match(\"<root><a>1</a><b>2</b></root>\", CONTAINS, \"<root><a>1</a><b>9</b></root>\", FAILS);\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\"EQUALS\", \"CONTAINS\", \"CONTAINS_DEEP\"})\n    void testXmlSchema(String matchType) {\n        match(\"<root></root>\", matchType, \"<root>#null</root>\"); // TODO controversial\n        match(\"<root></root>\", matchType, \"<root>#present</root>\");\n        match(\n                \"<root><a>x</a><b><c>y</c></b></root>\",\n                matchType,\n                \"<root><a>#string</a><b><c>#string</c></b></root>\");\n        match(\n                \"<root><a>x</a><b></b></root>\",\n                matchType,\n                \"<root><a>#string</a><b><c>#string</c></b></root>\",\n                FAILS);\n        match(\n                \"<root><a>x</a><b><c></c></b></root>\",\n                matchType,\n                \"<root><a>#string</a><b><c>#string</c></b></root>\",\n                FAILS);\n        match(\n                \"<root><a>x</a><b><c>y</c></b></root>\",\n                matchType,\n                \"<root><a>#string</a><b><c>#string</c></b></root>\");\n    }\n\n    @Test\n    void testApiUsage() {\n        Match.that(\"[1, 2, 3]\").contains(2);\n        Match.that(\"[1, 2, 3]\").isEachEqualTo(\"#number\");\n        Match.that(\"[1, 2, 3]\").containsOnly(\"[3, 2, 1]\");\n        Match.that(\"{ a: 1, b: 2 }\").contains(\"{ b: 2 }\");\n        Match.that(\"{ a: 1, b: 2, c: { d: 3, e: 4} }\").containsDeep(\"{ b: 2, c: { e: 4 } }\");\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\"EQUALS\", \"CONTAINS\", \"CONTAINS_DEEP\"})\n    void testRegex(String matchType) {\n        match(\n                \"{ number: '/en/search?q=test' }\",\n                matchType,\n                \"{ number: '#regex /\\\\\\\\w{2}/search\\\\\\\\?q=(.*)+' }\");\n        match(\n                \"{ number: '/us/search?q=test' }\",\n                matchType,\n                \"{ number: '#regex /\\\\\\\\w{2}/search\\\\\\\\?q=(.*)+' }\");\n        match(\n                \"{ number: '/en/search?q=test+whatever' }\",\n                matchType,\n                \"{ number: '#regex /\\\\\\\\w{2}/search\\\\\\\\?q=(.*)+' }\");\n    }\n\n    @Test\n    void testOptionalNotEquals() {\n\n        match(\"{ a: 1}\", NOT_EQUALS, \" { a: '#null' }\");\n        match(\"{ a: 1}\", NOT_EQUALS, \" { a: '##null' }\");\n        match(\"{ a: null}\", NOT_EQUALS, \" { a: '#notnull' }\");\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\"EQUALS\", \"CONTAINS\", \"CONTAINS_DEEP\"})\n    void testOptional(String matchType) {\n        match(\"{ number: '1234' }\", matchType, \"{ number: '##regex \\\\\\\\d+' }\");\n        match(\"{ }\", matchType, \"{ number: '##regex \\\\\\\\d+' }\");\n        match(\"{ 'foo': 'bar' }\", matchType, \"{ foo: '#string', number: '##regex \\\\\\\\d+' }\");\n        match(\"{ number: null }\", matchType, \"{ number: '##regex \\\\\\\\d+' }\");\n\n        match(\"{ number: 1234 }\", matchType, \"{ number: '##number' }\");\n        match(\"{ }\", matchType, \"{ number: '##number' }\");\n        match(\"{ 'foo': 'bar' }\", matchType, \"{ foo: '#string', number: '##number' }\");\n        match(\"{ number: null }\", matchType, \"{ number: '##number' }\");\n\n        match(\"{ 'foo': 'bar' }\", matchType, \"{ 'foo': '##string' }\");\n        match(\"{ }\", matchType, \"{ foo: '##string' }\");\n        match(\"{ 'foo': 'bar' }\", matchType, \"{ 'foo': '#string', 'bar': '##string' }\");\n        match(\"{ 'foo': null }\", matchType, \"{ 'foo': '##string' }\");\n\n        match(\"{ 'foo': 'a9f7a56b-8d5c-455c-9d13-808461d17b91' }\", matchType, \"{ 'foo': '##uuid' }\");\n        match(\"{ }\", matchType, \"{ foo: '##uuid' }\");\n        match(\"{ 'foo': 'bar' }\", matchType, \"{ 'foo': '#string', 'bar': '##uuid' }\");\n        match(\"{ 'foo': null }\", matchType, \"{ 'foo': '##uuid' }\");\n\n        match(\"{ 'foo': true }\", matchType, \"{ 'foo': '##boolean' }\");\n        match(\"{ }\", matchType, \"{ foo: '##string' }\");\n        match(\"{ 'foo': 'bar' }\", matchType, \"{ 'foo': '#string', 'bar': '##boolean' }\");\n        match(\"{ 'foo': null }\", matchType, \"{ 'foo': '##boolean' }\");\n\n        match(\"{ 'foo': { 'bar': 'bar' } }\", matchType, \"{ 'foo': '##object' }\");\n        match(\"{ }\", matchType, \"{ foo: '##object' }\");\n        match(\n                \"{ 'foo': 'test', 'bar': { 'bar': 'bar' } }\",\n                matchType,\n                \"{ 'foo': '#string', 'bar': '##object' }\");\n        match(\"{ 'foo': null }\", matchType, \"{ 'foo': '##object' }\");\n\n        match(\"{ 'foo': [ { 'bar': 'bar' }] }\", matchType, \"{ 'foo': '##array' }\");\n        match(\"{ }\", matchType, \"{ 'foo': '##array' }\");\n        match(\n                \"{ 'foo': 'test', 'bar' : [ { 'bar': 'bar' } ] }\",\n                matchType,\n                \"{ 'foo': '#string', 'bar': '##array' }\");\n\n\n        match(\"{ 'foo': null }\", matchType, \"{ 'foo': '##array' }\");\n\n        match(\"{ a: null}\", matchType, \" { a: '##notnull' }\");\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/SimplePojo.java",
    "content": "package com.intuit.karate;\n\n/**\n *\n * @author pthomas3\n */\npublic class SimplePojo {\n    \n    private String foo;\n    private int bar;\n\n    public String getFoo() {\n        return foo;\n    }\n\n    public void setFoo(String foo) {\n        this.foo = foo;\n    }\n\n    public int getBar() {\n        return bar;\n    }\n\n    public void setBar(int bar) {\n        this.bar = bar;\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/StringUtilsTest.java",
    "content": "package com.intuit.karate;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport com.intuit.karate.StringUtils.Pair;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass StringUtilsTest {\n\n    @Test\n    void testPair() {\n        assertEquals(new Pair(\"foo\", \"bar\"), StringUtils.pair(\"foo\", \"bar\"));\n    }\n\n    @Test\n    void testTrimToEmpty() {\n        assertEquals(\"\", StringUtils.trimToEmpty(null));\n        assertEquals(\"foo\", StringUtils.trimToEmpty(\"   foo   \"));\n    }\n\n    @Test\n    void testTrimToNull() {\n        assertNull(StringUtils.trimToNull(null));\n        assertNull(StringUtils.trimToNull(\"   \"));\n        assertEquals(\"foo\", StringUtils.trimToNull(\"   foo   \"));\n    }\n\n    @Test\n    void testRepeat() {\n        assertEquals(\"\\u0000\", StringUtils.repeat('\\u0000', 1));\n        assertEquals(\"aaaaa\", StringUtils.repeat('a', 5));\n        assertEquals(\"\", StringUtils.repeat('\\u0000', 0));\n    }\n\n    @Test\n    void testSplit() {\n        List<String> list = StringUtils.split(\"\", '/', false);\n        assertEquals(1, list.size());\n        assertEquals(\"\", list.get(0));\n        list = StringUtils.split(\"//\", '/', false);\n        assertEquals(0, list.size());\n        list = StringUtils.split(\"foo\", '/', false);\n        assertEquals(1, list.size());\n        assertEquals(\"foo\", list.get(0));\n        list = StringUtils.split(\"foo/\", '/', false);\n        assertEquals(1, list.size());\n        assertEquals(\"foo\", list.get(0));\n        list = StringUtils.split(\"/foo\", '/', false);\n        assertEquals(1, list.size());\n        assertEquals(\"foo\", list.get(0));\n        list = StringUtils.split(\"/foo/bar\", '/', false);\n        assertEquals(2, list.size());\n        assertEquals(\"foo\", list.get(0));\n        assertEquals(\"bar\", list.get(1));\n        list = StringUtils.split(\"|pi\\\\|pe|blah|\", '|', true);\n        assertEquals(2, list.size());\n        assertEquals(\"pi|pe\", list.get(0));\n        assertEquals(\"blah\", list.get(1));\n    }\n\n    @Test\n    void testJoin() {\n        String[] foo = {\"a\", \"b\"};\n        assertEquals(\"a,b\", StringUtils.join(foo, ','));\n        assertEquals(\"a,b\", StringUtils.join(Arrays.asList(foo), \",\"));\n    }\n\n    @Test\n    void testJsFunction() {\n        assertTrue(StringUtils.isJavaScriptFunction(\"function(){ return { bar: 'baz' } }\"));\n        assertTrue(StringUtils.isJavaScriptFunction(\"function() {   \\n\"\n                + \"  return { someConfig: 'someValue' }\\n\"\n                + \"}\"));\n        assertTrue(StringUtils.isJavaScriptFunction(\"function fn(){ return { bar: 'baz' } }\"));\n        assertEquals(\"function(){}\", StringUtils.fixJavaScriptFunction(\"function fn(){}\"));\n    }\n\n    void testIsBlank() {\n        assertTrue(StringUtils.isBlank(\"\"));\n        assertTrue(StringUtils.isBlank(null));\n        assertFalse(StringUtils.isBlank(\"foo\"));\n    }\n\n    @Test\n    void testToIdString() {\n        assertEquals(\"foo-bar\", StringUtils.toIdString(\"foo_bar\"));\n        assertEquals(\"foo-bar\", StringUtils.toIdString(\"foo_bar\"));\n        assertEquals(\"foo-bar\", StringUtils.toIdString(\"foo bar\"));\n        assertEquals(\"foo--bar\", StringUtils.toIdString(\"foo//bar\"));\n        assertEquals(\"foo-bar\", StringUtils.toIdString(\"foo\\\\bar\"));\n        assertEquals(\"foo-bar\", StringUtils.toIdString(\"foo:bar\"));\n        assertEquals(\"foo-bar\", StringUtils.toIdString(\"foo?bar\"));\n        assertEquals(\"foo-bar\", StringUtils.toIdString(\"foo\\\"bar\"));\n        assertEquals(\"foo-bar\", StringUtils.toIdString(\"foo*bar\"));\n        assertEquals(\"foo-bar\", StringUtils.toIdString(\"foo|bar\"));\n        assertEquals(\"foo--bar\", StringUtils.toIdString(\"foo<>bar\"));\n        assertEquals(\"\", StringUtils.toIdString(null)); // TODO\n    }\n\n    @Test\n    void testSplitByFirstLineFeed() {\n        assertEquals(new Pair(\"\", \"\"),\n                StringUtils.splitByFirstLineFeed(null));\n        assertEquals(new Pair(\"foo\", \"\"),\n                StringUtils.splitByFirstLineFeed(\"foo\"));\n        assertEquals(new Pair(\"foo\", \"bar\"),\n                StringUtils.splitByFirstLineFeed(\"foo\\nbar\"));\n    }\n\n    @Test\n    void testToStringLines() {\n        List<String> expected = Arrays.asList(\"foo\", \"bar\");\n        assertEquals(expected, StringUtils.toStringLines(\"foo\\nbar\\n\"));\n    }\n\n    @Test\n    void testCountLineFeeds() {\n        assertEquals(2, StringUtils.countLineFeeds(\"foo\\nbar\\n\"));\n        assertEquals(0, StringUtils.countLineFeeds(\"foobar\"));\n    }\n\n    @Test\n    void testWrappedLinesEstimate() {\n        assertEquals(6,\n                StringUtils.wrappedLinesEstimate(\"foobarbazfoobarbaz\", 3));\n        assertEquals(1,\n                StringUtils.wrappedLinesEstimate(\"foobarbazfoobarbaz\", 20));\n        assertEquals(0,\n                StringUtils.wrappedLinesEstimate(\"\", 2));\n    }\n\n    @Test\n    void testContainsIgnoreCase() {\n        List<String> list = Arrays.asList(\"foo\", \"bar\");\n        assertTrue(StringUtils.containsIgnoreCase(list, \"foo\"));\n        assertTrue(StringUtils.containsIgnoreCase(list, \"Foo\"));\n        assertFalse(StringUtils.containsIgnoreCase(list, \"baz\"));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/SystemPropertiesTest.java",
    "content": "package com.intuit.karate;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport com.intuit.karate.Runner.Builder;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class SystemPropertiesTest {\n\n    private static final String TEST_PROP = \"testProperty\";\n\n    @AfterEach\n    void clearTestProperty() {\n        System.clearProperty(TEST_PROP);\n    }\n\n    @Test\n    void testSystemPropertiesSetOnRunner() {\n        Builder<?> builder = Runner.builder().systemProperty(TEST_PROP, \"setOnRunner\");\n        builder.resolveAll();\n        String propertyValue = builder.systemProperties.get(TEST_PROP);\n        assertEquals(propertyValue, \"setOnRunner\");\n        String jvmValue = System.getProperty(TEST_PROP);\n        assertEquals(jvmValue, null);\n    }\n\n    @Test\n    void testSystemPropertiesSetOnJVM() {\n        System.setProperty(TEST_PROP, \"setOnJVM\"); // -DtestProperty=setOnJVM\n        Builder<?> builder = Runner.builder();\n        builder.resolveAll();\n        String propertyValue = builder.systemProperties.get(TEST_PROP);\n        assertEquals(propertyValue, \"setOnJVM\");\n        String jvmValue = System.getProperty(TEST_PROP);\n        assertEquals(jvmValue, \"setOnJVM\");\n    }\n\n    @Test\n    void testPrecedenceOfSystemPropertiesSetOnRunnerAndJVM() {\n        System.setProperty(TEST_PROP, \"setOnJVM\"); // -DtestProperty=setOnJVM\n        Builder<?> builder = Runner.builder().systemProperty(TEST_PROP, \"setOnRunner\");\n        builder.resolveAll();\n        String propertyValue = builder.systemProperties.get(TEST_PROP);        \n        assertEquals(propertyValue, \"setOnJVM\");\n        String jvmValue = System.getProperty(TEST_PROP);\n        assertEquals(jvmValue, \"setOnJVM\");\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/TestUtils.java",
    "content": "package com.intuit.karate;\n\nimport com.intuit.karate.core.*;\nimport com.intuit.karate.core.runner.NoopDriver;\nimport com.intuit.karate.driver.DriverRunner;\nimport com.intuit.karate.http.HttpClientFactory;\nimport com.intuit.karate.resource.MemoryResource;\nimport com.intuit.karate.resource.Resource;\nimport com.intuit.karate.resource.ResourceUtils;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport org.thymeleaf.util.StringUtils;\n\n/**\n *\n * @author pthomas3\n */\npublic class TestUtils {\n\n    public static void match(Object actual, Object expected) {\n        Match.Result mr = Match.evaluate(actual).isEqualTo(expected);\n        assertTrue(mr.pass, mr.message);\n    }\n\n    public static void matchContains(Object actual, Object expected) {\n        Match.Result mr = Match.evaluate(actual).contains(expected);\n        assertTrue(mr.pass, mr.message);\n    }\n    \n    public static void notContains(Object actual, Object expected) {\n        Match.Result mr = Match.evaluate(actual).isNotContaining(expected);\n        assertTrue(mr.pass, mr.message);\n    }\n\n    public static ScenarioEngine engine() {\n        return new ScenarioEngine(new Config(), runtime(), new HashMap(), new Logger());\n    }\n\n    public static Feature toFeature(String... lines) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"Feature:\\nScenario:\\n\");\n        boolean inDocString = false;\n        for (String line : lines) {\n            if (line.trim().startsWith(FeatureParser.TRIPLE_QUOTES)) {\n                inDocString = !inDocString; // toggle\n                sb.append(line).append('\\n');\n            } else if (inDocString) {\n                sb.append(line).append('\\n');\n            } else {\n                sb.append(\"* \").append(line).append('\\n');\n            }\n        }\n        File file = ResourceUtils.getFileRelativeTo(TestUtils.class, \"core/dummy.feature\");\n        Resource resource = new MemoryResource(file, sb.toString());\n        return Feature.read(resource);\n    }\n\n    public static ScenarioRuntime runtime() {\n        Feature feature = toFeature(\"* print 'test'\");\n        FeatureRuntime fr = FeatureRuntime.of(new FeatureCall(feature));\n        return new ScenarioIterator(fr).first();\n    }\n\n    public static ScenarioRuntime runScenario(HttpClientFactory clientFactory, String... lines) {\n        return run(clientFactory, toFeature(lines));\n    }\n\n    public static ScenarioRuntime run(HttpClientFactory clientFactory, Feature feature) {\n        Runner.Builder builder = Runner.builder();\n        builder.clientFactory(clientFactory);\n        String configDir = System.getProperty(\"karate.config.dir\");\n        if (configDir != null) {\n            builder.configDir = configDir;\n        }\n        FeatureRuntime fr = FeatureRuntime.of(new Suite(builder), new FeatureCall(feature));\n        ScenarioRuntime sr = new ScenarioIterator(fr).first();\n        sr.run();\n        return sr;\n    }\n\n    public static FeatureRuntime runFeature(String path) {\n        return runFeature(path, null);\n    }\n\n    public static FeatureRuntime runFeature(String path, String configDir) {\n        Map<String, DriverRunner> customDrivers = new HashMap<>();\n        customDrivers.put(NoopDriver.DRIVER_TYPE, NoopDriver::start);\n        Feature feature = Feature.read(path);\n        Runner.Builder rb = Runner.builder();\n        rb.features(feature);\n        rb.configDir(configDir);\n        rb.customDrivers(customDrivers);\n        FeatureRuntime fr = FeatureRuntime.of(new Suite(rb), new FeatureCall(feature));\n        fr.run();\n        return fr;\n    }\n\n    public static class FeatureBuilder {\n\n        private final List<String> list = new ArrayList();\n\n        public FeatureBuilder() {\n            list.add(\"Feature:\");\n            list.add(\"\\n\");\n        }\n\n        public static FeatureBuilder background(String... lines) {\n            FeatureBuilder fb = new FeatureBuilder();\n            if (lines.length > 0) {\n                fb.list.add(\"Background:\");\n                for (String line : lines) {\n                    fb.list.add(\"* \" + line);\n                }\n                fb.list.add(\"\\n\");\n            }\n            return fb;\n        }\n\n        public FeatureBuilder scenario(String exp, String... lines) {\n            list.add(\"Scenario: \" + exp);\n            for (String line : lines) {\n                list.add(\"* \" + line);\n            }\n            list.add(\"\\n\");\n            return this;\n        }\n\n        public Feature build() {\n            String text = StringUtils.join(list, '\\n');\n            File file = ResourceUtils.getFileRelativeTo(getClass(), \"core/dummy.feature\");\n            Resource resource = new MemoryResource(file, text);\n            return Feature.read(resource);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/ThreadCountTest.java",
    "content": "package com.intuit.karate;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\n\nimport static com.intuit.karate.Constants.KARATE_OPTIONS;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class ThreadCountTest {\n\n  private static final int THREAD_COUNT = 20;\n\n  @AfterEach\n  void clearKarateOptionsProperty() {\n    System.clearProperty(KARATE_OPTIONS);\n  }\n\n  @Test\n  void testThreadCountFromRunner() {\n    Runner.Builder<?> builder = Runner.builder();\n    builder.path(\"does-not-exist\").parallel(THREAD_COUNT);\n    assertEquals(builder.threadCount, THREAD_COUNT);\n  }\n\n  @Test\n  void testThreadCountFromKarateOptionsShortName() {\n    System.setProperty(KARATE_OPTIONS, \"-T\" + THREAD_COUNT);\n    Runner.Builder<?> builder = Runner.builder();\n    builder.path(\"does-not-exist\").parallel(1);\n    assertEquals(builder.threadCount, THREAD_COUNT);\n  }\n\n  @Test\n  void testThreadCountFromKarateOptionsLongName() {\n    System.setProperty(KARATE_OPTIONS, \"--threads=\" + THREAD_COUNT);\n    Runner.Builder<?> builder = Runner.builder();\n    builder.path(\"does-not-exist\").parallel(1);\n    assertEquals(builder.threadCount, THREAD_COUNT);\n  }\n\n  @Test\n  void testThreadCountFromRunnerAndKarateOptionsWithoutThreadOption() {\n    System.setProperty(KARATE_OPTIONS, \"--tags=@does-not-exist\");\n    Runner.Builder<?> builder = Runner.builder();\n    builder.path(\"does-not-exist\").parallel(THREAD_COUNT);\n    assertEquals(builder.threadCount, THREAD_COUNT);\n  }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/XmlUtilsTest.java",
    "content": "package com.intuit.karate;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Node;\n\n/**\n *\n * @author pthomas3\n */\nclass XmlUtilsTest {\n\n    static final Logger logger = LoggerFactory.getLogger(XmlUtilsTest.class);\n\n    final String ACTUAL = \"<env:Envelope xmlns:env=\\\"http://schemas.xmlsoap.org/soap/envelope/\\\" xmlns:S=\\\"http://schemas.xmlsoap.org/soap/envelope/\\\"><env:Header/><env:Body xmlns=\\\"http://www.intuit.com/iep/ServiceUsage/IntuitServiceUsageABO/V1\\\"><QueryUsageBalanceResponse xmlns=\\\"http://www.intuit.com/iep/ServiceUsage/IntuitServiceUsageABO/V1\\\"><Balance/><Result><Success/><Error><Category>DAT</Category><Code>DAT_USAGE_1003</Code><Description>Invalid Request: Invalid Input criteria: No asset found for license/eoc (630289335971198/855939).</Description><Source>SIEBEL</Source></Error></Result></QueryUsageBalanceResponse></env:Body></env:Envelope>\";\n\n    @Test\n    void testParsing() {\n        String xml = \"<foo></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        String rootName = doc.getDocumentElement().getNodeName();\n        assertEquals(\"foo\", rootName);\n    }\n\n    @Test\n    void testXpath() {\n        String xml = \"<foo><bar>baz</bar></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        Node node = XmlUtils.getNodeByPath(doc, \"/foo\", false);\n        assertEquals(\"foo\", node.getNodeName());\n        String value = XmlUtils.getTextValueByPath(doc, \"/foo/bar\");\n        assertEquals(\"baz\", value);\n    }\n\n    @Test\n    void testConvertingToMap() {\n        String xml = \"<foo><bar>baz</bar></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        Map<String, Object> map = (Map) XmlUtils.toObject(doc);\n        logger.trace(\"map: {}\", map);\n        Map inner = (Map) map.get(\"foo\");\n        assertEquals(\"baz\", inner.get(\"bar\"));\n    }\n\n    @Test\n    void testConvertingToMapWithoutNamespace() {\n        String xml = \"<foo xmlns=\\\"foobar\\\"><a:bar xmlns:a=\\\"test\\\">baz</a:bar></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        Map<String, Object> map = (Map) XmlUtils.toObject(doc, true);\n        logger.trace(\"map: {}\", map);\n        Map inner = (Map) map.get(\"foo\");\n        assertEquals(\"baz\", inner.get(\"bar\"));\n    }\n\n    @Test\n    void testComplexConversionToMap() {\n        Document doc = XmlUtils.toXmlDoc(ACTUAL);\n        Map<String, Object> map = (Map) XmlUtils.toObject(doc);\n        logger.debug(\"map: {}\", map);\n        Map in1 = (Map) map.get(\"env:Envelope\");\n        Map in11 = (Map) in1.get(\"_\");\n        Map in2 = (Map) in11.get(\"env:Body\");\n        Map in22 = (Map) in2.get(\"_\");\n        Map in3 = (Map) in22.get(\"QueryUsageBalanceResponse\");\n        Map in33 = (Map) in3.get(\"_\");\n        Map in4 = (Map) in33.get(\"Result\");\n        Map in5 = (Map) in4.get(\"Error\");\n        assertEquals(\"DAT_USAGE_1003\", in5.get(\"Code\"));\n    }\n\n    @Test\n    void testRepeatedXmlElementsToMap() {\n        String xml = \"<foo><bar>baz1</bar><bar>baz2</bar></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        Map<String, Object> map = (Map) XmlUtils.toObject(doc);\n        logger.trace(\"map: {}\", map);\n        Map in1 = (Map) map.get(\"foo\");\n        List list = (List) in1.get(\"bar\");\n        assertEquals(2, list.size());\n        assertEquals(\"baz1\", list.get(0));\n        assertEquals(\"baz2\", list.get(1));\n    }\n\n    @Test\n    void testAnotherXpath() {\n        String xml = \"<com.intuit.services.acs.domain.api.ACSDocumentDTO>\\n\"\n                + \"  <EntityId>b14712d1-df91-4111-a77f-ce48f066b4ab</EntityId>\\n\"\n                + \"  <Name>test.pdf</Name>\\n\"\n                + \"  <Size>100250</Size>\\n\"\n                + \"  <Created>2016-12-23 22:08:36.90 PST</Created>\\n\"\n                + \"  <Properties/>\\n\"\n                + \"</com.intuit.services.acs.domain.api.ACSDocumentDTO>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        String value = XmlUtils.getTextValueByPath(doc, \"/com.intuit.services.acs.domain.api.ACSDocumentDTO/EntityId\");\n        logger.trace(\"value: {}\", value);\n        assertEquals(\"b14712d1-df91-4111-a77f-ce48f066b4ab\", value);\n    }\n\n    @Test\n    void testSetStringValueByPath() {\n        String xml = \"<foo><bar>baz</bar></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        XmlUtils.setByPath(doc, \"/foo/bar\", \"hello\");\n        String result = XmlUtils.toString(doc);\n        assertEquals(result, \"<foo><bar>hello</bar></foo>\");\n    }\n\n    @Test\n    void testReplaceDomNodeByPath() {\n        String xml = \"<foo><bar>baz</bar></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        Node temp = XmlUtils.toXmlDoc(\"<hello>world</hello>\");\n        XmlUtils.setByPath(doc, \"/foo/bar\", temp);\n        String result = XmlUtils.toString(doc);\n        assertEquals(result, \"<foo><bar><hello>world</hello></bar></foo>\");\n    }\n\n    @Test\n    void testAppendDomNodeByPath() {\n        String xml = \"<foo><bar/></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        Node temp = XmlUtils.toXmlDoc(\"<hello>world</hello>\");\n        XmlUtils.setByPath(doc, \"/foo/bar\", temp);\n        String result = XmlUtils.toString(doc);\n        assertEquals(result, \"<foo><bar><hello>world</hello></bar></foo>\");\n    }\n\n    @Test\n    void testSetDomNodeWithAttributeByPath() {\n        String xml = \"<foo><bar>baz</bar></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        Node temp = XmlUtils.toXmlDoc(\"<baz hello=\\\"world\\\">ban</baz>\");\n        XmlUtils.setByPath(doc, \"/foo/bar\", temp);\n        String result = XmlUtils.toString(doc);\n        assertEquals(result, \"<foo><bar><baz hello=\\\"world\\\">ban</baz></bar></foo>\");\n    }\n\n    @Test\n    void testCreateElementByPath() {\n        Document doc = XmlUtils.newDocument();\n        XmlUtils.createNodeByPath(doc, \"/foo/bar\");\n        String result = XmlUtils.toString(doc);\n        assertEquals(result, \"<foo><bar/></foo>\");\n    }\n\n    @Test\n    void testSetElementCreatingNonExistentParents() {\n        String xml = \"<foo></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        Node temp = XmlUtils.toXmlDoc(\"<hello>world</hello>\");\n        XmlUtils.setByPath(doc, \"/foo/bar\", temp);\n        String result = XmlUtils.toString(doc);\n        assertEquals(result, \"<foo><bar><hello>world</hello></bar></foo>\");\n    }\n\n    @Test\n    void testSetAttributeCreatingNonExistentParents() {\n        String xml = \"<foo></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        XmlUtils.setByPath(doc, \"/foo/bar/@baz\", \"ban\");\n        String result = XmlUtils.toString(doc);\n        assertEquals(result, \"<foo><bar baz=\\\"ban\\\"/></foo>\");\n    }\n\n    private Document getDocument() {\n        return XmlUtils.newDocument();\n    }\n\n    @Test\n    void testCreateElement() {\n        Node node = XmlUtils.createElement(getDocument(), \"foo\", \"bar\", null);\n        String result = XmlUtils.toString(node);\n        assertEquals(result, \"<foo>bar</foo>\");\n    }\n\n    @Test\n    void testCreateElementWithAttributes() {\n        Map<String, Object> map = new LinkedHashMap<>();\n        map.put(\"hello\", \"world\");\n        Node node = XmlUtils.createElement(getDocument(), \"foo\", \"bar\", map);\n        String result = XmlUtils.toString(node);\n        assertEquals(result, \"<foo hello=\\\"world\\\">bar</foo>\");\n    }\n\n    @Test\n    void testXmlFromMap() {\n        Map<String, Object> map = new LinkedHashMap<>();\n        map.put(\"hello\", \"world\");\n        Node node = XmlUtils.fromObject(\"foo\", map);\n        String result = XmlUtils.toString(node);\n        assertEquals(result, \"<foo><hello>world</hello></foo>\");\n    }\n\n    @Test\n    void testXmlWithAttributesFromMap() {\n        Map<String, Object> map = new LinkedHashMap<>();\n        map.put(\"_\", \"world\");\n        Map<String, Object> attribs = new LinkedHashMap<>();\n        attribs.put(\"foo\", \"bar\");\n        map.put(\"@\", attribs);\n        Node node = XmlUtils.fromObject(\"hello\", map);\n        String result = XmlUtils.toString(node);\n        assertEquals(result, \"<hello foo=\\\"bar\\\">world</hello>\");\n    }\n\n    @Test\n    void testPrettyPrint() {\n        String xml = \"<foo><bar>baz</bar><ban><goo>moo</goo></ban></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        String temp = XmlUtils.toString(doc, true);\n        String expected\n                = \"<foo>\\n\"\n                + \"  <bar>baz</bar>\\n\"\n                + \"  <ban>\\n\"\n                + \"    <goo>moo</goo>\\n\"\n                + \"  </ban>\\n\"\n                + \"</foo>\\n\";\n\n        expected = expected.replace(\"\\n\", System.lineSeparator());\n        assertEquals(temp, expected);\n    }\n\n    @Test\n    void testPrettyPrintWithWhiteSpace() {\n        String xml = \"<foo><bar>baz</bar><ban><goo> moo   </goo></ban></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        String temp = XmlUtils.toString(doc, true);\n        String expected\n                = \"<foo>\\n\"\n                + \"  <bar>baz</bar>\\n\"\n                + \"  <ban>\\n\"\n                + \"    <goo> moo   </goo>\\n\"\n                + \"  </ban>\\n\"\n                + \"</foo>\\n\";\n\n        expected = expected.replace(\"\\n\", System.lineSeparator());\n        assertEquals(temp, expected);\n    }\n\n    @Test\n    void testNonPrettyPrintWithWhiteSpace() {\n        String xml = \"<foo><bar>baz</bar><ban><goo> moo   </goo></ban></foo>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        String temp = XmlUtils.toString(doc, false);\n        String expected = \"<foo><bar>baz</bar><ban><goo> moo   </goo></ban></foo>\";\n        assertEquals(temp, expected);\n    }\n\n    @Test\n    void testCreatingNewDocumentFromSomeChildNode() {\n        String xml = \"<root><foo><bar>baz</bar></foo></root>\";\n        Document doc = XmlUtils.toXmlDoc(xml);\n        Node node = XmlUtils.getNodeByPath(doc, \"/root/foo\", false);\n        Document tempDoc = XmlUtils.toNewDocument(node);\n        String tempString = XmlUtils.getTextValueByPath(tempDoc, \"/foo/bar\");\n        assertEquals(tempString, \"baz\");\n        Node tempNode = XmlUtils.getNodeByPath(tempDoc, \"/\", false);\n        assertEquals(XmlUtils.toString(tempNode), \"<foo><bar>baz</bar></foo>\");\n    }\n\n    @Test\n    void testStripNameSpacePrefixes() {\n        assertEquals(\"/\", XmlUtils.stripNameSpacePrefixes(\"/\"));\n        assertEquals(\"/foo\", XmlUtils.stripNameSpacePrefixes(\"/foo\"));\n        assertEquals(\"/bar\", XmlUtils.stripNameSpacePrefixes(\"/foo:bar\"));\n        assertEquals(\"/bar/baz\", XmlUtils.stripNameSpacePrefixes(\"/foo:bar/foo:baz\"));\n        assertEquals(\"/bar/baz/@ban\", XmlUtils.stripNameSpacePrefixes(\"/foo:bar/foo:baz/@ban\"));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/CallResponseTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author peter\n */\nclass CallResponseTest {\n\n    @Test\n    void testPayments() {\n        MockServer server = MockServer\n                .feature(\"classpath:com/intuit/karate/core/call-response-mock.feature\")\n                .http(0).build();        \n        Results results = Runner.path(\"classpath:com/intuit/karate/core/call-response.feature\")\n                .systemProperty(\"server.port\", server.getPort() + \"\")\n                .parallel(1);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n        server.stop();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/Cat.java",
    "content": "package com.intuit.karate.core;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class Cat {\n    \n    private int id;\n    private String name;\n    private List<Cat> kittens;\n    \n    public void addKitten(Cat kitten) {\n        if (kittens == null) {\n            kittens = new ArrayList<>();\n        }\n        kittens.add(kitten);\n    }\n\n    public int getId() {\n        return id;\n    }\n\n    public void setId(int id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }        \n\n    public List<Cat> getKittens() {\n        return kittens;\n    }\n\n    public void setKittens(List<Cat> kittens) {\n        this.kittens = kittens;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/ComplexPojo.java",
    "content": "package com.intuit.karate.core;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class ComplexPojo {\n    \n    private String foo;\n    private int bar;\n    private Map<String, String> baz;\n    private List<ComplexPojo> ban;\n\n    public String getFoo() {\n        return foo;\n    }\n\n    public void setFoo(String foo) {\n        this.foo = foo;\n    }\n\n    public int getBar() {\n        return bar;\n    }\n\n    public void setBar(int bar) {\n        this.bar = bar;\n    }\n\n    public Map<String, String> getBaz() {\n        return baz;\n    }\n\n    public void setBaz(Map<String, String> baz) {\n        this.baz = baz;\n    } \n\n    public List<ComplexPojo> getBan() {\n        return ban;\n    }\n\n    public void setBan(List<ComplexPojo> ban) {\n        this.ban = ban;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/DummyClient.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.http.HttpClient;\nimport com.intuit.karate.http.HttpRequest;\nimport com.intuit.karate.http.Response;\n\n/**\n *\n * @author pthomas3\n */\npublic class DummyClient implements HttpClient {\n\n    private Config config = new Config();\n    @Override\n    public void setConfig(Config config) {\n        this.config = config;\n    }\n\n    @Override\n    public Config getConfig() {\n        return config;\n    }\n\n    @Override\n    public Response invoke(HttpRequest request) {\n        throw new UnsupportedOperationException(\"not implemented\");\n    }   \n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/DummyUiTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.Suite;\nimport com.intuit.karate.core.runner.NoopDriver;\nimport com.intuit.karate.driver.DriverRunner;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\n\n/**\n *\n * @author pthomas3\n */\npublic class DummyUiTest {\n\n    FeatureRuntime fr;\n\n    public static FeatureRuntime runFeature(String path) {\n        Map<String, DriverRunner> customDrivers = new HashMap<>();\n        customDrivers.put(NoopDriver.DRIVER_TYPE, NoopDriver::start);\n        Feature feature = Feature.read(path);\n        Runner.Builder rb = Runner.builder();\n        rb.features(feature);\n        rb.configDir(\"classpath:com/intuit/karate/core\");\n        rb.customDrivers(customDrivers);\n        FeatureRuntime fr = FeatureRuntime.of(new Suite(rb), new FeatureCall(feature));\n        fr.run();\n        return fr;\n    }\n\n    @Test\n    void testUiGoogle() {\n        fr = runFeature(\"classpath:com/intuit/karate/core/dummy-ui-google.feature\");\n        assertFalse(fr.result.isFailed());\n    }\n\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/FeatureFailRunner.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.TestUtils;\nimport com.intuit.karate.report.Report;\nimport com.intuit.karate.report.SuiteReports;\nimport java.io.File;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass FeatureFailRunner {\n\n    static final Logger logger = LoggerFactory.getLogger(FeatureFailRunner.class);\n\n    FeatureRuntime fr;\n\n    private FeatureRuntime run(String name) {\n        return run(name, null);\n    }\n\n    private FeatureRuntime run(String name, String configDir) {\n        fr = TestUtils.runFeature(\"classpath:com/intuit/karate/core/\" + name, configDir);\n        return fr;\n    }\n\n    private File report() {\n        Report report = SuiteReports.DEFAULT.featureReport(fr.suite, fr.result);\n        File file = report.render(\"target/temp\");\n        logger.debug(\"saved report: {}\", file.getAbsolutePath());\n        return file;\n    }\n    \n    @Test\n    void testFailJs() {\n        run(\"fail-js.feature\");\n        assertTrue(fr.result.isFailed());\n        report();\n    }\n\n    @Test\n    void testCallSingleFail() {\n        System.setProperty(\"karate.config.dir\", \"classpath:com/intuit/karate/core\");\n        System.setProperty(\"karate.env\", \"csfail\");\n        run(\"call-single-fail.feature\");\n        assertTrue(fr.result.isFailed());\n        report();\n    }\n    \n    @Test\n    void testExec() {\n        run(\"exec.feature\");\n    }    \n\n    @Test\n    void testFork() {\n        run(\"fork.feature\");\n    }\n\n    @Test\n    void testForkListener() {\n        run(\"fork-listener.feature\");\n        assertFalse(fr.result.isFailed());\n    }\n\n    @Test\n    void testUsersDoc() {\n        run(\"users-doc.feature\");\n        assertFalse(fr.result.isFailed());\n        report();\n    }\n\n    @Test\n    void testUiGoogle() {\n        run(\"ui-google.feature\");\n        assertFalse(fr.result.isFailed());\n        report();\n    }\n    \n    @Test\n    void testOutlineDynamicFail() {\n        run(\"outline-dynamic-fail.feature\");\n        assertTrue(fr.result.isFailed());\n        report();        \n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/FeatureResultTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.TestUtils;\nimport static com.intuit.karate.TestUtils.*;\nimport com.intuit.karate.report.Report;\nimport com.intuit.karate.report.SuiteReports;\nimport java.io.File;\nimport java.util.Map;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass FeatureResultTest {\n    \n    static final Logger logger = LoggerFactory.getLogger(FeatureResultTest.class);\n\n    FeatureRuntime fr;\n\n    private FeatureRuntime run(String name) {\n        fr = TestUtils.runFeature(\"classpath:com/intuit/karate/core/\" + name);\n        assertFalse(fr.result.isFailed());\n        return fr;\n    }\n\n    @Test\n    void testJsonConversion() {\n        run(\"feature-result.feature\");\n        Map<String, Object> featureResult = fr.result.toKarateJson();\n        String expected = FileUtils.toString(new File(\"src/test/java/com/intuit/karate/core/feature-result.json\"));\n        match(featureResult, expected);\n        FeatureResult temp = FeatureResult.fromKarateJson(fr.suite.workingDir, featureResult);\n        Report report = SuiteReports.DEFAULT.featureReport(fr.suite, fr.result);\n        File file = report.render(\"target\");        \n        logger.debug(\"saved report: {}\", file.getAbsolutePath());        \n        Map<String, Object> karateClone = temp.toKarateJson();\n        match(featureResult, karateClone);\n        Map<String, Object> cucumberClone = temp.toCucumberJson();\n        expected = FileUtils.toString(new File(\"src/test/java/com/intuit/karate/core/feature-result-cucumber.json\"));\n        match(cucumberClone, expected);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/FeatureRuntimeTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.*;\nimport com.intuit.karate.report.Report;\nimport com.intuit.karate.report.SuiteReports;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.File;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n/**\n * @author pthomas3\n */\nclass FeatureRuntimeTest {\n\n    static final Logger logger = LoggerFactory.getLogger(FeatureRuntimeTest.class);\n\n    boolean fail;\n    FeatureRuntime fr;\n\n    @BeforeEach\n    void beforeEach() {\n        fail = false;\n    }\n\n    private FeatureRuntime run(String name) {\n        return run(name, null);\n    }\n\n    private FeatureRuntime run(String name, String configDir) {\n        fr = TestUtils.runFeature(\"classpath:com/intuit/karate/core/\" + name, configDir);\n        if (fail) {\n            assertTrue(fr.result.isFailed());\n        } else {\n            assertFalse(fr.result.isFailed());\n        }\n        return fr;\n    }\n\n    private File report() {\n        Report report = SuiteReports.DEFAULT.featureReport(fr.suite, fr.result);\n        File file = report.render(\"target/temp\");\n        logger.debug(\"saved report: {}\", file.getAbsolutePath());\n        return file;\n    }\n\n    private void match(Object actual, Object expected) {\n        Match.Result mr = Match.evaluate(actual).isEqualTo(expected);\n        assertTrue(mr.pass, mr.message);\n    }\n\n    private void matchContains(Object actual, Object expected) {\n        Match.Result mr = Match.evaluate(actual).contains(expected);\n        assertTrue(mr.pass, mr.message);\n    }\n\n    @Test\n    void testFailTag() {\n        fail = false;\n        run(\"fail-tag.feature\");\n    }\n\n    @Test\n    void testFailTagFailure() {\n        fail = true;\n        run(\"fail-tag-failure.feature\");\n    }\n\n    @Test\n    void testFail1() {\n        fail = true;\n        run(\"fail1.feature\");\n    }\n\n    @Test\n    void testCallOnce() {\n        run(\"callonce-bg.feature\");\n    }\n\n    @Test\n    void testCallOnceWithUtilsPresentInKarateConfig() {\n        run(\"callonce-bg.feature\", \"classpath:com/intuit/karate/core\");\n    }\n\n    @Test\n    void testCallOnceGlobal() {\n        run(\"callonce-global.feature\");\n    }\n\n    @Test\n    void testTags() {\n        run(\"tags.feature\");\n        match(fr.result.getVariables(), \"{ configSource: 'normal', functionFromKarateBase: '#notnull', tagNames: ['two=foo,bar', 'one'], tagValues: { one: [], two: ['foo', 'bar'] } }\");\n    }\n\n    @Test\n    void testAbort() {\n        run(\"abort.feature\");\n        match(fr.result.getVariables(), \"{ configSource: 'normal', functionFromKarateBase: '#notnull', before: true }\");\n    }\n\n    @Test\n    void testAlign() {\n        run(\"align.feature\");\n        match(fr.result.getVariables(), \"{ configSource: 'normal', functionFromKarateBase: '#notnull', text: 'hello bar world' , cats: '#notnull', myJson: {}}}\");\n    }\n\n    @Test\n    void testFailApi() {\n        fail = true;\n        run(\"fail-api.feature\");\n        match(fr.result.getVariables(), \"{ configSource: 'normal', functionFromKarateBase: '#notnull', before: true }\");\n    }\n\n    @Test\n    void testCallFeatureFromJs() {\n        run(\"call-js.feature\");\n        matchContains(fr.result.getVariables(), \"{ calledVar: 'hello world' }\");\n    }\n\n    @Test\n    void testCallJsFromFeatureUtilsDefinedInKarateConfig() {\n        run(\"karate-config-fn.feature\", \"classpath:com/intuit/karate/core/\");\n        matchContains(fr.result.getVariables(), \"{ helloVar: 'hello world' }\");\n    }\n\n    @Test\n    void testCallOnceJsFromFeatureUtilsDefinedInKarateConfig() {\n        System.setProperty(\"karate.env\", \"callonce\");\n        run(\"callonce-config.feature\", \"classpath:com/intuit/karate/core/\");\n        matchContains(fr.result.getVariables(), \"{ foo: 'hello foo' }\");\n        System.clearProperty(\"karate.env\");\n    }\n\n    @Test\n    void testKarateJsGetScenario() {\n        System.setProperty(\"karate.env\", \"getscenario\");\n        run(\"karate-config-getscenario.feature\", \"classpath:com/intuit/karate/core/\");\n        System.clearProperty(\"karate.env\");\n    }\n\n    @Test\n    void testKarateJsFromKarateBase() {\n        System.setProperty(\"karate.env\", \"frombase\");\n        run(\"karate-config-frombase.feature\", \"classpath:com/intuit/karate/core/\");\n        System.clearProperty(\"karate.env\");\n    }\n\n    @Test\n    void testCallByTag() {\n        run(\"call-by-tag.feature\");\n    }\n\n    @Test\n    void testCallByTagCalled() {\n        run(\"call-by-tag-called.feature\");\n        matchContains(fr.result.getVariables(), \"{ bar: 3 }\"); // last scenario\n    }\n\n    @Test\n    void testCopyAndClone() {\n        run(\"copy.feature\");\n    }\n\n    @Test\n    void testMatchEachMagicVariables() {\n        run(\"match-each-magic-variables.feature\");\n    }\n\n    @Test\n    void testEvalAndSet() {\n        run(\"eval-and-set.feature\");\n    }\n\n    @Test\n    void testEvalAssign() {\n        run(\"eval-assign.feature\");\n    }\n\n    @Test\n    void testExtract() {\n        run(\"extract.feature\");\n    }\n\n    @Test\n    void testConfigureInJs() {\n        run(\"configure-in-js.feature\");\n    }\n\n    @Test\n    void testTable() {\n        run(\"table.feature\");\n    }\n\n    @Test\n    void testSet() {\n        run(\"set.feature\");\n    }\n\n    @Test\n    void testSetXml() {\n        run(\"set-xml.feature\");\n    }\n\n    @Test\n    void testJsRead() {\n        run(\"jsread/js-read.feature\");\n    }\n\n    @Test\n    void testJsRead2() {\n        run(\"jsread/js-read-2.feature\");\n    }\n\n    @Test\n    void testJsRead3() {\n        run(\"jsread/js-read-3.feature\");\n    }\n\n    @Test\n    void testJsRead4() {\n        run(\"jsread/js-read-4.feature\");\n    }\n\n    @Test\n    void testJsMapRepeat() {\n        run(\"js-map-repeat.feature\");\n    }\n\n    @Test\n    void testCallFeature() {\n        run(\"call-feature.feature\");\n    }\n\n    @Test\n    void testOutlineGenerator() {\n        run(\"outline-generator.feature\");\n    }\n\n    @Test\n    void testToBean() {\n        run(\"to-bean.feature\");\n    }\n\n    @Test\n    void testOutline() {\n        run(\"outline.feature\");\n    }\n\n    @Test\n    void testOutlineBackground() {\n        run(\"outline-background.feature\");\n    }\n\n    @Test\n    void testOutlineDynamic() {\n        run(\"outline-dynamic.feature\");\n    }\n\n    @Test\n    void testOutlineSetupOnce() {\n        run(\"outline-setup-once.feature\");\n    }\n\n    @Test\n    void testOutlineConfigJsParallel() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/outline-config-js.feature\")\n                .configDir(\"src/test/java/com/intuit/karate/core\")\n                .parallel(2);\n        assertEquals(0, results.getFailCount());\n    }\n\n    @Test\n    void testOutlineConfigJsCallSingleParallel() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/outline-config-js.feature\")\n                .configDir(\"src/test/java/com/intuit/karate/core\")\n                .karateEnv(\"callsingle\")\n                .parallel(2);\n        assertEquals(0, results.getFailCount());\n    }\n\n    @Test\n    void testCallSingleOutlineExampleByTag() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/call-single-tag.feature\")\n                .configDir(\"src/test/java/com/intuit/karate/core\")\n                .karateEnv(\"callsingletag\")\n                .tags(\"@runme\")\n                .parallel(1);\n        assertEquals(0, results.getFailCount());\n    }\n\n    @Test\n    void testCallArg() {\n        run(\"call-arg.feature\");\n    }\n\n    @Test\n    void testCallArgNull() {\n        run(\"call-arg-null.feature\");\n    }\n\n    @Test\n    void testIgnoreStepFailure() {\n        fail = true;\n        run(\"ignore-step-failure.feature\");\n        Report report = SuiteReports.DEFAULT.featureReport(fr.suite, fr.result);\n        report.render(\"target/report-test\");\n        // error log will should have logs on all failures\n    }\n\n    @Test\n    void testKarateFork() {\n        run(\"fork.feature\");\n    }\n\n    @Test\n    void testCsv() {\n        run(\"csv.feature\");\n    }\n\n    @Test\n    void testXmlPretty() {\n        run(\"xml-pretty.feature\");\n    }\n\n    @Test\n    void testMatchStep() {\n        run(\"match-step.feature\");\n    }\n\n    @Test\n    void testCallJsonPath() {\n        run(\"call-jsonpath.feature\");\n    }\n\n    @Test\n    void testSchemaRead() {\n        run(\"schema-read.feature\");\n    }\n\n    @Test\n    void testTypeConversion() {\n        run(\"type-conversion.feature\");\n    }\n\n    @Test\n    void testKarateGet() {\n        run(\"karate-get.feature\");\n    }\n\n    @Test\n    void testOutlineCsv() {\n        run(\"outline-csv.feature\");\n    }\n\n    @Test\n    void testReadProperties() {\n        run(\"read-properties.feature\");\n    }\n\n    @Test\n    void testCallSelf() {\n        run(\"call-self.feature\");\n        matchContains(fr.result.getVariables(), \"{ result: 'second' }\");\n    }\n\n    @Test\n    void testBigDecimal() {\n        run(\"big-decimal.feature\");\n    }\n\n    @Test\n    void testJsArrays() {\n        run(\"js-arrays.feature\");\n    }\n\n    @Test\n    void testLowerCase() {\n        run(\"lower-case.feature\");\n    }\n\n    @Test\n    void testNotEquals() {\n        run(\"not-equals.feature\");\n    }\n\n    @Test\n    void testReplace() {\n        run(\"replace.feature\");\n    }\n\n    @Test\n    void testScenarioVariableScope() {\n        run(\"scenario-variable-scope.feature\");\n    }\n\n    @Test\n    void testSchemaLike() {\n        run(\"schema-like.feature\");\n    }\n\n    @Test\n    void testSortArray() {\n        run(\"sort-array.feature\");\n    } \n    \n    @Test\n    void testTypeConv() {\n        run(\"type-conv.feature\");\n    }\n\n    @Test\n    void testConfigureNtlmAuthentication() {\n        run(\"ntlm-authentication.feature\");\n    }\n\n    @Test\n    void testSingleScenario() {\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/single-scenario.feature\");\n        FeatureCall featureCall = new FeatureCall(feature, \"@Scenario2\", -1, null);\n        FeatureRuntime featureRuntime = FeatureRuntime.of(new Suite(), featureCall, null);\n        featureRuntime.run();\n\n        var resultVars = featureRuntime.result.getVariables();\n        matchContains(resultVars, \"{ result1: '#notpresent', result2: 'Two', result3: '#notpresent' }\");\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/HttpMockHandlerRunner.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2020 pthomas3.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.core;\n\nimport static com.intuit.karate.TestUtils.*;\nimport com.intuit.karate.http.ArmeriaHttpClient;\nimport com.intuit.karate.http.HttpRequestBuilder;\nimport com.intuit.karate.http.HttpServer;\nimport com.intuit.karate.http.Response;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass HttpMockHandlerRunner { // TODO investigate intermittent CI failure\n\n    static final Logger logger = LoggerFactory.getLogger(HttpMockHandlerRunner.class);\n\n    MockHandler handler;\n    HttpServer server;\n    FeatureBuilder mock;\n    HttpRequestBuilder http;\n    Response response;\n\n    HttpRequestBuilder handle() {\n        handler = new MockHandler(mock.build());\n        server = HttpServer.handler(handler).build();\n        ArmeriaHttpClient client = new ArmeriaHttpClient(new Config(), new com.intuit.karate.Logger());\n        http = new HttpRequestBuilder(client);\n        http.url(\"http://localhost:\" + server.getPort());\n        return http;\n    }\n\n    FeatureBuilder background(String... lines) {\n        mock = FeatureBuilder.background(lines);\n        return mock;\n    }\n\n    @AfterEach\n    void afterEach() {\n        server.stop();\n    }\n\n    @Test\n    void testProceed() {\n        FeatureBuilder fb = background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = 'world'\");\n        HttpServer downStream = HttpServer.handler(new MockHandler(fb.build())).build();\n        String downStreamUrl = \"http://localhost:\" + downStream.getPort();\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"karate.proceed('\" + downStreamUrl + \"')\",\n                \"def response = 'hello ' + response\");\n        response = handle().path(\"/hello\").invoke(\"get\");\n        match(response.getBodyAsString(), \"hello world\");\n        downStream.stop();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/HttpMockHandlerTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.Constants;\nimport static com.intuit.karate.TestUtils.*;\nimport com.intuit.karate.http.ApacheHttpClient;\nimport com.intuit.karate.http.HttpClientFactory;\nimport com.intuit.karate.http.HttpRequestBuilder;\nimport com.intuit.karate.http.HttpServer;\nimport com.intuit.karate.http.Response;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass HttpMockHandlerTest {\n\n    static final Logger logger = LoggerFactory.getLogger(HttpMockHandlerTest.class);\n\n    MockHandler handler;\n    HttpServer server;\n    FeatureBuilder mock;\n    HttpRequestBuilder http;\n    Response response;\n\n    HttpRequestBuilder handle() {\n        handler = new MockHandler(mock.build());\n        server = HttpServer.handler(handler).build();\n        ScenarioEngine se = ScenarioEngine.forTempUse(HttpClientFactory.DEFAULT);\n        ApacheHttpClient client = new ApacheHttpClient(se);\n        http = new HttpRequestBuilder(client);\n        http.url(\"http://localhost:\" + server.getPort());\n        return http;\n    }\n\n    HttpRequestBuilder handleWithOriginalHeaders() {\n        handler = new MockHandler(mock.build());\n        server = HttpServer.handler(handler).keepOriginalHeaders(true).build();\n        ScenarioEngine se = ScenarioEngine.forTempUse(HttpClientFactory.DEFAULT);\n        ApacheHttpClient client = new ApacheHttpClient(se);\n        http = new HttpRequestBuilder(client);\n        http.url(\"http://localhost:\" + server.getPort());\n        return http;\n    }\n\n    FeatureBuilder background(String... lines) {\n        mock = FeatureBuilder.background(lines);\n        return mock;\n    }\n\n    @AfterEach\n    void afterEach() {\n        server.stop();\n    }\n\n    @Test\n    void testSimpleGet() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = 'hello world'\");\n        response = handle().path(\"/hello\").invoke(\"get\");\n        match(response.getBodyAsString(), \"hello world\");\n    }\n\n    @Test\n    void testUrlWithSpecialCharacters() {\n        background().scenario(\n                \"pathMatches('/hello/{raw}')\",\n                \"def response = { success: true }\"\n        );\n        response = handle().path(\"/hello/�Ill~Formed@RequiredString!\").invoke(\"get\");\n        match(response.getBodyConverted(), \"{ success: true }\");\n    }\n\n    @Test\n    void testGraalJavaClassLoading() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def Utils = Java.type('com.intuit.karate.core.MockUtils')\",\n                \"def response = Utils.testBytes\"\n        );\n        response = handle().path(\"/hello\").invoke(\"get\");\n        match(response.getBody(), MockUtils.testBytes);\n    }\n\n    @Test\n    void testEmptyResponse() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = null\"\n        );\n        response = handle().path(\"/hello\").invoke(\"get\");\n        match(response.getBody(), Constants.ZERO_BYTES);\n    }\n\n    @Test\n    void testConfigureResponseHeaders() {\n        background(\"configure responseHeaders = { 'Content-Type': 'text/html' }\")\n                .scenario(\n                        \"pathMatches('/hello')\",\n                        \"def response = ''\");\n        response = handle().path(\"/hello\").invoke(\"get\");\n        match(response.getHeader(\"Content-Type\"), \"text/html\");\n    }\n\n    @Test\n    void testKeepOriginalResponseHeaders() {\n        background(\"configure responseHeaders = { 'X-Special-Header': 'test1' }\")\n                .scenario(\n                        \"pathMatches('/hello')\",\n                        \"def responseHeaders = { 'X-Custom-Header': 'test2' }\",\n                        \"def response = ''\");\n        response = handleWithOriginalHeaders().path(\"/hello\").invoke(\"get\");\n        match(response.getHeader(\"X-Special-Header\"), \"test1\");\n        match(response.getHeader(\"X-Custom-Header\"), \"test2\");\n\n        matchContains(response.getHeaders().keySet(), \"X-Special-Header\");\n        matchContains(response.getHeaders().keySet(), \"X-Custom-Header\");\n    }\n\n    @Test\n    void testResponseHavingForwardSlashes() {\n        background()\n                .scenario(\n                        \"pathMatches('/hello')\",\n                        \"def response = { 'url': 'https://test123' }\");\n        response = handle().path(\"/hello\").invoke(\"get\");\n        match(response.getBody(), \"{\\\"url\\\":\\\"https://test123\\\"}\".getBytes());\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/JsStubGenerator.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.StringUtils;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Parameter;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport javassist.Modifier;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nclass JsStubGenerator {\n\n    static final Logger logger = LoggerFactory.getLogger(JsStubGenerator.class);\n\n    @Test\n    void testGenerateKarateStub() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"function karate() {}\\n\");\n        Class clazz = ScenarioBridge.class;\n        Comparator<Method> comparator = (Method o1, Method o2) -> {\n            int nameResult = o1.getName().compareTo(o2.getName());\n            if (nameResult != 0) {\n                return nameResult;\n            }\n            return o1.getParameterCount() - o2.getParameterCount();            \n        };\n        List<Method> methods = new ArrayList();\n        for (Method method : clazz.getDeclaredMethods()) {\n            if (Modifier.isPublic(method.getModifiers())) {\n                methods.add(method);\n            }            \n        }\n        Collections.sort(methods, comparator);\n        for (Method method : methods) {\n            String name = method.getName();\n            if (name.startsWith(\"get\") && name.length() > 3) {\n                name = name.substring(3, 4).toLowerCase() + name.substring(4);\n                sb.append(\"karate.\").append(name).append(\" = \").append(\"{};\\n\");\n            } else {\n                List<String> params = new ArrayList();\n                for (Parameter p : method.getParameters()) {\n                    params.add(p.getName());\n                }\n                sb.append(\"karate.\").append(name).append(\" = function(\").append(StringUtils.join(params, \",\")).append(\") {};\\n\");\n            }\n\n        }\n        logger.debug(\"js:\\n{}\", sb);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/KarateHttpMockHandlerTest.java",
    "content": "package com.intuit.karate.core;\n\nimport static com.intuit.karate.TestUtils.*;\nimport static com.intuit.karate.TestUtils.runScenario;\nimport com.intuit.karate.http.HttpServer;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass KarateHttpMockHandlerTest {\n\n    static final Logger logger = LoggerFactory.getLogger(KarateHttpMockHandlerTest.class);\n\n    MockHandler handler;\n    HttpServer server;\n    FeatureBuilder mock;\n    ScenarioRuntime runtime;\n\n    String urlStep() {\n        return \"url 'http://localhost:\" + server.getPort() + \"'\";\n    }\n\n    void startMockServer() {\n        handler = new MockHandler(mock.build());\n        server = HttpServer.handler(handler).build();\n    }\n\n    FeatureBuilder background(String... lines) {\n        mock = FeatureBuilder.background(lines);\n        return mock;\n    }\n\n    Object get(String name) {\n        return runtime.engine.vars.get(name).getValue();\n    }\n\n    ScenarioRuntime run(String... lines) {\n        runtime = runScenario(null, lines);\n        return runtime;\n    }\n\n    private void matchVar(String name, Object expected) {\n        match(get(name), expected);\n    }\n\n    private void matchVarContains(String name, Object expected) {\n        matchContains(get(name), expected);\n    }\n\n    @AfterEach\n    void afterEach() {\n        server.stop();\n    }\n\n    @Test\n    void testSimpleGet() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = 'hello world'\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"hello world\");\n    }\n\n    @Test\n    void testUrlWithTrailingSlashAndPath() {\n        background().scenario(\n                \"pathMatches('/hello/world')\",\n                \"def response = requestUri\");\n        startMockServer();\n        run(\n                \"url 'http://localhost:\" + server.getPort() + \"/hello'\",\n                \"path '/world/'\",\n                \"method get\",\n                \"match response == '/hello/world'\"\n        );\n        matchVar(\"response\", \"/hello/world\");\n    }\n\n    @Test\n    void testRequestBodyAsInteger() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path '/hello'\",\n                \"request 42\",\n                \"method post\"\n        );\n        // for some reason, apache assumes this encoding if the request body is raw bytes TODO\n        matchVarContains(\"response\", \"{ 'content-type': ['application/octet-stream'] }\");\n    }\n\n    @Test\n    void testThatCookieIsPartOfRequest() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"cookie foo = 'bar'\",\n                \"method get\"\n        );\n        matchVarContains(\"response\", \"{ cookie: ['foo=bar'] }\");\n    }\n\n    @Test\n    void testSameSiteSecureCookieRequest() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"cookie foo = { value: 'bar', samesite: 'Strict', secure: true }\",\n                \"method get\"\n        );\n        matchVarContains(\"response\", \"{ cookie: ['foo=bar'] }\");\n    }\n\n    @Test\n    void testSameSiteSecureCookieResponse() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = { 'Set-Cookie': 'foo=bar; expires=Wed, 30-Dec-20 09:25:45 GMT; path=/; domain=.example.com; HttpOnly; SameSite=Lax; Secure' }\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVarContains(\"responseHeaders\", \"{ set-cookie: ['foo=bar; expires=Wed, 30-Dec-20 09:25:45 GMT; path=/; domain=.example.com; HttpOnly; SameSite=Lax; Secure'] }\");\n    }\n\n    @Test\n    void testMultipleCookies() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"cookie cookie1 = 'foo'\",\n                \"cookie cookie2 = 'bar'\",\n                \"method get\"\n        );\n        matchVarContains(\"response\", \"{ cookie: ['cookie1=foo; cookie2=bar'] }\");\n    }\n\n    @Test\n    void testThatExoticContentTypeIsPreserved() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"header Content-Type = 'application/xxx.pingixxxxxx.checkUsernamePassword+json'\",\n                \"method post\"\n        );\n        matchVarContains(\"response\", \"{ 'content-type': ['application/xxx.pingixxxxxx.checkUsernamePassword+json'] }\");\n    }\n\n    @Test\n    void testInspectRequestInHeadersFunction() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"configure headers = function(request){ return { 'api-key': request.bodyAsString } }\",\n                \"path 'hello'\",\n                \"request 'some text'\",\n                \"method post\"\n        );\n        matchVarContains(\"response\", \"{ 'api-key': ['some text'] }\");\n    }\n\n    @Test\n    void testKarateRemove() {\n        background().scenario(\n                \"pathMatches('/hello/{id}')\",\n                \"def temp = { '1': 'foo', '2': 'bar' }\",\n                \"karate.remove('temp', pathParams.id)\",\n                \"def response = temp\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello', '1'\",\n                \"method get\"\n        );\n        matchVarContains(\"response\", \"{ '2': 'bar' }\");\n    }\n\n    @Test\n    void testTransferEncoding() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = request\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"header Transfer-Encoding = 'chunked'\",\n                \"request { foo: 'bar' }\",\n                \"method post\"\n        );\n        matchVarContains(\"response\", \"{ foo: 'bar' }\");\n    }\n\n    @Test\n    void testMalformedMockResponse() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = '{ \\\"id\\\" \\\"123\\\" }'\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"method get\",\n                \"match response == '{ \\\"id\\\" \\\"123\\\" }'\",\n                \"match responseType == 'string'\"\n        );\n        Object response = get(\"response\");\n        assertEquals(response, \"{ \\\"id\\\" \\\"123\\\" }\");\n    }\n    \n    @Test\n    void testCookieDataIsNotLost() {\n        background()\n                .scenario(\"pathMatches('/first')\",\n                        \"def responseHeaders = { 'Set-Cookie': '1P_JAR=2022-11-02-11; expires=Fri, 02-Dec-2022 11:50:46 GMT; path=/; domain=.google.com; Secure' }\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'first'\",\n                \"method get\"\n        );\n        Map<String, Object> map = (Map) get(\"responseHeaders\");\n        List<String> list = (List) map.get(\"set-cookie\");\n        matchContains(list, \"1P_JAR=2022-11-02-11; expires=Fri, 02-Dec-2022 11:50:46 GMT; path=/; domain=.google.com; Secure\");    \n    }    \n\n    @Test\n    void testRedirectAfterPostWithCookie() {\n        background()\n                .scenario(\"pathMatches('/first')\",\n                        \"def responseHeaders = { 'Set-Cookie': 'foo1=bar1', Location: '/second' }\",\n                        \"def responseStatus = 302\")\n                .scenario(\"pathMatches('/second')\",\n                        \"def response = requestHeaders\",\n                        \"def responseHeaders = { 'Set-Cookie': 'foo2=bar2' }\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'first'\",\n                \"form fields { username: 'blah', password: 'blah' }\",\n                \"method post\"\n        );\n        matchVarContains(\"response\", \"{ cookie: ['foo1=bar1'] }\");\n        Map<String, Object> map = (Map) get(\"responseHeaders\");\n        List<String> list = (List) map.get(\"Set-Cookie\");\n        matchContains(list, \"['foo1=bar1; Domain=localhost', 'foo2=bar2']\");\n    }\n\n    @Test\n    void testOptionsCorsResponseHeaders() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = { 'access-control-allow-credentials': 'true' }\"\n        );\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"method get\"\n        );\n        Map<String, Object> map = (Map) get(\"responseHeaders\");\n        List<String> list = (List) map.get(\"access-control-allow-credentials\");\n        matchContains(list, \"true\");\n    }\n    \n    @Test\n    void testKarateResponse() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = { 'Some-Header': 'Some-Value' }\"\n        );\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"method get\",\n                \"def headerValue = karate.response.header('some-header')\",\n                \"def headerValues = karate.response.headerValues('some-header')\"\n        );\n        matchVar(\"headerValue\", \"Some-Value\");\n        matchVar(\"headerValues\", \"['Some-Value']\");\n    }\n    \n    @Test\n    void testKarateRequest() {\n        background().scenario(\n                \"karate.request.header('some-header') == 'Some-Value'\",\n                \"def response = 'success'\"\n        );\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"header Some-Header = 'Some-Value'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"success\");\n    }\n\n    @Test\n    void testEmptyCookieNameSet() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = {'Set-Cookie': ' '}\"\n\n        );\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVarContains(\"responseHeaders\", \"{ set-cookie: [''] }\");\n    }\n\n    @Test\n    void testEmptyCookieNameAndEmptyAttributesSet() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = {'Set-Cookie': ';'}\"\n\n        );\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVarContains(\"responseHeaders\", \"{ set-cookie: [';'] }\");\n    }\n\n    @Test\n    void testEmptyCookieNameAndNonEmptyAttributeSet() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = {'Set-Cookie': '; SameSite=Strict'}\");\n        startMockServer();\n        run(\n                urlStep(),\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVarContains(\"responseHeaders\", \"{ set-cookie: ['; SameSite=Strict'] }\");\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/KarateMockHandlerTest.java",
    "content": "package com.intuit.karate.core;\n\nimport static com.intuit.karate.TestUtils.*;\nimport static com.intuit.karate.TestUtils.runScenario;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Calendar;\n\n/**\n *\n * @author pthomas3\n */\nclass KarateMockHandlerTest {\n\n    static final Logger logger = LoggerFactory.getLogger(KarateMockHandlerTest.class);\n\n    String URL_STEP = \"url 'http://localhost:8080'\";\n    MockHandler handler;\n    FeatureBuilder mock;\n    ScenarioRuntime runtime;\n    SimpleDateFormat sdf = new SimpleDateFormat(\"EEE, dd-MMM-yy HH:mm:ss z\");\n\n    FeatureBuilder background(String... lines) {\n        mock = FeatureBuilder.background(lines);\n        return mock;\n    }\n\n    Object get(String name) {\n        return runtime.engine.vars.get(name).getValue();\n    }\n\n    ScenarioRuntime run(String... lines) {\n        handler = new MockHandler(mock.build());\n        MockClient client = new MockClient(handler);\n        runtime = runScenario(e -> client, lines);\n        assertFalse(runtime.isFailed(), runtime.result.getFailureMessageForDisplay());\n        return runtime;\n    }\n\n    private void matchVar(String name, Object expected) {\n        match(get(name), expected);\n    }\n\n    @Test\n    void testSimpleGet() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = 'hello world'\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"hello world\");\n    }\n\n    @Test\n    void testSimplePost() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"request { foo: 'bar' }\",\n                \"method post\"\n        );\n        matchVar(\"response\", \"{ 'Content-Type': ['application/json; charset=UTF-8'] }\");\n    }\n\n    @Test\n    void testPathSubstitution() {\n        background().scenario(\n                \"pathMatches('/hello/{id}')\",\n                \"def response = pathParams\");\n        run(\n                URL_STEP,\n                \"def id = 42\",\n                \"path 'hello', id\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ id: '42' }\");\n    }\n\n    @Test\n    void testParam() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParams\");\n        run(\n                URL_STEP,\n                \"param foo = 'bar'\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ foo: ['bar'] }\");\n    }\n    \n    @Test\n    void testRequestUri() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestUri\");\n        run(\n                URL_STEP,\n                \"param foo = 'bar'\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"/hello?foo=bar\");\n    }    \n    \n    @Test\n    void testRequestPath() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestPath\");\n        run(\n                URL_STEP,\n                \"param foo = 'bar'\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"/hello\");\n    }     \n\n    @Test\n    void testParams() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParams\");\n        run(\n                URL_STEP,\n                \"params { foo: 'bar' }\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ foo: ['bar'] }\");\n    }\n\n    @Test\n    void testParamWithEmbeddedCommas() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParams\");\n        run(\n                URL_STEP,\n                \"param foo = 'bar,baz'\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ foo: ['bar,baz'] }\");\n    }\n\n    @Test\n    void testParamMultiValue() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParams\");\n        run(\n                URL_STEP,\n                \"param foo = ['bar', 'baz']\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ foo: ['bar', 'baz'] }\");\n    }\n    \n    @Test\n    void testParamMultiValueVariables() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParams\");\n        run(\n                URL_STEP,\n                \"def first = 'bar'\",\n                \"def second = 'baz'\",\n                \"param foo = ['#(first)', '#(second)']\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ foo: ['bar', 'baz'] }\");\n    }    \n\n    @Test\n    void testRequestBodyAsInteger() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = request\");\n        run(\n                URL_STEP,\n                \"path '/hello'\",\n                \"request 42\",\n                \"method post\"\n        );\n        matchVar(\"response\", \"42\");\n    }\n\n    @Test\n    void testHeaders() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"header foo = 'bar'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ foo: ['bar'] }\");\n    }\n\n    @Test\n    void testHeaderMultiValue() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"def fun = function(arg){ return [arg.first, arg.second] }\",\n                \"header Authorization = call fun { first: 'foo', second: 'bar' }\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ Authorization: ['foo', 'bar'] }\");\n    }\n\n    @Test\n    void testRequestContentTypeForJson() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"request { foo: 'bar' }\",\n                \"method post\"\n        );\n        matchVar(\"response\", \"{ 'Content-Type': ['application/json; charset=UTF-8'] }\");\n    }\n\n    @Test\n    void testResponseContentTypeForJson() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = { 'Content-Type': 'application/json' }\",\n                \"def response = '{ \\\"foo\\\": \\\"bar\\\"}'\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"method get\",\n                \"match responseHeaders == { 'Content-Type': ['application/json'] }\",\n                \"match header content-type == 'application/json'\",\n                \"match responseType == 'json'\"\n        );\n    }\n\n    @Test\n    void testCookie() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        run(\n                URL_STEP,\n                \"cookie foo = 'bar'\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ Cookie: ['foo=bar'] }\");\n    }\n\n    @Test\n    void testCookieWithDateInThePast() {\n        Calendar calendar = Calendar.getInstance();\n        calendar.add(java.util.Calendar.DATE, -1);\n        String pastDate = sdf.format(calendar.getTime());\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        run(\n                URL_STEP,\n                \"cookie foo = {value:'bar', expires: '\" + pastDate + \"'}\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ Cookie: ['foo=bar'] }\");\n    }\n\n    @Test\n    void testCookieWithDateInTheFuture() {\n        Calendar calendar = Calendar.getInstance();\n        calendar.add(java.util.Calendar.DATE, +1);\n        String futureDate = sdf.format(calendar.getTime());\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        run(\n                URL_STEP,\n                \"cookie foo = { value: 'bar', expires: '\" + futureDate + \"' }\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ Cookie: ['foo=bar'] }\");\n    }\n\n    @Test\n    void testCookieWithMaxAgeZero() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\");\n        run(\n                URL_STEP,\n                \"cookie foo = { value: 'bar', max-age: '0' }\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ Cookie: ['#string'] }\");\n    }\n    \n    @Test\n    void testCookieMalformed() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = { 'Set-Cookie': '; Secure; HttpOnly' }\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"responseHeaders\", \"{'Set-Cookie': ['; Secure; HttpOnly']}\");        \n    }\n\n    @Test\n    void testFormFieldGet() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParams\");\n        run(\n                URL_STEP,\n                \"form field foo = 'bar'\",\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"response\", \"{ foo: ['bar'] }\");\n    }\n\n    @Test\n    void testFormFieldPost() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = request\");\n        run(\n                URL_STEP,\n                \"form field foo = 'bar'\",\n                \"path 'hello'\",\n                \"method post\"\n        );\n        matchVar(\"response\", \"foo=bar\");\n    }\n\n    @Test\n    void testFormFieldAsArray() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = request\");\n        run(\n                URL_STEP,\n                \"form field foo = ['bar1', 'bar2']\",\n                \"path 'hello'\",\n                \"method post\"\n        );\n        matchVar(\"response\", \"foo=bar1&foo=bar2\");\n    }\n\n    @Test\n    void testMultiPartField() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParams\");\n        run(\n                URL_STEP,\n                \"multipart field foo = 'bar'\",\n                \"path 'hello'\",\n                \"method post\"\n        );\n        matchVar(\"response\", \"{ foo: ['bar'] }\");\n    }\n\n    @Test\n    void testMultiPartFieldWithInteger() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParams\");\n        run(\n                URL_STEP,\n                \"def data = { foo: 'a', bar: 1}\",\n                \"multipart fields data\",\n                \"path 'hello'\",\n                \"method post\"\n        );\n        matchVar(\"response\", \"{  foo: ['a'], bar: ['1'] } }\");\n    }\n\n    @Test\n    void testMultiPartFieldWithFloat() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParams\");\n        run(\n                URL_STEP,\n                \"def data = { foo: 1, bar: 2.0}\",\n                \"multipart fields data\",\n                \"path 'hello'\",\n                \"method post\"\n        );\n        matchVar(\"response\", \"{ foo: ['1'], bar: ['2.0'] } }\");\n    }\n\n    @Test\n    void testMultiPartFile() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParts\");\n        run(\n                URL_STEP,\n                \"multipart file foo = { filename: 'foo.txt', value: 'hello' }\",\n                \"path 'hello'\",\n                \"method post\"\n        );\n        matchVar(\"response\", \"{ foo: [{ name: 'foo', value: '#notnull', contentType: 'text/plain', charset: 'UTF-8', filename: 'foo.txt', transferEncoding: '7bit' }] }\");\n    }\n\n    @Test\n    void testMultiPartFiles() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParts\");\n        run(\n                URL_STEP,\n                \"def file1 = { name: 'file', filename: 'file1.txt', value: 'Hello 1' }\", \n                \"def file2 = { name: 'file', filename: 'file2.txt', value: 'Hello 2' }\", \n                \"multipart files ([file1, file2])\",\n                \"path 'hello'\",\n                \"method post\"\n        );\n        runtime.engine.assign(AssignType.STRING, \"prevReqBody\", \"karate.prevRequest.body\", false);\n        notContains(get(\"prevReqBody\"), \"multipart/mixed\");\n    }\n\n    @Test\n    void testMultiPartFileNullCharset() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestParts\");\n        run(\n                \"configure charset = null\",\n                URL_STEP,\n                \"multipart file foo = { filename: 'foo.txt', value: 'hello' }\",\n                \"path 'hello'\",\n                \"method post\"\n        );\n        matchVar(\"response\", \"{ foo: [{ name: 'foo', value: '#notnull', contentType: 'text/plain', charset: 'UTF-8', filename: 'foo.txt', transferEncoding: '7bit' }] }\");\n    }\n\n    @Test\n    void testConfigureResponseHeaders() {\n        background(\"configure responseHeaders = { 'Content-Type': 'text/html' }\")\n                .scenario(\n                        \"pathMatches('/hello')\",\n                        \"def response = ''\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"responseHeaders\", \"{ 'Content-Type': ['text/html'] }\");\n    }\n\n    @Test\n    void testConfigureLowerCaseResponseHeaders() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = { 'Content-Type': 'text/html' }\",\n                \"def response = ''\");\n        run(\n                \"configure lowerCaseResponseHeaders = true\",\n                URL_STEP,\n                \"path 'hello'\",\n                \"method get\"\n        );\n        matchVar(\"responseHeaders\", \"{ 'content-type': ['text/html'] }\");\n    }\n\n    @Test\n    void testResponseContentTypeForXml() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = { 'Content-Type': 'application/xml' }\",\n                \"def response = '<hello>world</hello>'\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"method get\",\n                \"match header content-type == 'application/xml'\",\n                \"match responseType == 'xml'\",\n                \"match response.hello == 'world'\"\n        );\n    }\n\n    @Test\n    void testNoResponseAutoConversionForUnknownContentType() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = '<hello>world</hello>'\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"method get\",\n                \"match header content-type == 'text/plain'\",\n                \"match responseType == 'string'\"\n        );\n    }\n\n    @Test\n    void testResponseAutoConversionForJsonAsPlainText() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = '{ \\\"foo\\\": \\\"bar\\\"}'\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"method get\",\n                \"match header content-type == 'text/plain'\",\n                \"match responseType == 'json'\",\n                \"match response.foo == 'bar'\"\n        );\n    }\n\n    @Test\n    void testResponseAutoConversionForTextWithTags() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = '<http://example.org/#hello> a <http://example.org/#greeting> .'\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"method get\",\n                \"match header content-type == 'text/plain'\",\n                \"match responseType == 'string'\",\n                \"match response == '<http://example.org/#hello> a <http://example.org/#greeting> .'\"\n        );\n    }\n\n    @Test\n    void testResponseContentTypeForNonXmlWithTags() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = { 'Content-Type': 'text/turtle' }\",\n                \"def response = '<http://example.org/#hello> a <http://example.org/#greeting> .'\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"method get\",\n                \"match header content-type == 'text/turtle'\",\n                \"match responseType == 'string'\",\n                \"match response == '<http://example.org/#hello> a <http://example.org/#greeting> .'\"\n        );\n    }\n\n    @Test\n    void testResponseContentTypeForNonXmlWithTagsAndCharset() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def responseHeaders = { 'Content-Type': 'text/turtle; charset=UTF-8' }\",\n                \"def response = '<http://example.org/#hello> a <http://example.org/#greeting> .'\");\n        run(\n                URL_STEP,\n                \"path 'hello'\",\n                \"method get\",\n                \"match header content-type contains 'text/turtle'\",\n                \"match header content-type != 'text/turtle'\",\n                \"match responseType == 'string'\",\n                \"match response == '<http://example.org/#hello> a <http://example.org/#greeting> .'\"\n        );\n    }\n\n    @Test\n    void testWildcardLikePathMatch() {\n        background().scenario(\n                \"requestUri.startsWith('/hello/')\",\n                \"def response = requestUri\");\n        run(\n                URL_STEP,\n                \"path 'hello', 'foo', 'bar'\",\n                \"method get\",\n                \"match response == '/hello/foo/bar'\"\n        );\n    }\n\n    @Test\n    void testPathFromStringVariable() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestUri\");\n        run(\n                URL_STEP,\n                \" def temp = 'hello'\",\n                \"path temp\",\n                \"method get\",\n                \"match response == '/hello'\"\n        );\n    }\n\n    @Test\n    void testPathFromArrayVariable() {\n        background().scenario(\n                \"pathMatches('/hello/world')\",\n                \"def response = requestUri\");\n        run(\n                URL_STEP,\n                \" def temp = ['hello', 'world']\",\n                \"path temp\",\n                \"method get\",\n                \"match response == '/hello/world'\"\n        );\n    }\n\n    @Test\n    void testPathWithForwardSlashes() {\n        background().scenario(\n                \"pathMatches('/hello/world')\",\n                \"def response = requestUri\");\n        run(\n                URL_STEP,\n                \"path '/hello/world'\",\n                \"method get\",\n                \"match response == '/hello/world'\"\n        );\n    }\n\n    @Test\n    void testPathWithEscapedSlashes() {\n        background().scenario(\n                \"pathMatches('/hello/world')\",\n                \"def response = requestUri\");\n        run(\n                URL_STEP,\n                \"path '/hello\\\\\\\\/world'\",\n                \"method get\",\n                \"match response == '/hello%2Fworld'\"\n        );\n    }\n\n    @Test\n    void testPathWithTrailingSlash() {\n        background().scenario(\n                \"pathMatches('/hello/world')\",\n                \"def response = requestUri\");\n        run(\n                URL_STEP,\n                \"path '/hello/world', '/'\",\n                \"method get\",\n                \"match response == '/hello/world/'\"\n        );\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/MockClient.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.http.HttpClient;\nimport com.intuit.karate.http.HttpRequest;\nimport com.intuit.karate.http.Response;\nimport com.intuit.karate.http.ServerHandler;\n\n/**\n *\n * @author pthomas3\n */\npublic class MockClient implements HttpClient {\n\n    private final ServerHandler handler;\n    private Config config = new Config();\n    \n    public MockClient(ServerHandler handler) {\n        this.handler = handler;\n    }\n\n    @Override\n    public void setConfig(Config config) {\n        this.config = config;\n    }\n\n    @Override\n    public Config getConfig() {\n        return config;\n    }\n\n    @Override\n    public Response invoke(HttpRequest request) {\n        return handler.handle(request.toRequest());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/MockHandlerTest.java",
    "content": "package com.intuit.karate.core;\n\nimport static com.intuit.karate.TestUtils.*;\nimport com.intuit.karate.http.HttpClient;\nimport com.intuit.karate.http.HttpRequestBuilder;\nimport com.intuit.karate.http.Response;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass MockHandlerTest {\n\n    static final Logger logger = LoggerFactory.getLogger(MockHandlerTest.class);\n\n    HttpClient client = new DummyClient();\n    MockHandler handler;\n    FeatureBuilder feature;\n    HttpRequestBuilder request;\n    Response response;\n\n    @BeforeEach\n    void beforeEach() {\n        request = new HttpRequestBuilder(client).url(\"/\").method(\"GET\");\n    }\n\n    FeatureBuilder background(String... lines) {\n        feature = FeatureBuilder.background(lines);\n        return feature;\n    }\n\n    private Response handle() {\n        handler = new MockHandler(feature.build());\n        response = handler.handle(request.build().toRequest());\n        request = new HttpRequestBuilder(client).method(\"GET\");\n        return response;\n    }\n\n    @Test\n    void testSimpleResponse() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = 'hello world'\"\n        );\n        request.path(\"/hello\");\n        handle();\n        match(response.getBodyAsString(), \"hello world\");\n    }\n\n    @Test\n    void testRequestMethod() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def isPost = methodIs('post')\",\n                \"def method = requestMethod\",\n                \"def response = { isPost: '#(isPost)', method: '#(method)' }\"\n        );\n        request.path(\"/hello\").method(\"POST\");\n        handle();\n        match(response.getBodyConverted(), \"{ isPost: true, method: 'POST' }\");\n    }\n\n    @Test\n    void testPathParams() {\n        background().scenario(\n                \"pathMatches('/hello/{name}')\",\n                \"def response = 'hello ' + pathParams.name\"\n        );\n        request.path(\"/hello/john\");\n        handle();\n        match(response.getBodyAsString(), \"hello john\");\n    }\n\n    @Test\n    void testQueryParams() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = 'hello ' + paramValue('foo')\"\n        );\n        request.path(\"/hello\").param(\"foo\", \"world\");\n        handle();\n        match(response.getBodyAsString(), \"hello world\");\n    }\n\n    @Test\n    void testQueryParamExists() {\n        background().scenario(\n                \"pathMatches('/hello') && paramExists('foo')\",\n                \"def response = 'hello ' + paramValue('foo')\"\n        );\n        request.path(\"/hello\").param(\"foo\", \"world\");\n        handle();\n        match(response.getBodyAsString(), \"hello world\");\n    }\n\n    @Test\n    void testFormFieldsRequestPost() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = request\"\n        );\n        request.path(\"/hello\").formField(\"foo\", \"hello world\").method(\"POST\");\n        handle();\n        match(response.getBodyAsString(), \"foo=hello+world\");\n    }\n\n    @Test\n    void testFormFieldsRequestGet() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def exists = paramExists('foo')\",\n                \"def value = paramValue('foo')\",\n                \"def response = { exists: '#(exists)', value: '#(value)' }\"\n        );\n        request.path(\"/hello\").formField(\"foo\", \"hello world\").method(\"GET\");\n        handle();\n        match(response.getBodyConverted(), \"{ exists: true, value: 'hello world' }\");\n    }\n\n    @Test\n    void testTypeContains() {\n        background().scenario(\n                \"pathMatches('/hello') && typeContains('json')\",\n                \"def response = { success: true }\"\n        );\n        request.path(\"/hello\").contentType(\"application/json\").method(\"GET\");\n        handle();\n        match(response.getBodyConverted(), \"{ success: true }\");\n    }\n\n    @Test\n    void testAcceptContains() {\n        background().scenario(\n                \"pathMatches('/hello') && acceptContains('json')\",\n                \"def response = requestHeaders\"\n        );\n        request.path(\"/hello\").header(\"accept\", \"application/json\").method(\"GET\");\n        handle();\n        match(response.getBodyConverted(), \"{ accept: ['application/json'] }\");\n    }\n\n    @Test\n    void testHeaderContains() {\n        background().scenario(\n                \"pathMatches('/hello') && headerContains('foo', 'bar')\",\n                \"def response = { success: true }\"\n        );\n        request.path(\"/hello\").header(\"foo\", \"baabarbaa\").method(\"GET\");\n        handle();\n        match(response.getBodyConverted(), \"{ success: true }\");\n    }\n\n    @Test\n    void testRequestHeaders() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = requestHeaders\"\n        );\n        request.path(\"/hello\").header(\"foo\", \"bar\").method(\"GET\");\n        handle();\n        match(response.getBodyConverted(), \"{ foo: ['bar'] }\");\n    }\n\n    @Test\n    void testBodyPath() {\n        background().scenario(\n                \"pathMatches('/hello') && bodyPath('$.foo') == 'bar'\",\n                \"def response = { success: true }\"\n        );\n        request.path(\"/hello\").bodyJson(\"{ foo: 'bar' }\");\n        handle();\n        match(response.getBodyConverted(), \"{ success: true }\");\n    }\n\n    @Test\n    void testResponseStatus() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = { success: false }\",\n                \"def responseStatus = 404\"\n        );\n        request.path(\"/hello\");\n        handle();\n        match(response.getBodyConverted(), \"{ success: false }\");\n        match(response.getStatus(), 404);\n    }\n\n    @Test\n    void testResponseHeaders() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = { success: false }\",\n                \"def responseHeaders = { foo: 'bar' }\"\n        );\n        request.path(\"/hello\");\n        handle();\n        match(response.getBodyConverted(), \"{ success: false }\");\n        match(response.getHeader(\"foo\"), \"bar\");\n    }\n\n    @Test\n    void testMultiPart() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def foo = requestParams.foo[0]\",\n                \"string bar = requestParts.bar[0].value\",\n                \"def response = { foo: '#(foo)', bar: '#(bar)' }\"\n        );\n        request.path(\"/hello\")\n                .multiPartJson(\"{ name: 'foo', value: 'hello world' }\")\n                .multiPartJson(\"{ name: 'bar', value: 'some bytes', filename: 'bar.txt' }\")\n                .method(\"POST\");\n        handle();\n        match(response.getBodyConverted(), \"{ foo: 'hello world', bar: 'some bytes' }\");\n    }\n\n    @Test\n    void testAbort() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = 'before'\",\n                \"karate.abort()\",\n                \"def response = 'after'\"\n        );\n        request.path(\"/hello\");\n        handle();\n        match(response.getBodyAsString(), \"before\");\n    }\n\n    @Test\n    void testUrlWithSpecialCharacters() {\n        background().scenario(\n                \"pathMatches('/hello/{raw}')\",\n                \"def response = pathParams.raw\"\n        );\n        request.path(\"/hello/�Ill~Formed@RequiredString!\");\n        handle();\n        match(response.getBodyAsString(), \"�Ill~Formed@RequiredString!\");\n    }\n\n    @Test\n    void testGraalJavaClassLoading() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def Utils = Java.type('com.intuit.karate.core.MockUtils')\",\n                \"def response = Utils.testBytes\"\n        );\n        request.path(\"/hello\");\n        handle();\n        match(response.getBody(), MockUtils.testBytes);\n    }\n\n    @Test\n    void testJsVariableInBackground() {\n        background(\n                \"def nextId = call read('increment.js')\"\n        ).scenario(\n                \"pathMatches('/hello')\", \n                \"def response = nextId()\"\n        );\n        request.path(\"/hello\");\n        handle();\n        match(response.getBodyAsString(), \"1\");\n    }\n    \n    @Test\n    void testJsonBodyPathThatExists() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = bodyPath('root.foo')\"\n        );\n        request.path(\"/hello\")\n                .bodyJson(\"{ root: { foo: 'bar' } }\");\n        handle();\n        match(response.getBodyAsString(), \"bar\");        \n    } \n    \n    @Test\n    void testJsonBodyPathThatDoesNotExist() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def result = bodyPath('root.nope')\",\n                \"def response = result == null ? 'NULL' : 'NOTNULL'\"                \n        );\n        request.path(\"/hello\")\n                .bodyJson(\"{ root: { foo: 'bar' } }\");\n        handle();\n        match(response.getBodyAsString(), \"NULL\");        \n    }      \n    \n    @Test\n    void testXmlBodyPathThatExists() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def response = bodyPath('/root/foo')\"\n        );\n        request.path(\"/hello\")\n                .body(\"<root><foo>bar</foo></root>\")\n                .contentType(\"application/xml\");\n        handle();\n        match(response.getBodyAsString(), \"bar\");        \n    }\n    \n    @Test\n    void testXmlBodyPathThatDoesNotExist() {\n        background().scenario(\n                \"pathMatches('/hello')\",\n                \"def result = bodyPath('/root/nope')\",\n                \"def response = result == null ? 'NULL' : 'NOTNULL'\"\n        );\n        request.path(\"/hello\")\n                .body(\"<root><foo>bar</foo></root>\")\n                .contentType(\"application/xml\");\n        handle();\n        match(response.getBodyAsString(), \"NULL\");        \n    }    \n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/MockUtils.java",
    "content": "package com.intuit.karate.core;\n\n/**\n *\n * @author pthomas3\n */\npublic class MockUtils {\n    \n    public static final byte[] testBytes = new byte[]{15, 98, -45, 0, 0, 7, -124, 75, 12, 26, 0, 9};\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/PaymentsMockRunner.java",
    "content": "package com.intuit.karate.core;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author peter\n */\nclass PaymentsMockRunner {\n\n    @Test\n    void startMock() {\n        MockServer server = MockServer\n                .feature(\"classpath:com/intuit/karate/core/payments-mock.feature\")\n                .pathPrefix(\"/api\")\n                .http(8080).build();\n        server.waitSync();        \n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/PaymentsRunner.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author peter\n */\nclass PaymentsRunner {\n\n    @Test\n    void testPayments() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/payments.feature\").parallel(1);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/PerfHookTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.PerfHook;\nimport com.intuit.karate.Runner;\nimport static com.intuit.karate.TestUtils.*;\nimport com.intuit.karate.http.HttpRequest;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.UUID;\nimport org.junit.jupiter.api.AfterAll;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass PerfHookTest {\n\n    static final Logger logger = LoggerFactory.getLogger(PerfHookTest.class);\n\n    static MockServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        server = MockServer\n                .feature(\"classpath:com/intuit/karate/core/perf-mock.feature\")\n                .http(0).build();\n        System.setProperty(\"karate.server.port\", server.getPort() + \"\");\n    }\n\n    @BeforeEach\n    void beforeEach() {\n        eventName = null;\n        featureResult = null;\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n    }\n\n    @Test\n    void testPerfHook1() {\n        // run a passing scenario\n        String bar = UUID.randomUUID().toString().replaceAll(\"-\", \"\");\n        Map<String, Object> arg = Collections.singletonMap(\"bar\", bar);\n        Runner.callAsync(Runner.builder().tags(\"@name=pass\"), \"classpath:com/intuit/karate/core/perf.feature\", arg, perfHook);\n        assertEquals(eventName, \"http://localhost:\" + server.getPort() + \"/hello?foo=\" + bar);\n        assertNotNull(featureResult);\n        assertFalse(featureResult.isEmpty());\n        assertFalse(featureResult.isFailed());\n        assertEquals(featureResult.getScenarioCount(), 1);\n        assertEquals(featureResult.getPassedCount(), 1);\n        assertEquals(featureResult.getFailedCount(), 0);\n        matchContains(featureResult.getVariables(), \"{ configSource: 'normal', responseStatus: 200, response: { foo: ['\" + bar + \"'] } }\");\n    }\n\n    @Test\n    void testPerfHook2() {\n        // run a scenario which fails the status check\n        String bar = UUID.randomUUID().toString().replaceAll(\"-\", \"\");\n        Map<String, Object> arg = Collections.singletonMap(\"bar\", bar);\n        Runner.callAsync(Runner.builder().tags(\"@name=failStatus\"), \"classpath:com/intuit/karate/core/perf.feature\", arg, perfHook);\n        assertEquals(eventName, \"http://localhost:\" + server.getPort() + \"/hello?foo=\" + bar);\n        assertNotNull(featureResult);\n        assertFalse(featureResult.isEmpty());\n        assertTrue(featureResult.isFailed());\n        assertEquals(featureResult.getScenarioCount(), 1);\n        assertEquals(featureResult.getPassedCount(), 0);\n        assertEquals(featureResult.getFailedCount(), 1);\n        matchContains(featureResult.getVariables(), \"{ configSource: 'normal', responseStatus: 200, response: { foo: ['\" + bar + \"'] } }\");\n    }\n\n    @Test\n    void testPerfHook3() {\n        // run a scenario which fails the response match\n        String bar = UUID.randomUUID().toString().replaceAll(\"-\", \"\");\n        Map<String, Object> arg = Collections.singletonMap(\"bar\", bar);\n        Runner.callAsync(Runner.builder().tags(\"@name=failResponse\"), \"classpath:com/intuit/karate/core/perf.feature\", arg, perfHook);\n        assertEquals(eventName, \"http://localhost:\" + server.getPort() + \"/hello?foo=\" + bar);\n        assertNotNull(featureResult);\n        assertFalse(featureResult.isEmpty());\n        assertTrue(featureResult.isFailed());\n        assertEquals(featureResult.getScenarioCount(), 1);\n        assertEquals(featureResult.getPassedCount(), 0);\n        assertEquals(featureResult.getFailedCount(), 1);\n        matchContains(featureResult.getVariables(), \"{ configSource: 'normal', responseStatus: 200, response: { foo: ['\" + bar + \"'] } }\");\n    }\n\n    @Test\n    void testPerfHook4() {\n        // run a scenario without passing a required argument\n        Runner.callAsync(Runner.builder().tags(\"@name=pass\"), \"classpath:com/intuit/karate/core/perf.feature\", null, perfHook);\n        assertNull(eventName);\n        assertNotNull(featureResult);\n        assertFalse(featureResult.isEmpty());\n        assertTrue(featureResult.isFailed());\n        assertEquals(featureResult.getScenarioCount(), 1);\n        assertEquals(featureResult.getPassedCount(), 0);\n        assertEquals(featureResult.getFailedCount(), 1);\n        match(featureResult.getVariables(), \"{ configSource: 'normal', functionFromKarateBase: '#notnull' }\");\n    }\n\n    @Test\n    void testPerfHook5() {\n        // run a scenario which doesn't exist\n        Runner.callAsync(Runner.builder().tags(\"@name=doesntExist\"), \"classpath:com/intuit/karate/core/perf.feature\", null, perfHook);\n        assertNull(eventName);\n        assertNotNull(featureResult);\n        assertTrue(featureResult.isEmpty());\n        assertFalse(featureResult.isFailed());\n        assertEquals(featureResult.getScenarioCount(), 0);\n        assertEquals(featureResult.getPassedCount(), 0);\n        assertEquals(featureResult.getFailedCount(), 0);\n        assertEquals(featureResult.getVariables(), Collections.emptyMap());\n    }\n\n    @Test\n    void testPerfHook6() {\n        // run a feature which doesn't exist\n        String feature = \"com/intuit/karate/core/doesntExist.feature\";\n        try {\n            Runner.callAsync(Runner.builder(), \"classpath:\" + feature, null, perfHook);\n            fail(\"we expected execution to fail\");\n        } catch (RuntimeException e) {\n            assertEquals(e.getMessage(), \"not found: \" + feature);\n        }\n        assertNull(eventName);\n        assertNull(featureResult);\n    }\n\n    String eventName;\n    FeatureResult featureResult;\n    PerfHook perfHook = new PerfHook() {\n\n        @Override\n        public String getPerfEventName(HttpRequest request, ScenarioRuntime sr) {\n            return request.getUrl();\n        }\n\n        @Override\n        public void reportPerfEvent(PerfEvent event) {\n            eventName = event.getName();\n            logger.debug(\"perf event: {}\", eventName);\n        }\n\n        @Override\n        public void submit(Runnable runnable) {\n            logger.debug(\"submit called\");\n            runnable.run();\n        }\n\n        @Override\n        public void afterFeature(FeatureResult fr) {\n            featureResult = fr;\n            logger.debug(\"afterFeature called\");\n        }\n\n        @Override\n        public void pause(Number millis) {\n            \n        }                \n\n    };\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/PrintTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\npublic class PrintTest {\n\n    @Test\n    void testPrint() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/print.feature\").parallel(1);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/ScenarioBridgeTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.TestUtils;\nimport com.intuit.karate.graal.JsMap;\nimport org.graalvm.polyglot.Value;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ScenarioBridgeTest {\n    private static final Logger logger        = LoggerFactory.getLogger(ScenarioBridgeTest.class);\n\n    private static final String csDATA         = \"data\";\n    private static final String csFEATUREFILE  = \"classpath:com/intuit/karate/core/callSingleFeature.feature\";\n    private static final String csKEY          = \"receivedParam\";\n    private static final String csNOTHING      = \"Nothing\";\n    private static final String csSCENARIOFILE = \"classpath:com/intuit/karate/core/callSingleScenario.feature@storeValue\";\n\n    private ScenarioBridge  moTest;\n    private ScenarioEngine  moEngine;\n\n    @BeforeEach\n    void beforeEach() {\n        moEngine = TestUtils.engine();\n        moEngine.init();\n        moTest = new ScenarioBridge(moEngine);\n    }\n\n    /**\n     * Verify the answer\n     * @param inoToCheck Object to check\n     * @param insValue  Value to check\n     * @return true if the answer is correct\n     */\n    private boolean verifyAnswer(JsMap inoToCheck, String insValue) {\n        assertNotNull(inoToCheck);\n        assertEquals(insValue, inoToCheck.get(csKEY));\n        return true;\n    }\n\n    @Test\n    void testCallSingle() throws Exception {\n        JsMap oFound = (JsMap) moTest.callSingle(csFEATUREFILE);\n\n        assertTrue(verifyAnswer(oFound, csNOTHING));\n        JsMap oFound1 = (JsMap) moTest.callSingle(csFEATUREFILE);\n\n        assertTrue(verifyAnswer(oFound1, csNOTHING));\n\n        oFound1 = (JsMap) moTest.callSingle(csFEATUREFILE + \"?testWithoutParam\");\n        assertTrue(verifyAnswer(oFound1, csNOTHING));\n    }\n\n    @Test\n    void testCallSingleWithParam() throws Exception {\n        final Map<String, Object>   mapValue = new LinkedHashMap(3);\n        final Value                 oParam = Value.asValue(mapValue);\n        JsMap                       oFound = (JsMap) moTest.callSingle(csFEATUREFILE, oParam);\n        String                      sParam = \"first\";\n\n        assertTrue(verifyAnswer(oFound, csNOTHING));\n        JsMap oFound1 = (JsMap) moTest.callSingle(csFEATUREFILE, oParam);\n\n        assertTrue(verifyAnswer(oFound1, csNOTHING));\n\n        oFound1 = (JsMap) moTest.callSingle(csFEATUREFILE + \"?test\", oParam);\n        assertTrue(verifyAnswer(oFound1, csNOTHING));\n\n        mapValue.put(csDATA, sParam);\n        oFound1 = (JsMap) moTest.callSingle(csFEATUREFILE + \"?test\", oParam);\n        assertTrue(verifyAnswer(oFound1, csNOTHING));\n\n        oFound1 = (JsMap) moTest.callSingle(csFEATUREFILE + \"?first\", oParam);\n        assertTrue(verifyAnswer(oFound1, sParam));\n\n        mapValue.clear();\n        oFound1 = (JsMap) moTest.callSingle(csFEATUREFILE + \"?first\", oParam);\n        assertTrue(verifyAnswer(oFound1, sParam));\n    }\n\n    @Test\n    void testCallSingleScenarioWithParam() throws Exception {\n        final Map<String, Object>   mapValue = new LinkedHashMap(3);\n        final Value                 oParam = Value.asValue(mapValue);\n        JsMap                       oFound = (JsMap) moTest.callSingle(csSCENARIOFILE, oParam);\n        String                      sParam = \"two\";;\n\n        assertTrue(verifyAnswer(oFound, csNOTHING));\n        JsMap oFound1 = (JsMap) moTest.callSingle(csSCENARIOFILE, oParam);\n\n        assertTrue(verifyAnswer(oFound1, csNOTHING));\n\n        oFound1 = (JsMap) moTest.callSingle(csSCENARIOFILE + \"?test1\", oParam);\n        assertTrue(verifyAnswer(oFound1, csNOTHING));\n\n        mapValue.put(csDATA, sParam);\n        oFound1 = (JsMap) moTest.callSingle(csSCENARIOFILE + \"?test1\", oParam);\n        assertTrue(verifyAnswer(oFound1, csNOTHING));\n\n        oFound1 = (JsMap) moTest.callSingle(csSCENARIOFILE + \"?two\", oParam);\n        assertTrue(verifyAnswer(oFound1, sParam));\n\n        mapValue.clear();\n        oFound1 = (JsMap) moTest.callSingle(csSCENARIOFILE + \"?two\", oParam);\n        assertTrue(verifyAnswer(oFound1, sParam));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/ScenarioEngineTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.Json;\nimport com.intuit.karate.Match;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.TestUtils;\nimport com.intuit.karate.graal.JsValue;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * @author pthomas3\n */\npublic class ScenarioEngineTest {\n\n    static final Logger logger = LoggerFactory.getLogger(ScenarioEngineTest.class);\n\n    ScenarioEngine engine;\n\n    @BeforeEach\n    void beforeEach() {\n        engine = TestUtils.engine();\n        engine.init();\n    }\n\n    private void matchEval(Object before, Object after) {\n        Variable actual = new Variable(Match.parseIfJsonOrXmlString(before));\n        Variable expected = engine.evalEmbeddedExpressions(actual, false);\n        Match.Result mr = Match.evaluate(expected.getValue()).isEqualTo(Match.parseIfJsonOrXmlString(after));\n        assertTrue(mr.pass, mr.message);\n    }\n\n    private void assign(String name, String expression) {\n        engine.assign(AssignType.AUTO, name, expression, false);\n    }\n\n    private void matchEquals(String lhs, String rhs) {\n        Match.Result mr = engine.match(Match.Type.EQUALS, lhs, null, rhs);\n        assertTrue(mr.pass, mr.message);\n    }\n\n    private void matchNotEquals(String lhs, String rhs) {\n        assertFalse(engine.match(Match.Type.EQUALS, lhs, null, rhs).pass);\n    }\n\n    @Test\n    void testHelpers() {\n        assertTrue(ScenarioEngine.isVariable(\"foo\"));\n        assertTrue(ScenarioEngine.isXmlPath(\"/foo\"));\n        assertTrue(ScenarioEngine.isXmlPath(\"//foo\"));\n        assertTrue(ScenarioEngine.isXmlPathFunction(\"lower-case('Foo')\"));\n        assertTrue(ScenarioEngine.isXmlPathFunction(\"count(/journal/article)\"));\n        assertTrue(ScenarioEngine.isVariableAndSpaceAndPath(\"foo count(/journal/article)\"));\n        assertTrue(ScenarioEngine.isVariableAndSpaceAndPath(\"foo $\"));\n    }\n\n    @Test\n    void testVariableNameValidation() {\n        assertTrue(ScenarioEngine.isValidVariableName(\"foo\"));\n        assertTrue(ScenarioEngine.isValidVariableName(\"foo_bar\"));\n        assertTrue(ScenarioEngine.isValidVariableName(\"foo_\"));\n        assertTrue(ScenarioEngine.isValidVariableName(\"foo1\"));\n        assertTrue(ScenarioEngine.isValidVariableName(\"a\"));\n        assertTrue(ScenarioEngine.isValidVariableName(\"a1\"));\n        // bad\n        assertFalse(ScenarioEngine.isValidVariableName(\"foo.bar\"));\n        assertFalse(ScenarioEngine.isValidVariableName(\"foo-bar\"));\n        assertFalse(ScenarioEngine.isValidVariableName(\"$foo\"));\n        assertFalse(ScenarioEngine.isValidVariableName(\"$foo/bar\"));\n        assertFalse(ScenarioEngine.isValidVariableName(\"_foo\"));\n        assertFalse(ScenarioEngine.isValidVariableName(\"_foo_\"));\n        assertFalse(ScenarioEngine.isValidVariableName(\"0\"));\n        assertFalse(ScenarioEngine.isValidVariableName(\"2foo\"));\n    }\n\n    @Test\n    void testParsingVariableAndJsonPath() {\n        assertEquals(StringUtils.pair(\"foo\", \"$\"), ScenarioEngine.parseVariableAndPath(\"foo\"));\n        assertEquals(StringUtils.pair(\"foo\", \"$.bar\"), ScenarioEngine.parseVariableAndPath(\"foo.bar\"));\n        assertEquals(StringUtils.pair(\"foo\", \"$['bar']\"), ScenarioEngine.parseVariableAndPath(\"foo['bar']\"));\n        assertEquals(StringUtils.pair(\"foo\", \"$[0]\"), ScenarioEngine.parseVariableAndPath(\"foo[0]\"));\n        assertEquals(StringUtils.pair(\"foo\", \"$[0].bar\"), ScenarioEngine.parseVariableAndPath(\"foo[0].bar\"));\n        assertEquals(StringUtils.pair(\"foo\", \"$[0]['bar']\"), ScenarioEngine.parseVariableAndPath(\"foo[0]['bar']\"));\n        assertEquals(StringUtils.pair(\"foo\", \"/bar\"), ScenarioEngine.parseVariableAndPath(\"foo/bar\"));\n        assertEquals(StringUtils.pair(\"foo\", \"/\"), ScenarioEngine.parseVariableAndPath(\"foo/\"));\n        assertEquals(StringUtils.pair(\"foo\", \"/\"), ScenarioEngine.parseVariableAndPath(\"foo /\"));\n        assertEquals(StringUtils.pair(\"foo\", \"/bar\"), ScenarioEngine.parseVariableAndPath(\"foo /bar\"));\n        assertEquals(StringUtils.pair(\"foo\", \"/bar/baz[1]/ban\"), ScenarioEngine.parseVariableAndPath(\"foo/bar/baz[1]/ban\"));\n    }\n\n    @Test\n    void testJsFunction() {\n        assertTrue(ScenarioEngine.isJavaScriptFunction(\"function(){ return { bar: 'baz' } }\"));\n        assertTrue(ScenarioEngine.isJavaScriptFunction(\"function() {   \\n\"\n                + \"  return { someConfig: 'someValue' }\\n\"\n                + \"}\"));\n        assertTrue(ScenarioEngine.isJavaScriptFunction(\"function fn(){ return { bar: 'baz' } }\"));\n    }\n\n    @Test\n    void testEmbeddedString() {\n        matchEval(\"hello\", \"hello\");\n        matchEval(\"#(1)\", 1);\n        matchEval(\"#(null)\", null);\n        matchEval(\"#('foo')\", \"foo\");\n        matchEval(\"##('foo')\", \"foo\");\n        matchEval(\"##(null)\", null);\n        engine.evalJs(\"var bar = null\");\n        matchEval(\"##(bar)\", null);\n    }\n\n    @Test\n    void testEmbeddedList() {\n        engine.evalJs(\"var foo = 3\");\n        matchEval(\"[1, 2, '#(foo)']\", \"[1, 2, 3]\");\n        engine.evalJs(\"var foo = [3, 4]\");\n        matchEval(\"[1, 2, '#(foo)']\", \"[1, 2, [3, 4]]\");\n        engine.evalJs(\"var foo = null\");\n        matchEval(\"[1, 2, '#(foo)']\", \"[1, 2, null]\");\n        matchEval(\"[1, 2, '##(foo)']\", \"[1, 2]\");\n        matchEval(\"[1, '##(foo)', 3]\", \"[1, 3]\");\n        engine.evalJs(\"var bar = null\");\n        matchEval(\"['##(foo)', 2, '##(bar)']\", \"[2]\");\n    }\n\n    @Test\n    void testEmbeddedMap() {\n        engine.evalJs(\"var foo = 2\");\n        matchEval(\"{ a: 1, b: '#(foo)', c: 3}\", \"{ a: 1, b: 2, c: 3}\");\n        matchEval(\"{ a: 1, b: '#(foo)', c: '#(foo)'}\", \"{ a: 1, b: 2, c: 2}\");\n        engine.evalJs(\"var bar = null\");\n        matchEval(\"{ a: 1, b: '#(bar)', c: '#(foo)'}\", \"{ a: 1, b: null, c: 2}\");\n        matchEval(\"{ a: 1, b: '##(bar)', c: '#(foo)'}\", \"{ a: 1, c: 2}\");\n        assign(\"a\", \"1\");\n        assign(\"b\", \"2\");\n        assign(\"myJson\", \"{ foo: '#(a + b)' }\");\n        matchEquals(\"myJson.foo\", \"3\");\n        assign(\"ticket\", \"{ ticket: 'my-ticket', userId: '12345' }\");\n        assign(\"myJson\", \"{ foo: '#(ticket.userId)' }\");\n        matchEquals(\"myJson\", \"{ foo: '12345' }\");\n        assign(\"foo\", \"{ a: null, b: null }\");\n        assign(\"bar\", \"{ hello: '#(foo.a)', world: '##(foo.b)'  }\");\n        matchEquals(\"bar\", \"{ hello: null }\");\n    }\n\n    @Test\n    void testEmbeddedXml() {\n        assign(\"a\", \"1\");\n        assign(\"b\", \"2\");\n        assign(\"myXml\", \"<root><foo>#(a + b)</foo></root>\");\n        Variable value = engine.evalXmlPathOnVariableByName(\"myXml\", \"/root/foo\");\n        matchEval(value.getValue(), \"3\"); // TODO BREAKING '3' before graal  \n        assign(\"hello\", \"<hello>world</hello>\");\n        assign(\"myXml\", \"<foo><bar>#(hello)</bar></foo>\");\n        matchEquals(\"myXml\", \"<foo><bar><hello>world</hello></bar></foo>\");\n        assign(\"hello\", \"null\");\n        assign(\"myXml\", \"<foo><bar>#(hello)</bar></foo>\");\n        matchEquals(\"myXml\", \"<foo><bar></bar></foo>\");\n        matchEquals(\"myXml\", \"<foo><bar/></foo>\");\n        assign(\"a\", \"5\");\n        assign(\"myXml\", \"<foo bar=\\\"#(a)\\\">#(a)</foo>\");\n        matchEquals(\"myXml\", \"<foo bar=\\\"5\\\">5</foo>\");\n        assign(\"a\", \"null\");\n        assign(\"myXml\", \"<foo bar=\\\"##(a)\\\">baz</foo>\");\n        matchEquals(\"myXml\", \"<foo>baz</foo>\");\n        assign(\"myXml\", \"<foo><a>hello</a><b>##(a)</b></foo>\");\n        matchEquals(\"myXml\", \"<foo><a>hello</a></foo>\");\n    }\n\n    @Test\n    void testEvalXmlAndXpath() {\n        assign(\"myXml\", \"<root><foo>bar</foo><hello>world</hello></root>\");\n        Variable myXml = engine.vars.get(\"myXml\");\n        assertTrue(myXml.isXml());\n        Variable temp = engine.evalJs(\"myXml.root.foo\");\n        assertEquals(\"bar\", temp.getValue());\n        // xml with line breaks\n        assign(\"foo\", \"<records>\\n  <record>a</record>\\n  <record>b</record>\\n  <record>c</record>\\n</records>\");\n        assign(\"bar\", \"foo.records\");\n        Variable bar = engine.vars.get(\"bar\");\n        assertTrue(bar.isMap());\n        // match xml using json-path\n        matchEquals(\"bar.record\", \"['a', 'b', 'c']\");\n        engine.assertTrue(\"foo.records.record.length == 3\");\n        assign(\"myXml\", \"<cat><name>Billie</name><scores><score>2</score><score>5</score></scores></cat>\");\n        matchEquals(\"myXml/cat/scores/score[2]\", \"'5'\");\n        matchEquals(\"myXml.cat.scores.score[1]\", \"'5'\");\n        // xml with an empty tag, value should be null\n        assign(\"foo\", \"<records>\\n  <record>a</record>\\n  <record/>\\n</records>\");\n        assign(\"bar\", \"foo.records\");\n        matchEquals(\"bar.record\", \"['a', null]\");\n        assign(\"myXml\", \"<root><foo>bar</foo></root>\");\n        Variable value = engine.evalXmlPathOnVariableByName(\"myXml\", \"/root/foo\");\n        matchEval(value.getValue(), \"bar\");\n        value = engine.evalKarateExpression(\"$myXml/root/foo\");\n        matchEval(value.getValue(), \"bar\");\n        // present / notpresent\n        assign(\"xml\", \"<root><foo>bar</foo><baz/><ban></ban></root>\");\n        matchEquals(\"xml/root/foo\", \"'bar'\");\n        matchEquals(\"xml/root/baz\", \"''\");\n        matchEquals(\"xml/root/ban\", \"''\");\n        matchEquals(\"xml/root/foo\", \"'#present'\");\n        matchNotEquals(\"xml/root/foo\", \"'#notpresent'\");\n        matchEquals(\"xml/root/nope\", \"'#notpresent'\");\n        matchNotEquals(\"xml/root/nope\", \"'#present'\");\n        matchEquals(\"xml/root/nope\", \"'##string'\");\n        // xml and assign\n        assign(\"myXml\", \"<root><foo>bar</foo></root>\");\n        assign(\"myStr\", \"$myXml/root/foo\");\n        engine.assertTrue(\"myStr == 'bar'\");\n        assign(\"myXml\", \"<root><foo><bar>baz</bar></foo></root>\");\n        assign(\"myNode\", \"$myXml/root/foo\");\n        assign(\"expected\", \"<foo><bar>baz</bar></foo>\");\n        matchEquals(\"myNode\", \"expected\");\n        assign(\"myXml\", \"<root><foo><bar>one</bar><bar>two</bar></foo></root>\");\n        // xpath return json array\n        matchEquals(\"myXml/root/foo/bar\", \"['one', 'two']\");\n        assign(\"myJson\", \"[{ val: 'one' }, { val: 'two' }]\");\n        assign(\"myList\", \"get myJson $[*].val\");\n        assign(\"myXml\", \"<root><foo><bar>one</bar><bar>two</bar></foo></root>\");\n        matchEquals(\"myXml/root/foo/bar\", \"myList\");\n        assign(\"myXml\", \"<root><foo><bar>baz</bar></foo></root>\");\n        assign(\"myMap\", \"myXml\");\n        matchEquals(\"myMap/root/foo\", \"<foo><bar>baz</bar></foo>\");\n        assign(\"myXml\", \"<root><foo><bar>baz</bar></foo></root>\");\n        assign(\"myMap\", \"myXml\");\n        assign(\"temp\", \"get myXml /root/foo\");\n        matchEquals(\"temp\", \"<foo><bar>baz</bar></foo>\");\n        assign(\"temp\", \"get myMap /root/foo\");\n        matchEquals(\"temp\", \"<foo><bar>baz</bar></foo>\");\n\n        // preserves whitespace in content\n        assign(\"myXml\", \"<root><foo><bar> baz </bar></foo></root>\");\n        value = engine.evalKarateExpression(\"$myXml/root/foo/bar\");\n        matchEval(value.getValue(), \" baz \");\n    }\n\n    @Test\n    void testEvalJsonAndJsonPath() {\n        assign(\"myJson\", \"{ foo: 'bar', baz: [1, 2], ban: { hello: 'world' } }\");\n        Variable myXml = engine.vars.get(\"myJson\");\n        assertTrue(myXml.isMap());\n        Variable value = engine.evalJs(\"myJson.foo\");\n        assertEquals(\"bar\", value.getValue());\n        value = engine.evalJs(\"myJson.baz[1]\");\n        assertEquals(2, value.<Number>getValue());\n        value = engine.evalJs(\"myJson.ban.hello\");\n        assertEquals(\"world\", value.getValue());\n        // json-path\n        value = engine.evalJsonPathOnVariableByName(\"myJson\", \"$.baz[1]\");\n        assertEquals(2, value.<Number>getValue());\n        value = engine.evalJsonPathOnVariableByName(\"myJson\", \"$.baz\");\n        assertTrue(value.isList());\n        matchEval(value.getValue(), \"[1, 2]\");\n        value = engine.evalJsonPathOnVariableByName(\"myJson\", \"$.ban\");\n        assertTrue(value.isMap());\n        matchEval(value.getValue(), \"{ hello: 'world' }\");\n        value = engine.evalKarateExpression(\"$myJson.ban\");\n        matchEval(value.getValue(), \"{ hello: 'world' }\");\n        // tricky json-path\n        assign(\"foo\", \"{ a: 1, b: 2, c: 3 }\");\n        assign(\"bar\", \"{ 'sp ace': '#(foo.a)', 'hy-phen': '#(foo.b)', 'full.stop': '#(foo.c)' }\");\n        matchEquals(\"bar\", \"{ 'sp ace': 1, 'hy-phen': 2, 'full.stop': 3 }\");\n        // json-path on LHS\n        String json = \"[\\n\"\n                + \"    {\\n\"\n                + \"        \\\"a\\\": \\\"a\\\",\\n\"\n                + \"        \\\"b\\\": \\\"a\\\",\\n\"\n                + \"        \\\"c\\\": \\\"a\\\",\\n\"\n                + \"    },\\n\"\n                + \"    {\\n\"\n                + \"        \\\"a\\\": \\\"ab\\\",\\n\"\n                + \"        \\\"b\\\": \\\"ab\\\",\\n\"\n                + \"        \\\"c\\\": \\\"ab\\\",\\n\"\n                + \"    }\\n\"\n                + \"]\";\n        assign(\"response\", json);\n        matchEquals(\"response[?(@.b=='ab')]\", \"'#[1]'\");\n        assign(\"json\", \"{ foo: 'bar' }\");\n        matchEquals(\"json.foo\", \"'bar'\");\n        matchEquals(\"json.foo\", \"'#present'\");\n        matchNotEquals(\"json.foo\", \"'#notpresent'\");\n        matchEquals(\"json.nope\", \"'#notpresent'\");\n        matchEquals(\"json.foo\", \"'#ignore'\");\n        matchEquals(\"json.nope\", \"'#ignore'\");\n        matchEquals(\"json.foo\", \"'#string'\");\n        matchEquals(\"json.foo\", \"'##string'\");\n        matchNotEquals(\"json.nope\", \"'#string'\");\n        matchEquals(\"json.nope\", \"'##string'\");\n    }\n\n    @Test\n    void testMatchObjectsReturnedFromJs() {\n        assign(\"fun\", \"function(){ return { foo: 'bar' } }\");\n        assign(\"json\", \"{ foo: 'bar' }\");\n        assign(\"expected\", \"fun()\");\n        matchEquals(\"json\", \"fun()\");\n        matchEquals(\"expected\", \"{ foo: 'bar' }\");\n        assign(\"fun\", \"function(){ return [1, 2] }\");\n        assign(\"json\", \"[1, 2]\");\n        assign(\"expected\", \"fun()\");\n        matchEquals(\"json\", \"fun()\");\n        matchEquals(\"expected\", \"[1, 2]\");\n    }\n\n    @Test\n    void testTypeConversion() {\n        engine.assign(AssignType.STRING, \"myStr\", \"{ foo: { hello: 'world' } }\", false);\n        Variable value = engine.vars.get(\"myStr\");\n        assertTrue(value.isString());\n        // auto converts string to json before json-path\n        assign(\"foo\", \"$myStr.foo\");\n        matchEquals(\"foo\", \"{ hello: 'world' }\");\n        // json to string\n        engine.assign(AssignType.STRING, \"myStr\", \"{ root: { foo: 'bar' } }\", false);\n        matchEquals(\"myStr\", \"'{\\\"root\\\":{\\\"foo\\\":\\\"bar\\\"}}'\");\n        // string to json\n        assign(\"myStr\", \"'{\\\"root\\\":{\\\"foo\\\":\\\"bar\\\"}}'\");\n        engine.assign(AssignType.JSON, \"myJson\", \"myStr\", false);\n        value = engine.vars.get(\"myJson\");\n        assertTrue(value.isMap());\n        matchEquals(\"myJson\", \"{ root: { foo: 'bar' } }\");\n        // json to xml\n        engine.assign(AssignType.XML, \"myXml\", \"{ root: { foo: 'bar' } }\", false);\n        value = engine.vars.get(\"myXml\");\n        assertTrue(value.isXml());\n        matchEquals(\"myXml\", \"<root><foo>bar</foo></root>\");\n        // string to xml\n        assign(\"myStr\", \"'<root><foo>bar</foo></root>'\");\n        engine.assign(AssignType.XML, \"myXml\", \"myStr\", false);\n        matchEquals(\"myXml\", \"<root><foo>bar</foo></root>\");\n        // xml to string\n        engine.assign(AssignType.STRING, \"myStr\", \"<root><foo>bar</foo></root>\", false);\n        matchEquals(\"myStr\", \"'<root><foo>bar</foo></root>'\");\n        // xml attributes get re-ordered\n        engine.assign(AssignType.STRING, \"myStr\", \"<foo><bar bbb=\\\"2\\\" aaa=\\\"1\\\"/></foo>\", false);\n        matchEquals(\"myStr\", \"'<foo><bar aaa=\\\"1\\\" bbb=\\\"2\\\"/></foo>'\");\n        // pojo to json\n        assign(\"myPojo\", \"new com.intuit.karate.core.SimplePojo()\");\n        value = engine.vars.get(\"myPojo\");\n        assertTrue(value.isOther());\n        engine.assign(AssignType.JSON, \"myJson\", \"myPojo\", false);\n        matchEquals(\"myJson\", \"{ foo: null, bar: 0 }\");\n        // pojo to xml\n        engine.assign(AssignType.XML, \"myXml\", \"myPojo\", false);\n        matchEquals(\"myXml\", \"<root><foo></foo><bar>0</bar></root>\");\n        assign(\"myXml2\", \"<root><foo>bar</foo><hello><text>hello \\\"world\\\"</text></hello><hello><text>hello \\\"moon\\\"</text></hello></root>\");\n        matchNotEquals(\"myXml2/root/text\", \"'#notnull'\");\n        matchEquals(\"myXml2/root/text\", \"'#notpresent'\");\n        matchEquals(\"myXml2/root/text\", \"'#ignore'\");\n        matchEquals(\"myXml2/root/text\", \"'##notnull'\"); // optional parameter\n    }\n\n    @Test\n    void testResponseShortCuts() {\n        assign(\"response\", \"{ foo: 'bar' }\");\n        matchEquals(\"response\", \"{ foo: 'bar' }\");\n        matchEquals(\"$\", \"{ foo: 'bar' }\");\n        matchEquals(\"response.foo\", \"'bar'\");\n        matchEquals(\"$.foo\", \"'bar'\");\n        assign(\"response\", \"<root><foo>bar</foo></root>\");\n        matchEquals(\"response\", \"<root><foo>bar</foo></root>\");\n        matchEquals(\"/\", \"<root><foo>bar</foo></root>\");\n        matchEquals(\"response/\", \"<root><foo>bar</foo></root>\");\n        matchEquals(\"response /\", \"<root><foo>bar</foo></root>\");\n    }\n\n    @Test\n    void testSetAndRemove() {\n        assign(\"test\", \"{ foo: 'bar' }\");\n        engine.set(\"test\", \"$.bar\", \"'baz'\");\n        matchEquals(\"test\", \"{ foo: 'bar', bar: 'baz' }\");\n        engine.set(\"test\", \"$.foo\", \"null\");\n        matchEquals(\"test\", \"{ foo: null, bar: 'baz' }\");\n        engine.remove(\"test\", \"$.foo\");\n        matchEquals(\"test\", \"{ bar: 'baz' }\");\n        assign(\"test\", \"<root><foo>bar</foo></root>\");\n        engine.set(\"test\", \"/root/baz\", \"'ban'\");\n        matchEquals(\"test\", \"<root><foo>bar</foo><baz>ban</baz></root>\");\n        engine.remove(\"test\", \"/root/foo\");\n        matchEquals(\"test\", \"<root><baz>ban</baz></root>\");\n    }\n\n    @Test\n    void testSetViaTable() {\n        Json json = Json.of(\"[{path: 'bar', value: \\\"'baz'\\\" }]\");\n        engine.setViaTable(\"foo\", null, json.asList());\n        matchEquals(\"foo\", \"{ bar: 'baz' }\");\n        json = Json.of(\"[{path: 'bar', value: 'null' }]\"); // has no effect\n        engine.setViaTable(\"foo\", null, json.asList());\n        matchEquals(\"foo\", \"{ bar: 'baz' }\");\n        json = Json.of(\"[{path: 'bar', value: '(null)' }]\"); // has effect\n        engine.setViaTable(\"foo\", null, json.asList());\n        matchEquals(\"foo\", \"{ bar: null }\");\n    }\n\n    @Test\n    void testTable() {\n        Json json = Json.of(\"[{foo: '1', bar: \\\"'baz'\\\" }]\");\n        engine.table(\"tab\", json.asList());\n        matchEquals(\"tab\", \"[{ foo: 1, bar: 'baz' }]\");\n        json = Json.of(\"[{foo: '', bar: \\\"'baz'\\\" }]\");\n        engine.table(\"tab\", json.asList());\n        matchEquals(\"tab\", \"[{ bar: 'baz' }]\");\n        json = Json.of(\"[{foo: '(null)', bar: \\\"'baz'\\\" }]\");\n        engine.table(\"tab\", json.asList());\n        matchEquals(\"tab\", \"[{ foo: null, bar: 'baz' }]\");\n    }\n\n    @Test\n    void testReplace() {\n        assign(\"foo\", \"'hello <world>'\");\n        engine.replace(\"foo\", \"world\", \"'blah'\");\n        matchEquals(\"foo\", \"'hello blah'\");\n        assign(\"str\", \"'ha <foo> ha'\");\n        Json json = Json.of(\"[{token: 'foo', value: \\\"'bar'\\\" }]\");\n        engine.replaceTable(\"str\", json.asList());\n        matchEquals(\"str\", \"'ha bar ha'\");\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/ScenarioOutlineResultTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.TestUtils;\nimport static com.intuit.karate.TestUtils.*;\nimport com.intuit.karate.report.Report;\nimport com.intuit.karate.report.SuiteReports;\nimport java.io.File;\nimport java.util.Map;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author OwenK2\n */\nclass ScenarioOutlineResultTest {\n    \n    static final Logger logger = LoggerFactory.getLogger(ScenarioOutlineResultTest.class);\n\n    @Test\n    void testJsonConversion() {\n        FeatureRuntime fr = TestUtils.runFeature(\"classpath:com/intuit/karate/core/scenario-outline-result.feature\");\n        assertFalse(fr.result.isFailed());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/ScenarioRuntimeTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Match;\nimport com.intuit.karate.http.ResourceType;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.File;\nimport java.util.List;\n\nimport static com.intuit.karate.TestUtils.match;\nimport static com.intuit.karate.TestUtils.runScenario;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n/**\n *\n * @author pthomas3\n */\nclass ScenarioRuntimeTest {\n\n    static final Logger logger = LoggerFactory.getLogger(ScenarioRuntimeTest.class);\n\n    @BeforeEach\n    void beforeEach() {\n        fail = false;\n    }\n\n    boolean fail;\n    ScenarioRuntime sr;\n\n    Object get(String name) {\n        return sr.engine.vars.get(name).getValue();\n    }\n\n    ScenarioRuntime run(String... lines) {\n        sr = runScenario(null, lines);\n        if (fail) {\n            assertTrue(sr.result.isFailed());\n        } else {\n            assertFalse(sr.result.isFailed());\n        }\n        return sr;\n    }\n\n    private void matchVar(String name, Object expected) {\n        match(get(name), expected);\n    }\n\n    @Test\n    void testDefAndMatch() {\n        run(\n                \"def a = 1 + 2\",\n                \"match a == 3\"\n        );\n        assertEquals(3, get(\"a\"));\n        fail = true;\n        run(\n                \"def a = 1 + 2\",\n                \"match a == 4\"\n        );\n    }\n\n    @Test\n    void testDefAndMatchForArrays() {\n        run(\n                \"def arr = ['a','b','c','d','e']\",\n                \"def arrSize = karate.sizeOf(arr)\",\n                \"match arr == '##[_]'\",\n                \"match arr == '##[_ > 0]'\",\n                \"match arr == '##[_>0]'\",\n                \"match arr == '##[_<6]'\",\n                \"def two = 2\",\n                \"def three = 3\",\n                \"match arr == '##[two + three]'\",\n                \"def two_var = 2\",\n                \"def one_two_three = 3\",\n                \"match arr == '#[two_var+ one_two_three + _ > 0]'\",\n                \"match arr == '#[two_var+ one_two_three ]'\"\n        );\n        assertEquals(5, get(\"arrSize\"));\n    }\n    \n    @Test\n    void testSizeOfForByteArrays() {\n        run(\n                \"bytes data = 'foo'\",\n                \"def arrSize = karate.sizeOf(data)\"\n        );\n        assertEquals(3, get(\"arrSize\"));        \n    }\n\n    @Test\n    void testConfigAndEnv() {\n        System.clearProperty(\"karate.env\");\n        System.clearProperty(\"karate.config.dir\");\n        run(\"def foo = configSource\");\n        matchVar(\"foo\", \"normal\");\n        System.setProperty(\"karate.config.dir\", \"src/test/java/com/intuit/karate/core\");\n        run(\n                \"def foo = configSource\",\n                \"def bar = karate.env\"\n        );\n        matchVar(\"foo\", \"custom\");\n        matchVar(\"bar\", null);\n        System.setProperty(\"karate.env\", \"dev\");\n        run(\n                \"def foo = configSource\",\n                \"def bar = karate.env\"\n        );\n        matchVar(\"foo\", \"custom-env\");\n        matchVar(\"bar\", \"dev\");\n        // reset for other tests    \n        System.clearProperty(\"karate.env\");\n        System.clearProperty(\"karate.config.dir\");\n    }\n\n    @Test\n    void testFunctionsFromGlobalConfig() {\n        System.setProperty(\"karate.config.dir\", \"src/test/java/com/intuit/karate/core\");\n        run(\n                \"def foo = configUtilsJs.someText\",\n                \"def bar = configUtilsJs.someFun()\",\n                \"def res = call read('called2.feature')\"\n        );\n        matchVar(\"foo\", \"hello world\");\n        matchVar(\"bar\", \"hello world\");\n        Match.that(get(\"res\")).contains(\"{ calledBar: 'hello world' }\");\n        System.clearProperty(\"karate.env\");\n        System.clearProperty(\"karate.config.dir\");\n    }\n\n    @Test\n    void testReadFunction() {\n        run(\n                \"def foo = read('data.json')\",\n                \"def bar = karate.readAsString('data.json')\"\n        );\n        matchVar(\"foo\", \"{ hello: 'world' }\");\n        Variable bar = sr.engine.vars.get(\"bar\");\n        Match.that(bar.getValue()).isEqualTo(\"{ hello: 'world' }\");\n        // fixed for windows\n        assertEquals(((String) bar.getValue()).trim(), \"{ \\\"hello\\\": \\\"world\\\" }\");\n    }\n\n    @Test\n    void testReadFilesWithExpressions() {\n        run(\n                \"def foo = 'fooValue'\",\n                \"def bar = 'barValue'\",\n                \"def dataFromYml = read('read-expressions.yml')\",\n                \"def dataFromJson = read('read-expressions.json')\"\n        );\n        Variable dataFromYml = sr.engine.vars.get(\"dataFromYml\");\n        Variable dataFromJson = sr.engine.vars.get(\"dataFromJson\");\n        assertEquals(dataFromYml.getAsString(), dataFromJson.getAsString());\n        assertEquals(dataFromYml.getAsString(), \"[{\\\"item\\\":{\\\"foo\\\":\\\"fooValue\\\",\\\"nested\\\":{\\\"bar\\\":\\\"barValue\\\",\\\"notfound\\\":\\\"#(baz)\\\"}}}]\");\n        assertEquals(dataFromJson.getAsString(), \"[{\\\"item\\\":{\\\"foo\\\":\\\"fooValue\\\",\\\"nested\\\":{\\\"bar\\\":\\\"barValue\\\",\\\"notfound\\\":\\\"#(baz)\\\"}}}]\");\n    }\n\n    @Test\n    void testCallJsFunction() {\n        run(\n                \"def fun = function(a){ return a + 1 }\",\n                \"def foo = call fun 2\"\n        );\n        matchVar(\"foo\", 3);\n    }\n\n    @Test\n    void testCallJsFunctionFromFile() {\n        run(\n                \"def nextId = call read('increment.js')\",\n                \"def res1 = nextId()\",\n                \"def res2 = nextId()\"\n        );\n        matchVar(\"res1\", 1);\n        matchVar(\"res2\", 2);\n        matchVar(\"_curId\", 2);\n    }\n\n    @Test\n    void testCallKarateFeature() {\n        run(\n                \"def b = 'bar'\",\n                \"def res = call read('called1.feature')\"\n        );\n        matchVar(\"res\", \"{ a: 1, foo: { hello: 'world' } }\");\n        run(\n                \"def b = 'bar'\",\n                \"def res = call read('called1.feature') { foo: 'bar' }\"\n        );\n        matchVar(\"res\", \"{ a: 1, foo: { hello: 'world' } }\");\n        run(\n                \"def b = 'bar'\",\n                \"def res = call read('called1.feature') [{ foo: 'bar' }]\"\n        );\n        matchVar(\"res\", \"[{ a: 1, foo: { hello: 'world' } }]\");\n        run(\n                \"def b = 'bar'\",\n                \"def fun = function(i){ if (i == 1) return null; return { index: i } }\",\n                \"def res = call read('called1.feature') fun\"\n        );\n        matchVar(\"res\", \"[{ a: 1, foo: { hello: 'world' }, index: 0 }]\");\n    }\n\n    @Test\n    void testCallOnce() {\n        run(\n                \"def uuid = function(){ return java.util.UUID.randomUUID() + '' }\",\n                \"def first = callonce uuid\",\n                \"def second = callonce uuid\"\n        );\n        matchVar(\"first\", get(\"second\"));\n    }\n\n    @Test\n    void testCallSingle() {\n        run(\n                \"def first = karate.callSingle('uuid.js')\",\n                \"def second = karate.callSingle('uuid.js')\"\n        );\n        matchVar(\"first\", get(\"second\"));\n    }\n\n    @Test\n    void testCallSingleThatReturnsJson() {\n        run(\n                \"def res = karate.callSingle('called3.js')\"\n        );\n        matchVar(\"res\", \"{ varA: '2', varB: '3' }\");\n    }\n\n    @Test\n    void testKarateCallThatReturnsJson() {\n        run(\n                \"def res = karate.call('called3.js')\"\n        );\n        matchVar(\"res\", \"{ varA: '2', varB: '3' }\");\n    }\n\n    @Test\n    void testCallSingleWithinJs() {\n        run(\n                \"def res = karate.call('called3-caller1.js')\"\n        );\n        matchVar(\"res\", \"2\");\n    }\n\n    @Test\n    void testKarateCallWithinJs() {\n        run(\n                \"def res = karate.call('called3-caller2.js')\"\n        );\n        matchVar(\"res\", \"2\");\n    }\n\n    @Test\n    void testCallFromJs() {\n        run(\n                \"def res = karate.call('called1.feature')\"\n        );\n        matchVar(\"res\", \"{ a: 1, foo: { hello: 'world' } }\");\n    }\n\n    @Test\n    void testCallWithJsonArgument() {\n        run(\n                \"def fun = function(arg){ return [arg.first, arg.second] }\",\n                \"def res = call fun { first: 'foo', second: 'bar' }\"\n        );\n        matchVar(\"res\", \"['foo', 'bar']\");\n    }\n\n    @Test\n    void testToString() {\n        run(\n                \"def foo = { hello: 'world' }\",\n                \"def fooStr = karate.toString(foo)\",\n                \"def fooPretty = karate.pretty(foo)\",\n                \"def fooXml = karate.prettyXml(foo)\"\n        );\n        assertEquals(get(\"fooStr\"), \"{\\\"hello\\\":\\\"world\\\"}\");\n        assertEquals(get(\"fooPretty\"), \"{\\n  \\\"hello\\\": \\\"world\\\"\\n}\\n\");\n        // fixed for windows\n        assertEquals(((String) get(\"fooXml\")).trim(), \"<hello>world</hello>\");\n    }\n\n    @Test\n    void testGetSetAndRemove() {\n        run(\n                \"karate.set('foo', 1)\",\n                \"karate.set('bar', { hello: 'world' })\",\n                \"karate.set({ a: 2, b: 'hey' })\",\n                \"karate.setXml('fooXml', '<foo>bar</foo>')\",\n                \"copy baz = bar\",\n                \"karate.set('baz', '$.a', 1)\",\n                \"karate.remove('baz', 'hello')\",\n                \"copy bax = fooXml\",\n                \"karate.setXml('bax', '/foo', '<a>1</a>')\",\n                \"def getFoo = karate.get('foo')\",\n                \"def getNull = karate.get('blah')\",\n                \"def getDefault = karate.get('blah', 'foo')\",\n                \"def getPath = karate.get('bar.hello')\"\n        );\n        assertEquals(get(\"foo\"), 1);\n        assertEquals(get(\"a\"), 2);\n        assertEquals(get(\"b\"), \"hey\");\n        matchVar(\"bar\", \"{ hello: 'world' }\");\n        Object fooXml = get(\"fooXml\");\n        assertTrue(Match.that(fooXml).isXml());\n        matchVar(\"fooXml\", \"<foo>bar</foo>\");\n        matchVar(\"baz\", \"{ a: 1 }\");\n        Match.that(get(\"bax\")).isEqualTo(\"<foo><a>1</a></foo>\");\n        assertEquals(get(\"getFoo\"), 1);\n        assertEquals(get(\"getNull\"), null);\n        assertEquals(get(\"getDefault\"), \"foo\");\n        assertEquals(get(\"getPath\"), \"world\");\n    }\n\n    @Test\n    void testRemoveNotExistentPath() {\n        run(\n                \"def foo = { a: 1 }\",\n                \"remove foo.b\"\n        );\n        matchVar(\"foo\", \"{ a: 1 }\");\n    }\n\n    @Test\n    void testDelete() {\n        run(\n                \"def foo = { a: 1 }\",\n                \"delete foo.b\",\n                \"delete foo.a\"\n        );\n        matchVar(\"foo\", \"{}\");\n    }\n\n    @Test\n    void testCollections() {\n        run(\n                \"def foo = { a: 1, b: 2, c: 3 }\",\n                \"def fooSize = karate.sizeOf(foo)\",\n                \"def bar = [1, 2, 3]\",\n                \"def barSize = karate.sizeOf(bar)\",\n                \"def fooKeys = karate.keysOf(foo)\",\n                \"def fooVals = karate.valuesOf(foo)\"\n        );\n        assertEquals(get(\"fooSize\"), 3);\n        assertEquals(get(\"barSize\"), 3);\n        matchVar(\"fooKeys\", \"['a', 'b', 'c']\");\n        matchVar(\"fooVals\", \"[1, 2, 3]\");\n    }\n\n    @Test\n    void testCollectionsInFunctions() {\n        run(\n                \"def foo = { a: 1, b: 2, c: 3 }\",\n                \"def fun1 = function(arg){ return karate.sizeOf(arg) }\",\n                \"def res1 = fun1(foo)\",\n                \"def fun2 = function(arg){ return karate.keysOf(arg) }\",\n                \"def res2 = fun2(foo)\",\n                \"def fun3 = function(arg){ return karate.valuesOf(arg) }\",\n                \"def res3 = fun3(foo)\",\n                \"def fun4 = function(arg){ return karate.filterKeys(arg, 'a')}\",\n                \"def res4 = fun4(foo)\",\n                \"def fun5 = function(arg){ return karate.filterKeys(arg, 'a', 'b')}\",\n                \"def res5 = fun5(foo)\",\n                \"def fun6 = function(arg){ return karate.filterKeys(arg, ['a', 'b'])}\",\n                \"def res6 = fun6(foo)\",\n                \"def fun7 = function(arg){ return Object.keys(arg) }\",\n                \"def res7 = fun7(foo)\"\n        );\n        matchVar(\"res1\", 3);\n        matchVar(\"res2\", \"['a', 'b', 'c']\");\n        matchVar(\"res3\", \"[1, 2, 3]\");\n        matchVar(\"res4\", \"{ a: 1 }\");\n        matchVar(\"res5\", \"{ a: 1, b: 2 }\");\n        matchVar(\"res6\", \"{ a: 1, b: 2 }\");\n        matchVar(\"res7\", \"['a', 'b', 'c']\");\n    }\n\n    @Test\n    void testMatch() {\n        run(\n                \"def foo = { a: 1 }\",\n                \"def mat1 = karate.match(foo, {a: 2})\",\n                \"def mat2 = karate.match('foo == { a: 1 }')\",\n                \"def bar = []\",\n                \"def mat3 = karate.match(bar, [])\"\n        );\n        matchVar(\"mat1\", \"{ pass: false, message: '#notnull' }\");\n        matchVar(\"mat2\", \"{ pass: true, message: '#null' }\");\n        matchVar(\"mat3\", \"{ pass: true, message: '#null' }\");\n    }\n\n    @Test\n    void testForEach() {\n        run(\n                \"def foo = { a: 1, b: 2, c: 3 }\",\n                \"def res1 = { value: '' }\",\n                \"def fun = function(k, v, i){ res1.value += k + v + i }\",\n                \"karate.forEach(foo, fun)\",\n                \"def foo = ['a', 'b', 'c']\",\n                \"def res2 = { value: '' }\",\n                \"def fun = function(v, i){ res2.value += v + i }\",\n                \"karate.forEach(foo, fun)\"\n        );\n        matchVar(\"res1\", \"{ value: 'a10b21c32' }\");\n        matchVar(\"res2\", \"{ value: 'a0b1c2' }\");\n    }\n\n    @Test\n    void testMap() {\n        run(\n                \"def foo = [{ a: 1 }, { a: 2 }, { a: 3 }]\",\n                \"def fun = function(x){ return x.a }\",\n                \"def res = karate.map(foo, fun)\"\n        );\n        matchVar(\"res\", \"[1, 2, 3]\");\n        run(\n                \"def foo = [{ a: 1 }, { a: 2 }, { a: 3 }]\",\n                \"def res = karate.map(foo, x => x.a)\"\n        );\n        matchVar(\"res\", \"[1, 2, 3]\");\n        run(\n                \"def foo = [{ a: 1 }, { a: 2 }, { a: 3 }]\",\n                \"def fun = (x, i) => `${x.a}${i}`\",\n                \"def res1 = karate.map(foo, fun)\",\n                \"def res2 = foo.map(x => x.a)\"\n        );\n        matchVar(\"res1\", \"['10', '21', '32']\");\n        matchVar(\"res2\", \"[1, 2, 3]\");\n    }\n\n    @Test\n    void testFilter() {\n        run(\n                \"def foo = [{ a: 0 }, { a: 1 }, { a: 2 }]\",\n                \"def res = karate.filter(foo, x => x.a > 0)\"\n        );\n        matchVar(\"res\", \"[{ a: 1 }, { a: 2 }]\");\n    }\n\n    @Test\n    void testFilterKeys() {\n        run(\n                \"def foo = { a: 1, b: 2, c: 3 }\",\n                \"def res1 = karate.filterKeys(foo, 'a')\",\n                \"def res2 = karate.filterKeys(foo, 'a', 'c')\",\n                \"def res3 = karate.filterKeys(foo, ['b', 'c'])\",\n                \"def res4 = karate.filterKeys(foo, { a: 2, c: 5})\"\n        );\n        matchVar(\"res1\", \"{ a: 1 }\");\n        matchVar(\"res2\", \"{ a: 1, c: 3 }\");\n        matchVar(\"res3\", \"{ b: 2, c: 3 }\");\n        matchVar(\"res4\", \"{ a: 1, c: 3 }\");\n    }\n\n    @Test\n    void testRepeat() {\n        run(\n                \"def res1 = karate.repeat(3, i => i + 1 )\",\n                \"def res2 = karate.repeat(3, i => ({ a: 1 }))\",\n                \"def res3 = karate.repeat(3, i => ({ a: i + 1 }))\"\n        );\n        matchVar(\"res1\", \"[1, 2, 3]\");\n        matchVar(\"res2\", \"[{ a: 1 }, { a: 1 }, { a: 1 }]\");\n        matchVar(\"res3\", \"[{ a: 1 }, { a: 2 }, { a: 3 }]\");\n    }\n\n    @Test\n    void testMapWithKey() {\n        run(\n                \"def foo = [1, 2, 3]\",\n                \"def res = karate.mapWithKey(foo, 'val')\"\n        );\n        matchVar(\"res\", \"[{ val: 1 }, { val: 2 }, { val: 3 }]\");\n    }\n\n    @Test\n    void testMergeAndAppend() {\n        run(\n                \"def foo = { a: 1 }\",\n                \"def res1 = karate.merge(foo, { b: 2 })\",\n                \"def bar = [1, 2]\",\n                \"def res2 = karate.append(bar, [3, 4])\",\n                \"def res3 = [1, 2]\",\n                \"karate.appendTo('res3', [3, 4])\",\n                \"def res4 = [1, 2]\",\n                \"karate.appendTo(res4, [3, 4])\" // append to variable reference !\n        );\n        matchVar(\"res1\", \"{ a: 1, b: 2 }\");\n        matchVar(\"res2\", \"[1, 2, 3, 4]\");\n        matchVar(\"res3\", \"[1, 2, 3, 4]\");\n        matchVar(\"res4\", \"[1, 2, 3, 4]\");\n    }\n\n    @Test\n    void testJsonPath() {\n        run(\n                \"def foo = { a: 1, b: { a: 2 } }\",\n                \"def res1 = karate.jsonPath(foo, '$..a')\",\n                \"def arr = [ {'id': 1, 'name': 'test', 'age': 10}, {'id': 2, 'name': 'test2', 'age': 20} ]\",\n                \"def res2 = karate.jsonPath(arr, '$.[?(@.id == 1)].name')[0]\"\n        );\n        matchVar(\"res1\", \"[1, 2]\");\n        matchVar(\"res2\", \"test\");\n    }\n\n    @Test\n    void testLowerCase() {\n        run(\n                \"def foo = { HELLO: 'WORLD' }\",\n                \"def res1 = karate.lowerCase(foo)\"\n        );\n        matchVar(\"res1\", \"{ hello: 'world' }\");\n    }\n\n    @Test\n    void testXmlPath() {\n        run(\n                \"def foo = <bar><a><b>c</b></a></bar>\",\n                \"def res1 = karate.xmlPath(foo, '/bar/a')\"\n        );\n        matchVar(\"res1\", \"<a><b>c</b></a>\");\n    }\n\n    @Test\n    void testJsonToString() {\n        run(\n                \"def original = '{\\\"echo\\\":\\\"echo@gmail.com\\\",\\\"lambda\\\":\\\"Lambda\\\",\\\"bravo\\\":\\\"1980-01-01\\\"}'\",\n                \"json asJson = original\",\n                \"string asString = asJson\",\n                \"match original == asString\"\n        );\n    }\n\n    @Test\n    void testToBean() {\n        run(\n                \"def foo = { foo: 'hello', bar: 5 }\",\n                \"def res1 = karate.toBean(foo, 'com.intuit.karate.core.SimplePojo')\"\n        );\n        SimplePojo sp = (SimplePojo) get(\"res1\");\n        assertEquals(sp.getFoo(), \"hello\");\n        assertEquals(sp.getBar(), 5);\n    }\n\n    @Test\n    void testToJson() {\n        run(\n                \"def SP = Java.type('com.intuit.karate.core.SimplePojo')\",\n                \"def pojo = new SP()\",\n                \"def res1 = karate.toJson(pojo)\",\n                \"def res2 = karate.toJson(pojo, true)\"\n        );\n        matchVar(\"res1\", \"{ bar: 0, foo: null }\");\n        matchVar(\"res2\", \"{ bar: 0 }\");\n    }\n\n    @Test\n    void testToBeanAdvanced() {\n        run(\n                \"def pojoType = 'com.intuit.karate.core.SimplePojo'\",\n                \"def Pojo = Java.type(pojoType)\",\n                \"def toPojo = function(x){ return karate.toBean(x, pojoType) }\",\n                \"def toJson = function(x){ return karate.toJson(x, true) }\",\n                \"def bean = new Pojo()\",\n                \"bean.foo = 'hello'\",\n                \"def pojo = toPojo({ bar: 5 })\",\n                \"def json = toJson(pojo)\"\n        );\n        matchVar(\"json\", \"{ bar: 5 }\");\n        SimplePojo bean = (SimplePojo) get(\"bean\");\n        assertEquals(0, bean.getBar());\n        assertEquals(\"hello\", bean.getFoo());\n        SimplePojo pojo = (SimplePojo) get(\"pojo\");\n        assertEquals(5, pojo.getBar());\n        assertNull(pojo.getFoo());\n    }\n\n    @Test\n    void testToCsv() {\n        run(\n                \"def foo = [{a: 1, b: 2}, { a: 3, b: 4 }]\",\n                \"def res = karate.toCsv(foo)\"\n        );\n        // fixed for windows\n        match(((String) get(\"res\")).replaceAll(\"[\\r\\n]+\", \"@\"), \"a,b@1,2@3,4@\");\n    }\n\n    @Test\n    void testEval() {\n        run(\n                \"def foo = karate.eval('() => 1 + 2')\",\n                \"def bar = foo()\"\n        );\n        assertTrue(sr.engine.vars.get(\"foo\").isJsFunction());\n        matchVar(\"bar\", 3);\n    }\n\n    @Test\n    void testFromString() {\n        run(\n                \"def foo = karate.fromString('{ hello: \\\"world\\\" }')\",\n                \"def bar = karate.typeOf(foo)\"\n        );\n        assertTrue(sr.engine.vars.get(\"foo\").isMap());\n        matchVar(\"bar\", \"map\");\n    }\n\n    @Test\n    void testEmbed() {\n        run(\n                \"karate.embed('<h1>hello world</h1>', 'text/html')\"\n        );\n        List<StepResult> results = sr.result.getStepResults();\n        assertEquals(1, results.size());\n        List<Embed> embeds = results.get(0).getEmbeds();\n        assertEquals(1, embeds.size());\n        assertEquals(embeds.get(0).getAsString(), \"<h1>hello world</h1>\");\n        assertEquals(embeds.get(0).getResourceType(), ResourceType.HTML);\n    }\n\n    @Test\n    void testStepLog() {\n        run(\n                \"print 'hello world'\"\n        );\n        List<StepResult> results = sr.result.getStepResults();\n        assertEquals(1, results.size());\n        String log = results.get(0).getStepLog();\n        assertTrue(log.contains(\"[print] hello world\"));\n    }\n\n    @Test\n    void testWrite() {\n        run(\n                \"def file = karate.write('hello world', 'runtime-test.txt')\"\n        );\n        File file = (File) get(\"file\");\n        assertEquals(file.getParentFile().getName(), \"target\");\n        assertEquals(file.getName(), \"runtime-test.txt\");\n        assertEquals(FileUtils.toString(file), \"hello world\");\n    }\n\n    @Test\n    void testJavaClassAsVariable() {\n        run(\n                \"def Utils = Java.type('com.intuit.karate.core.MockUtils')\",\n                \"def res = Utils.testBytes\"\n        );\n        assertEquals(get(\"res\"), MockUtils.testBytes);\n    }\n\n    @Test\n    void testCallJsFunctionShared() {\n        run(\n                \"def myFn = function(x){ return { myVar: x } }\",\n                \"call myFn 'foo'\"\n        );\n        assertEquals(get(\"myVar\"), \"foo\");\n    }\n\n    @Test\n    void testCallJsFunctionSharedJson() {\n        run(\n                \"def myFn = function(x){ return { myVar: x.foo } }\",\n                \"call myFn { foo: 'bar' }\"\n        );\n        assertEquals(get(\"myVar\"), \"bar\");\n    }\n\n    @Test\n    void testSelfValidationWithVariables() {\n        run(\n                \"def date = { month: 3 }\",\n                \"def min = 1\",\n                \"def max = 12\",\n                \"match date == { month: '#? _ >= min && _ <= max' }\"\n        );\n    }\n\n    @Test\n    void testReadAndMatchBytes() {\n        run(\n                \"bytes data = read('karate-logo.png')\",\n                \"match data == read('karate-logo.png')\"\n        );\n    }\n\n    @Test\n    void testJsonEmbeddedExpressionFailuresAreNotBlockers() {\n        run(\n                \"def expected = { a: '#number', b: '#(_$.a * 2)' }\",\n                \"def actual = [{a: 1, b: 2}, {a: 2, b: 4}]\",\n                \"match each actual == expected\"\n        );\n    }\n\n    @Test\n    void testXmlEmbeddedExpressionFailuresAreNotBlockers() {\n        run(\n                \"def expected = <foo att='#(bar)'>#(bar)</foo>\",\n                \"def actual = <foo att=\\\"test\\\">test</foo>\",\n                \"def bar = 'test'\",\n                \"match actual == expected\"\n        );\n    }\n\n    @Test\n    void testMatchEachMagicVariablesDontLeak() {\n        run(\n                \"def actual = [{a: 1, b: 2}, {a: 2, b: 4}]\",\n                \"match each actual == { a: '#number', b: '#(_$.a * 2)' }\",\n                \"def res = { b: '#(_$.a * 2)' }\"\n        );\n        matchVar(\"res\", \"{ b: '#string' }\");\n    }\n\n    @Test\n    void testMatchMagicVariables() {\n        run(\n                \"def temperature = { celsius: 100, fahrenheit: 212 }\",\n                \"match temperature contains { fahrenheit: '#($.celsius * 1.8 + 32)' }\"\n        );\n    }\n\n    @Test\n    void testArrayOnLhs() {\n        run(\n                \"match [] == '#[]'\"\n        );\n    }\n\n    @Test\n    void testMatchContainsArrayOnLhs() {\n        run(\n                \"match ['foo', 'bar'] contains 'foo'\"\n        );\n    }\n\n    @Test\n    void testMatchEmbeddedOptionalObject() {\n        run(\n                \"def foo = { a: 1 }\",\n                \"def bar = { foo: '##(foo)' }\",\n                \"match bar == { foo: { a: 1 } }\"\n        );\n    }\n\n    @Test\n    void testMatchSchema() {\n        run(\n                \"def dogSchema = { id: '#string', color: '#string' }\",\n                \"def schema = ({ id: '#string', name: '#string', dog: '##(dogSchema)' })\",\n                \"def response1 = { id: '123', name: 'foo' }\",\n                \"match response1 == schema\",\n                \"def response2 = { id: '123', name: 'foo', dog: { id: '456', color: 'brown' } }\",\n                \"match response2 == schema\"\n        );\n    }\n\n    @Test\n    void testMatchSchemaArray() {\n        run(\n                \"def temp = { foo: '#string' }\",\n                \"def schema = '#[] temp'\",\n                \"match [{ foo: 'bar' }] == schema\",\n                \"configure matchEachEmptyAllowed = true\",\n                \"match [] == schema\"\n        );\n    }\n\n    @Test\n    void testMatchSchemaMagicVariables() {\n        run(\n                \"def response = { odds: [1, 2], count: 2 }\",\n                \"match response == { odds: '#[$.count]', count: '#number' }\"\n        );\n    }\n\n    @Test\n    void testMatchSchemaContainsDeep() {\n        run(\n                \"def array = [ 'a', 'b' ]\",\n                \"def response = { foo: [ 'a', 'b' ] } \",\n                \"match response contains deep { foo: '#(^array)' }\"\n        );\n    }\n\n    @Test\n    void testMatchContainsOnlyDeep() {\n        run(\n                \"def response = { foo: [ 'a', 'b' ] } \",\n                \"match response contains only deep { foo: [ 'b', 'a' ] }\"\n        );\n    }\n\n    @Test\n    void testMatchEachContainsDeep() {\n        run(\n                \"def response = [ { a: 1, arr: [ { b: 2, c: 3 }, { b: 4, c: 5 } ] } ]\",\n                \"match each response contains deep { a: 1, arr: [ { b: '#number', c: 3 } ] }\"\n        );\n    }\n\n    @Test\n    void testJavaInteropStatic() {\n        run(\n                \"def Utils = Java.type('com.intuit.karate.core.StaticUtils')\",\n                \"def array = ['a', 'b', 'c']\",\n                \"def res = Utils.concat(array)\"\n        );\n        matchVar(\"res\", \"abc\");\n    }\n\n    @Test\n    void testJavaInteropBase64() {\n        run(\n                \"def Base64 = Java.type('java.util.Base64')\",\n                \"def res = Base64.encoder.encodeToString('hello'.getBytes())\"\n        );\n        matchVar(\"res\", java.util.Base64.getEncoder().encodeToString(\"hello\".getBytes()));\n    }\n\n    @Test\n    void testTypeConversionCsvEmpty() {\n        run(\n                \"csv temp = ''\"\n        );\n        matchVar(\"temp\", \"[]\");\n    }\n\n    @Test\n    void testJavaInteropParameters() {\n        run(\n                \"def Utils = Java.type('com.intuit.karate.core.StaticUtils')\",\n                \"def res1 = Utils.fromInt(2)\",\n                \"def res2 = Utils.fromDouble(2)\",\n                \"def res3 = Utils.fromDouble(2.0)\",\n                \"def res4 = Utils.fromNumber(2.0)\"\n        );\n        matchVar(\"res1\", \"value is 2\");\n        matchVar(\"res2\", \"value is 2.0\");\n        matchVar(\"res3\", \"value is 2.0\");\n        matchVar(\"res4\", \"value is 2.0\");\n    }\n\n    @Test\n    void testTableWithInvalidVariableName() {\n        fail = true;\n        run(\n                \"table table1 =\",\n                \"| col |\",\n                \"| foo |\"\n        );\n    }\n\n    @Test\n    void testReplace() {\n        run(\n                \"def text = 'words that need to be {replaced}'\",\n                \"replace text.{replaced} = 'correct'\",\n                \"match text == 'words that need to be correct'\",\n                \"match text.toString() == 'words that need to be correct'\"\n        );\n        matchVar(\"text\", \"words that need to be correct\");\n    }\n\n    @Test\n    void testDistinct() {\n        run(\n                \"def list1 = ['abc', 'def', 'abc', 'def', 'ghi']\",\n                \"def res1 = karate.distinct(list1)\",\n                \"match res1 == ['abc', 'def', 'ghi']\",\n                \"def list2 = [1, 2, 1, 2, 3]\",\n                \"def res2 = karate.distinct(list2)\",\n                \"match res2 == [1, 2, 3]\"\n        );\n    }\n\n    @Test\n    void testSort() {\n        run(\n                \"def list1 = [{ num: 3 }, { num: 1 }, { num: 2 }]\",\n                \"def fun1 = x => x.num\",\n                \"def res1 = karate.sort(list1, fun1)\",\n                \"match res1 == [{ num: 1 }, { num: 2 }, { num: 3 }]\",\n                \"def list2 = [{ val: 'C' }, { val: 'B' }, { val: 'A' }, { val: 'B' }]\",\n                \"def res2 = karate.sort(list2, x => x.val)\",\n                \"match res2 == [{ val: 'A' }, { val: 'B' }, { val: 'B' }, { val: 'C' }]\",\n                \"def list3 = ['c', 'b', 'a']\",\n                \"def res3 = karate.sort(list3)\",\n                \"match res3 == ['a', 'b', 'c']\",\n                \"match res3.reverse() == ['c', 'b', 'a']\",\n                \"def list4 = ['a', 'a a', 'a']\",\n                \"match karate.sort(list4) == ['a', 'a', 'a a']\"\n        );\n    }\n\n    @Test\n    void testRange() {\n        run(\n                \"def list1 = karate.range(5, 10)\",\n                \"match list1 == [5, 6, 7, 8, 9, 10]\",\n                \"def list2 = karate.range(5, 10, 2)\",\n                \"match list2 == [5, 7, 9]\",\n                \"def list3 = karate.range(10, 5, 2)\",\n                \"match list3 == [10, 8, 6]\"\n        );\n        fail = true;\n        run(\n                \"def list = karate.range(10, 5, 0)\"\n        );\n        run(\n                \"def list = karate.range(10, 5, -1)\"\n        );\n    }\n\n    @Test\n    void testUrlEncodeAndDecode() {\n        run(\n                \"def raw = 'encoding%2Ffoo%2Bbar'\",\n                \"def decoded = karate.urlDecode(raw)\",\n                \"match decoded == 'encoding/foo+bar'\",\n                \"def encoded = karate.urlEncode(decoded)\",\n                \"match encoded == raw\"\n        );\n    }\n\n    @Test\n    void testMatchXmlXpath() {\n        fail = true;\n        run(\n                \"xml myXml = <root><foo>bar</foo><hello><text>hello \\\"world\\\"</text></hello><hello><text>hello \\\"moon\\\"</text></hello></root>\",\n                \"match myXml //myXml2/root/text == '#notnull'\"\n        );\n    }\n\n    @Test\n    void testContinueOnStepFailure() {\n        fail = true;\n        run(\n                \"def var = 'foo'\",\n                \"configure continueOnStepFailure = true\",\n                \"match var == 'bar'\",\n                \"match var == 'pub'\",\n                \"match var == 'crawl'\",\n                \"match var == 'foo'\",\n                \"configure continueOnStepFailure = false\",\n                \"match var == 'foo'\",\n                \"match var == 'bar2'\",\n                \"match var == 'foo'\"\n        );\n        // the last failed step will be show as the result failed step\n        // TODO: verify how this will look in the reports\n        assertEquals(\"match var == 'crawl'\", sr.result.getFailedStep().getStep().getText());\n    }\n\n    @Test\n    void testContinueOnStepFailure2() {\n        fail = true;\n        run(\n                \"def var = 'foo'\",\n                \"configure continueOnStepFailure = { enabled: true, continueAfter: true }\",\n                \"match var == 'bar'\",\n                \"match var == 'pub'\",\n                \"match var == 'crawl'\",\n                \"match var == 'foo'\",\n                \"configure continueOnStepFailure = false\",\n                \"match var == 'foo'\",\n                \"match var == 'bar2'\",\n                \"match var == 'foo'\"\n        );\n        assertEquals(\"match var == 'bar2'\", sr.result.getFailedStep().getStep().getText());\n    }\n\n    @Test\n    void testContinueOnStepFailure3() {\n        fail = true;\n        // bad idea to continue/ignore anything else other than match but ...\n        run(\n                \"def var = 'foo'\",\n                \"configure continueOnStepFailure = { enabled: true, continueAfter: true, keywords: ['match', 'def'] }\",\n                \"match var == 'bar'\",\n                \"def var2 = function() { syntax error in here };\",\n                \"match var == 'pub'\",\n                \"match var == 'crawl'\",\n                \"match var == 'foo'\",\n                \"configure continueOnStepFailure = { enabled: false }\",\n                \"match var == 'foo'\",\n                \"match var == 'bar2'\",\n                \"match var == 'foo'\"\n        );\n        assertEquals(\"match var == 'bar2'\", sr.result.getFailedStep().getStep().getText());\n    }\n\n    @Test\n    void testContinueOnStepFailure4() {\n        fail = true;\n        run(\n                \"def var = 'foo'\",\n                \"configure continueOnStepFailure = true\",\n                \"match var == 'bar'\",\n                \"match var == 'pub'\",\n                \"match var == 'crawl'\",\n                \"match var == 'foo'\",\n                \"configure continueOnStepFailure = false\",\n                \"match var == 'foo'\"\n        );\n        assertEquals(\"match var == 'crawl'\", sr.result.getFailedStep().getStep().getText());\n    }\n\n    @Test\n    void testContinueOnStepFailure5() {\n        fail = true;\n        run(\n                \"def var = 'foo'\",\n                \"configure continueOnStepFailure = { enabled: true, continueAfter: true }\",\n                \"match var == 'bar'\",\n                \"match var == 'pub'\",\n                \"match var == 'crawl'\",\n                \"match var == 'foo'\",\n                \"configure continueOnStepFailure = false\",\n                \"match var == 'foo'\",\n                \"match var == 'foo'\",\n                \"match var == 'bar'\",\n                \"match var == 'skipped'\"\n        );\n        // scenario will still be marked as failed (reduces non-deterministic tests and using keyword as if condition)\n        // the first failed step will be the one reported as failure\n        // but next steps after configure 'continueOnStepFailure = false' will continue to execute\n        assertEquals(\"match var == 'bar'\", sr.result.getFailedStep().getStep().getText());\n        assertEquals(\"[passed] * configure continueOnStepFailure = false\", sr.result.getStepResults().get(6).toString());\n        assertEquals(\"[passed] * match var == 'foo'\", sr.result.getStepResults().get(7).toString());\n        assertEquals(\"[passed] * match var == 'foo'\", sr.result.getStepResults().get(8).toString());\n        assertEquals(\"[failed] * match var == 'bar'\", sr.result.getStepResults().get(9).toString());\n        assertEquals(\"[skipped] * match var == 'skipped'\", sr.result.getStepResults().get(10).toString());\n    }\n\n    @Test\n    void testContinueOnStepFailure6() {\n        fail = true;\n        run(\n                \"def var = 'foo'\",\n                \"configure continueOnStepFailure = { enabled: true, continueAfter: true, keywords: ['match', 'eval', 'if'] }\",\n                \"match var == 'bar'\",\n                \"if(true == true) { syntax error within JS line }\",\n                \"match var == 'crawl'\",\n                \"match var == 'foo'\",\n                \"configure continueOnStepFailure = false\",\n                \"match var == 'foo'\",\n                \"match var == 'foo'\"\n        );\n        // the last failed step will be show as the result failed step\n        assertEquals(\"match var == 'crawl'\", sr.result.getFailedStep().getStep().getText());\n    }\n\n    @Test\n    void testContinueOnStepFailure7() {\n        // issue #1913\n        fail = true;\n        run(\n                \"def var = 'foo'\",\n                \"configure continueOnStepFailure = { enabled: true, continueAfter: true, keywords: [ 'eval' ] }\",\n                \"waitFor('#error').click()\",\n                \"match var == 'foo'\",\n                \"configure continueOnStepFailure = { enabled: false }\",\n                \"match var == 'foo'\"\n        );\n        assertEquals(\"waitFor('#error').click()\", sr.result.getFailedStep().getStep().getText());\n    }\n\n    @Test\n    void testContinueOnStepFailure8() {\n        // issue #1913\n        // side issue found - dsl keywords with multi line was not supported\n        fail = true;\n        run(\n                \"def var = 'foo'\",\n                \"configure continueOnStepFailure = { enabled: true, continueAfter: true, keywords: [ 'eval' ] }\",\n                \"match var == 'foo'\",\n                \"eval\",\n                \"\\\"\\\"\\\"\",\n                \"if(true == true) { syntax error within JS line }\",\n                \"\\\"\\\"\\\"\",\n                \"match var == 'foo'\",\n                \"configure continueOnStepFailure = { enabled: false }\",\n                \"match var == 'foo'\"\n        );\n        assertEquals(\"eval\", sr.result.getFailedStep().getStep().getText());\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/SimplePojo.java",
    "content": "package com.intuit.karate.core;\n\n/**\n *\n * @author pthomas3\n */\npublic class SimplePojo {\n    \n    private String foo;\n    private int bar;\n\n    public String getFoo() {\n        return foo;\n    }\n\n    public void setFoo(String foo) {\n        this.foo = foo;\n    }\n\n    public int getBar() {\n        return bar;\n    }\n\n    public void setBar(int bar) {\n        this.bar = bar;\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/StaticUtils.java",
    "content": "package com.intuit.karate.core;\n\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class StaticUtils {\n    \n    private StaticUtils() {\n        // only static methods\n    }\n    \n    public static String concat(List<String> list) {\n        StringBuilder sb = new StringBuilder();\n        for (String s : list) {\n            sb.append(s);\n        }\n        return sb.toString();\n    }\n    \n    public static String fromInt(int num) {\n        return \"value is \" + num;\n    }    \n    \n    public static String fromDouble(double num) {\n        return \"value is \" + num;\n    }\n    \n    public static String fromNumber(Number num) {\n        return \"value is \" + num.doubleValue();\n    }    \n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/StepRuntimeTest.java",
    "content": "package com.intuit.karate.core;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.stream.Stream;\n\npublic class StepRuntimeTest {\n\n    static final Logger logger = LoggerFactory.getLogger(StepRuntimeTest.class);\n\n\n\n    @ParameterizedTest\n    @MethodSource(\"testParameters\")\n    public void testConversionMethodToStringAndBack(String methodSignature, Class<?> methodClass, Method method, List<String> args, String karateExpr) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {\n        StepRuntime.MethodMatch methodMatch = StepRuntime.MethodMatch.getBySignatureAndArgs(methodSignature);\n\n        Assertions.assertNotNull(methodMatch);\n        Assertions.assertEquals(method, methodMatch.method);\n        Assertions.assertEquals(args, methodMatch.args);\n        Assertions.assertEquals(methodSignature, methodMatch.toString());\n\n        // it's ok reflection here, just for unit testing.\n        Method findMethodsMatchingMethod = StepRuntime.class.getDeclaredMethod(\"findMethodsMatching\", String.class);\n        findMethodsMatchingMethod.setAccessible(true);\n        List<StepRuntime.MethodMatch> methodMatchList = (List<StepRuntime.MethodMatch>) findMethodsMatchingMethod.invoke(StepRuntime.class, karateExpr);\n\n        Assertions.assertTrue(methodMatchList.stream().anyMatch(m -> m.getMethod().equals(method)));\n        Assertions.assertTrue(methodMatchList.stream().anyMatch(m -> m.getArgs().equals(args)));\n        Assertions.assertTrue(methodMatchList.stream().anyMatch(m -> m.toString().equals(methodSignature)));\n        System.out.println();\n    }\n\n    @Test\n    public void testConversionMethodWithNoParams() throws ClassNotFoundException, NoSuchMethodException {\n        StepRuntime.MethodMatch methodMatch = StepRuntime.MethodMatch.getBySignatureAndArgs(\"com.intuit.karate.ScenarioActions.getFailedReason() []\");\n        Assertions.assertNotNull(methodMatch);\n        Assertions.assertEquals(Class.forName(\"com.intuit.karate.ScenarioActions\").getMethod(\"getFailedReason\"), methodMatch.method);\n        Assertions.assertEquals(new ArrayList<>(), methodMatch.args);\n        Assertions.assertEquals(\"com.intuit.karate.ScenarioActions.getFailedReason() null\", methodMatch.toString());\n    }\n    @Test\n    public void testFindMethodsByKeywordNoResults() {\n        Collection<Method> defMethod = StepRuntime.findMethodsByKeyword(\"def\");\n        Assertions.assertFalse(defMethod.isEmpty());\n\n        Collection<Method> badMethodMethod = StepRuntime.findMethodsByKeyword(\"badMethod\");\n        Assertions.assertNotNull(badMethodMethod);\n        Assertions.assertTrue(badMethodMethod.isEmpty());\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"methodPatternAndKeywords\")\n    public void testMethodPatternAndKeywordMatch(Method scenarioActionMethod, String keyword) throws IllegalAccessException, NoSuchFieldException {\n        // test for some most used Karate keywords\n        When when = scenarioActionMethod.getDeclaredAnnotation(When.class);\n        final String methodRegex;\n        if (when != null) {\n            methodRegex = when.value();\n        } else {\n            methodRegex = null;\n        }\n\n        // it's ok reflection here, just for unit testing.\n        Field patternsField = StepRuntime.class.getDeclaredField(\"PATTERNS\");\n        patternsField.setAccessible(true);\n        Collection<StepRuntime.MethodPattern> patterns = (Collection<StepRuntime.MethodPattern>) patternsField.get(null);\n\n        Assertions.assertNotNull(methodRegex);\n        Assertions.assertTrue(patterns.stream().anyMatch(p -> p.regex.contentEquals(methodRegex) && p.keyword.equalsIgnoreCase(keyword)));;\n\n    }\n\n    private static Stream<Arguments> testParameters() throws ClassNotFoundException, NoSuchMethodException {\n        return Stream.of(\n                Arguments.of(\"com.intuit.karate.ScenarioActions.print(java.lang.String) [\\\"'name:', name\\\"]\",\n                        com.intuit.karate.ScenarioActions.class,\n                        com.intuit.karate.ScenarioActions.class.getMethod(\"print\", String.class),\n                        new ArrayList<String>() { { add(\"'name:', name\"); }},\n                        \"print 'name:', name\"),\n                Arguments.of(\"com.intuit.karate.ScenarioActions.configure(java.lang.String,java.lang.String) [\\\"continueOnStepFailure\\\",\\\"true\\\"]\",\n                        com.intuit.karate.ScenarioActions.class,\n                        com.intuit.karate.ScenarioActions.class.getMethod(\"configure\", String.class, String.class),\n                        new ArrayList<String>() { { add(\"continueOnStepFailure\"); add(\"true\"); }},\n                        \"configure continueOnStepFailure = true\"),\n                Arguments.of(\"com.intuit.karate.ScenarioActions.print(java.lang.String) [\\\"\\\\\\\"name:\\\\\\\", name\\\"]\",\n                        com.intuit.karate.ScenarioActions.class,\n                        com.intuit.karate.ScenarioActions.class.getMethod(\"print\", String.class),\n                        new ArrayList<String>() { { add(\"\\\"name:\\\", name\"); }},\n                        \"print \\\"name:\\\", name\"),\n                Arguments.of(\"com.intuit.karate.ScenarioActions.print(java.lang.String) [\\\"'test with/slash'\\\"]\", // JSON with forward slash\n                        com.intuit.karate.ScenarioActions.class,\n                        com.intuit.karate.ScenarioActions.class.getMethod(\"print\", String.class),\n                        new ArrayList<String>() { { add(\"'test with/slash'\"); }},\n                        \"print 'test with/slash'\")\n\n        );\n    }\n\n    private static Stream<Arguments> methodPatternAndKeywords() throws ClassNotFoundException, NoSuchMethodException {\n        return Stream.of(\n                Arguments.of(com.intuit.karate.ScenarioActions.class.getMethod(\"match\", String.class, String.class, String.class, String.class),\n                \"match\"),\n                Arguments.of(com.intuit.karate.ScenarioActions.class.getMethod(\"assertTrue\", String.class),\n                \"assert\"),\n                Arguments.of(com.intuit.karate.ScenarioActions.class.getMethod(\"status\", int.class),\n                \"status\"),\n                Arguments.of(com.intuit.karate.ScenarioActions.class.getMethod(\"eval\", String.class),\n                \"eval\"),\n                Arguments.of(com.intuit.karate.ScenarioActions.class.getMethod(\"evalIf\", String.class),\n                \"if\")\n\n\n        );\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/TagsTest.java",
    "content": "package com.intuit.karate.core;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\nclass TagsTest {\n\n    @Test\n    public void testCucumberOptionsTagsConversion() {\n        assertEquals(\"anyOf('@foo')\", Tags.fromKarateOptionsTags(\"@foo\"));\n        assertEquals(\"anyOf('@foo','@bar')\", Tags.fromKarateOptionsTags(\"@foo, @bar\"));\n        assertEquals(\"anyOf('@foo') && anyOf('@bar')\", Tags.fromKarateOptionsTags(\"@foo\", \"@bar\"));\n        assertEquals(\"anyOf('@foo') && not('@bar')\", Tags.fromKarateOptionsTags(\"@foo\", \"~@bar\"));\n        // detect new syntax and use as-is\n        assertEquals(\"anyOf('@foo')\", Tags.fromKarateOptionsTags(\"anyOf('@foo')\"));\n    }\n\n    private boolean eval(String tagSelector, String... strs) {\n        List<Tag> list = new ArrayList(strs.length);\n        for (String s : strs) {\n            list.add(new Tag(0, s));\n        }\n        Tags tags = new Tags(list);\n        return tags.evaluate(tagSelector, null);\n    }\n\n    @Test\n    public void testTagSelectors() {\n        assertTrue(eval(null));\n        assertFalse(eval(null, \"@ignore\"));\n        assertTrue(eval(null, \"@foo\", \"@bar\"));\n        assertFalse(eval(null, \"@foo\", \"@ignore\"));\n        assertTrue(eval(\"anyOf('@foo')\", \"@foo\", \"@bar\"));\n        assertTrue(eval(\"not('@ignore')\"));\n        assertTrue(eval(\"not('@ignore')\", \"@foo\", \"@bar\"));\n        assertTrue(eval(\"anyOf('@foo', '@bar')\", \"@foo\", \"@bar\"));\n        assertTrue(eval(\"anyOf('@foo', '@baz')\", \"@foo\", \"@bar\"));\n        assertTrue(eval(\"allOf('@foo')\", \"@foo\", \"@bar\"));\n        assertTrue(eval(\"allOf('@foo', '@bar')\", \"@foo\", \"@bar\"));\n        assertTrue(eval(\"allOf('@foo', '@bar') && not('@ignore')\", \"@foo\", \"@bar\"));\n        assertTrue(eval(\"anyOf('@foo') && !anyOf('@ignore')\", \"@foo\", \"@bar\"));\n        assertFalse(eval(\"!anyOf('@ignore')\", \"@ignore\"));\n        assertFalse(eval(\"not('@ignore')\", \"@ignore\"));\n        assertFalse(eval(\"not('@ignore', '@foo')\", \"@ignore\"));\n        assertFalse(eval(\"!anyOf('@ignore')\", \"@foo\", \"@bar\", \"@ignore\"));\n        assertFalse(eval(\"anyOf('@foo') && !anyOf('@ignore')\", \"@foo\", \"@bar\", \"@ignore\"));\n        assertFalse(eval(\"anyOf('@foo')\", \"@bar\", \"@ignore\"));\n        assertFalse(eval(\"allOf('@foo', '@baz')\", \"@foo\", \"@bar\"));\n        assertFalse(eval(\"anyOf('@foo') && anyOf('@baz')\", \"@foo\", \"@bar\"));\n        assertFalse(eval(\"!anyOf('@foo')\", \"@foo\", \"@bar\"));\n        assertFalse(eval(\"allOf('@foo', '@bar') && not('@ignore')\", \"@foo\", \"@bar\", \"@ignore\"));\n    }\n\n    @Test\n    public void testTagValueSelectors() {\n        assertFalse(eval(\"valuesFor('@id').isPresent\"));\n        assertFalse(eval(\"valuesFor('@id').isPresent\", \"@foo\"));\n        assertFalse(eval(\"valuesFor('@id').isPresent\", \"@id\"));\n        assertFalse(eval(\"valuesFor('@id').isPresent\", \"@foo\", \"@id\"));\n        assertFalse(eval(\"valuesFor('@id').isPresent\", \"@id=\"));\n        assertTrue(eval(\"valuesFor('@id').isPresent\", \"@id=1\"));\n        assertTrue(eval(\"valuesFor('@id').isOnly(1)\", \"@id=1\"));\n        assertTrue(eval(\"valuesFor('@id').isAnyOf(1)\", \"@id=1\"));\n        assertTrue(eval(\"valuesFor('@id').isAllOf(1)\", \"@id=1\"));\n        assertTrue(eval(\"valuesFor('@id').isAllOf(1)\", \"@id=1,2\"));\n        assertFalse(eval(\"valuesFor('@id').isAnyOf(2)\", \"@id=1\"));\n        assertTrue(eval(\"valuesFor('@id').isAnyOf(1)\", \"@id=1,2\"));\n        assertTrue(eval(\"valuesFor('@id').isAnyOf(2)\", \"@id=1,2\"));\n        assertTrue(eval(\"valuesFor('@id').isAllOf(1, 2)\", \"@id=1,2\"));\n        assertTrue(eval(\"valuesFor('@id').isOnly(1, 2)\", \"@id=1,2\"));\n        assertFalse(eval(\"valuesFor('@id').isOnly(1, 3)\", \"@id=1,2\"));\n        assertTrue(eval(\"valuesFor('@id').isAnyOf(1, 2)\", \"@id=1,2\"));\n        assertTrue(eval(\"valuesFor('@id').isAnyOf(1, 3)\", \"@id=1,2\"));\n        assertTrue(eval(\"valuesFor('@id').isEach(s => s.startsWith('1'))\", \"@id=100,1000\"));\n        assertTrue(eval(\"valuesFor('@id').isEach(s => /^1.*/.test(s))\", \"@id=100,1000\"));\n    }\n\n    private boolean evalEnv(String tagSelector, String karateEnv, String... strs) {\n        List<Tag> list = new ArrayList(strs.length);\n        for (String s : strs) {\n            list.add(new Tag(0, s));\n        }\n        Tags tags = new Tags(list);\n        return tags.evaluate(tagSelector, karateEnv);\n    }\n\n    @Test\n    public void testEnvSelectors() {\n        assertFalse(evalEnv(null, null, \"@env=foo\"));\n        assertTrue(evalEnv(null, \"foo\", \"@env=foo\"));\n        assertTrue(evalEnv(null, null, \"@envnot=foo\"));\n        assertFalse(evalEnv(null, \"foo\", \"@envnot=foo\"));\n        assertTrue(evalEnv(null, \"foo\", \"@env=foo\", \"@bar\"));\n        assertTrue(evalEnv(\"anyOf('@bar')\", \"foo\", \"@env=foo\", \"@bar\"));\n        assertFalse(evalEnv(\"anyOf('@baz')\", \"foo\", \"@env=foo\", \"@bar\"));\n        assertFalse(evalEnv(null, \"baz\", \"@env=foo\", \"@bar\"));\n        assertFalse(evalEnv(null, \"foo\", \"@envnot=foo\", \"@bar\"));\n        assertTrue(evalEnv(null, \"foo\", \"@envnot=baz\", \"@bar\"));\n        assertTrue(evalEnv(\"anyOf('@bar')\", \"foo\", \"@envnot=baz\", \"@bar\"));\n        assertFalse(evalEnv(\"anyOf('@baz')\", \"foo\", \"@envnot=baz\", \"@bar\"));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/TestLogAppender.java",
    "content": "package com.intuit.karate.core;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.encoder.PatternLayoutEncoder;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.AppenderBase;\nimport com.intuit.karate.FileUtils;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class TestLogAppender extends AppenderBase<ILoggingEvent> {\n\n    private final Logger logger;\n    private final PatternLayoutEncoder encoder;\n    private StringBuilder sb;\n\n    public TestLogAppender() {\n        sb = new StringBuilder();\n        logger = (Logger) LoggerFactory.getLogger(\"com.intuit.karate\");\n        setName(\"karate-test\");\n        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();\n        setContext(lc);\n        encoder = new PatternLayoutEncoder();\n        encoder.setPattern(\"%msg\");\n        encoder.setContext(context);\n        encoder.start();\n        start();\n        logger.addAppender(this);\n        logger.setLevel(Level.DEBUG);\n    }\n\n    public String collect() {\n        String temp = sb.toString();\n        sb = new StringBuilder();\n        return temp.replace(\"\\r\\n\", \"\\n\"); // fix for windows\n    }\n\n    @Override\n    protected void append(ILoggingEvent event) {\n        byte[] bytes = encoder.encode(event);\n        String line = FileUtils.toString(bytes);\n        sb.append(line).append('\\n');\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/VariableTest.java",
    "content": "package com.intuit.karate.core;\n\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.graal.JsEngine;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport org.graalvm.polyglot.Value;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\npublic class VariableTest {\n\n    static final Logger logger = LoggerFactory.getLogger(VariableTest.class);\n\n    JsEngine je;\n\n    @BeforeEach\n    void beforeEach() {\n        je = JsEngine.global();\n    }\n\n    @AfterEach\n    void afterEach() {\n        JsEngine.remove();\n    }\n\n    @Test\n    void testJsFunction() {\n        JsValue jv = je.eval(\"(function(a, b){ return a + b })\");\n        Variable var = new Variable(jv);\n        assertTrue(var.isJsFunction());\n        assertFalse(var.isJavaFunction());\n    }\n\n    @Test\n    void testPojo() {\n        JsValue jv = je.eval(\"new com.intuit.karate.core.SimplePojo()\");\n        assertTrue(jv.isOther());\n    }\n\n    @Test\n    void testClass() {\n        JsValue jv = je.eval(\"Java.type('com.intuit.karate.core.MockUtils')\");\n        assertTrue(jv.isOther());\n        Variable v = new Variable(jv);\n        assertEquals(v.type, Variable.Type.OTHER);\n        assertTrue(v.getValue() instanceof Value);\n    }\n    \n    public String simpleFunction(String arg) {\n        return arg;\n    }\n    \n    public String simpleBiFunction(String arg1, String arg2) {\n        return arg1 + arg2;\n    }    \n\n    @Test\n    void testJavaFunction() {\n        Variable v = new Variable((Function<String, String>) this::simpleFunction);\n        assertTrue(v.isJavaFunction());\n        v = new Variable((BiFunction<String, String, String>) this::simpleBiFunction);\n        // maybe we are ok with this, karate \"call\" can be used only with functions\n        assertFalse(v.isJavaFunction());        \n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/abort.feature",
    "content": "Feature:\n\nScenario:\n* def before = true\n* karate.abort()\n* def after = true\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/align.feature",
    "content": "Feature:\n\n  Scenario:\n    * assert      'red ' + 5         ==  'red 5'\n    * table       cats\n      | name   | age |\n      | 'Bob'  | 2   |\n    * match       cats               ==  [{name: 'Bob', age: 2}]\n\n    * def         text               =   'hello <foo> world'\n    * replace     text.foo           =   'bar'\n    * match       text               ==  'hello bar world'\n    * print       text\n\n    * def         myJson             =   {  foo: 'bar' }\n    * set         myJson.foo         =   'world'\n    * match       myJson             ==  { foo: 'world' }\n    * remove      myJson.foo\n\n    * configure   logPrettyResponse  =   true\n    * param       myParam            =   'foo'\n    * header      transaction-id     =   'test'\n    * cookie      cook               =   'bar'\n    * form field  username           =   'john'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/big-decimal.feature",
    "content": "Feature:\n\nScenario:\n* def nums = { profit: -1002.2000000000002 }\n* match nums == { profit: -1002.2000000000002 }\n* def nums = { profit: -1002.2000000000000 }\n* match nums == { profit: -1002.20 }\n* def nums = { profit: -1002.20 }\n* match nums == { profit: -1002.2000000000000 }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-arg-called.feature",
    "content": "Feature:\n\nBackground:\n* match __arg == { foo: 'bar' }\n* call read('call-arg-common.feature')\n\nScenario:\n* print 'in scenario'\n* match __arg == { foo: 'bar' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-arg-common.feature",
    "content": "Feature:\n\nScenario:\n* def hello = function(){ return 'hello' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-arg-null-called.feature",
    "content": "Feature:\n\nScenario:\n* match foo == 'bar'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-arg-null.feature",
    "content": "Feature:\n\nScenario:\n* def foo = null\n* karate.call('call-arg-null-called.feature', { foo: 'bar' })\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-arg.feature",
    "content": "Feature:\n\nBackground:\n* print 'in background'\n\nScenario:\n* def params = { 'foo': 'bar' }\n* call read('call-arg-called.feature') params\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-by-tag-called.feature",
    "content": "Feature:\n\n@name=first\nScenario:\n* def bar = 1\n\n@name=second\nScenario:\n* def bar = 2\n\n@name=third\nScenario:\n* def bar = 3\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-by-tag.feature",
    "content": "Feature:\n\nScenario:\n* def foo = call read('call-by-tag-called.feature@name=second')\n* match foo.bar == 2\n\nScenario:\n* def foo = call read('@sameFileTag')\n* match foo.bar == 2\n\n@ignore @sameFileTag\nScenario:\n* call read('call-by-tag-called.feature@name=second')\n* match bar == 2"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-feature-called.feature",
    "content": "Feature:\n\nScenario:\n* def res = foo\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-feature.feature",
    "content": "Feature:\n\nScenario:\n* def called = read('call-feature-called.feature')\n* def data = [{ foo: 'first' }, { foo: 'second' }]\n* def result = call called data\n# this is a source of bugs as the java objects in scope get serialized\n* def extracted = $result[*].res\n* match extracted == ['first', 'second']\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-js-called.feature",
    "content": "Feature:\n\nScenario:\n* print 'in called'\n* def calledVar = 'hello world'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-js.feature",
    "content": "Feature: will a js call show up in the report\n\nBackground:\n* def fun = function(){ return karate.call('call-js-called.feature') }\n\nScenario:\n* print 'before call'\n* call fun\n* print 'after call'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-jsonpath-called.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* def vals = $foo[*].id\n* print vals\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-jsonpath.feature",
    "content": "Feature:\n\nScenario:\n* def foo = [{a: 1}, {a: 2}]\n* def bar = call read('call-jsonpath-called.feature')\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-response-called.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* url urlBase\n* method get\n* status 200\n* match response == { success: true }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-response-mock.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* def response = { success: true }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-response.feature",
    "content": "Feature:\n\nScenario:\n* def urlBase = 'http://localhost:' + karate.properties['server.port']\n* call read('call-response-called.feature')\n* match responseTime == '#number'\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-self.feature",
    "content": "Feature:\n\n  Scenario:\n    * call read('@stub')\n    * print 'first'\n    * def result = 'first'\n\n  Scenario:\n    * print 'second'\n    * def result = 'second'\n\n  @stub @ignore\n  Scenario:\n    * print 'called'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-single-fail-called.feature",
    "content": "Feature:\n\nBackground:\n* def fun = function(){ throw 'fail-called'  }\n* fun()\n\nScenario:\n* print 'in fail-called'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-single-fail.feature",
    "content": "Feature:\n\nScenario: one\n* print 'in fail 1'\n\nScenario: two\n* print 'in fail 2'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-single-tag-called.feature",
    "content": "Feature:\n\nScenario Outline:\n  * print 'in called:', karate.tags\n  * def fromCallSingle = val\n\n  @callme\n  Examples:\n    | val             |\n    | hello world     |\n\n  @dontcallme\n  Examples:\n    | val             |\n    | not hello world |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/call-single-tag.feature",
    "content": "Feature:\n\n@runme\nScenario:\n* match fromCallSingle == 'hello world'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/callSingleFeature.feature",
    "content": "#!.karate\nFeature: sample feature used by karate.callSingle()\n\n  Scenario: Set a Variable with param value\n    * def receivedParam = (__arg && __arg.data) ? __arg.data : 'Nothing'\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/callSingleScenario.feature",
    "content": "#!.karate\nFeature: sample feature used by karate.callSingle()\n\n  @ignore\n  @storeValue\n  Scenario: Set a Variable with param value\n    * def receivedParam = (__arg && __arg.data) ? __arg.data : 'Nothing'\n\n\n  Scenario: Set a Variable with param value\n    * def sValue = \"test from scenario\"\n    * def oTest = call read(\"@storeValue\") { data: \"#(sValue)\" }\n    * match oTest.receivedParam == sValue\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/called1.feature",
    "content": "Feature:\n\nScenario:\n* def a = 1\n* def foo = { hello: 'world' }"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/called2.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* def calledBar = configUtilsJs.someFun()"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/called3-caller1.js",
    "content": "function fn() {\n  var result = karate.callSingle('called3.js');\n  karate.log('varA:', result.varA);\n  return result.varA;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/called3-caller2.js",
    "content": "function fn() {\n  var result = karate.call('called3.js');\n  karate.log('varA:', result.varA);\n  return result.varA;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/called3.js",
    "content": "function fn() {\n  var A = '2';\n  var B = '3';\n  return {\n    varA: A,\n    varB: B\n  };\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/callonce-bg-called.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n  * print 'in called'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/callonce-bg-outline.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n  * print 'in called outline value:', value\n  * match value == __num + 1 + ''\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/callonce-bg.feature",
    "content": "@ignore\nFeature:\n\nBackground:\n  * callonce read('callonce-bg-called.feature')\n\nScenario: first\n  * print 'in first'\n\nScenario Outline: outline <value>\n  * print 'in main outline value:', value\n  * call read('callonce-bg-outline.feature')\n  * match karate.info.scenarioName == 'outline ' + value\n\n  Examples:\n    | value |\n    | 1     |\n    | 2     |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/callonce-config-called.feature",
    "content": "Feature:\n\nScenario:\n* def hello = function(name){ return 'hello ' + name }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/callonce-config.feature",
    "content": "Feature:\n\nScenario:\n* def foo = hello('foo')\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/callonce-global-called.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/callonce-global.feature",
    "content": "Feature: make sure a callonce (with shared scope) in the background \n    does not leak variables created in the scenarios\n\nBackground:\n* callonce read('callonce-global-called.feature')\n\nScenario: first\n  * assert typeof email == 'undefined'\n  * def email = \"admin@admin.com\"\n\nScenario: second\n  * assert typeof email == 'undefined'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/configure-in-js.feature",
    "content": "Feature:\n\nScenario:\n* eval\n\"\"\"\nvar temp = {\n  proxy: { uri: 'http://my-proxy.com:3128', nonProxyHosts: [ 'my-api2.com' ]}\n};\nkarate.configure('proxy', temp.proxy);\n\"\"\"\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/copy-called-nested.feature",
    "content": "@ignore\nFeature: called file that overwites nested data\n\nScenario:\n    * def root = { name: { name: 'inner' } }"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/copy-called-overwrite.feature",
    "content": "@ignore\nFeature: called file that may or may not over-write variable in caller\n\nScenario:\n* def someString = 'after'\n* def someJson = { value: 'after' }\n* def fromCalled = { hello: 'world' }"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/copy-called.feature",
    "content": "@ignore\nFeature: called file should not clobber vars in caller\n\nScenario:\n* match foo == { key: 'value' }\n* foo.key = 'changed'\n* match foo == { key: 'changed' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/copy.feature",
    "content": "Feature: the difference with variable scoping in 'isolated' and 'shared' mode\n    since call is 'pass by reference' you need to clone using 'copy' if needed\n\nScenario: isolated scope: called feature does not over-write variables\n    * def someString = 'before'\n    * def someJson = { value: 'before' }\n    * def result = call read('copy-called-overwrite.feature')\n    * match someString == 'before'\n    * match someJson == { value: 'before' }\n    * assert typeof fromCalled == 'undefined'\n\nScenario: shared scope: called feature will over-write (and contribute) variables\n    * def someString = 'before'\n    * def someJson = { value: 'before' }\n    * call read('copy-called-overwrite.feature')\n    * match someString == 'after'\n    * match someJson == { value: 'after' }\n    * match fromCalled == { hello: 'world' }\n\nScenario: called feature (isolated scope) cannot mutate top-level variables\n    * def foo = { key: 'value' }\n    # json cannot be mutated in called features if isolated scope\n    * def result = call read('copy-called.feature')\n    * match foo == { key: 'value' }\n\nScenario: called feature (shared scope) can mutate top-level variables\n    * def foo = { key: 'value' }\n    # json can be mutated in called features if shared scope\n    * call read('copy-called.feature')\n    * match foo == { key: 'changed' }\n\nScenario: you can manually 'clone' a payload if needed\n    * def original = { key: 'value' }\n    # since the called feature mutates 'foo' we ensure it is a clone\n    * copy foo = original\n    * call read('copy-called.feature')\n    * match foo == { key: 'changed' }\n    # and original remains unchanged\n    * match original == { key: 'value' }\n\nScenario: clone should be 'deep' and work even for nested data\n    * def temp = call read('copy-called-nested.feature')\n    * def a = temp.root\n    * copy b = a\n    * set b.name.name = 'copy'\n    * match b.name.name == 'copy'\n    * match a.name.name == 'inner'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/csv.feature",
    "content": "Feature:\n\nScenario:\n* text foo =\n    \"\"\"\n    name,type\n    Billie,LOL\n    Bob,Wild\n    \"\"\"\n* csv bar = foo\n* match bar == [{ name: 'Billie', type: 'LOL' }, { name: 'Bob', type: 'Wild' }]\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/data.json",
    "content": "{ \"hello\": \"world\" }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/dummy-ui-google.feature",
    "content": "Feature:\n\nScenario: try to login to github\n  * configure driver = { type: 'noopdriver', showDriverLog: true }\n  * driver 'https://google.com'\n  * configUtils.existsFunction('#element')\n  * screenshot()"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/dummy.feature",
    "content": "Feature:\n\nScenario:\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/eval-and-set.feature",
    "content": "Feature:\n\nScenario:\n* eval\n\"\"\"\nvar foo = function(v){ return v * v };\nvar nums = [0, 1, 2, 3, 4];\nvar squares = [];\nfor (var n in nums) {\n  squares.push(foo(n));\n}\nkarate.set('temp', squares);\n\"\"\"\n* match temp == [0, 1, 4, 9, 16]"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/eval-assign.feature",
    "content": "Feature:\n\n  Scenario:\n    * def foo = { bar: 'one' }\n    * foo.bar =\n    \"\"\"\n    {\n      some: 'big',\n      message: 'content'\n    }\n    \"\"\"\n    * match foo == { bar: { some: 'big', message: 'content' } }"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/exec.feature",
    "content": "Feature:\n\nScenario:\n* def home = karate.exec('echo $HOME')\n* print 'home:', home\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/extract.feature",
    "content": "Feature: karate.extract()\n\nBackground:\n* def text = karate.readAsString('extract.html')\n\nScenario: extract first regex\n* def token = karate.extract(text, 'login_form_token.+value=\\\\\"([^\\\\\"]+)', 1)\n* match token == 'secret1'\n\nScenario: extract all regexes\n* def tokens = karate.extractAll(text, 'login_form.?_token.+value=\\\\\"([^\\\\\"]+)', 1)\n* match tokens == ['secret1', 'secret2']\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/extract.html",
    "content": "<html>\n<form name=\"login_form\" method=\"post\" action=\"/login\">\n  <input type=\"hidden\" id=\"login_form_token\" value=\"secret1\">\n</form>\n<form name=\"login_form2\" method=\"post\" action=\"/login\">\n  <input type=\"hidden\" id=\"login_form2_token\" value=\"secret2\">\n</form>\n</html>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/fail-api.feature",
    "content": "Feature:\n\nScenario:\n* def before = true\n* karate.fail('test fail')\n* def after = true\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/fail-js.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* def before = true\n* karate.read('missing.js')\n* def after = true\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/fail-tag-failure.feature",
    "content": "Feature: fail tag failure\n\n@fail\nScenario:\n* def a = 1 + 2\n* match a == 3\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/fail-tag.feature",
    "content": "Feature: fail tag\n\n@fail\nScenario:\n* def a = 1 + 2\n* match a == 4\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/fail1.feature",
    "content": "Feature: fail 1\n\nScenario:\n* def a = 1 + 2\n* match a == 4\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/feature-result-called.feature",
    "content": "Feature:\n\nScenario:\n* print 'in called'\n* karate.embed('<h1>hello world</h1>', 'text/html')\n* print 'after embed'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/feature-result-cucumber.json",
    "content": "{\n    \"line\": 2,\n    \"elements\": [\n        {\n            \"line\": 5,\n            \"name\": \"\",\n            \"description\": \"\",\n            \"type\": \"background\",\n            \"keyword\": \"Background\",\n            \"steps\": [\n                {\n                    \"name\": \"print 'in background'\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"*\",\n                    \"line\": 6,\n                    \"doc_string\": {\n                        \"content_type\": \"\",\n                        \"value\": \"#string\",\n                        \"line\": 6\n                    }\n                }\n            ]\n        },\n        {\n            \"line\": 9,\n            \"name\": \"hello world\",\n            \"description\": \"\",\n            \"id\": \"hello-world\",\n            \"type\": \"scenario\",\n            \"start_timestamp\": \"#string\",\n            \"keyword\": \"Scenario\",\n            \"steps\": [\n                {\n                    \"name\": \"print 'before'\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"*\",\n                    \"line\": 12,\n                    \"doc_string\": {\n                        \"content_type\": \"\",\n                        \"value\": \"#string\",\n                        \"line\": 12\n                    },\n                    \"comments\": [\n                        \"# Some comments\",\n                        \"# Some more comments\"\n                    ]\n                },\n                {\n                    \"name\": \"call read('feature-result-called.feature')\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"*\",\n                    \"line\": 13\n                },\n                {\n                    \"name\": \"com\\/intuit\\/karate\\/core\\/feature-result-called.feature\",\n                    \"result\": {\n                        \"duration\": 0,\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"\",\n                    \"line\": 13\n                },\n                {\n                    \"name\": \"print 'in called'\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"> *\",\n                    \"line\": 4,\n                    \"doc_string\": {\n                        \"content_type\": \"\",\n                        \"value\": \"#string\",\n                        \"line\": 4\n                    }\n                },\n                {\n                    \"name\": \"karate.embed('<h1>hello world<\\/h1>', 'text\\/html')\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"embeddings\": [\n                        {\n                            \"data\": \"PGgxPmhlbGxvIHdvcmxkPC9oMT4=\",\n                            \"mime_type\": \"text\\/html\"\n                        }\n                    ],\n                    \"keyword\": \"> *\",\n                    \"line\": 5\n                },\n                {\n                    \"name\": \"print 'after embed'\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"> *\",\n                    \"line\": 6,\n                    \"doc_string\": {\n                        \"content_type\": \"\",\n                        \"value\": \"#string\",\n                        \"line\": 6\n                    }\n                },\n                {\n                    \"name\": \"print 'after'\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"*\",\n                    \"line\": 14,\n                    \"doc_string\": {\n                        \"content_type\": \"\",\n                        \"value\": \"#string\",\n                        \"line\": 14\n                    }\n                }\n            ],\n            \"tags\": [\n                {\n                    \"name\": \"@one\",\n                    \"line\": 1\n                },\n                {\n                    \"name\": \"@two\",\n                    \"line\": 8\n                }\n            ]\n        },\n        {\n            \"line\": 5,\n            \"name\": \"\",\n            \"description\": \"\",\n            \"type\": \"background\",\n            \"keyword\": \"Background\",\n            \"steps\": [\n                {\n                    \"name\": \"print 'in background'\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"*\",\n                    \"line\": 6,\n                    \"doc_string\": {\n                        \"content_type\": \"\",\n                        \"value\": \"#string\",\n                        \"line\": 6\n                    }\n                }\n            ]\n        },\n        {\n            \"line\": 21,\n            \"name\": \"hello foo\",\n            \"description\": \"\",\n            \"id\": \"hello-foo\",\n            \"type\": \"scenario\",\n            \"start_timestamp\": \"#string\",\n            \"keyword\": \"Scenario Outline\",\n            \"steps\": [\n                {\n                    \"name\": \"print 'name:', name\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"*\",\n                    \"line\": 17,\n                    \"doc_string\": {\n                        \"content_type\": \"\",\n                        \"value\": \"#string\",\n                        \"line\": 17\n                    }\n                }\n            ],\n            \"tags\": [\n                {\n                    \"name\": \"@one\",\n                    \"line\": 1\n                }\n            ]\n        },\n        {\n            \"line\": 5,\n            \"name\": \"\",\n            \"description\": \"\",\n            \"type\": \"background\",\n            \"keyword\": \"Background\",\n            \"steps\": [\n                {\n                    \"name\": \"print 'in background'\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"*\",\n                    \"line\": 6,\n                    \"doc_string\": {\n                        \"content_type\": \"\",\n                        \"value\": \"#string\",\n                        \"line\": 6\n                    }\n                }\n            ]\n        },\n        {\n            \"line\": 22,\n            \"name\": \"hello bar\",\n            \"description\": \"\",\n            \"id\": \"hello-bar\",\n            \"type\": \"scenario\",\n            \"start_timestamp\": \"#string\",\n            \"keyword\": \"Scenario Outline\",\n            \"steps\": [\n                {\n                    \"name\": \"print 'name:', name\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"*\",\n                    \"line\": 17,\n                    \"doc_string\": {\n                        \"content_type\": \"\",\n                        \"value\": \"#string\",\n                        \"line\": 17\n                    }\n                }\n            ],\n            \"tags\": [\n                {\n                    \"name\": \"@one\",\n                    \"line\": 1\n                }\n            ]\n        },\n        {\n            \"line\": 5,\n            \"name\": \"\",\n            \"description\": \"\",\n            \"type\": \"background\",\n            \"keyword\": \"Background\",\n            \"steps\": [\n                {\n                    \"name\": \"print 'in background'\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"*\",\n                    \"line\": 6,\n                    \"doc_string\": {\n                        \"content_type\": \"\",\n                        \"value\": \"#string\",\n                        \"line\": 6\n                    }\n                }\n            ]\n        },\n        {\n            \"line\": 26,\n            \"name\": \"hello baz\",\n            \"description\": \"\",\n            \"id\": \"hello-baz\",\n            \"type\": \"scenario\",\n            \"start_timestamp\": \"#string\",\n            \"keyword\": \"Scenario Outline\",\n            \"steps\": [\n                {\n                    \"name\": \"print 'name:', name\",\n                    \"result\": {\n                        \"duration\": \"#number\",\n                        \"status\": \"passed\"\n                    },\n                    \"match\": {\n                        \"location\": \"karate\",\n                        \"arguments\": [\n                        ]\n                    },\n                    \"keyword\": \"*\",\n                    \"line\": 17,\n                    \"doc_string\": {\n                        \"content_type\": \"\",\n                        \"value\": \"#string\",\n                        \"line\": 17\n                    }\n                }\n            ],\n            \"tags\": [\n                {\n                    \"name\": \"@one\",\n                    \"line\": 1\n                }\n            ]\n        }\n    ],\n    \"name\": \"com\\/intuit\\/karate\\/core\\/feature-result.feature\",    \n    \"description\": \"my feature\\nmy description\",\n    \"id\": \"my-feature\",\n    \"keyword\": \"Feature\",\n    \"uri\": \"com\\/intuit\\/karate\\/core\\/feature-result.feature\",\n    \"tags\": [\n        {\n            \"name\": \"@one\",\n            \"line\": 1\n        }\n    ]\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/feature-result.feature",
    "content": "@one\nFeature: my feature\n    my description\n\nBackground:\n* print 'in background'\n\n@two\nScenario: hello world\n# Some comments\n# Some more comments\n* print 'before'\n* call read('feature-result-called.feature')\n* print 'after'\n\nScenario Outline: hello <name>\n* print 'name:', name\n\nExamples:\n| name |\n| foo  |\n| bar  |\n\nExamples:\n| name |\n| baz  |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/feature-result.json",
    "content": "{\n    \"scenarioResults\": [\n        {\n            \"stepResults\": [\n                {\n                    \"result\": {\n                        \"nanos\": \"#number\",\n                        \"millis\": \"#number\",\n                        \"status\": \"passed\",\n                        \"startTime\": \"#number\",\n                        \"endTime\": \"#number\"\n                    },\n                    \"step\": {\n                        \"background\": true,\n                        \"line\": 6,\n                        \"prefix\": \"*\",\n                        \"index\": 0,\n                        \"text\": \"print 'in background'\"\n                    },\n                    \"stepLog\": \"#string\"\n                },\n                {\n                    \"result\": {\n                        \"nanos\": \"#number\",\n                        \"millis\": \"#number\",\n                        \"status\": \"passed\",\n                        \"startTime\": \"#number\",\n                        \"endTime\": \"#number\"                        \n                    },\n                    \"step\": {\n                        \"comments\": [\n                            \"# Some comments\",\n                            \"# Some more comments\"\n                        ],\n                        \"line\": 12,\n                        \"prefix\": \"*\",\n                        \"index\": 0,\n                        \"text\": \"print 'before'\"\n                    },\n                    \"stepLog\": \"#string\"\n                },\n                {\n                    \"result\": {\n                        \"nanos\": \"#number\",\n                        \"millis\": \"#number\",\n                        \"status\": \"passed\",\n                        \"startTime\": \"#number\",\n                        \"endTime\": \"#number\"                        \n                    },\n                    \"step\": {\n                        \"line\": 13,\n                        \"prefix\": \"*\",\n                        \"index\": 1,\n                        \"text\": \"call read('feature-result-called.feature')\"\n                    },\n                    \"callResults\": [\n                        {\n                            \"scenarioResults\": [\n                                {\n                                    \"stepResults\": [\n                                        {\n                                            \"result\": {\n                                                \"nanos\": \"#number\",\n                                                \"millis\": \"#number\",\n                                                \"status\": \"passed\",\n                                                \"startTime\": \"#number\",\n                                                \"endTime\": \"#number\"                                                \n                                            },\n                                            \"step\": {\n                                                \"line\": 4,\n                                                \"prefix\": \"*\",\n                                                \"index\": 0,\n                                                \"text\": \"print 'in called'\"\n                                            },\n                                            \"stepLog\": \"#string\"\n                                        },\n                                        {\n                                            \"result\": {\n                                                \"nanos\": \"#number\",\n                                                \"millis\": \"#number\",\n                                                \"status\": \"passed\",\n                                                \"startTime\": \"#number\",\n                                                \"endTime\": \"#number\"                                                \n                                            },\n                                            \"step\": {\n                                                \"line\": 5,\n                                                \"prefix\": \"*\",\n                                                \"index\": 1,\n                                                \"text\": \"karate.embed('<h1>hello world<\\/h1>', 'text\\/html')\"\n                                            },\n                                            \"embeds\": [\n                                                {\n                                                    \"file\": \"#string\",\n                                                    \"resourceType\": \"HTML\",\n                                                    \"html\": \"<h1>hello world<\\/h1>\"\n                                                }\n                                            ]\n                                        },\n                                        {\n                                            \"result\": {\n                                                \"nanos\": \"#number\",\n                                                \"millis\": \"#number\",\n                                                \"status\": \"passed\",\n                                                \"startTime\": \"#number\",\n                                                \"endTime\": \"#number\"                                                \n                                            },\n                                            \"step\": {\n                                                \"line\": 6,\n                                                \"prefix\": \"*\",\n                                                \"index\": 2,\n                                                \"text\": \"print 'after embed'\"\n                                            },\n                                            \"stepLog\": \"#string\"\n                                        }\n                                    ],\n                                    \"executorName\": \"main\",\n                                    \"name\": \"\",\n                                    \"description\": \"\",\n                                    \"line\": 3,\n                                    \"sectionIndex\": 0,\n                                    \"startTime\": \"#number\",\n                                    \"endTime\": \"#number\",\n                                    \"exampleIndex\": -1,\n                                    \"refId\": \"[1:3]\",\n                                    \"durationMillis\": \"#number\",\n                                    \"failed\": false\n                                }\n                            ],\n                            \"prefixedPath\": \"classpath:com\\/intuit\\/karate\\/core\\/feature-result-called.feature\",\n                            \"relativePath\": \"com\\/intuit\\/karate\\/core\\/feature-result-called.feature\",\n                            \"packageQualifiedName\": \"com.intuit.karate.core.feature-result-called\",\n                            \"name\": \"\",\n                            \"description\": \"\",\n                            \"durationMillis\": \"#number\",\n                            \"resultDate\": \"#string\",\n                            \"passedCount\": 1,\n                            \"failedCount\": 0,\n                            \"callDepth\": 1,\n                            \"loopIndex\": -1\n                        }\n                    ]\n                },\n                {\n                    \"result\": {\n                        \"nanos\": \"#number\",\n                        \"millis\": \"#number\",\n                        \"status\": \"passed\",\n                        \"startTime\": \"#number\",\n                        \"endTime\": \"#number\"                        \n                    },\n                    \"step\": {\n                        \"line\": 14,\n                        \"prefix\": \"*\",\n                        \"index\": 2,\n                        \"text\": \"print 'after'\"\n                    },\n                    \"stepLog\": \"#string\"\n                }\n            ],\n            \"executorName\": \"main\",\n            \"name\": \"hello world\",\n            \"description\": \"\",\n            \"line\": 9,\n            \"sectionIndex\": 0,\n            \"startTime\": \"#number\",\n            \"endTime\": \"#number\",\n            \"exampleIndex\": -1,\n            \"refId\": \"[1:9]\",\n            \"durationMillis\": \"#number\",\n            \"failed\": false,\n            \"tags\": [\n                \"one\",\n                \"two\"\n            ]\n        },\n        {\n            \"stepResults\": [\n                {\n                    \"result\": {\n                        \"nanos\": \"#number\",\n                        \"millis\": \"#number\",\n                        \"status\": \"passed\",\n                        \"startTime\": \"#number\",\n                        \"endTime\": \"#number\"                        \n                    },\n                    \"step\": {\n                        \"background\": true,\n                        \"line\": 6,\n                        \"prefix\": \"*\",\n                        \"index\": 0,\n                        \"text\": \"print 'in background'\"\n                    },\n                    \"stepLog\": \"#string\"\n                },\n                {\n                    \"result\": {\n                        \"nanos\": \"#number\",\n                        \"millis\": \"#number\",\n                        \"status\": \"passed\",\n                        \"startTime\": \"#number\",\n                        \"endTime\": \"#number\"                        \n                    },\n                    \"step\": {\n                        \"line\": 17,\n                        \"prefix\": \"*\",\n                        \"index\": 0,\n                        \"text\": \"print 'name:', name\"\n                    },\n                    \"stepLog\": \"#string\"\n                }\n            ],\n            \"executorName\": \"main\",\n            \"name\": \"hello foo\",\n            \"description\": \"\",\n            \"line\": 21,\n            \"sectionIndex\": 1,\n            \"startTime\": \"#number\",\n            \"endTime\": \"#number\",\n            \"exampleIndex\": 0,\n            \"refId\": \"[2.1:21]\",\n            \"durationMillis\": \"#number\",\n            \"failed\": false,\n            \"tags\": [\n                \"one\"\n            ],\n            \"exampleData\": {\n                \"name\": \"foo\"\n            }\n        },\n        {\n            \"stepResults\": [\n                {\n                    \"result\": {\n                        \"nanos\": \"#number\",\n                        \"millis\": \"#number\",\n                        \"status\": \"passed\",\n                        \"startTime\": \"#number\",\n                        \"endTime\": \"#number\"                        \n                    },\n                    \"step\": {\n                        \"background\": true,\n                        \"line\": 6,\n                        \"prefix\": \"*\",\n                        \"index\": 0,\n                        \"text\": \"print 'in background'\"\n                    },\n                    \"stepLog\": \"#string\"\n                },\n                {\n                    \"result\": {\n                        \"nanos\": \"#number\",\n                        \"millis\": \"#number\",\n                        \"status\": \"passed\",\n                        \"startTime\": \"#number\",\n                        \"endTime\": \"#number\"                        \n                    },\n                    \"step\": {\n                        \"line\": 17,\n                        \"prefix\": \"*\",\n                        \"index\": 0,\n                        \"text\": \"print 'name:', name\"\n                    },\n                    \"stepLog\": \"#string\"\n                }\n            ],\n            \"executorName\": \"#string\",\n            \"name\": \"hello bar\",\n            \"description\": \"\",\n            \"line\": 22,\n            \"sectionIndex\": 1,\n            \"startTime\": \"#number\",\n            \"endTime\": \"#number\",\n            \"exampleIndex\": 1,\n            \"refId\": \"[2.2:22]\",\n            \"durationMillis\": \"#number\",\n            \"failed\": false,\n            \"tags\": [\n                \"one\"\n            ],\n            \"exampleData\": {\n                \"name\": \"bar\"\n            }\n        },\n        {\n            \"stepResults\": [\n                {\n                    \"result\": {\n                        \"nanos\": \"#number\",\n                        \"millis\": \"#number\",\n                        \"status\": \"passed\",\n                        \"startTime\": \"#number\",\n                        \"endTime\": \"#number\"                        \n                    },\n                    \"step\": {\n                        \"background\": true,\n                        \"line\": 6,\n                        \"prefix\": \"*\",\n                        \"index\": 0,\n                        \"text\": \"print 'in background'\"\n                    },\n                    \"stepLog\": \"#string\"\n                },\n                {\n                    \"result\": {\n                        \"nanos\": \"#number\",\n                        \"millis\": \"#number\",\n                        \"status\": \"passed\",\n                        \"startTime\": \"#number\",\n                        \"endTime\": \"#number\"                        \n                    },\n                    \"step\": {\n                        \"line\": 17,\n                        \"prefix\": \"*\",\n                        \"index\": 0,\n                        \"text\": \"print 'name:', name\"\n                    },\n                    \"stepLog\": \"#string\"\n                }\n            ],\n            \"executorName\": \"#string\",\n            \"name\": \"hello baz\",\n            \"description\": \"\",\n            \"line\": 26,\n            \"sectionIndex\": 1,\n            \"startTime\": \"#number\",\n            \"endTime\": \"#number\",\n            \"exampleIndex\": 0,\n            \"refId\": \"[2.1:26]\",\n            \"durationMillis\": \"#number\",\n            \"failed\": false,\n            \"tags\": [\n                \"one\"\n            ],\n            \"exampleData\": {\n                \"name\": \"baz\"\n            }\n        }\n    ],\n    \"prefixedPath\": \"classpath:com\\/intuit\\/karate\\/core\\/feature-result.feature\",\n    \"relativePath\": \"com\\/intuit\\/karate\\/core\\/feature-result.feature\",\n    \"packageQualifiedName\": \"com.intuit.karate.core.feature-result\",\n    \"name\": \"my feature\",\n    \"description\": \"my description\",\n    \"durationMillis\": \"#number\",\n    \"resultDate\": \"#string\",\n    \"passedCount\": 4,\n    \"failedCount\": 0,\n    \"callDepth\": 0,\n    \"loopIndex\": -1\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/features/RemainingFeaturesTest.java",
    "content": "package com.intuit.karate.core.features;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.Suite;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class RemainingFeaturesTest {\n\n  private static Suite suite;\n\n  @Test\n  void testRemainingFeaturesSingleThread() {\n    verifyRemainingFeaturesWithThreads(1);\n  }\n\n  @Test\n  void testRemainingFeaturesParallel() {\n    verifyRemainingFeaturesWithThreads(2);\n  }\n\n  /**\n   * Hooks into the current suite to return the remaining features within the test\n   * @return Remaining features count\n   */\n  public static long remainingFeatures() {\n    return suite.getFeaturesRemaining();\n  }\n\n  private void verifyRemainingFeaturesWithThreads(int threads) {\n    Runner.Builder<?> builder = Runner.builder()\n        .path(\"classpath:com/intuit/karate/core/features\")\n        .configDir(\"classpath:com/intuit/karate/core/features\")\n        .threads(threads);\n    builder.resolveAll();\n    suite = new Suite(builder);\n    suite.run();\n    Results results = suite.buildResults();\n    assertEquals(0, results.getFailCount(), results.getErrorMessages());\n  }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/features/feature1.feature",
    "content": "Feature:\n\n  Scenario: feature test 1\n    * def numOfFeaturesLeft = remainingFeatures()\n    * print 'Features left (including this one):', numOfFeaturesLeft\n    # this is the first feature that runs, so there should be more than 1 feature running\n    * assert numOfFeaturesLeft > 1\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/features/feature2.feature",
    "content": "Feature:\n\n  Scenario: feature test 2\n    * def numOfFeaturesLeft = remainingFeatures()\n    * print 'Features left (including this one):', numOfFeaturesLeft\n    # there should always be at least 1 feature left, even if it's the current feature running\n    * assert numOfFeaturesLeft >= 1\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/features/karate-config.js",
    "content": "function karateConfig() {\n  const config = {}\n\n  const RemainingFeaturesTest = Java.type('com.intuit.karate.core.features.RemainingFeaturesTest')\n  config.remainingFeatures = RemainingFeaturesTest.remainingFeatures\n\n  return config\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/fork-listener.feature",
    "content": "Feature: log listener example\n\nScenario: ping\n* def pingCount = { value: 0 }\n* def listener = \n\"\"\"\nfunction(line) {\n  if (line.contains('time=')) {\n    pingCount.value++;\n    karate.log('count is', pingCount.value);\n  }\n  if (pingCount.value == 3) {\n    karate.log('3 pings done, stopping');      \n    var proc = karate.get('proc');\n    karate.signal(proc.sysOut);\n  }\n}\n\"\"\"\n* def proc = karate.fork({ args: ['ping', 'google.com'], listener: listener })\n* listen 5000\n* print 'console output:', listenResult\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/fork.feature",
    "content": "Feature:\n\nBackground:\n* def command =\n\"\"\"\nfunction(line) {\n  var proc = karate.fork({ redirectErrorStream: false, useShell: true, line: line });\n  proc.waitSync();\n  karate.set('sysOut', proc.sysOut);\n  karate.set('exitCode', proc.exitCode);\n}\n\"\"\"\n\nScenario: java cli\n* if (karate.os.type == 'windows') karate.abort()\n* command('ls')\n* match exitCode == 0\n* match sysOut contains 'pom.xml'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/ignore-step-failure.feature",
    "content": "Feature: continue on step failure keyword\n\nBackground:\n  * configure continueOnStepFailure = true\n\nScenario: test\n  * def tmp = 'foo'\n  * configure continueOnStepFailure = true\n  * match tmp == 'bar'\n  * match tmp == 'pub'\n  * match tmp == 'crawl'\n  * match tmp == 'foo'\n  * configure continueOnStepFailure = false\n  * match tmp == 'foo'\n  * match tmp == 'bar2'\n\nScenario Outline: hello <name>\n  * print 'name:', name\n  * match name == 'foo'\n  * match name == 'bar'\n  * configure continueOnStepFailure = false\n  * match name == '<name>'\n  * match name == 'a failure'\n  * match name == 'skipped'\n\nExamples:\n  | name |\n  | foo  |\n  | bar  |\n\nScenario Outline: hello <name>\n  * print 'name:', name\n  * match name == 'foo'\n  * match name == 'bar'\n  * configure continueOnStepFailure = { enabled: false, continueAfter: true }\n  * match name == '<name>'\n  * match name == 'a failure'\n  * match name == 'skipped'\n\n  Examples:\n    | name |\n    | foo  |\n    | bar  |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/increment.js",
    "content": "function fn() {\n  karate.set('_curId', 0);\n  return function() {\n    var curId = karate.get('_curId');\n    var nextId = curId + 1;\n    karate.set('_curId', nextId);\n    return nextId;\n  };\n}  \n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/js-arrays-products.json",
    "content": "[\n   {\n      \"id\":15,\n      \"name\":\"Wotsit v1.5\",\n      \"partIDs\":[\n         {\n            \"id\":1\n         },\n         {\n            \"id\":2\n         },\n         {\n            \"id\":3\n         }\n      ]\n   },\n   {\n      \"id\":20,\n      \"name\":\"Wotsit v2.0\",\n      \"partIDs\":[\n         {\n            \"id\":4\n         },\n         {\n            \"id\":5\n         },\n         {\n            \"id\":6\n         }\n      ]\n   },\n   {\n      \"id\":25,\n      \"name\":\"Wotsit v2.5\",\n      \"partIDs\":[\n         {\n            \"id\":1\n         },\n         {\n            \"id\":2\n         },\n         {\n            \"id\":6\n         }\n      ]\n   }\n]"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/js-arrays.feature",
    "content": "Feature: various javascript tests\n\nScenario: arrays returned from js can be used in match\n    * def fun = function(){ return ['foo', 'bar', 'baz'] }\n    * def json = ['foo', 'bar', 'baz']\n    * match json == fun()\n    * def expected = fun()\n    * match json == expected\n\nScenario: arrays returned from js can be modified using 'set'\n    * def fun = function(){ return [{a: 1}, {a: 2}, {b: 3}] }\n    * def json = fun()\n    * set json[1].a = 5\n    * match json == [{a: 1}, {a: 5}, {b: 3}]\n\nScenario: json-path can be performed in js\n    * def json = [{foo: 1}, {foo: 2}]\n    * def fun = function(arg) { return karate.jsonPath(arg, '$[*].foo') }\n    * def res = call fun json\n    * match res == [1, 2]\n    # json-path in the form $varname.blah\n    * def foo = { bar: [{baz: 1}, {baz: 2}, {baz: 3}]}\n    * def fun = function(){ return karate.get('$foo.bar[*].baz') }\n    * def res = call fun\n    * match res == [1, 2, 3]\n\nScenario: set via json-path can be done in js\n    * def json = { foo: [] }\n    * karate.set('json', '$.foo[]', { bar: 'baz' })\n    * match json == { foo: [{ bar: 'baz' }] }\n\nScenario: ensure nested nashorn arrays convert correctly\n    * def actual = ({ a: [1, 2, 3] })\n    * match actual == { a: [1, 2, 3] }\n\nScenario: karate forEach operation on lists\n    * def res = []\n    * def fun = function(x){ karate.appendTo(res, x * x) }\n    * def list = [1, 2, 3]\n    * karate.forEach(list, fun)\n    * match res == [1, 4, 9]\n\nScenario: karate map operation\n    * def fun = function(x){ return x * x }\n    * def list = [1, 2, 3]\n    * def res = karate.map(list, fun)\n    * match res == [1, 4, 9]\n\nScenario: convert an array into a different shape\n    * def before = [{ foo: 1 }, { foo: 2 }, { foo: 3 }]\n    * def fun = function(x){ return { bar: x.foo } }\n    * def after = karate.map(before, fun)\n    * match after == [{ bar: 1 }, { bar: 2 }, { bar: 3 }]\n\nScenario: karate filter operation\n    * def fun = function(x){ return x % 2 == 0 }\n    * def list = [1, 2, 3, 4]\n    * def res = karate.filter(list, fun)\n    * match res == [2, 4]\n\nScenario: karate forEach operation on maps\n    * def keys = []\n    * def vals = []\n    * def idxs = []\n    * def fun = \n    \"\"\"\n    function(x, y, i) { \n      karate.appendTo(keys, x); \n      karate.appendTo(vals, y); \n      karate.appendTo(idxs, i); \n    }\n    \"\"\"\n    * def map = { a: 2, b: 4, c: 6 }\n    * karate.forEach(map, fun)\n    * match keys == ['a', 'b', 'c']\n    * match vals == [2, 4, 6]\n    * match idxs == [0, 1, 2]\n\nScenario: karate filter operation, using array indexes\n    * def fun = function(x, i){ return i % 2 == 0 }\n    * def list = [1, 2, 3, 4]\n    * def res = karate.filter(list, fun)\n    * match res == [1, 3]\n\nScenario: karate find index of first match (primitive)\n    * def list = [1, 2, 3, 4]\n    * def searchFor = 3\n    * def foundAt = []\n    * def fun = function(x, i){ if (x == searchFor) karate.appendTo(foundAt, i) }\n    * karate.forEach(list, fun)\n    * match foundAt == [2]\n\nScenario: karate find index of first match (complex)\n    * def list = [{ a: 1, b: 'x'}, { a: 2, b: 'y'}, { a: 3, b: 'z'}]\n    * def searchFor = { a: 2, b: '#string'}\n    * def foundAt = []\n    * def fun = function(x, i){ if (karate.match(x, searchFor).pass) karate.appendTo(foundAt, i) }\n    * karate.forEach(list, fun)\n    * match foundAt == [1]\n\nScenario: map with key - for the common case of converting arrays of primitives into arrays of objects\n    * def list = [ 'Bob', 'Wild', 'Nyan' ]\n    * def data = karate.mapWithKey(list, 'name')\n    * match data == [{ name: 'Bob' }, { name: 'Wild' }, { name: 'Nyan' }]\n\n    * def list = [ 1, 2, 3]\n    * def data = karate.mapWithKey(list, 'val')\n    * match data == [{ val: 1 }, { val: 2 }, { val: 3 }]\n\n    # will actually work for any kind of list\n    * def list = [{ a: 1 }, { b: 2 }]\n    * def data = karate.mapWithKey(list, 'foo')\n    * match data == [{ foo: { a: 1 } }, { foo: { b: 2 } }]\n\n    # should work for null edge case\n    * def list = null\n    * def data = karate.mapWithKey(list, 'foo')\n    * match data == []\n\nScenario: filterKeys\n    * def schema = { a: '#string', b: '#number', c: '#boolean' }\n    * def response = { a: 'x', c: true }\n    # very useful for validating a response against a schema \"super-set\"\n    * match response == karate.filterKeys(schema, response)\n    * match karate.filterKeys(response, 'b', 'c') == { c: true }\n    * match karate.filterKeys(response, ['a', 'b']) == { a: 'x' }\n\nScenario: merge\n    * def foo = { a: 1 }\n    * def bar = karate.merge(foo, { b: 2 })\n    * match bar == { a: 1, b: 2 }\n\nScenario: append\n    * def foo = [{ a: 1 }]\n    * def bar = karate.append(foo, { b: 2 })\n    * match bar == [{ a: 1 }, { b: 2 }]\n    * def foo = { a: 1 }\n    * def bar = karate.append(foo, { b: 2})\n    * match bar == [{ a: 1 }, { b: 2 }]\n    # ensure this works even within js\n    * def fun = function(){ var x = [1, 2]; return karate.append(x, 3, 4) }\n    * match fun() == [1, 2, 3, 4]\n\nScenario: sort\n    * def foo = [{a: { b: 3 }}, {a: { b: 1 }}, {a: { b: 2 }}]\n    * def fun = function(x){ return x.a.b }\n    * def bar = karate.sort(foo, fun)\n    * match bar == [{a: { b: 1 }}, {a: { b: 2 }}, {a: { b: 3 }}]\n    * match bar.reverse() == [{a: { b: 3 }}, {a: { b: 2 }}, {a: { b: 1 }}]\n\nScenario: get last array element (js)\n    * def list = [1, 2, 3, 4]\n    * def last = list[list.length-1]\n    * match last == 4\n\nScenario: get last array element (json-path)\n    * def list = [1, 2, 3, 4]\n    * def last = get[0] list[-1:]\n    * match last == 4\n\nScenario: advanced json-path that the jayway implementation has limitations with\n    * def response = read('js-arrays-products.json')\n    * def result = $[?(@.partIDs[?(@.id == 1)])]\n    # should be 2\n    * match result == '#[3]'\n\nScenario: work around for the above\n    * def hasId = \n        \"\"\"\n        function(product, id) {\n            return karate.jsonPath(product, '$.partIDs[?(@.id==' + id + ')]').length;\n        }\n        \"\"\"\n    * def products = read('js-arrays-products.json')\n    * def result = []\n    * karate.repeat(products.length, function(i){ if (hasId(products[i], 1)) result.push(products[i]) })\n    * match result[*].name == ['Wotsit v1.5', 'Wotsit v2.5']\n\nScenario: work around but using karate.filter\n    * def id = 1\n    * def hasId = function(x){ return karate.jsonPath(x, '$.partIDs[?(@.id==' + id + ')]').length != 0 }\n    * def products = read('js-arrays-products.json')\n    * def result = karate.filter(products, hasId)\n    * match result[*].name == ['Wotsit v1.5', 'Wotsit v2.5']\n\nScenario: table to json with expressions evaluated\n    * def one = 'hello'\n    * def two = { baz: 'world' }\n    * table json\n        | foo     | bar |\n        | one     | 1   |\n        | two.baz | 2   |\n    * match json == [{ foo: 'hello', bar: 1 }, { foo: 'world', bar: 2 }]\n\nScenario: table to json with expressions and empty / nulls\n    * def one = { baz: null }\n    * table json\n        | foo     | bar    |\n        | 'hello' |        |\n        | one.baz | (null) |\n        | 'world' | null   |\n    * match json == [{ foo: 'hello' }, { bar: null }, { foo: 'world' }]\n\nScenario: table to json with nested json\n    * def one = 'hello'\n    * def two = { baz: 'world' }\n    * table json\n        | foo     | bar            |\n        | one     | { baz: 1 }     |\n        | two.baz | ['baz', 'ban'] |\n        | true    | one == 'hello' |\n    * print json\n    * match json == [{ foo: 'hello', bar: { baz: 1 } }, { foo: 'world', bar: ['baz', 'ban'] }, { foo: true, bar: true }]\n\nScenario: json path with keys with spaces or other troublesome characters\n    * def json = { 'sp ace': 'foo', 'hy-phen': 'bar', 'full.stop': 'baz' }\n    * string jsonString = json\n    * match jsonString == '{\"sp ace\":\"foo\",\"hy-phen\":\"bar\",\"full.stop\":\"baz\"}'\n    # get comes to the rescue because spaces are a problem on the LHS\n    * def val1 = get json $['sp ace']\n    * match val1 == 'foo'\n    * match json['hy-phen'] == 'bar'\n    * match json['full.stop'] == 'baz'\n\nScenario: pass json var (becomes a map) to a function\n    * def json = { foo: 'bar', hello: 'world' }\n    * def fun = function(o){ return o }\n    * def result = call fun json\n    * match result == json\n\nScenario: remove json key using keyword\n    * def json = { foo: 'bar', hello: 'world' }\n    * remove json $.foo\n    * match json == { hello: 'world' }\n\nScenario: remove json key from js\n    * def json = { foo: 'bar', hello: 'world' }\n    * def fun = function(){ karate.remove('json', 'foo') }\n    * call fun\n    * match json == { hello: 'world' }\n\nScenario: remove json value\n    * def data = { a: 'hello', b: null, c: null }\n    * def json = { foo: '#(data.a)', bar: '#(data.b)', baz: '##(data.c)' }\n    * match json == { foo: 'hello', bar: null }\n\nScenario: optional json values\n    * def response = [{a: 'one', b: 'two'}, { a: 'one' }]\n    * match each response contains { a: 'one', b: '##string' }\n\nScenario: #null, ##null, #present and #notpresent\n    * def foo = { }\n    * match foo != { a: '#present' }\n    * match foo == { a: '#notpresent' }\n    * match foo == { a: '#ignore' }\n    * match foo == { a: '##null' }\n    * match foo != { a: '#null' }\n    * match foo != { a: '#notnull' }\n    * match foo == { a: '##notnull' }\n    * match foo != { a: null }\n\n    * def foo = { a: null }\n    * match foo == { a: null }\n    * match foo == { a: '#null' }    \n    * match foo == { a: '##null' }\n    * match foo != { a: '#notnull' }\n    # optional comparison so match evalutes to true\n    * match foo == { a: '##notnull' }\n    * match foo == { a: '#present' }\n    * match foo == { a: '#ignore' }\n    * match foo != { a: '#notpresent' }\n\n    * def foo = { a: 1 }\n    * match foo == { a: 1 }\n    * match foo == { a: '#number' }\n    * match foo == { a: '#notnull' }\n    * match foo == { a: '##notnull' }\n    * match foo != { a: '#null' }    \n    * match foo != { a: '##null' }\n    * match foo == { a: '#present' }\n    * match foo == { a: '#ignore' }\n    * match foo != { a: '#notpresent' }\n\nScenario: alternative notpresent check using json-path\n    * def foo = { a: 1 }\n    * match foo.a == '#present'\n    * match foo.nope == '#notpresent'\n\nScenario: regression test for edge case in fuzzy matches\n    * def answer = { foo: 'foo', bar: 'bar', baz: 'baz' }\n    * match answer != { foo: '#string', foobar: '#notpresent', foobaz: '#notpresent' }\n    * match answer != { foo: '#string', foobar: '##string', foobaz: '##string' }\n\nScenario: get and json path\n    * def foo = { bar: { baz: 'ban' } }\n    * def res = get foo $..bar[?(@.baz)]\n    * match res contains { baz: 'ban' }\n\nScenario: comparing 2 payloads\n    * def foo = { hello: 'world', baz: 'ban' }\n    * def bar = { baz: 'ban', hello: 'world' }\n    * match foo == bar\n\nScenario: [contains deep] will recurse nested json\n    * def original = { a: 1, b: 2, c: 3, d: { a: 1, b: 2 } }\n    * def expected = { a: 1, c: 3, d: { b: 2 } }\n    * match original !contains expected\n    * match original contains deep expected\n\nScenario: [contains deep] will recurse nested array\n    * def original = { a: 1, arr: [ { b: 2, c: 3 }, { b: 3, c: 4 } ] }\n    * def expected = { a: 1, arr: [ { b: 2 }, { c: 4 } ] }\n    * match original !contains expected    \n    * match original contains deep expected\n\nScenario: [contains deep] should not recurse in reverse\n    * def original = { \"a\": { \"b\": { \"c\": { \"d\":1, \"e\":2 } } } }\n    * def compared = { \"a\": { \"b\": { \"c\": { \"d\":1, \"e\":2, \"f\":3 } } } }\n    * match original !contains compared\n    * match compared !contains original\n    # * match original !contains deep compared\n    * match compared contains deep original\n\nScenario: contains deep should support multi-line / docstring r.h.s\n    * def message =\n      \"\"\"\n      {\n          order_id: 5,\n          products: [\n            { product_id: 100, name: \"bicycle\" },\n            { product_id: 101, name: \"car\" }\n          ]\n      }\n      \"\"\"\n    * match message contains deep\n      \"\"\"\n      {\n          order_id: 5,\n          products: [\n            { product_id: 101, name: \"car\" },\n            { product_id: 100, name: \"bicycle\" }\n          ]\n      }\n      \"\"\"\n\nScenario: js eval\n    * def temperature = { celsius: 100, fahrenheit: 212 }\n    * string expression = 'temperature.celsius'\n    * def celsius = karate.eval(expression)\n    * assert celsius == 100\n    * string expression = 'temperature.celsius * 1.8 + 32'\n    * match temperature.fahrenheit == karate.eval(expression)\n\nScenario: js match is strict for data types\n    * def foo = { a: '5', b: 5, c: true, d: 'true' }\n    * match foo !contains { a: 5 }\n    * match foo !contains { b: '5' }\n    * match foo !contains { c: 'true' }\n    * match foo !contains { d: true }\n    * match foo == { a: '5', b: 5, c: true, d: 'true' }\n\nScenario: json path in expressions\n    * table data\n        | a | b   |\n        | 1 | 'x' |\n        | 2 | 'y' |\n    * def foo = [{a: 1, b: 'x'}, {a: 2, b: 'y'}]\n    * match data == foo\n    * match foo == data\n    * match foo[*].a == [1, 2]\n    # the $ prefix indicates a path expression on a variable, it behaves like 'get'\n    * match foo[*].a == $data[*].a\n    * match foo[*].b == $data[*].b\n\nScenario: get combined with array index\n    * def foo = [{a: 1, b: 'x'}, {a: 2, b: 'y'}]\n    * def first = get[0] foo[*].a\n    * match first == 1\n    * match first == get[0] foo[*].a\n\nScenario: set via table\n    * def cat = { name: '' }\n    * set cat\n    | path   | value |\n    | name   | 'Bob' |\n    | age    | 5     |\n    * match cat == { name: 'Bob', age: 5 }\n\nScenario: set nested via table\n    * def cat = { name: 'Wild', kitten: null }\n    * set cat $.kitten\n    | path   | value |\n    | name   | 'Bob' |\n    | age    | 5     |\n    * match cat == { name: 'Wild', kitten: { name: 'Bob', age: 5 } }\n\nScenario: set variable plus path via table\n    * def cat = { name: 'Wild', kitten: null }\n    * set cat.kitten\n    | path   | value |\n    | name   | 'Bob' |\n    | age    | 5     |\n    * match cat == { name: 'Wild', kitten: { name: 'Bob', age: 5 } }\n                        \nScenario: set via table where variable does not exist\n    note how karate will create parent paths if needed\n\n    * set foo\n    | path | value      |\n    | bar  | 'baz'      |\n    | a.b  | 'c'        |\n    | fizz | { d: 'e' } |\n    * match foo == { bar: 'baz', a: { b: 'c' }, fizz: { d: 'e' } }\n\nScenario: set via table with fancy array paths and multi-dimensional arrays\n    * set foo\n    | path   | value   |\n    | bar[0] | 'baz'   |\n    | a[0].b | 'ban'   |\n    | c[0]   | [1, 2]  | \n    | c[1]   | [3, 4]  |   \n    * match foo == { bar: [ 'baz'], a: [{ b: 'ban' }], c: [[1, 2], [3, 4]] }\n\nScenario: set via table, complex paths\n    * set expected\n    | path            | value   |\n    | first           | 'hello' |\n    | client.id       | 'goodbye'            |\n    | client.foo.bar  | 'world' |\n    * match expected == { first: 'hello', client: { id: 'goodbye', foo: { bar: 'world' }}}\n\nScenario: set array via table where variable does not exist\n    * set foo\n    | path | 0     |\n    | bar  | 'baz' |\n    * match foo == [{ bar: 'baz' }]\n\nScenario: set array via table, multiple items, var does not exist\n    * set foo\n    | path | 0     | 1     |\n    | bar  | 'baz' | 'ban' |\n    * match foo == [{ bar: 'baz' }, { bar: 'ban' }]\n\nScenario: set array via table, var exists, indexes specified\n    the column headings are used as indexes\n\n    * def foo = [{ bar: 'a' }, { bar: 'b' }, { bar: 'c' }, { bar: 'd' }]\n    * set foo\n    | path | 3     | 1     |\n    | bar  | 'baz' | 'ban' |\n    * match foo == [{ bar: 'a' }, { bar: 'ban' }, { bar: 'c' }, { bar: 'baz' }]\n\nScenario: set array via table, var does not exist, no array indexes\n    if the column headings are not integers, karate uses the column position\n    but column headings have to be unique, they can be used to describe the column effectively, but are otherwise useless\n\n    * set foo\n    | path | one   | two   |\n    | bar  | 'baz' | 'ban' |\n    * match foo == [{ bar: 'baz' }, { bar: 'ban' }]\n\nScenario: set via table, var does not exist, different nesting options\n    * set first\n    | path | value          |\n    | one  | { bar: 'baz' } |\n    | two  | { bar: 'ban' } |\n    * match first == { one: { bar: 'baz' }, two: { bar: 'ban' } }\n\n    * set second\n    | path     | value |\n    | one.bar  | 'baz' |\n    | two.bar  | 'ban' |\n    * match second == first\n\nScenario: set via table, repeated paths at the top\n    * set foo.bar\n    | path   | value |\n    | one    | 1     |\n    | two[0] | 2     |\n    | two[1] | 3     |\n\n    * match foo == { bar: { one: 1, two: [2, 3] } }\n\nScenario Outline: examples and optional json keys (see outline.feature for a better version)\n    * def search = { name: { first: \"##(<first>)\", last: \"##(<last>)\" }, age: \"##(<age>)\" }\n    * match search == <expected>\n\n    Examples:\n    | first  | last    | age  | expected                                            |\n    | 'John' | 'Smith' | 20   | { name: { first: 'John', last: 'Smith' }, age: 20 } |\n    | 'Jane' | 'Doe'   | null | { name: { first: 'Jane', last: 'Doe' } }            |\n    | null   | 'Waldo' | null | { name: { last: 'Waldo' } }                         |\n\nScenario: set via table, since blanks are skipped, this is cleaner than the above\n    approach of using a scenario outline and examples\n\n    * set search\n        | path       | 0        | 1      | 2       |\n        | name.first | 'John'   | 'Jane' |         |\n        | name.last  | 'Smith'  | 'Doe'  | 'Waldo' |\n        | age        | 20       |        |         |\n\n    * match search[0] == { name: { first: 'John', last: 'Smith' }, age: 20 }\n    * match search[1] == { name: { first: 'Jane', last: 'Doe' } }\n    * match search[2] == { name: { last: 'Waldo' } } \n\nScenario: expressions are allowed, and the default behavior is to skip anything that evaluates to null.\n    this can be over-ridden by simply enclosing the expression in parentheses\n\n    * table data\n        | first  | last    | age  |\n        | 'John' | 'Smith' | 20   |\n        | 'Jane' | 'Doe'   |      |\n        |        | 'Waldo' |      |\n\n    # for column 2, note how the null is retained for name.first\n    * set search\n        | path       | 0             | 1             | 2               |\n        | name.first | data[0].first | data[1].first | (data[2].first) |\n        | name.last  | data[0].last  | data[1].last  | data[2].last    |\n        | age        | data[0].age   | data[1].age   | data[2].age     |\n\n    * match search[0] == { name: { first: 'John', last: 'Smith' }, age: 20 }\n    * match search[1] == { name: { first: 'Jane', last: 'Doe' } }\n    * match search[2] == { name: { first: null, last: 'Waldo' } } \n\nScenario: just to be clear about how to set a null if really needed in the resulting json\n    * set foo\n        | path       | value  |\n        | name.first | null   |\n        | name.last  | (null) |\n        | age        |        |\n    \n    * match foo == { name: { last: null } }\n\nScenario: contains / not contains\n    * def some = [1, 2]\n    * def actual = [1, 2, 3]\n    * def none = [4, 5]\n    * match actual contains some\n    * match actual == '#(^some)'\n    * match actual !contains none\n    * match actual == '#(!^none)'\n\nScenario: match in js\n    * def foo = { hello: 'world' }\n    * def res = karate.match(foo, { hello: '#string' } )\n    * match res == { pass: true, message: null }\n    * def res = karate.match(foo, { hello: '#number' } )\n    * match res == { pass: false, message: '#notnull' }\n\nScenario: advanced match in js\n    * def foo = { a: 1, b: 'foo' }\n    * def res = karate.match(\"foo contains { a: '#number' }\")\n    * match res == { pass: true, message: null }\n    * def res = karate.match(\"foo == { a: '#number' }\")\n    * match res == { pass: false, message: '#notnull' }\n    * def foo = [1, 2, 3]\n    * def res = karate.match(\"each foo == '#number'\")\n    * match res == { pass: true, message: null }\n\nScenario: karate.os\n    * def temp = karate.os\n    * print 'os:', temp\n    * match temp == { type: '#string', name: '#string' }\n\nScenario: using the js includes api\n    * def allowed = ['Music', 'Entertainment', 'Documentaries', 'Family']\n    * def actual = ['Entertainment', 'Family']\n    * match each actual == '#? allowed.includes(_)'\n\nScenario: using the java indexOf api (will change with graal)\n    * def response = [{ name: 'a' }, { name: 'b' }, { name: 'c' }]\n    * def names = $[*].name\n    * def index = names.indexOf('b')\n    * match index == 1\n\nScenario: karate.forEach() and js arguments\n    * def vals = []\n    * def fun = function(){ karate.forEach(arguments, function(x){ vals.push(x) }) }\n    * fun('a', 'b', 'c')\n    * match vals == ['a', 'b', 'c']\n\nScenario: lists - karate.sizeOf() keysOf() valuesOf() appendTo()\n    * def foo = [1, 2, 3]\n    * match karate.sizeOf(foo) == 3\n    * match karate.valuesOf(foo) == [1, 2, 3]\n    * def bar = karate.appendTo(foo, 4)\n    * match foo == [1, 2, 3, 4]\n    * match bar == [1, 2, 3, 4]\n    * def bar = karate.appendTo(foo, [5, 6])\n    * match foo == [1, 2, 3, 4, 5, 6]\n    * match bar == [1, 2, 3, 4, 5, 6]\n    * def fun = function(){ var x = [1, 2]; return karate.appendTo(x, 3, 4) }\n    * match fun() == [1, 2, 3, 4]\n\nScenario: maps - karate.sizeOf() keysOf() valuesOf() appendTo()\n    * def foo = { a: 1, b: 2, c: 3 }\n    * match karate.sizeOf(foo) == 3\n    * match karate.keysOf(foo) == ['a', 'b', 'c']\n\nScenario: map and repeat should not mangle js arrays\n* def foo = [1, 2]\n* def fun = function(x){ return { x: x, bar: [1, 2] } }\n* def res = karate.map(foo, fun)\n* match res == [{ x: 1, bar: [1, 2]}, { x: 2, bar: [1, 2] }]\n\n* def fun = function(i){ return { foo: [1, 2]} }\n* def bar = karate.repeat(2, fun)\n* match bar == [{ foo: [1, 2] }, { foo: [1, 2] }]\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/js-map-repeat.feature",
    "content": "Feature:\n\nScenario: map and repeat should not mangle js arrays\n* def foo = [1, 2]\n* def fun = function(x){ return { x: x, bar: [1, 2] } }\n* def res = karate.map(foo, fun)\n* match res == [{ x: 1, bar: [1, 2]}, { x: 2, bar: [1, 2] }]\n\n* def fun = function(i){ return { foo: [1, 2]} }\n* def bar = karate.repeat(2, fun)\n* match bar == [{ foo: [1, 2] }, { foo: [1, 2] }]\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall/JsCallTest.java",
    "content": "package com.intuit.karate.core.jscall;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class JsCallTest {\n\n    @Test\n    public void testParallel() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/jscall/js-call.feature\")\n                .configDir(\"classpath:com/intuit/karate/core/jscall\")\n                .parallel(5);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall/JsCallonceTest.java",
    "content": "package com.intuit.karate.core.jscall;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class JsCallonceTest {\n\n    @Test\n    public void testParallel() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/jscall/js-callonce.feature\")\n                .configDir(\"classpath:com/intuit/karate/core/jscall\")\n                .parallel(5);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall/dummy.feature",
    "content": "Feature:\r\n\r\nScenario:\r\n* def success = true\r\n\r\n    "
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall/js-call.feature",
    "content": "Feature:\n\nScenario:\n* def result = karate.call('classpath:com/intuit/karate/core/jscall/dummy.feature')\n* utils.sayHello()\n* utils.reuseExistingFunction()\n* karate.call('js-called.feature')"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall/js-called.feature",
    "content": "@ignore\nFeature:\n\nScenario: called scenario\n  * utils.sayHello()\n  * karate.call('dummy.feature')\n  * utils.sayHello()\n  * karate.call(true, 'dummy.feature')\n  * utils.sayHello()\n  * call read('dummy.feature')\n  * utils.sayHello()\n  * utils.reuseExistingFunction()"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall/js-callonce.feature",
    "content": "Feature:\n\nBackground:\n* callonce read('classpath:com/intuit/karate/core/jscall/dummy.feature')\n\nScenario:\n* utils.sayHello()\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall/karate-config.js",
    "content": "function fn() {\n  var config = {};\n  config.utils = karate.call('classpath:com/intuit/karate/core/jscall/utils.feature');\n  return config;\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall/utils.feature",
    "content": "Feature:\n\nScenario:\n* def sayHello = function() { return 'hello world!' }\n* def temp =\n  \"\"\"\n  function() {\n    return sayHello();\n  }\n  \"\"\"\n* def reuseExistingFunction = karate.wrapFunction(temp)\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall2/JsCall2Test.java",
    "content": "package com.intuit.karate.core.jscall2;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.MockHandler;\nimport com.intuit.karate.http.HttpServer;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\npublic class JsCall2Test {\n    \n    static HttpServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        MockHandler mock = new MockHandler(Feature.read(\"classpath:com/intuit/karate/core/jscall2/mock.feature\"));\n        server = HttpServer.handler(mock).build();\n    }    \n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/jscall2\")\n                .configDir(\"classpath:com/intuit/karate/core/jscall2\")\n                .systemProperty(\"server.port\", server.getPort() + \"\")\n                .parallel(5);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall2/all.feature",
    "content": "Feature: responseStatus variable tests\n\n  Scenario: config js test\n    * url serverUrl\n    * method get\n    * status 200\n    * print 'responseStatus: ' + responseStatus\n    * assert isResponseStatus200_config()\n\n  Scenario: call test\n    * url serverUrl\n    * method get\n    * status 200\n    * print 'responseStatus: ' + responseStatus\n    * assert isResponseStatus200_call()"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall2/call-once.feature",
    "content": "#@ignore\nFeature:: common setup\n\n  Scenario: common setup\n\n    * def isResponseStatus200_callOnce =\n    \"\"\"\n    function() {\n      if( responseStatus != 200){\n        karate.log(\"Retry since expectedStatus 200 != actual responseStatus: \" + responseStatus);\n        return false;\n      }\n      return true;\n    }\n    \"\"\""
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall2/call-single.feature",
    "content": "Feature:: common setup\n\n  Scenario: common setup\n\n    * def isResponseStatus200_callSingle =\n    \"\"\"\n    function() {\n      if( responseStatus != 200){\n        karate.log(\"Retry since expectedStatus 200 != actual responseStatus: \" + responseStatus);\n        return false;\n      }\n      return true;\n    }\n    \"\"\""
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall2/call.feature",
    "content": "#@ignore\nFeature:: common setup\n\n  Scenario: common setup\n\n    * def isResponseStatus200_call =\n    \"\"\"\n    function() {\n      if (responseStatus != 200) {\n        karate.log(\"Retry since expectedStatus 200 != actual responseStatus: \" + responseStatus);\n        return false;\n      }\n      return true;\n    }\n    \"\"\""
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall2/karate-config.js",
    "content": "function fn() {\n    let config = {\n      serverUrl: 'http://localhost:' + karate.properties['server.port']\n    };\n    config = karate.callSingle('classpath:com/intuit/karate/core/jscall2/call-single.feature', config);\n    config = karate.call('classpath:com/intuit/karate/core/jscall2/call.feature', config);\n    config.isResponseStatus200_config = function () {\n        if (responseStatus != 200) {\n            karate.log(\"retry since expectedStatus 200 != actual responseStatus: \" + responseStatus);\n            return false;\n        }\n        return true;\n    }\n    return config;\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall2/local.feature",
    "content": "#This example is in its own file since it prevents all tests from reading responseStatus\nFeature: responseStatus callOnce test\n\n  Background:\n\n    * def isResponseStatus200 =\n    \"\"\"\n    function() {\n      if( responseStatus != 200){\n        karate.log(\"Retry since expectedStatus 200 != actual responseStatus: \" + responseStatus);\n        return false;\n      }\n      return true;\n    }\n    \"\"\"\n\n    # if comment out callOnce the local js test will pass\n\n    * callonce read('classpath:com/intuit/karate/core/jscall2/call-once.feature')\n\n\n\n\n  #########################\n  #####    failing    #####\n  #########################\n\n  Scenario: callOnce test\n\n    * url serverUrl\n    * method get\n    * status 200\n\n    * print 'responseStatus: ' + responseStatus\n    * assert isResponseStatus200_callOnce()\n\n\n  #fails from callOnce\n  Scenario: local js test\n\n    * url serverUrl\n    * method get\n    * status 200\n\n    * print 'responseStatus: ' + responseStatus\n    * assert isResponseStatus200()"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jscall2/mock.feature",
    "content": "Feature:\n\nScenario:\n* def response = { success: true }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-2.feature",
    "content": "Feature:\n\n@setup\nScenario:\n    * def data = [{ name: 'one' }, { name: 'two' }]\n\nScenario Outline:\n    * match name == \"#present\"\n\nExamples:\n    | karate.setup().data |\n\nScenario Outline:\n    * match name == \"#present\"\n    * def params = { 'foo': 'bar' }\n    * call read('js-read-called-2.feature') params\n\nExamples:\n    | karate.setup().data |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-2.json",
    "content": "{\"errorvar\":[{\"id\":1},{\"id\":2}]}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-3.feature",
    "content": "Feature:\n\n@setup\nScenario:\n    * def x = read('js-read-3.json')\n    * def data = x.thirderror\n\nScenario Outline:\n    * match id == '#number'\n\nExamples:\n    | karate.setup().data |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-3.json",
    "content": "{\"thirderror\":[{\"id\":1},{\"id\":2}]}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-4.feature",
    "content": "Feature:\n\nBackground:\n\nScenario:\n    * def params = { 'foo': 'bar' }\n    * call read('js-read-called-2.feature') params\n\nScenario:\n    * def params = { 'foo': 'bar' }\n    * call read('js-read-called-3.feature') params\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-4.json",
    "content": "{\"fourtherror\":[{\"id\":1},{\"id\":2}]}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-5.json",
    "content": "{\"fiftherror\":[{\"id\":1},{\"id\":2}]}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-called-2.feature",
    "content": "Feature:\n\nBackground:\n    * call read('../utils-reuse-common.feature')\n\nScenario:\n    * match __arg == { foo: 'bar' }"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-called-3.feature",
    "content": "Feature:\n\n#see https://github.com/karatelabs/karate/pull/1436\nBackground:\n    * def called = call read('../utils-reuse-common.feature')\n\nScenario:\n    * print 'arg: ' + __arg\n    * match __arg == \"#present\"\n    * match __arg == \"#notnull\"\n    * match __arg == { 'foo': 'bar' }"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-called.feature",
    "content": "Feature:\n\n@name=checkUsageOfFunc\nScenario: trigger call fun from parent read feature\n    * def val = call fun\n    * match val == 2\n\n@name=checkReadingJsonKeys\nScenario: check json data from parent read feature\n    #* print error\n    * match error[0].id == 1\n\n@name=checkReadingSecondJsonKeys\nScenario: check json data from parent read feature\n    #* print error\n    * match errorvar[0].id == 1\n\n@name=checkReadingThirdJsonKeys\nScenario: check json data from parent read feature\n    * match thirderror[0].id == 1\n\n@name=checkReadingFourthJsonKeys\nScenario: check json data from parent read feature\n    * match fourtherror[0].id == 1\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-reuse-2.feature",
    "content": "Feature:\n\nBackground:\n    * call read 'js-read-5.json'\n    * def storedVar2 = call read 'js-read-5.json'\n\nScenario:\n    * print 'step in Scenario to trigger background steps'\n    #* karate.set('storedVar', storedVar)\n    #* karate.set('storedVar', `call read 'js-read-3.json'`)\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-reuse-only-background.feature",
    "content": "Feature:\n\nBackground:\n    * call read 'js-read-4.json'\n    * def storedVarBackground = call read 'js-read-4.json'\n\nScenario:\n    * print 'test'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read-reuse.feature",
    "content": "Feature:\n\nBackground:\n    * call read 'js-read-3.json'\n    * def storedVar = call read 'js-read-3.json'\n\nScenario:\n    * print 'step in Scenario to trigger background steps'\n    #* karate.set('storedVar', storedVar)\n    #* karate.set('storedVar', `call read 'js-read-3.json'`)\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read.feature",
    "content": "Feature:\n\nBackground:\n    * call read 'js-read-2.json'\n    * def varInContext = callonce read('this:js-read-reuse.feature')\n    * call read('this:js-read-reuse.feature')\n    * call read 'this:js-read-reuse-2.feature'\n    * def varInBackgroundContext = callonce read('this:js-read-reuse-only-background.feature')\n    * call read('this:js-read-reuse-only-background.feature')\n\nScenario: reading json 1\n    * def fun = function(){ var temp = read('js-read.json'); return temp.error[1].id }\n    * def val = call fun\n    * match val == 2\n\nScenario: reading json 2\n    * def fun = function(){ var temp = karate.read('js-read.json'); return temp.error[1].id }\n    * def val = call fun\n    * match val == 2\n\nScenario: reading json, calling feature and reusing json\n    * def fun = function(){ var temp = karate.read('js-read.json'); return temp.error[1].id }\n    * def result = call read('js-read-called.feature@name=checkUsageOfFunc')\n\nScenario: reading json, calling feature and reusing json\n    * call read 'js-read.json'\n    * def result = call read('js-read-called.feature@name=checkReadingJsonKeys')\n\nScenario: reading json (with callonce), calling feature and reusing json\n    * callonce read 'js-read.json'\n    * def result = call read('js-read-called.feature@name=checkReadingJsonKeys')\n\nScenario: calling feature and reusing json read in background section\n    * def result = call read('js-read-called.feature@name=checkReadingSecondJsonKeys')\n\nScenario: calling feature that will read a json file in its background (no scenario defined), then call a feature that uses data from that json\n    * print storedVarBackground\n    * match varInBackgroundContext.storedVarBackground.fourtherror[0].id == 1\n    * match varInBackgroundContext.fourtherror[0].id == 1\n    * def result = call read('js-read-called.feature@name=checkReadingFourthJsonKeys')\n\n\nScenario: calling feature that will read a json (with space after the read keyword) file and then call a feature that uses data from that json\n# note that when reading the reusable feature like this: * call read 'this:js-read-reuse-2.feature'\n# the scope of that feature file is not available in the caller\n    * def matchVar2 = karate.get('storedVar2')\n    * match matchVar2 == null\n    * def matchFifthError = karate.get('fiftherror')\n    * match matchFifthError == null\n\n\nScenario: calling feature that will read a json file and then call a feature that uses data from that json\n    * match varInContext.storedVar.thirderror[0].id == 1\n    * match varInContext.thirderror[0].id == 1\n    * match storedVar.thirderror[0].id == 1\n    * match thirderror[0].id == 1\n    * def result = call read('js-read-called.feature@name=checkReadingThirdJsonKeys')\n\nScenario Outline: using a scenario outline, call feature that will read a json file and then call a feature that uses data from that json\n    * match varInContext.storedVar.thirderror[0].id == <value>\n    * match varInContext.thirderror[0].id == <value>\n    * match storedVar.thirderror[0].id == <value>\n    * match thirderror[0].id == 1\n    * def result = call read('js-read-called.feature@name=checkReadingThirdJsonKeys')\n\nExamples:\n    | value |\n    | 1     |\n    | 1     |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/jsread/js-read.json",
    "content": "{\"error\":[{\"id\":1},{\"id\":2}]}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config-callonce.js",
    "content": "function fn() {\n  var config = karate.callonce('callonce-config-called.feature');\n  return config;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config-callsingle.js",
    "content": "function fn() {\n  var config = karate.callSingle('callonce-config-called.feature');\n  return config;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config-callsingletag.js",
    "content": "function fn() {\n  var config = karate.callSingle('call-single-tag-called.feature@callme');\n  return config;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config-csfail.js",
    "content": "function fn() {  \n  karate.callSingle('call-single-fail-called.feature');\n  return {};\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config-dev.js",
    "content": "// test comment\nfunction fn() {\n  return {\n    configSource: 'custom-env'\n  };\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config-fn.feature",
    "content": "Feature: feature reuse from karate-config.js\n\nScenario:\n* print 'before config feature'\n* call configUtils.hello\n* print 'after config feature'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config-frombase.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* match variableFromKarateBase == 'fromKarateBase'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config-frombase.js",
    "content": "function fn() {\n  return { variableFromKarateBase: functionFromKarateBase() };\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config-getscenario.feature",
    "content": "Feature:\n\nScenario: my name\n* match karate.scenario.name == 'my name'\n* match data.sectionIndex == 0\n* match data.exampleIndex == -1\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config-getscenario.js",
    "content": "function fn() {\n  var config = {};\n  config.data = karate.scenario;\n  return config;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config-utils.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* def hello = function(){ return { helloVar: 'hello world' } }\n* def existsFunction =\n  \"\"\"\n    function(element){\n        // solely for the purpose of testing the usage of a method\n        // from the Driver\n        return karate.get('driver').exists(element)\n    }\n  \"\"\"\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-config.js",
    "content": "function fn() {\n  var config = {\n    configSource: 'custom',\n    configUtilsJs: {\n      someText: 'hello world',\n      someFun: function () {\n        return 'hello world';\n      }\n    },\n    configUtils: karate.call('classpath:com/intuit/karate/core/karate-config-utils.feature')\n  };\n  return config;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-get-called.feature",
    "content": "Feature:\n\nScenario:\n* def foo = 'bar'\n* match foo == 'bar'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/karate-get.feature",
    "content": "Feature:\n\nScenario: shared scope\n* call read('karate-get-called.feature')\n* match foo == 'bar'\n\nScenario: isolated-scope\n* def result = call read('karate-get-called.feature')\n* match result.foo == 'bar'\n* match karate.get('foo') == null\n\nScenario: shared scope js\n* karate.call(true, 'karate-get-called.feature')\n* match foo == 'bar'\n\nScenario: isolated scope js\n* def result = karate.call('karate-get-called.feature')\n* match result.foo == 'bar'\n* match karate.get('foo') == null\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/lower-case.feature",
    "content": "Feature: lower case helper method\n\nScenario: json\n* def json = { FOO: 'BAR', Hello: 'World' }\n* def json = karate.lowerCase(json)\n* match json == { foo: 'bar', hello: 'world' }\n\nScenario: xml\n* def xml = <Foo><BAR NAME=\"Blah\">baZ</BAR></Foo>\n* xml xml = karate.lowerCase(xml)\n* match xml == <foo><bar name=\"blah\">baz</bar></foo>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/match-each-magic-variables.feature",
    "content": "Feature:\n\nScenario:\n\nGiven def temperature = { celsius: 100, fahrenheit: 212 }\nThen match temperature contains { fahrenheit: '#? _ == $.celsius * 1.8 + 32' }\n# using embedded expressions\nThen match temperature == { celsius: '#number', fahrenheit: '#($.celsius * 1.8 + 32)' }\n\nGiven def json =\n\"\"\"\n{\n  \"hotels\": [\n    { \"roomInformation\": [{ \"roomPrice\": 618.4 }], \"totalPrice\": 618.4  },\n    { \"roomInformation\": [{ \"roomPrice\": 679.79}], \"totalPrice\": 679.79 }\n  ]\n}\n\"\"\"\nThen match each json.hotels contains { totalPrice: '#? _ == _$.roomInformation[0].roomPrice' }\n# using embedded expressions\nThen match each json.hotels == { roomInformation: '#array', totalPrice: '#(_$.roomInformation[0].roomPrice)' }"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/match-step.feature",
    "content": "Feature: an edge case for com.intuit.karate.MatchStep\n\nScenario:\n* def response = { \"message\": \"A message with the word contains included\" }\n* match response == { \"message\": \"A message with the word contains included\" }\n* match response ==\n\"\"\"\n{ \"message\": \"A message with the word contains included\" }\n\"\"\"\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/MockRunner.java",
    "content": "package com.intuit.karate.core.mock;\n\nimport com.intuit.karate.core.MockServer;\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.http.HttpServer;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass MockRunner {\n\n    static final Logger logger = LoggerFactory.getLogger(MockRunner.class);\n\n    static HttpServer startMockServer() {\n        MockServer server = MockServer\n                .feature(\"classpath:com/intuit/karate/core/mock/_mock.feature\")\n                .http(0).build();\n        System.setProperty(\"karate.server.port\", server.getPort() + \"\");\n        return server;\n    }\n\n    @BeforeAll\n    static void beforeAll() {\n        startMockServer();\n    }\n\n    Results results;\n\n    private void run(String name) {\n        results = Runner.path(\"classpath:com/intuit/karate/core/mock/\" + name)\n                .configDir(\"classpath:com/intuit/karate/core/mock\")\n                .parallel(1);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n    @Test\n    void testBinary() {\n        run(\"binary.feature\");\n    }\n\n    @Test\n    void testBrotli() {\n        run(\"brotli.feature\");\n    }\n    \n    @Test\n    void testHtml() {\n        run(\"html.feature\");\n    } \n    \n    @Test\n    void testMalformed() {\n        run(\"malformed.feature\");\n    }     \n    \n    @Test\n    void testMultipart() {\n        run(\"upload.feature\");\n    }    \n\n    @Test\n    void testInvalidCookie() {\n    \trun(\"invalid-cookie.feature\");\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/MockSslTest.java",
    "content": "package com.intuit.karate.core.mock;\n\nimport com.intuit.karate.http.HttpServer;\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.MockServer;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass MockSslTest {\n\n    static final Logger logger = LoggerFactory.getLogger(MockSslTest.class);\n\n    static HttpServer startMockServer() {\n        MockServer server = MockServer.feature(\"classpath:com/intuit/karate/core/mock/_mock.feature\").https(0).build();\n        System.setProperty(\"karate.server.port\", server.getPort() + \"\");\n        return server;\n    }\n\n    @BeforeAll\n    static void beforeAll() {\n        startMockServer();\n    }\n\n    // @Test // TODO fails in jdk 17\n    void testMock() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/mock/hello-world.feature\")\n                .systemProperty(\"karate.ssl\", \"true\")\n                .configDir(\"classpath:com/intuit/karate/core/mock\")\n                .parallel(1);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/MockTest.java",
    "content": "package com.intuit.karate.core.mock;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.MockServer;\nimport com.intuit.karate.http.HttpServer;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass MockTest {\n\n    static final Logger logger = LoggerFactory.getLogger(MockTest.class);\n\n    static final AtomicInteger count = new AtomicInteger(0);\n\n    static HttpServer startMockServer() {\n        MockServer server = MockServer.featurePaths(\n                \"classpath:com/intuit/karate/core/mock/_simple.feature\",\n                \"classpath:com/intuit/karate/core/mock/_mock.feature\")\n                .pathPrefix(\"/\") // ensure cli default works\n                .interceptor((req, res, scenario) ->\n                        logger.debug(\"interceptor has been called %s times\"\n                            .formatted(count.incrementAndGet())))\n                .build();\n        System.setProperty(\"karate.server.port\", server.getPort() + \"\");\n        return server;\n    }\n\n    @BeforeAll\n    static void beforeAll() {\n        startMockServer();\n    }\n\n    @Test\n    void testMock() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/mock\")\n                .configDir(\"classpath:com/intuit/karate/core/mock\")\n                .parallel(1);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n        assertTrue(count.get() > 0);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/SimpleMockRunner.java",
    "content": "package com.intuit.karate.core.mock;\n\nimport com.intuit.karate.core.MockServer;\n\n/**\n *\n * @author pthomas3\n */\npublic class SimpleMockRunner {\n\n    public static void main(String[] args) {\n        MockServer server = MockServer\n                .feature(\"src/test/java/com/intuit/karate/core/mock/_simple.feature\")\n                .http(8080)\n                .watch(true)\n                .build();\n        server.waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/_mock.feature",
    "content": "@ignore\nFeature:\n\nBackground:\n* def uuid = function(){ return java.util.UUID.randomUUID() + '' }\n\nScenario: pathMatches('/v1/cats')\n    * def responseStatus = 201\n    * def response = { id: '#(uuid())', name: 'Billie' }\n\nScenario: pathMatches('/v1/cats/{uuid}')\n    * def response = { id: '#(uuid())', name: 'Billie' }\n\nScenario: pathMatches('/v1/dogs')\n    * def responseStatus = 201\n    * def response = { id: '#(uuid())', name: 'Dummy' }\n\nScenario: pathMatches('/v1/dogs/{uuid}')\n    * def response = { id: '#(uuid())', name: 'Dummy' }\n\nScenario: pathMatches('/v1/binary/download')\n    * def responseHeaders = { 'Content-Type': 'application/octet-stream' }\n    * def Utils = Java.type('com.intuit.karate.core.MockUtils')\n    * def response = Utils.testBytes\n\nScenario: pathMatches('/v1/binary/upload')\n    * def Utils = Java.type('com.intuit.karate.core.MockUtils')\n    * def success = java.util.Arrays.equals(Utils.testBytes, requestBytes)\n    * def response = ({ success: success })\n\nScenario: pathMatches('/v1/binary/brotli')\n    * def responseHeaders = { 'Content-Encoding': 'br', 'Content-Type': 'application/json' }\n    * def response = karate.readAsBytes('brotli.bin')\n\nScenario: pathMatches('/v1/patch')\n    * def responseStatus = 422\n    * def response = { success: true }\n\nScenario: pathMatches('/v1/delete')\n    * def response = { success: true }\n\nScenario: pathMatches('/v1/deleteEmptyResponse')\n    * def response = ''\n\nScenario: pathMatches('/v1/commas')\n    * def response = { success: true } \n    \nScenario: pathMatches('/v1/multiparams')\n    * def response = { success: true } \n\nScenario: pathMatches('/v1/german')\n    * def response = <name>Müller</name> \n\nScenario: pathMatches('/v1/encoding/{raw}')\n    * def response = { success: true }\n\nScenario: pathMatches('/v1/linefeed')\n    * def response = '\\n{ \"success\": true }'\n\nScenario: pathMatches('/v1/spaces')\n    * def response = '\\n    \\n'\n\nScenario: pathMatches('/v1/noheaders')    \n    * def responseStatus = 404\n\nScenario: pathMatches('/v1/cookies')    \n    * def responseHeaders = { 'Set-Cookie': 'foo=bar' }\n\nScenario: pathMatches('/v1/download')\n    * def response = read('test.pdf.zip')\n\nScenario: pathMatches('/v1/upload')\n    * def response = { size: '#(requestBytes.length)' }\n\nScenario: pathMatches('/v1/upload/excel')\n    * def filePart = requestParts['myFile'][0]\n    * def response = filePart\n\nScenario: pathMatches('/v1/multipart')\n    * def response = { success: true }\n\nScenario: pathMatches('/v1/multipart/json')\n    * json response = requestParams['message'][0]\n\nScenario: pathMatches('/v1/form')\n    # TODO urlencoded form handling on server side\n    * def response = { success: true }\n\nScenario: pathMatches('/v1/headers') && karate.get('requestHeaders.val[0]') == 'foo'\n    * def response = { val: 'foo' }\n\nScenario: pathMatches('/v1/headers') && headerContains('val', 'bar')\n    * def response = { val: 'bar' }\n\nScenario: pathMatches('/v1/malformed')\n    * def response = read('malformed.txt')\n\nScenario: pathMatches('/v1/jsonformed')\n    * def response = { hello: 'world' }\n\nScenario: pathMatches('/v1/xmlformed')\n    * def response = <hello>world</hello>\n\nScenario: pathMatches('/v1/stringformed')\n    * def response = 'hello world'\n\nScenario: pathMatches('/v1/html')\n    * text response =\n    \"\"\"\n    <!DOCTYPE html>\n    <html lang=\"en-US\">\n        <head>\n            <title>Hello world</title>\n            <!-- conforms to spec https://html.spec.whatwg.org/#unquoted -->\n            <script nonce=abc123 type=\"text/javascript\">\n            </script>\n        </head>\n        <body>\n         <h1>Hello</h1>\n        </body>\n    </html>\n    \"\"\"\n\nScenario: pathMatches('/v1/invalid-cookie')\n    * def responseHeaders = { 'Set-Cookie': 'detectedTimeZoneId=FLE Standard Time' }\n\nScenario: pathMatches('/v1/call-shared')\n    * def fromCaller = 'world'\n    * call read('called.feature') \n    * def response = message\n\nScenario: pathMatches('/v1/call-isolated')\n    * def fromCaller = 'world'\n    * def result = call read('called.feature') \n    * def response = result.message"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/_simple.feature",
    "content": "Feature:\n\nBackground:\n* def uuid = function(){ return java.util.UUID.randomUUID() + '' }\n\nScenario: pathMatches('/test')\n* def response = { success: true }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/binary.feature",
    "content": "Feature: testing binary response handling\n\nBackground:\n    * def Utils = Java.type('com.intuit.karate.core.MockUtils')\n\nScenario: get binary result and make sure it hasn't been corrupted\n    Given url mockServerUrl\n    And path 'binary', 'download'\n    When method get\n    Then status 200\n    Then match responseBytes == Utils.testBytes\n\nScenario: send binary content and make sure the server can see it\n    Given url mockServerUrl\n    And path 'binary', 'upload'\n    And request Utils.testBytes\n    When method post\n    Then status 200\n    And match response == { success: true }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/brotli.feature",
    "content": "Feature: brotli\n\nScenario: auto decode brotli compressed response\n    Given url mockServerUrl\n    And path 'binary', 'brotli'\n    When method get\n    Then match response == { hello: 'world' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/call.feature",
    "content": "Feature:\n\nBackground:\n* url mockServerUrl\n\n  Scenario:\n    * path 'call-shared'\n    * method get\n    * status 200\n    * match response == 'hello world'\n\n  Scenario:\n    * path 'call-isolated'\n    * method get\n    * status 200\n    * match response == 'hello world'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/called.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* def message = 'hello ' + fromCaller\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/cookies.feature",
    "content": "Feature: testing cookies from server\n\nBackground:\n* url mockServerUrl\n\nScenario: without request body\n    Given path 'cookies'\n    When method get\n    Then status 200\n    And match responseCookies.foo.value == 'bar'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/delete.feature",
    "content": "Feature: testing http delete method\n\nBackground:\n* url mockServerUrl\n\nScenario: without request body\n    Given path 'delete'\n    When method delete\n    Then status 200\n    And match response == { success: true }\n\nScenario: with request body\n    Given path 'delete'\n    And request { foo: 'bar' }\n    When method delete\n    Then status 200\n    And match response == { success: true }\n\nScenario: empty response body\n    Given path 'deleteEmptyResponse'\n    When method delete\n    Then status 200"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/download.feature",
    "content": "Feature: file serving / download\n\nScenario: return a binary from the file-system\nGiven url mockServerUrl + 'download'\nWhen method get\nThen match responseBytes == read('test.pdf.zip')\n\nScenario: upload a binary file\n* bytes body = read('test.pdf.zip')\nGiven url mockServerUrl + 'upload'\nAnd request body\nWhen method post\nThen status 200\nAnd match response.size == body.length\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/form.feature",
    "content": "Feature: testing form submits\n\nBackground:\n* url mockServerUrl\n\nScenario: without request body\n    Given path 'form'\n    And form field foo = 'bar'\n    When method post\n    Then status 200\n    And match response == { success: true }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/german.feature",
    "content": "Feature: german characters and response encoding\n\nScenario: umlauts in the response\n\nGiven url mockServerUrl\nAnd path 'german'\nWhen method get\nThen status 200\nAnd match response == <name>Müller</name>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/headers.feature",
    "content": "Feature: no response headers\n\nScenario Outline: route by header value: <value>\n    Given url mockServerUrl\n    And path 'headers'\n    And header val = value\n    When method get\n    Then status 200\n    And match response == { val: '#(value)' }\n\n  Examples:\n    | value |\n    | foo   |\n    | bar   |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/hello-data-driven.feature",
    "content": "Feature: a karate test using cucumber scenario outlines\n\nBackground:\nGiven url mockServerUrl + 'dogs'\n\nScenario Outline: create many dogs\n\nGiven request { name: '<name>', age: <age>, height: <height> }\nWhen method post\nThen status 201\n# And match response == { id: '#ignore', name: '<name>', age: <age>, height: <height> }\n\nGiven path response.id\nWhen method get\nThen status 200\n\nExamples:\n|  name  | age | height |\n| Snoopy |  2  | 12.1   |\n| Pluto  |  5  | 20.2   |\n| Scooby | 10  | 40.7   |\n| Spike  |  7  | 35.3   |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/hello-world.feature",
    "content": "Feature: simple example of a karate test\n\nBackground:\n# to test that expressions also work for the method keyword\n* def postMethod = 'post'\n* def getMethod = 'get'\n\nScenario: create and retrieve a cat\n\nGiven url mockServerUrl + 'cats'\nAnd request { name: 'Billie' }\nWhen method postMethod\nThen status 201\nAnd match response == { id: '#ignore', name: 'Billie' }\n# And assert responseTime < 1000\n\nGiven path response.id\nWhen method getMethod\nThen status 200\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/html.feature",
    "content": "Feature: html\n\nScenario: server response not well-formed html\n* configure logPrettyResponse = true\nGiven url mockServerUrl\nAnd path 'html'\nWhen method get\nThen status 200\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/invalid-cookie.feature",
    "content": "Feature:\n\nBackground:\n* url mockServerUrl\n\n  Scenario:\n    * path 'invalid-cookie'\n    * method get\n    * status 200\n\n    # check that 'invalid' cookie is passed to the next call\n    * method get\n    * status 404\n    * def temp = karate.prevRequest\n    * def invalidCookie = temp.headers['Cookie']\n    * match invalidCookie contains [\"detectedTimeZoneId=FLE Standard Time\"]"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/json-bad.feature",
    "content": "Feature: json\n\nScenario: \n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/json-order-mock-test.feature",
    "content": "Feature:\n\n  Scenario: verify json key ordering retained from response\n\n    #get payload & verify key ordering\n    * string originalString = '{\"echo\":\"echo@gmail.com\",\"lambda\":\"Lambda\",\"bravo\":\"1980-01-01\"}'\n    * json payload = originalString\n\n    #create mock and do call\n    * def port = karate.start('json-order-mock.feature').port\n    * def simpleUrl = 'http://localhost:' + port + '/json_order'\n\n    Given url simpleUrl\n    And request payload\n    When method POST\n    Then status 200\n\n    #verify response json key ordering\n    * string responseString = response\n    * match responseString == '{\"tango\":\"Alice\",\"foxtrot\":\"0.0.0.0\",\"sierra\":\"Bob\"}'\n\n    #verify request json key ordering\n    * string payloadString = payload\n    * match payloadString == originalString"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/json-order-mock.feature",
    "content": "Feature: simple mock\n\n  Scenario: pathMatches('/json_order') && methodIs('post')\n    * def response = {\"tango\":\"Alice\",\"foxtrot\":\"0.0.0.0\",\"sierra\":\"Bob\"}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/karate-config.js",
    "content": "function fn() {\n  var port = karate.properties['karate.server.port'] || 8080;\n  var prefix = karate.properties['karate.ssl'] ? 'https' : 'http';\n  if (prefix === 'https') {\n    karate.configure('ssl', true);\n  }\n  return {\n    mockServerUrl: prefix + '://localhost:' + port + '/v1/'\n  }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/malformed.feature",
    "content": "Feature: malformed response json\n\nBackground:\n* url mockServerUrl\n\nScenario:\nGiven path 'malformed'\nWhen method get\nThen status 200\nAnd match responseType == 'string'\n\nGiven path 'jsonformed'\nWhen method get\nThen status 200\nAnd match responseType == 'json'\n\nGiven path 'xmlformed'\nWhen method get\nThen status 200\nAnd match responseType == 'xml'\n\nGiven path 'stringformed'\nWhen method get\nThen status 200\nAnd match responseType == 'string'\n\n* configure logPrettyResponse = true\nGiven path 'malformed'\nWhen method get\nThen status 200\nAnd match responseType == 'string'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/malformed.txt",
    "content": "{\n   \"errors\":[\n      {\n         \"errCode\":\"TestCode1\",\n         \"errMsg\":\"TestCode1 Message\"\n      }x\n      {\n         \"errCode\":\"TestCode2\",\n         \"errMsg\":\"TestCode2 Message\"\n      }\n   ]\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/multi-params.feature",
    "content": "Feature: testing multi param values\n\n  Scenario: using params in a list\n\n    Given url mockServerUrl\n    And path 'multiparams'\n    And param foo = 'bar', 'baz'\n    When method get\n    Then status 200\n    And match response == { success: true }\n\n\n  Scenario: using json array\n\n    Given url mockServerUrl\n    And path 'multiparams'\n    And params { foo: ['bar', 'baz'] }\n    When method get\n    Then status 200\n    And match response == { success: true }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/no-headers.feature",
    "content": "Feature: no response headers\n\nScenario: test when mock routines return no content or headers\n    Given url mockServerUrl\n    And path 'noheaders'\n    When method get\n    Then status 404\n    And match response == ''\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/no-match.feature",
    "content": "Feature: no scenario matched\n\nScenario: test when mock does not match any \"route\"\n    Given url mockServerUrl\n    And path 'nomatch'\n    When method get\n    Then status 404\n    And match response == ''\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/param-commas.feature",
    "content": "Feature: testing commas in param values\n\n  Scenario: using csv params in a string\n\n    Given url mockServerUrl\n    And path 'commas'\n    And param foo = 'bar,baz'\n    When method get\n    Then status 200\n    And match response == { success: true }\n\n  Scenario: using json string\n\n    Given url mockServerUrl\n    And path 'commas'\n    And params { foo: 'bar,baz' }\n    When method get\n    Then status 200\n    And match response == { success: true }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/patch.feature",
    "content": "Feature: testing http patch method\n\nScenario:\n\nGiven url mockServerUrl\nAnd path 'patch'\nAnd request { foo: 'bar' }\nWhen method patch\nThen status 422\nAnd match response == { success: true }"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/upload.feature",
    "content": "Feature: upload file\n\nScenario: use defaults for file-name and unknown file-extension (content-type)\nGiven url mockServerUrl\nAnd path 'upload', 'excel'\nAnd multipart file myFile = { read: 'test.xlsx' }\nWhen method post\nThen status 200\nAnd match response == {charset: 'UTF-8', filename: 'test.xlsx', transferEncoding: 'binary', name: 'myFile', contentType: 'application/octet-stream', value: '#ignore' }\n\nScenario: user-specified file-name and content-type\nGiven url mockServerUrl\nAnd path 'upload', 'excel'\nAnd multipart file myFile = { read: 'test.xlsx', filename: 'my-file.xlsx', contentType: 'text/csv' }\nWhen method post\nThen status 200\nAnd match response == {charset: 'UTF-8', filename: 'my-file.xlsx', transferEncoding: 'binary', name: 'myFile', contentType: 'text/csv', value: '#ignore' }\n\nScenario: upload multipart\nGiven url mockServerUrl + 'multipart'\nAnd multipart file myFile = { read: 'test.pdf.zip', filename: 'test.pdf.zip', contentType: 'application/octet-stream' }\nAnd multipart field message = 'multipart test'\nWhen method post\nThen status 200\nAnd match response == { success: true }\n\nScenario: upload multipart with json part\nGiven url mockServerUrl + 'multipart/json'\nAnd multipart file myFile = { read: 'test.pdf.zip', filename: 'test.pdf.zip', contentType: 'application/octet-stream' }\nAnd multipart field message = { hello: 'world' }\nWhen method post\nThen status 200\nAnd match response == { hello: 'world' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/url-encoding.feature",
    "content": "Feature: url encoding\n\nScenario: special characters should not break the http client\n\nGiven url mockServerUrl + 'encoding/�Ill~Formed@RequiredString!'\nWhen method get\nThen status 200\nAnd match response == { success: true }\n\n\n\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/mock/white-space.feature",
    "content": "Feature: white space edge cases\n\nScenario: json response with leading line feed\n    Given url mockServerUrl\n    And path 'linefeed'\n    When method get\n    Then status 200\n    And match response == { success: true }\n\nScenario: string response which is pure white space with line feeds\n    Given url mockServerUrl\n    And path 'spaces'\n    When method get\n    Then status 200\n    And match response == '\\n    \\n'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/not-equals.feature",
    "content": "Feature: not equals tests\n\nScenario:\n\n* def expected = { foo: '#number' }\n* def test = { foo: 'bar' }\n* match test != { foo: 'baz' }\n* match test != null\n* match test != 1\n* match test != true\n* match test != 'foo'\n* match test != []\n* match test != {}\n* match test != '#array'\n* match test != '#(expected)'\n* match test != '#(^expected)'\n* match test != '#(^^expected)'\n* match test != '#(!^test)'\n\n* def expected = 'bar'\n* def test = 'foo'\n* match test != null\n* match test != 1\n* match test != true\n* match test != 'bar'\n* match test != []\n* match test != {}\n* match test != '#array'\n* match test != '#(expected)'\n* match test != '#regex .{2}'\n* match test != '#? _.length == 2'\n\n* def test = [1, 2]\n* match test != '#[1]'\n* match test != '#[]? _ > 2'\n\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/ntlm-authentication.feature",
    "content": "Feature: ntlm authentication\n\n  Scenario: various ways to configure ntlm authentication\n    * configure ntlmAuth = { username: 'admin', password: 'secret', domain: 'my.domain', workstation: 'my-pc' }\n    * configure ntlmAuth = { username: 'admin', password: 'secret' }\n    * configure ntlmAuth = null\n    * eval\n    \"\"\"\n    karate.configure('ntlmAuth', { username: 'admin', password: 'secret' })\n    \"\"\"\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/outline-background.feature",
    "content": "Feature:\n\n@setup\nScenario:\n* table data\n    | name  | extra        |\n    | 'one' |              |\n    | 'two' | configSource |\n\nScenario Outline:\n* assert name == 'one' || name == 'two'\n* assert name == 'two' ? extra == 'normal' : true\n\nExamples:\n| karate.setup().data |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/outline-cat.json",
    "content": "{\n  \"name\": \"Bob\",\n  \"age\": 5\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/outline-cats.json",
    "content": "[\n\t{\n\t\t\"name\": \"Bob\",\n\t\t\"age\": 5\n\t},\n\t{\n\t\t\"name\": \"Nyan\",\n\t\t\"age\": 7\n\t}\n]"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/outline-config-js.feature",
    "content": "Feature:\n\n@setup\nScenario:\n * def data = [{ name: 'value1' }, { name: 'value2' }, { name: 'value3' }, { name: 'value4' }]\n\nScenario Outline:\n* print 'name:', name\n\n  Examples:\n| karate.setup().data |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/outline-csv.csv",
    "content": "﻿test,birthDate\r\n'thh','2002-04-16'\r\n'thh','2002-04-16'\r\n'thh','2002-04-16'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/outline-csv.feature",
    "content": "Feature:\n\nScenario Outline: first column is <test>\n* match __row == { test: '#string', birthDate: '#string' }\n\nExamples:\n| read('outline-csv.csv') |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/outline-dynamic-fail.feature",
    "content": "Feature:\n\n# @setup\nScenario:\n* def data = [{a: 1}, {a: 2}]\n\nScenario Outline:\n* print __row\n\nExamples:\n| karate.setup().data |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/outline-dynamic.feature",
    "content": "Feature:\n\nBackground:\n* print 'in background', __num\n\n@setup\nScenario:\n* print 'in setup'\n* def data = [{a:1}, {a:2}]\n\nScenario Outline:\n* print __row\n\nExamples:\n| karate.setup().data |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/outline-generator.feature",
    "content": "Feature:\n\n@setup\nScenario:\n* def generator = function(i){ if (i == 5) return null; return { name: 'cat' + i, age: i } }\n\nScenario Outline:\n* match __num == age\n* match __row.name == 'cat' + age\n\nExamples:\n| karate.setup().generator |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/outline-setup-once.feature",
    "content": "Feature:\n\nBackground:\n* print 'in background', __num\n\n@setup\nScenario:\n* print 'in setup'\n* def data = [{a:1}, {a:2}]\n\nScenario Outline: first\n* print __row\n\nExamples:\n| karate.setupOnce().data |\n\nScenario Outline: second\n* print __row\n\nExamples:\n| karate.setupOnce().data |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/outline.feature",
    "content": "Feature: advanced scenario outline and examples table usage\n\nScenario Outline: name is <name> and age is <age>\n  * def temp = '<name>'\n  * match temp == name\n  * match temp == __row.name\n  * def expected = __num == 0 ? 'name is Bob and age is 5' : 'name is Nyan and age is 6'\n  * match expected == karate.info.scenarioName\n\n  Examples:\n    | name | age |\n    | Bob  | 5   |\n    | Nyan | 6   |\n\nScenario Outline: magic variables with type hints\n  * def expected = [{ name: 'Bob', age: 5 }, { name: 'Nyan', age: 6 }]\n  * match __row == expected[__num]\n\n  Examples:\n    | name | age! |\n    | Bob  | 5    |\n    | Nyan | 6    |\n\nScenario Outline: embedded expressions and type hints\n  * match __row == { name: '#(name)', alive: '#boolean' }\n\n  Examples:\n    | name | alive! |\n    | Bob  | false  |\n    | Nyan | true   |\n\nScenario Outline: inline json\n  * match __row == { first: 'hello', second: { a: 1 } }\n  * match first == 'hello'\n  * match second == { a: 1 }\n\n  Examples:\n    | first  | second!  |\n    | hello  | { a: 1 } |\n\nScenario Outline: mix reading from a dynamic file\n  * def cat = read(filename + '.json')\n  * match cat == { name: 'Bob', age: 5 }\n\n  Examples:\n    | filename |\n    | outline-cat |\n\nScenario Outline: using the optional ##() marker effectively with examples type-hints\n    * def search = { name: { first: \"##(first)\", last: \"#(last)\" }, age: \"##(age)\" }\n    * match search == expected\n\n    Examples:\n    | first | last  | age! | expected!                                           |\n    | John  | Smith | 20   | { name: { first: 'John', last: 'Smith' }, age: 20 } |\n    | Jane  | Doe   |      | { name: { first: 'Jane', last: 'Doe' } }            |\n    |       | Waldo |      | { name: { last: 'Waldo' } }                         |\n\nScenario Outline: dynamic scenario outline\n    * print 'row: ', __row\n    * match __row == { name: '#string', age: '#number' }\n\n    Examples:\n    | read('outline-cats.json') |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parajava/ParallelJavaTest.java",
    "content": "package com.intuit.karate.core.parajava;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nclass ParallelJavaTest {\n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/parajava\")\n                .configDir(\"classpath:com/intuit/karate/core/parajava\")\n                .karateEnv(\"foo\")\n                .parallel(5);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parajava/karate-config.js",
    "content": "function karateConfig() {\n  return karate.callSingle('setup.feature');\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parajava/parallel-java1.feature",
    "content": "Feature:\n\nScenario: test 1\n* match Hello.sayHello(\"foo\") == \"hello foo\"\n* match addVals(1, 2) == 3\n\nScenario: test 2\n* match Hello.sayHello(\"foo\") == \"hello foo\"\n* match addVals(1, 2) == 3\n\nScenario: test 3\n* match Hello.sayHello(\"foo\") == \"hello foo\"\n* match addVals(1, 2) == 3\n\nScenario: test 4\n* match Hello.sayHello(\"foo\") == \"hello foo\"\n* match addVals(1, 2) == 3\n\nScenario: test 5\n* match Hello.sayHello(\"foo\") == \"hello foo\"\n* match addVals(1, 2) == 3\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parajava/parallel-java2.feature",
    "content": "Feature:\n\nScenario: test 1\n* match Hello.sayHello(\"foo\") == \"hello foo\"\n* match addVals(1, 2) == 3\n\nScenario: test 2\n* match Hello.sayHello(\"foo\") == \"hello foo\"\n* match addVals(1, 2) == 3\n\nScenario: test 3\n* match Hello.sayHello(\"foo\") == \"hello foo\"\n* match addVals(1, 2) == 3\n\nScenario: test 4\n* match Hello.sayHello(\"foo\") == \"hello foo\"\n* match addVals(1, 2) == 3\n\nScenario: test 5\n* match Hello.sayHello(\"foo\") == \"hello foo\"\n* match addVals(1, 2) == 3\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parajava/setup.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* def addVals = function(val1, val2){ return val1 + val2 }\n* def Hello = Java.type('com.intuit.karate.core.parallel.Hello')\n* def BigDecimal = Java.type('java.math.BigDecimal')\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/Hello.java",
    "content": "package com.intuit.karate.core.parallel;\n\nimport java.util.function.Function;\n\n/**\n *\n * @author pthomas3\n */\npublic class Hello {\n\n    public static String sayHello(String message) {\n        return \"hello \" + message;\n    }\n\n    public static Function<String, String> sayHelloFactory() {\n        return s -> sayHello(s);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/HelloTest.java",
    "content": "package com.intuit.karate.core.parallel;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.MockHandler;\nimport com.intuit.karate.http.HttpServer;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass HelloTest {\n\n    static final Logger logger = LoggerFactory.getLogger(HelloTest.class);\n\n    static HttpServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        MockHandler mock = new MockHandler(Feature.read(\"classpath:com/intuit/karate/core/parallel/mock.feature\"));\n        server = HttpServer.handler(mock).build();\n    }\n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/parallel/hello.feature\")\n                .configDir(\"classpath:com/intuit/karate/core/parallel\")\n                .systemProperty(\"server.port\", server.getPort() + \"\")\n                .parallel(3);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/ParallelCsvTest.java",
    "content": "package com.intuit.karate.core.parallel;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.MockHandler;\nimport com.intuit.karate.http.HttpServer;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass ParallelCsvTest {\n\n    static final Logger logger = LoggerFactory.getLogger(ParallelCsvTest.class);\n\n    static HttpServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        MockHandler mock = new MockHandler(Feature.read(\"classpath:com/intuit/karate/core/parallel/mock.feature\"));\n        server = HttpServer.handler(mock).build();\n    }\n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/parallel/parallel-csv.feature\")\n                .configDir(\"classpath:com/intuit/karate/core/parallel\")\n                .systemProperty(\"server.port\", server.getPort() + \"\")\n                .parallel(3);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/ParallelOutlineTest.java",
    "content": "package com.intuit.karate.core.parallel;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.MockHandler;\nimport com.intuit.karate.http.HttpServer;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass ParallelOutlineTest {\n\n    static final Logger logger = LoggerFactory.getLogger(ParallelOutlineTest.class);\n\n    static HttpServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        MockHandler mock = new MockHandler(Feature.read(\"classpath:com/intuit/karate/core/parallel/mock.feature\"));\n        server = HttpServer.handler(mock).build();\n    }\n\n    @Test\n    void testParallelOutline() {\n        Results results = Runner.path(\n                \"classpath:com/intuit/karate/core/parallel/parallel-outline-1.feature\",\n                \"classpath:com/intuit/karate/core/parallel/parallel-outline-2.feature\")\n                .configDir(\"classpath:com/intuit/karate/core/parallel\")\n                .systemProperty(\"server.port\", server.getPort() + \"\")\n                .parallel(3);\n        assertEquals(2, results.getFeaturesPassed());\n        assertEquals(12, results.getScenariosPassed());\n        assertEquals(0, results.getFailCount());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/ParallelTest.java",
    "content": "package com.intuit.karate.core.parallel;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.MockHandler;\nimport com.intuit.karate.http.HttpServer;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass ParallelTest {\n\n    static final Logger logger = LoggerFactory.getLogger(ParallelTest.class);\n\n    static HttpServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        MockHandler mock = new MockHandler(Feature.read(\"classpath:com/intuit/karate/core/parallel/mock.feature\"));\n        server = HttpServer.handler(mock).build();\n    }\n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/parallel/parallel.feature\")\n                .configDir(\"classpath:com/intuit/karate/core/parallel\")\n                .systemProperty(\"server.port\", server.getPort() + \"\")\n                .parallel(3);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/call-once-from-feature.feature",
    "content": "Feature:\n\nScenario:\n* print 'before configure headers'\n* configure headers = read('headers.js')\n* def message = 'from common'\n* def HelloOnce = Java.type('com.intuit.karate.core.parallel.Hello')\n* def sayHelloOnce = function(name){ return 'hello ' + name }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/call-single-from-config.feature",
    "content": "Feature:\n\nScenario:\n* url serverUrl\n* path 'fromconfig'\n* method get\n* status 200\n* match response == { message: 'from config' }\n\n* def functionFromCallSingleFromConfig = function(){ return 'resultFromFunctionFromCallSingleFromConfig' }\n* def HelloSingle = Java.type('com.intuit.karate.core.parallel.Hello')\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/call-single-from-config2.feature",
    "content": "Feature:\n\nScenario:\n* match functionFromKarateConfig() == 'resultFromFunctionFromKarateConfig'\n* match __arg.functionFromKarateConfig() == 'resultFromFunctionFromKarateConfig'\n* match functionFromCallSingleFromConfig() == 'resultFromFunctionFromCallSingleFromConfig'\n* match __arg.functionFromCallSingleFromConfig() == 'resultFromFunctionFromCallSingleFromConfig'\n* def message = 'fromCallSingleFromConfig2'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/call-single-from-config3.js",
    "content": "function fn() {\n  var result = {};\n  var Hello = Java.type('com.intuit.karate.core.parallel.Hello');\n  // this is the recommended way to create a java function reference\n  // that can be re-used within karate JS blocks\n  result.sayHello = Hello.sayHelloFactory();\n  return result;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/call-single-from-feature.feature",
    "content": "Feature:\n\nScenario:\n* url serverUrl\n* path 'fromfeature'\n* method get\n* status 200\n* match response == { message: 'from feature' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/called.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* print 'in called'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/cookies.js",
    "content": "function fn() {\n  return { 'cookie-id': java.lang.System.currentTimeMillis() + '' };\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/data.csv",
    "content": "id,name,description\n0,\"zero\",zeroth\n1,\"one\",first\n2,\"two\",second\n3,\"three\",third\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/headers.js",
    "content": "function fn() {\n  return { 'test-id': java.lang.System.currentTimeMillis() + '' };\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/hello.feature",
    "content": "Feature:\n\nBackground:\n* url serverUrl\n* callonce read('call-once-from-feature.feature')\n\nScenario: 1\n* match HelloOnce.sayHello('world') == 'hello world'\n\nScenario: 2\n* match HelloOnce.sayHello('world') == 'hello world'\n\nScenario: 3\n* match HelloOnce.sayHello('world') == 'hello world'\n\nScenario: N\n* call sayHelloOnce 'three'\n\n* path 'three'\n* method get\n* status 200\n* match response == { three: '#string' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/karate-base.js",
    "content": "function fn() {   \n  return { functionFromKarateBase: function(){ return 'fromKarateBase'; } };\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/karate-config.js",
    "content": "function fn() {\n  var config = {\n    functionFromKarateConfig: function(){ return 'resultFromFunctionFromKarateConfig'; },\n    serverUrl: 'http://localhost:' + karate.properties['server.port']\n  };  \n  var result = karate.callSingle('call-single-from-config.feature', config);\n  config.message = result.response.message;\n  config.HelloConfigSingle = result.HelloSingle;\n  var result2 = karate.callSingle('call-single-from-config2.feature', result);\n  config.message2 = result2.message;\n  var result3 = karate.callSingle('call-single-from-config3.js');\n  config.sayHello = result3.sayHello;\n  return config;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/mock.feature",
    "content": "Feature:\n\nScenario: pathMatches('/one')\n* def response = ({ one: requestHeaders['test-id'][0] })\n\nScenario: pathMatches('/two')\n* def response = ({ two: requestHeaders['test-id'][0] })\n\nScenario: pathMatches('/three')\n* def response = ({ three: requestHeaders['test-id'][0] })\n\nScenario: pathMatches('/fromconfig')\n* def response = { message: 'from config' }\n\nScenario: pathMatches('/fromfeature')\n* def response = { message: 'from feature' }"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/parallel-csv.feature",
    "content": "Feature:\n\n@setup\nScenario:\n* def data = read('data.csv')\n* def exclude = karate.wrapFunction(x => data.filter(y => y.id != x))\n* def include = karate.wrapFunction(x => data.filter(y => y.id == x))\n\nScenario Outline:\n* assert id != '0'\n\nExamples:\n| karate.setup().data.filter(x => x.id != '0') |\n\nScenario Outline:\n* assert id != '1'\n\nExamples:\n| karate.setup().exclude('1') |\n\nScenario Outline:\n* assert id == '2'\n\nExamples:\n| karate.setup().include('2') |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/parallel-outline-1.feature",
    "content": "Feature:\n\nBackground:\n # background http builder should work even for a dynamic scenario outline\n * url serverUrl \n # java object that comes from a callSingle in the config\n * def HelloBg = HelloConfigSingle\n * callonce read('call-once-from-feature.feature')\n # cookies are normalized, so reading a JS function should have no impacts (will read as a null variable)\n * configure cookies = read('cookies.js')\n * configure afterFeature =\n \"\"\"\n   function fn() {\n      console.log('afterFeature');\n   }\n \"\"\"\n * configure afterScenarioOutline =\n \"\"\"\n   function fn() {\n      console.log('afterScenarioOutline');\n   }\n \"\"\"\n  * configure afterScenario =\n  \"\"\"\n    function fn() {\n       console.log('afterScenario');\n    }\n  \"\"\"\n\n@setup\nScenario:\n* def data = [ { name: 'value1' }, { name: 'value2' }, { name: 'value3' }, { name: 'value4' } ]\n\nScenario Outline:\n * call read('called.feature')\n * match functionFromKarateBase() == 'fromKarateBase'\n * path 'fromfeature'\n * method get\n * status 200\n * match response == { message: 'from feature' }\n \n * match HelloBg.sayHello('world') == 'hello world'\n * match HelloOnce.sayHello('world') == 'hello world'\n * match sayHello('world') == 'hello world'\n\n Examples:\n  | karate.setup().data |\n\nScenario Outline: validating background http context set in background will be shared in shared scope, with dynamic scenario outline\n * call read('called.feature')\n * match functionFromKarateBase() == 'fromKarateBase'\n * call read('parallel-outline-call-api.feature')\n * match response == { message: 'from feature' }\n\n\n * match HelloBg.sayHello('world') == 'hello world'\n * match HelloOnce.sayHello('world') == 'hello world'\n * match sayHello('world') == 'hello world'\n\n Examples:\n  | karate.setup().data |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/parallel-outline-2.feature",
    "content": "Feature:\n\n@setup\nScenario:\n  * def data = [{ name: 'value1' }, { name: 'value2' }, { name: 'value3' }, { name: 'value4' }]\n\nScenario Outline:\n  * match functionFromKarateBase() == 'fromKarateBase'\n\nExamples:\n  | karate.setup().data |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/parallel-outline-call-api.feature",
    "content": "Feature:\n\nScenario:\n  * url serverUrl\n  * path 'fromfeature'\n  * method get\n  * status 200"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parallel/parallel.feature",
    "content": "Feature:\n\nBackground:\n# background http builder should work even if a callonce follows\n* url serverUrl\n* match message == 'from config'\n* callonce read('call-once-from-feature.feature')\n* match message == 'from common'\n* match message2 == 'fromCallSingleFromConfig2'\n\nScenario: one\n* call sayHelloOnce 'one'\n* path 'one'\n* method get\n* status 200\n* match response == { one: '#string' }\n* def result = karate.callSingle('call-single-from-feature.feature')\n* match result.response == { message: 'from feature' }\n\n* match HelloConfigSingle.sayHello('world') == 'hello world'\n* match HelloOnce.sayHello('world') == 'hello world'\n* match sayHello('world') == 'hello world'\n* match sayHelloOnce('world') == 'hello world'\n\nScenario: two\n* call sayHelloOnce 'two'\n* path 'two'\n* method get\n* status 200\n* match response == { two: '#string' }\n* def result = karate.callSingle('call-single-from-feature.feature')\n* match result.response == { message: 'from feature' }\n\n* match HelloConfigSingle.sayHello('world') == 'hello world'\n* match HelloOnce.sayHello('world') == 'hello world'\n* match sayHello('world') == 'hello world'\n* match sayHelloOnce('world') == 'hello world'\n\nScenario: three\n* call sayHelloOnce 'three'\n* path 'three'\n* method get\n* status 200\n* match response == { three: '#string' }\n* def result = karate.callSingle('call-single-from-feature.feature')\n* match result.response == { message: 'from feature' }\n\n* match HelloConfigSingle.sayHello('world') == 'hello world'\n* match HelloOnce.sayHello('world') == 'hello world'\n* match sayHello('world') == 'hello world'\n* match sayHelloOnce('world') == 'hello world'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parasimple/ParallelOutlineSimpleTest.java",
    "content": "package com.intuit.karate.core.parasimple;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.MockHandler;\nimport com.intuit.karate.http.HttpServer;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class ParallelOutlineSimpleTest {\n    \n    static final Logger logger = LoggerFactory.getLogger(ParallelOutlineSimpleTest.class);\n\n    static HttpServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        MockHandler mock = new MockHandler(Feature.read(\"classpath:com/intuit/karate/core/parallel/mock.feature\"));\n        server = HttpServer.handler(mock).build();\n    }\n\n    @Test\n    void testParallelOutline() {\n        Results results = Runner.path(\n                \"classpath:com/intuit/karate/core/parasimple/parallel-outline-simple.feature\")\n                .configDir(\"classpath:com/intuit/karate/core/parasimple\")\n                .systemProperty(\"server.port\", server.getPort() + \"\")\n                .parallel(3);\n        assertEquals(0, results.getFeaturesFailed());\n        assertEquals(0, results.getScenariosFailed());\n        assertEquals(0, results.getFailCount());\n    }    \n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parasimple/headers.feature",
    "content": "Feature:\n\nScenario:\n* configure headers = read('headers.js')\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parasimple/headers.js",
    "content": "function fn() {\n  return {};\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parasimple/karate-config.js",
    "content": "function fn() {\n  return {};\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parasimple/parallel-outline-simple.feature",
    "content": "Feature:\n\nBackground:\n* call read('headers.feature')\n\n@setup\nScenario:\n * def data = [{ name: 'value1' }, { name: 'value2' }, { name: 'value3' }, { name: 'value4' }] \n\nScenario Outline:\n * url 'http://localhost:' + karate.properties['server.port']\n * path 'fromfeature'\n * method get\n * status 200\n * match response == { message: 'from feature' }\n\n Examples:\n  | karate.setup().data |\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/FeatureParserTest.java",
    "content": "package com.intuit.karate.core.parser;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.Suite;\nimport com.intuit.karate.core.ScenarioOutline;\nimport com.intuit.karate.report.ReportUtils;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.FeatureResult;\nimport com.intuit.karate.core.ScenarioResult;\nimport com.intuit.karate.core.Step;\nimport com.intuit.karate.core.StepResult;\nimport com.intuit.karate.Match;\nimport com.intuit.karate.core.FeatureCall;\nimport com.intuit.karate.core.FeatureRuntime;\nimport com.intuit.karate.core.Scenario;\nimport java.util.Map;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass FeatureParserTest {\n\n    static final Logger logger = LoggerFactory.getLogger(FeatureParserTest.class);\n\n    static FeatureResult execute(String name) {\n        return execute(name, null);\n    }\n\n    static FeatureResult execute(String name, String env) {\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/parser/\" + name);\n        Runner.Builder builder = Runner.builder();\n        builder.karateEnv(env);\n        FeatureRuntime fr = FeatureRuntime.of(new Suite(builder), new FeatureCall(feature));\n        fr.run();\n        return fr.result;\n    }\n    \n    private void match(Object actual, Object expected) {\n        Match.Result mr = Match.evaluate(actual).isEqualTo(expected);\n        assertTrue(mr.pass, mr.message);\n    }    \n\n    @Test\n    void testEngineForSimpleFeature() {\n        FeatureResult result = execute(\"test-simple.feature\");\n        Map<String, Object> map = result.toCucumberJson();\n        match(map.get(\"tags\"), \"[{ name: '@foo', line: 1 }]\");\n        ScenarioResult sr = result.getScenarioResults().get(0);\n        map = sr.toCucumberJson();\n        match(map.get(\"tags\"), \"[{ name: '@bar', line: 5 }, { name: '@foo', line: 1 }]\");\n        ReportUtils.saveCucumberJson(\"target\", result, null);\n        ReportUtils.saveJunitXml(\"target\", result, null);\n    }\n\n    @Test\n    void testEngineForSimpleFeatureWithBackground() {\n        FeatureResult result = execute(\"test-simple-background.feature\");\n        assertEquals(1, result.getScenarioResults().size());\n        ReportUtils.saveCucumberJson(\"target\", result, null);\n        ReportUtils.saveJunitXml(\"target\", result, null);\n    }\n\n    @Test\n    void testEngineForError() {\n        FeatureResult result = execute(\"test-error.feature\");\n        ReportUtils.saveCucumberJson(\"target\", result, null);\n        ReportUtils.saveJunitXml(\"target\", result, null);\n    }\n\n    @Test\n    void testParsingFeatureDescription() {\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/parser/test-simple.feature\");\n        assertEquals(\"the first line\", feature.getName());\n        assertEquals(\"and the second\", feature.getDescription());\n    }\n\n    @Test\n    void testFeatureWithIgnore() {\n        FeatureResult result = execute(\"test-ignore-feature.feature\");\n        assertEquals(0, result.getScenarioResults().size());\n    }\n\n    @Test\n    void testScenarioWithIgnore() {\n        FeatureResult result = execute(\"test-ignore-scenario.feature\");\n        assertEquals(1, result.getScenarioResults().size());\n    }\n\n    @Test\n    void testDefDocString() {\n        FeatureResult result = execute(\"test-def-docstring.feature\");\n        for (StepResult step : result.getScenarioResults().get(0).getStepResults()) {\n            assertEquals(\"passed\", step.getResult().getStatus());\n        }\n        Map<String, Object> map = result.getVariables();\n        match(map.get(\"backSlash\"), \"C:\\\\foo\\\\bar\\\\\");\n    }\n\n    @Test\n    void testSetTable() {\n        FeatureResult result = execute(\"test-set-table.feature\");\n        Map<String, Object> map = result.getVariables();\n        match(map.get(\"output\"), \"{ name: 'Bob', age: 2 }\");\n    }\n\n    @Test\n    void testEmptyFeature() {\n        try {\n            FeatureResult result = execute(\"test-empty.feature.txt\");\n            fail(\"we expected parsing to fail\");\n        } catch (Exception e) {\n            String message = e.getMessage();\n            assertTrue(e.getMessage().contains(\"mismatched input '<EOF>'\"));\n        }\n    }\n\n    @Test\n    void testEmptyFirstLine() {\n        FeatureResult result = execute(\"test-empty-first-line1.feature\");\n        Map<String, Object> map = result.getVariables();\n        match(map.get(\"foo\"), \"bar\");\n        result = execute(\"test-empty-first-line2.feature\");\n        map = result.getVariables();\n        match(map.get(\"foo\"), \"bar\");\n        result = execute(\"test-empty-first-line3.feature\");\n        map = result.getVariables();\n        match(map.get(\"foo\"), \"bar\");\n    }\n\n    @Test\n    void testFeatureHeaderOnly() {\n        FeatureResult result = execute(\"test-feature-header-only.feature\");\n    }\n\n    @Test\n    void testTablePipe() {\n        FeatureResult result = execute(\"test-table-pipe.feature\");\n        Map<String, Object> map = result.getVariables();\n        match(map.get(\"value\"), \"pi|pe\");\n    }\n\n    @Test\n    void testOutlineName() {\n        FeatureResult result = execute(\"test-outline-name.feature\");\n        Map<String, Object> map = result.getVariables();\n        match(map.get(\"name\"), \"Nyan\");\n        match(map.get(\"title\"), \"name is Nyan and age is 5\");\n    }\n\n    @Test\n    void testOutlineNameJs() {\n        FeatureResult result = execute(\"test-outline-name-js.feature\", \"unit-test\");\n        assertFalse(result.isFailed());\n    }\n\n    @Test\n    void testTagsMultiline() {\n        FeatureResult result = execute(\"test-tags-multiline.feature\");\n        Map<String, Object> map = result.getVariables();\n        Match.that(map.get(\"tags\")).contains(\"[ 'tag1', 'tag2', 'tag3', 'tag4' ]\");\n    }\n\n    @Test\n    void testEdgeCases() {\n        FeatureResult result = execute(\"test-edge-cases.feature\");\n    }\n\n    @Test\n    void testOutlineDynamic() {\n        FeatureResult result = execute(\"test-outline-dynamic.feature\");\n        assertEquals(2, result.getScenarioResults().size());\n        Map<String, Object> map = result.getVariables();\n        match(map.get(\"name\"), \"Nyan\");\n        match(map.get(\"title\"), \"name is Nyan and age is 7\");\n    }\n\n    @Test\n    void testStepEditing() throws Exception {\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/parser/test-simple.feature\");\n        Step step = feature.getStep(0, -1, 0);\n        assertEquals(\"def a = 1\", step.getText());\n        step.parseAndUpdateFrom(\"* def a = 2 - 1\");\n        assertEquals(\"def a = 2 - 1\", step.getText());\n    }\n\n    @Test\n    void testEmptyBackground() {\n        FeatureResult result = execute(\"test-empty-background.feature\");\n        assertFalse(result.isFailed());\n        Map<String, Object> map = result.getVariables();\n        match(map.get(\"temp\"), \"['foo']\");\n    }\n\n    @Test\n    void testHide() {\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/parser/test-hide.feature\");\n        Step step = feature.getStep(0, -1, 0);\n        assertTrue(step.isPrefixStar());\n        assertFalse(step.isPrint());\n        assertEquals(\"def a = 1\", step.getText());\n        step = feature.getStep(0, -1, 1);\n        assertTrue(step.isPrefixStar());\n        assertTrue(step.isPrint());\n        assertEquals(\"print a\", step.getText());\n        step = feature.getStep(0, -1, 2);\n        assertFalse(step.isPrefixStar());\n        assertTrue(step.isPrint());\n        assertEquals(\"print a\", step.getText());\n    }\n\n    @Test\n    void testComments() {\n        FeatureResult result = execute(\"test-comments.feature\");\n        assertFalse(result.isFailed());\n    }\n    \n    @Test\n    void testScenarioDescription() {\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/parser/test-scenario-description.feature\");\n        Scenario scenario = feature.getScenario(0, -1);\n        assertEquals(\"hello world\", scenario.getName());\n        assertEquals(\"another line\", scenario.getDescription());\n    }\n\n    @Test\n    void testScenariOutlineReadWithoutTags() {\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/parser/test-outline-dynamic.feature\");\n        Runner.Builder builder = Runner.builder();\n        builder.tags(\"@a-tag\");\n        FeatureRuntime fr = FeatureRuntime.of(new Suite(builder), new FeatureCall(feature));\n        ScenarioOutline outline = feature.getSection(1).getScenarioOutline();\n\n        assertEquals(1, outline.getScenarios(fr).size());\n\n        feature = Feature.read(\"classpath:com/intuit/karate/core/parser/test-outline-name.feature\");\n        fr = FeatureRuntime.of(new Suite(builder), new FeatureCall(feature));\n        outline = feature.getSection(1).getScenarioOutline();\n        assertEquals(2, outline.getScenarios(fr).size());\n\n        // using a tag that does not exist in the Examples Tables\n        // should not select anything\n        builder = Runner.builder();\n        builder.tags(\"@tag-not-present\");\n        feature = Feature.read(\"classpath:com/intuit/karate/core/parser/test-outline-examples-tags.feature\");\n        fr = FeatureRuntime.of(new Suite(builder), new FeatureCall(feature));\n        outline = feature.getSection(0).getScenarioOutline();\n        assertEquals(0, outline.getScenarios(fr).size());\n\n        builder = Runner.builder();\n        builder.tags(\"@three-examples\");\n        feature = Feature.read(\"classpath:com/intuit/karate/core/parser/test-outline-examples-tags.feature\");\n        fr = FeatureRuntime.of(new Suite(builder), new FeatureCall(feature));\n        outline = feature.getSection(0).getScenarioOutline();\n        assertEquals(3, outline.getScenarios(fr).size());\n\n        builder = Runner.builder();\n        builder.tags(\"@two-examples\");\n        feature = Feature.read(\"classpath:com/intuit/karate/core/parser/test-outline-examples-tags.feature\");\n        fr = FeatureRuntime.of(new Suite(builder), new FeatureCall(feature));\n        outline = feature.getSection(0).getScenarioOutline();\n        assertEquals(3, outline.getScenarios(fr).size());\n\n        // no tag selector\n        // bring all example tables\n        builder = Runner.builder();\n        feature = Feature.read(\"classpath:com/intuit/karate/core/parser/test-outline-examples-tags.feature\");\n        fr = FeatureRuntime.of(new Suite(builder), new FeatureCall(feature));\n        outline = feature.getSection(0).getScenarioOutline();\n        assertEquals(10, outline.getScenarios(fr).size());\n\n        // not the @two-examples Examples Table so will use all the other example tables\n        builder = Runner.builder();\n        builder.tags(\"~@two-examples\");\n        feature = Feature.read(\"classpath:com/intuit/karate/core/parser/test-outline-examples-tags.feature\");\n        fr = FeatureRuntime.of(new Suite(builder), new FeatureCall(feature));\n        outline = feature.getSection(0).getScenarioOutline();\n        assertEquals(7, outline.getScenarios(fr).size());\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-comments.feature",
    "content": "# before feature\nFeature: \n# after feature\n\n# before scenario 1\nScenario:\n# after scenario 1\n* print 'step 1'\n# after step 1\n\n# before scenario 2\nScenario:\n# after scenario 2\n* print 'step 2'\n# after step 2\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-def-docstring.feature",
    "content": "Feature: docstring with trailing white space\n\nScenario: first\n* def waitUntil = \n\"\"\"\n{ hello: 'world' }\n\"\"\"\n* text backSlash =\n\"\"\"\nC:\\foo\\bar\\\n\"\"\"\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-edge-cases.feature",
    "content": "  Feature: with spaces before\n\nBackground: with description\n\nScenario Outline:\n* print 'foo'\n\nExamples: with description\n| foo |\n| bar |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-empty-background.feature",
    "content": "Feature:\n\nBackground:\n\n@foo\nScenario: should have @foo as tag\n  * def temp = karate.tags\n  * match temp == ['foo']\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-empty-first-line1.feature",
    "content": "\nFeature:\n\nScenario: empty line before Feature\n  * def foo = 'bar'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-empty-first-line2.feature",
    "content": "\n@foo\nFeature:\n\nScenario: empty line before Feature tag\n  * def foo = 'bar'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-empty-first-line3.feature",
    "content": "\n# comment\nFeature:\n\nScenario: empty line before Feature comment\n  * def foo = 'bar'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-empty.feature.txt",
    "content": ""
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-error.feature",
    "content": "@ignore\nFeature: the first line\n\nScenario: has name\n  Given def a = 1\n  And def b = 2\n  When def c = a + b\n  Then match c == 3\n  And print 'success !'\n\nScenario:\n  Given def a = 1\n  And def b = 2\n  When def c = a + b\n  Then match c == 2\n  And print 'success !'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-feature-header-only.feature",
    "content": "Feature:\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-hide.feature",
    "content": "Feature: test\n\nScenario:\n  * def a = 1\n  * print a\n  When print a\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-ignore-feature.feature",
    "content": "@ignore\nFeature: the first line\n    and the second\n\nScenario:\n  Given def a = 1\n  And def b = 2\n  When def c = a + b\n  Then match c == 3\n  And print 'success !'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-ignore-scenario.feature",
    "content": "Feature: first scenario should be ignored\n\n@ignore\nScenario: first\n    * print 'first'\n\nScenario: second\n    * print 'second'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-outline-dynamic.feature",
    "content": "Feature:\n\n@setup\nScenario:\n* def cats = [{name: 'Bob', age: 5}, {name: 'Nyan', age: 7}]\n\nScenario Outline: name is <name> and age is <age>\n* def name = '<name>'\n* match name == \"#? _ == 'Bob' || _ == 'Nyan'\"\n* def title = karate.info.scenarioName\n\nExamples:\n| karate.setup().cats |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-outline-examples-tags.feature",
    "content": "Feature:\n\nBackground:\n  * def foo = 'bar'\n\nScenario Outline: name is <name>\n* def name = '<name>'\n\nExamples:\n| name    |\n| Bob     |\n| Nyan    |\n| Dylan   |\n| Tom   |\n\n@three-examples\nExamples:\n  | name    |\n  | Bob     |\n  | Nyan    |\n  | Dylan   |\n\n@two-examples\nExamples:\n  | name    |\n  | Bob     |\n  | Nyan    |\n  | Dylan   |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-outline-name-js.feature",
    "content": "Feature:\n\n@setup\nScenario:\n  * def sum = function(x,y){ return x + y; }\n  * def js_data =\n  \"\"\"\n  [\n    {\n      \"name\": \"Bob\",\n      \"age\": 10,\n      \"title\": \"name is Bob and age is 10\"\n    },\n    {\n      \"name\": \"Nyan\",\n      \"age\": 5,\n      \"title\": \"name is Nyan and age is 5\"\n    }\n  ]\n  \"\"\"\n\n  * def nested_js_data =\n  \"\"\"\n  [\n    {\n      \"name\": {\n        \"first\": \"Bob\",\n        \"last\": \"Dylan\"\n      },\n      \"age\": 10,\n      \"title\": \"name is Bob and age is 10\"\n    },\n    {\n      \"name\": {\n        \"first\": \"Nyan\",\n        \"last\": \"Cat\"\n      },\n      \"age\": 5,\n      \"title\": \"name is Nyan and age is 5\"\n    }\n  ]\n  \"\"\"\n\nScenario Outline: `name is ${name} and age is ${age}`\n  * def name = '<name>'\n  * match name == \"#? _ == 'Bob' || _ == 'Nyan'\"\n  * match title == karate.scenario.name\n\nExamples:\n| name   | age | title                     |\n| Bob    | 10  | name is Bob and age is 10 |\n| Nyan   | 5   | name is Nyan and age is 5 |\n\n\nScenario Outline: `name is ${name} and age is ${age}`\n  * def name = '<name>'\n  * match name == \"#? _ == 'Bob' || _ == 'Nyan'\"\n  * match title == karate.scenario.name\n\nExamples:\n  | karate.setup().js_data |\n\n\nScenario Outline: `name is ${name.first} and age is ${age}`\n  * match name.first == \"#? _ == 'Bob' || _ == 'Nyan'\"\n  * match title == karate.scenario.name\n\nExamples:\n  | karate.setup().nested_js_data |\n\n\nScenario Outline: `name is ${name.first} ${name.last} and age is ${age}`\n  * match name.first == \"#? _ == 'Bob' || _ == 'Nyan'\"\n  * match name.last == \"#? _ == 'Dylan' || _ == 'Cat'\"\n  * match title == karate.scenario.name\n\nExamples:\n  | name!                               | age | title                           |\n  | { \"first\": \"Bob\", \"last\": \"Dylan\" } | 10  | name is Bob Dylan and age is 10 |\n  | { \"first\": \"Nyan\", \"last\": \"Cat\" }  | 5   | name is Nyan Cat and age is 5   |\n\n\n# String interpolation allows you to use operators\nScenario: one plus one equals ${1 + 1}\n  * match karate.scenario.name == \"one plus one equals 2\"\n\nScenario: `one plus one equals ${1 + 1}`\n  * match karate.scenario.name == \"one plus one equals 2\"\n\n# can even access the karate object\nScenario: scenario execution (env = ${karate.env})\n  # the env is set on the unit test in FeatureParserTest.java\n  * match karate.scenario.name == \"scenario execution (env = unit-test)\"\n\n# functions can also be used, including access to the Java Interop API\nScenario: math scenario: should return ${java.lang.Math.pow(2, 2)}\n  * def powResult = java.lang.Math.pow(2, 2)\n  * match karate.scenario.name == \"math scenario: should return \" + powResult\n  * match karate.scenario.name == \"math scenario: should return 4\""
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-outline-name.feature",
    "content": "Feature:\n\n@setup\nScenario:\n  * def js_data =\n  \"\"\"\n  [\n    {\n      \"name\": \"Bob\",\n      \"age\": 10,\n      \"title\": \"name is Bob and age is 10\"\n    },\n    {\n      \"name\": \"Nyan\",\n      \"age\": 5,\n      \"title\": \"name is Nyan and age is 150\"\n    }\n  ]\n  \"\"\"\n\nScenario Outline: name is <name> and age is <age>\n* def name = '<name>'\n* match name == \"#? _ == 'Bob' || _ == 'Nyan'\"\n* def title = karate.info.scenarioName\n\nExamples:\n| name   | age | title |\n| Bob    | 10  | name is Bob and age is 10 |\n| Nyan   | 5   | name is Nyan and age is 5 |\n\n\nScenario Outline: name is <name> and age is <age>\n  * def name = '<name>'\n  * match name == \"#? _ == 'Bob' || _ == 'Nyan'\"\n  * def title = karate.info.scenarioName\n\nExamples:\n  | karate.setup().js_data |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-scenario-description.feature",
    "content": "Feature: feature name\n\nScenario: hello world\n   another line\n* print 'in scenario'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-set-table.feature",
    "content": "Feature:\n\nScenario:\n* def kittenName = 'Bob'\n* def kittenAge = 2\n\n* set output  \n| path       | value            |\n| name       | kittenName       |\n| age        | kittenAge        |\n\n* match output == { name: 'Bob', age: 2 }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-simple-background.feature",
    "content": "Feature: the first line\n    and the second\n\nBackground:\n  Given def a = 1\n  And def b = 2\n\nScenario:\n  When def c = a + b\n  Then match c == 3\n  And print 'success !'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-simple.feature",
    "content": "@foo\nFeature: the first line\n    and the second\n\n@bar\nScenario:\n  Given def a = 1\n  And def b = 2\n  When def c = a + b\n  Then match c == 3\n  And print 'success !'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-table-pipe.feature",
    "content": "Feature:\n\nScenario Outline:\n* def value = '<lhs>'\n* print 'value: ', value\n* match value == \"#? _ == 'hello' || _ == 'pi|pe'\"\n\nExamples:\n| lhs      | rhs     |\n| hello    | hello   |\n| pi\\|pe   | pi\\|pe  |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/parser/test-tags-multiline.feature",
    "content": "@tag1\n# @commented\n@tag2\n\n# this is a comment\n\nFeature:\n\n  @tag3\n  # @commented\n  @tag4\n  Scenario:\n  * def tags = karate.tags\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/payments-mock.feature",
    "content": "Feature:\n\nBackground:\n* def counter = 0\n* def payments = {}\n\nScenario: pathMatches('/payments') && methodIs('post')\n* def payment = request\n* def counter = counter + 1\n* def id = '' + counter\n* payment.id = id\n* payments[id] = payment\n* def response = payment \n\nScenario: pathMatches('/payments')\n* def response = $payments.*\n\nScenario: pathMatches('/payments/{id}') && methodIs('put')\n* payments[pathParams.id] = request\n* def response = request\n\nScenario: pathMatches('/payments/{id}') && methodIs('delete')\n* delete payments[pathParams.id]\n\nScenario: pathMatches('/payments/{id}')\n* def response = payments[pathParams.id]\n* def responseStatus = response ? 200 : 404\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/payments.feature",
    "content": "Feature:\n\nScenario:\n* url 'http://localhost:8080/api/payments'\n\n* request { amount: 5.67, description: 'test one' }\n* method post\n* status 200\n* match response == { id: '#string', amount: 5.67, description: 'test one' }\n* def id = response.id\n\n* path id\n* method get\n* status 200\n* match response == { id: '#(id)', amount: 5.67, description: 'test one' }\n\n* path id\n* request { id: '#(id)', amount: 5.67, description: 'test two' }\n* method put\n* status 200\n* match response == { id: '#(id)', amount: 5.67, description: 'test two' }\n\n* method get\n* status 200\n* match response contains { id: '#(id)', amount: 5.67, description: 'test two' }\n\n* path id\n* method delete\n* status 200\n\n* path id\n* method get\n* status 404\n\n* method get\n* status 200\n* match response !contains { id: '#(id)', amount: '#number', description: '#string' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/perf-mock.feature",
    "content": "Feature:\n\nScenario: pathMatches('/hello')\n* def response = requestParams\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/perf.feature",
    "content": "Feature:\n\nBackground:\n  * url 'http://localhost:' + karate.properties['karate.server.port']\n\n@name=pass\nScenario:\n* path 'hello'\n* param foo = bar\n* method get\n* status 200\n* match response == { foo: ['#(bar)'] }\n\n@name=failStatus\nScenario:\n* path 'hello'\n* param foo = bar\n* method get\n# The following line will fail\n* status 500\n\n@name=failResponse\nScenario:\n* url 'http://localhost:' + karate.properties['karate.server.port']\n* path 'hello'\n* param foo = bar\n* method get\n* status 200\n# The following line will fail\n* match response == {}\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/print.feature",
    "content": "Feature: test variations of the print keyword\n\nBackground:\n* def Logger = Java.type('com.intuit.karate.core.TestLogAppender')\n* def logger = new Logger()\n\nScenario: single literal\n* print 'foo'\n* def test = logger.collect()\n* match test == '[print] foo \\n'\n\nScenario: multi literal\n* print 'foo', 'bar'\n* def test = logger.collect()\n* match test == '[print] foo bar \\n'\n\nScenario: single expression\n* def foo = 'bar'\n* print foo\n* def test = logger.collect()\n* match test == '[print] bar \\n'\n\nScenario: multi expression\n* def foo = 'bar'\n* print foo, 1\n* def test = logger.collect()\n* match test == '[print] bar 1 \\n'\n\nScenario: json multi expression\n* def foo = { bar: 1 }\n* print foo, foo.bar\n* def test = logger.collect()\n* match test == '[print] {\\n  \"bar\": 1\\n}\\n 1 \\n'\n\nScenario: xml expression\n* def foo = <bar><baz>1</baz></bar>\n* print foo\n* def test = logger.collect()\n* match test == '[print] <bar>\\n  <baz>1</baz>\\n</bar>\\n \\n'\n\nScenario: typical json use case\n* def foo = { bar: 1 }\n* print 'the value of foo is:', foo\n* def test = logger.collect()\n* match test == '[print] the value of foo is: {\\n  \"bar\": 1\\n}\\n \\n'\n\nScenario: typical pretty json use case\n* def foo = { bar: 1 }\n* print 'the value of foo is:\\n' + karate.pretty(foo)\n* def test = logger.collect()\n* match test == '[print] the value of foo is:\\n{\\n  \"bar\": 1\\n}\\n \\n'\n\nScenario: troublesome commas\n* def foo = { bar: 1 }\n* print 'the value, of foo, is:', foo\n* def test = logger.collect()\n* match test == '[print] the value, of foo, is: {\\n  \"bar\": 1\\n}\\n \\n'\n\nScenario: forward slash in json\n* def foo = { bar: 'http://localhost:8080' }\n* print 'the value, of foo, is:', foo\n* def test = logger.collect()\n* match test == '[print] the value, of foo, is: {\\n  \"bar\": \"http://localhost:8080\"\\n}\\n \\n'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/read-expressions.json",
    "content": "[\n  {\n    \"item\": {\n      \"foo\": \"#(foo)\",\n      \"nested\": {\n        \"bar\": \"#(bar)\",\n        \"notfound\": \"#(baz)\"\n      }\n    }\n  }\n]"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/read-expressions.yml",
    "content": "- item:\n    foo: '#(foo)'\n    nested:\n      # this is a comment\n      bar: '#(bar)'\n      notfound: '#(baz)'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/read-properties.feature",
    "content": "Feature:\n\nScenario:\n* def readProps =\n\"\"\"\nfunction(path) {\n  var stream = karate.readAsStream(path);\n  var props = new java.util.Properties();\n  props.load(stream);\n  return props;\n}\n\"\"\"\n* def props = readProps('read-properties.properties')\n* match props == { hello: 'world' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/read-properties.properties",
    "content": "hello=world\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/replace.feature",
    "content": "Feature: replace keyword\n\nScenario: one-line default placeholder\n\n* def text = 'hello <foo> world'\n* replace text.foo = 'bar'\n* match text == 'hello bar world'\n\n\nScenario: one-line non-default placeholder 1\n\n* def text = 'hello ${foo} world'\n* replace text.${foo} = 'bar'\n* match text == 'hello bar world'\n\n\nScenario: one-line non-default placeholder 2\n\n* def text = 'hello @@foo@@ world'\n* replace text.@@foo@@ = 'bar'\n* match text == 'hello bar world'\n\n\nScenario: table default placeholder\n\n* def text = 'hello <one> world <two> bye'\n\n* replace text\n    | token | value   |\n    | one   | 'cruel' |\n    | two   | 'good'  |\n\n* match text == 'hello cruel world good bye'\n\n\nScenario: table non-default placeholder\n\n* def text = 'hello ${one} world @@two@@ bye'\n\n* replace text\n    | token   | value   |\n    | ${one}  | 'cruel' |\n    | @@two@@ | 'good'  |\n\n* match text == 'hello cruel world good bye'\n\n\nScenario: table with expression evaluation\n\n* def text = 'hello <one> world <two> bye'\n* def first = 'cruel'\n* def second = 'good'\n\n* replace text\n    | token | value  |\n    | one   | first  |\n    | two   | second |\n\n* match text == 'hello cruel world good bye'\n\n\nScenario: table with complex expression evaluation\n\n* def text = 'hello <one> world <two> bye'\n* def json = { first: 'cruel', second: 'good' }\n\n* replace text\n    | token | value       |\n    | one   | json.first  |\n    | two   | json.second |\n\n* match text == 'hello cruel world good bye'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/retry/RetryTest.java",
    "content": "package com.intuit.karate.core.retry;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.Scenario;\nimport com.intuit.karate.core.ScenarioResult;\nimport com.intuit.karate.core.Step;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass RetryTest {\n    \n    static final Logger logger = LoggerFactory.getLogger(RetryTest.class);   \n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/retry/test.feature\")\n                .reportDir(\"target/retry-test\")\n                .parallel(1);\n        assertEquals(1, results.getFailCount());\n        List<ScenarioResult> failed = results.getScenarioResults().filter(sr -> sr.isFailed()).collect(Collectors.toList());\n        assertEquals(1, failed.size());\n        Scenario scenario = failed.get(0).getScenario();\n        Step step = scenario.getSteps().get(0);\n        assertEquals(\"assert value != 1\", step.getText());\n        step.setText(\"assert value == 1\");\n        ScenarioResult sr = results.getSuite().retryScenario(scenario);\n        assertFalse(sr.isFailed());\n        results = results.getSuite().updateResults(sr);\n        assertEquals(0, results.getFailCount());\n    }\n\n    @Test\n    void testSetup() {\n        System.setProperty(\"CURRENT_VALUE\", \"a\");\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/retry/retry-with-setup.feature\").parallel(1);\n\n        System.setProperty(\"CURRENT_VALUE\", \"b\");\n        for (ScenarioResult scenarioResult : results.getScenarioResults().collect(Collectors.toList())) {\n            if (scenarioResult.isFailed()) {\n                ScenarioResult retryScenarioResult = results.getSuite().retryScenario(scenarioResult.getScenario());\n                results = results.getSuite().updateResults(retryScenarioResult);\n            }\n        }\n\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/retry/retry-with-setup.feature",
    "content": "Feature: Retry with setup\n\n  @setup\n  Scenario: Setup\n    * def data = [{ value: 'b' }, { value: 'a' }]\n\n  Scenario Outline: Scenario\n    * def currentValGetter = function () { return java.lang.System.getProperty('CURRENT_VALUE') }\n    * assert currentValGetter() == value\n\n    Examples:\n      | karate.setup().data |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/retry/test.feature",
    "content": "Feature: retry test\n\nBackground:\n* def value = 1\n\nScenario: one\n* assert value == 1\n\nScenario: two\n* assert value != 1\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/FeatureResultTest.java",
    "content": "package com.intuit.karate.core.runner;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Suite;\nimport com.intuit.karate.report.ReportUtils;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.FeatureCall;\nimport com.intuit.karate.core.FeatureResult;\nimport com.intuit.karate.core.FeatureRuntime;\nimport java.io.File;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.IntBinaryOperator;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * @author vmchukky\n */\npublic class FeatureResultTest {\n\n    static final Logger logger = LoggerFactory.getLogger(FeatureResultTest.class);\n\n    static FeatureResult result(String name) {\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/runner/\" + name);\n        FeatureRuntime fr = FeatureRuntime.of(new Suite(), new FeatureCall(feature));\n        fr.run();\n        return fr.result;\n    }\n\n    static String xml(FeatureResult result) {\n        File file = ReportUtils.saveJunitXml(\"target\", result, null);\n        return FileUtils.toString(file);\n    }\n\n    @Test\n    void testFailureMultiScenarioFeature() throws Exception {\n        FeatureResult result = result(\"failed.feature\");\n        assertEquals(2, result.getFailedCount());\n        assertEquals(3, result.getScenarioCount());\n        String contents = xml(result);\n        assertTrue(contents.contains(\"did not evaluate to 'true': a != 1\"));\n        assertTrue(contents.contains(\"did not evaluate to 'true': a == 3\"));\n\n        // failure1 should have first step as failure, and second step as skipped\n        // TODO: generate the expected content string, below code puts a hard dependency\n        // with KarateJunitFormatter$TestCase.addStepAndResultListing()\n        assertTrue(contents.contains(\"Then assert a != 1 ........................................................ failed\"));\n        assertTrue(contents.contains(\"And assert a == 2 ......................................................... skipped\"));\n\n        // failure2 should have first step as passed, and second step as failed\n        assertTrue(contents.contains(\"Then assert a != 2 ........................................................ passed\"));\n        assertTrue(contents.contains(\"And assert a == 3 ......................................................... failed\"));\n\n        // pass1 should have both steps as passed\n        assertTrue(contents.contains(\"Then assert a != 4 ........................................................ passed\"));\n        assertTrue(contents.contains(\"And assert a != 5 ......................................................... passed\"));\n    }\n\n    @Test\n    void testAbortMultiScenarioFeature() throws Exception {\n        FeatureResult result = result(\"aborted.feature\");\n        assertEquals(0, result.getFailedCount());\n        assertEquals(4, result.getScenarioCount());\n        String contents = xml(result);\n\n        // skip-pass and skip-fail both should have all steps as skipped\n        // TODO: generate the expected content string, below code puts a hard dependency\n        // with KarateJunitFormatter$TestCase.addStepAndResultListing()\n        assertTrue(contents.contains(\"* karate.abort() .......................................................... passed\"));\n        assertTrue(contents.contains(\"* assert a == 1 ........................................................... skipped\"));\n        assertTrue(contents.contains(\"* assert a == 2 ........................................................... skipped\"));\n        assertTrue(contents.contains(\"* assert a == 5 ........................................................... passed\"));\n\n        // noskip should have both steps as passed\n        assertTrue(contents.contains(\"Then assert a != 3 ........................................................ passed\"));\n        assertTrue(contents.contains(\"And assert a != 4 ......................................................... passed\"));\n    }\n\n    // has to be public, used by the feature\n    public static void addLambdaFunctionToMap(Map<String, Object> map) {\n        IntBinaryOperator plusOperation = (a, b) -> a + b;\n        map.put(\"javaSum\", plusOperation);\n    }\n\n    @Test\n    void testLambdaFunctionsInScenarioFeature() throws Exception {\n        FeatureResult result = result(\"caller-with-lambda-arg.feature\");\n        assertEquals(0, result.getFailedCount());\n        List data = (List) result.getVariables().get(\"data\");\n        assertTrue(((Map) data.get(0)).get(\"javaSum\") instanceof IntBinaryOperator);\n    }\n\n    // @Test // TODO fails in jdk 17\n    void testStackOverFlowError() {\n        FeatureResult result = result(\"stackoverflow-error.feature\");\n        assertTrue(result.isFailed()); \n        assertTrue(result.getScenarioResults().get(0).getErrorMessage().contains(\"StackOverflowError\"));\n    }\n\n    @Test\n    void testScenarioOutlineXmlResult() {\n        FeatureResult result = result(\"outline.feature\");\n        ReportUtils.saveJunitXml(\"target\", result, \"outline.xml\");\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/FeatureReuseTest.java",
    "content": "package com.intuit.karate.core.runner;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Suite;\nimport com.intuit.karate.report.ReportUtils;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.FeatureCall;\nimport com.intuit.karate.core.FeatureRuntime;\nimport java.io.File;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass FeatureReuseTest {\n\n    static final Logger logger = LoggerFactory.getLogger(FeatureReuseTest.class);\n\n    static String resultXml(String name) {\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/runner/\" + name);\n        FeatureRuntime fr = FeatureRuntime.of(new Suite(), new FeatureCall(feature));\n        fr.run();\n        File file = ReportUtils.saveJunitXml(\"target\", fr.result, null);\n        return FileUtils.toString(file);\n    }\n\n    @Test\n    void testFailureInCalledShouldFailTest() throws Exception {\n        String contents = resultXml(\"caller.feature\");\n        assertTrue(contents.contains(\"did not evaluate to 'true': input != 4\"));\n    }\n\n    @Test\n    void testArgumentsPassedForSharedScope() throws Exception {\n        String contents = resultXml(\"caller-shared.feature\");\n        assertTrue(contents.contains(\"passed\"));\n    }\n\n    @Test\n    void testCallerTwo() throws Exception {\n        String contents = resultXml(\"caller_2.feature\");\n        assertTrue(contents.contains(\"passed\"));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/NoopDriver.java",
    "content": "package com.intuit.karate.core.runner;\n\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.Driver;\nimport com.intuit.karate.driver.DriverOptions;\nimport com.intuit.karate.driver.Element;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\npublic class NoopDriver implements Driver {\n\n    public static final String DRIVER_TYPE = \"noopdriver\";\n    static final Logger logger = LoggerFactory.getLogger(NoopDriver.class);\n\n    public final DriverOptions options;\n\n    public static NoopDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        return new NoopDriver(map, sr);\n    }\n\n    public NoopDriver(Map<String, Object> map, ScenarioRuntime sr) {\n        this.options = new DriverOptions(map, sr, 8402, DRIVER_TYPE);\n    }\n\n    @Override\n    public void activate() {\n        logger.debug(\"NoopDriver: activate()\");\n    }\n\n    @Override\n    public void refresh() {\n        logger.debug(\"NoopDriver: refresh()\");\n\n    }\n\n    @Override\n    public void reload() {\n        logger.debug(\"NoopDriver: reload()\");\n\n    }\n\n    @Override\n    public void back() {\n        logger.debug(\"NoopDriver: back()\");\n\n    }\n\n    @Override\n    public void forward() {\n        logger.debug(\"NoopDriver: forward()\");\n\n    }\n\n    @Override\n    public void maximize() {\n        logger.debug(\"NoopDriver: maximize()\");\n\n    }\n\n    @Override\n    public void minimize() {\n        logger.debug(\"NoopDriver: minimize()\");\n\n    }\n\n    @Override\n    public void fullscreen() {\n        logger.debug(\"NoopDriver: fullscreen()\");\n\n    }\n\n    @Override\n    public void close() {\n        logger.debug(\"NoopDriver: close()\");\n\n    }\n\n    @Override\n    public void quit() {\n        logger.debug(\"NoopDriver: quit()\");\n\n    }\n\n    @Override\n    public void switchPage(String titleOrUrl) {\n        logger.debug(\"NoopDriver: switchPage()\");\n\n    }\n\n    @Override\n    public void switchPage(int index) {\n        logger.debug(\"NoopDriver: switchPage()\");\n\n    }\n\n    @Override\n    public void switchFrame(int index) {\n        logger.debug(\"NoopDriver: switchFrame()\");\n\n    }\n\n    @Override\n    public void switchFrame(String locator) {\n        logger.debug(\"NoopDriver: switchFrame()\");\n\n    }\n\n    @Override\n    public String getUrl() {\n        logger.debug(\"NoopDriver: getUrl()\");\n        return null;\n    }\n\n    @Override\n    public void setUrl(String url) {\n        logger.debug(\"NoopDriver: setUrl()\");\n\n    }\n\n    @Override\n    public Map<String, Object> getDimensions() {\n        logger.debug(\"NoopDriver: getDimensions()\");\n        return Collections.EMPTY_MAP;\n    }\n\n    @Override\n    public void setDimensions(Map<String, Object> map) {\n        logger.debug(\"NoopDriver: setDimensions()\");\n\n    }\n\n    @Override\n    public String getTitle() {\n        logger.debug(\"NoopDriver: getTitle()\");\n        return null;\n    }\n\n    @Override\n    public List<String> getPages() {\n        logger.debug(\"NoopDriver: getPages()\");\n        return Collections.EMPTY_LIST;\n    }\n\n    @Override\n    public String getDialogText() {\n        logger.debug(\"NoopDriver: getDialogText()\");\n        return null;\n    }\n\n    @Override\n    public byte[] screenshot(boolean embed) {\n        logger.debug(\"NoopDriver: screenshot()\");\n        return new byte[0];\n    }\n\n    @Override\n    public Map<String, Object> cookie(String name) {\n        logger.debug(\"NoopDriver: cookie()\");\n        return Collections.EMPTY_MAP;\n    }\n\n    @Override\n    public void cookie(Map<String, Object> cookie) {\n        logger.debug(\"NoopDriver: cookie()\");\n\n    }\n\n    @Override\n    public void deleteCookie(String name) {\n        logger.debug(\"NoopDriver: deleteCookie()\");\n\n    }\n\n    @Override\n    public void clearCookies() {\n        logger.debug(\"NoopDriver: clearCookies()\");\n\n    }\n\n    @Override\n    public List<Map> getCookies() {\n        logger.debug(\"NoopDriver: getCookies()\");\n        return Collections.EMPTY_LIST;\n    }\n\n    @Override\n    public void dialog(boolean accept) {\n        logger.debug(\"NoopDriver: dialog()\");\n\n    }\n\n    @Override\n    public void dialog(boolean accept, String input) {\n        logger.debug(\"NoopDriver: dialog()\");\n\n    }\n\n    @Override\n    public Object script(String expression) {\n        logger.debug(\"NoopDriver: script()\");\n        return null;\n    }\n\n    @Override\n    public boolean waitUntil(String expression) {\n        logger.debug(\"NoopDriver: waitUntil()\");\n        return false;\n    }\n\n    @Override\n    public Driver submit() {\n        logger.debug(\"NoopDriver: submit()\");\n        return this;\n    }\n\n    @Override\n    public Driver timeout(Integer millis) {\n        logger.debug(\"NoopDriver: timeout()\");\n        return this;\n    }\n\n    @Override\n    public Driver timeout() {\n        logger.debug(\"NoopDriver: timeout()\");\n        return this;\n    }\n\n    @Override\n    public Element focus(String locator) {\n        logger.debug(\"NoopDriver: focus()\");\n        return null;\n    }\n\n    @Override\n    public Element clear(String locator) {\n        logger.debug(\"NoopDriver: clear()\");\n        return null;\n    }\n\n    @Override\n    public Element click(String locator) {\n        logger.debug(\"NoopDriver: click()\");\n        return null;\n    }\n\n    @Override\n    public Element input(String locator, String value) {\n        logger.debug(\"NoopDriver: input()\");\n        return null;\n    }\n\n    @Override\n    public Element select(String locator, String text) {\n        logger.debug(\"NoopDriver: select()\");\n        return null;\n    }\n\n    @Override\n    public Element select(String locator, int index) {\n        logger.debug(\"NoopDriver: select()\");\n        return null;\n    }\n\n    @Override\n    public Element value(String locator, String value) {\n        logger.debug(\"NoopDriver: value()\");\n        return null;\n    }\n\n    @Override\n    public void actions(List<Map<String, Object>> actions) {\n        logger.debug(\"NoopDriver: actions()\");\n\n    }\n\n    @Override\n    public String html(String locator) {\n        logger.debug(\"NoopDriver: html()\");\n        return null;\n    }\n\n    @Override\n    public String text(String locator) {\n        logger.debug(\"NoopDriver: text()\");\n        return null;\n    }\n\n    @Override\n    public String value(String locator) {\n        logger.debug(\"NoopDriver: value()\");\n        return null;\n    }\n\n    @Override\n    public String attribute(String locator, String name) {\n        logger.debug(\"NoopDriver: attribute()\");\n        return null;\n    }\n\n    @Override\n    public String property(String locator, String name) {\n        logger.debug(\"NoopDriver: property()\");\n        return null;\n    }\n\n    @Override\n    public boolean enabled(String locator) {\n        logger.debug(\"NoopDriver: enabled()\");\n        return false;\n    }\n\n    @Override\n    public Map<String, Object> position(String locator) {\n        logger.debug(\"NoopDriver: position()\");\n        return Collections.EMPTY_MAP;\n    }\n\n    @Override\n    public Map<String, Object> position(String locator, boolean relative) {\n        logger.debug(\"NoopDriver: position()\");\n        return Collections.EMPTY_MAP;\n    }\n\n    @Override\n    public byte[] screenshot(String locator, boolean embed) {\n        logger.debug(\"NoopDriver: screenshot()\");\n        return new byte[0];\n    }\n\n    @Override\n    public byte[] pdf(Map<String, Object> options) {\n        logger.debug(\"NoopDriver: pdf()\");\n        return new byte[0];\n    }\n\n    @Override\n    public boolean isTerminated() {\n        logger.debug(\"NoopDriver: isTerminated()\");\n        return false;\n    }\n\n    @Override\n    public DriverOptions getOptions() {\n        logger.debug(\"NoopDriver: getOptions()\");\n        return this.options;\n    }\n\n    @Override\n    public Object elementId(String locator) {\n        logger.debug(\"NoopDriver: elementId()\");\n        return null;\n    }\n\n    @Override\n    public List elementIds(String locator) {\n        logger.debug(\"NoopDriver: elementIds()\");\n        return Collections.EMPTY_LIST;\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/RunnerTest.java",
    "content": "package com.intuit.karate.core.runner;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.KarateException;\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.report.ReportUtils;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.FeatureRuntime;\nimport java.io.File;\nimport java.util.Collections;\nimport java.util.Map;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass RunnerTest {\n\n    static final Logger logger = LoggerFactory.getLogger(RunnerTest.class);\n\n    boolean contains(String reportPath, String textToFind) {\n        String contents = FileUtils.toString(new File(reportPath));\n        return contents.contains(textToFind);\n    }\n\n    static String resultXml(String name) {\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/runner/\" + name);\n        FeatureRuntime fr = FeatureRuntime.of(feature);\n        fr.run();\n        File file = ReportUtils.saveJunitXml(\"target\", fr.result, null);\n        return FileUtils.toString(file);\n    }\n\n    @Test\n    void testScenario() throws Exception {\n        String contents = resultXml(\"scenario.feature\");\n        assertTrue(contents.contains(\"Then match b == { foo: 'bar'}\"));\n    }\n\n    @Test\n    void testScenarioOutline() throws Exception {\n        String contents = resultXml(\"outline.feature\");\n        assertTrue(contents.contains(\"When def a = 55\"));\n    }\n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\n                \"classpath:com/intuit/karate/core/runner/multi-scenario-fail.feature\",\n                \"classpath:com/intuit/karate/core/runner/no-scenario-name.feature\",\n                \"classpath:com/intuit/karate/core/runner/scenario.feature\",\n                \"classpath:com/intuit/karate/core/runner/outline.feature\"\n        ).outputJunitXml(true).parallel(1);\n        assertEquals(2, results.getFailCount());\n        String pathBase = \"target/karate-reports/com.intuit.karate.core.runner.\";\n        assertTrue(contains(pathBase + \"scenario.xml\", \"Then match b == { foo: 'bar'}\"));\n        assertTrue(contains(pathBase + \"outline.xml\", \"Then assert a == 55\"));\n        // a scenario failure should not stop other features from running\n        assertTrue(contains(pathBase + \"multi-scenario-fail.xml\", \"Then assert a != 2 ........................................................ passed\"));\n        assertEquals(2, results.getFailCount());\n    }\n\n    @Test\n    void testRunningFeatureFromJavaApi() {\n        Map<String, Object> result = Runner.runFeature(getClass(), \"scenario.feature\", null, true);\n        assertEquals(1, result.get(\"a\"));\n        Map<String, Object> temp = (Map) result.get(\"b\");\n        assertEquals(\"bar\", temp.get(\"foo\"));\n        assertEquals(\"normal\", result.get(\"configSource\"));\n    }\n\n    @Test\n    void testRunningFeatureWithFailAnnotationFromJavaApi() {\n    \tResults results = Runner.path(\"classpath:com/intuit/karate/core/fail-tag.feature\").parallel(1);\n    \tassertEquals(0, results.getFailCount());\n    }\n    \n    @Test\n    void testRunningFeatureWithFailAnnotationFailureFromJavaApi() {\n    \tResults results = Runner.path(\"classpath:com/intuit/karate/core/fail-tag-failure.feature\").parallel(1);\n    \tassertEquals(1, results.getFailCount());\n    }\n    \n    @Test\n    void testRunningFeatureFailureFromJavaApi() {\n        try {\n            Runner.runFeature(getClass(), \"multi-scenario-fail.feature\", null, true);\n            fail(\"expected exception to be thrown\");\n        } catch (Exception e) {\n            assertTrue(e instanceof KarateException);\n        }\n    }\n\n    @Test\n    void testRunningFeatureFailureFromRunner() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/runner/multi-scenario-fail.feature\").parallel(1);\n        assertEquals(1, results.getFailCount());\n    }\n\n    @Test\n    void testRunningRelativePathFeatureFromJavaApi() {\n        Map<String, Object> result = Runner.runFeature(\"classpath:com/intuit/karate/core/runner/test-called.feature\", null, true);\n        assertEquals(1, result.get(\"a\"));\n        assertEquals(2, result.get(\"b\"));\n        assertEquals(\"normal\", result.get(\"configSource\"));\n    }\n\n    @Test\n    void testCallerArg() throws Exception {\n        String contents = resultXml(\"caller-arg.feature\");\n        assertFalse(contents.contains(\"failed\"));\n        assertTrue(contents.contains(\"* def result = call read('called-arg-null.feature')\"));\n    }\n    \n    @Test\n    void testJavaApiWithArgAndConfig() {\n        Map<String, Object> result = Runner.runFeature(\"classpath:com/intuit/karate/core/runner/run-arg.feature\", Collections.singletonMap(\"foo\", \"hello\"), true);\n        assertEquals(\"hello world\", result.get(\"message\"));\n        assertEquals(\"normal\", result.get(\"configSource\"));\n    }  \n    \n    @Test\n    void testJavaApiWithArgNoConfig() {\n        Map<String, Object> result = Runner.runFeature(\"classpath:com/intuit/karate/core/runner/run-arg.feature\", Collections.singletonMap(\"foo\", \"hello\"), false);\n        assertEquals(\"hello world\", result.get(\"message\"));\n        assertEquals(null, result.get(\"configSource\"));\n    }\n    \n    @Test\n    void testJavaApiWithIgnoreTag() {\n        Map<String, Object> result = Runner.runFeature(\"classpath:com/intuit/karate/core/runner/run-ignore.feature\", Collections.singletonMap(\"foo\", \"hello\"), true);\n        assertEquals(Collections.emptyMap(), result);    \n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/TagTest.java",
    "content": "package com.intuit.karate.core.runner;\n\nimport com.intuit.karate.core.Tag;\nimport java.util.Arrays;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n  class TagTest {\n\n    final Tag tag = new Tag(5, \"@foo=bar,baz\");\n\n    @Test\n    void testGetLine() {\n        assertEquals(5, tag.getLine());\n    }\n\n    @Test\n    void testGetText() {\n        assertEquals(\"foo=bar,baz\", tag.getText());\n    }\n\n    @Test\n    void testGetName() {\n        assertEquals(\"foo\", tag.getName());\n    }\n\n    @Test\n    void testGetValues() {\n        assertEquals(Arrays.asList(\"bar\", \"baz\"), tag.getValues());\n    }\n\n    @Test\n    void testToString() {\n        assertEquals(\"@foo=bar,baz\", new Tag(5, \"@foo=bar,baz\").toString());\n        assertEquals(\"@foo=\", new Tag(5, \"@foo=\").toString());\n        assertEquals(\"@foobar,baz\", new Tag(5, \"@foobar,baz\").toString());\n    }\n\n    @Test\n    void testHashcode() {\n        assertEquals(894422763, tag.hashCode());\n    }\n\n    @Test\n    void testEquals() {\n        assertTrue(tag.equals(tag));\n        assertFalse(tag.equals(null));\n        assertFalse(tag.equals(new Tag(0, \"@baz=bar,foo\")));\n        assertFalse(tag.equals(\"foo\"));\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/aborted.feature",
    "content": "Feature: abort2pass1\nBackground:\nGiven def a = 1\n\nScenario: skip-pass\n* karate.abort()\n* assert a == 1\n\nScenario: skip-fail\n* karate.abort()\n* assert a == 2\n\nScenario: noskip\nThen assert a != 3\nAnd assert a != 4\n\nScenario: skip-pass-config\n* configure abortedStepsShouldPass = true\n* karate.abort()\n* assert a == 5\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/called-arg-loop.feature",
    "content": "@ignore\nFeature:\n\nScenario: loop arg\n* match __arg == karate.get('foos[' + __loop + ']')\n\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/called-arg-null.feature",
    "content": "@ignore\nFeature:\n\nScenario: null arg\n* match __arg == null\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/called-arg-single.feature",
    "content": "@ignore\nFeature:\n\nScenario: single arg\n* match __arg == { foo: 'bar' }\n* match foo == 'bar'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/called-shared.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* match foo == 'bar'\n* match __arg == { foo: 'bar' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/called-shared2.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* match foo == __loop\n* match __arg == { foo: '#(__loop)' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/called.feature",
    "content": "@ignore\nFeature: called feature\n\nScenario: called scenario\n* assert input != 4\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/called_2.feature",
    "content": "Feature:\n\n@foo=bar1\nScenario:\n* print 'bar1-1'\n* print 'bar1-2'\n\n@foo=bar2\nScenario:\n* print 'bar2-1'\n* print 'bar2-2'\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/caller-arg.feature",
    "content": "Feature:\n\nScenario: null arg\n* def result = call read('called-arg-null.feature')\n\nScenario: single arg\n* def result = call read('called-arg-single.feature') { foo: 'bar' }\n\nScenario: loop arg\n* table foos\n| foo   |\n| 'bar' |\n| 'baz' |\n| 'ban' |\n* def result = call read('called-arg-loop.feature') foos\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/caller-shared.feature",
    "content": "Feature:\n\nScenario:\n# in 'shared scope' mode, it actually does not make make sense to pass arguments \n# other than for readability - the variable 'foo' is going to get set 'globally'\n* call read('called-shared.feature') { foo: 'bar' }\n\nScenario:\n* call read('called-shared2.feature') [{ foo: 0 }, { foo: 1 }]\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/caller-with-lambda-arg.feature",
    "content": "Feature:\n\nBackground:\n  * def dataFunc =\n  \"\"\"\n    function() {\n      var element = {\n        \"something\": \"value\"\n      };\n\n      var intBinaryOperator = Java.type('java.util.function.IntBinaryOperator');\n      var plusOperation = Java.extend(intBinaryOperator, {\n          applyAsInt: function(left, right) {\n              return left + right;\n          }\n      });\n\n      var featureResultTestClass = Java.type('com.intuit.karate.core.runner.FeatureResultTest');\n      featureResultTestClass.addLambdaFunctionToMap(element);\n      element.sum = new plusOperation();\n\n      return element;\n    }\n  \"\"\"\n  * def elem = dataFunc()\n  * def data = [ \"#(elem)\" ]\n\nScenario:\n  # ensuring the called.feature returns success\n  # passing data with a functional interface should be correctly printed\n  # in Result obj\n  * def input = 3\n  * def result = call read('called.feature') data\n  * match data[0].something == \"value\"\n  * match data[0].javaSum(1,3) == 4\n  * match data[0].sum(1,3) == 4\n\n  * def left = 1\n  * def right = 2\n  * def payload = { \"leftSide\": '#(left)', \"rightSide\": '#(right)', \"sum\": '#(data[0].sum(left, right))' }\n  * match payload == { \"leftSide\": 1, \"rightSide\": 2, \"sum\": '#? _ == 1+2' }\n  * match payload == { \"leftSide\": 1, \"rightSide\": 2, \"sum\": '#? _ == data[0].sum( $.leftSide, $.rightSide)' }\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/caller.feature",
    "content": "Feature:\n\nBackground:\n* table data\n    | input |\n    | 1     |\n    | 2     |\n    | 3     |\n    | 4     |\n    | 5     |\n\nScenario:\n* def result = call read('called.feature') data\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/caller_2.feature",
    "content": "Feature:\n\nScenario:\n* def before = 'before'\n* print 1 + 2\n* def callresult = call read('called_2.feature@foo=bar2')\n* def after = 'after'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/failed.feature",
    "content": "Feature: fail2pass1\nBackground:\nGiven def a = 1\n\nScenario: failure1\nThen assert a != 1\nAnd assert a == 2\n\nScenario: failure2\nThen assert a != 2\nAnd assert a == 3\n\nScenario: pass1\nThen assert a != 4\nAnd assert a != 5\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/HooksTest.java",
    "content": "package com.intuit.karate.core.runner.hooks;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class HooksTest {\n\n    @Test\n    void testDynamicOutlineHook() {\n        TestRuntimeHook testRuntimeHook = new TestRuntimeHook();\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/runner/hooks/hook-dynamic-outline.feature\")\n                .hook(testRuntimeHook)\n                .configDir(\"classpath:com/intuit/karate/core/hooks\")\n                .parallel(1);\n        assertEquals(0, results.getFailCount());\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeSuite\").get(\"suite\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterSuite\").get(\"suite\"));\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeFeature\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterFeature\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(3, testRuntimeHook.getRuntimeHookTracker().get(\"beforeScenario\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(3, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertTrue(testRuntimeHook.getRuntimeHookTracker().get(\"beforeStep\").values().stream().mapToInt(Integer::intValue).sum() > 0);\n        assertTrue(testRuntimeHook.getRuntimeHookTracker().get(\"afterStep\").values().stream().mapToInt(Integer::intValue).sum() > 0);\n    }\n\n    @Test\n    void testMultipleDynamicOutlineMultipleTablesHook() {\n        TestRuntimeHook testRuntimeHook = new TestRuntimeHook();\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/runner/hooks/hook-multiple-dynamic-outline.feature\")\n                .hook(testRuntimeHook)\n                .configDir(\"classpath:com/intuit/karate/core/hooks\")\n                .parallel(1);\n        assertEquals(0, results.getFailCount());\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeSuite\").get(\"suite\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterSuite\").get(\"suite\"));\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeFeature\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterFeature\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(12, testRuntimeHook.getRuntimeHookTracker().get(\"beforeScenario\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(12, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertTrue(testRuntimeHook.getRuntimeHookTracker().get(\"beforeStep\").values().stream().mapToInt(Integer::intValue).sum() > 0);\n        assertTrue(testRuntimeHook.getRuntimeHookTracker().get(\"afterStep\").values().stream().mapToInt(Integer::intValue).sum() > 0);\n    }\n\n    @Test\n    void testMultipleDynamicOutlineMultipleTablesTagSelectHook() {\n        TestRuntimeHook testRuntimeHook = new TestRuntimeHook();\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/runner/hooks/hook-multiple-dynamic-outline.feature\")\n                .hook(testRuntimeHook)\n                .tags(\"@tagged\")\n                .configDir(\"classpath:com/intuit/karate/core/hooks\")\n                .parallel(1);\n        assertEquals(0, results.getFailCount());\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeSuite\").get(\"suite\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterSuite\").get(\"suite\"));\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeFeature\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterFeature\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(12, testRuntimeHook.getRuntimeHookTracker().get(\"beforeScenario\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(12, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").values().stream().mapToInt(Integer::intValue).sum());\n\n        // note how before scenario does not evaluate yet the name of the scenario, allowing you to inject stuff into it potentially?\n        assertEquals(7, testRuntimeHook.getRuntimeHookTracker().get(\"beforeScenario\").get(\"dogs: ${name}\"));\n        assertEquals(2, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").get(\"dogs: dog1\"));\n        assertEquals(2, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").get(\"dogs: dog2\"));\n        assertEquals(2, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").get(\"dogs: dog3\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").get(\"dogs: dog4\"));\n\n        assertEquals(2, testRuntimeHook.getRuntimeHookTracker().get(\"beforeScenario\").get(\"cats: ${name}\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").get(\"cats: cat1\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").get(\"cats: cat2\"));\n\n        assertTrue(testRuntimeHook.getRuntimeHookTracker().get(\"beforeStep\").values().stream().mapToInt(Integer::intValue).sum() > 0);\n        assertTrue(testRuntimeHook.getRuntimeHookTracker().get(\"afterStep\").values().stream().mapToInt(Integer::intValue).sum() > 0);\n    }\n\n    @Test\n    void testDynamicOutlineHookNoStepExecution() {\n        NoStepTestRuntimeHook testRuntimeHook = new NoStepTestRuntimeHook();\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/runner/hooks/hook-dynamic-outline.feature\")\n                .hook(testRuntimeHook)\n                .configDir(\"classpath:com/intuit/karate/core/hooks\")\n                .parallel(1);\n\n        // yes it will fail because we're not executing steps so the background '* def cats' won't be evaluated\n        assertEquals(1, results.getFailCount());\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeSuite\").get(\"suite\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterSuite\").get(\"suite\"));\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeFeature\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterFeature\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeScenario\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"beforeStep\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"afterStep\").values().stream().mapToInt(Integer::intValue).sum());\n    }\n\n    @Test\n    void testDynamicOutlineHookNoScenarioExecution() {\n        NoScenarioTestRuntimeHook testRuntimeHook = new NoScenarioTestRuntimeHook();\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/runner/hooks/hook-dynamic-outline.feature\")\n                .hook(testRuntimeHook)\n                .configDir(\"classpath:com/intuit/karate/core/hooks\")\n                .parallel(1);\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeSuite\").get(\"suite\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterSuite\").get(\"suite\"));\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeFeature\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterFeature\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeScenario\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").values().stream().mapToInt(Integer::intValue).sum());\n\n        // needed to provide the value on the Examples table\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"beforeStep\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"afterStep\").values().stream().mapToInt(Integer::intValue).sum());\n    }\n\n    @Test\n    void testDynamicOutlineHookNoFeatureExecution() {\n        NoFeatureTestRuntimeHook testRuntimeHook = new NoFeatureTestRuntimeHook();\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/runner/hooks/hook-dynamic-outline.feature\")\n                .hook(testRuntimeHook)\n                .configDir(\"classpath:com/intuit/karate/core/hooks\")\n                .parallel(1);\n\n        assertEquals(0, results.getFailCount());\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeSuite\").get(\"suite\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterSuite\").get(\"suite\"));\n\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"beforeFeature\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"afterFeature\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"beforeScenario\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"beforeStep\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"afterStep\").values().stream().mapToInt(Integer::intValue).sum());\n    }\n\n    @Test\n    void testOutlineHookNoStepExecutionWithoutError() {\n        NoStepTestRuntimeHook testRuntimeHook = new NoStepTestRuntimeHook();\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/runner/hooks/hook-outline.feature\")\n                .hook(testRuntimeHook)\n                .configDir(\"classpath:com/intuit/karate/core/hooks\")\n                .parallel(1);\n\n        assertEquals(0, results.getFailCount());\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeSuite\").get(\"suite\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterSuite\").get(\"suite\"));\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeFeature\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterFeature\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(2, testRuntimeHook.getRuntimeHookTracker().get(\"beforeScenario\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(2, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"beforeStep\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(0, testRuntimeHook.getRuntimeHookTracker().get(\"afterStep\").values().stream().mapToInt(Integer::intValue).sum());\n    }\n\n    // non-dynamic outline tests\n\n    @Test\n    void testOutlineHook() {\n        TestRuntimeHook testRuntimeHook = new TestRuntimeHook();\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/runner/hooks/hook-outline.feature\")\n                .hook(testRuntimeHook)\n                .configDir(\"classpath:com/intuit/karate/core/hooks\")\n                .parallel(1);\n\n        assertEquals(0, results.getFailCount());\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeSuite\").get(\"suite\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterSuite\").get(\"suite\"));\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeFeature\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterFeature\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(2, testRuntimeHook.getRuntimeHookTracker().get(\"beforeScenario\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(2, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertTrue(testRuntimeHook.getRuntimeHookTracker().get(\"beforeStep\").values().stream().mapToInt(Integer::intValue).sum() > 0);\n        assertTrue(testRuntimeHook.getRuntimeHookTracker().get(\"afterStep\").values().stream().mapToInt(Integer::intValue).sum() > 0);\n    }\n\n    @Test\n    void testScenarioHook() {\n        TestRuntimeHook testRuntimeHook = new TestRuntimeHook();\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/runner/hooks/hook-scenario.feature\")\n                .hook(testRuntimeHook)\n                .configDir(\"classpath:com/intuit/karate/core/hooks\")\n                .parallel(1);\n\n        assertEquals(0, results.getFailCount());\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeSuite\").get(\"suite\"));\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterSuite\").get(\"suite\"));\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeFeature\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterFeature\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"beforeScenario\").values().stream().mapToInt(Integer::intValue).sum());\n        assertEquals(1, testRuntimeHook.getRuntimeHookTracker().get(\"afterScenario\").values().stream().mapToInt(Integer::intValue).sum());\n\n        assertTrue(testRuntimeHook.getRuntimeHookTracker().get(\"beforeStep\").values().stream().mapToInt(Integer::intValue).sum() > 0);\n        assertTrue(testRuntimeHook.getRuntimeHookTracker().get(\"afterStep\").values().stream().mapToInt(Integer::intValue).sum() > 0);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/MandatoryTagHook.java",
    "content": "package com.intuit.karate.core.runner.hooks;\n\nimport com.intuit.karate.core.Tag;\nimport com.intuit.karate.RuntimeHook;\nimport com.intuit.karate.core.ScenarioRuntime;\n\n/**\n *\n * @author pthomas3\n */\npublic class MandatoryTagHook implements RuntimeHook {\n\n    @Override\n    public boolean beforeScenario(ScenarioRuntime sr) {\n        if (sr.caller.depth > 0) {\n            return true; // only enforce tags for top-level scenarios (not called ones)\n        }\n        boolean found = false;\n        for (Tag tag : sr.tags) {\n            if (\"testId\".equals(tag.getName())) {\n                found = true;\n                break;\n            }\n        }\n        if (!found) {\n            throw new RuntimeException(\"testId tag not present at line: \" + sr.scenario.getLine());\n        }\n        return true;\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/NoFeatureTestRuntimeHook.java",
    "content": "package com.intuit.karate.core.runner.hooks;\n\nimport com.intuit.karate.core.FeatureRuntime;\n\npublic class NoFeatureTestRuntimeHook extends TestRuntimeHook {\n\n    @Override\n    public boolean beforeFeature(FeatureRuntime fr) {\n        return false;\n    }\n\n    @Override\n    public void afterFeature(FeatureRuntime fr) {\n        // don't count if if not executing\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/NoScenarioTestRuntimeHook.java",
    "content": "package com.intuit.karate.core.runner.hooks;\n\nimport com.intuit.karate.core.ScenarioRuntime;\n\npublic class NoScenarioTestRuntimeHook extends TestRuntimeHook {\n\n    @Override\n    public boolean beforeScenario(ScenarioRuntime sr) {\n        super.beforeScenario(sr);\n        return false;\n    }\n\n    @Override\n    public void afterScenario(ScenarioRuntime sr) {\n        // don't count if not executing\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/NoStepTestRuntimeHook.java",
    "content": "package com.intuit.karate.core.runner.hooks;\n\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.core.Step;\nimport com.intuit.karate.core.StepResult;\n\npublic class NoStepTestRuntimeHook extends TestRuntimeHook {\n\n    @Override\n    public boolean beforeStep(Step step, ScenarioRuntime sr) {\n        return false;\n    }\n\n    @Override\n    public void afterStep(StepResult result, ScenarioRuntime sr) {\n        // don't count if if not executing\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/ScenarioHookSkipTest.java",
    "content": "package com.intuit.karate.core.runner.hooks;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass ScenarioHookSkipTest {\n\n    @Test\n    void testStopIfScenarioHasNoTags() {\n        String path = \"classpath:com/intuit/karate/core/runner/hooks/test-hook-skip.feature\";\n        Results results = Runner.path(path).hook(new SkipHook()).parallel(1);\n        assertEquals(1, results.getFeaturesTotal());\n        assertEquals(1, results.getScenariosPassed());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/ScenarioHookTest.java",
    "content": "package com.intuit.karate.core.runner.hooks;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass ScenarioHookTest {\n\n    @Test\n    void testStopIfScenarioHasNoTags() {\n        String path = \"classpath:com/intuit/karate/core/runner/hooks/test-hook-notags.feature\";\n        Results results = Runner.path(path).hook(new MandatoryTagHook()).parallel(1);\n        assertEquals(1, results.getFeaturesTotal());\n        assertEquals(1, results.getFailCount());\n    }\n\n    @Test\n    void testHookForExamplesWithTags() {\n        String path = \"classpath:com/intuit/karate/core/runner/hooks/test-hook-multiexample.feature\";\n        Results results = Runner.path(path).hook(new MandatoryTagHook()).parallel(1);\n        assertEquals(1, results.getFeaturesTotal());\n        assertEquals(7, results.getScenariosTotal());\n        assertEquals(0, results.getFailCount());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/SkipHook.java",
    "content": "package com.intuit.karate.core.runner.hooks;\n\nimport com.intuit.karate.RuntimeHook;\nimport com.intuit.karate.core.ScenarioRuntime;\n\n/**\n *\n * @author peter\n */\npublic class SkipHook implements RuntimeHook {\n\n    @Override\n    public boolean beforeScenario(ScenarioRuntime sr) {\n        return sr.scenario.getName().contains(\"one\");\n    }        \n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/TestRuntimeHook.java",
    "content": "package com.intuit.karate.core.runner.hooks;\n\nimport com.intuit.karate.RuntimeHook;\nimport com.intuit.karate.Suite;\nimport com.intuit.karate.core.FeatureRuntime;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.core.Step;\nimport com.intuit.karate.core.StepResult;\nimport com.intuit.karate.http.HttpRequest;\nimport com.intuit.karate.http.Response;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class TestRuntimeHook implements RuntimeHook {\n\n    private final Map<String, Map<String, Integer>> runtimeHookTracker = new HashMap<>();\n\n    public TestRuntimeHook() {\n        runtimeHookTracker.put(\"beforeScenario\", new HashMap<>());\n        runtimeHookTracker.put(\"afterScenario\", new HashMap<>());\n        runtimeHookTracker.put(\"beforeFeature\", new HashMap<>());\n        runtimeHookTracker.put(\"afterFeature\", new HashMap<>());\n        runtimeHookTracker.put(\"beforeStep\", new HashMap<>());\n        runtimeHookTracker.put(\"afterStep\", new HashMap<>());\n        runtimeHookTracker.put(\"beforeSuite\", new HashMap<>());\n        runtimeHookTracker.put(\"afterSuite\", new HashMap<>());\n    }\n\n    public Map<String, Map<String, Integer>> getRuntimeHookTracker() {\n        return this.runtimeHookTracker;\n    }\n\n    @Override\n    public boolean beforeScenario(ScenarioRuntime sr) {\n        runtimeHookTracker.get(\"beforeScenario\").compute(sr.scenario.getName(), (key, count) -> count == null ? 1 : count + 1);\n        return true;\n    }\n\n    @Override\n    public void afterScenario(ScenarioRuntime sr) {\n        runtimeHookTracker.get(\"afterScenario\").compute(sr.scenario.getName(), (key, count) -> count == null ? 1 : count + 1);\n    }\n\n    @Override\n    public boolean beforeFeature(FeatureRuntime fr) {\n        runtimeHookTracker.get(\"beforeFeature\").compute(fr.featureCall.feature.getName(), (key, count) -> count == null ? 1 : count + 1);\n        return true;\n    }\n\n    @Override\n    public void afterFeature(FeatureRuntime fr) {\n        runtimeHookTracker.get(\"afterFeature\").compute(fr.featureCall.feature.getName(), (key, count) -> count == null ? 1 : count + 1);\n    }\n\n    @Override\n    public void beforeSuite(Suite suite) {\n        runtimeHookTracker.get(\"beforeSuite\").compute(\"suite\", (key, count) -> count == null ? 1 : count + 1);\n    }\n\n    @Override\n    public void afterSuite(Suite suite) {\n        runtimeHookTracker.get(\"afterSuite\").compute(\"suite\", (key, count) -> count == null ? 1 : count + 1);\n    }\n\n    @Override\n    public boolean beforeStep(Step step, ScenarioRuntime sr) {\n        runtimeHookTracker.get(\"beforeStep\").compute(\"[ scenario = \" + sr.scenario.getName() + \" / step = \" + step.getText() + \" ]\", (key, count) -> count == null ? 1 : count + 1);\n        return true;\n    }\n\n    @Override\n    public void afterStep(StepResult result, ScenarioRuntime sr) {\n        runtimeHookTracker.get(\"afterStep\").compute(\"[ scenario = \" + sr.scenario.getName() + \" / step = \" + result.getStep().getText() + \" ]\", (key, count) -> count == null ? 1 : count + 1);\n    }\n\n    @Override\n    public void beforeHttpCall(HttpRequest request, ScenarioRuntime sr) {\n\n    }\n\n    @Override\n    public void afterHttpCall(HttpRequest request, Response response, ScenarioRuntime sr) {\n\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/hook-dynamic-outline.feature",
    "content": "Feature: Test Hook Feature\n\n@setup\nScenario:\n  * def cats = [{name: 'cat1'}, {name: 'cat2'}]\n\nScenario Outline: cats: ${name}\n  * match name == '<name>'\n  Examples:\n    | karate.setup().cats |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/hook-multiple-dynamic-outline.feature",
    "content": "@tagged\nFeature: Test Hook Feature\n\n@setup\nScenario:\n  * def cats = [{name: 'cat1'}, {name: 'cat2'}]\n  * def dogs = [{name: 'dog1'}, {name: 'dog2'}, {name: 'dog3'}]\n  * def taggedDogs = [{name: 'dog1'}, {name: 'dog2'}, {name: 'dog3'}, {name: 'dog4'}]\n\nScenario Outline: cats: ${name}\n  * match name == '<name>'\n  Examples:\n    | karate.setup().cats |\n\nScenario Outline: dogs: ${name}\n  * match name == '<name>'\n  Examples:\n    | karate.setup().dogs  |\n  @anothertag\n  Examples:\n    | karate.setup().taggedDogs  |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/hook-multiple-outlines.feature",
    "content": "Feature: Test Hook Feature\n\nBackground:\n\nScenario Outline: cats: ${name}\n  * match name == \"<name>\"\n  Examples:\n    | name |\n    | Mylo |\n    | Oscar |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/hook-outline.feature",
    "content": "Feature: Test Hook Feature\n\nBackground:\n\nScenario Outline: cats: ${name}\n  * match name == \"<name>\"\n  Examples:\n    | name |\n    | Mylo |\n    | Oscar |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/hook-scenario.feature",
    "content": "Feature: Test Hook Feature\n\nBackground:\n  * def name = \"Mylo\"\n\nScenario: cats: Mylo\n  * match name == \"Mylo\"\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/karate-base.js",
    "content": "function fn() {   \n  return { functionFromHookKarateBase: function(){ return 'fromHookKarateBase' } }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/karate-config.js",
    "content": "function fn() {\n  var config = {};\n  config.hookTest = true;\n  return config;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/test-hook-multiexample.feature",
    "content": "Feature: Tag Filter multi-scenario-outline feature.\n# Note, in this feature, tags are used in both scenario outlines. This should pass the tag filter which is checking that there are tags defined.\n\n  Background:\n\n  @domain=TXN-WORKFLOW\n  Scenario Outline: my outline 1\n    * print 'I am in outline 1.'\n\n  @testId=1\n    Examples:\n      | input|\n      | true |\n\n  @testId=2\n    Examples:\n      | input|\n      | true |\n\n  @testId=3\n    Examples:\n      | input|\n      | true |\n\n  @testId=4\n    Examples:\n      | input|\n      | true |\n\n  @testId=5\n    Examples:\n      | input|\n      | true |\n\n  @domain=TXN-WORKFLOW\n  Scenario Outline: my outline 2\n    * print 'I am in outline 2.'\n\n  @testId=6\n    Examples:\n      | input|\n      | true |\n\n  @testId=7\n    Examples:\n      | input|\n      | true |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/test-hook-notags.feature",
    "content": "Feature: no tags feature\n\n  Scenario: no tags scenario\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/hooks/test-hook-skip.feature",
    "content": "Feature:\n\nScenario: one\n* print 'one'\n\nScenario: two\n* print 'two'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/multi-scenario-fail.feature",
    "content": "Feature: multi scenario\n\nBackground:\nGiven def a = 1\n\nScenario: first \nThen assert a != 1\n\nScenario: second \nThen assert a != 2\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/multi-scenario.feature",
    "content": "Feature: multi scenario\n\nBackground:\nGiven def a = 1\n\nScenario: first \nThen assert a == 1\n\nScenario: second \nThen assert a != 2\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/no-scenario-name.feature",
    "content": "Feature:\n\nScenario:\n\n* assert 1 != 2\n\nScenario:\n\n* assert 2 == 2\n\nScenario Outline:\n\n* assert val != 3\n\nExamples:\n| val! |\n| 1    |\n| 2    |\n| 3    |\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/notEqualMatch.feature",
    "content": "Feature: not equal match test file\n\n# some comment\n\n  Background:\n    Given def a = 456\n    Given def b = 120\n\n  Scenario: test\n    Then match a != b\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/outline.feature",
    "content": "Feature: simple feature file\n\nScenario Outline: test\n\nWhen def a = <val>\nThen assert a == <val>\n\nExamples:\n| val |\n|   1 |\n|  10 |\n|  42 |\n|  55 |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/run-arg.feature",
    "content": "Feature:\n\nScenario:\n* print 'foo is:', foo\n* def message = foo + ' world'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/run-ignore.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* print 'foo is:', foo\n* def message = foo + ' world'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/scenario.feature",
    "content": "Feature: simple feature file\n\n# some comment\n\nBackground:\nGiven def a = 1\n\nScenario: test\nThen assert a == 1\n# another comment\nWhen def b = \n\"\"\"\n{ foo: 'bar' }\n\"\"\"\nThen match b == { foo: 'bar'}\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/signin.feature",
    "content": "Feature:\n\nScenario:\n* def iamTicket = { ticket: { foo: 'bar' } }\n* def ticket = iamTicket.ticket\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/stackoverflow-error.feature",
    "content": "Feature:\n  Scenario: StackOverflowError\n    * def s = \"\\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\n\"\n    * match s == \"#regex (.|\\n)*\""
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/table.feature",
    "content": "Feature:\n\nScenario:\n* table cats\n    | name   | age |\n    | 'Bob'  | 2   |\n    | 'Wild' | 4   |\n    | 'Nyan' | 3   |\n\n* match cats == [{name: 'Bob', age: 2}, {name: 'Wild', age: 4}, {name: 'Nyan', age: 3}]\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/test-called-embedded-file.feature",
    "content": "Feature:\n\nBackground:\n* def json = read('test.json')\n\nScenario:\n* def xml = read('test.xml')\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/test-called-embedded.feature",
    "content": "Feature:\n\nBackground:\n* def json = { hello: '#(foo)' }\n\nScenario:\n* def xml = <hello>#(foo)</hello>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/test-called.feature",
    "content": "Feature:\n\nBackground:\n* def a = 1\n\nScenario:\n* def b = 2\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/test.json",
    "content": "{ \"hello\": \"#(foo)\" }"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/runner/test.xml",
    "content": "<hello>#(foo)</hello>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/scenario-outline-result.feature",
    "content": "Feature: my feature\n\nScenario: single scenario\n    * match karate.scenarioOutline == null\n\nScenario Outline: outline\n    description of outline\n\n    # Confirm example index within table\n    * match __num == <num>\n    * match karate.scenario.exampleIndex == <num>\n\n    # Confirm scenarioOutline result\n    * match karate.scenarioOutline == \n    \"\"\"\n        {\n            \"sectionIndex\": 1,\n            \"numScenariosToExecute\": 4,\n            \"exampleTableCount\": 2,\n            \"line\": 6,\n            \"name\": \"outline\",\n            \"numScenariosExecuted\": <executedSoFar>,\n            \"description\": \"description of outline\",\n            \"exampleTables\": [\n                {\n                    \"data\": [\n                        {num: 0, executedSoFar: 1},\n                        {num: 1, executedSoFar: 2},\n                    ],\n                    \"tags\": [\"@one\", \"@two\"]\n                },\n                {\n                    \"data\": [\n                        {num: 0, executedSoFar: 3},\n                        {num: 1, executedSoFar: 4},\n                    ],\n                    \"tags\": [\"@three\", \"@four\"]\n                },\n            ],\n            \"scenarioResults\": \"#[<executedSoFar>] #object\"\n        }\n    \"\"\"\n\n    @one @two\n    Examples:\n    | num! | executedSoFar! |\n    | 0    | 1              |\n    | 1    | 2              |\n\n    @three @four\n    Examples:\n    | num! | executedSoFar! |\n    | 0    | 3              |\n    | 1    | 4              |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/scenario-variable-scope.feature",
    "content": "Feature:\n\nBackground:\n* def a = 1\n* def fun = function(){ return {} }\n* def c = callonce fun\n\nScenario:\n    * assert a == 1\n    * def a = 2\n    * def b = 3\n    * match c == {}\n    * set c.foo = 'bar'\n    # the callonce in the background is a snapshot at THAT point in time\n    # so the next scenario should \"rewind\" to that state\n\nScenario:\n    * assert a == 1\n    * assert typeof b == 'undefined'\n    # get else default value\n    * def b = karate.get('b', 42)\n    * match b == 42\n    * match c == {}\n    "
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/schema-like-odds.json",
    "content": "{\n   \"id\":\"390\",\n   \"count\": 4,\n   \"odd\":{\n      \"price\":\"1.20\",\n      \"status\":2,\n      \"ck\":12.2,\n      \"name\":\"2\"\n   },\n   \"data\":{\n      \"countryId\":35,\n      \"countryName\":\"Norway\",\n      \"status\":0,\n      \"sportName\":\"Soccer\",\n      \"time\":\"2016-06-12T12:00:00Z\"\n   },\n   \"odds\":[\n      {\n         \"price\":\"1.30\",\n         \"status\":0,\n         \"ck\":12.2,\n         \"name\":\"1\"\n      },\n      {\n         \"price\":\"5.25\",\n         \"status\":1,\n         \"name\":\"X\"\n      },\n      {\n         \"price\":\"2.70\",\n         \"status\":0,\n         \"ck\":12.2,\n         \"name\":\"0\"\n      },\n      {\n         \"price\":\"1.20\",\n         \"status\":2,\n         \"name\":\"2\"\n      }\n   ]\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/schema-like-time-validator.js",
    "content": "function fn(s) {\n  var SimpleDateFormat = Java.type(\"java.text.SimpleDateFormat\");\n  var sdf = new SimpleDateFormat(\"yyyy-MM-dd'T'HH:mm:ss'Z'\");\n  try {\n    sdf.parse(s).time;\n    return true;\n  } catch(e) {\n    karate.log('*** invalid date string:', s);\n    return false;\n  }\n} \n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/schema-like.feature",
    "content": "Feature: json-schema like validation\n\nScenario: but simpler and more powerful\n\n* def response = read('schema-like-odds.json')\n# here we enclose in round-brackets to preserve the optional embedded expression\n# so that it can be used later in a \"match\"\n* def oddSchema = ({ price: '#string', status: '#? _ < 3', ck: '##number', name: '#regex[0-9X]' })\n* def isValidTime = read('schema-like-time-validator.js')\n\nThen match response ==\n\"\"\"\n{ \n  id: '#regex[0-9]+',\n  count: '#number',\n  odd: '#(oddSchema)',\n  data: { \n    countryId: '#number', \n    countryName: '#string', \n    leagueName: '##string', \n    status: '#number? _ >= 0', \n    sportName: '#string',\n    time: '#? isValidTime(_)'\n  },\n  odds: '#[] oddSchema'  \n}\n\"\"\"\n# other examples\n\n# should be an array\n* match $.odds == '#[]'\n\n# should be an array of size 4\n* match $.odds == '#[4]'\n\n# optionally present (or null) and should be an array of size greater than zero\n* match $.odds == '##[_ > 0]'\n\n# should be an array of size equal to $.count\n* match $ contains { odds: '#[$.count]' }\n\n# use a predicate function to validate each array element\n* def isValidOdd = function(o){ return o.name.length == 1 }\n* match $.odds == '#[]? isValidOdd(_)'\n\n# for simple arrays, types can be 'in-line'\n* def foo = ['bar', 'baz']\n\n# should be an array\n* match foo == '#[]'\n\n# should be an array of size 2\n* match foo == '#[2]'\n\n# should be an array of strings with size 2\n* match foo == '#[2] #string'\n\n# each item of the array should be of length 3\n* match foo == '#[]? _.length == 3'\n\n# should be an array of strings each of length 3\n* match foo == '#[] #string? _.length == 3'\n\n# should be null or an array of strings\n* match foo == '##[] #string'\n\n# each item of the array should match regex (with backslash involved)\n* match foo == '#[] #regex \\\\w+'\n\n# contains\n* def actual = [{ a: 1, b: 'x' }, { a: 2, b: 'y' }]\n\n* def schema = { a: '#number', b: '#string' }\n* def partSchema = { a: '#number' }\n* def badSchema = { c: '#boolean' }\n* def mixSchema = { a: '#number', c: '#boolean' }\n\n* def shuffled = [{ a: 2, b: 'y' }, { b: 'x', a: 1 }]\n* def first = { a: 1, b: 'x' }\n* def part = { a: 1 }\n* def mix = { b: 'y', c: true }\n* def other = [{ a: 3, b: 'u' }, { a: 4, b: 'v' }]\n* def some = [{ a: 1, b: 'x' }, { a: 5, b: 'w' }]\n\n* match actual[0] == schema\n* match actual[0] == '#(schema)'\n\n* match actual[0] contains partSchema\n* match actual[0] == '#(^partSchema)'\n\n* match actual[0] contains any mixSchema\n* match actual[0] == '#(^*mixSchema)'\n\n* match actual[0] !contains badSchema\n* match actual[0] == '#(!^badSchema)'\n\n* match each actual == schema\n* match actual == '#[] schema'\n\n* match each actual contains partSchema\n* match actual == '#[] ^partSchema'\n\n* match each actual contains any mixSchema\n* match actual == '#[] ^*mixSchema'\n\n* match each actual !contains badSchema\n* match actual == '#[] !^badSchema'\n\n* match actual contains only shuffled\n* match actual == '#(^^shuffled)'\n\n* match actual contains first\n* match actual == '#(^first)'\n\n* match actual contains any some\n* match actual == '#(^*some)'\n\n* match actual !contains other\n* match actual == '#(!^other)'\n\n* match actual contains deep part\n* match actual == '#(^+part)'\n\n# no in-line equivalent !\n* match actual contains '#(^part)'\n\n# no in-line equivalent !\n* match actual contains '#(^*mix)'\n\n* assert actual.length == 2\n* match actual == '#[2]'\n\n# contains deep\n* def actualDeep = [{ a: [1, 2], b: 'x' }, { a: [3, 4], b: 'y' }]\n\n* def partDeep = { a: [1] }\n* match actual contains deep part\n\nScenario: complex nested arrays\n* def json =\n\"\"\"\n{\n  \"foo\": {\n    \"bars\": [\n      { \"barOne\": \"dc\", \"barTwos\": [{ title: 'blah' }] },\n      { \"barOne\": \"dc\", \"barTwos\": [{ title: 'blah' }], barThrees: [{ num: 1 }] }\n    ]\n  }\n}\n\"\"\"\n* def barTwo = { title: '#string' }\n* def barThree = { num: '#number' }\n* def bar = { barOne: '#string', barTwos: '#[] barTwo', barThrees: '##[] barThree' }\n* match json.foo.bars == '#[] bar'\n\nScenario: re-usable json chunks as nodes, but optional\n* def dogSchema = { id: '#string', color: '#string' }\n# here we enclose in round-brackets to preserve the optional embedded expression\n# so that it can be used later in a \"match\"\n* def schema = ({ id: '#string', name: '#string', dog: '##(dogSchema)' })\n\n* def response1 = { id: '123', name: 'foo' }\n* match response1 == schema\n\n* def response2 = { id: '123', name: 'foo', dog: { id: '456', color: 'brown' } }\n* match response2 == schema\n\nScenario: pretty print json\n* def json = read('schema-like-odds.json')\n* print 'pretty print:\\n' + karate.pretty(json)\n\nScenario: more pretty print\n* def myJson = { foo: 'bar', baz: [1, 2, 3]}\n* print 'pretty print:\\n' + karate.pretty(myJson)\n\nScenario: various ways of checking that a string ends with a number\n* def foo = 'hello1'\n* match foo == '#regex hello[0-9]+'\n* match foo == '#regex .+[0-9]+'\n* match foo contains 'hello'\n* assert foo.startsWith('hello')\n* def isHello = function(s){ return s.startsWith('hello') && karate.match(s, '#regex .+[0-9]+').pass }\n* match foo == '#? isHello(_)'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/schema-read-inner.json",
    "content": "{\n    \"a\": \"#number\"\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/schema-read.feature",
    "content": "Feature:\n\n  Scenario:\n    * def schema = \"#[] read('schema-read.json')\"\n    * def response = [{ foo: 'bar', items: [{ a: 1 }] }]\n    * match response == schema\n    * configure matchEachEmptyAllowed = true\n    * def response = [{ foo: 'bar', items: [] }]\n    * match response == schema"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/schema-read.json",
    "content": "{\n    \"foo\": \"#string\",\n    \"items\": \"#[] #(read('schema-read-inner.json'))\"\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/set-xml.feature",
    "content": "Feature:\n\nScenario: set via table, variable and xml nodes will be auto-built\n    * set search /acc:getAccountByPhoneNumber\n    | path                        | value |\n    | acc:phoneNumber             | 1234  |   \n    | acc:phoneNumberSearchOption | 'all' |\n\n    * match search ==\n    \"\"\"\n    <acc:getAccountByPhoneNumber>\n        <acc:phoneNumber>1234</acc:phoneNumber>\n        <acc:phoneNumberSearchOption>all</acc:phoneNumberSearchOption>        \n    </acc:getAccountByPhoneNumber>\n    \"\"\"\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/set.feature",
    "content": "Feature:\n\nScenario:\n* set cat\n| path   | value |\n| name   | 'Bob' |\n| age    | 5     |\n\n* match cat == { name: 'Bob', age: 5 }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/single-scenario.feature",
    "content": "Feature: sample feature with distinct signature for each scenario\n\n  @Scenario1\n  Scenario: Set a Variable to \"One\"\n    * def result1 = 'One'\n\n  @Scenario2\n  Scenario: Set a Variable to \"Two\"\n    * def result2 = 'Two'\n\n  @Scenario3\n  Scenario: Set a Variable to \"Three\"\n    * def result3 = 'Three'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/sort-array.feature",
    "content": "Feature: example of js java hybrid sort\n\nScenario: case-insensitive sort\n    * def ArrayList = Java.type('java.util.ArrayList')\n    * def Collections = Java.type('java.util.Collections')\n    * def json = [{ v: 'C' }, { v: 'b' }, { v: 'A' }]\n    * def actual = $json[*].v\n    * match actual == ['C', 'b', 'A']\n    * def list = new ArrayList()\n    * karate.repeat(actual.length, function(i){ list.push(actual[i]) })\n    * match list == ['C', 'b', 'A']\n    * Collections.sort(list, java.lang.String.CASE_INSENSITIVE_ORDER)\n    * match list == ['A', 'b', 'C']\n\nScenario: the example above implemented using a re-usable js function\n    * def json = [{ v: 'C' }, { v: 'b' }, { v: 'A' }]\n    * def actual = $json[*].v\n    * match actual == ['C', 'b', 'A']\n    * def sorted = call read('sort-array.js') actual\n    * match sorted == ['A', 'b', 'C']\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/sort-array.js",
    "content": "function fn(array) {\n  var ArrayList = Java.type('java.util.ArrayList')\n  var Collections = Java.type('java.util.Collections')\n  var list = new ArrayList();\n  for (var i = 0; i < actual.length; i++) {\n    list.add(actual[i]);\n  }\n  Collections.sort(list, java.lang.String.CASE_INSENSITIVE_ORDER)\n  return list;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/table.feature",
    "content": "Feature:\n\nScenario:\n* table data\n    | first  | last    | age |\n    | 'John' | 'Smith' |  20 |\n    | 'Bill' |         |  40 |\n* match data == [{ first: 'John', last: 'Smith', age: 20 }, { first: 'Bill', age: 40 }]\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/tags/TagsTest.java",
    "content": "package com.intuit.karate.core.tags;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class TagsTest {\n\n    @Test\n    public void testFeatureTag() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/tags/outline-tags.feature\")\n                .tags(\"@featuretag\")\n                .parallel(1);\n        assertEquals(4, results.getScenariosPassed());\n    }\n\n    @Test\n    public void testOutlineTag() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/tags/outline-tags.feature\")\n                .tags(\"@outlinetag\")\n                .parallel(1);\n        assertEquals(4, results.getScenariosPassed());\n    }\n\n    @Test\n    public void testOneTag() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/tags/outline-tags.feature\")\n                .tags(\"@one\")\n                .parallel(1);\n        assertEquals(2, results.getScenariosPassed());\n    }\n\n    @Test\n    public void testOneAndBoth() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/tags/outline-tags.feature\")\n                .tags(\"@one\", \"@both\")\n                .parallel(1);\n        assertEquals(2, results.getScenariosPassed());\n    }\n\n    @Test\n    public void testNoneOrBoth() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/tags/outline-tags.feature\")\n                .tags(\"@none,@both\")\n                .parallel(1);\n        assertEquals(4, results.getScenariosPassed());\n    }\n\n    @Test\n    public void testEnvNotFoo() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/tags/env-tags.feature\")\n                .parallel(1);\n        assertEquals(1, results.getScenariosPassed());\n        String name = results.getScenarioResults().findFirst().get().getScenario().getName();\n        assertEquals(\"envnot=foo\", name);\n    }\n\n    @Test\n    public void testEnvFoo() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/tags/env-tags.feature\")\n                .karateEnv(\"foo\")\n                .parallel(1);\n        assertEquals(1, results.getScenariosPassed());\n        String name = results.getScenarioResults().findFirst().get().getScenario().getName();\n        assertEquals(\"env=foo\", name);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/tags/env-tags.feature",
    "content": "Feature:\n\n@env=foo\nScenario: env=foo\n* print '@env=foo'\n\n@envnot=foo\nScenario: envnot=foo\n* print '@envnot=foo'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/tags/outline-tags.feature",
    "content": "@featuretag\nFeature: \n\n@outlinetag\nScenario Outline:\n* print __row\n\n@one @both\nExamples:\n| name   |\n| one-1  |\n| one-2  |\n\n@two @both\nExamples:\n| name   |\n| two-1  |\n| two-2  |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/tags.feature",
    "content": "@one\nFeature:\n\n@two=foo,bar\nScenario:\n* def tagNames = karate.tags\n* def tagValues = karate.tagValues\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/to-bean-called.feature",
    "content": "Feature:\n\nScenario:\n* def catType = 'com.intuit.karate.core.Cat'\n* def Cat = Java.type(catType)\n* def toCat = function(x){ return karate.toBean(x, catType) }\n# second argument (true) is to strip keys with null values\n* def toJson = function(x){ return karate.toJson(x, true) }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/to-bean.feature",
    "content": "Feature: demo calling java methods with complex types\n\n  Background:\n    * call read('to-bean-called.feature')\n\n  Scenario: using constructor and setters\n    * def billie = new Cat()\n    * billie.id = 1\n    * billie.name = 'Billie'\n    * match toJson(billie) == { id: 1, name: 'Billie' }\n\n  Scenario: using json and calling java methods\n    * def billie = toCat({ id: 1, name: 'Billie' })\n    * def bob = toCat({ id: 2, name: 'Bob' })\n    * def wild = toCat({ id: 3, name: 'Wild' })\n    * billie.addKitten(bob)\n    * billie.addKitten(wild)\n    * match toJson(billie) ==\n      \"\"\"\n      {\n        id: 1, name: 'Billie', kittens: [\n          { id: 2, name: 'Bob' },\n          { id: 3, name: 'Wild' }\n        ]\n      }\n      \"\"\"\n\n  Scenario Outline: data driven\n    * def billie = toCat({ id: 1, name: 'Billie' })\n    * def fun = function(n, i){ return { id: i + 2, name: n } }\n    * def kittens = karate.map(names, fun)\n    * billie.kittens = karate.toJava(kittens)\n    * match toJson(billie) contains expected\n    * match toJson(billie).kittens == expected.kittens\n\n    Examples:\n      | names!          | expected!           |\n      | ['Bob', 'Wild'] | { kittens: '#[2]' } |\n      | ['X', 'Y', 'Z'] | { kittens: '#[3]' } |\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/type-conv-query.txt",
    "content": "{\n  hero(name: \"Luke Skywalker\") {\n    height\n    mass\n  }\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/type-conv-query2.txt",
    "content": "{\nabcd\nefgh   \n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/type-conv.feature",
    "content": "Feature: convert between json, xml and string\n\nScenario: multi-line text\n    # although the value starts with '{' it is not parsed as JSON, and line-feeds are retained\n    * text query =\n    \"\"\"\n    {\n      hero(name: \"Luke Skywalker\") {\n        height\n        mass\n      }\n    }\n    \"\"\"\n    * match query == read('type-conv-query.txt').replaceAll(\"\\r\", \"\")\n\nScenario: multi-line text with the starting line indented\n    * text query =\n    \"\"\"\n      {\nabcd\nefgh   \n      }\n    \"\"\"\n    * match query == read('type-conv-query2.txt').replaceAll(\"\\r\", \"\")\n\nScenario Outline: multi-line text in a scenario outline\n    * text query =\n    \"\"\"\n    {\n      hero(name: \"<name>\") {\n        height\n        mass\n      }\n    }\n    \"\"\"\n    * match query == read('type-conv-query.txt').replaceAll(\"\\r\", \"\")\n\n    Examples:\n    | name           |\n    | Luke Skywalker |\n\nScenario: multi-line string expression\n    # this is normally never required since you can use replace\n    * def name = 'Luke Skywalker'\n    * string expectedOnUnix = '{\\n  hero(name: \"' + name + '\") {\\n    height\\n    mass\\n  }\\n}'\n    * string expectedOnWindows = '{\\r\\n  hero(name: \"' + name + '\") {\\r\\n    height\\r\\n    mass\\r\\n  }\\r\\n}'\n    * def actual = read('type-conv-query.txt')\n    * assert actual === expectedOnUnix || actual === expectedOnWindows\n\nScenario: string to json\n    # this would be of type string (not JSON)\n    * def strVar = '{ \"foo\": \"bar\" }'\n    * json jsonVar = strVar\n    * match jsonVar == { foo: 'bar' }\n\nScenario: json to string\n    * def jsonVar = { foo: 'bar' }\n    * string strVar = jsonVar\n    * assert strVar == '{\"foo\":\"bar\"}'\n\nScenario: string to xml\n    * def strVar = '<root><foo>bar</foo></root>'\n    * xml xmlVar = strVar\n    * match xmlVar == <root><foo>bar</foo></root>\n\nScenario: xml to string (improved in 1.0)\n    * def xmlVar = <root><foo>bar</foo></root>\n    # the parentheses forces evaluation as javascript and converts the xml to a map\n    * string strVar = (xmlVar)\n    # karate auto detects xml when needed\n    * match strVar == '<root><foo>bar</foo></root>'\n\nScenario: xml to string\n    * def xmlVar = <root><foo>bar</foo></root>\n    # note that the keyword here is 'xmlstring' not 'string'\n    * xmlstring strVar = xmlVar\n    * match strVar == '<root><foo>bar</foo></root>'\n\nScenario: xml to json\n    * def xmlVar = <root><foo>bar</foo></root>\n    * json jsonVar = xmlVar\n    * match jsonVar == { root: { foo: 'bar' } }\n\nScenario: json to xml\n    * def jsonVar = { root: { foo: 'bar' } }\n    * xml xmlVar = jsonVar\n    * match xmlVar == <root><foo>bar</foo></root>\n\nScenario: xml with attributes\n    * def xmlVar = <root><foo fizz=\"buzz\">bar</foo></root>\n    * json jsonVar = xmlVar\n    # it ain't pretty but this is how karate converts xml to a map-like object internally for parity with json\n    * match jsonVar == { root: { foo: { _ : 'bar', @: { fizz: 'buzz' }}}}\n    # which means that json can be used instead of xpath\n    * match jsonVar $.root.foo._ == 'bar'\n    * match jsonVar $.root.foo.@ == { fizz: 'buzz' }\n    * match jsonVar $.root.foo.@.fizz == 'buzz'\n    * match jsonVar $..foo.@.fizz == ['buzz']\n    * match jsonVar $..@.fizz contains 'buzz'\n    * match jsonVar $..foo.@ contains { fizz: 'buzz' }\n\nScenario: xml with namespaces\n    * def xmlVar = <ns1:root xmlns:ns1=\"http://foo.com\" xmlns:ns2=\"http://bar.com\"><ns2:foo fizz=\"buzz\" ping=\"pong\">bar</ns2:foo></ns1:root>\n    * json jsonVar = xmlVar\n    * match jsonVar == \n    \"\"\"\n    { \n      \"ns1:root\": {\n        \"@\": { \"xmlns:ns1\": \"http://foo.com\", \"xmlns:ns2\": \"http://bar.com\" },\n        \"_\": { \n          \"ns2:foo\": { \n            \"_\": \"bar\", \n            \"@\": { \"fizz\": \"buzz\", \"ping\": \"pong\" } \n          } \n        }     \n      }\n    }\n    \"\"\"\n    * match jsonVar $.ns1:root..ns2:foo.@ == [{ fizz: 'buzz', ping: 'pong' }]\n    * match jsonVar $..ns2:foo.@ == [{ fizz: 'buzz', ping: 'pong' }]\n    * match jsonVar $..ns2:foo.@ contains { fizz: 'buzz', ping: 'pong' }\n    * match jsonVar $..ns2:foo.@ contains only { fizz: 'buzz', ping: 'pong' }\n    * match each jsonVar $..ns2:foo.@ contains { ping: 'pong' }\n\nScenario: get the \"first key\" out of a given json\n    * def response = { \"key1\": { \"a\" : 1 }, \"key2\" : { \"b\": 1 } }\n    * def first = karate.keysOf(response)[0]\n    * match first == 'key1'\n\nScenario: java pojo to json\n    * def className = 'com.intuit.karate.core.SimplePojo'\n    * def Pojo = Java.type(className)\n    * def pojo = new Pojo()\n    * json jsonVar = pojo\n    * match jsonVar == { foo: null, bar: 0 }\n    * def testJson = { foo: 'hello', bar: 5 }\n    * def testPojo = karate.toBean(testJson, className)\n    * assert testPojo.foo == 'hello'\n    * assert testPojo.bar == 5\n\nScenario: java pojo to xml\n    * def Pojo = Java.type('com.intuit.karate.core.SimplePojo')\n    * def pojo = new Pojo()\n    * xml xmlVar = pojo\n    * match xmlVar == <root><foo></foo><bar>0</bar></root>\n\nScenario: parsing json, xml or string\n    * def temp = karate.fromString('{ \"foo\": \"bar\" }')\n    * match karate.typeOf(temp) == 'map'\n    * match temp == { foo: 'bar' }\n    * def temp = karate.fromString('<foo>bar</foo>')\n    * match karate.typeOf(temp) == 'xml'\n    * match temp == <foo>bar</foo>\n    * def temp = karate.fromString('random text')\n    * match karate.typeOf(temp) == 'string'\n    * match temp == 'random text'\n\nScenario: parsing json, xml or string within a js block (use asMap)   \n    * eval\n    \"\"\"\n    var temp = karate.fromString('{ \"foo\": \"bar\" }');\n    if (karate.typeOf(temp) != 'map') karate.fail('expected map');\n    var res = karate.match(temp, { foo: 'bar' });\n    if (!res.pass) karate.fail(res.message);\n    \"\"\"\n\nScenario: inspecting an arbitrary object\n    * def foo = { foo: 'bar' }\n    * match karate.typeOf(foo) == 'map'\n    * def foo = ['foo', 'bar']\n    * match karate.typeOf(foo) == 'list'\n\nScenario: json manipulation using string-replace\n    * def data =\n    \"\"\"\n    {\n      foo: '<foo>',\n      bar: { hello: '<bar>'}\n    }\n    \"\"\"\n    # replace is convenient sometimes because you don't need to worry about complex nested paths\n    * replace data\n        | token | value |\n        | foo   | 'bar' |\n        | bar   | 'baz' |\n\n    # don't forget to cast back to json though\n    * json data = data\n    * match data == { foo: 'bar', bar: { hello: 'baz' } }\n\nScenario: json path on a string should auto-convert\n    * def response = \"{ foo: { hello: 'world' } }\"\n    * def foo = $.foo\n    * match foo == { hello: 'world' }\n\nScenario: js and numbers - float vs int\n    * def foo = '10'\n    * string json = { bar: '#(1 * foo)' }\n    * match json == '{\"bar\":10}'\n\n    * string json = { bar: '#(parseInt(foo))' }\n    * match json == '{\"bar\":10}'\n\n    * def foo = 10\n    * string json = { bar: '#(foo)' }\n    * match json == '{\"bar\":10}'\n\n    * def foo = '10'\n    * string json = { bar: '#(~~foo)' }\n    * match json == '{\"bar\":10}'\n\n    # JS math can introduce a decimal point in some cases\n    * def foo = 100\n    * string json = { bar: '#(foo * 0.1)' }\n    * match json == '{\"bar\":10.0}'\n\n    # but you can easily coerce to an integer if needed\n    * string json = { bar: '#(~~(foo * 0.1))' }\n    * match json == '{\"bar\":10}'\n\nScenario: large numbers in json - use java BigDecimal\n   * def big = 123123123123\n   * string json = { num: '#(big)' }\n   * match json == '{\"num\":1.23123123123E11}'\n   * def big = new java.math.BigDecimal(123123123123)\n   * string json = { num: '#(big)' }\n   * match json == '{\"num\":123123123123}'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/type-conversion.feature",
    "content": "Feature:\n\nScenario: in-line csv\n* csv data =\n\"\"\"\nfirst,second\na1,a2\nb1,b2\n\"\"\"\n* match data == [{ first: 'a1', second: 'a2'}, { first: 'b1', second: 'b2' }]\n\nScenario: in-line yaml\n* yaml data =\n\"\"\"\nname: John\ninput:\n  id: 1\n  subType: \n    name: Smith\n    deleted: false\n\"\"\"\n* match data ==\n\"\"\"\n{\n  name: 'John',\n  input: { \n    id: 1,\n    subType: { name: 'Smith', deleted: false }    \n  }\n}\n\"\"\"\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/ui-google.feature",
    "content": "Feature:\n\nScenario: try to login to github\n  * configure driver = { type: 'chrome', showDriverLog: true }\n  * driver 'https://github.com/login'\n  * screenshot()"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/users-doc.feature",
    "content": "Feature:\n\n@one\nScenario:\n* url 'https://jsonplaceholder.typicode.com/users'\n* method get\n* doc { read: 'users.html' }\n\n* path '1'\n* method get\n* doc { read: 'users-single.html' }\n\n@one @two\nScenario:\n# some comment\n* print 'in second'\n# another comment"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/users-single.html",
    "content": "<div>\n  <div>ID <span th:text=\"response.id\"></span></div>\n  <div>Name: <span th:text=\"response.name\"></span></div>\n  <div>Company:\n    <span th:text=\"response.company.bs\"></span>\n  </div>\n</div>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/users.html",
    "content": "<table class=\"table table-striped\">\n  <thead>\n    <tr>\n      <th>ID</th>\n      <th>Name</th>\n      <th>E-Mail</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr th:each=\"user: response\">\n      <td th:text=\"user.id\"></td>\n      <td th:text=\"user.name\"></td>\n      <td th:text=\"user.email\"></td>\n    </tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/utils-reuse-common.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* def hello = function(){ return 'hello' }\n* def world = function(){ return 'world' }\n\n\n\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/utils-reuse.feature",
    "content": "Feature: json and js functions loaded in the karate-config.js\n\nScenario: function re-use, global / shared scope\n    * call read('config-reuse-common.feature')\n    * assert hello() == 'hello'\n    * assert world() == 'world'\n\nScenario: function re-use, isolated / name-spaced scope\n    * def utils = read('config-reuse-common.feature')\n    * assert utils.hello() == 'hello'\n    * assert utils.world() == 'world'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/uuid.js",
    "content": "function fn(){ return java.util.UUID.randomUUID() + ''; }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/websocket.feature",
    "content": "Feature:\n\nScenario:\n* def handler = function(msg){ karate.log('msg:', msg); return msg.startsWith('hello') }\n* def socket = karate.webSocket('ws://echo.websocket.org', handler)\n* socket.send('hello world!')\n* def result = socket.listen(5000)\n* match result == 'hello world!'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/xml/XmlTest.java",
    "content": "package com.intuit.karate.core.xml;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author peter\n */\nclass XmlTest {\n    \n    @Test\n    void testXml() {\n        Results results = Runner.path(\"classpath:com/intuit/karate/core/xml\").parallel(3);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());  \n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/xml/envelope1.xml",
    "content": "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"\n                  xmlns:acc=\"http://foo/bar\">\n    <soapenv:Header />\n    <soapenv:Body />\n</soapenv:Envelope>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/xml/envelope2.xml",
    "content": "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"\n                  xmlns:acc=\"http://foo/bar\">\n    <soapenv:Header />\n    <soapenv:Body>#(search)</soapenv:Body>\n</soapenv:Envelope>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/xml/soap1.xml",
    "content": "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"\n                  xmlns:acc=\"http://foo/bar\">\n    <soapenv:Header />\n    <soapenv:Body>\n        <acc:getAccountByPhoneNumber>\n            <acc:phoneNumber>#(phoneNumber)</acc:phoneNumber>\n            <acc:phoneNumberSearchOption>\n                <acc:searchWirelessInd>#(search.wireless)</acc:searchWirelessInd>\n                <acc:searchVoipInd>#(search.voip)</acc:searchVoipInd>\n                <acc:searchTollFreeInd>#(search.tollFree)</acc:searchTollFreeInd>\n            </acc:phoneNumberSearchOption>\n        </acc:getAccountByPhoneNumber>\n    </soapenv:Body>\n</soapenv:Envelope>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/xml/soap2.xml",
    "content": "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"\n                  xmlns:acc=\"http://foo/bar\">\n    <soapenv:Header />\n    <soapenv:Body>\n        <acc:getAccountByPhoneNumber>\n            <acc:phoneNumber>@@number@@</acc:phoneNumber>\n            <acc:phoneNumberSearchOption>\n                <acc:searchWirelessInd>@@wireless@@</acc:searchWirelessInd>\n                <acc:searchVoipInd>@@voip@@</acc:searchVoipInd>\n                <acc:searchTollFreeInd>@@tollFree@@</acc:searchTollFreeInd>\n            </acc:phoneNumberSearchOption>\n        </acc:getAccountByPhoneNumber>\n    </soapenv:Body>\n</soapenv:Envelope>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/xml/xml-and-xpath.feature",
    "content": "Feature: xml and xpath demos\n\nScenario: get for complex things such as xpath functions\n\n* def foo =\n\"\"\"\n<records>\n  <record index=\"1\">a</record>\n  <record index=\"2\">b</record>\n  <record index=\"3\" foo=\"bar\">c</record>\n</records>\n\"\"\"\n\n# json style\n* assert foo.records.record.length == 3\n\n# xpath assertions\n* match foo count(/records//record) == 3\n* match foo //record[@index=2] == 'b'\n* match foo //record[@foo='bar'] == 'c'\n\n* match foo == <records><record index=\"1\">a</record><record index=\"#? _ &gt; 1\">b</record><record index=\"3\" foo=\"bar\">#string</record></records>\n\n# this xml doc has white-space within it at troublesome places\n* def xml = <?xml version=\"1.0\" encoding=\"UTF-8\"?> <response> <result>succeed</result> <records> <record> <browser_port>8008</browser_port> <current_date_time>2017-04-03 20:29:58 CDT</current_date_time> <date_time_server_started>2017-03-21 12:23:55 CDT</date_time_server_started> <os_version>Red Hat Enterprise Linux 6 2.6.32-573.12.1.el6.x86_64, 64 Bit, x86_64</os_version> <product_version>R04M001170316</product_version> <product_database_version>20170131131718</product_database_version> <replication_heartbeat_timestamp>2017-04-03 20:25:00 CDT</replication_heartbeat_timestamp> </record> </records> </response>\n* match xml count(/response/records//record) == 1\n* match xml/response/result == 'succeed'\n\nScenario: when xpath exressions return xml chunks (or node lists)\n\n* def response = \n\"\"\"\n<teachers>\n\t<teacher department=\"science\" id=\"309\">\n\t\t<subject>math</subject>\n\t\t<subject>physics</subject>\n\t</teacher>\n\t<teacher department=\"arts\" id=\"310\">\n\t\t<subject>political education</subject>\n\t\t<subject>english</subject>\n\t</teacher>\n</teachers>\n\"\"\"\n* def expected = <teacher department=\"science\" id=\"309\"><subject>math</subject><subject>physics</subject></teacher>\n* def teacher = //teacher[@department='science']\n* match teacher == expected\n* def teacher = get response //teacher[@department='science']\n* match teacher == expected\n* match //teacher[@department='science'] == expected\n\n* def expected = ['math', 'physics']\n* def subjects = //teacher[@department='science']/subject\n* match subjects == expected\n* def subjects = get response //teacher[@department='science']/subject\n* match subjects == expected\n* match //teacher[@department='science']/subject == expected\n* match //teacher[@department='science']/subject == ['math', 'physics']\n* match //teacher[@department='science']/subject contains ['physics', 'math']\n\n* def teachers = response\n* def subjects = get teachers //teacher[@department='science']/subject\n* match subjects contains ['physics', 'math']\n* match teachers //teacher[@department='science']/subject == ['math', 'physics']\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/xml/xml-call.feature",
    "content": "Feature: Example\n\n@setup\nScenario:\n  * def prepare_data = call read('xml-called.feature')\n  * def data = prepare_data.data\n\nScenario Outline: make sure any json clone operations don't crash during call\n  * print name\n  * print stats\n\n  Examples:\n    | karate.setup().data |\n\nScenario: make sure call arg json conversion for reporting fails gracefully\n* def moreXml =\n\"\"\"\n<root>\n    <hello>\n        <there>everyone</there>\n    </hello>\n    <hello>\n        <there>anyone</there>\n    </hello>\n</root>\n\"\"\"\n* def nodes = get moreXml //hello\n* call read('xml-called.feature') { nodes: '#(nodes)' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/xml/xml-called.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* def xml =\n\"\"\"\n<root>\n    <hello>\n        <there>everyone</there>\n    </hello>\n    <hello>\n        <there>anyone</there>\n    </hello>\n</root>\n\"\"\"\n* def nodes = get xml //hello\n* def data = [{name: \"A\", stats: {\"vitality\":2}}, {name: \"B\", stats: {\"strength\":3}}, {name: \"C\", stats: {\"intelligence\":5}}]\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/xml/xml.feature",
    "content": "Feature: xml samples and tests\n\nScenario: xml empty elements and null\n    * def foo = <root><bar/></root>\n    # unfortunately XML does not have a concept of a null value\n    # empty tags are always considered to have the text value of ''\n    * match foo == <root><bar></bar></root>\n    * match foo/root/bar == ''\n    * match foo/root/bar == '#present'\n    # check if a path does not exist\n    * match foo/root/nope == '#notpresent'\n\nScenario: simple fuzzy matching\n    * def xml = <root><hello>world</hello><foo>bar</foo></root>\n    * match xml == <root><hello>world</hello><foo>#ignore</foo></root>\n    * def xml = <root><hello foo=\"bar\">world</hello></root>\n    * match xml == <root><hello foo=\"#ignore\">world</hello></root>\n\nScenario: pretty print xml\n    * def search = { number: '123456', wireless: true, voip: false, tollFree: false }\n    * def xml = read('soap1.xml')\n    * print 'pretty print:', xml\n\nScenario: test removing and adding elements / attributes\n    * def base = <query><name>foo</name></query>\n    * remove base /query/name\n    * match base == <query/>\n    * set base /query/foo = 'bar'\n    * set base /query/@baz = 'ban'\n    * match base == <query baz=\"ban\"><foo>bar</foo></query>\n    * remove base /query/@baz\n    * match base == <query><foo>bar</foo></query>\n\nScenario: test removing elements from xml from js\n    * def base = <query><name>foo</name></query>\n    * karate.remove('base', '/query/name')\n    * match base == <query/>\n\nScenario: dynamic xpath that uses variables\n    * def xml = <query><name><foo>bar</foo></name></query>\n    * def elementName = 'name'\n    * def name = karate.xmlPath(xml, '/query/' + elementName + '/foo')\n    * match name == 'bar'\n    * def queryName = karate.xmlPath(xml, '/query/' + elementName)\n    * match queryName == <name><foo>bar</foo></name>\n    * def foo = <root><a>1</a><a>2</a></root>\n    * def tmp = karate.xmlPath(foo, 'count(/root/a)')\n    * match tmp == 2\n\nScenario: placeholders using xml embedded expressions\n    * def phoneNumber = '123456'\n    * def search = { wireless: true, voip: false, tollFree: false }\n    * def req = read('soap1.xml')\n    * def phone = $req/Envelope/Body/getAccountByPhoneNumber\n    * match phone /getAccountByPhoneNumber/phoneNumber == '123456'\n    * match phone ==\n    \"\"\"\n    <acc:getAccountByPhoneNumber>\n        <acc:phoneNumber>123456</acc:phoneNumber>\n        <acc:phoneNumberSearchOption>\n            <acc:searchWirelessInd>true</acc:searchWirelessInd>\n            <acc:searchVoipInd>false</acc:searchVoipInd>\n            <acc:searchTollFreeInd>false</acc:searchTollFreeInd>\n        </acc:phoneNumberSearchOption>\n    </acc:getAccountByPhoneNumber>\n    \"\"\"\n    # string comparisons may be simpler than xpath in some cases\n    * xmlstring reqString = req\n    * match reqString contains '<acc:phoneNumber>123456</acc:phoneNumber>'\n\nScenario: placeholders using string replace\n    * def req = read('soap2.xml')\n    * replace req\n        | token        | value     |\n        | @@number@@   | '123456'  |\n        | @@wireless@@ | 'true'    |\n        | @@voip@@     | 'false'   |\n        | @@tollFree@@ | 'false'   |\n    # convert back to xml after a string replace\n    * xml req = req\n    * match req /Envelope/Body/getAccountByPhoneNumber/phoneNumber == '123456'\n\n\nScenario: set xml chunks using xpath\n    * def req = read('envelope1.xml')\n    * def phone = '123456'\n    * def search = \n    \"\"\"\n    <acc:getAccountByPhoneNumber>\n        <acc:phoneNumber>#(phone)</acc:phoneNumber>\n    </acc:getAccountByPhoneNumber>\n    \"\"\"\n    * set req /Envelope/Body = search\n    * match req ==\n    \"\"\"\n    <soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:acc=\"http://foo/bar\">\n        <soapenv:Header />\n        <soapenv:Body>\n            <acc:getAccountByPhoneNumber>\n                <acc:phoneNumber>123456</acc:phoneNumber>\n            </acc:getAccountByPhoneNumber>\n        </soapenv:Body>\n    </soapenv:Envelope>\n    \"\"\"\n\nScenario: set xml chunks using embedded expressions\n    * def phone = '123456'\n    # this will remove the <acc:phoneNumberSearchOption> element\n    * def searchOption = null\n    * def search = \n    \"\"\"\n    <acc:getAccountByPhoneNumber>\n        <acc:phoneNumber>#(phone)</acc:phoneNumber>\n        <acc:phoneNumberSearchOption>##(searchOption)</acc:phoneNumberSearchOption>        \n    </acc:getAccountByPhoneNumber>\n    \"\"\"\n    * def req = read('envelope2.xml')\n    * match req ==\n    \"\"\"\n    <soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:acc=\"http://foo/bar\">\n        <soapenv:Header/>\n        <soapenv:Body>\n            <acc:getAccountByPhoneNumber>\n                <acc:phoneNumber>123456</acc:phoneNumber>\n            </acc:getAccountByPhoneNumber>\n        </soapenv:Body>\n    </soapenv:Envelope>\n    \"\"\"\n\nScenario: set via table\n    * def search = \n    \"\"\"\n    <acc:getAccountByPhoneNumber>\n        <acc:phoneNumber></acc:phoneNumber>\n        <acc:phoneNumberSearchOption></acc:phoneNumberSearchOption>        \n    </acc:getAccountByPhoneNumber>\n    \"\"\"\n\n    * set search /getAccountByPhoneNumber\n    | path                    | value |\n    | phoneNumber             | 1234  |   \n    | phoneNumberSearchOption | 'all' |\n\n    * match search ==\n    \"\"\"\n    <acc:getAccountByPhoneNumber>\n        <acc:phoneNumber>1234</acc:phoneNumber>\n        <acc:phoneNumberSearchOption>all</acc:phoneNumberSearchOption>        \n    </acc:getAccountByPhoneNumber>\n    \"\"\"\n\nScenario: set via table, variable and xml nodes will be auto-built\n    * set search /acc:getAccountByPhoneNumber\n    | path                        | value |\n    | acc:phoneNumber             | 1234  |   \n    | acc:phoneNumberSearchOption | 'all' |\n\n    * match search ==\n    \"\"\"\n    <acc:getAccountByPhoneNumber>\n        <acc:phoneNumber>1234</acc:phoneNumber>\n        <acc:phoneNumberSearchOption>all</acc:phoneNumberSearchOption>        \n    </acc:getAccountByPhoneNumber>\n    \"\"\"\n\nScenario: set via table, mixing xml chunks\n    * set search /acc:getAccountByPhoneNumber\n    | path                        | value |\n    | acc:phoneNumber             | 1234  |\n    | acc:foo    | <acc:bar>baz</acc:bar> |\n\n    * match search ==\n    \"\"\"\n    <acc:getAccountByPhoneNumber>\n        <acc:phoneNumber>1234</acc:phoneNumber>\n        <acc:foo>\n            <acc:bar>baz</acc:bar>\n        </acc:foo>        \n    </acc:getAccountByPhoneNumber>\n    \"\"\"\n\nScenario: set via table, build xml including attributes and repeated elements\n    * set search /acc:getAccountByPhoneNumber\n    | path                        | value |\n    | acc:phone/@foo              | 'bar' |\n    | acc:phone/acc:number[1]     | 1234  |\n    | acc:phone/acc:number[2]     | 5678  |     \n    | acc:phoneNumberSearchOption | 'all' |\n\n    * match search ==\n    \"\"\"\n    <acc:getAccountByPhoneNumber>\n        <acc:phone foo=\"bar\">\n            <acc:number>1234</acc:number>\n            <acc:number>5678</acc:number>\n        </acc:phone>\n        <acc:phoneNumberSearchOption>all</acc:phoneNumberSearchOption>        \n    </acc:getAccountByPhoneNumber>\n    \"\"\"\n\nScenario Outline: conditionally build xml from scenario-outline and examples\n    * def xml = \n    \"\"\"\n    <query>\n      <name>\n        <firstName>##(<_firstName>)</firstName>\n        <lastName>##(<_lastName>)</lastName>\n      </name>\n      <age>##(<_age>)</age>\n    </query>\n    \"\"\"\n\n    * match xml == <_expected>\n\n    Examples:\n    | _firstName | _lastName | _age | _expected                                                                                      |\n    | 'John'     | 'Smith'   |   20 | <query><name><firstName>John</firstName><lastName>Smith</lastName></name><age>20</age></query> |\n    | 'Jane'     | 'Doe'     | null | <query><name><firstName>Jane</firstName><lastName>Doe</lastName></name></query>                |\n    | null       | 'Waldo'   | null | <query><name><lastName>Waldo</lastName></name></query>                                         |\n\n\nScenario: a cleaner way to achieve the above by using tables and the 'set' keyword\n    * set search /queries/query\n        | path           | 1        | 2      | 3       |\n        | name/firstName | 'John'   | 'Jane' |         |\n        | name/lastName  | 'Smith'  | 'Doe'  | 'Waldo' |\n        | age            | 20       |        |         |\n        \n    * match search/queries/query[1] == <query><name><firstName>John</firstName><lastName>Smith</lastName></name><age>20</age></query>\n    * match search/queries/query[2] == <query><name><firstName>Jane</firstName><lastName>Doe</lastName></name></query>\n    * match search/queries/query[3] == <query><name><lastName>Waldo</lastName></name></query>\n\nScenario: karate.set() is another way to conditionally modify xml\n    * table data\n        | first  | last    | age |\n        | 'John' | 'Smith' |  20 |\n        | 'Jane' | 'Doe'   |     |\n        |        | 'Waldo' |     |\n    * def fun =\n    \"\"\"\n    function(v) {\n      karate.setXml('temp', '<query/>');\n      if (v.first) karate.set('temp', '/query/name/firstName', v.first);\n      if (v.last) karate.set('temp', '/query/name/lastName', v.last);\n      if (v.age) karate.set('temp', '/query/age', v.age);\n      return karate.get('temp');\n    }    \n    \"\"\"\n    * call fun data[0]\n    * match temp == <query><name><firstName>John</firstName><lastName>Smith</lastName></name><age>20</age></query> \n    * call fun data[1]\n    * match temp == <query><name><firstName>Jane</firstName><lastName>Doe</lastName></name></query>\n    * call fun data[2]\n    * match temp == <query><name><lastName>Waldo</lastName></name></query>  \n\nScenario: creating xml with repeating elements in a loop\n    * table users\n      | accountNo   | subsID         | mobile       | subsType  |\n      | '113888572' | '113985218890' | '1135288836' | 'asd'     |\n      | '113888573' | '113985218891' | '1135288837' | 'qwe'     |\n      | '113888582' | '113985218810' | '1135288846' | 'asd'     |\n    \n    * def xml = <users></users>\n    * def fun =\n    \"\"\"\n    function(u, i) {\n      var base = '/users/user[' + (i + 1) + ']/';\n      karate.set('xml', base + 'account', u.accountNo);\n      karate.set('xml', base + 'mobile', u.mobile);\n      karate.set('xml', base + 'type', u.subsType);\n    }\n    \"\"\"\n    * karate.forEach(users, fun)\n    * match xml ==\n    \"\"\"\n    <users>\n      <user>\n        <account>113888572</account>\n        <mobile>1135288836</mobile>\n        <type>asd</type>\n      </user>\n      <user>\n        <account>113888573</account>\n        <mobile>1135288837</mobile>\n        <type>qwe</type>\n      </user>\n      <user>\n        <account>113888582</account>\n        <mobile>1135288846</mobile>\n        <type>asd</type>\n      </user>\n    </users>\n    \"\"\"\n\nScenario: xml containing DTD reference\n    * def xml = <!DOCTYPE USER SYSTEM \"http://127.0.0.1:5000/login/dtd\"><foo/>\n    * match xml == <foo></foo>\n\nScenario: xml containing DTD complex\n    * def xml = \n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n    <!DOCTYPE USER SYSTEM \"http://172.20.17.74:5000/login/dtd\">\n    <gpOBJECT>\n      <gpPARAM name=\"coconuts\">666</gpPARAM>\n    </gpOBJECT>\n    \"\"\"\n    * match xml == <gpOBJECT><gpPARAM name=\"coconuts\">666</gpPARAM></gpOBJECT>\n\nScenario: xml containing a CDATA section\n    * def xml =\n      \"\"\"\n    <ResponseSet vers=\"1.0\" svcid=\"com.iplanet.am.naming\" reqid=\"0\">\n        <Response><![CDATA[<NamingResponse vers=\"1.0\" reqid=\"0\">\n                <GetNamingProfile>\n                    <Attribute name=\"url\" value=\"localhost\"></Attribute>\n                 </GetNamingProfile>\n            </NamingResponse>]]>\n        </Response>\n    </ResponseSet>\n    \"\"\"\n    * xml naming = $xml /ResponseSet/Response\n    * match naming //Attribute[@name='url']/@value == 'localhost'\n\nScenario: CDATA and simple string embedded expression\n    * def foo = 'hello world'\n    * def xml = <bar><![CDATA[#(foo)]]></bar>\n    * match xml == <bar><![CDATA[hello world]]></bar>\n\nScenario: CDATA and xml embedded expression\n    * def foo = <bar>baz</bar>\n    * def xml =\n    \"\"\"\n    <ResponseSet vers=\"1.0\" svcid=\"com.iplanet.am.naming\" reqid=\"0\">\n        <Response><![CDATA[#(foo)]]></Response>\n    </ResponseSet>\n    \"\"\"\n    * match xml ==\n    \"\"\"\n    <ResponseSet vers=\"1.0\" svcid=\"com.iplanet.am.naming\" reqid=\"0\">\n        <Response><![CDATA[<bar>baz</bar>]]></Response>\n    </ResponseSet>\n    \"\"\"\n\nScenario: CDATA and xml string embedded expression\n    * def foo = <bar>baz</bar>\n    * xmlstring foo = foo\n    * def xml = <ResponseSet vers=\"1.0\" svcid=\"com.iplanet.am.naming\" reqid=\"0\"><Response><![CDATA[#(foo)]]></Response></ResponseSet>\n    * xmlstring xml = xml\n    # note that attributes get re-ordered / sorted by name\n    * match xml == '<ResponseSet reqid=\"0\" svcid=\"com.iplanet.am.naming\" vers=\"1.0\"><Response><![CDATA[<bar>baz</bar>]]></Response></ResponseSet>'\n\nScenario: two CDATA sections is tricky - but xpath returns a list\n    * def response = \n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n    <ResponseSet vers=\"1.0\" svcid=\"Session\" reqid=\"1\">\n        <Response><![CDATA[<SessionResponse vers=\"1.0\" reqid=\"0\">\n                <GetSession>\n                    <Session sid=\"1\">\n                        <Property name=\"CharSet\" value=\"UTF-8\"></Property>      \n                    </Session>\n                </GetSession>\n            </SessionResponse>]]>\n        </Response>\n        <Response><![CDATA[<SessionResponse vers=\"1.0\" reqid=\"1\">\n          <AddSessionListener>\n            <OK></OK>\n          </AddSessionListener>\n          </SessionResponse>]]>\n        </Response>\n    </ResponseSet>\n    \"\"\"\n    * def temp = $response //Response\n    * xml session = temp[0]\n    * match session == <SessionResponse reqid=\"0\" vers=\"1.0\"><GetSession><Session sid=\"1\"><Property name=\"CharSet\" value=\"UTF-8\"/></Session></GetSession></SessionResponse>\n\nScenario: xml with attributes but null value\n    * def xml = <foo><bar bbb=\"2\" aaa=\"1\"/></foo>\n    * match xml == <foo><bar bbb=\"2\" aaa=\"1\"/></foo>\n    * xmlstring temp = xml\n    # unfortunately xml attributes get re-ordered on string conversion / http request\n    * match temp == '<foo><bar aaa=\"1\" bbb=\"2\"/></foo>'\n    * def temp = karate.prettyXml(xml)\n    * match temp contains '<bar aaa=\"1\" bbb=\"2\"/>'\n\nScenario: attribute embedded expression but empty / null element text\n    * def request_uuid = 'foo'\n    * def response =\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n    <S:Envelope xmlns:S=\"http://www.w3.org/2003/05/soap-envelope\">\n        <S:Body>\n            <SucceededGetData RequestID=\"#(request_uuid)\">some text</SucceededGetData>\n            <MessageDelivered OfferID=\"#(request_uuid)\"/>\n        </S:Body>\n    </S:Envelope>\n    \"\"\"\n    * match response == \n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n    <S:Envelope xmlns:S=\"http://www.w3.org/2003/05/soap-envelope\">\n        <S:Body>\n            <SucceededGetData RequestID=\"foo\">some text</SucceededGetData>\n            <MessageDelivered OfferID=\"foo\"/>\n        </S:Body>\n    </S:Envelope>\n    \"\"\"\n\nScenario: attribute embedded expression but empty / null element text\n    * def request_uuid = 'foo'\n    * def response =\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n    <S:Envelope xmlns:S=\"http://www.w3.org/2003/05/soap-envelope\">\n        <S:Body>\n            <SucceededGetData RequestID=\"#(request_uuid)\">some text</SucceededGetData>\n            <MessageDelivered OfferID=\"#(request_uuid)\"/>\n            <Test OfferID=\"\"/>\n        </S:Body>\n    </S:Envelope>\n    \"\"\"\n    * set response /Envelope/Body/Test/@OfferID = 'bar'\n    * match response == \n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n    <S:Envelope xmlns:S=\"http://www.w3.org/2003/05/soap-envelope\">\n        <S:Body>\n            <SucceededGetData RequestID=\"foo\">some text</SucceededGetData>\n            <MessageDelivered OfferID=\"foo\"/>\n            <Test OfferID=\"bar\"/>\n        </S:Body>\n    </S:Envelope>\n    \"\"\"\n\nScenario: repeated xml elements and fuzzy matching\n    since this is tricky, convert to json first\n\n    * json response = <response><foo><bar><msg name=\"Hello\"/><msg name=\"World\"/></bar><bar><msg name=\"Hello\"/><msg name=\"World\"/></bar></foo></response>\n    * json bar = <bar><msg name=\"Hello\"/><msg name=\"World\"/></bar>\n    * match each response.response.foo.bar == bar.bar\n    * match response == { response: { foo: { bar: '#[] bar.bar' } } }\n    # so yes, we can express expected data in xml\n    * match response == <response><foo><bar>#[] bar.bar</bar></foo></response>\n\nScenario: matching ignores xml prefixes\n    * def search = { number: '123456', wireless: true, voip: false, tollFree: false }\n    * def xml = read('soap1.xml')\n\n    * def phoneNumberSearchOption =\n    \"\"\"\n    <foo:phoneNumberSearchOption xmlns:foo=\"http://foo/bar\">\n        <foo:searchWirelessInd>#(search.wireless)</foo:searchWirelessInd>\n        <foo:searchVoipInd>#(search.voip)</foo:searchVoipInd>\n        <foo:searchTollFreeInd>#(search.tollFree)</foo:searchTollFreeInd>\n    </foo:phoneNumberSearchOption>\n    \"\"\"\n    * match xml /Envelope/Body/getAccountByPhoneNumber/phoneNumberSearchOption == phoneNumberSearchOption\n\nScenario: xml to map conversion should ignore comments\n* def temp =\n\"\"\"\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Copyright 2001-2019 3rd party which has been removed. All rights reserved. -->\n\n<!-- PRODUCTION HEADER\n     produced on:        machine of third party\n     production time:    20190912T162512,4Z\n     production module:  3rd party module\n-->\n<hello>world</hello>\n\"\"\"\n* def message = karate.xmlPath(temp, \"/hello\")\n* match message == \"world\"\n\nScenario: xml matching involving karate-schema substitutions\n* def subSchema =\n\"\"\"\n<c>#string</c>\n\"\"\"\n* text schema =\n\"\"\"\n<root>\n  <a>#string</a>\n  <b>##(subSchema)</b>\n</root>\n\"\"\"\n# this is to avoid auto-substitution of the \"##(subSchema)\" part\n# since we need to \"preserve\" it for later use in a \"match\"\n* xml schema = schema\n* def test1 =\n\"\"\"\n<root>\n  <a>x</a>\n  <b>\n    <c>y</c>\n  </b>\n</root>\n\"\"\"\n* match test1 == schema\n* def test2 =\n\"\"\"\n<root>\n  <a>x</a>\n</root>\n\"\"\"\n* match test2 == schema\n\nScenario: Xml whitespace trim after print bug fix\n* def setForecastRequestXml =\n    \"\"\"\n    <myroot>\n        <myelement>T </myelement>\n    </myroot>\n    \"\"\"\n* match setForecastRequestXml //myelement == 'T '\n* print setForecastRequestXml\n* match setForecastRequestXml //myelement == 'T '\n* match setForecastRequestXml //myelement != 'T'\n\nScenario: Xml whitespace trim after karate.log bug fix\n* def setForecastRequestXml =\n    \"\"\"\n    <myroot>\n        <myelement> T </myelement>\n    </myroot>\n    \"\"\"\n* match setForecastRequestXml //myelement == ' T '\n* karate.log(setForecastRequestXml)\n* match setForecastRequestXml //myelement == ' T '\n* match setForecastRequestXml //myelement != 'T'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/core/xml-pretty.feature",
    "content": "Feature:\n\nScenario: make sure [set] updates variable for xml\n* def jsFunction =\n\"\"\"\n  function fn(xml){\n    return karate.prettyXml(xml);\n  }\n\"\"\"\n* def cat = <cat><name>Billie</name></cat>\n* set cat /cat/name = 'Jean'\n* xml temp = jsFunction(cat)\n* print temp\n* match temp / == <cat><name>Jean</name></cat>\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/driver/DriverElementTest.java",
    "content": "package com.intuit.karate.driver;\n\nimport com.intuit.karate.core.Variable;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass DriverElementTest {\n    \n    static final Logger logger = LoggerFactory.getLogger(DriverElementTest.class);\n\n    @Test\n    void testToJson() {       \n        Element de = DriverElement.locatorExists(null, \"foo\");\n        List list = Collections.singletonList(de);\n        Variable v = new Variable(list);\n        logger.debug(\"element serialized: {}\", v.getAsString());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/driver/DriverOptionsTest.java",
    "content": "package com.intuit.karate.driver;\n\nimport com.intuit.karate.TestUtils;\nimport java.util.Collections;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass DriverOptionsTest {\n\n    static void test(String in, String out) {\n        assertEquals(out, DriverOptions.preProcessWildCard(in));\n    }\n\n    @Test\n    void testPreProcess() {\n        test(\"{}hi\", \"//*[normalize-space(text())='hi']\");\n        test(\"{^}hi\", \"//*[contains(normalize-space(text()),'hi')]\");\n        test(\"{^:}hi\", \"//*[contains(normalize-space(text()),'hi')]\");\n        test(\"{^:0}hi\", \"//*[contains(normalize-space(text()),'hi')]\");\n        test(\"{^:2}hi\", \"/(//*[contains(normalize-space(text()),'hi')])[2]\");\n        test(\"{:2}hi\", \"/(//*[normalize-space(text())='hi'])[2]\");\n        test(\"{a}hi\", \"//a[normalize-space(text())='hi']\");\n        test(\"{a:2}hi\", \"/(//a[normalize-space(text())='hi'])[2]\");\n        test(\"{^a:}hi\", \"//a[contains(normalize-space(text()),'hi')]\");\n        test(\"{^a/p}hi\", \"//a/p[contains(normalize-space(text()),'hi')]\");\n        test(\"{^a:2}hi\", \"/(//a[contains(normalize-space(text()),'hi')])[2]\");\n    }\n\n    @Test\n    void testRetry() {\n        DriverOptions options = new DriverOptions(Collections.EMPTY_MAP, TestUtils.runtime(), 0, null);\n        options.retry(() -> 1, x -> x < 5, \"not 5\", false);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/driver/ElementFinderTest.java",
    "content": "package com.intuit.karate.driver;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass ElementFinderTest {\n\n    @Test\n    void testToJson() {\n        String condition = ElementFinder.exitCondition(\"{^a}Foo\");\n        assertEquals(\"e.textContent.trim().includes('Foo') && e.tagName == 'A'\", condition);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/driver/KeyTest.java",
    "content": "package com.intuit.karate.driver;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass KeyTest {\n\n    @Test\n    void testKey() {\n        assertTrue('a' < Key.INSTANCE.NULL);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/driver/appium/MobileDriverOptionsTest.java",
    "content": "package com.intuit.karate.driver.appium;\n\nimport com.intuit.karate.Json;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\nclass MobileDriverOptionsTest {\n\n    @Test\n    public void testGetBrowserName() {\n        String driverSession = \"{\\n\" +\n                \"          desiredCapabilities: { \\n\" +\n                \"            browserName: 'Safari',\\n\" +\n                \"          },\\n\" +\n                \"          capabilities: {\\n\" +\n                \"            firstMatch: [\\n\" +\n                \"                {\\n\" +\n                \"                  browserName: \\\"Safari\\\",\\n\" +\n                \"                  platformName: \\\"iOS\\\",\\n\" +\n                \"                  'appium:platformVersion' : \\\"15.0\\\",\\n\" +\n                \"                  'appium:deviceName' : \\\"iPhone 8 Simulator\\\",\\n\" +\n                \"                  'appium:screenResolution' : \\\"1024x768\\\",\\n\" +\n                \"                  idleTimeout : 12000,\\n\" +\n                \"                  avoidProxy : false,\\n\" +\n                \"                  'sauce:options': {\\n\" +\n                \"                      username: sauceUsername,\\n\" +\n                \"                      accessKey: sauceKey,\\n\" +\n                \"                      \\n\" +\n                \"                  }\\n\" +\n                \"                }\\n\" +\n                \"            ]\\n\" +\n                \"          }\\n\" +\n                \"        }\";\n\n        Map<String, Object> options = Json.of(driverSession).get(\"$\");\n        String browserName = MobileDriverOptions.getBrowserName(options);\n        Assertions.assertEquals(\"Safari\", browserName);\n\n\n        String driverSession2 = \"{\\n\" +\n                \"          capabilities: {\\n\" +\n                \"            firstMatch: [\\n\" +\n                \"                {\\n\" +\n                \"                  browserName: \\\"Safari\\\",\\n\" +\n                \"                  platformName: \\\"iOS\\\",\\n\" +\n                \"                  'appium:platformVersion' : \\\"15.0\\\",\\n\" +\n                \"                  'appium:deviceName' : \\\"iPhone 8 Simulator\\\",\\n\" +\n                \"                  'appium:screenResolution' : \\\"1024x768\\\",\\n\" +\n                \"                  idleTimeout : 12000,\\n\" +\n                \"                  avoidProxy : false,\\n\" +\n                \"                  'sauce:options': {\\n\" +\n                \"                      username: sauceUsername,\\n\" +\n                \"                      accessKey: sauceKey,\\n\" +\n                \"                      \\n\" +\n                \"                  }\\n\" +\n                \"                }\\n\" +\n                \"            ]\\n\" +\n                \"          }\\n\" +\n                \"        }\";\n\n        Map<String, Object> options2 = Json.of(driverSession2).get(\"$\");\n        String browserName2 = MobileDriverOptions.getBrowserName(options2);\n        Assertions.assertEquals(\"Safari\", browserName2);\n\n\n        String driverSession3 = \"{\\n\" +\n                \"          desiredCapabilities: {\\n\" +\n                \"                  browserName: \\\"Safari\\\",\\n\" +\n                \"                  platformName: \\\"iOS\\\",\\n\" +\n                \"                }\\n\" +\n                \"        }\";\n\n        Map<String, Object> options3 = Json.of(driverSession3).get(\"$\");\n        String browserName3 = MobileDriverOptions.getBrowserName(options3);\n        Assertions.assertEquals(\"Safari\", browserName3);\n\n\n        String driverSession4 = \"{\\n\" +\n                \"          capabilities: {\\n\" +\n                \"                  browserName: \\\"Safari\\\",\\n\" +\n                \"                  platformName: \\\"iOS\\\",\\n\" +\n                \"                  'appium:platformVersion' : \\\"15.0\\\",\\n\" +\n                \"          }\\n\" +\n                \"        }\";\n\n        Map<String, Object> options4 = Json.of(driverSession4).get(\"$\");\n        String browserName4 = MobileDriverOptions.getBrowserName(options4);\n        Assertions.assertEquals(\"Safari\", browserName4);\n\n\n        String driverSession5 = \"{\\n\" +\n                \"        }\";\n\n        Map<String, Object> options5 = Json.of(driverSession5).get(\"$\");\n        String browserName5 = MobileDriverOptions.getBrowserName(options5);\n        Assertions.assertEquals(null, browserName5);\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/driver/playwright/PlaywrightDriverRunner.java",
    "content": "package com.intuit.karate.driver.playwright;\n\nimport com.intuit.karate.driver.DriverOptions;\nimport java.util.Collections;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass PlaywrightDriverRunner {\n\n    static final Logger logger = LoggerFactory.getLogger(PlaywrightDriverRunner.class);\n\n    @Test\n    void testPlaywright() {\n        DriverOptions options = new DriverOptions(Collections.EMPTY_MAP, null, 0, null);\n        PlaywrightDriver driver = new PlaywrightDriver(options, null, \"ws://127.0.0.1:4444/a9a2cbe14cd3282908de74bf73d2e901\");\n        driver.setUrl(\"https://google.com\");\n        driver.screenshot();\n        driver.waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/ClientRunner.java",
    "content": "package com.intuit.karate.fatjar;\n\nimport com.intuit.karate.Runner;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass ClientRunner {\n\n    @Test\n    void testClient() {\n        Runner.runFeature(\"classpath:com/intuit/karate/fatjar/client.feature\", null, true);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/FeatureProxyRunner.java",
    "content": "package com.intuit.karate.fatjar;\n\nimport com.intuit.karate.core.MockServer;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass FeatureProxyRunner {\n\n    @Test\n    void testServer() {\n        MockServer server = MockServer.feature(\"classpath:com/intuit/karate/fatjar/proxy.feature\").http(8090).build();\n        server.waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/FeatureServerRunner.java",
    "content": "package com.intuit.karate.fatjar;\n\nimport com.intuit.karate.core.MockServer;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass FeatureServerRunner {\n\n    @Test\n    void testServer() {\n        MockServer server = MockServer.feature(\"classpath:com/intuit/karate/fatjar/server.feature\").http(8080).build();\n        server.waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/FeatureServerTest.java",
    "content": "package com.intuit.karate.fatjar;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.MockServer;\nimport org.junit.jupiter.api.AfterAll;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass FeatureServerTest {\n\n    static MockServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        server = MockServer\n                .feature(\"classpath:com/intuit/karate/fatjar/server.feature\")\n                .pathPrefix(\"/v1\")\n                .http(0).build();\n        int port = server.getPort();\n        System.setProperty(\"karate.server.port\", port + \"\");\n        // needed to ensure we undo what the other test does to the jvm else ci fails\n        System.setProperty(\"karate.server.ssl\", \"\");\n        System.setProperty(\"karate.server.proxy\", \"\");\n    }\n\n    @Test\n    void testClient() {\n        Results result = Runner.path(\"classpath:com/intuit/karate/fatjar/client.feature\")\n                .configDir(\"classpath:com/intuit/karate/fatjar\")\n                .parallel(1);\n        assertEquals(result.getFailCount(), 0, result.getErrorMessages());\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/MainRunner.java",
    "content": "package com.intuit.karate.fatjar;\n\nimport com.intuit.karate.Main;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass MainRunner {\n\n    @Test\n    void testMain() {\n        Main.main(new String[]{\"-m\", \"src/test/java/com/intuit/karate/fatjar/server.feature\", \"-p\", \"8080\"});\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/MainSslRunner.java",
    "content": "package com.intuit.karate.fatjar;\n\nimport com.intuit.karate.Main;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass MainSslRunner {\n\n    @Test\n    void testMain() {\n        Main.main(new String[]{\"-m\", \"src/test/java/com/intuit/karate/fatjar/server.feature\", \"-p\", \"8443\", \"-s\"});\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/ProxyServerRunner.java",
    "content": "package com.intuit.karate.fatjar;\n\nimport com.intuit.karate.http.ProxyServer;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass ProxyServerRunner {\n\n    private static final Logger logger = LoggerFactory.getLogger(ProxyServerRunner.class);\n\n    @Test\n    void testProxy() {\n        ProxyServer proxy = new ProxyServer(5000, req -> {\n            logger.info(\"*** {}\", req.uri());\n            return null;\n        }, null);\n        proxy.waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/ProxyServerSslMain.java",
    "content": "package com.intuit.karate.fatjar;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.http.ProxyServer;\nimport java.io.File;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass ProxyServerSslMain {\n\n    static final org.slf4j.Logger logger = LoggerFactory.getLogger(ProxyServerSslMain.class);\n\n    String html = FileUtils.toString(new File(\"src/test/java/com/intuit/karate/fatjar/temp.html\"));\n\n    @Test\n    void testProxy() {\n        ProxyServer server = new ProxyServer(8090,\n                req -> {\n                    if (\"httpbin.org\".equals(req.context.host)) {\n                        return req.fake(200, html).header(\"Content-Type\", \"text/html\");\n                    }\n                    return null;\n                },\n                res -> {\n                    if (\"corte.si\".equals(res.context.host) && res.uri().contains(\"/index.html\")) {\n                        return res.fake(200, html).header(\"Content-Type\", \"text/html\");\n                    }\n                    return null;\n                });\n        server.waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/ProxyServerSslTest.java",
    "content": "package com.intuit.karate.fatjar;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.http.LenientTrustManager;\nimport com.intuit.karate.http.ProxyServer;\nimport com.intuit.karate.core.MockServer;\nimport java.io.InputStream;\nimport java.nio.charset.StandardCharsets;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.TrustManager;\nimport org.apache.http.HttpEntity;\nimport org.apache.http.HttpHost;\nimport org.apache.http.HttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.client.methods.HttpUriRequest;\nimport org.apache.http.conn.ssl.NoopHostnameVerifier;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.StringEntity;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClients;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass ProxyServerSslTest {\n\n    static final org.slf4j.Logger logger = LoggerFactory.getLogger(ProxyServerSslTest.class);\n\n    static ProxyServer proxy;\n    static MockServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        proxy = new ProxyServer(0, null, null);\n        server = MockServer.feature(\"classpath:com/intuit/karate/fatjar/server.feature\").https(0).build();\n        int port = server.getPort();\n        System.setProperty(\"karate.server.port\", port + \"\");\n        System.setProperty(\"karate.server.ssl\", \"true\");\n        System.setProperty(\"karate.server.proxy\", \"http://localhost:\" + proxy.getPort());\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n        proxy.stop();\n    }\n\n    // @Test\n    void testProxy() throws Exception {\n        String url = \"https://localhost:\" + server.getPort() + \"/v1/cats\";\n        assertEquals(200, http(get(url)));\n        assertEquals(200, http(post(url, \"{ \\\"name\\\": \\\"Billie\\\" }\")));\n        Results results = Runner\n                .path(\"classpath:com/intuit/karate/fatjar/client.feature\")\n                .configDir(\"classpath:com/intuit/karate/fatjar\")\n                .parallel(1);\n    }\n\n    static HttpUriRequest get(String url) {\n        return new HttpGet(url);\n    }\n\n    static HttpUriRequest post(String url, String body) {\n        HttpPost post = new HttpPost(url);\n        HttpEntity entity = new StringEntity(body, ContentType.create(\"application/json\", StandardCharsets.UTF_8));\n        post.setEntity(entity);\n        return post;\n    }\n\n    int http(HttpUriRequest request) throws Exception {\n        // System.setProperty(\"javax.net.debug\", \"all\"); // -Djavax.net.debug=all\n        SSLContext sc = SSLContext.getInstance(\"SSL\");\n        sc.init(null, new TrustManager[]{LenientTrustManager.INSTANCE}, null);\n        CloseableHttpClient client = HttpClients.custom()\n                .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)\n                .setSSLContext(sc)\n                .setProxy(new HttpHost(\"localhost\", proxy.getPort()))\n                .build();\n        HttpResponse response = client.execute(request);\n        InputStream is = response.getEntity().getContent();\n        String responseString = FileUtils.toString(is);\n        logger.debug(\"response: {}\", responseString);\n        return response.getStatusLine().getStatusCode();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/ProxyServerTest.java",
    "content": "package com.intuit.karate.fatjar;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.http.ProxyServer;\nimport com.intuit.karate.core.MockServer;\nimport java.io.InputStream;\nimport java.nio.charset.StandardCharsets;\nimport org.apache.http.HttpEntity;\nimport org.apache.http.HttpHost;\nimport org.apache.http.HttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.client.methods.HttpUriRequest;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.StringEntity;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClients;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass ProxyServerTest {\n\n    static final org.slf4j.Logger logger = LoggerFactory.getLogger(ProxyServerTest.class);\n\n    static ProxyServer proxy;\n    static MockServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        proxy = new ProxyServer(0, null, null);\n        server = MockServer\n                .feature(\"classpath:com/intuit/karate/fatjar/server.feature\")\n                .pathPrefix(\"/v1\")\n                .http(0).build();\n        int port = server.getPort();\n        System.setProperty(\"karate.server.port\", port + \"\");\n        System.setProperty(\"karate.server.ssl\", \"\"); // for ci\n        System.setProperty(\"karate.server.proxy\", \"http://localhost:\" + proxy.getPort());\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n        proxy.stop();\n    }\n\n    @Test\n    void testProxy() throws Exception {\n        String url = \"http://localhost:\" + server.getPort() + \"/v1/cats\";\n        assertEquals(200, http(get(url)));\n        assertEquals(200, http(post(url, \"{ \\\"name\\\": \\\"Billie\\\" }\")));\n        Results results = Runner\n                .path(\"classpath:com/intuit/karate/fatjar/client.feature\")\n                .configDir(\"classpath:com/intuit/karate/fatjar\")\n                .parallel(1);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n\n    static HttpUriRequest get(String url) {\n        return new HttpGet(url);\n    }\n\n    static HttpUriRequest post(String url, String body) {\n        HttpPost post = new HttpPost(url);\n        HttpEntity entity = new StringEntity(body, ContentType.create(\"application/json\", StandardCharsets.UTF_8));\n        post.setEntity(entity);\n        return post;\n    }\n\n    static int http(HttpUriRequest request) throws Exception {\n        CloseableHttpClient client = HttpClients.custom()\n                .setProxy(new HttpHost(\"localhost\", proxy.getPort()))\n                .build();\n        HttpResponse response = client.execute(request);\n        InputStream is = response.getEntity().getContent();\n        String responseString = FileUtils.toString(is);\n        logger.debug(\"response: {}\", responseString);\n        return response.getStatusLine().getStatusCode();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/client.feature",
    "content": "Feature:\n\nScenario: cats crud\n    Given url mockServerUrl + '/cats'\n\n    Given path 0\n    When method get\n    Then status 404\n\n    Given request { name: 'Billie' }\n    When method post\n    Then status 200\n    And match response == { id: '#number', name: 'Billie' }\n    * def billie = response\n\n    Given path billie.id\n    When method get\n    Then status 200\n    And match response == billie\n\n    Given request { name: 'Wild' }\n    When method post\n    Then status 200\n    And match response == { id: '#number', name: 'Wild' }\n    * def wild = response\n\n    Given path wild.id\n    When method get\n    Then status 200\n    And match response == wild\n\n    When method get\n    Then status 200\n    And match response contains ([billie, wild])\n    # And match header Access-Control-Allow-Origin == '*'\n\nScenario: body json path expression\n    Given url mockServerUrl + '/body/json'\n    And request { name: 'Scooby' }\n    When method post\n    Then match response == { success: true }\n    \nScenario: body xml path expression\n    Given url mockServerUrl + '/body/xml'\n    And request <dog><name>Scooby</name></dog>\n    When method post\n    Then match response == { success: true }\n\nScenario: karate.abort() test\n    Given url mockServerUrl + '/abort'\n    When method get\n    Then match response == { success: true }\n    * karate.abort()\n    * match 1 == 2\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/karate-config.js",
    "content": "function fn() {\n  var port = karate.properties['karate.server.port'];\n  port = port || '8080';\n  var ssl = karate.properties['karate.server.ssl'];\n  if (ssl) {\n    karate.log('using ssl:', ssl);\n    karate.configure('ssl', true);\n  }\n  var proxy = karate.properties['karate.server.proxy'];\n  if (proxy) {\n    karate.log('using proxy:', proxy);\n    karate.configure('proxy', proxy);\n  }\n  return { mockServerUrl: (ssl ? 'https' : 'http') + '://localhost:' + port + '/v1' };\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/proxy.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* print '>>', request\n* karate.proceed()\n* print '<<', response\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/server.feature",
    "content": "@ignore\nFeature:\n\nBackground:\n* def cats = {}\n* def id = 0\n\nScenario: pathMatches('/cats') && methodIs('post')\n    * def cat = request\n    * def id = ~~(id + 1)\n    * cat.id = id\n    * cats[id + ''] = cat\n    * def response = cat\n\nScenario: pathMatches('/cats') && methodIs('get')\n    * def response = $cats.*\n\nScenario: pathMatches('/cats/{id}') && methodIs('get')\n    * def response = cats[pathParams.id]\n    * def responseStatus = response ? 200 : 404\n\nScenario: pathMatches('/body/json') && bodyPath('$.name') == 'Scooby'\n    * def response = { success: true }\n\nScenario: pathMatches('/body/xml') && bodyPath('/dog/name') == 'Scooby'\n    * def response = { success: true }\n\nScenario: pathMatches('/abort')\n    * def response = { success: true }\n    * if (response.success) karate.abort()\n    # the next line will not be executed\n    * def response = { success: false }\n\nScenario:\n    * def responseStatus = 404\n    * def responseHeaders = { 'Content-Type': 'text/html; charset=utf-8' }\n    * def response = <html><body>Not Found</body></html>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/fatjar/temp.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>blah</title>\n  </head>\n  <body>\n    <h1>Hello World!</h1>\n  </body>\n</html>\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/graal/JsEngineTest.java",
    "content": "package com.intuit.karate.graal;\n\nimport com.intuit.karate.Match;\nimport com.intuit.karate.core.MockUtils;\nimport com.intuit.karate.http.Request;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\nimport org.graalvm.polyglot.Value;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass JsEngineTest {\n\n    static final Logger logger = LoggerFactory.getLogger(JsEngineTest.class);\n\n    JsEngine je;\n\n    @BeforeEach\n    void beforeEach() {\n        je = JsEngine.global();\n    }\n\n    @AfterEach\n    void afterEach() {\n        JsEngine.remove();\n    }\n\n    @Test\n    void testFunctionExecute() {\n        JsValue v = je.eval(\"(function(){ return ['a', 'b', 'c'] })\");\n        JsValue res = new JsValue(JsEngine.execute(v.getOriginal()));\n        assertTrue(res.isArray());\n        assertEquals(\"[\\\"a\\\",\\\"b\\\",\\\"c\\\"]\", res.toJsonOrXmlString(false));\n        assertEquals(\"function(){ return ['a', 'b', 'c'] }\", v.toString());\n    }\n\n    @Test\n    void testArrowFunctionZeroArg() {\n        JsValue v = je.eval(\"() => ['a', 'b', 'c']\");\n        assertTrue(v.isFunction());\n        JsValue res = new JsValue(JsEngine.execute(v.getOriginal()));\n        assertTrue(res.isArray());\n        assertEquals(\"[\\\"a\\\",\\\"b\\\",\\\"c\\\"]\", res.toJsonOrXmlString(false));\n        assertEquals(\"() => ['a', 'b', 'c']\", v.toString());\n    }\n\n    @Test\n    void testJsFunctionToJavaFunction() {\n        Value v = je.evalForValue(\"() => 'hello'\");\n        assertTrue(v.canExecute());\n        Function temp = (Function) v.as(Object.class);\n        String res = (String) temp.apply(null);\n        assertEquals(res, \"hello\");\n        v = je.evalForValue(\"(a, b) => a + b\");\n        assertTrue(v.canExecute());\n        temp = v.as(Function.class);\n        Number num = (Number) temp.apply(new Object[]{1, 2});\n        assertEquals(num, 3);\n    }\n\n    @Test\n    void testArrowFunctionReturnsObject() {\n        Value v = je.evalForValue(\"() => { a: 1 }\");\n        assertTrue(v.canExecute());\n        Value res = v.execute();\n        // curly braces are interpreted as code blocks :(\n        assertTrue(res.isNull());\n        v = je.evalForValue(\"() => ({ a: 1 })\");\n        assertTrue(v.canExecute());\n        res = v.execute();\n        Match.that(res.as(Map.class)).isEqualTo(\"{ a: 1 }\");\n    }\n\n    @Test\n    void testArrowFunctionSingleArg() {\n        JsValue v = je.eval(\"x => [x, x]\");\n        assertTrue(v.isFunction());\n        JsValue res = new JsValue(JsEngine.execute(v.getOriginal(), 1));\n        assertTrue(res.isArray());\n        assertEquals(\"[1,1]\", res.toJsonOrXmlString(false));\n        assertEquals(\"x => [x, x]\", v.toString());\n    }\n\n    @Test\n    void testFunctionVariableExecute() {\n        je.eval(\"var add = function(a, b){ return a + b }\");\n        JsValue jv = je.eval(\"add(1, 2)\");\n        assertEquals(jv.<Integer>getValue(), 3);\n    }\n\n    @Test\n    void testJavaInterop() {\n        je.eval(\"var SimplePojo = Java.type('com.intuit.karate.graal.SimplePojo')\");\n        JsValue sp = je.eval(\"new SimplePojo()\");\n        Value ov = sp.getOriginal();\n        assertTrue(ov.isHostObject());\n        SimplePojo o = ov.as(SimplePojo.class);\n        assertEquals(null, o.getFoo());\n        assertEquals(0, o.getBar());\n    }\n\n    @Test\n    void testJavaStaticMethod() {\n        je.eval(\"var StaticPojo = Java.type('com.intuit.karate.graal.StaticPojo')\");\n        JsValue sp = je.eval(\"StaticPojo.sayHello\");\n        assertTrue(sp.isFunction());\n        Value ov = sp.getOriginal();\n        assertTrue(ov.canExecute());\n        assertFalse(ov.isHostObject());\n    }\n    \n    @Test\n    void testJsNestedArraysToJava() {\n        je.eval(\"var StaticPojo = Java.type('com.intuit.karate.graal.StaticPojo')\");\n        JsValue sp = je.eval(\"StaticPojo.convert({foo:[{a:1}]})\");\n        assertEquals(\"{\\\"foo\\\":[{\\\"a\\\":1}]}\", sp.getAsString()); // bug fixed in graal 22.1\n    }\n\n    @Test\n    void testJsOperations() {\n        je.eval(\"var foo = { a: 1 }\");\n        JsValue v = je.eval(\"foo.a\");\n        Object val = v.getValue();\n        assertEquals(val, 1);\n    }\n\n    @Test\n    void testMapOperations() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"foo\", \"bar\");\n        map.put(\"a\", 1);\n        map.put(\"child\", Collections.singletonMap(\"baz\", \"ban\"));\n        je.put(\"map\", map);\n        JsValue v1 = je.eval(\"map.foo\");\n        assertEquals(v1.getValue(), \"bar\");\n        JsValue v2 = je.eval(\"map.a\");\n        assertEquals(v2.<Integer>getValue(), 1);\n        JsValue v3 = je.eval(\"map.child\");\n        assertEquals(v3.getValue(), Collections.singletonMap(\"baz\", \"ban\"));\n        JsValue v4 = je.eval(\"map.child.baz\");\n        assertEquals(v4.getValue(), \"ban\");\n    }\n\n    @Test\n    void testListOperations() {\n        je.eval(\"var temp = [{a: 1}, {b: 2}]\");\n        JsValue temp = je.eval(\"temp\");\n        je.put(\"items\", temp.getValue());\n        je.eval(\"items.push({c: 3})\");\n        JsValue items = je.eval(\"items\");\n        assertTrue(items.isArray());\n        assertEquals(3, items.getAsList().size());\n        je.eval(\"items.splice(0, 1)\");\n        items = je.eval(\"items\");\n        assertEquals(2, items.getAsList().size());\n    }\n\n    @Test\n    void testRequestObject() {\n        Request request = new Request();\n        request.setMethod(\"GET\");\n        request.setPath(\"/index\");\n        Map<String, List<String>> params = new HashMap();\n        params.put(\"hello\", Collections.singletonList(\"world\"));\n        request.setParams(params);\n        je.put(\"request\", request);\n        JsValue jv = je.eval(\"request.params['hello']\");\n        assertEquals(jv.getAsList(), Collections.singletonList(\"world\"));\n        jv = je.eval(\"request.param('hello')\");\n        assertEquals(jv.getValue(), \"world\");\n    }\n\n    @Test\n    void testBoolean() {\n        assertFalse(je.eval(\"1 == 2\").isTrue());\n        assertTrue(je.eval(\"1 == 1\").isTrue());\n    }\n\n    @Test\n    void testStringInterpolation() {\n        je.put(\"name\", \"John\");\n        JsValue temp = je.eval(\"`hello ${name}`\");\n        assertEquals(temp.getValue(), \"hello John\");\n    }\n\n    @Test\n    void testHostBytes() {\n        JsValue v = je.eval(\"Java.type('com.intuit.karate.core.MockUtils')\");\n        je.put(\"Utils\", v.getValue());\n        JsValue val = je.eval(\"Utils.testBytes\");\n        assertEquals(MockUtils.testBytes, val.getOriginal().asHostObject());\n    }\n\n    @Test\n    void testValueAndNull() {\n        Value v = Value.asValue(null);\n        assertNotNull(v);\n        assertTrue(v.isNull());\n        JsValue jv = new JsValue(v);\n        assertTrue(jv.isNull());\n        assertNull(jv.getValue());\n    }\n\n    @Test\n    void testValueAndHostObject() {\n        SimplePojo sp = new SimplePojo();\n        Value v = Value.asValue(sp);\n        assertTrue(v.isHostObject());\n    }\n\n    @Test\n    void testJavaType() {\n        Value v = je.evalForValue(\"Java.type('com.intuit.karate.graal.SimplePojo')\");\n        assertTrue(v.isMetaObject());\n        assertTrue(v.isHostObject());\n    }\n\n    @Test\n    void testJavaFunction() {\n        Value v = je.evalForValue(\"Java.type('com.intuit.karate.graal.StaticPojo').sayHello\");\n        assertFalse(v.isMetaObject());\n        assertFalse(v.isHostObject());\n        assertTrue(v.canExecute());\n    }\n\n    @Test\n    void testJavaFunctionFactory() {\n        Value v = je.evalForValue(\"Java.type('com.intuit.karate.graal.StaticPojo').sayHelloFactory()\");\n        assertFalse(v.isMetaObject());\n        assertTrue(v.isHostObject());\n        assertTrue(v.canExecute());\n    }\n\n    @Test\n    void testEvalWithinFunction() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"a\", 1);\n        map.put(\"b\", 2);\n        String src = \"a + b\";\n        Value function = je.evalForValue(\"x => { var a = x.a; var b = x.b; return \" + src + \"; }\");\n        assertTrue(function.canExecute());\n        Value result = function.execute(JsValue.fromJava(map));\n        assertEquals(result.asInt(), 3);\n    }\n    \n    @Test\n    void testObjectsWithinFunction() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"a\", 1);\n        map.put(\"b\", 2);\n        je.put(\"o\", map);\n        JsValue jv = je.eval(\"(function(){ return Object.entries(o) })()\");\n        List result = jv.getAsList();\n        Match.that(result).isEqualTo(\"[[a, 1],[b, 2]]\");\n    }     \n\n    @Test\n    void testEvalLocal() {\n        Map<String, Object> map = new HashMap();\n        map.put(\"a\", 1);\n        map.put(\"b\", 2);\n        Value result = je.evalWith(map, \"a + b\", true);\n        assertEquals(result.asInt(), 3);\n    }\n\n    @Test\n    void testEc6ArrayFilling() {\n        je.eval(\"var repeat = n => Array.from({length: n}, (v, k) => k);\");\n        JsValue jv = je.eval(\"repeat(2)\");\n        assertTrue(jv.isArray());\n        List list = jv.getAsList();\n        assertEquals(0, list.get(0));\n        assertEquals(1, list.get(1));\n    }\n\n    @Test\n    void testEc6ArrayIncludes() {\n        je.eval(\"var temp = ['a', 'b'];\");\n        JsValue jv = je.eval(\"temp.includes('a')\");\n        assertTrue(jv.isTrue());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/graal/JsValueTest.java",
    "content": "package com.intuit.karate.graal;\n\nimport java.math.BigInteger;\nimport java.util.Collections;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\npublic class JsValueTest {\n\n    @Test\n    void testTruthy() {\n        assertFalse(JsValue.isTruthy(null));\n        assertFalse(JsValue.isTruthy(false));\n        assertFalse(JsValue.isTruthy(Boolean.FALSE));\n        assertTrue(JsValue.isTruthy(true));\n        assertTrue(JsValue.isTruthy(Boolean.TRUE));\n        assertFalse(JsValue.isTruthy(0));\n        assertFalse(JsValue.isTruthy(0.0));\n        assertFalse(JsValue.isTruthy(BigInteger.ZERO));\n        assertTrue(JsValue.isTruthy(1));\n        assertTrue(JsValue.isTruthy(1.0));        \n        assertTrue(JsValue.isTruthy(BigInteger.ONE));\n        assertTrue(JsValue.isTruthy(\"\"));\n        assertTrue(JsValue.isTruthy(Collections.emptyList()));\n        assertTrue(JsValue.isTruthy(Collections.emptyMap()));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/graal/SimplePojo.java",
    "content": "package com.intuit.karate.graal;\n\n/**\n *\n * @author pthomas3\n */\npublic class SimplePojo {\n    \n    private String foo;\n    private int bar;\n\n    public String getFoo() {\n        return foo;\n    }\n\n    public void setFoo(String foo) {\n        this.foo = foo;\n    }\n\n    public int getBar() {\n        return bar;\n    }\n\n    public void setBar(int bar) {\n        this.bar = bar;\n    }\n    \n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/graal/StaticPojo.java",
    "content": "package com.intuit.karate.graal;\n\nimport com.intuit.karate.Json;\nimport java.util.function.Function;\n\n/**\n *\n * @author pthomas3\n */\npublic class StaticPojo {\n\n    public static String sayHello(String input) {\n        return \"hello \" + input;\n    }\n\n    public static Function<String, String> sayHelloFactory() {\n        return s -> sayHello(s);\n    }\n    \n    public static String convert(Object o) {\n        return Json.of(o).toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/AwsLambdaHandlerTest.java",
    "content": "package com.intuit.karate.http;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Json;\nimport com.intuit.karate.Match;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.InputStream;\nimport java.util.Map;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass AwsLambdaHandlerTest {\n\n    static final Logger logger = LoggerFactory.getLogger(AwsLambdaHandlerTest.class);\n\n    AwsLambdaHandler handler;\n    String sessionId;\n\n    void init(boolean classpath) {\n        ServerConfig config = classpath ? new ServerConfig(\"classpath:demo\") : new ServerConfig(\"src/test/java/demo\");\n        config.autoCreateSession(true);\n        handler = new AwsLambdaHandler(new RequestHandler(config));\n    }\n\n    String handle(String file) {\n        Map<String, Object> res = handleAsMap(file);\n        Map<String, String> headers = (Map) res.get(\"headers\");\n        String cookie = headers.get(\"Set-Cookie\");\n        if (cookie != null) {\n            sessionId = cookie;\n        }\n        return (String) res.get(\"body\");\n    }\n\n    Map<String, Object> handleAsMap(String file) {\n        InputStream is = getClass().getResourceAsStream(file);\n        String body = FileUtils.toString(is);\n        if (sessionId != null) {\n            body = body.replace(\"%%cookie%%\", sessionId);\n        }\n        is = new ByteArrayInputStream(FileUtils.toBytes(body));\n        ByteArrayOutputStream os = new ByteArrayOutputStream();\n        try {\n            handler.handle(is, os);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        byte[] bytes = os.toByteArray();\n        String json = FileUtils.toString(bytes);\n        return Json.of(json).asMap();\n    }\n\n    void testFormInternal(boolean root) {\n        String name = root ? \"root.json\" : \"index.json\";\n        String body = handle(name);\n        // logger.debug(\"{}\", body);\n        assertTrue(body.startsWith(\"<!doctype html>\"));\n        assertTrue(body.contains(\"<span>John Smith</span>\"));\n        assertTrue(body.contains(\"<td>Apple</td>\"));\n        assertTrue(body.contains(\"<td>Orange</td>\"));\n        assertTrue(body.contains(\"<span>Billie</span>\"));\n        body = handle(\"form.json\");\n        assertTrue(body.contains(\"<span>John</span>\"));\n    }\n\n    @Test\n    void testFormClassPath() {\n        init(true);\n        testFormInternal(false);\n    }\n    \n    @Test\n    void testRootClassPath() {\n        init(true);\n        testFormInternal(true);\n    }    \n\n    @Test\n    void testRootFileSystem() {\n        init(false);\n        testFormInternal(true);\n    }\n\n    void testApiInternal() {\n        Map<String, Object> res = handleAsMap(\"api.json\");\n        Map<String, String> headers = (Map) res.get(\"headers\");\n        String val = headers.get(\"foo\");\n        assertEquals(val, \"bar\");\n        String body = (String) res.get(\"body\");\n        assertEquals(\"{\\\"hello\\\":\\\"world\\\"}\", body);\n        Integer status = (Integer) res.get(\"statusCode\");\n        assertEquals(201, status);\n    }\n\n    @Test\n    void testApiClassPath() {\n        init(true);\n        testApiInternal();\n    }\n\n    @Test\n    void testApiFileSystem() {\n        init(false);\n        testApiInternal();\n    }\n    \n    void testCatsInternal() {\n        Map<String, Object> res = handleAsMap(\"cats.json\");\n        Json body = Json.of(res.get(\"body\"));\n        Match.that(body.asMap()).isEqualTo(\"{ name: 'Billie', id: '#number' }\");\n        Integer status = (Integer) res.get(\"statusCode\");\n        assertEquals(200, status);\n    }\n    \n    @Test\n    void testCatsClassPath() {\n        init(true);\n        testCatsInternal();\n    }    \n    \n    @Test\n    void testCatsFileSystem() {\n        init(false);\n        testCatsInternal();\n    }     \n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/GenericHttpHeaderTrackingTest.java",
    "content": "package com.intuit.karate.http;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass GenericHttpHeaderTrackingTest {\n\n    private GenericHttpHeaderTracking httpHeaderTracking;\n\n    @BeforeEach\n    void beforeEach() {\n        httpHeaderTracking = new GenericHttpHeaderTracking();\n    }\n\n    @Test\n    void testPutHeader() {\n        String header = \"X-Special-Header\";\n\n        Assertions.assertDoesNotThrow(() -> httpHeaderTracking.putHeaderReference(header));\n    }\n\n    @Test\n    void testPutHeaderWithNull() {\n        String header = null;\n\n        Assertions.assertDoesNotThrow(() -> httpHeaderTracking.putHeaderReference(header));\n    }\n\n    @Test\n    void testGetOriginalHeader() {\n        String header = \"X-Special-Header\";\n        httpHeaderTracking.putHeaderReference(header);\n\n        String result = httpHeaderTracking.getOriginalHeader(header);\n        Assertions.assertEquals(header, result);\n    }\n\n    @Test\n    void testGetOriginalHeaderWithoutExistingHeaderInTracking() {\n        String header = \"X-Special-Header\";\n\n        String result = httpHeaderTracking.getOriginalHeader(header);\n        Assertions.assertEquals(header, result);\n    }\n\n    @Test\n    void testGetOriginalHeaderWithNull() {\n        String header = null;\n\n        String result = httpHeaderTracking.getOriginalHeader(header);\n        Assertions.assertNull(result);\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/HttpClientTester.java",
    "content": "package com.intuit.karate.http;\n\nimport com.intuit.karate.FileUtils;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass HttpClientTester {\n\n    static final Logger logger = LoggerFactory.getLogger(HttpClientTester.class);\n\n    @Test\n    void testGet() {\n        ArmeriaHttpClient client = new ArmeriaHttpClient(null, null);\n        HttpRequestBuilder http = new HttpRequestBuilder(client);\n        Response response = http.url(\"https://jsonplaceholder.typicode.com/users/1\").header(\"Accept\", \"application/json\").invoke();\n        String body = FileUtils.toString(response.getBody());\n        logger.debug(\"response: {}\", body);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/HttpHookTest.java",
    "content": "package com.intuit.karate.http;\n\nimport com.intuit.karate.Http;\nimport com.intuit.karate.RuntimeHook;\nimport com.intuit.karate.core.MockServer;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport org.junit.jupiter.api.BeforeAll;\n\nclass HttpHookTest {\n   \n    static MockServer server;\n    \n    @BeforeAll\n    static void beforeAll() {\n        server = MockServer\n                .feature(\"classpath:com/intuit/karate/http/mock.feature\")\n                .http(0).build();\n    }    \n\n    static class TestRuntimeHook implements RuntimeHook {\n        @Override\n        public void beforeHttpCall(HttpRequest request, ScenarioRuntime sr) {\n            String url = request.getUrl();\n            request.setUrl(url + \"/foo\");\n        }\n\n        @Override\n        public void afterHttpCall(HttpRequest request, Response response, ScenarioRuntime sr) {\n            response.setBody(response.json().set(\"bar\", \"baz\").toString());\n        }\n    }\n\n    @Test\n    void testInvokeWithoutHook() {\n        Response response = Http.to(\"http://localhost:\" + server.getPort() + \"/hello\").get();\n        assertEquals(\"/hello\", response.json().get(\"path\").toString());\n    }\n\n    @Test\n    void testInvokeWithHook() {\n        Response response = Http.to(\"http://localhost:\" + server.getPort() + \"/hello\")\n                .hook(new TestRuntimeHook()).get();\n        assertEquals(\"/hello/foo\", response.json().get(\"path\").toString());\n        assertEquals(\"baz\", response.json().get(\"bar\").toString());\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/HttpLoggerTest.java",
    "content": "package com.intuit.karate.http;\n\nimport com.intuit.karate.LogAppender;\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.core.Config;\nimport com.intuit.karate.core.DummyClient;\nimport com.intuit.karate.core.MockHandler;\nimport com.intuit.karate.core.Variable;\nimport com.intuit.karate.shell.StringLogAppender;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport static com.intuit.karate.TestUtils.FeatureBuilder;\nimport static com.intuit.karate.TestUtils.match;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n/**\n * Test body and content type handling for request and response logging.\n * @author edwardsph\n */\nclass HttpLoggerTest {\n\n    HttpClient client = new DummyClient();\n    MockHandler handler;\n    FeatureBuilder feature;\n    HttpRequestBuilder httpRequestBuilder;\n    HttpRequest request;\n    Logger testLogger = new Logger();\n    Config config;\n    LogAppender logAppender = new StringLogAppender(false);\n    HttpLogger httpLogger;\n\n    private static final String TURTLE_SAMPLE = \"<http://example.org/hello> <http://example.org/#linked> <http://example.org/world> .\";\n\n    @BeforeEach\n    void beforeEach() {\n        httpRequestBuilder = new HttpRequestBuilder(client).url(\"/\").method(\"GET\");\n        testLogger.setAppender(logAppender);\n        httpLogger = new HttpLogger(testLogger);\n        config = new Config();\n    }\n\n    void setup(String path, String body, String contentType) {\n        feature = FeatureBuilder.background().scenario(\n                \"pathMatches('/\"+ path + \"')\",\n                \"def response = '\" + body + \"'\",\n                \"def responseHeaders = {'Content-Type': '\" + contentType + \"'}\"\n        );\n    }\n\n    private Response handle() {\n        handler = new MockHandler(feature.build());\n        request = httpRequestBuilder.build();\n        Response response = handler.handle(request.toRequest());\n        httpRequestBuilder = new HttpRequestBuilder(client).method(\"GET\");\n        return response;\n    }\n\n    @Test\n    void testRequestLoggingPlain() {\n        HttpRequest httpRequest = httpRequestBuilder.body(\"hello\").contentType(\"text/plain\").path(\"/plain\").build();\n        httpLogger.logRequest(config, httpRequest);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(\"hello\"));\n        assertTrue(logs.contains(\"Content-Type: text/plain\"));\n    }\n\n    @Test\n    void testRequestLoggingJson() {\n        HttpRequest httpRequest = httpRequestBuilder.body(\"{a: 1}\").contentType(\"application/json\").path(\"/ttl\").build();\n        httpLogger.logRequest(config, httpRequest);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(\"{a: 1}\"));\n        assertTrue(logs.contains(\"Content-Type: application/json\"));\n    }\n\n    @Test\n    void testRequestLoggingXml() {\n        HttpRequest httpRequest = httpRequestBuilder.body(\"<hello>world</hello>\").contentType(\"application/xml\").path(\"/ttl\").build();\n        httpLogger.logRequest(config, httpRequest);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(\"<hello>world</hello>\"));\n        assertTrue(logs.contains(\"Content-Type: application/xml\"));\n    }\n\n    @Test\n    void testRequestLoggingTurtle() {\n        HttpRequest httpRequest = httpRequestBuilder.body(TURTLE_SAMPLE).contentType(\"text/turtle\").path(\"/ttl\").build();\n        httpLogger.logRequest(config, httpRequest);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(TURTLE_SAMPLE));\n        assertTrue(logs.contains(\"Content-Type: text/turtle\"));\n    }\n\n    @Test\n    void testRequestLoggingTurtleWithCharset() {\n        HttpRequest httpRequest = httpRequestBuilder.body(TURTLE_SAMPLE).contentType(\"text/turtle; charset=UTF-8\").path(\"/ttl\").build();\n        httpLogger.logRequest(config, httpRequest);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(TURTLE_SAMPLE));\n        assertTrue(logs.contains(\"Content-Type: text/turtle; charset=UTF-8\"));\n    }\n\n    @Test\n    void testRequestLoggingJsonPretty() {\n        config.configure(\"logPrettyRequest\", new Variable(true));\n        HttpRequest httpRequest = httpRequestBuilder.body(\"{a: 1}\").contentType(\"application/json\").path(\"/ttl\").build();\n        httpLogger.logRequest(config, httpRequest);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(\"{\\n  \\\"a\\\": 1\\n}\"));\n        assertTrue(logs.contains(\"Content-Type: application/json\"));\n    }\n\n    @Test\n    void testRequestLoggingXmlPretty() {\n        config.configure(\"logPrettyRequest\", new Variable(true));\n        HttpRequest httpRequest = httpRequestBuilder.body(\"<hello>world</hello>\").contentType(\"application/xml\").path(\"/ttl\").build();\n        httpLogger.logRequest(config, httpRequest);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(\"<hello>world</hello>\"));\n        assertTrue(logs.contains(\"Content-Type: application/xml\"));\n    }\n\n    @Test\n    void testRequestLoggingTurtlePretty() {\n        config.configure(\"logPrettyRequest\", new Variable(true));\n        HttpRequest httpRequest = httpRequestBuilder.body(TURTLE_SAMPLE).contentType(\"text/turtle\").path(\"/ttl\").build();\n        httpLogger.logRequest(config, httpRequest);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(TURTLE_SAMPLE));\n        assertTrue(logs.contains(\"Content-Type: text/turtle\"));\n    }\n\n    @Test\n    void testResponseLoggingPlain() {\n        setup(\"plain\", \"hello\", \"text/plain\");\n        httpRequestBuilder.path(\"/plain\");\n        Response response = handle();\n        match(response.getBodyAsString(), \"hello\");\n        match(response.getContentType(), \"text/plain\");\n\n        httpLogger.logResponse(config, request, response);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(\"hello\"));\n        assertTrue(logs.contains(\"Content-Type: text/plain\"));\n    }\n\n    @Test\n    void testResponseLoggingJson() {\n        setup(\"json\", \"{a: 1}\", \"application/json\");\n        httpRequestBuilder.path(\"/json\");\n        Response response = handle();\n        match(response.getBodyAsString(), \"{a: 1}\");\n        match(response.getContentType(), \"application/json\");\n\n        httpLogger.logResponse(config, request, response);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(\"{a: 1}\"));\n        assertTrue(logs.contains(\"Content-Type: application/json\"));\n    }\n\n    @Test\n    void testResponseLoggingXml() {\n        setup(\"xml\", \"<hello>world</hello>\", \"application/xml\");\n        httpRequestBuilder.path(\"/xml\");\n        Response response = handle();\n        match(response.getBodyAsString(), \"<hello>world</hello>\");\n        match(response.getContentType(), \"application/xml\");\n\n        httpLogger.logResponse(config, request, response);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(\"<hello>world</hello>\"));\n        assertTrue(logs.contains(\"Content-Type: application/xml\"));\n    }\n\n    @Test\n    void testResponseLoggingTurtle() {\n        setup(\"ttl\", TURTLE_SAMPLE, \"text/turtle\");\n        httpRequestBuilder.path(\"/ttl\");\n        Response response = handle();\n        assertEquals(response.getBodyAsString(), TURTLE_SAMPLE);\n        assertTrue(response.getContentType().contains(\"text/turtle\"));\n\n        httpLogger.logResponse(config, request, response);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(TURTLE_SAMPLE));\n        assertTrue(logs.contains(\"Content-Type: text/turtle\"));\n    }\n\n    @Test\n    void testResponseLoggingTurtleWithCharset() {\n        setup(\"ttl\", TURTLE_SAMPLE, \"text/turtle; charset=UTF-8\");\n        httpRequestBuilder.path(\"/ttl\");\n        Response response = handle();\n        assertEquals(response.getBodyAsString(), TURTLE_SAMPLE);\n        assertEquals(response.getContentType(), \"text/turtle; charset=UTF-8\");\n\n        httpLogger.logResponse(config, request, response);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(TURTLE_SAMPLE));\n        assertTrue(logs.contains(\"Content-Type: text/turtle; charset=UTF-8\"));\n    }\n\n    @Test\n    void testResponseLoggingJsonPretty() {\n        config.configure(\"logPrettyResponse\", new Variable(true));\n        setup(\"json\", \"{a: 1}\", \"application/json\");\n        httpRequestBuilder.path(\"/json\");\n        Response response = handle();\n        match(response.getBodyAsString(), \"{a: 1}\");\n        match(response.getContentType(), \"application/json\");\n\n        httpLogger.logResponse(config, request, response);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(\"{\\n  \\\"a\\\": 1\\n}\"));\n        assertTrue(logs.contains(\"Content-Type: application/json\"));\n    }\n\n    @Test\n    void testResponseLoggingXmlPretty() {\n        config.configure(\"logPrettyResponse\", new Variable(true));\n        setup(\"xml\", \"<hello>world</hello>\", \"application/xml\");\n        httpRequestBuilder.path(\"/xml\");\n        Response response = handle();\n        match(response.getBodyAsString(), \"<hello>world</hello>\");\n        match(response.getContentType(), \"application/xml\");\n\n        httpLogger.logResponse(config, request, response);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(\"<hello>world</hello>\"));\n        assertTrue(logs.contains(\"Content-Type: application/xml\"));\n    }\n\n    @Test\n    void testResponseLoggingTurtlePretty() {\n        config.configure(\"logPrettyResponse\", new Variable(true));\n        setup(\"ttl\", TURTLE_SAMPLE, \"text/turtle\");\n        httpRequestBuilder.path(\"/ttl\");\n        Response response = handle();\n        assertEquals(response.getBodyAsString(), TURTLE_SAMPLE);\n        assertTrue(response.getContentType().contains(\"text/turtle\"));\n\n        httpLogger.logResponse(config, request, response);\n        String logs = logAppender.collect();\n        assertTrue(logs.contains(TURTLE_SAMPLE));\n        assertTrue(logs.contains(\"Content-Type: text/turtle\"));\n    }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/HttpRequestBuilderTest.java",
    "content": "package com.intuit.karate.http;\n\nimport com.intuit.karate.core.ScenarioEngine;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass HttpRequestBuilderTest {\n\n    static final Logger logger = LoggerFactory.getLogger(HttpRequestBuilderTest.class);\n\n    HttpRequestBuilder http;\n\n    @BeforeEach\n    void beforeEach() {\n        ScenarioEngine se = ScenarioEngine.forTempUse(HttpClientFactory.DEFAULT);\n        http = new HttpRequestBuilder(HttpClientFactory.DEFAULT.create(se));\n    }\n\n    @Test\n    void testUrlAndPath() {\n        http.url(\"http://host/foo\");\n        assertEquals(\"http://host/foo\", http.getUri());\n        http.path(\"/bar\");\n        assertEquals(\"http://host/foo/bar\", http.getUri());\n    }\n\n    @Test\n    void testUrlAndPathWithSlash() {\n        http.url(\"http://host/foo/\");\n        assertEquals(\"http://host/foo/\", http.getUri());\n        http.path(\"/bar/\");\n        assertEquals(\"http://host/foo/bar\", http.getUri());\n    }\n    \n    @Test\n    void testUrlAndPathWithTrailingSlash() {\n        http.url(\"http://host/foo\");\n        assertEquals(\"http://host/foo\", http.getUri());\n        http.path(\"bar\");\n        http.path(\"/\");\n        assertEquals(\"http://host/foo/bar/\", http.getUri());\n    }    \n    \n    @Test\n    void testUrlAndPathWithEncodedSlash() {\n        http.url(\"http://host\");\n        assertEquals(\"http://host\", http.getUri());\n        http.path(\"foo\\\\/bar\");\n        assertEquals(\"http://host/foo%2Fbar\", http.getUri());\n    }     \n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/HttpUtilsTest.java",
    "content": "package com.intuit.karate.http;\n\nimport static com.intuit.karate.TestUtils.*;\nimport com.intuit.karate.StringUtils;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Map;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass HttpUtilsTest {    \n\n    @Test\n    void testParseContentTypeCharset() {\n        assertEquals(StandardCharsets.UTF_8, HttpUtils.parseContentTypeCharset(\"application/json; charset=UTF-8\"));\n        assertEquals(StandardCharsets.UTF_8, HttpUtils.parseContentTypeCharset(\"application/json; charset = UTF-8 \"));\n        assertEquals(StandardCharsets.UTF_8, HttpUtils.parseContentTypeCharset(\"application/json; charset=UTF-8; version=1.2.3\"));\n        assertEquals(StandardCharsets.UTF_8, HttpUtils.parseContentTypeCharset(\"application/json; charset = UTF-8 ; version=1.2.3\"));\n    }\n\n    @Test\n    void testParseContentTypeParams() {\n        Map<String, String> map = HttpUtils.parseContentTypeParams(\"application/json\");\n        assertNull(map);\n        map = HttpUtils.parseContentTypeParams(\"application/json; charset=UTF-8\");\n        match(map, \"{ charset: 'UTF-8' }\");\n        map = HttpUtils.parseContentTypeParams(\"application/json; charset = UTF-8 \");\n        match(map, \"{ charset: 'UTF-8' }\");\n        map = HttpUtils.parseContentTypeParams(\"application/json; charset=UTF-8; version=1.2.3\");\n        match(map, \"{ charset: 'UTF-8', version: '1.2.3' }\");\n        map = HttpUtils.parseContentTypeParams(\"application/json; charset = UTF-8 ; version=1.2.3\");\n        match(map, \"{ charset: 'UTF-8', version: '1.2.3' }\");\n        map = HttpUtils.parseContentTypeParams(\"application/vnd.app.test+json;ton-version=1\");\n        match(map, \"{ 'ton-version': '1' }\");\n    }\n\n    @Test\n    void testParseUriPathPatterns() {\n        Map<String, String> map = HttpUtils.parseUriPattern(\"/cats/{id}\", \"/cats/1\");\n        match(map, \"{ id: '1' }\");\n        map = HttpUtils.parseUriPattern(\"/cats/{id}/\", \"/cats/1\"); // trailing slash\n        match(map, \"{ id: '1' }\");\n        map = HttpUtils.parseUriPattern(\"/cats/{id}\", \"/cats/1/\"); // trailing slash\n        match(map, \"{ id: '1' }\");\n        map = HttpUtils.parseUriPattern(\"/cats/{id}\", \"/foo/bar\");\n        match(map, null);\n        map = HttpUtils.parseUriPattern(\"/cats\", \"/cats/1\"); // exact match\n        match(map, null);\n        map = HttpUtils.parseUriPattern(\"/{path}/{id}\", \"/cats/1\");\n        match(map, \"{ path: 'cats', id: '1' }\");\n        map = HttpUtils.parseUriPattern(\"/cats/{id}/foo\", \"/cats/1/foo\");\n        match(map, \"{ id: '1' }\");\n        map = HttpUtils.parseUriPattern(\"/api/{img}\", \"/api/billie.jpg\");\n        match(map, \"{ img: 'billie.jpg' }\");\n        map = HttpUtils.parseUriPattern(\"/hello/{raw}\", \"/hello/�Ill~Formed@RequiredString!\");\n        match(map, \"{ raw: '�Ill~Formed@RequiredString!' }\");\n    }\n    \n    static void splitUrl(String raw, String left, String right) {\n        StringUtils.Pair pair = HttpUtils.parseUriIntoUrlBaseAndPath(raw);\n        assertEquals(left, pair.left);\n        assertEquals(right, pair.right);\n    }\n\n    @Test\n    void testUriParsing() {\n        splitUrl(\"http://foo/bar\", \"http://foo\", \"/bar\");\n        splitUrl(\"/bar\", null, \"/bar\");\n        splitUrl(\"/bar?baz=ban\", null, \"/bar?baz=ban\");\n        splitUrl(\"http://foo/bar?baz=ban\", \"http://foo\", \"/bar?baz=ban\");\n        splitUrl(\"localhost:50856\", null, \"\");\n        splitUrl(\"127.0.0.1:50856\", null, \"\");\n        splitUrl(\"http://foo:8080/bar\", \"http://foo:8080\", \"/bar\");\n        splitUrl(\"http://foo.com:8080/bar\", \"http://foo.com:8080\", \"/bar\");\n        splitUrl(\"https://api.randomuser.me/?nat=us\", \"https://api.randomuser.me\", \"/?nat=us\");\n    }    \n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/MultiPartBuilderTest.java",
    "content": "package com.intuit.karate.http;\n\nimport com.intuit.karate.FileUtils;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass MultiPartBuilderTest {\n\n    static final Logger logger = LoggerFactory.getLogger(MultiPartBuilderTest.class);\n\n    String join(String... lines) {\n        StringBuilder sb = new StringBuilder();\n        Iterator<String> iterator = Arrays.asList(lines).iterator();\n        while (iterator.hasNext()) {\n            sb.append(iterator.next()).append('\\r').append('\\n');\n        }\n        return sb.toString();\n    }\n\n    @Test\n    void testMultiPart() {\n        MultiPartBuilder builder = new MultiPartBuilder(true, null);\n        builder.part(\"bar\", \"hello world\");\n        byte[] bytes = builder.build();\n        String boundary = builder.getBoundary();\n        String actual = FileUtils.toString(bytes);\n        String expected = join(\n                \"--\" + boundary,\n                \"content-disposition: form-data; name=\\\"bar\\\"\",\n                \"content-length: 11\",\n                \"content-type: text/plain\",\n                \"\",\n                \"hello world\",\n                \"--\" + boundary + \"--\"\n        );\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testUrlEncoded() {\n        MultiPartBuilder builder = new MultiPartBuilder(false, null);\n        builder.part(\"bar\", \"hello world\");\n        byte[] bytes = builder.build();\n        assertEquals(\"application/x-www-form-urlencoded\", builder.getContentTypeHeader());\n        String actual = FileUtils.toString(bytes);\n        assertEquals(\"bar=hello+world\", actual);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/ProxyContextTest.java",
    "content": "package com.intuit.karate.http;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass ProxyContextTest {\n\n    static void test(String uri, boolean ssl, String host, int port) {\n        ProxyContext hp = new ProxyContext(uri, ssl);\n        assertEquals(host, hp.host);\n        assertEquals(port, hp.port);\n    }\n\n    @Test\n    void testProxyContext() {\n        test(\"http://localhost:8080\", false, \"localhost\", 8080);\n        test(\"http://localhost:8080/foo\", false, \"localhost\", 8080);\n        test(\"localhost:8080\", false, \"localhost\", 8080);\n        test(\"localhost:8080/foo\", false, \"localhost\", 8080);\n        test(\"localhost\", false, \"localhost\", 80);\n        test(\"localhost/foo\", false, \"localhost\", 80);\n        test(\"http://localhost\", false, \"localhost\", 80);\n        test(\"http://localhost/foo\", false, \"localhost\", 80);\n        test(\"httpbin.org:443\", false, \"httpbin.org\", 443);\n        test(\"httpbin.org:443\", true, \"httpbin.org\", 443);\n        test(\"httpbin.org:443/foo\", true, \"httpbin.org\", 443);\n        test(\"httpbin.org\", true, \"httpbin.org\", 443);\n        test(\"httpbin.org/foo\", true, \"httpbin.org\", 443);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/RequestHandlerTest.java",
    "content": "package com.intuit.karate.http;\n\nimport com.intuit.karate.Match;\nimport java.util.List;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass RequestHandlerTest {\n\n    static final Logger logger = LoggerFactory.getLogger(RequestHandlerTest.class);\n\n    RequestHandler handler;\n    HttpRequestBuilder request;\n    Response response;\n    List<String> cookies;\n    String body;\n\n    @BeforeEach\n    void beforeEach() {\n        ServerConfig config = new ServerConfig(\"classpath:demo\");\n        config.autoCreateSession(true);\n        handler = new RequestHandler(config);\n        request = new HttpRequestBuilder(null).url(\"/\").method(\"GET\");\n    }\n\n    private Response handle() {\n        response = handler.handle(request.build().toRequest());\n        body = response.getBodyAsString();\n        cookies = response.getHeaderValues(\"Set-Cookie\");\n        request = new HttpRequestBuilder(null).url(\"/\").method(\"GET\");\n        if (cookies != null) {\n            request.header(\"Cookie\", cookies);\n        }\n        return response;\n    }\n\n    private void matchHeaderEquals(String name, String expected) {\n        Match.Result mr = Match.evaluate(response.getHeader(name)).isEqualTo(expected);\n        assertTrue(mr.pass, mr.message);\n    }\n\n    private void matchHeaderContains(String name, String expected) {\n        Match.Result mr = Match.evaluate(response.getHeader(name)).contains(expected);\n        assertTrue(mr.pass, mr.message);\n    }\n\n    @Test\n    void testIndexAndAjaxPost() {\n        request.path(\"index\");\n        handle();\n        matchHeaderContains(\"Set-Cookie\", \"karate.sid\");\n        matchHeaderEquals(\"Content-Type\", \"text/html\");\n        assertTrue(body.startsWith(\"<!doctype html>\"));\n        assertTrue(body.contains(\"<span>John Smith</span>\"));\n        assertTrue(body.contains(\"<td>Apple</td>\"));\n        assertTrue(body.contains(\"<td>Orange</td>\"));\n        assertTrue(body.contains(\"<span>Billie</span>\"));\n        request.path(\"person\")\n                .contentType(\"application/x-www-form-urlencoded\")\n                .header(\"HX-Request\", \"true\")\n                .body(\"firstName=John&lastName=Smith&email=john%40smith.com\")\n                .method(\"POST\");\n        handle();\n        assertTrue(body.contains(\"<span>John</span>\"));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/WebSocketClientRunner.java",
    "content": "package com.intuit.karate.http;\n\nimport com.intuit.karate.Logger;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass WebSocketClientRunner {\n\n    static final Logger logger = new Logger();\n    String result;\n\n    @Test\n    void testWebSockets() throws Exception {\n        String url = \"wss://ws.postman-echo.com/raw\";\n        WebSocketOptions options = new WebSocketOptions(url);\n        options.setTextConsumer(text -> {\n            synchronized (this) {\n                result = text;\n                notify();\n            }\n        });\n        WebSocketClient client = new WebSocketClient(options, logger);\n        client.send(\"hello world !\");\n        synchronized (this) {\n            wait();\n        }\n        assertEquals(\"hello world !\", result);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/WebSocketProxyRunner.java",
    "content": "package com.intuit.karate.http;\n\nimport com.intuit.karate.Logger;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass WebSocketProxyRunner {\n\n    static final Logger logger = new Logger();\n\n    @Test\n    void testProxy() {\n        String url = \"ws://127.0.0.1:4444/22c71715e7433fffe615b0b9b2583169\";\n        String path = url.substring(url.lastIndexOf('/'));\n        logger.debug(\"path: {}\", path);\n        WebSocketProxyServer server = new WebSocketProxyServer(8090, url, path);\n        server.waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/WebSocketTempRunner.java",
    "content": "package com.intuit.karate.http;\n\nimport com.intuit.karate.Logger;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass WebSocketTempRunner {\n\n    static final Logger logger = new Logger();\n\n    @Test\n    void testWebSockets() throws Exception {\n        String url = \"ws://127.0.0.1:49156/e543365b10ee4dca01cdd99171a58c5d\";\n        WebSocketOptions options = new WebSocketOptions(url);\n        options.setTextConsumer(text -> {\n            logger.debug(\"<< {}\", text);\n        });\n        WebSocketClient client = new WebSocketClient(options, logger);\n        client.waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/api.json",
    "content": "{\n    \"rawPath\": \"/api/demo\",\n    \"isBase64Encoded\": false,\n    \"requestContext\": {\n        \"http\": {\n            \"method\": \"GET\"\n        }\n    }\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/cats.json",
    "content": "{\n    \"rawPath\": \"/api/cats\",\n    \"isBase64Encoded\": false,\n    \"headers\": {\n        \"Content-Type\": \"application/x-www-form-urlencoded\"\n    },\n    \"requestContext\": {\n        \"http\": {\n            \"method\": \"POST\"\n        }\n    },\n    \"body\": \"{\\\"name\\\":\\\"Billie\\\"}\"\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/form.json",
    "content": "{\n    \"rawPath\": \"/person\",\n    \"isBase64Encoded\": false,\n    \"headers\": {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        \"hx-request\": \"true\",\n        \"cookie\": \"%%cookie%%\"\n    },\n    \"requestContext\": {\n        \"http\": {\n            \"method\": \"POST\"\n        }\n    },\n    \"body\": \"firstName=John&lastName=Smith&email=john%40smith.com\"\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/index.json",
    "content": "{\n    \"rawPath\": \"/\",\n    \"isBase64Encoded\": false,\n    \"queryStringParameters\": null,\n    \"headers\": null,\n    \"requestContext\": {\n        \"http\": {\n            \"method\": \"GET\"\n        }\n    },\n    \"body\": null\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/mock.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* def response = ({ body: null, path: requestPath })\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/http/root.json",
    "content": "{\n    \"rawPath\": \"/index\",\n    \"isBase64Encoded\": false,\n    \"queryStringParameters\": null,\n    \"headers\": null,\n    \"requestContext\": {\n        \"http\": {\n            \"method\": \"GET\"\n        }\n    },\n    \"body\": null\n}"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/malformed.txt",
    "content": "{\n   \"errors\":[\n      {\n         \"errCode\":\"TestCode1\",\n         \"errMsg\":\"TestCode1 Message\"\n      }x\n      {\n         \"errCode\":\"TestCode2\",\n         \"errMsg\":\"TestCode2 Message\"\n      }\n   ]\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/report/ReportUtilsTest.java",
    "content": "package com.intuit.karate.report;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.FeatureRuntime;\nimport com.intuit.karate.Suite;\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.Results;\nimport com.intuit.karate.core.FeatureCall;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.PrintStream;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass ReportUtilsTest {\n\n    static final Logger logger = LoggerFactory.getLogger(ReportUtilsTest.class);\n\n    @Test\n    void testReport() {\n        final ByteArrayOutputStream outContent = new ByteArrayOutputStream();\n        final PrintStream originalOut = System.out;\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/report/test.feature\");\n        FeatureRuntime fr = FeatureRuntime.of(feature);\n        fr.run();\n        Report report = SuiteReports.DEFAULT.featureReport(fr.suite, fr.result);\n        File file = report.render(\"target/report-test\");\n        String html = FileUtils.toString(file);\n        assertTrue(html.contains(\"<title>com.intuit.karate.report.test</title>\"));\n        assertTrue(html.contains(\"<img src=\\\"karate-labs-logo-ring.svg\\\" alt=\\\"Karate Labs\\\"/>\"));\n        assertTrue(html.contains(\"<div>Scenarios</div>\"));\n        assertTrue(html.contains(\"<a href=\\\"karate-summary.html\\\">Summary</a><span class=\\\"feature-label\\\">|</span>\"));\n        System.setOut(new PrintStream(outContent)); // Capture console output\n        fr.suite.buildResults();\n        assertFalse(outContent.toString().contains(\" | env: \"));\n        System.setOut(originalOut); // restore console output\n        // render summary report\n        Runner.Builder builder = new Runner.Builder();\n        builder.reportDir(\"target/report-test\");\n        Suite suite = new Suite(builder);\n        File jsonFile = ReportUtils.saveKarateJson(\"target/report-test\", fr.result, null);\n        suite.featureResultFiles.add(jsonFile);\n        Results results = Results.of(suite);  // this will render summary via constructor TODO improve        \n    }\n\n    @Test\n    void testReportWithEnv() {\n        final String sEnv = \"TestEnv\";\n        final ByteArrayOutputStream outContent = new ByteArrayOutputStream();\n        final PrintStream originalOut = System.out;\n        Feature oFeature = Feature.read(\"classpath:com/intuit/karate/report/test.feature\");\n        Suite oSuite = new Suite(Runner.builder().karateEnv(sEnv));\n        FeatureRuntime fr = FeatureRuntime.of(oSuite, new FeatureCall(oFeature));\n        fr.run();\n        Report oReport = SuiteReports.DEFAULT.featureReport(fr.suite, fr.result);\n        File oFile = oReport.render(\"target/report-test-env\");\n        String sHtml = FileUtils.toString(oFile);\n        assertTrue(sHtml.contains(\"<div id=\\\"nav-env\\\">\"));\n        assertTrue(sHtml.contains(sEnv));\n        System.setOut(new PrintStream(outContent)); // Capture console output\n        fr.suite.buildResults();\n        assertTrue(outContent.toString().contains(\" | env: \" + sEnv));\n        System.setOut(originalOut);  // restore console output\n    }\n\n    @Test\n    void testCustomTags() {\n        String expectedCustomTags = \"<properties><property name=\\\"requirement\\\" value=\\\"CALC-2\\\"/><property name=\\\"test_key\\\" value=\\\"CALC-2\\\"/></properties>\";\n        Feature feature = Feature.read(\"classpath:com/intuit/karate/report/customTags.feature\");\n        FeatureRuntime fr = FeatureRuntime.of(new Suite(), new FeatureCall(feature));\n        fr.run();\n        File file = ReportUtils.saveJunitXml(\"target\", fr.result, null);\n        assertTrue(FileUtils.toString(file).contains(expectedCustomTags));\n    }\n    \n    @Test\n    void testFailAnnotationXML() {\n    \tString expectedSystemout = \"<system-out>\"; \n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/fail-tag.feature\");\n        FeatureRuntime fr = FeatureRuntime.of(new Suite(), new FeatureCall(feature));\n        fr.run();\n        File file = ReportUtils.saveJunitXml(\"target\", fr.result, null);\n        assertTrue(FileUtils.toString(file).contains(expectedSystemout));\n    }\n    \n    @Test\n    void testFailAnnotationXMLFailure() {\n    \tString expectedSystemout = \"<failure\"; \n        Feature feature = Feature.read(\"classpath:com/intuit/karate/core/fail-tag-failure.feature\");\n        FeatureRuntime fr = FeatureRuntime.of(new Suite(), new FeatureCall(feature));\n        fr.run();\n        File file = ReportUtils.saveJunitXml(\"target\", fr.result, null);\n        assertTrue(FileUtils.toString(file).contains(expectedSystemout));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/report/called-loop.feature",
    "content": "@ignore\nFeature: called feature name\n\nScenario: name #${__loop} is: ${name}\n* print 'in loop:', __arg\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/report/called.feature",
    "content": "Feature:\n\nScenario:\n# called comment\n* print 'in called'\n* call read('called2.feature')\n* call read('called3.feature') { with: 'some arg' }\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/report/called2.feature",
    "content": "Feature:\n\nScenario:\n* print 'in called 2'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/report/called3.feature",
    "content": "Feature:\n\nScenario:\n* print 'in called 3'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/report/customTags.feature",
    "content": "Feature: Cusotm tags\n\n@requirement=CALC-2\n@test_key=CALC-2\nScenario:\n* print 'cusomt tags are present in xml'\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/report/data.csv",
    "content": "name,age\nJohn,42\nJill,35\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/report/test.feature",
    "content": "Feature: feature name\n\nBackground:\n# background comment\n* print 'in background'\n\n@one\nScenario: first one\n* print 'in first one'\n* def foo = 'no log'\n* call read('called.feature') { dummy: 'arg' }\n\n@one @two\nScenario: second one\n* print 'in second one'\n# some comment\n* def bar = 'no log'\n* table data\n    | foo | bar |\n    |   1 |   2 |\n    |   3 |   4 |\n* def large = \n\"\"\"\n{ \n  one: 'first', \n  two: 'second'\n}\n\"\"\"\n* print large\n\nScenario Outline: example ${data}\n# outline comment\n* print 'in outline'\n\nExamples:\n| data |\n| 1    | \n| 2    |\n\nScenario Outline: dynamic ${__num + 1}\n# dynamic outline comment\n* print 'row:', __row\n\nExamples:\n| read('data.csv') |\n\nScenario: calling feature in loop\n* def data = [{ name: 'one' }, { name: 'two' }]\n* call read('called-loop.feature') data\n\n@setup\nScenario:\n* def data = [{a: 1}, {a: 2}]\n\nScenario Outline:\n* print __row\n\nExamples:\n| karate.setup().data |"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/resource/ResourceUtilsTest.java",
    "content": "package com.intuit.karate.resource;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.FeatureCall;\nimport java.io.File;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass ResourceUtilsTest {\n\n    static final Logger logger = LoggerFactory.getLogger(ResourceUtilsTest.class);\n    \n    static File wd = FileUtils.WORKING_DIR;\n\n    @Test\n    void testFindFilesByExtension() {\n        Collection<Resource> list = ResourceUtils.findResourcesByExtension(wd, \"txt\", \"src/test/java/com/intuit/karate/resource\");\n        assertEquals(1, list.size());\n        Resource resource = list.iterator().next();\n        assertTrue(resource.isFile());\n        assertFalse(resource.isClassPath());\n        assertEquals(\"src/test/java/com/intuit/karate/resource/test1.txt\", resource.getRelativePath());\n        assertEquals(\"src/test/java/com/intuit/karate/resource/test1.txt\", resource.getPrefixedPath());\n        assertEquals(\"foo\", FileUtils.toString(resource.getStream()));\n    }\n\n    @Test\n    void testGetFileByPath() {\n        Resource resource = ResourceUtils.getResource(wd, \"src/test/java/com/intuit/karate/resource/test1.txt\");\n        assertTrue(resource.isFile());\n        assertFalse(resource.isClassPath());\n        assertEquals(\"src/test/java/com/intuit/karate/resource/test1.txt\", resource.getRelativePath());\n        assertEquals(\"src/test/java/com/intuit/karate/resource/test1.txt\", resource.getPrefixedPath());\n        assertEquals(\"foo\", FileUtils.toString(resource.getStream()));\n    }\n\n    @Test\n    void testResolveFile() {\n        Resource temp = ResourceUtils.getResource(wd, \"src/test/java/com/intuit/karate/resource/test1.txt\");\n        Resource resource = temp.resolve(\"test2.log\");\n        assertTrue(resource.isFile());\n        assertFalse(resource.isClassPath());\n        assertEquals(\"src/test/java/com/intuit/karate/resource/test2.log\", resource.getRelativePath());\n        assertEquals(\"src/test/java/com/intuit/karate/resource/test2.log\", resource.getPrefixedPath());\n        assertEquals(\"bar\", FileUtils.toString(resource.getStream()));\n    }\n\n    @Test\n    void testResolveRelativeFile() {\n        Resource temp = ResourceUtils.getResource(wd, \"src/test/java/com/intuit/karate/resource/dir1/dir1.log\");\n        Resource resource = temp.resolve(\"../dir2/dir2.log\");\n        assertTrue(resource.isFile());\n        assertFalse(resource.isClassPath());\n        assertEquals(\"src/test/java/com/intuit/karate/resource/dir1/../dir2/dir2.log\", resource.getRelativePath());\n        assertEquals(\"src/test/java/com/intuit/karate/resource/dir1/../dir2/dir2.log\", resource.getPrefixedPath());\n        assertEquals(\"src.test.java.com.intuit.karate.resource.dir1.dir2.dir2.log\", resource.getPackageQualifiedName());\n        assertEquals(\"bar\", FileUtils.toString(resource.getStream()));\n    }\n\n    @Test\n    void testFindClassPathFilesByExtension() {\n        Collection<Resource> list = ResourceUtils.findResourcesByExtension(wd, \"txt\", \"classpath:com/intuit/karate/resource\");\n        assertEquals(1, list.size());\n        Resource resource = list.iterator().next();\n        assertTrue(resource.isFile());\n        assertTrue(resource.isClassPath());\n        assertEquals(\"com/intuit/karate/resource/test1.txt\", resource.getRelativePath());\n        assertEquals(\"classpath:com/intuit/karate/resource/test1.txt\", resource.getPrefixedPath());\n        assertEquals(\"foo\", FileUtils.toString(resource.getStream()));\n    }\n\n    @Test\n    void testGetClassPathFileByPath() {\n        Resource resource = ResourceUtils.getResource(wd, \"classpath:com/intuit/karate/resource/test1.txt\");\n        assertTrue(resource.isFile());\n        assertTrue(resource.isClassPath());\n        assertEquals(\"com/intuit/karate/resource/test1.txt\", resource.getRelativePath());\n        assertEquals(\"classpath:com/intuit/karate/resource/test1.txt\", resource.getPrefixedPath());\n        assertEquals(\"foo\", FileUtils.toString(resource.getStream()));\n    }\n\n    @Test\n    void testResolveClassPathFile() {\n        Resource temp = ResourceUtils.getResource(wd, \"classpath:com/intuit/karate/resource/test1.txt\");\n        Resource resource = temp.resolve(\"test2.log\");\n        assertTrue(resource.isFile());\n        assertTrue(resource.isClassPath());\n        assertEquals(\"com/intuit/karate/resource/test2.log\", resource.getRelativePath());\n        assertEquals(\"classpath:com/intuit/karate/resource/test2.log\", resource.getPrefixedPath());\n        assertEquals(\"bar\", FileUtils.toString(resource.getStream()));\n    }\n\n    @Test\n    void testResolveRelativeClassPathFile() {\n        Resource temp = ResourceUtils.getResource(new File(\"\"), \"classpath:com/intuit/karate/resource/dir1/dir1.log\");\n        Resource resource = temp.resolve(\"../dir2/dir2.log\");\n        assertTrue(resource.isFile());\n        assertTrue(resource.isClassPath());\n        assertEquals(\"com/intuit/karate/resource/dir1/../dir2/dir2.log\", resource.getRelativePath());\n        assertEquals(\"classpath:com/intuit/karate/resource/dir1/../dir2/dir2.log\", resource.getPrefixedPath());\n        assertEquals(\"bar\", FileUtils.toString(resource.getStream()));\n    }\n\n    @Test\n    void testGetFeatureWithLineNumber() {\n        String path = \"classpath:com/intuit/karate/resource/test.feature:6\";\n        List<FeatureCall> features = ResourceUtils.findFeatureFiles(new File(\"\"), Collections.singletonList(path), null);\n        assertEquals(1, features.size());\n        assertEquals(6, features.get(0).callLine);\n    }\n    \n    @Test\n    void testGetFeatureWithCallTag() {\n        String path = \"classpath:com/intuit/karate/resource/test.feature@second\";\n        List<FeatureCall> features = ResourceUtils.findFeatureFiles(new File(\"\"), Collections.singletonList(path), null);\n        assertEquals(1, features.size());\n        assertEquals(\"@second\", features.get(0).callTag);\n    }    \n\n    @Test\n    void testClassPathToFileThatExists() {\n        File file = ResourceUtils.classPathToFile(\"com/intuit/karate/resource/test1.txt\");\n        assertTrue(file.exists());\n    }\n\n    @Test\n    void testClassPathToFileThatDoesNotExist() {\n        File file = ResourceUtils.classPathToFile(\"com/intuit/karate/resource/nope.txt\");\n        assertNull(file);\n    }    \n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/resource/dir1/dir1.log",
    "content": "foo"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/resource/dir2/dir2.log",
    "content": "bar"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/resource/test.feature",
    "content": "Feature:\n\nScenario:\n* print 'first'\n\n@second\nScenario:\n* print 'second'"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/resource/test1.txt",
    "content": "foo"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/resource/test2.log",
    "content": "bar"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/shell/CommandTest.java",
    "content": "package com.intuit.karate.shell;\n\nimport java.io.File;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\nimport com.intuit.karate.FileUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass CommandTest {\n\n    static final Logger logger = LoggerFactory.getLogger(CommandTest.class);\n\n    @Test\n    void testCommand() {\n        String cmd = FileUtils.isOsWindows() ? \"print \\\"hello\\\"\" : \"ls\";\n        Command command = new Command(false, null, null, \"target/command.log\", new File(\"src\"), cmd, \"-al\");\n        command.start();\n        int exitCode = command.waitSync();\n        assertEquals(exitCode, 0);\n    }\n\n    @Test\n    void testCommandReturn() {\n        String cmd = FileUtils.isOsWindows() ? \"cmd /c dir\" : \"ls\";\n        String result = Command.execLine(new File(\"target\"), cmd);\n        assertTrue(result.contains(\"karate\"));\n    }\n\n    @Test\n    void testTokenize() {\n        String[] args = Command.tokenize(\"hello \\\"foo bar\\\" world\");\n        assertEquals(3, args.length);\n        assertEquals(\"hello\", args[0]);\n        assertEquals(\"foo bar\", args[1]);\n        assertEquals(\"world\", args[2]);\n        args = Command.tokenize(\"-Dexec.classpathScope=test \\\"-Dexec.args=-f json test\\\"\");\n        assertEquals(2, args.length);\n        assertEquals(\"-Dexec.classpathScope=test\", args[0]);\n        assertEquals(\"-Dexec.args=-f json test\", args[1]);\n        args = Command.tokenize(\"-v \\\"$PWD\\\":/src -v \\\"$HOME/.m2\\\":/root/.m2 ptrthomas/karate-chrome\");\n        assertEquals(5, args.length);\n        assertEquals(\"-v\", args[0]);\n        assertEquals(\"\\\"$PWD\\\":/src\", args[1]);\n        assertEquals(\"-v\", args[2]);\n        assertEquals(\"\\\"$HOME/.m2\\\":/root/.m2\", args[3]);  \n        assertEquals(\"ptrthomas/karate-chrome\", args[4]);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/shell/CommandTester.java",
    "content": "package com.intuit.karate.shell;\n\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass CommandTester {\n\n    static final Logger logger = LoggerFactory.getLogger(CommandTester.class);\n\n    @Test\n    void testWaitForKeyboard() {\n        Command.waitForSocket(0);\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/TemplateTest.java",
    "content": "package com.intuit.karate.template;\n\nimport com.intuit.karate.graal.JsEngine;\nimport com.intuit.karate.resource.ResourceResolver;\nimport com.intuit.karate.resource.ResourceUtils;\nimport java.io.File;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass TemplateTest {\n\n    static final Logger logger = LoggerFactory.getLogger(TemplateTest.class);\n\n    private static String render(String resource) {\n        JsEngine je = JsEngine.local();\n        KarateTemplateEngine engine = TemplateUtils.forResourceRoot(je, \"classpath:com/intuit/karate/template\");\n        return engine.process(resource);\n    }\n\n    @Test\n    void testHtmlString() {\n        JsEngine je = JsEngine.global();\n        je.put(\"message\", \"hello world\");\n        KarateTemplateEngine engine = TemplateUtils.forStrings(je, new ResourceResolver(\"classpath:com/intuit/karate/template\"));\n        String rendered = engine.process(\"<div><div th:text=\\\"message\\\"></div><div th:replace=\\\"/temp.html\\\"></div></div>\");\n        assertEquals(\"<div><div>hello world</div><div>temp</div></div>\", rendered);\n    }\n\n    @Test\n    void testHtmlFile() {\n        String rendered = render(\"main.html\");\n        assertTrue(rendered.contains(\"<div id=\\\"before_one\\\"><span>js_one</span></div>\"));\n        assertTrue(rendered.contains(\"<div id=\\\"called_one\\\">called_one</div>\"));\n        assertTrue(rendered.contains(\"<div id=\\\"after_one\\\"><span>js_one</span></div>\"));\n    }\n\n    @Test\n    void testKaSet() {\n        String rendered = render(\"ka-set.html\");\n        assertEquals(rendered.replaceAll(\"\\\\r\", \"\").trim(), \"<div>\"\n                + \"first line\\n\"\n                + \"second line\"\n                + \"</div>\");\n    }\n\n    @Test\n    void testWith() {\n        String rendered = render(\"with\");\n        assertTrue(rendered.contains(\"<div>bar</div>\"));\n        assertTrue(rendered.contains(\"<div>hello world</div>\"));\n        assertTrue(rendered.contains(\"<div>with</div>\"));\n    }\n\n    @Test\n    void testAttr() {\n        String rendered = render(\"attr.html\");\n        assertTrue(rendered.contains(\"<div foo=\\\"a\\\">normal</div>\"));\n        assertTrue(rendered.contains(\"<div foo=\\\"xa\\\">append</div>\"));\n        assertTrue(rendered.contains(\"<div foo=\\\"ax\\\">prepend</div>\"));\n    }\n\n    @Test\n    void testNoCache() {\n        File file = ResourceUtils.getFileRelativeTo(getClass(), \"temp.js\");\n        String rendered = render(\"nocache.html\");\n        assertTrue(rendered.contains(\"<script src=\\\"temp.js?ts=\" + file.lastModified() + \"\\\"></script>\"));\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/attr.html",
    "content": "<div th:attr=\"foo: 'a', bar: null\">normal</div>\n<div foo=\"x\" th:attrappend=\"foo: 'a', bar: null\">append</div>\n<div foo=\"x\" th:attrprepend=\"foo: 'a', bar: null\">prepend</div>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/called1.html",
    "content": "<div>\n  <script ka:scope=\"local\">\n    _.childVar = 'called_' + item;\n  </script>  \n  <div th:id=\"'called_' + item\" th:text=\"childVar\"></div>\n</div>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/global.js",
    "content": "console.log('global.js called');\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/ka-set.html",
    "content": "<pre ka:set=\"lines\">\nfirst line\nsecond line\n</pre>\n<div th:text=\"lines\"></div>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/local.js",
    "content": "console.log('local.js called');\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/main.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <title>Test</title>  \n  </head>\n  <body>\n    <script ka:src=\"local.js\" ka:scope=\"local\"></script>\n    <script ka:src=\"global.js\"></script>\n    <div th:each=\"item: ['one', 'two']\">  \n      <script ka:scope=\"local\">\n        _.childVar = 'js_' + item;\n      </script>       \n      <div th:id=\"'before_' + item\"><span th:text=\"childVar\"></span></div>\n      <div th:replace=\"called1.html\"></div>\n      <div th:id=\"'after_' + item\"><span th:text=\"childVar\"></span></div>\n    </div>\n  </body>\n</html>\n\n\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/nocache.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <script src=\"temp.js\" ka:nocache=\"true\"></script>\n  </head>\n  <body></body>\n</html>"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/temp.html",
    "content": "<div>temp</div>"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/temp.js",
    "content": "\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/with-called.html",
    "content": "<div th:text=\"foo\"></div>\n<div th:text=\"msg\"></div>\n<div th:text=\"context.caller\"></div>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/template/with.html",
    "content": "<script ka:scope=\"global\">\n  var msg = 'hello world';\n</script>\n<div th:include=\"this:with-called\" th:with=\"foo: 'bar', msg: msg\"></div>\n"
  },
  {
    "path": "karate-core/src/test/java/com/intuit/karate/test/file-utils-test.feature",
    "content": "Feature:\n\nScenario:\n* print 'test'\n"
  },
  {
    "path": "karate-core/src/test/java/demo/ServerRunner.java",
    "content": "package demo;\n\nimport com.intuit.karate.http.HttpServer;\nimport com.intuit.karate.http.ServerConfig;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass ServerRunner {\n\n    @Test\n    void testServer() {\n        ServerConfig config = new ServerConfig(\"src/test/java/demo\")\n                .useGlobalSession(true)\n                .autoCreateSession(true);\n        HttpServer.config(config)\n                .http(8080)\n                .corsEnabled(true)\n                .build().waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-core/src/test/java/demo/api/cats.js",
    "content": "session.cats = session.cats || [];\nif (request.post) {\n  let cat = request.body;\n  cat.id = session.cats.length + 1;\n  session.cats.push(cat);\n  response.body = cat;\n} else if (request.pathMatches('/{resource}/{id}')) {\n  let id = ~~request.pathParams.id;\n  if (request.get) {\n    let cat = session.cats.find(c => c.id === id);\n    response.body = cat;\n  }\n} else { // get all\n  response.body = session.cats;\n}\n"
  },
  {
    "path": "karate-core/src/test/java/demo/api/demo.js",
    "content": "response.header('foo', 'bar');\nresponse.body = { hello: 'world' };\nresponse.status = 201\n"
  },
  {
    "path": "karate-core/src/test/java/demo/api/payments.js",
    "content": "session.payments = session.payments || {};\nsession.counter = session.counter || 1;\nif (request.pathMatches('/payments/{id}')) {\n  let id = request.pathParams.id;\n  if (request.put) {\n    var payment = request.body;\n    session.payments[id] = payment;\n    response.body = payment;\n  } else if (request.delete) {\n    delete session.payments[id];\n  } else { // get\n    response.body = session.payments[id];\n    if (!response.body) {\n      response.status = 404;\n    }\n  }\n} else if (request.post) {\n  var payment = request.body;\n  let id = '' + session.counter++;\n  payment.id = id;\n  session.payments[id] = payment;\n  response.body = payment;\n} else { // get all\n  response.body = Object.values(session.payments);\n}\n"
  },
  {
    "path": "karate-core/src/test/java/demo/api/render.js",
    "content": "var someName = 'John';\nvar msg1 = context.render('api/test');\nvar msg2 = context.render({path: 'api/test.html', variables: {someName: 'Smith'}});\nvar msg3 = context.render({html: '<div th:text=\"someName\"></div>'});\nvar msg4 = context.render({html: '<div th:text=\"someName\"></div>', variables: {someName: 'Smith'}});\nresponse.body = {msg1: msg1, msg2: msg2, msg3: msg3, msg4: msg4};\n"
  },
  {
    "path": "karate-core/src/test/java/demo/api/test.html",
    "content": "<h1 th:text=\"someName\"></h1>\n"
  },
  {
    "path": "karate-core/src/test/java/demo/apidocs.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>API Docs</title> \n    <link rel=\"stylesheet\" href=\"app.css\" type=\"text/css\"/>\n  </head>\n  <body>\n    <script ka:scope=\"global\">\n      var url = 'https://jsonplaceholder.typicode.com/users';\n      var req = null;\n      var method = 'GET';\n      var res = context.http(url).method(method).body(req).invoke().body;\n    </script>    \n    <div>\n      <h2><span th:text=\"method\"></span> <span th:text=\"url\"></span></h2>\n      <p>Request</p>\n      <code th:text=\"req\"></code>\n      <p>Response</p>\n      <code th:text=\"res\"></code>      \n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-core/src/test/java/demo/app.css",
    "content": "body { font-family: sans-serif; } \ntable { border-collapse: collapse; } \ntable td { border: 1px solid gray; padding: 0.1em 0.2em; }\nbutton { font-size: medium; padding: 0.5em; margin: 0.5em; }\n.nav a { display: inline-block; padding: 0.5em; }\n.error { background-color: lightpink; }\n"
  },
  {
    "path": "karate-core/src/test/java/demo/cats.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Cats</title>\n    <script src=\"https://code.jquery.com/jquery-3.2.1.min.js\" integrity=\"sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=\" crossorigin=\"anonymous\"></script>\n  </head>\n  <body>\n    <div>\n      <a href=\"http://localhost:8080/api/cats\">GET /cats</a>\n    </div>\n    <p/>    \n    <div>\n      <label for=\"catName\">Name: <input id=\"catName\" value=\"Billie\"/></label>\n      <button onclick=\"createCat()\">Create Cat</button>\n    </div>    \n    <p/>\n    <div id=\"response\"></div>\n    <script>\n      var createCat = function() {\n        var name = $('#catName').val();\n        $.ajax({          \n          url: 'http://localhost:8080/api/cats',\n          data: JSON.stringify({ name: name }),\n          method: 'POST',\n          success: function(result) {\n            $('#response').html('created cat: ' + JSON.stringify(result));\n          }\n        });\n      };\n    </script>    \n  </body>\n</html>\n\n"
  },
  {
    "path": "karate-core/src/test/java/demo/details.html",
    "content": "<script ka:scope=\"global\">\n  var productId = ~~request.param('id');\n  if (productId) {\n    var prod = session.products.find(x => x.id === productId);\n    var found = session.suppliers.filter(x => x.products.includes(productId));\n  }\n</script>\n<a th:unless=\"productId\" ka:get=\"this\" ka:vals=\"id:prod.id\" href=\"#\">(more)</a>\n<p th:if=\"productId\" th:each=\"supplier: found\">\n  <span th:text=\"supplier.name\">Acme Novelties</span>\n</p>"
  },
  {
    "path": "karate-core/src/test/java/demo/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>KarateJS Demo</title>\n    <script src=\"https://unpkg.com/htmx.org@1.7.0\"></script>\n    <link rel=\"stylesheet\" href=\"app.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"favicon.ico\">\n  </head>\n  <body>\n    <script ka:scope=\"global\">\n      username = 'John Smith';\n      session.products = session.products || [\n        {id: 1, name: 'Apple', price: 100},\n        {id: 2, name: 'Orange', price: 200}\n      ];\n      session.suppliers = session.suppliers || [\n        {id: 1, name: 'Acme Trading', products: [1]},\n        {id: 2, name: 'Omni Consumer', products: [1, 2]}\n      ];\n      session.person = session.person || {firstName: 'Billie', lastName: 'Cat', email: 'billie@cat.com'};\n    </script>    \n    <p><a href=\"cats\">Cats API Demo</a></p>\n    <p><a href=\"users\">Users API Client Demo</a></p>\n    <p><a href=\"apidocs\">API Docs Demo</a></p>\n    <p>User Name: <span th:text=\"username\">Replace Me</span></p>\n    <table>\n      <thead>\n        <tr>\n          <th>Name</th>\n          <th>Price</th>\n          <th>Suppliers</th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr th:each=\"prod: session.products\">\n          <td th:text=\"prod.name\">Pear</td>\n          <td th:text=\"prod.price\">50</td>\n          <td th:insert=\"details\" hx-target=\"this\"></td>\n        </tr>\n      </tbody>\n    </table>\n    <div th:insert=\"person\" hx-target=\"this\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-core/src/test/java/demo/person.html",
    "content": "<script ka:scope=\"global\">\n  var person = session.person;\n  var edit = request.param('edit') === 'true'\n  if (request.post) {\n    person.firstName = request.param('firstName');\n    person.lastName = request.param('lastName');\n    person.email = request.param('email');\n  }\n  var errors = {person: {}};\n  if (!person.firstName || person.firstName.length === 0) {\n    errors.person.firstName = 'should not be empty';\n    edit = true;\n  }\n</script>\n\n<div th:unless=\"edit\">\n  <div><label>First Name</label>: <span th:text=\"person.firstName\">Joe</span></div>\n  <div><label>Last Name</label>: <span th:text=\"person.lastName\">Blow</span></div>\n  <div><label>Email</label>: <span th:text=\"person.email\">joe@blow.com</span></div>\n  <button ka:get=\"this\" ka:vals=\"edit:true\">\n    Click To Edit\n  </button>\n</div>\n\n<form th:if=\"edit\" ka:post=\"this\">\n  <div th:classappend=\"errors.person.firstName ? 'error' : null\">\n    <label>First Name</label>\n    <input type=\"text\" name=\"firstName\" th:value=\"person.firstName\">\n    <span th:text=\"errors.person.firstName\"></span>\n  </div>\n  <div>\n    <label>Last Name</label>\n    <input type=\"text\" name=\"lastName\" th:value=\"person.lastName\">    \n  </div>\n  <div>\n    <label>Email address</label>\n    <input type=\"email\" name=\"email\" th:value=\"person.email\">    \n  </div>\n  <button>Submit</button>\n  <button ka:get=\"this\">Cancel</button>\n</form>\n"
  },
  {
    "path": "karate-core/src/test/java/demo/user.html",
    "content": "<script ka:scope=\"global\">\n  var userId = ~~request.param('id');\n  if (userId) {\n    var http = context.http('https://jsonplaceholder.typicode.com/users/' + userId);\n    var user = http.get().body;\n    context.switch('apidocs');\n  }\n</script>\n<a th:unless=\"userId\" ka:get=\"this\" ka:vals=\"id:user.id\" href=\"#\">(more)</a>\n<ul th:if=\"userId\">\n  <li th:text=\"user.phone\"></li>\n  <li th:text=\"user.website\"></li>\n</ul>"
  },
  {
    "path": "karate-core/src/test/java/demo/users.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Users</title>\n    <script ka:scope=\"global\"></script>\n    <link rel=\"stylesheet\" href=\"app.css\" type=\"text/css\"/>\n  </head>\n  <body>\n    <script ka:scope=\"global\">\n      var http = context.http('https://jsonplaceholder.typicode.com/users');\n      var users = http.get().body;\n      // console.log('users', users);\n    </script>    \n    <p>Users</p>\n    <table>\n      <thead>\n        <tr>\n          <th>ID</th>\n          <th>Name</th>\n          <th>E-Mail</th>\n          <th></th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr th:each=\"user: users\">\n          <td th:text=\"user.id\"></td>\n          <td th:text=\"user.name\"></td>\n          <td th:text=\"user.email\"></td>\n          <td th:insert=\"user\" hx-target=\"this\"></td>\n        </tr>\n      </tbody>\n    </table>    \n  </body>\n</html>\n\n\n\n\n"
  },
  {
    "path": "karate-core/src/test/java/karate-base.js",
    "content": "function fn() {   \n  return { functionFromKarateBase: function(){ return 'fromKarateBase' } }\n}\n"
  },
  {
    "path": "karate-core/src/test/java/karate-config.js",
    "content": "function fn() {   \n  return { configSource: 'normal' }\n}"
  },
  {
    "path": "karate-core/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit\" level=\"DEBUG\"/>\n    <logger name=\"com.intuit.karate.debug\" level=\"TRACE\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>"
  },
  {
    "path": "karate-core/src/test/java/payments.jmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<jmeterTestPlan version=\"1.2\" properties=\"5.0\" jmeter=\"5.3\">\n  <hashTree>\n    <TestPlan guiclass=\"TestPlanGui\" testclass=\"TestPlan\" testname=\"Test Plan\" enabled=\"true\">\n      <stringProp name=\"TestPlan.comments\"></stringProp>\n      <boolProp name=\"TestPlan.functional_mode\">false</boolProp>\n      <boolProp name=\"TestPlan.serialize_threadgroups\">false</boolProp>\n      <elementProp name=\"TestPlan.user_defined_variables\" elementType=\"Arguments\" guiclass=\"ArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n        <collectionProp name=\"Arguments.arguments\"/>\n      </elementProp>\n      <stringProp name=\"TestPlan.user_define_classpath\"></stringProp>\n    </TestPlan>\n    <hashTree>\n      <ThreadGroup guiclass=\"ThreadGroupGui\" testclass=\"ThreadGroup\" testname=\"Thread Group\" enabled=\"true\">\n        <stringProp name=\"ThreadGroup.on_sample_error\">continue</stringProp>\n        <elementProp name=\"ThreadGroup.main_controller\" elementType=\"LoopController\" guiclass=\"LoopControlPanel\" testclass=\"LoopController\" testname=\"Loop Controller\" enabled=\"true\">\n          <boolProp name=\"LoopController.continue_forever\">false</boolProp>\n          <stringProp name=\"LoopController.loops\">1</stringProp>\n        </elementProp>\n        <stringProp name=\"ThreadGroup.num_threads\">10</stringProp>\n        <stringProp name=\"ThreadGroup.ramp_time\">1</stringProp>\n        <longProp name=\"ThreadGroup.start_time\">1514991895000</longProp>\n        <longProp name=\"ThreadGroup.end_time\">1514991895000</longProp>\n        <boolProp name=\"ThreadGroup.scheduler\">false</boolProp>\n        <stringProp name=\"ThreadGroup.duration\"></stringProp>\n        <stringProp name=\"ThreadGroup.delay\"></stringProp>\n        <boolProp name=\"ThreadGroup.same_user_on_next_iteration\">true</boolProp>\n      </ThreadGroup>\n      <hashTree>\n        <ConfigTestElement guiclass=\"HttpDefaultsGui\" testclass=\"ConfigTestElement\" testname=\"HTTP Request Defaults\" enabled=\"true\">\n          <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\" guiclass=\"HTTPArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n            <collectionProp name=\"Arguments.arguments\"/>\n          </elementProp>\n          <stringProp name=\"HTTPSampler.domain\">localhost</stringProp>\n          <stringProp name=\"HTTPSampler.port\">8080</stringProp>\n          <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n          <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n          <stringProp name=\"HTTPSampler.path\"></stringProp>\n          <stringProp name=\"HTTPSampler.concurrentPool\">6</stringProp>\n          <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n          <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n        </ConfigTestElement>\n        <hashTree/>\n        <LoopController guiclass=\"LoopControlPanel\" testclass=\"LoopController\" testname=\"Loop Controller\" enabled=\"true\">\n          <boolProp name=\"LoopController.continue_forever\">true</boolProp>\n          <stringProp name=\"LoopController.loops\">100</stringProp>\n        </LoopController>\n        <hashTree>\n          <CounterConfig guiclass=\"CounterConfigGui\" testclass=\"CounterConfig\" testname=\"Counter\" enabled=\"true\">\n            <stringProp name=\"CounterConfig.start\"></stringProp>\n            <stringProp name=\"CounterConfig.end\"></stringProp>\n            <stringProp name=\"CounterConfig.incr\">1</stringProp>\n            <stringProp name=\"CounterConfig.name\">loopCount</stringProp>\n            <stringProp name=\"CounterConfig.format\"></stringProp>\n            <boolProp name=\"CounterConfig.per_user\">false</boolProp>\n          </CounterConfig>\n          <hashTree/>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"post\" enabled=\"true\">\n            <boolProp name=\"HTTPSampler.postBodyRaw\">true</boolProp>\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\">\n              <collectionProp name=\"Arguments.arguments\">\n                <elementProp name=\"\" elementType=\"HTTPArgument\">\n                  <boolProp name=\"HTTPArgument.always_encode\">false</boolProp>\n                  <stringProp name=\"Argument.value\">{ &quot;amount&quot;: ${loopCount}.${__threadNum}, &quot;description&quot;: &quot;before&quot; }</stringProp>\n                  <stringProp name=\"Argument.metadata\">=</stringProp>\n                </elementProp>\n              </collectionProp>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">api/payments</stringProp>\n            <stringProp name=\"HTTPSampler.method\">POST</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree>\n            <JSONPostProcessor guiclass=\"JSONPostProcessorGui\" testclass=\"JSONPostProcessor\" testname=\"JSON Extractor\" enabled=\"true\">\n              <stringProp name=\"JSONPostProcessor.referenceNames\">paymentId</stringProp>\n              <stringProp name=\"JSONPostProcessor.jsonPathExprs\">$.id</stringProp>\n              <stringProp name=\"JSONPostProcessor.match_numbers\"></stringProp>\n              <stringProp name=\"Scope.variable\">paymentId</stringProp>\n            </JSONPostProcessor>\n            <hashTree/>\n          </hashTree>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"get before\" enabled=\"true\">\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\" guiclass=\"HTTPArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n              <collectionProp name=\"Arguments.arguments\"/>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">api/payments/${paymentId}</stringProp>\n            <stringProp name=\"HTTPSampler.method\">GET</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree/>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"put\" enabled=\"true\">\n            <boolProp name=\"HTTPSampler.postBodyRaw\">true</boolProp>\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\">\n              <collectionProp name=\"Arguments.arguments\">\n                <elementProp name=\"\" elementType=\"HTTPArgument\">\n                  <boolProp name=\"HTTPArgument.always_encode\">false</boolProp>\n                  <stringProp name=\"Argument.value\">{ &quot;id&quot;: ${paymentId}, &quot;amount&quot;: ${loopCount}.${__threadNum}, &quot;description&quot;: &quot;@@${paymentId}@@&quot; }</stringProp>\n                  <stringProp name=\"Argument.metadata\">=</stringProp>\n                </elementProp>\n              </collectionProp>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">api/payments/${paymentId}</stringProp>\n            <stringProp name=\"HTTPSampler.method\">PUT</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree/>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"get after\" enabled=\"true\">\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\" guiclass=\"HTTPArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n              <collectionProp name=\"Arguments.arguments\"/>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">api/payments/${paymentId}</stringProp>\n            <stringProp name=\"HTTPSampler.method\">GET</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree>\n            <ResponseAssertion guiclass=\"AssertionGui\" testclass=\"ResponseAssertion\" testname=\"Response Assertion\" enabled=\"true\">\n              <collectionProp name=\"Asserion.test_strings\">\n                <stringProp name=\"370844781\">&quot;@@${paymentId}@@&quot;</stringProp>\n                <stringProp name=\"0\"></stringProp>\n              </collectionProp>\n              <stringProp name=\"Assertion.test_field\">Assertion.response_data</stringProp>\n              <boolProp name=\"Assertion.assume_success\">false</boolProp>\n              <intProp name=\"Assertion.test_type\">2</intProp>\n              <stringProp name=\"Assertion.custom_message\"></stringProp>\n            </ResponseAssertion>\n            <hashTree/>\n          </hashTree>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"delete\" enabled=\"true\">\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\" guiclass=\"HTTPArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n              <collectionProp name=\"Arguments.arguments\"/>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">api/payments/${paymentId}</stringProp>\n            <stringProp name=\"HTTPSampler.method\">DELETE</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree/>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"get all\" enabled=\"true\">\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\" guiclass=\"HTTPArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n              <collectionProp name=\"Arguments.arguments\"/>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">api/payments</stringProp>\n            <stringProp name=\"HTTPSampler.method\">GET</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree>\n            <ResponseAssertion guiclass=\"AssertionGui\" testclass=\"ResponseAssertion\" testname=\"Response Assertion\" enabled=\"true\">\n              <collectionProp name=\"Asserion.test_strings\">\n                <stringProp name=\"370844781\">&quot;@@${paymentId}@@&quot;</stringProp>\n              </collectionProp>\n              <stringProp name=\"Assertion.test_field\">Assertion.response_data</stringProp>\n              <boolProp name=\"Assertion.assume_success\">false</boolProp>\n              <intProp name=\"Assertion.test_type\">6</intProp>\n              <stringProp name=\"Scope.variable\">foundArray</stringProp>\n              <stringProp name=\"Assertion.custom_message\"></stringProp>\n            </ResponseAssertion>\n            <hashTree/>\n          </hashTree>\n        </hashTree>\n      </hashTree>\n      <ResultCollector guiclass=\"ViewResultsFullVisualizer\" testclass=\"ResultCollector\" testname=\"View Results Tree\" enabled=\"false\">\n        <boolProp name=\"ResultCollector.error_logging\">false</boolProp>\n        <objProp>\n          <name>saveConfig</name>\n          <value class=\"SampleSaveConfiguration\">\n            <time>true</time>\n            <latency>true</latency>\n            <timestamp>true</timestamp>\n            <success>true</success>\n            <label>true</label>\n            <code>true</code>\n            <message>true</message>\n            <threadName>true</threadName>\n            <dataType>true</dataType>\n            <encoding>false</encoding>\n            <assertions>true</assertions>\n            <subresults>true</subresults>\n            <responseData>false</responseData>\n            <samplerData>false</samplerData>\n            <xml>false</xml>\n            <fieldNames>true</fieldNames>\n            <responseHeaders>false</responseHeaders>\n            <requestHeaders>false</requestHeaders>\n            <responseDataOnError>false</responseDataOnError>\n            <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>\n            <assertionsResultsToSave>0</assertionsResultsToSave>\n            <bytes>true</bytes>\n            <sentBytes>true</sentBytes>\n            <threadCounts>true</threadCounts>\n            <idleTime>true</idleTime>\n            <connectTime>true</connectTime>\n          </value>\n        </objProp>\n        <stringProp name=\"filename\"></stringProp>\n      </ResultCollector>\n      <hashTree/>\n      <ResultCollector guiclass=\"SummaryReport\" testclass=\"ResultCollector\" testname=\"Summary Report\" enabled=\"true\">\n        <boolProp name=\"ResultCollector.error_logging\">false</boolProp>\n        <objProp>\n          <name>saveConfig</name>\n          <value class=\"SampleSaveConfiguration\">\n            <time>true</time>\n            <latency>true</latency>\n            <timestamp>true</timestamp>\n            <success>true</success>\n            <label>true</label>\n            <code>true</code>\n            <message>true</message>\n            <threadName>true</threadName>\n            <dataType>true</dataType>\n            <encoding>false</encoding>\n            <assertions>true</assertions>\n            <subresults>true</subresults>\n            <responseData>false</responseData>\n            <samplerData>false</samplerData>\n            <xml>false</xml>\n            <fieldNames>true</fieldNames>\n            <responseHeaders>false</responseHeaders>\n            <requestHeaders>false</requestHeaders>\n            <responseDataOnError>false</responseDataOnError>\n            <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>\n            <assertionsResultsToSave>0</assertionsResultsToSave>\n            <bytes>true</bytes>\n            <sentBytes>true</sentBytes>\n            <threadCounts>true</threadCounts>\n            <idleTime>true</idleTime>\n            <connectTime>true</connectTime>\n          </value>\n        </objProp>\n        <stringProp name=\"filename\"></stringProp>\n      </ResultCollector>\n      <hashTree/>\n    </hashTree>\n  </hashTree>\n</jmeterTestPlan>\n"
  },
  {
    "path": "karate-core/src/test/resources/analytics.md",
    "content": "# Karate Analytics\nFrom version [1.2.0 onwards](https://github.com/karatelabs/karate/releases/tag/v1.2.0) Karate sends a very limited set of events to a hosted instance of [Posthog](https://posthog.com) - an open-source analytics solution. Access to this is managed by the [Karate Labs team](#who-can-see-the-data).\n\nOnly one (anonymous) event is captured: which is when someone views the HTML report that Karate generates. Note that using the HTML report is completely optional.\n\nIf you set an OS environment property called `KARATE_TELEMETRY` to the value `false`, no events will be sent.\n\nUsers can also completely disable HTML reporting via the [API](https://github.com/karatelabs/karate#parallel-execution) or [command-line](https://github.com/karatelabs/karate/tree/master/karate-netty#output-format).\n\nThere is always the option of [using a third-party report](https://github.com/karatelabs/karate/tree/master/karate-demo#example-report) (via the JUnit-XML or Cucumber-JSON output).\n\n## Why We Need This Now\nKarate is an open-source project and there is no direct way to track usage and whether awareness camapigns are working.\n\nNow that we are a for-profit open-source company, a basic level of usage data will help us focus on the right areas of product development.\n\n## What Is Tracked \n* Anonymized Machine ID\n* Browser Details (including OS info)\n* Karate Version\n* How Karate was invoked (via IDE plugin, NPM, etc.)\n\n## Who Can See The Data\nThe dashboard is accessible only to the [Karate Labs](https://karatelabs.io) team that maintains Karate.\n"
  },
  {
    "path": "karate-core/src/test/resources/readme.txt",
    "content": "dev:\n====\nmvn versions:set versions:commit -DnewVersion=2.0.0\n\ncve check\n=========\n(do mvn versions:set to what should show in the report)\nmvn verify -P depcheck -pl karate-core -DnvdApiKey=YOUR_KEY\n\n(save the above report)\n\nprod:\n=====\nmvn versions:set versions:commit -DnewVersion=@@@\n\n# edit archetype karate.version\n# edit README.md maven 3 places\n# edit examples/gatling/build.gradle\n# edit examples/*/pom.xml\n\n# make release using [develop]\n# using github action: https://github.com/karatelabs/karate/actions/workflows/maven-release.yml\n# once release passes, download artifacts zip\n# make sure the release tag is against the right branch (master or develop)\n# upload following to github release notes\n    karate-core/target/karate-XXX.zip\n    karate-core/target/karate-XXX.jar\n    karate-robot/target/karate-robot-XXX.jar\n\n(save the sonatype report)\n\ngithub\n======\n# edit https://github.com/karatelabs/jbang-catalog\n# edit https://github.com/karatelabs/karate-template\n# edit https://github.com/karatelabs/karate-todo\n\ndocker (deprecated)\n===================\n# make sure docker is started and is running]\nrm -rf ~/.m2/repository/com/intuit/karate\nrm -rf karate-docker/karate-chrome/target\nmvn clean install -P pre-release -DskipTests\n./build-docker.sh\n\ndocker tag karate-chrome karatelabs/karate-chrome:@@@\ndocker tag karate-chrome karatelabs/karate-chrome:latest\n\ndocker push karatelabs/karate-chrome:@@@\ndocker push karatelabs/karate-chrome:latest\n"
  },
  {
    "path": "karate-demo/README.md",
    "content": "# Karate Demo\n\n> :warning: Note that this is *not* the best example of a skeleton Java / Maven project, as it is designed to be part of the Karate code-base and act as a suite of regression tests. For a good \"starter\" project, please refer to the [Karate Examples](https://github.com/karatelabs/karate-examples/blob/main/README.md)\n\nThis is a sample [Spring Boot](http://projects.spring.io/spring-boot/) web-application that exposes some functionality as web-service end-points. And includes a set of Karate examples that test these services as well as demonstrate various Karate features and best-practices.\n\n| Example | Demonstrates |\n| ------- | ------------ |\n[`greeting.feature`](src/test/java/demo/greeting/greeting.feature) | Simple GET requests and multiple scenarios in a test.\n[`headers.feature`](src/test/java/demo/headers/headers.feature)  | Multiple examples of [header management](https://github.com/karatelabs/karate#configure-headers) including dynamic setting of headers for each request using a JS file ([`classpath:headers.js`](src/test/java/headers.js)). Also demonstrates handling of cookies, and path / query parameters. There are also examples of how to set up a [re-usable `*.feature file`](src/test/java/demo/headers/call-updates-config.feature) for per-request secure / auth headers after a sign-in. [OAuth 2](src/test/java/demo/oauth/oauth2.feature), [OAuth 1](src/test/java/demo/oauth/oauth1.feature) and [JWT](src/test/java/demo/jwt/jwt.feature) samples are also available.\n[`sign-in.feature`](src/test/java/demo/signin/sign-in.feature) | HTML form POST example. Typically you use the response to get an authentication token that can be used to [build headers](https://github.com/karatelabs/karate#http-basic-authentication-example) for subsequent requests. This example also demonstrates getting past an end-point protected against [CSRF](https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html).\n[`cats.feature`](src/test/java/demo/cats/cats.feature) | Great example of [embedded-expressions](https://github.com/karatelabs/karate#embedded-expressions) (or JSON templating).\n[`kittens.feature`](src/test/java/demo/cats/kittens.feature) | Reading a complex payload expected response [from a file](https://github.com/karatelabs/karate#reading-files). You can do the same for request payloads as well.\n[`read-files.feature`](src/test/java/demo/read/read-files.feature) | The above example reads a file with [embedded expressions](https://github.com/karatelabs/karate#embedded-expressions) and this one reads normal JSON and XML for use in a [`match`](https://github.com/karatelabs/karate#match). Also a good example of using the [`set`](https://github.com/karatelabs/karate#set) and [`table`](https://github.com/karatelabs/karate#table) keywords to build JSON (or XML) payloads from scratch.\n[`graphql.feature`](src/test/java/demo/graphql/graphql.feature) | GraphQL example showing how easy it is to prepare queries and deal with the highly dynamic and deeply nested JSON responses - by focusing only on the parts you are interested in\n[`upload.feature`](src/test/java/demo/upload/upload.feature) | [Multi-part](https://github.com/karatelabs/karate#multipart-field) file-upload example, as well as comparing the binary content of a download. Also shows how to assert for expected response [headers](https://github.com/karatelabs/karate#match-header). Plus an example of how to call [custom Java code](https://github.com/karatelabs/karate#calling-java) from Karate.\n[`dogs.feature`](src/test/java/demo/dogs/dogs.feature) | How to easily use [Java interop](https://github.com/karatelabs/karate#calling-java) to make a JDBC / database call from a Karate test. Here is the utility-class code - which depends on [Spring JDBC](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-JdbcTemplate): [`DbUtils.java`](src/main/java/com/intuit/karate/demo/util/DbUtils.java). The same approach can be used to mix things like Selenium or WebDriver into Karate.\n[`cats-java.feature`](src/test/java/demo/java/cats-java.feature) | Another example of how to call [Java code](https://github.com/karatelabs/karate#calling-java) showing how you can pass JSON data around.\n[`schema.feature`](src/test/java/demo/schema/schema.feature) | Karate's [simpler approach](https://github.com/karatelabs/karate#schema-validation) to schema-validation [compared with](https://twitter.com/KarateDSL/status/878984854012022784) an actual JSON-schema example taken from [json-schema.org](http://json-schema.org/example1.html). If you really want to perform JSON-schema validation, this example shows you how you can easily do so using [Java interop](https://github.com/karatelabs/karate#calling-java) and the [json-schema-validator](https://github.com/java-json-tools/json-schema-validator), but should you really ?\n[`first.feature`](src/test/java/demo/tags/first.feature) | Along with [`second.feature`](src/test/java/demo/tags/second.feature) shows how you can use [tags](https://github.com/karatelabs/karate#tags) to selectively run tests or groups of tests from the command-line.\n[`soap.feature`](src/test/java/demo/soap/soap.feature) | Examples for both [SOAP](https://github.com/karatelabs/karate#soap) 1.1 and 1.2, also showing how you can [read](https://github.com/karatelabs/karate#reading-files) request payloads and even expected response XML from files. Note that examples of how to manipulate XML are linked from the [main documentation](https://github.com/karatelabs/karate#advanced-xpath) - for example this one: [`xml.feature`](../karate-core/src/test/java/com/intuit/karate/core/xml/xml.feature)\n[`dynamic-params.feature`](src/test/java/demo/search/dynamic-params.feature) | Multiple examples of data-driven testing including using a `Scenario Outline` and `Examples` - so that you can compare approaches. Since the [`params`](https://github.com/karatelabs/karate#params) keyword takes JSON (and keys with null values are ignored), you can easily script different permutations of query parameters. This example also uses a JavaScript function (to simplify a custom assertion), which is defined in a separate file.\n[`outline.feature`](../karate-junit4/src/test/java/com/intuit/karate/junit4/demos/outline.feature) | The Karate [enhanced `Scenario Outline`](https://github.com/karatelabs/karate#scenario-outline-enhancements) makes data-driven tests JSON-friendly.\n[`dynamic-csv.feature`](src/test/java/demo/outline/dynamic-csv.feature) | Using CSV for a data-driven [dynamic scenario outline](https://github.com/karatelabs/karate#dynamic-scenario-outline).\n[`cat.feature`](src/test/java/demo/unit/cat.feature) | So you like Karate's data-driven and assertion capabilities ? Try Karate for [unit testing Java code](https://twitter.com/ptrthomas/status/1132515667310047233) ! You can re-use the [glue code](src/test/java/demo/unit/common.feature) in multiple tests.\n[`call-feature.feature`](src/test/java/demo/callfeature/call-feature.feature) | How you can re-use a sequence of HTTP calls in a `*.feature` file from other test scripts. This is hugely useful for those common authentication or 'set up' flows that create users, etc. Refer to the [main documentation](https://github.com/karatelabs/karate#calling-other-feature-files) on how you can pass parameters in and get data back from the 'called' script.\n[`call-json-array.feature`](src/test/java/demo/callarray/call-json-array.feature) | This example loads JSON data from a file and uses it to call a `*.feature` file in a loop. This approach can enable very dynamic data-driven tests, since there are a variety of ways by which you can create the JSON data, for example by calling custom Java code.\n[`call-table.feature`](src/test/java/demo/calltable/call-table.feature) | This is a great example of how Karate combines with Cucumber and JsonPath to give you an extremely readable data-driven test. Karate's [`table`](https://github.com/karatelabs/karate#table) keyword is a super-elegant and readable way to create JSON arrays, perfect for setting up all manner of data-driven tests.\n[`call-dynamic-json.feature`](src/test/java/demo/calldynamic/call-dynamic-json.feature) | Shows how to dynamically create a JSON array and then use it to call a `*.feature` file in a loop. In this example, the JSON is created using a JavaScript function, but it can very well be the response from an HTTP call, the result of a JsonPath expression or even a List of HashMap-s acquired by [calling Java](https://github.com/karatelabs/karate#calling-java). This test actually calls a second `*.feature` file in a loop to validate a 'get by id'. Using JsonPath and [`match each`](https://github.com/karatelabs/karate#match-each) to validate all items within a JSON array is also demonstrated.\n[`call-once.feature`](src/test/java/demo/callonce/call-once.feature) | Cucumber has a [limitation](https://github.com/cucumber/cucumber-jvm/issues/515) where `Background` steps are re-run for every `Scenario` and even for every `Examples` row within a `Scenario Outline`. This is a problem when you have expensive and time-consuming HTTP calls in your 'set-up' routines. Fortunately you have an elegant work-around with Karate's [`callonce`](https://github.com/karatelabs/karate#callonce) keyword.\n[`polling.feature`](src/test/java/demo/polling/polling.feature) | [Retry support](https://github.com/karatelabs/karate#retry-until) is built-in to Karate, but you can also achieve this by combining JavaScript functions with a [`call` to another `*.feature` file](https://github.com/karatelabs/karate#calling-other-feature-files).\n[`websocket.feature`](src/test/java/demo/websocket/websocket.feature) | How to write [websocket](https://github.com/karatelabs/karate#websocket) tests, also see [`echo.feature`](src/test/java/demo/websocket/echo.feature).\n[`JavaApiTest.java`](src/test/java/demo/java/JavaApiTest.java) | If you need to call a Karate test from Java code you can do so using the [Java API](https://github.com/karatelabs/karate#java-api). Also see [this](https://twitter.com/KarateDSL/status/1353969718730788865).\n[`main.feature`](src/test/java/mock/async/main.feature) | You can take [Java interop](https://github.com/karatelabs/karate#calling-java) and [Karate test-doubles (mocks)](https://github.com/karatelabs/karate/tree/master/karate-netty) to extremes. This particular test is described [here](https://twitter.com/KarateDSL/status/1417023536082812935).\n\n## Configuration and Best Practices\n\n\n| File | Demonstrates |\n| ---- | ------------ |\n[`karate-config.js`](src/test/java/karate-config.js) | Shows how the `demoBaseUrl` property is injected into all the test scripts [on startup](https://github.com/karatelabs/karate#configuration). Notice how JavaScript allows you to perform simple conditional logic and string manipulation, while still being a 'devops-friendly' plain-text file. It is good practice to set the `connectTimeout` and `readTimeout` so that your tests 'fail fast' if servers don't respond. For advanced users - you can even run a 'global' init routine using [`karate.callSingle()`](https://github.com/karatelabs/karate#the-karate-object).\n[`TestBase.java`](src/test/java/demo/TestBase.java) | This is specific to Spring Boot, but this code takes care of starting the embedded app-server and dynamically chooses a free port. The chosen port value is passed to the above config routine via a Java `System.setProperty()` call.\n[`DemoTestParallel.java`](src/test/java/demo/DemoTestParallel.java) | Karate has a utility to [run tests in parallel](https://github.com/karatelabs/karate#parallel-execution) and this does not depend on JUnit, TestNG or even Maven. A Cucumber JSON report file would be generated for each feature executed. You can easily configure your CI with the location of these files so that you get proper test-reports after a build. This is **the** recommended way of running Karate as part of an automated build or CI pipeline. Here, the (optional) third-party [cucumber-reporting](https://github.com/damianszczepanik/cucumber-reporting) library is being used (see details below).\n[`pom.xml`](pom.xml) | Look out for how the [`maven-surefire-plugin`](http://maven.apache.org/surefire/maven-surefire-plugin/examples/inclusion-exclusion.html) can be configured to point to what is basically your 'test-suite'. You may not even need to do this if you follow the [recommended naming conventions and folder structure](https://github.com/karatelabs/karate#naming-conventions), and then Maven defaults would work as you would expect. Note that this demo application has many dependencies that you will *not* need for a typical Karate project.\n\n## Gradle\nRefer to the wiki page: [Gradle](https://github.com/karatelabs/karate/wiki/Gradle).\n\n## Example Report\nThis is optional because Karate's native HTML reports should serve all your needs. But you can ask Karate to emit the \"Cucumber JSON\" report data format, which can be consumed by third-party utilities in the Cucumber ecosystem.\n\nSince the [maven-cucumber-reporting](https://github.com/damianszczepanik/maven-cucumber-reporting) plugin [has an issue](https://github.com/damianszczepanik/maven-cucumber-reporting/issues/61#issuecomment-310815425) where reports will not be generated if the build fails, we recommend that you directly use the [cucumber-reporting](https://github.com/damianszczepanik/cucumber-reporting) library programmatically in combination with the [Karate parallel runner](https://github.com/karatelabs/karate#parallel-execution). Here is how:\n\n### Maven Dependency\nAdd the `net.masterthought:cucumber-reporting` jar as a dependency in `test` scope\n```xml\n<dependency>\n    <groupId>net.masterthought</groupId>\n    <artifactId>cucumber-reporting</artifactId>\n    <version>5.3.1</version>\n    <scope>test</scope>\n</dependency>\n```\n\n### Log4j Config File\nIf you don't already have log4j (v2) in the mix, place this minimal config on the classpath as `log4j2.properties` (in the same folder as `karate-config.js`).\n```\nlog4j.rootLogger = INFO, CONSOLE\nlog4j.appender.CONSOLE = org.apache.log4j.ConsoleAppender\nlog4j.appender.CONSOLE.layout = org.apache.log4j.PatternLayout\n```\n\n### Generate Report\nRefer to the code in the demo: [`DemoTestParallel.java`](src/test/java/demo/DemoTestParallel.java#L43), specifically the `generateReport()` method. Note that [`outputCucumberJson(true)`](https://github.com/karatelabs/karate#parallel-execution) is called on the `Runner` \"builder\".\n\nAnd here is the output, which goes into `target/cucumber-html-reports` if you follow the above steps:\n\n<img src=\"src/test/resources/karate-maven-report.jpg\" height=\"600px\"/>\n\n## Code Coverage using Jacoco\nIn the [`pom.xml`](pom.xml#L160), code coverage using [Jacoco](http://www.baeldung.com/jacoco) is also demonstrated. Since this is set-up as a [Maven profile](http://maven.apache.org/guides/introduction/introduction-to-profiles.html), instrumentation and code-coverage reporting would be performed only when you use the `coverage` profile. Note that code-coverage data (binary) would be saved to this file: `target/jacoco.exec`.\n\nSo to run tests and perform code-coverage:\n\n```\nmvn clean test -Pcoverage\n```\n\n And the HTML reports would be output to `target/site/jacoco/index.html`.\n\n<img src=\"src/test/resources/karate-jacoco.jpg\" height=\"300px\"/>\n\nAs this demo example shows - if you are able to start your app-server and run Karate tests in the same JVM process, code-coverage reports for even HTTP integration tests will be very easy to generate.\n## Code Coverage for non-Java Projects\nThis has been demonstrated for JavaScript by [Kirk Slota](https://twitter.com/kirk_slota). You can find a working sample here: [`karate-istanbul`](https://github.com/kirksl/karate-istanbul) - and you can read the [discussion at Stack Overflow](https://stackoverflow.com/q/59977566/143475) for more details.\n\nYou should be able to use the same approach for other platforms. Note that there are plenty of ways to start a Karate test via the command-line, such as the [standalone JAR](https://github.com/karatelabs/karate/tree/master/karate-netty#standalone-jar).\n\n"
  },
  {
    "path": "karate-demo/build.gradle",
    "content": "buildscript {\n    ext {\n        springBootVersion = '1.5.3.RELEASE'\n        springVersion = '4.3.8.RELEASE'\n        gradleVersionProperty = '4.1'\n        karateVersion = '1.5.0.RC1'\n        masterThoughtVersion = '3.8.0'\n        activeMqVersion = '5.15.2'\n    }\n    repositories {\n        maven { url 'https://plugins.gradle.org/m2/' }\n        maven { url \"http://repo.spring.io/release\" }\n        maven { url \"http://repo.spring.io/milestone\" }\n        maven { url \"http://repo.spring.io/snapshot\" }\n    }\n    dependencies {\n        classpath(\"org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}\")\n    }\n}\n\napply plugin: 'eclipse'\napply plugin: 'java'\napply plugin: 'idea'\napply plugin: 'org.springframework.boot'\n\njar {\n    baseName = 'karate-demo'\n    version = karateVersion\n}\n\nbootRun {\n    systemProperties = System.properties\n}\n\n// In this section you declare where to find the dependencies of your project\nrepositories {\n    mavenLocal()\n    mavenCentral()\n    maven { url \"http://repo.spring.io/release\" }\n    maven { url \"http://repo.spring.io/milestone\" }\n    maven { url \"http://repo.spring.io/snapshot\" }\n}\n\nsourceSets {\n    test {\n        java {\n            // Excluding UIRunner files as these require the javafx libraries\n            // which are not shipped with OpenJDK. These UIRunner classes are\n            // classes that allow developers to run/debug karate tests via a UI\n            // and as such are not required for headless runs on jenkins server\n            // but can run happily via IDE of the developer without needed to be\n            // compiled by gradle.\n            srcDir file('src/test/java')\n            exclude '**/*UiRunner*.java'\n        }\n        resources {\n            // Using recommended karate project layout where karate feature files\n            // and associated javascript resources sit in same /test/java folders\n            // as their java counterparts.\n            srcDir file('src/test/java')\n            exclude '**/*.java'\n        }\n    }\n}\n\ndependencies {\n    compile 'org.springframework.boot:spring-boot-starter-web'\n    compile 'org.springframework.boot:spring-boot-starter-security'\n    compile 'org.springframework:spring-jdbc:' + springVersion\n    compile 'org.springframework:spring-websocket:' + springVersion\n    compile 'com.github.java-json-tools:json-schema-validator:2.2.8'\n    compile 'commons-io:commons-io:2.5'\n    runtime 'com.h2database:h2:1.4.196'\n    testImplementation 'junit:junit'\n    testImplementation 'io.karatelabs:karate-junit5:' + karateVersion\n    testImplementation 'org.apache.activemq:activemq-broker:' + activeMqVersion\n    testImplementation 'org.apache.activemq:activemq-client:' + activeMqVersion\n    testImplementation 'org.apache.activemq:activemq-kahadb-store:' + activeMqVersion\n    testImplementation 'org.apache.geronimo.specs:geronimo-jms_1.1_spec:1.1.1'\n    testImplementation 'net.masterthought:cucumber-reporting:' + masterThoughtVersion\n}\n\ntest {\n    // When running the test task, only run the base Test class which runs all cucumber\n    // tests in parallel.\n    include '**/*DemoTestParallel*'\n    // Pull karate options into the runtime\n    systemProperty \"karate.options\", System.properties.getProperty(\"karate.options\")\n    // Pull karate options into the JVM\n    systemProperty \"karate.env\", System.properties.getProperty(\"karate.env\")\n    // Ensure tests are always run\n    outputs.upToDateWhen { false }\n    // attach debugger\n    if (System.getProperty('debug', 'false') == 'true') {\n        jvmArgs '-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=9009'\n    }\n}\n\nwrapper {\n    gradleVersion = gradleVersionProperty\n}\n"
  },
  {
    "path": "karate-demo/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>io.karatelabs</groupId>\n        <artifactId>karate-parent</artifactId>\n        <version>1.5.2</version>\n    </parent>\n    \n    <artifactId>karate-demo</artifactId>\n    <packaging>jar</packaging>  \n    \n    <properties>\n        <activemq.version>5.15.16</activemq.version>\n        <jaxb.version>2.3.0</jaxb.version>\n    </properties>\n\n    <!-- you will not need all these dependencies for a typical karate project !-->\n    <!-- for a skeleton project, use the archetype: https://github.com/karatelabs/karate#quickstart !-->\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-dependencies</artifactId>\n                <version>${spring.boot.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-websocket</artifactId>\n            <version>${spring.boot.version}</version>\n        </dependency>        \n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-security</artifactId>\n            <version>${spring.boot.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework</groupId>\n            <artifactId>spring-jdbc</artifactId>\n            <version>${spring.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-logging</artifactId>\n            <version>${spring.boot.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.h2database</groupId>\n            <artifactId>h2</artifactId>\n            <version>2.2.220</version>\n            <scope>runtime</scope>\n        </dependency>        \n        <dependency>\n            <groupId>commons-io</groupId>\n            <artifactId>commons-io</artifactId>\n            <version>2.14.0</version>\n        </dependency>\n        <dependency>\n            <groupId>commons-codec</groupId>\n            <artifactId>commons-codec</artifactId>\n            <version>1.14</version>\n            <scope>test</scope>\n        </dependency>        \n        <dependency>\n            <groupId>com.github.java-json-tools</groupId>\n            <artifactId>json-schema-validator</artifactId>\n            <version>2.2.8</version>\n        </dependency>\n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-junit5</artifactId>\n            <version>${project.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>net.masterthought</groupId>\n            <artifactId>cucumber-reporting</artifactId>\n            <version>5.3.1</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.activemq</groupId>\n            <artifactId>activemq-broker</artifactId>\n            <version>${activemq.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.activemq</groupId>\n            <artifactId>activemq-client</artifactId>\n            <version>${activemq.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.activemq</groupId>\n            <artifactId>activemq-kahadb-store</artifactId>\n            <version>${activemq.version}</version>\n            <scope>test</scope>\n        </dependency>                \n        <dependency>\n            <groupId>org.apache.geronimo.specs</groupId>\n            <artifactId>geronimo-jms_1.1_spec</artifactId>\n            <version>1.1.1</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>${maven.surefire.version}</version>\n                <configuration>\n                    <includes>\n                        <include>demo/DemoTestParallel.java</include>\n                            <!-- TODO fix problem with netty dependency in this pom, suspect spring pom import -->\n<!--                        <include>mock/contract/*Test.java</include>-->\n<!--                        <include>mock/async/*Test.java</include>-->\n<!--                        <include>mock/micro/*Runner.java</include>-->\n<!--                        <include>mock/proxy/*Runner.java</include>-->\n                        <include>ssl/*Test.java</include>\n                    </includes>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.sonatype.central</groupId>\n                <artifactId>central-publishing-maven-plugin</artifactId>\n                <version>${central.publishing.maven.plugin.version}</version>\n                <configuration>\n                    <skipPublishing>true</skipPublishing>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n    \n    <profiles>\n        <profile>\n            <id>coverage</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-surefire-plugin</artifactId>\n                        <version>${maven.surefire.version}</version>\n                        <configuration>\n                            <includes>\n                                <include>demo/DemoTestParallel.java</include>\n                            </includes>\n                            <!-- needed for jacoco to integrate properly -->\n                            <argLine>-Dfile.encoding=UTF-8 ${argLine}</argLine>\n                        </configuration>\n                    </plugin>                     \n                    <plugin>\n                        <groupId>org.jacoco</groupId>\n                        <artifactId>jacoco-maven-plugin</artifactId>\n                        <version>${jacoco.plugin.version}</version>\n                        <executions>\n                            <execution>\n                                <id>default-prepare-agent</id>\n                                <goals>\n                                    <goal>prepare-agent</goal>\n                                </goals>\n                            </execution>\n                            <execution>\n                                <id>default-report</id>\n                                <phase>test</phase>\n                                <goals>\n                                    <goal>report</goal>\n                                </goals>\n                            </execution>                             \n                        </executions>                \n                    </plugin>                     \n                </plugins>\n            </build>\n        </profile>\n        <profile>\n            <id>smoke</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-surefire-plugin</artifactId>\n                        <version>${maven.surefire.version}</version>\n                        <configuration>\n                            <includes>\n                                <include>demo/DemoTestParallel.java</include>\n                            </includes>\n                        </configuration>\n                    </plugin>\n                </plugins>                \n            </build>\n        </profile>            \n    </profiles>\n    \n</project>\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/Application.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.ConfigurableApplicationContext;\n\n/**\n *\n * @author pthomas3\n */\n@SpringBootApplication\npublic class Application {\n\n    public static void main(String[] args) {\n        run(args);\n    }\n\n    public static ConfigurableApplicationContext run(String[] args) {\n        return SpringApplication.run(Application.class, args);\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/config/ServerStartedInitializingBean.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.config;\n\nimport java.util.Arrays;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.boot.web.context.WebServerInitializedEvent;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.stereotype.Component;\n\n/**\n *\n * @author pthomas3\n */\n@Component\npublic class ServerStartedInitializingBean implements ApplicationRunner, ApplicationListener<WebServerInitializedEvent> {\n\n    private static final Logger logger = LoggerFactory.getLogger(ServerStartedInitializingBean.class);\n\n    private int localPort;\n\n    public int getLocalPort() {\n        return localPort;\n    }\n\n    @Override\n    public void run(ApplicationArguments aa) throws Exception {\n        logger.info(\"server started with args: {}\", Arrays.toString(aa.getSourceArgs()));\n    }\n\n    @Override\n    public void onApplicationEvent(WebServerInitializedEvent event) {\n        localPort = event.getWebServer().getPort();\n        logger.info(\"after runtime init, local server port: {}\", localPort);\n    }\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/config/TomcatConfig.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.config;\n\nimport org.apache.tomcat.util.http.LegacyCookieProcessor;\nimport org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;\nimport org.springframework.boot.web.server.WebServerFactoryCustomizer;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * @author pthomas3\n */\n@Configuration\npublic class TomcatConfig {\n\n    @Bean\n    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> cookieProcessorCustomizer() {\n        return factory -> {\n            if (factory != null) {\n                factory.addContextCustomizers(context -> context.setCookieProcessor(new LegacyCookieProcessor()));\n            }\n        };\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/config/WebSecurityConfig.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.config;\n\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;\n\n/**\n *\n * @author pthomas3\n */\n@Configuration\npublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {\n\n    @Override\n    protected void configure(HttpSecurity http) throws Exception {\n        http.csrf().ignoringAntMatchers(\n                \"/cats/**\",\n                \"/dogs/**\",\n                \"/files/**\",\n                \"/search/**\",\n                \"/redirect/**\",\n                \"/graphql/**\",\n                \"/soap/**\",\n                \"/echo/**\",\n                \"/websocket/**\",\n                \"/websocket-controller/**\"\n        );\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/config/WebSocketConfig.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.config;\n\nimport com.intuit.karate.demo.controller.WebSocketHandler;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.socket.config.annotation.EnableWebSocket;\nimport org.springframework.web.socket.config.annotation.WebSocketConfigurer;\nimport org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;\n\n/**\n *\n * @author pthomas3\n */\n@Configuration\n@EnableWebSocket\npublic class WebSocketConfig implements WebSocketConfigurer {\n\n    @Override\n    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {\n        registry.addHandler(handler(), \"/websocket\");\n    }\n\n    @Bean\n    WebSocketHandler handler() {\n        return new WebSocketHandler();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/CatsController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport com.intuit.karate.demo.domain.Cat;\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport org.springframework.web.bind.annotation.DeleteMapping;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.PutMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/cats\")\npublic class CatsController {\n\n    private final AtomicInteger counter = new AtomicInteger();\n    private final Map<Integer, Cat> cats = new ConcurrentHashMap();\n\n    @PostMapping\n    public Cat create(@RequestBody Cat cat) {\n        int id = counter.incrementAndGet();\n        cat.setId(id);\n        synchronized (this) {\n            cats.put(id, cat);\n        }\n        return cat;\n    }\n\n    @GetMapping\n    public Collection<Cat> list() {\n        return cats.values();\n    }\n\n    @GetMapping(\"/{id:.+}\")\n    public Cat get(@PathVariable int id) {\n        return cats.get(id);\n    }\n\n    @GetMapping(\"/{id:.+}/kittens\")\n    public Collection<Cat> getKittens(@PathVariable int id) {\n        return cats.get(id).getKittens();\n    }\n\n    @PutMapping(\"/{id:.+}\")\n    public Cat put(@PathVariable int id, @RequestBody Cat cat) {\n        synchronized (this) {\n            cats.put(id, cat);\n        }\n        return cat;\n    }\n\n    @DeleteMapping(\"/{id:.+}\")\n    public void delete(@PathVariable int id) {\n        Cat cat;\n        synchronized (this) {\n            cat = cats.remove(id);\n        }\n        if (cat == null) {\n            throw new RuntimeException(\"cat not found, id: \" + id);\n        }\n    }\n\n    @DeleteMapping\n    public void deleteWithBody(@RequestBody Cat cat) {\n        int id = cat.getId();\n        delete(id);\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/DogsController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport com.intuit.karate.demo.domain.Dog;\nimport java.util.Collection;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.jdbc.core.RowMapper;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/dogs\")\npublic class DogsController {\n\n    @Autowired(required = true)\n    private JdbcTemplate jdbc;\n\n    private final AtomicInteger counter = new AtomicInteger();\n    \n    private static final RowMapper<Dog> ROW_MAPPER = (rs, rowNum) -> new Dog(rs.getInt(\"ID\"), rs.getString(\"NAME\"));\n\n    @PostMapping\n    public Dog create(@RequestBody Dog dog) {\n        int id = counter.incrementAndGet();\n        dog.setId(id);\n        jdbc.update(\"INSERT INTO DOGS(ID, NAME) values(?, ?)\", dog.getId(), dog.getName());\n        return dog;\n    }\n\n    @GetMapping\n    public Collection<Dog> list() {\n        return jdbc.query(\"SELECT * FROM DOGS\", ROW_MAPPER);\n    }\n    \n    @GetMapping(\"/{id:.+}\")\n    public Dog get(@PathVariable int id) {\n        return jdbc.queryForObject(\"SELECT * FROM DOGS D WHERE D.ID = ?\", ROW_MAPPER, id);\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/EchoController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport com.intuit.karate.demo.domain.Binary;\nimport com.intuit.karate.demo.domain.Message;\nimport com.intuit.karate.demo.domain.SignIn;\nimport java.util.Arrays;\nimport java.util.Map;\nimport javax.servlet.http.HttpServletRequest;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.ModelAttribute;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/echo\")\npublic class EchoController {\n        \n    @PostMapping\n    public String echo(@RequestBody String request) {\n        return request;\n    }  \n    \n    @PostMapping\n    @RequestMapping(\"/message\")\n    public String echo(@ModelAttribute Message message) {\n        return message.getText();\n    }     \n    \n    @GetMapping\n    public Map<String, String[]> search(HttpServletRequest request) {\n        return request.getParameterMap();\n    }\n    \n    @PostMapping(\"/jwt\")\n    public ResponseEntity jwtPost(@RequestBody SignIn signin) {\n        if (\"john\".equals(signin.getUsername()) && \"secret\".equals(signin.getPassword())) {\n            return ResponseEntity.ok(\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdEBleGFtcGxlLmNvbSIsInJvbGUiOiJlZGl0b3IiLCJleHAiOjk5OTk5OTk5OSwiaXNzIjoia2xpbmdtYW4ifQ._D2tcNJN6mawerckbNotuINRm_8bRaXVi18hgsuOk9Y\");\n        } else {\n            return ResponseEntity.status(403).build();\n        }\n    }\n\n    @GetMapping(\"/jwt/resource\")\n    public String jwtResource() {\n        return \"success\";\n    }  \n    \n    @PostMapping(\"/binary\")\n    public Binary create(@RequestBody Binary bin) {\n        if (!bin.getMessage().equals(\"hello\")) {\n            throw new RuntimeException(\"expected message 'hello' but was: \" + bin.getMessage());\n        }\n        if (!Arrays.equals(\"hello\".getBytes(), bin.getData())) {\n            throw new RuntimeException(\"expected data 'hello' but was: \" + new String(bin.getData()));\n        }        \n        bin = new Binary();\n        bin.setMessage(\"world\");\n        bin.setData(\"world\".getBytes());\n        return bin;\n    }     \n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/EncodingController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/encoding\")\npublic class EncodingController {\n\n    @RequestMapping(\"/**\")\n    public String echoPath(HttpServletRequest httpServletRequest) {\n        // running from spring boot + tomcat\n        if (httpServletRequest.getPathInfo() == null) {\n            return httpServletRequest.getServletPath().replace(\"/encoding/\", \"\");\n\n        // running from mock spring mvc\n        } else {\n            return httpServletRequest.getPathInfo().replace(\"/encoding/\", \"\");\n        }\n    }\n    \n    @RequestMapping(\"/index.php?/api/v2/**\")\n    public String echoPathWithQuestion(HttpServletRequest httpServletRequest) {\n        // running from spring boot + tomcat\n        if (httpServletRequest.getPathInfo() == null) {\n            return httpServletRequest.getServletPath().replace(\"/encoding/index.php?/api/v2/\", \"\");\n\n        // running from mock spring mvc\n        } else {\n            return httpServletRequest.getPathInfo().replace(\"/encoding/index.php?/api/v2/\", \"\");\n        }\n    }    \n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/GraphqlController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport java.io.InputStream;\nimport java.util.Map;\nimport org.apache.commons.io.IOUtils;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/graphql\")\npublic class GraphqlController {\n    \n    @PostMapping\n    public String handle(@RequestBody Map<String, Object> request) throws Exception {\n        Object variables = request.get(\"variables\");\n        String filename = variables == null ? \"graphql-1.json\" : \"graphql-2.json\";\n        InputStream is = getClass().getClassLoader().getResourceAsStream(filename);\n        return IOUtils.toString(is, \"utf-8\");\n    }   \n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/GreetingController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport com.intuit.karate.demo.domain.Greeting;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/greeting\")\npublic class GreetingController {\n\n    private final AtomicInteger counter = new AtomicInteger();\n\n    @GetMapping(\"/reset\")\n    public String reset() {\n        int value = 0;\n        counter.set(value);\n        return \"{ \\\"counter\\\": 0 }\";\n    }\n\n    @GetMapping\n    public Greeting getGreeting(@RequestParam(value = \"name\", defaultValue = \"World\") String name) {\n        return new Greeting(counter.incrementAndGet(), \"Hello \" + name + \"!\");\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/HeadersController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport javax.servlet.http.Cookie;\nimport javax.servlet.http.HttpServletResponse;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.CookieValue;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestHeader;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/headers\")\npublic class HeadersController {\n\n    private final Map<String, String> tokens = new HashMap<>();\n\n    @GetMapping\n    public ResponseEntity getToken(HttpServletResponse response) {\n        String token = UUID.randomUUID().toString();\n        String time = System.currentTimeMillis() + \"\";\n        tokens.put(token, time);\n        response.addCookie(new Cookie(\"time\", time));\n        return ResponseEntity.ok().body(token);\n    }\n\n    @GetMapping(\"/{token:.+}\")\n    public ResponseEntity validateToken(@CookieValue(\"time\") String time,\n            @RequestHeader(\"Authorization\") String[] authorization, @PathVariable String token,\n            @RequestParam String url) {\n        String temp = tokens.get(token);\n        String auth = authorization[0];\n        if (auth.equals(\"dummy\")) {\n            auth = authorization[1];\n        }\n        if (temp.equals(time) && auth.equals(token + time + url)) {\n            return ResponseEntity.ok().build();\n        } else {\n            return ResponseEntity.badRequest().build();\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/RedirectController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport java.io.IOException;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/redirect\")\npublic class RedirectController {    \n    \n    @GetMapping\n    public void fromGet(HttpServletRequest request, HttpServletResponse response) throws IOException {\n        String url = request.getRequestURL().toString();\n        String uri = url.replace(\"/redirect\", \"\");\n        if (\"\".equals(uri)) {\n            uri = \"http://localhost:8080\"; // hard code for karate-mock-servlet\n        }\n        String append = \"\";\n        if (request.getParameter(\"foo\") != null) {\n            append = \"?foo=\" + request.getParameter(\"foo\");\n        }\n        response.sendRedirect(uri + \"/search\" + append);\n    } \n    \n    @PostMapping\n    public void fromPost(HttpServletRequest request, HttpServletResponse response) throws IOException {\n        fromGet(request, response);\n    }\n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/SearchController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Enumeration;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.servlet.http.Cookie;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport static org.springframework.web.bind.annotation.RequestMethod.*;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/search\")\npublic class SearchController {\n\n    @GetMapping\n    public Map<String, String[]> search(HttpServletRequest request) {\n        return request.getParameterMap();\n    }\n\n    @PostMapping\n    public String echo(@RequestBody String request) {\n        return request;\n    }\n\n    @RequestMapping(value = \"/headers\", method = {GET, POST})\n    public Map<String, Object> echoHeaders(HttpServletRequest request) {\n        Map<String, Object> map = new LinkedHashMap<>();\n        Enumeration<String> headerNames = request.getHeaderNames();\n        while (headerNames.hasMoreElements()) {\n            String headerName = headerNames.nextElement();\n            Enumeration<String> headerValues = request.getHeaders(headerName);\n            List<String> list = new ArrayList();\n            while (headerValues.hasMoreElements()) {\n                String headerValue = headerValues.nextElement();\n                list.add(headerValue);\n            }\n            map.put(headerName.toLowerCase(), list);\n        }\n        return map;\n    }\n\n    @RequestMapping(value = \"/cookies\", method = {GET, POST})\n    public List<Cookie> echoCookies(HttpServletRequest request, HttpServletResponse response) {\n        Cookie[] cookies = request.getCookies();\n        if (cookies == null) {\n            return Collections.emptyList();\n        } else {\n            String domain = request.getParameter(\"domain\");\n            for (Cookie cookie: cookies) {\n                if (domain != null) {\n                    cookie.setDomain(domain);\n                }\n                response.addCookie(cookie);\n            }\n            return Arrays.asList(cookies);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/SignInController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport com.intuit.karate.demo.domain.SignIn;\nimport javax.servlet.http.HttpServletRequest;\nimport org.springframework.security.web.csrf.CsrfToken;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.ModelAttribute;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/signin\")\npublic class SignInController {\n    \n    @GetMapping(\"/token\")\n    public String getCsrfToken(HttpServletRequest request) {\n        CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());\n        return token.getToken();\n    }\n\n    @PostMapping\n    public String signInPost(@ModelAttribute SignIn signin) {\n        if (\"john\".equals(signin.getUsername()) && \"secret\".equals(signin.getPassword())) {\n            return \"success\";\n        } else {\n            return \"failure\";\n        }\n    }\n    \n    @GetMapping\n    public String signInGet(@RequestParam String username, @RequestParam String password) {\n        if (\"john\".equals(username) && \"secret\".equals(password)) {\n            return \"success\";\n        } else {\n            return \"failure\";\n        }\n    }   \n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/SoapController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport java.io.InputStream;\nimport java.util.Map;\nimport javax.servlet.http.HttpServletRequest;\nimport org.apache.commons.io.IOUtils;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestHeader;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/soap\")\npublic class SoapController {\n    \n    @PostMapping\n    public String handle(HttpServletRequest request) throws Exception {\n        String soapAction = request.getHeader(\"SOAPAction\");\n        String filename = soapAction == null ? \"soap-1.xml\" : \"soap-2.xml\";\n        InputStream is = getClass().getClassLoader().getResourceAsStream(filename);\n        return IOUtils.toString(is, \"utf-8\");\n    }   \n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/UploadController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.intuit.karate.demo.domain.FileInfo;\nimport com.intuit.karate.demo.domain.Message;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport org.apache.commons.io.FileUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.core.io.Resource;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RequestPart;\nimport org.springframework.web.bind.annotation.ResponseBody;\nimport org.springframework.web.multipart.MultipartFile;\n\n/**\n *\n * @author pthomas3\n */\n@Controller\n@RequestMapping(\"/files\")\npublic class UploadController {\n\n    private static final Logger logger = LoggerFactory.getLogger(UploadController.class);\n\n    private static final String FILES_BASE = \"target/demofiles/\";\n\n    private final ObjectMapper mapper = new ObjectMapper();\n\n    public UploadController() throws Exception {\n        File file = new File(FILES_BASE);\n        FileUtils.forceMkdir(file);\n        logger.info(\"created directory: {}\", file);\n    }\n\n    @PostMapping\n    public @ResponseBody\n    FileInfo upload(@RequestParam(\"myFile\") MultipartFile file,\n            @RequestParam(\"message\") String message) throws Exception {\n        return getFileInfo(file, message);\n    }\n\n    @PostMapping(\"/multiple\")\n    public @ResponseBody\n    List<FileInfo> upload(@RequestParam(\"myFile1\") MultipartFile file1,\n            @RequestParam(\"myFile2\") MultipartFile file2, @RequestParam(\"message\") String message) throws Exception {\n        List<FileInfo> fileInfoList = new ArrayList<>();\n        fileInfoList.add(getFileInfo(file1, message));\n        fileInfoList.add(getFileInfo(file2, message));\n        return fileInfoList;\n    }\n\n    @PostMapping(\"/array\")\n    public @ResponseBody\n    List<FileInfo> upload(@RequestParam(\"myFiles\") MultipartFile[] files,\n            @RequestParam(\"message\") String message) throws Exception {\n        List<FileInfo> fileInfoList = new ArrayList<>();\n        fileInfoList.add(getFileInfo(files[0], message));\n        fileInfoList.add(getFileInfo(files[1], message));\n        return fileInfoList;\n    }\n\n    @PostMapping(\"/fields\")\n    public @ResponseBody\n    Map<String, Object> fields(\n            @RequestParam(\"message\") String message, @RequestParam(\"json\") String json) throws Exception {\n        Map<String, Object> map = new HashMap();\n        map.put(\"message\", message);\n        map.put(\"json\", mapper.readValue(json, HashMap.class));\n        return map;\n    }\n\n    private FileInfo getFileInfo(MultipartFile file, String message) throws Exception {\n        String uuid = UUID.randomUUID().toString();\n        String filePath = FILES_BASE + uuid;\n        FileUtils.copyToFile(file.getInputStream(), new File(filePath));\n        String filename1 = file.getOriginalFilename();\n        String contentType1 = file.getContentType();\n        FileInfo fileInfo = new FileInfo(uuid, filename1, message, contentType1);\n        String json = mapper.writeValueAsString(fileInfo);\n        FileUtils.writeStringToFile(new File(filePath + \"_meta.txt\"), json, \"utf-8\");\n        return fileInfo;\n    }\n\n    @PostMapping(\"/mixed\")\n    public @ResponseBody\n    FileInfo uploadMixed(@RequestPart(\"myJson\") String json,\n            @RequestPart(\"myFile\") MultipartFile file) throws Exception {\n        Message message = mapper.readValue(json, Message.class);\n        String text = message.getText();\n        return upload(file, text);\n    }\n\n    @GetMapping(\"/{id:.+}\")\n    public ResponseEntity<Resource> download(@PathVariable String id) throws Exception {\n        String filePath = FILES_BASE + id;\n        File file = new File(filePath);\n        File meta = new File(filePath + \"_meta.txt\");\n        String json = FileUtils.readFileToString(meta, \"utf-8\");\n        FileInfo fileInfo = mapper.readValue(json, FileInfo.class);\n        return ResponseEntity\n                .ok()\n                .header(HttpHeaders.CONTENT_DISPOSITION, \"attachment; filename=\\\"\" + fileInfo.getFilename() + \"\\\"\")\n                .header(HttpHeaders.CONTENT_TYPE, fileInfo.getContentType())\n                .body(new FileSystemResource(file));\n    }\n\n    @PostMapping(\"/binary\")\n    public @ResponseBody\n    FileInfo uploadBinary(@RequestParam String name, @RequestBody byte[] bytes) throws Exception {\n        String uuid = UUID.randomUUID().toString();\n        String filePath = FILES_BASE + uuid;\n        File file = new File(filePath);\n        FileUtils.writeByteArrayToFile(file, bytes);\n        FileInfo fileInfo = new FileInfo(uuid, name, \"hello world\", \"application/octet-stream\");\n        String json = mapper.writeValueAsString(fileInfo);\n        FileUtils.writeStringToFile(new File(filePath + \"_meta.txt\"), json, \"utf-8\");\n        return fileInfo;\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/WebSocketController.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.intuit.karate.demo.domain.Greeting;\nimport com.intuit.karate.demo.domain.Message;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n *\n * @author pthomas3\n */\n@RestController\n@RequestMapping(\"/websocket-controller\")\npublic class WebSocketController {\n\n    private static final Logger logger = LoggerFactory.getLogger(WebSocketController.class);\n\n    @Autowired(required = true)\n    private WebSocketHandler handler;\n    \n    private final ObjectMapper mapper = new ObjectMapper();\n\n    @PostMapping\n    public String greet(@RequestBody Message message) throws Exception {\n        long time = System.currentTimeMillis();\n        Greeting greeting = new Greeting(time, \"hello \" + message.getText() + \" !\");\n        String json = mapper.writeValueAsString(greeting);\n        handler.broadcast(null, json);\n        return \"{ \\\"id\\\": \" + time + \" }\";\n    }    \n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/controller/WebSocketHandler.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.controller;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.socket.CloseStatus;\nimport org.springframework.web.socket.TextMessage;\nimport org.springframework.web.socket.WebSocketSession;\nimport org.springframework.web.socket.handler.TextWebSocketHandler;\n\n/**\n *\n * @author pthomas3\n */\npublic class WebSocketHandler extends TextWebSocketHandler {\n\n    private static final Logger logger = LoggerFactory.getLogger(WebSocketHandler.class);\n\n    private final List<WebSocketSession> sessions = new ArrayList();\n\n    @Override\n    protected void handleTextMessage(WebSocketSession wss, TextMessage message) throws Exception {                \n        broadcast(wss.getId(), \"hello \" + message.getPayload() + \" !\");\n    }\n\n    @Override\n    public void afterConnectionEstablished(WebSocketSession session) throws Exception {\n        logger.debug(\"websocket session init: {}\", session);\n        synchronized (sessions) {\n            sessions.add(session);\n        }\n    }\n\n    @Override\n    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {\n        logger.debug(\"websocket session cleanup: {}\", session);\n        synchronized (sessions) {\n            sessions.remove(session);\n        }\n    }        \n    \n    public void broadcast(String id, String message) throws Exception {\n        logger.debug(\"sleeping before broadcast: {}\", message);\n        Thread.sleep(1000);\n        synchronized (sessions) {\n            for (WebSocketSession session : sessions) {\n                if (id == null || id == session.getId()) {\n                    logger.debug(\"sending to websocket session: {}\", session);\n                    try {\n                        session.sendMessage(new TextMessage(message));\n                    } catch (Exception e) {\n                        logger.warn(\"broadcast failed for session: {} - {}\", session, e.getMessage());\n                    }\n                }\n            } \n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/domain/Binary.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.domain;\n\n/**\n *\n * @author pthomas3\n */\npublic class Binary {\n    \n    private String message;\n    private byte[] data;\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void setMessage(String message) {\n        this.message = message;\n    }\n\n    public byte[] getData() {\n        return data;\n    }\n\n    public void setData(byte[] data) {\n        this.data = data;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/domain/Cat.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.domain;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class Cat {\n    \n    private int id;\n    private String name;\n    private List<Cat> kittens;\n    \n    public void addKitten(Cat kitten) {\n        if (kittens == null) {\n            kittens = new ArrayList<>();\n        }\n        kittens.add(kitten);\n    }\n\n    public int getId() {\n        return id;\n    }\n\n    public void setId(int id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }        \n\n    public List<Cat> getKittens() {\n        return kittens;\n    }\n\n    public void setKittens(List<Cat> kittens) {\n        this.kittens = kittens;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/domain/Dog.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.domain;\n\n/**\n *\n * @author pthomas3\n */\npublic class Dog {\n    \n    private int id;\n    private String name;\n    \n    public Dog() {\n        // zero arg constructor\n    }\n    \n    public Dog(int id, String name) {\n        this.id = id;\n        this.name = name;\n    }\n\n    public int getId() {\n        return id;\n    }\n\n    public void setId(int id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/domain/FileInfo.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.domain;\n\n/**\n *\n * @author pthomas3\n */\npublic class FileInfo {\n    \n    private String id;\n    private String filename;\n    private String message;\n    private String contentType;\n    \n    public FileInfo() {\n        // zero arg constructor\n    }\n    \n    public FileInfo(String id, String filename, String message, String contentType) {\n        this.id = id;\n        this.filename = filename;\n        this.message = message;\n        this.contentType = contentType;\n    }\n\n    public String getId() {\n        return id;\n    }\n\n    public String getFilename() {\n        return filename;\n    }  \n\n    public String getMessage() {\n        return message;\n    }        \n\n    public String getContentType() {\n        return contentType;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/domain/Greeting.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.domain;\n\n/**\n *\n * @author pthomas3\n */\npublic class Greeting {\n\n    private final long id;\n    private final String content;\n\n    public Greeting(long id, String content) {\n        this.id = id;\n        this.content = content;\n    }\n\n    public long getId() {\n        return id;\n    }\n\n    public String getContent() {\n        return content;\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/domain/Message.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.domain;\n\n/**\n *\n * @author pthomas3\n */\npublic class Message {\n    \n    private String text;\n\n    public String getText() {\n        return text;\n    }\n\n    public void setText(String text) {\n        this.text = text;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/domain/SignIn.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.demo.domain;\n\n/**\n *\n * @author pthomas3\n */\npublic class SignIn {\n    \n    private String username;\n    private String password;\n\n    public String getUsername() {\n        return username;\n    }\n\n    public void setUsername(String username) {\n        this.username = username;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n    public void setPassword(String password) {\n        this.password = password;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/exception/ErrorResponse.java",
    "content": "package com.intuit.karate.demo.exception;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\npublic class ErrorResponse {\n\n    @JsonProperty(\"status_code\")\n    private int code;\n    @JsonProperty(\"uri_path\")\n    private String path;\n    private String method;\n    @JsonProperty(\"error_message\")\n    private String message;\n\n    public ErrorResponse() {\n    }\n\n    public ErrorResponse(int code, String path, String method, String message) {\n        this.code = code;\n        this.path = path;\n        this.method = method;\n        this.message = message;\n    }\n\n    public int getCode() {\n        return code;\n    }\n\n    public void setCode(int code) {\n        this.code = code;\n    }\n\n    public String getPath() {\n        return path;\n    }\n\n    public void setPath(String path) {\n        this.path = path;\n    }\n\n    public String getMethod() {\n        return method;\n    }\n\n    public void setMethod(String method) {\n        this.method = method;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void setMessage(String message) {\n        this.message = message;\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/exception/GlobalExceptionHandler.java",
    "content": "package com.intuit.karate.demo.exception;\n\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.ControllerAdvice;\nimport org.springframework.web.context.request.ServletWebRequest;\nimport org.springframework.web.context.request.WebRequest;\nimport org.springframework.web.servlet.NoHandlerFoundException;\nimport org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;\n\n/**\n * @author nsehgal\n *\n */\n@ControllerAdvice\npublic class GlobalExceptionHandler extends ResponseEntityExceptionHandler {\n\n    /**\n     * Adding these properties will make the following code active:\n     * spring.mvc.throw-exception-if-no-handler-found=true\n     * spring.resources.add-mappings=false\n     *\n     * @param ex\n     * @param headers\n     * @param status\n     * @param webRequest\n     * @return\n     */\n    @Override\n    protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest webRequest) {\n        String uriPath = webRequest.getDescription(false).substring(4);\n        String message = \"The URL you have reached is not in service at this time\";\n        String method = ((ServletWebRequest) webRequest).getRequest().getMethod();\n        ErrorResponse errorResponse = new ErrorResponse(status.value(), uriPath, method, message);\n        return new ResponseEntity<>(errorResponse, status);\n    }\n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/util/DbUtils.java",
    "content": "package com.intuit.karate.demo.util;\n\nimport java.util.List;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.jdbc.datasource.DriverManagerDataSource;\n\n/**\n *\n * @author pthomas3\n */\npublic class DbUtils {\n    \n    private static final Logger logger = LoggerFactory.getLogger(DbUtils.class); \n    \n    private final JdbcTemplate jdbc;\n\n    public DbUtils(Map<String, Object> config) {\n        String url = (String) config.get(\"url\");\n        String username = (String) config.get(\"username\");\n        String password = (String) config.get(\"password\");\n        String driver = (String) config.get(\"driverClassName\");\n        DriverManagerDataSource dataSource = new DriverManagerDataSource();\n        dataSource.setDriverClassName(driver);\n        dataSource.setUrl(url);\n        dataSource.setUsername(username);\n        dataSource.setPassword(password);\n        jdbc = new JdbcTemplate(dataSource);\n        logger.info(\"init jdbc template: {}\", url);\n    }\n    \n    public Object readValue(String query) {\n        return jdbc.queryForObject(query, Object.class);\n    }    \n    \n    public Map<String, Object> readRow(String query) {\n        return jdbc.queryForMap(query);\n    }\n    \n    public List<Map<String, Object>> readRows(String query) {\n        return jdbc.queryForList(query);\n    }     \n    \n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/util/FileChecker.java",
    "content": "package com.intuit.karate.demo.util;\n\nimport org.apache.commons.io.FileUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.File;\n\n/**\n *\n * @author pthomas3\n */\npublic class FileChecker {\n\n    private static final Logger logger = LoggerFactory.getLogger(FileChecker.class);\n\n    public static String getMetadata(String id) throws Exception {\n        logger.info(\"java was called from karate ! id: {}\", id);\n        // behind the scenes the server is storing data in a file, we read it directly here\n        File file = new File(\"target/demofiles/\" + id + \"_meta.txt\");\n        return FileUtils.readFileToString(file, \"utf-8\");\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/util/FizzBuzz.java",
    "content": "package com.intuit.karate.demo.util;\n\n/**\n *\n * @author pthomas3\n */\npublic class FizzBuzz {\n\n    public static boolean isMultiple(int n, int i) {\n        return n % i == 0;\n    }\n\n    public static boolean isFizzy(int n) {\n        return isMultiple(n, 3);\n    }\n\n    public static boolean isBuzzy(int n) {\n        return isMultiple(n, 5);\n    }\n\n    public static String process(int n) {\n        return isFizzy(n) ? isBuzzy(n)\n                ? \"FizzBuzz\"\n                : \"Fizz\"\n                : isBuzzy(n)\n                ? \"Buzz\"\n                : n + \"\";\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/util/JavaDemo.java",
    "content": "package com.intuit.karate.demo.util;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class JavaDemo {\n\n    private static final Logger logger = LoggerFactory.getLogger(JavaDemo.class);\n\n    public static String getName(Map<String, Object> map) {\n        logger.debug(\"java got map: {}\", map);\n        return (String) map.get(\"name\");\n    }\n\n    public static List<String> getNames(List<Map<String, Object>> list) {\n        logger.debug(\"java got list: {}\", list);\n        List<String> temp = new ArrayList(list.size());\n        for (Map<String, Object> map : list) {\n            temp.add((String) map.get(\"name\"));\n        }\n        return temp;\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/java/com/intuit/karate/demo/util/SchemaUtils.java",
    "content": "package com.intuit.karate.demo.util;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.github.fge.jackson.JsonLoader;\nimport com.github.fge.jsonschema.core.report.ProcessingReport;\nimport com.github.fge.jsonschema.main.JsonSchema;\nimport com.github.fge.jsonschema.main.JsonSchemaFactory;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class SchemaUtils {\n\n    private static final Logger logger = LoggerFactory.getLogger(SchemaUtils.class);\n    \n    public static boolean isValid(String json, String schema) throws Exception {\n        JsonNode schemaNode = JsonLoader.fromString(schema);       \n        JsonSchemaFactory factory = JsonSchemaFactory.byDefault();\n        JsonSchema jsonSchema = factory.getJsonSchema(schemaNode); \n        JsonNode jsonNode = JsonLoader.fromString(json);\n        ProcessingReport report = jsonSchema.validate(jsonNode);\n        logger.debug(\"report: {}\", report);\n        return report.isSuccess();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/main/resources/application.properties",
    "content": "spring.jackson.default-property-inclusion=NON_NULL\nspring.mvc.throw-exception-if-no-handler-found=true\nspring.resources.add-mappings=false\n\nspring.datasource.generate-unique-name=false\n"
  },
  {
    "path": "karate-demo/src/main/resources/graphql-1.json",
    "content": "{\n    \"data\": {\n        \"user\": {\n            \"posts\": {\n                \"data\": [\n                    {\n                        \"id\": \"1\",\n                        \"title\": \"sunt aut facere repellat provident occaecati excepturi optio reprehenderit\"\n                    },\n                    {\n                        \"id\": \"2\",\n                        \"title\": \"qui est esse\"\n                    },\n                    {\n                        \"id\": \"3\",\n                        \"title\": \"ea molestias quasi exercitationem repellat qui ipsa sit aut\"\n                    },\n                    {\n                        \"id\": \"4\",\n                        \"title\": \"eum et est occaecati\"\n                    },\n                    {\n                        \"id\": \"5\",\n                        \"title\": \"nesciunt quas odio\"\n                    },\n                    {\n                        \"id\": \"6\",\n                        \"title\": \"dolorem eum magni eos aperiam quia\"\n                    },\n                    {\n                        \"id\": \"7\",\n                        \"title\": \"magnam facilis autem\"\n                    },\n                    {\n                        \"id\": \"8\",\n                        \"title\": \"dolorem dolore est ipsam\"\n                    },\n                    {\n                        \"id\": \"9\",\n                        \"title\": \"nesciunt iure omnis dolorem tempora et accusantium\"\n                    },\n                    {\n                        \"id\": \"10\",\n                        \"title\": \"optio molestias id quia eum\"\n                    }\n                ]\n            }\n        }\n    }\n}"
  },
  {
    "path": "karate-demo/src/main/resources/graphql-2.json",
    "content": "{\n    \"data\": {\n        \"user\": {\n            \"address\": {\n                \"geo\": {\n                    \"lng\": 81.1496,\n                    \"lat\": -37.3159\n                }\n            },\n            \"id\": \"1\",\n            \"email\": \"Sincere@april.biz\",\n            \"username\": \"Bret\"\n        }\n    }\n}"
  },
  {
    "path": "karate-demo/src/main/resources/schema.sql",
    "content": "CREATE TABLE DOGS (ID INT, NAME VARCHAR(50));"
  },
  {
    "path": "karate-demo/src/main/resources/soap-1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><soap:Body><AddResponse xmlns=\"http://tempuri.org/\"><AddResult>5</AddResult></AddResponse></soap:Body></soap:Envelope>"
  },
  {
    "path": "karate-demo/src/main/resources/soap-2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><soap:Body><AddResponse xmlns=\"http://tempuri.org/\"><AddResult>5</AddResult></AddResponse></soap:Body></soap:Envelope>"
  },
  {
    "path": "karate-demo/src/test/java/demo/DemoRunner.java",
    "content": "package demo;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.junit5.Karate;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author peter\n */\nclass DemoRunner {\n\n    @BeforeAll\n    static void beforeAll() {\n        TestBase.startServer();\n    }\n\n    @Karate.Test\n    Karate testAbort() {\n        // skip 'callSingle' in karate-config.js\n        return Karate.run(\"classpath:demo/abort\").karateEnv(\"mock\");\n    }\n\n    @Test\n    void testEncodingParallel() {\n        Results results = Runner.path(\"classpath:demo/encoding\")\n                .outputCucumberJson(true)\n                .parallel(5);\n        DemoTestParallel.generateReport(results.getReportDir());\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n    \n    @Karate.Test\n    Karate testOutline() {\n        return Karate.run(\"classpath:demo/outline/setup-outline.feature\");\n    }    \n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/DemoTestParallel.java",
    "content": "package demo;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport net.masterthought.cucumber.Configuration;\nimport net.masterthought.cucumber.ReportBuilder;\nimport org.apache.commons.io.FileUtils;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\npublic class DemoTestParallel {\n\n    @BeforeAll\n    static void beforeAll() {\n        TestBase.beforeAll();\n    }\n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:demo\")\n                .outputCucumberJson(true)\n                .karateEnv(\"demo\")\n                .parallel(5);\n        generateReport(results.getReportDir());\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n    public static void generateReport(String karateOutputPath) {\n        Collection<File> jsonFiles = FileUtils.listFiles(new File(karateOutputPath), new String[] {\"json\"}, true);\n        List<String> jsonPaths = new ArrayList<>(jsonFiles.size());\n        jsonFiles.forEach(file -> jsonPaths.add(file.getAbsolutePath()));\n        Configuration config = new Configuration(new File(\"target\"), \"demo\");\n        ReportBuilder reportBuilder = new ReportBuilder(jsonPaths, config);\n        reportBuilder.generateReports();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/DemoTestSelected.java",
    "content": "package demo;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.Results;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n * an alternative way to run selected paths, tags and even features and you can\n * dynamically determine the features that need to be executed\n *\n * @author pthomas3\n */\nclass DemoTestSelected {\n    \n    @BeforeAll\n    static void beforeAll() {\n        TestBase.beforeAll();\n    }     \n\n    @Test\n    void testSelected() {\n        List<String> tags = Arrays.asList(\"~@skipme\");\n        List<String> features = Arrays.asList(\"classpath:demo/cats\");\n        String karateOutputPath = \"target/surefire-reports\";\n        Results results = Runner.path(features)\n                .tags(tags)\n                .outputCucumberJson(true)\n                .reportDir(karateOutputPath).parallel(5);\n        DemoTestParallel.generateReport(karateOutputPath);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/TestBase.java",
    "content": "package demo;\n\nimport org.junit.jupiter.api.BeforeAll;\nimport test.ServerStart;\n\n/**\n *\n * @author pthomas3\n */\npublic abstract class TestBase {\n\n    static ServerStart server;\n\n    public static int startServer() {\n        if (server == null) { // keep spring boot side alive for all tests including package 'mock'\n            server = new ServerStart();\n            try {\n                server.start(new String[]{\"--server.port=0\"}, false);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n        System.setProperty(\"demo.server.port\", server.getPort() + \"\");\n        return server.getPort();\n    }\n\n    @BeforeAll\n    public static void beforeAll() {\n        startServer();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/abort/abort.feature",
    "content": "Feature: abort should skip (but not fail) a test\n\nScenario: you can conditionally exit a test\n    but please use sparingly\n\n  * configure abortedStepsShouldPass = true\n  * print 'before'\n  * if (true) karate.abort()\n  * print 'after'\n    "
  },
  {
    "path": "karate-demo/src/test/java/demo/binary/binary.feature",
    "content": "Feature: binary json\n\nBackground:\n    * url demoBaseUrl\n    * def Base64 = Java.type('java.util.Base64')\n\nScenario: json with byte-array\n    Given path 'echo', 'binary'\n    And def encoded = Base64.encoder.encodeToString('hello'.getBytes())\n    And request { message: 'hello', data: '#(encoded)' }\n    When method post\n    Then status 200\n    And def expected = Base64.encoder.encodeToString('world'.getBytes())\n    And match response == { message: 'world', data: '#(expected)' }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callarray/call-json-array.feature",
    "content": "Feature: calling another feature file in a loop\n\nBackground:\n    * url demoBaseUrl\n\n    * def creator = read('kitten-create.feature')\n    * def kittens = read('kittens.json')\n    * def result = call creator kittens\n\n    # the above could be written in one line as follows\n    # * def result = call read('kitten-create.feature') read('kittens.json')\n\n    * def created = $result[*].response\n\nScenario: create parent cat using kittens\n    # create mom cat\n    Given path 'cats'\n    And request { name: 'Billie', kittens: '#(created)' }\n    When method post\n    Then status 200\n    And def billie = response\n\n    # get kittens for billie\n    Given path 'cats', billie.id, 'kittens'\n    When method get\n    Then status 200\n    And match each response == { id: '#number', name: '#string' }\n    And match response[*].name contains ['LOL', 'Nyan']\n    And assert response.length == 6\n\n\n\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callarray/kitten-create.feature",
    "content": "@ignore\nFeature: re-usable feature to create a single cat\n\nScenario:\n\nGiven url demoBaseUrl\nAnd path 'cats'\nAnd request { name: '#(name)' }\nWhen method post\nThen status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callarray/kittens.json",
    "content": "[\n{ \"name\": \"Bob\" },\n{ \"name\": \"Wild\" },\n{ \"name\": \"Nyan\" },\n{ \"name\": \"Keyboard\" },\n{ \"name\": \"LOL\" },\n{ \"name\": \"Ceiling\" }\n]\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/calldynamic/call-dynamic-json.feature",
    "content": "Feature: dynamically creating json for a data-driven test\n\nBackground:\n    * url demoBaseUrl\n    * def creator = read('../callarray/kitten-create.feature')\n    * def kittensFn =\n    \"\"\"\n    function(count) {\n      var out = [];\n      for (var i = 0; i < count; i++) { \n        out.push({ name: 'Kit' + i });\n      }\n      return out;\n    }\n    \"\"\"\n\nScenario: create kittens and validate\n    * def kittens = call kittensFn 5\n    * def result1 = call creator kittens\n    * def created = $result1[*].response\n    * assert created.length == 5\n    * match each created == { id: '#number', name: '#regex Kit[0-4]' }\n    * match created[*].name contains [ 'Kit0', 'Kit1', 'Kit2', 'Kit3', 'Kit4' ]\n\n    # for each kitten created, 'get by id' and validate\n    * def result2 = call read('get-cat.feature') created\n    * match result2[*].response contains created\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/calldynamic/get-cat.feature",
    "content": "@ignore\nFeature:\n\nBackground:\n* url demoBaseUrl\n\nScenario:\nGiven path 'cats', id\nWhen method get\nThen status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callfeature/call-feature.feature",
    "content": "Feature: calling another feature file\n\nBackground:\n* url demoBaseUrl\n\nScenario: calling a feature with parameters\n    # the second parameter age is just for demo, it is ignored in 'create-cat.feature'\n    * configure report = false\n    * def result = call read('called-normal.feature') { name: 'Nyan', age: 10 }\n    # we need to 'unpack' variables out of the call result\n    * def nyan = result.response\n    * match nyan == { id: '#number', name: 'Nyan' }\n\nScenario: called feature uses '__arg', you can use variable references as the call argument\n    * def nyan = { name: 'Nyan' }\n    * def result = call read('called-arg.feature') nyan\n    * match result.response contains nyan\n\nScenario: called features will inherit parent scope\n    # this variable will be available in the called feature\n    * def name = 'Nyan'\n    # and we can call without an argument\n    * def result = call read('called-normal.feature')\n    * match result.response contains { name: '#(name)' }\n\nScenario: calling with shared scope, recommended only for 'set-up' re-usable routines\n    # this variable will be available in the called feature\n    * def name = 'Nyan'\n    # this will update variables 'globally'\n    * call read('called-normal.feature')\n    * match response contains { name: '#(name)' }\n\nScenario: create kittens and then create parent cat\n    * def kittens = call read('create-two-cats.feature')\n    * def bob = kittens.bob\n    * def wild = kittens.wild\n\n    # create mom cat\n    Given path 'cats'\n    # sometimes, enclosed javascript is more convenient than embedded expressions\n    And request ({ name: 'Billie', kittens: [bob, wild] })\n    When method post\n    Then status 200\n    And match response == read('../cats/billie-expected.json')\n    And def billie = response\n\n    # get kittens for billie\n    Given path 'cats', billie.id, 'kittens'\n    When method get\n    Then status 200\n    And match each response == { id: '#number', name: '#string' }\n    And match response contains { id: '#(wild.id)', name: 'Wild' }\n\nScenario: create kittens but calling a feature that has a scenario outline (not recommended)\n    but interesting example of a called feature updating a 'global' variable\n    \n    # init 'global' variable\n    * def kittens = []\n    * def result = call read('create-cats-outline.feature')   \n \n    Given path 'cats'\n    And request { name: 'Billie', kittens: '#(kittens)' }\n    When method post\n    Then status 200\n    And match response == { id: '#number', name: 'Billie', kittens: '#(^^kittens)' }"
  },
  {
    "path": "karate-demo/src/test/java/demo/callfeature/called-arg.feature",
    "content": "@ignore\nFeature: using __arg\n\nScenario:\nGiven url demoBaseUrl\nAnd path 'cats'\nAnd request __arg\nWhen method post\nThen status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callfeature/called-normal.feature",
    "content": "@ignore\nFeature: the name is expected to be set as a call arg\n\nScenario:\nGiven url demoBaseUrl\nAnd path 'cats'\nAnd request { name: '#(name)' }\nWhen method post\nThen status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callfeature/create-cats-outline.feature",
    "content": "@ignore\nFeature: create kittens\n\nBackground:\n* url demoBaseUrl\n\nScenario Outline: create kittens\n\n# create bob cat\nGiven path 'cats'\nAnd request { name: '<name>' }\nWhen method post\nThen status 200\nAnd kittens.push(response)\n\nExamples:\n| name |\n| Bob  |\n| Wild |\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callfeature/create-two-cats.feature",
    "content": "Feature: create kittens\n\nBackground:\n* url demoBaseUrl\n\nScenario: create kittens\n\n# create bob cat\nGiven path 'cats'\nAnd request { name: 'Bob' }\nWhen method post\nThen status 200\nAnd def bob = response\n\n# create wild cat\nGiven path 'cats'\nAnd request { name: 'Wild' }    \nWhen method post\nThen status 200\nAnd def wild = response\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callnested/call-nested.feature",
    "content": "Feature: calling a feature file which calls another feature file\n    this is not really recommended and this demo is just to test karate / reporting\n\nBackground:\n* url demoBaseUrl\n\nScenario: calling a feature with parameters\n    * print 'in main caller'\n    * call read('called1.feature')\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callnested/called1.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n    * print 'in called1'\n    * call read('called2.feature')\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callnested/called2.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n    * print 'in called2'\n    * call read('called3.feature')\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callnested/called3.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n    * print 'in called3'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/callonce/call-once.feature",
    "content": "Feature: test set-up routines that run only once, similar to how @BeforeAll works\n\nBackground:\n\n* url demoBaseUrl\n\n* table kittens\n    | name   | age |\n    | 'Bob'  | 2   |\n    | 'Wild' | 1   |\n    | 'Nyan' | 3   |\n\n# note the use of 'callonce' instead of 'call'\n* def result = callonce read('../callarray/kitten-create.feature') kittens\n\nScenario Outline: various tests on the cats created\n\n    Given path 'cats'\n    When method get\n    Then status 200\n    And match response[*].name contains '<name>'\n\n    # even though cucumber will re-run the 'Background:' section for each row in the 'Examples' below,\n    # kittens will be created by 'kitten-create.feature' only once\n\n    Examples:\n        | name |\n        | Bob  |\n        | Nyan |\n\n\nScenario: create a cat with kittens\n\n    # again, even though cucumber will re-run the 'Background:' section for each `Scenario:' in a feature file,\n    # 'kitten-create.feature' will not be called and the value of 'result' will be retrieved from cache\n    * def created = $result[*].response\n\n    Given path 'cats'\n    And request { name: 'Billie', kittens: '#(created)' }\n    When method post\n    Then status 200\n    And match response.kittens[*].name contains only ['Bob', 'Wild', 'Nyan']\n    # the ultimate data-driven test\n    And match response.kittens[*].name contains only $kittens[*].name\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/calltable/call-table.feature",
    "content": "Feature: calling another feature file in a loop\n\nBackground:\n* url demoBaseUrl\n\n* table kittens\n    | name       | age |\n    | 'Bob'      | 2   |\n    | 'Wild'     | 1   |\n    | 'Nyan'     | 3   |\n    | 'Keyboard' | 5   |\n    | 'LOL'      | 3   |\n    | 'Ceiling'  | 2   |\n\n* def result = call read('kitten-create.feature') kittens\n\n# use json-path to 'un-pack' the array of kittens created\n* def created = $result[*].response\n\n# which is not even needed for most data-driven assertions\n* match created[*].name == $kittens[*].name\n\nScenario: create parent cat using kittens\n\n# create mom cat\nGiven path 'cats'\nAnd request { name: 'Billie', kittens: '#(created)' }\nWhen method post\nThen status 200\n# the '^^' is an embeddable short-cut for 'contains only' !\nAnd match response == { id: '#number', name: 'Billie', kittens: '#(^^created)' }\n\n# get kittens for billie using the id from the previous response\nGiven path 'cats', response.id, 'kittens'\nWhen method get\nThen status 200\n\n# some demo match examples\n* match each response == { id: '#number', name: '#string' }\n* def schema = { id: '#number', name: '#string' }\n* match response == \"#[6] schema\"\n\n# pure data-driven assertion, compare with the original data\n* match response[*].name contains only $kittens[*].name\n\n* assert response.length == 6\n# prefer match instead of assert\n* match response == '#[6]'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/calltable/kitten-create.feature",
    "content": "@ignore\nFeature: re-usable feature to create a single cat\n\nScenario:\n\n# just to demo that we have two special variables __loop and __arg\n# this is a useful way to refer to any list-like variables defined in the caller\n# for example, you could load 2 different JSON arrays 'inputs' and 'outputs' \n# and refer to both in a data-driven test\n* match __arg == kittens[__loop]\n\nGiven url demoBaseUrl\nAnd path 'cats'\nAnd request { name: '#(name)' }\nWhen method post\nThen status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/cats/CatsJava.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage demo.cats;\n\nimport com.intuit.karate.demo.domain.Cat;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\npublic class CatsJava {\n\n    /*\n    {\n      name: 'Billie',\n      kittens: [\n          { id: 23, name: 'Bob' },\n          { id: 42, name: 'Wild' }\n      ]\n    }    \n     */\n    @Test\n    public void testMatchingUsingPojos() {\n\n        Cat billie = new Cat();\n        billie.setName(\"Billie\");\n        Cat bob = new Cat();\n        bob.setId(23);\n        bob.setName(\"Bob\");\n        billie.addKitten(bob);\n        Cat wild = new Cat();\n        wild.setId(42);\n        wild.setName(\"Wild\");\n        billie.addKitten(wild);\n\n        // * match billie.kittens contains { id: 42, name: 'Wild' }\n        boolean found = false;\n        if (billie.getKittens() != null) {\n            for (Cat kitten : billie.getKittens()) {\n                if (kitten.getId() == 42 && \"Wild\".equals(kitten.getName())) {\n                    found = true;\n                }\n            }\n        }\n        assertTrue(found);\n        \n        Cat test = new Cat();\n        test.setId(42);\n        test.setName(\"Wild\");\n        \n        assertTrue(hasKitten(billie, test));\n        \n    }\n\n    private static boolean hasKitten(Cat cat, Cat kitten) {\n        if (cat.getKittens() != null) {\n            for (Cat kit : cat.getKittens()) {\n                if (kit.getId() == kitten.getId()) {\n                    if (kit.getName() == null) {\n                        if (kitten.getName() == null) {\n                            return true;\n                        }\n                    } else if (kit.getName().equals(kitten.getName())) {\n                        return true;\n                    }\n                }\n            }\n        }\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/cats/billie-expected.json",
    "content": "{ \n  \"id\": \"#number\",\n  \"name\": \"Billie\",\n  \"kittens\": [\n    { \"id\": \"#(bob.id)\", \"name\": \"Bob\" },\n    { \"id\": \"#(wild.id)\", \"name\": \"Wild\" }\n  ]\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/cats/cats-put.feature",
    "content": "Feature: cats end-point\n\nBackground:\n* url demoBaseUrl\n\nScenario: create and retrieve a cat\n\n# create a new cat\nGiven path 'cats'\nAnd request { name: 'Billie' }\nWhen method post\nThen status 200\nAnd match response == { id: '#number', name: 'Billie' }\n\n* def id = response.id\n\n# get by id\nGiven path 'cats', id\nWhen method get\nThen status 200\nAnd match response == { id: '#(id)', name: 'Billie' }\n\n# update\nGiven path 'cats', id\nAnd request { id: '#(id)', name: 'Billie Edit' }\nWhen method put\nThen status 200\nAnd match response == { id: '#(id)', name: 'Billie Edit' }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/cats/cats.feature",
    "content": "Feature: cats end-point\n\nBackground:\n* url demoBaseUrl\n* configure logPrettyRequest = true\n* configure logPrettyResponse = true\n\nScenario: create and retrieve a cat\n\n# create a new cat\nGiven path 'cats'\nAnd request { name: 'Billie' }\nWhen method post\nThen status 200\nAnd match response == { id: '#number', name: 'Billie' }\n\n* def id = response.id\n\n# get by id\nGiven path 'cats', id\nWhen method get\nThen status 200\nAnd match response == { id: '#(id)', name: 'Billie' }\n\n# get all cats\nGiven path 'cats'\nWhen method get\nThen status 200\nAnd match response contains { id: '#(id)', name: 'Billie' }\n\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/cats/kittens.feature",
    "content": "Feature: cats with kittens\n\nBackground:\n* configure report = false\n* url demoBaseUrl\n\nScenario: create cat with kittens\n\n# create bob cat\nGiven path 'cats'\nAnd request { name: 'Bob' }\nWhen method post\nThen status 200\n* def bob = response\n* print 'bob:', bob\n\n# create wild cat\nGiven path 'cats'\nAnd request { name: 'Wild' }    \nWhen method post\nThen status 200\n* def wild = response\n* print 'wild:', wild\n\n* configure report = true\n\n# create mom cat\nGiven path 'cats'\n# sometimes, enclosed javascript is more convenient than embedded expressions\nAnd request ({ name: 'Billie', kittens: [bob, wild] })\nWhen method post\nThen status 200\nAnd match response == read('billie-expected.json')\n* def billie = response\n\n# get kittens for billie\nGiven path 'cats', billie.id, 'kittens'\nWhen method get\nThen status 200\nAnd match each response == { id: '#number', name: '#string' }\nAnd match response contains { id: '#(wild.id)', name: 'Wild' }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/cats/syntax-demo.feature",
    "content": "Feature: karate syntax\n\nScenario: demo of json handling\n\n* def billie =\n\"\"\"\n{\n  name: 'Billie',\n  kittens: [\n      { id: 23, name: 'Bob' },\n      { id: 42, name: 'Wild' }\n  ]\n} \n\"\"\"\n* match billie.kittens contains { id: 42, name: 'Wild' }\n\n* match billie.kittens contains { id: '#? _ > 25', name: '#string' }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/cookies/cookies.feature",
    "content": "Feature: modifying cookies\n\nBackground:\n* url demoBaseUrl\n\nScenario: no cookies\n    Given path 'search', 'cookies'\n    When method get\n    Then status 200\n    And match response == []\n\nScenario: one cookie, and it is sent automatically in the next request\n    Given path 'search', 'cookies'\n    And cookie foo = 'bar'\n    When method get\n    Then status 200\n    And match response == '#[1]'\n    And match response[0] contains { name: 'foo', value: 'bar' }\n\n    Given path 'search', 'cookies'\n    And request {}\n    When method post\n    Then status 200\n    And match response == '#[1]'\n    And match response[0] contains { name: 'foo', value: 'bar' }\n\n    * print 'cookies: ', responseCookies\n\n    # reset cookies\n    * configure cookies = null\n    Given path 'search', 'cookies'\n    When method get\n    Then status 200\n    And match response == []\n\n    # modify cookies\n    Given path 'search', 'cookies'\n    And cookie foo = 'blah'\n    And request {}\n    When method post\n    And match response == '#[1]'\n    And match response[0] contains { name: 'foo', value: 'blah' }\n\nScenario: cookie as json\n    Given path 'search', 'cookies'\n    And cookie foo = { value: 'bar' }\n    When method get\n    Then status 200\n    And match response[0] contains { name: 'foo', value: 'bar' }\n\nScenario: cookie returned has dots in the domain which violates RFC 2109\n    Given path 'search', 'cookies'\n    And cookie foo = { value: 'bar' }\n    And param domain = '.abc.com'\n    When method get\n    Then status 200\n    And match response[0] contains { name: 'foo', value: 'bar', domain: '.abc.com' }\n\nScenario: cookie returned has dots in the domain which violates RFC 2109\n    Given path 'search', 'cookies'\n    And cookie foo = { value: 'bar' }\n    And param domain = '.abc.com'\n    When method get\n    Then status 200\n    And match response[0] contains { name: 'foo', value: 'bar', domain: '.abc.com' }\n\nScenario: non-expired cookie is in response\n    * def futureDate =\n    \"\"\"\n    function() {\n      var SimpleDateFormat = Java.type('java.text.SimpleDateFormat');\n      var Calendar = Java.type('java.util.Calendar');\n      var currCalIns = Calendar.getInstance();\n      currCalIns.add(java.util.Calendar.DATE, +1);\n      var sdf = new SimpleDateFormat(\"EEE, dd-MMM-yy HH:mm:ss z\");\n      return sdf.format(currCalIns.getTime());\n    }\n    \"\"\"\n    * def date = futureDate()\n    Given path 'search', 'cookies'\n    And cookie foo = {value:'bar', expires:'#(date)', path:'/search'}\n    And param domain = '.abc.com'\n    When method get\n    Then status 200\n    And match response[0] contains { name: 'foo', value: 'bar', domain: '.abc.com' }\n\nScenario: max-age is -1, cookie should persist.\n    Given path 'search', 'cookies'\n    And cookie foo = { value: 'bar', 'max-age': '-1', path: '/search' }\n    And param domain = '.abc.com'\n    When method get\n    Then status 200\n    And match response[0] contains { name: 'foo', value: 'bar', domain: '.abc.com' }"
  },
  {
    "path": "karate-demo/src/test/java/demo/delete/delete.feature",
    "content": "Feature: test delete\n\nBackground:\n* url demoBaseUrl\n\nGiven path 'cats'\nAnd request { name: 'Billie' }\nWhen method post\nThen status 200\n* def cat = response\n\nScenario: normal delete without a body\n\nGiven path 'cats', cat.id\nWhen method delete\nThen status 200\n\nScenario: delete with a request body\n\nGiven path 'cats'\nAnd request cat\nWhen method delete\nThen status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/dogs/dogs.feature",
    "content": "Feature: dogs end-point that uses jdbc as part of the test\n\nBackground:\n* url demoBaseUrl\n\nScenario: create and retrieve a dog\n\n# create a dog\nGiven path 'dogs'\nAnd request { name: 'Scooby' }\nWhen method post\nThen status 200\nAnd match response == { id: '#number', name: 'Scooby' }\n\n* def id = response.id\n\n# get by id\nGiven path 'dogs', id\nWhen method get\nThen status 200\nAnd match response == { id: '#(id)', name: 'Scooby' }\n\n# get all dogs\nGiven path 'dogs'\nWhen method get\nThen status 200\nAnd match response contains { id: '#(id)', name: 'Scooby' }\n\n# use jdbc to validate\n* def config = { username: 'sa', password: '', url: 'jdbc:h2:mem:testdb', driverClassName: 'org.h2.Driver' }\n* def DbUtils = Java.type('com.intuit.karate.demo.util.DbUtils')\n* def db = new DbUtils(config)\n\n# since the DbUtils returns a Java List (of Map-s), it becomes normal JSON here !\n# which means that you can use the full power of Karate's 'match' syntax\n* def dogs = db.readRows('SELECT * FROM DOGS')\n* match dogs contains { ID: '#(id)', NAME: 'Scooby' }\n\n* def dog = db.readRow('SELECT * FROM DOGS D WHERE D.ID = ' + id)\n* match dog.NAME == 'Scooby'\n\n* def test = db.readValue('SELECT ID FROM DOGS D WHERE D.ID = ' + id)\n* match test == id\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/dsl/common.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n# in-line js function\n* def quack = function(){ karate.log('quack!') }\n\n# js function from file\n* def greet = read('greet.js')\n\n# feature from file\n* def login = read('login.feature')\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/dsl/dsl.feature",
    "content": "Feature: how you can define custom keywords\n\nBackground:\n* call read('common.feature')\n\nScenario: re-using code in a readable style\n  # invoke js function\n  * quack()\n\n  # call js function\n  * call greet 'John'\n\n  # call feature\n  * call login { name: 'John', type: 'admin' }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/dsl/greet.js",
    "content": "function fn(name) {\n  karate.log('hello ' + name + '!');\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/dsl/login.feature",
    "content": "@ignore\nFeature: re-usable feature\n\nScenario:\n# note that __arg is also available\n* print 'logged in user name:', name\n* print 'logged in user type:', type\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/embed/embed-pdf.js",
    "content": "function fn(o) {\n    var time = java.lang.System.currentTimeMillis();\n    var pdfPath = time + '.pdf';\n    karate.write(o, pdfPath);\n    karate.log('saved pdf to:', pdfPath);\n    var html = '<script src=\"https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.0/pdfobject.min.js\"></script>'\n        + '\\n<div id=\"divPdf\"></div>'\n        + '\\n<script>PDFObject.embed(\"' + pdfPath + '\", \"#divPdf\");</script>';\n    var htmlPath = time + '.html';\n    karate.write(html, htmlPath);\n    karate.log('saved html to:', htmlPath);\n    karate.embed('<a href=\"../' + htmlPath + '\" target=\"_blank\">(right-click and open in new tab)</a>', 'text/html');\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/embed/embed.feature",
    "content": "Feature: report embed\n\nScenario: embed html\n    * karate.embed('<h1>Hello World</h1>', 'text/html')\n\nScenario: embed images\n    * def bytes1 = read('../upload/karate-logo.jpg')\n    * karate.embed(bytes1, 'image/jpeg')\n    * def bytes2 = read('file:src/test/resources/karate-hello-world.jpg')\n    * karate.embed(bytes2, 'image/jpeg')\n\nScenario: embed pdf\n    # since the cucumber html reporting plugin does not support rendering PDF-s inline\n    # this is an example of how to create a custom HTML file that can show the PDF\n    # it does involve an extra click but there are limitations on loading PDF-s into IFRAME-s\n    * def bytes = read('../upload/test.pdf')\n    * def embedder = read('embed-pdf.js')\n    * embedder(bytes)\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/encoding/encoding.feature",
    "content": "@parallel=false\nFeature: encoding tests\n\nScenario: special characters in the url\n    Given url demoBaseUrl + '/encoding/�Ill~Formed@RequiredString!'\n    When method get\n    Then status 200\n    And match response == '�Ill~Formed@RequiredString!'\n\nScenario: special characters in the path\n    Given url demoBaseUrl\n    And path 'encoding', '�Ill~Formed@RequiredString!'\n    When method get\n    Then status 200\n    And match response == '�Ill~Formed@RequiredString!'\n\nScenario: question mark in the url\n    Given url demoBaseUrl + '/encoding/index.php%3F/api/v2'\n    And path 'hello'\n    When method get\n    Then status 200\n    And match response == 'hello'\n\nScenario: append trailing / to url\n    Given url demoBaseUrl\n    And path 'encoding', 'hello', ''\n    When method get\n    Then status 200\n    And match response == 'hello/'\n\nScenario: path escapes special characters\n    Given url demoBaseUrl\n    And path 'encoding', '\"<>#{}|\\^[]`'\n    When method get\n    Then status 200\n    And match response == '\"<>#{}|\\^[]`'\n\nScenario: leading / in path is not required\n    Given url demoBaseUrl\n    And path 'encoding', 'hello'\n    When method get\n    Then status 200\n    And match response == 'hello'\n\nScenario: manually decode before using as the url\n    * def encoded = 'encoding%2Ffoo%2Bbar'\n    * def decoded = karate.urlDecode(encoded)\n    Given url demoBaseUrl + '/' + decoded\n    When method get\n    Then status 200\n    And match response == 'foo+bar'\n\nScenario: path but with forward-slashes\n    Given url demoBaseUrl + '/encoding'\n    And path '/hello/world/123/'\n    When method get\n    Then status 200\n    And match response == 'hello/world/123'\n\nScenario: german xml\n    Given url demoBaseUrl\n    And path 'echo'\n    And request <name>Müller</name>\n    And header Content-Type = 'application/xml; charset=utf-8'\n    When method post\n    Then status 200\n    And xml response = response\n    And match response == <name>Müller</name>\n\nScenario: french json\n    Given url demoBaseUrl\n    And path 'echo'\n    And request { givenName: 'oliàèôç' }\n    And header Content-Type = 'application/json; charset=utf-8'\n    When method post\n    Then status 200\n    And match response == { givenName: 'oliàèôç' }\n\nScenario: french json ISO-8859-1\n    Given url demoBaseUrl\n    And path 'echo'\n    And request { givenName: 'oliàèôç' }\n    And header Content-Type = 'application/json; charset=ISO-8859-1'\n    When method post\n    Then status 200\n    And match response == { givenName: '#string' }\n    * def contentType = karate.prevRequest.headers['Content-Type'][0]\n    * match contentType contains 'application/json'\n    * match contentType contains 'charset=ISO-8859-1'\n\nScenario: french & german form field\n    Given url demoBaseUrl\n    And path 'echo', 'message'\n    And form field text = 'oliàèôç Müller'\n    And header Content-Type = 'application/x-www-form-urlencoded'\n    When method post\n    Then status 200\n    And match response == 'oliàèôç Müller'\n\nScenario: french & german multipart\n    Given url demoBaseUrl\n    Given path 'files'\n    And multipart file myFile = { read: '../upload/karate-logo.jpg', filename: 'karate-logo.jpg', contentType: 'image/jpg' }\n    And multipart field message = 'oliàèôç Müller'\n    And header Content-Type = 'multipart/form-data; charset=utf-8'\n    When method post\n    Then status 200\n    And match response == { id: '#uuid', filename: 'karate-logo.jpg', message: 'oliàèôç Müller', contentType: 'image/jpg' }\n\nScenario: multipart but forcing the charset to NOT be sent\n    Given url demoBaseUrl\n    Given path 'files'\n    And multipart file myFile = { read: '../upload/karate-logo.jpg', filename: 'karate-logo.jpg', contentType: 'image/jpg' }\n    And multipart field message = 'oliàèôç Müller'\n    # this ensures that \"charset\" does NOT appear in the Content-Type header\n    # which is often required as some servers don't like it\n    And configure charset = null\n    When method post\n    Then status 200\n    And match response == { id: '#uuid', filename: 'karate-logo.jpg', message: 'oliàèôç Müller', contentType: 'image/jpg' }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/error/error.feature",
    "content": "Feature: error simulation\n\nBackground:\n* url demoBaseUrl\n\nScenario: malformed json request\n    Given path 'cats'\n    And header Content-Type = 'application/json'\n    And request '{ \"name\": }'\n    When method post\n    Then status 400\n#    And match response contains { status: 400, error: 'Bad Request' }\n\nScenario: malformed json response\n    Given path 'echo'\n    And request '{ \"foo\": }'\n    When method post\n    Then status 200\n    And match response == '{ \"foo\": }'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/error/no-url.feature",
    "content": "Feature:  No URL found proper error response\n\n  Background:\n    * url demoBaseUrl\n    * configure lowerCaseResponseHeaders = true\n\n  Scenario: Invalid URL response\n    Given path 'hello'\n    When method get\n    Then status 404\n    And match header content-type contains 'application/json'\n    And match response.status == 404\n    And match response.path == '/hello'\n    And match response.error == 'Not Found'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/form/form.feature",
    "content": "Feature: test url-encoded form-field submissions\n\nScenario: should be able to over-ride the content-type\n    Given url demoBaseUrl\n    And path 'search', 'headers'\n    And form field text = 'hello'\n    And header Content-Type = 'application/json'\n    When method post\n    Then status 200\n    And def response = karate.lowerCase(response)\n    And match response['content-type'][0] contains 'application/json'\n\nScenario: normal form submit\n    Given url demoBaseUrl\n    And path 'echo'\n    And form field text = 'hello'\n    When method post\n    Then status 200\n    And match response == 'text=hello'\n    And match header Content-Type contains 'text/plain'\n\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/graphql/graphql.feature",
    "content": "Feature: test graphql end point\n\nBackground:\n* url demoBaseUrl + '/graphql'\n# this live url should work if you want to try this on your own\n# * url 'https://graphqlzero.almansi.me/api'\n\nScenario: simple graphql request\n    # note the use of text instead of def since this is NOT json    \n    Given text query =\n    \"\"\"\n    {\n      user(id: 1) {\n        posts {\n          data {\n            id\n            title\n          }\n        }\n      }\n    }\n    \"\"\"\n    And request { query: '#(query)' }\n    When method post\n    Then status 200\n\n    # pretty print the response\n    * print 'response:', response\n\n    # json-path makes it easy to focus only on the parts you are interested in\n    # which is especially useful for graph-ql as responses tend to be heavily nested\n    # '$' happens to be a JsonPath-friendly short-cut for the 'response' variable\n    * match $.data.user.posts.data[0] contains { id: '1' }    \n\n    # the '..' wildcard is useful for traversing deeply nested parts of the json\n    * def posts = $..posts.data[*]\n    * match posts[1] == { id: '2', title: 'qui est esse' }\n\nScenario: graphql query from a file and using variables\n    # here the query is read from a file\n    # note that the 'replace' keyword (not used here) can also be very useful for dynamic query building\n    Given def query = read('user-by-id.graphql')\n    And def variables = { id: 1 }\n    And request { query: '#(query)', variables: '#(variables)' }\n    When method post\n    Then status 200\n    And match response.data.user.address == { geo: { lng: 81.1496, lat: -37.3159 } }\n    And match response..username contains 'Bret'"
  },
  {
    "path": "karate-demo/src/test/java/demo/graphql/user-by-id.graphql",
    "content": "query ($id: ID!){\n  user(id: $id) {\n    id\n    username\n    email\n    address {\n      geo {\n        lat\n        lng\n      }\n    }\n  }\n}"
  },
  {
    "path": "karate-demo/src/test/java/demo/greeting/greeting.feature",
    "content": "Feature: greeting end-point\n\nBackground:\n* url demoBaseUrl\n\nScenario: get default greeting\n\n    Given path 'greeting'\n    When method get\n    Then status 200\n    And match response == { id: '#number', content: 'Hello World!' }\n\nScenario: get custom greeting\n\n    Given path 'greeting'\n    And param name = 'Billie'\n    When method get\n    Then status 200\n    And match response == { id: '#number', content: 'Hello Billie!' }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/DemoLogModifier.java",
    "content": "package demo.headers;\n\nimport com.intuit.karate.http.HttpLogModifier;\n\n/**\n *\n * @author pthomas3\n */\npublic class DemoLogModifier implements HttpLogModifier {\n    \n    public static final HttpLogModifier INSTANCE = new DemoLogModifier();\n\n    @Override\n    public boolean enableForUri(String uri) {\n        return uri.contains(\"/headers\");\n    }\n\n    @Override\n    public String uri(String uri) {\n        return uri;\n    }        \n\n    @Override\n    public String header(String header, String value) {\n        if (header.toLowerCase().contains(\"xss-protection\")) {\n            return \"***\";\n        }\n        return value;\n    }\n\n    @Override\n    public String request(String uri, String request) {\n        return request;\n    }\n\n    @Override\n    public String response(String uri, String response) {\n        // you can use a regex and find and replace if needed\n        return \"***\";\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/call-isolated-config.feature",
    "content": "Feature: a called feature will not clobber the parent context\n    if the variable assignment syntax is used\n\nBackground:\n# the shape of the next line is important. if the line starts with call (or callonce)\n# the called script will update the 'global' context here in this file.\n# but since we assigned it to a variable here on the next line - it does not\n# but we can still use the variable to get any results from the 'call' if required\n* def setup = callonce read('common.feature')\n* url demoBaseUrl\n\nScenario: fail with a 400 since the header was not set\n    Given path 'headers', setup.token\n    And param url = demoBaseUrl\n    When method get\n    # error bad request\n    Then status 400\n\nScenario: fail with a 400 since the cookie was not set\n    * configure headers = { Authorization: '#(setup.token + setup.time + demoBaseUrl)' }\n    Given path 'headers', setup.token\n    And param url = demoBaseUrl\n    When method get\n    Then status 400\n\nScenario: manually set header and cookie to pass\n    * headers { Authorization: '#(setup.token + setup.time + demoBaseUrl)' }\n    * cookie time = setup.time\n    Given path 'headers', setup.token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: alternative way of setting headers and cookies 1\n    * configure headers = { Authorization: '#(setup.token + setup.time + demoBaseUrl)' }\n    * configure cookies = { time: '#(setup.time)' }\n    Given path 'headers', setup.token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: alternative way of setting headers and cookies 2\n    * header Authorization = (setup.token + setup.time + demoBaseUrl)\n    * cookies { time: '#(setup.time)' }\n    Given path 'headers', setup.token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/call-isolated-headers.feature",
    "content": "Feature: when not using 'shared scope', 'configure headers' may need to be\n    duplicated even in 'called' features and the variables needed for the\n    headers JS routine should be returned and made available to the 'caller'\n\nBackground:\n# note how this next line has to be duplicated in 'common-multiple.feature'\n* configure headers = read('classpath:headers.js')\n\n* def setup = callonce read('common-multiple.feature')\n# and we have to ensure the 'time' and 'token' variables are set for 'headers.js' to work\n* def time = setup.time\n* def token = setup.token\n\n# a cookie is also needed in our authentication demo example\n* cookie time = setup.time\n\n# for an example of using 'shared scope' which simplifies the above\n# refer to 'call-updates-config.feature' and 'common.feature'\n\n* url demoBaseUrl\n\nScenario: all auth headers have been set via the background and 'common-multiple.feature'\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: make sure that the second scenario also works well with the 'callonce' in the background\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/call-updates-config.feature",
    "content": "Feature: a called feature can 'contribute' to variables and config \n    including headers and cookies\n\nBackground:\n# the shape of the next line is important. if the line starts with call (or callonce)\n# the called script will update the 'shared scope' here in this file\n# think of it as similar to an 'include' directive in some programming languages\n# detailed documentation: https://github.com/karatelabs/karate#shared-scope\n* callonce read('common.feature')\n* url demoBaseUrl\n\n# for an example of NOT using 'shared scope'\n# refer to 'call-isolated-headers.feature' and 'common-multiple.feature'\n\nScenario: no extra config - they have been set automatically by 'common.feature'\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: make sure that the second scenario works as well with callonce\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: here we erase the configured headers to get a 400\n    * configure headers = null\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 400\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/callonce-background-multiscenario.feature",
    "content": "Feature: ensure that a callonce and a header in the background\n    works fine even when there are multiple scenarios\n\nBackground:\n\n* def setup = callonce read('common-noheaders.feature')\n* def authToken = (setup.token + setup.time)\n* header Authorization = authToken + demoBaseUrl\n* cookie time = setup.time\n* url demoBaseUrl\n\nScenario: first scenario\n    Given path 'headers', setup.token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: second scenario\n    Given path 'headers', setup.token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/common-multiple.feature",
    "content": "@ignore\nFeature: common routine that updates the configured headers and cookies\n    and has multiple steps where the headers are set-up by the first step\n\nScenario:\nGiven url demoBaseUrl\nAnd path 'headers'\nWhen method get\nThen status 200\n\n# this is the line that has to be duplicated in the 'caller' feature\n* configure headers = read('classpath:headers.js')\n\n# and these variables need to be 'unpacked' by the 'caller' as well\n* def time = responseCookies.time.value\n* def token = response\n\n# all because we are not using 'shared scope'\n# and this second API call depends on the 'headers.js' configured above\nGiven path 'headers', token\nAnd param url = demoBaseUrl\nWhen method get\nThen status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/common-noheaders.feature",
    "content": "@ignore\nFeature: common routine that does not update headers\n    and the caller is expected to use what is returned\n\nScenario:\nGiven url demoBaseUrl\nAnd path 'headers'\nWhen method get\nThen status 200\n\n* def time = responseCookies.time.value\n* def token = response\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/common.feature",
    "content": "@ignore\nFeature: common routine that updates the configured headers and cookies\n\nScenario:\nGiven url demoBaseUrl\nAnd path 'headers'\nWhen method get\nThen status 200\n\n* def time = responseCookies.time.value\n* def token = response\n# cookies are auto-configured, i.e. they 'persist' for subsequent HTTP calls\n\n# if you are using 'shared scope': https://github.com/karatelabs/karate#shared-scope\n# this next line will update the global scope, which is the recommended approach for re-usable sign-in / auth flows\n* configure headers = read('classpath:headers.js')\n\n# if you have more HTTP / API calls as part of this 're-usable' sign-in flow\n# they can be made here, and they will use the 'headers.js' configured above\n\n# if you are NOT using 'shared scope', you will need to duplicate the\n# 'configure headers' line in your 'caller' feature for your main flow to work\n# and ensure that the 'time' and 'token' variables are returned from here \n# and set (using 'def') in the 'caller' feature, including cookies if needed\n\n# refer to 'call-isolated-headers.feature' and 'common-multiple.feature'\n# for an example of NOT using 'shared scope'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/content-type.feature",
    "content": "Feature: exotic content-type situations\n\nBackground:\n* url demoBaseUrl\n* configure lowerCaseResponseHeaders = true\n\nScenario: json post with charset   \n    Given path 'search', 'headers'\n    And header Content-Type = 'application/json; charset=utf-8'\n    And request { foo: 'bar' }\n    When method post\n    Then status 200    \n    And match header content-type contains 'application/json'\n    And def response = karate.lowerCase(response)\n    And def temp = response['content-type'][0]\n    And match temp contains 'application/json'\n\nScenario: form post with charset\n    Given path 'search', 'headers'\n    And header Content-Type = 'application/x-www-form-urlencoded; charset=utf-8'\n    And form field foo = 'bar'\n    When method post\n    Then status 200\n    And def response = karate.lowerCase(response)\n    And def temp = response['content-type'][0]\n    And match temp contains 'application/x-www-form-urlencoded'\n\nScenario: json post with with charset and version\n    Given path 'search', 'headers'\n    And header Content-Type = 'application/json; charset=utf-8; version=1.2.3'\n    And request { foo: 'bar' }\n    When method post\n    Then status 200\n    And def response = karate.lowerCase(response)\n    And def temp = response['content-type'][0]\n    And match temp contains 'application/json;'\n    And match temp contains 'charset=utf-8'\n    And match temp contains 'version=1.2.3'\n\nScenario: json post with with unusual content-type and parameter\n    Given path 'search', 'headers'\n    And header Content-Type = 'application/vnd.app.test+json;ton-version=1'\n    And request { foo: 'bar' }\n    When method post\n    Then status 200\n    And def response = karate.lowerCase(response)\n    And def temp = response['content-type'][0]\n    And match temp contains 'application/vnd.app.test+json;'\n    And match temp contains 'charset=utf-8'\n    And match temp contains 'ton-version=1'\n\nScenario: json post with with unusual content-type and configure-headers\n    * configure headers = function(){ return {'Content-Type': 'application/vnd.app.test+json;ton-version=1'} }\n    Given path 'search', 'headers'\n    And request { foo: 'bar' }\n    When method post\n    Then status 200\n    And def response = karate.lowerCase(response)\n    And def temp = response['content-type'][0]\n    And match temp contains 'application/vnd.app.test+json;'\n    And match temp contains 'charset=utf-8'\n    And match temp contains 'ton-version=1'\n\nScenario: empty string as content-type\n    Given path 'search', 'headers'\n    And header Content-Type = ''\n    And request { foo: 'bar' }\n    When method post\n    Then status 200\n    And def temp = response['content-type'][0]\n    And match temp == ''\n\nScenario: json post with header but NO charset   \n    Given path 'search', 'headers'\n    And configure charset = null\n    And header Content-Type = 'application/json'\n    And request { foo: 'bar' }\n    When method post\n    Then status 200\n    And def response = karate.lowerCase(response)\n    And def temp = response['content-type'][0]\n    And match temp contains 'application/json'\n    And match temp !contains 'charset=utf-8'\n\nScenario: json post with default header but NO charset   \n    Given path 'search', 'headers'\n    And configure charset = null\n    And request { foo: 'bar' }\n    When method post\n    Then status 200\n    And def response = karate.lowerCase(response)\n    And def temp = response['content-type'][0]\n    And match temp contains 'application/json'\n    And match temp !contains 'charset=utf-8'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/headers-alt.js",
    "content": "function fn() {\n  var uuid = java.util.UUID.randomUUID() + '';\n  karate.set('prevUuid', uuid);\n  return { token: uuid };\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/headers-background-configure.feature",
    "content": "Feature: headers configured in the background apply for all scenarios\n\nBackground: \n\nGiven url demoBaseUrl\nAnd path 'headers'\nWhen method get\nThen status 200\nAnd def token = response\nAnd def time = responseCookies.time.value\n* configure headers = read('classpath:headers.js')\n\nScenario: first\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: second\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/headers-background.feature",
    "content": "Feature: header set in the background apply for all scenarios\n\nBackground: \n\nGiven url demoBaseUrl\nAnd path 'headers'\nWhen method get\nThen status 200\nAnd def token = response\nAnd def time = responseCookies.time.value\n* header Authorization = token + time + demoBaseUrl\n\nScenario: first\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: second\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/headers-form-get.feature",
    "content": "Feature: headers with a form field and method get\n\nScenario:\n\nGiven url demoBaseUrl\nAnd path 'search', 'headers'\nAnd header Authorization = 'foo'\nAnd form field q = 'bar'\nWhen method get\nThen status 200\nAnd def response = karate.lowerCase(response)\nAnd match response contains { authorization: ['foo'] }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/headers-masking.feature",
    "content": "Feature: how to mask headers or payload if needed, see Java code in demo.headers.DemoLogModifier\n\nBackground:\n    # if this was in karate-config.js, it would apply \"globally\"\n    * def LM = Java.type('demo.headers.DemoLogModifier')\n    * configure logModifier = new LM()\n\n    Given url demoBaseUrl\n    And path 'headers'\n    When method get\n    Then status 200\n    And def token = response\n    And def time = responseCookies.time.value \n\nScenario: set header\n    * header Authorization = token + time + demoBaseUrl\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/headers-override.feature",
    "content": "Feature: scenarios can override headers set in the background\n\nBackground: \n\nGiven url demoBaseUrl\nAnd path 'headers'\nWhen method get\nThen status 200\nAnd def token = response\nAnd def time = responseCookies.time.value\n* header Authorization = 'invalid'\n\nScenario:\n\n* header Authorization = token + time + demoBaseUrl\n\nGiven path 'headers', token\nAnd param url = demoBaseUrl\nWhen method get\nThen status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/headers-single.feature",
    "content": "Feature: the karate-config.js can perform 'singleton' style one-time init of auth\n    instead of re-doing it for every feature in a test-suite, even for multi-threaded / parallel runs\n\nBackground:\n* url demoBaseUrl\n\n# refer to karate-config.js to see how these were initialized\n* def time = authInfo.authTime\n* def token = authInfo.authToken\n\n# we now have enough information to set up auth / headers for all scenarios\n* cookie time = time\n* configure headers = read('classpath:headers.js')\n\nScenario: no extra config - they have been set automatically by the background \n    and the 'callSingle' in karate-config.js\n\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/headers-uuid.feature",
    "content": "Feature: show how a headers js routine can pass variables to the calling\n    or currently executing feature\n\nScenario: check that the header sent was as expected\n    * configure headers = read('headers-alt.js')\n    Given url demoBaseUrl\n    And path 'search', 'headers'\n    When method get\n    Then status 200\n    And match prevUuid == response.token[0]\n  \n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/headers.feature",
    "content": "Feature: multiple header management approaches that demonstrate how after\n    an initial 'sign-in' that retrieves some secure tokens, every subsequent\n    request can have the 'Authorization' header set in a way that the server expects\n\nBackground:\n    \n    # the call below performs the function of a sign-in\n    # a string token is returned, which needs to be combined with a cookie and the url\n    # to form the 'Authorization' header. calls to /headers/{token} will fail unless\n    # the Authorization header is set correctly.\n\n    Given url demoBaseUrl\n    And path 'headers'\n    When method get\n    Then status 200\n    And def token = response\n    And def time = responseCookies.time.value\n\n    # the above flow will typically need to be re-used by multiple features\n    # refer to 'call-updates-config.feature' for the recommended approach\n\n    # note that the responseCookies will be auto-sent as cookies for all future requests\n    # even the responseCookies can be validated using 'match'\n    And match responseCookies contains { time: '#notnull' }\n    # example of how to check that a cookie does NOT exist\n    And match responseCookies !contains { blah: '#notnull' }\n   \nScenario: configure function\n    this is the approach that most projects would use, especially if some header needs\n    to be dynamic for each request. for e.g. see how a 'request_id' header is set in 'headers.js'\n    for an example of how the steps in the 'Background:' can be moved into a re-usable feature\n    refer to 'call-updates-config.feature' and 'common.feature'\n\n    * configure headers = read('classpath:headers.js')\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: configure json\n    * configure headers = { Authorization: '#(token + time + demoBaseUrl)' }\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: set header\n    * header Authorization = token + time + demoBaseUrl\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: multi-value headers\n    * header Authorization = 'dummy', token + time + demoBaseUrl\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: set headers using json\n    * headers { Authorization: '#(token + time + demoBaseUrl)' }\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: set multi-value headers using json\n    * headers { Authorization: ['dummy', '#(token + time + demoBaseUrl)'] }\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n\nScenario: set multi-value headers using function call\n    # this is a test case for an edge case where commas in json confuse cucumber\n    * def fun = function(arg){ return [arg.first, arg.second] }\n    * header Authorization = call fun { first: 'dummy', second: '#(token + time + demoBaseUrl)' }\n    Given path 'headers', token\n    And param url = demoBaseUrl\n    When method get\n    Then status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/headers/null-header.feature",
    "content": "Feature: test sending a null with the headers keyword\n\nScenario:\n\n* def blah = null\n\nGiven url demoBaseUrl\nAnd path 'search', 'headers'\nAnd headers { foo: 'bar', blah: '#(blah)' }\nWhen method get\nThen status 200\nAnd match response contains { foo: ['bar'] }\nAnd match response !contains { blah: '#notnull' }\nAnd match response contains { blah: '#notpresent' }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/hooks/after-feature.feature",
    "content": "@ignore\nFeature: to demo that features can be used in an 'afterFeature' hook\n\nScenario:\n* print '*** in \"after-feature.feature ***\"'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/hooks/after-scenario.feature",
    "content": "@ignore\nFeature: to demo that features can be used in an 'afterScenario' hook\n\nScenario:\n* print 'in \"after-scenario.feature\", caller:', caller\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/hooks/called.feature",
    "content": "@ignore\nFeature:\n\nBackground:\n# 'afterScenario', 'afterScenarioOutline', and 'afterFeature' are NOT supported when a feature is called\n# so this will have no effect, UNLESS this feature is run directly\n* configure afterScenario = function(){ karate.log('end called scenario') }\n\nScenario: called\n* print 'in called'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/hooks/hooks.feature",
    "content": "Feature: demo karate's equivalent of before and after hooks\n    note that 'afterScenario' / 'afterScenarioOutline' / 'afterFeature' if set up using 'configure'\n    is not supported within features invoked using the 'call' or 'callonce' keywords \n\nBackground:\n# anything here is run before every scenario (and every Example row for Scenario Outline-s)\n# use the 'callonce' keyword if you want \"once only\" control: https://github.com/karatelabs/karate#callonce\n# any variable here is \"global\" to all scenarios\n* def foo = 'hello'\n\n# for custom code to run after every scenario / feature: https://github.com/karatelabs/karate#configure\n# note that these can be complex JS functions that you can read from separate files and re-use in multiple features\n# and you can give control to another (re-usable) feature via 'karate.call' if needed\n\n# the JSON returned from 'karate.scenario' and 'karate.feature' is explained here: \n# https://github.com/karatelabs/karate/wiki/1.0-upgrade-guide#karateinfo-deprecated\n\n* configure afterScenario = \n\"\"\"\nfunction(){\n  karate.log('after scenario:', karate.scenario.name);\n  karate.call('after-scenario.feature', { caller: karate.feature.fileName });\n}\n\"\"\"\n\n# note that afterFeature will not work with the JUnit runners\n# use the Runner API instead: https://github.com/karatelabs/karate#parallel-execution\n\n* configure afterFeature = function(){ karate.call('after-feature.feature'); }\n\n# Only runs at the end of a scenario outline after all examples have been run\n# This hook will be called after the last scenario in the outline is executed\n# It will also run after any configured 'afterScenario's for that outline\n# NOTE if using parallel, last Scenario executed may not be the last example in the outline\n* configure afterScenarioOutline = \n\"\"\"\nfunction(){\n  karate.log('after scenario outline:', karate.scenarioOutline.name);\n}\n\"\"\"\n\nScenario: first\n    * print foo\n\nScenario: second\n    * print foo\n\nScenario Outline:\n    * print <bar>\n\n    Examples:\n    | bar     |\n    | foo + 1 |\n    | foo + 2 |\n\nScenario: 'after' hooks do not apply to called features\n    # 'afterScenario' and 'afterFeature' only work in the \"top-level\" feature\n    #  and are NOT supported in 'called' features\n    * def result = call read('called.feature')\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/info/info.feature",
    "content": "Feature: runtime metadata\n    such as the feature file name and scenario name\n\nScenario: first scenario\n* def info = karate.info\n* print 'info:', info\n* match info contains { scenarioName: 'first scenario', featureFileName: 'info.feature' }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/java/JavaApiTest.java",
    "content": "package demo.java;\n\nimport com.intuit.karate.Runner;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass JavaApiTest {\n    \n    @BeforeAll\n    static void beforeAll() {\n        // skip 'callSingle' in karate-config.js\n        System.setProperty(\"karate.env\", \"mock\"); \n    }    \n    \n    @Test\n    void testCallingFeatureFromJava() {\n        Map<String, Object> args = new HashMap();\n        args.put(\"name\", \"World\");\n        Map<String, Object> result = Runner.runFeature(getClass(), \"from-java.feature\", args, true);\n        assertEquals(\"Hello World\", result.get(\"greeting\"));\n    }\n    \n    @Test\n    void testCallingClasspathFeatureFromJava() {\n        Map<String, Object> args = new HashMap();\n        args.put(\"name\", \"World\");\n        Map<String, Object> result = Runner.runFeature(\"classpath:demo/java/from-java.feature\", args, true);\n        assertEquals(\"Hello World\", result.get(\"greeting\"));\n    }    \n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/java/cats-java.feature",
    "content": "Feature: cats end-point\n\nBackground:\n* url demoBaseUrl\n* def JavaDemo = Java.type('com.intuit.karate.demo.util.JavaDemo')\n\nScenario: pass json to java\n\nGiven path 'cats'\nAnd request { name: 'Sillie' }\nWhen method post\nThen status 200\n\n* def name = JavaDemo.getName(response)\n* assert name == 'Sillie'\n\nGiven path 'cats'\nWhen method get\nThen status 200\n\n# here the response is a json array\n* def names = JavaDemo.getNames(response)\n* match names contains ['Sillie']\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/java/from-java.feature",
    "content": "@ignore\nFeature: demo of calling a feature via the java api\n\nScenario:\n\n* def greeting = 'Hello ' + name\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/jwt/jwt.feature",
    "content": "@ignore\nFeature: jwt test\n\nBackground:\n* url demoBaseUrl\n* def parseJwtPayload =\n  \"\"\"\n  function(token) {\n      var base64Url = token.split('.')[1];\n      var base64Str = base64Url.replace(/-/g, '+').replace(/_/g, '/');\n      var Base64 = Java.type('java.util.Base64');\n      var decoded = Base64.getDecoder().decode(base64Str);\n      var String = Java.type('java.lang.String');\n      return new String(decoded);\n  }\n  \"\"\"\n\nScenario: jwt flow\n    Given path 'echo', 'jwt'\n    And request { username: 'john', password: 'secret' }\n    When method POST\n    Then status 200\n    And json accessToken = parseJwtPayload(response)\n    And match accessToken == { user: 'test@example.com', role: 'editor', exp: '#number', iss: 'klingman' }\n\n    Given path 'echo', 'jwt', 'resource'\n    And header Authorization = 'Bearer ' + accessToken\n    When method get\n    Then status 200\n    And match response == 'success'\n\nScenario: access denied\n    Given path 'echo', 'jwt'\n    And request { username: 'john', password: 'wrong' }\n    When method POST\n    Then status 403\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/oauth/Signer.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage demo.oauth;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport static org.apache.commons.codec.digest.DigestUtils.md5Hex;\nimport static org.apache.commons.codec.digest.DigestUtils.sha256Hex;\n\n/**\n *\n * @author pthomas3\n */\npublic class Signer {\n    \npublic static void sign(String token, Map<String, String> params) {\n        List<String> list = new ArrayList();\n        String tokenClientSlat = \"\";\n        for (Map.Entry<String, String> entry : params.entrySet()) {\n            String key = entry.getKey();\n            if (key.equals(\"token_client_salt\")) {\n                tokenClientSlat = entry.getValue();\n            }\n            String paramString = key + \"=\" + entry.getValue();\n            list.add(paramString);\n        }\n        Collections.sort(list);\n        StringBuilder sb = new StringBuilder();\n        for (String s : list) {\n            sb.append(s);\n        }\n        sb.append(token);\n        String sig = md5Hex(sb.toString());\n        String tokenSig = sha256Hex(sig + tokenClientSlat);\n        params.put(\"sig\", sig);\n        params.put(\"__NStokensig\", tokenSig);\n    }    \n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/oauth/oauth1.feature",
    "content": "@ignore\nFeature: oauth1 example\n    which is just example code as we couldn't find an online sandbox to test\n    contributions welcome !\n\nBackground:\n    * url demoBaseUrl\n\nScenario:\n    * def Signer = Java.type('demo.oauth.Signer')    \n    * def params =\n    \"\"\"\n    { \n      'userId': '399645532', \n      'os':'android', \n      'client_key': '3c2cd3f3',\n      'token': '141a649988c946ae9b5356049c316c5d-838424771',\n      'token_client_salt': 'd340a54c43d5642e21289f7ede858995'\n    }\n    \"\"\"\n    * Signer.sign('382700b563f4', params)\n    * path 'echo'\n    * form fields params\n    * method post\n    * status 200\n    \n"
  },
  {
    "path": "karate-demo/src/test/java/demo/oauth/oauth2.feature",
    "content": "@ignore\nFeature: oauth 2 test using\n    http://brentertainment.com/oauth2\n\nBackground:\n* url 'http://brentertainment.com/oauth2/lockdin'\n\nScenario: oauth 2 flow\n\n* path 'token'\n* form field grant_type = 'password'\n* form field client_id = 'demoapp'\n* form field client_secret = 'demopass'\n* form field username = 'demouser'\n* form field password = 'testpass'\n* method post\n* status 200\n\n* def accessToken = response.access_token\n\n* path 'resource'\n* header Authorization = 'Bearer ' + accessToken\n# * param access_token = accessToken\n* method get\n* status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/outline/dynamic-csv.feature",
    "content": "Feature: scenario outline using a dynamic table\n    from a csv file\n\nScenario Outline: cat name: <name>\n    Given url demoBaseUrl\n    And path 'cats'\n    And request { name: '#(name)', age: '#(age)' }\n    When method post\n    Then status 200\n    And match response == { id: '#number', name: '#(name)' }\n\n    # the single cell can be any valid karate expression\n    # and even reference a variable defined in the Background\n    Examples:\n    | read('kittens.csv') |\n    "
  },
  {
    "path": "karate-demo/src/test/java/demo/outline/dynamic-generator.feature",
    "content": "Feature: scenario outline using a dynamic generator function\n\n@setup\nScenario:\n    * def generator = function(i){ if (i == 10) return null; return { name: 'DynaCat' + i, age: i } }\n\nScenario Outline: cat name: <name>\n    Given url demoBaseUrl\n    And path 'cats'\n    And request { name: '#(name)', age: '#(age)' }\n    When method post\n    Then status 200\n    And match response == { id: '#number', name: '#(name)' }\n\n    Examples:\n    | karate.setup().generator |\n    "
  },
  {
    "path": "karate-demo/src/test/java/demo/outline/dynamic.feature",
    "content": "Feature: scenario outline using a dynamic table\n\n@setup\nScenario:\n    * def kittens = read('../callarray/kittens.json')\n\nScenario Outline: cat name: <name>\n    Given url demoBaseUrl\n    And path 'cats'\n    And request { name: '#(name)' }\n    When method post\n    Then status 200\n    And match response == { id: '#number', name: '#(name)' }\n\n    # the single cell can be any valid karate expression\n    # and even reference a variable defined in the Background\n    Examples:\n    | karate.setup().kittens |\n    "
  },
  {
    "path": "karate-demo/src/test/java/demo/outline/examples.feature",
    "content": "Feature: patterns for using cucumber scenario-outline and examples with karate\n\nBackground:\n    * url demoBaseUrl\n\nScenario Outline: name: <name> and country: <country>\n    avoid empty cells and use null in 'Examples' to work better with karate (or use type hints, see below)\n        and also consider stuffing whole chunks of json into cells\n\n    Given path 'search'\n    And params { name: <name>, country: <country>, active: <active>, limit: <limit> }\n    When method get\n    Then status 200\n    And match response == <expected>\n\n    Examples:\n        | name   | country   | active | limit | expected                                                                         |\n        | 'foo'  | 'IN'      | true   |     1 | { name: '#notnull', country: '#notnull', active: '#notnull', limit: '#notnull' } |\n        | 'bar'  | null      | null   |     5 | { name: '#notnull', limit: '#notnull' }                                          |\n        | 'baz'  | 'JP'      | null   |  null | { name: '#notnull', country: '#notnull' }                                        |\n        | null   | 'US'      | null   |     3 | { country: '#notnull', limit: '#notnull' }                                       |\n        | null   | null      | false  |  null | { active: '#notnull' }                                                           |\n\nScenario Outline: name: <name> and country: <country>\n    above example simplified / improved using type-hints and karate's enhancements to example row handling\n\n    Given path 'search'\n    # since the next line is standard JSON + embedded-expressions, it can be easily extracted into a re-usable file\n    And params { name: '#(name)', country: '#(country)', active: '#(active)', limit: '#(limit)' }\n    When method get\n    Then status 200\n    And match response == expected\n\n    Examples:\n        | name | country | active! | limit! | expected!                                                                        |\n        | foo  | IN      | true    |      1 | { name: '#notnull', country: '#notnull', active: '#notnull', limit: '#notnull' } |\n        | bar  |         |         |      5 | { name: '#notnull', limit: '#notnull' }                                          |\n        | baz  | JP      |         |        | { name: '#notnull', country: '#notnull' }                                        |\n        |      | US      |         |      3 | { country: '#notnull', limit: '#notnull' }                                       |\n        |      |         | false   |        | { active: '#notnull' }                                                           |\n\nScenario Outline: expressions - index: <index> and country: <country>\n    combine 'Examples' embedded expressions and karate expression evaluation\n\n    * def names = { first: 'foo', second: 'bar', third: 'baz', fourth: null, fifth: null }\n    * def expected =\n    \"\"\"\n    [\n        { name: '#notnull', country: '#notnull', active: '#notnull', limit: '#notnull' },\n        { name: '#notnull', limit: '#notnull' },\n        { name: '#notnull', country: '#notnull' },\n        { country: '#notnull', limit: '#notnull' },\n        { active: '#notnull' }\n    ]\n    \"\"\"    \n\n    Given path 'search'\n    # note how the Examples column for 'name' works here\n    And params { name: '#(<name>)', country: <country>, active: <active>, limit: <limit> }\n    When method get\n    Then status 200\n    And match response == expected[<index>]\n\n    Examples:\n        | name         | country   | active | limit | index |\n        | names.first  | 'IN'      | true   |     1 | 0     |\n        | names.second | null      | null   |     5 | 1     |\n        | names.third  | 'JP'      | null   |  null | 2     |\n        | names.fourth | 'US'      | null   |     3 | 3     |\n        | names.fifth  | null      | false  |  null | 4     |\n        \nScenario Outline: expressions - index: <index> and country: <country>\n    the above outline re-written to use karate's enhanced row-handling\n\n    * def names = { first: 'foo', second: 'bar', third: 'baz', fourth: null, fifth: null }\n    * def expected =\n    \"\"\"\n    [\n        { name: '#notnull', country: '#notnull', active: '#notnull', limit: '#notnull' },\n        { name: '#notnull', limit: '#notnull' },\n        { name: '#notnull', country: '#notnull' },\n        { country: '#notnull', limit: '#notnull' },\n        { country: '#notnull', active: '#notnull' }\n    ]\n    \"\"\"  \n\n    Given path 'search'\n    # both mechanisms for data substitution are available at the same time\n    And params { name: '#(<name>)', country: '#(country)', active: '#(active)', limit: '#(limit)' }\n    When method get\n    Then status 200\n    # note how the row index is a magic variable\n    And match response == expected[__num]\n\n    # if you really wanted an empty or blank string as row-data, just use type-hints combined with quoted strings\n    Examples:\n        | name         | country! | active! | limit! |\n        | names.first  | 'IN'     | true    |      1 |\n        | names.second |          |         |      5 |\n        | names.third  | 'JP'     |         |        |\n        | names.fourth | 'US'     |         |      3 |\n        | names.fifth  | ''       | false   |        |\n "
  },
  {
    "path": "karate-demo/src/test/java/demo/outline/kittens.csv",
    "content": "name,age\r\nBob,10\r\nWild,5\r\nNyan,3"
  },
  {
    "path": "karate-demo/src/test/java/demo/outline/setup-outline.feature",
    "content": "Feature:\n\nBackground:\n* print 'in background'\n\n@setup\nScenario:\n* def data = [{a: 1}, {a: 2}]\n\nScenario Outline:\n* print __row\n\nExamples:\n| karate.setup().data |\n\n\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/params/params.feature",
    "content": "Feature: parameters (also see the search demo)\n\nScenario Outline: first scenario as an outline \n    (to prevent a particular bug from re-appearing)\n    \n    Given url demoBaseUrl\n    And path 'echo'\n    And param p = <param>\n    When method get\n    Then status 200\n    And match response == { p: <value> }\n\nExamples:\n    | param      | value      |\n    | 'a'        | ['a']      |\n    | ['a', 'b'] | ['a', 'b'] |\n\nScenario: comma delimited param value (normal)\n    Given url demoBaseUrl\n    And path 'echo'\n    And param fieldList = 'name,id,date_created,date_modified,created_id,modified_id'\n    When method get\n    Then status 200\n    And match response.fieldList[0] == 'name,id,date_created,date_modified,created_id,modified_id'\n\nScenario: comma delimited param value (in url)\n    Given url demoBaseUrl + '/echo?fieldList=name,id,date_created,date_modified,created_id,modified_id'\n    When method get\n    Then status 200\n    And match response.fieldList[0] == 'name,id,date_created,date_modified,created_id,modified_id'\n\nScenario: parameter which is an array and dynamic\n    * def fun =\n    \"\"\"\n    function(){\n        var temp = [];\n        for(var i = 0; i < 5; i++) {          \n            temp.push('r');\n        }\n        return temp;\n    }\n    \"\"\"\n    * json array = fun()\n    Given url demoBaseUrl\n    And path 'echo'\n    And params { pl: '#(array)' }\n    When method get\n    Then status 200\n    And match response == { pl: ['r', 'r', 'r', 'r', 'r'] }\n    "
  },
  {
    "path": "karate-demo/src/test/java/demo/polling/get.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* configure report = false\nGiven url demoBaseUrl\nAnd path 'greeting'\nWhen method get\nThen status 200\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/polling/polling.feature",
    "content": "Feature: demo of how to poll until a certain condition is met\n\nBackground:\n    # first we re-set the counter to avoid collisions with other tests\n    Given url demoBaseUrl\n    And path 'greeting', 'reset'\n    When method get\n    Then status 200\n    And match response == { counter: 0 }\n\n    # you may prefer to read the javascript from a file instead of having it in-line\n    * def waitUntil = \n    \"\"\"\n    function(x) {\n      while (true) {\n        var result = karate.call('get.feature');\n        var greeting = result.response;\n        karate.log('poll response', greeting); //<\n        if (greeting.id >= x) {\n          karate.log('condition satisfied, exiting');\n          return;\n        }\n        karate.log('sleeping');\n        // uncomment / modify the sleep time as per your wish\n        // java.lang.Thread.sleep(1000);\n      }\n    }\n    \"\"\"\n\nScenario: get greeting and keep polling until id is n + 5\n    using javascript, and a second feature\n    * def result = call read('get.feature')\n    * def current = result.response\n    * print 'current: ' + current\n    * def target = current.id + 5\n    * call waitUntil target\n\nScenario: using the karate retry syntax\n    # if not configured, 'retry' defaults to\n    # { count: 3, interval: 3000 } (milliseconds)\n    * configure retry = { count: 5, interval: 0 }\n    Given url demoBaseUrl\n    And path 'greeting'\n    And retry until responseStatus == 200 && response.id > 3\n    When method get\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/read/read-files.feature",
    "content": "Feature: demo reading files and using in a test\n\nBackground:\n* url demoBaseUrl\n# a POST to /echo will simply echo the request payload\n* path 'echo'\n\nScenario: using json from a file\n    * table employees\n        | firstName | lastName |\n        | 'John'    | 'Smith'  |\n        | 'Jane'    | 'Doe'    |\n    Given request ({ employees: employees })\n    When method post\n    Then status 200\n    And match $ == read('sample.json')\n\nScenario: using xml from a file\n    * set payload /employees\n        | path                  | value    |\n        | employee[1]/firstName | 'John'   |\n        | employee[1]/lastName  | 'Smith'  |\n        | employee[2]/firstName | 'Jane'   |\n        | employee[2]/lastName  | 'Doe'    |        \n\n    Given request payload\n    When method post\n    Then status 200\n    And xml response = response\n    And match response == read('sample.xml')\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/read/sample.json",
    "content": "{\n  \"employees\": [\n    {\n      \"firstName\": \"John\",\n      \"lastName\": \"Smith\"\n    },\n    {\n      \"firstName\": \"Jane\",\n      \"lastName\": \"Doe\"\n    }\n  ]\n}"
  },
  {
    "path": "karate-demo/src/test/java/demo/read/sample.xml",
    "content": "<employees>\n    <employee>\n        <firstName>John</firstName>\n        <lastName>Smith</lastName>\n    </employee>\n    <employee>\n        <firstName>Jane</firstName>\n        <lastName>Doe</lastName>\n    </employee>\n</employees>"
  },
  {
    "path": "karate-demo/src/test/java/demo/redirect/redirect.feature",
    "content": "Feature: disable redirects in order to assert against the location header\n\nBackground:\n* url demoBaseUrl\n\nScenario: get redirects are followed by default\n    Given path 'redirect'\n    And param foo = 'bar'\n    When method get\n    Then status 200\n    And match response == { foo: ['bar'] }\n\nScenario: get redirects can be disabled\n    * configure followRedirects = false\n    Given path 'redirect'\n    When method get\n    Then status 302\n    And match header Location == demoBaseUrl + '/search'\n\n    * def location = responseHeaders['Location'][0]\n\n    Given url location\n    And param foo = 'bar'\n    When method get\n    Then status 200\n    And match response == { foo: ['bar'] }\n\nScenario: post redirects are followed by default\n    Given path 'redirect'\n    And param foo = 'bar'\n    And request {}\n    When method post\n    Then status 200\n    And match response == { foo: ['bar'] }\n\nScenario: post redirects can be disabled\n    * configure followRedirects = false\n    Given path 'redirect'\n    And request {}\n    When method post\n    Then status 302\n    And match header Location == demoBaseUrl + '/search'\n\n    * def location = responseHeaders['Location'][0]\n\n    Given url location\n    And param foo = 'bar'\n    When method get\n    Then status 200\n    And match response == { foo: ['bar'] }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/request/request.feature",
    "content": "Feature: test accessing the 'actual' request made\n\nBackground:\n* url demoBaseUrl\n\nScenario: create cat\n    Given path 'cats'\n    And param foo = 'bar'\n    And request { name: 'Billie' }\n    When method post\n    Then status 200\n    And match response == { id: '#number', name: 'Billie' }\n\n    * def temp = karate.prevRequest\n    * def requestMethod = temp.method\n    * match requestMethod == 'POST'\n    * def requestHeaders = temp.headers\n    * def contentType = temp.headers['Content-Type'][0]\n    * match contentType contains 'application/json'\n    * match contentType contains 'charset=UTF-8'\n    * def requestUri = temp.url\n    * match requestUri == demoBaseUrl + '/cats?foo=bar'\n    # this will be of java type byte[]\n    * def requestBody = temp.body\n    # convert byte array to  string\n    * def requestString = new java.lang.String(requestBody, 'utf-8')\n    * match requestString == '{\"name\":\"Billie\"}'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/schema/products-schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"title\": \"Product set\",\n  \"type\": \"array\",\n  \"items\": {\n    \"title\": \"Product\",\n    \"type\": \"object\",\n    \"properties\": {\n      \"id\": {\n        \"description\": \"The unique identifier for a product\",\n        \"type\": \"number\"\n      },\n      \"name\": {\n        \"type\": \"string\"\n      },\n      \"price\": {\n        \"type\": \"number\",\n        \"minimum\": 0,\n        \"exclusiveMinimum\": true\n      },\n      \"tags\": {\n        \"type\": \"array\",\n        \"items\": {\n          \"type\": \"string\"\n        },\n        \"minItems\": 1,\n        \"uniqueItems\": true\n      },\n      \"dimensions\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"length\": {\n            \"type\": \"number\"\n          },\n          \"width\": {\n            \"type\": \"number\"\n          },\n          \"height\": {\n            \"type\": \"number\"\n          }\n        },\n        \"required\": [\n          \"length\",\n          \"width\",\n          \"height\"\n        ]\n      },\n      \"warehouseLocation\": {\n        \"description\": \"Coordinates of the warehouse with the product\",\n        \"$ref\": \"http://json-schema.org/geo\"\n      }\n    },\n    \"required\": [\n      \"id\",\n      \"name\",\n      \"price\",\n      \"dimensions\"\n    ]\n  }\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/schema/products.json",
    "content": "[\n    {\n        \"id\": 2,\n        \"name\": \"An ice sculpture\",\n        \"price\": 12.50,\n        \"tags\": [\"cold\", \"ice\"],\n        \"dimensions\": {\n            \"length\": 7.0,\n            \"width\": 12.0,\n            \"height\": 9.5\n        },\n        \"warehouseLocation\": {\n            \"latitude\": -78.75,\n            \"longitude\": 20.4\n        }\n    },\n    {\n        \"id\": 3,\n        \"name\": \"A blue mouse\",\n        \"price\": 25.50,\n        \"dimensions\": {\n            \"length\": 3.1,\n            \"width\": 1.0,\n            \"height\": 1.0\n        },\n        \"warehouseLocation\": {\n            \"latitude\": 54.4,\n            \"longitude\": -32.7\n        }\n    }\n]\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/schema/schema.feature",
    "content": "Feature: json schema validation\n\n# ignore because it does not work offline and slows down the Karate CI build\n# one more reason to avoid using JSON schema !\n@ignore\nScenario: using a third-party lib and a schema file\n    * string schema = read('products-schema.json')\n    * string json = read('products.json')\n    * def SchemaUtils = Java.type('com.intuit.karate.demo.util.SchemaUtils')\n    * assert SchemaUtils.isValid(json, schema)\n\nScenario: using karate's simpler alternative to json-schema\n    * def warehouseLocation = { latitude: '#number', longitude: '#number' }\n    * def productStructure =\n    \"\"\"\n    {\n      id: '#number',\n      name: '#string',\n      price: '#number? _ > 0',\n      tags: '##[_ > 0] #string',\n      dimensions: {\n        length: '#number',\n        width: '#number',\n        height: '#number'\n      },\n      warehouseLocation: '##(warehouseLocation)'\n    }\n    \"\"\"\n    * def json = read('products.json')\n    * match json == '#[] productStructure'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/search/dynamic-params.feature",
    "content": "Feature: dynamic params using scenario-outline, examples and json\n    see also the file demo/outline/examples.feature\n\nBackground:\n    * url demoBaseUrl\n\nScenario Outline: using a javascript function to pre-process the search parameters\n    this particular example has been deliberately over-complicated, the next scenario-outline below is simpler\n\n    * def query = { name: '<name>', country: '<country>', active: '<active>', limit: '<limit>' }\n    # all this function does is to set any empty string value to null, because that is what empty cells in 'Examples' become\n    * def nullify = \n    \"\"\"\n    function(o) {\n      for (var key in o) {\n        if (o[key] == '') o[key] = null;\n      }\n      return o;\n    }\n    \"\"\"\n    # here we load a java-script function from a re-usable file\n    * def getResponseParam = read('get-response-param.js')\n    * def query = nullify(query)\n    * print query\n\n    Given path 'search'\n    # the 'params' keyword takes json, and will ignore any key that has a null value\n    And params query\n    When method get\n    Then status 200\n\n    And assert getResponseParam('name') == query.name\n    And assert getResponseParam('country') == query.country\n    And assert getResponseParam('active') == query.active\n    And assert getResponseParam('limit') == query.limit\n\n    # response should NOT contain a key expected to be missing\n    And match response !contains { '<missing>': '#notnull' }\n\n    Examples:\n    | name | country | active | limit | missing |\n    | foo  | IN      | true   |     1 |         |\n    | bar  |         |        |     5 | country |\n    | baz  | JP      |        |       | active  |\n    |      | US      |        |     3 | name    |\n    |      |         | false  |       | limit   |\n\nScenario Outline: here the parameters are set to null within the 'Examples' table itself\n    # notice how this is different from the above, the quotes come from the 'Examples' section\n    * def query = { name: <name>, country: <country>, active: <active>, limit: <limit> }\n    * print query\n\n    Given path 'search'\n    And params query\n    When method get\n    Then status 200\n    # response should NOT contain a key expected to be missing\n    And match response !contains <missing>\n\n    # observe how strings are enclosed in quotes, and we set null-s here below\n    # and you can get creative by stuffing json into table cells !\n    Examples:\n    | name   | country   | active | limit | missing                                                      |\n    | 'foo'  | 'IN'      | true   |     1 | {}                                                           |\n    | 'bar'  | null      | null   |     5 | { country: '#notnull', active: '#notnull' }                  |\n    | 'baz'  | 'JP'      | null   |  null | { active: '#notnull', limit: '#notnull' }                    |\n    | null   | 'US'      | null   |     3 | { name: '#notnull', active: '#notnull' }                     |\n    | null   | null      | false  |  null | { name: '#notnull', country: '#notnull', limit: '#notnull' } |\n\nScenario: using a data-driven called feature instead of a scenario outline\n    this and the above example are the two fundamentally different ways of \n    data-driven test 'looping' in Karate\n\n    * table data\n    | name   | country   | active | limit | missing                      |\n    | 'foo'  | 'IN'      | true   |     1 | []                           |\n    | 'bar'  |           |        |     5 | ['country', 'active']        |\n    | 'baz'  | 'JP'      |        |       | ['active', 'limit']          |\n    |        | 'US'      |        |     3 | ['name', 'active']           |\n    |        |           | true   |       | ['name', 'country', 'limit'] |\n    \n    # the assertions in the called feature use some js for the sake of demo\n    # but the next scenario below is far simpler and does not use js at all\n    * def result = call read('search-complex.feature') data\n\nScenario: using the set keyword to build json and nulls are skipped by default\n    this is possibly the simplest form of all the above, avoiding any javascript\n    but does require however - a 'call' to a second feature file\n\n    # table would have been sufficient below, but here we demo how 'set' is simply a 'transpose' of table\n    * set data\n    | path    | 0       | 1       | 2       | 3       | 4       |\n    | name    | 'foo'   | 'bar'   | 'baz'   |         |         |\n    | country | 'IN'    |         | 'JP'    | 'US'    |         |\n    | active  | true    |         |         |         | false   |\n    | limit   | 1       | 5       |         | 3       |         |\n    \n    # note how you can 'compose' complex JSON by referring to existing JSON chunks, e.g: 'data[0]'\n    * table search\n    | params  | expected                                                         | missing                                                      |\n    | data[0] | { name: '#[1]', country: '#[1]', active: '#[1]', limit: '#[1]' } | {}                                                           |\n    | data[1] | { name: ['bar'], limit: ['5'] }                                  | { country: '#notnull', active: '#notnull' }                  |\n    | data[2] | { name: ['#(data[2].name)'], country: ['#(data[2].country)'] }   | { active: '#notnull', limit: '#notnull' }                    |\n    | data[3] | { country: '#[1]', limit: '#[1]' }                               | { name: '#notnull', active: '#notnull' }                     |\n    | data[4] | { active: '#[1]' }                                               | { name: '#notnull', country: '#notnull', limit: '#notnull' } |\n\n    * def result = call read('search-simple.feature') search\n\nScenario: params json with embedded expressions\n    * def data = { one: 'one', two: 'two' }\n\n    Given path 'search'\n    # using enclosed javascript instead of an embedded expression for convenience\n    And params ({ name: data.one, country: data.two })\n    When method get\n    Then status 200\n    And match response == { name: ['one'], country: ['two'] }\n\nScenario: test that multi-params work as expected\n    \n    Given path 'search'\n    And param foo = ['bar', 'baz']\n    When method get\n    Then status 200\n    And match response == { foo: ['bar', 'baz'] }\n\n    Given path 'search'\n    And params { foo: ['bar', 'baz'] }\n    When method get\n    Then status 200\n    And match response == { foo: ['bar', 'baz'] }\n\n    Given path 'search'\n    And param foo = 'bar,baz'\n    When method get\n    Then status 200\n    And match response == { foo: ['bar,baz'] }\n\n    Given path 'search'\n    And params { foo: 'bar,baz' }\n    When method get\n    Then status 200\n    And match response == { foo: ['bar,baz'] }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/search/get-response-param.js",
    "content": "function fn(name) {\n  var response = karate.get('response');\n  var list = response[name];\n  if (!list) {\n    return null;\n  }\n  return list[0];\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/search/search-complex.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n# pre-process call argument\n* def search = __arg\n* remove search.missing\n\n* url demoBaseUrl\nGiven path 'search'\nAnd params search\nWhen method get\nThen status 200\n\n# '#[1]' means validate if an array, and length is 1\n* def exists = function(v){ return v ? '#[1]' : null }\n\n# besides the built-in variable '__arg', each key within it is available by name\n# note how the '##' marker is used to auto-remove BEFORE the match\n# there are simpler ways to do this, but just for demo\n* def expected = { name: '##(exists(name))', country: '##(exists(country))', active: '##(exists(active))', limit: '##(exists(limit))' }\n* match response == expected\n\n# demo of how to turn an array of strings into json keys\n* def fun =\n\"\"\"\nfunction(arr) {\n  var res = {};\n  for (var i = 0; i < arr.length; i++) {\n    var key = arr[i];\n    res[key] = '#notnull';\n  }\n  return res;\n}\n\"\"\"\n# response should NOT contain a key expected to be missing\n* match response !contains fun(missing)\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/search/search-simple.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n\nGiven url demoBaseUrl\nAnd path 'search'\nAnd params params\nWhen method get\nThen status 200\nAnd match response == expected\nAnd match response !contains missing\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/signin/sign-in.feature",
    "content": "Feature: csrf and sign-in end point\n\nBackground:\n* url demoBaseUrl\n\nGiven path 'signin', 'token'\nWhen method get\nThen status 200\nAnd header X-CSRF-TOKEN = response\n\nScenario: html url encoded form submit - post\n    Given path 'signin'\n    And form field username = 'john'\n    And form field password = 'secret'\n    When method post\n    Then status 200\n    And match response == 'success'\n\nScenario: html url encoded form submit - get\n    Given path 'signin'\n    And form field username = 'john'\n    And form field password = 'secret'\n    When method get\n    Then status 200\n    And match response == 'success'\n\nScenario: html url encoded form submit - manually forming the request / NOT using 'form field'\n    Given path 'signin'\n    And request 'username=john&password=secret'\n    And header Content-Type = 'application/x-www-form-urlencoded'\n    When method post\n    Then status 200\n    And match response == 'success'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/soap/expected.xml",
    "content": "<AddResponse xmlns=\"#ignore\">\n  <AddResult>5</AddResult>\n</AddResponse>\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/soap/request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<soap12:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap12=\"http://www.w3.org/2003/05/soap-envelope\">\n  <soap12:Body>\n    <Add xmlns=\"http://tempuri.org/\">\n      <intA>2</intA>\n      <intB>3</intB>\n    </Add>\n  </soap12:Body>\n</soap12:Envelope>\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/soap/soap.feature",
    "content": "Feature: test soap end point\n\nBackground:\n* url demoBaseUrl + '/soap'\n# this live url should work if you want to try this on your own\n# * url 'http://www.dneonline.com/calculator.asmx'\n\nScenario: soap 1.1\n    Given request\n    \"\"\"\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n      <soap:Body>\n        <Add xmlns=\"http://tempuri.org/\">\n          <intA>2</intA>\n          <intB>3</intB>\n        </Add>\n      </soap:Body>\n    </soap:Envelope>\n    \"\"\"\n    When soap action 'http://tempuri.org/Add'\n    Then status 200\n    And match /Envelope/Body/AddResponse/AddResult == '5'\n    And print 'response: ', response\n\nScenario: soap 1.2\n    Given request read('request.xml')\n    # soap is just an HTTP POST, so here we set the required header manually ..\n    And header Content-Type = 'application/soap+xml; charset=utf-8'\n    # .. and then we use the 'method keyword' instead of 'soap action'\n    When method post\n    Then status 200\n    # note how we focus only on the relevant part of the payload and read expected XML from a file\n    And match /Envelope/Body/AddResponse == read('expected.xml')\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/tags/TagsRunner.java",
    "content": "package demo.tags;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author peter\n */\nclass TagsRunner {\n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:demo/tags\")\n                .configDir(\"classpath:demo/tags\")\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/tags/first.feature",
    "content": "@smoke\nFeature: tags demo - first\n    run the following example from the command line:\n    mvn test -Dkarate.options=\"--tags @smoke\" -Dtest=TagsRunner\n\nScenario: f1 - s1\n    * print 'first feature:@smoke, first scenario'\n\n@fire\nScenario: f1 - s2\n    * print 'first feature:@smoke, second scenario:@fire'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/tags/second.feature",
    "content": "Feature: tags demo - second\n    run the following example from the command line:\n    mvn test -Dkarate.options=\"--tags @smoke\" -Dtest=TagsRunner\n\nScenario: f2 - s1\n    * print 'second feature, first scenario'\n\n@smoke @fire\nScenario: f2 - s2\n    * print 'second feature, second scenario:@smoke @fire'\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/unit/cat.feature",
    "content": "Feature: demo calling java methods with complex types\n\n  Background:\n    * call read('common.feature')\n\n  Scenario: using constructor and setters\n    * def billie = new Cat()\n    * billie.id = 1\n    * billie.name = 'Billie'\n    * match toJson(billie) == { id: 1, name: 'Billie' }\n\n  Scenario: using json and calling java methods\n    * def billie = toCat({ id: 1, name: 'Billie' })\n    * def bob = toCat({ id: 2, name: 'Bob' })\n    * def wild = toCat({ id: 3, name: 'Wild' })\n    * billie.addKitten(bob)\n    * billie.addKitten(wild)\n    * match toJson(billie) ==\n      \"\"\"\n      {\n        id: 1, name: 'Billie', kittens: [\n          { id: 2, name: 'Bob' },\n          { id: 3, name: 'Wild' }\n        ]\n      }\n      \"\"\"\n\n  Scenario Outline: data driven\n    * def billie = toCat({ id: 1, name: 'Billie' })\n    * def fun = function(n, i){ return { id: i + 2, name: n } }\n    * def kittens = karate.map(names, fun)\n    * billie.kittens = karate.toJava(kittens)\n    * match toJson(billie) contains expected\n    * match toJson(billie).kittens == expected.kittens\n\n    Examples:\n      | names!          | expected!           |\n      | ['Bob', 'Wild'] | { kittens: '#[2]' } |\n      | ['X', 'Y', 'Z'] | { kittens: '#[3]' } |\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/unit/common.feature",
    "content": "@ignore\nFeature:\n\n  Scenario:\n    * def catType = 'com.intuit.karate.demo.domain.Cat'\n    * def Cat = Java.type(catType)\n    * def toCat = function(x){ return karate.toBean(x, catType) }\n    # second argument (true) is to strip keys with null values\n    * def toJson = function(x){ return karate.toJson(x, true) }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/unit/fizz-buzz.feature",
    "content": "Feature: demo of data-driven unit-testing\n\n  Background:\n    * def FB = Java.type('com.intuit.karate.demo.util.FizzBuzz')\n    * def fb = n => FB.process(n)\n\n  Scenario: simple assertions\n    * match fb(1) == '1'\n    * match fb(3) == 'Fizz'\n    * match fb(5) == 'Buzz'\n    * match fb(15) == 'FizzBuzz'\n\n  Scenario Outline: data-driven assertions: ${val}\n    * match fb(val) == expected\n\n    Examples:\n      | val! | expected |\n      | 1    | 1        |\n      | 3    | Fizz     |\n      | 5    | Buzz     |\n      | 15   | FizzBuzz |\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/upload/upload-image.feature",
    "content": "Feature: file upload end-point\n\nBackground:\n* url demoBaseUrl\n\nScenario: upload image - multipart\n    Given path 'files'\n    And multipart file myFile = { read: 'karate-logo.jpg', filename: 'karate-logo.jpg', contentType: 'image/jpg' }\n    And multipart field message = 'image test'\n    When method post\n    Then status 200\n    And match response == { id: '#uuid', filename: 'karate-logo.jpg', message: 'image test', contentType: 'image/jpg' }\n    And def id = response.id\n\n    Given path 'files', id\n    When method get\n    Then status 200\n    And match response == read('karate-logo.jpg')\n    And match header Content-Disposition contains 'attachment'\n    And match header Content-Disposition contains 'karate-logo.jpg'\n    And match header Content-Type == 'image/jpg'\n\nScenario: upload image - binary request body\n    Given path 'files', 'binary'\n    And param name = 'karate-logo.jpg'\n    And request read('karate-logo.jpg')\n    When method post\n    Then status 200\n    And match response contains { id: '#uuid' }\n    And def id = response.id\n\n    Given path 'files', id\n    When method get\n    Then status 200\n    And match responseBytes == read('karate-logo.jpg')\n    And match header Content-Disposition contains 'karate-logo.jpg'\n\nScenario: upload stream - content-length should be sent correctly\n    Given path 'search', 'headers'\n    And param name = 'karate-logo.jpg'\n    And request read('karate-logo.jpg')\n    When method post\n    Then status 200\n    And def response = karate.lowerCase(response)\n    And match response['content-length'][0] == '13575'\n\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/upload/upload-multiple-fields.feature",
    "content": "Feature: multipart fields (multiple)\n\nBackground:\n* url demoBaseUrl\n\nScenario: upload multiple fields\n    Given path 'files', 'fields'\n    And multipart fields { message: 'hello world', json: { value: { foo: 'bar' } } }\n    When method post\n    Then status 200\n    And match response == { message: 'hello world', json: { foo: 'bar' } }\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/upload/upload-multiple-files.feature",
    "content": "Feature: multipart files (multiple)\n\nBackground:\n* url demoBaseUrl\n\nScenario: upload multiple files\n    * def json = {}\n    * set json.myFile1 = { read: 'test.pdf', filename: 'upload-name1.pdf', contentType: 'application/pdf' }\n    # if you have dynamic keys you can do this\n    * def key = 'myFile2'\n    * json[key] = { read: 'test.pdf', filename: 'upload-name2.pdf', contentType: 'application/pdf' }\n    Given path 'files', 'multiple'\n    # so you can dynamically construct this json if there are multiple files\n    And multipart files json\n    And multipart field message = 'hello world'\n    When method post\n    Then status 200\n    And match response == [{ id: '#uuid', filename: 'upload-name1.pdf', message: 'hello world', contentType: 'application/pdf' }, { id: '#uuid', filename: 'upload-name2.pdf', message: 'hello world', contentType: 'application/pdf' }]\n    And def id1 = response[0].id\n    And def id2 = response[1].id\n\n    Given path 'files', id1\n    When method get\n    Then status 200\n    And match response == read('test.pdf')\n    And match header Content-Disposition contains 'attachment'\n    And match header Content-Disposition contains 'upload-name1.pdf'\n    And match header Content-Type == 'application/pdf'\n\n    Given path 'files', id2\n    When method get\n    Then status 200\n    And match response == read('test.pdf')\n    And match header Content-Disposition contains 'attachment'\n    And match header Content-Disposition contains 'upload-name2.pdf'\n    And match header Content-Type == 'application/pdf'\n\n# flaky in ci\n@ignore\nScenario: upload array of files (field name is the same)\n    # just use the same name, and behind the scenes an array of multi-parts will be sent in the request body    \n    * def first = { name: 'myFiles', read: 'test.pdf', filename: 'upload-name1.pdf', contentType: 'application/pdf' }\n    * def second = { name: 'myFiles', read: 'test.pdf', filename: 'upload-name2.pdf', contentType: 'application/pdf' }\n    \n    * path 'files', 'array'\n    # note how we support an array of files, which can be programmatically built\n    # here we use enclosed javascript (refer docs) for convenience, note the round-brackets\n    * multipart files ([ first, second ])\n    * multipart field message = 'hello world'\n    * method post\n    * status 200\n    * match response == [{ id: '#uuid', filename: 'upload-name1.pdf', message: 'hello world', contentType: 'application/pdf' }, { id: '#uuid', filename: 'upload-name2.pdf', message: 'hello world', contentType: 'application/pdf' }] \n"
  },
  {
    "path": "karate-demo/src/test/java/demo/upload/upload-retry.feature",
    "content": "Feature: file upload retry\n\nBackground:\n* url demoBaseUrl\n\nScenario: upload file\n    * def count = { value: 0 }\n    * configure retry = { interval: 100 }\n    * def done = function(){ return count.value++ == 1 }\n    Given path 'files'    \n    And multipart file myFile = { read: 'test.pdf', filename: 'upload-name.pdf', contentType: 'application/pdf' }\n    And multipart field message = 'hello world'\n    And retry until done()\n    When method post\n    Then status 200\n    And match response == { id: '#uuid', filename: 'upload-name.pdf', message: 'hello world', contentType: 'application/pdf' }\n    And def id = response.id\n\n    Given path 'files', id\n    When method get\n    Then status 200\n    And match response == read('test.pdf')\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/upload/upload.feature",
    "content": "Feature: file upload end-point\n\nBackground:\n* url demoBaseUrl\n\nScenario: upload file\n    Given path 'files'\n    # refer to the second scenario in this file for how to set the upload filename using the 'multipart file' syntax\n    And multipart file myFile = { read: 'test.pdf' }\n    And multipart field message = 'hello world'\n    When method post\n    Then status 200\n    And match response == { id: '#uuid', filename: 'test.pdf', message: 'hello world', contentType: 'application/pdf' }\n    And def id = response.id\n\n    Given path 'files', id\n    When method get\n    Then status 200\n    And match response == read('test.pdf')\n    And match header Content-Disposition contains 'attachment'\n    And match header Content-Disposition contains 'test.pdf'\n    And match header Content-Type == 'application/pdf'\n\n    # example of calling custom java code from karate\n    * def FileChecker = Java.type('com.intuit.karate.demo.util.FileChecker')\n    # example of parsing a string into json by karate\n    * json fileInfo = FileChecker.getMetadata(id)\n    * match fileInfo == { id: '#(id)', filename: 'test.pdf', message: 'hello world', contentType: 'application/pdf' }\n\nScenario: upload with filename and content-type specified\n    Given path 'files'\n    And multipart file myFile = { read: 'test.pdf', filename: 'upload-name.pdf', contentType: 'application/pdf' }\n    And multipart field message = 'hello world'\n    When method post\n    Then status 200\n    And match response == { id: '#uuid', filename: 'upload-name.pdf', message: 'hello world', contentType: 'application/pdf' }\n    And def id = response.id\n\n    Given path 'files', id\n    When method get\n    Then status 200\n    And match responseBytes == read('test.pdf')\n    And match header Content-Disposition contains 'attachment'\n    And match header Content-Disposition contains 'upload-name.pdf'\n    And match header Content-Type == 'application/pdf'\n\nScenario: upload with content created dynamically\n    Given path 'files'\n    And def value = 'lorem ipsum'\n    And multipart file myFile = { value: '#(value)', filename: 'hello.txt', contentType: 'text/plain' }\n    And multipart field message = 'hello world'\n    When method post\n    Then status 200\n    And match response contains { id: '#uuid', filename: 'hello.txt', message: 'hello world' }\n    And def id = response.id\n\n    Given path 'files', id\n    When method get\n    Then status 200\n    And match response == 'lorem ipsum'\n    And match header Content-Disposition contains 'attachment'\n    And match header Content-Disposition contains 'hello.txt'\n    And match header Content-Type contains 'text/plain'\n\nScenario: upload multipart/mixed\n    Given path 'files', 'mixed'\n    And multipart field myJson = { text: 'hello world' }\n    And multipart file myFile = { read: 'test.pdf', filename: 'upload-name.pdf', contentType: 'application/pdf' }\n    And header Content-Type = 'multipart/mixed'\n    When method post\n    Then status 200\n    And match response == { id: '#uuid', filename: 'upload-name.pdf', message: 'hello world', contentType: 'application/pdf' }\n\nScenario: multipart upload has content-length header set\n    Given path 'search', 'headers'\n    And multipart field myFile = read('test.pdf')\n    And multipart field message = 'hello world'\n    When method post\n    Then status 200\n    And match response['content-length'][0] == '#notnull'\n\n    Given path 'search', 'headers'\n    And multipart file myFile = { read: 'test.pdf', filename: 'upload-name.pdf', contentType: 'application/pdf' }\n    And multipart field message = 'hello world'\n    When method post\n    Then status 200\n    And match response['content-length'][0] == '#notnull'"
  },
  {
    "path": "karate-demo/src/test/java/demo/websocket/WebSocketClientRunner.java",
    "content": "package demo.websocket;\n\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.http.WebSocketClient;\nimport com.intuit.karate.http.WebSocketOptions;\nimport demo.TestBase;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n/**\n *\n * @author pthomas3\n */\nclass WebSocketClientRunner {\n\n    private static final Logger logger = new Logger();\n\n    private WebSocketClient client;\n    private String result;\n\n    @BeforeAll\n    static void beforeAll() {\n        TestBase.beforeAll();\n    }\n\n    @Test\n    void testWebSocketClient() throws Exception {\n        String port = System.getProperty(\"demo.server.port\");\n        WebSocketOptions options = new WebSocketOptions(\"ws://localhost:\" + port + \"/websocket\");\n        options.setTextConsumer(text -> {\n            logger.debug(\"websocket listener text: {}\", text);\n            synchronized (this) {\n                result = text;\n                notify();\n            }\n        });\n        client = new WebSocketClient(options, logger);\n        client.send(\"Billie\");\n        synchronized (this) {\n            wait();\n        }\n        assertEquals(\"hello Billie !\", result);\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/demo/websocket/echo.feature",
    "content": "@ignore\nFeature: public test at \n    http://www.websocket.org/echo.html\n\nScenario: text messages\n    And def socket = karate.webSocket('ws://echo.websocket.org')\n    When socket.send('hello world!')\n    And listen 5000\n    Then match listenResult == 'hello world!'\n\n    When socket.send('another test')\n    And listen 5000\n    Then match listenResult == 'another test'\n\nScenario: binary message\n    And def socket = karate.webSocketBinary('ws://echo.websocket.org')\n    And bytes data = read('../upload/test.pdf')\n    When socket.sendBytes(data)\n    And listen 5000\n    # the result data-type is byte-array, but this comparison works\n    Then match listenResult == read('../upload/test.pdf')\n\nScenario: sub protocol\n    Given def demoBaseUrl = 'wss://subscriptions.graph.cool/v1/cizfapt9y2jca01393hzx96w9'\n    And def options = { subProtocol: 'graphql-subscriptions', headers: { Authorization: 'Bearer foo' } }\n    And def socket = karate.webSocket(demoBaseUrl, null, options)\n    And def txt = '{\"type\": \"connection_init\", \"payload\": {}}'\n    When socket.send(txt)\n    And listen 5000\n    Then match listenResult == { type: 'connection_ack' }"
  },
  {
    "path": "karate-demo/src/test/java/demo/websocket/websocket.feature",
    "content": "@ignore\nFeature: websocket testing\n\nScenario: only listening to websocket messages\n    * def handler = function(msg){ return msg.startsWith('{') }\n    * def socket = karate.webSocket(demoBaseUrl + '/websocket', handler)\n\n    # first we post to the /websocket-controller end-point which will broadcast a message\n    # to any websocket clients that are connected - but after a delay of 1 second    \n    Given url demoBaseUrl\n    And path 'websocket-controller'\n    And request { text: 'Rudy' }\n    When method post\n    Then status 200\n    And def id = response.id\n\n    # this line will wait until the handler returns true\n    * listen 5000\n    * json result = listenResult\n    * match result == { id: '#(id)', content: 'hello Rudy !' }\n\nScenario: using the websocket instance to send as well as receive messages\n    * def handler = function(msg){ return msg.startsWith('hello') }\n    * def socket = karate.webSocket(demoBaseUrl + '/websocket', handler)\n    * socket.send('Billie')\n    * listen 5000\n    * match listenResult == 'hello Billie !'\n\nScenario: listen for multiple websocket messages\n    * def handler = function(msg){ return msg.startsWith('hello') }\n    * def socket = karate.webSocket(demoBaseUrl + '/websocket', handler)\n    * socket.send('Billie')\n    * listen 5000\n    * match listenResult == 'hello Billie !'\n    * socket.send('Bob')\n    * listen 5000\n    * match listenResult == 'hello Bob !'\n\nScenario: change the websocket handler for messages\n    * def handler = function(msg){ return msg.contains('Billie') }\n    * def socket = karate.webSocket(demoBaseUrl + '/websocket', handler)\n    * socket.send('Billie')\n    * listen 5000\n    * match listenResult == 'hello Billie !'\n    * def handler = function(msg){ return msg.contains('Bob') }\n    * socket.setTextHandler(handler)\n    * socket.send('Bob')\n    * listen 5000\n    * match listenResult == 'hello Bob !'"
  },
  {
    "path": "karate-demo/src/test/java/demo/xml/preserve-whitespace.feature",
    "content": "Feature: Preserve white space in text content\nScenario:\n    * def xml =\n    \"\"\"\n    <myRoot xml:space=\"preserve\">\n        <myNode> myValue </myNode>\n    </myRoot>\n    \"\"\"\n    * match karate.xmlPath(xml, '/myRoot/myNode') == ' myValue '"
  },
  {
    "path": "karate-demo/src/test/java/driver/demo/Demo01JavaRunner.java",
    "content": "package driver.demo;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.driver.chrome.Chrome;\nimport java.io.File;\nimport com.intuit.karate.driver.microsoft.EdgeChromium;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass Demo01JavaRunner {\n\n    static final Logger logger = LoggerFactory.getLogger(Demo01JavaRunner.class);\n\n    @Test\n    void testChrome() throws Exception {\n        Chrome driver = Chrome.start();\n        driver.setUrl(\"https://github.com/login\");\n        driver.input(\"#login_field\", \"dummy\");\n        driver.input(\"#password\", \"world\");\n        driver.submit().click(\"input[name=commit]\");\n        String html = driver.html(\".flash-error\");\n        assertTrue(html.contains(\"Incorrect username or password.\"));\n        driver.setUrl(\"https://google.com\");\n        driver.input(\"textarea[name=q]\", \"karate dsl\");\n        driver.submit().click(\"input[name=btnI]\");\n        assertEquals(\"https://github.com/karatelabs/karate\", driver.getUrl());\n        byte[] bytes = driver.screenshot();\n        // byte[] bytes = driver.screenshotFull();\n        FileUtils.writeToFile(new File(\"target/screenshot.png\"), bytes);\n        driver.quit();\n    }\n\n    // @Test\n    void testEdge() throws Exception {\n        EdgeChromium driver = EdgeChromium.start();\n        driver.setUrl(\"https://github.com/login\");\n        driver.input(\"#login_field\", \"dummy\");\n        driver.input(\"#password\", \"world\");\n        driver.submit().click(\"input[name=commit]\");\n        String html = driver.html(\".flash-error\");\n        assertTrue(html.contains(\"Incorrect username or password.\"));\n        driver.setUrl(\"https://google.com\");\n        driver.input(\"textarea[name=q]\", \"karate dsl\");\n        driver.submit().click(\"input[name=btnI]\");\n        assertEquals(\"https://github.com/karatelabs/karate\", driver.getUrl());\n        byte[] bytes = driver.screenshot();\n        // byte[] bytes = driver.screenshotFull();\n        FileUtils.writeToFile(new File(\"target/screenshot.png\"), bytes);\n        driver.quit();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/demo/demo-01.feature",
    "content": "Feature: browser automation 1\n\n  Background:\n    * configure driver = { type: 'chrome', showDriverLog: true }\n  # * configure driverTarget = { docker: 'justinribeiro/chrome-headless', showDriverLog: true }\n  # * configure driverTarget = { docker: 'ptrthomas/karate-chrome', showDriverLog: true }\n  # * configure driver = { type: 'chromedriver', showDriverLog: true }\n  # * configure driver = { type: 'geckodriver', showDriverLog: true }\n  # * configure driver = { type: 'safaridriver', showDriverLog: true }\n  # * configure driver = { type: 'iedriver', showDriverLog: true, httpConfig: { readTimeout: 120000 } }\n\n  Scenario: try to login to github\n  and then do a google search\n\n    Given driver 'https://github.com/login'\n    And input('#login_field', 'dummy')\n    And input('#password', 'world')\n    When submit().click(\"input[name=commit]\")\n    Then match html('.flash-error') contains 'Incorrect username or password.'\n\n    Given driver 'https://google.com'\n    And input(\"textarea[name=q]\", 'karate dsl')\n    When submit().click(\"input[name=btnI]\")\n    Then waitForUrl('https://github.com/karatelabs/karate')"
  },
  {
    "path": "karate-demo/src/test/java/driver/demo/demo-02.feature",
    "content": "Feature: browser automation 2\n\n  Background:\n    * configure driver = { type: 'chrome' }\n    # * configure driverTarget = { docker: 'ptrthomas/karate-chrome', showDriverLog: true }\n\n  Scenario: google search, land on the karate github page, and search for a file\n\n    Given driver 'https://google.com'\n    And input('textarea[name=q]', 'karate dsl')\n    When click('input[name=btnI]')\n    Then waitForUrl('https://github.com/karatelabs/karate')\n\n    When click('{a}Go to file')\n    And def searchField = waitFor('input[name=query]')\n    Then match driver.url == 'https://github.com/karatelabs/karate/find/master'\n\n    When searchField.input('karate-logo.png')    \n    And def innerText = function(locator){ return scriptAll(locator, '_.innerText') }\n    And def searchFunction =\n      \"\"\"\n      function() {\n        var results = innerText('.js-tree-browser-result-path');\n        return results.size() == 2 ? results : null;\n      }\n      \"\"\"\n    And def searchResults = waitUntil(searchFunction)\n    Then match searchResults contains 'karate-core/src/main/resources/karate-logo.png'\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/demo/demo-03.feature",
    "content": "Feature: 3 scenarios\n\n  Background:\n    # * configure driver = { type: 'chromedriver', showDriverLog: true }\n    * configure driverTarget = { docker: 'ptrthomas/karate-chrome', showDriverLog: true }\n\n  Scenario: try to login to github\n  and then do a google search\n\n    Given driver 'https://github.com/login'\n    And input('#login_field', 'dummy')\n    And input('#password', 'world')\n    When submit().click(\"input[name=commit]\")\n    Then match html('.flash-error') contains 'Incorrect username or password.'\n\n    Given driver 'https://google.com'\n    And input(\"textarea[name=q]\", 'karate dsl')\n    When submit().click(\"input[name=btnI]\")\n    Then match driver.url == 'https://github.com/karatelabs/karate'\n\n  Scenario: google search, land on the karate github page, and search for a file\n\n    Given driver 'https://google.com'\n    And input('textarea[name=q]', 'karate dsl')\n    When click('input[name=btnI]')\n    Then waitForUrl('https://github.com/karatelabs/karate')\n\n    When click('{a}Go to file')\n    And def searchField = waitFor('input[name=query]')\n    Then match driver.url == 'https://github.com/karatelabs/karate/find/master'\n\n    When searchField.input('karate-logo.png')\n    Then def searchResults = waitForResultCount('.js-tree-browser-result-path', 2, '_.innerText')\n    Then match searchResults contains 'karate-core/src/main/resources/karate-logo.png'\n\n  Scenario: test-automation challenge\n    Given driver 'https://semantic-ui.com/modules/dropdown.html'\n    And def locator = \"select[name=skills]\"\n    Then scroll(locator)\n    And click(locator)\n    And click('div[data-value=css]')\n    And click('div[data-value=html]')\n    And click('div[data-value=ember]')\n    And delay(1000)"
  },
  {
    "path": "karate-demo/src/test/java/driver/demo/demo-04.feature",
    "content": "Feature:\n\nScenario:\n* configure driver = { type: 'chrome', showDriverLog: true }\n* driver 'https://www.seleniumeasy.com/test/drag-and-drop-demo.html'\n* script(\"var myDragEvent = new Event('dragstart'); myDragEvent.dataTransfer = new DataTransfer()\")\n* waitFor('{}Draggable 1').script(\"_.dispatchEvent(myDragEvent)\")\n* script(\"var myDropEvent = new Event('drop'); myDropEvent.dataTransfer = myDragEvent.dataTransfer\")\n* script('#mydropzone', \"_.dispatchEvent(myDropEvent)\")\n* screenshot()\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/demo/demo-05.feature",
    "content": "Feature:\n\nScenario:\n* configure driver = { type: 'chrome' }\n* driver 'http://the-internet.herokuapp.com/upload'\n* driver.inputFile('#file-upload', '../../demo/upload/test.pdf')\n* submit().click('#file-submit')\n* waitForText('#uploaded-files', 'test.pdf')\n* screenshot()\n\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/demo/demo-06.feature",
    "content": "Feature: browser automation 1\n\nBackground:\n  * configure driver = { type: 'chrome', showDriverLog: true }\n  # * configure driverTarget = { docker: 'justinribeiro/chrome-headless', showDriverLog: true }\n  # * configure driverTarget = { docker: 'ptrthomas/karate-chrome', showDriverLog: true }\n  # * configure driver = { type: 'chromedriver', showDriverLog: true }\n  # * configure driver = { type: 'geckodriver', showDriverLog: true }\n  # * configure driver = { type: 'safaridriver', showDriverLog: true }\n  # * configure driver = { type: 'iedriver', showDriverLog: true, httpConfig: { readTimeout: 120000 } }\n  * url demoBaseUrl\n\n Scenario: pass cookie from API call to UI call\n  Given path 'search', 'cookies'\n  * cookies { someKey: 'someValue', foo: 'bar' }\n  When method get\n  Then status 200\n  And match response == '#[2]'\n  And match response[0] contains { name: 'foo', value: 'bar' }\n\n  Given driver demoBaseUrl + '/search/cookies'\n  * print responseCookies\n  # set responseCookies from API call to UI(driver)\n  When setCookies(responseCookies)\n  Then match driver.cookies == '#[2]'\n\nScenario: pass cookie from a UI call to a certain API call\n  Given driver demoBaseUrl + '/search/cookies'\n  Given def cookie2 = { name: 'hello', value: 'world' }\n  When cookie(cookie2)\n  Then match driver.cookies contains '#(^cookie2)'\n\n  Given path 'search', 'cookies'\n  # set driver cookies for api call\n  * cookies driver.cookies\n  When method get\n  Then status 200\n  And match response == '#[1]'\n  And match response[0] contains { name: 'hello', value: 'world' }\n\nScenario: pass cookie from a UI call to a certain API call - negative\n  Given driver demoBaseUrl + '/search/cookies'\n  Given def cookie2 = { name: 'hello', value: 'world' }\n  When cookie(cookie2)\n  Then match driver.cookies contains '#(^cookie2)'\n\n  When clearCookies()\n  Then match driver.cookies == '#[0]'\n\n  Given path 'search', 'cookies'\n  * cookies driver.cookies\n  When method get\n  Then status 200\n  And match response == '#[0]'\n\n\n\n\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/mock/demo-01.feature",
    "content": "Feature: intercepting browser requests\n\nScenario: \n# intercepting browser http requests is supported only for chrome native\n* configure driver = { type: 'chrome', showDriverLog: true }\n\n# if you need to set up the interceptor before the target page is loaded\n# use 'about:blank' as the init url, e.g. \"* driver 'about:blank'\"\n* driver 'https://www.seleniumeasy.com/test/dynamic-data-loading-demo.html'\n\n# the mock feature supports the usual prefixes such as \"classpath:\"\n# note that switching on \"* configure cors = true\" may be needed\n* driver.intercept({ patterns: [{ urlPattern: '*randomuser.me/*' }], mock: 'mock-01.feature' })\n\n# useful for demo-ing this\n# * karate.stop(9000)\n\n* click('{}Get New User')\n* delay(2000)\n* screenshot()\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/mock/demo-02.feature",
    "content": "Feature: intercepting all requests !\n\nScenario: \n* configure driver = { type: 'chrome', showDriverLog: true }\n\n# this will send every request the browser makes to the mock !\n* driver 'about:blank'\n* driver.intercept({ patterns: [{ urlPattern: '*' }], mock: 'mock-02.feature' })\n\n* driver 'https://github.com/login'\n\n# * karate.stop()\n\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/mock/mock-01.feature",
    "content": "@ignore\nFeature:\n\nBackground:\n* configure cors = true\n* def count = 0\n\nScenario: pathMatches('/api/{img}')\n* def response = read('billie.jpg')\n* def responseHeaders = { 'Content-Type': 'image/jpeg' }\n\nScenario:\n* def count = count + 1\n* def lastName = 'Request #' + count\n* def response = read('response.json')\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/mock/mock-02.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* karate.proceed()\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/mock/response.json",
    "content": "{\n   \"results\": [\n      {\n         \"name\": {\n            \"first\": \"Billie\",\n            \"last\": \"#(lastName)\"\n         },\n         \"picture\": {\n            \"large\": \"https://randomuser.me/api/billie.jpg\"\n         }\n      }\n   ]\n}"
  },
  {
    "path": "karate-demo/src/test/java/driver/screenshot/ChromeFullPageRunner.java",
    "content": "package driver.screenshot;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.driver.chrome.Chrome;\nimport java.io.File;\nimport java.util.Collections;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.junit.jupiter.api.Test;\n\n\n/**\n *\n * @author pthomas3\n */\nclass ChromeFullPageRunner {\n    \n    static final Logger logger = LoggerFactory.getLogger(ChromeFullPageRunner.class);  \n    \n    @Test\n    void testChrome() {\n        Chrome chrome = Chrome.startHeadless();\n        chrome.setUrl(\"https://github.com/karatelabs/karate/graphs/contributors\");\n        byte[] bytes = chrome.pdf(Collections.EMPTY_MAP);\n        FileUtils.writeToFile(new File(\"target/fullscreen.pdf\"), bytes);\n        bytes = chrome.screenshot(true);\n        FileUtils.writeToFile(new File(\"target/fullscreen.png\"), bytes);\n        chrome.quit();\n    }\n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/screenshot/ChromePdfRunner.java",
    "content": "package driver.screenshot;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.driver.chrome.Chrome;\nimport java.io.File;\nimport java.util.Collections;\n\n/**\n *\n * @author pthomas3\n */\npublic class ChromePdfRunner {\n\n    public static void main(String[] args) {\n        Chrome chrome = Chrome.startHeadless();\n        chrome.setUrl(\"https://github.com/login\");\n        byte[] bytes = chrome.pdf(Collections.EMPTY_MAP);\n        FileUtils.writeToFile(new File(\"target/github.pdf\"), bytes);\n        bytes = chrome.screenshot();\n        FileUtils.writeToFile(new File(\"target/github.png\"), bytes);\n        chrome.quit();\n    }\n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/screenshot/EdgeChromiumFullPageRunner.java",
    "content": "package driver.screenshot;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.driver.microsoft.EdgeChromium;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport java.io.File;\nimport java.util.Collections;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author sixdouglas\n */\nclass EdgeChromiumFullPageRunner {\n    \n    static final Logger logger = LoggerFactory.getLogger(EdgeChromiumFullPageRunner.class);\n    \n    @Test\n    void testEdge() {\n        EdgeChromium edgeChromium = EdgeChromium.startHeadless();\n        edgeChromium.setUrl(\"https://github.com/karatelabs/karate/graphs/contributors\");\n        byte[] bytes = edgeChromium.pdf(Collections.EMPTY_MAP);\n        FileUtils.writeToFile(new File(\"target/fullscreen.pdf\"), bytes);\n        bytes = edgeChromium.screenshot(true);\n        FileUtils.writeToFile(new File(\"target/fullscreen.png\"), bytes);\n        edgeChromium.quit();\n    }\n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/screenshot/EdgeChromiumPdfRunner.java",
    "content": "package driver.screenshot;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.driver.microsoft.EdgeChromium;\n\nimport java.io.File;\nimport java.util.Collections;\n\n/**\n *\n * @author sixdouglas\n */\npublic class EdgeChromiumPdfRunner {\n\n    public static void main(String[] args) {\n        EdgeChromium edgeChromium = EdgeChromium.startHeadless();\n        edgeChromium.setUrl(\"https://github.com/login\");\n        byte[] bytes = edgeChromium.pdf(Collections.EMPTY_MAP);\n        FileUtils.writeToFile(new File(\"target/github.pdf\"), bytes);\n        bytes = edgeChromium.screenshot();\n        FileUtils.writeToFile(new File(\"target/github.png\"), bytes);\n        edgeChromium.quit();\n    }\n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/driver/windows/calc.feature",
    "content": "Feature:\n\n  Background:\n    * def session = { desiredCapabilities: { app: 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App' } }\n\n    Scenario:\n      Given driver { type: 'winappdriver', webDriverSession: '#(session)' }\n      And driver.click('One')\n      And driver.click('Plus')\n      And driver.click('Seven')\n      When driver.click('Equals')\n      Then match driver.text('@CalculatorResults') contains '8'\n"
  },
  {
    "path": "karate-demo/src/test/java/headers.js",
    "content": "function fn() {\n  var token = karate.get('token');\n  var time = karate.get('time');\n  if (token && time) {\n    var uuid = java.util.UUID.randomUUID(); // create a unique id for each request\n    // demoBaseUrl was available at the time this function was declared\n    // and so behaves like a constant, use 'karate.get' for dynamic values\n    return { \n        Authorization: token + time + demoBaseUrl,\n        request_id: uuid + '' // convert the java uuid into a string\n    };\n  } else {\n    return {};\n  }\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/karate-config.js",
    "content": "function fn() {\n  karate.configure('connectTimeout', 5000);\n  karate.configure('readTimeout', 5000);  \n  // karate.configure('abortSuiteOnFailure', true);\n  var port = karate.properties['demo.server.port'] || '8080';\n  var protocol = 'http';\n  if (karate.properties['demo.server.https'] === 'true') {\n    protocol = 'https';\n    karate.configure('ssl', true);\n  }  \n  var config = { demoBaseUrl: protocol + '://127.0.0.1:' + port };\n  if (karate.env !== 'mock') {\n    // karate.configure('callSingleCache', { minutes: 1 });\n    // 'callSingle' is guaranteed to run only once even across all threads\n    var result = karate.callSingle('classpath:demo/headers/common-noheaders.feature', config);\n    // and it sets a variable called 'authInfo' used in headers-single.feature\n    config.authInfo = { authTime: result.time, authToken: result.token };\n  }\n  return config;\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/log4j2.properties",
    "content": "# this is only for the net.masterthought.cucumber.ReportBuilder in DemoTestParallel.java\nlog4j.rootLogger = INFO, CONSOLE\nlog4j.appender.CONSOLE = org.apache.log4j.ConsoleAppender\nlog4j.appender.CONSOLE.layout = org.apache.log4j.PatternLayout"
  },
  {
    "path": "karate-demo/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit.karate\" level=\"DEBUG\"/>\n    <logger name=\"demo\" level=\"DEBUG\"/>\n    <logger name=\"mock\" level=\"DEBUG\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>"
  },
  {
    "path": "karate-demo/src/test/java/mock/async/AsyncTest.java",
    "content": "package mock.async;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass AsyncTest { \n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:mock/async/main.feature\")\n                .configDir(\"classpath:mock/async\")\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/async/QueueConsumer.java",
    "content": "package mock.async;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Predicate;\nimport javax.jms.Connection;\nimport javax.jms.Destination;\nimport javax.jms.MessageConsumer;\nimport javax.jms.Session;\nimport javax.jms.TextMessage;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class QueueConsumer {\n\n    private static final Logger logger = LoggerFactory.getLogger(QueueConsumer.class);\n\n    public static final String QUEUE_NAME = \"MOCK.ASYNC\";\n\n    private final Connection connection;\n    private final MessageConsumer consumer;\n    private final Session session;\n\n    // in more complex tests or for re-usability, this field and append() /\n    // collect() / clear() methods can be in a separate / static class\n    private final List messages = new ArrayList();\n\n    public synchronized void append(Object message) {\n        messages.add(message);\n        if (condition.test(message)) {\n            logger.debug(\"condition met, will signal completion\");\n            future.complete(Boolean.TRUE);\n        } else {\n            logger.debug(\"condition not met, will continue waiting\");\n        }\n    }\n\n    public synchronized List collect() {\n        return messages;\n    }\n    \n    private CompletableFuture future = new CompletableFuture();\n    private Predicate condition = o -> true; // just a default\n    \n    // note how you can pass data in from the test for very dynamic checks\n    public List waitUntilCount(int count) {        \n        condition = o -> messages.size() == count;\n        try {\n            future.get(5000, TimeUnit.MILLISECONDS);\n        } catch (Exception e) {\n            logger.error(\"wait timed out: {}\", e + \"\");\n        }\n        return messages;\n    }\n\n    public QueueConsumer() {\n        this.connection = QueueUtils.getConnection();\n        try {\n            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);\n            Destination destination = session.createQueue(QUEUE_NAME);\n            consumer = session.createConsumer(destination);\n            consumer.setMessageListener(message -> {\n                TextMessage tm = (TextMessage) message;\n                try {\n                    // this is where we \"collect\" messages for assertions later\n                    append(tm.getText());\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n            });\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/async/QueueUtils.java",
    "content": "package mock.async;\n\nimport javax.jms.*;\n\nimport org.apache.activemq.ActiveMQConnectionFactory;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class QueueUtils {\n\n    private static final Logger logger = LoggerFactory.getLogger(QueueUtils.class);\n\n    private static final Connection connection;\n\n    public static Connection getConnection() {\n        return connection;\n    }\n\n    static {\n        try {\n            logger.debug(\"waiting for queue / connection ...\");\n            ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(\"vm://localhost?broker.persistent=false&waitForStart=10000\");\n            connection = connectionFactory.createConnection();\n            connection.start();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void send(String text, int delayMillis) {\n        new Thread(() -> {\n            try {\n                logger.info(\"*** scheduled delay: {}\", delayMillis);\n                Thread.sleep(delayMillis);\n                Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);\n                Destination destination = session.createQueue(QueueConsumer.QUEUE_NAME);\n                MessageProducer producer = session.createProducer(destination);\n                producer.setDeliveryMode(DeliveryMode.PERSISTENT);\n                TextMessage message = session.createTextMessage(text);\n                producer.send(message);\n                logger.info(\"*** sent message: {}\", text);\n                session.close();\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }).start();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/async/karate-config.js",
    "content": "function fn() {\n  return {};\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/async/main.feature",
    "content": "Feature:\n\nScenario:\n* def QueueConsumer = Java.type('mock.async.QueueConsumer')\n# this will start listening to messages and collecting them\n* def queue = new QueueConsumer()\n\n* def port = karate.start('mock.feature').port\n* url 'http://localhost:' + port\n* path 'send';\n* method get\n* status 200\n\n# * java.lang.Thread.sleep(1000)\n# * def messages = queue.collect()\n\n# smarter wait instead of the above two lines\n* def messages = queue.waitUntilCount(3)\n\n* match messages == ['first', 'second', 'third']\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/async/mock.feature",
    "content": "Feature:\n\nBackground:\n* def QueueUtils = Java.type('mock.async.QueueUtils')\n\nScenario: pathMatches('/send')\n* QueueUtils.send('first', 100)\n* QueueUtils.send('second', 200)\n* QueueUtils.send('third', 300)\n* def response = { success: true }\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/Consumer.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.JsonUtils;\nimport java.net.HttpURLConnection;\nimport java.net.InetSocketAddress;\nimport java.net.Proxy;\nimport java.net.URL;\nimport javax.jms.TextMessage;\nimport org.apache.commons.io.IOUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class Consumer {\n\n    private static final Logger logger = LoggerFactory.getLogger(Consumer.class);\n\n    private final String paymentServiceUrl;\n    private final String proxyHost;\n    private final Integer proxyPort;\n    private final QueueConsumer queueConsumer;\n\n    public Consumer(String paymentServiceUrl, String queueName) {\n        this(paymentServiceUrl, null, null, queueName);\n    }\n\n    public Consumer(String paymentServiceUrl, String proxyHost, Integer proxyPort, String queueName) {\n        this.paymentServiceUrl = paymentServiceUrl;\n        this.proxyHost = proxyHost;\n        this.proxyPort = proxyPort;\n        queueConsumer = new QueueConsumer(queueName);\n    }\n\n    private HttpURLConnection getConnection(String path) throws Exception {\n        URL url = new URL(paymentServiceUrl + path);\n        if (proxyHost != null) {\n            Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));\n            return (HttpURLConnection) url.openConnection(proxy);\n        } else {\n            return (HttpURLConnection) url.openConnection();\n        }\n    }\n\n    public Payment create(Payment payment) {\n        try {\n            HttpURLConnection con = getConnection(\"/payments\");\n            con.setRequestMethod(\"POST\");\n            con.setDoOutput(true);\n            con.setRequestProperty(\"Content-Type\", \"application/json\");\n            String json = JsonUtils.toJson(payment);\n            IOUtils.write(json, con.getOutputStream(), \"utf-8\");\n            int status = con.getResponseCode();\n            if (status != 200) {\n                throw new RuntimeException(\"status code was \" + status);\n            }\n            String content = IOUtils.toString(con.getInputStream(), \"utf-8\");\n            return JsonUtils.fromJson(content, Payment.class);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void listen(java.util.function.Consumer<String> handler) {\n        queueConsumer.setMessageListener(message -> {\n            try {\n                TextMessage tm = (TextMessage) message;\n                String json = tm.getText();\n                logger.info(\"*** received message: {}\", json);\n                handler.accept(json);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        });\n    }\n\n    public void stopQueueConsumer() {\n        queueConsumer.setMessageListener(null);\n        queueConsumer.stop();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/ConsumerIntegrationTest.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.JsonUtils;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass ConsumerIntegrationTest {\n    \n    static ConfigurableApplicationContext context;\n    static Consumer consumer;\n    \n    @BeforeAll\n    static void beforeAll() {\n        String queueName = \"DEMO.INTEGRATION\";\n        context = PaymentService.start(queueName, false);\n        String paymentServiceUrl = \"http://localhost:\" + PaymentService.getPort(context);\n        consumer = new Consumer(paymentServiceUrl, queueName); \n    }\n    \n    @Test\n    void testPaymentCreate() throws Exception {\n        Payment payment = new Payment();\n        payment.setAmount(5.67);\n        payment.setDescription(\"test one\");\n        Payment result = consumer.create(payment);\n        assertTrue(result.getId() > 0);\n        assertEquals(result.getAmount(), 5.67, 0);\n        assertEquals(result.getDescription(), \"test one\");\n        consumer.listen(json -> {\n            Shipment shipment = JsonUtils.fromJson(json, Shipment.class);\n            assertEquals(result.getId(), shipment.getPaymentId());\n            assertEquals(\"shipped\", shipment.getStatus()); \n            synchronized(this) {\n                notify();\n            }\n        });\n        synchronized(this) {\n            wait(10000);\n        }\n    }\n    \n    @AfterAll\n    static void afterAll() {\n        PaymentService.stop(context);\n        consumer.stopQueueConsumer();\n    }\n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/ConsumerUsingMockTest.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.core.MockServer;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass ConsumerUsingMockTest {\n    \n    static final Logger logger = LoggerFactory.getLogger(ConsumerUsingMockTest.class);\n    \n    static MockServer server;\n    static Consumer consumer;\n    \n    @BeforeAll\n    static void beforeAll() {\n        String queueName = \"DEMO.MOCK\";\n        server = MockServer\n                .feature(\"classpath:mock/contract/payment-service-mock.feature\")\n                .arg(\"queueName\", queueName)\n                .http(0).build();          \n        String paymentServiceUrl = \"http://localhost:\" + server.getPort();\n        consumer = new Consumer(paymentServiceUrl, queueName);        \n    }    \n    \n    @Test\n    void testPaymentCreate() throws Exception {\n        Payment payment = new Payment();\n        payment.setAmount(5.67);\n        payment.setDescription(\"test one\");\n        Payment result = consumer.create(payment);\n        assertTrue(result.getId() > 0);\n        assertEquals(result.getAmount(), 5.67, 0);\n        assertEquals(result.getDescription(), \"test one\");\n        consumer.listen(json -> {\n            Shipment shipment = JsonUtils.fromJson(json, Shipment.class);\n            assertEquals(result.getId(), shipment.getPaymentId());\n            assertEquals(\"shipped\", shipment.getStatus()); \n            synchronized(this) {\n                notify();\n            }\n        });\n        synchronized(this) {\n            wait(10000);\n        }       \n    }\n    \n    @AfterAll\n    static void afterAll() {\n        server.stop();\n        consumer.stopQueueConsumer();\n    }    \n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/ConsumerUsingProxyHttpTest.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.core.MockServer;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass ConsumerUsingProxyHttpTest {\n\n    static ConfigurableApplicationContext context;\n    static MockServer server;\n    static Consumer consumer;\n\n    @BeforeAll\n    static void beforeAll() {\n        // actual service\n        String queueName = \"DEMO.PROXY.HTTP\";\n        context = PaymentService.start(queueName, false);\n        String paymentServiceUrl = \"http://localhost:\" + PaymentService.getPort(context);\n        // proxy\n        server = MockServer\n                .feature(\"classpath:mock/contract/payment-service-proxy.feature\")\n                // setting 'paymentServiceUrl' to null uses request url as-is (no re-writing) - so acts as an http proxy\n                .arg(\"paymentServiceUrl\", null)\n                .http(0).build();\n        // consumer (using http proxy)\n        consumer = new Consumer(paymentServiceUrl, \"localhost\", server.getPort(), queueName);\n    }\n\n    // @Test // TODO armeria upgrade\n    void testPaymentCreate() throws Exception {\n        Payment payment = new Payment();\n        payment.setAmount(5.67);\n        payment.setDescription(\"test one\");\n        Payment result = consumer.create(payment);\n        assertTrue(result.getId() > 0);\n        assertEquals(result.getAmount(), 5.67, 0);\n        assertEquals(result.getDescription(), \"test one\");\n        consumer.listen(json -> {\n            Shipment shipment = JsonUtils.fromJson(json, Shipment.class);\n            assertEquals(result.getId(), shipment.getPaymentId());\n            assertEquals(\"shipped\", shipment.getStatus());\n            synchronized (this) {\n                notify();\n            }\n        });\n        synchronized (this) {\n            wait(10000);\n        }\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n        PaymentService.stop(context);\n        consumer.stopQueueConsumer();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/ConsumerUsingProxyRewriteSslTest.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.core.MockServer;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass ConsumerUsingProxyRewriteSslTest {\n\n    static ConfigurableApplicationContext context;\n    static MockServer server;\n    static Consumer consumer;\n\n    @BeforeAll\n    static void beforeAll() {\n        // actual service      \n        String queueName = \"DEMO.PROXY.REWRITE.SSL\";\n        context = PaymentService.start(queueName, true);\n        String paymentServiceUrl = \"https://localhost:\" + PaymentService.getPort(context);\n        // proxy\n        server = MockServer\n                .feature(\"classpath:mock/contract/payment-service-proxy.feature\")\n                // requests will be forwarded / url re-written to paymentServiceUrl\n                .arg(\"paymentServiceUrl\", paymentServiceUrl)\n                .http(0).build();\n        // consumer\n        String proxyUrl = \"http://localhost:\" + server.getPort();\n        consumer = new Consumer(proxyUrl, queueName);\n    }\n\n    @Test\n    public void testPaymentCreate() throws Exception {\n        Payment payment = new Payment();\n        payment.setAmount(5.67);\n        payment.setDescription(\"test one\");\n        Payment result = consumer.create(payment);\n        assertTrue(result.getId() > 0);\n        assertEquals(result.getAmount(), 5.67, 0);\n        assertEquals(result.getDescription(), \"test one\");\n        consumer.listen(json -> {\n            Shipment shipment = JsonUtils.fromJson(json, Shipment.class);\n            assertEquals(result.getId(), shipment.getPaymentId());\n            assertEquals(\"shipped\", shipment.getStatus());\n            synchronized (this) {\n                notify();\n            }\n        });\n        synchronized (this) {\n            wait(10000);\n        }\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n        PaymentService.stop(context);\n        consumer.stopQueueConsumer();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/ConsumerUsingProxyRewriteTest.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.core.MockServer;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass ConsumerUsingProxyRewriteTest {\n\n    static ConfigurableApplicationContext context;\n    static MockServer server;\n    static Consumer consumer;\n\n    @BeforeAll\n    static void beforeAll() {\n        // actual service      \n        String queueName = \"DEMO.PROXY.REWRITE\";\n        context = PaymentService.start(queueName, false);\n        String paymentServiceUrl = \"http://localhost:\" + PaymentService.getPort(context);\n        // proxy\n        server = MockServer\n                .feature(\"classpath:mock/contract/payment-service-proxy.feature\")\n                // requests will be forwarded / url re-written to paymentServiceUrl\n                .arg(\"paymentServiceUrl\", paymentServiceUrl)\n                .http(0).build();\n        // consumer\n        String proxyUrl = \"http://localhost:\" + server.getPort();\n        consumer = new Consumer(proxyUrl, queueName);\n    }\n\n    @Test\n    void testPaymentCreate() throws Exception {\n        Payment payment = new Payment();\n        payment.setAmount(5.67);\n        payment.setDescription(\"test one\");\n        Payment result = consumer.create(payment);\n        assertTrue(result.getId() > 0);\n        assertEquals(result.getAmount(), 5.67, 0);\n        assertEquals(result.getDescription(), \"test one\");\n        consumer.listen(json -> {\n            Shipment shipment = JsonUtils.fromJson(json, Shipment.class);\n            assertEquals(result.getId(), shipment.getPaymentId());\n            assertEquals(\"shipped\", shipment.getStatus());\n            synchronized (this) {\n                notify();\n            }\n        });\n        synchronized (this) {\n            wait(10000);\n        }\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n        PaymentService.stop(context);\n        consumer.stopQueueConsumer();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/Payment.java",
    "content": "package mock.contract;\n\n/**\n *\n * @author pthomas3\n */\npublic class Payment {\n    \n    private int id;\n    private double amount;\n    private String description;\n\n    public int getId() {\n        return id;\n    }\n\n    public void setId(int id) {\n        this.id = id;\n    }\n\n    public double getAmount() {\n        return amount;\n    }\n\n    public void setAmount(double amount) {\n        this.amount = amount;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public void setDescription(String description) {\n        this.description = description;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/PaymentService.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.JsonUtils;\nimport com.intuit.karate.demo.config.ServerStartedInitializingBean;\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Stream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.EnableAutoConfiguration;\nimport org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;\nimport org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.bind.annotation.*;\nimport org.springframework.web.server.ResponseStatusException;\n\n/**\n *\n * @author pthomas3\n */\n@Configuration\n@EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class, DataSourceAutoConfiguration.class})\npublic class PaymentService {\n\n    private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);\n\n    @Value(\"${queue.name}\")\n    private String queueName;\n\n    @RestController\n    @RequestMapping(\"/payments\")\n    class PaymentController {\n\n        private final AtomicInteger counter = new AtomicInteger();\n        private final Map<Integer, Payment> payments = new ConcurrentHashMap();\n\n        @PostMapping\n        public Payment create(@RequestBody Payment payment) {\n            int id = counter.incrementAndGet();\n            payment.setId(id);\n            payments.put(id, payment);\n            Shipment shipment = new Shipment();\n            shipment.setPaymentId(id);\n            shipment.setStatus(\"shipped\");\n            QueueUtils.send(queueName, JsonUtils.toJson(shipment), 25);\n            return payment;\n        }\n\n        @PutMapping(\"/{id:.+}\")\n        public Payment update(@PathVariable int id, @RequestBody Payment payment) {\n            payments.put(id, payment);\n            return payment;\n        }\n\n        @GetMapping\n        public Collection<Payment> list() {\n            return payments.values();\n        }\n\n        @GetMapping(\"/{id:.+}\")\n        public Payment get(@PathVariable int id) {\n            Payment payment = payments.get(id);\n            if (payment == null) {\n                throw new ResponseStatusException(HttpStatus.NOT_FOUND);\n            }\n            return payment;\n        }\n\n        @DeleteMapping(\"/{id:.+}\")\n        public void delete(@PathVariable int id) {\n            Payment payment = payments.remove(id);\n            if (payment == null) {\n                throw new RuntimeException(\"payment not found, id: \" + id);\n            }\n        }\n\n    }\n\n    public static ConfigurableApplicationContext start(String queueName, boolean ssl) {\n        return start(queueName, ssl, 0);\n    }\n\n    public static ConfigurableApplicationContext start(String queueName, boolean ssl, int port) {\n        Stream<String> args = Stream.of(\"--server.port=\" + port, \"--queue.name=\" + queueName);\n        if (ssl) {\n            args = Stream.concat(args, Stream.of(\n                    \"--server.ssl.key-store=src/test/java/server-keystore.p12\",\n                    \"--server.ssl.key-store-password=karate-mock\",\n                    \"--server.ssl.keyStoreType=PKCS12\",\n                    \"--server.ssl.keyAlias=karate-mock\"));\n        }\n        return SpringApplication.run(PaymentService.class, args.toArray(String[]::new));\n    }\n\n    public static void stop(ConfigurableApplicationContext context) {\n        SpringApplication.exit(context, () -> 0);\n    }\n\n    public static int getPort(ConfigurableApplicationContext context) {\n        ServerStartedInitializingBean ss = context.getBean(ServerStartedInitializingBean.class);\n        return ss.getLocalPort();\n    }\n\n    @Bean\n    public ServerStartedInitializingBean getInitializingBean() {\n        return new ServerStartedInitializingBean();\n    }\n\n    public static void main(String[] args) {\n        start(\"TEMP\", false, 8090);\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/PaymentServiceContractSslTest.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass PaymentServiceContractSslTest {\n    \n    static ConfigurableApplicationContext context;\n    static String queueName = \"DEMO.CONTRACT.SSL\";\n    \n    @BeforeAll\n    static void beforeAll() {   \n        context = PaymentService.start(queueName, true);\n    }\n    \n    @Test\n    void testPaymentService() {\n        String paymentServiceUrl = \"https://localhost:\" + PaymentService.getPort(context);      \n        Results results = Runner.path(\"classpath:mock/contract/payment-service.feature\")\n                .configDir(\"classpath:mock/contract\")\n                .systemProperty(\"payment.service.url\", paymentServiceUrl)\n                .systemProperty(\"shipping.queue.name\", queueName)\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());        \n    }\n    \n    @AfterAll\n    static void afterAll() {\n        PaymentService.stop(context);\n    }\n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/PaymentServiceContractTest.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass PaymentServiceContractTest {\n    \n    static ConfigurableApplicationContext context;\n    static String queueName = \"DEMO.CONTRACT\";\n    \n    @BeforeAll\n    static void beforeAll() {\n        context = PaymentService.start(queueName, false);\n    }\n    \n    @Test\n    void testPaymentService() {\n        String paymentServiceUrl = \"http://localhost:\" + PaymentService.getPort(context);      \n        Results results = Runner.path(\"classpath:mock/contract/payment-service.feature\")\n                .configDir(\"classpath:mock/contract\")\n                .systemProperty(\"payment.service.url\", paymentServiceUrl)\n                .systemProperty(\"shipping.queue.name\", queueName)\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());        \n    }    \n    \n    @AfterAll\n    static void afterAll() {\n        PaymentService.stop(context);\n    }\n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/PaymentServiceContractUsingMockSslTest.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.MockServer;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass PaymentServiceContractUsingMockSslTest {\n\n    static MockServer server;\n    static String queueName = \"DEMO.CONTRACT.MOCK.SSL\";\n\n    @BeforeAll\n    static void beforeAll() {\n        server = MockServer\n                .feature(\"classpath:mock/contract/payment-service-mock.feature\")\n                .arg(\"queueName\", queueName)\n                .https(0).build();\n    }\n    \n    // @Test // TODO jdk 17\n    void testPaymentService() {\n        String paymentServiceUrl = \"https://localhost:\" + server.getPort();      \n        Results results = Runner.path(\"classpath:mock/contract/payment-service.feature\")\n                .configDir(\"classpath:mock/contract\")\n                .systemProperty(\"payment.service.url\", paymentServiceUrl)\n                .systemProperty(\"shipping.queue.name\", queueName)\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());        \n    }     \n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/PaymentServiceContractUsingMockTest.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.MockServer;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass PaymentServiceContractUsingMockTest {\n\n    static MockServer server;\n    static String queueName = \"DEMO.CONTRACT.MOCK\";\n\n    @BeforeAll\n    static void beforeAll() {\n        server = MockServer\n                .feature(\"classpath:mock/contract/payment-service-mock.feature\")\n                .arg(\"queueName\", queueName)\n                .http(0).build();\n    }\n    \n    @Test\n    void testPaymentService() {\n        String paymentServiceUrl = \"http://localhost:\" + server.getPort();      \n        Results results = Runner.path(\"classpath:mock/contract/payment-service.feature\")\n                .configDir(\"classpath:mock/contract\")\n                .systemProperty(\"payment.service.url\", paymentServiceUrl)\n                .systemProperty(\"shipping.queue.name\", queueName)\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());        \n    }     \n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/PaymentServiceMockMain.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.core.MockServer;\n\n/**\n *\n * @author pthomas3\n */\npublic class PaymentServiceMockMain {\n\n    public static void main(String[] args) {\n        MockServer server = MockServer\n                .feature(\"classpath:mock/contract/payment-service-mock.feature\")\n                .arg(\"queueName\", \"DEMO.MOCK.8080\")\n                .http(8080).build();\n        server.waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/PaymentServiceMockSslMain.java",
    "content": "package mock.contract;\n\nimport com.intuit.karate.core.MockServer;\nimport java.io.File;\n\n/**\n *\n * @author pthomas3\n */\npublic class PaymentServiceMockSslMain {\n\n    public static void main(String[] args) {\n        File certFile = new File(\"src/test/java/mock-cert.pem\");\n        File privateKeyFile = new File(\"src/test/java/mock-key.pem\");\n        MockServer server = MockServer\n                .feature(\"classpath:mock/contract/payment-service-mock.feature\")\n                .certFile(certFile)\n                .keyFile(privateKeyFile)\n                .arg(\"queueName\", \"DEMO.MOCK.8443\")\n                .https(8443).build();\n        server.waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/QueueConsumer.java",
    "content": "package mock.contract;\n\nimport javax.jms.Connection;\nimport javax.jms.Destination;\nimport javax.jms.JMSException;\nimport javax.jms.Message;\nimport javax.jms.MessageConsumer;\nimport javax.jms.MessageListener;\nimport javax.jms.Session;\nimport javax.jms.TextMessage;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class QueueConsumer {\n\n    private static final Logger logger = LoggerFactory.getLogger(QueueConsumer.class);\n\n    private final Connection connection;\n    private final MessageConsumer consumer;\n    private final String queueName;\n    private final Session session;\n\n    public QueueConsumer(String queueName) {\n        this.queueName = queueName;\n        this.connection = QueueUtils.getConnection();\n        try {\n            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);\n            Destination destination = session.createQueue(queueName);\n            consumer = session.createConsumer(destination);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void listen(java.util.function.Consumer<String> handler) {\n        setMessageListener(message -> {\n            TextMessage tm = (TextMessage) message;\n            try {\n                handler.accept(tm.getText());\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        });\n    }   \n\n    public void setMessageListener(MessageListener ml) {\n        try {\n            consumer.setMessageListener(ml);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public String waitForNextMessage() {\n        try {\n            TextMessage tm = (TextMessage) consumer.receive();\n            return tm.getText();\n        } catch (JMSException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void purgeMessages() {\n        try {\n            consumer.setMessageListener(null);\n            while (true) {\n                Message message = consumer.receive(50);\n                if (message == null) {\n                    logger.info(\"*** no more messages to purge: {}\", queueName);\n                    break;\n                }\n                logger.info(\"*** purged message: {} - {}\", queueName, message);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void stop() {\n        try {\n            consumer.close();\n            session.close();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/QueueUtils.java",
    "content": "package mock.contract;\n\nimport javax.jms.Connection;\nimport javax.jms.DeliveryMode;\nimport javax.jms.Destination;\nimport javax.jms.MessageProducer;\nimport javax.jms.Session;\nimport javax.jms.TextMessage;\nimport org.apache.activemq.ActiveMQConnectionFactory;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class QueueUtils {\n\n    private static final Logger logger = LoggerFactory.getLogger(QueueUtils.class);\n\n    private static final Connection connection;\n\n    public static Connection getConnection() {\n        return connection;\n    }        \n\n    static {\n        try {\n            logger.debug(\"waiting for activemq connection ...\");\n            ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(\"vm://localhost?broker.persistent=false&waitForStart=10000\");\n            connection = connectionFactory.createConnection();\n            connection.start();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void send(String queueName, String text, int delayMillis) {\n         new Thread(() -> {\n            try {\n                logger.info(\"*** artificial delay {}: {}\", queueName, delayMillis);\n                Thread.sleep(delayMillis);\n                Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);\n                Destination destination = session.createQueue(queueName);\n                MessageProducer producer = session.createProducer(destination);\n                producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);\n                TextMessage message = session.createTextMessage(text);\n                producer.send(message);\n                logger.info(\"*** sent message {}: {}\", queueName, text);\n                session.close();\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n         }).start();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/QueueUtilsTest.java",
    "content": "package mock.contract;\n\nimport javax.jms.JMSException;\nimport javax.jms.TextMessage;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass QueueUtilsTest {\n    \n    static final Logger logger = LoggerFactory.getLogger(QueueUtilsTest.class);\n\n    boolean passed = false;\n\n    @Test\n    void testQueueOperations() throws Exception {\n        String queueName = \"DEMO.TEST\";\n        QueueUtils.send(queueName, \"first\", 0);\n        QueueConsumer consumer = new QueueConsumer(queueName);\n        String text = consumer.waitForNextMessage();\n        assertEquals(\"first\", text);\n        QueueUtils.send(queueName, \"second\", 0);\n        QueueUtils.send(queueName, \"third\", 0);\n        consumer.purgeMessages();\n        QueueUtils.send(queueName, \"foo\", 25);\n        consumer.setMessageListener(m -> {\n            TextMessage tm = (TextMessage) m;\n            try {\n                logger.info(\"*** received message: {}\", tm.getText());\n                assertEquals(\"foo\", tm.getText());\n                passed = true;\n                synchronized (consumer) {\n                    consumer.notify();\n                }\n            } catch (JMSException e) {\n                throw new RuntimeException(e);\n            }\n        });\n        synchronized (consumer) {\n            consumer.wait(10000);\n        }\n        assertTrue(passed);\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/Shipment.java",
    "content": "package mock.contract;\n\n/**\n *\n * @author pthomas3\n */\npublic class Shipment {\n\n    private int paymentId;\n    private String status;\n\n    public int getPaymentId() {\n        return paymentId;\n    }\n\n    public void setPaymentId(int paymentId) {\n        this.paymentId = paymentId;\n    }\n\n    public String getStatus() {\n        return status;\n    }\n\n    public void setStatus(String status) {\n        this.status = status;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/increment.js",
    "content": "function fn() {\n  karate.set('_curId', 0);\n  return function() {\n    var curId = karate.get('_curId');\n    var nextId = curId + 1;\n    karate.set('_curId', nextId);\n    return ~~nextId;\n  }\n}  \n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/karate-config.js",
    "content": "function fn() {\n  var config = {};\n  config.paymentServiceUrl = karate.properties['payment.service.url'];    \n  config.queueName = karate.properties['shipping.queue.name'];\n    if (config.paymentServiceUrl.startsWith('https')) {\n      if (config.queueName == 'DEMO.CONTRACT.SSL') {\n        karate.configure('ssl', { trustStore: 'classpath:server-keystore.p12', password: 'karate-mock', type: 'pkcs12' });\n      } else {\n        karate.configure('ssl', true);\n      }\n    }\n  return config;\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/payment-service-mock.feature",
    "content": "Feature: payment service mock\n\nBackground:\n* def nextId = call read('increment.js')\n* def payments = {}\n* def QueueUtils = Java.type('mock.contract.QueueUtils')\n* configure cors = true\n\nScenario: pathMatches('/payments') && methodIs('post')\n    * def payment = request\n    * def id = nextId()\n    * set payment.id = id\n    * payments[id + ''] = payment\n    * def response = payment\n    * string json  = { paymentId: '#(id)', status: 'shipped' }\n    * QueueUtils.send(queueName, json, 25)    \n\nScenario: pathMatches('/payments')\n    * def response = $payments.*\n\nScenario: pathMatches('/payments/{id}') && methodIs('put')\n    * payments[pathParams.id] = request\n    * def response = request\n\nScenario: pathMatches('/payments/{id}') && methodIs('delete')\n    * karate.remove('payments', pathParams.id)\n\nScenario: pathMatches('/payments/{id}')\n    * def response = payments[pathParams.id]\n\nScenario: pathMatches('/')\n    * def responseHeaders = { 'Content-Type': 'text/html' }\n    * def response = read('payments.html')\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/payment-service-proxy.feature",
    "content": "Feature: payment service proxy (or api-gateway !)\n\nBackground:\n* if (paymentServiceUrl && paymentServiceUrl.startsWith('https')) karate.configure('ssl', true)\n\nScenario: pathMatches('/payments') && methodIs('post')\n    * karate.proceed(paymentServiceUrl)\n    # example of adding delay via a post-processing hook\n    * def responseDelay = 3000\n\nScenario: pathMatches('/payments')\n    * karate.proceed(paymentServiceUrl)\n    * def responseDelay = 200 + Math.random() * 400\n\nScenario: pathMatches('/payments/{id}') && methodIs('delete')\n    * karate.proceed(paymentServiceUrl)\n\nScenario: pathMatches('/payments/{id}')    \n    * karate.proceed(paymentServiceUrl)\n\n# 'catch-all' rule\nScenario:  \n    # if arg to karate.proceed() is null, incoming url will be used as-is (http proxy)\n    * karate.proceed(paymentServiceUrl)\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/payment-service.feature",
    "content": "Feature: payment service\n\nBackground:\n* def QueueConsumer = Java.type('mock.contract.QueueConsumer')\n* def queue = new QueueConsumer(queueName)\n* def handler = function(msg){ karate.signal(msg) }\n* queue.listen(karate.toJava(handler))\n* url paymentServiceUrl + '/payments'\n\nScenario: create, get, update, list and delete payments\n    Given request { amount: 5.67, description: 'test one' }\n    When method post\n    Then status 200\n    And match response == { id: '#number', amount: 5.67, description: 'test one' }\n    And def id = response.id\n    * listen 5000\n    * json shipment = listenResult\n    * print '### received:', listenResult\n    * match shipment == { paymentId: '#(id)', status: 'shipped' }\n\n    Given path id\n    When method get\n    Then status 200\n    And match response == { id: '#(id)', amount: 5.67, description: 'test one' }\n\n    Given path id\n    And request { id: '#(id)', amount: 5.67, description: 'test two' }\n    When method put\n    Then status 200\n    And match response == { id: '#(id)', amount: 5.67, description: 'test two' }\n\n    When method get\n    Then status 200\n    And match response contains { id: '#(id)', amount: 5.67, description: 'test two' }\n\n    Given path id\n    When method delete\n    Then status 200\n\n    When method get\n    Then status 200\n    And match response !contains { id: '#(id)', amount: '#number', description: '#string' }\n    "
  },
  {
    "path": "karate-demo/src/test/java/mock/contract/payments.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Payments</title>\n    <script src=\"https://code.jquery.com/jquery-3.2.1.min.js\" integrity=\"sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=\" crossorigin=\"anonymous\"></script>\n  </head>\n  <body>\n    <div><a href=\"/payments\">Get Payments</a></div>\n    <button onclick=\"createPayment()\">Create</button>\n    <div id=\"response\"></div>\n    <script>\n      var createPayment = function() {\n        $.ajax({          \n          url: '/payments',\n          data: JSON.stringify({ amount: 5.67, description: 'test one' }),\n          method: 'POST',\n          success: function(result) {\n            $('#response').html('<strong>' + result.id + '</strong> created');\n          }\n        });\n      };\n    </script>    \n</body>\n</html>\n\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/micro/CatsMockRunner.java",
    "content": "package mock.micro;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.MockServer;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n/**\n *\n * @author pthomas3\n */\nclass CatsMockRunner {\n\n    static MockServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        server = MockServer\n                .feature(\"classpath:mock/micro/cats-mock.feature\")\n                .arg(\"demoServerPort\", null)\n                .http(0).build();\n    }\n\n    @Test\n    void testMock() {\n        Results results = Runner.path(\"classpath:mock/micro/cats.feature\")\n                .karateEnv(\"mock\")\n                .systemProperty(\"mock.cats.url\", \"http://localhost:\" + server.getPort() + \"/cats\")\n                .parallel(1);\n        assertTrue( results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/micro/cats-mock.feature",
    "content": "Feature:\n\nBackground:\n* def id = 0\n* def m = {}\n\nScenario: methodIs('post')\n* def c = request\n* def id = ~~(id + 1)\n* c.id = id\n* m[id + ''] = c\n* def response = c\n\nScenario: pathMatches('/cats/{id}')\n* def response = m[pathParams.id]\n\nScenario:\n* def response = $m.*"
  },
  {
    "path": "karate-demo/src/test/java/mock/micro/cats.feature",
    "content": "Feature: cats integration test\n\nBackground:\n    * url karate.properties['mock.cats.url']\n\nScenario: create cat\n    Given request { name: 'Billie' }\n    When method post\n    Then status 200    \n    And match response == { id: 1, name: 'Billie' }\n\n    Given path 1\n    When method get\n    Then status 200\n    And match response == { id: 1, name: 'Billie' }\n\n    When method get\n    Then status 200\n    And match response == [{ id: 1, name: 'Billie' }]\n\n    Given request { name: 'Bob' }\n    When method post\n    Then status 200    \n    And match response == { id: 2, name: 'Bob' }\n\n    Given path 2\n    When method get\n    Then status 200\n    And match response == { id: 2, name: 'Bob' }\n\n    When method get\n    Then status 200\n    And match response == [{ id: 1, name: 'Billie' },{ id: 2, name: 'Bob' }]\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/proxy/DemoMockProceedRunner.java",
    "content": "package mock.proxy;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.Results;\nimport com.intuit.karate.core.MockServer;\nimport demo.TestBase;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass DemoMockProceedRunner {\n\n    static MockServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        int port = TestBase.startServer();\n        server = MockServer\n                .feature(\"classpath:mock/proxy/demo-mock-proceed.feature\")\n                .arg(\"demoServerPort\", port)\n                .http(0).build();\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n    }\n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:demo/cats\", \"classpath:demo/greeting\")\n                .configDir(\"classpath:mock/proxy\")\n                .systemProperty(\"demo.server.port\", server.getPort() + \"\")\n                .systemProperty(\"demo.server.https\", \"false\")\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/proxy/DemoMockProxyRunner.java",
    "content": "package mock.proxy;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.Results;\nimport com.intuit.karate.core.MockServer;\nimport demo.TestBase;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass DemoMockProxyRunner {\n\n    static MockServer server;\n    static int demoServerPort;\n\n    @BeforeAll\n    static void beforeAll() throws Exception {\n        demoServerPort = TestBase.startServer();\n        server = MockServer\n                .feature(\"classpath:mock/proxy/demo-mock-proceed.feature\")\n                .arg(\"demoServerPort\", null) // don't rewrite url\n                .http(0).build();\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n    }\n\n    // @Test // TODO armeria upgrade\n    void testParallel() {\n        Results results = Runner.path(\"classpath:demo/cats\", \"classpath:demo/greeting\")\n                .configDir(\"classpath:mock/proxy\")\n                .systemProperty(\"demo.server.port\", demoServerPort + \"\")\n                .systemProperty(\"demo.proxy.port\", server.getPort() + \"\")\n                .systemProperty(\"demo.server.https\", \"false\")\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/proxy/DemoMockProxySslRunner.java",
    "content": "package mock.proxy;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.Results;\nimport com.intuit.karate.core.MockServer;\nimport demo.TestBase;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass DemoMockProxySslRunner {\n\n    static MockServer server;\n    static int demoServerPort;\n\n    @BeforeAll\n    static void beforeAll() {\n        demoServerPort = TestBase.startServer();\n        server = MockServer\n                .feature(\"classpath:mock/proxy/demo-mock-proceed.feature\")\n                .arg(\"demoServerPort\", null) // don't rewrite url\n                .https(0).build();\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n    }\n\n    // @Test TODO SSL proxy\n    void testParallel() {\n        Results results = Runner.path(\"classpath:demo/cats\", \"classpath:demo/greeting\")\n                .configDir(\"classpath:mock/proxy\")\n                .systemProperty(\"demo.server.port\", demoServerPort + \"\")\n                .systemProperty(\"demo.proxy.port\", server.getPort() + \"\")\n                .systemProperty(\"demo.server.https\", \"true\")\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/proxy/DemoMockRunner.java",
    "content": "package mock.proxy;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.Results;\nimport com.intuit.karate.core.MockServer;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass DemoMockRunner {\n\n    static MockServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        server = MockServer.feature(\"classpath:mock/proxy/demo-mock.feature\").http(0).build();\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n    }\n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:demo/cats\", \"classpath:demo/greeting\")\n                .configDir(\"classpath:mock/proxy\")\n                .systemProperty(\"demo.server.port\", server.getPort() + \"\")\n                .systemProperty(\"demo.server.https\", \"false\")\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/proxy/DemoMockSslRunner.java",
    "content": "package mock.proxy;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.Results;\nimport com.intuit.karate.core.MockServer;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass DemoMockSslRunner {\n\n    static MockServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        server = MockServer.feature(\"classpath:mock/proxy/demo-mock.feature\").https(0).build();\n    }\n\n    @AfterAll\n    static void afterAll() {\n        server.stop();\n    }\n\n    // @Test TODO investigate CI troubles\n    void testParallel() {\n        Results results = Runner.path(\"classpath:demo/cats\", \"classpath:demo/greeting\")\n                .configDir(\"classpath:mock/proxy\")\n                .systemProperty(\"demo.server.port\", server.getPort() + \"\")\n                .systemProperty(\"demo.server.https\", \"true\")\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/proxy/demo-mock-proceed.feature",
    "content": "Feature: mock that simply forwards to actual host\n\nBackground:\n* def cats = {}\n# if argument to karate.proceed() is null, url of incoming request is used (no url re-writing)\n* def targetUrlBase = demoServerPort ? 'http://127.0.0.1:' + demoServerPort : null\n* print 'init target url:', targetUrlBase\n\nScenario: pathMatches('/greeting') && paramExists('name')\n* print '*** param exists: name', targetUrlBase\n* requestHeaders['host'] = 'myhost:123'\n* karate.proceed(targetUrlBase)\n\n# 'catch-all' rule\nScenario:\n* karate.proceed(targetUrlBase)\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/proxy/demo-mock.feature",
    "content": "Feature: stateful mock server\n\nBackground:\n* def curId = 0\n* def nextId = function(){ return ~~curId++ }\n* def cats = {}\n\nScenario: pathMatches('/greeting') && paramExists('name')\n    * def content = 'Hello ' + paramValue('name') + '!'\n    * def response = { id: '#(nextId())', content: '#(content)' }\n\nScenario: pathMatches('/greeting')\n    * def response = { id: '#(nextId())', content: 'Hello World!' }\n\nScenario: pathMatches('/cats') && methodIs('post') && typeContains('xml')\n    * def cat = request    \n    * def id = nextId()\n    * set cat /cat/id = id    \n    * set catJson\n        | path | value        |\n        | id   | id           |\n        | name | cat.cat.name |\n    * cats[id + ''] = catJson\n    * def response = cat\n\nScenario: pathMatches('/cats') && methodIs('post')\n    * def cat = request\n    * def id = nextId()\n    * set cat.id = id\n    * cats[id + ''] = cat\n    * def response = cat\n\nScenario: pathMatches('/cats')\n    * def response = $cats.*\n\nScenario: pathMatches('/cats/{id}') && methodIs('put')\n    * def cat = request\n    * def id = pathParams.id\n    * cats[id + ''] = cat\n    * def response = cat\n\nScenario: pathMatches('/cats/{id}') && acceptContains('xml')\n    * def cat = cats[pathParams.id]\n    * def response = <cat><id>#(cat.id)</id><name>#(cat.name)</name></cat>\n\nScenario: pathMatches('/cats/{id}')\n    * def response = cats[pathParams.id]\n\nScenario: pathMatches('/cats/{id}/kittens')\n    * def response = cats[pathParams.id].kittens\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/proxy/karate-config.js",
    "content": "function fn() { \n  var port = karate.properties['demo.server.port'] || '8080';\n  var protocol = 'http';\n  if (karate.properties['demo.server.https'] === 'true') {\n    protocol = 'https';\n    karate.configure('ssl', true);\n  }  \n  var config = { demoBaseUrl: protocol + '://127.0.0.1:' + port };\n  karate.log('demoBaseUrl:', config.demoBaseUrl);\n  var proxyPort = karate.properties['demo.proxy.port'];\n  if (proxyPort) {\n    karate.configure('proxy', 'http://127.0.0.1:' + proxyPort);\n  }\n  return config;\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/web/CatsMockRunner.java",
    "content": "package mock.web;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass CatsMockRunner {\n\n    @Test\n    void testParallel() {\n        Results results = Runner.path(\"classpath:mock/web/cats-test.feature\")\n                .karateEnv(\"mock\")\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/web/CatsMockStarter.java",
    "content": "package mock.web;\n\nimport com.intuit.karate.core.MockServer;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass CatsMockStarter {\n\n    @Test\n    void testStart() {\n        MockServer server = MockServer.feature(\"classpath:mock/web/cats-mock.feature\").http(8080).build();\n        server.waitSync();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/web/CatsTestRunner.java",
    "content": "package mock.web;\n\nimport com.intuit.karate.Runner;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass CatsTestRunner {\n    \n    @Test\n    void testMockOnPort8080() {\n        Runner.runFeature(getClass(), \"cats-test.feature\", null, false);\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/web/cats-mock.feature",
    "content": "Feature: stateful mock server\n\nBackground:\n* def uuid = function(){ return java.util.UUID.randomUUID() + '' }\n* def cats = {}\n* configure cors = true\n\nScenario: pathMatches('/cats') && methodIs('post')\n    * def cat = request\n    * def id = uuid()\n    * cat.id = id\n    * cats[id] = cat\n    * def response = cat\n\nScenario: pathMatches('/cats')\n    * def response = $cats.*\n\nScenario: pathMatches('/cats/{id}')\n    * def response = cats[pathParams.id]\n\nScenario: pathMatches('/hardcoded')\n    * def response = { hello: 'world' }\n\nScenario:\n    # catch-all\n    * def responseStatus = 404\n    * def responseHeaders = { 'Content-Type': 'text/html; charset=utf-8' }\n    * def response = <html><body>Not Found</body></html>\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/web/cats-test.feature",
    "content": "Feature: integration test\n\nBackground:\n    * def port = karate.env == 'mock' ? karate.start('cats-mock.feature').port : 8080\n    * url 'http://localhost:' + port + '/cats'\n\nScenario: create cat\n    Given request { name: 'Billie' }\n    When method post\n    Then status 200    \n    And match response == { id: '#uuid', name: 'Billie' }\n    And def id = response.id\n\n    Given path id\n    When method get\n    Then status 200\n    And match response == { id: '#(id)', name: 'Billie' }\n\n    When method get\n    Then status 200\n    And match response contains [{ id: '#(id)', name: 'Billie' }]\n\n    Given request { name: 'Bob' }\n    When method post\n    Then status 200    \n    And match response == { id: '#uuid', name: 'Bob' }\n    And def id = response.id\n\n    Given path id\n    When method get\n    Then status 200\n    And match response == { id: '#(id)', name: 'Bob' }\n\n    When method get\n    Then status 200\n    And match response contains [{ id: '#uuid', name: 'Billie' },{ id: '#(id)', name: 'Bob' }]\n"
  },
  {
    "path": "karate-demo/src/test/java/mock/web/cats.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Cats</title>\n    <script src=\"https://code.jquery.com/jquery-3.2.1.min.js\" integrity=\"sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=\" crossorigin=\"anonymous\"></script>\n  </head>\n  <body>\n    <div>\n      <a href=\"http://localhost:8080/cats\">GET /cats</a> | <a href=\"http://localhost:8080/hardcoded\">GET /hardcoded</a>\n    </div>\n    <p/>    \n    <div>\n      <label for=\"catName\">Name: <input id=\"catName\" value=\"Billie\"/></label>\n      <button onclick=\"createCat()\">Create Cat</button>\n    </div>    \n    <p/>\n    <div id=\"response\"></div>\n    <script>\n      var createCat = function() {\n        var name = $('#catName').val();\n        $.ajax({          \n          url: 'http://localhost:8080/cats',\n          data: JSON.stringify({ name: name }),\n          method: 'POST',\n          success: function(result) {\n            $('#response').html('created cat: ' + JSON.stringify(result));\n          }\n        });\n      };\n    </script>    \n  </body>\n</html>\n\n"
  },
  {
    "path": "karate-demo/src/test/java/mock-cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBqTCCARKgAwIBAgIIT0xFd/5uogEwDQYJKoZIhvcNAQEFBQAwFjEUMBIGA1UEAxMLZXhhbXBs\nZS5jb20wIBcNMTcwMTIwMTczOTIwWhgPOTk5OTEyMzEyMzU5NTlaMBYxFDASBgNVBAMTC2V4YW1w\nbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2Tl2MdaUFmjAaYwmEwgEVRfVqwJO4\nY+7Vxm4UqQRKNucpGUwUBo9FSvuQACpnJwHsK2WhiuSpVkunhmSx5Qb4KVSH2RT2vHBUsA3t12S2\n1Vkskiya3E7QR91zZGVxZyB4gSBVhvSVXeP9+RogLLziki/VDXXKT4TIuyML1eUQ2QIDAQABMA0G\nCSqGSIb3DQEBBQUAA4GBAGfw0xavZSJXxuFAwxCZBtne9BAtk+SmfKkTI21v8Tx6w/p5Yt0IIvF3\n0wCES7YVZ+zUc8vtVVyk1q3f1ZqXqVvzRCjzLzQnu6VVLBaiZPH9SYNX6j0pHhBvx1ZUMopJPr2D\navTXCTSHY5JoX20KEwfu8QQXQRDUzyc0QKn9SiE3\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "karate-demo/src/test/java/mock-contract.jmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<jmeterTestPlan version=\"1.2\" properties=\"5.0\" jmeter=\"5.3\">\n  <hashTree>\n    <TestPlan guiclass=\"TestPlanGui\" testclass=\"TestPlan\" testname=\"Test Plan\" enabled=\"true\">\n      <stringProp name=\"TestPlan.comments\"></stringProp>\n      <boolProp name=\"TestPlan.functional_mode\">false</boolProp>\n      <boolProp name=\"TestPlan.serialize_threadgroups\">false</boolProp>\n      <elementProp name=\"TestPlan.user_defined_variables\" elementType=\"Arguments\" guiclass=\"ArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n        <collectionProp name=\"Arguments.arguments\"/>\n      </elementProp>\n      <stringProp name=\"TestPlan.user_define_classpath\"></stringProp>\n    </TestPlan>\n    <hashTree>\n      <ThreadGroup guiclass=\"ThreadGroupGui\" testclass=\"ThreadGroup\" testname=\"Thread Group\" enabled=\"true\">\n        <stringProp name=\"ThreadGroup.on_sample_error\">continue</stringProp>\n        <elementProp name=\"ThreadGroup.main_controller\" elementType=\"LoopController\" guiclass=\"LoopControlPanel\" testclass=\"LoopController\" testname=\"Loop Controller\" enabled=\"true\">\n          <boolProp name=\"LoopController.continue_forever\">false</boolProp>\n          <stringProp name=\"LoopController.loops\">1</stringProp>\n        </elementProp>\n        <stringProp name=\"ThreadGroup.num_threads\">10</stringProp>\n        <stringProp name=\"ThreadGroup.ramp_time\">1</stringProp>\n        <longProp name=\"ThreadGroup.start_time\">1514991895000</longProp>\n        <longProp name=\"ThreadGroup.end_time\">1514991895000</longProp>\n        <boolProp name=\"ThreadGroup.scheduler\">false</boolProp>\n        <stringProp name=\"ThreadGroup.duration\"></stringProp>\n        <stringProp name=\"ThreadGroup.delay\"></stringProp>\n        <boolProp name=\"ThreadGroup.same_user_on_next_iteration\">true</boolProp>\n      </ThreadGroup>\n      <hashTree>\n        <ConfigTestElement guiclass=\"HttpDefaultsGui\" testclass=\"ConfigTestElement\" testname=\"HTTP Request Defaults\" enabled=\"true\">\n          <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\" guiclass=\"HTTPArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n            <collectionProp name=\"Arguments.arguments\"/>\n          </elementProp>\n          <stringProp name=\"HTTPSampler.domain\">localhost</stringProp>\n          <stringProp name=\"HTTPSampler.port\">8080</stringProp>\n          <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n          <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n          <stringProp name=\"HTTPSampler.path\"></stringProp>\n          <stringProp name=\"HTTPSampler.concurrentPool\">6</stringProp>\n          <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n          <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n        </ConfigTestElement>\n        <hashTree/>\n        <LoopController guiclass=\"LoopControlPanel\" testclass=\"LoopController\" testname=\"Loop Controller\" enabled=\"true\">\n          <boolProp name=\"LoopController.continue_forever\">true</boolProp>\n          <stringProp name=\"LoopController.loops\">100</stringProp>\n        </LoopController>\n        <hashTree>\n          <CounterConfig guiclass=\"CounterConfigGui\" testclass=\"CounterConfig\" testname=\"Counter\" enabled=\"true\">\n            <stringProp name=\"CounterConfig.start\"></stringProp>\n            <stringProp name=\"CounterConfig.end\"></stringProp>\n            <stringProp name=\"CounterConfig.incr\">1</stringProp>\n            <stringProp name=\"CounterConfig.name\">loopCount</stringProp>\n            <stringProp name=\"CounterConfig.format\"></stringProp>\n            <boolProp name=\"CounterConfig.per_user\">false</boolProp>\n          </CounterConfig>\n          <hashTree/>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"post\" enabled=\"true\">\n            <boolProp name=\"HTTPSampler.postBodyRaw\">true</boolProp>\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\">\n              <collectionProp name=\"Arguments.arguments\">\n                <elementProp name=\"\" elementType=\"HTTPArgument\">\n                  <boolProp name=\"HTTPArgument.always_encode\">false</boolProp>\n                  <stringProp name=\"Argument.value\">{ &quot;amount&quot;: ${loopCount}.${__threadNum}, &quot;description&quot;: &quot;before&quot; }</stringProp>\n                  <stringProp name=\"Argument.metadata\">=</stringProp>\n                </elementProp>\n              </collectionProp>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">payments</stringProp>\n            <stringProp name=\"HTTPSampler.method\">POST</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree>\n            <JSONPostProcessor guiclass=\"JSONPostProcessorGui\" testclass=\"JSONPostProcessor\" testname=\"JSON Extractor\" enabled=\"true\">\n              <stringProp name=\"JSONPostProcessor.referenceNames\">paymentId</stringProp>\n              <stringProp name=\"JSONPostProcessor.jsonPathExprs\">$.id</stringProp>\n              <stringProp name=\"JSONPostProcessor.match_numbers\"></stringProp>\n              <stringProp name=\"Scope.variable\">paymentId</stringProp>\n            </JSONPostProcessor>\n            <hashTree/>\n          </hashTree>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"get before\" enabled=\"true\">\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\" guiclass=\"HTTPArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n              <collectionProp name=\"Arguments.arguments\"/>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">payments/${paymentId}</stringProp>\n            <stringProp name=\"HTTPSampler.method\">GET</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree/>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"put\" enabled=\"true\">\n            <boolProp name=\"HTTPSampler.postBodyRaw\">true</boolProp>\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\">\n              <collectionProp name=\"Arguments.arguments\">\n                <elementProp name=\"\" elementType=\"HTTPArgument\">\n                  <boolProp name=\"HTTPArgument.always_encode\">false</boolProp>\n                  <stringProp name=\"Argument.value\">{ id: ${paymentId}, &quot;amount&quot;: ${loopCount}.${__threadNum}, &quot;description&quot;: &quot;after&quot; }</stringProp>\n                  <stringProp name=\"Argument.metadata\">=</stringProp>\n                </elementProp>\n              </collectionProp>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">payments/${paymentId}</stringProp>\n            <stringProp name=\"HTTPSampler.method\">PUT</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree/>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"get after\" enabled=\"true\">\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\" guiclass=\"HTTPArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n              <collectionProp name=\"Arguments.arguments\"/>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">payments/${paymentId}</stringProp>\n            <stringProp name=\"HTTPSampler.method\">GET</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree>\n            <ResponseAssertion guiclass=\"AssertionGui\" testclass=\"ResponseAssertion\" testname=\"Response Assertion\" enabled=\"true\">\n              <collectionProp name=\"Asserion.test_strings\">\n                <stringProp name=\"-1309830040\">&quot;after&quot;</stringProp>\n                <stringProp name=\"0\"></stringProp>\n              </collectionProp>\n              <stringProp name=\"Assertion.test_field\">Assertion.response_data</stringProp>\n              <boolProp name=\"Assertion.assume_success\">false</boolProp>\n              <intProp name=\"Assertion.test_type\">2</intProp>\n              <stringProp name=\"Assertion.custom_message\"></stringProp>\n            </ResponseAssertion>\n            <hashTree/>\n          </hashTree>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"delete\" enabled=\"true\">\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\" guiclass=\"HTTPArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n              <collectionProp name=\"Arguments.arguments\"/>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">payments/${paymentId}</stringProp>\n            <stringProp name=\"HTTPSampler.method\">DELETE</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree/>\n          <HTTPSamplerProxy guiclass=\"HttpTestSampleGui\" testclass=\"HTTPSamplerProxy\" testname=\"get all\" enabled=\"true\">\n            <elementProp name=\"HTTPsampler.Arguments\" elementType=\"Arguments\" guiclass=\"HTTPArgumentsPanel\" testclass=\"Arguments\" testname=\"User Defined Variables\" enabled=\"true\">\n              <collectionProp name=\"Arguments.arguments\"/>\n            </elementProp>\n            <stringProp name=\"HTTPSampler.domain\"></stringProp>\n            <stringProp name=\"HTTPSampler.port\"></stringProp>\n            <stringProp name=\"HTTPSampler.protocol\"></stringProp>\n            <stringProp name=\"HTTPSampler.contentEncoding\"></stringProp>\n            <stringProp name=\"HTTPSampler.path\">payments</stringProp>\n            <stringProp name=\"HTTPSampler.method\">GET</stringProp>\n            <boolProp name=\"HTTPSampler.follow_redirects\">true</boolProp>\n            <boolProp name=\"HTTPSampler.auto_redirects\">false</boolProp>\n            <boolProp name=\"HTTPSampler.use_keepalive\">true</boolProp>\n            <boolProp name=\"HTTPSampler.DO_MULTIPART_POST\">false</boolProp>\n            <stringProp name=\"HTTPSampler.embedded_url_re\"></stringProp>\n            <stringProp name=\"HTTPSampler.connect_timeout\"></stringProp>\n            <stringProp name=\"HTTPSampler.response_timeout\"></stringProp>\n          </HTTPSamplerProxy>\n          <hashTree>\n            <ResponseAssertion guiclass=\"AssertionGui\" testclass=\"ResponseAssertion\" testname=\"Response Assertion\" enabled=\"true\">\n              <collectionProp name=\"Asserion.test_strings\">\n                <stringProp name=\"1564264776\">&quot;id&quot;:${paymentId}</stringProp>\n              </collectionProp>\n              <stringProp name=\"Assertion.test_field\">Assertion.response_data</stringProp>\n              <boolProp name=\"Assertion.assume_success\">false</boolProp>\n              <intProp name=\"Assertion.test_type\">6</intProp>\n              <stringProp name=\"Scope.variable\">foundArray</stringProp>\n              <stringProp name=\"Assertion.custom_message\"></stringProp>\n            </ResponseAssertion>\n            <hashTree/>\n          </hashTree>\n        </hashTree>\n      </hashTree>\n      <ResultCollector guiclass=\"ViewResultsFullVisualizer\" testclass=\"ResultCollector\" testname=\"View Results Tree\" enabled=\"false\">\n        <boolProp name=\"ResultCollector.error_logging\">false</boolProp>\n        <objProp>\n          <name>saveConfig</name>\n          <value class=\"SampleSaveConfiguration\">\n            <time>true</time>\n            <latency>true</latency>\n            <timestamp>true</timestamp>\n            <success>true</success>\n            <label>true</label>\n            <code>true</code>\n            <message>true</message>\n            <threadName>true</threadName>\n            <dataType>true</dataType>\n            <encoding>false</encoding>\n            <assertions>true</assertions>\n            <subresults>true</subresults>\n            <responseData>false</responseData>\n            <samplerData>false</samplerData>\n            <xml>false</xml>\n            <fieldNames>true</fieldNames>\n            <responseHeaders>false</responseHeaders>\n            <requestHeaders>false</requestHeaders>\n            <responseDataOnError>false</responseDataOnError>\n            <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>\n            <assertionsResultsToSave>0</assertionsResultsToSave>\n            <bytes>true</bytes>\n            <sentBytes>true</sentBytes>\n            <threadCounts>true</threadCounts>\n            <idleTime>true</idleTime>\n            <connectTime>true</connectTime>\n          </value>\n        </objProp>\n        <stringProp name=\"filename\"></stringProp>\n      </ResultCollector>\n      <hashTree/>\n      <ResultCollector guiclass=\"SummaryReport\" testclass=\"ResultCollector\" testname=\"Summary Report\" enabled=\"true\">\n        <boolProp name=\"ResultCollector.error_logging\">false</boolProp>\n        <objProp>\n          <name>saveConfig</name>\n          <value class=\"SampleSaveConfiguration\">\n            <time>true</time>\n            <latency>true</latency>\n            <timestamp>true</timestamp>\n            <success>true</success>\n            <label>true</label>\n            <code>true</code>\n            <message>true</message>\n            <threadName>true</threadName>\n            <dataType>true</dataType>\n            <encoding>false</encoding>\n            <assertions>true</assertions>\n            <subresults>true</subresults>\n            <responseData>false</responseData>\n            <samplerData>false</samplerData>\n            <xml>false</xml>\n            <fieldNames>true</fieldNames>\n            <responseHeaders>false</responseHeaders>\n            <requestHeaders>false</requestHeaders>\n            <responseDataOnError>false</responseDataOnError>\n            <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>\n            <assertionsResultsToSave>0</assertionsResultsToSave>\n            <bytes>true</bytes>\n            <sentBytes>true</sentBytes>\n            <threadCounts>true</threadCounts>\n            <idleTime>true</idleTime>\n            <connectTime>true</connectTime>\n          </value>\n        </objProp>\n        <stringProp name=\"filename\"></stringProp>\n      </ResultCollector>\n      <hashTree/>\n    </hashTree>\n  </hashTree>\n</jmeterTestPlan>\n"
  },
  {
    "path": "karate-demo/src/test/java/mock-key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALZOXYx1pQWaMBpjCYTCARVF9WrA\nk7hj7tXGbhSpBEo25ykZTBQGj0VK+5AAKmcnAewrZaGK5KlWS6eGZLHlBvgpVIfZFPa8cFSwDe3X\nZLbVWSySLJrcTtBH3XNkZXFnIHiBIFWG9JVd4/35GiAsvOKSL9UNdcpPhMi7IwvV5RDZAgMBAAEC\ngYAaSNgyDTA6y41N8KOJsZMIZyrINnXV6wqfZdmvPuMwdBQGF/ChHoT/n5z/mRaEAtrDG0qu7OCl\nDZ0gzT6ta3ECjlw+xPj8wTlVAvayo8SSToSGMiOcoi7nZ0eAn4+eIYkXpZGKDoUpa8dh3I2z4xfR\n+ldc3xotF04fSIq8CF7ckQJBANxOsvW3BkOnQxSsurh24PRdbTuAd+usjf4FKcjFFcyoESjcIqyY\ng/HCH8jzZQ6JWkcRgL1fzTW7aooXrG5gCfUCQQDT1421nSCOS3XJxTyy9V2AyAj1d/Y+DO8+d+vI\npeuSCdU+EActgGavYcokIkaY6Z8MCYQsWWPumElylNpnJKjVAkAlHJzJB6vmeaazNOW/bUc34wUj\noOCSst64i+YeDBVABI/fcjXlHUwczbbNAzNi34B1uF0XiavoAUpROOuzLDqBAkEAnCKASL5Bk38c\nlpUv0rqzqspEiB9dt4gzATjD6MQZpy5mI/MOR0Qe6t7JbO5yWBvAZM/Swhk0ZVOKts/tVR4Y7QJB\nAILwR9prGyUKn7eCN/khkfhGQggvPpbNqPe4CnTjfCBdatNyfhd4oCnvpp8Q2SWAfJt3zULGxRsa\nvYYUFMGnfAI=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "karate-demo/src/test/java/server-keystore-cert.pem",
    "content": "Bag Attributes\n    friendlyName: karate-mock\n    localKeyID: 54 69 6D 65 20 31 35 31 35 38 32 37 38 31 33 36 38 37 \nsubject=/C=Unknown/ST=Unknown/L=Unknown/O=Unknown/OU=Unknown/CN=Unknown\nissuer=/C=Unknown/ST=Unknown/L=Unknown/O=Unknown/OU=Unknown/CN=Unknown\n-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIEcKOr+zANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdV\nbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD\nVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du\nMB4XDTE4MDExMzA3MTY1M1oXDTI4MDExMTA3MTY1M1owbDEQMA4GA1UEBhMHVW5r\nbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE\nChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMDsODYDtcxM5IT1sEEVBeBw\nxk/IHxlRbKeF1LypSafRw3DvrP6hcqeyaiQaJJHm4Vkd4o/UCWSH+j3Zpd7pehGn\nbJjsAbIYSpA1NEDbWbYD1uNs2YTpSc7iDquqPPms/ciHJOQUsBNbJFqeGMnxWxOm\ntfyAeib+3wY+lAMlJGQ0f+LXiLwBgT45ye1ZTLXU/TMQfhtcFqycVciqpAHR+Lvi\n5P0PY0ZC2cxMC4GY5Znm7OOQ84B6VKW2t/K/U26iTCKjZNNptywP7CB2OQB6r3uI\nXSO6xD196ce5JfeJq1rm4gqhc8iwl1qS4Vzn3XkGF0SHAgwI9afMDRHGA/RBI1UC\nAwEAAaMhMB8wHQYDVR0OBBYEFOsz+AZ2dVqulT033sPWWYRyvnVAMA0GCSqGSIb3\nDQEBCwUAA4IBAQAjLN+PrzbmW+ufTCXzFtyyP6+w4rMsyn0OaC/UxZZdkPSBB9of\n5gsQY+S2JwKAyM8lS1plDgPE1f3CyAFDqma+lfPsW7zoadRHikB9rFhBtn/3vqAG\nv1g5lc2pKjEqzl/L3FCW0ZpQmy7Yc7cY3w+GSX0u/pxsdZmKWIWbklUc/H/AEvg5\nOjtBpdTpf88k4MUmpIqAPf3A3TsTguqmuRQ3jk9jwuGoZk6TR6L7lVDoevOVJRgl\nWC9+zyIdLyQOA7TgB8q+UsuwPmpAIxX6ZJ4UGzjdqx3BxtSU5IYVzYDsNtQTxtgs\nSIP4CZE7bW0B8xSHCbu3efU3WwJKk06ju0+J\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "karate-demo/src/test/java/server-keystore-key.pem",
    "content": "Bag Attributes\n    friendlyName: karate-mock\n    localKeyID: 54 69 6D 65 20 31 35 31 35 38 32 37 38 31 33 36 38 37 \nKey Attributes: <No Attributes>\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDA7Dg2A7XMTOSE\n9bBBFQXgcMZPyB8ZUWynhdS8qUmn0cNw76z+oXKnsmokGiSR5uFZHeKP1Alkh/o9\n2aXe6XoRp2yY7AGyGEqQNTRA21m2A9bjbNmE6UnO4g6rqjz5rP3IhyTkFLATWyRa\nnhjJ8VsTprX8gHom/t8GPpQDJSRkNH/i14i8AYE+OcntWUy11P0zEH4bXBasnFXI\nqqQB0fi74uT9D2NGQtnMTAuBmOWZ5uzjkPOAelSltrfyv1Nuokwio2TTabcsD+wg\ndjkAeq97iF0jusQ9fenHuSX3iata5uIKoXPIsJdakuFc5915BhdEhwIMCPWnzA0R\nxgP0QSNVAgMBAAECggEAKdSOM5gGWS/q9LFY8COgzJNnTHE13QV8q64m/GkwYUTo\nPZqaRfO3qojS3tUUTNZk0i/aqhjtmHCrX7HLd1PkdeN5doblrzn5IN+IXu+wufiH\nJXS6jUkQCd2WFL6qzfAmbs5uv02FCA1hCozxDwhYGSCyoPcyhTYKIY58BVqOU3N1\nqBIcIm3jb0iMEm9BpMaJSe0DGjKG955ikdF/6Q/1Q3FvRn95DUpoUhhm1euJ1s25\nPeCAOXiTpX6hdNqo3/l2jh3zuc4ChALxVhCaLu8fYF35ATP7ZZKc04kqmsJnAQ56\nlPzLCsUg1hQisOs/sa5HQPHsmBXVLJE2804511m3WQKBgQD5sfQC/vM3GbKqIS8o\n133P32cfXizChct8tqwuIqVb5CTbpaXGnfQZwXeA8ZfMAKvHrVKwhV8YHPeJTAwE\ng8DquthrHagmAJpKxsAh1rbgRGidwbcjZHdxzPO9VnkaetHjJt+r+g2b7PrhPsaM\nSyB4xoM49F7bUkBm5MdMfmqL/wKBgQDFy0kgQ/3KNi7umPoJkLoCQvAUkMPven3L\nDPLKkgjKwclyAzsRa4h6kMfE/7rFmq4ejaix/ieTL/Yl+hwpy0+pZyKbBmXvPwi4\nbZn7WwgeWTiVkNYuta3A1N3fBzr6hyGBQmKxumJZhOyfkl37ceNL2eiXJ0IYRe/l\nXV0GaWpgqwKBgQDY6QI06A2YvDY9HP6+2BRvVH9c/I6dnWUcPgRtP6OupA5w0QGl\n/OlkxnBuD3HywlJ379V5iyME64UOPIXkFiCsyQvgYa8E9FkUxHt76e6L/GF1Sicy\nE8C/l2/V9xzVKATU85wy5dKUtdVrfwE0Nr9KrAiqnX8Zv+Y6fFu108vzOQKBgEY6\nIFOhcGmshUtX62ccCL8mzyEuKTNNFX7TsRy1bwO2fHTLqtxLhuClqRhMCB+DBRF6\nEIxqBdkfrVDclcVNF5K5OLM8OVMR2WHKIGL7dk14Njv8ed+JyBBwQ/qdEZbEBeRk\nErCx0ZuhM3aQvGe7jw1uaEPKv4/ovP1+Abp2ETwTAoGBAM5BXJgvnZ978QVEU2kb\n63iIOdjBWeuIu3+kLUug+ufKnSlwHRzsGrKJs8GoCWxoCC6/mg8n0FxIR+TNqSM+\nfowrIDBt6DJNlVKAssVScX9CyTqxKgU/+9iXN+l9e0E64m6ljyCKMOdlB3evgZqc\nieudxMhFEaRAtTXEGOdrgvhb\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "karate-demo/src/test/java/server-keystore.pem",
    "content": "Bag Attributes\n    friendlyName: karate-mock\n    localKeyID: 54 69 6D 65 20 31 35 31 35 38 32 37 38 31 33 36 38 37 \nKey Attributes: <No Attributes>\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDA7Dg2A7XMTOSE\n9bBBFQXgcMZPyB8ZUWynhdS8qUmn0cNw76z+oXKnsmokGiSR5uFZHeKP1Alkh/o9\n2aXe6XoRp2yY7AGyGEqQNTRA21m2A9bjbNmE6UnO4g6rqjz5rP3IhyTkFLATWyRa\nnhjJ8VsTprX8gHom/t8GPpQDJSRkNH/i14i8AYE+OcntWUy11P0zEH4bXBasnFXI\nqqQB0fi74uT9D2NGQtnMTAuBmOWZ5uzjkPOAelSltrfyv1Nuokwio2TTabcsD+wg\ndjkAeq97iF0jusQ9fenHuSX3iata5uIKoXPIsJdakuFc5915BhdEhwIMCPWnzA0R\nxgP0QSNVAgMBAAECggEAKdSOM5gGWS/q9LFY8COgzJNnTHE13QV8q64m/GkwYUTo\nPZqaRfO3qojS3tUUTNZk0i/aqhjtmHCrX7HLd1PkdeN5doblrzn5IN+IXu+wufiH\nJXS6jUkQCd2WFL6qzfAmbs5uv02FCA1hCozxDwhYGSCyoPcyhTYKIY58BVqOU3N1\nqBIcIm3jb0iMEm9BpMaJSe0DGjKG955ikdF/6Q/1Q3FvRn95DUpoUhhm1euJ1s25\nPeCAOXiTpX6hdNqo3/l2jh3zuc4ChALxVhCaLu8fYF35ATP7ZZKc04kqmsJnAQ56\nlPzLCsUg1hQisOs/sa5HQPHsmBXVLJE2804511m3WQKBgQD5sfQC/vM3GbKqIS8o\n133P32cfXizChct8tqwuIqVb5CTbpaXGnfQZwXeA8ZfMAKvHrVKwhV8YHPeJTAwE\ng8DquthrHagmAJpKxsAh1rbgRGidwbcjZHdxzPO9VnkaetHjJt+r+g2b7PrhPsaM\nSyB4xoM49F7bUkBm5MdMfmqL/wKBgQDFy0kgQ/3KNi7umPoJkLoCQvAUkMPven3L\nDPLKkgjKwclyAzsRa4h6kMfE/7rFmq4ejaix/ieTL/Yl+hwpy0+pZyKbBmXvPwi4\nbZn7WwgeWTiVkNYuta3A1N3fBzr6hyGBQmKxumJZhOyfkl37ceNL2eiXJ0IYRe/l\nXV0GaWpgqwKBgQDY6QI06A2YvDY9HP6+2BRvVH9c/I6dnWUcPgRtP6OupA5w0QGl\n/OlkxnBuD3HywlJ379V5iyME64UOPIXkFiCsyQvgYa8E9FkUxHt76e6L/GF1Sicy\nE8C/l2/V9xzVKATU85wy5dKUtdVrfwE0Nr9KrAiqnX8Zv+Y6fFu108vzOQKBgEY6\nIFOhcGmshUtX62ccCL8mzyEuKTNNFX7TsRy1bwO2fHTLqtxLhuClqRhMCB+DBRF6\nEIxqBdkfrVDclcVNF5K5OLM8OVMR2WHKIGL7dk14Njv8ed+JyBBwQ/qdEZbEBeRk\nErCx0ZuhM3aQvGe7jw1uaEPKv4/ovP1+Abp2ETwTAoGBAM5BXJgvnZ978QVEU2kb\n63iIOdjBWeuIu3+kLUug+ufKnSlwHRzsGrKJs8GoCWxoCC6/mg8n0FxIR+TNqSM+\nfowrIDBt6DJNlVKAssVScX9CyTqxKgU/+9iXN+l9e0E64m6ljyCKMOdlB3evgZqc\nieudxMhFEaRAtTXEGOdrgvhb\n-----END PRIVATE KEY-----\nBag Attributes\n    friendlyName: karate-mock\n    localKeyID: 54 69 6D 65 20 31 35 31 35 38 32 37 38 31 33 36 38 37 \nsubject=/C=Unknown/ST=Unknown/L=Unknown/O=Unknown/OU=Unknown/CN=Unknown\nissuer=/C=Unknown/ST=Unknown/L=Unknown/O=Unknown/OU=Unknown/CN=Unknown\n-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIEcKOr+zANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdV\nbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD\nVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du\nMB4XDTE4MDExMzA3MTY1M1oXDTI4MDExMTA3MTY1M1owbDEQMA4GA1UEBhMHVW5r\nbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE\nChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMDsODYDtcxM5IT1sEEVBeBw\nxk/IHxlRbKeF1LypSafRw3DvrP6hcqeyaiQaJJHm4Vkd4o/UCWSH+j3Zpd7pehGn\nbJjsAbIYSpA1NEDbWbYD1uNs2YTpSc7iDquqPPms/ciHJOQUsBNbJFqeGMnxWxOm\ntfyAeib+3wY+lAMlJGQ0f+LXiLwBgT45ye1ZTLXU/TMQfhtcFqycVciqpAHR+Lvi\n5P0PY0ZC2cxMC4GY5Znm7OOQ84B6VKW2t/K/U26iTCKjZNNptywP7CB2OQB6r3uI\nXSO6xD196ce5JfeJq1rm4gqhc8iwl1qS4Vzn3XkGF0SHAgwI9afMDRHGA/RBI1UC\nAwEAAaMhMB8wHQYDVR0OBBYEFOsz+AZ2dVqulT033sPWWYRyvnVAMA0GCSqGSIb3\nDQEBCwUAA4IBAQAjLN+PrzbmW+ufTCXzFtyyP6+w4rMsyn0OaC/UxZZdkPSBB9of\n5gsQY+S2JwKAyM8lS1plDgPE1f3CyAFDqma+lfPsW7zoadRHikB9rFhBtn/3vqAG\nv1g5lc2pKjEqzl/L3FCW0ZpQmy7Yc7cY3w+GSX0u/pxsdZmKWIWbklUc/H/AEvg5\nOjtBpdTpf88k4MUmpIqAPf3A3TsTguqmuRQ3jk9jwuGoZk6TR6L7lVDoevOVJRgl\nWC9+zyIdLyQOA7TgB8q+UsuwPmpAIxX6ZJ4UGzjdqx3BxtSU5IYVzYDsNtQTxtgs\nSIP4CZE7bW0B8xSHCbu3efU3WwJKk06ju0+J\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "karate-demo/src/test/java/ssl/SslTest.java",
    "content": "package ssl;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\n/**\n *\n * @author pthomas3\n */\nclass SslTest {\n    \n    static ConfigurableApplicationContext context;\n    \n    @BeforeAll\n    static void beforeAll() {\n        context = TestService.start();      \n    }\n    \n    @Test\n    void testParallel() {\n        int port = TestService.getPort(context);\n        Results results = Runner.path(\"classpath:ssl\")\n                .karateEnv(\"mock\") // skip callSingle, note that the karate-config.js copied from demo may be present\n                .systemProperty(\"jersey.ssl.port\", port + \"\")\n                .parallel(1);\n        assertTrue(results.getFailCount() == 0, results.getErrorMessages());\n    }    \n\n    @AfterAll\n    static void afterAll() {\n        TestService.stop(context);\n    }\n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/ssl/TestService.java",
    "content": "package ssl;\n\nimport com.intuit.karate.demo.config.ServerStartedInitializingBean;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.EnableAutoConfiguration;\nimport org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;\nimport org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.bind.annotation.*;\n\n/**\n *\n * @author pthomas3\n */\n@Configuration\n@EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class, DataSourceAutoConfiguration.class})\npublic class TestService {\n\n    private static final Logger logger = LoggerFactory.getLogger(TestService.class);\n\n    @RestController\n    @RequestMapping(\"/test\")\n    class TestController {\n\n        @GetMapping\n        public String test() {\n            return \"{ \\\"success\\\": true }\";\n        }\n\n    }\n\n    public static ConfigurableApplicationContext start() {\n        String[] args = {\n            \"--server.port=0\",\n            \"--server.ssl.key-store=src/test/java/server-keystore.p12\",\n            \"--server.ssl.key-store-password=karate-mock\",\n            \"--server.ssl.keyStoreType=PKCS12\",\n            \"--server.ssl.keyAlias=karate-mock\"};\n        return SpringApplication.run(TestService.class, args);\n    }\n\n    public static void stop(ConfigurableApplicationContext context) {\n        SpringApplication.exit(context, () -> 0);\n    }\n\n    public static int getPort(ConfigurableApplicationContext context) {\n        ServerStartedInitializingBean ss = context.getBean(ServerStartedInitializingBean.class);\n        return ss.getLocalPort();\n    }\n\n    @Bean\n    public ServerStartedInitializingBean getInitializingBean() {\n        return new ServerStartedInitializingBean();\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/ssl/ssl-keystore.feature",
    "content": "Feature: jersey ssl with trust store / cert\n\nBackground:\n    * configure ssl = { keyStore: 'classpath:server-keystore.p12', keyStorePassword: 'karate-mock', keyStoreType: 'pkcs12', trustStore: 'classpath:server-keystore.p12', trustStorePassword: 'karate-mock', trustStoreType: 'pkcs12' }\n    * url 'https://localhost:' + karate.properties['jersey.ssl.port']\n\nScenario:\n    Given path 'test'\n    When method get\n    Then status 200\n    And match response == { success: true }\n"
  },
  {
    "path": "karate-demo/src/test/java/ssl/ssl-truststore.feature",
    "content": "Feature: jersey ssl with trust store / cert\n\nBackground:\n    * configure ssl = { trustStore: 'classpath:server-keystore.p12', trustStorePassword: 'karate-mock', trustStoreType: 'pkcs12' }\n    * url 'https://localhost:' + karate.properties['jersey.ssl.port']\n\nScenario:\n    Given path 'test'\n    When method get\n    Then status 200\n    And match response == { success: true }\n"
  },
  {
    "path": "karate-demo/src/test/java/test/MonitorThread.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage test;\n\nimport java.io.BufferedReader;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.net.InetAddress;\nimport java.net.ServerSocket;\nimport java.net.Socket;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class MonitorThread extends Thread {\n    \n    private static final Logger logger = LoggerFactory.getLogger(MonitorThread.class);\n    \n    private Stoppable stoppable;\n    private ServerSocket socket;\n\n    public MonitorThread(int port, Stoppable stoppable) {\n        this.stoppable = stoppable;\n        setDaemon(true);\n        setName(\"stop-monitor-\" + port);\n        try {\n            socket = new ServerSocket(port, 1, InetAddress.getByName(\"127.0.0.1\"));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void run() {\n        logger.info(\"starting thread: {}\", getName());\n        Socket accept;\n        try {\n            accept = socket.accept();\n            BufferedReader reader = new BufferedReader(new InputStreamReader(accept.getInputStream()));\n            reader.readLine();\n            logger.info(\"shutting down thread: {}\", getName());\n            stoppable.stop();\n            accept.close();\n            socket.close();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }   \n    \n    public static void stop(int port) {\n         try {\n            Socket s = new Socket(InetAddress.getByName(\"127.0.0.1\"), port);\n            OutputStream out = s.getOutputStream();\n            logger.info(\"sending stop request to monitor thread on port: {}\", port);\n            out.write((\"\\r\\n\").getBytes());\n            out.flush();\n            s.close();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }       \n    }\n    \n}\n"
  },
  {
    "path": "karate-demo/src/test/java/test/ServerStart.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage test;\n\nimport com.intuit.karate.demo.Application;\nimport com.intuit.karate.demo.config.ServerStartedInitializingBean;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\npublic class ServerStart {\n\n    static final Logger logger = LoggerFactory.getLogger(ServerStart.class);\n\n    ConfigurableApplicationContext context;\n    MonitorThread monitor;\n    int port = 0;\n\n    public void start(String[] args, boolean wait) throws Exception {\n        if (wait) {\n            try {\n                logger.info(\"attempting to stop server if it is already running\");\n                new ServerStop().stopServer();\n            } catch (Exception e) {\n                logger.info(\"failed to stop server (was probably not up): {}\", e.getMessage());\n            }\n        }\n        context = Application.run(args);\n        ServerStartedInitializingBean ss = context.getBean(ServerStartedInitializingBean.class);\n        port = ss.getLocalPort();\n        logger.info(\"started server on port: {}\", port);\n        if (wait) {\n            int stopPort = port + 1;\n            logger.info(\"will use stop port as {}\", stopPort);\n            monitor = new MonitorThread(stopPort, () -> context.close());\n            monitor.start();\n            monitor.join();\n        }\n    }\n\n    public int getPort() {\n        return port;\n    }\n\n    @Test\n    void startServer() throws Exception {\n        start(new String[]{}, true);\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/test/ServerStop.java",
    "content": "package test;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass ServerStop {\n\n    @Test\n    void stopServer() {\n        MonitorThread.stop(8081);\n    }\n\n}\n"
  },
  {
    "path": "karate-demo/src/test/java/test/Stoppable.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage test;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Stoppable {\n    \n    void stop() throws Exception;\n    \n}\n"
  },
  {
    "path": "karate-docker/karate-chrome/Dockerfile",
    "content": "FROM maven:3-amazoncorretto-17-debian\n\nLABEL maintainer=\"Peter Thomas\"\nLABEL url=\"https://github.com/karatelabs/karate/tree/master/karate-docker/karate-chrome\"\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    wget \\\n    gnupg2 \\\n    ca-certificates\n\nRUN mkdir -p /etc/apt/keyrings \\\n  && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub \\\n     | gpg --dearmor > /etc/apt/keyrings/google-linux-signing-key.gpg \\\n  && echo \"deb [arch=amd64 signed-by=/etc/apt/keyrings/google-linux-signing-key.gpg] http://dl.google.com/linux/chrome/deb/ stable main\" \\\n     > /etc/apt/sources.list.d/google-chrome.list \\\n  && apt-get update \\\n  && apt-get install -y --no-install-recommends \\\n       google-chrome-stable\n\nRUN useradd chrome --shell /bin/bash --create-home \\\n  && usermod -a -G sudo chrome \\\n  && echo 'ALL ALL = (ALL) NOPASSWD: ALL' >> /etc/sudoers \\\n  && echo 'chrome:karate' | chpasswd\n\nRUN apt-get install -y --no-install-recommends \\\n  xvfb \\\n  x11vnc \\\n  xterm \\\n  fluxbox \\\n  wmctrl \\\n  supervisor \\\n  socat \\\n  ffmpeg \\\n  locales \\\n  locales-all\n\nENV LANG en_US.UTF-8\n\nRUN apt-get clean \\\n  && rm -rf /var/cache/* /var/log/apt/* /var/lib/apt/lists/* /tmp/* \\\n  && mkdir ~/.vnc \\\n  && x11vnc -storepasswd karate ~/.vnc/passwd \\\n  && locale-gen ${LANG} \\\n  && dpkg-reconfigure --frontend noninteractive locales \\\n  && update-locale LANG=${LANG}\n\nCOPY supervisord.conf /etc\nCOPY entrypoint.sh /\nRUN chmod +x /entrypoint.sh\n\nEXPOSE 5900 9222\n\nADD target/karate.jar /\nADD target/repository /usr/share/maven/ref/repository\n\nCMD [\"/bin/bash\", \"/entrypoint.sh\"]\n"
  },
  {
    "path": "karate-docker/karate-chrome/entrypoint.sh",
    "content": "#!/bin/bash\nset -x -e\nif [ -z \"$KARATE_JOBURL\" ]\n  then\n    export KARATE_OPTIONS=\"-h\"\n    export KARATE_START=\"false\"\n  else\n    export KARATE_START=\"true\"\n    export KARATE_OPTIONS=\"-j $KARATE_JOBURL\"\nfi\nif [ -z \"$KARATE_SOCAT_START\" ]\n  then\n\texport KARATE_SOCAT_START=\"false\"\n\texport KARATE_CHROME_PORT=\"9222\"\n  else\n\texport KARATE_SOCAT_START=\"true\"\n\texport KARATE_CHROME_PORT=\"9223\"\nfi\n[ -z \"$KARATE_CHROME_ADD_OPTIONS\" ] && export KARATE_CHROME_ADD_OPTIONS=\"\"\n[ -z \"$KARATE_WIDTH\" ] && export KARATE_WIDTH=\"1280\"\n[ -z \"$KARATE_HEIGHT\" ] && export KARATE_HEIGHT=\"720\"\nexec /usr/bin/supervisord -c /etc/supervisord.conf\n"
  },
  {
    "path": "karate-docker/karate-chrome/supervisord.conf",
    "content": "[supervisord]\nuser=root\nnodaemon=true\n\n[unix_http_server]\nfile=/tmp/supervisor.sock\nusername=dummy\npassword=dummy\n\n[rpcinterface:supervisor]\nsupervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface\n\n[supervisorctl]\nserverurl=unix:///tmp/supervisor.sock\nusername=dummy\npassword=dummy\n\n[program:xvfb]\ncommand=/usr/bin/Xvfb :1 -screen 0 %(ENV_KARATE_WIDTH)sx%(ENV_KARATE_HEIGHT)sx24 +extension RANDR\nautorestart=true\npriority=100\n\n[program:fluxbox]\nenvironment=DISPLAY=\":1\"\ncommand=/usr/bin/fluxbox -display :1\nautorestart=true\npriority=200\n\n[program:x11vnc]\ncommand=/usr/bin/x11vnc -display :1 -usepw -shared -forever -xrandr\nautorestart=true\npriority=300\n\n[program:chrome]\nuser=chrome\nenvironment=HOME=\"/home/chrome\",USER=\"chrome\",DISPLAY=\":1\",DBUS_SESSION_BUS_ADDRESS=\"unix:path=/dev/null\"\ncommand=/usr/bin/google-chrome\n    --user-data-dir=/home/chrome\n    --no-first-run\n    --disable-translate\n    --disable-notifications\n    --disable-popup-blocking\n    --disable-infobars\n    --disable-gpu\n    --mute-audio\n    --dbus-stub\n    --disable-dev-shm-usage\n    --enable-logging=stderr\n    --log-level=0\n    --window-position=0,0 \n    --window-size=%(ENV_KARATE_WIDTH)s,%(ENV_KARATE_HEIGHT)s\n    --force-device-scale-factor=1\n    --remote-allow-origins=*\n    --remote-debugging-port=%(ENV_KARATE_CHROME_PORT)s\n    %(ENV_KARATE_CHROME_ADD_OPTIONS)s\nautorestart=true\npriority=400\n\n[program:socat]\ncommand=/usr/bin/socat tcp-listen:9222,fork tcp:localhost:9223\nautorestart=true\nautostart=%(ENV_KARATE_SOCAT_START)s\npriority=500\n\n[program:ffmpeg]\ncommand=/usr/bin/ffmpeg -y -f x11grab -r 16 -s %(ENV_KARATE_WIDTH)sx%(ENV_KARATE_HEIGHT)s -i :1 -vcodec libx264 -pix_fmt yuv420p -preset fast /tmp/karate.mp4\nautostart=%(ENV_KARATE_SOCAT_START)s\npriority=600\n\n[program:karate]\ncommand=/usr/bin/java -jar karate.jar %(ENV_KARATE_OPTIONS)s\nautorestart=false\nautostart=%(ENV_KARATE_START)s\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\npriority=700\n"
  },
  {
    "path": "karate-docker/karate-chromium/Dockerfile",
    "content": "FROM maven:3-amazoncorretto-17-debian\n\nLABEL maintainer=\"Peter Thomas\"\nLABEL url=\"https://github.com/karatelabs/karate/tree/master/karate-docker/karate-chromium\"\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    wget \\\n    gnupg2\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    chromium\n\nRUN useradd chrome --shell /bin/bash --create-home \\\n  && usermod -a -G sudo chrome \\\n  && echo 'ALL ALL = (ALL) NOPASSWD: ALL' >> /etc/sudoers \\\n  && echo 'chrome:karate' | chpasswd\n\nRUN apt-get install -y --no-install-recommends \\\n  xvfb \\\n  x11vnc \\\n  xterm \\\n  fluxbox \\\n  wmctrl \\\n  supervisor \\\n  socat \\\n  ffmpeg \\\n  locales \\\n  locales-all\n\nENV LANG en_US.UTF-8\n\nRUN apt-get clean \\\n  && rm -rf /var/cache/* /var/log/apt/* /var/lib/apt/lists/* /tmp/* \\\n  && mkdir ~/.vnc \\\n  && x11vnc -storepasswd karate ~/.vnc/passwd \\\n  && locale-gen ${LANG} \\\n  && dpkg-reconfigure --frontend noninteractive locales \\\n  && update-locale LANG=${LANG}\n\nCOPY supervisord.conf /etc\nCOPY entrypoint.sh /\nRUN chmod +x /entrypoint.sh\n\nEXPOSE 5900 9222\n\nADD target/karate.jar /\nADD target/repository /usr/share/maven/ref/repository\n\nCMD [\"/bin/bash\", \"/entrypoint.sh\"]\n"
  },
  {
    "path": "karate-docker/karate-chromium/entrypoint.sh",
    "content": "#!/bin/bash\nset -x -e\nif [ -z \"$KARATE_JOBURL\" ]\n  then\n    export KARATE_OPTIONS=\"-h\"\n    export KARATE_START=\"false\"\n  else\n    export KARATE_START=\"true\"\n    export KARATE_OPTIONS=\"-j $KARATE_JOBURL\"\nfi\nif [ -z \"$KARATE_SOCAT_START\" ]\n  then\n\texport KARATE_SOCAT_START=\"false\"\n\texport KARATE_CHROME_PORT=\"9222\"\n  else\n\texport KARATE_SOCAT_START=\"true\"\n\texport KARATE_CHROME_PORT=\"9223\"\nfi\n[ -z \"$KARATE_CHROME_ADD_OPTIONS\" ] && export KARATE_CHROME_ADD_OPTIONS=\"\"\n[ -z \"$KARATE_WIDTH\" ] && export KARATE_WIDTH=\"1280\"\n[ -z \"$KARATE_HEIGHT\" ] && export KARATE_HEIGHT=\"720\"\nexec /usr/bin/supervisord -c /etc/supervisord.conf\n"
  },
  {
    "path": "karate-docker/karate-chromium/supervisord.conf",
    "content": "[supervisord]\nuser=root\nnodaemon=true\n\n[unix_http_server]\nfile=/tmp/supervisor.sock\nusername=dummy\npassword=dummy\n\n[rpcinterface:supervisor]\nsupervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface\n\n[supervisorctl]\nserverurl=unix:///tmp/supervisor.sock\nusername=dummy\npassword=dummy\n\n[program:xvfb]\ncommand=/usr/bin/Xvfb :1 -screen 0 %(ENV_KARATE_WIDTH)sx%(ENV_KARATE_HEIGHT)sx24 +extension RANDR\nautorestart=true\npriority=100\n\n[program:fluxbox]\nenvironment=DISPLAY=\":1\"\ncommand=/usr/bin/fluxbox -display :1\nautorestart=true\npriority=200\n\n[program:x11vnc]\ncommand=/usr/bin/x11vnc -display :1 -usepw -shared -forever -xrandr\nautorestart=true\npriority=300\n\n[program:chrome]\nuser=chrome\nenvironment=HOME=\"/home/chrome\",USER=\"chrome\",DISPLAY=\":1\",DBUS_SESSION_BUS_ADDRESS=\"unix:path=/dev/null\"\ncommand=/usr/bin/chromium\n    --user-data-dir=/home/chrome\n    --no-first-run\n    --disable-translate\n    --disable-notifications\n    --disable-popup-blocking\n    --disable-infobars\n    --disable-gpu\n    --mute-audio\n    --dbus-stub\n    --disable-dev-shm-usage\n    --enable-logging=stderr\n    --log-level=0\n    --window-position=0,0 \n    --window-size=%(ENV_KARATE_WIDTH)s,%(ENV_KARATE_HEIGHT)s\n    --force-device-scale-factor=1\n    --remote-allow-origins=*\n    --remote-debugging-port=%(ENV_KARATE_CHROME_PORT)s\n    %(ENV_KARATE_CHROME_ADD_OPTIONS)s\nautorestart=true\npriority=400\n\n[program:socat]\ncommand=/usr/bin/socat tcp-listen:9222,fork tcp:localhost:9223\nautorestart=true\nautostart=%(ENV_KARATE_SOCAT_START)s\npriority=500\n\n[program:ffmpeg]\ncommand=/usr/bin/ffmpeg -y -f x11grab -r 16 -s %(ENV_KARATE_WIDTH)sx%(ENV_KARATE_HEIGHT)s -i :1 -vcodec libx264 -pix_fmt yuv420p -preset fast /tmp/karate.mp4\nautostart=%(ENV_KARATE_SOCAT_START)s\npriority=600\n\n[program:karate]\ncommand=/usr/bin/java -jar karate.jar %(ENV_KARATE_OPTIONS)s\nautorestart=false\nautostart=%(ENV_KARATE_START)s\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\npriority=700\n"
  },
  {
    "path": "karate-docker/karate-firefox/Dockerfile",
    "content": "FROM maven:3-jdk-8\n\nLABEL maintainer=\"Peter Thomas\"\nLABEL url=\"https://github.com/karatelabs/karate/tree/master/karate-docker/karate-firefox\"\n\n#RUN echo uname -a\n#\n#RUN echo \"deb http://downloads.sourceforge.net/project/ubuntuzilla/mozilla/apt all main\" | tee -a /etc/apt/sources.list > /dev/null \\\n#    && apt-key adv --recv-keys --keyserver keyserver.ubuntu.com B7B9C16F2667CA5C \\\n#    && apt-get update \\\n#    && apt-get install firefox-mozilla-build\n\nRUN apt-get update \\\n    && apt-get install -y firefox-esr\n\n# GeckoDriver v0.19.1\nRUN wget -q \"https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-linux64.tar.gz\" -O /tmp/geckodriver.tgz \\\n    && tar zxf /tmp/geckodriver.tgz -C /usr/local/bin/ \\\n    && rm /tmp/geckodriver.tgz\n\n\nRUN useradd firefox --shell /bin/bash --create-home \\\n  && usermod -a -G sudo firefox \\\n  && echo 'ALL ALL = (ALL) NOPASSWD: ALL' >> /etc/sudoers \\\n  && echo 'firefox:karate' | chpasswd\n\nRUN apt-get install -y --no-install-recommends \\\n  xvfb \\\n  x11vnc \\\n  xterm \\\n  fluxbox \\\n  wmctrl \\\n  supervisor \\\n  socat \\\n  ffmpeg \\\n  locales \\\n  locales-all\n\nENV LANG en_US.UTF-8\n\nRUN apt-get clean \\\n  && rm -rf /var/cache/* /var/log/apt/* /var/lib/apt/lists/* /tmp/* \\\n  && mkdir ~/.vnc \\\n  && x11vnc -storepasswd karate ~/.vnc/passwd \\\n  && locale-gen ${LANG} \\\n  && dpkg-reconfigure --frontend noninteractive locales \\\n  && update-locale LANG=${LANG}\n\nCOPY supervisord.conf /etc\nCOPY entrypoint.sh /\nRUN chmod +x /entrypoint.sh\n\nEXPOSE 5900 4444\n\nADD target/karate.jar /\nADD target/repository /root/.m2/repository\n\nCMD [\"/bin/bash\",\"/entrypoint.sh\"]\n"
  },
  {
    "path": "karate-docker/karate-firefox/build.sh",
    "content": "#!/bin/bash\nset -x -e\ncd ../..\ndocker run -it --rm -v \"$(pwd)\":/karate -w /karate -v \"$(pwd)\"/karate-docker/karate-firefox/target:/root/.m2 maven:3-jdk-8 bash karate-docker/karate-firefox/install.sh\ncd karate-docker/karate-firefox\ndocker build -t karate-firefox .\n"
  },
  {
    "path": "karate-docker/karate-firefox/entrypoint.sh",
    "content": "#!/bin/bash\nset -x -e\nif [ -z \"$KARATE_JOBURL\" ]\n  then\n    export KARATE_OPTIONS=\"-h\"\n    export KARATE_START=\"false\"\n  else\n    export KARATE_START=\"true\"\n    export KARATE_OPTIONS=\"-j $KARATE_JOBURL\"\nfi\nif [ -z \"$KARATE_SOCAT_START\" ]\n  then\n\texport KARATE_SOCAT_START=\"false\"\n\texport KARATE_CHROME_PORT=\"9222\"\n  else\n\texport KARATE_SOCAT_START=\"true\"\n\texport KARATE_CHROME_PORT=\"9223\"\nfi\n[ -z \"$KARATE_CHROME_ADD_OPTIONS\" ] && export KARATE_CHROME_ADD_OPTIONS=\"\"\n[ -z \"$KARATE_WIDTH\" ] && export KARATE_WIDTH=\"1280\"\n[ -z \"$KARATE_HEIGHT\" ] && export KARATE_HEIGHT=\"720\"\nexec /usr/bin/supervisord\n"
  },
  {
    "path": "karate-docker/karate-firefox/install.sh",
    "content": "#!/bin/bash\nset -x -e\nmvn clean install -DskipTests -P pre-release -Djavacpp.platform=linux-x86_64\nKARATE_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)\nmvn -f karate-netty/pom.xml install -DskipTests -P fatjar\ncp karate-netty/target/karate-${KARATE_VERSION}.jar /root/.m2/karate.jar\nmvn -f examples/jobserver/pom.xml test-compile exec:java -Dexec.mainClass=common.Main -Dexec.classpathScope=test\nmvn -f examples/gatling/pom.xml test\n"
  },
  {
    "path": "karate-docker/karate-firefox/supervisord.conf",
    "content": "[supervisord]\nnodaemon=true\n\n[unix_http_server]\nfile=/tmp/supervisor.sock\n\n[rpcinterface:supervisor]\nsupervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface\n\n[supervisorctl]\nserverurl=unix:///tmp/supervisor.sock\n\n[program:xvfb]\ncommand=/usr/bin/Xvfb :1 -screen 0 %(ENV_KARATE_WIDTH)sx%(ENV_KARATE_HEIGHT)sx24 +extension RANDR\nautorestart=true\npriority=100\n\n[program:fluxbox]\nenvironment=DISPLAY=\":1\"\ncommand=/usr/bin/fluxbox -display :1\nautorestart=true\npriority=200\n\n[program:x11vnc]\ncommand=/usr/bin/x11vnc -display :1 -usepw -shared -forever -xrandr\nautorestart=true\npriority=300\n\n[program:firefox]\nuser=firefox\nenvironment=HOME=\"/home/firefox\",USER=\"firefox\",DISPLAY=\":1\",DBUS_SESSION_BUS_ADDRESS=\"unix:path=/dev/null\"\ncommand=/usr/bin/firefox\n    --user-data-dir=/home/firefox\n    --no-first-run\n    --disable-translate\n    --disable-notifications\n    --disable-infobars    \n    --disable-gpu\n    --mute-audio\n    --dbus-stub\n    --disable-dev-shm-usage\n    --enable-logging=stderr\n    --log-level=0\n    --window-position=0,0 \n    --window-size=%(ENV_KARATE_WIDTH)s,%(ENV_KARATE_HEIGHT)s\n    --force-device-scale-factor=1\n    --remote-debugging-port=%(ENV_KARATE_CHROME_PORT)s\n    %(ENV_KARATE_CHROME_ADD_OPTIONS)s\nautorestart=true\npriority=400\n\n[program:geckodriver]\nuser=firefox\nenvironment=HOME=\"/home/firefox\",USER=\"firefox\",DISPLAY=\":1\",DBUS_SESSION_BUS_ADDRESS=\"unix:path=/dev/null\"\ncommand=geckodriver\nautorestart=true\npriority=400\n\n[program:socat]\ncommand=/usr/bin/socat tcp-listen:9222,fork tcp:localhost:9223\nautorestart=true\nautostart=%(ENV_KARATE_SOCAT_START)s\npriority=500\n\n[program:ffmpeg]\ncommand=/usr/bin/ffmpeg -y -f x11grab -r 16 -s %(ENV_KARATE_WIDTH)sx%(ENV_KARATE_HEIGHT)s -i :1 -vcodec libx264 -pix_fmt yuv420p -preset fast /tmp/karate.mp4\nautostart=%(ENV_KARATE_SOCAT_START)s\npriority=600\n\n[program:karate]\ncommand=%(ENV_JAVA_HOME)s/bin/java -jar karate.jar %(ENV_KARATE_OPTIONS)s\nautorestart=false\nautostart=%(ENV_KARATE_START)s\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\npriority=700\n"
  },
  {
    "path": "karate-e2e-tests/README.md",
    "content": "# karate-e2e-tests\n\nThis is a regression test suite for [Karate UI](https://github.com/karatelabs/karate/tree/master/karate-core). You can find an explanation [here](https://stackoverflow.com/a/66005331/143475)."
  },
  {
    "path": "karate-e2e-tests/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>io.karatelabs</groupId>\n        <artifactId>karate-parent</artifactId>\n        <version>1.5.2</version>\n    </parent>\n    <artifactId>karate-e2e-tests</artifactId>\n    <packaging>jar</packaging>\n\n    <dependencies>        \n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-playwright</artifactId>\n            <version>${project.version}</version>\n            <scope>test</scope>         \n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <version>${junit5.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-engine</artifactId>\n            <version>${junit5.version}</version>\n            <scope>runtime</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>\n        <plugins>\n            <plugin>\n                <groupId>org.sonatype.central</groupId>\n                <artifactId>central-publishing-maven-plugin</artifactId>\n                <version>${central.publishing.maven.plugin.version}</version>\n                <configuration>\n                    <skipPublishing>true</skipPublishing>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>           \n    \n</project>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/axe/AxeRunner.java",
    "content": "package axe;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\n\nimport org.junit.jupiter.api.Test;\n\npublic class AxeRunner {\n\n    @Test\n    public void axeSimpleTest() {\n        Results results = Runner.path(\"classpath:axe/axe.feature\")\n                .parallel(1);\n        assertEquals(0, results.getFailCount());\n    }\n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/axe/axe-report.html",
    "content": "<table class=\"table table-striped\">\n  <thead>\n    <tr>\n      <th>Rule</th>\n      <th>Impact</th>\n      <th>Target</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr th:each=\"v: axeResponse.violations\">\n      <td><a th:href=\"v.helpUrl\" th:text=\"v.help\" target=\"_blank\"></a></td>\n      <td th:text=\"v.impact\"></td>\n      <td>\n        <ul>\n          <li th:each=\"node: v.nodes\" th:text=\"node.target\"></li>\n        </ul>\n      </td>\n    </tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/axe/axe.feature",
    "content": "Feature: axe accessibility native\n\n  Background:\n    * configure driver = { type: 'chrome' }\n\n  Scenario:\n    # get axe script\n    * def axeJs = karate.http('https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.7.0/axe.min.js').get().body\n    * driver 'https://dequeuniversity.com/demo/dream'\n    * waitFor('.submit-search')\n    # inject axe script\n    * driver.script(axeJs)\n    # execute axe\n    * def axeResponse = driver.scriptAwait('axe.run()')\n    * karate.write(axeResponse, 'axe-response.json')\n    * doc { read: 'axe-report.html' }    "
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/00.feature",
    "content": "Feature:\n\nBackground:\n* driver serverUrl + '/00'\n* def dimensions = karate.get('dimensions')\n* if (dimensions) driver.dimensions = dimensions\n* def skipSlowTests = karate.properties['skip.slow.tests']\n\nScenario:\n# driver.send() (has to be first)\n# * if (driverType == 'chrome') karate.call('12.feature')\n\n# driver.url | driver.title | waitForUrl() | refresh() | back() | forward() | driver.dimensions\n# * call read('01.feature')\n\n# waitFor() | waitForText() | waitForEnabled()\n* call read('02.feature')\n\n# script() | waitUntil()\n* call read('03.feature')\n\n# cookies\n* if (driverType != 'safaridriver' &&  driverType != 'playwright') karate.call('04.feature')\n\n# driver.intercept\n* if (driverType == 'chrome' || driverType == 'playwright') karate.call('05.feature')\n\n# position()\n* call read('06.feature')\n\n# input() | value() | text() | html()\n* call read('07.feature')\n\n# switchFrame()\n* call read('08.feature')\n\n# wildcard locators\n* call read('09.feature')\n\n# element position\n* call read('10.feature')\n\n# switchPage()\n* if ((driverType == 'chrome' || driverType == 'playwright') && !skipSlowTests) karate.call('11.feature')\n\n# switchPage() with external URLs\n* if (driverType == 'playwright' && !skipSlowTests) karate.call('13.feature')\n\n# survive Target.detachedFromTarget with nested iframes\n* if ((driverType == 'chrome' || driverType == 'playwright') && !skipSlowTests) karate.call('14.feature')\n\n# xpath locators\n* call read('15.feature')\n\n# image comparison\n* if (!skipSlowTests) karate.call('16.feature')\n\n# switch to root session on page close\n* call read('17.feature')\n\n# submit and retry Should in theory pass for all browsers but playwright handles retries a bit different so expectations that work with it may not work with other implementations.  \n* if (driverType == 'playwright') karate.call('18.feature')\n\n# friendly locators\n* if (driverType != 'geckodriver') karate.call('19.feature')"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/00_outline.feature",
    "content": "Feature:\n\nScenario Outline: ${driverType}\n* call read('00.feature')\n\nExamples:\n| driverType   | dimensions!                                |\n| chrome       | { x: 0, y: 0, width: 300, height: 800 }    |\n| chromedriver | { x: 300, y: 0, width: 250, height: 800 }  |\n| geckodriver  | { x: 600, y: 0, width: 300, height: 800 }  |\n| safaridriver | { x: 1000, y: 0, width: 400, height: 800 } |\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/01.feature",
    "content": "Feature:\n\nBackground:\n* driver serverUrl + '/01'\n\nScenario:\n# assert page url\n# match driver.url == serverUrl + '/01'\n# safer\n* waitForUrl(serverUrl + '/01')\n\n# assert page title\n* match driver.title == 'Page 01'\n\n# before refresh()\n* match text('#pageLoadCount') == '1'\n\n# refresh page\n* refresh()\n* def expected = driverType == 'safaridriver' ? '1' : '2'\n* waitForText('#pageLoadCount', expected)\n\n# reload page\n* reload()\n* def expected = driverType == 'safaridriver' ? '1' : '3'\n* waitForText('#pageLoadCount', expected)\n\n# navigate to new page\n* click('a')\n\n# wait for new page to load\n* waitForUrl(serverUrl + '/02')\n\n# assert for driver title\n# * match driver.title == 'Page 02'\n# safer\n* waitUntil(\"document.title == 'Page 02'\")\n\n# browser navigation: back\n* back()\n\n# wait for page to re-load\n* waitForUrl(serverUrl + '/01')\n\n# * match driver.title == 'Page 01'\n# safer\n* waitUntil(\"document.title == 'Page 01'\")\n\n# browser navigation: forward\n* forward()\n\n# wait for page to re-load\n* waitForUrl(serverUrl + '/02')\n\n# * match driver.title == 'Page 02'\n# safer\n* waitUntil(\"document.title == 'Page 02'\")\n\n* if (driverType == 'playwright') karate.abort()\n\n# driver.dimensions\n* match driver.dimensions contains { x: '#number', y: '#number', width: '#number', height: '#number' }\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/02.feature",
    "content": "Feature:\n\nBackground:\n* driver serverUrl + '/02'\n\nScenario:\n# wait for slow loading element\n* waitFor('#slowDiv')\n\n# this is a string \"contains\" match for convenience\n* waitForText('#slowDiv', 'APPEARED')\n\n# how to search the whole html\n* waitForText('body', 'APPEARED')\n\n# will be false if disabled\n* waitForEnabled('#slowDiv')"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/03.feature",
    "content": "Feature:\n\nBackground:\n* driver serverUrl + '/03'\n\nScenario:\n# different ways to use waitUntil()\n* waitUntil('#helloDiv', \"function(e){ return e.innerHTML == 'hello world' }\")\n* waitUntil('#helloDiv', \"_.innerHTML == 'hello world'\")\n* waitUntil('#helloDiv', '!_.disabled')\n\n# different ways to use script()\n* match script('#helloDiv', \"function(e){ return e.innerHTML }\") == 'hello world'\n* match script('#helloDiv', '_.innerHTML') == 'hello world'\n* match script('#helloDiv', '!_.disabled') == true\n* match script('#helloDiv', \"_.style['color']\") == 'red'\n* match script('.styled-div', \"function(e){ return getComputedStyle(e)['font-size'] }\") == '30px'\n* match script('.styled-div', \"_ => getComputedStyle(_)['font-size']\") == '30px'\n\n# Regression test for https://github.com/karatelabs/karate/issues/1786\n# Tests that Karate can handle console logging -- we can't assert it is working, but we can check that it doesn't crash devtools connection\n# Expect Karate to log \"[console] testing\"\n* script(\"console.log('testing')\")\n\n# Expect Karate to log \"[console] true\"\n* script(\"console.log(true)\")\n\n# Expect Karate to log \"[console] 1\"\n* script(\"console.log(1)\")\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/04.feature",
    "content": "Feature:\n\nBackground:\n* driver serverUrl + '/04'\n\nScenario:\n# foo=bar is set by the server\n* def cookie1 = { name: 'foo', value: 'bar' }\nAnd match driver.cookies contains deep cookie1\nAnd match cookie('foo') contains deep cookie1\n\n* def cookie2 = { name: 'hello', value: 'world' }\n* cookie(cookie2)\n* match driver.cookies contains deep cookie2\n\n# delete cookie\n* deleteCookie('foo')\n* match driver.cookies !contains '#(^cookie1)'\n\n# clear cookies\n* clearCookies()\n* match driver.cookies == '#[0]'\n\n# set multiple cookies at once e.g. from an API call\n* def data = [{ name: 'one', value: '1' }, { name: 'two', value: '2' }]\n* driver.cookies = data\n* match driver.cookies contains deep data\n\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/05.feature",
    "content": "Feature:\n\nBackground:\n* driver serverUrl + '/05'\n\nScenario:\n* url serverUrl + '/api/05'\n* method get\n* match response == { message: 'hello world' }\n\n* click('button')\n* waitForText('#containerDiv', 'hello world')\n\n* def mock = driver.intercept({ patterns: [{ urlPattern: '*/api/*' }], mock: '05_mock.feature' })\n\n* click('button')\n* waitForText('#containerDiv', 'hello faked')\n\n* def requests = mock.get('savedRequests')\n* match requests == [{ path: '/api/05', params: { foo: ['bar'] } }]"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/05_mock.feature",
    "content": "Feature:\n\nBackground:\n* def savedRequests = []\n\nScenario: pathMatches('/api/05')\n* savedRequests.push({ path: requestPath, params: requestParams })\n* print 'saved:', savedRequests\n* def response = { message: 'hello faked' }\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/06.feature",
    "content": "Feature:\n\nBackground:\n* driver serverUrl + '/06'\n\nScenario:\n* def loc1 = position('#first')\n* match loc1 contains { x: '#number', y: '#number', width: '#number', height: '#number' }\n* def loc2 = position('#second')\n* match loc1.y == loc2.y\n* match loc1.width == 102\n* match loc1.width == loc2.width"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/07.feature",
    "content": "Feature:\n\nBackground:\n* driver serverUrl + '/07'\n\nScenario:\n* input('#inputId', 'hello world')\n* click('input[name=submitName]')\n* match value('#inputId') == 'hello world'\n* match text('#valueId') == 'hello world'\n* match html('#valueId') == '<div id=\"valueId\">hello world</div>'\n* def expected = '72d72u69d69u76d76u76d76u79d79u32d32u87d87u79d79u82d82u76d76u68d68u'\n* if (driverType == 'chrome') expected = '104d104u101d101u108d108u108d108u111d111u32d32u119d119u111d111u114d114u108d108u100d100u'\n* if (driverType == 'safaridriver') expected = '72d72u69d69u76d76u76d76u79d79u65d65u87d87u79d79u82d82u76d76u68d68u'\n* match text('#pressedId') == expected\n\n* clear('#inputId')\n* waitFor('#inputId').input('hello world')\n* waitFor('input[name=submitName]').click()\n* match value('#inputId') == 'hello world'\n* match text('#valueId') == 'hello world'\n* match html('#valueId') == '<div id=\"valueId\">hello world</div>'\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/08.feature",
    "content": "Feature: \n\nBackground:\n* driver serverUrl + '/08'\n\nScenario: using css selector\n* match text('#messageId') == 'this div is outside the iframe'\n* switchFrame('#frameId')\n* input('#inputId', 'hello world')\n* click('input[name=submitName]')\n* match value('#inputId') == 'hello world'\n* match text('#valueId') == 'hello world'\n\n# switch back to parent frame\n* switchFrame(-1)\n* match text('#messageId') == 'this div is outside the iframe'\n\nScenario: using frame index\n* match text('#messageId') == 'this div is outside the iframe'\n* switchFrame(0)\n* input('#inputId', 'hello world')\n* click('input[name=submitName]')\n* match value('#inputId') == 'hello world'\n* match text('#valueId') == 'hello world'\n\n# switch back to parent frame\n* switchFrame(-1)\n* match text('#messageId') == 'this div is outside the iframe'\n\nScenario: upload within frame\n* if (driverType != 'chrome') karate.abort()\n* switchFrame('#uploadFrameId')\n* driver.inputFile('#fileToUpload', '08.pdf')\n* click('#uploadButton')\n* waitForText('#uploadMessage', '08.pdf')\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/09.feature",
    "content": "Feature: \n\nBackground:\n* driver serverUrl + '/09'\n\nScenario: Wildcard locators\n* def parent = waitFor('.div-02')\n# make sure the find-by-text works relative to search node and not from document root\n* def found = parent.locate('{}Some Text')\n* match found.attribute('class') == 'div-04'\n\n# find all, exact match\n* def list1 = locateAll('{}Some Text')\n* assert list1.length == 2\n\n# find all, contains match\n* def list2 = locateAll('{^}Text')\n* assert list2.length == 2\n\n# works with exists too\n* match exists('{}Some Text') == true"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/10.feature",
    "content": "Feature:\n\nBackground:\n* driver serverUrl + '/10'\n\nScenario: scroll to a text and type in the input on the right\n* waitFor('{}this test verifies an element can be located even when the page needs scrolling')\n* scroll('{}Label without scroll :').input('it works')\n\n# TODO rightOf() not working only on firefox (is apple m1 the reason ?)\n* if (driverType == 'geckodriver') karate.abort()\n* scroll('{}Label with scroll :').rightOf().input('it should not fail')\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/11.feature",
    "content": "Feature: \n\nBackground:\n* driver serverUrl + '/11'\n\nScenario:\n* click(\"#helloDiv\")\n* switchPage('/11_tab')\n* waitForUrl('/11_tab')\n* match text('#content') == 'Page 11 Tab'"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/12.feature",
    "content": "Feature:\n\nScenario:\n* driver 'https://invalid/url'\n* def frameTree = driver.send({ method: 'Page.getFrameTree' })\n* def unreachableUrl = frameTree.result.frameTree.frame.unreachableUrl\n* match unreachableUrl == 'https://invalid/url'\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/13.feature",
    "content": "Feature:\n\nBackground:\n  * driver serverUrl + '/13'\n\nScenario: try out iframe scenarios\n  * match text(\"div#messageId\") == \"this div is outside the iframe\"\n  * waitFor(\"#myiframe\")\n  * switchFrame(\"#myiframe\")\n  ## matching Wikipedia page title\n  ## hopefully won't change often :)\n   * match driver.title == \"Office Space - Wikipedia\"\n#  * click('#p-search a')\n#  * input('body', 'karate' + Key.ENTER)\n#  * waitFor('a[title=Karate]').click()\n#  * waitForUrl('https://en.wikipedia.org/wiki/Karate')\n#  * match driver.title == \"Karate - Wikipedia\"\n  * switchFrame(null)\n  # trying the same thing with locate chained by switchFrame()\n  * locate(\"iframe[id='myiframe']\").switchFrame()\n  * switchFrame(null)\n  * switchFrame(\"#frameId\")\n  * input(\"input#inputId\", \"testing input\")\n  * click(\"input#submitId\")\n  * match text(\"div#valueId\") == \"testing input\"\n  * match optional(\"div#messageId\").present == false\n  * switchFrame(null)\n  * match text(\"div#messageId\") == \"this div is outside the iframe\"\n\n  # maybe fix also solves for #1715\n  * switchFrame(\"#iframe08\")\n  * switchFrame(\"#frameId\")\n  * input(\"input#inputId\", \"testing input\")\n  * click(\"input#submitId\")\n  * match text(\"div#valueId\") == \"testing input\"\n  * switchFrame(null)\n\n    # * locate(\"iframe[name='myiframe']\").switchFrame()\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/14.feature",
    "content": "Feature:\n\nBackground:\n  * driver serverUrl + '/14'\n\nScenario: try out nested iframe scenarios\n  # switch into nested iframe and fill out form\n  * switchFrame(\"#nestedParent\")\n  * waitFor(\"#nestedChild\")\n  * switchFrame(\"#nestedChild\")\n  * waitFor(\"input[name='search']\")\n  * input(\"input[name='search']\", \"hello world\")\n\n  # switch back to root frame and click \"continue\"\n  * switchFrame(null)\n  * click('#continueButton')\n\n  # wait for \"processing\" => nested iframe is replaced and is then removed in background\n  * waitForText('body', 'Success')\n\n  # after successful processing we'll get a \"finish button\" that creates our final state\n  * click('#finish')\n  * match html('#result') == '<p id=\"result\">Done</p>'\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/15.feature",
    "content": "Feature:\n\nBackground:\n* driver serverUrl + '/15'\n\nScenario:\n# xpath script in whole document\n* def first = script('//li', '_.textContent')\n* match first == 'item 1'\n\n# xpath scriptAll in whole document\n* def list = scriptAll('//li', '_.textContent')\n* match list == ['item 1', 'item 2', 'item 2.1', 'item 2.2', 'item 3']\n\n# get first ul in entire document\n* def first = locate('//ul')\n# validates that operations other than locate also work...\n* match attribute('//ul', 'id') == 'first'\n\n# relative xpath - locate\n* def second = first.locate('./ul/li')\n* match second.text == 'item 2.1'\n\n# relative xpath - locate all\n* def seconds = second.locateAll('..//li')\n* assert seconds.length == 2\n* match (seconds[1].text) == 'item 2.2'\n\n# relative xpath - script all\n* match first.locateAll('./li').length == 3\n* def level1 = first.scriptAll('./li', '_.textContent')\n* match level1 == ['item 1', 'item 2', 'item 3']\n\n# relative xpath (any depth) script all\n* def all = first.scriptAll('.//li', '_.textContent')\n* match all == ['item 1', 'item 2', 'item 2.1', 'item 2.2', 'item 3']\n\n* def children = first.children\n* match (children.length) == 4\n* match (children[0].text) == 'item 1'\n* match (children[0].parent).attribute('id') == 'first'"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/16.feature",
    "content": "Feature:\n\n  Background:\n    * driver serverUrl + '/16'\n\n  Scenario:\n    # compare screenshot (normally would read a baseline from disk but we'll simulate here)\n    * def baselineBytes = screenshot()\n    * compareImage { baseline: '#(baselineBytes)', latest: '#(baselineBytes)' }\n\n    # compare mismatched screenshot with ignoredBoxes\n    * click('#show')\n    * def latestBytes = screenshot()\n    * def ignoredBoxes =\n    \"\"\"\n    [{\n      top: 0,\n      bottom: 200,\n      left: 0,\n      right: 200\n    }]\n    \"\"\"\n    * compareImage { baseline: '#(baselineBytes)', latest: '#(baselineBytes)', options: { ignoredBoxes: '#(ignoredBoxes)' } }\n\n    # compare mismatched screenshot: allowed to fail with custom failureThreshold\n    * compareImage { baseline: '#(baselineBytes)', latest: '#(latestBytes)', options: { failureThreshold: 99.9 } }\n\n    # compare mismatched screenshot: allowed to fail with mismatchShouldPass\n    * configure imageComparison = { mismatchShouldPass: true }\n    * def result = karate.compareImage(baselineBytes, latestBytes)\n    * match result.isMismatch == true\n\n    # missing baseline screenshot: allowed to fail with mismatchShouldPass\n    * def result = karate.compareImage(null, latestBytes)\n    * match result.isBaselineMissing == true\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/17.feature",
    "content": "Feature:\n\n  Background:\n    * driver serverUrl + '/17_a'\n\n  Scenario:\n    * def openPageInNewTab = (pageUrl) => driver.script(`window.open('${pageUrl}', '_blank', 'noopener')`)\n\n    * waitFor('input[name=a]').input('a1')\n\n    * openPageInNewTab(serverUrl + '/17_b')\n    * switchPage('17_b')\n    * waitFor('input[name=b]').input('b1')\n    * close()\n\n    * switchPage('17_a')\n    * input('input[name=a]', 'a2')\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/18.feature",
    "content": "Feature:\n\n  Background:\n \n  Scenario: Retry\n\n    * driver serverUrl + '/02'\n    * def start = new Date().getTime();\n# wait for slow loading element\n    * retry(3, 1500).click('#slowDiv')\n    * def end = new Date().getTime();\n    * def elapsedTime = end - start\n    * print \"Elapsed time:\", elapsedTime\n# setTimeout in the html page kicked off when the page was loaded, which is a few (50-ish) ms before we get here.\n# Since slowDiv takes 2s to load, click() should return not faster than 2s - 50ms, and not longer than 2s + some arbitrary buffer \n    * assert elapsedTime > 1550\n    * assert elapsedTime < 3500    \n\n  Scenario: submit\n\n    * driver serverUrl + '/18'\n    * submit().click('#slowlink')\n    * match optional('#containerDiv').present == true"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/19.feature",
    "content": "Feature:\n\n  Background:\n    * driver serverUrl + '/19'\n    * timeout(2000)\n\n  Scenario: Friendly locators\n\n    * match locate('{^i}Center').rightOf().find('span').attribute('id') == 'e'\n    * match rightOf('{^i}Center').find('span').attribute('id') == 'e'\n    * match leftOf('{^i}Center').find('span').attribute('id') == 'w'\n    * match above('{^i}Center').find('span').attribute('id') == 'n'\n    * match below('{^i}Center').find('span').attribute('id') == 's'        \n    * match leftOf('{^i}Center').find('{^b}West').attribute('id') == 'w_nested'\n# PW returns them all sorted by distance. Currently, the Karate API only allows find() to return one element.\n    * match near('{^i}Center').find('span').attribute('id') == 'n' \n    * if (driverType != 'playwright') karate.abort()\n    # Drivers other than playwright will fail the tests below because:\n    # they will fail calling rightOf('{^i}Center').find('{^b}West') rather than return an element with present = false\n    # they don't support xpath in find()\n    * match rightOf('{^i}Center').find('{^b}West').present == false\n    * match locate('//i').rightOf().find('//span').attribute('id') == 'e'\n    * match locate('{^i}Center').rightOf().find('//span').attribute('id') == 'e'    \n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/99_bootstrap.feature",
    "content": "Feature: \n\nBackground:\n* driver serverUrl + '/99_bootstrap'\n\nScenario: using the mouse to click and select something in a js-powered dropdown\n\n# can help for some pages that take time to load\n* waitUntil(\"document.readyState == 'complete'\")\n\n# click on button to show dropdown options\n* mouse('button').click()\n\n# click on first option\n* mouse('a.dropdown-item').click()\n\n# asserted expected result\n* match text('#container') == 'First'\n\n\nScenario: looping over data to repeat an action\n\n# can help for some pages that take time to load\n* waitUntil(\"document.readyState == 'complete'\")\n\n# get all possible drop-down elements\n* def list = locateAll('a.dropdown-item')\n* def results = []\n* def fun =\n\"\"\"\nfunction(e) {\n    mouse('button').click();\n    e.mouse().click();\n    let result = text('#container').trim();\n    results.push(result);\n    delay(2000);\n}\n\"\"\"\n\n# perform the loop to click on all dropdown items\n* list.forEach(fun)\n\n# assert at the end for the data collected\n* match results == ['First', 'Second', 'Third']\n\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/DockerRunner.java",
    "content": "package driver;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.http.HttpServer;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n * @author pthomas3\n */\nclass DockerRunner {\n\n    static HttpServer server;\n\n    @BeforeAll\n    static void beforeAll() {\n        server = ServerStarter.start(0);\n    }\n\n    @Test\n    void testAll() {\n        Results results = Runner.path(\"src/test/java/driver/00.feature\")\n                .systemProperty(\"server.port\", server.getPort() + \"\")\n                .karateEnv(\"docker\")\n                .configDir(\"src/test/java/driver\").parallel(1);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }\n    \n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/JavaApiPlaywrightRunner.java",
    "content": "package driver;\n\nimport com.intuit.karate.Match;\nimport com.intuit.karate.driver.Driver;\nimport com.intuit.karate.http.HttpServer;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass JavaApiPlaywrightRunner {\n\n    static HttpServer server;\n    static String serverUrl;\n\n    @BeforeAll\n    static void beforeAll() {\n        server = ServerStarter.start(0);\n        serverUrl = \"http://localhost:\" + server.getPort();\n    }\n\n    @Test\n    void testPlaywright() {\n        Driver driver = Driver.start(\"playwright\");\n        driver.setUrl(serverUrl + \"/01\");\n        driver.waitForUrl(serverUrl + \"/01\");\n        Match.that(driver.getTitle()).isEqualTo(\"Page 01\");\n        Match.that(driver.text(\"#pageLoadCount\")).isEqualTo(\"1\");\n        driver.refresh();\n        driver.waitForText(\"#pageLoadCount\", \"2\");\n        driver.reload();\n        driver.waitForText(\"#pageLoadCount\", \"3\");        \n        driver.quit();\n    }\n\n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/JavaApiRunner.java",
    "content": "package driver;\n\nimport com.intuit.karate.Http;\nimport com.intuit.karate.Json;\nimport com.intuit.karate.Match;\nimport com.intuit.karate.driver.Driver;\nimport com.intuit.karate.driver.chrome.Chrome;\nimport com.intuit.karate.http.HttpServer;\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass JavaApiRunner {\n\n    static HttpServer server;\n    static String serverUrl;\n\n    @BeforeAll\n    static void beforeAll() {\n        server = ServerStarter.start(0);\n        serverUrl = \"http://localhost:\" + server.getPort();\n    }\n\n    @Test\n    void testChromeHybrid() {\n        Driver driver = Driver.start(\"chrome\");\n        driver.setUrl(serverUrl + \"/05\");\n        \n        Map response = Http.to(serverUrl + \"/api/05\").get().json().asMap();\n        Match.that(response).isEqualTo(\"{ message: 'hello world' }\");\n        \n        driver.click(\"button\");\n        driver.waitForText(\"#containerDiv\", \"hello world\");\n        \n        Chrome chrome = (Chrome) driver; // intercept() is only supported by chrome\n        Json json = Json.of(\"{ patterns: [{ urlPattern: '*/api/*' }] }\");\n        json.set(\"mock\", \"src/test/java/driver/05_mock.feature\");\n        chrome.intercept(json.asMap());\n        \n        driver.click(\"button\");\n        driver.waitForText(\"#containerDiv\", \"hello faked\");\n        driver.quit();\n    }\n\n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/LocalParallelRunner.java",
    "content": "package driver;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.http.HttpServer;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass LocalParallelRunner {\n    \n    static HttpServer server;\n    \n    @BeforeAll\n    static void beforeAll() {\n        server = ServerStarter.start(0);        \n    }\n    \n    @Test\n    void testMock() {\n        Results results = Runner.path(\"src/test/java/driver/00_outline.feature\")\n                .systemProperty(\"server.port\", server.getPort() + \"\")\n                .systemProperty(\"skip.slow.tests\", \"true\")\n                .karateEnv(\"xbrowser\")               \n                .configDir(\"src/test/java/driver\").parallel(5);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }    \n    \n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/LocalPlaywrightRunner.java",
    "content": "package driver;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.http.HttpServer;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass LocalPlaywrightRunner {\n    \n    static HttpServer server;\n    \n    @BeforeAll\n    static void beforeAll() {\n        server = ServerStarter.start(0);        \n    }\n    \n    @Test\n    void testMock() {\n        Results results = Runner.path(\"src/test/java/driver/00.feature\")\n                .systemProperty(\"server.port\", server.getPort() + \"\")\n                .systemProperty(\"driver.type\", \"playwright\")\n                .karateEnv(\"playwright\")\n                .configDir(\"src/test/java/driver\").parallel(1);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }    \n    \n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/LocalSingleRunner.java",
    "content": "package driver;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.http.HttpServer;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass LocalSingleRunner {\n    \n    static HttpServer server;\n    \n    @BeforeAll\n    static void beforeAll() {\n        server = ServerStarter.start(0);        \n    }\n    \n    void run(String id) {\n        Results results = Runner.path(\"src/test/java/driver/\" + id + \".feature\")\n                .karateEnv(\"single\")\n                .systemProperty(\"server.port\", server.getPort() + \"\")\n                .systemProperty(\"driver.type\", \"chrome\")\n                .configDir(\"src/test/java/driver\").parallel(1);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());        \n    }\n    \n    @Test\n    void testSingle() {\n        run(\"00\");\n    }\n\n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/ServerStarter.java",
    "content": "package driver;\n\nimport com.intuit.karate.http.HttpServer;\nimport com.intuit.karate.http.ServerConfig;\nimport com.intuit.karate.http.ServerContext;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\npublic class ServerStarter {\n\n    @Test\n    void testServer() {\n        HttpServer server = start(8080);\n        server.waitSync();\n    }\n\n    public static HttpServer start(int port) {\n        ServerConfig config = new ServerConfig(\"src/test/java/driver/html\")                \n                .autoCreateSession(true)\n                .homePagePath(\"/00\");\n        return HttpServer.config(config).http(port).build();\n    }\n\n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/00.css",
    "content": "body { font-family: sans-serif; } \ntable { border-collapse: collapse; } \ntable td { border: 1px solid gray; padding: 0.1em 0.2em; }\nbutton { font-size: medium; padding: 0.5em; margin: 0.5em; }\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/00.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Driver Tests Home Page</title>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n    <p><a href=\"/01\">01</a></p>\n    <p><a href=\"/02\">02</a></p>\n    <p><a href=\"/03\">03</a></p>\n  </body>\n</html>\n\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/00.js",
    "content": "var karate = {};\nkarate.get = function(id) { return document.getElementById(id) };\nkarate.setHtml = function(id, value) { this.get(id).innerHTML = value };\nkarate.addHtml = function(id, value) { var e = this.get(id); e.innerHTML = e.innerHTML + value };\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/01.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 01</title>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n    <script ka:scope=\"global\">\n      session.counter01 = session.counter01 || 0;\n      session.counter01++;\n    </script>    \n    <div>this page has been loaded\n      <span th:text=\"session.counter01\" id=\"pageLoadCount\"></span> times\n    </div>\n    <a href=\"/02\">Page 02</a>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/02.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 02</title>\n    <script src=\"00.js\"></script>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n    <script>\n      setTimeout(function () {\n        karate.setHtml('containerDiv', '<div id=\"slowDiv\">APPEARED!</div>')\n      }, 2000)\n    </script>\n  </head>\n  <body>\n    <p>this test will wait for a slow loading element</p>\n    <div id=\"containerDiv\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/03.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 03</title>\n    <script src=\"00.js\"></script>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n    <script>\n      setTimeout(function () {\n        karate.setHtml('helloDiv', 'hello world');\n      }, 2000);\n    </script>\n    <style>\n      .styled-div { font-size: 30px; }\n    </style>     \n  </head>\n  <body>\n    <p>this test will wait until some text appears</p>\n    <div id=\"helloDiv\" style=\"color:red\"></div>\n    <p>the test also gets the style of the text below</p>\n    <div class=\"styled-div\">Some Other Text</div>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/04.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 04</title>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">  \n  </head>\n  <body>\n    <script ka:scope=\"global\">\n      response.header('Set-Cookie', 'foo=bar')\n    </script>\n    <p>cookies test</p>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/05.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 05</title>\n    <script src=\"00.js\"></script>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n    <script>\n      var makeApiCall = function() {\n        fetch('/api/05?foo=bar')\n            .then(response => response.json())\n            .then(data => karate.setHtml('containerDiv', data.message));\n      }\n    </script>  \n  </head>\n  <body>\n    <p>driver.intercept</p>\n    <button onclick=\"makeApiCall()\">Click Me</button>\n    <div id=\"containerDiv\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/06.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 06</title>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n    <style>\n      #containerDiv div { display: inline-block; border: 1px solid gray; width: 100px; height: 100px; }\n    </style>\n  </head>\n  <body>\n    <p>position</p>\n    <div id=\"containerDiv\">\n      <div id=\"first\">first</div>\n      <div id=\"second\">second</div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/07.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 07</title>\n    <script src=\"00.js\"></script>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n    <script>\n      function keyPressed(e, t) {\n        var div = karate.get('pressedId');\n        var v = div.innerHTML + e.keyCode + t;\n        div.innerHTML = v;\n      }\n    </script>\n  </head>\n  <body>\n    <p>input</p>\n    <input name=\"inputName\" id=\"inputId\" type=\"text\" onkeydown=\"return keyPressed(event, 'd')\" onkeyup=\"return keyPressed(event, 'u')\"/>\n    <input name=\"submitName\" id=\"submitId\" type=\"submit\" onclick=\"karate.setHtml('valueId', karate.get('inputId').value)\"/>\n    <div id=\"valueId\"></div>\n    <div id=\"pressedId\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/08.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 08</title>\n    <script src=\"00.js\"></script>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n    <div id=\"messageId\">this div is outside the iframe</div>\n    <iframe src=\"07\" id=\"frameId\"></iframe>\n    <p>&nbsp;</p>\n    <iframe src=\"08_upload\" id=\"uploadFrameId\"></iframe>\n  </body>\n</html>\n\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/08_upload.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 08 upload</title>\n    <script src=\"00.js\"></script>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <script ka:scope=\"global\">\n    if (request.post) {\n      var uploaded = request.multiPart('myFile');\n    }\n  </script>  \n  <body>\n    <h1 th:if=\"request.post\" th:text=\"uploaded.filename\" id=\"uploadMessage\"></h1>\n    <form th:if=\"!request.post\" action=\"08_upload\" method=\"post\" enctype=\"multipart/form-data\">\n      <input type=\"file\" name=\"myFile\" id=\"fileToUpload\"/>\n      <input type=\"submit\" value=\"Upload\" id=\"uploadButton\"/>\n    </form>\n  </body>\n</html>\n\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/09.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 09</title>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n    <div class=\"div-01\">Some Text</div>\n    <div class=\"div-02\">\n      <div class=\"div-03\">\n        <div class=\"div-04\">Some Text</div>\n      </div>\n    </div>    \n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/10.html",
    "content": "<!doctype html>\n<html>\n<head>\n  <title>Page 09</title>\n  <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>\n  <link rel=\"icon\" href=\"00.ico\">\n</head>\n<body>\n<h1>this test verifies an element can be located even when the page needs scrolling</h1>\n<form>\n  <label for=\"working\">Label without scroll :</label><input type=\"text\" name=\"working\" id=\"working\"/>\n  <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>\n  <label for=\"should_work\">Label with scroll :</label><input type=\"text\" name=\"should_work\" id=\"should_work\"/>\n</form>\n</body>\n</html>"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/11.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 11</title>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n    <a id=\"helloDiv\" href=\"11_tab\" target=\"_blank\">Click Me</a>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/11_tab.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 11 Tab</title>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n    <div id=\"content\">Page 11 Tab</div>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/13.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 13</title>\n    <script src=\"00.js\"></script>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n    <div id=\"messageId\">this div is outside the iframe</div>\n    <p>&nbsp;</p>\n    <iframe src=\"07\" id=\"frameId\"></iframe>\n    <br />\n    <p></p>\n    <iframe src=\"https://en.wikipedia.org/wiki/Office_Space\" id=\"myiframe\" width=\"800\" height=\"400\"></iframe>\n    <br />\n    <p></p>\n    <iframe src=\"08\" id=\"iframe08\" width=\"800\" height=\"400\"></iframe>\n  </body>\n</html>\n\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/14.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 14</title>\n    <script src=\"00.js\"></script>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n    <script>\n      function simulateProcessing() {\n        karate.get('nestedParent').src = '14_processing.html';\n        setTimeout(function () {\n          karate.setHtml('result', 'Success');\n          karate.get('container2').style.display = 'inline';\n          karate.get('container1').remove();\n        }, 3000);\n      }\n\n      function finish() {\n        karate.setHtml('result', 'Done');\n        karate.get('finish').remove();\n      }\n    </script>\n  </head>\n  <body>\n    <div id=\"container1\">\n      <iframe src=\"14_embedded\" id=\"nestedParent\" width=\"1200\" height=\"400\"></iframe>\n      <br/>\n      <button id=\"continueButton\" onclick=\"simulateProcessing()\">Continue</button>\n    </div>\n    <div id=\"container2\" style=\"display:none\">\n      <p id=\"result\"></p>\n      <button id=\"finish\" onclick=\"finish()\">Finish</button>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/14_embedded.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 14 - Embedded</title>\n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n    <iframe id=\"nestedChild\" src=\"https://en.wikipedia.org/wiki/Main_Page\" width=\"1200\" height=\"400\"></iframe>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/14_processing.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 14 - Processing</title>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n    <p>Processing...</p>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/15.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 15</title>\n    <script src=\"00.js\"></script>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n    <ul id=\"first\">\n      <li>item 1</li>\n      <li>item 2</li>\n      <ul>\n        <li>item 2.1</li>\n        <li>item 2.2</li>\n      </ul>\n      <li>item 3</li>\n    </ul>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/16.html",
    "content": "<!doctype html>\n<html>\n<head>\n    <title>Page 16</title>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>\n    <link rel=\"icon\" href=\"00.ico\">\n    <style>\n        .img-container {\n            margin: 0;\n            padding: 0;\n            width: 200px;\n            height: 200px;\n            position: absolute;\n            top: 0;\n            left: 0;\n        }\n        .button-container {\n            position: absolute;\n            top: 220px;\n            left: 0;\n        }\n    </style>\n</head>\n<body>\n<div class=\"img-container\">\n    <img id='gif' src=\"16.gif\" alt=\"Karate animated GIF\" style=\"visibility:hidden\"/>\n</div>\n<div class=\"button-container\">\n    <button id=\"show\" onclick=\"document.getElementById('gif').style.visibility='visible'\">Show</button>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/17_a.html",
    "content": "<!doctype html>\n<html>\n<head>\n    <title>Page 17_a</title>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>\n    <link rel=\"icon\" href=\"00.ico\">\n</head>\n<body>\n<input type=\"text\" name=\"a\">\n</body>\n</html>"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/17_b.html",
    "content": "<!doctype html>\n<html>\n<head>\n    <title>Page 17_b</title>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>\n    <link rel=\"icon\" href=\"00.ico\">\n</head>\n<body>\n<input type=\"text\" name=\"b\">\n</body>\n</html>"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/18.html",
    "content": "<!doctype html>\n<html>\n<head>\n    <title>Page 18</title>\n</head>\n<body>\n    <button id=\"slowlink\" onclick=\"setTimeout(function() {window.location='06.html'}, 600)\">Slow link</button>\n</body>\n</html>"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/19.html",
    "content": "<!doctype html>\n<html>\n<head>\n    <title>Page 19</title>\n</head>\n<body>\n<table>\n    <tr>\n        <td><span id=\"nw\">Span <b>North West</b></span></td><td><span id=\"n\">Span <b>North</b></span></td><td><span id=\"ne\">Span <b>North East</b></span></td>\n    </tr>\n\n    <tr>\n        <td><span id=\"w\">Span <b id=\"w_nested\">West</b></span></td><td><i>Center</i></td><td><span id=\"e\">Span <b>East</b></span></td>        \n    </tr>\n    <tr>\n        <!-- Addind an extra line so that near() unambigiously points to north (without it, north and south would be equally close to center) -->\n        <td>&nbsp;</td>\n    </tr>\n    <tr>\n        <td><span id=\"sw\">Span <b>South West</b></span></td><td><span id=\"s\">Span <b>South</b></span></td><td><span id=\"se\">Span <b>South East</b></span></td>\n    </tr>\n</table>    \n\n</body>\n</html>"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/99_bootstrap.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Bootstrap</title>\n    <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ\" crossorigin=\"anonymous\">\n    <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe\" crossorigin=\"anonymous\"></script>\n    <script src=\"00.js\"></script>\n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n    <div class=\"dropdown\">\n      <button class=\"btn btn-secondary dropdown-toggle\" type=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n        Dropdown button\n      </button>\n      <ul class=\"dropdown-menu\">\n        <li><a class=\"dropdown-item\" href=\"#\" onclick=\"karate.setHtml('container', this.textContent)\">First</a></li>\n        <li><a class=\"dropdown-item\" href=\"#\" onclick=\"karate.setHtml('container', this.textContent)\">Second</a></li>\n        <li><a class=\"dropdown-item\" href=\"#\" onclick=\"karate.setHtml('container', this.textContent)\">Third</a></li>\n      </ul>\n    </div>\n    <div id=\"container\"></div>\n  </body>\n</html>\n\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/api/05.js",
    "content": "response.body = {\n  message: 'hello world'\n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/html/scratch.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Scratch</title>\n    <script src=\"00.js\"></script>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n  </head>\n  <body>\n   \n  </body>\n</html>\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/karate-config-docker.js",
    "content": "function fn() {\n  karate.configure('driver', {\n    type: 'chrome',\n    showDriverLog: true,\n    start: false,\n    beforeStart: 'supervisorctl start ffmpeg',\n    afterStop: 'supervisorctl stop ffmpeg',\n    videoFile: '/tmp/karate.mp4'\n  });\n  var hostname = com.intuit.karate.FileUtils.isOsWindows() ? 'host.docker.internal' : 'localhost'\n  var serverPort = karate.properties['server.port'] || 8080;\n  var config = {\n        driverType: 'chrome',\n        serverUrl: 'http://' + hostname + ':' + serverPort\n    };\n  return config;\n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/karate-config-playwright.js",
    "content": "function fn() {\n  var driverType = karate.properties['driver.type'] || 'playwright';\n  karate.log('using driver:', driverType);\n  // the executable defaults to 'playwright' so a shell script with that name in PATH will do\n  karate.configure('driver', {type: driverType, showDriverLog: true});\n  return { driverType: driverType };\n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/karate-config-single.js",
    "content": "function fn() {\n  var driverType = karate.properties['driver.type'] || 'chrome';\n  karate.log('using driver:', driverType);\n  karate.configure('driver', {type: driverType, showDriverLog: true});\n  return {driverType: driverType};\n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/karate-config-xbrowser.js",
    "content": "function fn() {\n  karate.log('using driver:', driverType);\n  karate.configure('driver', {type: driverType, showDriverLog: true});\n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/karate-config.js",
    "content": "function fn() {\n  var serverPort = karate.properties['server.port'] || 8080;\n  var config = {\n    serverUrl: 'http://localhost:' + serverPort\n  };\n  return config;\n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/driver/scratch.feature",
    "content": "Feature:\n\nScenario:\n* driver serverUrl + '/scratch'\n\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit.karate\" level=\"DEBUG\"/>\n    <logger name=\"driver\" level=\"DEBUG\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>"
  },
  {
    "path": "karate-e2e-tests/src/test/java/regex/RegexRunner.java",
    "content": "package regex;\n\nimport com.intuit.karate.Results;\nimport com.intuit.karate.Runner;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\nclass RegexRunner {\n    \n    @Test\n    void testMock() {\n        Results results = Runner.path(\"src/test/java/regex/regex.feature\").parallel(1);\n        assertEquals(0, results.getFailCount(), results.getErrorMessages());\n    }    \n    \n}\n"
  },
  {
    "path": "karate-e2e-tests/src/test/java/regex/regex.feature",
    "content": "Feature:\n\nScenario:\n* def temp = 'regex ng'.replace(/ng/, 'ok')\n* match temp == 'regex ok'\n"
  },
  {
    "path": "karate-gatling/README.md",
    "content": "# Karate Gatling\n## API Perf-Testing Made `Simple.`\n\n# Index\n\n<table>\n<tr>\n  <th>Start</th>\n  <td>\n      <a href=\"#maven\">Maven</a>\n    | <a href=\"#gradle\">Gradle</a>\n    | <a href=\"#java-dsl\">Java DSL</a>\n    | <a href=\"#logging\">Logging</a>\n    | <a href=\"#limitations\">Limitations</a>\n    | <a href=\"#usage\">Usage</a>    \n  </td>\n</tr>\n<tr>\n  <th>API</th>\n  <td>\n      <a href=\"#karateprotocol\"><code>karateProtocol()</code></a>\n    | <a href=\"#nameresolver\"><code>nameResolver</code></a>\n    | <a href=\"#pausefor\"><code>pauseFor()</code></a>\n    | <a href=\"#runner\"><code>runner</code></a>\n    | <a href=\"#karatefeature\"><code>karateFeature()</code></a>\n    | <a href=\"#karateset\"><code>karateSet()</code></a>\n    | <a href=\"#tag-selector\">Tag Selector</a>\n    | <a href=\"#ignore-tags\">Ignore Tags</a>\n  </td>\n</tr>\n<tr>\n  <th>Advanced</th>\n  <td>\n      <a href=\"#gatling-session\">Session</a>\n    | <a href=\"#feeders\">Feeders</a>\n    | <a href=\"#chaining\">Chaining</a>\n    | <a href=\"#karatecallsingle\"><code>karate.callSingle()</code></a>\n    | <a href=\"#detecting-gatling-at-run-time\">Detecting Gatling At Run Time</a>\n    | <a href=\"#think-time\">Think Time</a>\n    | <a href=\"#configure-localaddress\"><code>configure localAddress</code></a>\n    | <a href=\"#custom\">Profiling Custom Java Code</a>\n    | <a href=\"#captureperfevent\"><code>PerfContext.capturePerfEvent()</code></a>\n    | <a href=\"#increasing-thread-pool-size\">Increasing Thread Pool Size</a>\n    | <a href=\"#distributed-testing\">Distributed Testing</a>   \n  </td>\n</tr>\n</table>\n\n### Capabilities\n* Re-use Karate tests as performance tests executed by [Gatling](https://gatling.io)\n* Use Gatling (and Scala) only for defining the load-model, everything else can be in Karate\n* Karate assertion failures appear in Gatling report, along with the line-numbers that failed\n* Leverage Karate's powerful assertion capabilities to check that server responses are as expected under load - which is much harder to do in Gatling and other performance testing tools\n* API invocation sequences that represent end-user workflows are much easier to express in Karate\n* [*Anything*](#custom) that can be written in Java can be performance tested !\n* Option to scale out by [distributing a test](#distributed-testing) over multiple hardware nodes or Docker containers\n\n## Demo Video\nRefer to this [webinar recording](https://www.youtube.com/watch?v=WT4gg7Jutzg&t=714s).\n\n### Maven\n\n> also see [using a Maven Profile](#using-a-maven-profile).\n\n```xml\n<dependency>\n    <groupId>io.karatelabs</groupId>\n    <artifactId>karate-gatling</artifactId>\n    <version>${karate.version}</version>\n    <scope>test</scope>\n</dependency>  \n```\n\nYou will also need the [Gatling Maven Plugin](https://github.com/gatling/gatling-maven-plugin) and the [Scala Maven Pugin](https://github.com/davidB/scala-maven-plugin). Refer to the [sample project](../examples/gatling) for how to use this for a typical Karate project where feature files are in `src/test/java`. For convenience we recommend you keep even the Gatling simulation files in the same folder hierarchy, even though they are technically files with a `*.scala` extension.\n\n```xml\n  <plugin>\n      <groupId>io.gatling</groupId>\n      <artifactId>gatling-maven-plugin</artifactId>\n      <version>${gatling.plugin.version}</version>\n      <configuration>\n          <simulationsFolder>src/test/java</simulationsFolder>\n          <includes>\n              <include>mock.CatsKarateSimulation</include>\n          </includes>\n      </configuration>               \n  </plugin>\n```\n\nTo run the Gatling test:\n\n```\nmvn clean test-compile gatling:test\n```\n\nAnd in case you have multiple Gatling simulation files and you want to choose only one to run:\n\n```\nmvn clean test-compile gatling:test -Dgatling.simulationClass=mock.CatsKarateSimulation\n```\n\nIt is worth calling out that in the sample project, we are perf-testing [Karate test-doubles](https://hackernoon.com/api-consumer-contract-tests-and-test-doubles-with-karate-72c30ea25c18) ! A truly self-contained demo.\n\n#### Using a Maven Profile\nMixing `karate-gatling` into a project that already has other frameworks in `test` scope can cause problems such as library version conflicts or just slowing down your normal unit tests. You can use [Maven profiles](https://maven.apache.org/guides/introduction/introduction-to-profiles.html) to keep the Gatling dependencies and executions in a separate scope. Your \"primary\" Maven `<dependencies>` section can depend on `karate-core` or `karate-junit5` like normal.\n\nHere is an example:\n\n```xml\n<profiles>\n    <profile> \n        <id>gatling</id>\n        <dependencies>\n            <dependency>\n                <groupId>com.intuit.karate</groupId>\n                <artifactId>karate-gatling</artifactId>\n                <version>${karate.version}</version>\n                <scope>test</scope>\n            </dependency>                  \n        </dependencies>\n        <build>\n            <plugins>\n                <plugin>\n                    <groupId>net.alchim31.maven</groupId>\n                    <artifactId>scala-maven-plugin</artifactId>\n                    <version>4.5.6</version>\n                    <executions>\n                        <execution>\n                            <goals>\n                                <goal>testCompile</goal>\n                            </goals>\n                            <configuration>\n                                <args>\n                                    <arg>-Jbackend:GenBCode</arg>\n                                    <arg>-Jdelambdafy:method</arg>\n                                    <arg>-target:jvm-1.8</arg>\n                                    <arg>-deprecation</arg>\n                                    <arg>-feature</arg>\n                                    <arg>-unchecked</arg>\n                                    <arg>-language:implicitConversions</arg>\n                                    <arg>-language:postfixOps</arg>\n                                </args>\n                            </configuration>\n                        </execution>\n                    </executions>\n                </plugin>            \n                <plugin>\n                    <groupId>io.gatling</groupId>\n                    <artifactId>gatling-maven-plugin</artifactId>\n                    <version>${gatling.plugin.version}</version>\n                    <configuration>\n                        <simulationsFolder>src/test/java</simulationsFolder>\n                        <includes>\n                            <include>app.perf.TodoSimulation</include>\n                        </includes>\n                    </configuration>\n                    <executions>\n                        <execution>\n                            <phase>test</phase>\n                            <goals>\n                                <goal>test</goal>\n                            </goals>\n                        </execution>\n                    </executions>                                       \n                </plugin> \n            </plugins>\n        </build>\n    </profile>\n</profiles>\n```\n\nTo use the `gatling` Maven profile and run the performance-test simulation:\n\n```\nmvn clean test -P gatling\n```\n\nNote that because the `<execution>` phase is defined for `test`, just running `mvn clean test` will work. If you don't want to run Gatling tests as part of the normal Maven `test` lifecycle, you can avoid the `<executions>` section as described previously.\n\n### Gradle\n\nFor those who use [Gradle](https://gradle.org), this sample [`build.gradle`](../examples/gatling/build.gradle) provides a `gatlingRun` task that executes the Gatling test of the `karate-netty` project - which you can use as a reference. The approach is fairly simple, and does not require the use of any Gradle Gatling plugins.\n\nMost problems when using Karate with Gradle occur when \"test-resources\" are not configured properly. So make sure that all your `*.js` and `*.feature` files are copied to the \"resources\" folder - when you build the project.\n\n## Limitations\nKarate introduces a different threading model for the HTTP Client, so if you have to attain a high RPS (Requests Per Second) value, you need to introduce a config-file that is normally not required for \"vanilla\" Gatling. We have found that by default an RPS of around 30 is suppported, but to go higher - please see [Increasing Thread Pool Size](#increasing-thread-pool-size).\n\nAs of now the Gatling concept of [\"throttle\" and related syntax](https://gatling.io/docs/2.3/general/simulation_setup/#simulation-setup-throttling) is not supported. Most teams don't need this, but you can declare \"pause\" times in Karate, see [`pauseFor()`](#pausefor).\n\n## Logging\nOnce you have your performance tests working, you may want to tune the logging config. Note that there are options to [reduce or completely suppress](https://github.com/karatelabs/karate#logging) the console logging.\n\nAlso note that the [`logback-test.xml`](../examples/gatling/src/test/java/logback-test.xml) in the examples project uses [`<immediateFlush>false</immediateFlush>`](http://logback.qos.ch/manual/appenders.html#OutputStreamAppender).\n\n## Java DSL\nThe Gatling project now gives you the option to script performance tests in [Java instead of Scala](https://gatling.io/2021/11/gatling-3-7-java-dsl-kotlin-and-much-more/). This was [recently added to Karate](https://github.com/karatelabs/karate/pull/2434) as well, so please look at [this example](src/test/java/mock/CatsSimulation.java) until the documentation is updated.\n\n## Usage\n\nLet's look at an [example](src/test/java/mock/CatsSimulation.java):\n\n```java\npackage mock;\n\nimport com.intuit.karate.gatling.javaapi.KarateProtocolBuilder;\n\nimport io.gatling.javaapi.core.ScenarioBuilder;\nimport io.gatling.javaapi.core.Simulation;\n\nimport static io.gatling.javaapi.core.CoreDsl.*;\nimport static com.intuit.karate.gatling.javaapi.KarateDsl.*;\n\npublic class CatsSimulation extends Simulation {\n\n  public CatsSimulation() {\n      \n    KarateProtocolBuilder protocol = karateProtocol(\n      uri(\"/cats/{id}\").nil(),\n      uri(\"/cats\").pauseFor(method(\"get\", 15), method(\"post\", 25)\n    ));\n\n    protocol.nameResolver = (req, ctx) -> req.getHeader(\"karate-name\");\n    protocol.runner.karateEnv(\"perf\");\n\n    ScenarioBuilder create = scenario(\"create\").exec(karateFeature(\"classpath:mock/cats-create.feature\"));\n\n    ScenarioBuilder delete = scenario(\"delete\").exec(karateFeature(\"classpath:mock/cats-delete.feature@name=delete\"));\n\n    setUp(\n      create.injectOpen(rampUsers(10).during(5)).protocols(protocol),\n      delete.injectOpen(rampUsers(5).during(5)).protocols(protocol)\n    );\n  }\n}\n```\n\nor using the [Scala api](src/test/scala/mock/CatsSimulation.scala):\n\n```scala\npackage mock\n\nimport com.intuit.karate.gatling.PreDef._\nimport io.gatling.core.Predef._\nimport scala.concurrent.duration._\n\nclass CatsSimulation extends Simulation {\n\n  val protocol = karateProtocol(\n    \"/cats/{id}\" -> Nil,\n    \"/cats\" -> pauseFor(\"get\" -> 15, \"post\" -> 25)\n  )\n\n  protocol.nameResolver = (req, ctx) => req.getHeader(\"karate-name\")\n  protocol.runner.karateEnv(\"perf\")\n\n  val create = scenario(\"create\").exec(karateFeature(\"classpath:mock/cats-create.feature\"))\n  val delete = scenario(\"delete\").exec(karateFeature(\"classpath:mock/cats-delete.feature@name=delete\"))\n\n  setUp(\n    create.inject(rampUsers(10) during (5 seconds)).protocols(protocol),\n    delete.inject(rampUsers(5) during (5 seconds)).protocols(protocol)\n  )\n\n}\n```\n### `karateProtocol()`\nThis piece is needed because Karate is responsible for making HTTP requests while Gatling is only measuring the timings and managing threads. In order for HTTP requests to \"aggregate\" correctly in the Gatling report, you need to declare the URL patterns involved in your test. For example, in the example above, the `{id}` would be random - and Gatling would by default report each one as a different request.\n\n#### `nameResolver`\nThis is optional, and is useful for teams that need more control over the \"segregation\" of requests described above. This is especially needed for GraphQL and SOAP - where the URI and request-paths remain constant and only the payload changes. You can supply a function that takes 2 Karate core-objects as arguments. The first argument [`HttpRequestBuilder`](../karate-core/src/main/java/com/intuit/karate/http/HttpRequestBuilder.java) is all you would typically need, and gives you ways to access the HTTP request such as `getUrlAndPath()`, `getHeader(name)` and `getParameter(name)`. The example below over-rides the \"request name\" with the value of a custom-header:\n\n```scala\n protocol.nameResolver = (req, ctx) => req.getHeader(\"karate-name\")\n```\n\nFor convenience, if the `nameResolver` returns `null`, Karate will fall-back to  the default strategy. And `HttpRequestBuilder.getHeader(name)` happens to return `null` if the header does not exist.\n\nSo any HTTP request where a `karate-name` header is present can be \"collected\" in the Gatling report under a different name. This is how it could look like in a Karate feature ([example](src/test/scala/mock/cats-delete-one.feature)):\n\n```cucumber\nGiven path id\nAnd header karate-name = 'cats-get-404'\nWhen method get\n```\n\n#### `pauseFor()`\nYou can also set pause times (in milliseconds) per URL pattern *and* HTTP method (`get`, `post` etc.) if needed (see [limitations](#limitations)). If non-zero, this pause will be applied *before* the invocation of the matching HTTP request.\n\nWe recommend you set that to `0` for everything unless you really need to artifically limit the requests per second. Note how you can use `Nil` to default to `0` for all HTTP methods for a URL pattern. Make sure you wire up the `protocol` in the Gatling `setUp`. If you use a [`nameResolver`](#nameresolver), even those names can be used in the `pauseFor` lookup (instead of a URL pattern).\n\nAlso see how to [`pause()`](#think-time) without blocking threads if you really need to do it *within* a Karate feature, for e.g. to simulate user \"think time\" - in more detail.\n\n#### `runner`\nWhich feature to call and what tags to use are driven by the [`karateFeature()`](#karatefeature) syntax as described in later sections. Most of the time this would be sufficient. But in cases where you have custom configuration, you will need a way to replicate what you may be doing using the [`Runner.Builder`](https://github.com/karatelabs/karate#parallel-execution) methods. Most of the time this would be setting the `karate.env`.\n\nTo enable this, a `Runner.Builder` instance is made available on the `protocol` in a variable called `runner` and all the builder methods such as `karateEnv()`, `configDir()` and `systemProperty()` can be configured.\n\nNote that tags are typically set by the use of `karateFeature()`. If you call `tags()` on the `runner` instance, they will be inherited by all `karateFeature()` calls, where you can add more tags, but you can't *remove* any that were set on the `runner`.\n\nHere is an example of setting the `karate.env` to `perf` which means that `karate-config-perf.js` will be used in addition to `karate-config.js` for [bootstrapping the config](https://github.com/karatelabs/karate#configuration) for each `Scenario`.\n\n```scala\n  protocol.runner.karateEnv(\"perf\")\n```\n\nBut the alternate mechanism of setting a Java system-property `karate.env` via the command-line is always an option, so using the `runner` can be avoided in most cases.\n\n### `karateFeature()`\nThis declares a whole Karate feature as a \"flow\". Note how you can have concurrent flows in the same Gatling simulation.\n\n#### Tag Selector\nIn the code above, note how a single `Scenario` (or multiple) can be \"chosen\" by appending the [tag](https://github.com/karatelabs/karate#tags) name to the `Feature` path. This allows you to re-use only selected tests out of your existing functional or regression test suites for composing a performance test-suite.\n\nIf multiple `Scenario`-s have the tag on them, they will all be executed. The order of execution will be the order in which they appear in the `Feature`.\n\n> The tag does not need to be in the `@key=value` form and you can use the plain \"`@foo`\" form if you want to. But using the pattern `@name=someName` is arguably more readable when it comes to giving multiple `Scenario`-s meaningful names.\n\n#### Ignore Tags\nThe above [Tag Selector](#tag-selector) approach is designed for simple cases where you have to pick and run only one `Scenario` out of many. Sometimes you will need the full flexibility of [tag combinations](https://github.com/karatelabs/karate#tags) and \"ignore\". The `karateFeature()` method takes an optional (vararg) set of Strings after the first feature-path argument. For example you can do this:\n\n```scala\n  val delete = scenario(\"delete\").exec(karateFeature(\"classpath:mock/cats-delete.feature\", \"@name=delete\"))\n```\n\nTo exclude (note that `@ignore` is skipped by default):\n\n```scala\n  val delete = scenario(\"delete\").exec(karateFeature(\"classpath:mock/cats-delete.feature\", \"~@skipme\"))\n```\n\nTo run scenarios tagged `foo` OR `bar`\n\n```scala\n  val delete = scenario(\"delete\").exec(karateFeature(\"classpath:mock/cats-delete.feature\", \"@foo,@bar\"))\n```\n\nAnd to run scenarios tagged `foo` AND `bar`\n\n```scala\n  val delete = scenario(\"delete\").exec(karateFeature(\"classpath:mock/cats-delete.feature\", \"@foo\", \"@bar\"))\n```\n\n#### Silent execution\nIt is possible to set a `karateFeature()` to be silent, this allows the request executions to not be counter towards the gatling statistics, this is specially useful if you are planning to execute a warm-up process that could call the possible flows before the performance test starts, making sure all the flows will be compiled on the server before counting statistics like request time.\n\n### Karate Variables\nOn the Scala side, after a `scenario` involving a [`karateFeature()`](#karatefeature) completes, the Karate variables that were part of the feature will be added to the [Gatling session](#gatling-session).\n\nThis is rarely needed - but useful when you want to pass data across feature files or do some assertions on the Gatling side. Here is an [example](src/test/scala/mock/CatsSimulation.scala):\n\n```scala\nval create = scenario(\"create\").exec(karateFeature(\"classpath:mock/cats-create.feature\")).exec(session => {\n    println(\"*** id in gatling: \" + session(\"id\").as[String])\n    println(\"*** session status in gatling: \" + session.status)\n    session\n  })\n```\n\nHere above, the variable `id` that was defined (using `def`) in the [Karate feature](src/test/scala/mock/cats-create.feature) - is being retrieved on the Gatling side using the Scala API.\n\nOn the Karate side, after a scenario involving a [`karateFeature()`](#karatefeature) completes, the variables  are passed onto any other [`karateFeature()`](#karatefeature) invocations as shown in this [example](src/test/scala/mock/CatsChainedSimulation.scala). Note how the Gatling scenario called `read` which uses a Karate `Scenario` (`cats-chained.feature@name=read`) depends on the `id` variable mentioned above.\n\nAlso see [chaining](#chaining) - where Karate variables created in one `Scenario` can flow into others in advanced Gatling set-ups.\n\n### Gatling Session\nThe [Gatling session](https://gatling.io/docs/current/session/session_api/) attributes and `userId` would be available in a Karate variable under the name-space `__gatling`. So you can refer to the user-id for the thread as follows:\n\n```cucumber\n* print 'gatling userId:', __gatling.userId\n```\n\nThis is useful as an alternative to using a random UUID where you want to create unique users, and makes it easy to co-relate values to your test-run in some situations.\n\nFor advanced Gatling simulations -  the state of Karate variables at the end of a `Feature` execution will be automatically injected into the Gatling session and available to other `karateFeature()` executions within the same Gatling \"scenario\" (note that the terminology \"scenario\" here is specific to Gatling). See [Chaining](#chaining) for more.\n\nSo there are two ways to pass data from Gatling to a Karate `Feature`. The first is the use of the `__gatling` \"special\" variable. But when you want to minimize conditional logic in your Karate scripts, you can use [`karateSet()`](#karateset) to set-up variables directly. So prefer the second approach, as the goal should be to re-use Karate functional-tests as performance-tests without making any changes to the `Feature` files.\n\n### Feeders\nBecause of the above mechanism which allows Karate to \"see\" Gatling session data, you can use [feeders](https://gatling.io/docs/current/session/feeder) effectively. For example:\n\n```scala\nval feeder = Iterator.continually(Map(\"catName\" -> MockUtils.getNextCatName, \"someKey\" -> \"someValue\"))\n\nval create = scenario(\"create\").feed(feeder).exec(karateFeature(\"classpath:mock/cats-create.feature\"))\n```\n\nThere is some [Java code behind the scenes](../examples/gatling/src/test/java/mock/MockUtils.java) that takes care of dispensing a new `catName` every time `getNextCatName()` is invoked:\n\n```java\nprivate static final AtomicInteger counter = new AtomicInteger();\n\npublic static String getNextCatName() {\n    return catNames.get(counter.getAndIncrement() % catNames.size());\n}\n```\n\nThe `List` of `catNames` above is actually initialized (only once) by a [Java API call](https://github.com/karatelabs/karate#java-api) to another Karate feature (see below). If you use `true` instead of `false`, the `karate-config.js` will be honored. You could also pass custom config via the second `Map` argument to `Runner.runFeature()`. This is just to demonstrate some possibilities, and you can use any combination of Java or [Scala](https://gatling.io/docs/current/session/feeder) (even without Karate) - to set up feeders.\n\n```java\nList<String> catNames = (List) Runner.runFeature(\"classpath:mock/feeder.feature\", null, false).get(\"names\");\n```\n\nAnd now in the feature file you can do this:\n\n```cucumber\n* print __gatling.catName\n```\n\n### Chaining\nNormally we recommend that `Scenario`-s should be self-contained and that you should model any \"flows\" of calls made to API-s or code within a single `Scenario` itself or by using [`call`](https://github.com/karatelabs/karate#call).\n\nBut for advanced load-modelling, you may want to compose `Scenario`-s along with Gatling [feeders](#feeders) and have variables \"flow\" from one `Scenario` into another. This way you will be able to use all of Gatling's features such as [grouping](https://gatling.io/docs/current/general/scenario/#groups-definition), [assertions](https://gatling.io/docs/current/general/assertions/) and control over each [sub-execution](https://gatling.io/docs/current/general/scenario/#exec).\n\nSo the rule is that any variable created within the `exec()` of a [`karateFeature()`](#karatefeature) would return back to the Gatling session, and be automatically injected into any subsequent Karate feature within the same Gatling scenario.\n\nAnd if you have a need to create new variables on the \"Gatling side\" and inject them into Karate features, you can use `karateSet()`, explained below.\n\n#### `karateSet()`\n[This example](src/test/scala/mock/CatsChainedSimulation.scala) shows how you can use the `karateSet()` Gatling action to pipe data from the Gatling session (typically a feeder) into variables that Karate can access.\n\n#### `karate.callSingle()`\nA common need is to run a routine, typically a sign-in and setting up of an `Authorization` header only *once* - for all `Feature` invocations. Keep in mind that when you use Gatling, what used to be a single `Feature` in \"normal\" Karate will now be multiplied by the number of users you define. But [`callonce`](https://github.com/karatelabs/karate#callonce) is designed so that it will be \"once for everything\" when Karate is running in Gatling \"perf\" mode.\n\nYou can also use [`karate.callSingle()`](https://github.com/karatelabs/karate#hooks) in these situations. Ideally you should use [Feeders](#feeders) since `callonce` and `karate.callSingle()` will lock all threads - which may not play very well with Gatling. But when you want to quickly re-use existing Karate tests as performance tests, this will work nicely.\n\n#### Detecting Gatling At Run Time\nYou would typically want your feature file to be usable when not being run via Gatling, so you can use this pattern, since [`karate.get()`](https://github.com/karatelabs/karate#karate-get) has an optional second argument to use as a \"default\" value if the variable does not exist or is `null`.\n\n```cucumber\n* def name = karate.get('__gatling.catName', 'Billie')\n```\n\nFor a full, working, stand-alone example, refer to the [`karate-gatling-demo`](../examples/gatling/src/test/java/mock).\n\n#### Think Time\nGatling provides a way to [`pause()`](https://gatling.io/docs/current/general/scenario/#scenario-pause) between HTTP requests, to simulate user \"think time\". But when you have all your requests in a Karate feature file, this can be difficult to simulate - and you may think that adding `java.lang.Thread.sleep()` here and there will do the trick. But no, what a `Thread.sleep()` will do is *block threads* - which is a very bad thing in a load simulation. This will get in the way of Gatling, which is specialized to generate load in a non-blocking fashion.\n\nThe [`karate.pause()`](https://github.com/karatelabs/karate#karate-pause) function is specially designed to use the Gatling session if applicable - or do nothing.\n\n```cucumber\n* karate.pause(5000)\n```\n\nYou normally don't want to slow down your functional tests. But you can use the [`configure pauseIfNotPerf`](https://github.com/karatelabs/karate#configure) flag (default `false`) to have `karate.pause()` work even in \"normal\" mode.\n\n## `configure localAddress`\nGatling has a way to bind the HTTP \"protocol\" to [use a specific \"local address\"](https://gatling.io/docs/3.2/http/http_protocol/#local-address), which is useful when you want to use an IP range to avoid triggering rate-limiting on the server under test etc. But since Karate makes the HTTP requests, you can use the [`configure`](https://github.com/karatelabs/karate#configure) keyword, and this can actually be done *any* time within a Karate script or `*.feature` file. Note that the IP address needs to be [*physically assigned* to the local machine](https://www.blazemeter.com/blog/how-to-send-jmeter-requests-from-different-ips/).\n\n```cucumber\n* configure localAddress = '123.45.67.89'\n```\n\nOne easy way to achieve a \"round-robin\" effect is to write a simple Java static method that will return a random IP out of a pool. See [feeders](#feeders) for example code. Note that you can \"conditionally\" perform a `configure` by using the JavaScript API on the `karate` object:\n\n```cucumber\n* if (__gatling) karate.configure('localAddress', MyUtil.getIp())\n```\n\nSince you can [use Java code](https://github.com/karatelabs/karate#calling-java), any kind of logic or strategy should be possible, and you can refer to [config or variables](https://github.com/karatelabs/karate#configuration) if needed.\n\n## Custom\nYou can even include any custom code you write in Java into a performance test, complete with full Gatling reporting.\n\nWhat this means is that you can easily script performance tests for database-access, [gRPC](https://grpc.io), proprietary non-HTTP protocols or pretty much *anything*, really.\n\nJust use a single Karate interface called [`PerfContext`](../karate-core/src/main/java/com/intuit/karate/PerfContext.java). Here is an [example](src/test/scala/mock/MockUtils.java):\n\n ```java\npublic static Map<String, Object> myRpc(Map<String, Object> map, PerfContext context) {\n    long startTime = System.currentTimeMillis();\n    // this is just an example, you can put any kind of code here\n    int sleepTime = (Integer) map.get(\"sleep\");\n    try {\n        Thread.sleep(sleepTime);\n    } catch (Exception e) {\n        throw new RuntimeException(e);\n    }\n    long endTime = System.currentTimeMillis();\n    // and here is where you send the performance data to the reporting engine\n    context.capturePerfEvent(\"myRpc-\" + sleepTime, startTime, endTime);\n    return Collections.singletonMap(\"success\", true);\n}\n ```\n\n### `capturePerfEvent()`\nThe `PerfContext.capturePerfEvent()` method takes these arguments:\n* `eventName` - string, which will show up in the Gatling report\n* `startTime` - long\n* `endTime` - long\n\n### `PerfContext`\nTo get a reference to the current `PerfContext`, just pass the built-in `karate` JavaScript object from the \"Karate side\" to the \"Java side\". For [example](src/test/scala/mock/custom-rpc.feature):\n\n```cucumber\nBackground:\n  * def Utils = Java.type('mock.MockUtils')\n\nScenario: fifty\n  * def payload = { sleep: 50 }\n  * def response = Utils.myRpc(payload, karate)\n  * match response == { success: true }\n```\n\nThe `karate` object happens to implement the `PerfContext` interface and keeps your code simple. Note how the `myRpc` method has been [implemented to accept a `Map`](https://github.com/karatelabs/karate#calling-java) (auto-converted from JSON) and the `PerfContext` as arguments. \n\nLike the built-in HTTP support, any test failures are automatically linked to the previous \"perf event\" captured.\n\n## Increasing Thread Pool Size\nThe defaults should suffice most of the time, but if you see odd behavior such as freezing of a test, you can change the settings for the underlying Akka engine. A typical situation is when one of your responses takes a very long time to respond (30-60 seconds) and the system is stuck waiting for threads to be freed.\n\nAdd a file called [`gatling-akka.conf`](src/test/resources/gatling-akka.conf) to the root of the classpath (typically `src/test/resources`). Here is an example:\n\n```\nakka {\n  actor {\n    default-dispatcher {\n      type = Dispatcher\n      executor = \"thread-pool-executor\"\n      thread-pool-executor {\n        fixed-pool-size = 100\n      }\n      throughput = 1\n    }\n  }\n}\n```\n\nSo now the system can go up to 100 threads waiting for responses. You can experiment with more settings as [described here](https://doc.akka.io/docs/akka/current/typed/dispatchers.html). Of course a lot will depend on the compute resources (CPU, RAM) available on the machine on which you are running a test.\n\n## Distributed Testing\nSee wiki: [Distributed Testing](https://github.com/karatelabs/karate/wiki/Distributed-Testing)\n"
  },
  {
    "path": "karate-gatling/dummy.txt",
    "content": ""
  },
  {
    "path": "karate-gatling/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>io.karatelabs</groupId>\n        <artifactId>karate-parent</artifactId>\n        <version>1.5.2</version>\n    </parent>\n    <artifactId>karate-gatling</artifactId>\n    <packaging>jar</packaging>\n    <name>${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.gatling.highcharts</groupId>\n            <artifactId>gatling-charts-highcharts</artifactId>\n            <version>3.11.5</version>\n            <exclusions>\n                <exclusion>\n                    <groupId>io.netty</groupId>\n                    <artifactId>netty-codec-http</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>org.scala-lang</groupId>\n            <artifactId>scala-library</artifactId>\n            <version>2.13.13</version>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <version>${junit5.version}</version>\n\t\t\t<scope>test</scope>\n        </dependency>\n\t\t\t\t\n\t\t\t\t\n    </dependencies>\n\n    <build>\n\n      <testResources>\n            <testResource>\n                <directory>src/test/resources</directory>\n            </testResource>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>\n\n\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>default-compile</id>\n                        <!-- Disable java compiler since Zinc will compile both scala and java sources -->\n                        <phase>none</phase>\n                    </execution>\n                    <execution>\n                        <id>default-testCompile</id>\n                        <!-- Disable java compiler since Zinc will compile both scala and java sources -->\n                        <phase>none</phase>\n                    </execution>\n                </executions>    \n            </plugin>\n            <plugin>\n                <groupId>net.alchim31.maven</groupId>\n                <artifactId>scala-maven-plugin</artifactId>\n                <version>4.8.1</version>\n                    <configuration>\n                        <args>\n                            <arg>-Jbackend:GenBCode</arg>\n                            <arg>-Jdelambdafy:method</arg>\n                            <arg>-release:11</arg>\n                            <arg>-deprecation</arg>\n                            <arg>-feature</arg>\n                            <arg>-unchecked</arg>\n                            <arg>-language:implicitConversions</arg>\n                            <arg>-language:postfixOps</arg>\n                        </args>\n                    </configuration>                    \n                <executions>\n                    <execution>\n\t\t\t\t\t\t<id>scala-compile</id>\n\t\t\t\t\t\t<phase>compile</phase>\n\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t<goal>add-source</goal>\n\t\t\t\t\t\t\t<goal>compile</goal>\n\t\t\t\t\t\t</goals>\n                    </execution>\n\t\t\t\t\t<execution>\n\t\t\t\t\t\t<id>scala-test-compile</id>\n\t\t\t\t\t\t<phase>test-compile</phase>\n\t\t\t\t\t\t<goals>                        \n\t\t\t\t\t\t\t<goal>add-source</goal>\n\t\t\t\t\t\t\t<goal>testCompile</goal>\n\t\t\t\t\t\t</goals>\n\t\t\t\t\t</execution>                    \n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>io.gatling</groupId>\n                <artifactId>gatling-maven-plugin</artifactId>\n                <version>4.3.4</version>\n                <configuration>\n                    <disableCompiler>true</disableCompiler>\n                    <skip>${skipTests}</skip>\n                    <runMultipleSimulations>true</runMultipleSimulations>\n                </configuration>\n                <executions>\n                    <execution>\n                        <phase>test</phase>\n                        <goals>\n                            <goal>test</goal>\n                        </goals>\n                    </execution>\n                </executions>                \n            </plugin>\n            <!-- next 2 are purely for the release profile -->\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-javadoc-plugin</artifactId>\n                <version>${maven.javadoc.version}</version>\n                <configuration>\n                    <skip>true</skip>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-jar-plugin</artifactId>\n                <version>3.3.0</version>\n                <executions>\n                    <execution>\n                        <id>jar-javadoc</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>jar</goal>\n                        </goals>\n                        <configuration>\n                            <classifier>javadoc</classifier>\n                            <includes>\n                                <include>dummy.txt</include>\n                            </includes>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "karate-gatling/src/main/java/com/intuit/karate/gatling/javaapi/KarateDsl.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.gatling.javaapi;\n\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport io.gatling.javaapi.core.*;\nimport io.gatling.javaapi.core.internal.Converters;\nimport io.gatling.javaapi.http.*;\nimport scala.collection.mutable.ArrayBuffer;\nimport scala.collection.mutable.Buffer;\nimport scala.collection.immutable.Seq;\n\nimport com.intuit.karate.gatling.PreDef;\nimport com.intuit.karate.gatling.javaapi.KarateUriPattern;\nimport com.intuit.karate.gatling.javaapi.KarateUriPattern.KarateUriPatternBuilder;\nimport com.intuit.karate.gatling.MethodPause;\n\nimport static io.gatling.javaapi.core.CoreDsl.*;\nimport static io.gatling.javaapi.http.HttpDsl.*;\n\npublic class KarateDsl {\n\n  public static KarateUriPatternBuilder uri(String uri) {\n    return new KarateUriPatternBuilder(uri);\n  }\n  \n  public static KarateProtocolBuilder karateProtocol(KarateUriPattern... patterns) {\n    return new KarateProtocolBuilder(Arrays.stream(patterns).collect(Collectors.toMap(KarateUriPattern::getUri, pattern -> Converters.toScalaSeq(pattern.getPauses()))));\n  }\n  \n    public static ActionBuilder karateFeature(String name, String... tags) {\n       return new KarateFeatureBuilder(name, tags);\n    }\n\n\n    public static ActionBuilder karateSet(String key, final Function<Session, Object> supplier) {\n       return () -> PreDef.karateSet(key, session -> supplier.apply(new Session(session)));\n    }\n\n    public static MethodPause method(String method, int durationInMillis) {\n      return new MethodPause(method, durationInMillis);\n    }\n  \n}\n"
  },
  {
    "path": "karate-gatling/src/main/java/com/intuit/karate/gatling/javaapi/KarateFeatureBuilder.java",
    "content": "package com.intuit.karate.gatling.javaapi;\n\nimport com.intuit.karate.gatling.KarateFeatureActionBuilder;\nimport com.intuit.karate.gatling.PreDef;\nimport io.gatling.javaapi.core.ActionBuilder;\nimport io.gatling.javaapi.core.internal.Converters;\n\npublic class KarateFeatureBuilder implements ActionBuilder {\n\n    \n    public KarateFeatureActionBuilder builder;\n\n    public KarateFeatureBuilder(String name, String... tags) {\n        builder = PreDef.karateFeature(name, Converters.toScalaSeq(tags));\n    }\n\n    public KarateFeatureBuilder silent() {\n        builder = builder.silent();\n        return this;\n    }\n\n    @Override\n    public io.gatling.core.action.builder.ActionBuilder asScala() {\n        return builder;\n    }\n}\n"
  },
  {
    "path": "karate-gatling/src/main/java/com/intuit/karate/gatling/javaapi/KarateProtocolBuilder.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.gatling.javaapi;\n\n\nimport java.util.Collections;\nimport java.util.function.BiFunction;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.gatling.KarateProtocol;\nimport com.intuit.karate.gatling.MethodPause;\nimport com.intuit.karate.http.HttpRequest;\n\nimport io.gatling.core.protocol.Protocol;\nimport io.gatling.javaapi.core.ProtocolBuilder;\nimport io.gatling.javaapi.core.internal.Converters;\nimport scala.collection.immutable.Seq;\nimport scala.collection.immutable.Map;\n\npublic class KarateProtocolBuilder implements ProtocolBuilder {\n\n    public BiFunction<HttpRequest, ScenarioRuntime, String> nameResolver;\n    public Runner.Builder runner = new Runner.Builder();    \n\n    private final Map<String, Seq<MethodPause>> uriPatterns;\n\n    // Takes a JAVA Map (easier for testing) containaing SCALA MethodPauses (easier to read, saves an extra Java MethodPause class and another conversion)\n    public KarateProtocolBuilder(java.util.Map<String, Seq<MethodPause>> uriPatterns) {\n        this.uriPatterns = Converters.toScalaMap(uriPatterns);\n    }\n\n    public KarateProtocolBuilder nameResolver(BiFunction<HttpRequest, ScenarioRuntime, String> nameResolver) {\n        this.nameResolver = nameResolver;\n        return this;\n    }\n\n    @Override\n    public KarateProtocol protocol() {\n        KarateProtocol protocol = new KarateProtocol(uriPatterns);\n        if (nameResolver != null) {\n            protocol.nameResolver_$eq((req, sr) -> nameResolver.apply(req, sr));                \n        }\n        protocol.runner_$eq(runner);\n        return protocol;\n    }\n\n\n}\n"
  },
  {
    "path": "karate-gatling/src/main/java/com/intuit/karate/gatling/javaapi/KarateUriPattern.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.gatling.javaapi;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\nimport com.intuit.karate.gatling.MethodPause;\n\n/** CLass to be used as a parameter of KarateDsl.karateProtocol.\n * \n * Instances are obtained from KarateDsl.uri(<uri>) chained with nil() or pauseFor() and won't typically be created directly.\n */\npublic class KarateUriPattern {\n    final String uri;\n    final List<MethodPause> pauses;\n    \n    KarateUriPattern(String uri, List<MethodPause> pauses) {\n        this.uri = uri;\n        this.pauses = pauses;\n    }\n\n    String getUri() {\n        return uri;\n    }\n\n    List<MethodPause> getPauses() {\n        return pauses;\n    }\n    \n    public static class KarateUriPatternBuilder {\n        private final String uri;\n\n        KarateUriPatternBuilder(String uri) {\n           this.uri = uri;     \n        }\n\n        /**\n         * Creates a uriPattern with no pauses\n         * @return\n         */\n        public KarateUriPattern nil() {\n           return new KarateUriPattern(uri, Collections.emptyList());     \n        }\n\n        public KarateUriPattern pauseFor(String method, int durationInMillis) {\n            return pauseFor(KarateDsl.method(method, durationInMillis));\n        }\n\n        public KarateUriPattern pauseFor(String method1, int durationInMillis1, String method2, int durationInMillis2) {\n            return pauseFor(KarateDsl.method(method1, durationInMillis1), KarateDsl.method(method2, durationInMillis2));\n        }\n\n        public KarateUriPattern pauseFor(MethodPause... pauses) {\n            return new KarateUriPattern(uri, Arrays.asList(pauses));\n        }\n    }\n}\n\n"
  },
  {
    "path": "karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateActions.scala",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.gatling\n\nimport java.util.function.Consumer\n\nimport akka.actor.ActorSystem\nimport com.intuit.karate.core._\nimport com.intuit.karate.http.HttpRequest\nimport com.intuit.karate.{PerfHook, Runner}\nimport io.gatling.commons.stats.{KO, OK}\nimport io.gatling.commons.util.Clock\nimport io.gatling.core.action.builder.ActionBuilder\nimport io.gatling.core.action.{Action, ExitableAction}\nimport io.gatling.core.session.Session\nimport io.gatling.core.stats.StatsEngine\nimport io.gatling.core.structure.ScenarioContext\nimport io.gatling.core.util.NameGen\n\nimport scala.jdk.CollectionConverters._\nimport scala.concurrent.duration.{Duration, MILLISECONDS}\nimport scala.concurrent.{Await, ExecutionContextExecutor, Future}\n\nclass KarateFeatureAction(val name: String, val tags: Seq[String], val protocol: KarateProtocol, val system: ActorSystem,\n                          val statsEngine: StatsEngine, val clock: Clock, val next: Action, val isSilent: Boolean = false) extends ExitableAction {\n\n  override def execute(session: Session) = {\n\n    implicit val executor: ExecutionContextExecutor = system.dispatcher\n\n    def pauseInternal(time: Int) = {\n      val duration = Duration(time, MILLISECONDS)\n      try {\n        Await.result(Future.never, duration)\n      } catch {\n        // we do all this to achieve a non-blocking \"pause\"\n        // and the timeout exception will ALWAYS be thrown\n        case e: Throwable => // do nothing\n      }\n    }\n\n    val pauseFunction: Consumer[java.lang.Number] = t => pauseInternal(t.intValue())\n\n    val perfHook = new PerfHook {\n\n      override def getPerfEventName(req: HttpRequest, sr: ScenarioRuntime): String = {\n        val customName = protocol.nameResolver.apply(req, sr)\n        val finalName = if (customName != null) customName else protocol.defaultNameResolver.apply(req, sr)\n        val pauseTime = protocol.pauseFor(finalName, req.getMethod)\n        if (pauseTime > 0) pause(pauseTime)\n        return if (customName != null) customName else req.getMethod + \" \" + finalName\n      }\n\n      override def reportPerfEvent(event: PerfEvent): Unit = {\n        if(isSilent){\n          return\n        }\n        val okOrNot = if (event.isFailed) KO else OK\n        val message = if (event.getMessage == null) None else Option(event.getMessage)\n        statsEngine.logResponse(session.scenario, session.groups, event.getName, event.getStartTime, event.getEndTime, okOrNot, Option(event.getStatusCode.toString), message)\n      }\n\n      override def submit(r: Runnable): Unit = Future {\n        r.run()\n      }\n\n      override def afterFeature(fr: FeatureResult): Unit = {\n        val vars: java.util.Map[String, Object] = fr.getVariables\n        val attributes: Map[String, AnyRef] = if (vars == null) Map.empty else {\n          vars.remove(KarateProtocol.GATLING_KEY)\n          vars.asScala.toMap\n        }\n        if (fr.isEmpty || fr.isFailed) {\n          next ! session.markAsFailed.set(KarateProtocol.KARATE_KEY, attributes).setAll(attributes)\n        } else {\n          next ! session.set(KarateProtocol.KARATE_KEY, attributes).setAll(attributes)\n        }\n      }\n\n      override def pause(millis: java.lang.Number): Unit = pauseFunction.accept(millis)\n\n    }\n\n    val gatlingSessionMap: java.util.Map[String, Any] = new java.util.HashMap(session.attributes.asInstanceOf[Map[String, Any]].asJava)\n    val callArg: java.util.Map[String, Any] = {\n      if (gatlingSessionMap.containsKey(KarateProtocol.KARATE_KEY)) {\n        val incomingData = gatlingSessionMap.remove(KarateProtocol.KARATE_KEY).asInstanceOf[Map[String, Any]].asJava\n        new java.util.HashMap[String, Any](incomingData)\n      } else {\n        new java.util.HashMap[String, Any](1)\n      }\n    }\n    gatlingSessionMap.put(\"userId\", session.userId)\n    gatlingSessionMap.put(\"pause\", pauseFunction)\n    callArg.put(KarateProtocol.GATLING_KEY, gatlingSessionMap)\n\n    val runner = protocol.runner.copy()\n    runner.callSingleCache(protocol.callSingleCache)\n    runner.callOnceCache(protocol.callOnceCache)\n    runner.tags(tags.asJava)\n\n    Runner.callAsync(runner, name, callArg, perfHook)\n\n  }\n\n}\n\nclass KarateFeatureActionBuilder(name: String, tags: Seq[String]) extends ActionBuilder {\n\n  private var isSilent = false\n  \n  def silent(): KarateFeatureActionBuilder = {\n      this.isSilent = true;\n      this;\n  }\n\n  override def build(ctx: ScenarioContext, next: Action): Action = {\n    val karateComponents = ctx.protocolComponentsRegistry.components(KarateProtocol.KarateProtocolKey)\n    new KarateFeatureAction(name, tags, karateComponents.protocol, karateComponents.system, ctx.coreComponents.statsEngine, ctx.coreComponents.clock, next, isSilent)\n  }\n\n}\n\nclass KarateSetAction(key: String, valueSupplier: Session => AnyRef,\n                      val statsEngine: StatsEngine, val clock: Clock, val next: Action) extends ExitableAction with NameGen {\n\n  override val name: String = genName(\"karateSet\")\n\n  override def execute(session: Session): Unit = {\n    val karateContext = session(KarateProtocol.KARATE_KEY).asOption[Map[String, AnyRef]].getOrElse(Map.empty)\n    next ! session.set(KarateProtocol.KARATE_KEY, karateContext + (key -> valueSupplier(session)))\n  }\n\n}\n\nclass KarateSetActionBuilder(key: String, valueSupplier: Session => AnyRef) extends ActionBuilder {\n\n  override def build(ctx: ScenarioContext, next: Action): Action = {\n    new KarateSetAction(key, valueSupplier, ctx.coreComponents.statsEngine, ctx.coreComponents.clock, next)\n  }\n\n}\n"
  },
  {
    "path": "karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateProtocol.scala",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.gatling\n\nimport akka.actor.ActorSystem\nimport com.intuit.karate.Runner\nimport com.intuit.karate.http.{HttpRequest, HttpUtils}\nimport com.intuit.karate.core.{ScenarioCall, ScenarioRuntime}\nimport io.gatling.core.CoreComponents\nimport io.gatling.core.config.GatlingConfiguration\nimport io.gatling.core.protocol.{Protocol, ProtocolComponents, ProtocolKey}\nimport io.gatling.core.session.Session\n\ncase class MethodPause(val method: String, pause: Int)\n\nclass KarateProtocol(val uriPatterns: Map[String, Seq[MethodPause]]) extends Protocol {\n  def pathMatches(uri: String): Option[String] = uriPatterns.keys.find(HttpUtils.parseUriPattern(_, uri) != null)\n  def pauseFor(requestName: String, method: String) = {\n    val methodPause = uriPatterns.getOrElse(requestName, Nil).find(mp => method.equalsIgnoreCase(mp.method))\n    if (methodPause.isDefined) methodPause.get.pause else 0\n  }\n  val defaultNameResolver = (req: HttpRequest, ctx: ScenarioRuntime) => {\n    val pathPair = HttpUtils.parseUriIntoUrlBaseAndPath(req.getUrl)\n    val matchedUri = pathMatches(pathPair.right)\n    if (matchedUri.isDefined) matchedUri.get else pathPair.right\n  }\n  var nameResolver: (HttpRequest, ScenarioRuntime) => String = (req, ctx) => null\n  var runner = new Runner.Builder\n  val callSingleCache = new java.util.HashMap[String, AnyRef]\n  val callOnceCache = new java.util.HashMap[String, ScenarioCall.Result]\n}\n\nobject KarateProtocol {\n  val KARATE_KEY = \"__karate\"\n  val GATLING_KEY = \"__gatling\"\n  val KarateProtocolKey = new ProtocolKey[KarateProtocol, KarateComponents] {\n    override def defaultProtocolValue(configuration: GatlingConfiguration) = new KarateProtocol(Map.empty)\n    override def newComponents(coreComponents: CoreComponents)=\n      karateProtocol => KarateComponents(karateProtocol, coreComponents.actorSystem)\n    override def protocolClass= classOf[KarateProtocol].asInstanceOf[Class[io.gatling.core.protocol.Protocol]]\n  }\n}\n\ncase class KarateComponents(val protocol: KarateProtocol, val system: ActorSystem) extends ProtocolComponents {\n  override def onStart: Session => Session = Session.Identity\n  override def onExit: Session => Unit = ProtocolComponents.NoopOnExit\n}\n"
  },
  {
    "path": "karate-gatling/src/main/scala/com/intuit/karate/gatling/PreDef.scala",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.gatling\n\nimport io.gatling.core.session.Session\n\nobject PreDef {\n  def karateProtocol(uriPatterns: (String, Seq[MethodPause])*) = new KarateProtocol(uriPatterns.toMap)\n  def karateFeature(name: String, tags: String *) = new KarateFeatureActionBuilder(name, tags)\n  def karateSet(key: String, valueSupplier: Session => AnyRef ) = new KarateSetActionBuilder(key, valueSupplier)\n  def pauseFor(list: (String, Int)*) = list.map(mp => MethodPause(mp._1, mp._2))\n}\n"
  },
  {
    "path": "karate-gatling/src/test/java/com/intuit/karate/gatling/javaapi/KarateProtocolBuilderTest.java",
    "content": "package com.intuit.karate.gatling.javaapi;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.lang.reflect.Field;\nimport java.util.Collections;\nimport java.util.function.BiFunction;\n\nimport org.junit.jupiter.api.Test;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.http.HttpRequest;\n\nimport io.gatling.javaapi.core.internal.Converters;\n\nimport com.intuit.karate.gatling.KarateProtocol;\nimport com.intuit.karate.gatling.MethodPause;\n\n\nclass KarateProtocolBuilderTest   {\n\n    // Validates that the supplied nameResolver, runner and uriPatterns are taken into account.\n    @Test\n    void karateProtocol() throws Exception {\n        KarateProtocolBuilder protocolBuilder = new KarateProtocolBuilder(Collections.singletonMap(\"foo\", Converters.toScalaSeq(Collections.singletonList(new MethodPause(\"get\", 110)))));\n\n        protocolBuilder.nameResolver = (req, sr) -> \"test name resolver\";\n\n        protocolBuilder.runner.karateEnv(\"test\");\n\n        KarateProtocol protocol = protocolBuilder.protocol();\n\n        assertEquals(\"test name resolver\", protocol.nameResolver().apply(null, null));\n\n        Field envField = Runner.Builder.class.getDeclaredField(\"env\");\n        envField.setAccessible(true);\n        String env = (String)envField.get(protocol.runner());\n\n        assertEquals(\"test\", env);\n\n        assertEquals(110, protocol.pauseFor(\"foo\", \"get\"));\n    }\n\n    @Test\n    void uriPatterns() {\n\n        KarateProtocol protocol = KarateDsl.karateProtocol(\n            KarateDsl.uri(\"foo\").nil(),\n            KarateDsl.uri(\"bar\").pauseFor(\"get\", 110, \"post\", 220)  \n        ).protocol();        \n\n        assertEquals(110, protocol.pauseFor(\"bar\", \"get\"));\n        assertEquals(220, protocol.pauseFor(\"bar\", \"post\"));\n        assertEquals(0, protocol.pauseFor(\"bar\", \"put\"));\n        assertEquals(0, protocol.pauseFor(\"foo\", \"get\"));\n        assertEquals(0, protocol.pauseFor(\"foobar\", \"get\"));\n\n        assertTrue(protocol.pathMatches(\"/foo\").isDefined());\n        assertTrue(protocol.pathMatches(\"/bar\").isDefined());\n        assertFalse(protocol.pathMatches(\"/foobar\").isDefined());\n\n    }\n}\n"
  },
  {
    "path": "karate-gatling/src/test/java/mock/CatsChainedSimulation.java",
    "content": "package mock;\n\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Random;\nimport java.util.stream.Stream;\nimport static com.intuit.karate.gatling.javaapi.KarateDsl.*;\nimport static io.gatling.javaapi.core.CoreDsl.details;\nimport static io.gatling.javaapi.core.CoreDsl.feed;\nimport static io.gatling.javaapi.core.CoreDsl.rampUsers;\nimport static io.gatling.javaapi.core.CoreDsl.scenario;\n\nimport io.gatling.javaapi.core.*;\n\npublic class CatsChainedSimulation extends Simulation {\n\n  public CatsChainedSimulation() {\n    MockUtils.startServer(0);\n\n    ProtocolBuilder protocol = karateProtocol(uri(\"/cats/{id}\").nil());\n  \n    Iterator<Map<String, Object>> feeder = Stream.generate(() -> Collections.<String, Object>singletonMap(\"name\", new Random().nextInt(20)+\"-name\")).iterator();\n  \n    ScenarioBuilder createAndRead = scenario(\"createAndRead\")\n    .group(\"createAndRead\").on(\n      feed(() -> feeder)\n        .exec(karateSet(\"name\", session -> session.getString(\"name\")))\n        .exec(karateFeature(\"classpath:mock/cats-chained.feature@name=create\"))\n        // for demo: injecting a new variable name expected by the 'read' feature\n        .exec(karateSet(\"expectedName\", session -> session.getString(\"name\")))\n        .exec(karateFeature(\"classpath:mock/cats-chained.feature@name=read\")).exec(session -> {\n      System.out.println(\"*** id in gatling: \" + session.getString(\"id\"));\n      System.out.println(\"*** session status in gatling: \" + session.asScala().status());\n      return session;\n    }));\n    \n  \n    setUp(\n      createAndRead.injectOpen(rampUsers(10).during(5)).protocols(protocol)\n    ).assertions(details(\"createAndRead\").failedRequests().percent().is(0d));\n  \n  }\n\n}\n"
  },
  {
    "path": "karate-gatling/src/test/java/mock/CatsSimulation.java",
    "content": "package mock;\n\nimport com.intuit.karate.gatling.javaapi.KarateProtocolBuilder;\n\nimport io.gatling.javaapi.core.ScenarioBuilder;\nimport io.gatling.javaapi.core.Simulation;\n\nimport static io.gatling.javaapi.core.CoreDsl.*;\nimport static com.intuit.karate.gatling.javaapi.KarateDsl.*;\n\npublic class CatsSimulation extends Simulation {\n\n  public CatsSimulation() {\n      \n    MockUtils.startServer(0);\n\n    KarateProtocolBuilder protocol = karateProtocol(\n      uri(\"/cats/{id}\").nil(),\n      uri(\"/cats\").pauseFor(method(\"get\", 15), method(\"post\", 25)\n    ));\n\n    protocol.nameResolver = (req, ctx) -> req.getHeader(\"karate-name\");\n    protocol.runner.karateEnv(\"perf\");\n\n    ScenarioBuilder create = scenario(\"create\").exec(karateFeature(\"classpath:mock/cats-create.feature\")).exec(session -> {\n      System.out.println(\"*** id in gatling: \" + session.getString(\"id\"));\n      System.out.println(\"*** session status in gatling: \" + session.asScala().status());\n      return session;\n    });\n\n    ScenarioBuilder delete = scenario(\"delete\").group(\"delete cats\").on(\n      exec(karateFeature(\"classpath:mock/cats-delete.feature@name=delete\"))\n    );\n\n    ScenarioBuilder custom = scenario(\"custom\").exec(karateFeature(\"classpath:mock/custom-rpc.feature\"));\n\n    setUp(\n      create.injectOpen(rampUsers(10).during(5)).protocols(protocol),\n      delete.injectOpen(rampUsers(5).during(5)).protocols(protocol),\n      custom.injectOpen(rampUsers(10).during(5)).protocols(protocol)\n    );\n  }\n}\n"
  },
  {
    "path": "karate-gatling/src/test/java/mock/MockUtils.java",
    "content": "package mock;\n\nimport com.intuit.karate.PerfContext;\nimport com.intuit.karate.core.MockServer;\n\nimport java.util.Collections;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class MockUtils {\n    \n    public static void main(String[] args) {\n        startServer(8080);\n    }\n    \n    public static void startServer(int port) {\n        MockServer server = MockServer.feature(\"classpath:mock/mock.feature\").http(port).build();\n        System.setProperty(\"mock.port\", server.getPort() + \"\");        \n    }\n\n    public static Map<String, Object> myRpc(Map<String, Object> map, PerfContext context) {\n        long startTime = System.currentTimeMillis();\n        // this is just an example, you can put any kind of code here\n        int sleepTime = (Integer) map.get(\"sleep\");\n        try {\n            Thread.sleep(sleepTime);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        long endTime = System.currentTimeMillis();\n        // and here is where you send the performance data to the reporting engine\n        context.capturePerfEvent(\"myRpc-\" + sleepTime, startTime, endTime);\n        return Collections.singletonMap(\"success\", true);\n    }\n    \n}\n"
  },
  {
    "path": "karate-gatling/src/test/java/mock/cats-chained.feature",
    "content": "Feature: cats crud\n\n  Background:\n    * url baseUrl\n\n  @name=create\n  Scenario: create\n\n    Given request { name: '#(name)' }\n    When method post\n    Then status 200\n    And match response == { id: '#uuid', name: '#(name)' }\n    * def id = response.id\n\n  @name=read\n  Scenario: read\n    # note how 'id' and 'expectedName' are passed in via the gatling session\n    Given path id\n    When method get\n    Then status 200\n    And match response == { id: '#(id)', name: '#(expectedName)' }\n\n"
  },
  {
    "path": "karate-gatling/src/test/java/mock/cats-create.feature",
    "content": "Feature: cats crud\n\n  Background:\n    * url baseUrl\n    * print 'gatling userId:', __gatling.userId\n\n  Scenario: create, get and update cat\n    Given request { name: 'Billie' }\n    When method post\n    Then status 200\n    And match response == { id: '#uuid', name: 'Billie' }\n    * def id = response.id\n\n    Given path id\n    When method get\n    # this step may randomly fail because another thread is doing deletes\n    Then status 200\n    # intentional assertion failure\n    And match response == { id: '#(id)', name: 'Billi' }\n\n    Given path id\n    When request { id: '#(id)', name: 'Bob' }\n    When method put\n    Then status 200\n    And match response == { id: '#(id)', name: 'Bob' }\n\n    # since we failed above, these lines will not be executed\n    When method get\n    Then status 200\n    And match response contains { id: '#(id)', name: 'Bob' }\n"
  },
  {
    "path": "karate-gatling/src/test/java/mock/cats-delete-one.feature",
    "content": "@ignore\nFeature: delete cat by id and verify\n\n  Scenario:\n    Given url baseUrl\n    And path id\n    When method delete\n    Then status 200\n    And match response == ''\n\n    Given path id\n    And header karate-name = 'cats-get-404'\n    When method get\n    Then status 404\n"
  },
  {
    "path": "karate-gatling/src/test/java/mock/cats-delete.feature",
    "content": "Feature: delete all cats found\n\n  Background:\n    * url baseUrl\n\n  Scenario: this scenario will be ignored because the gatling script looks for the tag @name=delete\n    * print 'this should not appear in the logs !'\n    When method get\n    Then status 400\n\n  @name=delete\n  Scenario: get all cats and then delete each by id\n    When method get\n    Then status 200\n\n    * def delete = read('cats-delete-one.feature')\n    * def result = call delete response\n"
  },
  {
    "path": "karate-gatling/src/test/java/mock/custom-rpc.feature",
    "content": "Feature: even java interop performance test reports are possible\n\n  Background:\n    * def Utils = Java.type('mock.MockUtils')\n\n  Scenario: fifty\n    * def payload = { sleep: 50 }\n    * def response = Utils.myRpc(payload, karate)\n    * match response == { success: true }\n\n  Scenario: seventy five\n    * def payload = { sleep: 75 }\n    * def response = Utils.myRpc(payload, karate)\n    # this is deliberately set up to fail\n    * match response == { success: false }\n\n  Scenario: hundred\n    * def payload = { sleep: 100 }\n    * def response = Utils.myRpc(payload, karate)\n    * match response == { success: true }\n"
  },
  {
    "path": "karate-gatling/src/test/java/mock/mock.feature",
    "content": "Feature: cats stateful crud\n\n  Background:\n    * def uuid = function(){ return java.util.UUID.randomUUID() + '' }\n    * def cats = {}\n\n  Scenario: pathMatches('/cats') && methodIs('post')\n    * def cat = request\n    * def id = uuid()\n    * cat.id = id\n    * cats[id] = cat\n    * def response = cat\n\n  Scenario: pathMatches('/cats')\n    * def response = $cats.*\n\n  Scenario: pathMatches('/cats/{id}') && methodIs('put')\n    * cats[pathParams.id] = request\n    * def response = request\n\n  Scenario: pathMatches('/cats/{id}') && methodIs('delete')\n    * karate.remove('cats', pathParams.id)\n    * def responseDelay = 850\n\n  Scenario: pathMatches('/cats/{id}')\n    * def response = cats[pathParams.id]\n    * def responseStatus = response ? 200 : 404\n"
  },
  {
    "path": "karate-gatling/src/test/resources/gatling-akka.conf",
    "content": "akka {\n  actor {\n    default-dispatcher {\n      type = Dispatcher\n      executor = \"thread-pool-executor\"\n      thread-pool-executor {\n        fixed-pool-size = 100\n      }\n      throughput = 1\n    }\n  }\n}\n"
  },
  {
    "path": "karate-gatling/src/test/resources/karate-config-perf.js",
    "content": "function fn() {\n    karate.log('*** perf config init');\n}"
  },
  {
    "path": "karate-gatling/src/test/resources/karate-config.js",
    "content": "function fn() {\n    var port = karate.properties['mock.port'];\n    if (!port) port = 8080;\n    return { baseUrl: 'http://localhost:' + port + '/cats' };\n}"
  },
  {
    "path": "karate-gatling/src/test/resources/logback-test.xml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n\n\t<appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n\t\t<immediateFlush>false</immediateFlush>\n\t\t<encoder>\n\t\t\t<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n\t\t</encoder>\n\t</appender>\n\n\t<appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n\t\t<immediateFlush>false</immediateFlush>\n\t\t<file>target/karate.log</file>\n\t\t<encoder>\n\t\t\t<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n\t\t</encoder>\n\t</appender>\n\n\t<logger name=\"com.intuit.karate\" level=\"INFO\"/>\n\n\t<root level=\"WARN\">\n\t\t<appender-ref ref=\"STDOUT\" />\n\t\t<appender-ref ref=\"FILE\" />\n\t</root>\n\n</configuration>\n"
  },
  {
    "path": "karate-gatling/src/test/resources/test.feature",
    "content": "Feature:\n\n  Scenario:\n\n    * url 'http://computer-database.gatling.io/computers'\n\n    When method get\n    Then status 200\n\n    Given param f = 'macbook'\n    When method get\n    Then status 200\n\n    Given path 6\n    When method get\n    Then status 200\n\n    Given form field name = 'Beautiful Computer'\n    And form field introduced = '2012-05-30'\n    And form field discontinued = ''\n    And form field company = '37'\n    When method post\n    Then status 200\n\n\n\n"
  },
  {
    "path": "karate-gatling/src/test/scala/mock/CatsChainedScalaSimulation.scala",
    "content": "package mock\n\nimport com.intuit.karate.gatling.PreDef._\nimport io.gatling.core.Predef._\n\nimport scala.concurrent.duration._\nimport scala.util.Random\n\nclass CatsChainedScalaSimulation extends Simulation {\n\n  MockUtils.startServer(0)\n\n  val protocol = karateProtocol(\n    \"/cats/{id}\" -> Nil\n  )\n\n  val feeder = Iterator.continually(Map(\"name\" -> (Random.alphanumeric.take(20).mkString + \"-name\")))\n\n  val feederToKarate = scenario(\"feederToKarate\")\n    .exec(karateSet(\"name\", session => session(\"name\").as[String]))\n\n  val create = scenario(\"create\").exec(karateFeature(\"classpath:mock/cats-chained.feature@name=create\"))\n\n  val read = scenario(\"read\").exec(karateFeature(\"classpath:mock/cats-chained.feature@name=read\")).exec(session => {\n    println(\"*** id in gatling: \" + session(\"id\").as[String])\n    println(\"*** session status in gatling: \" + session.status)\n    session\n  })\n\n  val createAndRead = scenario(\"createAndRead\").group(\"createAndRead\") {\n    feed(feeder)\n      .exec(feederToKarate)\n      .exec(create)\n      // for demo: injecting a new variable name expected by the 'read' feature\n      .exec(karateSet(\"expectedName\", session => session(\"name\").as[String]))\n      .exec(read)\n  }\n\n  setUp(\n    createAndRead.inject(rampUsers(10) during (5 seconds)).protocols(protocol)\n  ).assertions(details(\"createAndRead\").failedRequests.percent.is(0))\n\n}\n"
  },
  {
    "path": "karate-gatling/src/test/scala/mock/CatsScalaSimulation.scala",
    "content": "package mock\n\nimport com.intuit.karate.Runner\nimport com.intuit.karate.gatling.PreDef._\nimport io.gatling.core.Predef._\n\nimport scala.concurrent.duration._\n\nclass CatsScalaSimulation extends Simulation {\n\n  MockUtils.startServer(0)\n\n  val protocol = karateProtocol(\n    \"/cats/{id}\" -> Nil,\n    \"/cats\" -> pauseFor(\"get\" -> 15, \"post\" -> 25)\n  )\n\n  protocol.nameResolver = (req, ctx) => req.getHeader(\"karate-name\")\n  protocol.runner.karateEnv(\"perf\")\n\n  val create = scenario(\"create\").exec(karateFeature(\"classpath:mock/cats-create.feature\")).exec(session => {\n    println(\"*** id in gatling: \" + session(\"id\").as[String])\n    println(\"*** session status in gatling: \" + session.status)\n    session\n  })\n  val delete = scenario(\"delete\").group(\"delete cats\") {\n    exec(karateFeature(\"classpath:mock/cats-delete.feature@name=delete\"))\n  }\n  val custom = scenario(\"custom\").exec(karateFeature(\"classpath:mock/custom-rpc.feature\"))\n\n  setUp(\n    create.inject(rampUsers(10) during (5 seconds)).protocols(protocol),\n    delete.inject(rampUsers(5) during (5 seconds)).protocols(protocol),\n    custom.inject(rampUsers(10) during (5 seconds)).protocols(protocol)\n  )\n\n}\n"
  },
  {
    "path": "karate-gatling/src/test/scala/mock/CatsSimulationWithSilentWarmUp.scala",
    "content": "package mock\n\nimport com.intuit.karate.Runner\nimport com.intuit.karate.gatling.KarateProtocol\nimport com.intuit.karate.gatling.PreDef._\nimport io.gatling.core.Predef._\nimport io.gatling.core.structure.ScenarioBuilder\n\nimport scala.concurrent.duration._\n\nclass CatsSimulationWithSilentWarmUp extends Simulation {\n\n  MockUtils.startServer(0)\n\n  val protocol: KarateProtocol = karateProtocol(\n    \"/cats/{id}\" -> Nil,\n    \"/cats\" -> pauseFor(\"get\" -> 15, \"post\" -> 25)\n  )\n\n  protocol.nameResolver = (req, ctx) => req.getHeader(\"karate-name\")\n  protocol.runner.karateEnv(\"perf\")\n\n  val createWarmup: ScenarioBuilder = scenario(\"create warm-up\").exec(karateFeature(\"classpath:mock/cats-create.feature\").silent())\n  val create: ScenarioBuilder = scenario(\"create\").exec(karateFeature(\"classpath:mock/cats-create.feature\")).exec(session => {\n    println(\"*** id in gatling: \" + session(\"id\").as[String])\n    println(\"*** session status in gatling: \" + session.status)\n    session\n  })\n\n  val deleteWarmup: ScenarioBuilder = scenario(\"delete warm-up\").exec(karateFeature(\"classpath:mock/cats-delete.feature\").silent())\n  val delete: ScenarioBuilder = scenario(\"delete\").group(\"delete cats\") {\n    exec(karateFeature(\"classpath:mock/cats-delete.feature@name=delete\"))\n  }\n  \n  val customWarmup: ScenarioBuilder = scenario(\"custom warm-up\").exec(karateFeature(\"classpath:mock/custom-rpc.feature\").silent())\n  val custom: ScenarioBuilder = scenario(\"custom\").exec(karateFeature(\"classpath:mock/custom-rpc.feature\"))\n\n  setUp(\n    createWarmup.inject(rampUsers(1) during (5 seconds)).andThen(\n      create.inject(rampUsers(10) during (5 seconds))\n    ),\n    deleteWarmup.inject(rampUsers(1) during (5 seconds)).andThen(\n      delete.inject(rampUsers(5) during (5 seconds))\n    ),\n    customWarmup.inject(rampUsers(1) during (5 seconds)).andThen(\n      custom.inject(rampUsers(10) during (5 seconds))\n    )\n  ).protocols(protocol)\n\n}\n"
  },
  {
    "path": "karate-junit5/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>io.karatelabs</groupId>\n        <artifactId>karate-parent</artifactId>\n        <version>1.5.2</version>\n    </parent>\n    <artifactId>karate-junit5</artifactId>\n    <packaging>jar</packaging>\n    <name>${project.artifactId}</name>\n\n    <dependencies>        \n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <version>${junit5.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-engine</artifactId>\n            <version>${junit5.version}</version>\n            <scope>runtime</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>\n    </build>           \n    \n</project>\n"
  },
  {
    "path": "karate-junit5/src/main/java/com/intuit/karate/junit5/FeatureNode.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.junit5;\n\nimport com.intuit.karate.Suite;\nimport com.intuit.karate.core.Feature;\nimport com.intuit.karate.core.FeatureCall;\nimport com.intuit.karate.core.FeatureResult;\nimport com.intuit.karate.core.FeatureRuntime;\nimport com.intuit.karate.core.ScenarioIterator;\nimport com.intuit.karate.core.ScenarioRuntime;\n\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.DynamicTest;\n\n/**\n *\n * @author pthomas3\n */\npublic class FeatureNode implements Iterator<DynamicTest>, Iterable<DynamicTest> {\n\n    public final List<CompletableFuture> futures;\n    public final Suite suite;\n    public final FeatureRuntime featureRuntime;\n    private final Iterator<ScenarioRuntime> scenarios;\n\n    public FeatureNode(Suite suite, List<CompletableFuture> futures, FeatureCall featureCall, String tagSelector) {\n        this.suite = suite;\n        this.futures = futures;\n        featureRuntime = FeatureRuntime.of(suite, featureCall);\n        CompletableFuture future = new CompletableFuture();\n        futures.add(future);\n        featureRuntime.setNext(() -> future.complete(Boolean.TRUE));\n        scenarios = new ScenarioIterator(featureRuntime).filterSelected().iterator();\n    }\n\n    @Override\n    public boolean hasNext() {\n        return scenarios.hasNext();\n    }\n\n    @Override\n    public DynamicTest next() {\n        ScenarioRuntime runtime = scenarios.next();\n        return DynamicTest.dynamicTest(runtime.scenario.getRefIdAndName(), runtime.scenario.getUriToLineNumber(), () -> {\n            if (featureRuntime.beforeHook()) { // minimal code duplication from feature-runtime\n                runtime.run();\n                featureRuntime.result.addResult(runtime.result);\n            } else {\n                runtime.logger.info(\"before-feature hook returned [false], aborting: \", featureRuntime);\n            }\n            boolean failed = runtime.result.isFailed();\n            if (!scenarios.hasNext()) {\n                featureRuntime.afterFeature();\n                FeatureResult result = featureRuntime.result;\n                if (!result.isEmpty()) {\n                    suite.saveFeatureResults(result);\n                }\n                saveSummaryIfAllComplete();\n            }\n            if (failed) {\n                Assertions.fail(runtime.result.getError().getMessage());\n            }\n        });\n    }\n\n    @Override\n    public Iterator<DynamicTest> iterator() {\n        return this;\n    }\n\n    private void saveSummaryIfAllComplete() {\n        for (CompletableFuture cf : futures) {\n            if (!cf.isDone()) {\n                return;\n            }\n        }\n        suite.buildResults();\n    }\n\n}\n"
  },
  {
    "path": "karate-junit5/src/main/java/com/intuit/karate/junit5/Karate.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.junit5;\n\nimport com.intuit.karate.Runner;\nimport com.intuit.karate.Suite;\nimport com.intuit.karate.core.FeatureCall;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.DynamicContainer;\nimport org.junit.jupiter.api.DynamicNode;\nimport org.junit.jupiter.api.TestFactory;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.concurrent.CompletableFuture;\n\npublic class Karate extends Runner.Builder<Karate> implements Iterable<DynamicNode> {\n\n    @Target(ElementType.METHOD)\n    @Retention(RetentionPolicy.RUNTIME)\n    @TestFactory\n    public @interface Test {\n\n    }\n\n    // short cut for new Karate().path()\n    public static Karate run(String... paths) {\n        return new Karate().path(paths);\n    }\n\n    @Override\n    public Iterator<DynamicNode> iterator() {\n        Suite suite = new Suite(this);\n        List<DynamicNode> list = new ArrayList();\n        List<CompletableFuture> futures = new ArrayList();\n        for (FeatureCall featureCall : suite.features) {\n            FeatureNode featureNode = new FeatureNode(suite, futures, featureCall, suite.tagSelector);\n            if (!featureNode.hasNext()) // if no scenarios to execute, just skip the feature\n                continue;\n            String testName = featureCall.feature.getResource().getFileNameWithoutExtension();\n            DynamicNode node = DynamicContainer.dynamicContainer(testName, featureNode);\n            list.add(node);\n        }\n        if (suite.failWhenNoScenariosFound && list.isEmpty()) {\n            Assertions.fail(\"no features or scenarios found: \" + this);\n        }\n        return list.iterator();\n    }\n\n}\n"
  },
  {
    "path": "karate-junit5/src/test/java/karate/NoFeatureNoScenarioTest.java",
    "content": "package karate;\n\nimport com.intuit.karate.junit5.Karate;\n\nclass NoFeatureNoScenarioTest {\n\n    @Karate.Test\n    Karate testHasScenariosWithFailWhenNoScenariosFound() {\n        return Karate.run(\"noFeatureNoScenario\")\n                     .tags(\"@smoke\")\n                     .failWhenNoScenariosFound(true)\n                     .relativeTo(getClass());\n    }\n\n    @Karate.Test\n    Karate testNoScenarios() {\n        return Karate.run(\"noFeatureNoScenario\")\n                     .tags(\"@tagnotexist\")\n                     .failWhenNoScenariosFound(false)\n                     .relativeTo(getClass());\n    }\n}\n"
  },
  {
    "path": "karate-junit5/src/test/java/karate/SampleCustomTagsTest.java",
    "content": "package karate;\n\nimport com.intuit.karate.junit5.Karate;\nimport com.intuit.karate.Results;\n\n\nclass SampleCustomTagsTest {\n\n    @Karate.Test\n    Karate testXrayTags() {\n        return Karate.run(\"classpath:karate/customTags.feature\").outputJunitXml(true);\n    }\n}\n"
  },
  {
    "path": "karate-junit5/src/test/java/karate/SampleTest.java",
    "content": "package karate;\n\nimport com.intuit.karate.junit5.Karate;\n\nclass SampleTest {\n    @Karate.Test\n    // Uncomment @ignore on embed.feature for development verification\n    Karate testScreenshotIsEmbeddedOnTheCorrectStepOnFailure() {\n        return Karate.run(\"embed\").relativeTo(getClass());\n    }\n\n    @Karate.Test\n    Karate testSample() {\n        return Karate.run(\"sample\").relativeTo(getClass());\n    }\n\n    @Karate.Test\n    Karate testTags() {\n        return Karate.run(\"tags\").tags(\"@second\").relativeTo(getClass());\n    }\n\n    @Karate.Test\n    Karate testTagsWithoutFeatureName() {\n        return Karate.run().tags(\"@second\").relativeTo(getClass());\n    }\n\n    @Karate.Test\n    Karate testFullPath() {\n        return Karate.run(\"classpath:karate/tags.feature\").tags(\"@first\");\n    }\n\n    @Karate.Test\n    Karate testSystemProperty() {\n        return Karate.run(\"classpath:karate/tags.feature\")\n                .tags(\"@second\")\n                .karateEnv(\"e2e\")\n                .systemProperty(\"foo\", \"bar\");\n    }\n\n    @Karate.Test\n    Karate testAll() {\n        return Karate.run().relativeTo(getClass());\n    }\n\n}\n"
  },
  {
    "path": "karate-junit5/src/test/java/karate/SetupDryRunTest.java",
    "content": "package karate;\n\nimport com.intuit.karate.junit5.Karate;\n\nclass SetupDryRunTest {\n\n    @Karate.Test\n    Karate testDryRunStaticExamples() {\n        return Karate.run(\"setup-with-dryrun\").tags(\"static\").dryRun(true).relativeTo(getClass());\n    }\n    @Karate.Test\n    Karate testDryRunWithDynamicSamplesFromSetup() {\n        return Karate.run(\"setup-with-dryrun\").dryRun(true).tags(\"dynamic\").relativeTo(getClass());\n    }\n }\n"
  },
  {
    "path": "karate-junit5/src/test/java/karate/customTags.feature",
    "content": "Feature: cusotm tags test\n\n@first\n@requirement=CALC-2\n@test_key=CALC-2\nScenario: xray simple scenario\n  * print 'xray simple example'\n\n@second\n@requirement=CALC-3\nScenario: xray link to requirement\n  * print 'xray simple requirement'\n\n\n@third\n@test=CALC-4\nScenario: xray link to test\n  * print 'xray simple test'\n\n@fourth\nScenario: no tags\n  * print 'without additional tags'\n\n\n"
  },
  {
    "path": "karate-junit5/src/test/java/karate/embed.feature",
    "content": "Feature: browser automation demo\n\n  Background:\n    * configure driver = { type: 'chrome' }\n  @ignore\n  Scenario: try to login to github and then do a google search\n    Simulates step failure for testing embedded screenshots. Remove @ignore for developement\n\n    Given driver 'https://github.com/login'\n    And input('#login_field', 'YYY')\n    And input('#password', 'world')\n    When submit().click(\"input[name=commit]\")\n    Then match html('.flash-error') contains 'Bad username or password.'\n\n    Given driver 'https://google.com'\n    # And click('{}Accept all')\n    And input(\"[name=q][name=q]\", 'karate dsl')\n    When submit().click(\"input[name=btnI]\")\n    Then waitForUrl('https://github.com/karatelabs/karate')\n\n   Scenario: Dummy not to fail build\n    * print 'dummy'"
  },
  {
    "path": "karate-junit5/src/test/java/karate/noFeatureNoScenario.feature",
    "content": "Feature: ignoreJunitNoScenariosAssertion argument for Karate runner\n\n  @smoke\n  Scenario: smoke\n    * print 'smoke'\n"
  },
  {
    "path": "karate-junit5/src/test/java/karate/sample.feature",
    "content": "Feature: sample\n\n  Scenario: first hello world\n    * print 'hello'\n\n  Scenario: second scenario\n    * print 'second'"
  },
  {
    "path": "karate-junit5/src/test/java/karate/setup-with-dryrun.feature",
    "content": "Feature: names\n\nBackground:\n    * print 'background'\n\n@setup\nScenario: first hello world\n    * def names = [{\"name\": \"dynamic_1\"}, {\"name\": \"dynamic_2\"}]\n    * print 'setup'\n\n@dynamic\nScenario Outline: Dynamic examples from setup <name>\n    * print '<name>'\n    Examples:\n        | karate.setup().names |\n\n@static\nScenario Outline: Static examples <name>\n    * print '<name>'\n    Examples:\n        | name   |\n        | static_1 |\n        | static_2 |"
  },
  {
    "path": "karate-junit5/src/test/java/karate/tags.feature",
    "content": "Feature: tags test\n\n@first\nScenario: first\n  * print 'first'\n\n@second\nScenario: second\n  * print 'second'\n  * print 'system property foo:', karate.properties['foo']"
  },
  {
    "path": "karate-junit5/src/test/java/karate-config.js",
    "content": "function fn(){ return {} }\n"
  },
  {
    "path": "karate-junit5/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit\" level=\"DEBUG\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>"
  },
  {
    "path": "karate-netty/README.md",
    "content": "# Karate API Mocks\n## API Test-Doubles Made `Simple.`\nAnd [Consumer Driven Contracts](https://martinfowler.com/articles/consumerDrivenContracts.html) made easy.\n\n# Index\n\n<table>\n<tr>\n  <th>Start</th>\n  <td>\n      <a href=\"#standalone-jar\">Standalone JAR</a>\n    | <a href=\"#jbang\">jbang</a>\n    | <a href=\"#downloading\">Downloading</a>\n    | <a href=\"#quick-start\">Quick Start</a>\n    | <a href=\"#usage\">Usage</a>\n    | <a href=\"#logging\">Logging</a>\n    | <a href=\"#the-worlds-smallest-microservice-\">The World's Smallest Microservice</a>\n  </td>\n</tr>\n<tr>\n  <th>Life Cycle</th>\n  <td>\n      <a href=\"#embedding\">Java / JUnit</a>\n    | <a href=\"#within-a-karate-test\">Within a Karate Test</a>  \n    | <a href=\"#background\"><code>Background</code></a>\n    | <a href=\"#scenario\"><code>Scenario</code></a>\n    | <a href=\"#stopping\">Stopping</a>\n  </td>\n</tr>\n<tr>\n  <th>Request</th>\n  <td>\n      <a href=\"#request\"><code>request</code></a>\n    | <a href=\"#requestbytes\"><code>requestBytes</code></a>\n    | <a href=\"#requesturlbase\"><code>requestUrlBase</code></a>\n    | <a href=\"#requestpath\"><code>requestPath</code></a>\n    | <a href=\"#requesturi\"><code>requestUri</code></a>\n    | <a href=\"#requestmethod\"><code>requestMethod</code></a>\n    | <a href=\"#requestheaders\"><code>requestHeaders</code></a>\n    | <a href=\"#requestparams\"><code>requestParams</code></a>\n    | <a href=\"#requestparts\"><code>requestParts</code></a>\n    | <a href=\"#pathmatches\"><code>pathMatches()</code></a>\n    | <a href=\"#pathparams\"><code>pathParams</code></a>\n    | <a href=\"#methodis\"><code>methodIs()</code></a>\n    | <a href=\"#paramexists\"><code>paramExists()</code></a>\n    | <a href=\"#paramvalue\"><code>paramValue()</code></a>\n    | <a href=\"#typecontains\"><code>typeContains()</code></a>\n    | <a href=\"#acceptcontains\"><code>acceptContains()</code></a>\n    | <a href=\"#headercontains\"><code>headerContains()</code></a>\n    | <a href=\"#bodypath\"><code>bodyPath()</code></a>\n  </td>\n</tr>\n<tr>\n  <th>Response</th>\n  <td>\n      <a href=\"#response\"><code>response</code></a>\n    | <a href=\"#responsestatus\"><code>responseStatus</code></a>\n    | <a href=\"#responseheaders\"><code>responseHeaders</code></a>\n    | <a href=\"#responsedelay\"><code>responseDelay</code></a>\n    | <a href=\"#afterscenario\"><code>afterScenario</code></a>\n    | <a href=\"#karateabort\"><code>karate.abort()</code></a>\n    | <a href=\"#responsestatus\"><code>responseStatus</code></a>\n  </td>\n</tr>\n<tr>\n  <th>Advanced</th>\n  <td>\n      <a href=\"#configure-cors\"><code>configure cors</code></a>  \n    | <a href=\"#configure-afterscenario\"><code>configure afterScenario</code></a>\n    | <a href=\"#configure-responseheaders\"><code>configure responseHeaders</code></a>    \n    | <a href=\"#proxy-mode\"><code>Proxy Mode</code></a>\n    | <a href=\"#karateabort\"><code>karate.abort()</code></a>\n    | <a href=\"#karateproceed\"><code>karate.proceed()</code></a>\n    | <a href=\"#consumer-provider-example\"><code>Consumer Driven Contracts</code></a>\n    | <a href=\"#javascript-mocks\">JavaScript Mocks</a>       \n  </td>\n</tr>\n</table>\n\n### Capabilities\n* Everything on `localhost` or within your network, no need to worry about your data leaking into the cloud\n* Super-easy 'hard-coded' mocks ([example](../karate-core/src/test/java/com/intuit/karate/core/mock/_mock.feature))\n* Stateful mocks that can fully simulate CRUD for a micro-service ([example](../karate-demo/src/test/java/mock/proxy/demo-mock.feature))\n* Not only JSON but first-class support for XML, plain-text, binary, etc.\n* Convert JSON or XML into dynamic responses with ease\n* Maintain and read large payloads from the file-system if needed\n* Mocks are plain-text files - easily collaborate within or across teams using Git / SCM\n* Easy HTTP request matching by path, method, headers, body etc.\n* Use the full power of JavaScript expressions for HTTP request matching\n* SSL / HTTPS with built-in self-signed certificate\n* Forward HTTP requests to other URL-s (URL re-writing)\n* Usable as a standard HTTP proxy server - simplifying configuration set-up for consuming applications\n* Start and stop mock servers in milliseconds\n* Super-fast HTTP response times (~20ms) for typical in-memory CRUD / JsonPath (as long as you don't do I/O)\n* Thread-safe - use concurrent consumers or async flows without fear\n* Simulate [slow, delayed](#configure-responsedelay), error or malformed responses with ease\n* Zero errors even under load / stress - see this [benchmark comparison with other tools](https://twitter.com/KarateDSL/status/1083775218873581571)\n* Easy integration into Java / JUnit test-suites via API\n* Server can dynamically choose free port\n* Support for [hot-reload](#hot-reload) while editing a mock in development mode\n* Think of it as a scriptable 'API gateway' or 'AOP for web-services' - insert custom functions before / after an HTTP request is handled\n* Just *one* file can script the above aspects, simplifying the mental-model you need to have for advanced scenarios such as [Consumer Driven Contracts](https://martinfowler.com/articles/consumerDrivenContracts.html)\n* Easily integrate messaging or async flows using Java-interop if required\n* Enables consumer or even UI dev teams to work in parallel as the provider service is being developed\n* [Stand-alone executable JAR](#standalone-jar) (50 MB) which only requires a JRE to run, ideal for web-developers or anyone who needs to quickly experiment with services.\n* Single-step install option via [jbang](#jbang) - which even takes care of installing a Java runtime if required\n* Built-in [CORS](#configure-cors) support for the ease of web-dev teams using the mock service\n* Option to use an existing certificate and private-key for server-side SSL - making it easier for UI dev / browser consumers in some situations\n* Configure a 'global' response header routine, ideal for browser consumers to add headers common for *all* responses - yet dynamic if needed\n* Provider service dev team can practice TDD using the mock + contract-test\n* The mock + contract-test serves as the ultimate form of documentation of the 'contract' including payload / schema details\n\n## Using\nNote that you can use this as a [stand-alone JAR executable](#standalone-jar) which means that you don't even need to compile Java or use an IDE. If you need to embed the mock-server into a JUnit test, you can easily do so.\n\n### Maven\nThe mock capabilities are a core part of Karate, and no separate library or dependency is needed.\n\n## Consumer-Provider Example\n\n<img src=\"../karate-core/src/test/resources/karate-test-doubles.jpg\" height=\"720px\"/>\n\nWe use a simplified example of a Java 'consumer' which makes HTTP calls to a Payment Service (provider) where `GET`, `POST`, `PUT` and `DELETE` have been implemented. The 'provider' implements CRUD for the [`Payment.java`](../karate-demo/src/test/java/mock/contract/Payment.java) 'POJO', and the `POST` (or create) results in a message ([`Shipment.java`](../karate-demo/src/test/java/mock/contract/Shipment.java) as JSON) being placed on a queue, which the consumer is listening to.\n\n[ActiveMQ](http://activemq.apache.org) is being used for the sake of mixing an asynchronous flow into this example, and with the help of some [simple](../karate-demo/src/test/java/mock/contract/QueueUtils.java) [utilities](../karate-demo/src/test/java/mock/contract/QueueConsumer.java), we are able to mix asynchronous messaging into a Karate test *as well as* the test-double. Also refer to the documentation on [handling async flows in Karate](https://github.com/karatelabs/karate#async).\n\nA simpler stand-alone example (without ActiveMQ / messaging) is also available here: [`examples/consumer-driven-contracts`](../examples/consumer-driven-contracts). This is a stand-alone Maven project for convenience, and you just need to clone or download a ZIP of the Karate source code to get it. You can compare and contrast this example with how other frameworks approach [Consumer Driven Contract](https://www.thoughtworks.com/radar/techniques/consumer-driven-contract-testing) testing.\n\n| Key    | Source Code | Description |\n| ------ | ----------- | ----------- |\nC | [`Consumer.java`](../karate-demo/src/test/java/mock/contract/Consumer.java) | The 'consumer' or client application that consumes the demo 'Payment Service' and also listens to a queue\nP | [`PaymentService.java`](../karate-demo/src/test/java/mock/contract/PaymentService.java) | The provider 'Payment Service'\n1 | [`ConsumerIntegrationTest.java`](../karate-demo/src/test/java/mock/contract/ConsumerIntegrationTest.java) | An end-to-end integration test of the consumer that needs the *real* provider to be up and running\nKC | [`payment-service.feature`](../karate-demo/src/test/java/mock/contract/payment-service.feature) | A 'normal' Karate functional-test that tests the 'contract' of the Payment Service from the perspective of the consumer\n2 | [`PaymentServiceContractTest.java`](../karate-demo/src/test/java/mock/contract/PaymentServiceContractTest.java) | JUnit runner for the above Karate 'contract' test, that depends on the *real* provider being up and running\nKP | [`payment-service-mock.feature`](../karate-demo/src/test/java/mock/contract/payment-service-mock.feature) | A 'state-ful' mock (or stub) that *fully* implements the 'contract' ! Yes, *really*.\n3 | [`PaymentServiceContractUsingMockTest.java`](../karate-demo/src/test/java/mock/contract/PaymentServiceContractUsingMockTest.java) | Uses the above 'stub' to run the Payment Service 'contract' test\n4 | [`ConsumerUsingMockTest.java`](../karate-demo/src/test/java/mock/contract/ConsumerUsingMockTest.java) | Uses the 'fake' Payment Service 'stub' to run an integration test for the *real* consumer\nKX | [`payment-service-proxy.feature`](../karate-demo/src/test/java/mock/contract/payment-service-proxy.feature) | Karate can act as a proxy with 'gateway like' capabilities, you can choose to either stub a response or delegate to a remote provider, depending on the incoming request. Think of the 'X' as being able to *transform* the HTTP request and response payloads as they pass through (and before returning)\n5a | [`ConsumerUsingProxyHttpTest.java`](../karate-demo/src/test/java/mock/contract/ConsumerUsingProxyHttpTest.java) | Here Karate is set up to act as an HTTP proxy, the advantage is that the consumer can use the 'real' provider URL, which simplifies configuration, provided that you can configure the consumer to use an HTTP proxy (ideally in a non-invasive fashion)\n5b | [`ConsumerUsingProxyRewriteTest.java`](../karate-demo/src/test/java/mock/contract/ConsumerUsingProxyRewriteTest.java) | Karate acts as a URL 're-writing' proxy. Here the consumer 'knows' only about the proxy. In this mode (as well as the above 'HTTP proxy' mode which uses the *same* script file), you can choose to either stub a response - or even forward the incoming HTTP request onto any remote URL you choose.\n\n> Karate mocking a Queue has not been implemented for the last two flows (5) but can easily be derived from the other examples. So in (5) the Consumer is using the *real* queue.\n\nThis article by the creator of Karate is highly recommended as a reference: [API Contract Testing - Visual Guide](https://www.linkedin.com/pulse/api-contract-testing-visual-guide-peter-thomas/).\n\nRead this answer on Stack Overflow if you want to know how [Karate compares to Pact](https://stackoverflow.com/a/64218355/143475).\n\nAlso see this [blog post](https://hackernoon.com/api-consumer-contract-tests-and-test-doubles-with-karate-72c30ea25c18) for an additional diagram explaining how a mock-service can be implemented.\n\nAnd for more ideas, refer to [this example](https://twitter.com/KarateDSL/status/1417023536082812935) of using a mock to listen for an async \"callback\" and integrating that kind of flow into your test.\n\n### Server-Side Karate\n#### A perfect match !\nIt is worth calling out *why* Karate on the 'other side of the fence' (*handling* HTTP requests instead of *making* them) - turns out to be remarkably effective, yet simple.\n\n* 'Native' support for expressing JSON and XML payloads\n* [Embedded Expressions](https://github.com/karatelabs/karate#embedded-expressions) are perfect for those parts of the payload that need to be dynamic, and JS functions can be 'in-lined' into the JSON or XML\n* Manipulate or even transform payloads\n* Validate payloads if needed, using a [simpler alternative to JSON schema](https://twitter.com/KarateDSL/status/878984854012022784)\n* Karate is *all* about making HTTP calls, giving you the flexibility to call 'downstream' services if needed\n* In-memory JSON and JsonPath solves for ['state' and filtering](https://twitter.com/KarateDSL/status/946607931327266816) if needed\n* Mix [custom JavaScript (or even Java code)](https://github.com/karatelabs/karate#karate-expressions) if needed - for complex logic\n* Easily 'seed' data or switch environment / config on start\n* Read initial 'state' from a JSON file if needed\n\nIf you think about it, all the above are *sufficient* to implement *any* micro-service. Karate's DSL syntax is *focused* on exactly these aspects, thus opening up interesting possibilities. It may be hard to believe that you can spin-up a 'usable' micro-service in minutes with Karate - but do try it and see !\n\n# Standalone JAR\n*All* of Karate (core API testing, parallel-runner / HTML reports, mocks and web / UI automation) is available as a *single*, executable JAR file.\n\n## Downloading\nThe only pre-requisite (if not using [jbang](#jbang)) is the [Java Runtime Environment](http://www.oracle.com/technetwork/java/javase/downloads/index.html). Note that the \"lighter\" JRE is sufficient, not the full-blown JDK (Java Development Kit). At least Java 11 is required, and there's a good chance you already have it installed. You can confirm this by typing `java -version` on the command line.\n\nLook for the [latest release](https://github.com/karatelabs/karate/releases) on GitHub and scroll down to find the \"Assets\". Look for the file with the name: `karate-<version>.jar`. Download it to the root of your project folder, and rename the file to `karate.jar` to make commands easier to type.\n\n## Usage\n\n### Help\nYou can view the command line help with the `-h` option:\n```\njava -jar karate.jar -h\n```\n\n### Running Tests\nFeature files (or search paths) to be tested don't need command-line flags or options and can be just listed at the end of the command.\n\nHere is how you can run a single feature file:\n\n```\njava -jar karate.jar my-test.feature\n```\n\nYou can run all tests within a directory if you provide a directory path:\n\n```\njava -jar karate.jar some/folder\n```\n\nYou can have multiple features (separated by spaces) or even folder paths as the last part of the command. Karate will run all feature files found in sub-directories.\n\nFor filtering tests to run, see the [tags](#tags) and [scenario name](#scenario-name) options below.\n\nAlso see [custom classpath](#custom-classpath) and how to [use a batch file](#using-a-batch-file) for convenience.\n\n### Mock Server\nTo start a mock server, the 2 mandatory arguments are the path of the feature file 'mocks' `-m` and the port `-p`\n\n```\njava -jar karate.jar -m my-mock.feature -m my-2nd-mock.feature -p 8080\n```\n\n> Acting as an HTTP proxy server is not possible in 1.0 (it used to be possible in the past), and needs [community contribution](https://github.com/line/armeria/issues/3168) to revive.\n\n#### SSL\nFor SSL, use the `-s` flag. If you don't provide a certificate and key (see next section), it will automatically create `cert.pem` and `key.pem` in the current working directory, and the next time you re-start the mock server - these will be re-used. This is convenient for web / UI developers because you then need to set the certificate 'exception' only once in the browser.\n\n```\njava -jar karate.jar -m my-mock.feature -p 8443 -s\n```\n\nIf you have a custom certificate and private-key (in PEM format) you can specify them, perhaps because these are your actual certificates or because they are trusted within your organization.\n\n```\njava -jar karate.jar -m my-mock.feature -p 8443 -s -c my-cert.crt -k my-key.key\n```\n\n#### Keep Original Headers\nBy default, Karate mock server converts all response headers to lowercase according to HTTP/2 standard. By adding the `--keep-original-headers` option it allows you to retain the original header format, enabling integration with legacy systems or HTTP/1.\n\n```\njava -jar karate.jar -m my-mock.feature -p 8080 --keep-original-headers\n```\n\n#### Hot Reload\nYou can hot-reload a mock feature file for changes by adding the -W or --watch option.\n\nNote that if you are loading from the `classpath:` your build system may need to update the file in the `target` (or `build`) folder when the source file changes. Or you could load the mock from the file-system using something like `file:src/test/java/some/folder/my.feature`.\n\n#### Scenario Name\nIf you only want to run a single `Scenario` by name, use the `-n` or `--name` option:\n\n```\njava -jar karate.jar -n \"^some name$\" my-test.feature\n```\n\nNote that you can run a single `Scenario` by line number - by appending it at the end of the feature name with a colon character. For `Scenario Outline`-s, you can even select a single `Examples` row by line-number.\n\n```\njava -jar karate.jar my-test.feature:42\n```\n\n#### Tags\nYou can specify [tags](https://github.com/karatelabs/karate#tags) to include (or exclude) using the `-t` or `--tags`  option as follows. Note that the special, built-in tag `@ignore` is *always* skipped.\n\n```\njava -jar karate.jar -t @smoke,~@skipme my-test.feature\n```\n\nFor an \"AND\" operation, repeat the CLI option:\n\n```\njava -jar karate.jar -t @one -t @two,@three my-test.feature\n```\n\nThis has the effect of \"one AND (two OR three)\"\n\n#### Dry Run\nThe option is `-D` or `--dryrun` to [run tests in “dry run” mode](https://github.com/karatelabs/karate#dry-run).\n\n#### `karate.env`\nIf your test depends on the `karate.env` [environment 'switch'](https://github.com/karatelabs/karate#switching-the-environment), you can specify that using the `-e` (env) option:\n\n```\njava -jar karate.jar -e e2e my-test.feature\n```\n\n#### `karate-config.js`\nIf [`karate-config.js`](https://github.com/karatelabs/karate#configuration) exists in the current working directory, it will be used. You can specify a full path by setting the system property `karate.config.dir`. Note that this is an easy way to set a bunch of variables, just return a JSON with the keys and values you need.\n\n```\njava -Dkarate.config.dir=parentdir/somedir -jar karate.jar my-test.feature\n```\n\nIf you want to pass any custom or environment variables, make sure they are *before* the `-jar` part else they will not be passed to the JVM. For example:\n\n```cucumber\njava -Dfoo=bar -Dbaz=ban -jar karate.jar my-test.feature\n```\n\nAnd now you can get the value of `foo` from JavaScript or a [Karate expression](https://github.com/karatelabs/karate#karate-expressions) as follows:\n\n```javascript\nvar foo = karate.properties['foo']\n```\n\n#### Parallel Execution\nIf you provide a directory in which multiple feature files are present (even in sub-folders), they will be all run. You can even specify the number of threads to run in parallel using `-T` or `--threads` (not to be confused with `-t` for tags):\n\n```\njava -jar karate.jar -T 5 -t @smoke src/features\n```\n\n#### Output Directory\nThe output directory where the `karate.log` file and reports would be output - will default to `target` in the current working directory. The HTML reports would be found in a folder called `karate-reports` within this \"output\" folder. You can change the output folder using the `-o` or `--output` option:\n\n```\njava -jar karate.jar -T 5 -t ~@skipme -o /my/custom/dir src/features\n```\n\n#### Output Format\nBy default, the JUnit XML or Cucumber JSON report data will not be output. You can use the `-f` or `--format` option:\n\n```\njava -jar karate.jar -f junit:xml src/features\n```\n\nYou can use comma-delimited values, for example: `-f junit:xml,cucumber:json`.\n\nTo suppress the Karate HTML report output by default add `~html`.\n\n#### Clean\nThe [output directory](#output-directory) will be deleted before the test runs if you use the `-C` or `--clean` option.\n\n```\njava -jar karate.jar -T 5 -C src/features\n```\n\n#### Debug Server\nThe `-d` or `--debug` option will start a debug server. See the [Debug Server wiki](https://github.com/karatelabs/karate/wiki/Debug-Server) for more details.\n\n## Custom Classpath\nKarate allows you to use custom Java code or 3rd party Java libraries using [Java interop](https://github.com/karatelabs/karate#calling-java). Normally those who do this use Karate in the context of [Maven](https://maven.apache.org) or [Gradle](https://gradle.org) - and the [classpath](https://github.com/karatelabs/karate#classpath) would be set automatically.\n\nYou can use the standalone JAR and still depend on external Java code - but you have to set the classpath for this to work. The entry-point for the Karate command-line app is `com.intuit.karate.Main`.\n\n```\njava -cp karate.jar:karate-robot.jar com.intuit.karate.Main test.feature\n```\n\nIf on Windows, note that the path-separator is `;` instead of `:` as seen above for Mac / Linux. Refer this [post](https://stackoverflow.com/a/56458094/143475) for more details.\n\nThis approach is useful if you are trying to point the standalone Karate JAR file to a project structure that comes from the Java / Maven world. And the [`karate-config.js`](https://github.com/karatelabs/karate#configuration) will be looked for in the classpath itself.\n\n### Using A Batch File\nWhen using the standalone JAR, you can create a batch file (or shell script) to make it easier to run tests. This is useful especially if you need to manage a [custom classpath](#custom-classpath) as desribed above.\n\nHere is an example for Windows systems, name it as `karate.bat` for convenience:\n\n```bat\njava -cp karate.jar;. com.intuit.karate.Main %*\n```\n\nThen you can just do `karate my-test.feature` on the command-line. All options and arguments after `karate` will be processed as explained in [usage](#usage).\n\nAnd here is an example for Linux / Mac systems, name it as `karate` for convenience, and give it executable permissions by running (once): `chmod +x karate`\n\n```sh\n#!/bin/bash\njava -cp \"$(dirname \"$0\")/karate.jar\":. com.intuit.karate.Main \"$@\"\n```\n\nThen you can just do `./karate my-test.feature` on the command-line. All options and arguments after `./karate` will be processed as explained in [usage](#usage).\n\nBoth batch-file examples above add the current directory to the classpath (as `.`), which is useful if you want to load a `karate-config.js` file from the current directory. You can easily customize which `java` executable is used, and the location of not just the Karate JAR, but any other JAR files containing even custom code.\n\n## jbang\nNote that you can easily run Karate or even install applications based on Karate using [`jbang`](https://www.jbang.dev). It will take care of setting up a local Java runtime, which is really convenient. Note that jbang itself is [super-easy to install](https://www.jbang.dev/documentation/guide/latest/installation.html) and there is even a \"[Zero Install](https://www.jbang.dev/documentation/guide/latest/installation.html#zero-install)\" option.\n\nWith jbang installed, you can do this (since a [`jbang-catalog.json`](https://www.jbang.dev/documentation/guide/latest/alias_catalogs.html) is present within the [karatelabs/jbang-catalog](https://github.com/karatelabs/jbang-catalog) GitHub repository):\n\n```\njbang karate@karatelabs -h\n```\n\nWhat's *really* interesting is that you can install `karate` as a local command-line application !\n\n> please replace `RELEASE` with the exact / version of Karate you intend to use if applicable\n\n```\n jbang app install --name karate com.intuit.karate:karate-core:RELEASE:all\n```\n\nAnd now the command `karate` will be available in your terminal (after opening a new one or having re-loaded environment settings).\n\nWhich would make using Karate as easy as this !\n\n```\nkarate -h\n```\n\nYou can script complex automation, using the [Java API](https://github.com/karatelabs/karate#java-api) that Karate makes available. So if you have a file called `myscript.java` written as a jbang script, you can install it as a system-wide command called `myscript` like this:\n\n```\n jbang app install --name myscript myscript.java\n```\n\nRefer to the [jbang documentation](https://github.com/jbangdev/jbang) for more options.\n\n## Logging\nA default [logback configuration file](https://logback.qos.ch/manual/configuration.html) (named [`logback-fatjar.xml`](../karate-core/src/main/java/logback-fatjar.xml)) is present within the stand-alone JAR.\n\nFor convenience, if `logback-test.xml` or `logback.xml` exists on the root of the [classpath](#custom-classpath) (or the root of the working directory) - it will be used instead.\n\nAnother way to customize logging is set the system property `logback.configurationFile` to point to your custom config:\n\n```\njava -jar -Dlogback.configurationFile=my-logback.xml karate.jar my-test.feature\n```\n\nHere is the 'out-of-the-box' default which you can customize. Note that the default creates a folder called `target` and within it, logs will be in `karate.log`.\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>${karate.output.dir}/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit.karate\" level=\"DEBUG\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>\n```\n\n# Embedding\nStarting and stopping a Karate server can be done via the Java API and this easily allows you to mix Karate into Java code, JUnit tests and Continuous Integration pipelines.\n\nThe [`com.intuit.karate.core.MockServer`](../karate-core/src/main/java/com/intuit/karate/core/MockServer.java) class has static \"builder\" methods to configure and then launch a server. Here is an [example](../karate-core/src/test/java/com/intuit/karate/core/PerfHookTest.java).\n\nYou can call a `getPort()` method on the `MockServer` instance to get the port on which the server was started. Calling the `stop()` method will [stop](#stopping) the server.\n\nYou can look at this demo example for reference: [ConsumerUsingMockTest.java](../karate-demo/src/test/java/mock/contract/ConsumerUsingMockTest.java) - note how the dynamic port number can be retrieved and passed to other elements in your test set-up.\n\n## Continuous Integration\nTo include mocks into a test-suite that consists mostly of Karate tests, the easiest way is to use JUnit with the above approach, and ensure that the JUnit class is \"included\" in your test run. One way is to ensure that the JUnit \"runner\" follows the naming convention (`*Test.java`) or you can explicity include the mock \"runners\" in your Maven setup.\n\nYou will also need to ensure that your mock feature is *not* picked up by the regular test-runners, and an `@ignore` [tag](https://github.com/karatelabs/karate#tags) typically does the job.\n\nFor more details, refer to this [answer on Stack Overflow](https://stackoverflow.com/a/57746457/143475).\n\n## Within a Karate Test\nTeams that are using the [standalone JAR](#standalone-jar) and *don't* want to use Java at all can directly start a mock from within a Karate test script using the `karate.start()` API. The argument can be a string or JSON. If a string, it is processed as the path to the mock feature file, and behaves like the [`read()`](https://github.com/karatelabs/karate#reading-files) function.\n\nSo starting a mock from a Karate test is simple. This example also shows how [conditional logic](https://github.com/karatelabs/karate#conditional-logic) can be used effectively.\n\n```feature\nBackground:\n  * def port = karate.env == 'mock' ? karate.start('cats-mock.feature').port : 8080\n  * url 'http://localhost:' + port + '/cats'\n```\n\nFor more control, the argument to `karate.start()` can be a JSON with the following keys expected, only the `mock` is mandatory:\n\n* `mock` - (string) path to the mock feature file, e.g. `classpath:my-mock.feature` or relative paths work just like [`read()`](https://github.com/karatelabs/karate#reading-files).\n* `port` - (int) defaults to `0`, see section on [embedding](#embedding) above\n* `ssl` - (boolean) defaults to `false`, see above\n* `cert` - (string) see above\n* `key` - (string) see above\n* `arg` - (json) see above\n* `pathPrefix` - (string) see above\n\nSo if you want to \"hard-code\" the port, you can do this:\n\n```\n* karate.start({ mock: 'cats-mock.feature', port: 9000 })\n```\n\nFor the full example, look at [`cats-test.feature`](../karate-demo/src/test/java/mock/web/cats-test.feature).\n\nThe object returned by `karate.start()` is an instance of [`MockServer`](../karate-core/src/main/java/com/intuit/karate/core/MockServer.java). So in addition to the `port` \"getter\", you can call `stop()` on it, but you normally don't need to - as it should stop automatically when the test (or JVM) ends.\n\n# Server Life Cycle\nWriting a mock can get complicated for real-life API interactions, and most other frameworks attempt to solve this using declarative approaches, such as expecting you to create a large, complicated JSON to model all requests and responses. You can think of Karate's approach as combining the best of both the worlds of declarative and imperative programming. Combined with the capability to maintain state in the form of JSON objects in memory, and Karate's native support for [Json-Path](https://github.com/karatelabs/karate#jsonpath-filters), XML and [`embedded expressions`](https://github.com/karatelabs/karate#embedded-expressions) - you have a very powerful toolkit at your disposal. And Karate's intelligent defaults keep things dead simple.\n\nThe Karate 'server' life-cycle is simple and has only 2 phases - the `Background` and `Scenario`. You can see that the existing [`Gherkin`](https://github.com/cucumber/cucumber/wiki/Gherkin) format has been 're-purposed' for HTTP request handling. This means that you get the benefit of IDE support and syntax coloring for your mocks.\n\nRefer to this example: [`demo-mock.feature`](../karate-demo/src/test/java/mock/proxy/demo-mock.feature).\n\nAlso see [how to stop](#stopping) a running server.\n\n## `Background`\nThis is executed on start-up. You can read files and set-up common functions and 'global' state here. Note that unlike the life-cycle of ['normal' Karate](https://github.com/karatelabs/karate#script-structure), the `Background` is *not* executed before each `Scenario`.\n\nHere's an example of setting up a [function to generate primary keys](https://github.com/karatelabs/karate#commonly-needed-utilities) which can be invoked like this: `uuid()`\n\n```cucumber\nFeature: stateful mock server\n\nBackground:\n  * configure cors = true\n  * def uuid = function(){ return java.util.UUID.randomUUID() + '' }\n  * def cats = {}\n\nScenario: pathMatches('/cats') && methodIs('post')\n    * def cat = request\n    * def id = uuid()\n    * cat.id = id\n    * cats[id] = cat\n    * def response = cat\n\nScenario: pathMatches('/cats')\n    * def response = $cats.*\n\nScenario: pathMatches('/cats/{id}')\n    * def response = cats[pathParams.id]\n\nScenario:\n    def responseStatus = 404\n```\n\nThe main [Karate](https://github.com/karatelabs/karate) documentation explains things like the [`def`](https://github.com/karatelabs/karate#def), [`set`](https://github.com/karatelabs/karate#set) and the [`eval`](https://github.com/karatelabs/karate#eval) keywords, [Karate expressions](https://github.com/karatelabs/karate#karate-expressions) and [JsonPath](https://github.com/karatelabs/karate#get-short-cut).\n\nThe other parts of the simple example above are explained in the sections below.\n\n> Note that [`karate-config.js`](https://github.com/karatelabs/karate#configuration) does *not* come into the picture here. But if for some reason you need to re-use an existing one, you can do this in the `Background`: `* call read('classpath:karate-config.js')` - and you can use any JS or JSON file in this manner to initialize a bunch of seed data or \"intial state\".\n\n## `Scenario`\nA server-side `Feature` file can have multiple `Scenario` sections in it. Each Scenario is expected to have a JavaScript expression as the content of the `Scenario` description which we will refer to as the \"request matcher\".\n\n> Note that the [`Scenario Outline`](https://github.com/karatelabs/karate#data-driven-tests) is *not* supported when Karate is in \"mock mode\".\n\nOn each incoming HTTP request, the `Scenario` expressions are evaluated in order, starting from the first one within the `Feature`. If the expression evaluates to `true`, the body of the `Scenario` is evaluated and the HTTP response is returned.\n\n> It is good practice to have the last `Scenario` in the file with an empty description, (which will evaluate to `true`) so that it can act as a 'catch-all' and log or throw an error / `404 Not Found` in response.\n\n# Request Handling\nThe Karate \"server-side\" has a set of \"built-in\" variables or helper-functions. They have been carefully designed to solve for matching and processing that you commonly need to do against the incoming HTTP request.\n\nYou can use these in the \"request matcher\" described above. This is how you can \"route\" incoming HTTP requests to the blocks of code within the individual `Scenario`-s. And you can also use them in the `Scenario` body, to process the request, URL, and maybe the headers, and then form the [response](#response-building).\n\n> The [`pathParams`](#pathparams) is a special case. For each request, it will be initialized only if, and after you have used [`pathMatches()`](#pathmatches). In other words you have to call `pathMatches()` first - typically in the \"request matcher\" and then you will be able to unpack URL parameters in the `Scenario` body.\n\n## Scenario selection\n\nWhen multiple files are provided, they are evaluated in supplied order\n- `Example 1`: When a feature file contains same scenario:\n```cucumber\nScenario: pathMatches('/test')\n* def response = read('/example/Bye.txt')\n\nScenario: pathMatches('/test')\n* def response = read('/example/hi.txt')\n```\nHere the first scenario will be picked and Bye.txt will be returned as response.\n\n- `Example 2`: When same scenario exists in two separate files.  \n  Feature-file1.feature\n```cucumber\n##Feature-file1.feature\nScenario: pathMatches('/test')\n* def response = read('/example/Bye.txt')\n\n```\n  Feature-file2.feature\n```cucumber\n##Feature-file2.feature\nScenario: pathMatches('/test')\n* def response = read('/example/Hi.txt')\n\n```\nThe response will be determined by the order of file.\n`java -jar karate.jar -m Feature-file1.feature -m Feature-file2.feature`  \nHere `Bye.txt` will be returned as response.  \n\n`java -jar karate.jar -m Feature-file2.feature -m Feature-file1.feature`  \nHere `Hi.txt` will be returned as response.  \n\n## `request`\nThis variable holds the value of the request body. It will be a JSON or XML object if it can be parsed as such. Else it would be a string.\n\n## `requestBytes`\nRarely used, unless you are expecting incoming binary content. This variable holds the value of the raw request bytes. Here is an example: [`_mock.feature`](../karate-junit4/src/test/java/com/intuit/karate/mock/_mock.feature).\n\n## `requestUrlBase`\nHolds the value of the \"base URL\". This will be in the form `http://somehost:8080` and will include the port number if needed. It may start with `https` if applicable.\n\n## `requestPath`\nOnly the path part, starting with `/`. The query string will *not* be included. For example, if the request URL was `http://foo/bar?baz=ban` - the value of `requestPath` will be `/bar`.\n\n## `requestUri`\nEverything on the right side of the [`requestUrlBase`](#requesturlbase). This will include query string parameters if present. For example, if the request URL was `http://foo/bar?baz=ban` - the value of `requestUri` will be `/bar?baz=ban`.\n\n## `requestMethod`\nThe HTTP method, for e.g. `GET`. It will be in capital letters. Instead of doing things like: `requestMethod == 'GET'` - \"best practice\" is to use the [`methodIs()`](#methodis) helper function for request matching.\n\n## `requestHeaders`\nNote that this will be a Map of List-s. For request matching, the [`typeContains()`](#typecontains), [`acceptContains()`](#acceptcontains) or [`headerContains()`](#headercontains) helpers are what you would use most of the time.\n\nIf you really need to \"route\" to a `Scenario` based on a custom header value, use the [`karate.request`](https://github.com/karatelabs/karate#karate-request) API. The advantage here is that it gets the value for a header ignoring-case. So here it doesn't matter if the header key is `Foo` or `foo`:\n\n```cucumber\nScenario: pathMatches('/v1/headers') && karate.request.header('foo') == 'bar'\n```\n\nFor completeness, you can use the [`karate.get()`](https://github.com/karatelabs/karate#karate-get) API - which will gracefully return `null` if the JsonPath does not exist. For example, the following would match a header of the form: `val: foo`\n\n```cucumber\nScenario: pathMatches('/v1/headers') && karate.get('requestHeaders.val[0]') == 'foo'\n```\n\nNote that you can define your custom JS re-usable functions in the `Background` which can make complex matching logic easier to implement.\n\n## `requestParams`\nA map-like' object of all query-string parameters and the values will always be an array. The built-in convenience function [`paramExists()`](#paramexists) is what you would use most of the time.\n\n## `requestParts`\nThis can be used to handle file-upload use-cases. If the incoming request is a multipart, this variable will be set and it is a `Map` of `List`-s. For example - if a file was in the request under the name `myFile`, you can get the details like this:\n\n```cucumber\nScenario: pathMatches('/v1/upload/excel')\n    * def filePart = requestParts['myFile'][0]\n```\n\nEach \"part\" (`filePart` in the above example) will contain the following properties:\n\n* `name` - the name (which will be `myFile` in the above example)\n* `charset`\n* `transferEncoding`\n* `filename` - the file name of this part\n* `contentType` - the content-type of this part\n* `value` - the content as raw bytes\n\nThis is one of the rare places in Karate where raw bytes are exposed, but it should be easy for you to [convert it into a string](https://github.com/karatelabs/karate#type-conversion) if needed.\n\n```cucumber\n* string message = filePart.value\n```\n\nAlso refer to [this article](https://software-that-matters.com/2021/07/02/mock-a-file-server-with-karate-test-framework/) by Peter Quiel.\n\n## `pathMatches()`\nHelper function that makes it easy to match a URI pattern as well as set [path parameters](#pathparams) up for extraction later using curly-braces. For example:\n\n```cucumber\nScenario: pathMatches('/v1/cats/{id}')\n    * def id = pathParams.id\n```\n\nThe curly-braces can match only one \"segment\" of the path at any time. But you can pull off any kind of complicated match using plain old JavaScript and inspecting the other objects such as [`requestUri`](#requesturi). For example, to match *any* path that starts with `/foo/` such as `/foo/bar` and `/foo/bar/baz`:\n\n```cucumber\nScenario: requestUri.startsWith('foo/')\n```\n\nAnd since this is plain-old JavaScript you can even do this:\n\n```cucumber\nScenario: !requestUri.startsWith('foo/')\n```\n\n## `pathParams`\nJSON variable (not a function) allowing you to extract values by name. See [`pathMatches()`](#pathmatches) above.\n\n## `methodIs()`\nHelper function that you will use a lot along with [`pathMatches()`](#pathmatches). Lower-case is fine. For example:\n\n```cucumber\nScenario: pathMatches('/v1/cats/{id}') && methodIs('get')\n    * def response = cats[pathParams.id]\n```\n\n## `paramExists()`\nFunction (not a variable) designed to match request on query parameter instead of [`requestParams`](#requestparams). Returns a boolean.\n```cucumber\nScenario: pathMatches('/greeting') && paramExists('name')\n```\n\n## `paramValue()`\nFunction (not a variable) designed to make it easier to work with query parameters instead of [`requestParams`](#requestparams). It will return a single (string) value (instead of an array) if the size of the parameter-list for that name is 1, which is what you need most of the time. For example:\n\n```cucumber\nScenario: pathMatches('/greeting') && paramExists('name')\n    * def content = 'Hello ' + paramValue('name') + '!'\n    * def response = { id: '#(nextId())', content: '#(content)' }\n```\n\n## `typeContains()`\nFunction to make matching the `Content-Type` header easier. And it uses a string \"contains\" match so that `typeContains('xml')` will match both `text/xml` or `application/xml`. Note how using JavaScript expressions makes all kinds of complex matching possible.\n\n```cucumber\nScenario: pathMatches('/cats') && methodIs('post') && typeContains('xml')\n```\n\n## `acceptContains()`\nJust like the above, to make matching the `Accept` header easier.\n\n```cucumber\nScenario: pathMatches('/cats/{id}') && acceptContains('xml')\n    * def cat = cats[pathParams.id]\n    * def response = <cat><id>#(cat.id)</id><name>#(cat.name)</name></cat>\n```\n\n## `headerContains()`\nThis should be sufficient to test that a particular header has a certain value, even though between the scenes it does a string \"contains\" check which can be convenient. If you really need an \"exact\" match, see [`requestHeaders`](#requestheaders).\n\nFor example, the following would match a header of the form: `val: foo`\n\n```cucumber\nScenario: pathMatches('/v1/headers') && headerContains('val', 'foo')\n```\n\n## `bodyPath()`\nA very powerful helper function that can run JsonPath or XPath expressions against the request body or payload.\n\nJSON example:\n\n```cucumber\nScenario: pathMatches('/v1/body/json') && bodyPath('$.name') == 'Scooby'\n```\n\nIt is worth mentioning that because of Karate's \"native\" support for JSON, you don't need it most of the time as the below is equivalent to the above. You just use the [`request`](#request) object directly:\n\n```cucumber\nScenario: pathMatches('/v1/body/json') && request.name == 'Scooby'\n```\n\nXML example:\n```cucumber\nScenario: pathMatches('/v1/body/xml') && bodyPath('/dog/name') == 'Scooby'\n```\n\nRefer to this example: [`server.feature`](src/test/java/com/intuit/karate/server.feature).\n\n# Response Building\nShaping the HTTP response is very easy - you just set a bunch of variables. This is surprisingly effective, and gives you the flexibility to perform multiple steps as part of request processing. You don't need to build the whole response and \"return\" it on the last line. And the order of what you define does not matter.\n\n## `response`\nThe actual response body or payload. Can be any [Karate data-type](https://github.com/karatelabs/karate#native-data-types) such as JSON or XML.\n\nSince you can use [embedded-expressions](https://github.com/karatelabs/karate#embedded-expressions), you can create dynamic responses with a minimum of effort:\n\n```cucumber\nScenario: pathMatches('/v1/cats')\n    * def responseStatus = 201\n    * def response = { id: '#(uuid())', name: 'Billie' }\n```\n\nSee the [`Background`](#background) example for how the `uuid` function can be defined.\n\nOne of the great things about Karate is how easy it is to [read JSON or XML from files](https://github.com/karatelabs/karate#reading-files). So when you have large complex responses, you can easily do this:\n\n```cucumber\n    * def response = read('get-cats-response.json')\n```\n\nNote that [embedded expressions](https://github.com/karatelabs/karate#embedded-expressions) work even for content loaded using `read()` !\n\nTo give you some interesting ideas, say you had a program written in a different language (e.g. Python) and it happened to be invoke-able on the command line. And if it returns a JSON string on the console output - then using the [`karate.exec()`](https://github.com/karatelabs/karate#karate-exec) API you can actually do this:\n\n```cucumber\n    * def response = karate.exec('some os command')\n```\n\nBecause of Karate's [Java interop capabilities](https://github.com/karatelabs/karate#calling-java) there is no limit to what you can do. Need to call a database and return data ? No problem ! Of course at this point you may need to stop and think if you need to use a *real* app server. But that said, Karate gives you a way to create full fledged micro-services in minutes - far faster than how you would using traditional technologies such as Tomcat, Node / Express, Flask / Django and the like.\n\n## `responseStatus`\nThe HTTP response code. This defaults to `200` for convenience, so you don't need to set it at all for \"happy path\" cases. Here's an example of conditionally setting a `404`:\n\n```cucumber\nScenario: pathMatches('/v1/cats/{id}') && methodIs('get')\n    * def response = cats[pathParams.id]\n    * def responseStatus = response ? 200 : 404\n```\n\n## `responseHeaders`\nYou can easily set multiple headers as JSON in one step as follows:\n\n```cucumber\nScenario: pathMatches('/v1/test')\n    * def responseHeaders = { 'Content-Type': 'application/octet-stream' }\n```\n\n## `configure responseHeaders`\nMany times you want a set of \"common\" headers to be returned for *every* end-point within a server-feature. You can use the [`Background`](#background) section to set this up as follows:\n\n```cucumber\nBackground:\n    * configure responseHeaders = { 'Content-Type': 'application/json' }\n```\n\nNote that `Scenario` level [`responseHeaders`](#responseheaders) can over-ride anything set by the \"global\" `configure responseHeaders`. This is convenient, as you may have a majority of end-points with the same `Content-Type`, and only one or two exceptions such as `text/html` or `text/javascript`.\n\n## `configure cors`\nThis allows a wide range of browsers or HTTP clients to make requests to a Karate server without running into [CORS issues](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). And this is perfect for UI / Front-End teams who can even work off an HTML file on the file-system.\n\nLike [`configure responseHeaders`](#configure-responseheaders), this is also meant to be declared in the [`Background`](#background).\n\n```cucumber\nBackground:\n    * configure cors = true\n````\n\nThis automatically adds the following headers to *every* response:\n\n```\nAllow: GET, HEAD, POST, PUT, DELETE, PATCH\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, PATCH\n```\n\n## `responseDelay`\nYou can easily set response delay in milliseconds\n\n```cucumber\nScenario: pathMatches('/v1/test')\n    * def responseDelay = 4000\n```\n\nRefer to this example: [`payment-service-proxy.feature`](../karate-demo/src/test/java/mock/contract/payment-service-proxy.feature).\n\nFor more dynamic \"global\" behavior such as a random delay for *every* `Scenario`, look at [`configure afterScenario`](#configure-afterscenario).\n\n## `afterScenario`\nUse this to add re-use any behaviour after scenario run, e.g. logging. For example:\n\n```cucumber\n* def afterScenario =\n\"\"\"\nfunction(){\n    karate.log('finished')\n}\n\"\"\"\n```\n\n### `configure afterScenario`\nJust like the above, but you can set this \"globally\" for all route-handlers in the [`Background`](#background). Here is an example of setting a random delay.\n\n```cucumber\nBackground:\n* configure afterScenario = function(){ karate.set('responseDelay', 200 + Math.random() * 400) }\n```\n\n## `karate.abort()`\nStop evaluating any more steps in the `Scenario` and return the `response`. Useful when combined with [`eval`](https://github.com/karatelabs/karate#eval) and conditional checks in JavaScript.\n\n```cucumber\nScenario: pathMatches('/v1/abort')\n    * def response = { success: true }\n    * if (response.success) karate.abort()\n    * print 'this will not be printed'\n```\n\n# Proxy Mode\n## `karate.proceed()`\nIt is easy to set up a Karate server to \"intercept\" HTTP and then delegate them to a target server only if needed. Think of this as \"[AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming)\" for web services !\n\n> (*Intercepting HTTPS is not possible in 1.0, and needs community contribution to revive*)\n\nIf you invoke the built in Karate function `karate.proceed(url)` - Karate will make an HTTP request to the URL using the current values of the [`request`](#request) and [`requestHeaders`](#requestheaders). Since the [request](#request-handling) is *mutable* this gives rise to some very interesting possibilities. For example, you can modify the request or decide to return a response without calling a downstream service.\n\nA twist here is that if the parameter is `null` Karate will use the host in the incoming HTTP request as the target URL - which is what you want when you run Karate as an HTTP proxy.\n\nRefer to this example: [`payment-service-proxy.feature`](../karate-demo/src/test/java/mock/contract/payment-service-proxy.feature) and also row (5) of the [Consumer-Provider example](#consumer-provider-example)\n\nIf not-null, the parameter has to be a URL that starts with `http` or `https`.\n\nAfter the execution of `karate.proceed()` completes, the values of [`response`](#response) and [`responseHeaders`](#responseheaders) would be ready for returning to the consumer. And you again have the option of mutating the [response](#response-building).\n\nSo you have control before and after the actual call, and you can modify the request or response - or introduce a time-delay using [`afterScenario`](#afterscenario).\n\n# Stopping\nA simple HTTP `GET` to `/__admin/stop` is sufficient to stop a running server gracefully. So you don't need to resort to killing the process, which can lead to issues especially on Windows - such as the port not being released.\n\n> Tip: for stopping HTTPS servers, you can use [curl](https://curl.haxx.se) like this: `curl -k https://localhost:8443/__admin/stop`\n\nIf you have started the server programmatically via Java, you can keep a reference to the `FeatureServer` instance and call the `stop()` method. Here is an example: [ConsumerUsingMockTest.java](../karate-demo/src/test/java/mock/contract/ConsumerUsingMockTest.java).\n\n# Other Examples\n## The World's Smallest MicroService !\n\nWhich at 267 characters - is small enough to fit within a single tweet ! It implements a '`POST`', '`GET` by id' and '`GET` all' for a `/cats` resource:\n\n```cucumber\nFeature:\n\nBackground:\n* def id = 0\n* def m = {}\n\nScenario: methodIs('post')\n* def c = request\n* def id = ~~(id + 1)\n* c.id = id\n* m[id + ''] = c\n* def response = c\n\nScenario: pathMatches('/cats/{id}')\n* def response = m[pathParams.id]\n\nScenario:\n* def response = $m.*\n```\n\n> To understand what the `~~` is doing, refer to the main Karate documentation on [type conversion](https://github.com/karatelabs/karate#floats-and-integers).\n\nTo get an idea of how much functionality the above code packs, have a look at the integration test for this service: [`cats.feature`](../karate-demo/src/test/java/mock/micro/cats.feature).\n\nWant to try this out now ? It takes only [2 minutes](#quick-start).\n\n## BenTen\nThe [BenTen](https://github.com/karatelabs/benten) project is a great example of the usage of Karate test-doubles. This team was able to create a mock-service that simulates almost the entire life-cycle of an [Atlassian JIRA](https://www.atlassian.com/software/jira) ticket.\n\nHere is the source code: [`benten-mock.feature`](https://github.com/karatelabs/benten/blob/master/benten-mock/src/main/resources/benten-mock.feature). Note how complex JSON payloads have been separated out into [files](https://github.com/karatelabs/benten/blob/master/benten-mock/src/main/resources/transitions.json) and elegantly loaded using the [`read`](https://github.com/karatelabs/karate#reading-files) function. State management *just works* and has been implemented in a few lines of extremely readable code.\n\n# JavaScript Mocks\nKarate 1.3.0 onwards offers an option to write mocks in JavaScript which is suited for more complex server-side logic, validations or state-handling. Refer to the [wiki for more](https://github.com/karatelabs/karate/wiki/Karate-JavaScript-Mocks).\n"
  },
  {
    "path": "karate-playwright/.gitignore",
    "content": "playwright/**/*"
  },
  {
    "path": "karate-playwright/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>io.karatelabs</groupId>\n        <artifactId>karate-parent</artifactId>\n        <version>1.5.2</version>\n    </parent>\n\n    <artifactId>karate-playwright</artifactId>\n    <packaging>jar</packaging>\n    <name>${project.artifactId}</name>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.microsoft.playwright</groupId>\n            <artifactId>playwright</artifactId>\n            <version>1.38.0</version>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <version>${junit5.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-engine</artifactId>\n            <version>${junit5.version}</version>\n            <scope>runtime</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <version>5.5.0</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n    \n</project>\n"
  },
  {
    "path": "karate-playwright/src/main/java/com/intuit/karate/driver/playwright/PlaywrightDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.driver.playwright;\n\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.playwright.driver.PlaywrightDriverOptions;\nimport com.microsoft.playwright.Browser;\nimport com.microsoft.playwright.Playwright;\n\nimport java.util.Map;\n\n/**\n * A drop-in replacement for the \"legacy\" implementation. May help as long as\n * both the old and the new drivers are in the code-base, should be removed\n * if/when one gets dropped.\n *\n * To enable the new implementation, one just needs to add the karate-playwright\n * dependency in their pom.xml before karate-core.\n *\n * To conditionally enable the new implementation is bit more challenging. Maven\n * profiles are typically used for this, but unfortunately, dependencies from\n * profiles are added AFTER regular dependencies, so that won't work.\n *\n * One solution would be to declare karate-playwright as a regular dependency,\n * with its scope controlled by a property. By default, the property is\n * \"import\", so it won't be picked up by Maven and the legacy driver will be\n * used. But in the profile, the property is set to \"compile\" so that it gets\n * picked up. A bit of a hack, but it should work.\n *\n */\npublic class PlaywrightDriver extends com.intuit.karate.playwright.driver.PlaywrightDriver {\n\n    public static PlaywrightDriver start(Map<String, Object> map, ScenarioRuntime sr) {\n        return com.intuit.karate.playwright.driver.PlaywrightDriver.start(map, sr, PlaywrightDriver::new);\n    }\n\n    public PlaywrightDriver(PlaywrightDriverOptions options, Browser browser, Playwright playwright) {\n        super(options, browser, playwright);\n        options.driverLogger.info(\"Using native PlaywrightDriver\");\n    }\n    \n}\n"
  },
  {
    "path": "karate-playwright/src/main/java/com/intuit/karate/playwright/driver/InvocationHandlers.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.playwright.driver;\n\nimport com.intuit.karate.Logger;\n\nimport java.io.Closeable;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.InvocationTargetException;\n\npublic class InvocationHandlers {\n\n\n    /**\n     * Handler that may be used with retry action operations.\n     * Retries for actions must be signaled specifically by retry() and are disabled when the operation returns. \n     * \n     * @param delegate\n     * @param options\n     * @return\n     */\n    static InvocationHandler retryHandler(Object delegate, Integer count, Integer interval, PlaywrightDriverOptions options) {\n        options.enableRetry(count, interval);\n        return (proxy, method, args) -> {\n            try {\n                // no for (int i=0; i<options.getRetryCount()  loop. We will leverage PW's autowait and perform just one call with timeout count * interval)\n                return method.invoke(delegate, args);\n            } catch (InvocationTargetException ite) {\n                throw ite.getCause();\n            } finally {\n                options.disableRetry();\n            }\n        };\n    }\n\n    static InvocationHandler submitHandler(Object delegate, Runnable waitingForPage) {\n        return (proxy, method, args) -> {\n            Object response = method.invoke(delegate, args);\n            waitingForPage.run();\n            if (waitingForPage instanceof Closeable) {\n                ((Closeable) waitingForPage).close();\n            }\n            return response;\n        };\n    }\n\n}\n"
  },
  {
    "path": "karate-playwright/src/main/java/com/intuit/karate/playwright/driver/PlaywrightDriver.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.playwright.driver;\n\nimport com.intuit.karate.core.*;\nimport com.intuit.karate.driver.Mouse;\nimport com.intuit.karate.driver.*;\nimport com.intuit.karate.graal.JsValue;\nimport com.intuit.karate.http.HttpRequest;\nimport com.intuit.karate.http.ResourceType;\nimport com.intuit.karate.http.Response;\nimport com.microsoft.playwright.Frame;\nimport com.microsoft.playwright.BrowserContext.WaitForConditionOptions;\nimport com.microsoft.playwright.Page.NavigateOptions;\nimport com.microsoft.playwright.Page.WaitForFunctionOptions;\nimport com.microsoft.playwright.assertions.LocatorAssertions.HasCountOptions;\nimport com.microsoft.playwright.*;\nimport com.microsoft.playwright.options.*;\nimport org.graalvm.polyglot.Value;\n\nimport static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;\n\nimport java.io.Closeable;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Proxy;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\n/**\n * Implementation of a Karate Driver for Playwright.\n *\n * Unlike the original PlaywrightDriver living in karate-core which was based on\n * the internal wire protocol, this one uses the public Playwright APIs. To use\n * it, make sure the karate-playwright dependency is added in your pom.xml, and\n * located before karate-core.\n *\n * It supports: - xpath, css, wildcard locators as well as friendly locators\n * (through custom locators which we believe are better suited to Karate than\n * the 'right-of' pseudo selectors offered by PW) - headless tests - http\n * requests intercepting (basic support, only urlPatterns are matched) - and of\n * course all the browsers supported by Playwright.\n *\n * This driver will start up a Playwright engine unless the playwrightUrl is\n * specified, in which case the driver will try to connect to that url.\n *\n * A couple of additional options may be specified in the playwrightOptions\n * property: - installBrowsers (true/false): whether PW will automatically\n * download and install the browsers - channel (e.g. \"chrome\"): for the\n * \"chromium\" browserType, Playwright allows us to pick the underlying engine.\n *\n * The following points are not 100% identical to the other Drivers - Cookies\n * are supported but a domain/path or url key is mandatory - Retries are\n * supported but, per doc, drivers should wait the specified\n * <pre>interval</pre> number of milliseconds before retrying. This driver will\n * however wait <i>at most</i><pre>interval</pre> milliseconds, but it leverages\n * Playwright's auto-wait/auto-retry to return as soon as the element is\n * available and won't wait for the full specified interval.But ig(count: 3,\n * interval: 3000 milliseconds) means try three times, and wait for 3 seconds\n * before the next re-try attempt. In fact, if slowDiv takes 2 seconds to load,\n * retry(3, 1500).click('#slowDiv') will return in roughly 2s. So will retry(2,\n * 2000), and retry(5, 800). Of course, retry(3, 500) will fail.\n */\n/*\n * Possible improvements:\n * - add option to enable tracing\n * - take advantage of PW's multi browser capability.\n * \n */\npublic class PlaywrightDriver implements Driver {\n\n    // Revert back to options.timeout\n    private static final Integer DEFAULT_TIMEOUT = null;\n\n    private static final String FRIENDLY_ENGINE = \"{\\n\"\n            + \"  queryAll(root,args) {\\n\"\n            + \"    function retain_right(rootRect, itemRect) {\\n\"\n            + \"       return itemRect.x >= (rootRect.x + rootRect.width) && itemRect.y <= (rootRect.y + rootRect.height) && (itemRect.y + itemRect.height) >=rootRect.y;\\n\"\n            + \"    }\\n\"\n            + \"    function retain_left(rootRect, itemRect) {\\n\"\n            + \"       return (itemRect.x + itemRect.width) <= rootRect.x && itemRect.y <= (rootRect.y + rootRect.height) && (itemRect.y + itemRect.height) >=rootRect.y;\\n\"\n            + \"    }\\n\"\n            + \"    function retain_below(rootRect, itemRect) {\\n\"\n            + \"       return itemRect.y >= (rootRect.y + rootRect.height) && itemRect.x <= (rootRect.x + rootRect.width) && (itemRect.x + itemRect.width) >=rootRect.x;\\n\"\n            + \"    }\\n\"\n            + \"    function retain_above(rootRect, itemRect) {\\n\"\n            + \"       return (itemRect.y + itemRect.height) <= rootRect.y && itemRect.x <= (rootRect.x + rootRect.width) && (itemRect.x + itemRect.width) >=rootRect.x;\\n\"\n            + \"    }\\n\"\n            + \"    function retain_near(rootRect, itemRect) {\\n\"\n            + \"       return true;\\n\"\n            + \"    }\\n\"\n            + \"    function items_list(selector) {\\n\"\n            + \"       if (selector.startsWith('/') || selector.startsWith('xpath=')) {\\n\"\n            + \"            let items_list = [];\\n\"\n            + \"            let query = document.evaluate(argsParts[1],document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);\\n\"\n            + \"            for (let i = 0; i<query.snapshotLength; i++) {\\n\"\n            + \"                items_list.push(query.snapshotItem(i));\\n\"\n            + \"            }\\n\"\n            + \"            return items_list;\\n\"\n            + \"       } else {\\n\"\n            + \"            return Array.from(document.querySelectorAll(selector));\\n\"\n            + \"       }\\n\"\n            + \"    }\\n\"\n            + \"    let rootRect = root.getBoundingClientRect();\\n\"\n            + \"    let argsParts = args.split(':');\\n\"\n            + \"    let itemsByDistance = new Map();\\n\"\n            + \"    let items = items_list(argsParts[1]);\\n\"\n            + \"    for (let i = 0; i<items.length; i++) {\\n\"\n            + \"      let item = items[i];\\n\"\n            + \"      let itemRect = item.getBoundingClientRect();\\n\"\n            + \"      if (eval('retain_'+argsParts[0])(rootRect, itemRect)){\\n\"\n            + // distance between root's center and item's. This is actually the squared distance but the actual values do not matter, as long as they are comparable with each other, which squared distances are.\n            \"        let distance = Math.pow(rootRect.x+rootRect.width/2-(itemRect.x+itemRect.width/2), 2) + Math.pow(rootRect.y+rootRect.height/2-(itemRect.y+itemRect.height/2), 2);\\n\"\n            + \"        itemsByDistance.set(item, distance);\\n\"\n            + \"      }\\n\"\n            + \"    }\\n\"\n            + \"    return [...itemsByDistance].sort((a, b) => a[1] - b[1]).map(item => item[0]);\\n\"\n            + \"  }\\n\"\n            + \"}\";\n\n    final PlaywrightDriverOptions options;\n    private final Playwright playwright;\n    private final Browser browser;\n    private final BrowserContext browserContext;\n    Page page;\n\n    private FrameTrait root;\n\n    private boolean terminated = false;\n    private String dialogText;\n\n    public interface PlaywrightDriverFactory<T extends Driver> {\n\n        T create(PlaywrightDriverOptions options, Browser browser, Playwright playwright);\n    }\n\n    public static Driver start(Map<String, Object> map, ScenarioRuntime sr) {\n        return start(map, sr, PlaywrightDriver::new);\n    }\n\n    public static <T extends Driver> T start(Map<String, Object> map, ScenarioRuntime sr, PlaywrightDriverFactory<T> factory) {\n\n        PlaywrightDriverOptions options = new PlaywrightDriverOptions(map, sr, 4444, \"playwright\");\n\n        Map<String, Object> pwOptions = options.playwrightOptions == null ? Collections.emptyMap() : options.playwrightOptions;\n        String browserTypeOption = (String) pwOptions.getOrDefault(\"browserType\", \"chromium\");\n\n        Browser browser;\n        if (Boolean.valueOf(pwOptions.getOrDefault(\"installBrowsers\", true) == Boolean.FALSE)) {\n            options.driverLogger.debug(\"Playwright browsers will not be installed.\");\n            // ensureDriverInstalled is called by Playwright.create, but the installBrowsers is forced to true.\n            // We call it here with a falsy installBrowsers, the singleton will be created and subsequent call from Playwright.create will have no effect\n\t\t\t// Disabling auto install might be useful when behind a firewall. playwrightOptions.channel might then need to be specified for Playwright to run the locally installed browser, else it will complain.\n            com.microsoft.playwright.impl.driver.Driver.ensureDriverInstalled(Collections.emptyMap(), false);\n        } else {\n            // Will actually be installed by the Playwright.create call below \n            options.driverLogger.info(\"Installing Playwright browsers (this may take some time)...\");\n        }\n        try {\n            Playwright playwright = Playwright.create();\n            playwright.selectors().register(\"friendly\", FRIENDLY_ENGINE);\n            Method browserTypeMethod = Playwright.class.getDeclaredMethod(browserTypeOption);\n            BrowserType browserType = (BrowserType) browserTypeMethod.invoke(playwright);\n            if (options.start) {\n                browser = browserType.launch(new BrowserType.LaunchOptions()\n                        .setHeadless(options.headless)\n                        .setChannel((String) pwOptions.getOrDefault(\"channel\", \"chromium\"))\n                        .setArgs(options.addOptions));\n            } else {\n\n                String playwrightUrl = options.playwrightUrl;\n                if (playwrightUrl == null) {\n                    throw new RuntimeException(\"playwrightUrl is mandatory if start == false\");\n                }\n                browser = browserType.connect(playwrightUrl);\n            }\n            T driver = factory.create(options, browser, playwright);\n            return driver;\n        } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public PlaywrightDriver(PlaywrightDriverOptions options, Browser browser, Playwright playwright) {\n        this.options = options;\n        this.playwright = playwright;\n        options.setDriver(this);\n        this.browser = browser;\n        this.browserContext = browser.newContext();\n        this.timeout(null);\n        setPage(browserContext.newPage());\n    }\n\n    private void setPage(Page page) {\n        this.page = page;\n        this.root = FrameTrait.of(page);\n    }\n\n    @Override\n    public void quit() {\n        if (!terminated) {\n            terminated = true;\n            browserContext.close();\n            browser.close();\n            playwright.close();\n        }\n    }\n\n    @Override\n    public String getDialogText() {\n        return dialogText;\n    }\n\n    // private Locator locator(String locator) {\n    //     return this.locator.locatorFor(locator);\n    // }\n    @Override\n    public void dialog(boolean accept) {\n        dialog(accept, Dialog::accept);\n    }\n\n    @Override\n    public void dialog(boolean accept, String input) {\n        dialog(accept, dialog -> dialog.accept(input));\n    }\n\n    private void dialog(boolean accept, Consumer<Dialog> onAccept) {\n        page.onDialog(dialog -> {\n            if (\"alert\".equals(dialog.type()) || !accept) {\n                dialog.dismiss();\n                dialogText = null;\n            } else {\n                onAccept.accept(dialog);\n                this.dialogText = dialog.message();\n            }\n        });\n    }\n\n    @Override\n    public Element waitFor(String locator) {\n        return rootElement(locator).waitFor();\n    }\n\n    @Override\n    public Element waitForAny(String locator1, String locator2) {\n        return waitForAny(new String[]{locator1, locator2});\n    }\n\n    @Override\n    public Element waitForAny(String[] locators) {\n        List<Locator> pwLocators = Arrays.stream(locators).map(token -> root.locator(token)).collect(Collectors.toList());\n\n        Locator orLocators = pwLocators.get(0);\n        for (int i = 1; i < pwLocators.size(); i++) {\n            orLocators = orLocators.or(pwLocators.get(i));\n        }\n        orLocators.waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.VISIBLE).setTimeout(waitTimeout()));\n\n        // Find which locator is available, and return it.\n        // This is on par with waitForAny specs. However, I wonder if just returning orLocators and let PW work out which one is available in the subsequent calls (click, ...) would work.\n        // Im not completely sold it would ( and that''s without even touching on how to create an element from a locator...)\n        for (int i = 0; i < pwLocators.size(); i++) {\n            if (pwLocators.get(i).isVisible()) {\n                return rootElement(locators[i]);\n            }\n        }\n\n        throw new IllegalStateException();\n    }\n\n    @Override\n    public Element waitForEnabled(String locator) {\n        // Per https://playwright.dev/java/docs/actionability, Playwright will auto-wait for enabled when the next action (click, ...) is invoked.\n        // Nothing to do here.\n        // TODO test this\n        return rootElement(locator);\n    }\n\n    @Override\n    public Element waitForText(String locator, String text) {\n        return rootElement(locator).waitForText(text);\n    }\n\n    @Override\n    public Element waitUntil(String locator, String expression) {\n        return rootElement(locator).waitUntil(expression);\n    }\n\n    public List<Element> waitForResultCount(String locator, int count) {\n        Locator allLocators = root.locator(locator);\n        try {\n            assertThat(allLocators).hasCount(count, new HasCountOptions().setTimeout(waitTimeout()));\n            return locateAll(locator);\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    public List<Object> waitForResultCount(String locator, int count, String expression) {\n        String jsExpression = toJsExpression(expression);\n        List<Element> elements = waitForResultCount(locator, count);\n        return elements.stream().map(element -> element.script(jsExpression)).collect(Collectors.toList());\n    }\n\n    @Override\n    public Element focus(String locator) {\n        return rootElement(locator).focus();\n    }\n\n    @Override\n    public Element clear(String locator) {\n        return rootElement(locator).clear();\n    }\n\n    @Override\n    public Element click(String locator) {\n        return rootElement(locator).click();\n    }\n\n    @Override\n    public Element value(String locator, String value) {\n        PlaywrightElement element = rootElement(locator);\n        element.setValue(value);\n        return element;\n    }\n\n    @Override\n    public Element input(String locator, String value) {\n        return rootElement(locator).input(value);\n    }\n\n    @Override\n    public Element input(String locator, String value, int delay) {\n        String[] array = value.chars().mapToObj(ch -> String.valueOf((char) ch)).toArray(String[]::new);\n        return rootElement(locator).input(array, delay);\n    }\n\n    public Element input(String locator, String[] values, int delay) {\n        return rootElement(locator).input(values, delay);\n    }\n\n    @Override\n    public Element select(String locator, String text) {\n        return rootElement(locator).select(text);\n    }\n\n    @Override\n    public Element select(String locator, int index) {\n        return rootElement(locator).select(index);\n    }\n\n    @Override\n    public String html(String locator) {\n        return rootElement(locator).getHtml();\n    }\n\n    @Override\n    public String text(String locator) {\n        return rootElement(locator).getText();\n    }\n\n    @Override\n    public String value(String locator) {\n        return rootElement(locator).getValue();\n    }\n\n    @Override\n    public String attribute(String locator, String name) {\n        return rootElement(locator).attribute(name);\n    }\n\n    @Override\n    public String property(String locator, String name) {\n        return rootElement(locator).property(name);\n    }\n\n    @Override\n    public boolean enabled(String locator) {\n        return rootElement(locator).isEnabled();\n    }\n\n    @Override\n    public Object script(String expression) {\n        return page.evaluate(toJsExpression(expression));\n    }\n\n    @Override\n    public Object script(String locator, String expression) {\n        return rootElement(locator).script(expression);\n    }\n\n    @Override\n    public List<Object> scriptAll(String locator, String expression) {\n        // element.script most likely convert expression so we will pay the conversion price for every item in the list.\n        // But this makes the code consistently working with Element. \n        return locateAll(locator).stream().map(element -> element.script(expression)).toList();\n    }\n\n    @Override\n    public Finder rightOf(String locator) {\n        return rootElement(locator).rightOf();\n//        return new PlaywrightFinder(this, ofRoot(locator), \"right-of\");\n    }\n\n    @Override\n    public Finder leftOf(String locator) {\n        return rootElement(locator).leftOf();\n    }\n\n    @Override\n    public Finder near(String locator) {\n        return rootElement(locator).near();        \n    }\n\n    @Override\n    public Finder above(String locator) {\n        return rootElement(locator).above();\n    }\n\n    @Override\n    public Finder below(String locator) {\n        return rootElement(locator).below();\n    }\n\n    public Element highlight(String locator, int millis) {\n        // todo millis not taken into account.\n        return rootElement(locator).highlight();\n    }\n\n    public void highlightAll(String locator, int millis) {\n        // todo millis not taken into account.\n        locateAll(locator).forEach(Element::highlight);\n    }\n\n    ///////////////////////////////////////////////\n    // Locate and its lenient counterpart optional\n    //////////////////////////////////////////////\n    @Override\n    public List<Element> locateAll(String locator) {\n        return rootToken(locator).findAll(this);\n\n    }\n\n    @Override\n    public Element locate(String locator) {\n        return rootToken(locator).find(this).orElseThrow(() -> new IllegalArgumentException(locator+\" not found\"));\n    }\n\n\n    @Override\n    public Element optional(String locator) {\n        return rootToken(locator).find(this).orElseGet(() -> new MissingElement(this, locator));\n    }\n\n    @Override\n    public boolean exists(String locator) {\n        return rootToken(locator).find(this).isPresent();\n    }\n\n       \n    ////////////////////////////////////////////////////////////\n    // Position, scroll, screenshot locator based-operations\n    ///////////////////////////////////////////////////////////\n    @Override\n    public Map<String, Object> position(String locator) {\n        return rootElement(locator).getPosition();\n    }\n\n    static Map<String, Object> asCoordinatesMap(double x, double y, double width, double height) {\n        Map<String, Object> position = new HashMap<>();\n        position.put(\"x\", x);\n        position.put(\"y\", y);\n        position.put(\"width\", Math.round(width));\n        position.put(\"height\", Math.round(height));\n        return position;\n    }\n\n    @Override\n    public Map<String, Object> position(String locator, boolean relative) {\n        if (!relative) {\n            return position(locator);\n        }\n        return (Map<String, Object>) script(DriverOptions.getPositionJs(locator));\n    }\n\n    @Override\n    public Element scroll(String locator) {\n        return rootElement(locator).scroll();\n    }\n\n    @Override\n    public byte[] screenshot(String locator, boolean embed) {\n        byte[] screenshot = rootElement(locator).screenshot();\n        if (embed) {\n            getRuntime().embed(screenshot, ResourceType.PNG);\n        }\n        return screenshot;\n    }\n\n    /////////////////////////////////////////////////////\n    // Page based-operations\n    /////////////////////////////////////////////////////\n    @Override\n    public void activate() {\n        page.bringToFront();\n    }\n\n    @Override\n    public void refresh() {\n        page.reload();\n    }\n\n    @Override\n    public void reload() {\n        // https://playwright.dev/java/docs/api/class-page#page-route Enabling routing disables http cache(?)\n        page.route(\"*\", Route::resume);\n        page.reload();\n        page.unroute(\"*\");\n    }\n\n    @Override\n    public void back() {\n        page.goBack();\n    }\n\n    @Override\n    public void forward() {\n        page.goForward();\n    }\n\n    @Override\n    public void maximize() {\n\n    }\n\n    @Override\n    public void minimize() {\n\n    }\n\n    @Override\n    public void fullscreen() {\n    }\n\n    @Override\n    public void close() {\n        page.close();\n    }\n\n    @Override\n    public String getUrl() {\n        return page.url();\n    }\n\n    @Override\n    public void setUrl(String url) {\n        page.navigate(url, new NavigateOptions().setTimeout(waitTimeout()));\n    }\n\n    @Override\n    public Map<String, Object> getDimensions() {\n        ViewportSize viewportSize = page.viewportSize();\n        return asCoordinatesMap(0, 0, viewportSize.width, viewportSize.height);\n    }\n\n    @Override\n    public void setDimensions(Map<String, Object> map) {\n        page.setViewportSize((Integer) map.get(\"width\"), (Integer) map.get(\"height\"));\n    }\n\n    @Override\n    public String getTitle() {\n        // works both with frame and page.\n        return root.getTitle();\n    }\n\n    @Override\n    public byte[] screenshot(boolean embed) {\n        byte[] screenshot = page.screenshot();\n        if (embed) {\n            getRuntime().embed(screenshot, ResourceType.PNG);\n        }\n        return screenshot;\n    }\n\n    @Override\n    public byte[] pdf(Map<String, Object> options) {\n        return page.pdf(new Page.PdfOptions().setLandscape(\"landscape\".equalsIgnoreCase((String) options.get(\"orientation\"))));\n    }\n\n    @Override\n    public boolean waitUntil(String expression) {\n        try {\n            waitForFunction(expression, null);\n            return true;\n        } catch (Exception e) {\n            options.driverLogger.warn(\"waitUntil evaluate failed: {}\", e.getMessage());\n            return false;\n        }\n    }\n\n    @Override\n    public String waitForUrl(String url) {\n        page.waitForURL(\"**\" + url);\n        return getUrl();\n    }\n\n    ///////////////////////////////////////////////////////\n    // Page(s), cookies, timeouts and other context-based operations\n    // (also contains swtichFrames method altough arguably they should be somewhere else)\n    ///////////////////////////////////////////////////////\n    // Sets PWs NAVIGATION timeout.\n    // See also waitTimeout()\n    @Override\n    public Driver timeout(Integer millis) {\n        browserContext.setDefaultNavigationTimeout(millis == DEFAULT_TIMEOUT ? options.timeout : millis.doubleValue());\n        return this;\n    }\n\n    @Override\n    public Driver timeout() {\n        return timeout(DEFAULT_TIMEOUT);\n    }\n\n    /**\n     * Timeout to be used for actions.\n     * \n     * See https://github.com/karatelabs/karate/issues/2291 for a discussion on its implementation.\n     */\n    int actionWaitTimeout() {\n        return options.isRetryEnabled() ? waitTimeout() : options.getRetryInterval();\n    }\n\n    int waitTimeout() {\n        return options.getRetryInterval() * options.getRetryCount();\n    }\n\n\n    @Override\n    public void switchPage(String titleOrUrl) {\n        browserContext.waitForCondition(() -> findPage(titleOrUrl).isPresent(), new WaitForConditionOptions().setTimeout(waitTimeout()));\n        findPage(titleOrUrl).ifPresent(this::setPage);\n    }\n\n    private Optional<Page> findPage(String titleOrUrl) {\n        return browserContext.pages().stream().filter(candidate -> candidate.url().contains(titleOrUrl) || candidate.title().contains(titleOrUrl)).findAny();\n    }\n\n    @Override\n    public void switchPage(int index) {\n        setPage(browserContext.pages().get(index));\n    }\n\n    @Override\n    public void switchFrame(int index) {\n        if (index == -1) {\n            setPage(page);\n        } else {\n            int frameIndex = index + 1; // frame[0]is the main frame\n            this.root = FrameTrait.of(page.frames().get(frameIndex));\n        }\n    }\n\n    @Override\n    public void switchFrame(String locator) {\n        if (locator == null) {\n            setPage(page);\n        } else {\n            this.root = FrameTrait.of(this.root.frameLocator(locator));\n        }\n    }\n\n    void switchTo(Locator locator) {\n        this.root = FrameTrait.of(locator.frameLocator(\":root\"));\n    }\n\n    @Override\n    public List<String> getPages() {\n        return browserContext.pages().stream().map(Page::toString).collect(Collectors.toList());\n    }\n\n    @Override\n    public Map<String, Object> cookie(String name) {\n        return browserContext.cookies().stream().filter(cookie -> name.equals(cookie.name))\n                .findAny()\n                .map(this::asCookieMap)\n                // TODO: what is the expected behavior if no cookie found?\n                .orElse(Collections.emptyMap());\n    }\n\n    @Override\n    public void cookie(Map<String, Object> cookieMap) {\n        browserContext.addCookies(Arrays.asList(\n                new Cookie((String) cookieMap.get(\"name\"), (String) cookieMap.get(\"value\"))\n                        .setDomain((String) cookieMap.get(\"domain\"))\n                        .setPath((String) cookieMap.get(\"path\"))\n                        .setUrl((String) cookieMap.get(\"url\")))\n        );\n    }\n\n    @Override\n    public void deleteCookie(String name) {\n        List<Cookie> cookies = browserContext.cookies();\n        browserContext.clearCookies();\n        browserContext.addCookies(cookies.stream().filter(cookie -> !cookie.name.equals(name)).collect(Collectors.toList()));\n    }\n\n    @Override\n    public void clearCookies() {\n        browserContext.clearCookies();\n    }\n\n    @Override\n    public List<Map> getCookies() {\n        return browserContext.cookies().stream().map(this::asCookieMap).collect(Collectors.toList());\n    }\n\n    private Map<String, Object> asCookieMap(Cookie cookie) {\n        Map<String, Object> map = new LinkedHashMap<>();\n        map.put(\"name\", cookie.name);\n        map.put(\"value\", cookie.value);\n        map.put(\"url\", cookie.url);\n        map.put(\"domain\", cookie.domain);\n        map.put(\"path\", cookie.path);\n        return map;\n    }\n\n    public DevToolsMock intercept(Value value) {\n        Map<String, Object> config = (Map) JsValue.toJava(value);\n        config = new Variable(config).getValue();\n        return intercept(config);\n    }\n\n    public DevToolsMock intercept(Map<String, Object> config) {\n        List<Map<String, String>> patterns = (List<Map<String, String>>) Objects.requireNonNull(config.get(\"patterns\"), \"missing array argument 'patterns'\");\n        List<Pattern> urlPatterns = patterns.stream().map(pattern -> Pattern.compile(pattern.get(\"urlPattern\").replace(\"*\", \".*\").replace(\"?\", \".?\"))).collect(Collectors.toList());\n        String mock = (String) Objects.requireNonNull(config.get(\"mock\"), \"missing argument 'mock'\");\n\n        Object o = getRuntime().engine.fileReader.readFile(mock);\n        if (!(o instanceof FeatureCall)) {\n            throw new IllegalArgumentException(\"'mock' is not a feature file: \" + mock);\n        }\n        FeatureCall fc = (FeatureCall) o;\n        MockHandler mockHandler = new MockHandler(fc.feature);\n        page.route(url -> matches(url, urlPatterns), route -> {\n            HttpRequest karateRequest = new HttpRequest();\n            karateRequest.setUrl(route.request().url());\n            karateRequest.setMethod(route.request().method());\n            karateRequest.setBody(route.request().postDataBuffer());\n            route.request().headers().forEach(karateRequest::putHeader);\n            Response karateResponse = mockHandler.handle(karateRequest.toRequest());\n            Map<String, String> responseHeaders = new HashMap<>();\n            karateResponse.getHeaders().forEach((k, v) -> responseHeaders.put(k, v.get(0)));\n            route.fulfill(new Route.FulfillOptions().setStatus(karateResponse.getStatus()).setBodyBytes(karateResponse.getBody()).setHeaders(responseHeaders));\n        });\n        return new DevToolsMock(mockHandler);\n    }\n\n    private boolean matches(String url, List<Pattern> urlPatterns) {\n        for (Pattern urlPattern : urlPatterns) {\n            if (urlPattern.matcher(url).matches()) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /////////////////////////////////////////////////\n    // Chaining stuff\n    /////////////////////////////////////////////////\n    private Driver driverProxy(InvocationHandler h) {\n        return (Driver) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Driver.class}, h);\n    }\n\n    @Override\n    public Driver submit() {\n        InvocationHandler h = InvocationHandlers.submitHandler(this, getWaitingForPage());\n        return driverProxy(h);\n    }\n\n    @Override\n    public Driver retry(Integer count, Integer interval) {\n        return driverProxy(InvocationHandlers.retryHandler(this, count, interval, options));\n    }\n\n    /////////////////////////////////////////////////\n    // Mouse\n    /////////////////////////////////////////////////\n    @Override\n    public Mouse mouse() {\n        return mouse(\":root\");\n    }\n\n    @Override\n    public Mouse mouse(String locator) {\n        return rootElement(locator).mouse();\n    }\n\n    @Override\n    public Mouse mouse(Number x, Number y) {\n        return mouse().move(x, y);\n    }\n\n    /////////////////////////////////////////////////\n    // Driver APIs that probably should not be public\n    ////////////////////////////////////////////////\n    @Override\n    public boolean isTerminated() {\n        return terminated;\n    }\n\n    @Override\n    public DriverOptions getOptions() {\n        return options;\n    }\n\n    @Override\n    public void actions(List<Map<String, Object>> actions) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Object elementId(String locator) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public List elementIds(String locator) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Object waitUntil(Supplier<Object> condition) {\n        throw new UnsupportedOperationException();\n    }\n\n    /////////////////////////////////////////////////\n    // PlaywrightDriver protected APIs\n    ////////////////////////////////////////////////\n\n    // Cannot call Thread.sleep or else no messages will be sent.\n    void sleep(int millis) {\n        page.waitForTimeout(millis);\n    }\n\n    WaitForPageLoaded getWaitingForPage() {\n        return new WaitForPageLoaded(page, browserContext);\n    }\n\n    com.microsoft.playwright.Mouse getMouse() {\n        return page.mouse();\n    }\n\n    static String toJsExpression(String expression) {\n        return expression.startsWith(\"_.\") ? (\"el => el.\" + expression.substring(2))\n                : expression.startsWith(\"!_.\") ? (\"el => !el.\" + expression.substring(3))\n                : expression;\n    }\n\n    void waitForFunction(String expression, ElementHandle elementHandle) {\n        page.waitForFunction(toJsExpression(expression), elementHandle, new WaitForFunctionOptions().setTimeout(waitTimeout()));\n    }\n\n    private PlaywrightToken rootToken(String locator) {\n        return PlaywrightToken.root(root, locator);\n    }\n\n    private PlaywrightElement rootElement(String locator) {\n        return rootToken(locator).create(this);\n    }    \n\n\n    /**\n     * <p>\n     * A Frame has a title and can create {@link FrameLocator}s as well as\n     * regular {@link Locator}s. So have other Frame-like classes.\n     * </p>\n     * This class acts as a common interface for different Playwright classes\n     * providing these capabilities.\n     *\n     */\n    public static interface FrameTrait {\n\n        public String getTitle();\n\n        public FrameLocator frameLocator(String token);\n\n        public Locator locator(String token);\n\n        public static FrameTrait of(Page page) {\n            return new FrameTrait() {\n                @Override\n                public String getTitle() {\n                    return page.title();\n                }\n\n                @Override\n                public FrameLocator frameLocator(String token) {\n                    return page.frameLocator(token);\n                }\n\n                @Override\n                public Locator locator(String token) {\n                    return page.locator(token);\n                }\n            };\n        }\n\n        public static FrameTrait of(Frame frame) {\n            return new FrameTrait() {\n                @Override\n                public String getTitle() {\n                    return frame.title();\n                }\n\n                @Override\n                public FrameLocator frameLocator(String token) {\n                    return frame.frameLocator(token);\n                }\n\n                @Override\n                public Locator locator(String token) {\n                    return frame.locator(token);\n                }\n            };\n        }\n\n        public static FrameTrait of(FrameLocator frameLocator) {\n            return new FrameTrait() {\n                @Override\n                public String getTitle() {\n                    return (String) frameLocator.locator(\":root\").evaluate(\"document.title\");\n                }\n\n                @Override\n                public FrameLocator frameLocator(String token) {\n                    return frameLocator.frameLocator(token);\n                }\n\n                @Override\n                public Locator locator(String token) {\n                    return frameLocator.locator(token);\n                }\n            };\n        }\n    }\n\n    public class WaitForPageLoaded implements Runnable, Closeable {\n\n        private final Page page;\n\n        private final BrowserContext browserContext;\n        private Consumer<Page> listener;\n\n        public WaitForPageLoaded(Page page, BrowserContext browserContext) {\n            this.page = page;\n            this.browserContext = browserContext;\n        }\n\n        @Override\n        public void run() {\n            // from the doc, my understanding is that submit() applies when navigating within the same page rather than in a new page.\n            // For the latter, the very handy browserContext.waitForPage method could be used.\n            // (which unfortunately can not be used to implement switchPage since it requires some chaining).\n            // For the former, waitForCondition is used, waiting for a DOMContentLoaded notification.\n            AtomicBoolean pageIsLoaded = new AtomicBoolean(false);\n            listener = page -> pageIsLoaded.set(true);\n            page.onDOMContentLoaded(listener);\n\n            browserContext.waitForCondition(pageIsLoaded::get, new WaitForConditionOptions().setTimeout(waitTimeout()));\n        }\n\n        public void close() {\n            page.offDOMContentLoaded(listener);\n        }\n    }\n    \n}\n"
  },
  {
    "path": "karate-playwright/src/main/java/com/intuit/karate/playwright/driver/PlaywrightDriverOptions.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.playwright.driver;\n\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.DriverOptions;\n\nimport java.util.Map;\n\npublic class PlaywrightDriverOptions extends DriverOptions {\n\n    private PlaywrightDriver driver;\n\n    public PlaywrightDriverOptions(Map<String, Object> options, ScenarioRuntime sr, int defaultPort, String defaultExecutable) {\n        super(options, sr, defaultPort, defaultExecutable);\n    }\n\n    public void setDriver(PlaywrightDriver driver) {\n        this.driver = driver;\n    }\n\n    @Override\n    public void sleep(int millis) {\n        driver.sleep(millis);\n    }\n    \n}\n"
  },
  {
    "path": "karate-playwright/src/main/java/com/intuit/karate/playwright/driver/PlaywrightElement.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.playwright.driver;\n\nimport com.intuit.karate.driver.*;\nimport com.microsoft.playwright.Locator;\nimport com.microsoft.playwright.Locator.ClearOptions;\nimport com.microsoft.playwright.Locator.ClickOptions;\nimport com.microsoft.playwright.Locator.FilterOptions;\nimport com.microsoft.playwright.Locator.FillOptions;\nimport com.microsoft.playwright.Locator.FocusOptions;\nimport com.microsoft.playwright.Locator.PressOptions;\nimport com.microsoft.playwright.Locator.ScrollIntoViewIfNeededOptions;\nimport com.microsoft.playwright.Locator.WaitForOptions;\nimport com.microsoft.playwright.options.BoundingBox;\nimport com.microsoft.playwright.options.ScreenshotType;\nimport com.microsoft.playwright.options.SelectOption;\nimport com.microsoft.playwright.options.WaitForSelectorState;\n\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Proxy;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\npublic class PlaywrightElement implements Element {\n\n    private final PlaywrightDriver driver;\n    private final PlaywrightToken token;\n    private Boolean present;\n\n    protected PlaywrightElement(PlaywrightDriver driver, PlaywrightToken token) {\n        this.driver = driver;\n        this.token = token;\n        present = null;\n    }\n\n    // TO be used by locate and optional, which knows whether the element exists or not.\n    // In all other cases, the other constructor should be used.\n    protected PlaywrightElement(PlaywrightDriver driver, PlaywrightToken token, boolean present) {\n        this(driver, token);\n        this.present = present;\n    }\n\n    @Override\n    public String getLocator() {\n        return token.getPlaywrightToken();\n    }\n\n    @Override\n    public boolean isEnabled() {\n        return resolveLocator().isEnabled();\n    }\n\n    @Override\n    public Map<String, Object> getPosition() {\n        BoundingBox boundingBox = this.resolveLocator().boundingBox();\n        return PlaywrightDriver.asCoordinatesMap(boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height);\n    }\n\n    @Override\n    public byte[] screenshot() {\n        return resolveLocator().screenshot(new Locator.ScreenshotOptions().setType(ScreenshotType.PNG));\n    }\n\n    @Override\n    public Element highlight() {\n        resolveLocator().highlight();\n        return this;\n    }\n\n    @Override\n    public Element focus() {\n        resolveLocator().focus(new FocusOptions().setTimeout(driver.actionWaitTimeout()));\n        return this;\n    }\n\n    @Override\n    public Element clear() {\n        resolveLocator().clear(new ClearOptions().setTimeout(driver.actionWaitTimeout()));\n        return this;\n    }\n\n    @Override\n    public Element click() {\n        resolveLocator().click(new ClickOptions().setTimeout(driver.actionWaitTimeout()));\n        return this;\n    }\n\n    @Override\n    public Element input(String value) {\n        return input(new String[]{value});\n    }\n\n    @Override\n    public Element input(String[] values) {\n        return input(values, 0);\n    }\n\n    @Override\n    public Element input(String[] values, int delay) {\n        for (String input : values) {\n            StringBuilder press = new StringBuilder();\n            int standardChars = 0;\n            for (int i = 0; i < input.length(); i++) {\n                char c = input.charAt(i);\n                String charValue = Keys.keyValue(c);\n                if (charValue != null) {// special value, handle it\n                    if (standardChars > 0) {\n                        resolveLocator().pressSequentially(input.substring(i - standardChars, i), new Locator.PressSequentiallyOptions().setDelay(delay).setTimeout(driver.actionWaitTimeout()));\n                        standardChars = 0;\n                    }\n                    if (press.length() > 0) {\n                        press.append(\"+\");\n                    }\n                    press.append(charValue);\n                    if (!Keys.isModifier(c)) {\n                        // send it straight away\n                        resolveLocator().press(press.toString(), new PressOptions().setTimeout(driver.actionWaitTimeout()));\n                        press.setLength(0);\n                    }\n                } else {\n                    if (press.length() > 0) {\n                        resolveLocator().press(press.append(\"+\").append(c).toString(), new PressOptions().setTimeout(driver.actionWaitTimeout()));\n                        press.setLength(0);\n                    } else {\n                        standardChars++;\n                    }\n                }\n            }\n\n            if (standardChars > 0) {\n                resolveLocator().pressSequentially(input.substring(input.length() - standardChars), new Locator.PressSequentiallyOptions().setDelay(delay).setTimeout(driver.actionWaitTimeout()));\n            }\n        }\n        return this;\n    }\n\n    @Override\n    public Element select(String text) {\n        // selectOption(String) matches by label OR value https://playwright.dev/java/docs/api/class-locator#locator-select-option\n        // My understanding of the Karate doc is that only the former should be checked.\n        if (!resolveLocator().selectOption(new SelectOption().setLabel(text)).isEmpty()) {\n            return this;\n        }\n        return missingElement();\n    }\n\n    @Override\n    public Element select(int index) {\n        if (!resolveLocator().selectOption(new SelectOption().setIndex(index)).isEmpty()) {\n            return this;\n        }\n        return missingElement();\n    }\n\n    @Override\n    public Element scroll() {\n        resolveLocator().scrollIntoViewIfNeeded(new ScrollIntoViewIfNeededOptions().setTimeout(driver.actionWaitTimeout()));\n        return this;\n    }\n\n    @Override\n    public void setValue(String value) {\n        resolveLocator().fill(value, new FillOptions().setTimeout(driver.actionWaitTimeout()));\n    }\n\n    @Override\n    public Element submit() {\n        InvocationHandler h = InvocationHandlers.submitHandler(this, driver.getWaitingForPage());\n        return elementProxy(h);\n    }\n\n    private Element elementProxy(InvocationHandler h) {\n        return (Element) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Element.class}, h);\n    }\n\n    @Override\n    public Mouse mouse() {\n        return new PlaywrightMouse(driver, token);\n    }\n\n    @Override\n    public Element switchFrame() {\n        driver.switchTo(this.resolveLocator());\n        return this;\n    }\n\n    @Override\n    public Element delay(int millis) {\n        driver.sleep(millis);\n        return this;\n    }\n\n    @Override\n    public Element retry() {\n        return retry(null, null);\n    }\n\n    @Override\n    public Element retry(int count) {\n        return retry(count, null);\n    }\n\n    @Override\n    public Element retry(Integer count, Integer interval) {\n        return elementProxy(InvocationHandlers.retryHandler(this, count, interval, driver.options));\n    }\n\n    @Override\n    public Element waitFor() {\n        resolveLocator().waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.VISIBLE).setTimeout(driver.waitTimeout()));\n        return this;\n    }\n\n    @Override\n    public Element waitUntil(String expression) {\n        driver.waitForFunction(expression, this.resolveLocator().elementHandle());\n        return this;\n    }\n\n    @Override\n    public Element waitForText(String text) {\n        resolveLocator().filter(new FilterOptions().setHasText(text)).waitFor(new WaitForOptions().setTimeout(driver.waitTimeout()));\n        return this;\n    }\n\n\n    @Override\n    public boolean isPresent() {\n        if (present == null) {\n            present = resolveLocator().isVisible();\n            // Per doc, isVisible does not wait and returns immediately, exactly what we need!\n        }\n        return present;\n    }\n\n\n    @Override\n    public Element optional(String locator) {\n        return token.child(locator).find(driver).orElseGet(() -> new MissingElement(driver, locator));\n    }\n\n    @Override\n    public boolean exists(String locator) {\n        return token.child(locator).find(driver).isPresent();        \n    }\n    \n\n    @Override\n    public Element locate(String locator) {\n        return token.child(locator).find(driver)\n            .orElseThrow(() -> new IllegalArgumentException(locator+\" not found\"));\n    }\n\n    @Override\n    public List<Element> locateAll(String locator) {\n        return token.child(locator).findAll(driver);\n    }\n\n    @Override\n    public Object script(String expression) {\n        return resolveLocator().evaluate(PlaywrightDriver.toJsExpression(expression));        \n    }\n\n    @Override\n    public List<Object> scriptAll(String locator, String expression) {\n        // element.script most likely convert expression so we will pay the conversion price for every item in the list.\n        // But this makes the code consistently working with Element. \n        return locateAll(locator).stream().map(element -> element.script(expression)).toList();\n    }\n\n    @Override\n    public String getHtml() {\n        return (String) script(\"_.outerHTML\");\n    }\n\n    @Override\n    public void setHtml(String html) {\n        resolveLocator().evaluate(\"el => el.innerHTML =\" + html);\n    }\n\n    @Override\n    public String getText() {\n        return resolveLocator().textContent();\n    }\n\n    @Override\n    public void setText(String text) {\n        resolveLocator().fill(text);\n    }\n\n    @Override\n    public String getValue() {\n        return resolveLocator().inputValue();\n    }\n\n    @Override\n    public String attribute(String name) {\n        return resolveLocator().getAttribute(name);\n    }\n\n    @Override\n    public String property(String name) {        \n        return Objects.toString(resolveLocator().elementHandle().getProperty(name));\n    }\n\n    @Override\n    public Element getParent() {\n        return token.child(\"xpath=..\").create(driver);\n    }\n\n    @Override\n    public Element getFirstChild() {\n        return token.child(\"nth=0\").create(driver);\n    }\n\n    @Override\n    public Element getLastChild() {\n        return token.child(\"nth=-1\").create(driver);\n    }\n\n    @Override\n    public Element getPreviousSibling() {\n        return token.child(\"/preceding-sibling\").create(driver);\n    }\n\n    @Override\n    public Element getNextSibling() {\n        return token.child(\"/following-sibling\").create(driver);\n    }\n\n    @Override\n    public List<Element> getChildren() {\n        return token.child(\"xpath=child::*\").findAll(driver);\n    }\n\n    @Override\n    public Finder rightOf() {\n        return new PlaywrightFinder(driver, token, \"right-of\");\n    }\n\n    @Override\n    public Finder leftOf() {\n        return new PlaywrightFinder(driver, token, \"left-of\");\n    }\n\n    @Override\n    public Finder above() {\n        return new PlaywrightFinder(driver, token, \"above\");\n    }\n\n    @Override\n    public Finder below() {\n        return new PlaywrightFinder(driver, token, \"below\");\n    }\n\n    @Override\n    public Finder near() {\n        return new PlaywrightFinder(driver, token, \"near\");\n    }\n\n    private MissingElement missingElement() {\n        return new MissingElement(driver, token.getPlaywrightToken());\n    }\n\n    private Locator resolveLocator() {\n        return token.resolveLocator();\n    }\n\n}\n"
  },
  {
    "path": "karate-playwright/src/main/java/com/intuit/karate/playwright/driver/PlaywrightFinder.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.playwright.driver;\n\nimport com.intuit.karate.driver.Element;\nimport com.intuit.karate.driver.Finder;\n\nimport java.lang.reflect.Proxy;\n\npublic class PlaywrightFinder implements Finder {\n\n    private final PlaywrightDriver driver;\n    private final PlaywrightToken token;\n    private final String type;\n\n    public PlaywrightFinder(PlaywrightDriver driver, PlaywrightToken token, String type) {\n        this.driver = driver;\n        this.token = token;\n        this.type = type;\n    }\n\n    @Override\n    public Element input(String value) {\n        return find(\"input\").input(value);\n    }\n\n    @Override\n    public Element select(String value) {\n        return find(\"select\").select(value);\n    }\n\n    @Override\n    public Element select(int index) {\n        return find(\"select\").select(index);\n    }\n\n    @Override\n    public Element click() {\n        return find().click();\n    }\n\n    @Override\n    public String getValue() {\n        return find().getValue();\n    }\n\n    @Override\n    public Element clear() {\n        return token.create(driver).clear();\n    }\n\n    @Override\n    public Element find() {\n        return find(\"input\");\n    }\n\n    @Override\n    public Element find(String tag) {\n        return token.friendlyLocator(type, tag).create(driver);\n    }\n\n    @Override\n    public Element highlight() {\n        return find().highlight();\n    }\n\n    @Override\n    public Element retry() {\n        return retry(null, null);\n    }\n\n    @Override\n    public Element retry(int count) {\n        return retry(count, null);\n    }\n\n    @Override\n    public Element retry(Integer count, Integer interval) {\n        PlaywrightElement element = token.create(driver);\n        return (Element) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Element.class}, InvocationHandlers.retryHandler(element, count, interval, driver.options));\n    }\n\n}\n"
  },
  {
    "path": "karate-playwright/src/main/java/com/intuit/karate/playwright/driver/PlaywrightMouse.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.playwright.driver;\n\nimport com.intuit.karate.driver.Mouse;\nimport com.microsoft.playwright.Locator;\n\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Proxy;\n\npublic class PlaywrightMouse implements Mouse {\n\n    private final PlaywrightDriver driver;\n    final PlaywrightToken token;\n    private Integer duration;\n\n    public PlaywrightMouse(PlaywrightDriver driver, PlaywrightToken token) {\n        this.driver = driver;\n        this.token = token;\n    }\n\n    @Override\n    public Mouse move(String locator) {\n        // IS hover exactly what move() is about?\n        resolveLocator().hover();\n        if (duration != null) {\n            pause(duration);\n        }\n        return this;\n    }\n\n    @Override\n    public Mouse move(Number x, Number y) {\n        getMouse().move(x.doubleValue(), y.doubleValue());\n        return this;\n    }\n\n    @Override\n    public Mouse offset(Number x, Number y) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Mouse down() {\n        getMouse().down();\n        return this;\n    }\n\n    @Override\n    public Mouse up() {\n        getMouse().up();\n        return this;\n    }\n\n    @Override\n    public Mouse submit() {\n        InvocationHandler h = InvocationHandlers.submitHandler(this, driver.getWaitingForPage());\n        return (Mouse) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Mouse.class}, h);\n    }\n\n    @Override\n    public Mouse click() {\n        resolveLocator().click();\n        return this;\n    }\n\n    @Override\n    public Mouse doubleClick() {\n        resolveLocator().dblclick();\n        return this;\n    }\n\n    @Override\n    public Mouse go() {\n        return this;\n    }\n\n    @Override\n    public Mouse duration(Integer duration) {\n        this.duration = duration;\n        return this;\n    }\n\n    @Override\n    public Mouse pause(Integer duration) {\n        driver.sleep(duration);\n        return this;\n    }\n\n    private com.microsoft.playwright.Mouse getMouse() {\n        return driver.getMouse();\n    }\n\n    private Locator resolveLocator() {\n        return token.resolveLocator();\n    }    \n}\n"
  },
  {
    "path": "karate-playwright/src/main/java/com/intuit/karate/playwright/driver/PlaywrightToken.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.playwright.driver;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\n\nimport com.intuit.karate.driver.Element;\nimport com.intuit.karate.playwright.driver.PlaywrightDriver.FrameTrait;\nimport com.intuit.karate.playwright.driver.util.KarateTokenParser;\nimport com.microsoft.playwright.Locator;\n\n/**\n * A (possibly chained) token, where a token is a selector or a locator, which\n * can be resolved into a {link com.microsoft.playwright.Locator}.\n *\n * \n * A {@link PlaywrightToken} provides two factory methods to create {@link PlaywrightElement}\n * {@link #create(PlaywrightDriver)}, {@link #find(PlaywrightDriver)}.\n * which should be used over calling the constructor.\n * \n * \n * It also provides a single place where:\n * - Karate's friendly and wildcard locators are handled.\n * - Playwright's strict mode is handled.\n * \n * Karate's friendly and wildcard locators will be automatically converted \n*  into Playwright compatible locators (xpath, css, pseudo or even possibly\n * custom locators).\n * \n * Playwright's strict mode is designed to fail when a {link com.microsoft.playwright.Locator} \n * references more than one element.\n * \n * A solution to this problem is typically to call locator.first() to resolve the conflict.\n * However, since Karate does not - and should not - expose such an API, {@link PlaywrightToken} \n * must be smart enough to do it automatically.\n * This is done in {@link #resolveLocator()} which is expected to be called whenever:\n * - an \"action\" (UI actions such as click(), scroll(), input(), or state actions such as getText(), getAttribute()) happens on the current locator\n * - or a sub locator is created through {@link #child(String)} or {@link #friendlyLocator(String, String)}.\n * \n * For example:\n * <pre>\n * Element first = driver.locate(\"foo\") // Not yet resolved\n * Element second = first.child(\"bar\")  // first resolved, second not yet resolved\n * second.click(); // resolved\n * driver.click(\"bar\"); // an internal Element is created, similar to first, and resolved when click is called.\n * </pre>\n * \n *                      \n * Note that there is another solution to resolve the conflict, which is to call locator.all() to get a list of all the matching elements.\n * {@link PlaywrightToken} also supports this through the {@link #findAll(PlaywrightDriver)} method.\n *  \n * */\npublic class PlaywrightToken {\n\n    private final Locator locator;\n\n    PlaywrightToken(Locator root) {\n        this.locator = Objects.requireNonNull(root);\n    }\n\n    public String getPlaywrightToken() {\n        return locator.toString();\n    }\n\n    public String toString() {\n        return locator.toString();\n    }\n\n    public static PlaywrightToken root(FrameTrait root, String token) {\n        return of(root.locator(KarateTokenParser.toPlaywrightToken(token)));\n    }\n\n    public static PlaywrightToken of(Locator locator) {\n        return new PlaywrightToken(locator);\n    }\n\n    public PlaywrightToken child(String karateToken) {\n        String newToken = KarateTokenParser.toPlaywrightToken(karateToken);\n        return new PlaywrightToken(resolveLocator().locator(newToken));\n    }\n\n    public PlaywrightToken friendlyLocator(String type, String token) {\n        // friendly is registered as a customer selector in PlaywrightDriver\n        return new PlaywrightToken(resolveLocator().locator(\"friendly=\" + type.replace(\"-of\", \"\") + \":\" + KarateTokenParser.toPlaywrightToken(token)));\n        // alternative implementation, using PW's native locators, which unfortunately does not seem to support xpath tokens.\n        // Note that it would require KarateTokenParser to use toPplaywrightToken.\n        // return new PlaywrightElement(driver, token.wrap(pwLoc -> KarateTokenParser.toPlaywrightToken(tag)+\":\"+type+\"(\"+ pwLoc+\")\").first());\n\n    }\n\n    public Locator resolveLocator() {\n        return locator.first();\n    }\n\n    Optional<Element> find(PlaywrightDriver driver) {\n        // Per doc, isVisible does not wait and returns immediately, exactly what we need!    \n        // Also, isPresent implemented using isVisible() seems to be the general view per https://stackoverflow.com/questions/64784781/how-to-check-if-an-element-exists-on-the-page-in-playwright-js\n        // although, if isAttached was available, it probably would have made more sense.            \n        if (locator.count() > 0) {\n            // Make locator the new root. Note that any Element located is unambigously \"resolved\" by using the first locator.\n            return Optional.of(new PlaywrightElement(driver, of(locator), true));\n        }\n        return Optional.empty();    \n    }\n\n    List<Element> findAll(PlaywrightDriver driver) {\n        List<Locator> locators = locator.all();\n        List<Element> elements = new ArrayList<>(locators.size());\n        for (Locator locator: locators) {\n            elements.add(new PlaywrightElement(driver, of(locator), true));\n        }\n        return elements;        \n    }\n\n    /**\n     * Returns an Element, which may exist or not.\n     *\n     * If it does not exist, next call to click, text, or any action or state method, will fail.\n     * {@link Element#isPresent()}, however, will never fail and may be used to check the existence of the element. \n     * @param driver\n     * @return\n     */\n    PlaywrightElement create(PlaywrightDriver driver) {\n        return new PlaywrightElement(driver, of(locator));\n    }\n}\n"
  },
  {
    "path": "karate-playwright/src/main/java/com/intuit/karate/playwright/driver/util/KarateTokenParser.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2023 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.playwright.driver.util;\n\nimport java.util.Optional;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class KarateTokenParser {\n\n    private static final Pattern WILDCARD_LOCATOR_PATTERN = Pattern.compile(\"\\\\{(\\\\^)?([a-zA-Z*/]+)?(:\\\\d+)?\\\\}(.+)\");\n\n    public static String parse(String karateToken, KarateTokenParserListener listener) {\n        Matcher matcher = WILDCARD_LOCATOR_PATTERN.matcher(karateToken);\n        if (matcher.find()) {\n            boolean isContain = matcher.group(1) != null;\n            String tag = matcher.group(2);\n            String indexStr = matcher.group(3);\n            String text = matcher.group(4);\n\n            return listener.onText(isContain, text, Optional.ofNullable(tag), Optional.ofNullable(indexStr).map(s -> Integer.parseInt(s.substring(1) /* remove the initial : */)));\n        } else {\n            return karateToken.startsWith(\"./\") ? \"xpath=\" + karateToken : karateToken;\n        }\n    }\n\n    public static String toPlaywrightToken(String karateToken) {\n        return parse(karateToken, KarateTokenParser::toXPathToken);\n    }\n\n    public static interface KarateTokenParserListener {\n\n        String onText(boolean isContain, String text, Optional<String> tag, Optional<Integer> index);\n    }\n\n// Example of alternative implementation\n    // private static String toPlaywrightToken(boolean isContain, String text, Optional<String> tag, Optional<Integer> index) {\n    //     String token = tag.orElse(\"*\") + \":\"+ (isContain ? \"text\" : \"text-is\") + \"('\" + text + \"')\";\n    //     return index.map(idx -> \":nth-match(\"+token+\",\"+idx+\")\").orElse(token);\n    // }\n    private static String toXPathToken(boolean isContain, String text, Optional<String> tag, Optional<Integer> index) {\n        String token = \"//\" + tag.orElse(\"*\") + (isContain ? \"[contains(normalize-space(text()),'\" + text + \"')]\" : \"[normalize-space(text())='\" + text + \"']\");\n        return index.map(idx -> \"/(\" + token + \")[\" + idx + \"]\").orElse(token);\n    }\n\n}\n"
  },
  {
    "path": "karate-playwright/src/test/java/com/intuit/karate/playwright/driver/PlaywrightDriverTest.java",
    "content": "package com.intuit.karate.playwright.driver;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mockito;\n\nimport com.intuit.karate.core.Config;\nimport com.intuit.karate.core.ScenarioEngine;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.driver.Driver;\nimport com.microsoft.playwright.TimeoutError;\n\n/** Run actions and waits with global /specific retry settings and measure \n* Almost all tests (the one exception being {@link #actionWithWait}) within this class are configured with 2 retries of 200ms each, so 0.4s, but the div takes 2.5 to be visible, so they are expected to timeout.\n* However, this will validate that the specified settings are correctly taken into account.\n*\n* Compare that with a test configured with 3 * 1500, it will pass and will take 2.5s but we can't tell if the settings were used or if it's just PW's autowait returning as soon as the div is visible.\n* Put it this way: the test would stil pass if the settings were not used and we used PW's default timeout of 30s.\n*/\npublic class PlaywrightDriverTest {\n\n    private static final int RETRY_COUNT = 2;\n    private static final int RETRY_INTERVAL = 300;\n\n    private static Driver driver;\n    private static ScenarioEngine scenarioEngine;\n    private static Config scenarioEngineConfig;\n\n    @BeforeAll\n    public static void beforeAll() {\n        ScenarioRuntime sr = Mockito.mock(ScenarioRuntime.class, Mockito.RETURNS_DEEP_STUBS);\n        Map<String, Object> options = new LinkedHashMap<>();        \n        options.put(\"userDataDir\", \"target\");\n        options.put(\"headless\", true);\n        Map<String, Object> playwrightOptions = new LinkedHashMap<>();\n        options.put(\"playwrightOptions\", playwrightOptions);\n        playwrightOptions.put(\"installBrowsers\", false);\n        playwrightOptions.put(\"channel\", \"chrome\");\n\n        driver = PlaywrightDriver.start(options, sr);\n    }\n\n    @AfterAll\n    public static void cleanup() {\n        if (driver != null){\n            driver.close();\n        }\n    }\n\n    // @Test\n    void actionWithRetry() {\n\n        driver.setUrl(\"file://\"+System.getProperty(\"user.dir\")+\"/src/test/resources/html/02.html\");\n \n        TimeoutError te = assertThrows(TimeoutError.class, () ->\n                driver.retry(RETRY_COUNT, RETRY_INTERVAL).click(\"#slowDiv\"));\n\n        assertTrue(te.getMessage().contains(\"Timeout 600ms exceeded\"));\n    }\n\n\n    // @Test\n    void waitForWithRetry() {\n\n        driver.setUrl(\"file://\"+System.getProperty(\"user.dir\")+\"/src/test/resources/html/02.html\");\n\n        TimeoutError te = assertThrows(TimeoutError.class, () ->\n                driver.retry(RETRY_COUNT, RETRY_INTERVAL).waitFor(\"#slowDiv\"));\n\n        assertTrue(te.getMessage().contains(\"Timeout 600ms exceeded\"));\n    }\n\n    // make sure that waitFor() and click() are consistent with each other.\n    // Previous implementation of waitFor was based on State.ATTACHED, so would return as soon as the element is attached in the DOM.\n    // click(), on the other end, would autowait until the element is VISIBLE https://playwright.dev/docs/actionability\n    // Since actions' timeout is 100ms, if it takes more than 100 ms for the element to transition from ATTACHED to VISIBLE, click() would timeout.\n    // See issue in #2291.\n    // @Test\n    void actionWithWait() {\n        // Note this page is set up so that the element is visible 500 ms (> 100 ms) after it was attached.\n        driver.setUrl(\"file://\"+System.getProperty(\"user.dir\")+\"/src/test/resources/html/02.html\");\n        driver.waitFor(\"#slowDiv\")\n                .click();           \n    }    \n\n    // @Nested\n    class GlobalRetryTest {\n        \n        @BeforeAll\n        public static void beforeAll() {\n            scenarioEngineConfig = new Config();\n            scenarioEngineConfig.setRetryCount(RETRY_COUNT);\n            scenarioEngineConfig.setRetryInterval(RETRY_INTERVAL);\n            scenarioEngine = Mockito.mock(ScenarioEngine.class);\n            Mockito.when(scenarioEngine.getConfig()).thenReturn(scenarioEngineConfig);\n\n            ScenarioEngine.set(scenarioEngine);\n        }\n\n        // Per doc and other driver implementations, global retry count is not taken into account for actions, but retry interval is (and defines the wait timeout in PW).\n        // @Test\n        void actionWithGlobalRetry() {\n\n            // global retry/\n            driver.setUrl(\"file://\"+System.getProperty(\"user.dir\")+\"/src/test/resources/html/02.html\");\n\n            TimeoutError te = assertThrows(TimeoutError.class, () -> driver.click(\"#slowDiv\"));\n            assertTrue(te.getMessage().contains(\"Timeout \"+RETRY_INTERVAL+\"ms exceeded\"), te.getMessage());\n        }\n\n\n        // @Test\n        void waitForWithGlobalRetry() {\n\n            driver.setUrl(\"file://\"+System.getProperty(\"user.dir\")+\"/src/test/resources/html/02.html\");\n\n            TimeoutError te = assertThrows(TimeoutError.class, () -> driver.waitFor(\"#slowDiv\"));\n            assertTrue(te.getMessage().contains(\"Timeout 600ms exceeded\"), te.getMessage());\n        }\n\n    } \n\n}"
  },
  {
    "path": "karate-playwright/src/test/java/com/intuit/karate/playwright/driver/PlaywrightElementTest.java",
    "content": "package com.intuit.karate.playwright.driver;\n\nimport com.intuit.karate.driver.Key;\nimport com.microsoft.playwright.Locator;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentMatcher;\nimport org.mockito.Mockito;\n\nimport static org.mockito.ArgumentMatchers.*;\nimport static org.mockito.Mockito.when;\n\nclass PlaywrightElementTest {\n\n    private Locator locator;\n    private PlaywrightDriver driver;\n    private PlaywrightElement element;\n\n    @BeforeEach\n    void beforeEach() {\n        locator = Mockito.mock(Locator.class);\n        Locator rootLocator = Mockito.mock(Locator.class);        \n        when(rootLocator.first()).thenReturn(locator);        \n        driver = Mockito.mock(PlaywrightDriver.class);\n        PlaywrightToken token = new PlaywrightToken(rootLocator);\n        element = new PlaywrightElement(driver, token);\n    }\n\n    // @Test\n    void textEnter() {\n        element.input(\"Karate\" + Key.INSTANCE.ENTER);\n        Mockito.verify(locator).pressSequentially(eq(\"Karate\"), argThat(matchesDelay(0)));\n        Mockito.verify(locator).press(Mockito.eq(\"Enter\"), any());\n    }\n\n    // @Test\n    void shiftTextEnter() {\n        element.input(Key.INSTANCE.SHIFT + \"karate\" + Key.INSTANCE.ENTER);\n        Mockito.verify(locator).press(eq(\"Shift+k\"), any());\n        Mockito.verify(locator).pressSequentially(eq(\"arate\"), argThat(matchesDelay(0)));\n        Mockito.verify(locator).press(eq(\"Enter\"), any());\n    }\n\n    // @Test\n    void ctrlShiftText() {\n        element.input(new StringBuilder().append(Key.INSTANCE.CONTROL).append(Key.INSTANCE.SHIFT).append(\"karate\").toString());\n        Mockito.verify(locator).press(eq(\"Control+Shift+k\"), any());\n        Mockito.verify(locator).pressSequentially(eq(\"arate\"), argThat(matchesDelay(0)));\n    }\n\n    // @Test\n    void withDelay() {\n        element.input(new String[]{\"Input\", \"Karate\"}, 10);\n        Mockito.verify(locator).pressSequentially(eq(\"Input\"), argThat(matchesDelay(10)));\n        Mockito.verify(locator).pressSequentially(eq(\"Karate\"), argThat(matchesDelay(10)));\n\n    }\n\n    private ArgumentMatcher<Locator.PressSequentiallyOptions> matchesDelay(int delay) {\n        return options -> delay == options.delay.intValue();\n    }\n\n}\n"
  },
  {
    "path": "karate-playwright/src/test/resources/html/02.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page 02</title>\n    <script src=\"00.js\"></script>\n    <link rel=\"stylesheet\" href=\"00.css\" type=\"text/css\"/>  \n    <link rel=\"icon\" href=\"00.ico\">\n    <script>\n      setTimeout(function () {\n        let slowDiv = document.createElement('div');\n        slowDiv.id = 'slowDiv';\n        slowDiv.style.display = 'none'\n        slowDiv.appendChild(document.createTextNode('APPEARED!'));\n        document.getElementById('containerDiv').appendChild(slowDiv);\n        setTimeout(function() {\n          slowDiv.style.display = 'block'  \n        }, 1500)\n      }, 500)\n    </script>\n  </head>\n  <body>\n    <p>this test will wait for a slow loading element</p>\n    <div id=\"containerDiv\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "karate-robot/README.md",
    "content": "# Karate Robot\n\n## Desktop Automation Made `Simple.`\n\n# Index\n\n<table>\n<tr>\n  <th>Start</th>\n  <td>\n      <a href=\"#maven\">Maven</a>\n    | <a href=\"https://github.com/karatelabs/karate/wiki/Karate-Robot-Windows-Install-Guide\">Windows Install Guide</a>\n    | <a href=\"#jbang\">jbang</a>\n    | <a href=\"#debugging\">Debugging</a>\n    | <a href=\"https://github.com/karatelabs/karate#index\">Karate - Main Index</a>\n  </td>\n</tr>\n<tr>\n  <th>Config</th>\n  <td>\n      <a href=\"#robot\"><code>driver</code></a>\n    | <a href=\"#robot-options\"><code>robot</code> options</a>\n  </td>\n</tr>\n<tr>\n  <th>Concepts</th>\n  <td>\n      <a href=\"#methods\">Methods</a>\n    | <a href=\"#element-api\"><code>Element</code></a>\n    | <a href=\"#window-api\"><code>Window</code></a>\n    | <a href=\"#finding-windows\">Finding Windows</a>\n    | <a href=\"https://github.com/karatelabs/karate/wiki/Karate-Robot-Windows-Install-Guide#debug-mode\">Debugging</a>\n    | <a href=\"#retry\">Retries</a>\n    | <a href=\"#karatefork\"><code>karate.fork()</code></a>\n    | <a href=\"#utility-functions\">Utility Functions</a>\n    | <a href=\"#conditional-start\">Conditional Start</a>\n  </td>\n</tr>\n<tr>\n  <th>Locators</th>\n  <td>\n      <a href=\"#windows-locators\">Windows Locators</a>\n    | <a href=\"#image-locators\">Image Locators</a> \n    | <a href=\"#ocr-locators\">OCR Locators</a>\n  </td>\n</tr>\n<tr>\n  <th>App</th>\n  <td>\n      <a href=\"#window\"><code>window()</code></a>    \n    | <a href=\"#windowexists\"><code>windowExists()</code></a> \n    | <a href=\"#windowoptional\"><code>windowOptional()</code></a> \n    | <a href=\"#waitforwindowoptional\"><code>waitForWindowOptional()</code></a> \n    | <a href=\"#robotroot\"><code>robot.root</code></a> \n    | <a href=\"#robotactive\"><code>robot.active</code></a>   \n    | <a href=\"#robotfocused\"><code>robot.focused</code></a>\n    | <a href=\"#robotlocation\"><code>robot.location</code></a>\n    | <a href=\"#robotregion\"><code>robot.region()</code></a>\n    | <a href=\"#robotclipboard\"><code>robot.clipboard</code></a>\n    | <a href=\"#robotallwindows\"><code>robot.allWindows</code></a>\n    | <a href=\"#screenshot\"><code>screenshot()</code></a>  \n    | <a href=\"#screenshotactive\"><code>screenshotActive()</code></a>    \n  </td>\n</tr>\n<tr>\n  <th>Actions</th>\n  <td>\n      <a href=\"#click\"><code>click()</code></a>\n    | <a href=\"#doubleclick\"><code>doubleClick()</code></a>\n    | <a href=\"#rightclick\"><code>rightClick()</code></a>\n    | <a href=\"#move\"><code>move()</code></a>\n    | <a href=\"#press\"><code>press()</code></a>\n    | <a href=\"#release\"><code>release()</code></a>\n    | <a href=\"#input\"><code>input()</code></a>\n    | <a href=\"#focus\"><code>focus()</code></a>\n    | <a href=\"#select\"><code>select()</code></a>\n    | <a href=\"#highlight\"><code>highlight()</code></a>\n    | <a href=\"#highlightall\"><code>highlightAll()</code></a>\n    | <a href=\"#scroll\"><code>scroll()</code></a>\n  </td>\n</tr>\n<tr>\n  <th>State</th>\n  <td>\n      <a href=\"#exists\"><code>exists()</code></a>\n    | <a href=\"#optional\"><code>optional()</code></a> \n    | <a href=\"#waitforoptional\"><code>waitForOptional()</code></a>    \n    | <a href=\"#locate\"><code>locate()</code></a>    \n    | <a href=\"#locateall\"><code>locateAll()</code></a>\n  </td>\n</tr>\n<tr>\n  <th>Retry / Wait</th>\n  <td>\n      <a href=\"#retry\"><code>retry()</code></a>\n    | <a href=\"#waitfor\"><code>waitFor()</code></a>\n    | <a href=\"#waituntil\"><code>waitUntil()</code></a>    \n    | <a href=\"#delay\"><code>delay()</code></a>\n  </td>\n</tr>\n</table>\n\n## Capabilities\n* Available as a standalone binary\n* Native Mouse Events\n* Native Keyboard Events\n* Windows object-recognition using [Microsoft UI Automation](https://docs.microsoft.com/en-us/windows/win32/winauto/entry-uiauto-win32)\n* [Navigation via image detection](#image-locators) - cross-platform (mac, win, linux) via [JavaCPP and OpenCV](https://github.com/bytedeco/javacpp-presets/tree/master/opencv)\n* [OCR driven navigation](#ocr-locators) and text extraction - cross-platform (mac, win, linux) via [JavaCPP and Tesseract](https://github.com/bytedeco/javacpp-presets/tree/master/tesseract)\n* Tightly integrated into [Karate](https://github.com/karatelabs/karate) - which means a [debugger, HTML reports](#debugging), and more\n\n### Demo Videos\n* Clicking the *native* \"File Upload\" button in a Web Page - [Link](https://twitter.com/ptrthomas/status/1253373486384295936)\n  * details, code and explanation [here](https://stackoverflow.com/a/61393515/143475)\n* Clicking a button in an iOS Mobile Emulator - [Link](https://twitter.com/ptrthomas/status/1217479362666041344)\n* Windows automation by natively accessing UI controls and the window / object tree - [Link](https://twitter.com/ptrthomas/status/1261183808985948160)\n\n## Examples\n* Refer to the [`examples/robot-test`](../examples/robot-test) project which is a stand-alone Maven project that can be used as a starting point\n* Opening a browser tab and performing actions - [Link](src/test/java/robot/core/chrome.feature)\n\n## Using\nIf you are not that experienced with programming - or don't want to set up a Java development environment, please look at the [standalone JAR](https://github.com/karatelabs/karate/wiki/ZIP-Release#karate-robot) which you can run using [Visual Studio Code](https://github.com/karatelabs/karate/wiki/IDE-Support#visual-studio-code).\n\nMaven (or Gradle) users can read on below. Make sure you follow the [Karate conventions](https://github.com/karatelabs/karate#folder-structure) and you can use the [`examples/robot-test`](../examples/robot-test) project as a template.\n\nThe `karate-robot` capabilities are not part of the `karate-core`, because they bring in a few extra dependencies.\n\n### Maven\nAdd this to the `<dependencies>`:\n\n```xml\n    <dependency>\n        <groupId>com.intuit.karate</groupId>\n        <artifactId>karate-robot</artifactId>\n        <version>${karate.version}</version>\n        <scope>test</scope>\n    </dependency> \n```\n\nThis may result in a few large JAR files getting downloaded by default because of the [`javacpp-presets`](https://github.com/bytedeco/javacpp-presets) dependency. But you can narrow down to what is sufficient for your OS by [following these instructions](https://github.com/bytedeco/javacpp-presets/wiki/Reducing-the-Number-of-Dependencies).\n\n## jbang\nThis is an interesting option to create scripts using the underlying Java API directly and even make them local executables. Refer to the [main documentation](https://github.com/karatelabs/karate#java-api) for more.\n\nNote that starting an instance of the Windows Robot using Java is easy, just call the static `start(Map)` method on the [`WinRobot`](src/main/java/com/intuit/karate/robot/win/WinRobot.java) class.\n\n## Debugging\nThis is one of the highlights of Karate's capabilities. You can see a video of it in action [here](https://twitter.com/ptrthomas/status/1261183808985948160).\n\nRefer to the documentation on how to set it up and use it: [Karate Robot Windows Install Guide](https://github.com/karatelabs/karate/wiki/Karate-Robot-Windows-Install-Guide#install-visual-studio-code).\n\n## `robot`\nKarate Robot is designed to only activate when you use the `robot` keyword, and if the `karate-robot` Java / JAR dependency is present in the project classpath.\n\nHere Karate will look for an application window called `Chrome` and will \"focus\" it so that it becomes the top-most window, and be visible. This will work on Mac, Windows and Linux (X Window System / X11).\n\n```cucumber\n* robot { window: 'Chrome' }\n```\n\nIn development mode, you can switch on a red highlight border around areas that Karate finds via image matching. Note that the `^` prefix means that Karate will look for a window where the name *contains* `Chrome`.\n\n```cucumber\n* robot { window: '^Chrome', highlight: true }\n```\n\nYou can use `fork` to run a console command to start an application if needed, before \"activating\" it. Also see [`karate.fork()`](#karatefork)\n\n> If you want to do conditional logic depending on the OS, you can use [`karate.os`](https://github.com/karatelabs/karate#karate-os) - for e.g. `* if (karate.os.type == 'windows') karate.set('filename', 'start.bat')`\n \n\n### `robot` options\n\nThe keys that the `robot` keyword supports are the following:\n\nkey | description\n--- | -----------\n`window` | (optional) the name of the window to bring to focus, and you can use a `^` prefix to do a string \"contains\" match or `~` for a regular-expression match, also see [`window()`](#window)\n`fork` | (optional) calls an OS executable and takes a string (e.g. `'some.exe -h'`), string-array (e.g. `['some.exe', '-h']`) or JSON as per [`karate.fork()`](https://github.com/karatelabs/karate#karate-fork)\n`autoClose` | default `true` - to close the current window if fork was used on startup \n`attach` | defult `true` - if the `window` exists, `fork` will not be executed\n`basePath` | defaults to `null`, which means the \"find by image\" search will be relative to the \"entry point\" feature file, but can be used to point to [prefixed / relative paths](https://github.com/karatelabs/karate#reading-files) such as `classpath:some/folder`\n`highlight` | default `false` if an element (or image) match should be highlighted\n`highlightDuration` | default `3000` - time to `highlight` in milliseconds\n`retryCount` | default [normally `3`](https://github.com/karatelabs/karate#retry-until) - overrides the default [`retry()`](#retry) count, this applies only for finding the `window` *after* a `fork` was executed \n`retryInterval` | default [normally `3000`](https://github.com/karatelabs/karate#retry-until) - overrides the default [`retry()`](#retry) interval, this applies only for finding the `window` *after* a `fork` was executed \n`autoDelay` | default `0` - time delay added (in milliseconds) after a native action (key press, mouse click), you can set this to a small value e.g. `40` only in case of any issues with OS actions being too fast, etc\n`tessData` | default `tessdata` - the path to a directory where the Tesseract (OCR engine) [data files](#ocr-locators) will be looked for, this is needed only if you use an [OCR Locator](#ocr-locators) or attempt to call [`Element.extract()`](#elementextract). Note that the default *value* \"`tessdata`\" is all lower-case.\n`tessLang` | default `eng` - the default OCR language to use, see [OCR Locator](#ocr-locators)\n\n### `configure robot`\nFor convenience, the same pattern in [Karate UI](https://github.com/karatelabs/karate/tree/master/karate-core#configure-driver) is supported, where you can have a \"central\" config, perhaps set-up in [`karate-config.js`](https://github.com/karatelabs/karate#configuration) - and have your tests specify the \"intent\" (or even over-ride \"global\" config) more clearly:\n\n```cucumber\n* configure robot = { highlight: true }\n# and then later\n* robot { window: '^My App' }\n# or even\n* robot '^My App'\n```\n\n### `karate.fork()`\nThe `fork` option simply calls [`karate.fork()`](https://github.com/karatelabs/karate#karate-fork) which means that you can use it directly within a test any time you want to start any OS process. This is convenient to implement conditional logic, for e.g. to start an application involving a *different* main window - if a certain window [does not exist](#windowexists).\n\nHere's an example using [`karate.call()`](https://github.com/karatelabs/karate#call-vs-read):\n\n```cucumber\n* robot { highlight: true, highlightDuration: 500, autoClose: false }\n* if (!windowExists('^Main Window')) karate.call('sign-in.feature')\n```\n\nAnd `sign-in.feature` looks like this. This example code below also showcases a few Karate capabilities extremely relevant for testing GUI-s such as [`retry()`](#retry) and [`waitFor()`](#waitfor).\n\n```cucumber\n@ignore\nFeature:\n\nScenario:\n* karate.fork('C:/MyDir/my.exe')\n* retry(5).window('Sign In')\n* waitFor('#userid').input(testUser)\n* input('#password', testPassword)\n* click('#submit-btn')\n```\n\nAlso see [Conditional Start](#conditional-start) which is a more advanced version of the above flow, when the \"Sign In\" window title is different.\n\nNote how you can [inject variables from global config](https://github.com/karatelabs/karate#karate-configjs) e.g. `testUser` and `testPassword` using Karate.\n\n### Finding Windows\nFinding Windows and dialogs is a critical aspect of UI automation and Karate makes easy the process of handling even dynamic Window titles or un-predictable Windows.\n\nHere's a typical situation with some challenges, and the script that solves them:\n\n* if the app is already running, don't start it\n* the window title is un-predictable, it can be \"MyApp\" or \"MYAPP\"\n* the app takes almost 20 seconds to start\n* after the application starts, a modal dialog with the title \"Tips on Startup\" may or may not appear\n\n```cucumber\n* def windowName = '~MyApp|MYAPP'\n* robot { window: '#(windowName)', fork: 'C:/Program Files (x86)/MyApp/myapp.exe', retryCount: 10 }\n* windowOptional('Tips on Startup').locate('Close').click()\n* window(windowName)\n```\n\nExplanation:\n* `robot { window: '<name>' }` will not call [`fork`](#karatefork) if the window was found to be already present\n* the `~` prefix means that Karate will use a regex (regular expression) match to find the window by title\n* `retryCount: 10` means that if `fork` was executed, Karate will wait `10 x 3000` milliseconds where `3000` is the default [`retryDuration`](#retry)\n* [`windowOptional()`](#windowoptional) will do nothing if the window does not exist\n* note how the variable `windowName` can be used as an [embedded expression](https://github.com/karatelabs/karate#embedded-expressions) or directly when within \"round brackets\", e.g. [`window(windowName)`](#window)\n* the last line makes sure that we switch back to the main window and make it [\"active\"](#robotactive)\n\n# API\nPlease refer to the available methods in [`Robot.java`](src/main/java/com/intuit/karate/robot/Robot.java). Most of them are \"chainable\". The built-in `robot` JS object is where you script UI automation. It will be initialized only after the [`robot`](#robot) keyword has been used to start / attach to a desktop window.\n\n## `Element` API\nAny method on the [`Robot`](#api) type that returns [`Element`](src/main/java/com/intuit/karate/robot/Element.java) can be chained for convenience. Here is an example:\n\n```cucumber\n* locate('Taxpayer').click(20, 40)\n```\n\nThis [locates](#windows-locators) a UI control by name, and then within the bounds of that element, proceeds to click the mouse at an inner offset of 20 pixels(horizontal) and 40 pixels (vertical) from the top-left corner of the element.\n\nAlso see [`windowOptional()`](#windowoptional) for a good example of chaining a [`click()`](#click) after calling [`locate()`](#locate).\n\n### Tree Walking\nThe following *properties* (Java getters) are available on an [`Element`](#element-api) instance:\n\n* `parent`\n* `children` (returns a list / array of `Element`-s)\n* `firstChild`\n* `lastChild`\n* `nextSibling`\n* `previousSibling`\n\nThis is convenient in some cases, for example:\n\n```cucumber\n* locate('SomeName').parent.click('Close')\n* waitFor('//pane{Info}').children[3].click()\n\n```\n\n## `Window` API\nA call to [`window()`](#window) will set the current or \"active\" window and also return an object of type [`Window`](src/main/java/com/intuit/karate/robot/Window.java) (which extends [`Element`](#element-api)). So to set the window and `restore()` it in one step you could do this:\n\n```cucumber\n* window('^Tax Organizer').restore()\n```\n\n## Methods\nAs a convenience, *all* the methods on the `robot` have been injected into the context as special (JavaScript) variables so you can omit the \"`robot.`\" part and save a lot of typing. For example instead of:\n\n```cucumber\n* robot { window: '^Chrome', highlight: true }\n* robot.input(Key.META + 't')\n* robot.input('karate dsl' + Key.ENTER)\n* robot.click('tams.png')\n```\n\nYou can shorten all that to:\n\n```cucumber\n* robot { window: '^Chrome', highlight: true }\n* input(Key.META + 't')\n* input('karate dsl' + Key.ENTER)\n* click('tams.png')\n```\n\nThe above flow performs the following operations:\n* finds an already open window where the name contains \"Chrome\"\n  * note that on Windows you may need to use \"New Tab\" instead\n* enables \"highlight\" mode for ease of development / troubleshooting\n* triggers keyboard events for [COMMAND + t] which will open a new browser tab\n  * on Windows this should be `Key.CONTROL` instead\n* triggers keyboard events for the input \"karate dsl\" and an ENTER key-press\n* waits for a section of the screen defined by [`tams.png`](src/test/java/robot/core/tams.png) to appear - and clicks in the center of that region\n  * Karate will try to use different scaling factors for an image match, for best results - try to use images that are the same resolution (or as close) as the desktop resolution\n  * if you run into issues, try re-taking a PNG capture of the area to click-on\n\nAlso see [Image Locators](#image-locators)\n\n## `Key`\nJust [like Karate UI](https://github.com/karatelabs/karate/tree/master/karate-core#special-keys), the special keys are made available under the namespace `Key`. You can see all the available codes [here](https://github.com/karatelabs/karate/blob/master/karate-core/src/main/java/com/intuit/karate/driver/Key.java).\n\n```cucumber\n* input('karate dsl' + Key.ENTER)\n```\n\n## `robot.basePath`\nRarely used since `basePath` would typically be set by the [`robot` options](#robot). But you can do this any time during a test to \"switch\". Note that [`classpath:`](https://github.com/karatelabs/karate#classpath) would [typically resolve](https://github.com/karatelabs/karate#folder-structure) to `src/test/java`.\n\n```cucumber\n* robot.basePath = 'classpath:some/package'\n```\n\n## Image Locators\nImages have to be in PNG format, and with the extension `*.png`. Karate will attempt to find images that are smaller or larger to a certain extent. But for the best results, try to save images that are the same resolution as the application under test. Also see [`robot.basePath`](#robotbasepath)\n\n```cucumber\n* click('someimage.png')\n```\n\nSo any string that ends with `.png` will be treated as an \"image locator\".\n\n### Image Match Strictness\nYou can optionally prefix a number and `:` to the image path like this:\n\n```cucumber\n* click('5:someimage.png')\n```\n\nThis number is a \"strictness\" factor, 1 for being the most strict and 10 (the default) for \"normal\". As of now, consider this experimental while we try to arrive at the values that will work for most real-life situations.\n\nIn case you find it really hard to get a \"match\", you can try providing values greater than 10 which means Karate will look for more \"lenient\" matches.\n\nTip: use the [debugger](#debugging) and [`highlight()`](#highlight) or [`highlightAll()`](#highlightall) to troubleshoot image matching.\n\n## OCR Locators\nAny string that starts with the `{lang}` pattern will be treated as an OCR locator. \n\nKarate uses the [Tesseract](https://tesseract-ocr.github.io) OCR engine (v4.X). You will need to acquire [data files](https://tesseract-ocr.github.io/tessdoc/Data-Files.html) for the language of your choice, e.g. English (`eng`). You can choose between the options \"tessdata\", \"tessdata-fast\" and \"tessdata-best\" depending on the quality vs speed (and data-file size) compromise you are willing to make. So for example here is the English data file for \"tessdata-best\": [link](https://github.com/tesseract-ocr/tessdata_best/blob/master/eng.traineddata). You can download it and make it available in a directory called \"tessdata\" in the root directory of the project you are working in. To change the \"tessdata\" location, look at the `tessData` [configuration option](#robot-options).\n\nSo to find the text \"Click Me\" and click on it:\n\n```cucumber\n* click('{eng}Click Me')\n```\n\nA variation is that if the language-key is prefixed with a `-`, the screen or element-region capture will be converted to a \"negative\" before OCR processing. This is useful in cases the text is in a light font against a dark background.\n\n```\n* click('{-eng}Dark Mode')\n```\n\nYou can omit the language in which case the `tessLang` [configuration option](#robot-options) will be used:\n\n```\n* click('{}Some Text')\n```\n\n### `Element.extract()`\nThe [`Element`](#element-api) has an `extract()` method which can scrape out the text via OCR from the bounds of an Element position on the screen. Results may vary and include line-breaks and white-space, but you may be able to pull-off some string-contains comparisons:\n\n```cucumber\n* match locate('Some Pane').extract('eng') contains 'Search Results'\n```\n\nIf you don't pass the language-key to the `extract()` method like you see `eng` above, the default `tessLang` [configured](#robot-options) will be used:\n\n```cucumber\n* def text = locate('Some Pane').extract()\n```\n\nTo extract the text from the whole screen (desktop), you can do this via the [`robot.root`](#robotroot) API:\n\n```cucumber\n* def text = robot.root.extract()\n```\n\n### `Element.debugExtract()`\nFor debugging and troubleshooting, there is an [`Element.debugExtract()`](#element-api) API. This will highlight all the words found within the given `Element`. This is super-useful during a [step-through debugger session](#debugging).\n\n## Windows Locators\nPrefixing with a `#` means using the \"Automation ID\" which may or may not be available depending on the application under test. And finding by \"name\" is the default, if the first character is not `/` or `#`. As a convenience, you can use the `^` prefix for a name \"contains\" match and `~` for a name regular-expression match.\n\nBut the most useful locator strategy is an XPath-like one. While it does not support all the extensions and functions in proper XPath, it is designed to make selecting elements super-easy and for improved performance, you can \"scope\" to parent element / paths and make these selectors robust.\n\nHere are examples:\n\nLocator | Description\n------- | -----------\n`click('Click Me')` | the first control (any type) where the name is *exactly*: \"Click Me\"\n`click('//*{Click Me}')` | the \"long-form\" of the above. Try to use more specific path-selectors for better performance.\n`click('^Click')` | the first control (any type) where the name *contains*: \"Click\"\n`click('//*{^Click}')` | the \"long-form\" of the above. Try to use more specific path-selectors for better performance.\n`click('//button{Click Me}')` | the first button where the name is equal to \"Click Me\"\n`click('/pane[2]/button')` | absolute path, the second pane on the active window, and the first button on it\n`click('//pane/*/button')` | other examples of what you can use, the `*` will match any control type\n`click('//button.TButton{^Click}')` | the first button with a \"class name\" of \"TButton\" and the name contains \"Click\"\n`click('//.TButton/{^Click}')` | a different example, so you can use only a \"class name\" or element name, note the position of the `/`\n\nUse a tool like [Inspect.exe](https://docs.microsoft.com/en-us/windows/win32/winauto/inspect-objects) to identify the properties needed for automation from an application window.\n\n### Root Scope\nBy default, all the locators above would be from the currently [active](#robotactive) Window or Element, but you can force the search from the Desktop onwards like this:\n\n```cucumber\n* def allPopUps = locateAll('/root//window')\n```\n\nThis is of course extremely useful in some situations.\n\n### Control Type\nThe [control \"type\"](https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-controltypesoverview) is case-insensitive. Examples are `edit`, `button` and `checkbox`. The complete list of types can be [found here](src/main/java/com/intuit/karate/robot/win/ControlType.java). You don't have to rely on the `LocalizedControlType` shown in tools such as \"Inspect.exe\" because Karate uses the `ControlType`.\n\nSimilarly, the \"class name\" is not case-sensitive. This can be useful in some cases, for example in Delphi you can use values such as `TScrollBox` and `TEdit`.\n\nAlso see [`locateAll()`](#locateall) for ways to find the n-th control on a page that matches a locator and do something with it.\n\n### Property Value\nFor Windows `Element`-s you can retrieve the value by property name or ID using the `property(key)` method:\n\n```cucumber\n* def button = locate('Close')\n* def isOffScreen = button.property('IsOffscreen')\n```\n\nThe argument to `property()` can also be an integer. To see all names and id values, see [`Property`](src/main/java/com/intuit/karate/robot/win/Property.java).\n\n### Calculator Example\n\n[Here is an example](../examples/robot-test/src/test/java/win/calc.feature) that operates the Calculator app on Windows.\n\n```cucumber\nFeature: windows calculator\n\nScenario:\n* robot { window: 'Calculator', fork: 'calc' }\n* click('Clear')\n* click('One')\n* click('Plus')\n* click('Two')\n* click('Equals')\n* match locate('#CalculatorResults').name == 'Display is 3'\n* screenshot()\n* click('Close Calculator')\n```\n\n## `retry()`\nPlease refer to the documentation for the Karate browser-automation syntax for [`retry()`](https://github.com/karatelabs/karate/tree/master/karate-core#retry). It is the same for Karate Robot.\n\n## `waitFor()`\nConvenient to wait for an element. Try to use this only when necessary, for example once a Window loads, all components within it would be immediately accessible without needing to \"wait\". So you can use a `waitFor()` only for the first element within that window that you need to act upon:\n\n```cucumber\n* waitFor('Add New').click()\n```\n\n## `waitUntil()`\nWait for the [JS function](https://github.com/karatelabs/karate#javascript-functions) to evaluate to `true`. Will poll using the [retry()](#retry) settings configured.\n\n```cucumber\n* def fun = function(){ return optional('Close').enabled }\n* waitUntil(fun)\n```\n\nThis gives you a lot of flexibility. Note that Karate can call OS commands using [`karate.exec()`](https://github.com/karatelabs/karate#karate-exec) or even make [HTTP API requests](https://github.com/karatelabs/karate#core-keywords). You can even [call Java code](https://github.com/karatelabs/karate#calling-java) if required.\n\n## `optional()`\nWill return a \"real\" [`Element`](#element-api) if it exists or a \"fake\" object if it does not.\n\nThis is useful to perform conditional logic as one-liners:\n\n```cucumber\n* optional('//pane{Warning}').locate('Close').click()\n```\n\nNote that `optional()`, [`exists()`](#exists), [`windowExists()`](#windowexists) and [`windowOptional()`](#windowoptional) are a little different from the other actions such as [`locate()`](#locate), because they will *not* honor any intent to [`retry()`](#retry) and *immediately* check the [active window](#robotactive) for the given locator. This is important because they are designed to answer the question: \"*does the element exist in the application __right now__ ?*\"\n\nIf you want to *wait* but move on even if something was not found, you can use [`waitForOptional()`](#waitforoptional) and [`waitForWindowOptional()`](#waitforwindowoptional).\n\n## `windowOptional()`\nReturns an \"optional\" [`Window`](#window-api) object and will not update the \"active\" window. You can call `activate()` on the returned `Window` object to set it as the current, typically after [checking that it exists](#optional) (by using the `present` property getter).\n\nHere's an example of clicking a button within an \"optional\" modal pop-up only if it exists:\n\n```cucumber\n* windowOptional('Tips on Startup').locate('Close').click()\n```\n\nNote that on the [`Element` API](#element-api), there is no `click(locator)` API, but you can chain a [`locate()`](#locate) and then call [`click()`](#click).\n\nAlso see [finding windows](#finding-windows) and [conditional start](#conditional-start).\n\n## `waitForOptional()`\nUseful for those cases, where you want to *wait* for something that may *not* appear. Note that since the [retry()](#retry) count defaults to 3, you may want to tone-down the wait like this:\n\n```cucumber\n* retry(1).waitForOptional('Schrodingers Pane')\n```\n\n## `waitForWindowOptional()`\nJust like `windowOptional()` but can [retry](#retry) *and* move on:\n\n```cucumber\n* retry(1).waitForWindowOptional('^My Window')\n```\n\n## `exists()`\nSimilar to [`optional()`](#optional) but returns a boolean, convenient to use with the [`assert`](https://github.com/karatelabs/karate#assert) keyword:\n\n```cucumber\n* assert exists('//pane{Main}')\n```\n\nThe above is functionally equivalent to:\n\n```cucumber\n* assert optional('//pane{Main}').present\n```\n\n## `windowExists()`\nReturns `true` or `false` and will not set or \"activate\" the current window.\n\nSee also [`windowOptional()`](#windowoptional).\n\n## `window()`\nSets focus (and activates as \"current\") to the window by title, prefix with `^` for a string \"contains\" match or `~` for a regular-expression match. The \"active\" window will be used as the root of all operations such as [locating controls](#windows-locators).\n\nAlso see [finding windows](#finding-windows).\n\n## `activate()`\nShort-cut to activate any `Element` by locator. The difference from [`window()`](#window) is that this uses the [Windows Locator](#windows-locators) system to find elements. If you do this at the start of a test without a window activated or if [`robot.active`](#robotactive) is `null`, the search-root will be [`robot.root`](#robotroot) or the \"Desktop\". This can be useful in rare cases where the application under test lives under a \"pane\" [Control Type](#control-type) instead of a \"window\".\n\n```cucumber\n* activate('//pane{Some Name}')\n```\n\n## `robot.root`\nGets the root of all available objects as an [`Element`](#element-api) reference. Useful when you want to search within the entire \"Desktop\" on Windows. Try to avoid \"any-depth\" e.g. `robot.root.locate('//button')` kinds of searches on this element, and stick to things like `robot.root.locate('/pane')`.\n\nNote that using the [`/root`](#root-scope) as the start of a [locator](#windows-locators) can be used instead.\n\n## `robot.active`\nReturns the currently \"active\" element, typically set after a previous call to [`window()`](#window) or [`windowOptional()`](#windowoptional). This will fail the test if a window (or any other `Element` type) has not been \"activated\".\n\nThe [`Element`](#element-api) API has an `activate()` method, so you can also do this:\n\n```cucumber\n* robot.root.locate('//pane{Some Name}').activate()\n```\n\nBut it can be more convenient to use the below pattern, as `active` is also a \"setter\" property on the `robot` object:\n\n```cucumber\n* def e = locate('//{Some Name}')\n* robot.active = e\n```\n\n## `robot.focused`\nReturns the [`Element`](#element) that currently has \"focus\" on the screen, no matter where or what type it is.\n\n## `robot.location`\nReturns a [`Location`](src/main/java/com/intuit/karate/robot/Location.java) instance that represents the mouse position, useful for troubleshooting in debug mode.\n\n```cucumber\n* def region = locate('foo').region\n* region.inset(30, region.height / 6).move()\n* robot.location.highlight()\n# you can also construct a location\n* robot.location(885, 406).highlight()\n```\n\n## `robot.region()`\nConstructs a [`Region`](src/main/java/com/intuit/karate/robot/Region.java) instance that can be used for debugging:\n\n```cucumber\n* def region = robot.region({ x: 100, y: 100, width: 100, height: 100 })\n* region.debugCapture()\n```\n\n## `robot.clipboard`\nReturns the clipboard contents as text. This can be convenient to validate text in non-standard controls where `Element.value` does not work.\n\n```cucumber\n# assume that a control containing text has focus\n* input(Key.CONTROL + 'a')\n* input(Key.CONTROL + 'c')\n* match robot.clipboard == 'hello world'\n```\n\n## `robot.allWindows`\nReturns an array of all windows that exist on the desktop. This is convenient to quickly list all window names on the console, especially in [debug mode](#debugging). Also you could loop over all of them and call methods on the [`Element`](#element-api) or [`Window`](#window-api) instance.\n\n```cucumber\n* print robot.allWindows\n```\n\nNote that this is equivalent to the below, but with the difference that the returned elements are of type [`Window`](#window-api) for the above but are of type [`Element`](#element-api) for the below.\n\n```cucumber\n* print robot.root.locateAll('//window')\n```\n\nAlso note that you can use [`Element.children`](#element-api) to get all direct children of any element:\n\n```cucumber\n* print robot.root.children\n```\n\n## `locate()`\nRarely used, but when you want to just instantiate an [`Element`](src/main/java/com/intuit/karate/robot/Element.java) instance, typically when you are writing custom re-usable functions, or using an element as a \"waypoint\" to access other elements in a large, complex \"tree\".\n\n```cucumber\n* def e = locate('//pane{Some Pane}')\n# now you can have multiple steps refer to \"e\"\n* e.locate('//edit').input('foo')\n* e.locate('//button').click()\n```\nNote that `locate()` will fail the test if the element was not found. Think of it as just like [`waitFor()`](#waitfor) but without the \"wait\" part.\n\nAlso see [`exists()`](#exists) and [`optional()`](#optional).\n\n## `locateAll()`'\nThis can be convenient if you need to loop over a bunch of element and do something. More useful is the ability to target a single item by index. For example, here is how you can find the *second* control with the name \"Address\" and click on it:\n\n```cucumber\n* locateAll('Address')[1].click()\n```\n\n## `highlight()`\nDesigned for use within a [debug session](#debugging), very convenient to interactively locate an element by trial and error.\n\n```cucumber\n* highlight('Some Name')\n```\n\nNote that the [`Element` API](#element-api) also has an `activate()` method so you can do things like this in debug mode:\n\n```\n* robot.active.highlight()\n```\n\nWhich will highlight the [currently \"active\"](#robotactive) element.\n\n## `highlightAll()`\nLike [`highlight()`](#highlight) and super convenient, you can try doing the following to show *all* buttons on a window !\n\n```cucumber\n* highlightAll('//button')\n```\n\n## `click()`\nDefaults to a \"left-click\", pass 1, 2 or 3 as the argument to specify left, middle or right mouse button.\n\n```cucumber\n* click('Continue')\n```\n\nYou can also click on any X and Y co-ordinate. Note that (0, 0) is the top, left of the screen.\n\n```cucumber\n* click(100, 200)\n```\n\n## `doubleClick()`\nPerforms a double-click at the current mouse position. Note that you can also chain this off an [`Element`](#element-api).\n\n## `doubleClick()`\nPerforms a right-click at the current mouse position.\n\n## `move()`\nArgument can be `x, y` co-ordinates or typically the name of an image, which will be looked for in the [`basePath`](#robot). Note that relative paths will work.\n\n## `delay()`\nNot recommended unless un-avoidable. Argument is time in milliseconds.\n\n## `input()`\nThe single string argument can include special characters such as a line-feed:\n\n```cucumber\n* input('karate dsl' + Key.ENTER)\n```\n\nIf you need to simulate key combinations, just ensure that the [modifier keys](#key) such as `Key.CTRL`, `Key.ALT` are the first in the sequence (they will be auto-released at the end):\n\n```cucumber\n* input(Key.META + 't')\n```\n\nFor convenience, you can pass an array of strings as a single argument, convenient for a lot of \"brute force\" keyboard navigation:\n\n```cucumber\n* input([Key.DOWN, Key.RIGHT, Key.ENTER])\n```\n\nAnd you can also add a second argument to the above case, convenient when you want to slow-down things because for e.g. Karate is too fast for the UI to perform validations or refresh:\n\n```cucumber\n* input([Key.DOWN, Key.RIGHT, Key.ENTER], 100)\n```\n\nAnd a string argument is also supported in which case each the delay is before each character.\n\n```cucumber\n* input('type this slowly', 100)\n```\n\n## `select()`\nTo select from a drop-down, for elements with a control-type of `itemtype`. The pattern is to get a reference to the item and call `select()` on it:\n\n```cucumber\n* locate('Some Text').select()\n```\n\n## `press()`\nA mouse press that will be held down, useful for simulating a drag and drop operation.\n\n## `release()`\nRelease mouse button, useful for simulating a drag and drop operation.\n\n## `screenshot()`\nWill save a screenshot of the viewport (entire desktop), which will appear in the HTML report. Note that this returns a byte-array of the PNG image.\n\n```cucumber\n* screenshot()\n```\n\nNote that you can call this *on* an [`Element`](#element-api) instance if you really want to \"zoom in\":\n\n```cucumber\n* locate('//pane{Tree}').screenshot()\n```\n\n## `scroll()`\nThe following methods are available only *on* Windows `Element`-s. Note that they will work only if the \"[Scroll Pattern](https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nn-uiautomationclient-iuiautomationscrollpattern)\" is available. \n\n> Note that `scroll()` has not been tested, please contribute if you can. Also refer to the [diff](https://github.com/karatelabs/karate/commit/10228725a97939dc8fb72499a7a2d52d9366a01f) as an example of how to add an un-implemented \"pattern\" to `karate-robot`.\n\n### `scroll(horizontalPercent, verticalPercent)`\n### `scrollUp()`\n### `scrollDown()`\nBoth `scrollUp()` and `scrollDown()` take an optional boolean argument to specify if a \"large\" increment should be used, e.g: `scrollDown(true)`.\n\n## `screenshotActive()`\nThis will screenshot only the [active](#robotactive) control, typically the [window](#window) having focus.\n\n```cucumber\n* screenshotActive()\n```\n\nNote that this is a convenience short-cut for:\n\n```cucumber\n* robot.active.screenshot()\n```\n\n# Conditional Start\nA useful pattern is to run an app-boot and sign-in sequence only if the main application window is not present. Note how [`karate.abort()`](https://github.com/karatelabs/karate#karate-abort) can be used to conditionaly exit a \"called\" feature early.\n\nThis is also a great example of using [`windowOptional()`](#windowoptional).\n\n```cucumber\n* def mainWindowName = '^MyApp'\n* robot {}\n* def mainWindow = windowOptional(mainWindowName)\n* if (mainWindow.present) { mainWindow.activate(); karate.abort() }\n* karate.fork('C:/myapp/app.exe')\n* retry(10).window('Sign In')\n* waitFor('#userid').input('john@smith.com')\n* input('#password', 'Test@123')\n* click('#submit-btn')\n* retry(10).window(lacWindowName)\n```\n\nAnd the \"calling feature\" can directly jump into the flow to be tested after making a [`call`](https://github.com/karatelabs/karate#calling-other-feature-files) to the above:\n\n```cucumber\nFeature: main\n\nBackground:\n* call read('start.feature')\n\nScenario:\n# main flow\n* click('#some-btn')\n```\n\nAlso see [finding windows](#finding-windows).\n\n# Utility Functions\nSome of the [Karate JS API](https://github.com/karatelabs/karate#the-karate-object) that are more relevant to desktop or Windows app testing are described here:\n\n## [`karate.toAbsolutePath()`](https://github.com/karatelabs/karate#karate-toabsolutepath)\nThis will return the OS specific path form, for example on Windows, back-slash characters will be used. This is useful to generate file-names needed to [`input()`](#input) into file-chooser dialogs and the like.\n\nHere is an example of creating a random file-name on Windows. Also refer to [commonly needed utilities](https://github.com/karatelabs/karate#commonly-needed-utilities). The reason we use `target` here is that because it is the standard build-output directory where temp-files and reports are created.\n\n```cucumber\n* def random = function(){ return java.lang.System.currentTimeMillis() + '' }\n* def dataFolder = function(){ return karate.toAbsolutePath('file:target') }\n* def tempTextFile = function(){ return dataFolder() + '\\\\' + random() + '.txt' }\n```\n\nThe [multiple functions in one file](https://github.com/karatelabs/karate#multiple-functions-in-one-file) pattern can be used to set up these common utilities, and now within a feature-file you can do this:\n\n```cucumber\n* def tempFile = tempTextFile()\n```\n\n## [`karate.exec()`](https://github.com/karatelabs/karate#karate-exec)\nCan execute any OS command, wait for it it terminate, and return the system / console output as a string.\n\nAlso see [`karate.fork()`](#karatefork)\n\n# Standalone JAR\nThe `karate-robot` for Windows is around 150 MB and hence not distributed with the [standalone JAR or IDE plugins](https://github.com/karatelabs/karate/wiki/IDE-Support). But you can download it separately, and it can be easily added to the classpath. You can find instructions [here](https://github.com/karatelabs/karate/wiki/Karate-Robot-Windows-Install-Guide).\n\n## Building\nFor MacOSX, Linux, Android or iOS, you can build a stand-alone JAR by following the [Developer Guide](https://github.com/karatelabs/karate/wiki/Developer-Guide#build-standalone-karate-robot-jar).\n"
  },
  {
    "path": "karate-robot/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>io.karatelabs</groupId>\n        <artifactId>karate-parent</artifactId>\n        <version>1.5.2</version>\n    </parent>\n    <artifactId>karate-robot</artifactId>\n    <packaging>jar</packaging>\n    <name>${project.artifactId}</name>\n    \n    <properties>        \n        <javacpp.version>1.5.3</javacpp.version>\n        <opencv.version>4.3.0</opencv.version>\n        <tesseract.version>4.1.1</tesseract.version>\n        <ffmpeg.version>4.2.2</ffmpeg.version>\n    </properties>      \n\n    <dependencies>                         \n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-core</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>net.java.dev.jna</groupId>\n            <artifactId>jna-platform</artifactId>\n            <version>5.5.0</version>\n        </dependency>              \n        <dependency>\n            <groupId>org.bytedeco</groupId>\n            <artifactId>javacv</artifactId>\n            <version>${javacpp.version}</version>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.bytedeco</groupId>\n                    <artifactId>flycapture</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.bytedeco</groupId>\n                    <artifactId>libdc1394</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.bytedeco</groupId>\n                    <artifactId>libfreenect</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.bytedeco</groupId>\n                    <artifactId>libfreenect2</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.bytedeco</groupId>\n                    <artifactId>librealsense</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.bytedeco</groupId>\n                    <artifactId>librealsense2</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.bytedeco</groupId>\n                    <artifactId>videoinput</artifactId>\n                </exclusion> \n                <exclusion>\n                    <groupId>org.bytedeco</groupId>\n                    <artifactId>artoolkitplus</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.bytedeco</groupId>\n                    <artifactId>flandmark</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.bytedeco</groupId>\n                    <artifactId>ffmpeg</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>                   \n        <dependency>\n            <groupId>org.bytedeco</groupId>\n            <artifactId>opencv-platform</artifactId>\n            <version>${opencv.version}-${javacpp.version}</version>          \n        </dependency>\n        <dependency>\n            <groupId>org.bytedeco</groupId>\n            <artifactId>tesseract-platform</artifactId>\n            <version>${tesseract.version}-${javacpp.version}</version>          \n        </dependency>\n        <dependency>\n            <groupId>io.karatelabs</groupId>\n            <artifactId>karate-junit5</artifactId>\n            <version>${project.version}</version>\n            <scope>test</scope>\n        </dependency>                                         \t\t\n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <excludes>\n                    <exclude>**/*.java</exclude>\n                </excludes>\n            </testResource>\n        </testResources>       \n    </build>\n    \n    <profiles>\n        <profile>\n            <id>fatjar</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-shade-plugin</artifactId>\n                        <version>${maven.shade.version}</version>\n                        <executions>\n                            <execution>\n                                <phase>package</phase>\n                                <goals>\n                                    <goal>shade</goal>\n                                </goals>\n                                <configuration>\n                                    <finalName>karate-robot-${project.version}</finalName>\n                                    <artifactSet>\n                                        <includes>\n                                            <include>net.java.dev.jna:*</include>\n                                            <include>org.bytedeco:*</include>\n                                        </includes>\n                                        <excludes>\n                                            <exclude>*:*:jar:android-*:*</exclude>\n                                            <exclude>*:*:jar:ios-*:*</exclude>\n                                            <exclude>*:*:jar:linux-*:*</exclude>\n                                            <exclude>*:*:jar:macosx-*:*</exclude>\n                                            <!-- <exclude>*:*:jar:windows-x86:*</exclude> -->\n                                            <!-- <exclude>*:*:jar:windows-x86_64:*</exclude> -->\n                                        </excludes>                                        \n                                    </artifactSet>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>                              \n                </plugins>                 \n            </build>\n        </profile>\n    </profiles>                           \n    \n</project>"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/Element.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport com.intuit.karate.core.Config;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Element {\n\n    RobotBase getRobot();\n\n    default Location inset(int fromLeft, int fromTop) {\n        Region r = getRegion();\n        Location l = new Location(r.robot, r.x + fromLeft, r.y + fromTop);\n        return l;\n    }\n\n    boolean isPresent(); // getter    \n\n    boolean isImage(); // getter\n\n    boolean isEnabled(); // getter    \n\n    default Map<String, Object> getPosition() { // getter\n        return getRegion().getPosition();\n    }\n\n    Region getRegion();\n\n    default byte[] screenshot() {\n        return getRegion().screenshot();\n    }\n\n    Element focus();\n    \n    default Element focus(String locator) {\n        RobotBase robot = getRobot();\n        return robot.locate(robot.getHighlightDuration(), this, locator).focus();\n    }    \n\n    default Element move(int fromLeft, int fromTop) {\n        inset(fromLeft, fromTop).move();\n        return this;\n    }\n\n    Element click();\n    \n    default Element click(String locator) {\n        RobotBase robot = getRobot();\n        return robot.locate(robot.getHighlightDuration(), this, locator).click();\n    }\n\n    Element clear();\n\n    default Element click(int fromLeft, int fromTop) {\n        inset(fromLeft, fromTop).click();\n        return this;\n    }\n\n    default Element doubleClick(int fromLeft, int fromTop) {\n        inset(fromLeft, fromTop).doubleClick();\n        return this;\n    }\n\n    Element move();\n\n    Element press();\n\n    Element release();\n\n    String getName(); // getter\n\n    String getValue(); // getter\n\n    Element input(String value);\n\n    Element delay(int millis);\n\n    default Element retry() {\n        getRobot().retry();\n        return this;\n    }\n\n    default Element retry(int count) {\n        getRobot().retry(count);\n        return this;\n    }\n    \n    default Element waitFor(String locator) {\n        return getRobot().retryForAny(true, this, locator);\n    }\n    \n    default Element waitForAny(String locator1, String locator2) {\n        return getRobot().retryForAny(true, this, locator1, locator2);\n    }\n    \n    default Element waitForAny(String[] locators) {\n        return getRobot().retryForAny(true, this, locators);\n    }    \n\n    default Element retry(Integer count, Integer interval) {\n        getRobot().retry(count, interval);\n        return this;\n    }\n\n    default Element locate(String locator) {\n        RobotBase robot = getRobot();\n        return robot.locate(robot.getHighlightDuration(), this, locator);\n    }\n\n    default List<Element> locateAll(String locator) {\n        RobotBase robot = getRobot();\n        return robot.locateAll(robot.getHighlightDuration(), this, locator);\n    }\n    \n    default Element highlight(int millis) {\n        getRegion().highlight(millis);\n        return this;\n    }    \n\n    default Element highlight() {\n        return highlight(Config.DEFAULT_HIGHLIGHT_DURATION);\n    }\n\n    default Element highlight(String locator) {\n        RobotBase robot = getRobot();\n        return robot.locate(Config.DEFAULT_HIGHLIGHT_DURATION, this, locator);\n    }\n\n    default List<Element> highlightAll(String locator) {\n        RobotBase robot = getRobot();\n        return robot.locateAll(Config.DEFAULT_HIGHLIGHT_DURATION, this, locator);\n    }\n\n    default Element optional(String locator) {\n        return getRobot().optional(this, locator);\n    }\n\n    default boolean exists(String locator) {\n        return optional(locator).isPresent();\n    }\n\n    List<Element> getChildren();\n\n    Element getParent();\n\n    <T> T toNative();\n\n    String getDebugString();\n\n    Element select();\n    \n    default Element select(String locator) {\n        RobotBase robot = getRobot();\n        return robot.locate(robot.getHighlightDuration(), this, locator).select();        \n    }\n\n    default String extract() {\n        return extract(null, false);\n    }\n\n    default String extract(String lang, boolean debug) {\n        return getRegion().extract(lang, debug);\n    }\n\n    default Element activate() {\n        getRobot().setActive(this);\n        return this;\n    }\n\n    default void debugCapture() {\n        getRegion().debugCapture();\n    }\n\n    default String debugExtract() {\n        return extract(null, true);\n    }\n    \n    default String debugExtract(String lang) {\n        return extract(lang, true);\n    }    \n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/ImageElement.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class ImageElement implements Element {\n\n    private final Region region;\n    private final RobotBase robot;\n    private final String value;\n\n    public ImageElement(Region region) {\n        this(region, null);\n    }\n    \n    public ImageElement(Region region, String value) {\n        this.region = region;\n        robot = region.robot;\n        this.value = value == null ? region.toString() : value;\n    }    \n\n    @Override\n    public RobotBase getRobot() {\n        return robot;\n    }\n\n    @Override\n    public boolean isPresent() {\n        return true;\n    }\n\n    @Override\n    public boolean isEnabled() {\n        return true;\n    }\n\n    @Override\n    public boolean isImage() {\n        return true;\n    }\n\n    @Override\n    public Region getRegion() {\n        return region;\n    }\n\n    @Override\n    public Element focus() {\n        region.click();\n        return this;\n    }\n\n    @Override\n    public Element click() {\n        region.click();\n        return this;\n    }\n\n    @Override\n    public Element move() {\n        region.move();\n        return this;\n    }\n\n    @Override\n    public Element press() {\n        region.press();\n        return this;\n    }\n\n    @Override\n    public Element release() {\n        region.release();\n        return this;\n    }\n\n    @Override\n    public String getName() {\n        return region.toString();\n    }\n\n    @Override\n    public String getValue() {\n        return value;\n    }\n\n    @Override\n    public Element input(String value) {\n        region.click();\n        robot.input(value);\n        return this;\n    }\n\n    @Override\n    public Element clear() {\n        region.click();\n        robot.clearFocused();\n        return this;\n    }\n\n    @Override\n    public Element delay(int millis) {\n        robot.delay(millis);\n        return this;\n    }\n\n    @Override\n    public List<Element> getChildren() {\n        return Collections.EMPTY_LIST;\n    }        \n\n    @Override\n    public Element getParent() {\n        return null;\n    }        \n\n    @Override\n    public Region toNative() {\n        return region;\n    }\n\n    @Override\n    public String getDebugString() {\n        return getName();\n    }\n\n    @Override\n    public Element select() {\n        return this;\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/Location.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport com.intuit.karate.core.Config;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class Location {\n\n    public final RobotBase robot;\n    public final int x;\n    public final int y;\n\n    public Location(RobotBase robot, int x, int y) {\n        this.robot = robot;\n        this.x = x;\n        this.y = y;\n    }\n\n    public Location move() {\n        robot.move(x, y);\n        return this;\n    }\n\n    public Location click() {\n        return click(1);\n    }\n\n    public Location click(int num) {\n        robot.move(x, y); // do not chain, causes recursion\n        robot.click(num);\n        return this;\n    }\n\n    public Location doubleClick() {\n        robot.move(x, y); // do not chain, causes recursion        \n        robot.doubleClick();\n        return this;\n    }\n\n    public Location press() {\n        robot.move(x, y); // do not chain, causes recursion\n        robot.press();\n        return this;\n    }\n\n    public Location release() {\n        robot.move(x, y); // do not chain, causes recursion\n        robot.release();\n        return this;\n    }\n    \n    public Location highlight() {\n        return highlight(Config.DEFAULT_HIGHLIGHT_DURATION);\n    }\n\n    public Location highlight(int duration) {\n        new Region(robot, x - 5, y - 5, 10, 10).highlight(duration);\n        return this;\n    }\n\n    public Map<String, Object> asMap() {\n        Map<String, Object> map = new HashMap(2);\n        map.put(\"x\", x);\n        map.put(\"y\", y);\n        return map;\n    }\n\n    @Override\n    public String toString() {\n        return asMap().toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/MissingElement.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class MissingElement implements Element {\n\n    private final RobotBase robot;\n\n    public MissingElement(RobotBase robot) {\n        this.robot = robot;\n    }\n\n    @Override\n    public RobotBase getRobot() {\n        return robot;\n    }\n\n    @Override\n    public boolean isPresent() {\n        return false;\n    }\n\n    @Override\n    public boolean isEnabled() {\n        return false;\n    }\n\n    @Override\n    public boolean isImage() {\n        return false;\n    }\n\n    @Override\n    public Region getRegion() {\n        return robot.screen;\n    }\n\n    @Override\n    public Element focus() {\n        return this;\n    }\n\n    @Override\n    public Element click() {\n        return this;\n    }\n\n    @Override\n    public Element click(String locator) {\n        return this;\n    }     \n\n    @Override\n    public Element click(int fromLeft, int fromTop) {\n        return this;\n    }        \n\n    @Override\n    public Element move() {\n        return this;\n    }\n\n    @Override\n    public Element press() {\n        return this;\n    }\n\n    @Override\n    public Element release() {\n        return this;\n    }\n\n    @Override\n    public Element highlight() {\n        return this;\n    }\n\n    @Override\n    public String getName() {\n        return null;\n    }\n\n    @Override\n    public String getValue() {\n        return null;\n    }\n\n    @Override\n    public Element input(String value) {\n        return this;\n    }\n\n    @Override\n    public Element clear() {\n        return this;\n    }\n\n    @Override\n    public Element delay(int millis) {\n        robot.delay(millis);\n        return this;\n    }\n\n    @Override\n    public Element locate(String locator) {\n        return this;\n    }\n\n    @Override\n    public List<Element> getChildren() {\n        return Collections.EMPTY_LIST;\n    }        \n\n    @Override\n    public Element getParent() {\n        return this;\n    }        \n\n    @Override\n    public <T> T toNative() {\n        return null;\n    }\n\n    @Override\n    public String getDebugString() {\n        return \"(missing element)\";\n    }\n\n    @Override\n    public Element select() {\n        return this;\n    }\n\n    @Override\n    public Element select(String locator) {\n        return this;\n    }        \n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/OpenCvUtils.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport java.awt.Image;\nimport java.awt.image.BufferedImage;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.imageio.ImageIO;\nimport javax.swing.WindowConstants;\nimport org.bytedeco.javacpp.DoublePointer;\nimport org.bytedeco.javacpp.Pointer;\nimport org.bytedeco.javacv.CanvasFrame;\nimport org.bytedeco.javacv.Java2DFrameConverter;\nimport org.bytedeco.javacv.Java2DFrameUtils;\nimport org.bytedeco.javacv.OpenCVFrameConverter;\nimport static org.bytedeco.opencv.global.opencv_core.findNonZero;\nimport static org.bytedeco.opencv.global.opencv_core.minMaxLoc;\nimport static org.bytedeco.opencv.global.opencv_core.bitwise_not;\nimport static org.bytedeco.opencv.global.opencv_imgcodecs.*;\nimport static org.bytedeco.opencv.global.opencv_imgproc.*;\nimport org.bytedeco.opencv.opencv_core.AbstractScalar;\nimport org.bytedeco.opencv.opencv_core.Mat;\nimport org.bytedeco.opencv.opencv_core.Point;\nimport org.bytedeco.opencv.opencv_core.Point2f;\nimport org.bytedeco.opencv.opencv_core.Point2fVector;\nimport org.bytedeco.opencv.opencv_core.Rect;\nimport org.bytedeco.opencv.opencv_core.Scalar;\nimport org.bytedeco.opencv.opencv_core.Size;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class OpenCvUtils {\n    \n    private static final Logger logger = LoggerFactory.getLogger(OpenCvUtils.class);\n    \n    private OpenCvUtils() {\n        // only static methods\n    }\n    \n    public static Region find(int strictness, RobotBase robot, Region source, byte[] bytes, boolean resize) {\n        Region found = find(strictness, robot, toMat(source.captureGreyScale()), read(bytes), resize);\n        if (found == null) {\n            return null;\n        }\n        return found.toAbsolute(source);\n    }\n\n    public static Region find(int strictness, RobotBase robot, Mat source, Mat target, boolean resize) {\n        List<Region> found = find(strictness, false, robot, source, target, resize);\n        if (found.isEmpty()) {\n            return null;\n        }\n        return found.get(0);\n    }\n\n    public static List<Region> findAll(int strictness, RobotBase robot, Region source, byte[] bytes, boolean resize) {\n        List<Region> found = find(strictness, true, robot, toMat(source.captureGreyScale()), read(bytes), resize);\n        List<Region> list = new ArrayList(found.size());\n        for (Region r : found) {\n            list.add(r.toAbsolute(source));\n        }\n        return list;\n    }\n\n    public static Mat rescale(Mat mat, double scale) {\n        Mat resized = new Mat();\n        resize(mat, resized, new Size(), scale, scale, CV_INTER_AREA);\n        return resized;\n    }\n\n    private static final int TARGET_MINVAL_FACTOR = 150; // magic number, lower is stricter\n    private static final int BLOCK_SIZE = 5;\n\n    private static List<int[]> getPointsBelowThreshold(Mat src, double threshold) {\n        Mat dst = new Mat();\n        threshold(src, dst, threshold, 1, CV_THRESH_BINARY_INV);\n        Mat non = new Mat();\n        findNonZero(dst, non);\n        int len = (int) non.total();\n        int xPrev = -BLOCK_SIZE;\n        int yPrev = -BLOCK_SIZE;\n        int countPrev = 0;\n        int xSum = 0;\n        int ySum = 0;\n        List<int[]> points = new ArrayList(len);\n        for (int i = 0; i < len; i++) {\n            Pointer ptr = non.ptr(i);\n            Point p = new Point(ptr);\n            int x = p.x();\n            int y = p.y();\n            int xDelta = Math.abs(x - xPrev);\n            int yDelta = Math.abs(y - yPrev);\n            if (xDelta < BLOCK_SIZE && yDelta < BLOCK_SIZE) {\n                countPrev++;\n                xSum += x;\n                ySum += y;\n            } else {\n                if (countPrev > 0) {\n                    int xFinal = Math.floorDiv(xSum, countPrev);\n                    int yFinal = Math.floorDiv(ySum, countPrev);\n                    // logger.debug(\"end: {}:{}\", xFinal, yFinal);\n                    points.add(new int[]{xFinal, yFinal});\n                }\n                xSum = x;\n                ySum = y;\n                countPrev = 1;\n            }\n            xPrev = x;\n            yPrev = y;\n        }\n        if (countPrev > 0) {\n            int xFinal = Math.floorDiv(xSum, countPrev);\n            int yFinal = Math.floorDiv(ySum, countPrev);\n            points.add(new int[]{xFinal, yFinal});\n        }\n        return points;\n    }\n\n    private static Region toRegion(RobotBase robot, int[] p, double scale, int targetWidth, int targetHeight) {\n        int x = (int) Math.round(p[0] / scale);\n        int y = (int) Math.round(p[1] / scale);\n        int width = (int) Math.round(targetWidth / scale);\n        int height = (int) Math.round(targetHeight / scale);\n        return new Region(robot, x, y, width, height);\n    }\n\n    private static int[] templateAndMin(int strictness, double scale, Mat source, Mat target, Mat result) {\n        Mat resized = scale == 1 ? source : rescale(source, scale);\n        matchTemplate(resized, target, result, CV_TM_SQDIFF);\n        DoublePointer minValPtr = new DoublePointer(1);\n        DoublePointer maxValPtr = new DoublePointer(1);\n        Point minPt = new Point();\n        Point maxPt = new Point();\n        minMaxLoc(result, minValPtr, maxValPtr, minPt, maxPt, null);\n        int minVal = (int) minValPtr.get();\n        int x = minPt.x();\n        int y = minPt.y();\n        return new int[]{x, y, minVal};\n    }\n\n    private static int collect(int strictness, List<Region> found, boolean findAll, RobotBase robot, Mat source, Mat target, double scale) {\n        int targetWidth = target.cols();\n        int targetHeight = target.rows();\n        int targetMinVal = targetWidth * targetHeight * TARGET_MINVAL_FACTOR * strictness;  \n        Mat result = new Mat();\n        int[] minData = templateAndMin(strictness, scale, source, target, result);\n        int minValue = minData[2];\n        if (minValue > targetMinVal) {\n            logger.debug(\"no match at scale {}, minVal: {} / {} at {}:{}\", scale, minValue, targetMinVal, minData[0], minData[1]);\n            if (robot != null && robot.debug) {\n                Rect rect = new Rect(minData[0], minData[1], targetWidth, targetHeight);\n                Mat temp = drawOnImage(source, rect, Scalar.RED);\n                show(temp, scale + \" \" +  minData[0] + \":\" + minData[1] + \" \" + minValue + \" / \" + targetMinVal);\n            }\n            return minData[2];\n        }\n        logger.debug(\"found match at scale {}, minVal: {} / {} at {}:{}\", scale, minValue, targetMinVal, minData[0], minData[1]);\n        if (findAll) {\n            List<int[]> points = getPointsBelowThreshold(result, targetMinVal);\n            for (int[] p : points) {\n                Region region = toRegion(robot, p, scale, targetWidth, targetHeight);\n                found.add(region);\n            }\n        } else {\n            Region region = toRegion(robot, minData, scale, targetWidth, targetHeight);\n            found.add(region);\n        }\n        return minValue;\n    }\n\n    public static List<Region> find(int strictness, boolean findAll, RobotBase robot, Mat source, Mat target, boolean resize) {\n        List<Region> found = new ArrayList();\n        collect(strictness, found, findAll, robot, source, target, 1);\n        if (!found.isEmpty()) {\n            return found;\n        }\n        int stepUp = collect(strictness, found, findAll, robot, source, target, 1.1);\n        if (!found.isEmpty()) {\n            return found;\n        }\n        int stepDown = collect(strictness, found, findAll, robot, source, target, 0.9);\n        if (!found.isEmpty()) {\n            return found;\n        }\n        boolean goUpFirst = stepUp < stepDown;\n        for (int step = 2; step < 6; step++) {\n            double scale = 1 + 0.1 * step * (goUpFirst ? 1 : -1);\n            collect(strictness, found, findAll, robot, source, target, scale);\n        }\n        if (!findAll && !found.isEmpty()) {\n            return found;\n        }\n        for (int step = 2; step < 6; step++) {\n            double scale = 1 + 0.1 * step * (goUpFirst ? -1 : 1);\n            collect(strictness, found, findAll, robot, source, target, scale);\n        }        \n        return found;\n    }\n\n    public static Mat loadAndShowOrExit(File file, int flags) {\n        Mat image = read(file, flags);\n        show(image, file.getName());\n        return image;\n    }\n\n    public static BufferedImage readImageAsGreyScale(File file) {\n        Mat mat = read(file, IMREAD_GRAYSCALE);\n        return toBufferedImage(mat);\n    }\n\n    public static byte[] toBytes(BufferedImage img) {\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        try {\n            ImageIO.write(img, \"png\", baos);\n            return baos.toByteArray();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static Mat read(File file) {\n        return read(file, IMREAD_GRAYSCALE);\n    }\n\n    public static Mat read(byte[] bytes) {\n        return read(bytes, IMREAD_GRAYSCALE);\n    }\n\n    public static Mat read(byte[] bytes, int flags) {\n        Mat image = imdecode(new Mat(bytes), flags);\n        if (image.empty()) {\n            throw new RuntimeException(\"image decode failed\");\n        }\n        return image;\n    }\n\n    public static Mat read(File file, int flags) {\n        Mat image = imread(file.getAbsolutePath(), flags);\n        if (image.empty()) {\n            throw new RuntimeException(\"image not found: \" + file.getAbsolutePath());\n        }\n        return image;\n    }\n\n    public static File save(BufferedImage image, File file) {\n        try {\n            ImageIO.write(image, \"png\", file);\n            return file;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n    \n    public static void show(byte[] bytes, String title) {\n        Mat mat = read(bytes);\n        show(toBufferedImage(mat), title);\n    }    \n\n    public static void show(Mat mat, String title) {\n        show(toBufferedImage(mat), title);\n    }\n\n    public static void show(Image image, String title) {\n        CanvasFrame canvas = new CanvasFrame(title, 1);\n        canvas.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);\n        canvas.showImage(image);\n    }\n\n    public static void save(Mat image, File file) {\n        imwrite(file.getAbsolutePath(), image);\n    }\n\n    public static Mat drawOnImage(Mat image, Point2fVector points) {\n        Mat dest = image.clone();\n        int radius = 5;\n        Scalar red = new Scalar(0, 0, 255, 0);\n        for (int i = 0; i < points.size(); i++) {\n            Point2f p = points.get(i);\n            circle(dest, new Point(Math.round(p.x()), Math.round(p.y())), radius, red);\n        }\n        return dest;\n    }\n\n    public static Mat drawOnImage(Mat image, Rect overlay, Scalar color) {\n        Mat dest = image.clone();\n        rectangle(dest, overlay, color);\n        return dest;\n    }\n    \n    public static Mat negative(Mat src) {\n        Mat dest = new Mat();\n        bitwise_not(src, dest);\n        return dest;\n    }\n    \n    public static Mat toMat(BufferedImage bi) {\n        return Java2DFrameUtils.toMat(bi);\n    }    \n\n    public static BufferedImage toBufferedImage(Mat mat) {\n        OpenCVFrameConverter.ToMat openCVConverter = new OpenCVFrameConverter.ToMat();\n        Java2DFrameConverter java2DConverter = new Java2DFrameConverter();\n        return java2DConverter.convert(openCVConverter.convert(mat));\n    }    \n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/Region.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport com.intuit.karate.core.Config;\nimport java.awt.Graphics;\nimport java.awt.Image;\nimport java.awt.Rectangle;\nimport java.awt.image.BufferedImage;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class Region  {\n\n    public final RobotBase robot;\n    public final int x;\n    public final int y;\n    public final int width;\n    public final int height;\n    \n    Region toAbsolute(Region offset) {\n        return new Region(robot, x + offset.x, y + offset.y, width, height);\n    }\n\n    public Region(RobotBase robot, int x, int y) {\n        this(robot, x, y, 0, 0);\n    }\n\n    public Region(RobotBase robot, int x, int y, int width, int height) {\n        this.robot = robot;\n        this.x = x;\n        this.y = y;\n        this.width = width;\n        this.height = height;\n    }\n    \n    private BufferedImage capture(int type) {\n        Image image = robot.robot.createScreenCapture(new Rectangle(x, y, width, height));\n        BufferedImage bi = new BufferedImage(width, height, type);\n        Graphics g = bi.createGraphics();\n        g.drawImage(image, 0, 0, width, height, null);\n        return bi;\n    }    \n    \n    public BufferedImage capture() {\n        return Region.this.capture(BufferedImage.TYPE_INT_RGB);\n    }\n    \n    public BufferedImage captureGreyScale() {\n        return Region.this.capture(BufferedImage.TYPE_BYTE_GRAY);\n    }    \n\n    public Location getCenter() {\n        return new Location(robot, x + width / 2, y + height / 2);\n    }\n    \n    public Location inset(int deltaX, int deltaY) {\n        return new Location(robot, x + deltaX, y + deltaY);\n    }\n\n    public void highlight() {\n        highlight(Config.DEFAULT_HIGHLIGHT_DURATION);\n    }\n    \n    public void highlight(int millis) {\n        RobotUtils.highlight(this, millis);\n    }    \n\n    public Region click() {\n        return click(1);\n    }\n\n    public Region click(int num) {\n        getCenter().click(num);\n        return this;\n    }\n\n    public Region move() {\n        getCenter().move();\n        return this;\n    }\n\n    public Region press() {\n        getCenter().press();\n        return this;\n    }\n    \n    public Region release() {\n        getCenter().release();\n        return this;\n    }    \n    \n    public Map<String, Object> getPosition() {\n        Map<String, Object> map = new HashMap(4);\n        map.put(\"x\", x);\n        map.put(\"y\", y);\n        map.put(\"width\", width);\n        map.put(\"height\", height);\n        return map;\n    }\n    \n    public byte[] screenshot() {\n        return robot.screenshot(this);\n    }   \n    \n    public String extract(String lang, boolean debug) {\n        if (lang == null) {\n            lang = robot.tessLang;\n        }\n        if (lang.length() < 2) {\n            lang = lang + robot.tessLang;\n        }\n        boolean negative = lang.charAt(0) == '-';\n        if (negative) {\n            lang = lang.substring(1);\n        }\n        Tesseract tess = Tesseract.init(robot, lang, this, negative);\n        if (debug) {\n            tess.highlightWords(robot, this, Config.DEFAULT_HIGHLIGHT_DURATION);\n        }\n        return tess.getAllText();\n    }    \n    \n    public void debugCapture() {\n        OpenCvUtils.show(capture(), toString());\n    } \n    \n    public String debugExtract() {\n        return extract(null, true);\n    }\n    \n    public String debugExtract(String lang) {\n        return extract(lang, true);\n    }    \n\n    @Override\n    public String toString() {\n        return x + \":\" + y + \"(\" + width + \":\" + height + \")\";\n    }        \n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/Robot.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport com.intuit.karate.core.AutoDef;\nimport com.intuit.karate.core.Plugin;\nimport java.util.List;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Robot extends Plugin {\n\n    static final List<String> METHOD_NAMES = Plugin.methodNames(Robot.class);\n\n    @Override\n    default List<String> methodNames() {\n        return METHOD_NAMES;\n    }\n\n    @AutoDef\n    Robot retry();\n\n    @AutoDef\n    Robot retry(int count);\n\n    @AutoDef\n    Robot retry(Integer count, Integer interval);\n\n    @AutoDef\n    Robot delay(int millis);\n\n    @AutoDef\n    Robot click();\n\n    @AutoDef\n    Robot click(int num);\n\n    @AutoDef\n    Robot doubleClick();\n    \n    @AutoDef\n    Robot rightClick();    \n\n    @AutoDef\n    Robot press();\n\n    @AutoDef\n    Robot release();\n\n    @AutoDef\n    Robot input(String[] values);\n    \n    @AutoDef\n    Robot input(String[] values, int delay);  \n    \n    @AutoDef\n    Robot input(String chars, int delay);\n\n    @AutoDef\n    Robot input(String value);\n\n    @AutoDef\n    Element input(String locator, String value);\n\n    @AutoDef\n    byte[] screenshot();\n\n    @AutoDef\n    byte[] screenshotActive();\n    \n    @AutoDef\n    Robot move(int x, int y);\n\n    @AutoDef\n    Robot click(int x, int y);\n\n    @AutoDef\n    Element highlight(String locator);\n    \n    @AutoDef\n    List<Element> highlightAll(String locator);    \n\n    @AutoDef\n    Element locate(String locator);\n    \n    @AutoDef\n    List<Element> locateAll(String locator);\n\n    @AutoDef\n    Element optional(String locator);\n\n    @AutoDef\n    boolean exists(String locator);\n\n    @AutoDef\n    Element move(String locator);\n    \n    @AutoDef\n    Element focus(String locator);    \n\n    @AutoDef\n    Element click(String locator);\n    \n    @AutoDef\n    Element select(String locator);\n\n    @AutoDef\n    Element press(String locator);\n\n    @AutoDef\n    Element release(String locator);\n\n    @AutoDef\n    Element window(String title);\n\n    @AutoDef\n    Element window(Predicate<String> condition);\n    \n    @AutoDef\n    boolean windowExists(String locator);\n\n    @AutoDef\n    Element windowOptional(String locator);   \n    \n    @AutoDef\n    Element waitForWindowOptional(String locator);     \n\n    @AutoDef\n    Object waitUntil(Supplier<Object> condition);\n    \n    @AutoDef\n    Object waitUntilOptional(Supplier<Object> condition);    \n\n    @AutoDef\n    Element waitFor(String locator);\n    \n    @AutoDef\n    Element waitForOptional(String locator);    \n\n    @AutoDef\n    Element waitForAny(String locator1, String locator2);\n\n    @AutoDef\n    Element waitForAny(String[] locators);\n    \n    @AutoDef\n    Element activate(String locator);\n    \n    List<Window> getAllWindows(); // purely for debug convenience        \n    \n    Element getActive(); // getter\n    \n    Robot setActive(Element e); // setter    \n\n    Element getRoot(); // getter\n\n    Element getFocused(); // getter\n\n    String getClipboard(); // getter\n    \n    Location getLocation(); // getter\n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/RobotBase.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport com.intuit.karate.core.Config;\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.core.Plugin;\nimport com.intuit.karate.driver.Keys;\nimport com.intuit.karate.KarateException;\nimport com.intuit.karate.core.ScenarioEngine;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.core.StepResult;\nimport com.intuit.karate.core.Variable;\nimport com.intuit.karate.http.ResourceType;\nimport com.intuit.karate.shell.Command;\nimport java.awt.Dimension;\nimport java.awt.MouseInfo;\nimport java.awt.Point;\nimport java.awt.Toolkit;\nimport java.awt.datatransfer.DataFlavor;\nimport java.awt.event.InputEvent;\nimport java.awt.image.BufferedImage;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\n/**\n *\n * @author pthomas3\n */\npublic abstract class RobotBase implements Robot, Plugin {\n\n    private static final int CLICK_POST_DELAY = 500;\n\n    public final java.awt.Robot robot;\n    public final Toolkit toolkit;\n    public final Dimension dimension;\n    public final Map<String, Object> options;\n\n    public final boolean autoClose;\n    public final boolean screenshotOnFailure;\n    public final int autoDelay;\n    public final Region screen;\n    public final String tessData;\n    public final String tessLang;\n\n    // mutables\n    private String basePath;\n    protected Command command;\n    protected ScenarioEngine engine;\n    protected Logger logger;\n    protected Element currentWindow;\n\n    // retry\n    private boolean retryEnabled;\n    private Integer retryIntervalOverride = null;\n    private Integer retryCountOverride = null;\n\n    // debug\n    protected boolean debug;\n    public boolean highlight;\n    public int highlightDuration;\n\n    public void setDebug(boolean debug) {\n        this.debug = debug;\n    }\n\n    public void setHighlight(boolean highlight) {\n        this.highlight = highlight;\n    }\n\n    public void setHighlightDuration(int highlightDuration) {\n        this.highlightDuration = highlightDuration;\n    }\n\n    public void disableRetry() {\n        retryEnabled = false;\n        retryCountOverride = null;\n        retryIntervalOverride = null;\n    }\n\n    public void enableRetry(Integer count, Integer interval) {\n        retryEnabled = true;\n        retryCountOverride = count; // can be null\n        retryIntervalOverride = interval; // can be null\n    }\n\n    private int getRetryCount() {\n        return retryCountOverride == null ? engine.getConfig().getRetryCount() : retryCountOverride;\n    }\n\n    private int getRetryInterval() {\n        return retryIntervalOverride == null ? engine.getConfig().getRetryInterval() : retryIntervalOverride;\n    }\n\n    private <T> T get(String key, T defaultValue) {\n        T temp = (T) options.get(key);\n        return temp == null ? defaultValue : temp;\n    }\n\n    public Logger getLogger() {\n        return logger;\n    }\n\n    public RobotBase(ScenarioRuntime runtime) {\n        this(runtime, Collections.EMPTY_MAP);\n    }\n\n    public RobotBase(ScenarioRuntime runtime, Map<String, Object> options) {\n        this.engine = runtime.engine;\n        this.logger = runtime.logger;\n        try {\n            this.options = options;\n            basePath = get(\"basePath\", null);\n            highlight = get(\"highlight\", false);\n            highlightDuration = get(\"highlightDuration\", Config.DEFAULT_HIGHLIGHT_DURATION);\n            autoDelay = get(\"autoDelay\", 0);\n            tessData = get(\"tessData\", \"tessdata\");\n            tessLang = get(\"tessLang\", \"eng\");\n            toolkit = Toolkit.getDefaultToolkit();\n            dimension = toolkit.getScreenSize();\n            screen = new Region(this, 0, 0, dimension.width, dimension.height);\n            logger.debug(\"screen dimensions: {}\", screen);\n            robot = new java.awt.Robot();\n            robot.setAutoDelay(autoDelay);\n            robot.setAutoWaitForIdle(true);\n            //==================================================================\n            screenshotOnFailure = get(\"screenshotOnFailure\", true);\n            autoClose = get(\"autoClose\", true);\n            boolean attach = get(\"attach\", true);\n            String window = get(\"window\", null);\n            if (window != null) {\n                currentWindow = window(window, false, false); // don't retry\n            }\n            if (currentWindow != null && attach) {\n                logger.debug(\"window found, will re-use: {}\", window);\n            } else {\n                Variable v = new Variable(options.get(\"fork\"));\n                if (v.isString()) {\n                    command = engine.fork(true, v.getAsString());\n                } else if (v.isList()) {\n                    command = engine.fork(true, v.<List>getValue());\n                } else if (v.isMap()) {\n                    command = engine.fork(true, v.<Map>getValue());\n                }\n                if (command != null) {\n                    delay(500); // give process time to start\n                    if (command.isFailed()) {\n                        throw new KarateException(\"robot fork command failed: \" + command.getFailureReason().getMessage());\n                    }\n                    if (window != null) {\n                        retryCountOverride = get(\"retryCount\", null);\n                        retryIntervalOverride = get(\"retryInterval\", null);\n                        currentWindow = window(window); // will retry\n                        logger.debug(\"attached to process window: {} - {}\", currentWindow, command.getArgList());\n                    }\n                }\n                if (currentWindow == null && window != null) {\n                    throw new KarateException(\"failed to find window: \" + window);\n                }\n            }\n        } catch (Exception e) {\n            String message = \"robot init failed: \" + e.getMessage();\n            throw new KarateException(message, e);\n        }\n    }\n\n    public <T> T retry(Supplier<T> action, Predicate<T> condition, String logDescription, boolean failWithException) {\n        long startTime = System.currentTimeMillis();\n        int count = 0, max = getRetryCount();\n        int interval = getRetryInterval();\n        disableRetry(); // always reset\n        T result;\n        boolean success;\n        do {\n            if (count > 0) {\n                logger.debug(\"{} - retry #{}\", logDescription, count);\n                delay(interval);\n            }\n            result = action.get();\n            success = condition.test(result);\n        } while (!success && count++ < max);\n        if (!success) {\n            long elapsedTime = System.currentTimeMillis() - startTime;\n            String message = logDescription + \": failed after \" + (count - 1) + \" retries and \" + elapsedTime + \" milliseconds\";\n            logger.warn(message);\n            if (failWithException) {\n                throw new RuntimeException(message);\n            }\n        }\n        return result;\n    }\n\n    public void setBasePath(String basePath) {\n        this.basePath = basePath;\n    }\n\n    private byte[] readBytes(String path) {\n        if (basePath != null) {\n            String slash = basePath.endsWith(\":\") ? \"\" : \"/\";\n            path = basePath + slash + path;\n        }\n        return engine.fileReader.readFileAsBytes(path);\n    }\n\n    @Override\n    public void onFailure(StepResult stepResult) {\n        if (screenshotOnFailure && !stepResult.isWithCallResults()) {\n            byte[] bytes = screenshot();\n\n        }\n    }\n\n    @Override\n    public Robot retry() {\n        return retry(null, null);\n    }\n\n    @Override\n    public Robot retry(int count) {\n        return retry(count, null);\n    }\n\n    @Override\n    public Robot retry(Integer count, Integer interval) {\n        enableRetry(count, interval);\n        return this;\n    }\n\n    @Override\n    public Robot delay(int millis) {\n        robot.delay(millis);\n        return this;\n    }\n\n    private static int mask(int num) {\n        switch (num) {\n            case 2:\n                return InputEvent.BUTTON2_DOWN_MASK;\n            case 3:\n                return InputEvent.BUTTON3_DOWN_MASK;\n            default:\n                return InputEvent.BUTTON1_DOWN_MASK;\n        }\n    }\n\n    @Override\n    public Robot click() {\n        return click(1);\n    }\n\n    @Override\n    public Robot rightClick() {\n        return click(2);\n    }\n\n    @Override\n    public Robot click(int num) {\n        int mask = mask(num);\n        robot.mousePress(mask);\n        if (highlight) {\n            getLocation().highlight(highlightDuration);\n            int toDelay = CLICK_POST_DELAY - highlightDuration;\n            if (toDelay > 0) {\n                RobotUtils.delay(toDelay);\n            }\n        } else {\n            RobotUtils.delay(CLICK_POST_DELAY);\n        }\n        robot.mouseRelease(mask);\n        return this;\n    }\n\n    @Override\n    public Robot doubleClick() {\n        if (highlight) {\n            getLocation().highlight(highlightDuration);\n            int toDelay = highlightDuration;\n            if (toDelay > 0) {\n                RobotUtils.delay(toDelay);\n            }\n        }\n        int clickType = mask(1);\n        robot.mousePress(clickType);\n        robot.mouseRelease(clickType);\n        RobotUtils.delay(100);\n        robot.mousePress(clickType);\n        robot.mouseRelease(clickType);\n\n        return this;\n    }\n\n    @Override\n    public Robot press() {\n        int mask = mask(1);\n        robot.mousePress(mask);\n        return this;\n    }\n\n    @Override\n    public Robot release() {\n        int mask = mask(1);\n        robot.mouseRelease(mask);\n        return this;\n    }\n\n    @Override\n    public Robot input(String[] values) {\n        return input(values, 0);\n    }\n\n    @Override\n    public Robot input(String chars, int delay) {\n        String[] array = new String[chars.length()];\n        for (int i = 0; i < array.length; i++) {\n            array[i] = Character.toString(chars.charAt(i));\n        }\n        return input(array, delay);\n    }\n\n    @Override\n    public Robot input(String[] values, int delay) {\n        for (String s : values) {\n            if (delay > 0) {\n                delay(delay);\n            }\n            input(s);\n        }\n        return this;\n    }\n\n    @Override\n    public Robot input(String value) {\n        if (highlight) {\n            getFocused().highlight(highlightDuration);\n        }\n        StringBuilder sb = new StringBuilder();\n        for (char c : value.toCharArray()) {\n            if (Keys.isModifier(c)) {\n                sb.append(c);\n                int[] codes = RobotUtils.KEY_CODES.get(c);\n                if (codes == null) {\n                    logger.warn(\"cannot resolve char: {}\", c);\n                    robot.keyPress(c);\n                } else {\n                    robot.keyPress(codes[0]);\n                }\n                continue;\n            }\n            int[] codes = RobotUtils.KEY_CODES.get(c);\n            if (codes == null) {\n                logger.warn(\"cannot resolve char: {}\", c);\n                robot.keyPress(c);\n                robot.keyRelease(c);\n            } else if (codes.length > 1) {\n                robot.keyPress(codes[0]);\n                robot.keyPress(codes[1]);\n                robot.keyRelease(codes[1]);\n                robot.keyRelease(codes[0]);\n            } else {\n                robot.keyPress(codes[0]);\n                robot.keyRelease(codes[0]);\n            }\n        }\n        for (char c : sb.toString().toCharArray()) {\n            int[] codes = RobotUtils.KEY_CODES.get(c);\n            if (codes == null) {\n                logger.warn(\"cannot resolve char: {}\", c);\n                robot.keyRelease(c);\n            } else {\n                robot.keyRelease(codes[0]);\n            }\n        }\n        return this;\n    }\n\n    public Robot clearFocused() {\n        return input(Keys.CONTROL + \"a\" + Keys.DELETE);\n    }\n\n    protected int getHighlightDuration() {\n        return highlight ? highlightDuration : -1;\n    }\n\n    @Override\n    public Element input(String locator, String value) {\n        return locate(locator).input(value);\n    }\n\n    @Override\n    public byte[] screenshot() {\n        return screenshot(screen);\n    }\n\n    @Override\n    public byte[] screenshotActive() {\n        return getActive().screenshot();\n    }\n\n    public byte[] screenshot(int x, int y, int width, int height) {\n        return screenshot(new Region(this, x, y, width, height));\n    }\n\n    public byte[] screenshot(Region region) {\n        BufferedImage image = region.capture();\n        byte[] bytes = OpenCvUtils.toBytes(image);\n        getRuntime().embed(bytes, ResourceType.PNG);\n        return bytes;\n    }\n\n    @Override\n    public Robot move(int x, int y) {\n        robot.mouseMove(x, y);\n        return this;\n    }\n\n    @Override\n    public Robot click(int x, int y) {\n        return move(x, y).click();\n    }\n\n    @Override\n    public Element highlight(String locator) {\n        return locate(Config.DEFAULT_HIGHLIGHT_DURATION, getSearchRoot(), locator);\n    }\n\n    @Override\n    public List<Element> highlightAll(String locator) {\n        return locateAll(Config.DEFAULT_HIGHLIGHT_DURATION, getSearchRoot(), locator);\n    }\n\n    @Override\n    public Element focus(String locator) {\n        return locate(getHighlightDuration(), getSearchRoot(), locator).focus();\n    }\n\n    @Override\n    public Element locate(String locator) {\n        return locate(getHighlightDuration(), getSearchRoot(), locator);\n    }\n\n    @Override\n    public List<Element> locateAll(String locator) {\n        return locateAll(getHighlightDuration(), getSearchRoot(), locator);\n    }\n\n    @Override\n    public boolean exists(String locator) {\n        return optional(locator).isPresent();\n    }\n\n    @Override\n    public Element optional(String locator) {\n        return optional(getSearchRoot(), locator);\n    }\n\n    @Override\n    public boolean windowExists(String locator) {\n        return windowOptional(locator).isPresent();\n    }\n\n    @Override\n    public Element windowOptional(String locator) {\n        return waitForWindowOptional(locator, false);\n    }\n\n    @Override\n    public Element waitForWindowOptional(String locator) {\n        return waitForWindowOptional(locator, true);\n    }\n\n    protected Element waitForWindowOptional(String locator, boolean retry) {\n        Element prevWindow = currentWindow;\n        Element window = window(locator, retry, false); // will update currentWindow\n        currentWindow = prevWindow; // so we reset it\n        if (window == null) {\n            return new MissingElement(this);\n        }\n        // note that currentWindow will NOT point to the new window located\n        return window;\n    }\n\n    protected Element optional(Element searchRoot, String locator) {\n        Element found = locateImageOrElement(searchRoot, locator);\n        if (found == null) {\n            logger.warn(\"element does not exist: {}\", locator);\n            return new MissingElement(this);\n        }\n        if (highlight) {\n            found.highlight();\n        }\n        return found;\n    }\n\n    protected Element locate(int duration, Element searchRoot, String locator) {\n        Element found;\n        if (retryEnabled) {\n            found = retryForAny(true, searchRoot, locator);\n        } else {\n            found = locateImageOrElement(searchRoot, locator);\n            if (found == null) {\n                String message = \"cannot locate: '\" + locator + \"' (\" + searchRoot.getDebugString() + \")\";\n                logger.error(message);\n                throw new RuntimeException(message);\n            }\n            if (duration > 0) {\n                found.getRegion().highlight(duration);\n            }\n        }\n        return found;\n    }\n\n    protected List<Element> locateAll(int duration, Element searchRoot, String locator) {\n        List<Element> found;\n        if (locator.endsWith(\".png\")) {\n            found = locateAllImages(searchRoot, locator);\n        } else if (locator.startsWith(\"{\")) {\n            found = locateAllText(searchRoot, locator);\n        } else {\n            found = locateAllInternal(searchRoot, locator);\n        }\n        if (duration > 0) {\n            RobotUtils.highlightAll(searchRoot.getRegion(), found, duration, false);\n        }\n        return found;\n    }\n\n    @Override\n    public Element move(String locator) {\n        return locate(getHighlightDuration(), getSearchRoot(), locator).move();\n    }\n\n    @Override\n    public Element click(String locator) {\n        return locate(getHighlightDuration(), getSearchRoot(), locator).click();\n    }\n\n    @Override\n    public Element select(String locator) {\n        return locate(getHighlightDuration(), getSearchRoot(), locator).select();\n    }\n\n    @Override\n    public Element press(String locator) {\n        return locate(getHighlightDuration(), getSearchRoot(), locator).press();\n    }\n\n    @Override\n    public Element release(String locator) {\n        return locate(getHighlightDuration(), getSearchRoot(), locator).release();\n    }\n\n    private StringUtils.Pair parseOcr(String raw) { // TODO make object\n        int pos = raw.indexOf('}');\n        String lang = raw.substring(1, pos);\n        if (lang.length() < 2) {\n            lang = lang + tessLang;\n        }\n        String text = raw.substring(pos + 1);\n        return StringUtils.pair(lang, text);\n    }\n\n    public List<Element> locateAllText(Element searchRoot, String path) {\n        StringUtils.Pair pair = parseOcr(path);\n        String lang = pair.left;\n        boolean negative = lang.charAt(0) == '-';\n        if (negative) {\n            lang = lang.substring(1);\n        }\n        String text = pair.right;\n        return Tesseract.findAll(this, lang, searchRoot.getRegion(), text, negative);\n    }\n\n    public Element locateText(Element searchRoot, String path) {\n        StringUtils.Pair pair = parseOcr(path);\n        String lang = pair.left;\n        boolean negative = lang.charAt(0) == '-';\n        if (negative) {\n            lang = lang.substring(1);\n        }\n        String text = pair.right;\n        return Tesseract.find(this, lang, searchRoot.getRegion(), text, negative);\n    }\n\n    private static class PathAndStrict {\n\n        final int strictness;\n        final String path;\n\n        public PathAndStrict(String path) {\n            int pos = path.indexOf(':');\n            if (pos > 0 && pos < 3) {\n                strictness = Integer.valueOf(path.substring(0, pos));\n                this.path = path.substring(pos + 1);\n            } else {\n                strictness = 10;\n                this.path = path;\n            }\n        }\n\n    }\n\n    public List<Element> locateAllImages(Element searchRoot, String path) {\n        PathAndStrict ps = new PathAndStrict(path);\n        List<Region> found = OpenCvUtils.findAll(ps.strictness, this, searchRoot.getRegion(), readBytes(ps.path), true);\n        List<Element> list = new ArrayList(found.size());\n        for (Region region : found) {\n            list.add(new ImageElement(region));\n        }\n        return list;\n    }\n\n    public Element locateImage(Region region, String path) {\n        PathAndStrict ps = new PathAndStrict(path);\n        return locateImage(region, ps.strictness, readBytes(ps.path));\n    }\n\n    public Element locateImage(Region searchRegion, int strictness, byte[] bytes) {\n        Region region = OpenCvUtils.find(strictness, this, searchRegion, bytes, true);\n        if (region == null) {\n            return null;\n        }\n        return new ImageElement(region);\n    }\n\n    @Override\n    public Element window(String title) {\n        return window(title, true, true);\n    }\n\n    private Element window(String title, boolean retry, boolean failWithException) {\n        return window(new StringMatcher(title), retry, failWithException);\n    }\n\n    @Override\n    public Element window(Predicate<String> condition) {\n        return window(condition, true, true);\n    }\n\n    private Element window(Predicate<String> condition, boolean retry, boolean failWithException) {\n        try {\n            currentWindow = retry ? retry(() -> windowInternal(condition), w -> w != null, \"find window\", failWithException) : windowInternal(condition);\n        } catch (Exception e) {\n            if (failWithException) {\n                throw e;\n            }\n            logger.warn(\"failed to find window: {}\", e.getMessage());\n            currentWindow = null;\n        }\n        if (currentWindow != null && highlight) { // currentWindow can be null\n            currentWindow.highlight(getHighlightDuration());\n        }\n        return currentWindow;\n    }\n\n    protected Element getSearchRoot() {\n        if (currentWindow == null) {\n            logger.warn(\"using desktop as search root, activate a window or parent element for better performance\");\n            return getRoot();\n        }\n        return currentWindow;\n    }\n\n    @Override\n    public Object waitUntil(Supplier<Object> condition) {\n        return waitUntil(condition, true);\n    }\n\n    @Override\n    public Object waitUntilOptional(Supplier<Object> condition) {\n        return waitUntil(condition, false);\n    }\n\n    protected Object waitUntil(Supplier<Object> condition, boolean failWithException) {\n        return retry(() -> condition.get(), o -> o != null, \"waitUntil (function)\", failWithException);\n    }\n\n    @Override\n    public Element waitFor(String locator) {\n        return retryForAny(true, getSearchRoot(), locator);\n    }\n\n    @Override\n    public Element waitForOptional(String locator) {\n        return retryForAny(false, getSearchRoot(), locator);\n    }\n\n    @Override\n    public Element waitForAny(String locator1, String locator2) {\n        return retryForAny(true, getSearchRoot(), locator1, locator2);\n    }\n\n    @Override\n    public Element waitForAny(String[] locators) {\n        return retryForAny(true, getSearchRoot(), locators);\n    }\n\n    protected Element retryForAny(boolean failWithException, Element searchRoot, String... locators) {\n        Element found = retry(() -> waitForAny(searchRoot, locators), r -> r != null, \"find by locator(s): \" + Arrays.asList(locators), failWithException);\n        return found == null ? new MissingElement(this) : found;\n    }\n\n    private Element waitForAny(Element searchRoot, String... locators) {\n        for (String locator : locators) {\n            Element found = locateImageOrElement(searchRoot, locator);\n            if (found != null) {\n                if (highlight) {\n                    found.getRegion().highlight(highlightDuration);\n                }\n                return found;\n            }\n        }\n        return null;\n    }\n\n    private Element locateImageOrElement(Element searchRoot, String locator) {\n        if (locator.endsWith(\".png\")) {\n            return locateImage(searchRoot.getRegion(), locator);\n        } else if (locator.startsWith(\"{\")) {\n            return locateText(searchRoot, locator);\n        } else if (searchRoot.isImage()) {\n            // TODO\n            throw new RuntimeException(\"todo find non-image elements within region\");\n        } else {\n            return locateInternal(searchRoot, locator);\n        }\n    }\n\n    @Override\n    public Element activate(String locator) {\n        return locate(locator).activate();\n    }\n\n    @Override\n    public Element getActive() {\n        if (currentWindow == null) {\n            throw new RuntimeException(\"no window has been selected or activated\");\n        }\n        return currentWindow;\n    }\n\n    @Override\n    public Robot setActive(Element e) {\n        if (e.isPresent()) {\n            currentWindow = e;\n        }\n        return this;\n    }\n\n    public void debugImage(String path) {\n        byte[] bytes = readBytes(path);\n        OpenCvUtils.show(bytes, path);\n    }\n\n    @Override\n    public String getClipboard() {\n        try {\n            return (String) toolkit.getSystemClipboard().getData(DataFlavor.stringFlavor);\n        } catch (Exception e) {\n            logger.warn(\"unable to return clipboard as string: {}\", e.getMessage());\n            return null;\n        }\n    }\n\n    @Override\n    public Location getLocation() {\n        Point p = MouseInfo.getPointerInfo().getLocation();\n        return new Location(this, p.x, p.y);\n    }\n\n    public Location location(int x, int y) {\n        return new Location(this, x, y);\n    }\n\n    public Region region(Map<String, Integer> map) {\n        return new Region(this, map.get(\"x\"), map.get(\"y\"), map.get(\"width\"), map.get(\"height\"));\n    }\n\n    @Override\n    public abstract Element getRoot();\n\n    @Override\n    public abstract Element getFocused();\n\n    //==========================================================================\n    //\n    protected abstract Element windowInternal(String title);\n\n    protected abstract Element windowInternal(Predicate<String> condition);\n\n    protected abstract Element locateInternal(Element root, String locator);\n\n    protected abstract List<Element> locateAllInternal(Element root, String locator);\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/RobotFactory.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport com.intuit.karate.FileUtils;\nimport com.intuit.karate.robot.linux.LinuxRobot;\nimport com.intuit.karate.robot.mac.MacRobot;\nimport com.intuit.karate.robot.win.WinRobot;\nimport java.util.Map;\nimport com.intuit.karate.core.PluginFactory;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport java.util.HashMap;\n\n/**\n *\n * @author pthomas3\n */\npublic class RobotFactory implements PluginFactory {\n    \n    private static final FileUtils.OsType OS_TYPE = FileUtils.getOsType();    \n\n    @Override\n    public Robot create(ScenarioRuntime runtime, Map<String, Object> options) {\n        if (options == null) {\n            options = new HashMap();\n        }\n        switch (OS_TYPE) {\n            case LINUX:\n                return new LinuxRobot(runtime, options);\n            case MACOSX: \n                return new MacRobot(runtime, options);\n            case WINDOWS: \n                return new WinRobot(runtime, options);\n            default:\n                throw new RuntimeException(\"os not supported: \" + OS_TYPE);\n        }\n    }        \n   \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/RobotUtils.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport com.intuit.karate.driver.Keys;\nimport java.awt.BasicStroke;\nimport java.awt.Color;\nimport java.awt.FontMetrics;\nimport java.awt.Graphics;\nimport java.awt.Graphics2D;\nimport java.awt.event.KeyEvent;\nimport java.awt.geom.Rectangle2D;\nimport java.awt.image.BufferedImage;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport javax.swing.BorderFactory;\nimport javax.swing.JComponent;\nimport javax.swing.JFrame;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class RobotUtils {\n\n    private static final Logger logger = LoggerFactory.getLogger(RobotUtils.class);\n\n    public static void highlight(Region region, int time) {\n        JFrame f = new JFrame();\n        f.setUndecorated(true);\n        f.setBackground(new Color(0, 0, 0, 0));\n        f.setAlwaysOnTop(true);\n        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);\n        f.setType(JFrame.Type.UTILITY);\n        f.setFocusableWindowState(false);\n        f.setAutoRequestFocus(false);\n        f.setLocation(region.x, region.y);\n        f.setSize(region.width, region.height);\n        f.getRootPane().setBorder(BorderFactory.createLineBorder(Color.RED, 3));\n        f.setVisible(true);\n        delay(time);\n        f.dispose();\n    }\n\n    static class RegionBox {\n\n        RegionBox(int x, int y, int width, int height, String text) {\n            this.x = x;\n            this.y = y;\n            this.width = width;\n            this.height = height;\n            this.text = text;\n        }\n\n        int x;\n        int y;\n        int width;\n        int height;\n        String text;\n\n    }\n\n    public static void highlightAll(Region parent, List<Element> elements, int time, boolean showValue) {\n        JFrame f = new JFrame();\n        f.setUndecorated(true);\n        f.setBackground(new Color(0, 0, 0, 0));\n        f.setAlwaysOnTop(true);\n        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);\n        f.setType(JFrame.Type.UTILITY);\n        f.setFocusableWindowState(false);\n        f.setAutoRequestFocus(false);\n        f.setLocation(parent.x, parent.y);\n        f.setSize(parent.width, parent.height);\n        f.getRootPane().setBorder(BorderFactory.createLineBorder(Color.YELLOW, 3));\n        // important to extract these so that swing awt ui thread doesn't clash with AUT native ui rendering\n        List<RegionBox> boxes = new ArrayList(elements.size());\n        for (Element e : elements) {\n            Region region = e.getRegion();\n            int x = region.x - parent.x;\n            int y = region.y - parent.y;\n            if (x > 0 && y > 0 && region.width > 0 && region.height > 0) {\n                boxes.add(new RegionBox(x, y, region.width, region.height, showValue ? e.getValue() : null));\n            }\n        }\n        f.add(new JComponent() {\n            @Override\n            public void paint(Graphics g) {\n                Graphics2D g2d = (Graphics2D) g;\n                g2d.setStroke(new BasicStroke(2));\n                for (RegionBox box : boxes) {\n                    g.setColor(Color.RED);\n                    g.drawRect(box.x, box.y, box.width, box.height);\n                    if (showValue) {\n                        String text = box.text;\n                        FontMetrics fm = g.getFontMetrics();\n                        Rectangle2D rect = fm.getStringBounds(text, g);\n                        g.setColor(Color.BLACK);\n                        g.fillRect(box.x, box.y - fm.getAscent(), (int) rect.getWidth(), (int) rect.getHeight());\n                        g.setColor(Color.YELLOW);\n                        g.drawString(box.text, box.x, box.y);\n                    }                    \n                }\n            }\n        });\n        f.setVisible(true);\n        delay(time);\n        if (showValue) {\n            BufferedImage image = new BufferedImage(f.getWidth(), f.getHeight(), BufferedImage.TYPE_INT_RGB);\n            f.paint(image.getGraphics());\n            OpenCvUtils.show(image, parent.toString());\n        }        \n        f.dispose();\n    }\n\n    public static void delay(int millis) {\n        try {\n            Thread.sleep(millis);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    //==========================================================================\n    //\n    public static final Map<Character, int[]> KEY_CODES = new HashMap();\n\n    private static void key(char c, int... i) {\n        KEY_CODES.put(c, i);\n    }\n\n    static {\n        key('a', KeyEvent.VK_A);\n        key('b', KeyEvent.VK_B);\n        key('c', KeyEvent.VK_C);\n        key('d', KeyEvent.VK_D);\n        key('e', KeyEvent.VK_E);\n        key('f', KeyEvent.VK_F);\n        key('g', KeyEvent.VK_G);\n        key('h', KeyEvent.VK_H);\n        key('i', KeyEvent.VK_I);\n        key('j', KeyEvent.VK_J);\n        key('k', KeyEvent.VK_K);\n        key('l', KeyEvent.VK_L);\n        key('m', KeyEvent.VK_M);\n        key('n', KeyEvent.VK_N);\n        key('o', KeyEvent.VK_O);\n        key('p', KeyEvent.VK_P);\n        key('q', KeyEvent.VK_Q);\n        key('r', KeyEvent.VK_R);\n        key('s', KeyEvent.VK_S);\n        key('t', KeyEvent.VK_T);\n        key('u', KeyEvent.VK_U);\n        key('v', KeyEvent.VK_V);\n        key('w', KeyEvent.VK_W);\n        key('x', KeyEvent.VK_X);\n        key('y', KeyEvent.VK_Y);\n        key('z', KeyEvent.VK_Z);\n        key('A', KeyEvent.VK_SHIFT, KeyEvent.VK_A);\n        key('B', KeyEvent.VK_SHIFT, KeyEvent.VK_B);\n        key('C', KeyEvent.VK_SHIFT, KeyEvent.VK_C);\n        key('D', KeyEvent.VK_SHIFT, KeyEvent.VK_D);\n        key('E', KeyEvent.VK_SHIFT, KeyEvent.VK_E);\n        key('F', KeyEvent.VK_SHIFT, KeyEvent.VK_F);\n        key('G', KeyEvent.VK_SHIFT, KeyEvent.VK_G);\n        key('H', KeyEvent.VK_SHIFT, KeyEvent.VK_H);\n        key('I', KeyEvent.VK_SHIFT, KeyEvent.VK_I);\n        key('J', KeyEvent.VK_SHIFT, KeyEvent.VK_J);\n        key('K', KeyEvent.VK_SHIFT, KeyEvent.VK_K);\n        key('L', KeyEvent.VK_SHIFT, KeyEvent.VK_L);\n        key('M', KeyEvent.VK_SHIFT, KeyEvent.VK_M);\n        key('N', KeyEvent.VK_SHIFT, KeyEvent.VK_N);\n        key('O', KeyEvent.VK_SHIFT, KeyEvent.VK_O);\n        key('P', KeyEvent.VK_SHIFT, KeyEvent.VK_P);\n        key('Q', KeyEvent.VK_SHIFT, KeyEvent.VK_Q);\n        key('R', KeyEvent.VK_SHIFT, KeyEvent.VK_R);\n        key('S', KeyEvent.VK_SHIFT, KeyEvent.VK_S);\n        key('T', KeyEvent.VK_SHIFT, KeyEvent.VK_T);\n        key('U', KeyEvent.VK_SHIFT, KeyEvent.VK_U);\n        key('V', KeyEvent.VK_SHIFT, KeyEvent.VK_V);\n        key('W', KeyEvent.VK_SHIFT, KeyEvent.VK_W);\n        key('X', KeyEvent.VK_SHIFT, KeyEvent.VK_X);\n        key('Y', KeyEvent.VK_SHIFT, KeyEvent.VK_Y);\n        key('Z', KeyEvent.VK_SHIFT, KeyEvent.VK_Z);\n        key('1', KeyEvent.VK_1);\n        key('2', KeyEvent.VK_2);\n        key('3', KeyEvent.VK_3);\n        key('4', KeyEvent.VK_4);\n        key('5', KeyEvent.VK_5);\n        key('6', KeyEvent.VK_6);\n        key('7', KeyEvent.VK_7);\n        key('8', KeyEvent.VK_8);\n        key('9', KeyEvent.VK_9);\n        key('0', KeyEvent.VK_0);\n        key('!', KeyEvent.VK_SHIFT, KeyEvent.VK_1);\n        key('@', KeyEvent.VK_SHIFT, KeyEvent.VK_2);\n        key('#', KeyEvent.VK_SHIFT, KeyEvent.VK_3);\n        key('$', KeyEvent.VK_SHIFT, KeyEvent.VK_4);\n        key('%', KeyEvent.VK_SHIFT, KeyEvent.VK_5);\n        key('^', KeyEvent.VK_SHIFT, KeyEvent.VK_6);\n        key('&', KeyEvent.VK_SHIFT, KeyEvent.VK_7);\n        key('*', KeyEvent.VK_SHIFT, KeyEvent.VK_8);\n        key('(', KeyEvent.VK_SHIFT, KeyEvent.VK_9);\n        key(')', KeyEvent.VK_SHIFT, KeyEvent.VK_0);\n        key('`', KeyEvent.VK_BACK_QUOTE);\n        key('~', KeyEvent.VK_SHIFT, KeyEvent.VK_BACK_QUOTE);\n        key('-', KeyEvent.VK_MINUS);\n        key('_', KeyEvent.VK_SHIFT, KeyEvent.VK_MINUS);\n        key('=', KeyEvent.VK_EQUALS);\n        key('+', KeyEvent.VK_SHIFT, KeyEvent.VK_EQUALS);\n        key('[', KeyEvent.VK_OPEN_BRACKET);\n        key('{', KeyEvent.VK_SHIFT, KeyEvent.VK_OPEN_BRACKET);\n        key(']', KeyEvent.VK_CLOSE_BRACKET);\n        key('}', KeyEvent.VK_SHIFT, KeyEvent.VK_CLOSE_BRACKET);\n        key('\\\\', KeyEvent.VK_BACK_SLASH);\n        key('|', KeyEvent.VK_SHIFT, KeyEvent.VK_BACK_SLASH);\n        key(';', KeyEvent.VK_SEMICOLON);\n        key(':', KeyEvent.VK_SHIFT, KeyEvent.VK_SEMICOLON);\n        key('\\'', KeyEvent.VK_QUOTE);\n        key('\"', KeyEvent.VK_SHIFT, KeyEvent.VK_QUOTE);\n        key(',', KeyEvent.VK_COMMA);\n        key('<', KeyEvent.VK_SHIFT, KeyEvent.VK_COMMA);\n        key('.', KeyEvent.VK_PERIOD);\n        key('>', KeyEvent.VK_SHIFT, KeyEvent.VK_PERIOD);\n        key('/', KeyEvent.VK_SLASH);\n        key('?', KeyEvent.VK_SHIFT, KeyEvent.VK_SLASH);\n        //======================================================================\n        key('\\b', KeyEvent.VK_BACK_SPACE);\n        key('\\t', KeyEvent.VK_TAB);\n        key('\\r', KeyEvent.VK_ENTER);\n        key('\\n', KeyEvent.VK_ENTER);\n        key(' ', KeyEvent.VK_SPACE);\n        key(Keys.CONTROL, KeyEvent.VK_CONTROL);\n        key(Keys.ALT, KeyEvent.VK_ALT);\n        key(Keys.META, KeyEvent.VK_META);\n        key(Keys.SHIFT, KeyEvent.VK_SHIFT);\n        key(Keys.TAB, KeyEvent.VK_TAB);\n        key(Keys.ENTER, KeyEvent.VK_ENTER);\n        key(Keys.SPACE, KeyEvent.VK_SPACE);\n        key(Keys.BACK_SPACE, KeyEvent.VK_BACK_SPACE);\n        //======================================================================\n        key(Keys.UP, KeyEvent.VK_UP);\n        key(Keys.RIGHT, KeyEvent.VK_RIGHT);\n        key(Keys.DOWN, KeyEvent.VK_DOWN);\n        key(Keys.LEFT, KeyEvent.VK_LEFT);\n        key(Keys.PAGE_UP, KeyEvent.VK_PAGE_UP);\n        key(Keys.PAGE_DOWN, KeyEvent.VK_PAGE_DOWN);\n        key(Keys.END, KeyEvent.VK_END);\n        key(Keys.HOME, KeyEvent.VK_HOME);\n        key(Keys.DELETE, KeyEvent.VK_DELETE);\n        key(Keys.ESCAPE, KeyEvent.VK_ESCAPE);\n        key(Keys.F1, KeyEvent.VK_F1);\n        key(Keys.F2, KeyEvent.VK_F2);\n        key(Keys.F3, KeyEvent.VK_F3);\n        key(Keys.F4, KeyEvent.VK_F4);\n        key(Keys.F5, KeyEvent.VK_F5);\n        key(Keys.F6, KeyEvent.VK_F6);\n        key(Keys.F7, KeyEvent.VK_F7);\n        key(Keys.F8, KeyEvent.VK_F8);\n        key(Keys.F9, KeyEvent.VK_F9);\n        key(Keys.F10, KeyEvent.VK_F10);\n        key(Keys.F11, KeyEvent.VK_F11);\n        key(Keys.F12, KeyEvent.VK_F12);\n        key(Keys.INSERT, KeyEvent.VK_INSERT);\n        key(Keys.PAUSE, KeyEvent.VK_PAUSE);\n        key(Keys.NUMPAD1, KeyEvent.VK_NUMPAD1);\n        key(Keys.NUMPAD2, KeyEvent.VK_NUMPAD2);\n        key(Keys.NUMPAD3, KeyEvent.VK_NUMPAD3);\n        key(Keys.NUMPAD4, KeyEvent.VK_NUMPAD4);\n        key(Keys.NUMPAD5, KeyEvent.VK_NUMPAD5);\n        key(Keys.NUMPAD6, KeyEvent.VK_NUMPAD6);\n        key(Keys.NUMPAD7, KeyEvent.VK_NUMPAD7);\n        key(Keys.NUMPAD8, KeyEvent.VK_NUMPAD8);\n        key(Keys.NUMPAD9, KeyEvent.VK_NUMPAD9);\n        key(Keys.NUMPAD0, KeyEvent.VK_NUMPAD0);\n        key(Keys.SEPARATOR, KeyEvent.VK_SEPARATOR);\n        key(Keys.ADD, KeyEvent.VK_ADD);\n        key(Keys.SUBTRACT, KeyEvent.VK_SUBTRACT);\n        key(Keys.MULTIPLY, KeyEvent.VK_MULTIPLY);\n        key(Keys.DIVIDE, KeyEvent.VK_DIVIDE);\n        key(Keys.DECIMAL, KeyEvent.VK_DECIMAL);\n        // TODO SCROLL_LOCK, NUM_LOCK, CAPS_LOCK, PRINTSCREEN, CONTEXT_MENU, WINDOWS\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/StringMatcher.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport java.util.function.Predicate;\n\n/**\n *\n * @author pthomas3\n */\npublic class StringMatcher implements Predicate<String> {\n    \n    private final String orig;\n    private final String text;\n    private final Predicate<String> pred;\n    \n    public StringMatcher(String raw) {\n        this.orig = raw;\n        if (raw.startsWith(\"^\")) {\n            text = raw.substring(1);\n            pred = s -> s.contains(text);\n        } else if (raw.startsWith(\"~\")) {\n            text = raw.substring(1);\n            java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(text);\n            pred = s -> pattern.matcher(s).find();\n        } else {\n            text = raw;\n            pred = s -> s.equals(text);            \n        }        \n    }\n\n    @Override\n    public boolean test(String t) {\n        return pred.test(t);\n    }\n\n    @Override\n    public String toString() {\n        return orig;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/Tesseract.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.StringTokenizer;\nimport java.util.function.Supplier;\nimport org.bytedeco.javacpp.BytePointer;\nimport org.bytedeco.javacpp.IntPointer;\nimport org.bytedeco.opencv.opencv_core.Mat;\nimport org.bytedeco.tesseract.ResultIterator;\nimport org.bytedeco.tesseract.TessBaseAPI;\nimport org.bytedeco.tesseract.global.tesseract;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class Tesseract {\n\n    private static final Logger logger = LoggerFactory.getLogger(Tesseract.class);\n\n    private final TessBaseAPI tess;\n    private final Supplier<IntPointer> INT = () -> new IntPointer(new int[1]);\n\n    private String allText;\n    private List<Word> words;\n\n    public String getAllText() {\n        return allText;\n    }\n\n    public List<Word> getWords() {\n        return words;\n    }\n\n    public static class Word {\n\n        final String text;\n        final int x;\n        final int y;\n        final int width;\n        final int height;\n        final float confidence;\n\n        final Word prev;\n        Word next;\n\n        public Word(Word prev, String text, int x, int y, int width, int height, float confidence) {\n            if (prev != null) {\n                prev.next = this;\n            }\n            this.prev = prev;\n            this.text = text;\n            this.x = x;\n            this.y = y;\n            this.width = width;\n            this.height = height;\n            this.confidence = confidence;\n        }\n\n        @Override\n        public String toString() {\n            return text + \" \" + x + \":\" + y + \"(\" + width + \":\" + height + \") \" + confidence;\n        }\n\n    }\n\n    public Tesseract(File dataDir, String language) {\n        tess = new TessBaseAPI();\n        String dataPath = dataDir.getPath();\n        if (tess.Init(dataPath, language) != 0) {\n            throw new RuntimeException(\"tesseract init failed: \" + dataDir.getAbsolutePath() + \", \" + language);\n        }\n    }\n\n    public static final Tesseract init(RobotBase robot, String lang, Region region, boolean negative) {\n        File file = new File(robot.tessData);\n        Tesseract tess = new Tesseract(file, lang);\n        tess.process(region, negative);\n        return tess;\n    }\n\n    public static Element find(RobotBase robot, String lang, Region sr, String text, boolean negative) {\n        Tesseract tess = init(robot, lang, sr, negative);\n        List<int[]> list = tess.find(false, text);\n        if (list.isEmpty()) {\n            return null;\n        }\n        int[] b = list.get(0);\n        Region region = new Region(robot, sr.x + b[0], sr.y + b[1], b[2], b[3]);\n        return new ImageElement(region);\n    }\n\n    public static List<Element> findAll(RobotBase robot, String lang, Region sr, String text, boolean negative) {\n        Tesseract tess = init(robot, lang, sr, negative);\n        List<int[]> list = tess.find(true, text);\n        List<Element> found = new ArrayList(list.size());\n        for (int[] b : list) {\n            Region region = new Region(robot, sr.x + b[0], sr.y + b[1], b[2], b[3]);\n            found.add(new ImageElement(region));\n        }\n        return found;\n    }\n\n    public void process(Region region, boolean negative) {\n        BufferedImage bi = region.captureGreyScale();\n        if (region.robot.highlight) {\n            region.highlight(region.robot.highlightDuration);\n        }\n        Mat mat = OpenCvUtils.toMat(bi);\n        process(mat, negative);\n    }\n\n    public void highlightWords(RobotBase robot, Region parent, int millis) {\n        List<Element> elements = new ArrayList();\n        for (Tesseract.Word word : words) {\n            Region region = new Region(robot, parent.x + word.x, parent.y + word.y, word.width, word.height);\n            Element e = new ImageElement(region, word.text);\n            elements.add(e);\n        }\n        RobotUtils.highlightAll(parent, elements, millis, true);\n    }\n\n    public void process(Mat mat, boolean negative) {\n        if (negative) {\n            mat = OpenCvUtils.negative(mat);\n        }\n        int srcWidth = mat.size().width();\n        int srcHeight = mat.size().height();\n        int channels = mat.channels();\n        int bytesPerLine = srcWidth * channels * (int) mat.elemSize1();\n        tess.SetImage(mat.data().asBuffer(), srcWidth, srcHeight, channels, bytesPerLine);\n        //======================================================================\n        BytePointer textPtr = tess.GetUTF8Text();\n        allText = textPtr.getString();\n        textPtr.deallocate();\n        //======================================================================\n        ResultIterator ri = tess.GetIterator();\n        int level = tesseract.RIL_WORD;\n        words = new ArrayList();\n        Word prev = null;\n        do {\n            float confidence = ri.Confidence(level);\n            if (confidence < 50) {\n                continue;\n            }\n            textPtr = ri.GetUTF8Text(level);\n            String text = textPtr.getString().trim();\n            textPtr.deallocate();\n            IntPointer x1 = INT.get();\n            IntPointer y1 = INT.get();\n            IntPointer x2 = INT.get();\n            IntPointer y2 = INT.get();\n            boolean found = ri.BoundingBox(level, x1, y1, x2, y2);\n            int x = x1.get();\n            int y = y1.get();\n            int width = x2.get() - x;\n            int height = y2.get() - y;\n            if (!found) {\n                logger.warn(\"no such rectangle: {}:{}:{}:{}\", x, y, width, height);\n                continue;\n            }\n            Word word = new Word(prev, text, x, y, width, height, confidence);\n            words.add(word);\n            prev = word;\n        } while (ri.Next(level));\n    }\n\n    public List<int[]> find(boolean findAll, String text) {\n        StringTokenizer st = new StringTokenizer(text);\n        String[] args = new String[st.countTokens()];\n        for (int i = 0; st.hasMoreTokens(); i++) {\n            args[i] = st.nextToken();\n        }\n        List<int[]> list = new ArrayList();\n        for (Word w : words) {\n            boolean found = false;\n            int i = 0;\n            Word current = w;\n            Word prev = null;\n            do {\n                String s = args[i];\n                found = s.contains(current.text);\n                prev = current;\n                current = current.next;\n            } while (found && ++i < args.length && current != null);\n            if (found && i == args.length) {\n                Word first = w;\n                Word last = prev;\n                int x = first.x;\n                int y = first.y;\n                int width = last.x + last.width - first.x;\n                int height = Math.max(first.height, last.height);\n                int[] bounds = new int[]{x, y, width, height};\n                if (!findAll) {\n                    return Collections.singletonList(bounds);\n                }\n                list.add(bounds);\n            }\n        }\n        return list;\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/Window.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot;\n\n/**\n *\n * @author pthomas3\n */\npublic interface Window extends Element {\n    \n    void close();\n    \n    void restore();\n    \n    void minimize();\n    \n    void maximize();    \n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/linux/LinuxRobot.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.linux;\n\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.robot.Element;\nimport com.intuit.karate.robot.ImageElement;\nimport com.intuit.karate.robot.RobotBase;\nimport com.intuit.karate.robot.Window;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.shell.Command;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\n\n/**\n *\n * @author pthomas3\n */\npublic class LinuxRobot extends RobotBase {\n\n    public LinuxRobot(ScenarioRuntime runtime, Map<String, Object> options) {\n        super(runtime, options);\n    }\n\n    @Override\n    public Map<String, Object> afterScenario() {\n        return Collections.EMPTY_MAP;\n    }\n\n    @Override\n    protected Element windowInternal(String title) {\n        Command.exec(true, null, \"wmctrl\", \"-FR\", title);\n        return new ImageElement(screen); // TODO\n    }\n\n    @Override\n    public Element windowInternal(Predicate<String> condition) {\n        String res = Command.exec(true, null, \"wmctrl\", \"-l\");\n        List<String> lines = StringUtils.split(res, '\\n', false);\n        for (String line : lines) {\n            List<String> cols = StringUtils.split(line, ' ', false);\n            String id = cols.get(0);\n            String host = cols.get(2);\n            int pos = line.indexOf(host);\n            String name = line.substring(pos + host.length() + 1);\n            if (condition.test(name)) {\n                Command.exec(true, null, \"wmctrl\", \"-iR\", id);\n                return new ImageElement(screen); // TODO\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public List<Element> locateAllInternal(Element searchRoot, String locator) {\n        throw new UnsupportedOperationException(\"not supported yet.\");\n    }\n\n    @Override\n    public Element locateInternal(Element root, String locator) {\n        throw new UnsupportedOperationException(\"not supported yet.\");\n    }\n\n    @Override\n    public Element getRoot() {\n        return new ImageElement(screen); // TODO\n    }\n\n    @Override\n    public Element getFocused() {\n        return new ImageElement(screen); // TODO\n    }\n\n    @Override\n    public List<Window> getAllWindows() {\n        throw new UnsupportedOperationException(\"not supported yet.\");\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/mac/MacRobot.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.mac;\n\nimport com.intuit.karate.StringUtils;\nimport com.intuit.karate.robot.Element;\nimport com.intuit.karate.robot.ImageElement;\nimport com.intuit.karate.robot.RobotBase;\nimport com.intuit.karate.robot.Window;\nimport com.intuit.karate.core.ScenarioEngine;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.shell.Command;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\n\n/**\n *\n * @author pthomas3\n */\npublic class MacRobot extends RobotBase {\n\n    public MacRobot(ScenarioRuntime runtime, Map<String, Object> options) {\n        super(runtime, options);\n    }\n\n    @Override\n    public Map<String, Object> afterScenario() {\n        return Collections.EMPTY_MAP;\n    }\n\n    private static final String MAC_GET_PROCS\n            = \"    tell application \\\"System Events\\\"\"\n            + \"\\n    set procs to (processes whose background only is false)\"\n            + \"\\n    set results to {}\"\n            + \"\\n    repeat with n from 1 to the length of procs\"\n            + \"\\n      set p to item n of procs\"\n            + \"\\n      set entry to { name of p as text,\\\"|\\\"}\"\n            + \"\\n      set end of results to entry\"\n            + \"\\n    end repeat\"\n            + \"\\n  end tell\"\n            + \"\\n  results\";\n\n    public static List<String> getAppsMacOs() {\n        String res = Command.exec(true, null, \"osascript\", \"-e\", MAC_GET_PROCS);\n        res = res + \", \";\n        res = res.replace(\", |, \", \"\\n\");\n        return StringUtils.split(res, '\\n', false);\n    }\n\n    @Override\n    public Element windowInternal(String title) {\n        Command.exec(true, null, \"osascript\", \"-e\", \"tell app \\\"\" + title + \"\\\" to activate\");\n        return new ImageElement(screen); // TODO\n    }\n\n    @Override\n    public Element windowInternal(Predicate<String> condition) {\n        List<String> list = getAppsMacOs();\n        for (String s : list) {\n            if (condition.test(s)) {\n                Command.exec(true, null, \"osascript\", \"-e\", \"tell app \\\"\" + s + \"\\\" to activate\");\n                return new ImageElement(screen); // TODO\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public List<Element> locateAllInternal(Element searchRoot, String locator) {\n        throw new UnsupportedOperationException(\"not supported yet.\");\n    } \n\n    @Override\n    public Element locateInternal(Element root, String locator) {\n        throw new UnsupportedOperationException(\"not supported yet.\");\n    }\n\n    @Override\n    public Element getRoot() {\n        return new ImageElement(screen);\n    }\n\n    @Override\n    public Element getFocused() {\n        return new ImageElement(screen);\n    }\n\n    @Override\n    public List<Window> getAllWindows() {\n        throw new UnsupportedOperationException(\"not supported yet.\");\n    }        \n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/ComAllocated.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\n/**\n *\n * @author pthomas3\n */\npublic interface ComAllocated {\n    \n    Object value();\n    \n    void free();\n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/ComAllocatedStr.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.sun.jna.platform.win32.OleAuto;\nimport com.sun.jna.platform.win32.WTypes;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ComAllocatedStr implements ComAllocated {\n    \n    private static final Logger logger = LoggerFactory.getLogger(ComAllocatedStr.class);\n\n    private final String value;\n    private final WTypes.BSTR sysAllocated;\n\n    public ComAllocatedStr(String value) {\n        this.value = value;\n        sysAllocated = OleAuto.INSTANCE.SysAllocString(value);\n        if (logger.isTraceEnabled()) {\n            logger.trace(\"allocated string: '{}'\", value);\n        }\n    }\n\n    @Override\n    public Object value() {\n        return sysAllocated;\n    }    \n    \n    @Override\n    public void free() {\n        OleAuto.INSTANCE.SysFreeString(sysAllocated);\n        if (logger.isTraceEnabled()) {\n            logger.trace(\"dellocated string: '{}'\", value);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/ComAllocatedVarInt.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.sun.jna.platform.win32.Variant;\n\n/**\n *\n * @author pthomas3\n */\npublic class ComAllocatedVarInt implements ComAllocated {\n\n    private final Variant.VARIANT variant;\n\n    public ComAllocatedVarInt(int value) {\n        this.variant = new Variant.VARIANT.ByValue();\n        variant.setValue(Variant.VT_INT, value);\n    }\n\n    @Override\n    public Object value() {\n        return variant;\n    }\n\n    @Override\n    public void free() {\n        // do nothing\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/ComAllocatedVarStr.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.sun.jna.platform.win32.OleAuto;\nimport com.sun.jna.platform.win32.Variant;\nimport com.sun.jna.platform.win32.WTypes;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ComAllocatedVarStr implements ComAllocated {\n    \n    private static final Logger logger = LoggerFactory.getLogger(ComAllocatedVarStr.class);\n\n    private final String value;\n    private final Variant.VARIANT variant;\n    private final WTypes.BSTR sysAllocated;\n\n    public ComAllocatedVarStr(String value) {\n        this.value = value;\n        variant = new Variant.VARIANT.ByValue();\n        sysAllocated = OleAuto.INSTANCE.SysAllocString(value);\n        variant.setValue(Variant.VT_BSTR, sysAllocated);\n        if (logger.isTraceEnabled()) {\n            logger.trace(\"allocated string: '{}'\", value);\n        }\n    }\n\n    @Override\n    public Object value() {\n        return variant;\n    }    \n    \n    @Override\n    public void free() {\n        OleAuto.INSTANCE.SysFreeString(sysAllocated);\n        if (logger.isTraceEnabled()) {\n            logger.trace(\"dellocated string: '{}'\", value);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/ComFunction.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class ComFunction {\n    \n    public final String name;\n    public final int vtableId;\n    public final int memberId;\n    public final List<String> args = new ArrayList();\n    \n    public ComFunction(String name, int vtableId, int memberId) {\n        this.name = name;\n        this.vtableId = vtableId;\n        this.memberId = memberId;\n    }\n    \n    public void addArg(String arg) {\n        args.add(arg);\n    }\n\n    @Override\n    public String toString() {\n        return vtableId + \" \" + name + \" \" + args;\n    }        \n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/ComInterface.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.sun.jna.Function;\nimport com.sun.jna.Native;\nimport com.sun.jna.Pointer;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic class ComInterface {\n\n    public final String name;\n    public final String implementing;\n    public final String guid;\n    public final Map<String, ComFunction> functions = new LinkedHashMap();\n\n    public ComInterface(String name, String implementing, String guid) {\n        this.name = name;\n        this.implementing = implementing;\n        this.guid = guid;\n    }\n\n    public void add(ComFunction function) {\n        functions.put(function.name, function);\n    }\n\n    public Function getFunction(String functionName, Pointer p) {\n        ComFunction cf = functions.get(functionName);\n        if (cf == null) {\n           throw new RuntimeException(\"no such function: \" + functionName + \" in: \" + name);\n        }\n        Pointer tableRef = p.getPointer(0);\n        Pointer functionRef = tableRef.getPointer(cf.vtableId);\n        return Function.getFunction(functionRef, Function.ALT_CONVENTION);\n    }\n\n    public Function getFunction(int offset, Pointer p) {\n        Pointer tableRef = p.getPointer(0);\n        Pointer functionRef = tableRef.getPointer(offset * Native.POINTER_SIZE);\n        return Function.getFunction(functionRef, Function.ALT_CONVENTION);\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(name).append(\" [\").append(implementing).append(\"] \").append(guid);\n        for (ComFunction cf : functions.values()) {\n            sb.append('\\n').append(cf);\n        }\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/ComLibrary.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.sun.jna.platform.win32.COM.COMUtils;\nimport com.sun.jna.platform.win32.COM.ITypeInfo;\nimport com.sun.jna.platform.win32.COM.TypeInfo;\nimport com.sun.jna.platform.win32.COM.TypeLib;\nimport com.sun.jna.platform.win32.Guid;\nimport com.sun.jna.platform.win32.Kernel32;\nimport com.sun.jna.platform.win32.OaIdl;\nimport com.sun.jna.platform.win32.Ole32;\nimport com.sun.jna.platform.win32.OleAuto;\nimport com.sun.jna.platform.win32.Variant;\nimport com.sun.jna.platform.win32.WTypes;\nimport com.sun.jna.platform.win32.WinDef;\nimport com.sun.jna.platform.win32.WinNT;\nimport com.sun.jna.ptr.PointerByReference;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ComLibrary {\n\n    private static final Logger logger = LoggerFactory.getLogger(ComLibrary.class);      \n\n    public final String name;\n    public final String clsId;\n    public final int majorVersion;\n    public final int minorVersion;\n    public final Map<String, Map<String, Integer>> enumKeyValues = new HashMap();\n    public final Map<String, Map<Integer, String>> enumValueKeys = new HashMap();\n    public final Map<String, ComInterface> interfaces = new HashMap();\n\n    public ComLibrary(String typeLibClsId, int majorVersion, int minorVersion) {\n        // load registered class id\n        WinDef.LCID lcId = Kernel32.INSTANCE.GetUserDefaultLCID();\n        Guid.CLSID.ByReference clsIdRef = new Guid.CLSID.ByReference();\n        WinNT.HRESULT hr = Ole32.INSTANCE.CLSIDFromString(typeLibClsId, clsIdRef);\n        COMUtils.checkRC(hr);\n\n        // load type library\n        PointerByReference typeLibRef = new PointerByReference();\n        hr = OleAuto.INSTANCE.LoadRegTypeLib(clsIdRef, majorVersion, minorVersion, lcId, typeLibRef);\n        COMUtils.checkRC(hr);\n\n        TypeLib typeLib = new TypeLib(typeLibRef.getValue());\n        name = getName(typeLib, -1);\n        clsId = clsIdRef.toGuidString();\n        this.majorVersion = majorVersion;\n        this.minorVersion = minorVersion;\n        logger.debug(\"loaded: {}, clsid: {}, majorVersion: {}, minorVersion: {}\",\n                name, clsId, majorVersion, minorVersion);\n\n        int typeCount = typeLib.GetTypeInfoCount().intValue();\n        if (logger.isTraceEnabled()) {\n            logger.trace(\"name: {}, types: {}\", name, typeCount);\n        }\n        for (int i = 0; i < typeCount; i++) {\n            String typeName = getName(typeLib, i);\n            ITypeInfo typeInfo = getTypeInfo(typeLib, i);\n            OaIdl.TYPEATTR typeAttr = getTypeAttr(typeInfo);\n            String guid = typeAttr.guid.toGuidString();\n            OaIdl.TYPEKIND typeKind = getTypeKind(typeLib, i);\n            switch (typeKind.value) {\n                case OaIdl.TYPEKIND.TKIND_ENUM:\n                case OaIdl.TYPEKIND.ALIGN_GNUC: // UIA_PropertyIds etc.                    \n                    getEnums(typeName, typeInfo, typeAttr);\n                    break;\n                case OaIdl.TYPEKIND.TKIND_INTERFACE:\n                case OaIdl.TYPEKIND.TKIND_DISPATCH:\n                case OaIdl.TYPEKIND.TKIND_COCLASS:\n                    getInterfaces(guid, typeName, typeInfo, typeAttr);\n                    break;\n                default:\n                    if (logger.isTraceEnabled()) {\n                        logger.trace(\"==== ignore: {}\", typeName);\n                    }\n            }\n        }\n    }\n\n    //==========================================================================\n    //\n    private void getInterfaces(String guid, String interfaceName, ITypeInfo typeInfo, OaIdl.TYPEATTR typeAttr) {\n        int implCount = typeAttr.cImplTypes.intValue();\n        if (implCount > 0) {\n            for (int i = 0; i < implCount; i++) {\n                OaIdl.HREFTYPE refTypeOfImplType = getRefType(typeInfo, i);\n                ITypeInfo refTypeInfo = getRefTypeInfo(typeInfo, refTypeOfImplType);\n                String implementingName = getName(refTypeInfo, new OaIdl.MEMBERID(-1));\n                ComInterface ci = new ComInterface(interfaceName, implementingName, guid);\n                interfaces.put(interfaceName, ci);\n                getFunctions(ci, typeInfo);\n                if (logger.isTraceEnabled()) {\n                    logger.trace(\"==== interface: {}\", ci);\n                }\n            }\n        }\n    }\n\n    private void getFunctions(ComInterface ci, ITypeInfo typeInfo) {\n        OaIdl.TYPEATTR typeAttr = getTypeAttr(typeInfo);\n        int count = typeAttr.cFuncs.intValue();\n        for (int i = 0; i < count; i++) {\n            OaIdl.FUNCDESC funcDesc = getFuncDesc(typeInfo, i);\n            int paramCount = funcDesc.cParams.shortValue();\n            int vtableId = funcDesc.oVft.intValue();\n            int memberId = funcDesc.memid.intValue();\n            String[] names = getNames(typeInfo, funcDesc.memid, paramCount + 1);\n            String functionName = names[0];\n            ComFunction cf = new ComFunction(functionName, vtableId, memberId);\n            ci.add(cf);\n            getArgs(cf, names, typeInfo, funcDesc);\n        }\n    }\n\n    private void getArgs(ComFunction cf, String[] names, ITypeInfo typeInfo, OaIdl.FUNCDESC funcDesc) {\n        for (int i = 1; i < names.length; i++) {\n            OaIdl.ELEMDESC elemdesc = funcDesc.lprgelemdescParam.elemDescArg[i - 1];\n            cf.addArg(names[i]);\n        }\n    }\n\n    private static String[] getNames(ITypeInfo typeInfo, OaIdl.MEMBERID memberId, int maxNames) {\n        WTypes.BSTR[] namesRef = new WTypes.BSTR[maxNames];\n        WinDef.UINTByReference indexRef = new WinDef.UINTByReference();\n        WinNT.HRESULT hr = typeInfo.GetNames(memberId, namesRef, new WinDef.UINT(maxNames), indexRef);\n        COMUtils.checkRC(hr);\n        int cNames = indexRef.getValue().intValue();\n        String[] result = new String[cNames];\n        for (int i = 0; i < result.length; i++) {\n            result[i] = namesRef[i].getValue();\n            OleAuto.INSTANCE.SysFreeString(namesRef[i]);\n        }\n        return result;\n    }\n\n    private static OaIdl.FUNCDESC getFuncDesc(ITypeInfo typeInfo, int index) {\n        PointerByReference funcDescRef = new PointerByReference();\n        WinNT.HRESULT hr = typeInfo.GetFuncDesc(new WinDef.UINT(index), funcDescRef);\n        COMUtils.checkRC(hr);\n        return new OaIdl.FUNCDESC(funcDescRef.getValue());\n    }\n\n    private static OaIdl.HREFTYPE getRefType(ITypeInfo typeInfo, int index) {\n        OaIdl.HREFTYPEByReference refTypeRef = new OaIdl.HREFTYPEByReference();\n        WinNT.HRESULT hr = typeInfo.GetRefTypeOfImplType(new WinDef.UINT(index), refTypeRef);\n        COMUtils.checkRC(hr);\n        return refTypeRef.getValue();\n    }\n\n    private static ITypeInfo getRefTypeInfo(ITypeInfo typeInfo, OaIdl.HREFTYPE hrefType) {\n        PointerByReference refTypeInfoRef = new PointerByReference();\n        WinNT.HRESULT hr = typeInfo.GetRefTypeInfo(hrefType, refTypeInfoRef);\n        COMUtils.checkRC(hr);\n        return new TypeInfo(refTypeInfoRef.getValue());\n    }\n\n    private void getEnums(String enumName, ITypeInfo typeInfo, OaIdl.TYPEATTR typeAttr) {\n        int varCount = typeAttr.cVars.intValue();\n        Map<String, Integer> keyValues = new LinkedHashMap();\n        this.enumKeyValues.put(enumName, keyValues); \n        Map<Integer, String> valueKeys = new HashMap();\n        this.enumValueKeys.put(enumName, valueKeys);        \n        if (varCount > 0) {\n            for (int i = 0; i < varCount; i++) {\n                OaIdl.VARDESC varDesc = getVarDesc(typeInfo, i);\n                Variant.VARIANT constValue = varDesc._vardesc.lpvarValue;\n                Object value = constValue.getValue();\n                OaIdl.MEMBERID memberId = varDesc.memid;\n                String name = getName(typeInfo, memberId);\n                Integer intValue = Integer.valueOf(value.toString());\n                keyValues.put(name, intValue);\n                valueKeys.put(intValue, name);\n            }\n        }\n        if (logger.isTraceEnabled()) {\n            logger.trace(\"enum: {} - {}\", enumName, keyValues);\n        }\n    }\n\n    private static OaIdl.VARDESC getVarDesc(ITypeInfo typeInfo, int index) {\n        PointerByReference varDescRef = new PointerByReference();\n        WinNT.HRESULT hr = typeInfo.GetVarDesc(new WinDef.UINT(index), varDescRef);\n        COMUtils.checkRC(hr);\n        return new OaIdl.VARDESC(varDescRef.getValue());\n    }\n\n    private static String getName(TypeLib typeLib, int index) {\n        WTypes.BSTRByReference nameRef = new WTypes.BSTRByReference();\n        WTypes.BSTRByReference docRef = new WTypes.BSTRByReference();\n        WinDef.DWORDByReference helpRef = new WinDef.DWORDByReference();\n        WTypes.BSTRByReference helpFileRef = new WTypes.BSTRByReference();\n        WinNT.HRESULT hr = typeLib.GetDocumentation(index, nameRef, docRef, helpRef, helpFileRef);\n        COMUtils.checkRC(hr);\n        String name = nameRef.getString();\n        OleAuto.INSTANCE.SysFreeString(nameRef.getValue());\n        OleAuto.INSTANCE.SysFreeString(docRef.getValue());\n        OleAuto.INSTANCE.SysFreeString(helpFileRef.getValue());\n        return name;\n    }\n\n    private static String getName(ITypeInfo typeInfo, OaIdl.MEMBERID memberId) {\n        WTypes.BSTRByReference nameRef = new WTypes.BSTRByReference();\n        WTypes.BSTRByReference docRef = new WTypes.BSTRByReference();\n        WinDef.DWORDByReference helpRef = new WinDef.DWORDByReference();\n        WTypes.BSTRByReference helpFileRef = new WTypes.BSTRByReference();\n        WinNT.HRESULT hr = typeInfo.GetDocumentation(memberId, nameRef, docRef, helpRef, helpFileRef);\n        COMUtils.checkRC(hr);\n        String name = nameRef.getString();\n        OleAuto.INSTANCE.SysFreeString(nameRef.getValue());\n        OleAuto.INSTANCE.SysFreeString(docRef.getValue());\n        OleAuto.INSTANCE.SysFreeString(helpFileRef.getValue());\n        return name;\n    }\n\n    private static OaIdl.TYPEKIND getTypeKind(TypeLib typeLib, int index) {\n        OaIdl.TYPEKIND.ByReference typeKind = new OaIdl.TYPEKIND.ByReference();\n        WinNT.HRESULT hr = typeLib.GetTypeInfoType(new WinDef.UINT(index), typeKind);\n        COMUtils.checkRC(hr);\n        return typeKind;\n    }\n\n    private static ITypeInfo getTypeInfo(TypeLib typeLib, int index) {\n        PointerByReference typeInfoRef = new PointerByReference();\n        WinNT.HRESULT hr = typeLib.GetTypeInfo(new WinDef.UINT(index), typeInfoRef);\n        COMUtils.checkRC(hr);\n        return new TypeInfo(typeInfoRef.getValue());\n    }\n\n    private static OaIdl.TYPEATTR getTypeAttr(ITypeInfo typeInfo) {\n        PointerByReference typeAttrRef = new PointerByReference();\n        WinNT.HRESULT hr = typeInfo.GetTypeAttr(typeAttrRef);\n        COMUtils.checkRC(hr);\n        return new OaIdl.TYPEATTR(typeAttrRef.getValue());\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/ComRef.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.sun.jna.ptr.PointerByReference;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ComRef {\n\n    protected static final Logger logger = LoggerFactory.getLogger(ComRef.class);\n\n    protected final PointerByReference REF;\n    private boolean valid = true;\n\n    public ComRef() {\n        REF = new PointerByReference();\n    }\n\n    public ComRef(PointerByReference ref) {\n        this.REF = ref;\n    }\n    \n    public boolean isNull() {\n        return REF.getValue() == null;\n    }    \n\n    public int asInt() {\n        return REF.getValue().getInt(0);\n    }\n\n    public String asString() {\n        return REF.getValue().getWideString(0);\n    }\n\n    public void setValid(boolean valid) {\n        this.valid = valid;\n    }\n\n    public boolean isValid() {\n        return valid && !isNull();\n    }        \n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/ComUtils.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.sun.jna.Pointer;\nimport com.sun.jna.platform.win32.COM.COMUtils;\nimport com.sun.jna.platform.win32.Guid;\nimport com.sun.jna.platform.win32.Ole32;\nimport com.sun.jna.platform.win32.WTypes;\nimport com.sun.jna.platform.win32.WinNT;\nimport com.sun.jna.ptr.PointerByReference;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\npublic class ComUtils {\n    \n    protected static final Logger logger = LoggerFactory.getLogger(ComUtils.class);\n    \n    private ComUtils() {\n        // only static methods\n    }\n    \n    public static final ComLibrary LIBRARY;\n    public static final ComInterface AUTO_INTERFACE;\n    public static final PointerByReference AUTO_REF;\n\n    static {\n        LIBRARY = new ComLibrary(\"{944DE083-8FB8-45CF-BCB7-C477ACB2F897}\", 1, 0);\n        ComInterface autoClass = LIBRARY.interfaces.get(\"CUIAutomation\");\n        AUTO_INTERFACE = LIBRARY.interfaces.get(\"IUIAutomation\");\n        Ole32.INSTANCE.CoInitializeEx(Pointer.NULL, Ole32.COINIT_APARTMENTTHREADED);\n        Guid.GUID CLSID = new Guid.GUID(autoClass.guid);\n        Guid.IID IID = new Guid.IID(AUTO_INTERFACE.guid);\n        AUTO_REF = new PointerByReference();\n        WinNT.HRESULT hr = Ole32.INSTANCE.CoCreateInstance(CLSID, null, WTypes.CLSCTX_SERVER, IID, AUTO_REF);\n        COMUtils.checkRC(hr);\n        logger.debug(\"init: {}, interface id: {}\", autoClass.guid, AUTO_INTERFACE.guid);\n    } \n    \n    public static int enumValue(String name, String key) {\n        Map<String, Integer> map = LIBRARY.enumKeyValues.get(name);\n        if (map == null) {\n            throw new RuntimeException(\"no such enum: \" + name);\n        }\n        Integer value = map.get(key);\n        if (value == null) {\n            throw new RuntimeException(\"enum: \" + name + \" does not contain key: \" + key);\n        }\n        return value;\n    }    \n    \n    public static String enumKey(String name, int value) {\n        Map<Integer, String> map = LIBRARY.enumValueKeys.get(name);\n        if (map == null) {\n            throw new RuntimeException(\"no such enum: \" + name);\n        }\n        String key = map.get(value);\n        if (key == null) {\n            throw new RuntimeException(\"enum: \" + name + \" does not contain value: \" + value);\n        }\n        return key;\n    }     \n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/ControlType.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic enum ControlType {\n\n    Button(50000),\n    Calendar(50001),\n    CheckBox(50002),\n    ComboBox(50003),\n    Edit(50004),\n    Hyperlink(50005),\n    Image(50006),\n    ListItem(50007),\n    List(50008),\n    Menu(50009),\n    MenuBar(50010),\n    MenuItem(50011),\n    ProgressBar(50012),\n    RadioButton(50013),\n    ScrollBar(50014),\n    Slider(50015),\n    Spinner(50016),\n    StatusBar(50017),\n    Tab(50018),\n    TabItem(50019),\n    Text(50020),\n    ToolBar(50021),\n    ToolTip(50022),\n    Tree(50023),\n    TreeItem(50024),\n    Custom(50025),\n    Group(50026),\n    Thumb(50027),\n    DataGrid(50028),\n    DataItem(50029),\n    Document(50030),\n    SplitButton(50031),\n    Window(50032),\n    Pane(50033),\n    Header(50034),\n    HeaderItem(50035),\n    Table(50036),\n    TitleBar(50037),\n    Separator(50038),\n    SemanticZoom(50039),\n    AppBar(50040);\n\n    public final int value;\n\n    private ControlType(int value) {\n        this.value = value;\n    }\n\n    private final static Map<Integer, ControlType> FROM_VALUE;\n    private final static Map<String, ControlType> FROM_NAME;\n\n    static {\n        ControlType[] values = ControlType.values();\n        FROM_VALUE = new HashMap(values.length);\n        FROM_NAME = new HashMap(values.length);\n        for (ControlType ct : values) {\n            FROM_VALUE.put(ct.value, ct);\n            FROM_NAME.put(ct.name().toLowerCase(), ct);\n        }\n    }\n\n    public static ControlType fromValue(int value) {\n        return FROM_VALUE.get(value);\n    }\n    \n    public static ControlType fromName(String name) {\n        return FROM_NAME.get(name.toLowerCase());\n    }    \n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/IUIAutomation.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.sun.jna.platform.win32.WinDef;\n\n/**\n *\n * @author pthomas3\n */\npublic class IUIAutomation extends IUIAutomationBase {\n    \n    public static final IUIAutomation INSTANCE = new IUIAutomation();\n\n    private IUIAutomation() {\n        super(ComUtils.AUTO_REF); // important !\n    }\n\n    public IUIAutomationElement getRootElement() {\n        return invokeForElement(\"GetRootElement\");\n    }\n\n    public IUIAutomationElement getFocusedElement() {\n        return invokeForElement(\"GetFocusedElement\");\n    }\n\n    public IUIAutomationElement elementFromPoint(int x, int y) {\n        return invokeForElement(\"ElementFromPoint\", new WinDef.POINT(x, y));\n    }\n    \n    public IUIAutomationElement elementFromHandle(WinDef.HWND hwnd) {\n        return invokeForElement(\"ElementFromHandle\", hwnd);\n    }    \n\n    public IUIAutomationCondition createTrueCondition() {\n        return invokeForCondition(\"CreateTrueCondition\");\n    }\n\n    public IUIAutomationCondition createFalseCondition() {\n        return invokeForCondition(\"CreateFalseCondition\");\n    }\n\n    public IUIAutomationCondition createPropertyCondition(Property property, ComAllocated value) {\n        return invokeForCondition(\"CreatePropertyCondition\", property.value, value);\n    }\n    \n    public IUIAutomationCondition createPropertyCondition(Property property, String value) {\n        return createPropertyCondition(property, new ComAllocatedVarStr(value));\n    }   \n    \n    public IUIAutomationCondition createPropertyCondition(Property property, int value) {   \n        return createPropertyCondition(property, new ComAllocatedVarInt(value));\n    }    \n\n    public IUIAutomationCondition getContentViewCondition() {\n        return invokeForCondition(\"ContentViewCondition\");\n    }\n\n    public IUIAutomationCondition getControlViewCondition() {\n        return invokeForCondition(\"ControlViewCondition\");\n    }\n\n    public IUIAutomationCondition getRawViewCondition() {\n        return invokeForCondition(\"RawViewCondition\");\n    }\n\n    public IUIAutomationCondition createAndCondition(IUIAutomationCondition c1, IUIAutomationCondition c2) {\n        return invokeForCondition(\"CreateAndCondition\", c1, c2);\n    }\n\n    public IUIAutomationCondition createOrCondition(IUIAutomationCondition c1, IUIAutomationCondition c2) {\n        return invokeForCondition(\"CreateOrCondition\", c1, c2);\n    }\n\n    public IUIAutomationCondition createNotCondition(IUIAutomationCondition c) {\n        return invokeForCondition(\"CreateNotCondition\", c);\n    }\n    \n    public IUIAutomationTreeWalker getControlViewWalker() {\n        return invoke(IUIAutomationTreeWalker.class, \"ControlViewWalker\");\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/IUIAutomationBase.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.sun.jna.Function;\nimport com.sun.jna.ptr.DoubleByReference;\nimport com.sun.jna.ptr.IntByReference;\nimport com.sun.jna.ptr.PointerByReference;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic abstract class IUIAutomationBase extends ComRef {\n\n    protected final ComInterface INTERFACE;\n\n    public IUIAutomationBase() {\n        this(new PointerByReference());\n    }\n\n    public IUIAutomationBase(PointerByReference ref) {\n        super(ref);\n        String interfaceName = getClass().getSimpleName();\n        INTERFACE = ComUtils.LIBRARY.interfaces.get(interfaceName);\n        if (INTERFACE == null) {\n            throw new RuntimeException(\"could not resolve interface: \" + interfaceName);\n        }\n    }\n\n    protected static int enumValue(String name, String key) {\n        return ComUtils.enumValue(name, key);\n    }\n\n    protected static String enumKey(String name, int value) {\n        return ComUtils.enumKey(name, value);\n    }\n\n    public int invoke(int offset, Object... args) {\n        Function function = INTERFACE.getFunction(offset, REF.getValue());\n        return invoke(\"offset: \" + offset, function, args);\n    }\n\n    public int invoke(String name, Object... args) {\n        Function function = INTERFACE.getFunction(name, REF.getValue());\n        return invoke(name, function, args);\n    }\n\n    public int invoke(String name, Function function, Object... args) {\n        int res = -1;\n        List<ComAllocated> toFree = new ArrayList(args.length);\n        ComRef lastArg = null;\n        try {\n            Object[] refs = new Object[args.length + 1];\n            refs[0] = REF.getValue();\n            for (int i = 0; i < args.length; i++) {\n                Object arg = args[i];\n                Object val;\n                if (arg instanceof ComRef) {\n                    ComRef ref = (ComRef) arg;\n                    if (i == args.length - 1) { // if last arg\n                        val = ref.REF; // reference to pointer\n                        lastArg = ref;\n                    } else {\n                        val = ref.REF.getValue(); // pointer\n                    }\n                } else if (arg instanceof ComAllocated) {\n                    ComAllocated ca = (ComAllocated) arg;\n                    toFree.add(ca);\n                    val = ca.value();\n                } else {\n                    val = arg;\n                }\n                refs[i + 1] = val;\n            }\n            res = function.invokeInt(refs);\n            if (res != 0) {\n                logger.warn(\"{}.{} returned non-zero: {}\", INTERFACE.name, name, res);\n                if (lastArg != null) {\n                    lastArg.setValid(false);\n                }\n            }\n            if (lastArg != null && lastArg.isNull() && logger.isTraceEnabled()) {\n                logger.trace(\"{}.{} returned null: {}\", INTERFACE.name, name, lastArg.REF);\n            }\n        } catch (Exception e) {\n            String message = INTERFACE.name + \".\" + name + \" failed with exception: \" + e.getMessage();\n            logger.error(message);\n            throw new RuntimeException(e);\n        } finally {\n            toFree.forEach(ComAllocated::free);\n        }\n        return res;\n    }\n\n    public <T> T invoke(Class<T> clazz, String name, Object... args) {\n        T ref;\n        try {\n            ref = (T) clazz.newInstance();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        Object[] refs = new Object[args.length + 1];\n        System.arraycopy(args, 0, refs, 0, args.length);\n        refs[args.length] = ref;\n        invoke(name, refs);\n        return ref;\n    }\n\n    public IUIAutomationElement invokeForElement(String name, Object... args) {\n        return invoke(IUIAutomationElement.class, name, args);\n    }\n\n    public IUIAutomationCondition invokeForCondition(String name, Object... args) {\n        return invoke(IUIAutomationCondition.class, name, args);\n    }\n\n    public String invokeForString(String name) {\n        ComRef ref = new ComRef();\n        invoke(name, ref);\n        return ref.isNull() ? \"\" : ref.asString();\n    }\n\n    public int invokeForInt(String name) {\n        IntByReference ref = new IntByReference();\n        invoke(name, ref);\n        return ref.getValue();\n    }\n\n    public boolean invokeForBool(String name) {\n        return invokeForInt(name) != 0;\n    }\n\n    public double invokeForDouble(String name) {\n        DoubleByReference ref = new DoubleByReference();\n        invoke(name, ref);\n        return ref.getValue();\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/IUIAutomationCondition.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\n/**\n *\n * @author pthomas3\n */\npublic class IUIAutomationCondition extends IUIAutomationBase {  \n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/IUIAutomationElement.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.sun.jna.platform.win32.Variant;\nimport com.sun.jna.platform.win32.WinDef;\nimport com.sun.jna.ptr.PointerByReference;\n\n/**\n *\n * @author pthomas3\n */\npublic class IUIAutomationElement extends IUIAutomationBase {\n\n    public String getCurrentName() {\n        return invokeForString(\"CurrentName\");\n    }\n\n    public ControlType getControlType() {\n        int controlType = getCurrentPropertyValue(Property.ControlType).intValue();\n        return ControlType.fromValue(controlType);\n    }\n\n    public String getClassName() {\n        return getCurrentPropertyValue(Property.ClassName).stringValue();\n    }\n\n    public String getAutomationId() {\n        return getCurrentPropertyValue(Property.AutomationId).stringValue();\n    }\n\n    public IUIAutomationElement findFirst(TreeScope scope, IUIAutomationCondition condition) {\n        return invokeForElement(\"FindFirst\", scope.value, condition);\n    }\n\n    public IUIAutomationElementArray findAll(TreeScope scope, IUIAutomationCondition condition) {\n        return invoke(IUIAutomationElementArray.class, \"FindAll\", scope.value, condition);\n    }\n\n    public Variant.VARIANT getCurrentPropertyValue(Property property) {\n        return invoke(Variant.VARIANT.class, \"GetCurrentPropertyValue\", property.value);\n    }\n\n    public void setFocus() {\n        invoke(\"SetFocus\");\n    }\n\n    public WinDef.POINT getClickablePoint() {\n        WinDef.POINT point = new WinDef.POINT.ByReference();\n        WinDef.BOOLByReference status = new WinDef.BOOLByReference();\n        invoke(\"GetClickablePoint\", point, status);\n        if (!status.getValue().booleanValue()) {\n            logger.warn(\"failed to get clickable point\");\n            return null;\n        }\n        return point;\n    }\n\n    public WinDef.RECT getCurrentBoundingRectangle() {\n        return invoke(WinDef.RECT.class, \"CurrentBoundingRectangle\");\n    }\n\n    public <T> T getCurrentPattern(Class<T> type) {\n        Pattern pattern = Pattern.fromType(type);\n        if (pattern == null) {\n            throw new RuntimeException(\"unsupported pattern: \" + type);\n        }\n        return invoke(type, \"GetCurrentPattern\", pattern.value);\n    }\n\n    public WinDef.HWND getCurrentNativeWindowHandle() {\n        PointerByReference pbr = new PointerByReference();\n        invoke(\"CurrentNativeWindowHandle\", pbr);\n        return new WinDef.HWND(pbr.getValue());\n    }\n    \n    public boolean getCurrentIsEnabled() {\n        return invokeForBool(\"CurrentIsEnabled\");\n    }    \n\n    @Override\n    public String toString() {\n        if (!isValid()) {\n            return \"(stale)\";\n        }\n        try {\n            return getControlType() + \":\" + getCurrentName();\n        } catch (Exception e) {\n            return \"(stale) \" + e.getMessage();\n        }\n    }    \n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/IUIAutomationElementArray.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\n/**\n *\n * @author pthomas3\n */\npublic class IUIAutomationElementArray extends IUIAutomationBase {\n\n    public int getLength() {\n        return invokeForInt(\"Length\");\n    }\n\n    public IUIAutomationElement getElement(int index) {\n        return invokeForElement(\"GetElement\", index);\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/IUIAutomationInvokePattern.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\n/**\n *\n * @author pthomas3\n */\npublic class IUIAutomationInvokePattern extends IUIAutomationBase {\n    \n    public void invoke() {\n        invoke(\"Invoke\");\n    }\n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/IUIAutomationScrollPattern.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\n/**\n *\n * @author pthomas3\n */\npublic class IUIAutomationScrollPattern extends IUIAutomationBase {\n\n    public boolean getCurrentHorizontallyScrollable() {\n        return invokeForBool(\"CurrentHorizontallyScrollable\");\n    }\n\n    public double getCurrentHorizontalScrollPercent() {\n        return invokeForDouble(\"CurrentHorizontalScrollPercent\");\n    }\n\n    public double getCurrentHorizontalViewSize() {\n        return invokeForDouble(\"CurrentHorizontalViewSize\");\n    }\n\n    public boolean getCurrentVerticallyScrollable() {\n        return invokeForBool(\"CurrentVerticallyScrollable\");\n    }\n\n    public double getCurrentVerticalScrollPercent() {\n        return invokeForDouble(\"CurrentVerticalScrollPercent\");\n    }\n\n    public double getCurrentVerticalViewSize() {\n        return invokeForDouble(\"CurrentVerticalViewSize\");\n    }\n\n    public void scroll(ScrollAmount scrollAmount) {\n        invoke(\"Scroll\", scrollAmount.value);\n    }\n\n    public void setScrollPercent(double horizontalPercent, double verticalPercent) {\n        invoke(\"SetScrollPercent\", horizontalPercent, verticalPercent);\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/IUIAutomationSelectionItemPattern.java",
    "content": "/*\r\n * The MIT License\r\n *\r\n * Copyright 2022 Karate Labs Inc.\r\n *\r\n * Permission is hereby granted, free of charge, to any person obtaining a copy\r\n * of this software and associated documentation files (the \"Software\"), to deal\r\n * in the Software without restriction, including without limitation the rights\r\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\n * copies of the Software, and to permit persons to whom the Software is\r\n * furnished to do so, subject to the following conditions:\r\n *\r\n * The above copyright notice and this permission notice shall be included in\r\n * all copies or substantial portions of the Software.\r\n *\r\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r\n * THE SOFTWARE.\r\n */\r\npackage com.intuit.karate.robot.win;\r\n\r\n/**\r\n * @author babusekaran\r\n */\r\npublic class IUIAutomationSelectionItemPattern extends IUIAutomationBase {\r\n\r\n    public void select() {\r\n        invoke(\"Select\");\r\n    }\r\n\r\n}\r\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/IUIAutomationTreeWalker.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\n/**\n *\n * @author pthomas3\n */\npublic class IUIAutomationTreeWalker extends IUIAutomationBase {\n\n    public IUIAutomationElement getFirstChildElement(IUIAutomationElement e) {\n        return invokeForElement(\"GetFirstChildElement\", e);\n    }\n    \n    public IUIAutomationElement getLastChildElement(IUIAutomationElement e) {\n        return invokeForElement(\"GetLastChildElement\", e);\n    }    \n\n    public IUIAutomationElement getNextSiblingElement(IUIAutomationElement e) {\n        return invokeForElement(\"GetNextSiblingElement\", e);\n    }\n\n    public IUIAutomationElement getParentElement(IUIAutomationElement e) {\n        return invokeForElement(\"GetParentElement\", e);\n    }\n\n    public IUIAutomationElement getPreviousSiblingElement(IUIAutomationElement e) {\n        return invokeForElement(\"GetPreviousSiblingElement\", e);\n    }\n    \n    public IUIAutomationElement normalizeElement(IUIAutomationElement e) {\n        return invokeForElement(\"NormalizeElement\", e);\n    }    \n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/IUIAutomationValuePattern.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\n/**\n *\n * @author pthomas3\n */\npublic class IUIAutomationValuePattern extends IUIAutomationBase {\n    \n    public boolean getCurrentIsReadOnly() {\n        return invokeForBool(\"CurrentIsReadOnly\");\n    }\n    \n    public String getCurrentValue() {\n        return invokeForString(\"CurrentValue\");\n    }\n    \n    public void setCurrentValue(String value) {\n        invoke(\"SetValue\", new ComAllocatedStr(value));\n    }\n    \n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/IUIAutomationWindowPattern.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.sun.jna.ptr.IntByReference;\n\n/**\n *\n * @author pthomas3\n */\npublic class IUIAutomationWindowPattern extends IUIAutomationBase {\n\n    public void close() {\n        invoke(\"Close\");\n    }\n\n    public boolean canMaximize() {\n        return invokeForBool(\"CurrentCanMaximize\");\n    }\n\n    public boolean canMinimize() {\n        return invokeForBool(\"CurrentCanMinimize\");\n    }\n\n    public boolean isModal() {\n        return invokeForBool(\"CurrentIsModal\");\n    }\n\n    public boolean isTopmost() {\n        return invokeForBool(\"CurrentIsTopmost\");\n    }\n\n    public int getCurrentWindowInteractionState() {\n        return invokeForInt(\"CurrentWindowInteractionState\");\n    }\n\n    public int getCurrentWindowVisualState() {\n        return invokeForInt(\"CurrentWindowVisualState\");\n    }\n\n    public void setWindowVisualState(int state) {\n        invoke(\"SetWindowVisualState\", state);\n    }\n    \n    public void minimize() {\n        setWindowVisualState(2);\n    }\n    \n    public void maximize() {\n        setWindowVisualState(1);\n    }    \n    \n    public void restore() {\n        setWindowVisualState(0);\n    }    \n\n    public boolean waitForInputIdle(int timeoutMillis) {\n        IntByReference intRef = new IntByReference();\n        invoke(\"WaitForInputIdle\", timeoutMillis, intRef);\n        return intRef.getValue() != 0;\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/PathSearch.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.intuit.karate.StringUtils;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Predicate;\nimport java.util.regex.Matcher;\n\n/**\n *\n * @author pthomas3\n */\npublic class PathSearch {\n\n    public static boolean isWildcard(String locator) {\n        char firstChar = locator.charAt(0);\n        return firstChar == '^' || firstChar == '~';\n    }\n\n    private static final java.util.regex.Pattern PATH_CHUNK = java.util.regex.Pattern.compile(\"([^.{]+)?(\\\\.[^{]+)?(\\\\{.+\\\\})?\");\n\n    protected static class Chunk {\n\n        final String raw;\n        final boolean anyDepth;\n        final String controlType;\n        final String className;\n        final int index;\n        final Predicate<String> nameCondition;\n        final String name;\n\n        Chunk(boolean anyDepth, String raw) {\n            this.anyDepth = anyDepth;\n            this.raw = raw;\n            Matcher matcher = PATH_CHUNK.matcher(raw);\n            if (!matcher.find()) {\n                throw new RuntimeException(\"invalid path pattern: \" + raw);\n            }\n            String typeAndIndex = matcher.group(1);\n            if (typeAndIndex == null) {\n                index = -1;\n                controlType = null;\n            } else {\n                int pos = typeAndIndex.indexOf('[');\n                if (pos != -1) {\n                    int endPos = raw.indexOf(']', pos);\n                    String temp = raw.substring(pos + 1, endPos);\n                    index = Integer.valueOf(temp) - 1;\n                    controlType = typeAndIndex.substring(0, pos);\n                } else {\n                    index = -1;\n                    controlType = typeAndIndex;\n                }\n            }\n            String dotAndClassName = matcher.group(2);\n            className = dotAndClassName == null ? null : dotAndClassName.substring(1);\n            String prefixAndName = matcher.group(3);\n            if (prefixAndName == null) {\n                name = null;\n                nameCondition = null;\n            } else {\n                prefixAndName = prefixAndName.substring(1, prefixAndName.length() - 1);\n                switch (prefixAndName.charAt(0)) {\n                    case '^':\n                        name = prefixAndName.substring(1);\n                        nameCondition = s -> s.contains(name);\n                        break;\n                    case '~':\n                        name = prefixAndName.substring(1);\n                        java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(name);\n                        nameCondition = s -> pattern.matcher(s).find();\n                        break;\n                    default:\n                        name = StringUtils.trimToNull(prefixAndName);\n                        nameCondition = name == null ? null : s -> s.equals(name);\n                }\n            }\n        }\n\n        @Override\n        public String toString() {\n            return (anyDepth ? \"//\" : \"/\") + raw;\n        }\n\n    }\n\n    private static final char SLASH = '/';\n\n    public final boolean findAll;\n    private final String path;\n    public final List<Chunk> chunks;\n\n    public PathSearch(String path, boolean findAll) {\n        this.path = path;\n        this.findAll = findAll;\n        chunks = split(path);\n    }\n\n    public static List<Chunk> split(String s) {\n        int pos = s.indexOf(SLASH);\n        if (pos == -1) {\n            throw new RuntimeException(\"path did not start with or contain '/'\");\n        }\n        List<Chunk> list = new ArrayList();\n        int startPos = 0;\n        int searchPos = 0;\n        boolean anyDepth = false;\n        while (pos != -1) {\n            if (pos == 0) {\n                startPos = 1;\n                searchPos = 1;\n            } else if (s.charAt(pos - 1) == '\\\\') {\n                s = s.substring(0, pos - 1) + s.substring(pos);\n                searchPos = pos;\n            } else {\n                String temp = s.substring(startPos, pos);\n                if (temp.isEmpty()) {\n                    anyDepth = true;\n                } else {\n                    list.add(new Chunk(anyDepth, temp));\n                    anyDepth = false; // reset                   \n                }\n                startPos = pos + 1;\n                searchPos = startPos;\n            }\n            pos = s.indexOf(SLASH, searchPos);\n        }\n        if (startPos != s.length()) {\n            String temp = s.substring(startPos);\n            if (!temp.isEmpty()) {\n                list.add(new Chunk(anyDepth, temp));\n            }\n        }\n        return list;\n    }\n\n    @Override\n    public String toString() {\n        return path;\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/Pattern.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2020 pthomas3.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic enum Pattern {\n\n    Invoke(10000, IUIAutomationInvokePattern.class),\n    Selection(10001),\n    Value(10002, IUIAutomationValuePattern.class),\n    RangeValue(10003),\n    Scroll(10004, IUIAutomationScrollPattern.class),\n    ExpandCollapse(10005),\n    Grid(10006),\n    GridItem(10007),\n    MultipleView(10008),\n    Window(10009, IUIAutomationWindowPattern.class),\n    SelectionItem(10010, IUIAutomationSelectionItemPattern.class),\n    Dock(10011),\n    Table(10012),\n    TableItem(10013),\n    Text(10014),\n    Toggle(10015),\n    Transform(10016),\n    ScrollItem(10017),\n    LegacyIAccessible(10018),\n    ItemContainer(10019),\n    VirtualizedItem(10020),\n    SynchronizedInput(10021),\n    ObjectModel(10022),\n    Annotation(10023),\n    Text2(10024),\n    Styles(10025),\n    Spreadsheet(10026),\n    SpreadsheetItem(10027),\n    Transform2(10028),\n    TextChild(10029),\n    Drag(10030),\n    DropTarget(10031),\n    TextEdit(10032),\n    CustomNavigation(10033);\n\n    public final int value;\n    public final Class type;\n\n    private Pattern(int value) {\n        this(value, null);\n    }\n\n    private Pattern(int value, Class type) {\n        this.value = value;\n        this.type = type;\n    }\n\n    private static final Map<String, Pattern> FROM_CLASS;\n    private static final Map<String, Pattern> FROM_NAME;\n\n    static {\n        Pattern[] values = Pattern.values();\n        FROM_CLASS = new HashMap(values.length);\n        FROM_NAME = new HashMap(values.length);\n        for (Pattern p : values) {\n            if (p.type != null) {\n                FROM_CLASS.put(p.type.getSimpleName(), p);\n            }\n            FROM_NAME.put(p.name().toLowerCase(), p);\n        }\n    }\n\n    public static Pattern fromType(Class type) {\n        return FROM_CLASS.get(type.getSimpleName());\n    }\n\n    public static Pattern fromName(String name) {\n        return FROM_NAME.get(name.toLowerCase());\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/Property.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n *\n * @author pthomas3\n */\npublic enum Property {\n\n    RuntimeId(30000),\n    BoundingRectangle(30001),\n    ProcessId(30002),\n    ControlType(30003),\n    LocalizedControlType(30004),\n    Name(30005),\n    AcceleratorKey(30006),\n    AccessKey(30007),\n    HasKeyboardFocus(30008),\n    IsKeyboardFocusable(30009),\n    IsEnabled(30010),\n    AutomationId(30011),\n    ClassName(30012),\n    HelpText(30013),\n    ClickablePoint(30014),\n    Culture(30015),\n    IsControlElement(30016),\n    IsContentElement(30017),\n    LabeledBy(30018),\n    IsPassword(30019),\n    NativeWindowHandle(30020),\n    ItemType(30021),\n    IsOffscreen(30022),\n    Orientation(30023),\n    FrameworkId(30024),\n    IsRequiredForForm(30025),\n    ItemStatus(30026),\n    IsDockPatternAvailable(30027),\n    IsExpandCollapsePatternAvailable(30028),\n    IsGridItemPatternAvailable(30029),\n    IsGridPatternAvailable(30030),\n    IsInvokePatternAvailable(30031),\n    IsMultipleViewPatternAvailable(30032),\n    IsRangeValuePatternAvailable(30033),\n    IsScrollPatternAvailable(30034),\n    IsScrollItemPatternAvailable(30035),\n    IsSelectionItemPatternAvailable(30036),\n    IsSelectionPatternAvailable(30037),\n    IsTablePatternAvailable(30038),\n    IsTableItemPatternAvailable(30039),\n    IsTextPatternAvailable(30040),\n    IsTogglePatternAvailable(30041),\n    IsTransformPatternAvailable(30042),\n    IsValuePatternAvailable(30043),\n    IsWindowPatternAvailable(30044),\n    ValueValue(30045),\n    ValueIsReadOnly(30046),\n    RangeValueValue(30047),\n    RangeValueIsReadOnly(30048),\n    RangeValueMinimum(30049),\n    RangeValueMaximum(30050),\n    RangeValueLargeChange(30051),\n    RangeValueSmallChange(30052),\n    ScrollHorizontalScrollPercent(30053),\n    ScrollHorizontalViewSize(30054),\n    ScrollVerticalScrollPercent(30055),\n    ScrollVerticalViewSize(30056),\n    ScrollHorizontallyScrollable(30057),\n    ScrollVerticallyScrollable(30058),\n    SelectionSelection(30059),\n    SelectionCanSelectMultiple(30060),\n    SelectionIsSelectionRequired(30061),\n    GridRowCount(30062),\n    GridColumnCount(30063),\n    GridItemRow(30064),\n    GridItemColumn(30065),\n    GridItemRowSpan(30066),\n    GridItemColumnSpan(30067),\n    GridItemContainingGrid(30068),\n    DockDockPosition(30069),\n    ExpandCollapseExpandCollapseState(30070),\n    MultipleViewCurrentView(30071),\n    MultipleViewSupportedViews(30072),\n    WindowCanMaximize(30073),\n    WindowCanMinimize(30074),\n    WindowWindowVisualState(30075),\n    WindowWindowInteractionState(30076),\n    WindowIsModal(30077),\n    WindowIsTopmost(30078),\n    SelectionItemIsSelected(30079),\n    SelectionItemSelectionContainer(30080),\n    TableRowHeaders(30081),\n    TableColumnHeaders(30082),\n    TableRowOrColumnMajor(30083),\n    TableItemRowHeaderItems(30084),\n    TableItemColumnHeaderItems(30085),\n    ToggleToggleState(30086),\n    TransformCanMove(30087),\n    TransformCanResize(30088),\n    TransformCanRotate(30089),\n    IsLegacyIAccessiblePatternAvailable(30090),\n    LegacyIAccessibleChildId(30091),\n    LegacyIAccessibleName(30092),\n    LegacyIAccessibleValue(30093),\n    LegacyIAccessibleDescription(30094),\n    LegacyIAccessibleRole(30095),\n    LegacyIAccessibleState(30096),\n    LegacyIAccessibleHelp(30097),\n    LegacyIAccessibleKeyboardShortcut(30098),\n    LegacyIAccessibleSelection(30099),\n    LegacyIAccessibleDefaultAction(30100),\n    AriaRole(30101),\n    AriaProperties(30102),\n    IsDataValidForForm(30103),\n    ControllerFor(30104),\n    DescribedBy(30105),\n    FlowsTo(30106),\n    ProviderDescription(30107),\n    IsItemContainerPatternAvailable(30108),\n    IsVirtualizedItemPatternAvailable(30109),\n    IsSynchronizedInputPatternAvailable(30110),\n    OptimizeForVisualContent(30111),\n    IsObjectModelPatternAvailable(30112),\n    AnnotationAnnotationTypeId(30113),\n    AnnotationAnnotationTypeName(30114),\n    AnnotationAuthor(30115),\n    AnnotationDateTime(30116),\n    AnnotationTarget(30117),\n    IsAnnotationPatternAvailable(30118),\n    IsTextPattern2Available(30119),\n    StylesStyleId(30120),\n    StylesStyleName(30121),\n    StylesFillColor(30122),\n    StylesFillPatternStyle(30123),\n    StylesShape(30124),\n    StylesFillPatternColor(30125),\n    StylesExtendedProperties(30126),\n    IsStylesPatternAvailable(30127),\n    IsSpreadsheetPatternAvailable(30128),\n    SpreadsheetItemFormula(30129),\n    SpreadsheetItemAnnotationObjects(30130),\n    SpreadsheetItemAnnotationTypes(30131),\n    IsSpreadsheetItemPatternAvailable(30132),\n    Transform2CanZoom(30133),\n    IsTransformPattern2Available(30134),\n    LiveSetting(30135),\n    IsTextChildPatternAvailable(30136),\n    IsDragPatternAvailable(30137),\n    DragIsGrabbed(30138),\n    DragDropEffect(30139),\n    DragDropEffects(30140),\n    IsDropTargetPatternAvailable(30141),\n    DropTargetDropTargetEffect(30142),\n    DropTargetDropTargetEffects(30143),\n    DragGrabbedItems(30144),\n    Transform2ZoomLevel(30145),\n    Transform2ZoomMinimum(30146),\n    Transform2ZoomMaximum(30147),\n    FlowsFrom(30148),\n    IsTextEditPatternAvailable(30149),\n    IsPeripheral(30150),\n    IsCustomNavigationPatternAvailable(30151),\n    PositionInSet(30152),\n    SizeOfSet(30153),\n    Level(30154),\n    AnnotationTypes(30155),\n    AnnotationObjects(30156);\n\n    public final int value;\n\n    private Property(int value) {\n        this.value = value;\n    }\n\n    private static final Map<Integer, Property> FROM_ID;\n\n    static {\n        Property[] values = Property.values();\n        FROM_ID = new HashMap(values.length);\n        for (Property p : values) {\n            FROM_ID.put(p.value, p);\n        }\n    }\n    \n    public static Property fromId(int id) {\n        return FROM_ID.get(id);\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/ScrollAmount.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\n/**\n *\n * @author pthomas3\n */\npublic enum ScrollAmount {\n\n    LargeDecrement(0),\n    SmallDecrement(1),\n    NoAmount(2),\n    LargeIncrement(3),\n    SmallIncrement(4);\n\n    public final int value;\n\n    private ScrollAmount(int value) {\n        this.value = value;\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/TreeScope.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\n/**\n *\n * @author pthomas3\n */\npublic enum TreeScope {\n\n    Element(1),\n    Children(2),\n    Descendants(4),\n    Parent(8),\n    Ancestors(16),\n    Subtree(7);\n\n    public final int value;\n\n    private TreeScope(int value) {\n        this.value = value;\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/WinElement.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.intuit.karate.Logger;\nimport com.intuit.karate.robot.Element;\nimport com.intuit.karate.robot.Location;\nimport com.intuit.karate.robot.Region;\nimport com.intuit.karate.robot.RobotBase;\nimport com.sun.jna.platform.win32.Variant;\nimport com.sun.jna.platform.win32.WinDef;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n *\n * @author pthomas3\n */\npublic class WinElement implements Element {\n\n    protected final IUIAutomationElement e;\n    private final WinRobot robot;\n    private final Logger logger;\n\n    public WinElement(WinRobot robot, IUIAutomationElement e) {\n        this.robot = robot;\n        this.e = e;\n        this.logger = robot.getLogger();\n    }\n\n    @Override\n    public RobotBase getRobot() {\n        return robot;\n    }\n\n    @Override\n    public boolean isPresent() {\n        return e.isValid() && !e.isNull();\n    }\n\n    @Override\n    public boolean isEnabled() {\n        return e.getCurrentIsEnabled();\n    }\n\n    @Override\n    public boolean isImage() {\n        return false;\n    }\n\n    @Override\n    public Region getRegion() {\n        WinDef.RECT rect = e.getCurrentBoundingRectangle();\n        return new Region(robot, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);\n    }\n\n    private Location getClickablePoint() {\n        WinDef.POINT p = e.getClickablePoint();\n        return p == null ? getRegion().getCenter() : new Location(robot, p.x, p.y);\n    }\n\n    @Override\n    public Element focus() {\n        e.setFocus();\n        return this;\n    }\n\n    public Element invoke() {\n        if (isInvokePatternAvailable()) {\n            IUIAutomationInvokePattern invokePattern = e.getCurrentPattern(IUIAutomationInvokePattern.class);\n            invokePattern.invoke();\n        } else {\n            click();\n        }\n        return this;\n    }\n\n    @Override\n    public Element click() {\n        getClickablePoint().click();\n        return this;\n    }\n\n    @Override\n    public Element move() {\n        getClickablePoint().move();\n        return this;\n    }\n\n    @Override\n    public Element press() {\n        getClickablePoint().press();\n        return this;\n    }\n\n    @Override\n    public Element release() {\n        getClickablePoint().release();\n        return this;\n    }\n\n    @Override\n    public String getName() {\n        return e.getCurrentName();\n    }\n\n    private boolean isValuePatternAvailable() {\n        Variant.VARIANT variant = e.getCurrentPropertyValue(Property.IsValuePatternAvailable);\n        return variant.booleanValue();\n    }\n\n    private boolean isInvokePatternAvailable() {\n        Variant.VARIANT variant = e.getCurrentPropertyValue(Property.IsInvokePatternAvailable);\n        return variant.booleanValue();\n    }\n\n    private boolean isScrollPatternAvailable() {\n        Variant.VARIANT variant = e.getCurrentPropertyValue(Property.IsScrollPatternAvailable);\n        return variant.booleanValue();\n    }\n\n    @Override\n    public String getValue() {\n        if (isValuePatternAvailable()) {\n            return e.getCurrentPattern(IUIAutomationValuePattern.class).getCurrentValue();\n        }\n        return null;\n    }\n\n    @Override\n    public Element clear() {\n        if (isValuePatternAvailable()) {\n            IUIAutomationValuePattern valuePattern = e.getCurrentPattern(IUIAutomationValuePattern.class);\n            valuePattern.setCurrentValue(\"\");\n        } else {\n            e.setFocus();\n            robot.clearFocused();\n        }\n        return this;\n    }\n\n    @Override\n    public Element input(String value) {\n        if (isValuePatternAvailable()) {\n            IUIAutomationValuePattern valuePattern = e.getCurrentPattern(IUIAutomationValuePattern.class);\n            valuePattern.setCurrentValue(value);\n        } else {\n            e.setFocus();\n            robot.input(value);\n        }\n        return this;\n    }\n\n    @Override\n    public Element delay(int millis) {\n        robot.delay(millis);\n        return this;\n    }\n\n    @Override\n    public List<Element> getChildren() {\n        IUIAutomationElementArray array = e.findAll(TreeScope.Children, WinRobot.UIA.createTrueCondition());\n        int count = array.getLength();\n        List<Element> list = new ArrayList(count);\n        for (int i = 0; i < count; i++) {\n            IUIAutomationElement child = array.getElement(i);\n            list.add(new WinElement(robot, child));\n        }\n        return list;\n    }\n\n    private IUIAutomationTreeWalker walk() {\n        return WinRobot.UIA.getControlViewWalker();\n    }\n\n    @Override\n    public Element getParent() {\n        return new WinElement(robot, walk().getParentElement(e));\n    }\n\n    public Element getFirstChild() {\n        return new WinElement(robot, walk().getFirstChildElement(e));\n    }\n\n    public Element getLastChild() {\n        return new WinElement(robot, walk().getLastChildElement(e));\n    }\n\n    public Element getNextSibling() {\n        return new WinElement(robot, walk().getNextSiblingElement(e));\n    }\n\n    public Element getPreviousSibling() {\n        return new WinElement(robot, walk().getPreviousSiblingElement(e));\n    }\n\n    @Override\n    public IUIAutomationElement toNative() {\n        return e;\n    }\n\n    public Object property(String propertyName) {\n        Property property = Property.valueOf(propertyName);\n        Variant.VARIANT variant = e.getCurrentPropertyValue(property);\n        return variant.getValue();\n    }\n\n    public Object property(int propertyId) {\n        Property property = Property.fromId(propertyId);\n        Variant.VARIANT variant = e.getCurrentPropertyValue(property);\n        return variant.getValue();\n    }\n\n    @Override\n    public String getDebugString() {\n        if (!e.isValid()) {\n            return \"(null)\";\n        }\n        try {\n            return \"'\" + e.getCurrentName() + \"' \" + e.getControlType();\n        } catch (Exception e) {\n            return \"(stale) \" + e.getMessage();\n        }\n    }\n\n    @Override\n    public String toString() {\n        return getDebugString();\n    }\n\n    @Override\n    public Element select() {\n        IUIAutomationSelectionItemPattern pattern = e.getCurrentPattern(IUIAutomationSelectionItemPattern.class);\n        pattern.select();\n        return this;\n    }\n\n    public Element scrollDown() {\n        return scrollDown(false);\n    }\n\n    public Element scrollUp() {\n        return scrollUp(false);\n    }\n\n    public Element scrollDown(boolean large) {\n        if (isScrollPatternAvailable()) {\n            IUIAutomationScrollPattern pattern = e.getCurrentPattern(IUIAutomationScrollPattern.class);\n            ScrollAmount sa = large ? ScrollAmount.LargeIncrement : ScrollAmount.SmallIncrement;\n            pattern.scroll(sa);\n        } else {\n            logger.warn(\"scroll pattern not available on: {}\", getName());\n        }\n        return this;\n    }\n\n    public Element scrollUp(boolean large) {\n        if (isScrollPatternAvailable()) {\n            IUIAutomationScrollPattern pattern = e.getCurrentPattern(IUIAutomationScrollPattern.class);\n            ScrollAmount sa = large ? ScrollAmount.LargeDecrement : ScrollAmount.SmallDecrement;\n            pattern.scroll(sa);\n        } else {\n            logger.warn(\"scroll pattern not available on: {}\", getName());\n        }\n        return this;\n    }\n\n    public Element scroll(double horizontalPercent, double verticalPercent) {\n        if (isScrollPatternAvailable()) {\n            IUIAutomationScrollPattern pattern = e.getCurrentPattern(IUIAutomationScrollPattern.class);\n            pattern.setScrollPercent(horizontalPercent, verticalPercent);\n        } else {\n            logger.warn(\"scroll pattern not available on: {}\", getName());\n        }\n        return this;\n    }\n\n    public Object as(String patternName) {\n        Pattern pattern = Pattern.fromName(patternName);\n        if (pattern == null) {\n            throw new RuntimeException(\"no such pattern: \" + patternName);\n        }\n        if (pattern.type == null) {\n            throw new RuntimeException(\"pattern not implemented: \" + pattern);\n        }\n        return e.getCurrentPattern(pattern.type);\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/WinRobot.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.intuit.karate.core.AutoDef;\nimport com.intuit.karate.core.FeatureRuntime;\nimport com.intuit.karate.core.ScenarioEngine;\nimport com.intuit.karate.robot.Element;\nimport com.intuit.karate.robot.Location;\nimport com.intuit.karate.robot.Robot;\nimport com.intuit.karate.robot.RobotBase;\nimport com.intuit.karate.robot.StringMatcher;\nimport com.intuit.karate.robot.Window;\nimport com.intuit.karate.core.ScenarioRuntime;\nimport com.intuit.karate.http.HttpClientFactory;\nimport com.sun.jna.platform.win32.BaseTSD.ULONG_PTR;\n\nimport com.sun.jna.platform.win32.User32;\nimport com.sun.jna.platform.win32.WinDef.DWORD;\nimport com.sun.jna.platform.win32.WinDef.LONG;\nimport com.sun.jna.platform.win32.WinUser;\nimport com.sun.jna.platform.win32.WinUser.INPUT;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\n\n/**\n *\n * @author pthomas3\n */\npublic class WinRobot extends RobotBase {\n\n    protected static final IUIAutomation UIA = IUIAutomation.INSTANCE;\n\n    public WinRobot(ScenarioRuntime runtime, Map<String, Object> options) {\n        super(runtime, options);\n    }\n    \n    public static WinRobot start(Map<String, Object> options) {\n        ScenarioRuntime runtime = FeatureRuntime.forTempUse(HttpClientFactory.DEFAULT).scenarios.next();\n        ScenarioEngine.set(runtime.engine);\n        return new WinRobot(runtime, options);\n    }\n\n    @Override\n    public Map<String, Object> afterScenario() {\n        logger.debug(\"after scenario, current window: {}\", currentWindow);\n        if (autoClose && command != null && currentWindow != null) {\n            logger.debug(\"will attempt to close window for: {}\", currentWindow.getName());\n            WinUser.HWND hwnd = currentWindow.<IUIAutomationElement>toNative().getCurrentNativeWindowHandle();\n            User32.INSTANCE.PostMessage(hwnd, WinUser.WM_QUIT, null, null);\n            command.close(false);\n        }\n        return Collections.EMPTY_MAP;\n    }\n\n    @Override\n    public List<Window> getAllWindows() {\n        IUIAutomationCondition isWindow = UIA.createPropertyCondition(Property.ControlType, ControlType.Window.value);\n        IUIAutomationElementArray array = UIA.getRootElement().findAll(TreeScope.Descendants, isWindow);\n        int count = array.getLength();\n        List<Window> list = new ArrayList(count);\n        for (int i = 0; i < count; i++) {\n            IUIAutomationElement e = array.getElement(i);\n            if (e.isValid()) {\n                list.add(new WinWindow(this, e));\n            }\n        }\n        return list;\n    }\n\n    @Override\n    protected Element windowInternal(String title) {\n        return windowInternal(new StringMatcher(title));\n    }\n\n    @Override\n    protected Element windowInternal(Predicate<String> condition) {\n        IUIAutomationCondition isWindow = UIA.createPropertyCondition(Property.ControlType, ControlType.Window.value);\n        IUIAutomationElementArray windows = UIA.getRootElement().findAll(TreeScope.Descendants, isWindow);\n        int count = windows.getLength();\n        for (int i = 0; i < count; i++) {\n            IUIAutomationElement child = windows.getElement(i);\n            if (!child.isValid()) {\n                logger.warn(\"invalid window: {}\", child);\n                continue;\n            }\n            String name = child.getCurrentName();\n            if (name == null) {\n                logger.warn(\"name is null for window: {}\", child);\n                continue;\n            }\n            if (logger.isTraceEnabled()) {\n                logger.trace(\"scanning window: {}\", name);\n            }\n            if (condition.test(name)) {\n                if (logger.isTraceEnabled()) {\n                    logger.trace(\"found window: {}\", name);\n                }\n                return new WinWindow(this, child).focus();\n            }\n        }\n        logger.warn(\"failed to find window: {}\", condition);\n        return null;\n    }\n\n    private IUIAutomationCondition by(Property property, String value) {\n        return UIA.createPropertyCondition(property, value);\n    }\n\n    protected List<Element> toElements(IUIAutomationElementArray array) {\n        int count = array.getLength();\n        List<Element> list = new ArrayList(count);\n        for (int i = 0; i < count; i++) {\n            IUIAutomationElement e = array.getElement(i);\n            if (e.isValid()) {\n                list.add(new WinElement(this, e));\n            }\n        }\n        return list;\n    }\n\n    @Override\n    public List<Element> locateAllInternal(Element root, String locator) {\n        IUIAutomationElement parent = root.<IUIAutomationElement>toNative();\n        IUIAutomationCondition condition;\n        if (PathSearch.isWildcard(locator)) {\n            locator = \"//*{\" + locator + \"}\";\n        }\n        if (locator.startsWith(\"/\")) {\n            if (locator.startsWith(\"/root\")) {\n                locator = locator.substring(5);\n                parent = UIA.getRootElement();\n            }\n            List<Element> searchResults = new ArrayList();\n            PathSearch search = new PathSearch(locator, true);\n            walkPathAndFind(searchResults, search, UIA.getControlViewWalker(), parent, 0);\n            return searchResults;\n        } else if (locator.startsWith(\"#\")) {\n            condition = by(Property.AutomationId, locator.substring(1));\n        } else {\n            condition = by(Property.Name, locator);\n        }\n        IUIAutomationElementArray found = parent.findAll(TreeScope.Descendants, condition);\n        return toElements(found);\n    }\n\n    @Override\n    public Element locateInternal(Element root, String locator) {\n        IUIAutomationElement parent = root.<IUIAutomationElement>toNative();\n        IUIAutomationCondition condition;\n        if (PathSearch.isWildcard(locator)) {\n            locator = \"//*{\" + locator + \"}\";\n        }\n        if (locator.startsWith(\"/\")) {\n            if (locator.startsWith(\"/root\")) {\n                locator = locator.substring(5);\n                parent = UIA.getRootElement();\n            }\n            List<Element> searchResults = new ArrayList();\n            PathSearch search = new PathSearch(locator, false);\n            walkPathAndFind(searchResults, search, UIA.getControlViewWalker(), parent, 0);\n            if (searchResults.isEmpty()) {\n                return null;\n            } else {\n                return searchResults.get(0);\n            }\n        } else if (locator.startsWith(\"#\")) {\n            condition = by(Property.AutomationId, locator.substring(1));\n        } else {\n            condition = by(Property.Name, locator);\n        }\n        IUIAutomationElement found = parent.findFirst(TreeScope.Descendants, condition);\n        if (!found.isValid()) { // important in this case\n            return null;\n        }\n        return new WinElement(this, found);\n    }\n\n    @Override\n    public Element getRoot() {\n        return new WinElement(this, UIA.getRootElement());\n    }\n\n    @AutoDef\n    @Override\n    public Element getFocused() {\n        return new WinElement(this, UIA.getFocusedElement());\n    }\n\n    private void walkPathAndFind(List<Element> searchResults, PathSearch search,\n            IUIAutomationTreeWalker walker, IUIAutomationElement e, int depth) {\n        PathSearch.Chunk chunk = search.chunks.get(depth);\n        IUIAutomationCondition condition;\n        ControlType controlType;\n        if (chunk.controlType == null || \"*\".equals(chunk.controlType)) {\n            condition = UIA.getControlViewCondition();\n            controlType = null;\n        } else {\n            controlType = ControlType.fromName(chunk.controlType);\n            condition = UIA.createPropertyCondition(Property.ControlType, controlType.value);\n        }\n        IUIAutomationElementArray array = e.findAll(chunk.anyDepth ? TreeScope.Descendants : TreeScope.Children, condition);\n        if (!array.isValid()) { // the tree can be unstable\n            return;\n        }\n        int count = array.getLength();\n        boolean leaf = depth == search.chunks.size() - 1;\n        for (int i = 0; i < count; i++) {\n            if (chunk.index != -1 && chunk.index != i) {\n                continue;\n            }\n            IUIAutomationElement child = array.getElement(i);\n            if (!child.isValid()) { // the tree can be unstable\n                continue;\n            }\n            if (chunk.nameCondition != null) {\n                String name = child.getCurrentName();\n                if (!chunk.nameCondition.test(name)) {\n                    continue;\n                }\n            }\n            if (chunk.className != null) {\n                String className = child.getClassName();\n                if (!chunk.className.equalsIgnoreCase(className)) {\n                    continue;\n                }\n            }\n            if (leaf) {\n                // already filtered to content-type, so we have a match !\n                searchResults.add(new WinElement(this, child));\n                if (!search.findAll) {\n                    return; // exit early\n                }\n            } else {\n                walkPathAndFind(searchResults, search, walker, child, depth + 1);\n            }\n        }\n    }\n\n    @Override\n    public Robot move(int x, int y) {\n        super.move(x, y);\n        Location loc = getLocation();\n        moveInternal(-loc.x / 4, -loc.y / 4);\n        while (getLocation().x < x - 1) {\n            moveInternal(1, 0);\n        }\n        while (getLocation().y < y - 1) {\n            moveInternal(0, 1);\n        }\n        return this;\n    }\n\n    private static void moveInternal(int x, int y) {\n        INPUT input = new INPUT();\n        input.type = new DWORD(INPUT.INPUT_MOUSE);\n        input.input.setType(\"mi\");\n        input.input.mi.dx = new LONG(x);\n        input.input.mi.dy = new LONG(y);\n        input.input.mi.time = new DWORD(0);\n        input.input.mi.dwExtraInfo = new ULONG_PTR(0);\n        input.input.mi.dwFlags = new DWORD(1); // mouse move\n        User32.INSTANCE.SendInput(new DWORD(1), new INPUT[]{input}, input.size());\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/main/java/com/intuit/karate/robot/win/WinWindow.java",
    "content": "/*\n * The MIT License\n *\n * Copyright 2022 Karate Labs Inc.\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\n * all 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\n * THE SOFTWARE.\n */\npackage com.intuit.karate.robot.win;\n\nimport com.intuit.karate.robot.Window;\n\n/**\n *\n * @author pthomas3\n */\npublic class WinWindow extends WinElement implements Window {\n    \n    private final IUIAutomationWindowPattern window;\n    \n    public WinWindow(WinRobot robot, IUIAutomationElement e) {\n        super(robot, e);\n        window = e.getCurrentPattern(IUIAutomationWindowPattern.class);\n    }\n\n    @Override\n    public void close() {\n        window.close();\n    }\n\n    @Override\n    public void restore() {\n        window.restore();\n    }\n\n    @Override\n    public void minimize() {\n        window.minimize();\n    }\n\n    @Override\n    public void maximize() {\n        window.maximize();\n    }        \n    \n}\n"
  },
  {
    "path": "karate-robot/src/test/java/com/intuit/karate/robot/OpenCvUtilsTest.java",
    "content": "package com.intuit.karate.robot;\n\nimport java.io.File;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass OpenCvUtilsTest {\n\n    private static final Logger logger = LoggerFactory.getLogger(OpenCvUtilsTest.class);\n\n    // @Test // TODO jdk 17 apple silicon\n    void testOpenCv() {\n        // System.setProperty(\"org.bytedeco.javacpp.logger.debug\", \"true\");\n        File target = new File(\"src/test/java/search.png\");\n        File source = new File(\"src/test/java/desktop01.png\");\n        Region region = OpenCvUtils.find(1, null, OpenCvUtils.read(source), OpenCvUtils.read(target), false);\n        assertEquals(1605, region.x);\n        assertEquals(1, region.y);\n        target = new File(\"src/test/java/search-1_5.png\");\n        region = OpenCvUtils.find(10, null, OpenCvUtils.read(source), OpenCvUtils.read(target), true);\n        assertEquals(1604, region.x);\n        assertEquals(0, region.y);\n        region = OpenCvUtils.find(2, null, OpenCvUtils.read(source), OpenCvUtils.read(target), true);\n        assertEquals(1605, region.x);\n        assertEquals(1, region.y);        \n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/test/java/com/intuit/karate/robot/TesseractRunner.java",
    "content": "package com.intuit.karate.robot;\n\nimport java.awt.image.BufferedImage;\nimport java.io.File;\nimport org.bytedeco.opencv.opencv_core.Mat;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport robot.core.ChromeJavaRunner;\n\n/**\n *\n * @author pthomas3\n */\nclass TesseractRunner {\n\n    private static final Logger logger = LoggerFactory.getLogger(TesseractRunner.class);\n\n    @Test\n    void testTess() {\n        // File source = new File(\"src/test/java/some-text.png\");        \n        RobotBase robot = ChromeJavaRunner.getRobot();\n        Element window = robot.window(\"Safari\");\n        // window = robot.window(\"Preview\");\n        robot.delay(1000);\n        BufferedImage bi = window.getRegion().captureGreyScale();\n        Mat mat = OpenCvUtils.toMat(bi);\n        Tesseract tess = new Tesseract(new File(\"tessdata\"), \"eng\");\n        tess.process(mat, false);\n        String text = tess.getAllText();\n        logger.debug(\"all text: {}\", text);\n        tess.highlightWords(robot, robot.screen, 20000);\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/test/java/com/intuit/karate/robot/win/IUIAutomationRunner.java",
    "content": "package com.intuit.karate.robot.win;\n\nimport com.intuit.karate.StringUtils;\nimport com.sun.jna.platform.win32.User32;\nimport com.sun.jna.platform.win32.WinDef;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass IUIAutomationRunner {\n\n    private static final Logger logger = LoggerFactory.getLogger(IUIAutomationRunner.class);\n\n    @Test\n    void testAutomation() {\n        IUIAutomation ui = IUIAutomation.INSTANCE;\n        IUIAutomationElement rootElement = ui.getRootElement();\n        assertEquals(\"Desktop\", rootElement.getCurrentName());\n        IUIAutomationElement focused = ui.getFocusedElement();\n        String focusedName = focused.getCurrentName();\n        logger.debug(\"focused element name: {}\", focusedName);\n        IUIAutomationCondition trueCondition = ui.createTrueCondition();\n        IUIAutomationElementArray children = rootElement.findAll(TreeScope.Children, trueCondition);\n        int count = children.getLength();\n        logger.debug(\"child length: {}\", count);\n        for (int i = 0; i < count; i++) {\n            IUIAutomationElement e = children.getElement(i);\n            logger.debug(\"name {}: {}\", i, e.getCurrentName());\n        }\n        IUIAutomationCondition nameCondition = ui.createPropertyCondition(Property.Name, \"Program Manager\");\n        IUIAutomationElement found = rootElement.findFirst(TreeScope.Children, nameCondition);\n        assertEquals(\"Program Manager\", found.getCurrentName());\n        \n        String testName = found.getCurrentPropertyValue(Property.Name).stringValue();\n        assertEquals(\"Program Manager\", testName);\n        \n        IUIAutomationCondition andCondition = ui.createAndCondition(nameCondition, trueCondition);\n        children = rootElement.findAll(TreeScope.Children, andCondition);\n        assertEquals(1, children.getLength());\n        assertEquals(\"Program Manager\", children.getElement(0).getCurrentName());\n        IUIAutomationCondition windowCondition = ui.createPropertyCondition(Property.ControlType, ControlType.Window.value);\n        children = rootElement.findAll(TreeScope.Children, windowCondition);\n        count = children.getLength();\n        logger.debug(\"windows length: {}\", count);\n        IUIAutomationElement last = null;\n        for (int i = 0; i < count; i++) {\n            last = children.getElement(i);\n            String windowName = last.getCurrentName();\n            logger.debug(\"name {}: {}\", i, windowName);\n            WinDef.HWND hwnd = last.getCurrentNativeWindowHandle();\n            IUIAutomationElement temp1 = ui.elementFromHandle(hwnd);\n            assertEquals(temp1.getCurrentName(), windowName);\n            WinDef.HWND temp2 = User32.INSTANCE.FindWindow(null, windowName);\n            IUIAutomationElement temp3 = ui.elementFromHandle(temp2);\n            assertEquals(temp3.getCurrentName(), windowName);\n        }        \n        IUIAutomationTreeWalker walker = ui.getControlViewWalker();\n        walk(walker, last, 0);       \n    }\n    \n    private static void walk(IUIAutomationTreeWalker walker, IUIAutomationElement e, int depth) {\n        String indent = StringUtils.repeat(' ', depth * 2);\n        logger.debug(\"{}{}:{}|{}\", indent, e.getControlType(), e.getClassName(), e.getCurrentName());\n        IUIAutomationElement child = walker.getFirstChildElement(e);\n        while (!child.isNull()) {\n            walk(walker, child, depth + 1);\n            child = walker.getNextSiblingElement(child);\n        }\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/test/java/com/intuit/karate/robot/win/PathSearchTest.java",
    "content": "package com.intuit.karate.robot.win;\n\nimport java.util.List;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n *\n * @author pthomas3\n */\nclass PathSearchTest {\n\n    private static final Logger logger = LoggerFactory.getLogger(PathSearchTest.class);\n\n    @Test\n    void testPathParsing() {\n        List<PathSearch.Chunk> list = PathSearch.split(\"//hello/world//foo/bar\");\n        logger.debug(\"list: {}\", list);\n        PathSearch.Chunk first = list.get(0);\n        assertTrue(first.anyDepth);\n        assertEquals(\"hello\", first.controlType);\n        PathSearch.Chunk second = list.get(1);\n        assertFalse(second.anyDepth);\n        assertEquals(\"world\", second.controlType);\n        PathSearch.Chunk third = list.get(2);\n        assertTrue(third.anyDepth);\n        assertEquals(\"foo\", third.controlType);\n    }\n\n    @Test\n    void testPathEdge() {\n        List<PathSearch.Chunk> list = PathSearch.split(\"/hello//world\");\n        logger.debug(\"list: {}\", list);\n        PathSearch.Chunk first = list.get(0);\n        assertFalse(first.anyDepth);\n        assertEquals(\"hello\", first.controlType);\n        PathSearch.Chunk second = list.get(1);\n        assertTrue(second.anyDepth);\n        assertEquals(\"world\", second.controlType);\n    }\n\n    @Test\n    void testIndex() {\n        List<PathSearch.Chunk> list = PathSearch.split(\"/hello[3]//world\");\n        logger.debug(\"list: {}\", list);\n        PathSearch.Chunk first = list.get(0);\n        assertFalse(first.anyDepth);\n        assertEquals(\"hello\", first.controlType);\n        assertEquals(2, first.index);\n        PathSearch.Chunk second = list.get(1);\n        assertTrue(second.anyDepth);\n        assertEquals(-1, second.index);\n        assertEquals(\"world\", second.controlType);\n    }\n\n    @Test\n    void testClassName() {\n        List<PathSearch.Chunk> list = PathSearch.split(\"/hello[3]//world.Foo/.Bar\");\n        logger.debug(\"list: {}\", list);\n        PathSearch.Chunk first = list.get(0);\n        assertFalse(first.anyDepth);\n        assertEquals(\"hello\", first.controlType);\n        assertNull(first.className);\n        assertEquals(2, first.index);\n        PathSearch.Chunk second = list.get(1);\n        assertTrue(second.anyDepth);\n        assertEquals(\"world\", second.controlType);\n        assertEquals(\"Foo\", second.className);\n        PathSearch.Chunk third = list.get(2);\n        assertFalse(third.anyDepth);\n        assertEquals(null, third.controlType);\n        assertEquals(\"Bar\", third.className);\n    }\n\n    @Test\n    void testOnlyName() {\n        List<PathSearch.Chunk> list = PathSearch.split(\"//foo//{Bar One}/{Baz}\");\n        logger.debug(\"list: {}\", list);\n        PathSearch.Chunk first = list.get(0);\n        assertTrue(first.anyDepth);\n        assertEquals(\"foo\", first.controlType);\n        assertNull(first.className);\n        assertNull(first.name);\n        assertEquals(-1, first.index);\n        PathSearch.Chunk second = list.get(1);\n        assertTrue(second.anyDepth);\n        assertEquals(null, second.controlType);\n        assertEquals(null, second.className);\n        assertEquals(\"Bar One\", second.name);\n        PathSearch.Chunk third = list.get(2);\n        assertFalse(third.anyDepth);\n        assertEquals(null, third.controlType);\n        assertEquals(\"Baz\", third.name);      \n    }\n    \n    @Test\n    void testOnlyName2() {\n        List<PathSearch.Chunk> list = PathSearch.split(\"//listitem/{Taxpayer Information}\");\n        logger.debug(\"list: {}\", list);\n        PathSearch.Chunk first = list.get(0);\n        assertTrue(first.anyDepth);\n        assertEquals(\"listitem\", first.controlType);\n        assertNull(first.className);\n        assertNull(first.name);\n        assertEquals(-1, first.index);\n        PathSearch.Chunk second = list.get(1);\n        assertFalse(second.anyDepth);\n        assertEquals(null, second.controlType);\n        assertEquals(null, second.className);\n        assertEquals(\"Taxpayer Information\", second.name); \n        assertNotNull(second.nameCondition);\n    }    \n\n}\n"
  },
  {
    "path": "karate-robot/src/test/java/com/intuit/karate/robot/win/WinRobotRunner.java",
    "content": "package com.intuit.karate.robot.win;\n\nimport com.intuit.karate.robot.Element;\nimport com.intuit.karate.robot.Robot;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport robot.core.ChromeJavaRunner;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\n/**\n *\n * @author pthomas3\n */\npublic class WinRobotRunner {\n\n    private static final Logger logger = LoggerFactory.getLogger(WinRobotRunner.class);\n\n    @Test\n    public void testRobot() {\n        Robot robot = ChromeJavaRunner.getRobot();\n        Element e = robot.getRoot();\n        assertEquals(\"Desktop\", e.getName());\n        Element win = robot.window(\"^NetBeans\");\n        assertNotNull(win);\n        logger.debug(\"name: {}\", win.getName());\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/test/java/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n  \n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>target/karate.log</file>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>    \n   \n    <logger name=\"com.intuit.karate\" level=\"DEBUG\"/>\n    <logger name=\"robot\" level=\"DEBUG\"/>\n   \n    <root level=\"warn\">\n        <appender-ref ref=\"STDOUT\" />\n        <appender-ref ref=\"FILE\" />\n    </root>\n  \n</configuration>"
  },
  {
    "path": "karate-robot/src/test/java/robot/core/ChromeJavaRunner.java",
    "content": "package robot.core;\r\n\r\nimport com.intuit.karate.Suite;\r\nimport com.intuit.karate.core.Feature;\r\nimport com.intuit.karate.core.FeatureCall;\r\nimport com.intuit.karate.driver.Keys;\r\nimport com.intuit.karate.robot.RobotBase;\r\nimport com.intuit.karate.robot.RobotFactory;\r\nimport com.intuit.karate.core.FeatureRuntime;\r\nimport com.intuit.karate.core.ScenarioRuntime;\r\nimport org.junit.jupiter.api.Test;\r\n\r\n\r\n/**\r\n *\r\n * @author pthomas3\r\n */\r\npublic class ChromeJavaRunner {\r\n\r\n    public static RobotBase getRobot() {\r\n        Feature feature = Feature.read(\"classpath:robot/core/dummy.feature\");\r\n        FeatureRuntime fr = FeatureRuntime.of(new Suite(), new FeatureCall(feature));\r\n        ScenarioRuntime sr = fr.scenarios.next();\r\n        return (RobotBase) new RobotFactory().create(sr, null);\r\n    }\r\n\r\n    @Test\r\n    void testChrome() {\r\n        RobotBase bot = getRobot();\r\n        // make sure Chrome is open\r\n        bot.window(t -> t.contains(\"Chrome\"));\r\n        bot.input(Keys.META + \"t\");\r\n        bot.input(\"karate dsl\" + Keys.ENTER);\r\n        bot.waitFor(\"tams.png\").click();\r\n    }\r\n\r\n}\r\n"
  },
  {
    "path": "karate-robot/src/test/java/robot/core/CoreRunner.java",
    "content": "package robot.core;\n\nimport com.intuit.karate.junit5.Karate;\n\n/**\n *\n * @author peter\n */\nclass CoreRunner {\n\n    @Karate.Test\n    Karate testCalc() {\n        return Karate.run(\"calc\").relativeTo(getClass());\n    }\n\n    @Karate.Test\n    Karate testCaller() {\n        return Karate.run(\"caller\").relativeTo(getClass());\n    }\n\n    @Karate.Test\n    Karate testChrome() {\n        return Karate.run(\"chrome\").relativeTo(getClass());\n    }\n\n    @Karate.Test\n    Karate testIphone() {\n        return Karate.run(\"iphone\").relativeTo(getClass());\n    }\n\n    @Karate.Test\n    Karate testUpload() {\n        return Karate.run(\"upload\").relativeTo(getClass());\n    }\n\n    @Karate.Test\n    Karate testWordpad() {\n        return Karate.run(\"wordpad\").relativeTo(getClass());\n    }\n\n}\n"
  },
  {
    "path": "karate-robot/src/test/java/robot/core/calc.feature",
    "content": "Feature: windows calculator\n\nScenario:\n* robot { window: 'Calculator', fork: 'calc', highlight: true }\n* click('Clear')\n* click('One')\n* click('Plus')\n* click('Two')\n* click('Equals')\n* match locate('#CalculatorResults').name == 'Display is  3 '\n* screenshot()\n"
  },
  {
    "path": "karate-robot/src/test/java/robot/core/called.feature",
    "content": "@ignore\nFeature:\n\nScenario:\n* print 'called'\n"
  },
  {
    "path": "karate-robot/src/test/java/robot/core/caller.feature",
    "content": "Feature:\n\nBackground:\n#* def utils = call read('')\n* def data = [{a: 1},{a: 2}]\n* robot { window: '^Chrome' }\n\nScenario Outline:\n* print __num\n* screenshot()\n\nExamples:\n| data |\n\n\n"
  },
  {
    "path": "karate-robot/src/test/java/robot/core/chrome.feature",
    "content": "Feature: browser + robot test\n\nScenario:\n# make sure chrome is running\n# * karate.exec('Chrome')\n# on windows you may need to change this to \"New Tab\"\n* robot { window: '^Chrome', highlight: true, highlightDuration: 500 }\n# on windows use Key.CONTROL\n* input(Key.META + 't')\n* input('karate dsl' + Key.ENTER)\n# if this does not work try to re-create the PNG image\n* waitFor('tams.png').click()\n* delay(2000)\n* screenshot()\n"
  },
  {
    "path": "karate-robot/src/test/java/robot/core/dummy.feature",
    "content": "Feature:\n\nScenario:\n* print 'dummy'\n"
  },
  {
    "path": "karate-robot/src/test/java/robot/core/iphone.feature",
    "content": "Feature: browser + robot test\n\nScenario:\n# * karate.exec('Chrome')\n* robot { window: '^Simulator', highlight: true }\n* click('iphone-click.png')\n"
  },
  {
    "path": "karate-robot/src/test/java/robot/core/upload.feature",
    "content": "Feature:\n\nScenario:\n* configure driver = { type: 'chrome' }\n* driver 'http://the-internet.herokuapp.com/upload'\n* robot { window: '^Chrome', highlight: true }\n# since we have the driver active, the \"robot\" namespace is needed\n* robot.waitFor('choose-file.png').click().delay(1000)\n* robot.input('/Users/pthomas3/Desktop' + Key.ENTER)\n* robot.waitFor('file-name.png').click()\n* robot.input(Key.ENTER).delay(1000)\n* submit().click('#file-submit')\n* waitForText('#uploaded-files', 'billie.jpg')\n* screenshot()\n"
  },
  {
    "path": "karate-robot/src/test/java/robot/core/wordpad.feature",
    "content": "Feature: windows notepad\n\n  Scenario:\n    * robot { window: 'Document - WordPad', fork: 'write', highlight: true }\n    * def sWidget = '//document'\n    * def sText = 'This is a test'\n    * locate(sWidget).click()\n    * input(sText + ' with press & release !')\n    * match locate(sWidget).value contains sText\n    # Sélectionne l'écran et copie le contenu dans le clipboard\n    * def oRegion = locate(sWidget).region\n    * print 'region:', oRegion\n    * move(oRegion.x + 1, oRegion.y + 1).press().move(oRegion.x + 10, oRegion.y + 10).release()\n    * input(Key.CONTROL + 'c')\n    * print 'clipboard:', robot.clipboard.trim()\n\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>io.karatelabs</groupId>\n    <artifactId>karate-parent</artifactId>\n    <version>1.5.2</version>\n    <packaging>pom</packaging>\n    \n    <name>${project.artifactId}</name>\n    <description>Test Automation Made Simple</description>\n    <url>https://github.com/karatelabs/karate</url>\n    <licenses>\n        <license>\n            <name>MIT License</name>\n            <url>https://www.opensource.org/licenses/mit-license.php</url>\n            <distribution>repo</distribution>\n        </license>\n    </licenses>\n    <scm>\n        <url>https://github.com/karatelabs/karate</url>\n        <connection>scm:git:git://github.com/karatelabs/karate.git</connection>\n        <developerConnection>scm:git:git@github.com:karatelabs/karate.git</developerConnection>\n    </scm>\n    <developers>\n        <developer>\n            <name>Peter Thomas</name>\n            <id>ptrthomas</id>\n            <organization>Karate Labs Inc.</organization>\n            <organizationUrl>https://karatelabs.io</organizationUrl>\n        </developer>\n    </developers>       \n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <java.version>17</java.version>\n        <maven.compiler.version>3.13.0</maven.compiler.version>\n        <maven.surefire.version>3.5.2</maven.surefire.version>\n        <maven.shade.version>3.6.0</maven.shade.version>\n        <maven.javadoc.version>3.6.3</maven.javadoc.version>\n        <junit5.version>5.11.4</junit5.version>\n        <central.publishing.maven.plugin.version>0.8.0</central.publishing.maven.plugin.version>\n        <spring.version>5.3.19</spring.version>\n        <spring.boot.version>2.6.6</spring.boot.version>\n        <jacoco.plugin.version>0.8.12</jacoco.plugin.version>\n    </properties>\n\n    <modules>\n        <module>karate-core</module>        \n        <module>karate-junit5</module>\n        <module>karate-demo</module>        \n        <module>karate-gatling</module>\n        <module>karate-playwright</module>\n        <module>karate-robot</module>        \n        <module>karate-e2e-tests</module>\n        <module>karate-archetype</module>\n    </modules>\n\n    <build>       \n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${maven.compiler.version}</version>\n                <configuration>\n                    <encoding>UTF-8</encoding>\n                    <source>${java.version}</source>\n                    <target>${java.version}</target>\n                    <!-- <parameters>true</parameters> -->\n                    <!-- <compilerArgument>-Werror</compilerArgument> -->\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>${maven.surefire.version}</version>\n                <configuration>\n                    <argLine>-Dfile.encoding=UTF-8</argLine>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-source-plugin</artifactId>\n                <version>2.2.1</version>\n                <executions>\n                    <execution>\n                        <id>attach-sources</id>\n                        <goals>\n                            <goal>jar-no-fork</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>                                                          \n        </plugins>\n    </build>\n    \n    <profiles>\n        <profile> \n            <id>depcheck</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.owasp</groupId>\n                        <artifactId>dependency-check-maven</artifactId>\n                        <version>12.1.0</version>\n                        <configuration>\n                            <skipProvidedScope>true</skipProvidedScope>\n                            <nvdApiDelay>3000</nvdApiDelay>\n                        </configuration>                        \n                        <executions>\n                            <execution>\n                                <goals>\n                                    <goal>aggregate</goal>\n                                </goals>\n                                <configuration>\n                                    <failBuildOnCVSS>0</failBuildOnCVSS>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin> \n                </plugins>\n            </build>\n        </profile>        \n        <profile> \n            <id>release</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-javadoc-plugin</artifactId>\n                        <version>${maven.javadoc.version}</version>\n                        <executions>\n                            <execution>\n                                <id>attach-javadocs</id>\n                                <goals>\n                                    <goal>jar</goal>\n                                </goals>\n                                <configuration>\n                                    <doclint>none</doclint>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-gpg-plugin</artifactId>\n                        <version>3.2.2</version>\n                        <executions>\n                            <execution>\n                                <id>sign-artifacts</id>\n                                <phase>verify</phase>\n                                <goals>\n                                    <goal>sign</goal>\n                                </goals>\n                                <configuration>\n                                    <gpgArguments>\n                                        <arg>--pinentry-mode</arg>\n                                        <arg>loopback</arg>\n                                    </gpgArguments>\n                                </configuration>                                                           \n                            </execution>                            \n                        </executions>                                                                                         \n                    </plugin>\n                    <plugin>\n                        <groupId>org.sonatype.central</groupId>\n                        <artifactId>central-publishing-maven-plugin</artifactId>\n                        <version>${central.publishing.maven.plugin.version}</version>\n                        <extensions>true</extensions>\n                        <configuration>\n                            <publishingServerId>central</publishingServerId>\n                            <autoPublish>true</autoPublish>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n\n</project>"
  }
]