diff --git a/.coveralls.yml b/.coveralls.yml
index 8115fc995..5ae55a93b 100644
--- a/.coveralls.yml
+++ b/.coveralls.yml
@@ -1,2 +1,2 @@
-coverage_clover: ./logs/coverage-clover.xml
+coverage_clover: ./logs/clover.xml
json_path: ./logs/coveralls-upload.json
diff --git a/.gitattributes b/.gitattributes
index c3b6383af..2fdc4ff88 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,8 +1,15 @@
* text=auto
+/scripts export-ignore
/tests export-ignore
+/tools export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
-/.travis.yml export-ignore
/example.php export-ignore
/phpunit.xml.dist export-ignore
+/.github export-ignore
+/.php-cs-fixer.dist.php export-ignore
+/phpstan.neon export-ignore
+/.coveralls.yml export-ignore
+/logs export-ignore
+/mlc_config.json export-ignore
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index b7fa587cc..a79b02b1f 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -2,62 +2,93 @@
We love to have your help to make php-webdriver better!
-Feel free to open an [issue](https://github.com/facebook/php-webdriver/issues) if you run into any problem, or
+Feel free to open an [issue](https://github.com/php-webdriver/php-webdriver/issues) if you run into any problem, or
send a pull request (see bellow) with your contribution.
-## Code of Conduct
-The code of conduct is described in [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md)
+## Before you contribute
+
+Do not hesitate to ask for a guidance before you implement notable change, or a new feature - use the associated [issue](https://github.com/php-webdriver/php-webdriver/issues) or use [Discussions](https://github.com/php-webdriver/php-webdriver/discussions).
+Because any new code means increased effort in library maintenance (which is being done by volunteers in their free time),
+please understand not every pull request is automatically accepted. This is why we recommend using the mentioned channels to discuss bigger changes in the source code first.
+
+When you are going to contribute, also please keep in mind that this webdriver client aims to be similar to clients in languages Java/Ruby/Python/C#.
+Here is the [official documentation](https://www.selenium.dev/documentation/en/) and overview of [the official Java API](http://seleniumhq.github.io/selenium/docs/api/java/)
## Workflow when contributing a patch
1. Fork the project on GitHub
2. Implement your code changes into separate branch
-3. Make sure all PHPUnit tests passes and code-style matches PSR-2 (see below). We also have Travis CI build which will automatically run tests on your pull request.
-4. When implementing notable change, fix or a new feature, add record to Unreleased section of [CHANGELOG.md](CHANGELOG.md)
-5. Submit your [pull request](https://github.com/facebook/php-webdriver/pulls) against community branch
-
-When you are going to contribute, please keep in mind that this webdriver client aims to be as close as possible to other languages Java/Ruby/Python/C#.
-FYI, here is the overview of [the official Java API](http://seleniumhq.github.io/selenium/docs/api/java/)
+3. Make sure all PHPUnit tests passes and code-style matches PSR-2 (see below). We also have CI builds which will automatically run tests on your pull request. Make sure to fix any reported issues reported by these automated tests.
+4. When implementing a notable change, fix or a new feature, add record to the Unreleased section of [CHANGELOG.md](../CHANGELOG.md)
+5. Submit your [pull request](https://github.com/php-webdriver/php-webdriver/pulls) against `main` branch
+
+### Run automated code checks
+
+To make sure your code comply with [PSR-2](http://www.php-fig.org/psr/psr-2/) coding style, tests passes and to execute other automated checks, run locally:
+
+```sh
+composer all
+```
-### Run unit tests
+To run functional tests locally there is some additional setup needed - see below. Without this setup, functional tests will be skipped.
-There are two test-suites: one with unit tests only, second with functional tests, which require running selenium server.
-To execute all tests simply run:
+For easier development there are also few other prepared commands:
+- `composer fix` - to auto-fix the codestyle and composer.json
+- `composer analyze` - to run only code analysis (without tests)
+- `composer test` - to run all tests
- ./vendor/bin/phpunit
+### Unit tests
-If you want to execute just the unit tests, run:
+There are two test-suites: one with **unit tests** only (`unit`), and second with **functional tests** (`functional`),
+which requires running Selenium server and local PHP server.
- ./vendor/bin/phpunit --testsuite unit
+To execute **all tests** in both suites run:
-For the functional tests you must first [download](http://selenium-release.storage.googleapis.com/index.html) and start
-the selenium standalone server, start the local PHP server which will serve the test pages and then run the `functional`
-test suite:
+```sh
+composer test
+```
- java -jar selenium-server-standalone-3.9.1.jar -log selenium.log &
- php -S localhost:8000 -t tests/functional/web/ &
- ./vendor/bin/phpunit --testsuite functional
+If you want to execute **just the unit tests**, run:
-The functional tests will be started in HtmlUnit headless browser by default. If you want to run them in eg. Firefox,
-simply set the `BROWSER_NAME` environment variable:
+```sh
+composer test -- --testsuite unit
+```
- ...
- export BROWSER_NAME="firefox"
- ./vendor/bin/phpunit --testsuite functional
+**Functional tests** are run against a real browser. It means they take a bit longer and also require an additional setup:
+you must first [download](https://www.selenium.dev/downloads/) and start the Selenium standalone server,
+then start the local PHP server which will serve the test pages and then run the `functional` test suite:
-To test with Geckodriver, [download](https://github.com/mozilla/geckodriver/releases) and start the server, then run:
+```sh
+export BROWSER_NAME="htmlunit" # see below for other browsers
+java -jar selenium-server-X.XX.0.jar standalone --log selenium.log &
+php -S localhost:8000 -t tests/functional/web/ &
+# Use following to run both unit and functional tests
+composer all
+# Or this to run only functional tests:
+composer test -- --testsuite functional
+```
- export GECKODRIVER=1
- export BROWSER_NAME=firefox
- ./vendor/bin/phpunit --testsuite functional
+If you want to run tests in different browser then "htmlunit" (Chrome or Firefox), you need to set up the browser driver (Chromedriver/Geckodriver), as it is [explained in wiki](https://github.com/php-webdriver/php-webdriver/wiki/Chrome)
+and then the `BROWSER_NAME` environment variable:
-### Check coding style
+```sh
+...
+export BROWSER_NAME="chrome"
+composer all
+```
-Your code-style should comply with [PSR-2](http://www.php-fig.org/psr/psr-2/). To make sure your code matches this requirement run:
+To test with Firefox/Geckodriver, you must also set `GECKODRIVER` environment variable:
- composer codestyle:check
+```sh
+export GECKODRIVER=1
+export BROWSER_NAME="firefox"
+composer all
+```
-To auto-fix the codestyle simply run:
+To see the tests as they are happening (in the browser window), you can disable headless mode. This is useful eg. when debugging the tests or writing a new one:
- composer codestyle:fix
+```sh
+export DISABLE_HEADLESS="1"
+composer all
+```
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
deleted file mode 100644
index 3eef8cde7..000000000
--- a/.github/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,21 +0,0 @@
-### What are you trying to achieve? (Expected behavior)
-
-
-### What do you get instead? (Actual behavior)
-
-
-### How could the issue be reproduced? (Steps to reproduce)
-
-
-```php
-// You can insert your PHP code here (or remove this block if it is not relevant for the issue).
-```
-
-### Details
-
-
-* Php-webdriver version:
-* PHP version:
-* Selenium server version:
-* Operating system:
-* Browser used + version:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 000000000..bac315d1d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,124 @@
+name: 🐛 Bug report
+description: Create a bug report to help us improve php-webdriver
+labels: [ "bug" ]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ If you have a question, [ask in Discussions](https://github.com/php-webdriver/php-webdriver/discussions) instead of filling a bug.
+
+ If you are reporting a bug, please **fill as much as possible information**, otherwise the community and maintainers cannot provide a prompt feedback and help solving the issue.
+ - type: textarea
+ id: bug-description
+ attributes:
+ label: Bug description
+ description: |
+ A clear description of what the bug is.
+ validations:
+ required: true
+
+ - type: textarea
+ id: steps-to-reproduce
+ attributes:
+ label: How could the issue be reproduced
+ description: |
+ Provide steps to reproduce the behavior. Please include everything relevant - the PHP code you use to initialize driver instance, the PHP code causing the error, HTML snippet or URL of the page where you encounter the issue etc.
+ This will be automatically formatted into code, so no need for backticks ```.
+ placeholder: |
+ // For example you can provide how you create WebDriver instance:
+ $capabilities = DesiredCapabilities::chrome();
+ $driver = RemoteWebDriver::create('http://localhost:4444/', $capabilities);
+ // And the code you use to execute the php-webdriver commands, for example:
+ $driver->get('http://site.localhost/foo.html');
+ $button = $driver->findElement(WebDriverBy::cssSelector('#foo'));
+ $button->click();
+
+
+ Foo
+
+
+ render: shell
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected-behavior
+ attributes:
+ label: Expected behavior
+ description: |
+ A clear and concise description of what you expected to happen.
+ validations:
+ required: false
+
+ - type: input
+ id: php-webdriver-version
+ attributes:
+ label: Php-webdriver version
+ description: You can run `composer show php-webdriver/webdriver` to find the version number
+ placeholder: |
+ For example: 1.13.0
+ validations:
+ required: true
+
+ - type: input
+ id: php-version
+ attributes:
+ label: PHP version
+ description: You can run `php -v` to find the version
+ placeholder: |
+ For example: 8.1.11
+ validations:
+ required: true
+
+ - type: input
+ id: how-start
+ attributes:
+ label: How do you start the browser driver or Selenium server
+ description: |
+ For example: Selenium server jar, Selenium in Docker, chromedriver command, Laravel Dusk, SauceLabs etc.
+ If relevant, provide the complete command you use to start the browser driver or Selenium server
+ validations:
+ required: true
+
+ - type: input
+ id: selenium-version
+ attributes:
+ label: Selenium server / Selenium Docker image version
+ description: Relevant only if you use Selenium server / Selenium in Docker
+ validations:
+ required: false
+
+ - type: input
+ id: browser-driver
+ attributes:
+ label: Browser driver (chromedriver/geckodriver...) version
+ description: You can run `chromedriver --version` or `geckodriver --version` to find the version
+ placeholder: |
+ For example: geckodriver 0.31.0
+ validations:
+ required: false
+
+ - type: input
+ id: browser
+ attributes:
+ label: Browser name and version
+ placeholder: |
+ For example: Firefox 105.0.2
+ validations:
+ required: false
+
+ - type: input
+ id: operating-system
+ attributes:
+ label: Operating system
+ validations:
+ required: false
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional context
+ description: |
+ Add any other context or you notes about the problem here.
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..e9a259980
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: ❓ Questions and Help
+ url: https://github.com/php-webdriver/php-webdriver/discussions
+ about: Please ask and answer questions here
+ - name: 💡 Ideas and feature requests
+ url: https://github.com/php-webdriver/php-webdriver/discussions
+ about: Suggest an idea for php-webdriver
diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
new file mode 100644
index 000000000..50112e87a
--- /dev/null
+++ b/.github/dependabot.yaml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: daily
+ time: "04:00"
diff --git a/.github/workflows/coveralls-workaround.yaml b/.github/workflows/coveralls-workaround.yaml
new file mode 100644
index 000000000..d6044861d
--- /dev/null
+++ b/.github/workflows/coveralls-workaround.yaml
@@ -0,0 +1,48 @@
+name: Coveralls coverage
+# Must be run in separate workflow to have access to repository secrets even for PR from forks.
+# See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
+
+permissions:
+ contents: read
+
+on:
+ workflow_run:
+ workflows: [ "Tests" ]
+ types:
+ - completed
+
+jobs:
+ coveralls:
+ name: Coveralls coverage workaround
+ runs-on: ubuntu-latest
+ if: >
+ github.event.workflow_run.event == 'pull_request' &&
+ github.event.workflow_run.conclusion == 'success'
+ steps:
+ # see https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
+ - name: 'Download artifact'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: ${{github.event.workflow_run.id }},
+ });
+ var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
+ return artifact.name == "data"
+ })[0];
+ var download = await github.rest.actions.downloadArtifact({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ artifact_id: matchArtifact.id,
+ archive_format: 'zip',
+ });
+ var fs = require('fs');
+ fs.writeFileSync('${{github.workspace}}/data.zip', Buffer.from(download.data));
+ - run: unzip data.zip
+ - name: Coveralls coverage workaround
+ # see https://github.com/lemurheavy/coveralls-public/issues/1653#issuecomment-1251587119
+ run: |
+ BUILD_NUM=$(cat run_id)
+ curl --location --request GET "https://coveralls.io/rerun_build?repo_token=${{%20secrets.COVERALLS_REPO_TOKEN%20}}&build_num=$BUILD_NUM"
diff --git a/.github/workflows/docs-lint.yml b/.github/workflows/docs-lint.yml
new file mode 100644
index 000000000..42baa0000
--- /dev/null
+++ b/.github/workflows/docs-lint.yml
@@ -0,0 +1,26 @@
+name: Lint PHP documentation
+
+permissions:
+ contents: read
+
+on:
+ push:
+ pull_request:
+ branches:
+ - 'main'
+
+jobs:
+ lint-docs:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Lint PHP documentation
+ uses: sudo-bot/action-doctum@v5
+ with:
+ config-file: scripts/doctum.php
+ method: 'parse'
+ cli-args: '--output-format=github --no-ansi --no-progress -v --ignore-parse-errors'
diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml
new file mode 100644
index 000000000..d0da12f8b
--- /dev/null
+++ b/.github/workflows/docs-publish.yml
@@ -0,0 +1,38 @@
+name: Publish API documentation
+
+permissions:
+ contents: read
+
+on:
+ repository_dispatch:
+ types: [ run-build-api-docs ]
+ workflow_dispatch:
+ schedule:
+ - cron: "00 12 * * *"
+
+jobs:
+ publish-pages:
+ environment:
+ name: API documentation
+ url: https://php-webdriver.github.io/php-webdriver/
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ ssh-key: ${{ secrets.SSH_KEY_DEPLOY }}
+
+ - name: Build PHP documentation
+ uses: sudo-bot/action-doctum@v5
+ with:
+ config-file: scripts/doctum.php
+ method: 'update'
+ cli-args: '--output-format=github --no-ansi --no-progress -v --ignore-parse-errors'
+
+ - name: Set commit author
+ run: |
+ git config user.name "Automated"
+ git config user.email "actions@users.noreply.github.com"
+ - name: Push the changes
+ run: ./scripts/update-built-docs.sh
diff --git a/.github/workflows/no-response.yaml b/.github/workflows/no-response.yaml
new file mode 100644
index 000000000..7147e3792
--- /dev/null
+++ b/.github/workflows/no-response.yaml
@@ -0,0 +1,29 @@
+name: No Response
+
+permissions:
+ issues: write
+
+# Both `issue_comment` and `scheduled` event types are required for this Action to work properly.
+on:
+ issue_comment:
+ types: [created]
+ schedule:
+ - cron: '* */8 * * *' # every hour at :33
+
+jobs:
+ noResponse:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: lee-dohm/no-response@v0.5.0
+ with:
+ token: ${{ github.token }}
+ daysUntilClose: 14
+ responseRequiredLabel: 'waiting for reaction'
+ closeComment: >
+ This issue has been automatically closed because there has been no response
+ to our request for more information from the original author. With only the
+ information that is currently in the issue, we don't have enough information
+ to take action.
+
+ If the original issue author adds comment with more information,
+ this issue will be automatically reopened and we can investigate further.
diff --git a/.github/workflows/php.yaml b/.github/workflows/php.yaml
deleted file mode 100644
index 04fd5b9da..000000000
--- a/.github/workflows/php.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: PHP
-
-on:
- push:
- pull_request:
- schedule:
- - cron: '0 3 * * *'
-
-jobs:
- analyze:
- name: "Code style and static analysis"
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v1
- with:
- php-version: '7.4'
- extensions: mbstring, intl, zip
-
- - name: Install dependencies
- run: |
- composer update --no-interaction
- composer require phpstan/phpstan # Not part of require-dev, because it won't install on PHP 5.6
-
- - name: Run checks
- run: |
- composer analyze
- composer codestyle:check
diff --git a/.github/workflows/sauce-labs.yaml b/.github/workflows/sauce-labs.yaml
new file mode 100644
index 000000000..812fbfe25
--- /dev/null
+++ b/.github/workflows/sauce-labs.yaml
@@ -0,0 +1,74 @@
+name: Sauce Labs
+
+permissions:
+ contents: read
+
+on:
+ push:
+ schedule:
+ - cron: '0 3 * * *'
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+ # Source: https://github.community/t/do-not-run-cron-workflows-in-forks/17636/2
+ if: (github.event_name == 'schedule' && github.repository == 'php-webdriver/php-webdriver') || (github.event_name != 'schedule')
+ env:
+ SAUCELABS: 1
+ SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
+ SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # Chrome 74 is the last version which doesn't use W3C WebDriver by default and rather use OSS protocol
+ - { name: "Chrome 74, OSS protocol", BROWSER_NAME: "chrome", VERSION: "74.0", PLATFORM: "Windows 11", w3c: false, tunnel-name: "gh-1-chrome-oss-legacy" }
+ - { name: "Chrome latest, W3C protocol", BROWSER_NAME: "chrome", VERSION: "latest", PLATFORM: "Windows 11", w3c: true, tunnel-name: "gh-2-chrome-w3c" }
+ - { name: "Edge latest, W3C protocol", BROWSER_NAME: "MicrosoftEdge", VERSION: "latest", PLATFORM: "Windows 11", w3c: true, tunnel-name: "gh-3-MicrosoftEdge" }
+
+ name: ${{ matrix.name }} (${{ matrix.tunnel-name }})
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: mbstring, intl, zip
+ coverage: none
+
+ - name: Install PHP dependencies
+ run: composer update --no-interaction
+
+ - name: Start local PHP server
+ run: |
+ php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log &
+
+ - name: Start Sauce Connect
+ uses: saucelabs/sauce-connect-action@v3
+ with:
+ username: ${{ secrets.SAUCE_USERNAME }}
+ accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
+ tunnelName: ${{ matrix.tunnel-name }}
+ proxyLocalhost: allow
+ region: 'us-west-1'
+
+ - name: Run tests
+ env:
+ BROWSER_NAME: ${{ matrix.BROWSER_NAME }}
+ VERSION: ${{ matrix.VERSION }}
+ PLATFORM: ${{ matrix.PLATFORM }}
+ DISABLE_W3C_PROTOCOL: "${{ matrix.w3c && '0' || '1' }}"
+ SAUCE_TUNNEL_NAME: ${{ matrix.tunnel-name }}
+ run: |
+ if [ -n "$SAUCELABS" ]; then EXCLUDE_GROUP+="exclude-saucelabs,"; fi
+ if [ "$BROWSER_NAME" = "MicrosoftEdge" ]; then EXCLUDE_GROUP+="exclude-edge,"; fi
+ if [ "$BROWSER_NAME" = "firefox" ]; then EXCLUDE_GROUP+="exclude-firefox,"; fi
+ if [ "$BROWSER_NAME" = "chrome" ]; then EXCLUDE_GROUP+="exclude-chrome,"; fi
+ if [ -n "$EXCLUDE_GROUP" ]; then EXTRA_PARAMS+=" --exclude-group $EXCLUDE_GROUP"; fi
+ ./vendor/bin/phpunit --testsuite functional $EXTRA_PARAMS
+
+ - name: Print logs
+ if: ${{ always() }}
+ run: |
+ if [ -f ./logs/php-server.log ]; then cat ./logs/php-server.log; fi
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
new file mode 100644
index 000000000..d215e92ce
--- /dev/null
+++ b/.github/workflows/tests.yaml
@@ -0,0 +1,214 @@
+name: Tests
+
+permissions:
+ contents: read
+
+on:
+ push:
+ pull_request:
+ schedule:
+ - cron: '0 3 * * *'
+
+jobs:
+ analyze:
+ name: "Code style and static analysis"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: mbstring, intl, zip
+
+ - name: Install PHP dependencies
+ run: composer update --no-interaction
+
+ - name: Lint
+ run: composer lint
+
+ - name: Run analysis
+ run: composer analyze
+
+ markdown-link-check:
+ name: "Markdown link check"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - uses: tcort/github-action-markdown-link-check@v1
+ with:
+ use-verbose-mode: 'yes'
+
+ unit-tests:
+ name: "Unit tests"
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ php-version: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
+ dependencies: ['']
+ include:
+ - { php-version: '7.3', dependencies: '--prefer-lowest' }
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-version }}
+ extensions: mbstring, intl, zip
+ coverage: xdebug
+ ini-values: ${{ matrix.xdebug-ini-values }}
+
+ - name: Install PHP dependencies
+ run: composer update --no-interaction ${{ matrix.dependencies }}
+
+ - name: Run tests
+ run: vendor/bin/phpunit --testsuite unit --colors=always --coverage-clover ./logs/clover.xml
+
+ - name: Submit coverage to Coveralls
+ # We use php-coveralls library for this, as the official Coveralls GitHub Action lacks support for clover reports:
+ # https://github.com/coverallsapp/github-action/issues/15
+ env:
+ COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ COVERALLS_PARALLEL: true
+ COVERALLS_FLAG_NAME: ${{ github.job }}-PHP-${{ matrix.php-version }} ${{ matrix.dependencies }}
+ run: |
+ composer global require php-coveralls/php-coveralls
+ ~/.composer/vendor/bin/php-coveralls -v
+
+ functional-tests:
+ runs-on: ${{ matrix.os }}
+ env:
+ SELENIUM_SERVER_DOWNLOAD_URL: https://github.com/SeleniumHQ/selenium/releases/download/selenium-4.38.0/selenium-server-4.38.0.jar
+
+ strategy:
+ fail-fast: false
+ matrix:
+ os: ['ubuntu-latest']
+ browser: ['chrome', 'firefox']
+ selenium-server: [true, false] # Whether to run via Selenium server or directly via browser driver
+ w3c: [true] # Although all builds negotiate protocol by default, it implies W3C protocol for both Chromedriver and Geckodriver
+ include:
+ - { browser: 'safari', os: 'macos-latest', selenium-server: false, w3c: true }
+ # Force OSS (JsonWire) protocol on ChromeDriver - to make sure we keep compatibility:
+ - { browser: 'chrome', os: 'ubuntu-latest', selenium-server: false, w3c: false }
+
+ name: "Functional tests (${{ matrix.browser }}, Selenium server: ${{ matrix.selenium-server }}, W3C: ${{ matrix.w3c }})"
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: mbstring, intl, zip
+ coverage: xdebug
+
+ - name: Install PHP dependencies
+ run: composer update --no-interaction
+
+ - name: Start Selenium standalone server
+ # If you want to run your Selenium WebDriver tests on GitHub actions, we recommend using service containers
+ # with eg. selenium/standalone-chrome image. See https://docs.github.com/en/actions/guides/about-service-containers
+ # But for the purpose of testing this library itself, we need more control, so we set everything up manually.
+ if: ${{ matrix.selenium-server }}
+ run: |
+ mkdir -p build logs
+ wget -q -t 3 -O build/selenium-server.jar $SELENIUM_SERVER_DOWNLOAD_URL
+ java -jar build/selenium-server.jar standalone --version
+ xvfb-run --server-args="-screen 0, 1280x720x24" --auto-servernum java -jar build/selenium-server.jar standalone --log logs/selenium-server.log &
+
+ - name: Start ChromeDriver
+ if: ${{ !matrix.selenium-server && matrix.browser == 'chrome' }}
+ run: |
+ google-chrome --version
+ xvfb-run --server-args="-screen 0, 1280x720x24" --auto-servernum \
+ chromedriver --port=4444 &> ./logs/chromedriver.log &
+
+ - name: Start GeckoDriver
+ if: ${{ !matrix.selenium-server && matrix.browser == 'firefox' }}
+ run: |
+ firefox --version
+ geckodriver --version
+ xvfb-run --server-args="-screen 0, 1280x720x24" --auto-servernum \
+ geckodriver &> ./logs/geckodriver.log &
+
+ - name: Start SafariDriver
+ if: ${{ !matrix.selenium-server && matrix.browser == 'safari' }}
+ run: |
+ defaults read /Applications/Safari.app/Contents/Info CFBundleShortVersionString
+ /usr/bin/safaridriver -p 4444 --diagnose &
+
+ - name: Start local PHP server
+ run: |
+ php -S 127.0.0.1:8000 -t tests/functional/web/ &> ./logs/php-server.log &
+
+ - name: Wait for browser & PHP to start
+ timeout-minutes: 1
+ run: |
+ while ! nc -z localhost 4444 ./data/run_id
+ - uses: actions/upload-artifact@v6
+ with:
+ name: data
+ path: data/
diff --git a/.gitignore b/.gitignore
index deb25f3ae..a9384110d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,13 @@
composer.phar
composer.lock
vendor
+tools/php-cs-fixer/vendor
.php_cs.cache
+.php-cs-fixer.cache
+.phpunit.result.cache
phpunit.xml
logs/
+build/
# generic files to ignore
*.lock
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 000000000..f754fb8b7
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,139 @@
+notPath('Firefox/FirefoxProfile.php') // need to use str_* instead of mb_str_* methods
+ ->in([__DIR__ . '/lib', __DIR__ . '/tests']);
+
+return (new PhpCsFixer\Config())
+ ->setRules([
+ '@PSR2' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'binary_operator_spaces' => true,
+ 'blank_line_before_statement' => ['statements' => ['return', 'try']],
+ 'braces' => ['allow_single_line_anonymous_class_with_empty_body' => true, 'allow_single_line_closure' => true],
+ 'cast_spaces' => true,
+ 'class_attributes_separation' => ['elements' => ['method' => 'one', 'trait_import' => 'none']],
+ 'clean_namespace' => true,
+ 'combine_nested_dirname' => true,
+ 'compact_nullable_typehint' => true,
+ 'concat_space' => ['spacing' => 'one'],
+ 'declare_equal_normalize' => true,
+ //'declare_strict_types' => true, // TODO: used only in tests, use in lib in next major version
+ 'fopen_flag_order' => true,
+ 'fopen_flags' => true,
+ 'full_opening_tag' => true,
+ 'function_typehint_space' => true,
+ 'heredoc_indentation' => ['indentation' => 'same_as_start'],
+ 'implode_call' => true,
+ 'is_null' => true,
+ 'lambda_not_used_import' => true,
+ 'list_syntax' => true,
+ 'lowercase_cast' => true,
+ 'lowercase_static_reference' => true,
+ 'magic_constant_casing' => true,
+ 'magic_method_casing' => true,
+ 'mb_str_functions' => true,
+ 'method_argument_space' => ['after_heredoc' => true],
+ 'native_function_casing' => true,
+ 'native_function_type_declaration_casing' => true,
+ 'new_with_braces' => true,
+ 'no_alias_functions' => true,
+ 'no_blank_lines_after_class_opening' => true,
+ 'no_blank_lines_after_phpdoc' => true,
+ 'no_empty_comment' => true,
+ 'no_empty_phpdoc' => true,
+ 'no_empty_statement' => true,
+ 'normalize_index_brace' => true,
+ 'no_extra_blank_lines' => [
+ 'tokens' => [
+ 'break',
+ 'case',
+ 'continue',
+ 'curly_brace_block',
+ 'default',
+ 'extra',
+ 'parenthesis_brace_block',
+ 'return',
+ 'square_brace_block',
+ 'switch',
+ 'throw',
+ 'use',
+ ],
+ ],
+ 'no_leading_import_slash' => true,
+ 'no_leading_namespace_whitespace' => true,
+ 'no_singleline_whitespace_before_semicolons' => true,
+ 'no_superfluous_phpdoc_tags' => [
+ 'allow_mixed' => true,
+ 'remove_inheritdoc' => true,
+ 'allow_unused_params' => true, // Used in RemoteWebDriver::createBySessionID to maintain BC
+ ],
+ 'no_trailing_comma_in_singleline' => true,
+ 'no_unreachable_default_argument_value' => true,
+ 'no_unused_imports' => true,
+ 'no_useless_else' => true,
+ 'no_useless_return' => true,
+ 'no_useless_sprintf' => true,
+ 'no_whitespace_before_comma_in_array' => ['after_heredoc' => true],
+ 'no_whitespace_in_blank_line' => true,
+ 'non_printable_character' => true,
+ 'nullable_type_declaration' => [
+ 'syntax' => 'question_mark',
+ ],
+ 'nullable_type_declaration_for_default_null_value' => true,
+ 'object_operator_without_whitespace' => true,
+ 'ordered_class_elements' => true,
+ 'ordered_imports' => true,
+ 'php_unit_construct' => true,
+ 'php_unit_dedicate_assert' => true,
+ 'php_unit_dedicate_assert_internal_type' => true,
+ 'php_unit_expectation' => ['target' => '8.4'],
+ 'php_unit_method_casing' => ['case' => 'camel_case'],
+ 'php_unit_mock_short_will_return' => true,
+ 'php_unit_mock' => true,
+ 'php_unit_namespaced' => ['target' => '6.0'],
+ 'php_unit_no_expectation_annotation' => true,
+ 'php_unit_set_up_tear_down_visibility' => true,
+ 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'],
+ 'phpdoc_add_missing_param_annotation' => true,
+ 'phpdoc_indent' => true,
+ 'phpdoc_no_access' => true,
+ // 'phpdoc_no_empty_return' => true, // disabled to allow forward compatibility with PHP 8.1
+ 'phpdoc_no_package' => true,
+ 'phpdoc_order_by_value' => ['annotations' => ['covers', 'group', 'throws']],
+ 'phpdoc_order' => true,
+ 'phpdoc_return_self_reference' => true,
+ 'phpdoc_scalar' => true,
+ 'phpdoc_single_line_var_spacing' => true,
+ 'phpdoc_trim' => true,
+ //'phpdoc_to_param_type' => true, // TODO: used only in tests, use in lib in next major version
+ //'phpdoc_to_return_type' => true, // TODO: used only in tests, use in lib in next major version
+ 'phpdoc_types' => true,
+ 'phpdoc_var_annotation_correct_order' => true,
+ 'pow_to_exponentiation' => true,
+ 'psr_autoloading' => true,
+ 'random_api_migration' => true,
+ 'self_accessor' => true,
+ 'set_type_to_cast' => true,
+ 'short_scalar_cast' => true,
+ 'single_blank_line_before_namespace' => true,
+ 'single_quote' => true,
+ 'single_space_after_construct' => true,
+ 'single_trait_insert_per_statement' => true,
+ 'space_after_semicolon' => true,
+ 'standardize_not_equals' => true,
+ 'strict_param' => true,
+ 'switch_continue_to_break' => true,
+ 'ternary_operator_spaces' => true,
+ 'ternary_to_elvis_operator' => true,
+ 'ternary_to_null_coalescing' => true,
+ 'trailing_comma_in_multiline' => ['elements' => ['arrays'], 'after_heredoc' => true],
+ 'trim_array_spaces' => true,
+ 'unary_operator_spaces' => true,
+ 'visibility_required' => ['elements' => ['method', 'property', 'const']],
+ //'void_return' => true, // TODO: used only in tests, use in lib in next major version
+ 'whitespace_after_comma_in_array' => true,
+ 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false],
+ ])
+ ->setRiskyAllowed(true)
+ ->setFinder($finder);
diff --git a/.php_cs.dist b/.php_cs.dist
deleted file mode 100644
index b771f8b04..000000000
--- a/.php_cs.dist
+++ /dev/null
@@ -1,91 +0,0 @@
-in([__DIR__ . '/lib', __DIR__ . '/tests']);
-
-return PhpCsFixer\Config::create()
- ->setRules([
- '@PSR2' => true,
- 'array_syntax' => ['syntax' => 'short'],
- 'binary_operator_spaces' => true,
- 'blank_line_before_return' => true,
- 'cast_spaces' => true,
- 'concat_space' => ['spacing' => 'one'],
- 'function_typehint_space' => true,
- 'general_phpdoc_annotation_remove' => ['author'],
- 'implode_call' => true,
- 'is_null' => true,
- 'linebreak_after_opening_tag' => true,
- 'lowercase_cast' => true,
- 'mb_str_functions' => true,
- 'method_separation' => true,
- 'native_function_casing' => true,
- 'new_with_braces' => true,
- 'no_alias_functions' => true,
- 'no_blank_lines_after_class_opening' => true,
- 'no_blank_lines_after_phpdoc' => true,
- 'no_empty_comment' => true,
- 'no_empty_phpdoc' => true,
- 'no_empty_statement' => true,
- 'no_extra_consecutive_blank_lines' => [
- 'use',
- 'break',
- 'continue',
- 'extra',
- 'return',
- 'throw',
- 'useTrait',
- 'curly_brace_block',
- 'parenthesis_brace_block',
- 'square_brace_block',
- ],
- 'no_leading_import_slash' => true,
- 'no_leading_namespace_whitespace' => true,
- 'no_singleline_whitespace_before_semicolons' => true,
- 'no_trailing_comma_in_singleline_array' => true,
- 'no_unreachable_default_argument_value' => true,
- 'no_unused_imports' => true,
- 'no_useless_else' => true,
- 'no_useless_return' => true,
- 'no_whitespace_in_blank_line' => true,
- 'object_operator_without_whitespace' => true,
- 'ordered_class_elements' => true,
- 'ordered_imports' => true,
- 'php_unit_construct' => true,
- 'php_unit_dedicate_assert' => true,
- 'php_unit_expectation' => ['target' => '5.6'],
- 'php_unit_method_casing' => ['case' => 'camel_case'],
- 'php_unit_mock' => true,
- 'php_unit_mock_short_will_return' => true,
- 'php_unit_namespaced' => ['target' => '5.7'],
- 'php_unit_no_expectation_annotation' => true,
- 'php_unit_ordered_covers' => true,
- 'php_unit_set_up_tear_down_visibility' => true,
- 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'],
- 'phpdoc_add_missing_param_annotation' => true,
- 'phpdoc_indent' => true,
- 'phpdoc_no_access' => true,
- 'phpdoc_no_empty_return' => true,
- 'phpdoc_no_package' => true,
- 'phpdoc_order' => true,
- 'phpdoc_scalar' => true,
- 'phpdoc_single_line_var_spacing' => true,
- 'phpdoc_trim' => true,
- 'phpdoc_types' => true,
- 'psr4' => true,
- 'self_accessor' => true,
- 'short_scalar_cast' => true,
- 'single_blank_line_before_namespace' => true,
- 'single_quote' => true,
- 'space_after_semicolon' => true,
- 'standardize_not_equals' => true,
- 'ternary_operator_spaces' => true,
- 'trailing_comma_in_multiline_array' => true,
- 'trim_array_spaces' => true,
- 'unary_operator_spaces' => true,
- 'visibility_required' => true,
- 'whitespace_after_comma_in_array' => true,
- 'yoda_style' => false,
- ])
- ->setRiskyAllowed(true)
- ->setFinder($finder);
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index a773b2c6f..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,172 +0,0 @@
-language: php
-dist: xenial
-
-php:
- - '5.6'
- - '7.0'
- - '7.1'
- - '7.2'
- - '7.3'
- - '7.4'
-
-env:
- global:
- - DISPLAY=:99.0
- - BROWSER_NAME="htmlunit"
- - SELENIUM_SERVER="https://selenium-release.storage.googleapis.com/3.14/selenium-server-standalone-3.14.0.jar" # Latest version including HtmlUnit
-
-services:
- - xvfb
-
-matrix:
- include:
- # Build with lowest possible dependencies on lowest possible PHP
- - name: 'Lowest dependencies build'
- php: '5.6'
- env: DEPENDENCIES="--prefer-lowest"
-
- # Firefox inside Travis environment
- - name: 'Firefox 45 on Travis (OSS protocol); via legacy Selenium server'
- php: '7.3'
- env:
- - BROWSER_NAME="firefox"
- - SELENIUM_SERVER="legacy"
- addons:
- firefox: "45.8.0esr"
-
- # Firefox with Geckodriver (W3C mode) inside Travis environment
- - name: 'Firefox latest on Travis (W3C protocol); no Selenium server'
- php: '7.3'
- env:
- - BROWSER_NAME="firefox"
- - GECKODRIVER="1"
- addons:
- firefox: latest
-
- # Stable Chrome + Chromedriver (W3C mode) inside Travis environment via Selenium server proxy
- - name: 'Chrome stable on Travis (W3C protocol); via Selenium server'
- php: '7.3'
- env:
- - BROWSER_NAME="chrome"
- addons:
- chrome: stable
-
- # Stable Chrome + Chromedriver (W3C mode) inside Travis environment directly via Chromedriver
- - name: 'Chrome stable on Travis (W3C protocol); no Selenium server'
- php: '7.3'
- env:
- - BROWSER_NAME="chrome"
- - CHROMEDRIVER="1"
- addons:
- chrome: stable
-
- # Stable Chrome + Chromedriver (JsonWire OSS mode) inside Travis environment directly via Chromedriver
- - name: 'Chrome stable on Travis (OSS protocol); no Selenium server'
- php: '7.3'
- env:
- - BROWSER_NAME="chrome"
- - CHROMEDRIVER="1"
- - DISABLE_W3C_PROTOCOL="1"
- addons:
- chrome: stable
-
- # Saucelabs builds
- - name: 'Sauce Labs, Firefox 47, OSS protocol'
- php: '7.3'
- env: SAUCELABS=1 BROWSER_NAME="firefox" VERSION="47.0" PLATFORM="Windows 10" DISABLE_W3C_PROTOCOL="1"
- before_script:
- - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log &
- - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started"
- addons:
- sauce_connect: true
- jwt:
- secure: 5s8iQfH1dHgEm0DeP9VZ/MCzCeiE/HnMWqPFzRmg6VD2qJ53oYdseo8j+QCbE25MIwoSnIbKzlnbCN6fVzZc/0S7Mo45xJiq8xVLPSdMjDoOeqYE4of+t5Srq4iSzGLPCLiMTtB4xDEl6blUVGhYxN5rA/tVN+cVtLNQvo3ovRon3Mw3MqR4pgCE6PofcLXtyJc3KuOBlUJLWdPGRdlZrpKWE5ogyj4a1h4bVwidckZqkOF+gm58Gf0zVfFazDQFIw2Xuq7SZmiNgdOD5yUEePkrMhy2tbOlPNAIgHCpzHldv5Y+GYyxIYHZ0mGlGxHrfjrcAoSA6r2iXB9q2ijLVwqOARpcvGcBzZBil9aMAHRIXHAOV9Ihv4velrzmiLKADtD60Bfj2zzntGYZA3EGucitMMkkP7vfAa769i5QWK1Lniq3+VUuGNVjRzl4GuQPpc0wMWeJvQGc5Uf9Kk/sOCkPp0SPWcZ6nNAUebRy3V5OoADA9IntyXxfTlZdOHSbJTsG+eOGve0uLGRAOS+oeCstO7Gk4e/Ylozju+ixkINEY7HHDGt6AyHGtjPdy08Y0XrIqs0JMxsHKrtTVNxDjIFKbMees+vtxU3DEr/tNo1sTo34ieGKZP2Cp5mG/IrcjD1saebUaCngQO3QfeuKcU8pBTR7l7PtFNHm3HrmdkY=
-
- - name: 'Sauce Labs, Chrome 74, OSS protocol'
- php: '7.3'
- env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="74.0" PLATFORM="Windows 10" DISABLE_W3C_PROTOCOL="1" # 74 is the last version which don't use W3C WebDriver by default
- before_script:
- - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log &
- - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started"
- addons:
- sauce_connect: true
- jwt:
- secure: 5s8iQfH1dHgEm0DeP9VZ/MCzCeiE/HnMWqPFzRmg6VD2qJ53oYdseo8j+QCbE25MIwoSnIbKzlnbCN6fVzZc/0S7Mo45xJiq8xVLPSdMjDoOeqYE4of+t5Srq4iSzGLPCLiMTtB4xDEl6blUVGhYxN5rA/tVN+cVtLNQvo3ovRon3Mw3MqR4pgCE6PofcLXtyJc3KuOBlUJLWdPGRdlZrpKWE5ogyj4a1h4bVwidckZqkOF+gm58Gf0zVfFazDQFIw2Xuq7SZmiNgdOD5yUEePkrMhy2tbOlPNAIgHCpzHldv5Y+GYyxIYHZ0mGlGxHrfjrcAoSA6r2iXB9q2ijLVwqOARpcvGcBzZBil9aMAHRIXHAOV9Ihv4velrzmiLKADtD60Bfj2zzntGYZA3EGucitMMkkP7vfAa769i5QWK1Lniq3+VUuGNVjRzl4GuQPpc0wMWeJvQGc5Uf9Kk/sOCkPp0SPWcZ6nNAUebRy3V5OoADA9IntyXxfTlZdOHSbJTsG+eOGve0uLGRAOS+oeCstO7Gk4e/Ylozju+ixkINEY7HHDGt6AyHGtjPdy08Y0XrIqs0JMxsHKrtTVNxDjIFKbMees+vtxU3DEr/tNo1sTo34ieGKZP2Cp5mG/IrcjD1saebUaCngQO3QfeuKcU8pBTR7l7PtFNHm3HrmdkY=
-
- - name: 'Sauce Labs, Chrome latest, W3C protocol'
- php: '7.3'
- env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="latest" PLATFORM="Windows 10"
- before_script:
- - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log &
- - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started"
- addons:
- sauce_connect: true
- jwt:
- secure: 5s8iQfH1dHgEm0DeP9VZ/MCzCeiE/HnMWqPFzRmg6VD2qJ53oYdseo8j+QCbE25MIwoSnIbKzlnbCN6fVzZc/0S7Mo45xJiq8xVLPSdMjDoOeqYE4of+t5Srq4iSzGLPCLiMTtB4xDEl6blUVGhYxN5rA/tVN+cVtLNQvo3ovRon3Mw3MqR4pgCE6PofcLXtyJc3KuOBlUJLWdPGRdlZrpKWE5ogyj4a1h4bVwidckZqkOF+gm58Gf0zVfFazDQFIw2Xuq7SZmiNgdOD5yUEePkrMhy2tbOlPNAIgHCpzHldv5Y+GYyxIYHZ0mGlGxHrfjrcAoSA6r2iXB9q2ijLVwqOARpcvGcBzZBil9aMAHRIXHAOV9Ihv4velrzmiLKADtD60Bfj2zzntGYZA3EGucitMMkkP7vfAa769i5QWK1Lniq3+VUuGNVjRzl4GuQPpc0wMWeJvQGc5Uf9Kk/sOCkPp0SPWcZ6nNAUebRy3V5OoADA9IntyXxfTlZdOHSbJTsG+eOGve0uLGRAOS+oeCstO7Gk4e/Ylozju+ixkINEY7HHDGt6AyHGtjPdy08Y0XrIqs0JMxsHKrtTVNxDjIFKbMees+vtxU3DEr/tNo1sTo34ieGKZP2Cp5mG/IrcjD1saebUaCngQO3QfeuKcU8pBTR7l7PtFNHm3HrmdkY=
-
- - name: 'Sauce Labs, Edge latest, W3C protocol'
- php: '7.3'
- env: SAUCELABS=1 BROWSER_NAME="MicrosoftEdge" VERSION="latest" PLATFORM="Windows 10"
- before_script:
- - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log &
- - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started"
- addons:
- sauce_connect: true
- jwt:
- secure: 5s8iQfH1dHgEm0DeP9VZ/MCzCeiE/HnMWqPFzRmg6VD2qJ53oYdseo8j+QCbE25MIwoSnIbKzlnbCN6fVzZc/0S7Mo45xJiq8xVLPSdMjDoOeqYE4of+t5Srq4iSzGLPCLiMTtB4xDEl6blUVGhYxN5rA/tVN+cVtLNQvo3ovRon3Mw3MqR4pgCE6PofcLXtyJc3KuOBlUJLWdPGRdlZrpKWE5ogyj4a1h4bVwidckZqkOF+gm58Gf0zVfFazDQFIw2Xuq7SZmiNgdOD5yUEePkrMhy2tbOlPNAIgHCpzHldv5Y+GYyxIYHZ0mGlGxHrfjrcAoSA6r2iXB9q2ijLVwqOARpcvGcBzZBil9aMAHRIXHAOV9Ihv4velrzmiLKADtD60Bfj2zzntGYZA3EGucitMMkkP7vfAa769i5QWK1Lniq3+VUuGNVjRzl4GuQPpc0wMWeJvQGc5Uf9Kk/sOCkPp0SPWcZ6nNAUebRy3V5OoADA9IntyXxfTlZdOHSbJTsG+eOGve0uLGRAOS+oeCstO7Gk4e/Ylozju+ixkINEY7HHDGt6AyHGtjPdy08Y0XrIqs0JMxsHKrtTVNxDjIFKbMees+vtxU3DEr/tNo1sTo34ieGKZP2Cp5mG/IrcjD1saebUaCngQO3QfeuKcU8pBTR7l7PtFNHm3HrmdkY=
-
-cache:
- directories:
- - $HOME/.composer/cache
-
-install:
- - travis_retry composer self-update
- - travis_retry composer update --no-interaction $DEPENDENCIES
-
-before_script:
- - if [ "$BROWSER_NAME" = "chrome" ]; then
- mkdir chromedriver;
- CHROME_VERSION=$(google-chrome --product-version);
- CHROME_VERSION=${CHROME_VERSION%.*};
- wget -q -t 3 https://chromedriver.storage.googleapis.com/$(curl -L https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_VERSION})/chromedriver_linux64.zip;
- unzip chromedriver_linux64.zip -d chromedriver;
- fi
- - if [ "$BROWSER_NAME" = "chrome" ]; then export CHROMEDRIVER_PATH=$PWD/chromedriver/chromedriver; fi
- - if [ "$GECKODRIVER" = "1" ]; then mkdir -p geckodriver; wget -q -t 3 https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz; tar xzf geckodriver-v0.26.0-linux64.tar.gz -C geckodriver; fi
- - if [ ! -f jar/selenium-server-standalone.jar ] && [ -n "$SELENIUM_SERVER" ]; then
- mkdir -p jar;
- if [ "$SELENIUM_SERVER" = "legacy" ]; then
- wget -q -t 3 -O jar/selenium-server-standalone.jar https://selenium-release.storage.googleapis.com/3.8/selenium-server-standalone-3.8.1.jar;
- else
- wget -q -t 3 -O jar/selenium-server-standalone.jar $SELENIUM_SERVER;
- fi
- fi
- - if [ "$GECKODRIVER" = "1" ]; then
- geckodriver/geckodriver &> ./logs/geckodriver.log &
- elif [ "$CHROMEDRIVER" = "1" ]; then
- chromedriver/chromedriver --port=4444 --url-base=/wd/hub &> ./logs/chromedriver.log &
- elif [ "$SELENIUM_SERVER" = "legacy" ]; then
- java -Dwebdriver.firefox.marionette=false -Dwebdriver.chrome.driver="$PWD/chromedriver/chromedriver" -jar jar/selenium-server-standalone.jar -enablePassThrough false -log ./logs/selenium.log &
- else
- java -Dwebdriver.chrome.driver="$PWD/chromedriver/chromedriver" -Dwebdriver.gecko.driver="$PWD/geckodriver/geckodriver" -jar jar/selenium-server-standalone.jar -log ./logs/selenium.log &
- fi
- - until $(echo | nc localhost 4444); do sleep 1; echo Waiting for Selenium server on port 4444...; done; echo "Selenium server started"
- - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log &
- - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started"
-
-script:
- - if [ -n "$SAUCELABS" ]; then EXCLUDE_GROUP+="exclude-saucelabs,"; fi
- - if [ "$BROWSER_NAME" = "MicrosoftEdge" ]; then EXCLUDE_GROUP+="exclude-edge,"; fi
- - if [ "$BROWSER_NAME" = "firefox" ]; then EXCLUDE_GROUP+="exclude-firefox,"; fi
- - if [ "$BROWSER_NAME" = "chrome" ]; then EXCLUDE_GROUP+="exclude-chrome,"; fi
- - if [ -n "$EXCLUDE_GROUP" ]; then EXTRA_PARAMS+=" --exclude-group $EXCLUDE_GROUP"; fi
- - ./vendor/bin/phpunit --coverage-clover ./logs/coverage-clover.xml $EXTRA_PARAMS
-
-after_script:
- - if [ -f ./logs/selenium.log ]; then cat ./logs/selenium.log; fi
- - if [ -f ./logs/php-server.log ]; then cat ./logs/php-server.log; fi
- - if [ -f ./logs/geckodriver.log ]; then cat ./logs/geckodriver.log; fi
- - if [ -f ./logs/chromedriver.log ]; then cat ./logs/chromedriver.log; fi
-
-after_success:
- - travis_retry php vendor/bin/php-coveralls -v
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f9317baab..a468c26f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,10 +3,153 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
+## 1.15.2 - 2024-11-21
+### Fixed
+- PHP 8.4 deprecation notices, especially in nullable type-hints.
+- Docs: Fix static return types in RemoteWebElement phpDoc.
+- Tests: Disable chrome 127+ search engine pop-up in tests
+- Tests: Enable Shadow DOM tests in Geckodriver
+
+### Added
+- Tests: Allow running tests in headfull (not headless) mode using `DISABLE_HEADLESS` environment variable.
+
+### Changed
+- Docs: Update selenium server host URL in example.
+
+## 1.15.1 - 2023-10-20
+- Update `symfony/process` dependency to support upcoming Symfony 7.
+
+## 1.15.0 - 2023-08-29
+### Changed
+- Capability key `ChromeOptions::CAPABILITY_W3C` used to set ChromeOptions is now deprecated in favor of `ChromeOptions::CAPABILITY`, which now also contains the W3C compatible value (`goog:chromeOptions`).
+- ChromeOptions are now passed to the driver always as a W3C compatible key `goog:chromeOptions`, even in the deprecated OSS JsonWire payload (as ChromeDriver [supports](https://bugs.chromium.org/p/chromedriver/issues/detail?id=1786) this since 2017).
+- Improve Safari compatibility for `` element.
+- Remove no longer needed compatibility layer with old Symfony.
+- Docs: Document exception throwing in findElement.
+
+### Fixed
+- Handle unexpected response when getting element(s) by throwing an exception, not triggering fatal error.
+
+## 1.14.0 - 2023-02-09
+### Added
+- `PhpWebDriverExceptionInterface` as a common interface to identify all exceptions thrown in php-webdriver.
+
+### Changed
+- Require PHP ^7.3.
+- Capabilities must be either explicitly provided or retrievable from Selenium Grid when resuing session with `createBySessionID()`.
+- Throw `UnexpectedResponseException` instead of `UnknownErrorException` in `findElement()` and `findElements()` methods.
+- Throw custom php-webdriver exceptions instead of native PHP SPL exceptions.
+- Do not mix internal non-W3C WebDriver exceptions, separate them into own namespaces.
+
+## 1.13.1 - 2022-10-11
+### Fixed
+- Do not fail when using `isDisplayed()` and capabilities are missing in WebDriver instance. (Happens when driver instance was created using `RemoteWebDriver::createBySessionID()`.)
+
+## 1.13.0 - 2022-10-03
+### Added
+- Support for current Firefox XPI extension format. Extensions could now be loaded into `FirefoxProfile` using `addExtension()` method.
+- `setProfile()` method to `FirefoxOptions`, which is now a preferred way to set Firefox Profile.
+- Element `isDisplayed()` can now be used even for browsers not supporting native API endpoint (like Safari), thanks to javascript atom workaround.
+
+### Changed
+- Handle errors when taking screenshots. `WebDriverException` is thrown if WebDriver returns empty or invalid screenshot data.
+- Deprecate `FirefoxDriver::PROFILE` constant. Instead, use `setProfile()` method of `FirefoxOptions` to set Firefox Profile.
+- Deprecate `getAllSessions()` method of `RemoteWebDriver` (which is not part of W3C WebDriver).
+- Increase default request timeout to 3 minutes (instead of 30 seconds).
+
+### Fixed
+- Throw `UnknownErrorException` instead of fatal error if remote end returns invalid response for `findElement()`/`findElements()` commands.
+
+## 1.12.1 - 2022-05-03
+### Fixed
+- Improper PHP documentation for `getAttribute()` and `getDomProperty()`.
+- Unsafe use of `static::` when accessing private property in `DesiredCapabilities`.
+- PHP 8.1 deprecations in the `Cookie` class.
+
+### Changed
+- Docs: Extend `findElement()`/`findElements()` method documentation to better explain XPath behavior.
+- Add `@return` and `@param` type annotations to Cookie class to avoid deprecations in PHP 8.1.
+
+## 1.12.0 - 2021-10-14
+### Added
+- `RemoteWebElement::getDomProperty()` method to read JavaScript properties of an element (like the value of `innerHTML` etc.) in W3C mode.
+- `WebDriverCommand::newSession()` constructor to create new session command without violating typehints.
+
+### Changed
+- Allow installation of Symfony 6 components.
+
+### Fixed
+- PHP 8.1 compatibility.
+
+## 1.11.1 - 2021-05-21
+### Fixed
+- `RemoteWebElement::getLocationOnScreenOnceScrolledIntoView()` was missing polyfill implementation for W3C mode and not working in eg. Safari.
+
+## 1.11.0 - 2021-05-03
+### Added
+- `FirefoxOptions` class to simplify passing Firefox capabilities. Usage is covered [in our wiki](https://github.com/php-webdriver/php-webdriver/wiki/Firefox#firefoxoptions).
+- `FirefoxDriver` to easy local start of Firefox instance without a need to start the `geckodriver` process manually. [See wiki](https://github.com/php-webdriver/php-webdriver/wiki/Firefox#start-directly-using-firefoxdriver-class) for usage examples.
+- Method `ChromeDriver::startUsingDriverService()` to be used for creating ChromeDriver instance with custom service.
+
+### Fixed
+- Driver capabilities received from the browser when creating now session were not set to the instance of ChromeDriver (when ChromeDriver::start() was used).
+
+### Changed
+- Deprecate `ChromeDriver::startSession`. However, the method was supposed to be used only internally.
+- KeyDown and KeyUp actions will throw an exception when not used with modifier keys.
+
+## 1.10.0 - 2021-02-25
+### Added
+- Support for sending Chrome DevTools Protocol commands (see details in [wiki](https://github.com/php-webdriver/php-webdriver/wiki/Chrome#chrome-devtools-protocol-cdp)).
+- Option to specify type of new window (window or tab) when using `$driver->switchTo()->newWindow()`.
+
+### Fixed
+- Actually start ChromeDriver in W3C mode if it is supported by the browser driver. Until now, when it was initialized using `ChromeDriver::start()`, it has always been unintentionally started in OSS mode.
+- ChromeOptions were ignored when passed to DesiredCapabilities as `ChromeOptions::CAPABILITY_W3C`.
+- Clicking a block element inside `` element in Firefox (workaround for GeckoDriver bug [1374283](https://bugzilla.mozilla.org/show_bug.cgi?id=1374283)).
+
+### Changed
+- Throw `DriverServerDiedException` on local driver process terminating unexpectedly and provide full details of original exception to improve debugging.
+- Do not require `WEBDRIVER_CHROME_DRIVER` environment variable to be set if `chromedriver` binary is already available via system PATH.
+- Mark PhantomJS deprecated, as it is no longer developed and maintained.
+- Deprecate `RemoteWebDriver::newWindow()` in favor of `$driver->switchTo()->newWindow()`.
+- Don't escape slashes in CURL exception message to improve readability.
+
+## 1.9.0 - 2020-11-19
+### Added
+- Support of SameSite cookie property.
+- Command `RemoteWebDriver::newWindow()` for W3C mode to open new top-level browsing context (aka window).
+- PHP 8.0 support.
+
+## 1.8.3 - 2020-10-06
+### Fixed
+- Make `alertIsPresent()` condition working in W3C mode.
+- `RemoteWebDriver::create()` cannot be used without providing the second parameter (which is in fact optional).
+- `ChromeDriver::start()` starts in inconsistent state mixing W3C/OSS mode.
+- Modifier keys are not released when sending NULL key in GeckoDriver (workaround for GeckoDriver bug [1494661](https://bugzilla.mozilla.org/show_bug.cgi?id=1494661)).
+- Do not set unnecessary `binary` value of `goog:chromeOptions` while keep the object in proper data type required by ChromeDriver.
+
+## 1.8.2 - 2020-03-04
+### Changed
+- Reimplement element `equals()` method to be working in W3C mode.
+- New instance of `RemoteWebDriver` created via `createBySessionID()` by default expects W3C mode. This could be disabled using fifth parameter of `createBySessionID()`.
+- Disable JSON viewer in Firefox to let JSON response be rendered as-is.
+
+### Fixed
+- Properly read fifth parameter whether W3C compliant instance should be created when using `createBySessionID()`.
+- Creating of Firefox profile with libzip 1.6+ (eg. on Mac OS Catalina).
+
+## 1.8.1 - 2020-02-17
+### Fixed
+- Accept array as possible input to `sendKeys()` method. (Unintentional BC break in 1.8.0.)
+- Use relative offset when moving mouse pointer in W3C WebDriver mode.
+
+## 1.8.0 - 2020-02-10
### Added
- Experimental W3C WebDriver protocol support. The protocol will be used automatically when remote end (like Geckodriver, newer Chromedriver etc.) supports it.
- `getStatus()` method of `RemoteWebDriver` to get information about remote-end readiness to create new sessions.
- `takeElementScreenshot()` method of `RemoteWebElement` to do the obvious - take screenshot of the particular element.
+- Support for sending custom commands via `executeCustomCommand()`. See [wiki](https://github.com/php-webdriver/php-webdriver/wiki/Custom-commands) for more information.
### Changed
- The repository was migrated to [`php-webdriver/php-webdriver`](https://github.com/php-webdriver/php-webdriver/).
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index 0a45f9bd5..000000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Code of Conduct
-
-Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated.
diff --git a/README.md b/README.md
index e05ff8526..f3e8d3801 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,20 @@
# php-webdriver – Selenium WebDriver bindings for PHP
-[](https://packagist.org/packages/php-webdriver/webdriver)
-[](https://travis-ci.com/php-webdriver/php-webdriver)
-[](https://saucelabs.com/u/php-webdriver)
-[](https://packagist.org/packages/php-webdriver/webdriver)
+[](https://packagist.org/packages/php-webdriver/webdriver)
+[](https://github.com/php-webdriver/php-webdriver/actions)
+[](https://saucelabs.com/u/php-webdriver)
+[](https://packagist.org/packages/php-webdriver/webdriver)
## Description
Php-webdriver library is PHP language binding for Selenium WebDriver, which allows you to control web browsers from PHP.
This library is compatible with Selenium server version 2.x, 3.x and 4.x.
-The library supports [JsonWireProtocol](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol) and also
-implements **experimental support** of [W3C WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html).
-The W3C WebDriver support is not yet full-featured, however it should allow to control Firefox via Geckodriver and new
-versions of Chrome and Chromedriver with just a slight limitations.
+The library supports modern [W3C WebDriver](https://w3c.github.io/webdriver/) protocol, as well
+as legacy [JsonWireProtocol](https://www.selenium.dev/documentation/legacy/json_wire_protocol/).
-The concepts of this library are very similar to the "official" Java, .NET, Python and Ruby bindings from the
-[Selenium project](https://github.com/SeleniumHQ/selenium/).
-
-Looking for API documentation of php-webdriver? See [https://php-webdriver.github.io/php-webdriver/](https://php-webdriver.github.io/php-webdriver/latest/)
-
-Any complaints, questions, or ideas? Post them in the user group https://www.facebook.com/groups/phpwebdriver/.
+The concepts of this library are very similar to the "official" Java, JavaScript, .NET, Python and Ruby libraries
+which are developed as part of the [Selenium project](https://github.com/SeleniumHQ/selenium/).
## Installation
@@ -34,86 +28,201 @@ Then install the library:
php composer.phar require php-webdriver/webdriver
+## Upgrade from version <1.8.0
+
+Starting from version 1.8.0, the project has been renamed from `facebook/php-webdriver` to `php-webdriver/webdriver`.
+
+In order to receive the new version and future updates, **you need to rename it in your composer.json**:
+
+```diff
+"require": {
+- "facebook/webdriver": "(version you use)",
++ "php-webdriver/webdriver": "(version you use)",
+}
+```
+
+and run `composer update`.
+
## Getting started
-### Start Server
+### 1. Start server (aka. remote end)
-The required server is the `selenium-server-standalone-#.jar` file provided here: http://selenium-release.storage.googleapis.com/index.html
+To control a browser, you need to start a *remote end* (server), which will listen to the commands sent
+from this library and will execute them in the respective browser.
-Download and run the server by **replacing #** with the current server version. Keep in mind **you must have Java 8+ installed to run this command**.
+This could be Selenium standalone server, but for local development, you can send them directly to so-called "browser driver" like Chromedriver or Geckodriver.
- java -jar selenium-server-standalone-#.jar
+#### a) Chromedriver
-### Create a Browser Session
+📙 Below you will find a simple example. Make sure to read our wiki for [more information on Chrome/Chromedriver](https://github.com/php-webdriver/php-webdriver/wiki/Chrome).
-When creating a browser session, be sure to pass the url of your running server.
+Install the latest Chrome and [Chromedriver](https://sites.google.com/chromium.org/driver/downloads).
+Make sure to have a compatible version of Chromedriver and Chrome!
-```php
-// This would be the url of the host running the server-standalone.jar
-$host = 'http://localhost:4444/wd/hub'; // this is the default url and port where Selenium server starts
+Run `chromedriver` binary, you can pass `port` argument, so that it listens on port 4444:
+
+```sh
+chromedriver --port=4444
```
-##### Launch Chrome
+#### b) Geckodriver
-Install latest Chrome and [Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads).
+📙 Below you will find a simple example. Make sure to read our wiki for [more information on Firefox/Geckodriver](https://github.com/php-webdriver/php-webdriver/wiki/Firefox).
-The `chromedriver` binary must be placed in system `PATH` directory, otherwise you must provide the path when starting Selenium server
-(eg. `java -Dwebdriver.chrome.driver="/path/to/chromedriver" -jar selenium-server-standalone-#.jar`).
+Install the latest Firefox and [Geckodriver](https://github.com/mozilla/geckodriver/releases).
+Make sure to have a compatible version of Geckodriver and Firefox!
-```php
-$driver = RemoteWebDriver::create($host, DesiredCapabilities::chrome());
+Run `geckodriver` binary (it start to listen on port 4444 by default):
+
+```sh
+geckodriver
```
-##### Launch Firefox
+#### c) Selenium standalone server
+
+Selenium server can be useful when you need to execute multiple tests at once,
+when you run tests in several different browsers (like on your CI server), or when you need to distribute tests amongst
+several machines in grid mode (where one Selenium server acts as a hub, and others connect to it as nodes).
+
+Selenium server then act like a proxy and takes care of distributing commands to the respective nodes.
+
+The latest version can be found on the [Selenium download page](https://www.selenium.dev/downloads/).
+
+📙 You can find [further Selenium server information](https://github.com/php-webdriver/php-webdriver/wiki/Selenium-server)
+in our wiki.
+
+#### d) Docker
+
+Selenium server could also be started inside Docker container - see [docker-selenium project](https://github.com/SeleniumHQ/docker-selenium).
+
+### 2. Create a Browser Session
+
+When creating a browser session, be sure to pass the url of your running server.
-Install latest Firefox and [Geckodriver](https://github.com/mozilla/geckodriver/releases).
+For example:
-The `geckodriver` binary must be placed in system `PATH` directory, otherwise you must provide the path when starting Selenium server
-(eg. `java -Dwebdriver.gecko.driver="/path/to/geckodriver" -jar selenium-server-standalone-#.jar`).
+```php
+// Chromedriver (if started using --port=4444 as above)
+$serverUrl = 'http://localhost:4444/';
+// Geckodriver
+$serverUrl = 'http://localhost:4444/';
+// selenium-server-standalone-#.jar (version 2.x or 3.x)
+$serverUrl = 'http://localhost:4444/wd/hub';
+// selenium-server-standalone-#.jar (version 4.x)
+$serverUrl = 'http://localhost:4444/';
+```
+Now you can start browser of your choice:
```php
-$driver = RemoteWebDriver::create($host, DesiredCapabilities::firefox());
+use Facebook\WebDriver\Remote\RemoteWebDriver;
+
+// Chrome
+$driver = RemoteWebDriver::create($serverUrl, DesiredCapabilities::chrome());
+// Firefox
+$driver = RemoteWebDriver::create($serverUrl, DesiredCapabilities::firefox());
+// Microsoft Edge
+$driver = RemoteWebDriver::create($serverUrl, DesiredCapabilities::microsoftEdge());
```
-### Customize Desired Capabilities
+### 3. Customize Desired Capabilities
+
+Desired capabilities define properties of the browser you are about to start.
+
+They can be customized:
```php
+use Facebook\WebDriver\Firefox\FirefoxOptions;
+use Facebook\WebDriver\Remote\DesiredCapabilities;
+
$desiredCapabilities = DesiredCapabilities::firefox();
+
+// Disable accepting SSL certificates
$desiredCapabilities->setCapability('acceptSslCerts', false);
-$driver = RemoteWebDriver::create($host, $desiredCapabilities);
+
+// Add arguments via FirefoxOptions to start headless firefox
+$firefoxOptions = new FirefoxOptions();
+$firefoxOptions->addArguments(['-headless']);
+$desiredCapabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions);
+
+$driver = RemoteWebDriver::create($serverUrl, $desiredCapabilities);
```
-* See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities for more details.
+Capabilities can also be used to [📙 configure a proxy server](https://github.com/php-webdriver/php-webdriver/wiki/HowTo-Work-with-proxy) which the browser should use.
+
+To configure browser-specific capabilities, you may use [📙 ChromeOptions](https://github.com/php-webdriver/php-webdriver/wiki/Chrome#chromeoptions)
+or [📙 FirefoxOptions](https://github.com/php-webdriver/php-webdriver/wiki/Firefox#firefoxoptions).
+
+* See [legacy JsonWire protocol](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities) documentation or [W3C WebDriver specification](https://w3c.github.io/webdriver/#capabilities) for more details.
-**NOTE:** Above snippets are not intended to be a working example by simply copy-pasting. See [example.php](example.php) for working example.
+### 4. Control your browser
+
+```php
+// Go to URL
+$driver->get('https://en.wikipedia.org/wiki/Selenium_(software)');
+
+// Find search element by its id, write 'PHP' inside and submit
+$driver->findElement(WebDriverBy::id('searchInput')) // find search input element
+ ->sendKeys('PHP') // fill the search box
+ ->submit(); // submit the whole form
+
+// Find element of 'History' item in menu by its css selector
+$historyButton = $driver->findElement(
+ WebDriverBy::cssSelector('#ca-history a')
+);
+// Read text of the element and print it to output
+echo 'About to click to a button with text: ' . $historyButton->getText();
+
+// Click the element to navigate to revision history page
+$historyButton->click();
+
+// Make sure to always call quit() at the end to terminate the browser session
+$driver->quit();
+```
+
+See [example.php](example.php) for full example scenario.
+Visit our GitHub wiki for [📙 php-webdriver command reference](https://github.com/php-webdriver/php-webdriver/wiki/Example-command-reference) and further examples.
+
+**NOTE:** Above snippets are not intended to be a working example by simply copy-pasting. See [example.php](example.php) for a working example.
## Changelog
For latest changes see [CHANGELOG.md](CHANGELOG.md) file.
## More information
-Some how-tos are provided right here in [our GitHub wiki](https://github.com/php-webdriver/php-webdriver/wiki).
+Some basic usage example is provided in [example.php](example.php) file.
-You may also want to check out the Selenium [docs](http://docs.seleniumhq.org/docs/) and [wiki](https://github.com/SeleniumHQ/selenium/wiki).
+How-tos are provided right here in [📙 our GitHub wiki](https://github.com/php-webdriver/php-webdriver/wiki).
+
+If you don't use IDE, you may use [API documentation of php-webdriver](https://php-webdriver.github.io/php-webdriver/latest/).
+
+You may also want to check out the Selenium project [docs](https://selenium.dev/documentation/en/) and [wiki](https://github.com/SeleniumHQ/selenium/wiki).
## Testing framework integration
To take advantage of automatized testing you may want to integrate php-webdriver to your testing framework.
There are some projects already providing this:
+- [Symfony Panther](https://github.com/symfony/panther) uses php-webdriver and integrates with PHPUnit using `PantherTestCase`
+- [Laravel Dusk](https://laravel.com/docs/dusk) is another project using php-webdriver, could be used for testing via `DuskTestCase`
- [Steward](https://github.com/lmc-eu/steward) integrates php-webdriver directly to [PHPUnit](https://phpunit.de/), and provides parallelization
-- [Codeception](http://codeception.com) testing framework provides BDD-layer on top of php-webdriver in its [WebDriver module](http://codeception.com/docs/modules/WebDriver)
-- You can also check out this [blogpost](http://codeception.com/11-12-2013/working-with-phpunit-and-selenium-webdriver.html) + [demo project](https://github.com/DavertMik/php-webdriver-demo), describing simple [PHPUnit](https://phpunit.de/) integration
+- [Codeception](https://codeception.com/) testing framework provides BDD-layer on top of php-webdriver in its [WebDriver module](https://codeception.com/docs/modules/WebDriver)
+- You can also check out this [blogpost](https://codeception.com/11-12-2013/working-with-phpunit-and-selenium-webdriver.html) + [demo project](https://github.com/DavertMik/php-webdriver-demo), describing simple [PHPUnit](https://phpunit.de/) integration
## Support
We have a great community willing to help you!
-- **Via our Facebook Group** - If you have questions or are an active contributor consider joining our [facebook group](https://www.facebook.com/groups/phpwebdriver/) and contribute to communal discussion and support
-- **Via StackOverflow** - You can also [ask a question](https://stackoverflow.com/questions/ask?tags=php+selenium-webdriver) or find many already answered question on StackOverflow
-- **Via GitHub** - Another option if you have a question (or bug report) is to [submit it here](https://github.com/php-webdriver/php-webdriver/issues/new) as a new issue
+❓ Do you have a **question, idea or some general feedback**? Visit our [Discussions](https://github.com/php-webdriver/php-webdriver/discussions) page.
+(Alternatively, you can [look for many answered questions also on StackOverflow](https://stackoverflow.com/questions/tagged/php+selenium-webdriver)).
+
+🐛 Something isn't working, and you want to **report a bug**? [Submit it here](https://github.com/php-webdriver/php-webdriver/issues/new) as a new issue.
+
+📙 Looking for a **how-to** or **reference documentation**? See [our wiki](https://github.com/php-webdriver/php-webdriver/wiki).
+
+## Contributing ❤️
-## Contributing
+We love to have your help to make php-webdriver better. See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for more information about contributing and developing php-webdriver.
-We love to have your help to make php-webdriver better. See [CONTRIBUTING.md](CONTRIBUTING.md) for more information about contributing and developing php-webdriver.
+Php-webdriver is community project - if you want to join the effort with maintaining and developing this library, the best is to look on [issues marked with "help wanted"](https://github.com/php-webdriver/php-webdriver/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
+label. Let us know in the issue comments if you want to contribute and if you want any guidance, and we will be delighted to help you to prepare your pull request.
diff --git a/composer.json b/composer.json
index f370c2fb0..f44663100 100644
--- a/composer.json
+++ b/composer.json
@@ -1,67 +1,98 @@
{
"name": "php-webdriver/webdriver",
"description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.",
- "keywords": ["webdriver", "selenium", "php", "geckodriver", "chromedriver"],
- "homepage": "https://github.com/php-webdriver/php-webdriver",
- "type": "library",
"license": "MIT",
- "minimum-stability": "beta",
+ "type": "library",
+ "keywords": [
+ "webdriver",
+ "selenium",
+ "php",
+ "geckodriver",
+ "chromedriver"
+ ],
+ "homepage": "https://github.com/php-webdriver/php-webdriver",
"require": {
- "php": "^5.6 || ~7.0",
+ "php": "^7.3 || ^8.0",
"ext-curl": "*",
"ext-json": "*",
"ext-zip": "*",
"symfony/polyfill-mbstring": "^1.12",
- "symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0"
+ "symfony/process": "^5.0 || ^6.0 || ^7.0 || ^8.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^2.0",
- "jakub-onderka/php-parallel-lint": "^1.0",
- "php-coveralls/php-coveralls": "^2.0",
- "php-mock/php-mock-phpunit": "^1.1",
- "phpunit/phpunit": "^5.7",
- "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0",
- "sminnee/phpunit-mock-objects": "^3.4",
+ "ergebnis/composer-normalize": "^2.20.0",
+ "ondram/ci-detector": "^4.0",
+ "php-coveralls/php-coveralls": "^2.4",
+ "php-mock/php-mock-phpunit": "^2.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpunit/phpunit": "^9.3",
"squizlabs/php_codesniffer": "^3.5",
- "symfony/var-dumper": "^3.3 || ^4.0 || ^5.0"
+ "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0 || ^8.0"
+ },
+ "replace": {
+ "facebook/webdriver": "*"
},
"suggest": {
- "ext-SimpleXML": "For Firefox profile creation"
+ "ext-simplexml": "For Firefox profile creation"
},
+ "minimum-stability": "dev",
+ "prefer-stable": true,
"autoload": {
- "files": [
- "lib/Exception/TimeoutException.php"
- ],
"psr-4": {
"Facebook\\WebDriver\\": "lib/"
- }
+ },
+ "files": [
+ "lib/Exception/TimeoutException.php"
+ ]
},
"autoload-dev": {
"psr-4": {
- "Facebook\\WebDriver\\": ["tests/unit", "tests/functional"]
+ "Facebook\\WebDriver\\": [
+ "tests/unit",
+ "tests/functional"
+ ]
},
- "classmap": ["tests/functional/"]
+ "classmap": [
+ "tests/functional/"
+ ]
+ },
+ "config": {
+ "allow-plugins": {
+ "ergebnis/composer-normalize": true
+ },
+ "sort-packages": true
},
"scripts": {
- "codestyle:check": [
- "vendor/bin/php-cs-fixer fix --diff --diff-format=udiff --dry-run -vvv --ansi",
- "vendor/bin/phpcs --standard=PSR2 ./lib/ ./tests/"
+ "post-install-cmd": [
+ "@composer install --working-dir=tools/php-cs-fixer --no-progress --no-interaction",
+ "@composer install --working-dir=tools/phpstan --no-progress --no-interaction"
+ ],
+ "post-update-cmd": [
+ "@composer update --working-dir=tools/php-cs-fixer --no-progress --no-interaction",
+ "@composer update --working-dir=tools/phpstan --no-progress --no-interaction"
],
- "codestyle:fix": [
- "vendor/bin/php-cs-fixer fix --diff --diff-format=udiff -vvv || exit 0",
- "vendor/bin/phpcbf --standard=PSR2 ./lib/ ./tests/"
+ "all": [
+ "@lint",
+ "@analyze",
+ "@test"
],
"analyze": [
- "vendor/bin/parallel-lint -j 10 ./lib ./tests",
- "vendor/bin/phpstan.phar analyze ./lib ./tests --level 2 -c phpstan.neon --ansi"
+ "@php tools/phpstan/vendor/bin/phpstan analyze -c phpstan.neon --ansi",
+ "@php tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --diff --dry-run -vvv --ansi",
+ "@php vendor/bin/phpcs --standard=PSR2 --ignore=*.js ./lib/ ./tests/"
+ ],
+ "fix": [
+ "@composer normalize",
+ "@php tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --diff -vvv || exit 0",
+ "@php vendor/bin/phpcbf --standard=PSR2 --ignore=*.js ./lib/ ./tests/"
+ ],
+ "lint": [
+ "@php vendor/bin/parallel-lint -j 10 ./lib ./tests example.php",
+ "@composer validate",
+ "@composer normalize --dry-run"
+ ],
+ "test": [
+ "@php vendor/bin/phpunit --colors=always"
]
- },
- "config": {
- "sort-packages": true
- },
- "extra": {
- "branch-alias": {
- "dev-community": "1.8.x-dev"
- }
}
}
diff --git a/example.php b/example.php
index ab7464a3a..92cd431d6 100644
--- a/example.php
+++ b/example.php
@@ -1,7 +1,7 @@
get('https://www.seleniumhq.org/');
+$driver = RemoteWebDriver::create($host, $capabilities);
-// adding cookie
-$driver->manage()->deleteAllCookies();
+// navigate to Selenium page on Wikipedia
+$driver->get('https://en.wikipedia.org/wiki/Selenium_(software)');
-$cookie = new Cookie('cookie_name', 'cookie_value');
-$driver->manage()->addCookie($cookie);
+// write 'PHP' in the search box
+$driver->findElement(WebDriverBy::id('searchInput')) // find search input element
+ ->sendKeys('PHP') // fill the search box
+ ->submit(); // submit the whole form
-$cookies = $driver->manage()->getCookies();
-print_r($cookies);
+// wait until 'PHP' is shown in the page heading element
+$driver->wait()->until(
+ WebDriverExpectedCondition::elementTextContains(WebDriverBy::id('firstHeading'), 'PHP')
+);
+
+// print title of the current page to output
+echo "The title is '" . $driver->getTitle() . "'\n";
+
+// print URL of current page to output
+echo "The current URL is '" . $driver->getCurrentURL() . "'\n";
-// click the link 'About'
-$link = $driver->findElement(
- WebDriverBy::id('menu_about')
+// find element of 'History' item in menu
+$historyButton = $driver->findElement(
+ WebDriverBy::cssSelector('#ca-history a')
);
-$link->click();
-// wait until the page is loaded
+// read text of the element and print it to output
+echo "About to click to button with text: '" . $historyButton->getText() . "'\n";
+
+// click the element to navigate to revision history page
+$historyButton->click();
+
+// wait until the target page is loaded
$driver->wait()->until(
- WebDriverExpectedCondition::titleContains('About')
+ WebDriverExpectedCondition::titleContains('Revision history')
);
// print the title of the current page
echo "The title is '" . $driver->getTitle() . "'\n";
// print the URI of the current page
+
echo "The current URI is '" . $driver->getCurrentURL() . "'\n";
-// write 'php' in the search box
-$driver->findElement(WebDriverBy::id('q'))
- ->sendKeys('php') // fill the search box
- ->submit(); // submit the whole form
+// delete all cookies
+$driver->manage()->deleteAllCookies();
-// wait at most 10 seconds until at least one result is shown
-$driver->wait(10)->until(
- WebDriverExpectedCondition::presenceOfAllElementsLocatedBy(
- WebDriverBy::className('gsc-result')
- )
-);
+// add new cookie
+$cookie = new Cookie('cookie_set_by_selenium', 'cookie_value');
+$driver->manage()->addCookie($cookie);
+
+// dump current cookies to output
+$cookies = $driver->manage()->getCookies();
+print_r($cookies);
-// close the browser
+// terminate the session and close the browser
$driver->quit();
diff --git a/lib/AbstractWebDriverCheckboxOrRadio.php b/lib/AbstractWebDriverCheckboxOrRadio.php
index 9cd04c967..00aee242a 100644
--- a/lib/AbstractWebDriverCheckboxOrRadio.php
+++ b/lib/AbstractWebDriverCheckboxOrRadio.php
@@ -2,9 +2,9 @@
namespace Facebook\WebDriver;
+use Facebook\WebDriver\Exception\InvalidElementStateException;
use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Exception\UnexpectedTagNameException;
-use Facebook\WebDriver\Exception\WebDriverException;
use Facebook\WebDriver\Support\XPathEscaper;
/**
@@ -30,7 +30,7 @@ public function __construct(WebDriverElement $element)
$this->name = $element->getAttribute('name');
if ($this->name === null) {
- throw new WebDriverException('The input does not have a "name" attribute.');
+ throw new InvalidElementStateException('The input does not have a "name" attribute.');
}
$this->element = $element;
@@ -66,7 +66,7 @@ public function getFirstSelectedOption()
}
throw new NoSuchElementException(
- sprintf('No %s are selected', 'radio' === $this->type ? 'radio buttons' : 'checkboxes')
+ sprintf('No %s are selected', $this->type === 'radio' ? 'radio buttons' : 'checkboxes')
);
}
diff --git a/lib/Chrome/ChromeDevToolsDriver.php b/lib/Chrome/ChromeDevToolsDriver.php
new file mode 100644
index 000000000..2d95d274b
--- /dev/null
+++ b/lib/Chrome/ChromeDevToolsDriver.php
@@ -0,0 +1,46 @@
+ 'POST',
+ 'url' => '/session/:sessionId/goog/cdp/execute',
+ ];
+
+ /**
+ * @var RemoteWebDriver
+ */
+ private $driver;
+
+ public function __construct(RemoteWebDriver $driver)
+ {
+ $this->driver = $driver;
+ }
+
+ /**
+ * Executes a Chrome DevTools command
+ *
+ * @param string $command The DevTools command to execute
+ * @param array $parameters Optional parameters to the command
+ * @return array The result of the command
+ */
+ public function execute($command, array $parameters = [])
+ {
+ $params = ['cmd' => $command, 'params' => (object) $parameters];
+
+ return $this->driver->executeCustomCommand(
+ self::SEND_COMMAND['url'],
+ self::SEND_COMMAND['method'],
+ $params
+ );
+ }
+}
diff --git a/lib/Chrome/ChromeDriver.php b/lib/Chrome/ChromeDriver.php
index 1c9828c8e..e947a498e 100644
--- a/lib/Chrome/ChromeDriver.php
+++ b/lib/Chrome/ChromeDriver.php
@@ -2,87 +2,106 @@
namespace Facebook\WebDriver\Chrome;
-use Facebook\WebDriver\Exception\WebDriverException;
+use Facebook\WebDriver\Local\LocalWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
-use Facebook\WebDriver\Remote\DriverCommand;
-use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\Service\DriverCommandExecutor;
use Facebook\WebDriver\Remote\WebDriverCommand;
-class ChromeDriver extends RemoteWebDriver
+class ChromeDriver extends LocalWebDriver
{
+ /** @var ChromeDevToolsDriver */
+ private $devTools;
+
/**
+ * Creates a new ChromeDriver using default configuration.
+ * This includes starting a new chromedriver process each time this method is called. However this may be
+ * unnecessary overhead - instead, you can start the process once using ChromeDriverService and pass
+ * this instance to startUsingDriverService() method.
+ *
+ * @todo Remove $service parameter. Use `ChromeDriver::startUsingDriverService` to pass custom $service instance.
* @return static
*/
- public static function start(DesiredCapabilities $desired_capabilities = null, ChromeDriverService $service = null)
- {
- if ($desired_capabilities === null) {
- $desired_capabilities = DesiredCapabilities::chrome();
- }
- if ($service === null) {
+ public static function start(
+ ?DesiredCapabilities $desired_capabilities = null,
+ ?ChromeDriverService $service = null
+ ) {
+ if ($service === null) { // TODO: Remove the condition (always create default service)
$service = ChromeDriverService::createDefaultService();
}
+
+ return static::startUsingDriverService($service, $desired_capabilities);
+ }
+
+ /**
+ * Creates a new ChromeDriver using given ChromeDriverService.
+ * This is usable when you for example don't want to start new chromedriver process for each individual test
+ * and want to reuse the already started chromedriver, which will lower the overhead associated with spinning up
+ * a new process.
+
+ * @return static
+ */
+ public static function startUsingDriverService(
+ ChromeDriverService $service,
+ ?DesiredCapabilities $capabilities = null
+ ) {
+ if ($capabilities === null) {
+ $capabilities = DesiredCapabilities::chrome();
+ }
+
$executor = new DriverCommandExecutor($service);
- $driver = new static($executor, null, $desired_capabilities);
- $driver->startSession($desired_capabilities);
+ $newSessionCommand = WebDriverCommand::newSession(
+ [
+ 'capabilities' => [
+ 'firstMatch' => [(object) $capabilities->toW3cCompatibleArray()],
+ ],
+ 'desiredCapabilities' => (object) $capabilities->toArray(),
+ ]
+ );
+
+ $response = $executor->execute($newSessionCommand);
- return $driver;
+ /*
+ * TODO: in next major version we may not need to use this method, because without OSS compatibility the
+ * driver creation is straightforward.
+ */
+ return static::createFromResponse($response, $executor);
}
+ /**
+ * @todo Remove in next major version. The class is internally no longer used and is kept only to keep BC.
+ * @deprecated Use start or startUsingDriverService method instead.
+ * @codeCoverageIgnore
+ * @internal
+ */
public function startSession(DesiredCapabilities $desired_capabilities)
{
- $command = new WebDriverCommand(
- null,
- DriverCommand::NEW_SESSION,
+ $command = WebDriverCommand::newSession(
[
- 'desiredCapabilities' => $desired_capabilities->toArray(),
+ 'capabilities' => [
+ 'firstMatch' => [(object) $desired_capabilities->toW3cCompatibleArray()],
+ ],
+ 'desiredCapabilities' => (object) $desired_capabilities->toArray(),
]
);
$response = $this->executor->execute($command);
+ $value = $response->getValue();
+
+ if (!$this->isW3cCompliant = isset($value['capabilities'])) {
+ $this->executor->disableW3cCompliance();
+ }
+
$this->sessionID = $response->getSessionID();
}
/**
- * Always throws an exception. Use ChromeDriver::start() instead.
- *
- * @param string $selenium_server_url
- * @param DesiredCapabilities|array $desired_capabilities
- * @param int|null $connection_timeout_in_ms
- * @param int|null $request_timeout_in_ms
- * @param string|null $http_proxy
- * @param int|null $http_proxy_port
- * @param DesiredCapabilities $required_capabilities
- * @throws WebDriverException
- * @return RemoteWebDriver
+ * @return ChromeDevToolsDriver
*/
- public static function create(
- $selenium_server_url = 'http://localhost:4444/wd/hub',
- $desired_capabilities = null,
- $connection_timeout_in_ms = null,
- $request_timeout_in_ms = null,
- $http_proxy = null,
- $http_proxy_port = null,
- DesiredCapabilities $required_capabilities = null
- ) {
- throw new WebDriverException('Please use ChromeDriver::start() instead.');
- }
+ public function getDevTools()
+ {
+ if ($this->devTools === null) {
+ $this->devTools = new ChromeDevToolsDriver($this);
+ }
- /**
- * Always throws an exception. Use ChromeDriver::start() instead.
- *
- * @param string $session_id The existing session id
- * @param string $selenium_server_url The url of the remote Selenium WebDriver server
- * @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server
- * @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server
- * @throws WebDriverException
- * @return RemoteWebDriver|void
- */
- public static function createBySessionID(
- $session_id,
- $selenium_server_url = 'http://localhost:4444/wd/hub',
- $connection_timeout_in_ms = null,
- $request_timeout_in_ms = null
- ) {
- throw new WebDriverException('Please use ChromeDriver::start() instead.');
+ return $this->devTools;
}
}
diff --git a/lib/Chrome/ChromeDriverService.php b/lib/Chrome/ChromeDriverService.php
index 0a48c626a..902a48ded 100644
--- a/lib/Chrome/ChromeDriverService.php
+++ b/lib/Chrome/ChromeDriverService.php
@@ -10,19 +10,28 @@ class ChromeDriverService extends DriverService
* The environment variable storing the path to the chrome driver executable.
* @deprecated Use ChromeDriverService::CHROME_DRIVER_EXECUTABLE
*/
- const CHROME_DRIVER_EXE_PROPERTY = 'webdriver.chrome.driver';
- // The environment variable storing the path to the chrome driver executable
- const CHROME_DRIVER_EXECUTABLE = 'WEBDRIVER_CHROME_DRIVER';
+ public const CHROME_DRIVER_EXE_PROPERTY = 'webdriver.chrome.driver';
+ /** @var string The environment variable storing the path to the chrome driver executable */
+ public const CHROME_DRIVER_EXECUTABLE = 'WEBDRIVER_CHROME_DRIVER';
+ /**
+ * @var string Default executable used when no other is provided
+ * @internal
+ */
+ public const DEFAULT_EXECUTABLE = 'chromedriver';
/**
* @return static
*/
public static function createDefaultService()
{
- $exe = getenv(self::CHROME_DRIVER_EXECUTABLE) ?: getenv(self::CHROME_DRIVER_EXE_PROPERTY);
+ $pathToExecutable = getenv(self::CHROME_DRIVER_EXECUTABLE) ?: getenv(self::CHROME_DRIVER_EXE_PROPERTY);
+ if ($pathToExecutable === false || $pathToExecutable === '') {
+ $pathToExecutable = static::DEFAULT_EXECUTABLE;
+ }
+
$port = 9515; // TODO: Get another port if the default port is used.
$args = ['--port=' . $port];
- return new static($exe, $port, $args);
+ return new static($pathToExecutable, $port, $args);
}
}
diff --git a/lib/Chrome/ChromeOptions.php b/lib/Chrome/ChromeOptions.php
index af99d3cfe..daea44c67 100644
--- a/lib/Chrome/ChromeOptions.php
+++ b/lib/Chrome/ChromeOptions.php
@@ -3,23 +3,24 @@
namespace Facebook\WebDriver\Chrome;
use Facebook\WebDriver\Remote\DesiredCapabilities;
+use JsonSerializable;
+use ReturnTypeWillChange;
/**
* The class manages the capabilities in ChromeDriver.
*
- * @see https://sites.google.com/a/chromium.org/chromedriver/capabilities
+ * @see https://sites.google.com/chromium.org/driver/capabilities
*/
-class ChromeOptions
+class ChromeOptions implements JsonSerializable
{
/**
- * The key of chrome options desired capabilities (in legacy OSS JsonWire protocol)
- * @deprecated
+ * The key of chromeOptions in desired capabilities
*/
- const CAPABILITY = 'chromeOptions';
+ public const CAPABILITY = 'goog:chromeOptions';
/**
- * The key of chrome options desired capabilities (in W3C compatible protocol)
+ * @deprecated Use CAPABILITY instead
*/
- const CAPABILITY_W3C = 'goog:chromeOptions';
+ public const CAPABILITY_W3C = self::CAPABILITY;
/**
* @var array
*/
@@ -37,6 +38,17 @@ class ChromeOptions
*/
private $experimentalOptions = [];
+ /**
+ * Return a version of the class which can JSON serialized.
+ *
+ * @return array
+ */
+ #[ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ return $this->toArray();
+ }
+
/**
* Sets the path of the Chrome executable. The path should be either absolute
* or relative to the location running ChromeDriver server.
@@ -52,7 +64,6 @@ public function setBinary($path)
}
/**
- * @param array $arguments
* @return ChromeOptions
*/
public function addArguments(array $arguments)
@@ -66,7 +77,6 @@ public function addArguments(array $arguments)
* Add a Chrome extension to install on browser startup. Each path should be
* a packed Chrome extension.
*
- * @param array $paths
* @return ChromeOptions
*/
public function addExtensions(array $paths)
@@ -94,6 +104,9 @@ public function addEncodedExtensions(array $encoded_extensions)
/**
* Sets an experimental option which has not exposed officially.
*
+ * When using "prefs" to set Chrome preferences, please be aware they are so far not supported by
+ * Chrome running in headless mode, see https://bugs.chromium.org/p/chromium/issues/detail?id=775911
+ *
* @param string $name
* @param mixed $value
* @return ChromeOptions
@@ -117,17 +130,19 @@ public function toCapabilities()
}
/**
- * @return array
+ * @return \ArrayObject|array
*/
public function toArray()
{
- $options = $this->experimentalOptions;
-
// The selenium server expects a 'dictionary' instead of a 'list' when
// reading the chrome option. However, an empty array in PHP will be
- // converted to a 'list' instead of a 'dictionary'. To fix it, we always
- // set the 'binary' to avoid returning an empty array.
- $options['binary'] = $this->binary;
+ // converted to a 'list' instead of a 'dictionary'. To fix it, we work
+ // with `ArrayObject`
+ $options = new \ArrayObject($this->experimentalOptions);
+
+ if (!empty($this->binary)) {
+ $options['binary'] = $this->binary;
+ }
if (!empty($this->arguments)) {
$options['args'] = $this->arguments;
diff --git a/lib/Cookie.php b/lib/Cookie.php
index 0e1436089..2ae257bd1 100644
--- a/lib/Cookie.php
+++ b/lib/Cookie.php
@@ -2,14 +2,14 @@
namespace Facebook\WebDriver;
-use InvalidArgumentException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
/**
* Set values of an cookie.
*
* Implements ArrayAccess for backwards compatibility.
*
- * @see https://w3c.github.io/webdriver/webdriver-spec.html#cookies
+ * @see https://w3c.github.io/webdriver/#cookies
*/
class Cookie implements \ArrayAccess
{
@@ -36,10 +36,10 @@ public function __construct($name, $value)
public static function createFromArray(array $cookieArray)
{
if (!isset($cookieArray['name'])) {
- throw new InvalidArgumentException('Cookie name should be set');
+ throw LogicException::forError('Cookie name should be set');
}
if (!isset($cookieArray['value'])) {
- throw new InvalidArgumentException('Cookie value should be set');
+ throw LogicException::forError('Cookie value should be set');
}
$cookie = new self($cookieArray['name'], $cookieArray['value']);
@@ -58,6 +58,9 @@ public static function createFromArray(array $cookieArray)
if (isset($cookieArray['httpOnly'])) {
$cookie->setHttpOnly($cookieArray['httpOnly']);
}
+ if (isset($cookieArray['sameSite'])) {
+ $cookie->setSameSite($cookieArray['sameSite']);
+ }
return $cookie;
}
@@ -104,7 +107,7 @@ public function getPath()
public function setDomain($domain)
{
if (mb_strpos($domain, ':') !== false) {
- throw new InvalidArgumentException(sprintf('Cookie domain "%s" should not contain a port', $domain));
+ throw LogicException::forError(sprintf('Cookie domain "%s" should not contain a port', $domain));
}
$this->offsetSet('domain', $domain);
@@ -172,6 +175,24 @@ public function isHttpOnly()
return $this->offsetGet('httpOnly');
}
+ /**
+ * The cookie's same-site value.
+ *
+ * @param string $sameSite
+ */
+ public function setSameSite($sameSite)
+ {
+ $this->offsetSet('sameSite', $sameSite);
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getSameSite()
+ {
+ return $this->offsetGet('sameSite');
+ }
+
/**
* @return array
*/
@@ -186,16 +207,32 @@ public function toArray()
return $cookie;
}
+ /**
+ * @param mixed $offset
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return isset($this->cookie[$offset]);
}
+ /**
+ * @param mixed $offset
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->offsetExists($offset) ? $this->cookie[$offset] : null;
}
+ /**
+ * @param mixed $offset
+ * @param mixed $value
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
if ($value === null) {
@@ -205,6 +242,11 @@ public function offsetSet($offset, $value)
}
}
+ /**
+ * @param mixed $offset
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
unset($this->cookie[$offset]);
@@ -216,11 +258,11 @@ public function offsetUnset($offset)
protected function validateCookieName($name)
{
if ($name === null || $name === '') {
- throw new InvalidArgumentException('Cookie name should be non-empty');
+ throw LogicException::forError('Cookie name should be non-empty');
}
if (mb_strpos($name, ';') !== false) {
- throw new InvalidArgumentException('Cookie name should not contain a ";"');
+ throw LogicException::forError('Cookie name should not contain a ";"');
}
}
@@ -230,7 +272,7 @@ protected function validateCookieName($name)
protected function validateCookieValue($value)
{
if ($value === null) {
- throw new InvalidArgumentException('Cookie value is required when setting a cookie');
+ throw LogicException::forError('Cookie value is required when setting a cookie');
}
}
}
diff --git a/lib/Exception/DetachedShadowRootException.php b/lib/Exception/DetachedShadowRootException.php
new file mode 100644
index 000000000..b9bfb39cb
--- /dev/null
+++ b/lib/Exception/DetachedShadowRootException.php
@@ -0,0 +1,10 @@
+getCommandLine(),
+ $process->getErrorOutput()
+ )
+ );
+ }
+}
diff --git a/lib/Exception/Internal/UnexpectedResponseException.php b/lib/Exception/Internal/UnexpectedResponseException.php
new file mode 100644
index 000000000..18cdc88cc
--- /dev/null
+++ b/lib/Exception/Internal/UnexpectedResponseException.php
@@ -0,0 +1,51 @@
+getMessage()
+ )
+ );
+ }
+}
diff --git a/lib/Exception/Internal/WebDriverCurlException.php b/lib/Exception/Internal/WebDriverCurlException.php
new file mode 100644
index 000000000..ac81f97e2
--- /dev/null
+++ b/lib/Exception/Internal/WebDriverCurlException.php
@@ -0,0 +1,22 @@
+setProfile($profile->encode());
+ * $capabilities = DesiredCapabilities::firefox();
+ * $capabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions);
+ */
+ public const PROFILE = 'firefox_profile';
- private function __construct()
+ /**
+ * Creates a new FirefoxDriver using default configuration.
+ * This includes starting a new geckodriver process each time this method is called. However this may be
+ * unnecessary overhead - instead, you can start the process once using FirefoxDriverService and pass
+ * this instance to startUsingDriverService() method.
+ *
+ * @return static
+ */
+ public static function start(?DesiredCapabilities $capabilities = null)
{
+ $service = FirefoxDriverService::createDefaultService();
+
+ return static::startUsingDriverService($service, $capabilities);
+ }
+
+ /**
+ * Creates a new FirefoxDriver using given FirefoxDriverService.
+ * This is usable when you for example don't want to start new geckodriver process for each individual test
+ * and want to reuse the already started geckodriver, which will lower the overhead associated with spinning up
+ * a new process.
+ *
+ * @return static
+ */
+ public static function startUsingDriverService(
+ FirefoxDriverService $service,
+ ?DesiredCapabilities $capabilities = null
+ ) {
+ if ($capabilities === null) {
+ $capabilities = DesiredCapabilities::firefox();
+ }
+
+ $executor = new DriverCommandExecutor($service);
+ $newSessionCommand = WebDriverCommand::newSession(
+ [
+ 'capabilities' => [
+ 'firstMatch' => [(object) $capabilities->toW3cCompatibleArray()],
+ ],
+ ]
+ );
+
+ $response = $executor->execute($newSessionCommand);
+
+ $returnedCapabilities = DesiredCapabilities::createFromW3cCapabilities($response->getValue()['capabilities']);
+ $sessionId = $response->getSessionID();
+
+ return new static($executor, $sessionId, $returnedCapabilities, true);
}
}
diff --git a/lib/Firefox/FirefoxDriverService.php b/lib/Firefox/FirefoxDriverService.php
new file mode 100644
index 000000000..83c6a28fb
--- /dev/null
+++ b/lib/Firefox/FirefoxDriverService.php
@@ -0,0 +1,34 @@
+setPreference(FirefoxPreferences::READER_PARSE_ON_LOAD_ENABLED, false);
+ // disable JSON viewer and let JSON be rendered as raw data
+ $this->setPreference(FirefoxPreferences::DEVTOOLS_JSONVIEW, false);
+ }
+
+ /**
+ * Directly set firefoxOptions.
+ * Use `addArguments` to add command line arguments and `setPreference` to set Firefox about:config entry.
+ *
+ * @param string $name
+ * @param mixed $value
+ * @return self
+ */
+ public function setOption($name, $value)
+ {
+ if ($name === self::OPTION_PREFS) {
+ throw LogicException::forError('Use setPreference() method to set Firefox preferences');
+ }
+ if ($name === self::OPTION_ARGS) {
+ throw LogicException::forError('Use addArguments() method to add Firefox arguments');
+ }
+ if ($name === self::OPTION_PROFILE) {
+ throw LogicException::forError('Use setProfile() method to set Firefox profile');
+ }
+
+ $this->options[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Command line arguments to pass to the Firefox binary.
+ * These must include the leading dash (-) where required, e.g. ['-headless'].
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#args
+ * @param string[] $arguments
+ * @return self
+ */
+ public function addArguments(array $arguments)
+ {
+ $this->arguments = array_merge($this->arguments, $arguments);
+
+ return $this;
+ }
+
+ /**
+ * Set Firefox preference (about:config entry).
+ *
+ * @see http://kb.mozillazine.org/About:config_entries
+ * @see https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#prefs
+ * @param string $name
+ * @param string|bool|int $value
+ * @return self
+ */
+ public function setPreference($name, $value)
+ {
+ $this->preferences[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * @see https://github.com/php-webdriver/php-webdriver/wiki/Firefox#firefox-profile
+ * @return self
+ */
+ public function setProfile(FirefoxProfile $profile)
+ {
+ $this->profile = $profile;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray()
+ {
+ $array = $this->options;
+ if (!empty($this->arguments)) {
+ $array[self::OPTION_ARGS] = $this->arguments;
+ }
+ if (!empty($this->preferences)) {
+ $array[self::OPTION_PREFS] = $this->preferences;
+ }
+ if (!empty($this->profile)) {
+ $array[self::OPTION_PROFILE] = $this->profile->encode();
+ }
+
+ return $array;
+ }
+
+ #[ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ return new \ArrayObject($this->toArray());
+ }
+}
diff --git a/lib/Firefox/FirefoxPreferences.php b/lib/Firefox/FirefoxPreferences.php
index 96248aa98..2a33fb003 100644
--- a/lib/Firefox/FirefoxPreferences.php
+++ b/lib/Firefox/FirefoxPreferences.php
@@ -11,11 +11,13 @@
class FirefoxPreferences
{
/** @var string Port WebDriver uses to communicate with Firefox instance */
- const WEBDRIVER_FIREFOX_PORT = 'webdriver_firefox_port';
+ public const WEBDRIVER_FIREFOX_PORT = 'webdriver_firefox_port';
/** @var string Should the reader view (FF 38+) be enabled? */
- const READER_PARSE_ON_LOAD_ENABLED = 'reader.parse-on-load.enabled';
+ public const READER_PARSE_ON_LOAD_ENABLED = 'reader.parse-on-load.enabled';
/** @var string Browser homepage */
- const BROWSER_STARTUP_HOMEPAGE = 'browser.startup.homepage';
+ public const BROWSER_STARTUP_HOMEPAGE = 'browser.startup.homepage';
+ /** @var string Should the Devtools JSON view be enabled? */
+ public const DEVTOOLS_JSONVIEW = 'devtools.jsonview.enabled';
private function __construct()
{
diff --git a/lib/Firefox/FirefoxProfile.php b/lib/Firefox/FirefoxProfile.php
index 04e31dbc0..95e8004a8 100644
--- a/lib/Firefox/FirefoxProfile.php
+++ b/lib/Firefox/FirefoxProfile.php
@@ -2,7 +2,9 @@
namespace Facebook\WebDriver\Firefox;
-use Facebook\WebDriver\Exception\WebDriverException;
+use Facebook\WebDriver\Exception\Internal\IOException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
+use Facebook\WebDriver\Exception\Internal\RuntimeException;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
@@ -71,7 +73,7 @@ public function setRdfFile($rdf_file)
/**
* @param string $key
* @param string|bool|int $value
- * @throws WebDriverException
+ * @throws LogicException
* @return FirefoxProfile
*/
public function setPreference($key, $value)
@@ -85,7 +87,7 @@ public function setPreference($key, $value)
if (is_bool($value)) {
$value = $value ? 'true' : 'false';
} else {
- throw new WebDriverException(
+ throw LogicException::forError(
'The value of the preference should be either a string, int or bool.'
);
}
@@ -148,8 +150,10 @@ public function encode()
}
file_put_contents($temp_dir . '/user.js', $content);
+ // Intentionally do not use `tempnam()`, as it creates empty file which zip extension may not handle.
+ $temp_zip = sys_get_temp_dir() . '/' . uniqid('WebDriverFirefoxProfileZip', false);
+
$zip = new ZipArchive();
- $temp_zip = tempnam(sys_get_temp_dir(), 'WebDriverFirefoxProfileZip');
$zip->open($temp_zip, ZipArchive::CREATE);
$dir = new RecursiveDirectoryIterator($temp_dir);
@@ -182,52 +186,34 @@ public function encode()
/**
* @param string $extension The path to the extension.
- * @param string $profile_dir The path to the profile directory.
- * @return string The path to the directory of this extension.
+ * @param string $profileDir The path to the profile directory.
+ * @throws IOException
*/
- private function installExtension($extension, $profile_dir)
+ private function installExtension($extension, $profileDir)
{
- $temp_dir = $this->createTempDirectory('WebDriverFirefoxProfileExtension');
- $this->extractTo($extension, $temp_dir);
-
- // This is a hacky way to parse the id since there is no offical RDF parser library.
- // Find the correct namespace for the id element.
- $install_rdf_path = $temp_dir . '/install.rdf';
- $xml = simplexml_load_file($install_rdf_path);
- $ns = $xml->getDocNamespaces();
- $prefix = '';
- if (!empty($ns)) {
- foreach ($ns as $key => $value) {
- if (mb_strpos($value, '//www.mozilla.org/2004/em-rdf') > 0) {
- if ($key != '') {
- $prefix = $key . ':'; // Separate the namespace from the name.
- }
- break;
- }
- }
+ $extensionCommonName = $this->parseExtensionName($extension);
+
+ // install extension to profile directory
+ $extensionDir = $profileDir . '/extensions/';
+ if (!is_dir($extensionDir) && !mkdir($extensionDir, 0777, true) && !is_dir($extensionDir)) {
+ throw IOException::forFileError(
+ 'Cannot install Firefox extension - cannot create directory',
+ $extensionDir
+ );
}
- // Get the extension id from the install manifest.
- $matches = [];
- preg_match('#<' . $prefix . 'id>([^<]+)' . $prefix . 'id>#', $xml->asXML(), $matches);
- if (isset($matches[1])) {
- $ext_dir = $profile_dir . '/extensions/' . $matches[1];
- mkdir($ext_dir, 0777, true);
- $this->extractTo($extension, $ext_dir);
- } else {
- $this->deleteDirectory($temp_dir);
- throw new WebDriverException('Cannot get the extension id from the install manifest.');
+ if (!copy($extension, $extensionDir . $extensionCommonName . '.xpi')) {
+ throw IOException::forFileError(
+ 'Cannot install Firefox extension - cannot copy file',
+ $extension
+ );
}
-
- $this->deleteDirectory($temp_dir);
-
- return $ext_dir;
}
/**
* @param string $prefix Prefix of the temp directory.
*
- * @throws WebDriverException
+ * @throws IOException
* @return string The path to the temp directory created.
*/
private function createTempDirectory($prefix = '')
@@ -237,7 +223,10 @@ private function createTempDirectory($prefix = '')
unlink($temp_dir);
mkdir($temp_dir);
if (!is_dir($temp_dir)) {
- throw new WebDriverException('Cannot create firefox profile.');
+ throw IOException::forFileError(
+ 'Cannot install Firefox extension - cannot create directory',
+ $temp_dir
+ );
}
}
@@ -267,7 +256,7 @@ private function deleteDirectory($directory)
* @param string $xpi The path to the .xpi extension.
* @param string $target_dir The path to the unzip directory.
*
- * @throws \Exception
+ * @throws IOException
* @return FirefoxProfile
*/
private function extractTo($xpi, $target_dir)
@@ -278,12 +267,52 @@ private function extractTo($xpi, $target_dir)
$zip->extractTo($target_dir);
$zip->close();
} else {
- throw new \Exception("Failed to open the firefox extension. '$xpi'");
+ throw IOException::forFileError('Failed to open the firefox extension.', $xpi);
}
} else {
- throw new \Exception("Firefox extension doesn't exist. '$xpi'");
+ throw IOException::forFileError('Firefox extension doesn\'t exist.', $xpi);
}
return $this;
}
+
+ private function parseExtensionName($extensionPath)
+ {
+ $temp_dir = $this->createTempDirectory();
+
+ $this->extractTo($extensionPath, $temp_dir);
+
+ $mozillaRsaPath = $temp_dir . '/META-INF/mozilla.rsa';
+ $mozillaRsaBinaryData = file_get_contents($mozillaRsaPath);
+ $mozillaRsaHex = bin2hex($mozillaRsaBinaryData);
+
+ //We need to find the plugin id. This is the second occurrence of object identifier "2.5.4.3 commonName".
+
+ //That is marker "2.5.4.3 commonName" in hex:
+ $objectIdentifierHexMarker = '0603550403';
+
+ $firstMarkerPosInHex = strpos($mozillaRsaHex, $objectIdentifierHexMarker); // phpcs:ignore
+
+ $secondMarkerPosInHexString =
+ strpos($mozillaRsaHex, $objectIdentifierHexMarker, $firstMarkerPosInHex + 2); // phpcs:ignore
+
+ if ($secondMarkerPosInHexString === false) {
+ throw RuntimeException::forError('Cannot install extension. Cannot fetch extension commonName');
+ }
+
+ // phpcs:ignore
+ $commonNameStringPositionInBinary = ($secondMarkerPosInHexString + strlen($objectIdentifierHexMarker)) / 2;
+
+ $commonNameStringLength = ord($mozillaRsaBinaryData[$commonNameStringPositionInBinary + 1]);
+ // phpcs:ignore
+ $extensionCommonName = substr(
+ $mozillaRsaBinaryData,
+ $commonNameStringPositionInBinary + 2,
+ $commonNameStringLength
+ );
+
+ $this->deleteDirectory($temp_dir);
+
+ return $extensionCommonName;
+ }
}
diff --git a/lib/Interactions/Internal/WebDriverCoordinates.php b/lib/Interactions/Internal/WebDriverCoordinates.php
index 387bdbea6..3a92f0a64 100644
--- a/lib/Interactions/Internal/WebDriverCoordinates.php
+++ b/lib/Interactions/Internal/WebDriverCoordinates.php
@@ -11,7 +11,8 @@
class WebDriverCoordinates
{
/**
- * @var null
+ * @var mixed
+ * @todo remove in next major version (if it is unused)
*/
private $onScreen;
/**
@@ -28,9 +29,7 @@ class WebDriverCoordinates
private $auxiliary;
/**
- * @param null $on_screen
- * @param callable $in_view_port
- * @param callable $on_page
+ * @param mixed $on_screen
* @param string $auxiliary
*/
public function __construct($on_screen, callable $in_view_port, callable $on_page, $auxiliary)
diff --git a/lib/Interactions/Internal/WebDriverKeyDownAction.php b/lib/Interactions/Internal/WebDriverKeyDownAction.php
index 24ffe4eed..415ebe7f1 100644
--- a/lib/Interactions/Internal/WebDriverKeyDownAction.php
+++ b/lib/Interactions/Internal/WebDriverKeyDownAction.php
@@ -2,9 +2,7 @@
namespace Facebook\WebDriver\Interactions\Internal;
-use Facebook\WebDriver\WebDriverAction;
-
-class WebDriverKeyDownAction extends WebDriverSingleKeyAction implements WebDriverAction
+class WebDriverKeyDownAction extends WebDriverSingleKeyAction
{
public function perform()
{
diff --git a/lib/Interactions/Internal/WebDriverKeyUpAction.php b/lib/Interactions/Internal/WebDriverKeyUpAction.php
index 7df738c64..0cdb3a84f 100644
--- a/lib/Interactions/Internal/WebDriverKeyUpAction.php
+++ b/lib/Interactions/Internal/WebDriverKeyUpAction.php
@@ -2,9 +2,7 @@
namespace Facebook\WebDriver\Interactions\Internal;
-use Facebook\WebDriver\WebDriverAction;
-
-class WebDriverKeyUpAction extends WebDriverSingleKeyAction implements WebDriverAction
+class WebDriverKeyUpAction extends WebDriverSingleKeyAction
{
public function perform()
{
diff --git a/lib/Interactions/Internal/WebDriverKeysRelatedAction.php b/lib/Interactions/Internal/WebDriverKeysRelatedAction.php
index 69f4aa179..a5ba0875f 100644
--- a/lib/Interactions/Internal/WebDriverKeysRelatedAction.php
+++ b/lib/Interactions/Internal/WebDriverKeysRelatedAction.php
@@ -24,15 +24,10 @@ abstract class WebDriverKeysRelatedAction
*/
protected $locationProvider;
- /**
- * @param WebDriverKeyboard $keyboard
- * @param WebDriverMouse $mouse
- * @param WebDriverLocatable $location_provider
- */
public function __construct(
WebDriverKeyboard $keyboard,
WebDriverMouse $mouse,
- WebDriverLocatable $location_provider = null
+ ?WebDriverLocatable $location_provider = null
) {
$this->keyboard = $keyboard;
$this->mouse = $mouse;
diff --git a/lib/Interactions/Internal/WebDriverMouseAction.php b/lib/Interactions/Internal/WebDriverMouseAction.php
index 5cb0cfd10..ecb1127ee 100644
--- a/lib/Interactions/Internal/WebDriverMouseAction.php
+++ b/lib/Interactions/Internal/WebDriverMouseAction.php
@@ -19,11 +19,7 @@ class WebDriverMouseAction
*/
protected $locationProvider;
- /**
- * @param WebDriverMouse $mouse
- * @param WebDriverLocatable|null $location_provider
- */
- public function __construct(WebDriverMouse $mouse, WebDriverLocatable $location_provider = null)
+ public function __construct(WebDriverMouse $mouse, ?WebDriverLocatable $location_provider = null)
{
$this->mouse = $mouse;
$this->locationProvider = $location_provider;
diff --git a/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php b/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php
index 98fd824d7..c865da46a 100644
--- a/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php
+++ b/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php
@@ -18,14 +18,12 @@ class WebDriverMoveToOffsetAction extends WebDriverMouseAction implements WebDri
private $yOffset;
/**
- * @param WebDriverMouse $mouse
- * @param WebDriverLocatable|null $location_provider
* @param int|null $x_offset
* @param int|null $y_offset
*/
public function __construct(
WebDriverMouse $mouse,
- WebDriverLocatable $location_provider = null,
+ ?WebDriverLocatable $location_provider = null,
$x_offset = null,
$y_offset = null
) {
diff --git a/lib/Interactions/Internal/WebDriverSendKeysAction.php b/lib/Interactions/Internal/WebDriverSendKeysAction.php
index 4e65cc27c..2ed3cfd06 100644
--- a/lib/Interactions/Internal/WebDriverSendKeysAction.php
+++ b/lib/Interactions/Internal/WebDriverSendKeysAction.php
@@ -15,15 +15,12 @@ class WebDriverSendKeysAction extends WebDriverKeysRelatedAction implements WebD
private $keys = '';
/**
- * @param WebDriverKeyboard $keyboard
- * @param WebDriverMouse $mouse
- * @param WebDriverLocatable $location_provider
* @param string $keys
*/
public function __construct(
WebDriverKeyboard $keyboard,
WebDriverMouse $mouse,
- WebDriverLocatable $location_provider = null,
+ ?WebDriverLocatable $location_provider = null,
$keys = ''
) {
parent::__construct($keyboard, $mouse, $location_provider);
diff --git a/lib/Interactions/Internal/WebDriverSingleKeyAction.php b/lib/Interactions/Internal/WebDriverSingleKeyAction.php
index ccdf68cea..6efe9384a 100644
--- a/lib/Interactions/Internal/WebDriverSingleKeyAction.php
+++ b/lib/Interactions/Internal/WebDriverSingleKeyAction.php
@@ -2,23 +2,53 @@
namespace Facebook\WebDriver\Interactions\Internal;
+use Facebook\WebDriver\Exception\Internal\LogicException;
use Facebook\WebDriver\Internal\WebDriverLocatable;
use Facebook\WebDriver\WebDriverAction;
use Facebook\WebDriver\WebDriverKeyboard;
+use Facebook\WebDriver\WebDriverKeys;
use Facebook\WebDriver\WebDriverMouse;
abstract class WebDriverSingleKeyAction extends WebDriverKeysRelatedAction implements WebDriverAction
{
+ public const MODIFIER_KEYS = [
+ WebDriverKeys::SHIFT,
+ WebDriverKeys::LEFT_SHIFT,
+ WebDriverKeys::RIGHT_SHIFT,
+ WebDriverKeys::CONTROL,
+ WebDriverKeys::LEFT_CONTROL,
+ WebDriverKeys::RIGHT_CONTROL,
+ WebDriverKeys::ALT,
+ WebDriverKeys::LEFT_ALT,
+ WebDriverKeys::RIGHT_ALT,
+ WebDriverKeys::META,
+ WebDriverKeys::RIGHT_META,
+ WebDriverKeys::COMMAND,
+ ];
+
/** @var string */
- protected $key = '';
+ protected $key;
+ /**
+ * @param string $key
+ * @todo Remove default $key value in next major version (BC)
+ */
public function __construct(
WebDriverKeyboard $keyboard,
WebDriverMouse $mouse,
- WebDriverLocatable $location_provider = null,
+ ?WebDriverLocatable $location_provider = null,
$key = ''
) {
parent::__construct($keyboard, $mouse, $location_provider);
+
+ if (!in_array($key, self::MODIFIER_KEYS, true)) {
+ throw LogicException::forError(
+ sprintf(
+ 'keyDown / keyUp actions can only be used for modifier keys, but "%s" was given',
+ $key
+ )
+ );
+ }
$this->key = $key;
}
}
diff --git a/lib/Interactions/Touch/WebDriverDownAction.php b/lib/Interactions/Touch/WebDriverDownAction.php
index 2e0f1e5d1..225726fcf 100644
--- a/lib/Interactions/Touch/WebDriverDownAction.php
+++ b/lib/Interactions/Touch/WebDriverDownAction.php
@@ -16,7 +16,6 @@ class WebDriverDownAction extends WebDriverTouchAction implements WebDriverActio
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
* @param int $x
* @param int $y
*/
diff --git a/lib/Interactions/Touch/WebDriverFlickAction.php b/lib/Interactions/Touch/WebDriverFlickAction.php
index 5430852ac..acdb7fc3e 100644
--- a/lib/Interactions/Touch/WebDriverFlickAction.php
+++ b/lib/Interactions/Touch/WebDriverFlickAction.php
@@ -16,7 +16,6 @@ class WebDriverFlickAction extends WebDriverTouchAction implements WebDriverActi
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
* @param int $x
* @param int $y
*/
diff --git a/lib/Interactions/Touch/WebDriverFlickFromElementAction.php b/lib/Interactions/Touch/WebDriverFlickFromElementAction.php
index 799febe10..28d359718 100644
--- a/lib/Interactions/Touch/WebDriverFlickFromElementAction.php
+++ b/lib/Interactions/Touch/WebDriverFlickFromElementAction.php
@@ -21,8 +21,6 @@ class WebDriverFlickFromElementAction extends WebDriverTouchAction implements We
private $speed;
/**
- * @param WebDriverTouchScreen $touch_screen
- * @param WebDriverElement $element
* @param int $x
* @param int $y
* @param int $speed
diff --git a/lib/Interactions/Touch/WebDriverMoveAction.php b/lib/Interactions/Touch/WebDriverMoveAction.php
index 8cdf5eb99..d0a5f85f9 100644
--- a/lib/Interactions/Touch/WebDriverMoveAction.php
+++ b/lib/Interactions/Touch/WebDriverMoveAction.php
@@ -10,7 +10,6 @@ class WebDriverMoveAction extends WebDriverTouchAction implements WebDriverActio
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
* @param int $x
* @param int $y
*/
diff --git a/lib/Interactions/Touch/WebDriverScrollAction.php b/lib/Interactions/Touch/WebDriverScrollAction.php
index 0fd40c5b4..952d57e34 100644
--- a/lib/Interactions/Touch/WebDriverScrollAction.php
+++ b/lib/Interactions/Touch/WebDriverScrollAction.php
@@ -10,7 +10,6 @@ class WebDriverScrollAction extends WebDriverTouchAction implements WebDriverAct
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
* @param int $x
* @param int $y
*/
diff --git a/lib/Interactions/Touch/WebDriverScrollFromElementAction.php b/lib/Interactions/Touch/WebDriverScrollFromElementAction.php
index ba68bc62c..217564dc7 100644
--- a/lib/Interactions/Touch/WebDriverScrollFromElementAction.php
+++ b/lib/Interactions/Touch/WebDriverScrollFromElementAction.php
@@ -11,8 +11,6 @@ class WebDriverScrollFromElementAction extends WebDriverTouchAction implements W
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
- * @param WebDriverElement $element
* @param int $x
* @param int $y
*/
diff --git a/lib/Interactions/Touch/WebDriverTouchAction.php b/lib/Interactions/Touch/WebDriverTouchAction.php
index 3919170a9..10100ea21 100644
--- a/lib/Interactions/Touch/WebDriverTouchAction.php
+++ b/lib/Interactions/Touch/WebDriverTouchAction.php
@@ -19,13 +19,9 @@ abstract class WebDriverTouchAction
*/
protected $locationProvider;
- /**
- * @param WebDriverTouchScreen $touch_screen
- * @param WebDriverLocatable $location_provider
- */
public function __construct(
WebDriverTouchScreen $touch_screen,
- WebDriverLocatable $location_provider = null
+ ?WebDriverLocatable $location_provider = null
) {
$this->touchScreen = $touch_screen;
$this->locationProvider = $location_provider;
diff --git a/lib/Interactions/Touch/WebDriverTouchScreen.php b/lib/Interactions/Touch/WebDriverTouchScreen.php
index 21696fc90..ff9f9c4d1 100644
--- a/lib/Interactions/Touch/WebDriverTouchScreen.php
+++ b/lib/Interactions/Touch/WebDriverTouchScreen.php
@@ -12,7 +12,6 @@ interface WebDriverTouchScreen
/**
* Single tap on the touch enabled device.
*
- * @param WebDriverElement $element
* @return $this
*/
public function tap(WebDriverElement $element);
@@ -20,7 +19,6 @@ public function tap(WebDriverElement $element);
/**
* Double tap on the touch screen using finger motion events.
*
- * @param WebDriverElement $element
* @return $this
*/
public function doubleTap(WebDriverElement $element);
@@ -48,7 +46,6 @@ public function flick($xspeed, $yspeed);
* Flick on the touch screen using finger motion events.
* This flickcommand starts at a particular screen location.
*
- * @param WebDriverElement $element
* @param int $xoffset
* @param int $yoffset
* @param int $speed
@@ -64,7 +61,6 @@ public function flickFromElement(
/**
* Long press on the touch screen using finger motion events.
*
- * @param WebDriverElement $element
* @return $this
*/
public function longPress(WebDriverElement $element);
@@ -92,7 +88,6 @@ public function scroll($xoffset, $yoffset);
* Scroll on the touch screen using finger based motion events. Use this
* command to start scrolling at a particular screen location.
*
- * @param WebDriverElement $element
* @param int $xoffset
* @param int $yoffset
* @return $this
diff --git a/lib/Interactions/WebDriverActions.php b/lib/Interactions/WebDriverActions.php
index aefd07faf..031d91ac6 100644
--- a/lib/Interactions/WebDriverActions.php
+++ b/lib/Interactions/WebDriverActions.php
@@ -25,9 +25,6 @@ class WebDriverActions
protected $mouse;
protected $action;
- /**
- * @param WebDriverHasInputDevices $driver
- */
public function __construct(WebDriverHasInputDevices $driver)
{
$this->driver = $driver;
@@ -48,10 +45,9 @@ public function perform()
* Mouse click.
* If $element is provided, move to the middle of the element first.
*
- * @param WebDriverElement $element
* @return WebDriverActions
*/
- public function click(WebDriverElement $element = null)
+ public function click(?WebDriverElement $element = null)
{
$this->action->addAction(
new WebDriverClickAction($this->mouse, $element)
@@ -64,10 +60,9 @@ public function click(WebDriverElement $element = null)
* Mouse click and hold.
* If $element is provided, move to the middle of the element first.
*
- * @param WebDriverElement $element
* @return WebDriverActions
*/
- public function clickAndHold(WebDriverElement $element = null)
+ public function clickAndHold(?WebDriverElement $element = null)
{
$this->action->addAction(
new WebDriverClickAndHoldAction($this->mouse, $element)
@@ -80,10 +75,9 @@ public function clickAndHold(WebDriverElement $element = null)
* Context-click (right click).
* If $element is provided, move to the middle of the element first.
*
- * @param WebDriverElement $element
* @return WebDriverActions
*/
- public function contextClick(WebDriverElement $element = null)
+ public function contextClick(?WebDriverElement $element = null)
{
$this->action->addAction(
new WebDriverContextClickAction($this->mouse, $element)
@@ -96,10 +90,9 @@ public function contextClick(WebDriverElement $element = null)
* Double click.
* If $element is provided, move to the middle of the element first.
*
- * @param WebDriverElement $element
* @return WebDriverActions
*/
- public function doubleClick(WebDriverElement $element = null)
+ public function doubleClick(?WebDriverElement $element = null)
{
$this->action->addAction(
new WebDriverDoubleClickAction($this->mouse, $element)
@@ -111,8 +104,6 @@ public function doubleClick(WebDriverElement $element = null)
/**
* Drag and drop from $source to $target.
*
- * @param WebDriverElement $source
- * @param WebDriverElement $target
* @return WebDriverActions
*/
public function dragAndDrop(WebDriverElement $source, WebDriverElement $target)
@@ -133,7 +124,6 @@ public function dragAndDrop(WebDriverElement $source, WebDriverElement $target)
/**
* Drag $source and drop by offset ($x_offset, $y_offset).
*
- * @param WebDriverElement $source
* @param int $x_offset
* @param int $y_offset
* @return WebDriverActions
@@ -174,7 +164,6 @@ public function moveByOffset($x_offset, $y_offset)
* Extra shift, calculated from the top-left corner of the element, can be set by passing $x_offset and $y_offset
* parameters.
*
- * @param WebDriverElement $element
* @param int $x_offset
* @param int $y_offset
* @return WebDriverActions
@@ -195,10 +184,9 @@ public function moveToElement(WebDriverElement $element, $x_offset = null, $y_of
* Release the mouse button.
* If $element is provided, move to the middle of the element first.
*
- * @param WebDriverElement $element
* @return WebDriverActions
*/
- public function release(WebDriverElement $element = null)
+ public function release(?WebDriverElement $element = null)
{
$this->action->addAction(
new WebDriverButtonReleaseAction($this->mouse, $element)
@@ -212,11 +200,10 @@ public function release(WebDriverElement $element = null)
* If $element is provided, focus on that element first.
*
* @see WebDriverKeys for special keys like CONTROL, ALT, etc.
- * @param WebDriverElement $element
* @param string $key
* @return WebDriverActions
*/
- public function keyDown(WebDriverElement $element = null, $key = null)
+ public function keyDown(?WebDriverElement $element = null, $key = null)
{
$this->action->addAction(
new WebDriverKeyDownAction($this->keyboard, $this->mouse, $element, $key)
@@ -230,11 +217,10 @@ public function keyDown(WebDriverElement $element = null, $key = null)
* If $element is provided, focus on that element first.
*
* @see WebDriverKeys for special keys like CONTROL, ALT, etc.
- * @param WebDriverElement $element
* @param string $key
* @return WebDriverActions
*/
- public function keyUp(WebDriverElement $element = null, $key = null)
+ public function keyUp(?WebDriverElement $element = null, $key = null)
{
$this->action->addAction(
new WebDriverKeyUpAction($this->keyboard, $this->mouse, $element, $key)
@@ -245,14 +231,13 @@ public function keyUp(WebDriverElement $element = null, $key = null)
/**
* Send keys by keyboard.
- * If $element is provided, focus on that element first.
+ * If $element is provided, focus on that element first (using single mouse click).
*
* @see WebDriverKeys for special keys like CONTROL, ALT, etc.
- * @param WebDriverElement $element
* @param string $keys
* @return WebDriverActions
*/
- public function sendKeys(WebDriverElement $element = null, $keys = null)
+ public function sendKeys(?WebDriverElement $element = null, $keys = null)
{
$this->action->addAction(
new WebDriverSendKeysAction(
diff --git a/lib/Interactions/WebDriverCompositeAction.php b/lib/Interactions/WebDriverCompositeAction.php
index 168bf5ff5..955d61986 100644
--- a/lib/Interactions/WebDriverCompositeAction.php
+++ b/lib/Interactions/WebDriverCompositeAction.php
@@ -17,7 +17,6 @@ class WebDriverCompositeAction implements WebDriverAction
/**
* Add an WebDriverAction to the sequence.
*
- * @param WebDriverAction $action
* @return WebDriverCompositeAction The current instance.
*/
public function addAction(WebDriverAction $action)
diff --git a/lib/Interactions/WebDriverTouchActions.php b/lib/Interactions/WebDriverTouchActions.php
index da0e47815..fd3298410 100644
--- a/lib/Interactions/WebDriverTouchActions.php
+++ b/lib/Interactions/WebDriverTouchActions.php
@@ -33,7 +33,6 @@ public function __construct(WebDriver $driver)
}
/**
- * @param WebDriverElement $element
* @return WebDriverTouchActions
*/
public function tap(WebDriverElement $element)
@@ -102,7 +101,6 @@ public function scroll($x, $y)
}
/**
- * @param WebDriverElement $element
* @param int $x
* @param int $y
* @return WebDriverTouchActions
@@ -117,7 +115,6 @@ public function scrollFromElement(WebDriverElement $element, $x, $y)
}
/**
- * @param WebDriverElement $element
* @return WebDriverTouchActions
*/
public function doubleTap(WebDriverElement $element)
@@ -130,7 +127,6 @@ public function doubleTap(WebDriverElement $element)
}
/**
- * @param WebDriverElement $element
* @return WebDriverTouchActions
*/
public function longPress(WebDriverElement $element)
@@ -157,7 +153,6 @@ public function flick($x, $y)
}
/**
- * @param WebDriverElement $element
* @param int $x
* @param int $y
* @param int $speed
diff --git a/lib/Local/LocalWebDriver.php b/lib/Local/LocalWebDriver.php
new file mode 100644
index 000000000..a23aefe1a
--- /dev/null
+++ b/lib/Local/LocalWebDriver.php
@@ -0,0 +1,37 @@
+setCustomRequestParameters($url, $method);
+
+ parent::__construct($session_id, DriverCommand::CUSTOM_COMMAND, $parameters);
+ }
+
+ /**
+ * @throws WebDriverException
+ * @return string
+ */
+ public function getCustomUrl()
+ {
+ if ($this->customUrl === null) {
+ throw LogicException::forError('URL of custom command is not set');
+ }
+
+ return $this->customUrl;
+ }
+
+ /**
+ * @throws WebDriverException
+ * @return string
+ */
+ public function getCustomMethod()
+ {
+ if ($this->customMethod === null) {
+ throw LogicException::forError('Method of custom command is not set');
+ }
+
+ return $this->customMethod;
+ }
+
+ /**
+ * @param string $custom_url
+ * @param string $custom_method
+ * @throws WebDriverException
+ */
+ protected function setCustomRequestParameters($custom_url, $custom_method)
+ {
+ $allowedMethods = [static::METHOD_GET, static::METHOD_POST];
+ if (!in_array($custom_method, $allowedMethods, true)) {
+ throw LogicException::forError(
+ sprintf(
+ 'Invalid custom method "%s", must be one of [%s]',
+ $custom_method,
+ implode(', ', $allowedMethods)
+ )
+ );
+ }
+ $this->customMethod = $custom_method;
+
+ if (mb_strpos($custom_url, '/') !== 0) {
+ throw LogicException::forError(
+ sprintf('URL of custom command has to start with / but is "%s"', $custom_url)
+ );
+ }
+ $this->customUrl = $custom_url;
+ }
+}
diff --git a/lib/Remote/DesiredCapabilities.php b/lib/Remote/DesiredCapabilities.php
index 7eef1de29..88aa6b141 100644
--- a/lib/Remote/DesiredCapabilities.php
+++ b/lib/Remote/DesiredCapabilities.php
@@ -2,10 +2,10 @@
namespace Facebook\WebDriver\Remote;
-use Exception;
use Facebook\WebDriver\Chrome\ChromeOptions;
+use Facebook\WebDriver\Exception\UnsupportedOperationException;
use Facebook\WebDriver\Firefox\FirefoxDriver;
-use Facebook\WebDriver\Firefox\FirefoxPreferences;
+use Facebook\WebDriver\Firefox\FirefoxOptions;
use Facebook\WebDriver\Firefox\FirefoxProfile;
use Facebook\WebDriver\WebDriverCapabilities;
use Facebook\WebDriver\WebDriverPlatform;
@@ -20,7 +20,6 @@ class DesiredCapabilities implements WebDriverCapabilities
WebDriverCapabilityType::PLATFORM => 'platformName',
WebDriverCapabilityType::VERSION => 'browserVersion',
WebDriverCapabilityType::ACCEPT_SSL_CERTS => 'acceptInsecureCerts',
- ChromeOptions::CAPABILITY => ChromeOptions::CAPABILITY_W3C,
];
public function __construct(array $capabilities = [])
@@ -30,7 +29,7 @@ public function __construct(array $capabilities = [])
public static function createFromW3cCapabilities(array $capabilities = [])
{
- $w3cToOss = array_flip(static::$ossToW3c);
+ $w3cToOss = array_flip(self::$ossToW3c);
foreach ($w3cToOss as $w3cCapability => $ossCapability) {
// Copy W3C capabilities to OSS ones
@@ -96,6 +95,15 @@ public function getCapability($name)
*/
public function setCapability($name, $value)
{
+ // When setting 'moz:firefoxOptions' from an array and not from instance of FirefoxOptions, we must merge
+ // it with default FirefoxOptions to keep previous behavior (where the default preferences were added
+ // using FirefoxProfile, thus not overwritten by adding 'moz:firefoxOptions')
+ // TODO: remove in next major version, once FirefoxOptions are only accepted as object instance and not as array
+ if ($name === FirefoxOptions::CAPABILITY && is_array($value)) {
+ $defaultOptions = (new FirefoxOptions())->toArray();
+ $value = array_merge($defaultOptions, $value);
+ }
+
$this->set($name, $value);
return $this;
@@ -143,7 +151,7 @@ public function isJavascriptEnabled()
* This is a htmlUnit-only option.
*
* @param bool $enabled
- * @throws Exception
+ * @throws UnsupportedOperationException
* @return DesiredCapabilities
* @see https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities#read-write-capabilities
*/
@@ -151,7 +159,7 @@ public function setJavascriptEnabled($enabled)
{
$browser = $this->getBrowserName();
if ($browser && $browser !== WebDriverBrowserType::HTMLUNIT) {
- throw new Exception(
+ throw new UnsupportedOperationException(
'isJavascriptEnabled() is a htmlunit-only option. ' .
'See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities#read-write-capabilities.'
);
@@ -163,7 +171,7 @@ public function setJavascriptEnabled($enabled)
}
/**
- * @todo Remove side-effects - not change ie. ChromeOptions::CAPABILITY from instance of ChromeOptions to an array
+ * @todo Remove side-effects - not change eg. ChromeOptions::CAPABILITY from instance of ChromeOptions to an array
* @return array
*/
public function toArray()
@@ -175,6 +183,13 @@ public function toArray()
$this->capabilities[ChromeOptions::CAPABILITY]->toArray();
}
+ if (isset($this->capabilities[FirefoxOptions::CAPABILITY]) &&
+ $this->capabilities[FirefoxOptions::CAPABILITY] instanceof FirefoxOptions
+ ) {
+ $this->capabilities[FirefoxOptions::CAPABILITY] =
+ $this->capabilities[FirefoxOptions::CAPABILITY]->toArray();
+ }
+
if (isset($this->capabilities[FirefoxDriver::PROFILE]) &&
$this->capabilities[FirefoxDriver::PROFILE] instanceof FirefoxProfile
) {
@@ -213,16 +228,16 @@ public function toW3cCompatibleArray()
}
// Convert capabilities with changed name
- if (array_key_exists($capabilityKey, static::$ossToW3c)) {
+ if (array_key_exists($capabilityKey, self::$ossToW3c)) {
if ($capabilityKey === WebDriverCapabilityType::PLATFORM) {
- $w3cCapabilities[static::$ossToW3c[$capabilityKey]] = mb_strtolower($capabilityValue);
+ $w3cCapabilities[self::$ossToW3c[$capabilityKey]] = mb_strtolower($capabilityValue);
// Remove platformName if it is set to "any"
- if ($w3cCapabilities[static::$ossToW3c[$capabilityKey]] === 'any') {
- unset($w3cCapabilities[static::$ossToW3c[$capabilityKey]]);
+ if ($w3cCapabilities[self::$ossToW3c[$capabilityKey]] === 'any') {
+ unset($w3cCapabilities[self::$ossToW3c[$capabilityKey]]);
}
} else {
- $w3cCapabilities[static::$ossToW3c[$capabilityKey]] = $capabilityValue;
+ $w3cCapabilities[self::$ossToW3c[$capabilityKey]] = $capabilityValue;
}
}
@@ -234,22 +249,15 @@ public function toW3cCompatibleArray()
// Convert ChromeOptions
if (array_key_exists(ChromeOptions::CAPABILITY, $ossCapabilities)) {
- if (array_key_exists(ChromeOptions::CAPABILITY_W3C, $ossCapabilities)) {
- $w3cCapabilities[ChromeOptions::CAPABILITY_W3C] = array_merge_recursive(
- $ossCapabilities[ChromeOptions::CAPABILITY],
- $ossCapabilities[ChromeOptions::CAPABILITY_W3C]
- );
- } else {
- $w3cCapabilities[ChromeOptions::CAPABILITY_W3C] = $ossCapabilities[ChromeOptions::CAPABILITY];
- }
+ $w3cCapabilities[ChromeOptions::CAPABILITY] = $ossCapabilities[ChromeOptions::CAPABILITY];
}
// Convert Firefox profile
if (array_key_exists(FirefoxDriver::PROFILE, $ossCapabilities)) {
// Convert profile only if not already set in moz:firefoxOptions
- if (!array_key_exists('moz:firefoxOptions', $ossCapabilities)
- || !array_key_exists('profile', $ossCapabilities['moz:firefoxOptions'])) {
- $w3cCapabilities['moz:firefoxOptions']['profile'] = $ossCapabilities[FirefoxDriver::PROFILE];
+ if (!array_key_exists(FirefoxOptions::CAPABILITY, $ossCapabilities)
+ || !array_key_exists('profile', $ossCapabilities[FirefoxOptions::CAPABILITY])) {
+ $w3cCapabilities[FirefoxOptions::CAPABILITY]['profile'] = $ossCapabilities[FirefoxDriver::PROFILE];
}
}
@@ -288,10 +296,7 @@ public static function firefox()
WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY,
]);
- // disable the "Reader View" help tooltip, which can hide elements in the window.document
- $profile = new FirefoxProfile();
- $profile->setPreference(FirefoxPreferences::READER_PARSE_ON_LOAD_ENABLED, false);
- $caps->setCapability(FirefoxDriver::PROFILE, $profile);
+ $caps->setCapability(FirefoxOptions::CAPABILITY, new FirefoxOptions()); // to add default options
return $caps;
}
@@ -387,6 +392,8 @@ public static function safari()
}
/**
+ * @deprecated PhantomJS is no longer developed and its support will be removed in next major version.
+ * Use headless Chrome or Firefox instead.
* @return static
*/
public static function phantomjs()
@@ -416,8 +423,6 @@ private function set($key, $value)
*/
private function get($key, $default = null)
{
- return isset($this->capabilities[$key])
- ? $this->capabilities[$key]
- : $default;
+ return $this->capabilities[$key] ?? $default;
}
}
diff --git a/lib/Remote/DriverCommand.php b/lib/Remote/DriverCommand.php
index 34daf3e42..a3a230b7c 100644
--- a/lib/Remote/DriverCommand.php
+++ b/lib/Remote/DriverCommand.php
@@ -9,137 +9,143 @@
*/
class DriverCommand
{
- const GET_ALL_SESSIONS = 'getAllSessions';
- const GET_CAPABILITIES = 'getCapabilities';
- const NEW_SESSION = 'newSession';
- const STATUS = 'status';
- const CLOSE = 'close';
- const QUIT = 'quit';
- const GET = 'get';
- const GO_BACK = 'goBack';
- const GO_FORWARD = 'goForward';
- const REFRESH = 'refresh';
- const ADD_COOKIE = 'addCookie';
- const GET_ALL_COOKIES = 'getCookies';
- const DELETE_COOKIE = 'deleteCookie';
- const DELETE_ALL_COOKIES = 'deleteAllCookies';
- const FIND_ELEMENT = 'findElement';
- const FIND_ELEMENTS = 'findElements';
- const FIND_CHILD_ELEMENT = 'findChildElement';
- const FIND_CHILD_ELEMENTS = 'findChildElements';
- const CLEAR_ELEMENT = 'clearElement';
- const CLICK_ELEMENT = 'clickElement';
- const SEND_KEYS_TO_ELEMENT = 'sendKeysToElement';
- const SEND_KEYS_TO_ACTIVE_ELEMENT = 'sendKeysToActiveElement';
- const SUBMIT_ELEMENT = 'submitElement';
- const UPLOAD_FILE = 'uploadFile';
- const GET_CURRENT_WINDOW_HANDLE = 'getCurrentWindowHandle';
- const GET_WINDOW_HANDLES = 'getWindowHandles';
- const GET_CURRENT_CONTEXT_HANDLE = 'getCurrentContextHandle';
- const GET_CONTEXT_HANDLES = 'getContextHandles';
+ public const GET_ALL_SESSIONS = 'getAllSessions';
+ public const GET_CAPABILITIES = 'getCapabilities';
+ public const NEW_SESSION = 'newSession';
+ public const STATUS = 'status';
+ public const CLOSE = 'close';
+ public const QUIT = 'quit';
+ public const GET = 'get';
+ public const GO_BACK = 'goBack';
+ public const GO_FORWARD = 'goForward';
+ public const REFRESH = 'refresh';
+ public const ADD_COOKIE = 'addCookie';
+ public const GET_ALL_COOKIES = 'getCookies';
+ public const DELETE_COOKIE = 'deleteCookie';
+ public const DELETE_ALL_COOKIES = 'deleteAllCookies';
+ public const FIND_ELEMENT = 'findElement';
+ public const FIND_ELEMENTS = 'findElements';
+ public const FIND_CHILD_ELEMENT = 'findChildElement';
+ public const FIND_CHILD_ELEMENTS = 'findChildElements';
+ public const CLEAR_ELEMENT = 'clearElement';
+ public const CLICK_ELEMENT = 'clickElement';
+ public const SEND_KEYS_TO_ELEMENT = 'sendKeysToElement';
+ public const SEND_KEYS_TO_ACTIVE_ELEMENT = 'sendKeysToActiveElement';
+ public const SUBMIT_ELEMENT = 'submitElement';
+ public const UPLOAD_FILE = 'uploadFile';
+ public const GET_CURRENT_WINDOW_HANDLE = 'getCurrentWindowHandle';
+ public const GET_WINDOW_HANDLES = 'getWindowHandles';
+ public const GET_CURRENT_CONTEXT_HANDLE = 'getCurrentContextHandle';
+ public const GET_CONTEXT_HANDLES = 'getContextHandles';
// Switching between to window/frame/iframe
- const SWITCH_TO_WINDOW = 'switchToWindow';
- const SWITCH_TO_CONTEXT = 'switchToContext';
- const SWITCH_TO_FRAME = 'switchToFrame';
- const SWITCH_TO_PARENT_FRAME = 'switchToParentFrame';
- const GET_ACTIVE_ELEMENT = 'getActiveElement';
+ public const SWITCH_TO_WINDOW = 'switchToWindow';
+ public const SWITCH_TO_CONTEXT = 'switchToContext';
+ public const SWITCH_TO_FRAME = 'switchToFrame';
+ public const SWITCH_TO_PARENT_FRAME = 'switchToParentFrame';
+ public const GET_ACTIVE_ELEMENT = 'getActiveElement';
// Information of the page
- const GET_CURRENT_URL = 'getCurrentUrl';
- const GET_PAGE_SOURCE = 'getPageSource';
- const GET_TITLE = 'getTitle';
+ public const GET_CURRENT_URL = 'getCurrentUrl';
+ public const GET_PAGE_SOURCE = 'getPageSource';
+ public const GET_TITLE = 'getTitle';
// Javascript API
- const EXECUTE_SCRIPT = 'executeScript';
- const EXECUTE_ASYNC_SCRIPT = 'executeAsyncScript';
+ public const EXECUTE_SCRIPT = 'executeScript';
+ public const EXECUTE_ASYNC_SCRIPT = 'executeAsyncScript';
// API getting information from an element.
- const GET_ELEMENT_TEXT = 'getElementText';
- const GET_ELEMENT_TAG_NAME = 'getElementTagName';
- const IS_ELEMENT_SELECTED = 'isElementSelected';
- const IS_ELEMENT_ENABLED = 'isElementEnabled';
- const IS_ELEMENT_DISPLAYED = 'isElementDisplayed';
- const GET_ELEMENT_LOCATION = 'getElementLocation';
- const GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW = 'getElementLocationOnceScrolledIntoView';
- const GET_ELEMENT_SIZE = 'getElementSize';
- const GET_ELEMENT_ATTRIBUTE = 'getElementAttribute';
- const GET_ELEMENT_VALUE_OF_CSS_PROPERTY = 'getElementValueOfCssProperty';
- const ELEMENT_EQUALS = 'elementEquals';
- const SCREENSHOT = 'screenshot';
+ public const GET_ELEMENT_TEXT = 'getElementText';
+ public const GET_ELEMENT_TAG_NAME = 'getElementTagName';
+ public const IS_ELEMENT_SELECTED = 'isElementSelected';
+ public const IS_ELEMENT_ENABLED = 'isElementEnabled';
+ public const IS_ELEMENT_DISPLAYED = 'isElementDisplayed';
+ public const GET_ELEMENT_LOCATION = 'getElementLocation';
+ public const GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW = 'getElementLocationOnceScrolledIntoView';
+ public const GET_ELEMENT_SIZE = 'getElementSize';
+ public const GET_ELEMENT_ATTRIBUTE = 'getElementAttribute';
+ public const GET_ELEMENT_VALUE_OF_CSS_PROPERTY = 'getElementValueOfCssProperty';
+ public const ELEMENT_EQUALS = 'elementEquals';
+ public const SCREENSHOT = 'screenshot';
// Alert API
- const ACCEPT_ALERT = 'acceptAlert';
- const DISMISS_ALERT = 'dismissAlert';
- const GET_ALERT_TEXT = 'getAlertText';
- const SET_ALERT_VALUE = 'setAlertValue';
+ public const ACCEPT_ALERT = 'acceptAlert';
+ public const DISMISS_ALERT = 'dismissAlert';
+ public const GET_ALERT_TEXT = 'getAlertText';
+ public const SET_ALERT_VALUE = 'setAlertValue';
// Timeout API
- const SET_TIMEOUT = 'setTimeout';
- const IMPLICITLY_WAIT = 'implicitlyWait';
- const SET_SCRIPT_TIMEOUT = 'setScriptTimeout';
+ public const SET_TIMEOUT = 'setTimeout';
+ public const IMPLICITLY_WAIT = 'implicitlyWait';
+ public const SET_SCRIPT_TIMEOUT = 'setScriptTimeout';
/** @deprecated */
- const EXECUTE_SQL = 'executeSQL';
- const GET_LOCATION = 'getLocation';
- const SET_LOCATION = 'setLocation';
- const GET_APP_CACHE = 'getAppCache';
- const GET_APP_CACHE_STATUS = 'getStatus';
- const CLEAR_APP_CACHE = 'clearAppCache';
- const IS_BROWSER_ONLINE = 'isBrowserOnline';
- const SET_BROWSER_ONLINE = 'setBrowserOnline';
+ public const EXECUTE_SQL = 'executeSQL';
+ public const GET_LOCATION = 'getLocation';
+ public const SET_LOCATION = 'setLocation';
+ public const GET_APP_CACHE = 'getAppCache';
+ public const GET_APP_CACHE_STATUS = 'getStatus';
+ public const CLEAR_APP_CACHE = 'clearAppCache';
+ public const IS_BROWSER_ONLINE = 'isBrowserOnline';
+ public const SET_BROWSER_ONLINE = 'setBrowserOnline';
// Local storage
- const GET_LOCAL_STORAGE_ITEM = 'getLocalStorageItem';
- const GET_LOCAL_STORAGE_KEYS = 'getLocalStorageKeys';
- const SET_LOCAL_STORAGE_ITEM = 'setLocalStorageItem';
- const REMOVE_LOCAL_STORAGE_ITEM = 'removeLocalStorageItem';
- const CLEAR_LOCAL_STORAGE = 'clearLocalStorage';
- const GET_LOCAL_STORAGE_SIZE = 'getLocalStorageSize';
+ public const GET_LOCAL_STORAGE_ITEM = 'getLocalStorageItem';
+ public const GET_LOCAL_STORAGE_KEYS = 'getLocalStorageKeys';
+ public const SET_LOCAL_STORAGE_ITEM = 'setLocalStorageItem';
+ public const REMOVE_LOCAL_STORAGE_ITEM = 'removeLocalStorageItem';
+ public const CLEAR_LOCAL_STORAGE = 'clearLocalStorage';
+ public const GET_LOCAL_STORAGE_SIZE = 'getLocalStorageSize';
// Session storage
- const GET_SESSION_STORAGE_ITEM = 'getSessionStorageItem';
- const GET_SESSION_STORAGE_KEYS = 'getSessionStorageKey';
- const SET_SESSION_STORAGE_ITEM = 'setSessionStorageItem';
- const REMOVE_SESSION_STORAGE_ITEM = 'removeSessionStorageItem';
- const CLEAR_SESSION_STORAGE = 'clearSessionStorage';
- const GET_SESSION_STORAGE_SIZE = 'getSessionStorageSize';
+ public const GET_SESSION_STORAGE_ITEM = 'getSessionStorageItem';
+ public const GET_SESSION_STORAGE_KEYS = 'getSessionStorageKey';
+ public const SET_SESSION_STORAGE_ITEM = 'setSessionStorageItem';
+ public const REMOVE_SESSION_STORAGE_ITEM = 'removeSessionStorageItem';
+ public const CLEAR_SESSION_STORAGE = 'clearSessionStorage';
+ public const GET_SESSION_STORAGE_SIZE = 'getSessionStorageSize';
// Screen orientation
- const SET_SCREEN_ORIENTATION = 'setScreenOrientation';
- const GET_SCREEN_ORIENTATION = 'getScreenOrientation';
+ public const SET_SCREEN_ORIENTATION = 'setScreenOrientation';
+ public const GET_SCREEN_ORIENTATION = 'getScreenOrientation';
// These belong to the Advanced user interactions - an element is optional for these commands.
- const CLICK = 'mouseClick';
- const DOUBLE_CLICK = 'mouseDoubleClick';
- const MOUSE_DOWN = 'mouseButtonDown';
- const MOUSE_UP = 'mouseButtonUp';
- const MOVE_TO = 'mouseMoveTo';
+ public const CLICK = 'mouseClick';
+ public const DOUBLE_CLICK = 'mouseDoubleClick';
+ public const MOUSE_DOWN = 'mouseButtonDown';
+ public const MOUSE_UP = 'mouseButtonUp';
+ public const MOVE_TO = 'mouseMoveTo';
// Those allow interactions with the Input Methods installed on the system.
- const IME_GET_AVAILABLE_ENGINES = 'imeGetAvailableEngines';
- const IME_GET_ACTIVE_ENGINE = 'imeGetActiveEngine';
- const IME_IS_ACTIVATED = 'imeIsActivated';
- const IME_DEACTIVATE = 'imeDeactivate';
- const IME_ACTIVATE_ENGINE = 'imeActivateEngine';
+ public const IME_GET_AVAILABLE_ENGINES = 'imeGetAvailableEngines';
+ public const IME_GET_ACTIVE_ENGINE = 'imeGetActiveEngine';
+ public const IME_IS_ACTIVATED = 'imeIsActivated';
+ public const IME_DEACTIVATE = 'imeDeactivate';
+ public const IME_ACTIVATE_ENGINE = 'imeActivateEngine';
// These belong to the Advanced Touch API
- const TOUCH_SINGLE_TAP = 'touchSingleTap';
- const TOUCH_DOWN = 'touchDown';
- const TOUCH_UP = 'touchUp';
- const TOUCH_MOVE = 'touchMove';
- const TOUCH_SCROLL = 'touchScroll';
- const TOUCH_DOUBLE_TAP = 'touchDoubleTap';
- const TOUCH_LONG_PRESS = 'touchLongPress';
- const TOUCH_FLICK = 'touchFlick';
+ public const TOUCH_SINGLE_TAP = 'touchSingleTap';
+ public const TOUCH_DOWN = 'touchDown';
+ public const TOUCH_UP = 'touchUp';
+ public const TOUCH_MOVE = 'touchMove';
+ public const TOUCH_SCROLL = 'touchScroll';
+ public const TOUCH_DOUBLE_TAP = 'touchDoubleTap';
+ public const TOUCH_LONG_PRESS = 'touchLongPress';
+ public const TOUCH_FLICK = 'touchFlick';
// Window API (beta)
- const SET_WINDOW_SIZE = 'setWindowSize';
- const SET_WINDOW_POSITION = 'setWindowPosition';
- const GET_WINDOW_SIZE = 'getWindowSize';
- const GET_WINDOW_POSITION = 'getWindowPosition';
- const MAXIMIZE_WINDOW = 'maximizeWindow';
- const FULLSCREEN_WINDOW = 'fullscreenWindow';
+ public const SET_WINDOW_SIZE = 'setWindowSize';
+ public const SET_WINDOW_POSITION = 'setWindowPosition';
+ public const GET_WINDOW_SIZE = 'getWindowSize';
+ public const GET_WINDOW_POSITION = 'getWindowPosition';
+ public const MAXIMIZE_WINDOW = 'maximizeWindow';
+ public const FULLSCREEN_WINDOW = 'fullscreenWindow';
// Logging API
- const GET_AVAILABLE_LOG_TYPES = 'getAvailableLogTypes';
- const GET_LOG = 'getLog';
- const GET_SESSION_LOGS = 'getSessionLogs';
+ public const GET_AVAILABLE_LOG_TYPES = 'getAvailableLogTypes';
+ public const GET_LOG = 'getLog';
+ public const GET_SESSION_LOGS = 'getSessionLogs';
// Mobile API
- const GET_NETWORK_CONNECTION = 'getNetworkConnection';
- const SET_NETWORK_CONNECTION = 'setNetworkConnection';
+ public const GET_NETWORK_CONNECTION = 'getNetworkConnection';
+ public const SET_NETWORK_CONNECTION = 'setNetworkConnection';
+ // Custom command
+ public const CUSTOM_COMMAND = 'customCommand';
// W3C specific
- const ACTIONS = 'actions';
- const GET_ELEMENT_PROPERTY = 'getElementProperty';
- const GET_NAMED_COOKIE = 'getNamedCookie';
- const TAKE_ELEMENT_SCREENSHOT = 'takeElementScreenshot';
- const MINIMIZE_WINDOW = 'minimizeWindow';
+ public const ACTIONS = 'actions';
+ public const GET_ELEMENT_PROPERTY = 'getElementProperty';
+ public const GET_NAMED_COOKIE = 'getNamedCookie';
+ public const NEW_WINDOW = 'newWindow';
+ public const TAKE_ELEMENT_SCREENSHOT = 'takeElementScreenshot';
+ public const MINIMIZE_WINDOW = 'minimizeWindow';
+ public const GET_ELEMENT_SHADOW_ROOT = 'getElementShadowRoot';
+ public const FIND_ELEMENT_FROM_SHADOW_ROOT = 'findElementFromShadowRoot';
+ public const FIND_ELEMENTS_FROM_SHADOW_ROOT = 'findElementsFromShadowRoot';
private function __construct()
{
diff --git a/lib/Remote/ExecuteMethod.php b/lib/Remote/ExecuteMethod.php
index 3f1636e5b..ba659e6d2 100644
--- a/lib/Remote/ExecuteMethod.php
+++ b/lib/Remote/ExecuteMethod.php
@@ -6,7 +6,6 @@ interface ExecuteMethod
{
/**
* @param string $command_name
- * @param array $parameters
* @return WebDriverResponse
*/
public function execute($command_name, array $parameters = []);
diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php
index dcaa1374c..3e3ef1719 100644
--- a/lib/Remote/HttpCommandExecutor.php
+++ b/lib/Remote/HttpCommandExecutor.php
@@ -2,18 +2,18 @@
namespace Facebook\WebDriver\Remote;
-use BadMethodCallException;
-use Facebook\WebDriver\Exception\WebDriverCurlException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
+use Facebook\WebDriver\Exception\Internal\UnexpectedResponseException;
+use Facebook\WebDriver\Exception\Internal\WebDriverCurlException;
use Facebook\WebDriver\Exception\WebDriverException;
use Facebook\WebDriver\WebDriverCommandExecutor;
-use InvalidArgumentException;
/**
* Command executor talking to the standalone server via HTTP.
*/
class HttpCommandExecutor implements WebDriverCommandExecutor
{
- const DEFAULT_HTTP_HEADERS = [
+ public const DEFAULT_HTTP_HEADERS = [
'Content-Type: application/json;charset=UTF-8',
'Accept: application/json',
];
@@ -130,6 +130,7 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
DriverCommand::TOUCH_MOVE => ['method' => 'POST', 'url' => '/session/:sessionId/touch/move'],
DriverCommand::TOUCH_SCROLL => ['method' => 'POST', 'url' => '/session/:sessionId/touch/scroll'],
DriverCommand::TOUCH_UP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/up'],
+ DriverCommand::CUSTOM_COMMAND => [],
];
/**
* @var array Will be merged with $commands
@@ -140,6 +141,14 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/dismiss'],
DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/async'],
DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'],
+ DriverCommand::FIND_ELEMENT_FROM_SHADOW_ROOT => [
+ 'method' => 'POST',
+ 'url' => '/session/:sessionId/shadow/:id/element',
+ ],
+ DriverCommand::FIND_ELEMENTS_FROM_SHADOW_ROOT => [
+ 'method' => 'POST',
+ 'url' => '/session/:sessionId/shadow/:id/elements',
+ ],
DriverCommand::FULLSCREEN_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/fullscreen'],
DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'GET', 'url' => '/session/:sessionId/element/active'],
DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'],
@@ -149,6 +158,10 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
'method' => 'GET',
'url' => '/session/:sessionId/element/:id/property/:name',
],
+ DriverCommand::GET_ELEMENT_SHADOW_ROOT => [
+ 'method' => 'GET',
+ 'url' => '/session/:sessionId/element/:id/shadow',
+ ],
DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'],
DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window/handles'],
DriverCommand::GET_WINDOW_POSITION => ['method' => 'GET', 'url' => '/session/:sessionId/window/rect'],
@@ -156,11 +169,14 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
DriverCommand::MAXIMIZE_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/maximize'],
DriverCommand::MINIMIZE_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/minimize'],
+ DriverCommand::NEW_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/new'],
DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert/text'],
DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
DriverCommand::SET_WINDOW_SIZE => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'],
DriverCommand::SET_WINDOW_POSITION => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'],
+ // Selenium extension of W3C protocol
+ DriverCommand::UPLOAD_FILE => ['method' => 'POST', 'url' => '/session/:sessionId/se/file'],
];
/**
* @var string
@@ -206,8 +222,9 @@ public function __construct($url, $http_proxy = null, $http_proxy_port = null)
curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($this->curl, CURLOPT_HTTPHEADER, static::DEFAULT_HTTP_HEADERS);
- $this->setRequestTimeout(30000);
- $this->setConnectionTimeout(30000);
+
+ $this->setConnectionTimeout(30 * 1000); // 30 seconds
+ $this->setRequestTimeout(180 * 1000); // 3 minutes
}
public function disableW3cCompliance()
@@ -255,29 +272,16 @@ public function setRequestTimeout($timeout_in_ms)
}
/**
- * @param WebDriverCommand $command
- *
- * @throws WebDriverException
* @return WebDriverResponse
*/
public function execute(WebDriverCommand $command)
{
- $commandName = $command->getName();
- if (!isset(self::$commands[$commandName])) {
- if ($this->isW3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) {
- throw new InvalidArgumentException($command->getName() . ' is not a valid command.');
- }
- }
-
- if ($this->isW3cCompliant) {
- $raw = self::$w3cCompliantCommands[$command->getName()];
- } else {
- $raw = self::$commands[$command->getName()];
- }
+ $http_options = $this->getCommandHttpOptions($command);
+ $http_method = $http_options['method'];
+ $url = $http_options['url'];
- $http_method = $raw['method'];
- $url = $raw['url'];
- $url = str_replace(':sessionId', $command->getSessionID(), $url);
+ $sessionID = $command->getSessionID();
+ $url = str_replace(':sessionId', $sessionID ?? '', $url);
$params = $command->getParameters();
foreach ($params as $name => $value) {
if ($name[0] === ':') {
@@ -287,13 +291,7 @@ public function execute(WebDriverCommand $command)
}
if (is_array($params) && !empty($params) && $http_method !== 'POST') {
- throw new BadMethodCallException(sprintf(
- 'The http method called for %s is %s but it has to be POST' .
- ' if you want to pass the JSON params %s',
- $url,
- $http_method,
- json_encode($params)
- ));
+ throw LogicException::forInvalidHttpMethod($url, $http_method, $params);
}
curl_setopt($this->curl, CURLOPT_URL, $this->url . $url);
@@ -305,7 +303,7 @@ public function execute(WebDriverCommand $command)
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $http_method);
}
- if (in_array($http_method, ['POST', 'PUT'])) {
+ if (in_array($http_method, ['POST', 'PUT'], true)) {
// Disable sending 'Expect: 100-Continue' header, as it is causing issues with eg. squid proxy
// https://tools.ietf.org/html/rfc7231#section-5.1.1
curl_setopt($this->curl, CURLOPT_HTTPHEADER, array_merge(static::DEFAULT_HTTP_HEADERS, ['Expect:']));
@@ -329,30 +327,13 @@ public function execute(WebDriverCommand $command)
$raw_results = trim(curl_exec($this->curl));
if ($error = curl_error($this->curl)) {
- $msg = sprintf(
- 'Curl error thrown for http %s to %s',
- $http_method,
- $url
- );
- if (is_array($params) && !empty($params)) {
- $msg .= sprintf(' with params: %s', json_encode($params));
- }
-
- throw new WebDriverCurlException($msg . "\n\n" . $error);
+ throw WebDriverCurlException::forCurlError($http_method, $url, $error, is_array($params) ? $params : null);
}
$results = json_decode($raw_results, true);
if ($results === null && json_last_error() !== JSON_ERROR_NONE) {
- throw new WebDriverException(
- sprintf(
- "JSON decoding of remote response failed.\n" .
- "Error code: %d\n" .
- "The response: '%s'\n",
- json_last_error(),
- $raw_results
- )
- );
+ throw UnexpectedResponseException::forJsonDecodingError(json_last_error(), $raw_results);
}
$value = null;
@@ -374,13 +355,13 @@ public function execute(WebDriverCommand $command)
$sessionId = $results['sessionId'];
}
- // @see https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors
+ // @see https://w3c.github.io/webdriver/#errors
if (isset($value['error'])) {
// W3C's WebDriver
WebDriverException::throwException($value['error'], $message, $results);
}
- $status = isset($results['status']) ? $results['status'] : 0;
+ $status = $results['status'] ?? 0;
if ($status !== 0) {
// Legacy JsonWire
WebDriverException::throwException($status, $message, $results);
@@ -400,4 +381,36 @@ public function getAddressOfRemoteServer()
{
return $this->url;
}
+
+ /**
+ * @return array
+ */
+ protected function getCommandHttpOptions(WebDriverCommand $command)
+ {
+ $commandName = $command->getName();
+ if (!isset(self::$commands[$commandName])) {
+ if ($this->isW3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) {
+ throw LogicException::forError($command->getName() . ' is not a valid command.');
+ }
+ }
+
+ if ($this->isW3cCompliant) {
+ $raw = self::$w3cCompliantCommands[$command->getName()];
+ } else {
+ $raw = self::$commands[$command->getName()];
+ }
+
+ if ($command instanceof CustomWebDriverCommand) {
+ $url = $command->getCustomUrl();
+ $method = $command->getCustomMethod();
+ } else {
+ $url = $raw['url'];
+ $method = $raw['method'];
+ }
+
+ return [
+ 'url' => $url,
+ 'method' => $method,
+ ];
+ }
}
diff --git a/lib/Remote/JsonWireCompat.php b/lib/Remote/JsonWireCompat.php
index 65a6956ba..b9e1b5ee4 100644
--- a/lib/Remote/JsonWireCompat.php
+++ b/lib/Remote/JsonWireCompat.php
@@ -2,6 +2,7 @@
namespace Facebook\WebDriver\Remote;
+use Facebook\WebDriver\Exception\Internal\UnexpectedResponseException;
use Facebook\WebDriver\WebDriverBy;
/**
@@ -14,12 +15,21 @@ abstract class JsonWireCompat
/**
* Element identifier defined in the W3C's WebDriver protocol.
*
- * @see https://w3c.github.io/webdriver/webdriver-spec.html#elements
+ * @see https://w3c.github.io/webdriver/#elements
*/
- const WEB_DRIVER_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf';
+ public const WEB_DRIVER_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf';
- public static function getElement(array $rawElement)
+ /**
+ * @param mixed $rawElement Value is validated to by an array, exception is thrown otherwise
+ * @throws UnexpectedResponseException When value of other type than array is given
+ */
+ public static function getElement($rawElement)
{
+ // The method intentionally accept mixed, so that assertion of the rawElement format could be done on one place
+ if (!is_array($rawElement)) {
+ throw UnexpectedResponseException::forElementNotArray($rawElement);
+ }
+
if (array_key_exists(self::WEB_DRIVER_ELEMENT_IDENTIFIER, $rawElement)) {
// W3C's WebDriver
return $rawElement[self::WEB_DRIVER_ELEMENT_IDENTIFIER];
@@ -30,7 +40,6 @@ public static function getElement(array $rawElement)
}
/**
- * @param WebDriverBy $by
* @param bool $isW3cCompliant
*
* @return array
diff --git a/lib/Remote/RemoteExecuteMethod.php b/lib/Remote/RemoteExecuteMethod.php
index 6cc0c26a7..265abcc1e 100644
--- a/lib/Remote/RemoteExecuteMethod.php
+++ b/lib/Remote/RemoteExecuteMethod.php
@@ -9,9 +9,6 @@ class RemoteExecuteMethod implements ExecuteMethod
*/
private $driver;
- /**
- * @param RemoteWebDriver $driver
- */
public function __construct(RemoteWebDriver $driver)
{
$this->driver = $driver;
@@ -19,7 +16,6 @@ public function __construct(RemoteWebDriver $driver)
/**
* @param string $command_name
- * @param array $parameters
* @return mixed
*/
public function execute($command_name, array $parameters = [])
diff --git a/lib/Remote/RemoteMouse.php b/lib/Remote/RemoteMouse.php
index e95a12fe3..fee209f94 100644
--- a/lib/Remote/RemoteMouse.php
+++ b/lib/Remote/RemoteMouse.php
@@ -10,6 +10,13 @@
*/
class RemoteMouse implements WebDriverMouse
{
+ /** @internal */
+ public const BUTTON_LEFT = 0;
+ /** @internal */
+ public const BUTTON_MIDDLE = 1;
+ /** @internal */
+ public const BUTTON_RIGHT = 2;
+
/**
* @var RemoteExecuteMethod
*/
@@ -20,7 +27,6 @@ class RemoteMouse implements WebDriverMouse
private $isW3cCompliant;
/**
- * @param RemoteExecuteMethod $executor
* @param bool $isW3cCompliant
*/
public function __construct(RemoteExecuteMethod $executor, $isW3cCompliant = false)
@@ -30,11 +36,9 @@ public function __construct(RemoteExecuteMethod $executor, $isW3cCompliant = fal
}
/**
- * @param null|WebDriverCoordinates $where
- *
* @return RemoteMouse
*/
- public function click(WebDriverCoordinates $where = null)
+ public function click(?WebDriverCoordinates $where = null)
{
if ($this->isW3cCompliant) {
$moveAction = $where ? [$this->createMoveAction($where)] : [];
@@ -54,18 +58,16 @@ public function click(WebDriverCoordinates $where = null)
$this->moveIfNeeded($where);
$this->executor->execute(DriverCommand::CLICK, [
- 'button' => 0,
+ 'button' => self::BUTTON_LEFT,
]);
return $this;
}
/**
- * @param WebDriverCoordinates $where
- *
* @return RemoteMouse
*/
- public function contextClick(WebDriverCoordinates $where = null)
+ public function contextClick(?WebDriverCoordinates $where = null)
{
if ($this->isW3cCompliant) {
$moveAction = $where ? [$this->createMoveAction($where)] : [];
@@ -78,13 +80,11 @@ public function contextClick(WebDriverCoordinates $where = null)
'actions' => array_merge($moveAction, [
[
'type' => 'pointerDown',
- 'duration' => 0,
- 'button' => 2,
+ 'button' => self::BUTTON_RIGHT,
],
[
'type' => 'pointerUp',
- 'duration' => 0,
- 'button' => 2,
+ 'button' => self::BUTTON_RIGHT,
],
]),
],
@@ -96,22 +96,20 @@ public function contextClick(WebDriverCoordinates $where = null)
$this->moveIfNeeded($where);
$this->executor->execute(DriverCommand::CLICK, [
- 'button' => 2,
+ 'button' => self::BUTTON_RIGHT,
]);
return $this;
}
/**
- * @param WebDriverCoordinates $where
- *
* @return RemoteMouse
*/
- public function doubleClick(WebDriverCoordinates $where = null)
+ public function doubleClick(?WebDriverCoordinates $where = null)
{
if ($this->isW3cCompliant) {
$clickActions = $this->createClickActions();
- $moveAction = null === $where ? [] : [$this->createMoveAction($where)];
+ $moveAction = $where === null ? [] : [$this->createMoveAction($where)];
$this->executor->execute(DriverCommand::ACTIONS, [
'actions' => [
[
@@ -133,11 +131,9 @@ public function doubleClick(WebDriverCoordinates $where = null)
}
/**
- * @param WebDriverCoordinates $where
- *
* @return RemoteMouse
*/
- public function mouseDown(WebDriverCoordinates $where = null)
+ public function mouseDown(?WebDriverCoordinates $where = null)
{
if ($this->isW3cCompliant) {
$this->executor->execute(DriverCommand::ACTIONS, [
@@ -150,8 +146,7 @@ public function mouseDown(WebDriverCoordinates $where = null)
$this->createMoveAction($where),
[
'type' => 'pointerDown',
- 'duration' => 0,
- 'button' => 0,
+ 'button' => self::BUTTON_LEFT,
],
],
],
@@ -168,14 +163,13 @@ public function mouseDown(WebDriverCoordinates $where = null)
}
/**
- * @param WebDriverCoordinates $where
* @param int|null $x_offset
* @param int|null $y_offset
*
* @return RemoteMouse
*/
public function mouseMove(
- WebDriverCoordinates $where = null,
+ ?WebDriverCoordinates $where = null,
$x_offset = null,
$y_offset = null
) {
@@ -211,11 +205,9 @@ public function mouseMove(
}
/**
- * @param WebDriverCoordinates $where
- *
* @return RemoteMouse
*/
- public function mouseUp(WebDriverCoordinates $where = null)
+ public function mouseUp(?WebDriverCoordinates $where = null)
{
if ($this->isW3cCompliant) {
$moveAction = $where ? [$this->createMoveAction($where)] : [];
@@ -229,8 +221,7 @@ public function mouseUp(WebDriverCoordinates $where = null)
'actions' => array_merge($moveAction, [
[
'type' => 'pointerUp',
- 'duration' => 0,
- 'button' => 0,
+ 'button' => self::BUTTON_LEFT,
],
]),
],
@@ -246,10 +237,7 @@ public function mouseUp(WebDriverCoordinates $where = null)
return $this;
}
- /**
- * @param WebDriverCoordinates $where
- */
- protected function moveIfNeeded(WebDriverCoordinates $where = null)
+ protected function moveIfNeeded(?WebDriverCoordinates $where = null)
{
if ($where) {
$this->mouseMove($where);
@@ -257,26 +245,27 @@ protected function moveIfNeeded(WebDriverCoordinates $where = null)
}
/**
- * @param WebDriverCoordinates $where
* @param int|null $x_offset
* @param int|null $y_offset
*
* @return array
*/
private function createMoveAction(
- WebDriverCoordinates $where = null,
+ ?WebDriverCoordinates $where = null,
$x_offset = null,
$y_offset = null
) {
$move_action = [
'type' => 'pointerMove',
- 'duration' => 0,
- 'x' => $x_offset === null ? 0 : $x_offset,
- 'y' => $y_offset === null ? 0 : $y_offset,
+ 'duration' => 100, // to simulate human delay
+ 'x' => $x_offset ?? 0,
+ 'y' => $y_offset ?? 0,
];
if ($where !== null) {
$move_action['origin'] = [JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $where->getAuxiliary()];
+ } else {
+ $move_action['origin'] = 'pointer';
}
return $move_action;
@@ -290,13 +279,11 @@ private function createClickActions()
return [
[
'type' => 'pointerDown',
- 'duration' => 0,
- 'button' => 0,
+ 'button' => self::BUTTON_LEFT,
],
[
'type' => 'pointerUp',
- 'duration' => 0,
- 'button' => 0,
+ 'button' => self::BUTTON_LEFT,
],
];
}
diff --git a/lib/Remote/RemoteStatus.php b/lib/Remote/RemoteStatus.php
index 3d123bd45..73ebeba21 100644
--- a/lib/Remote/RemoteStatus.php
+++ b/lib/Remote/RemoteStatus.php
@@ -29,7 +29,6 @@ protected function __construct($isReady, $message, array $meta = [])
}
/**
- * @param array $responseBody
* @return RemoteStatus
*/
public static function createFromResponse(array $responseBody)
diff --git a/lib/Remote/RemoteTargetLocator.php b/lib/Remote/RemoteTargetLocator.php
index a5bd9e03c..979b9c4f6 100644
--- a/lib/Remote/RemoteTargetLocator.php
+++ b/lib/Remote/RemoteTargetLocator.php
@@ -2,7 +2,7 @@
namespace Facebook\WebDriver\Remote;
-use Facebook\WebDriver\WebDriver;
+use Facebook\WebDriver\Exception\Internal\LogicException;
use Facebook\WebDriver\WebDriverAlert;
use Facebook\WebDriver\WebDriverElement;
use Facebook\WebDriver\WebDriverTargetLocator;
@@ -12,14 +12,14 @@
*/
class RemoteTargetLocator implements WebDriverTargetLocator
{
- /** @var ExecuteMethod */
+ /** @var RemoteExecuteMethod */
protected $executor;
- /** @var WebDriver */
+ /** @var RemoteWebDriver */
protected $driver;
/** @var bool */
protected $isW3cCompliant;
- public function __construct($executor, $driver, $isW3cCompliant = false)
+ public function __construct(RemoteExecuteMethod $executor, RemoteWebDriver $driver, $isW3cCompliant = false)
{
$this->executor = $executor;
$this->driver = $driver;
@@ -27,10 +27,7 @@ public function __construct($executor, $driver, $isW3cCompliant = false)
}
/**
- * Switch to the main document if the page contains iframes. Otherwise, switch
- * to the first frame on the page.
- *
- * @return WebDriver The driver focused on the top window or the first frame.
+ * @return RemoteWebDriver
*/
public function defaultContent()
{
@@ -41,16 +38,10 @@ public function defaultContent()
}
/**
- * Switch to the iframe by its id or name.
- *
- * @param WebDriverElement|null|int|string $frame The WebDriverElement,
- * the id or the name of the frame.
- * When null, switch to the current top-level browsing context
- * When int, switch to the WindowProxy identified by the value
- * When an Element, switch to that Element.
- *
- * @throws \InvalidArgumentException
- * @return WebDriver The driver focused on the given frame.
+ * @param WebDriverElement|null|int|string $frame The WebDriverElement, the id or the name of the frame.
+ * When null, switch to the current top-level browsing context When int, switch to the WindowProxy identified
+ * by the value. When an Element, switch to that Element.
+ * @return RemoteWebDriver
*/
public function frame($frame)
{
@@ -62,7 +53,7 @@ public function frame($frame)
} elseif (is_int($frame)) {
$id = $frame;
} else {
- throw new \InvalidArgumentException(
+ throw LogicException::forError(
'In W3C compliance mode frame must be either instance of WebDriverElement, integer or null'
);
}
@@ -87,7 +78,7 @@ public function frame($frame)
/**
* Switch to the parent iframe.
*
- * @return WebDriver The driver focused on the given frame.
+ * @return RemoteWebDriver This driver focused on the parent frame
*/
public function parent()
{
@@ -97,11 +88,8 @@ public function parent()
}
/**
- * Switch the focus to another window by its handle.
- *
* @param string $handle The handle of the window to be focused on.
- * @return WebDriver The driver focused on the given window.
- * @see WebDriver::getWindowHandles
+ * @return RemoteWebDriver
*/
public function window($handle)
{
@@ -117,20 +105,38 @@ public function window($handle)
}
/**
- * Switch to the currently active modal dialog for this particular driver
- * instance.
+ * Creates a new browser window and switches the focus for future commands of this driver to the new window.
*
- * @return WebDriverAlert
+ * @see https://w3c.github.io/webdriver/#new-window
+ * @param string $windowType The type of a new browser window that should be created. One of [tab, window].
+ * The created window is not guaranteed to be of the requested type; if the driver does not support the requested
+ * type, a new browser window will be created of whatever type the driver does support.
+ * @throws LogicException
+ * @return RemoteWebDriver This driver focused on the given window
*/
+ public function newWindow($windowType = self::WINDOW_TYPE_TAB)
+ {
+ if ($windowType !== self::WINDOW_TYPE_TAB && $windowType !== self::WINDOW_TYPE_WINDOW) {
+ throw LogicException::forError('Window type must by either "tab" or "window"');
+ }
+
+ if (!$this->isW3cCompliant) {
+ throw LogicException::forError('New window is only supported in W3C mode');
+ }
+
+ $response = $this->executor->execute(DriverCommand::NEW_WINDOW, ['type' => $windowType]);
+
+ $this->window($response['handle']);
+
+ return $this->driver;
+ }
+
public function alert()
{
return new WebDriverAlert($this->executor);
}
/**
- * Switches to the element that currently has focus within the document
- * currently "switched to", or the body element if this cannot be detected.
- *
* @return RemoteWebElement
*/
public function activeElement()
diff --git a/lib/Remote/RemoteTouchScreen.php b/lib/Remote/RemoteTouchScreen.php
index 889c12e6c..951c8619a 100644
--- a/lib/Remote/RemoteTouchScreen.php
+++ b/lib/Remote/RemoteTouchScreen.php
@@ -15,17 +15,12 @@ class RemoteTouchScreen implements WebDriverTouchScreen
*/
private $executor;
- /**
- * @param RemoteExecuteMethod $executor
- */
public function __construct(RemoteExecuteMethod $executor)
{
$this->executor = $executor;
}
/**
- * @param WebDriverElement $element
- *
* @return RemoteTouchScreen The instance.
*/
public function tap(WebDriverElement $element)
@@ -39,8 +34,6 @@ public function tap(WebDriverElement $element)
}
/**
- * @param WebDriverElement $element
- *
* @return RemoteTouchScreen The instance.
*/
public function doubleTap(WebDriverElement $element)
@@ -86,7 +79,6 @@ public function flick($xspeed, $yspeed)
}
/**
- * @param WebDriverElement $element
* @param int $xoffset
* @param int $yoffset
* @param int $speed
@@ -106,8 +98,6 @@ public function flickFromElement(WebDriverElement $element, $xoffset, $yoffset,
}
/**
- * @param WebDriverElement $element
- *
* @return RemoteTouchScreen The instance.
*/
public function longPress(WebDriverElement $element)
@@ -153,7 +143,6 @@ public function scroll($xoffset, $yoffset)
}
/**
- * @param WebDriverElement $element
* @param int $xoffset
* @param int $yoffset
*
diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php
index 5cac97247..3d65aaf0b 100644
--- a/lib/Remote/RemoteWebDriver.php
+++ b/lib/Remote/RemoteWebDriver.php
@@ -2,8 +2,11 @@
namespace Facebook\WebDriver\Remote;
+use Facebook\WebDriver\Exception\Internal\UnexpectedResponseException;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Facebook\WebDriver\JavaScriptExecutor;
+use Facebook\WebDriver\Support\IsElementDisplayedAtom;
+use Facebook\WebDriver\Support\ScreenshotHelper;
use Facebook\WebDriver\WebDriver;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverCapabilities;
@@ -21,7 +24,7 @@ class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInpu
*/
protected $executor;
/**
- * @var WebDriverCapabilities
+ * @var WebDriverCapabilities|null
*/
protected $capabilities;
@@ -51,24 +54,19 @@ class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInpu
protected $isW3cCompliant;
/**
- * @param HttpCommandExecutor $commandExecutor
* @param string $sessionId
- * @param WebDriverCapabilities|null $capabilities
* @param bool $isW3cCompliant false to use the legacy JsonWire protocol, true for the W3C WebDriver spec
*/
protected function __construct(
HttpCommandExecutor $commandExecutor,
$sessionId,
- WebDriverCapabilities $capabilities = null,
+ WebDriverCapabilities $capabilities,
$isW3cCompliant = false
) {
$this->executor = $commandExecutor;
$this->sessionID = $sessionId;
$this->isW3cCompliant = $isW3cCompliant;
-
- if ($capabilities !== null) {
- $this->capabilities = $capabilities;
- }
+ $this->capabilities = $capabilities;
}
/**
@@ -91,9 +89,8 @@ public static function create(
$request_timeout_in_ms = null,
$http_proxy = null,
$http_proxy_port = null,
- DesiredCapabilities $required_capabilities = null
+ ?DesiredCapabilities $required_capabilities = null
) {
- // BC layer to not break the method signature
$selenium_server_url = preg_replace('#/+$#', '', $selenium_server_url);
$desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities);
@@ -109,12 +106,12 @@ public static function create(
// W3C
$parameters = [
'capabilities' => [
- 'firstMatch' => [$desired_capabilities->toW3cCompatibleArray()],
+ 'firstMatch' => [(object) $desired_capabilities->toW3cCompatibleArray()],
],
];
if ($required_capabilities !== null && !empty($required_capabilities->toArray())) {
- $parameters['capabilities']['alwaysMatch'] = $required_capabilities->toW3cCompatibleArray();
+ $parameters['capabilities']['alwaysMatch'] = (object) $required_capabilities->toW3cCompatibleArray();
}
// Legacy protocol
@@ -122,45 +119,37 @@ public static function create(
// TODO: Selenium (as of v3.0.1) does accept requiredCapabilities only as a property of desiredCapabilities.
// This has changed with the W3C WebDriver spec, but is the only way how to pass these
// values with the legacy protocol.
- $desired_capabilities->setCapability('requiredCapabilities', $required_capabilities->toArray());
+ $desired_capabilities->setCapability('requiredCapabilities', (object) $required_capabilities->toArray());
}
- $parameters['desiredCapabilities'] = $desired_capabilities->toArray();
+ $parameters['desiredCapabilities'] = (object) $desired_capabilities->toArray();
- $command = new WebDriverCommand(
- null,
- DriverCommand::NEW_SESSION,
- $parameters
- );
+ $command = WebDriverCommand::newSession($parameters);
$response = $executor->execute($command);
- $value = $response->getValue();
- if (!$isW3cCompliant = isset($value['capabilities'])) {
- $executor->disableW3cCompliance();
- }
-
- if ($isW3cCompliant) {
- $returnedCapabilities = DesiredCapabilities::createFromW3cCapabilities($value['capabilities']);
- } else {
- $returnedCapabilities = new DesiredCapabilities($value);
- }
-
- $driver = new static($executor, $response->getSessionID(), $returnedCapabilities, $isW3cCompliant);
-
- return $driver;
+ return static::createFromResponse($response, $executor);
}
/**
* [Experimental] Construct the RemoteWebDriver by an existing session.
*
- * This constructor can boost the performance a lot by reusing the same browser for the whole test suite.
- * You cannot pass the desired capabilities because the session was created before.
+ * This constructor can boost the performance by reusing the same browser for the whole test suite. On the other
+ * hand, because the browser is not pristine, this may lead to flaky and dependent tests. So carefully
+ * consider the tradeoffs.
+ *
+ * To create the instance, we need to know Capabilities of the previously created session. You can either
+ * pass them in $existingCapabilities parameter, or we will attempt to receive them from the Selenium Grid server.
+ * However, if Capabilities were not provided and the attempt to get them was not successful,
+ * exception will be thrown.
*
- * @param string $selenium_server_url The url of the remote Selenium WebDriver server
* @param string $session_id The existing session id
+ * @param string $selenium_server_url The url of the remote Selenium WebDriver server
* @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server
* @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server
+ * @param bool $isW3cCompliant True to use W3C WebDriver (default), false to use the legacy JsonWire protocol
+ * @param WebDriverCapabilities|null $existingCapabilities Provide capabilities of the existing previously created
+ * session. If not provided, we will attempt to read them, but this will only work when using Selenium Grid.
* @return static
*/
public static function createBySessionID(
@@ -170,7 +159,9 @@ public static function createBySessionID(
$request_timeout_in_ms = null
) {
// BC layer to not break the method signature
- $w3c_compliant = func_num_args() > 3 ? func_get_arg(3) : false;
+ $isW3cCompliant = func_num_args() > 4 ? func_get_arg(4) : true;
+ $existingCapabilities = func_num_args() > 5 ? func_get_arg(5) : null;
+
$executor = new HttpCommandExecutor($selenium_server_url, null, null);
if ($connection_timeout_in_ms !== null) {
$executor->setConnectionTimeout($connection_timeout_in_ms);
@@ -179,7 +170,16 @@ public static function createBySessionID(
$executor->setRequestTimeout($request_timeout_in_ms);
}
- return new static($executor, $session_id, null, $w3c_compliant);
+ if (!$isW3cCompliant) {
+ $executor->disableW3cCompliance();
+ }
+
+ // if capabilities were not provided, attempt to read them from the Selenium Grid API
+ if ($existingCapabilities === null) {
+ $existingCapabilities = self::readExistingCapabilitiesFromSeleniumGrid($session_id, $executor);
+ }
+
+ return new static($executor, $session_id, $existingCapabilities, $isW3cCompliant);
}
/**
@@ -194,10 +194,21 @@ public function close()
return $this;
}
+ /**
+ * Create a new top-level browsing context.
+ *
+ * @codeCoverageIgnore
+ * @deprecated Use $driver->switchTo()->newWindow()
+ * @return WebDriver The current instance.
+ */
+ public function newWindow()
+ {
+ return $this->switchTo()->newWindow();
+ }
+
/**
* Find the first WebDriverElement using the given mechanism.
*
- * @param WebDriverBy $by
* @return RemoteWebElement NoSuchElementException is thrown in HttpCommandExecutor if no element is found.
* @see WebDriverBy
*/
@@ -214,7 +225,6 @@ public function findElement(WebDriverBy $by)
/**
* Find all WebDriverElements within the current page using the given mechanism.
*
- * @param WebDriverBy $by
* @return RemoteWebElement[] A list of all WebDriverElements, or an empty array if nothing matches
* @see WebDriverBy
*/
@@ -225,6 +235,10 @@ public function findElements(WebDriverBy $by)
JsonWireCompat::getUsing($by, $this->isW3cCompliant)
);
+ if (!is_array($raw_elements)) {
+ throw UnexpectedResponseException::forError('Server response to findElements command is not an array');
+ }
+
$elements = [];
foreach ($raw_elements as $raw_element) {
$elements[] = $this->newElement(JsonWireCompat::getElement($raw_element));
@@ -294,6 +308,9 @@ public function getWindowHandle()
/**
* Get all window handles available to the current session.
*
+ * Note: Do not use `end($driver->getWindowHandles())` to find the last open window, for proper solution see:
+ * https://github.com/php-webdriver/php-webdriver/wiki/Alert,-tabs,-frames,-iframes#switch-to-the-new-window
+ *
* @return array An array of string containing all available window handles.
*/
public function getWindowHandles()
@@ -361,21 +378,7 @@ public function executeAsyncScript($script, array $arguments = [])
*/
public function takeScreenshot($save_as = null)
{
- $screenshot = base64_decode(
- $this->execute(DriverCommand::SCREENSHOT)
- );
-
- if ($save_as !== null) {
- $directoryPath = dirname($save_as);
-
- if (!file_exists($directoryPath)) {
- mkdir($directoryPath, 0777, true);
- }
-
- file_put_contents($save_as, $screenshot);
- }
-
- return $screenshot;
+ return (new ScreenshotHelper($this->getExecuteMethod()))->takePageScreenshot($save_as);
}
/**
@@ -544,7 +547,7 @@ public function getSessionID()
/**
* Get capabilities of the RemoteWebDriver.
*
- * @return WebDriverCapabilities
+ * @return WebDriverCapabilities|null
*/
public function getCapabilities()
{
@@ -554,6 +557,7 @@ public function getCapabilities()
/**
* Returns a list of the currently active sessions.
*
+ * @deprecated Removed in W3C WebDriver.
* @param string $selenium_server_url The url of the remote Selenium WebDriver server
* @param int $timeout_in_ms
* @return array
@@ -574,6 +578,19 @@ public static function getAllSessions($selenium_server_url = 'http://localhost:4
public function execute($command_name, $params = [])
{
+ // As we so far only use atom for IS_ELEMENT_DISPLAYED, this condition is hardcoded here. In case more atoms
+ // are used, this should be rewritten and separated from this class (e.g. to some abstract matcher logic).
+ if ($command_name === DriverCommand::IS_ELEMENT_DISPLAYED
+ && (
+ // When capabilities are missing in php-webdriver 1.13.x, always fallback to use the atom
+ $this->getCapabilities() === null
+ // If capabilities are present, use the atom only if condition matches
+ || IsElementDisplayedAtom::match($this->getCapabilities()->getBrowserName())
+ )
+ ) {
+ return (new IsElementDisplayedAtom($this))->execute($params);
+ }
+
$command = new WebDriverCommand(
$this->sessionID,
$command_name,
@@ -589,6 +606,34 @@ public function execute($command_name, $params = [])
return null;
}
+ /**
+ * Execute custom commands on remote end.
+ * For example vendor-specific commands or other commands not implemented by php-webdriver.
+ *
+ * @see https://github.com/php-webdriver/php-webdriver/wiki/Custom-commands
+ * @param string $endpointUrl
+ * @param string $method
+ * @param array $params
+ * @return mixed|null
+ */
+ public function executeCustomCommand($endpointUrl, $method = 'GET', $params = [])
+ {
+ $command = new CustomWebDriverCommand(
+ $this->sessionID,
+ $endpointUrl,
+ $method,
+ $params
+ );
+
+ if ($this->executor) {
+ $response = $this->executor->execute($command);
+
+ return $response->getValue();
+ }
+
+ return null;
+ }
+
/**
* @internal
* @return bool
@@ -598,10 +643,33 @@ public function isW3cCompliant()
return $this->isW3cCompliant;
}
+ /**
+ * Create instance based on response to NEW_SESSION command.
+ * Also detect W3C/OSS dialect and setup the driver/executor accordingly.
+ *
+ * @internal
+ * @return static
+ */
+ protected static function createFromResponse(WebDriverResponse $response, HttpCommandExecutor $commandExecutor)
+ {
+ $responseValue = $response->getValue();
+
+ if (!$isW3cCompliant = isset($responseValue['capabilities'])) {
+ $commandExecutor->disableW3cCompliance();
+ }
+
+ if ($isW3cCompliant) {
+ $returnedCapabilities = DesiredCapabilities::createFromW3cCapabilities($responseValue['capabilities']);
+ } else {
+ $returnedCapabilities = new DesiredCapabilities($responseValue);
+ }
+
+ return new static($commandExecutor, $response->getSessionID(), $returnedCapabilities, $isW3cCompliant);
+ }
+
/**
* Prepare arguments for JavaScript injection
*
- * @param array $arguments
* @return array
*/
protected function prepareScriptArguments(array $arguments)
@@ -667,4 +735,26 @@ protected static function castToDesiredCapabilitiesObject($desired_capabilities
return $desired_capabilities;
}
+
+ protected static function readExistingCapabilitiesFromSeleniumGrid(
+ string $session_id,
+ HttpCommandExecutor $executor
+ ): DesiredCapabilities {
+ $getCapabilitiesCommand = new CustomWebDriverCommand($session_id, '/se/grid/session/:sessionId', 'GET', []);
+
+ try {
+ $capabilitiesResponse = $executor->execute($getCapabilitiesCommand);
+
+ $existingCapabilities = DesiredCapabilities::createFromW3cCapabilities(
+ $capabilitiesResponse->getValue()['capabilities']
+ );
+ if ($existingCapabilities === null) {
+ throw UnexpectedResponseException::forError('Empty capabilities received');
+ }
+ } catch (\Exception $e) {
+ throw UnexpectedResponseException::forCapabilitiesRetrievalError($e);
+ }
+
+ return $existingCapabilities;
+ }
}
diff --git a/lib/Remote/RemoteWebElement.php b/lib/Remote/RemoteWebElement.php
index cb0caee46..e0ce43b55 100644
--- a/lib/Remote/RemoteWebElement.php
+++ b/lib/Remote/RemoteWebElement.php
@@ -2,10 +2,15 @@
namespace Facebook\WebDriver\Remote;
+use Facebook\WebDriver\Exception\ElementNotInteractableException;
+use Facebook\WebDriver\Exception\Internal\IOException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
+use Facebook\WebDriver\Exception\Internal\UnexpectedResponseException;
+use Facebook\WebDriver\Exception\PhpWebDriverExceptionInterface;
use Facebook\WebDriver\Exception\UnsupportedOperationException;
-use Facebook\WebDriver\Exception\WebDriverException;
use Facebook\WebDriver\Interactions\Internal\WebDriverCoordinates;
use Facebook\WebDriver\Internal\WebDriverLocatable;
+use Facebook\WebDriver\Support\ScreenshotHelper;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverDimension;
use Facebook\WebDriver\WebDriverElement;
@@ -36,7 +41,6 @@ class RemoteWebElement implements WebDriverElement, WebDriverLocatable
protected $isW3cCompliant;
/**
- * @param RemoteExecuteMethod $executor
* @param string $id
* @param bool $isW3cCompliant
*/
@@ -51,7 +55,7 @@ public function __construct(RemoteExecuteMethod $executor, $id, $isW3cCompliant
/**
* Clear content editable or resettable element
*
- * @return RemoteWebElement The current instance.
+ * @return $this The current instance.
*/
public function clear()
{
@@ -66,14 +70,21 @@ public function clear()
/**
* Click this element.
*
- * @return RemoteWebElement The current instance.
+ * @return $this The current instance.
*/
public function click()
{
- $this->executor->execute(
- DriverCommand::CLICK_ELEMENT,
- [':id' => $this->id]
- );
+ try {
+ $this->executor->execute(
+ DriverCommand::CLICK_ELEMENT,
+ [':id' => $this->id]
+ );
+ } catch (ElementNotInteractableException $e) {
+ // An issue with geckodriver (https://github.com/mozilla/geckodriver/issues/653) prevents clicking on a link
+ // if the first child is a block-level element.
+ // The workaround in this case is to click on a child element.
+ $this->clickChildElement($e);
+ }
return $this;
}
@@ -81,9 +92,11 @@ public function click()
/**
* Find the first WebDriverElement within this element using the given mechanism.
*
- * @param WebDriverBy $by
- * @return RemoteWebElement NoSuchElementException is thrown in
- * HttpCommandExecutor if no element is found.
+ * When using xpath be aware that webdriver follows standard conventions: a search prefixed with "//" will
+ * search the entire document from the root, not just the children (relative context) of this current node.
+ * Use ".//" to limit your search to the children of this element.
+ *
+ * @return static NoSuchElementException is thrown in HttpCommandExecutor if no element is found.
* @see WebDriverBy
*/
public function findElement(WebDriverBy $by)
@@ -102,8 +115,11 @@ public function findElement(WebDriverBy $by)
/**
* Find all WebDriverElements within this element using the given mechanism.
*
- * @param WebDriverBy $by
- * @return RemoteWebElement[] A list of all WebDriverElements, or an empty
+ * When using xpath be aware that webdriver follows standard conventions: a search prefixed with "//" will
+ * search the entire document from the root, not just the children (relative context) of this current node.
+ * Use ".//" to limit your search to the children of this element.
+ *
+ * @return static[] A list of all WebDriverElements, or an empty
* array if nothing matches
* @see WebDriverBy
*/
@@ -116,6 +132,10 @@ public function findElements(WebDriverBy $by)
$params
);
+ if (!is_array($raw_elements)) {
+ throw UnexpectedResponseException::forError('Server response to findChildElements command is not an array');
+ }
+
$elements = [];
foreach ($raw_elements as $raw_element) {
$elements[] = $this->newElement(JsonWireCompat::getElement($raw_element));
@@ -125,10 +145,13 @@ public function findElements(WebDriverBy $by)
}
/**
- * Get the value of a the given attribute of the element.
+ * Get the value of the given attribute of the element.
+ * Attribute is meant what is declared in the HTML markup of the element.
+ * To read a value of a IDL "JavaScript" property (like `innerHTML`), use `getDomProperty()` method.
*
* @param string $attribute_name The name of the attribute.
- * @return string|null The value of the attribute.
+ * @return string|true|null The value of the attribute. If this is boolean attribute, return true if the element
+ * has it, otherwise return null.
*/
public function getAttribute($attribute_name)
{
@@ -156,6 +179,28 @@ public function getAttribute($attribute_name)
return $this->executor->execute(DriverCommand::GET_ELEMENT_ATTRIBUTE, $params);
}
+ /**
+ * Gets the value of a IDL JavaScript property of this element (for example `innerHTML`, `tagName` etc.).
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Glossary/IDL
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Element#properties
+ * @param string $propertyName
+ * @return mixed|null The property's current value or null if the value is not set or the property does not exist.
+ */
+ public function getDomProperty($propertyName)
+ {
+ if (!$this->isW3cCompliant) {
+ throw new UnsupportedOperationException('This method is only supported in W3C mode');
+ }
+
+ $params = [
+ ':name' => $propertyName,
+ ':id' => $this->id,
+ ];
+
+ return $this->executor->execute(DriverCommand::GET_ELEMENT_PROPERTY, $params);
+ }
+
/**
* Get the value of a given CSS property.
*
@@ -198,10 +243,25 @@ public function getLocation()
*/
public function getLocationOnScreenOnceScrolledIntoView()
{
- $location = $this->executor->execute(
- DriverCommand::GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW,
- [':id' => $this->id]
- );
+ if ($this->isW3cCompliant) {
+ $script = <<executor->execute(DriverCommand::EXECUTE_SCRIPT, [
+ 'script' => $script,
+ 'args' => [[JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $this->id]],
+ ]);
+ $location = ['x' => $result['x'], 'y' => $result['y']];
+ } else {
+ $location = $this->executor->execute(
+ DriverCommand::GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW,
+ [':id' => $this->id]
+ );
+ }
return new WebDriverPoint($location['x'], $location['y']);
}
@@ -214,10 +274,10 @@ public function getCoordinates()
$element = $this;
$on_screen = null; // planned but not yet implemented
- $in_view_port = function () use ($element) {
+ $in_view_port = static function () use ($element) {
return $element->getLocationOnScreenOnceScrolledIntoView();
};
- $on_page = function () use ($element) {
+ $on_page = static function () use ($element) {
return $element->getLocation();
};
$auxiliary = $this->getID();
@@ -305,7 +365,7 @@ public function isEnabled()
}
/**
- * Determine whether or not this element is selected or not.
+ * Determine whether this element is selected or not.
*
* @return bool
*/
@@ -321,54 +381,69 @@ public function isSelected()
* Simulate typing into an element, which may set its value.
*
* @param mixed $value The data to be typed.
- * @return RemoteWebElement The current instance.
+ * @return static The current instance.
*/
public function sendKeys($value)
{
$local_file = $this->fileDetector->getLocalFile($value);
+ $params = [];
if ($local_file === null) {
if ($this->isW3cCompliant) {
- $params = [
- 'text' => (string) $value,
- ':id' => $this->id,
- ];
+ // Work around the Geckodriver NULL issue by splitting on NULL and calling sendKeys multiple times.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1494661.
+ $encodedValues = explode(WebDriverKeys::NULL, WebDriverKeys::encode($value, true));
+ foreach ($encodedValues as $encodedValue) {
+ $params[] = [
+ 'text' => $encodedValue,
+ ':id' => $this->id,
+ ];
+ }
} else {
- $params = [
+ $params[] = [
'value' => WebDriverKeys::encode($value),
':id' => $this->id,
];
}
} else {
if ($this->isW3cCompliant) {
- $params = [
- 'text' => $local_file,
+ try {
+ // Attempt to upload the file to the remote browser.
+ // This is so far non-W3C compliant method, so it may fail - if so, we just ignore the exception.
+ // @see https://github.com/w3c/webdriver/issues/1355
+ $fileName = $this->upload($local_file);
+ } catch (PhpWebDriverExceptionInterface $e) {
+ $fileName = $local_file;
+ }
+
+ $params[] = [
+ 'text' => $fileName,
':id' => $this->id,
];
} else {
- $params = [
+ $params[] = [
'value' => WebDriverKeys::encode($this->upload($local_file)),
':id' => $this->id,
];
}
}
- $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params);
+ foreach ($params as $param) {
+ $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $param);
+ }
return $this;
}
/**
- * Set the fileDetector in order to let the RemoteWebElement to know that
- * you are going to upload a file.
+ * Set the fileDetector in order to let the RemoteWebElement to know that you are going to upload a file.
*
* Basically, if you want WebDriver trying to send a file, set the fileDetector
* to be LocalFileDetector. Otherwise, keep it UselessFileDetector.
*
* eg. `$element->setFileDetector(new LocalFileDetector);`
*
- * @param FileDetector $detector
- * @return RemoteWebElement
+ * @return $this
* @see FileDetector
* @see LocalFileDetector
* @see UselessFileDetector
@@ -383,17 +458,28 @@ public function setFileDetector(FileDetector $detector)
/**
* If this current element is a form, or an element within a form, then this will be submitted to the remote server.
*
- * @return RemoteWebElement The current instance.
+ * @return $this The current instance.
*/
public function submit()
{
if ($this->isW3cCompliant) {
+ // Submit method cannot be called directly in case an input of this form is named "submit".
+ // We use this polyfill to trigger 'submit' event using form.dispatchEvent().
+ $submitPolyfill = <<executor->execute(DriverCommand::EXECUTE_SCRIPT, [
- // cannot call the submit method directly in case an input of this form is named "submit"
- 'script' => sprintf(
- 'return Object.getPrototypeOf(%1$s).submit.call(%1$s);',
- 'form' === $this->getTagName() ? 'arguments[0]' : 'arguments[0].form'
- ),
+ 'script' => $submitPolyfill,
'args' => [[JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $this->id]],
]);
@@ -419,42 +505,25 @@ public function getID()
}
/**
- * Take screenshot of a specific element.
+ * Take a screenshot of a specific element.
*
* @param string $save_as The path of the screenshot to be saved.
* @return string The screenshot in PNG format.
*/
public function takeElementScreenshot($save_as = null)
{
- $screenshot = base64_decode(
- $this->executor->execute(
- DriverCommand::TAKE_ELEMENT_SCREENSHOT,
- [':id' => $this->id]
- )
- );
-
- if ($save_as !== null) {
- $directoryPath = dirname($save_as);
- if (!file_exists($directoryPath)) {
- mkdir($directoryPath, 0777, true);
- }
-
- file_put_contents($save_as, $screenshot);
- }
-
- return $screenshot;
+ return (new ScreenshotHelper($this->executor))->takeElementScreenshot($this->id, $save_as);
}
/**
- * Test if two element IDs refer to the same DOM element.
+ * Test if two elements IDs refer to the same DOM element.
*
- * @param WebDriverElement $other
* @return bool
*/
public function equals(WebDriverElement $other)
{
if ($this->isW3cCompliant) {
- throw new UnsupportedOperationException('"elementEquals" is not supported by the W3C specification');
+ return $this->getID() === $other->getID();
}
return $this->executor->execute(DriverCommand::ELEMENT_EQUALS, [
@@ -463,6 +532,61 @@ public function equals(WebDriverElement $other)
]);
}
+ /**
+ * Get representation of an element's shadow root for accessing the shadow DOM of a web component.
+ *
+ * @return ShadowRoot
+ */
+ public function getShadowRoot()
+ {
+ if (!$this->isW3cCompliant) {
+ throw new UnsupportedOperationException('This method is only supported in W3C mode');
+ }
+
+ $response = $this->executor->execute(
+ DriverCommand::GET_ELEMENT_SHADOW_ROOT,
+ [
+ ':id' => $this->id,
+ ]
+ );
+
+ return ShadowRoot::createFromResponse($this->executor, $response);
+ }
+
+ /**
+ * Attempt to click on a child level element.
+ *
+ * This provides a workaround for geckodriver bug 653 whereby a link whose first element is a block-level element
+ * throws an ElementNotInteractableException could not scroll into view exception.
+ *
+ * The workaround provided here attempts to click on a child node of the element.
+ * In case the first child is hidden, other elements are processed until we run out of elements.
+ *
+ * @param ElementNotInteractableException $originalException The exception to throw if unable to click on any child
+ * @see https://github.com/mozilla/geckodriver/issues/653
+ * @see https://bugzilla.mozilla.org/show_bug.cgi?id=1374283
+ */
+ protected function clickChildElement(ElementNotInteractableException $originalException)
+ {
+ $children = $this->findElements(WebDriverBy::xpath('./*'));
+ foreach ($children as $child) {
+ try {
+ // Note: This does not use $child->click() as this would cause recursion into all children.
+ // Where the element is hidden, all children will also be hidden.
+ $this->executor->execute(
+ DriverCommand::CLICK_ELEMENT,
+ [':id' => $child->id]
+ );
+
+ return;
+ } catch (ElementNotInteractableException $e) {
+ // Ignore the ElementNotInteractableException exception on this node. Try the next child instead.
+ }
+ }
+
+ throw $originalException;
+ }
+
/**
* Return the WebDriverElement with $id
*
@@ -480,35 +604,47 @@ protected function newElement($id)
*
* @param string $local_file
*
- * @throws WebDriverException
+ * @throws LogicException
* @return string The remote path of the file.
*/
protected function upload($local_file)
{
if (!is_file($local_file)) {
- throw new WebDriverException('You may only upload files: ' . $local_file);
+ throw LogicException::forError('You may only upload files: ' . $local_file);
}
+ $temp_zip_path = $this->createTemporaryZipArchive($local_file);
+
+ $remote_path = $this->executor->execute(
+ DriverCommand::UPLOAD_FILE,
+ ['file' => base64_encode(file_get_contents($temp_zip_path))]
+ );
+
+ unlink($temp_zip_path);
+
+ return $remote_path;
+ }
+
+ /**
+ * @param string $fileToZip
+ * @return string
+ */
+ protected function createTemporaryZipArchive($fileToZip)
+ {
// Create a temporary file in the system temp directory.
- $temp_zip = tempnam(sys_get_temp_dir(), 'WebDriverZip');
+ // Intentionally do not use `tempnam()`, as it creates empty file which zip extension may not handle.
+ $tempZipPath = sys_get_temp_dir() . '/' . uniqid('WebDriverZip', false);
+
$zip = new ZipArchive();
- if (($errorCode = $zip->open($temp_zip, ZipArchive::CREATE)) !== true) {
- throw new WebDriverException(sprintf('Error creating zip archive: %s', $errorCode));
+ if (($errorCode = $zip->open($tempZipPath, ZipArchive::CREATE)) !== true) {
+ throw IOException::forFileError(sprintf('Error creating zip archive: %s', $errorCode), $tempZipPath);
}
- $info = pathinfo($local_file);
+ $info = pathinfo($fileToZip);
$file_name = $info['basename'];
- $zip->addFile($local_file, $file_name);
+ $zip->addFile($fileToZip, $file_name);
$zip->close();
- $params = [
- 'file' => base64_encode(file_get_contents($temp_zip)),
- ];
- $remote_path = $this->executor->execute(
- DriverCommand::UPLOAD_FILE,
- $params
- );
- unlink($temp_zip);
- return $remote_path;
+ return $tempZipPath;
}
}
diff --git a/lib/Remote/Service/DriverCommandExecutor.php b/lib/Remote/Service/DriverCommandExecutor.php
index 4e364e073..5e3ef8399 100644
--- a/lib/Remote/Service/DriverCommandExecutor.php
+++ b/lib/Remote/Service/DriverCommandExecutor.php
@@ -2,6 +2,7 @@
namespace Facebook\WebDriver\Remote\Service;
+use Facebook\WebDriver\Exception\Internal\DriverServerDiedException;
use Facebook\WebDriver\Exception\WebDriverException;
use Facebook\WebDriver\Remote\DriverCommand;
use Facebook\WebDriver\Remote\HttpCommandExecutor;
@@ -9,8 +10,7 @@
use Facebook\WebDriver\Remote\WebDriverResponse;
/**
- * A HttpCommandExecutor that talks to a local driver service instead of
- * a remote server.
+ * A HttpCommandExecutor that talks to a local driver service instead of a remote server.
*/
class DriverCommandExecutor extends HttpCommandExecutor
{
@@ -26,10 +26,8 @@ public function __construct(DriverService $service)
}
/**
- * @param WebDriverCommand $command
- *
- * @throws WebDriverException
* @throws \Exception
+ * @throws WebDriverException
* @return WebDriverResponse
*/
public function execute(WebDriverCommand $command)
@@ -47,7 +45,7 @@ public function execute(WebDriverCommand $command)
return $value;
} catch (\Exception $e) {
if (!$this->service->isRunning()) {
- throw new WebDriverException('The driver server has died.');
+ throw new DriverServerDiedException($e);
}
throw $e;
}
diff --git a/lib/Remote/Service/DriverService.php b/lib/Remote/Service/DriverService.php
index f64c3b8c8..028b8cd83 100644
--- a/lib/Remote/Service/DriverService.php
+++ b/lib/Remote/Service/DriverService.php
@@ -2,13 +2,14 @@
namespace Facebook\WebDriver\Remote\Service;
-use Exception;
+use Facebook\WebDriver\Exception\Internal\IOException;
+use Facebook\WebDriver\Exception\Internal\RuntimeException;
use Facebook\WebDriver\Net\URLChecker;
use Symfony\Component\Process\Process;
-use Symfony\Component\Process\ProcessBuilder;
/**
* Start local WebDriver service (when remote WebDriver server is not used).
+ * This will start new process of respective browser driver and take care of its lifecycle.
*/
class DriverService
{
@@ -45,7 +46,7 @@ class DriverService
*/
public function __construct($executable, $port, $args = [], $environment = null)
{
- $this->executable = self::checkExecutable($executable);
+ $this->setExecutable($executable);
$this->url = sprintf('http://localhost:%d', $port);
$this->args = $args;
$this->environment = $environment ?: $_ENV;
@@ -71,6 +72,8 @@ public function start()
$this->process = $this->createProcess();
$this->process->start();
+ $this->checkWasStarted($this->process);
+
$checker = new URLChecker();
$checker->waitUntilAvailable(20 * 1000, $this->url . '/status');
@@ -108,44 +111,73 @@ public function isRunning()
}
/**
- * Check if the executable is executable.
- *
+ * @deprecated Has no effect. Will be removed in next major version. Executable is now checked
+ * when calling setExecutable().
* @param string $executable
- * @throws Exception
* @return string
*/
protected static function checkExecutable($executable)
{
- if (!is_file($executable)) {
- throw new Exception("'$executable' is not a file.");
- }
+ return $executable;
+ }
- if (!is_executable($executable)) {
- throw new Exception("'$executable' is not executable.");
+ /**
+ * @param string $executable
+ * @throws IOException
+ */
+ protected function setExecutable($executable)
+ {
+ if ($this->isExecutable($executable)) {
+ $this->executable = $executable;
+
+ return;
}
- return $executable;
+ throw IOException::forFileError(
+ 'File is not executable. Make sure the path is correct or use environment variable to specify'
+ . ' location of the executable.',
+ $executable
+ );
}
/**
- * @return Process
+ * @param Process $process
*/
- private function createProcess()
+ protected function checkWasStarted($process)
{
- // BC: ProcessBuilder deprecated since Symfony 3.4 and removed in Symfony 4.0.
- if (class_exists(ProcessBuilder::class)
- && false === mb_strpos('@deprecated', (new \ReflectionClass(ProcessBuilder::class))->getDocComment())
- ) {
- $processBuilder = (new ProcessBuilder())
- ->setPrefix($this->executable)
- ->setArguments($this->args)
- ->addEnvironmentVariables($this->environment);
-
- return $processBuilder->getProcess();
+ usleep(10000); // wait 10ms, otherwise the asynchronous process failure may not yet be propagated
+
+ if (!$process->isRunning()) {
+ throw RuntimeException::forDriverError($process);
}
- // Safe to use since Symfony 3.3
+ }
+
+ private function createProcess(): Process
+ {
$commandLine = array_merge([$this->executable], $this->args);
return new Process($commandLine, null, $this->environment);
}
+
+ /**
+ * Check whether given file is executable directly or using system PATH
+ */
+ private function isExecutable(string $filename): bool
+ {
+ if (is_executable($filename)) {
+ return true;
+ }
+ if ($filename !== basename($filename)) { // $filename is an absolute path, do no try to search it in PATH
+ return false;
+ }
+
+ $paths = explode(PATH_SEPARATOR, getenv('PATH'));
+ foreach ($paths as $path) {
+ if (is_executable($path . DIRECTORY_SEPARATOR . $filename)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/lib/Remote/ShadowRoot.php b/lib/Remote/ShadowRoot.php
new file mode 100644
index 000000000..5d419b19a
--- /dev/null
+++ b/lib/Remote/ShadowRoot.php
@@ -0,0 +1,98 @@
+executor = $executor;
+ $this->id = $id;
+ }
+
+ /**
+ * @return self
+ */
+ public static function createFromResponse(RemoteExecuteMethod $executor, array $response)
+ {
+ if (empty($response[self::SHADOW_ROOT_IDENTIFIER])) {
+ throw new UnknownErrorException('Shadow root is missing in server response');
+ }
+
+ return new self($executor, $response[self::SHADOW_ROOT_IDENTIFIER]);
+ }
+
+ /**
+ * @return RemoteWebElement
+ */
+ public function findElement(WebDriverBy $locator)
+ {
+ $params = JsonWireCompat::getUsing($locator, true);
+ $params[':id'] = $this->id;
+
+ $rawElement = $this->executor->execute(
+ DriverCommand::FIND_ELEMENT_FROM_SHADOW_ROOT,
+ $params
+ );
+
+ return new RemoteWebElement($this->executor, JsonWireCompat::getElement($rawElement), true);
+ }
+
+ /**
+ * @return WebDriverElement[]
+ */
+ public function findElements(WebDriverBy $locator)
+ {
+ $params = JsonWireCompat::getUsing($locator, true);
+ $params[':id'] = $this->id;
+
+ $rawElements = $this->executor->execute(
+ DriverCommand::FIND_ELEMENTS_FROM_SHADOW_ROOT,
+ $params
+ );
+
+ if (!is_array($rawElements)) {
+ throw UnexpectedResponseException::forError(
+ 'Server response to findElementsFromShadowRoot command is not an array'
+ );
+ }
+
+ $elements = [];
+ foreach ($rawElements as $rawElement) {
+ $elements[] = new RemoteWebElement($this->executor, JsonWireCompat::getElement($rawElement), true);
+ }
+
+ return $elements;
+ }
+
+ /**
+ * @return string
+ */
+ public function getID()
+ {
+ return $this->id;
+ }
+}
diff --git a/lib/Remote/WebDriverBrowserType.php b/lib/Remote/WebDriverBrowserType.php
index bc7c041a5..9e3f4e0ff 100644
--- a/lib/Remote/WebDriverBrowserType.php
+++ b/lib/Remote/WebDriverBrowserType.php
@@ -9,26 +9,30 @@
*/
class WebDriverBrowserType
{
- const FIREFOX = 'firefox';
- const FIREFOX_PROXY = 'firefoxproxy';
- const FIREFOX_CHROME = 'firefoxchrome';
- const GOOGLECHROME = 'googlechrome';
- const SAFARI = 'safari';
- const SAFARI_PROXY = 'safariproxy';
- const OPERA = 'opera';
- const MICROSOFT_EDGE = 'MicrosoftEdge';
- const IEXPLORE = 'iexplore';
- const IEXPLORE_PROXY = 'iexploreproxy';
- const CHROME = 'chrome';
- const KONQUEROR = 'konqueror';
- const MOCK = 'mock';
- const IE_HTA = 'iehta';
- const ANDROID = 'android';
- const HTMLUNIT = 'htmlunit';
- const IE = 'internet explorer';
- const IPHONE = 'iphone';
- const IPAD = 'iPad';
- const PHANTOMJS = 'phantomjs';
+ public const FIREFOX = 'firefox';
+ public const FIREFOX_PROXY = 'firefoxproxy';
+ public const FIREFOX_CHROME = 'firefoxchrome';
+ public const GOOGLECHROME = 'googlechrome';
+ public const SAFARI = 'safari';
+ public const SAFARI_PROXY = 'safariproxy';
+ public const OPERA = 'opera';
+ public const MICROSOFT_EDGE = 'MicrosoftEdge';
+ public const IEXPLORE = 'iexplore';
+ public const IEXPLORE_PROXY = 'iexploreproxy';
+ public const CHROME = 'chrome';
+ public const KONQUEROR = 'konqueror';
+ public const MOCK = 'mock';
+ public const IE_HTA = 'iehta';
+ public const ANDROID = 'android';
+ public const HTMLUNIT = 'htmlunit';
+ public const IE = 'internet explorer';
+ public const IPHONE = 'iphone';
+ public const IPAD = 'iPad';
+ /**
+ * @deprecated PhantomJS is no longer developed and its support will be removed in next major version.
+ * Use headless Chrome or Firefox instead.
+ */
+ public const PHANTOMJS = 'phantomjs';
private function __construct()
{
diff --git a/lib/Remote/WebDriverCapabilityType.php b/lib/Remote/WebDriverCapabilityType.php
index 45833339a..ee2538061 100644
--- a/lib/Remote/WebDriverCapabilityType.php
+++ b/lib/Remote/WebDriverCapabilityType.php
@@ -9,22 +9,22 @@
*/
class WebDriverCapabilityType
{
- const BROWSER_NAME = 'browserName';
- const VERSION = 'version';
- const PLATFORM = 'platform';
- const JAVASCRIPT_ENABLED = 'javascriptEnabled';
- const TAKES_SCREENSHOT = 'takesScreenshot';
- const HANDLES_ALERTS = 'handlesAlerts';
- const DATABASE_ENABLED = 'databaseEnabled';
- const LOCATION_CONTEXT_ENABLED = 'locationContextEnabled';
- const APPLICATION_CACHE_ENABLED = 'applicationCacheEnabled';
- const BROWSER_CONNECTION_ENABLED = 'browserConnectionEnabled';
- const CSS_SELECTORS_ENABLED = 'cssSelectorsEnabled';
- const WEB_STORAGE_ENABLED = 'webStorageEnabled';
- const ROTATABLE = 'rotatable';
- const ACCEPT_SSL_CERTS = 'acceptSslCerts';
- const NATIVE_EVENTS = 'nativeEvents';
- const PROXY = 'proxy';
+ public const BROWSER_NAME = 'browserName';
+ public const VERSION = 'version';
+ public const PLATFORM = 'platform';
+ public const JAVASCRIPT_ENABLED = 'javascriptEnabled';
+ public const TAKES_SCREENSHOT = 'takesScreenshot';
+ public const HANDLES_ALERTS = 'handlesAlerts';
+ public const DATABASE_ENABLED = 'databaseEnabled';
+ public const LOCATION_CONTEXT_ENABLED = 'locationContextEnabled';
+ public const APPLICATION_CACHE_ENABLED = 'applicationCacheEnabled';
+ public const BROWSER_CONNECTION_ENABLED = 'browserConnectionEnabled';
+ public const CSS_SELECTORS_ENABLED = 'cssSelectorsEnabled';
+ public const WEB_STORAGE_ENABLED = 'webStorageEnabled';
+ public const ROTATABLE = 'rotatable';
+ public const ACCEPT_SSL_CERTS = 'acceptSslCerts';
+ public const NATIVE_EVENTS = 'nativeEvents';
+ public const PROXY = 'proxy';
private function __construct()
{
diff --git a/lib/Remote/WebDriverCommand.php b/lib/Remote/WebDriverCommand.php
index 9b893697a..27f21b605 100644
--- a/lib/Remote/WebDriverCommand.php
+++ b/lib/Remote/WebDriverCommand.php
@@ -4,18 +4,19 @@
class WebDriverCommand
{
+ /** @var string|null */
+ protected $sessionID;
/** @var string */
- private $sessionID;
- /** @var string */
- private $name;
+ protected $name;
/** @var array */
- private $parameters;
+ protected $parameters;
/**
* @param string $session_id
* @param string $name Constant from DriverCommand
* @param array $parameters
* @todo In 2.0 force parameters to be an array, then remove is_array() checks in HttpCommandExecutor
+ * @todo In 2.0 make constructor private. Use by default static `::create()` with sessionID type string.
*/
public function __construct($session_id, $name, $parameters)
{
@@ -24,6 +25,15 @@ public function __construct($session_id, $name, $parameters)
$this->parameters = $parameters;
}
+ /**
+ * @return self
+ */
+ public static function newSession(array $parameters)
+ {
+ // TODO: In 2.0 call empty constructor and assign properties directly.
+ return new self(null, DriverCommand::NEW_SESSION, $parameters);
+ }
+
/**
* @return string
*/
@@ -33,7 +43,7 @@ public function getName()
}
/**
- * @return string
+ * @return string|null Could be null for newSession command
*/
public function getSessionID()
{
diff --git a/lib/Support/Events/EventFiringWebDriver.php b/lib/Support/Events/EventFiringWebDriver.php
index 81a27816b..21e5a6887 100644
--- a/lib/Support/Events/EventFiringWebDriver.php
+++ b/lib/Support/Events/EventFiringWebDriver.php
@@ -26,11 +26,7 @@ class EventFiringWebDriver implements WebDriver, JavaScriptExecutor
*/
protected $dispatcher;
- /**
- * @param WebDriver $driver
- * @param WebDriverDispatcher $dispatcher
- */
- public function __construct(WebDriver $driver, WebDriverDispatcher $dispatcher = null)
+ public function __construct(WebDriver $driver, ?WebDriverDispatcher $dispatcher = null)
{
$this->dispatcher = $dispatcher ?: new WebDriverDispatcher();
if (!$this->dispatcher->getDefaultDriver()) {
@@ -63,6 +59,7 @@ public function getWebDriver()
public function get($url)
{
$this->dispatch('beforeNavigateTo', $url, $this);
+
try {
$this->driver->get($url);
} catch (WebDriverException $exception) {
@@ -75,7 +72,6 @@ public function get($url)
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return array
*/
@@ -99,7 +95,6 @@ public function findElements(WebDriverBy $by)
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return EventFiringWebElement
*/
@@ -121,7 +116,6 @@ public function findElement(WebDriverBy $by)
/**
* @param string $script
- * @param array $arguments
* @throws WebDriverException
* @return mixed
*/
@@ -149,7 +143,6 @@ public function executeScript($script, array $arguments = [])
/**
* @param string $script
- * @param array $arguments
* @throws WebDriverException
* @return mixed
*/
@@ -162,6 +155,7 @@ public function executeAsyncScript($script, array $arguments = [])
}
$this->dispatch('beforeScript', $script, $this);
+
try {
$result = $this->driver->executeAsyncScript($script, $arguments);
} catch (WebDriverException $exception) {
@@ -373,7 +367,6 @@ public function execute($name, $params)
}
/**
- * @param WebDriverElement $element
* @return EventFiringWebElement
*/
protected function newElement(WebDriverElement $element)
@@ -394,9 +387,6 @@ protected function dispatch($method, ...$arguments)
$this->dispatcher->dispatch($method, $arguments);
}
- /**
- * @param WebDriverException $exception
- */
protected function dispatchOnException(WebDriverException $exception)
{
$this->dispatch('onException', $exception, $this);
diff --git a/lib/Support/Events/EventFiringWebDriverNavigation.php b/lib/Support/Events/EventFiringWebDriverNavigation.php
index 59eab3fd7..27ea34205 100644
--- a/lib/Support/Events/EventFiringWebDriverNavigation.php
+++ b/lib/Support/Events/EventFiringWebDriverNavigation.php
@@ -17,10 +17,6 @@ class EventFiringWebDriverNavigation implements WebDriverNavigationInterface
*/
protected $dispatcher;
- /**
- * @param WebDriverNavigationInterface $navigator
- * @param WebDriverDispatcher $dispatcher
- */
public function __construct(WebDriverNavigationInterface $navigator, WebDriverDispatcher $dispatcher)
{
$this->navigator = $navigator;
@@ -49,6 +45,7 @@ public function back()
'beforeNavigateBack',
$this->getDispatcher()->getDefaultDriver()
);
+
try {
$this->navigator->back();
} catch (WebDriverException $exception) {
@@ -68,6 +65,7 @@ public function forward()
'beforeNavigateForward',
$this->getDispatcher()->getDefaultDriver()
);
+
try {
$this->navigator->forward();
} catch (WebDriverException $exception) {
@@ -130,9 +128,6 @@ protected function dispatch($method, ...$arguments)
$this->dispatcher->dispatch($method, $arguments);
}
- /**
- * @param WebDriverException $exception
- */
protected function dispatchOnException(WebDriverException $exception)
{
$this->dispatch('onException', $exception);
diff --git a/lib/Support/Events/EventFiringWebElement.php b/lib/Support/Events/EventFiringWebElement.php
index 0e9271665..6caa08684 100644
--- a/lib/Support/Events/EventFiringWebElement.php
+++ b/lib/Support/Events/EventFiringWebElement.php
@@ -22,10 +22,6 @@ class EventFiringWebElement implements WebDriverElement, WebDriverLocatable
*/
protected $dispatcher;
- /**
- * @param WebDriverElement $element
- * @param WebDriverDispatcher $dispatcher
- */
public function __construct(WebDriverElement $element, WebDriverDispatcher $dispatcher)
{
$this->element = $element;
@@ -56,6 +52,7 @@ public function getElement()
public function sendKeys($value)
{
$this->dispatch('beforeChangeValueOf', $this);
+
try {
$this->element->sendKeys($value);
} catch (WebDriverException $exception) {
@@ -74,6 +71,7 @@ public function sendKeys($value)
public function click()
{
$this->dispatch('beforeClickOn', $this);
+
try {
$this->element->click();
} catch (WebDriverException $exception) {
@@ -86,7 +84,6 @@ public function click()
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return EventFiringWebElement
*/
@@ -117,7 +114,6 @@ public function findElement(WebDriverBy $by)
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return array
*/
@@ -129,6 +125,7 @@ public function findElements(WebDriverBy $by)
$this,
$this->dispatcher->getDefaultDriver()
);
+
try {
$elements = [];
foreach ($this->element->findElements($by) as $element) {
@@ -352,7 +349,6 @@ public function getID()
/**
* Test if two element IDs refer to the same DOM element.
*
- * @param WebDriverElement $other
* @return bool
*/
public function equals(WebDriverElement $other)
@@ -365,9 +361,26 @@ public function equals(WebDriverElement $other)
}
}
- /**
- * @param WebDriverException $exception
- */
+ public function takeElementScreenshot($save_as = null)
+ {
+ try {
+ return $this->element->takeElementScreenshot($save_as);
+ } catch (WebDriverException $exception) {
+ $this->dispatchOnException($exception);
+ throw $exception;
+ }
+ }
+
+ public function getShadowRoot()
+ {
+ try {
+ return $this->element->getShadowRoot();
+ } catch (WebDriverException $exception) {
+ $this->dispatchOnException($exception);
+ throw $exception;
+ }
+ }
+
protected function dispatchOnException(WebDriverException $exception)
{
$this->dispatch(
@@ -391,7 +404,6 @@ protected function dispatch($method, ...$arguments)
}
/**
- * @param WebDriverElement $element
* @return static
*/
protected function newElement(WebDriverElement $element)
diff --git a/lib/Support/IsElementDisplayedAtom.php b/lib/Support/IsElementDisplayedAtom.php
new file mode 100644
index 000000000..d95e4f01f
--- /dev/null
+++ b/lib/Support/IsElementDisplayedAtom.php
@@ -0,0 +1,71 @@
+driver = $driver;
+ }
+
+ public static function match($browserName)
+ {
+ return !in_array($browserName, self::BROWSERS_WITH_ENDPOINT_SUPPORT, true);
+ }
+
+ public function execute($params)
+ {
+ $element = new RemoteWebElement(
+ new RemoteExecuteMethod($this->driver),
+ $params[':id'],
+ $this->driver->isW3cCompliant()
+ );
+
+ return $this->executeAtom('isElementDisplayed', $element);
+ }
+
+ protected function executeAtom($atomName, ...$params)
+ {
+ return $this->driver->executeScript(
+ sprintf('%s; return (%s).apply(null, arguments);', $this->loadAtomScript($atomName), $atomName),
+ $params
+ );
+ }
+
+ private function loadAtomScript($atomName)
+ {
+ return file_get_contents(__DIR__ . '/../scripts/' . $atomName . '.js');
+ }
+}
diff --git a/lib/Support/ScreenshotHelper.php b/lib/Support/ScreenshotHelper.php
new file mode 100644
index 000000000..956f56147
--- /dev/null
+++ b/lib/Support/ScreenshotHelper.php
@@ -0,0 +1,81 @@
+executor = $executor;
+ }
+
+ /**
+ * @param string|null $saveAs
+ * @throws WebDriverException
+ * @return string
+ */
+ public function takePageScreenshot($saveAs = null)
+ {
+ $commandToExecute = [DriverCommand::SCREENSHOT];
+
+ return $this->takeScreenshot($commandToExecute, $saveAs);
+ }
+
+ public function takeElementScreenshot($elementId, $saveAs = null)
+ {
+ $commandToExecute = [DriverCommand::TAKE_ELEMENT_SCREENSHOT, [':id' => $elementId]];
+
+ return $this->takeScreenshot($commandToExecute, $saveAs);
+ }
+
+ private function takeScreenshot(array $commandToExecute, $saveAs = null)
+ {
+ $response = $this->executor->execute(...$commandToExecute);
+
+ if (!is_string($response)) {
+ throw UnexpectedResponseException::forError(
+ 'Error taking screenshot, no data received from the remote end'
+ );
+ }
+
+ $screenshot = base64_decode($response, true);
+
+ if ($screenshot === false) {
+ throw UnexpectedResponseException::forError('Error decoding screenshot data');
+ }
+
+ if ($saveAs !== null) {
+ $this->saveScreenshotToPath($screenshot, $saveAs);
+ }
+
+ return $screenshot;
+ }
+
+ private function saveScreenshotToPath($screenshot, $path)
+ {
+ $this->createDirectoryIfNotExists(dirname($path));
+
+ file_put_contents($path, $screenshot);
+ }
+
+ private function createDirectoryIfNotExists($directoryPath)
+ {
+ if (!file_exists($directoryPath)) {
+ if (!mkdir($directoryPath, 0777, true) && !is_dir($directoryPath)) {
+ throw IOException::forFileError('Directory cannot be not created', $directoryPath);
+ }
+ }
+ }
+}
diff --git a/lib/WebDriver.php b/lib/WebDriver.php
index a15d1a06b..52120a7d7 100755
--- a/lib/WebDriver.php
+++ b/lib/WebDriver.php
@@ -127,4 +127,17 @@ public function switchTo();
* @return mixed
*/
public function execute($name, $params);
+
+ // TODO: Add in next major release (BC)
+ ///**
+ // * Execute custom commands on remote end.
+ // * For example vendor-specific commands or other commands not implemented by php-webdriver.
+ // *
+ // * @see https://github.com/php-webdriver/php-webdriver/wiki/Custom-commands
+ // * @param string $endpointUrl
+ // * @param string $method
+ // * @param array $params
+ // * @return mixed|null
+ // */
+ //public function executeCustomCommand($endpointUrl, $method = 'GET', $params = []);
}
diff --git a/lib/WebDriverCheckboxes.php b/lib/WebDriverCheckboxes.php
index 5467589c8..abc8cc46c 100644
--- a/lib/WebDriverCheckboxes.php
+++ b/lib/WebDriverCheckboxes.php
@@ -2,7 +2,7 @@
namespace Facebook\WebDriver;
-use Facebook\WebDriver\Exception\WebDriverException;
+use Facebook\WebDriver\Exception\InvalidElementStateException;
/**
* Provides helper methods for checkboxes.
@@ -15,7 +15,7 @@ public function __construct(WebDriverElement $element)
$this->type = $element->getAttribute('type');
if ($this->type !== 'checkbox') {
- throw new WebDriverException('The input must be of type "checkbox".');
+ throw new InvalidElementStateException('The input must be of type "checkbox".');
}
}
diff --git a/lib/WebDriverCommandExecutor.php b/lib/WebDriverCommandExecutor.php
index 4031c235d..7f6bb3ece 100644
--- a/lib/WebDriverCommandExecutor.php
+++ b/lib/WebDriverCommandExecutor.php
@@ -11,8 +11,6 @@
interface WebDriverCommandExecutor
{
/**
- * @param WebDriverCommand $command
- *
* @return WebDriverResponse
*/
public function execute(WebDriverCommand $command);
diff --git a/lib/WebDriverDispatcher.php b/lib/WebDriverDispatcher.php
index 851214719..fe1ecb0f4 100644
--- a/lib/WebDriverDispatcher.php
+++ b/lib/WebDriverDispatcher.php
@@ -19,7 +19,6 @@ class WebDriverDispatcher
* this is needed so that EventFiringWebElement can pass the driver to the
* exception handling
*
- * @param EventFiringWebDriver $driver
* @return $this
*/
public function setDefaultDriver(EventFiringWebDriver $driver)
@@ -38,7 +37,6 @@ public function getDefaultDriver()
}
/**
- * @param WebDriverEventListener $listener
* @return $this
*/
public function register(WebDriverEventListener $listener)
@@ -49,7 +47,6 @@ public function register(WebDriverEventListener $listener)
}
/**
- * @param WebDriverEventListener $listener
* @return $this
*/
public function unregister(WebDriverEventListener $listener)
diff --git a/lib/WebDriverElement.php b/lib/WebDriverElement.php
index 538e0a8e5..8ffa18383 100644
--- a/lib/WebDriverElement.php
+++ b/lib/WebDriverElement.php
@@ -2,6 +2,8 @@
namespace Facebook\WebDriver;
+use Facebook\WebDriver\Remote\ShadowRoot;
+
/**
* Interface for an HTML element in the WebDriver framework.
*/
@@ -22,13 +24,26 @@ public function clear();
public function click();
/**
- * Get the value of a the given attribute of the element.
+ * Get the value of the given attribute of the element.
+ * Attribute is meant what is declared in the HTML markup of the element.
+ * To read a value of a IDL "JavaScript" property (like `innerHTML`), use `getDomProperty()` method.
*
* @param string $attribute_name The name of the attribute.
* @return string|null The value of the attribute.
*/
public function getAttribute($attribute_name);
+ /*
+ * Gets the value of a IDL JavaScript property of this element (for example `innerHTML`, `tagName` etc.).
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Glossary/IDL
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Element#properties
+ * @param string $propertyName
+ * @return string|null The property's current value or null if the value is not set or the property does not exist.
+ * @todo Add in next major release (BC)
+ */
+ // public function getDomProperty($propertyName);
+
/**
* Get the value of a given CSS property.
*
@@ -128,4 +143,12 @@ public function getID();
* @todo Add in next major release (BC)
*/
//public function takeElementScreenshot($save_as = null);
+
+ /**
+ * Get representation of an element's shadow root for accessing the shadow DOM of a web component.
+ *
+ * @return ShadowRoot
+ * @todo Add in next major release (BC)
+ */
+ //public function getShadowRoot();
}
diff --git a/lib/WebDriverEventListener.php b/lib/WebDriverEventListener.php
index 673d85bee..9ef98d8f3 100644
--- a/lib/WebDriverEventListener.php
+++ b/lib/WebDriverEventListener.php
@@ -10,85 +10,43 @@ interface WebDriverEventListener
{
/**
* @param string $url
- * @param EventFiringWebDriver $driver
*/
public function beforeNavigateTo($url, EventFiringWebDriver $driver);
/**
* @param string $url
- * @param EventFiringWebDriver $driver
*/
public function afterNavigateTo($url, EventFiringWebDriver $driver);
- /**
- * @param EventFiringWebDriver $driver
- */
public function beforeNavigateBack(EventFiringWebDriver $driver);
- /**
- * @param EventFiringWebDriver $driver
- */
public function afterNavigateBack(EventFiringWebDriver $driver);
- /**
- * @param EventFiringWebDriver $driver
- */
public function beforeNavigateForward(EventFiringWebDriver $driver);
- /**
- * @param EventFiringWebDriver $driver
- */
public function afterNavigateForward(EventFiringWebDriver $driver);
- /**
- * @param WebDriverBy $by
- * @param EventFiringWebElement|null $element
- * @param EventFiringWebDriver $driver
- */
public function beforeFindBy(WebDriverBy $by, $element, EventFiringWebDriver $driver);
- /**
- * @param WebDriverBy $by
- * @param EventFiringWebElement|null $element
- * @param EventFiringWebDriver $driver
- */
public function afterFindBy(WebDriverBy $by, $element, EventFiringWebDriver $driver);
/**
* @param string $script
- * @param EventFiringWebDriver $driver
*/
public function beforeScript($script, EventFiringWebDriver $driver);
/**
* @param string $script
- * @param EventFiringWebDriver $driver
*/
public function afterScript($script, EventFiringWebDriver $driver);
- /**
- * @param EventFiringWebElement $element
- */
public function beforeClickOn(EventFiringWebElement $element);
- /**
- * @param EventFiringWebElement $element
- */
public function afterClickOn(EventFiringWebElement $element);
- /**
- * @param EventFiringWebElement $element
- */
public function beforeChangeValueOf(EventFiringWebElement $element);
- /**
- * @param EventFiringWebElement $element
- */
public function afterChangeValueOf(EventFiringWebElement $element);
- /**
- * @param WebDriverException $exception
- * @param EventFiringWebDriver $driver
- */
- public function onException(WebDriverException $exception, EventFiringWebDriver $driver = null);
+ public function onException(WebDriverException $exception, ?EventFiringWebDriver $driver = null);
}
diff --git a/lib/WebDriverExpectedCondition.php b/lib/WebDriverExpectedCondition.php
index 23fc92fba..188f3c4d8 100644
--- a/lib/WebDriverExpectedCondition.php
+++ b/lib/WebDriverExpectedCondition.php
@@ -2,7 +2,8 @@
namespace Facebook\WebDriver;
-use Facebook\WebDriver\Exception\NoAlertOpenException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
+use Facebook\WebDriver\Exception\NoSuchAlertException;
use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Exception\NoSuchFrameException;
use Facebook\WebDriver\Exception\StaleElementReferenceException;
@@ -375,9 +376,7 @@ public static function invisibilityOfElementLocated(WebDriverBy $by)
function (WebDriver $driver) use ($by) {
try {
return !$driver->findElement($by)->isDisplayed();
- } catch (NoSuchElementException $e) {
- return true;
- } catch (StaleElementReferenceException $e) {
+ } catch (NoSuchElementException|StaleElementReferenceException $e) {
return true;
}
}
@@ -397,9 +396,7 @@ public static function invisibilityOfElementWithText(WebDriverBy $by, $text)
function (WebDriver $driver) use ($by, $text) {
try {
return !($driver->findElement($by)->getText() === $text);
- } catch (NoSuchElementException $e) {
- return true;
- } catch (StaleElementReferenceException $e) {
+ } catch (NoSuchElementException|StaleElementReferenceException $e) {
return true;
}
}
@@ -422,6 +419,7 @@ function (WebDriver $driver) use ($visibility_of_element_located) {
$visibility_of_element_located->getApply(),
$driver
);
+
try {
if ($element !== null && $element->isEnabled()) {
return $element;
@@ -525,7 +523,7 @@ function (WebDriver $driver) use ($element_or_by, $selected) {
);
}
- throw new \InvalidArgumentException('Instance of either WebDriverElement or WebDriverBy must be given');
+ throw LogicException::forError('Instance of either WebDriverElement or WebDriverBy must be given');
}
/**
@@ -545,7 +543,7 @@ function (WebDriver $driver) {
$alert->getText();
return $alert;
- } catch (NoAlertOpenException $e) {
+ } catch (NoSuchAlertException $e) {
return null;
}
}
diff --git a/lib/WebDriverKeys.php b/lib/WebDriverKeys.php
index 77f75b4ea..40f3ffbf5 100644
--- a/lib/WebDriverKeys.php
+++ b/lib/WebDriverKeys.php
@@ -9,92 +9,94 @@
*/
class WebDriverKeys
{
- const NULL = "\xEE\x80\x80";
- const CANCEL = "\xEE\x80\x81";
- const HELP = "\xEE\x80\x82";
- const BACKSPACE = "\xEE\x80\x83";
- const TAB = "\xEE\x80\x84";
- const CLEAR = "\xEE\x80\x85";
- const RETURN_KEY = "\xEE\x80\x86";
- const ENTER = "\xEE\x80\x87";
- const SHIFT = "\xEE\x80\x88";
- const CONTROL = "\xEE\x80\x89";
- const ALT = "\xEE\x80\x8A";
- const PAUSE = "\xEE\x80\x8B";
- const ESCAPE = "\xEE\x80\x8C";
- const SPACE = "\xEE\x80\x8D";
- const PAGE_UP = "\xEE\x80\x8E";
- const PAGE_DOWN = "\xEE\x80\x8F";
- const END = "\xEE\x80\x90";
- const HOME = "\xEE\x80\x91";
- const ARROW_LEFT = "\xEE\x80\x92";
- const ARROW_UP = "\xEE\x80\x93";
- const ARROW_RIGHT = "\xEE\x80\x94";
- const ARROW_DOWN = "\xEE\x80\x95";
- const INSERT = "\xEE\x80\x96";
- const DELETE = "\xEE\x80\x97";
- const SEMICOLON = "\xEE\x80\x98";
- const EQUALS = "\xEE\x80\x99";
- const NUMPAD0 = "\xEE\x80\x9A";
- const NUMPAD1 = "\xEE\x80\x9B";
- const NUMPAD2 = "\xEE\x80\x9C";
- const NUMPAD3 = "\xEE\x80\x9D";
- const NUMPAD4 = "\xEE\x80\x9E";
- const NUMPAD5 = "\xEE\x80\x9F";
- const NUMPAD6 = "\xEE\x80\xA0";
- const NUMPAD7 = "\xEE\x80\xA1";
- const NUMPAD8 = "\xEE\x80\xA2";
- const NUMPAD9 = "\xEE\x80\xA3";
- const MULTIPLY = "\xEE\x80\xA4";
- const ADD = "\xEE\x80\xA5";
- const SEPARATOR = "\xEE\x80\xA6";
- const SUBTRACT = "\xEE\x80\xA7";
- const DECIMAL = "\xEE\x80\xA8";
- const DIVIDE = "\xEE\x80\xA9";
- const F1 = "\xEE\x80\xB1";
- const F2 = "\xEE\x80\xB2";
- const F3 = "\xEE\x80\xB3";
- const F4 = "\xEE\x80\xB4";
- const F5 = "\xEE\x80\xB5";
- const F6 = "\xEE\x80\xB6";
- const F7 = "\xEE\x80\xB7";
- const F8 = "\xEE\x80\xB8";
- const F9 = "\xEE\x80\xB9";
- const F10 = "\xEE\x80\xBA";
- const F11 = "\xEE\x80\xBB";
- const F12 = "\xEE\x80\xBC";
- const META = "\xEE\x80\xBD";
- const ZENKAKU_HANKAKU = "\xEE\x80\xC0";
- const RIGHT_SHIFT = "\xEE\x81\x90";
- const RIGHT_CONTROL = "\xEE\x81\x91";
- const RIGHT_ALT = "\xEE\x81\x92";
- const RIGHT_META = "\xEE\x81\x93";
- const NUMPAD_PAGE_UP = "\xEE\x81\x94";
- const NUMPAD_PAGE_DOWN = "\xEE\x81\x95";
- const NUMPAD_END = "\xEE\x81\x96";
- const NUMPAD_HOME = "\xEE\x81\x97";
- const NUMPAD_ARROW_LEFT = "\xEE\x81\x98";
- const NUMPAD_ARROW_UP = "\xEE\x81\x99";
- const NUMPAD_ARROW_RIGHT = "\xEE\x81\x9A";
- const NUMPAD_ARROW_DOWN = "\xEE\x81\x9B";
- const NUMPAD_ARROW_INSERT = "\xEE\x81\x9C";
- const NUMPAD_ARROW_DELETE = "\xEE\x81\x9D";
+ public const NULL = "\xEE\x80\x80";
+ public const CANCEL = "\xEE\x80\x81";
+ public const HELP = "\xEE\x80\x82";
+ public const BACKSPACE = "\xEE\x80\x83";
+ public const TAB = "\xEE\x80\x84";
+ public const CLEAR = "\xEE\x80\x85";
+ public const RETURN_KEY = "\xEE\x80\x86";
+ public const ENTER = "\xEE\x80\x87";
+ public const SHIFT = "\xEE\x80\x88";
+ public const CONTROL = "\xEE\x80\x89";
+ public const ALT = "\xEE\x80\x8A";
+ public const PAUSE = "\xEE\x80\x8B";
+ public const ESCAPE = "\xEE\x80\x8C";
+ public const SPACE = "\xEE\x80\x8D";
+ public const PAGE_UP = "\xEE\x80\x8E";
+ public const PAGE_DOWN = "\xEE\x80\x8F";
+ public const END = "\xEE\x80\x90";
+ public const HOME = "\xEE\x80\x91";
+ public const ARROW_LEFT = "\xEE\x80\x92";
+ public const ARROW_UP = "\xEE\x80\x93";
+ public const ARROW_RIGHT = "\xEE\x80\x94";
+ public const ARROW_DOWN = "\xEE\x80\x95";
+ public const INSERT = "\xEE\x80\x96";
+ public const DELETE = "\xEE\x80\x97";
+ public const SEMICOLON = "\xEE\x80\x98";
+ public const EQUALS = "\xEE\x80\x99";
+ public const NUMPAD0 = "\xEE\x80\x9A";
+ public const NUMPAD1 = "\xEE\x80\x9B";
+ public const NUMPAD2 = "\xEE\x80\x9C";
+ public const NUMPAD3 = "\xEE\x80\x9D";
+ public const NUMPAD4 = "\xEE\x80\x9E";
+ public const NUMPAD5 = "\xEE\x80\x9F";
+ public const NUMPAD6 = "\xEE\x80\xA0";
+ public const NUMPAD7 = "\xEE\x80\xA1";
+ public const NUMPAD8 = "\xEE\x80\xA2";
+ public const NUMPAD9 = "\xEE\x80\xA3";
+ public const MULTIPLY = "\xEE\x80\xA4";
+ public const ADD = "\xEE\x80\xA5";
+ public const SEPARATOR = "\xEE\x80\xA6";
+ public const SUBTRACT = "\xEE\x80\xA7";
+ public const DECIMAL = "\xEE\x80\xA8";
+ public const DIVIDE = "\xEE\x80\xA9";
+ public const F1 = "\xEE\x80\xB1";
+ public const F2 = "\xEE\x80\xB2";
+ public const F3 = "\xEE\x80\xB3";
+ public const F4 = "\xEE\x80\xB4";
+ public const F5 = "\xEE\x80\xB5";
+ public const F6 = "\xEE\x80\xB6";
+ public const F7 = "\xEE\x80\xB7";
+ public const F8 = "\xEE\x80\xB8";
+ public const F9 = "\xEE\x80\xB9";
+ public const F10 = "\xEE\x80\xBA";
+ public const F11 = "\xEE\x80\xBB";
+ public const F12 = "\xEE\x80\xBC";
+ public const META = "\xEE\x80\xBD";
+ public const ZENKAKU_HANKAKU = "\xEE\x80\xC0";
+ public const RIGHT_SHIFT = "\xEE\x81\x90";
+ public const RIGHT_CONTROL = "\xEE\x81\x91";
+ public const RIGHT_ALT = "\xEE\x81\x92";
+ public const RIGHT_META = "\xEE\x81\x93";
+ public const NUMPAD_PAGE_UP = "\xEE\x81\x94";
+ public const NUMPAD_PAGE_DOWN = "\xEE\x81\x95";
+ public const NUMPAD_END = "\xEE\x81\x96";
+ public const NUMPAD_HOME = "\xEE\x81\x97";
+ public const NUMPAD_ARROW_LEFT = "\xEE\x81\x98";
+ public const NUMPAD_ARROW_UP = "\xEE\x81\x99";
+ public const NUMPAD_ARROW_RIGHT = "\xEE\x81\x9A";
+ public const NUMPAD_ARROW_DOWN = "\xEE\x81\x9B";
+ public const NUMPAD_ARROW_INSERT = "\xEE\x81\x9C";
+ public const NUMPAD_ARROW_DELETE = "\xEE\x81\x9D";
// Aliases
- const LEFT_SHIFT = self::SHIFT;
- const LEFT_CONTROL = self::CONTROL;
- const LEFT_ALT = self::ALT;
- const LEFT = self::ARROW_LEFT;
- const UP = self::ARROW_UP;
- const RIGHT = self::ARROW_RIGHT;
- const DOWN = self::ARROW_DOWN;
- const COMMAND = self::META;
+ public const LEFT_SHIFT = self::SHIFT;
+ public const LEFT_CONTROL = self::CONTROL;
+ public const LEFT_ALT = self::ALT;
+ public const LEFT = self::ARROW_LEFT;
+ public const UP = self::ARROW_UP;
+ public const RIGHT = self::ARROW_RIGHT;
+ public const DOWN = self::ARROW_DOWN;
+ public const COMMAND = self::META;
/**
- * Encode input of `sendKeys()`.
+ * Encode input of `sendKeys()` to appropriate format according to protocol.
+ *
* @param string|array|int|float $keys
- * @return array
+ * @param bool $isW3cCompliant
+ * @return array|string
*/
- public static function encode($keys)
+ public static function encode($keys, $isW3cCompliant = false)
{
if (is_numeric($keys)) {
$keys = (string) $keys;
@@ -105,7 +107,11 @@ public static function encode($keys)
}
if (!is_array($keys)) {
- return [];
+ if (!$isW3cCompliant) {
+ return [];
+ }
+
+ return '';
}
$encoded = [];
@@ -117,6 +123,10 @@ public static function encode($keys)
$encoded[] = (string) $key;
}
- return $encoded;
+ if (!$isW3cCompliant) {
+ return $encoded;
+ }
+
+ return implode('', $encoded);
}
}
diff --git a/lib/WebDriverMouse.php b/lib/WebDriverMouse.php
index 5178a314b..7c9362329 100644
--- a/lib/WebDriverMouse.php
+++ b/lib/WebDriverMouse.php
@@ -10,31 +10,26 @@
interface WebDriverMouse
{
/**
- * @param WebDriverCoordinates $where
* @return WebDriverMouse
*/
public function click(WebDriverCoordinates $where);
/**
- * @param WebDriverCoordinates $where
* @return WebDriverMouse
*/
public function contextClick(WebDriverCoordinates $where);
/**
- * @param WebDriverCoordinates $where
* @return WebDriverMouse
*/
public function doubleClick(WebDriverCoordinates $where);
/**
- * @param WebDriverCoordinates $where
* @return WebDriverMouse
*/
public function mouseDown(WebDriverCoordinates $where);
/**
- * @param WebDriverCoordinates $where
* @param int $x_offset
* @param int $y_offset
* @return WebDriverMouse
@@ -46,7 +41,6 @@ public function mouseMove(
);
/**
- * @param WebDriverCoordinates $where
* @return WebDriverMouse
*/
public function mouseUp(WebDriverCoordinates $where);
diff --git a/lib/WebDriverOptions.php b/lib/WebDriverOptions.php
index 84f2270eb..c84757fe8 100644
--- a/lib/WebDriverOptions.php
+++ b/lib/WebDriverOptions.php
@@ -2,10 +2,10 @@
namespace Facebook\WebDriver;
+use Facebook\WebDriver\Exception\Internal\LogicException;
use Facebook\WebDriver\Exception\NoSuchCookieException;
use Facebook\WebDriver\Remote\DriverCommand;
use Facebook\WebDriver\Remote\ExecuteMethod;
-use InvalidArgumentException;
/**
* Managing stuff you would do in a browser.
@@ -40,7 +40,7 @@ public function addCookie($cookie)
$cookie = Cookie::createFromArray($cookie);
}
if (!$cookie instanceof Cookie) {
- throw new InvalidArgumentException('Cookie must be set from instance of Cookie class or from array.');
+ throw LogicException::forError('Cookie must be set from instance of Cookie class or from array.');
}
$this->executor->execute(
diff --git a/lib/WebDriverPlatform.php b/lib/WebDriverPlatform.php
index 0b45800d5..a589f3013 100644
--- a/lib/WebDriverPlatform.php
+++ b/lib/WebDriverPlatform.php
@@ -9,15 +9,15 @@
*/
class WebDriverPlatform
{
- const ANDROID = 'ANDROID';
- /** @deprecated */
- const ANY = 'ANY';
- const LINUX = 'LINUX';
- const MAC = 'MAC';
- const UNIX = 'UNIX';
- const VISTA = 'VISTA';
- const WINDOWS = 'WINDOWS';
- const XP = 'XP';
+ public const ANDROID = 'ANDROID';
+ /** @deprecated ANY has no meaning in W3C WebDriver, see https://github.com/php-webdriver/php-webdriver/pull/731 */
+ public const ANY = 'ANY';
+ public const LINUX = 'LINUX';
+ public const MAC = 'MAC';
+ public const UNIX = 'UNIX';
+ public const VISTA = 'VISTA';
+ public const WINDOWS = 'WINDOWS';
+ public const XP = 'XP';
private function __construct()
{
diff --git a/lib/WebDriverRadios.php b/lib/WebDriverRadios.php
index e1687983e..aeaaaecac 100644
--- a/lib/WebDriverRadios.php
+++ b/lib/WebDriverRadios.php
@@ -2,8 +2,8 @@
namespace Facebook\WebDriver;
+use Facebook\WebDriver\Exception\InvalidElementStateException;
use Facebook\WebDriver\Exception\UnsupportedOperationException;
-use Facebook\WebDriver\Exception\WebDriverException;
/**
* Provides helper methods for radio buttons.
@@ -16,7 +16,7 @@ public function __construct(WebDriverElement $element)
$this->type = $element->getAttribute('type');
if ($this->type !== 'radio') {
- throw new WebDriverException('The input must be of type "radio".');
+ throw new InvalidElementStateException('The input must be of type "radio".');
}
}
diff --git a/lib/WebDriverSearchContext.php b/lib/WebDriverSearchContext.php
index 8a723d8e7..5fb1daaf9 100644
--- a/lib/WebDriverSearchContext.php
+++ b/lib/WebDriverSearchContext.php
@@ -2,19 +2,18 @@
namespace Facebook\WebDriver;
+use Facebook\WebDriver\Exception\NoSuchElementException;
+
/**
- * The interface for WebDriver and WebDriverElement which is able to search for
- * WebDriverElement inside.
+ * The interface for WebDriver and WebDriverElement which is able to search for WebDriverElement inside.
*/
interface WebDriverSearchContext
{
/**
- * Find the first WebDriverElement within this element using the given
- * mechanism.
+ * Find the first WebDriverElement within this element using the given mechanism.
*
- * @param WebDriverBy $locator
- * @return WebDriverElement NoSuchElementException is thrown in
- * HttpCommandExecutor if no element is found.
+ * @throws NoSuchElementException If no element is found
+ * @return WebDriverElement
* @see WebDriverBy
*/
public function findElement(WebDriverBy $locator);
@@ -22,9 +21,7 @@ public function findElement(WebDriverBy $locator);
/**
* Find all WebDriverElements within this element using the given mechanism.
*
- * @param WebDriverBy $locator
- * @return WebDriverElement[] A list of all WebDriverElements, or an empty array if
- * nothing matches
+ * @return WebDriverElement[] A list of all WebDriverElements, or an empty array if nothing matches
* @see WebDriverBy
*/
public function findElements(WebDriverBy $locator);
diff --git a/lib/WebDriverSelect.php b/lib/WebDriverSelect.php
index 53d894b96..85e1d26e4 100644
--- a/lib/WebDriverSelect.php
+++ b/lib/WebDriverSelect.php
@@ -26,7 +26,14 @@ public function __construct(WebDriverElement $element)
}
$this->element = $element;
$value = $element->getAttribute('multiple');
- $this->isMulti = $value === 'true';
+
+ /**
+ * There is a bug in safari webdriver that returns 'multiple' instead of 'true' which does not match the spec.
+ * Apple Feedback #FB12760673
+ *
+ * @see https://www.w3.org/TR/webdriver2/#get-element-attribute
+ */
+ $this->isMulti = $value === 'true' || $value === 'multiple';
}
public function isMultiple()
@@ -223,7 +230,6 @@ public function deselectByVisiblePartialText($text)
/**
* Mark option selected
- * @param WebDriverElement $option
*/
protected function selectOption(WebDriverElement $option)
{
@@ -234,7 +240,6 @@ protected function selectOption(WebDriverElement $option)
/**
* Mark option not selected
- * @param WebDriverElement $option
*/
protected function deselectOption(WebDriverElement $option)
{
diff --git a/lib/WebDriverTargetLocator.php b/lib/WebDriverTargetLocator.php
index 413e25b4b..8787f66c5 100644
--- a/lib/WebDriverTargetLocator.php
+++ b/lib/WebDriverTargetLocator.php
@@ -7,9 +7,14 @@
*/
interface WebDriverTargetLocator
{
+ /** @var string */
+ public const WINDOW_TYPE_WINDOW = 'window';
+ /** @var string */
+ public const WINDOW_TYPE_TAB = 'tab';
+
/**
- * Switch to the main document if the page contains iframes. Otherwise, switch
- * to the first frame on the page.
+ * Set the current browsing context to the current top-level browsing context.
+ * This is the same as calling `RemoteTargetLocator::frame(null);`
*
* @return WebDriver The driver focused on the top window or the first frame.
*/
@@ -18,17 +23,20 @@ public function defaultContent();
/**
* Switch to the iframe by its id or name.
*
- * @param WebDriverElement|string $frame The WebDriverElement,
- * the id or the name of the frame.
+ * @param WebDriverElement|null|int|string $frame The WebDriverElement, the id or the name of the frame.
+ * When null, switch to the current top-level browsing context When int, switch to the WindowProxy identified
+ * by the value. When an Element, switch to that Element.
+ *
+ * @throws \InvalidArgumentException
* @return WebDriver The driver focused on the given frame.
*/
public function frame($frame);
+ // TODO: Add in next major release (BC)
///**
// * Switch to the parent iframe.
// *
- // * @todo Add in next major release (BC)
- // * @return WebDriver The driver focused on the given frame.
+ // * @return WebDriver This driver focused on the parent frame
// */
//public function parent();
@@ -41,17 +49,19 @@ public function frame($frame);
*/
public function window($handle);
+ // TODO: Add in next major release (BC)
+ //public function newWindow($windowType = self::WINDOW_TYPE_TAB);
+
/**
- * Switch to the currently active modal dialog for this particular driver
- * instance.
+ * Switch to the currently active modal dialog for this particular driver instance.
*
* @return WebDriverAlert
*/
public function alert();
/**
- * Switches to the element that currently has focus within the document
- * currently "switched to", or the body element if this cannot be detected.
+ * Switches to the element that currently has focus within the document currently "switched to",
+ * or the body element if this cannot be detected.
*
* @return WebDriverElement
*/
diff --git a/lib/WebDriverTimeouts.php b/lib/WebDriverTimeouts.php
index add6a0900..73a5cddf8 100644
--- a/lib/WebDriverTimeouts.php
+++ b/lib/WebDriverTimeouts.php
@@ -28,7 +28,7 @@ public function __construct(ExecuteMethod $executor, $isW3cCompliant = false)
/**
* Specify the amount of time the driver should wait when searching for an element if it is not immediately present.
*
- * @param int $seconds Wait time in second.
+ * @param null|int|float $seconds Wait time in seconds.
* @return WebDriverTimeouts The current instance.
*/
public function implicitlyWait($seconds)
@@ -36,15 +36,18 @@ public function implicitlyWait($seconds)
if ($this->isW3cCompliant) {
$this->executor->execute(
DriverCommand::IMPLICITLY_WAIT,
- ['implicit' => $seconds * 1000]
+ ['implicit' => $seconds === null ? null : floor($seconds * 1000)]
);
return $this;
}
+ if ($seconds === null) {
+ throw new \InvalidArgumentException('JsonWire Protocol implicit-wait-timeout value cannot be null');
+ }
$this->executor->execute(
DriverCommand::IMPLICITLY_WAIT,
- ['ms' => $seconds * 1000]
+ ['ms' => floor($seconds * 1000)]
);
return $this;
@@ -53,7 +56,7 @@ public function implicitlyWait($seconds)
/**
* Set the amount of time to wait for an asynchronous script to finish execution before throwing an error.
*
- * @param int $seconds Wait time in second.
+ * @param null|int|float $seconds Wait time in seconds.
* @return WebDriverTimeouts The current instance.
*/
public function setScriptTimeout($seconds)
@@ -61,15 +64,18 @@ public function setScriptTimeout($seconds)
if ($this->isW3cCompliant) {
$this->executor->execute(
DriverCommand::SET_SCRIPT_TIMEOUT,
- ['script' => $seconds * 1000]
+ ['script' => $seconds === null ? null : floor($seconds * 1000)]
);
return $this;
}
+ if ($seconds === null) {
+ throw new \InvalidArgumentException('JsonWire Protocol script-timeout value cannot be null');
+ }
$this->executor->execute(
DriverCommand::SET_SCRIPT_TIMEOUT,
- ['ms' => $seconds * 1000]
+ ['ms' => floor($seconds * 1000)]
);
return $this;
@@ -78,7 +84,7 @@ public function setScriptTimeout($seconds)
/**
* Set the amount of time to wait for a page load to complete before throwing an error.
*
- * @param int $seconds Wait time in second.
+ * @param null|int|float $seconds Wait time in seconds.
* @return WebDriverTimeouts The current instance.
*/
public function pageLoadTimeout($seconds)
@@ -86,15 +92,18 @@ public function pageLoadTimeout($seconds)
if ($this->isW3cCompliant) {
$this->executor->execute(
DriverCommand::SET_SCRIPT_TIMEOUT,
- ['pageLoad' => $seconds * 1000]
+ ['pageLoad' => $seconds === null ? null : floor($seconds * 1000)]
);
return $this;
}
+ if ($seconds === null) {
+ throw new \InvalidArgumentException('JsonWire Protocol page-load-timeout value cannot be null');
+ }
$this->executor->execute(DriverCommand::SET_TIMEOUT, [
'type' => 'page load',
- 'ms' => $seconds * 1000,
+ 'ms' => floor($seconds * 1000),
]);
return $this;
diff --git a/lib/WebDriverUpAction.php b/lib/WebDriverUpAction.php
index 62d275dea..3b045df4b 100644
--- a/lib/WebDriverUpAction.php
+++ b/lib/WebDriverUpAction.php
@@ -11,7 +11,6 @@ class WebDriverUpAction extends WebDriverTouchAction implements WebDriverAction
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
* @param int $x
* @param int $y
*/
diff --git a/lib/WebDriverWait.php b/lib/WebDriverWait.php
index 34dd057ae..d2176b92a 100644
--- a/lib/WebDriverWait.php
+++ b/lib/WebDriverWait.php
@@ -28,7 +28,7 @@ class WebDriverWait
public function __construct(WebDriver $driver, $timeout_in_second = null, $interval_in_millisecond = null)
{
$this->driver = $driver;
- $this->timeout = isset($timeout_in_second) ? $timeout_in_second : 30;
+ $this->timeout = $timeout_in_second ?? 30;
$this->interval = $interval_in_millisecond ?: 250;
}
@@ -38,9 +38,9 @@ public function __construct(WebDriver $driver, $timeout_in_second = null, $inter
* @param callable|WebDriverExpectedCondition $func_or_ec
* @param string $message
*
+ * @throws \Exception
* @throws NoSuchElementException
* @throws TimeoutException
- * @throws \Exception
* @return mixed The return value of $func_or_ec
*/
public function until($func_or_ec, $message = '')
diff --git a/lib/WebDriverWindow.php b/lib/WebDriverWindow.php
index e212f92ed..2a69fe240 100644
--- a/lib/WebDriverWindow.php
+++ b/lib/WebDriverWindow.php
@@ -3,6 +3,7 @@
namespace Facebook\WebDriver;
use Facebook\WebDriver\Exception\IndexOutOfBoundsException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
use Facebook\WebDriver\Exception\UnsupportedOperationException;
use Facebook\WebDriver\Remote\DriverCommand;
use Facebook\WebDriver\Remote\ExecuteMethod;
@@ -120,7 +121,6 @@ public function fullscreen()
* Set the size of the current window. This will change the outer window
* dimension, not just the view port.
*
- * @param WebDriverDimension $size
* @return WebDriverWindow The instance.
*/
public function setSize(WebDriverDimension $size)
@@ -139,7 +139,6 @@ public function setSize(WebDriverDimension $size)
* Set the position of the current window. This is relative to the upper left
* corner of the screen.
*
- * @param WebDriverPoint $position
* @return WebDriverWindow The instance.
*/
public function setPosition(WebDriverPoint $position)
@@ -175,10 +174,8 @@ public function getScreenOrientation()
public function setScreenOrientation($orientation)
{
$orientation = mb_strtoupper($orientation);
- if (!in_array($orientation, ['PORTRAIT', 'LANDSCAPE'])) {
- throw new IndexOutOfBoundsException(
- 'Orientation must be either PORTRAIT, or LANDSCAPE'
- );
+ if (!in_array($orientation, ['PORTRAIT', 'LANDSCAPE'], true)) {
+ throw LogicException::forError('Orientation must be either PORTRAIT, or LANDSCAPE');
}
$this->executor->execute(
diff --git a/lib/scripts/isElementDisplayed.js b/lib/scripts/isElementDisplayed.js
new file mode 100644
index 000000000..f24bfa55e
--- /dev/null
+++ b/lib/scripts/isElementDisplayed.js
@@ -0,0 +1,219 @@
+/*
+ * Imported from WebdriverIO project.
+ * https://github.com/webdriverio/webdriverio/blob/main/packages/webdriverio/src/scripts/isElementDisplayed.ts
+ *
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * check if element is visible
+ * @param {HTMLElement} elem element to check
+ * @return {Boolean} true if element is within viewport
+ */
+function isElementDisplayed(element) {
+ function nodeIsElement(node) {
+ if (!node) {
+ return false;
+ }
+
+ switch (node.nodeType) {
+ case Node.ELEMENT_NODE:
+ case Node.DOCUMENT_NODE:
+ case Node.DOCUMENT_FRAGMENT_NODE:
+ return true;
+ default:
+ return false;
+ }
+ }
+ function parentElementForElement(element) {
+ if (!element) {
+ return null;
+ }
+ return enclosingNodeOrSelfMatchingPredicate(element.parentNode, nodeIsElement);
+ }
+ function enclosingNodeOrSelfMatchingPredicate(targetNode, predicate) {
+ for (let node = targetNode; node && node !== targetNode.ownerDocument; node = node.parentNode)
+ if (predicate(node)) {
+ return node;
+ }
+ return null;
+ }
+ function enclosingElementOrSelfMatchingPredicate(targetElement, predicate) {
+ for (let element = targetElement; element && element !== targetElement.ownerDocument; element = parentElementForElement(element))
+ if (predicate(element)) {
+ return element;
+ }
+ return null;
+ }
+ function cascadedStylePropertyForElement(element, property) {
+ if (!element || !property) {
+ return null;
+ }
+ // if document-fragment, skip it and use element.host instead. This happens
+ // when the element is inside a shadow root.
+ // window.getComputedStyle errors on document-fragment.
+ if (element instanceof ShadowRoot) {
+ element = element.host;
+ }
+ let computedStyle = window.getComputedStyle(element);
+ let computedStyleProperty = computedStyle.getPropertyValue(property);
+ if (computedStyleProperty && computedStyleProperty !== 'inherit') {
+ return computedStyleProperty;
+ }
+ // Ideally getPropertyValue would return the 'used' or 'actual' value, but
+ // it doesn't for legacy reasons. So we need to do our own poor man's cascade.
+ // Fall back to the first non-'inherit' value found in an ancestor.
+ // In any case, getPropertyValue will not return 'initial'.
+ // FIXME: will this incorrectly inherit non-inheritable CSS properties?
+ // I think all important non-inheritable properties (width, height, etc.)
+ // for our purposes here are specially resolved, so this may not be an issue.
+ // Specification is here: https://drafts.csswg.org/cssom/#resolved-values
+ let parentElement = parentElementForElement(element);
+ return cascadedStylePropertyForElement(parentElement, property);
+ }
+ function elementSubtreeHasNonZeroDimensions(element) {
+ let boundingBox = element.getBoundingClientRect();
+ if (boundingBox.width > 0 && boundingBox.height > 0) {
+ return true;
+ }
+ // Paths can have a zero width or height. Treat them as shown if the stroke width is positive.
+ if (element.tagName.toUpperCase() === 'PATH' && boundingBox.width + boundingBox.height > 0) {
+ let strokeWidth = cascadedStylePropertyForElement(element, 'stroke-width');
+ return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);
+ }
+ let cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow');
+ if (cascadedOverflow === 'hidden') {
+ return false;
+ }
+ // If the container's overflow is not hidden and it has zero size, consider the
+ // container to have non-zero dimensions if a child node has non-zero dimensions.
+ return Array.from(element.childNodes).some((childNode) => {
+ if (childNode.nodeType === Node.TEXT_NODE) {
+ return true;
+ }
+ if (nodeIsElement(childNode)) {
+ return elementSubtreeHasNonZeroDimensions(childNode);
+ }
+ return false;
+ });
+ }
+ function elementOverflowsContainer(element) {
+ let cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow');
+ if (cascadedOverflow !== 'hidden') {
+ return false;
+ }
+ // FIXME: this needs to take into account the scroll position of the element,
+ // the display modes of it and its ancestors, and the container it overflows.
+ // See Selenium's bot.dom.getOverflowState atom for an exhaustive list of edge cases.
+ return true;
+ }
+ function isElementSubtreeHiddenByOverflow(element) {
+ if (!element) {
+ return false;
+ }
+ if (!elementOverflowsContainer(element)) {
+ return false;
+ }
+ if (!element.childNodes.length) {
+ return false;
+ }
+ // This element's subtree is hidden by overflow if all child subtrees are as well.
+ return Array.from(element.childNodes).every((childNode) => {
+ // Returns true if the child node is overflowed or otherwise hidden.
+ // Base case: not an element, has zero size, scrolled out, or doesn't overflow container.
+ // Visibility of text nodes is controlled by parent
+ if (childNode.nodeType === Node.TEXT_NODE) {
+ return false;
+ }
+ if (!nodeIsElement(childNode)) {
+ return true;
+ }
+ if (!elementSubtreeHasNonZeroDimensions(childNode)) {
+ return true;
+ }
+ // Recurse.
+ return isElementSubtreeHiddenByOverflow(childNode);
+ });
+ }
+ // walk up the tree testing for a shadow root
+ function isElementInsideShadowRoot(element) {
+ if (!element) {
+ return false;
+ }
+ if (element.parentNode && element.parentNode.host) {
+ return true;
+ }
+ return isElementInsideShadowRoot(element.parentNode);
+ }
+ // This is a partial reimplementation of Selenium's "element is displayed" algorithm.
+ // When the W3C specification's algorithm stabilizes, we should implement that.
+ // If this command is misdirected to the wrong document (and is NOT inside a shadow root), treat it as not shown.
+ if (!isElementInsideShadowRoot(element) && !document.contains(element)) {
+ return false;
+ }
+ // Special cases for specific tag names.
+ switch (element.tagName.toUpperCase()) {
+ case 'BODY':
+ return true;
+ case 'SCRIPT':
+ case 'NOSCRIPT':
+ return false;
+ case 'OPTGROUP':
+ case 'OPTION': {
+ // Option/optgroup are considered shown if the containing is shown.
+ let enclosingSelectElement = enclosingNodeOrSelfMatchingPredicate(element, (e) => e.tagName.toUpperCase() === 'SELECT');
+ return isElementDisplayed(enclosingSelectElement);
+ }
+ case 'INPUT':
+ // is considered not shown.
+ if (element.type === 'hidden') {
+ return false;
+ }
+ break;
+ // case 'MAP':
+ // FIXME: Selenium has special handling for elements. We don't do anything now.
+ default:
+ break;
+ }
+ if (cascadedStylePropertyForElement(element, 'visibility') !== 'visible') {
+ return false;
+ }
+ let hasAncestorWithZeroOpacity = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {
+ return Number(cascadedStylePropertyForElement(e, 'opacity')) === 0;
+ });
+ let hasAncestorWithDisplayNone = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {
+ return cascadedStylePropertyForElement(e, 'display') === 'none';
+ });
+ if (hasAncestorWithZeroOpacity || hasAncestorWithDisplayNone) {
+ return false;
+ }
+ if (!elementSubtreeHasNonZeroDimensions(element)) {
+ return false;
+ }
+ if (isElementSubtreeHiddenByOverflow(element)) {
+ return false;
+ }
+ return true;
+}
+
diff --git a/mlc_config.json b/mlc_config.json
new file mode 100644
index 000000000..5d70a03e0
--- /dev/null
+++ b/mlc_config.json
@@ -0,0 +1,7 @@
+{
+ "ignorePatterns": [
+ {
+ "pattern": "^https://stackoverflow\\.com/questions/tagged/php\\+selenium-webdriver"
+ }
+ ]
+}
diff --git a/phpstan.neon b/phpstan.neon
index de6574f0b..b4c1e307d 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,13 +1,29 @@
parameters:
+ level: 2
+ paths:
+ - lib/
+ - tests/
+
ignoreErrors:
- - '#Class Symfony\\Component\\Process\\ProcessBuilder not found.#'
- - '#Instantiated class Symfony\\Component\\Process\\ProcessBuilder not found.#'
- - '#Call to method setPrefix\(\) on an unknown class Symfony\\Component\\Process\\ProcessBuilder#'
- # To be fixed:
- - '#Call to an undefined method RecursiveIteratorIterator::getSubPathName\(\)#'
- - '#Call to an undefined method Facebook\\WebDriver\\WebDriver::getTouch\(\)#'
- - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::getCoordinates\(\)#'
- - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::equals\(\)#'
+ # To be fixed in next major version:
+ - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriver::getTouch\(\)#'
+ path: 'lib/Interactions/WebDriverTouchActions.php'
+ - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriver::getTouch\(\)#'
+ path: 'lib/Support/Events/EventFiringWebDriver.php'
+ - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::getCoordinates\(\)#'
+ path: 'lib/Support/Events/EventFiringWebElement.php'
+ - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::equals\(\)#'
+ path: 'lib/Support/Events/EventFiringWebElement.php'
+ - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::takeElementScreenshot\(\)#'
+ path: 'lib/Support/Events/EventFiringWebElement.php'
+ - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::getShadowRoot\(\)#'
+ path: 'lib/Support/Events/EventFiringWebElement.php'
- '#Unsafe usage of new static\(\)#'
+ # Parameter is intentionally not part of signature to not break BC
+ - message: '#PHPDoc tag \@param references unknown parameter: \$isW3cCompliant#'
+ path: 'lib/Remote/RemoteWebDriver.php'
+ - message: '#PHPDoc tag \@param references unknown parameter: \$existingCapabilities#'
+ path: 'lib/Remote/RemoteWebDriver.php'
+
inferPrivatePropertyTypeFromConstructor: true
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 430a8fb5c..c58c72eaa 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,20 +1,9 @@
-
-
-
-
+
tests/unit
@@ -24,14 +13,13 @@
-
-
+
+
./lib
-
-
+
+
-
diff --git a/scripts/docs-template.html b/scripts/docs-template.html
new file mode 100644
index 000000000..4814f5ec5
--- /dev/null
+++ b/scripts/docs-template.html
@@ -0,0 +1,7 @@
+
+
+
+
+ Taking you to the latest documentation.
+
+
\ No newline at end of file
diff --git a/scripts/doctum.php b/scripts/doctum.php
new file mode 100644
index 000000000..08e722917
--- /dev/null
+++ b/scripts/doctum.php
@@ -0,0 +1,30 @@
+files()
+ ->name('*.php')
+ ->in($srcRoot . 'lib');
+
+$versions = GitVersionCollection::create($srcRoot)
+ ->addFromTags('1.*') // only latest minor version
+ ->addFromTags('0.6.0')
+ ->add('main', 'main branch')
+;
+
+return new Doctum($iterator, [
+ 'title' => 'php-webdriver API',
+ 'theme' => 'default',
+ 'build_dir' => $root . '/build/dist/%version%/',
+ 'cache_dir' => $root . '/build/cache/%version%/',
+ 'include_parent_data' => true,
+ 'remote_repository' => new GitHubRemoteRepository('php-webdriver/php-webdriver', $srcRoot),
+ 'versions' => $versions,
+ 'base_url' => 'https://php-webdriver.github.io/php-webdriver/%version%/'
+]);
diff --git a/scripts/update-built-docs.sh b/scripts/update-built-docs.sh
new file mode 100755
index 000000000..76646bed5
--- /dev/null
+++ b/scripts/update-built-docs.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+set -e
+
+cleanup() {
+ git ls-files ./ | xargs -r -n 1 rm
+ rm -rfd ./*
+}
+
+copyToTemp() {
+ TEMP_DIR="$(mktemp -d --suffix=_doctum-build-php-webdriver)"
+ cp -rp build/dist/* "${TEMP_DIR}"
+ cp ./scripts/docs-template.html "${TEMP_DIR}/index.html"
+}
+
+emptyAndRemoveTemp() {
+ mv "${TEMP_DIR}"/* ./
+ # Create symlink for main to latest
+ ln -s -r ./main ./latest
+ # Create symlink for main to master
+ ln -s -r ./main ./master
+ # Create symlink for main to community
+ ln -s -r ./main ./community
+ rm -rf "${TEMP_DIR}"
+}
+
+commitAndPushChanges() {
+ # Push the changes, only if there is changes
+ git add -A
+ git diff-index --quiet HEAD || git commit -m "Api documentations update ($(date --rfc-3339=seconds --utc))" -m "#apidocs" && if [ -z "${SKIP_PUSH}" ]; then git push; fi
+}
+
+if [ ! -d ./build/dist ]; then
+ echo 'Missing built docs'
+ exit 1
+fi
+
+# Remove cache dir, do not upload it
+rm -rf ./build/cache
+
+copyToTemp
+# Remove build dir, do not upload it
+rm -rf ./build
+
+git checkout gh-pages
+
+cleanup
+emptyAndRemoveTemp
+commitAndPushChanges
+
+git checkout - > /dev/null
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 6c8c4f51b..d9cb0c2c7 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,3 +1,3 @@
-desiredCapabilities->getBrowserName() !== WebDriverBrowserType::CHROME) {
+ $this->markTestSkipped('ChromeDevTools are available only in Chrome');
+ }
+ }
+
+ public function testShouldExecuteDevToolsCommandWithoutParameters(): void
+ {
+ $devTools = new ChromeDevToolsDriver($this->driver);
+
+ $result = $devTools->execute('Performance.enable');
+
+ $this->assertSame([], $result);
+ }
+
+ public function testShouldExecuteDevToolsCommandWithParameters(): void
+ {
+ $devTools = new ChromeDevToolsDriver($this->driver);
+
+ $result = $devTools->execute('Runtime.evaluate', [
+ 'returnByValue' => true,
+ 'expression' => '42 + 1',
+ ]);
+
+ $this->assertSame('number', $result['result']['type']);
+ $this->assertSame(43, $result['result']['value']);
+ }
+}
diff --git a/tests/functional/Chrome/ChromeDriverServiceTest.php b/tests/functional/Chrome/ChromeDriverServiceTest.php
index a21727781..9b6c46336 100644
--- a/tests/functional/Chrome/ChromeDriverServiceTest.php
+++ b/tests/functional/Chrome/ChromeDriverServiceTest.php
@@ -1,7 +1,8 @@
-markTestSkipped('ChromeDriverServiceTest is run only when running against local chrome');
+ if (getenv('BROWSER_NAME') !== 'chrome' || empty(getenv('CHROMEDRIVER_PATH'))
+ || WebDriverTestCase::isSauceLabsBuild()) {
+ $this->markTestSkipped('The test is run only when running against local chrome');
}
}
- public function testShouldStartAndStopServiceCreatedUsingShortcutConstructor()
+ protected function tearDown(): void
+ {
+ if ($this->driverService !== null && $this->driverService->isRunning()) {
+ $this->driverService->stop();
+ }
+ }
+
+ public function testShouldStartAndStopServiceCreatedUsingShortcutConstructor(): void
{
// The createDefaultService() method expect path to the executable to be present in the environment variable
putenv(ChromeDriverService::CHROME_DRIVER_EXECUTABLE . '=' . getenv('CHROMEDRIVER_PATH'));
- $driverService = ChromeDriverService::createDefaultService();
+ $this->driverService = ChromeDriverService::createDefaultService();
- $this->assertSame('http://localhost:9515/', $driverService->getURL());
+ $this->assertSame('http://localhost:9515/', $this->driverService->getURL());
- $this->assertInstanceOf(ChromeDriverService::class, $driverService->start());
- $this->assertTrue($driverService->isRunning());
+ $this->assertInstanceOf(ChromeDriverService::class, $this->driverService->start());
+ $this->assertTrue($this->driverService->isRunning());
- $this->assertInstanceOf(ChromeDriverService::class, $driverService->start());
+ $this->assertInstanceOf(ChromeDriverService::class, $this->driverService->start());
- $this->assertInstanceOf(ChromeDriverService::class, $driverService->stop());
- $this->assertFalse($driverService->isRunning());
+ $this->assertInstanceOf(ChromeDriverService::class, $this->driverService->stop());
+ $this->assertFalse($this->driverService->isRunning());
- $this->assertInstanceOf(ChromeDriverService::class, $driverService->stop());
+ $this->assertInstanceOf(ChromeDriverService::class, $this->driverService->stop());
}
- public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor()
+ public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor(): void
{
- $driverService = new ChromeDriverService(getenv('CHROMEDRIVER_PATH'), 9515, ['--port=9515']);
+ $this->driverService = new ChromeDriverService(getenv('CHROMEDRIVER_PATH'), 9515, ['--port=9515']);
- $this->assertSame('http://localhost:9515/', $driverService->getURL());
+ $this->assertSame('http://localhost:9515/', $this->driverService->getURL());
- $driverService->start();
- $this->assertTrue($driverService->isRunning());
+ $this->driverService->start();
+ $this->assertTrue($this->driverService->isRunning());
- $driverService->stop();
- $this->assertFalse($driverService->isRunning());
+ $this->driverService->stop();
+ $this->assertFalse($this->driverService->isRunning());
}
- public function testShouldThrowExceptionIfExecutableCannotBeFound()
+ public function testShouldThrowExceptionIfExecutableIsNotExecutable(): void
{
- putenv(ChromeDriverService::CHROME_DRIVER_EXECUTABLE . '=/not/existing');
+ putenv(ChromeDriverService::CHROME_DRIVER_EXECUTABLE . '=' . __FILE__);
$this->expectException(\Exception::class);
- $this->expectExceptionMessage('\'/not/existing\' is not a file.');
+ $this->expectExceptionMessage('is not executable');
ChromeDriverService::createDefaultService();
}
- public function testShouldThrowExceptionIfExecutableIsNotExecutable()
+ public function testShouldUseDefaultExecutableIfNoneProvided(): void
{
- putenv(ChromeDriverService::CHROME_DRIVER_EXECUTABLE . '=' . __FILE__);
+ // Put path where ChromeDriver binary is actually located to system PATH, to make sure we can locate it
+ putenv('PATH=' . getenv('PATH') . ':' . dirname(getenv('CHROMEDRIVER_PATH')));
- $this->expectException(\Exception::class);
- $this->expectExceptionMessage('is not executable');
- ChromeDriverService::createDefaultService();
+ // Unset CHROME_DRIVER_EXECUTABLE so that ChromeDriverService will attempt to run the binary from system PATH
+ putenv(ChromeDriverService::CHROME_DRIVER_EXECUTABLE . '=');
+
+ $this->driverService = ChromeDriverService::createDefaultService();
+
+ $this->assertSame('http://localhost:9515/', $this->driverService->getURL());
+
+ $this->assertInstanceOf(ChromeDriverService::class, $this->driverService->start());
+ $this->assertTrue($this->driverService->isRunning());
}
}
diff --git a/tests/functional/Chrome/ChromeDriverTest.php b/tests/functional/Chrome/ChromeDriverTest.php
index 26c9de81d..407b65bd8 100644
--- a/tests/functional/Chrome/ChromeDriverTest.php
+++ b/tests/functional/Chrome/ChromeDriverTest.php
@@ -1,55 +1,97 @@
-markTestSkipped('ChromeDriverServiceTest is run only when running against local chrome');
+ if (getenv('BROWSER_NAME') !== 'chrome' || empty(getenv('CHROMEDRIVER_PATH'))
+ || WebDriverTestCase::isSauceLabsBuild()) {
+ $this->markTestSkipped('The test is run only when running against local chrome');
}
}
- protected function tearDown()
+ protected function tearDown(): void
{
if ($this->driver instanceof RemoteWebDriver && $this->driver->getCommandExecutor() !== null) {
$this->driver->quit();
}
}
- public function testShouldStartChromeDriver()
+ /**
+ * @dataProvider provideDialect
+ */
+ public function testShouldStartChromeDriver(bool $isW3cDialect): void
+ {
+ $this->startChromeDriver($isW3cDialect);
+ $this->assertInstanceOf(ChromeDriver::class, $this->driver);
+ $this->assertInstanceOf(DriverCommandExecutor::class, $this->driver->getCommandExecutor());
+
+ // Make sure actual browser capabilities were set
+ $this->assertNotEmpty($this->driver->getCapabilities()->getVersion());
+ $this->assertNotEmpty($this->driver->getCapabilities()->getCapability('goog:chromeOptions'));
+
+ $this->driver->get('http://localhost:8000/');
+
+ $this->assertSame('http://localhost:8000/', $this->driver->getCurrentURL());
+ }
+
+ /**
+ * @return array[]
+ */
+ public function provideDialect(): array
+ {
+ return [
+ 'w3c' => [true],
+ 'oss' => [false],
+ ];
+ }
+
+ public function testShouldInstantiateDevTools(): void
+ {
+ $this->startChromeDriver();
+
+ $devTools = $this->driver->getDevTools();
+
+ $this->assertInstanceOf(ChromeDevToolsDriver::class, $devTools);
+
+ $this->driver->get('http://localhost:8000/');
+
+ $cdpResult = $devTools->execute(
+ 'Runtime.evaluate',
+ ['expression' => 'window.location.toString()']
+ );
+
+ $this->assertSame(['result' => ['type' => 'string', 'value' => 'http://localhost:8000/']], $cdpResult);
+ }
+
+ private function startChromeDriver($w3cDialect = true): void
{
// The createDefaultService() method expect path to the executable to be present in the environment variable
putenv(ChromeDriverService::CHROME_DRIVER_EXECUTABLE . '=' . getenv('CHROMEDRIVER_PATH'));
// Add --no-sandbox as a workaround for Chrome crashing: https://github.com/SeleniumHQ/selenium/issues/4961
$chromeOptions = new ChromeOptions();
- $chromeOptions->addArguments(['--no-sandbox', '--headless']);
+ $chromeOptions->addArguments(['--no-sandbox', '--headless', '--disable-search-engine-choice-screen']);
+ $chromeOptions->setExperimentalOption('w3c', $w3cDialect);
$desiredCapabilities = DesiredCapabilities::chrome();
$desiredCapabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions);
$this->driver = ChromeDriver::start($desiredCapabilities);
-
- $this->assertInstanceOf(ChromeDriver::class, $this->driver);
- $this->assertInstanceOf(DriverCommandExecutor::class, $this->driver->getCommandExecutor());
-
- $this->driver->get('http://localhost:8000/');
-
- $this->assertSame('http://localhost:8000/', $this->driver->getCurrentURL());
-
- $this->driver->quit();
}
}
diff --git a/tests/functional/FileUploadTest.php b/tests/functional/FileUploadTest.php
index f4728be41..e812353ea 100644
--- a/tests/functional/FileUploadTest.php
+++ b/tests/functional/FileUploadTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('upload.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::UPLOAD));
$fileElement = $this->driver->findElement(WebDriverBy::name('upload'));
@@ -43,7 +45,7 @@ public function testShouldUploadAFile()
$this->assertSame('10', $uploadedFileSize);
}
- private function getTestFilePath()
+ private function getTestFilePath(): string
{
return __DIR__ . '/Fixtures/FileUploadTestFile.txt';
}
diff --git a/tests/functional/Firefox/FirefoxDriverServiceTest.php b/tests/functional/Firefox/FirefoxDriverServiceTest.php
new file mode 100644
index 000000000..feabe0dbf
--- /dev/null
+++ b/tests/functional/Firefox/FirefoxDriverServiceTest.php
@@ -0,0 +1,83 @@
+markTestSkipped('The test is run only when running against local firefox');
+ }
+ }
+
+ protected function tearDown(): void
+ {
+ if ($this->driverService !== null && $this->driverService->isRunning()) {
+ $this->driverService->stop();
+ }
+ }
+
+ public function testShouldStartAndStopServiceCreatedUsingShortcutConstructor(): void
+ {
+ // The createDefaultService() method expect path to the executable to be present in the environment variable
+ putenv(FirefoxDriverService::WEBDRIVER_FIREFOX_DRIVER . '=' . getenv('GECKODRIVER_PATH'));
+
+ $this->driverService = FirefoxDriverService::createDefaultService();
+
+ $this->assertSame('http://localhost:9515/', $this->driverService->getURL());
+
+ $this->assertInstanceOf(FirefoxDriverService::class, $this->driverService->start());
+ $this->assertTrue($this->driverService->isRunning());
+
+ $this->assertInstanceOf(FirefoxDriverService::class, $this->driverService->start());
+
+ $this->assertInstanceOf(FirefoxDriverService::class, $this->driverService->stop());
+ $this->assertFalse($this->driverService->isRunning());
+
+ $this->assertInstanceOf(FirefoxDriverService::class, $this->driverService->stop());
+ }
+
+ public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor(): void
+ {
+ $this->driverService = new FirefoxDriverService(getenv('GECKODRIVER_PATH'), 9515, ['-p=9515']);
+
+ $this->assertSame('http://localhost:9515/', $this->driverService->getURL());
+
+ $this->driverService->start();
+ $this->assertTrue($this->driverService->isRunning());
+
+ $this->driverService->stop();
+ $this->assertFalse($this->driverService->isRunning());
+ }
+
+ public function testShouldUseDefaultExecutableIfNoneProvided(): void
+ {
+ // Put path where geckodriver binary is actually located to system PATH, to make sure we can locate it
+ putenv('PATH=' . getenv('PATH') . ':' . dirname(getenv('GECKODRIVER_PATH')));
+
+ // Unset WEBDRIVER_FIREFOX_BINARY so that FirefoxDriverService will attempt to run the binary from system PATH
+ putenv(FirefoxDriverService::WEBDRIVER_FIREFOX_DRIVER . '=');
+
+ $this->driverService = FirefoxDriverService::createDefaultService();
+
+ $this->assertSame('http://localhost:9515/', $this->driverService->getURL());
+
+ $this->assertInstanceOf(FirefoxDriverService::class, $this->driverService->start());
+ $this->assertTrue($this->driverService->isRunning());
+ }
+}
diff --git a/tests/functional/Firefox/FirefoxDriverTest.php b/tests/functional/Firefox/FirefoxDriverTest.php
new file mode 100644
index 000000000..900746d2f
--- /dev/null
+++ b/tests/functional/Firefox/FirefoxDriverTest.php
@@ -0,0 +1,86 @@
+markTestSkipped('The test is run only when running against local firefox');
+ }
+ }
+
+ protected function tearDown(): void
+ {
+ if ($this->driver instanceof RemoteWebDriver && $this->driver->getCommandExecutor() !== null) {
+ $this->driver->quit();
+ }
+ }
+
+ public function testShouldStartFirefoxDriver(): void
+ {
+ $this->startFirefoxDriver();
+ $this->assertInstanceOf(FirefoxDriver::class, $this->driver);
+ $this->assertInstanceOf(DriverCommandExecutor::class, $this->driver->getCommandExecutor());
+
+ // Make sure actual browser capabilities were set
+ $this->assertNotEmpty($this->driver->getCapabilities()->getVersion());
+ $this->assertNotEmpty($this->driver->getCapabilities()->getCapability('moz:profile'));
+ $this->assertTrue($this->driver->getCapabilities()->getCapability('moz:headless'));
+
+ // Ensure browser is responding to basic command
+ $this->driver->get('http://localhost:8000/');
+ $this->assertSame('http://localhost:8000/', $this->driver->getCurrentURL());
+ }
+
+ public function testShouldSetPreferenceWithFirefoxOptions(): void
+ {
+ $firefoxOptions = new FirefoxOptions();
+ $firefoxOptions->setPreference('javascript.enabled', false);
+
+ $this->startFirefoxDriver($firefoxOptions);
+
+ $this->driver->get('http://localhost:8000/');
+
+ $noScriptElement = $this->driver->findElement(WebDriverBy::id('noscript'));
+ $this->assertEquals(
+ 'This element is only shown with JavaScript disabled.',
+ $noScriptElement->getText()
+ );
+ }
+
+ private function startFirefoxDriver(?FirefoxOptions $firefoxOptions = null): void
+ {
+ // The createDefaultService() method expect path to the executable to be present in the environment variable
+ putenv(FirefoxDriverService::WEBDRIVER_FIREFOX_DRIVER . '=' . getenv('GECKODRIVER_PATH'));
+
+ if ($firefoxOptions === null) {
+ $firefoxOptions = new FirefoxOptions();
+ }
+ $firefoxOptions->addArguments(['-headless']);
+ $desiredCapabilities = DesiredCapabilities::firefox();
+ $desiredCapabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions);
+
+ $this->driver = FirefoxDriver::start($desiredCapabilities);
+ }
+}
diff --git a/tests/functional/Firefox/FirefoxProfileTest.php b/tests/functional/Firefox/FirefoxProfileTest.php
new file mode 100644
index 000000000..a8ff2c62d
--- /dev/null
+++ b/tests/functional/Firefox/FirefoxProfileTest.php
@@ -0,0 +1,128 @@
+` element
+ * with some text at the end of each page Firefox renders.
+ *
+ * In case the extension will need to be modified, steps below must be followed,
+ * otherwise firefox won't load the modified extension:
+ *
+ * - Extract the xpi file (it is a zip archive) to some temporary directory
+ * - Make needed changes in the files
+ * - Install web-ext tool from Mozilla (@see https://github.com/mozilla/web-ext)
+ * - Sign in to https://addons.mozilla.org/cs/developers/addon/api/key/ to get your JWT API key and JWT secret
+ * - Run `web-ext sign --channel=unlisted --api-key=[you-api-key] --api-secret=[your-api-secret]` in the extension dir
+ * - Store the output file (`web-ext-artifacts/[...].xpi`) to the Fixtures/ directory
+ *
+ * @group exclude-saucelabs
+ * @covers \Facebook\WebDriver\Firefox\FirefoxProfile
+ */
+class FirefoxProfileTest extends TestCase
+{
+ /** @var FirefoxDriver */
+ protected $driver;
+
+ protected $firefoxTestExtensionFilename = __DIR__ . '/Fixtures/FirefoxExtension.xpi';
+
+ protected function setUp(): void
+ {
+ if (getenv('BROWSER_NAME') !== 'firefox' || empty(getenv('GECKODRIVER_PATH'))
+ || WebDriverTestCase::isSauceLabsBuild()) {
+ $this->markTestSkipped('The test is run only when running against local firefox');
+ }
+ }
+
+ protected function tearDown(): void
+ {
+ if ($this->driver instanceof RemoteWebDriver && $this->driver->getCommandExecutor() !== null) {
+ $this->driver->quit();
+ }
+ }
+
+ public function testShouldStartDriverWithEmptyProfile(): void
+ {
+ $firefoxProfile = new FirefoxProfile();
+ $this->startFirefoxDriverWithProfile($firefoxProfile);
+
+ $this->driver->get('http://localhost:8000/');
+ $element = $this->driver->findElement(WebDriverBy::id('welcome'));
+ $this->assertSame(
+ 'Welcome to the php-webdriver testing page.',
+ $element->getText()
+ );
+ }
+
+ public function testShouldInstallExtension(): void
+ {
+ $firefoxProfile = new FirefoxProfile();
+ $firefoxProfile->addExtension($this->firefoxTestExtensionFilename);
+ $this->startFirefoxDriverWithProfile($firefoxProfile);
+
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
+
+ $this->assertInstanceOf(RemoteWebDriver::class, $this->driver);
+
+ // it sometimes takes split of a second for the extension to render the element, so we must use wait
+ $element = $this->driver->wait(5, 1)->until(
+ WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::id('webDriverExtensionTest'))
+ );
+
+ $this->assertEquals('This element was added by browser extension', $element->getText());
+ }
+
+ public function testShouldUseProfilePreferences(): void
+ {
+ $firefoxProfile = new FirefoxProfile();
+
+ // Please note, although it is possible to set preferences right into the profile (what this test does),
+ // we recommend using the setPreference() method on FirefoxOptions instead, so that you don't need to
+ // create FirefoxProfile.
+ $firefoxProfile->setPreference('javascript.enabled', false);
+ $this->assertSame('false', $firefoxProfile->getPreference('javascript.enabled'));
+
+ $this->startFirefoxDriverWithProfile($firefoxProfile);
+ $this->driver->get('http://localhost:8000/');
+
+ $noScriptElement = $this->driver->findElement(WebDriverBy::id('noscript'));
+ $this->assertEquals(
+ 'This element is only shown with JavaScript disabled.',
+ $noScriptElement->getText()
+ );
+ }
+
+ protected function getTestPageUrl($path): string
+ {
+ $host = 'http://localhost:8000/';
+ if ($alternateHost = getenv('FIXTURES_HOST')) {
+ $host = $alternateHost;
+ }
+
+ return $host . '/' . $path;
+ }
+
+ private function startFirefoxDriverWithProfile(FirefoxProfile $firefoxProfile): void
+ {
+ // The createDefaultService() method expect path to the executable to be present in the environment variable
+ putenv(FirefoxDriverService::WEBDRIVER_FIREFOX_DRIVER . '=' . getenv('GECKODRIVER_PATH'));
+
+ $firefoxOptions = new FirefoxOptions();
+ $firefoxOptions->addArguments(['-headless']);
+ $firefoxOptions->setProfile($firefoxProfile);
+ $desiredCapabilities = DesiredCapabilities::firefox();
+ $desiredCapabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions);
+
+ $this->driver = FirefoxDriver::start($desiredCapabilities);
+ }
+}
diff --git a/tests/functional/Firefox/Fixtures/FirefoxExtension.xpi b/tests/functional/Firefox/Fixtures/FirefoxExtension.xpi
new file mode 100644
index 000000000..c5805fbeb
Binary files /dev/null and b/tests/functional/Firefox/Fixtures/FirefoxExtension.xpi differ
diff --git a/tests/functional/Remote/JsonWireCompatTest.php b/tests/functional/Remote/JsonWireCompatTest.php
new file mode 100644
index 000000000..8dfb5e29a
--- /dev/null
+++ b/tests/functional/Remote/JsonWireCompatTest.php
@@ -0,0 +1,17 @@
+expectException(UnexpectedResponseException::class);
+ $this->expectExceptionMessage('Unexpected server response for getting an element. Expected array');
+
+ JsonWireCompat::getElement(null);
+ }
+}
diff --git a/tests/functional/RemoteKeyboardTest.php b/tests/functional/RemoteKeyboardTest.php
index 3da1341c0..897d33cf7 100644
--- a/tests/functional/RemoteKeyboardTest.php
+++ b/tests/functional/RemoteKeyboardTest.php
@@ -1,29 +1,27 @@
-desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) {
- $this->markTestSkipped('Not peorperly supported by HtmlUnit browser');
- }
-
- $this->driver->get($this->getTestPageUrl('events.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::EVENTS));
$this->driver->getKeyboard()->sendKeys('ab');
$this->driver->getKeyboard()->pressKey(WebDriverKeys::SHIFT);
@@ -55,7 +53,7 @@ public function testShouldPressSendAndReleaseKeys()
'keyup "Shift"',
'keyup "f"',
],
- $this->retrieveLoggedEvents()
+ $this->retrieveLoggedKeyboardEvents()
);
} else {
$this->assertEquals(
@@ -79,18 +77,8 @@ public function testShouldPressSendAndReleaseKeys()
'keydown "f"',
'keyup "f"',
],
- $this->retrieveLoggedEvents()
+ $this->retrieveLoggedKeyboardEvents()
);
}
}
-
- /**
- * @return array
- */
- private function retrieveLoggedEvents()
- {
- $logElement = $this->driver->findElement(WebDriverBy::id('keyboardEventsLog'));
-
- return explode("\n", $logElement->getText());
- }
}
diff --git a/tests/functional/RemoteTargetLocatorTest.php b/tests/functional/RemoteTargetLocatorTest.php
index e47612173..a3080e67a 100644
--- a/tests/functional/RemoteTargetLocatorTest.php
+++ b/tests/functional/RemoteTargetLocatorTest.php
@@ -1,7 +1,8 @@
-driver->get($this->getTestPageUrl('open_new_window.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::OPEN_NEW_WINDOW));
$originalWindowHandle = $this->driver->getWindowHandle();
$windowHandlesBefore = $this->driver->getWindowHandles();
@@ -23,7 +24,7 @@ public function testShouldSwitchToWindow()
);
// At first the window should not be switched
- $this->assertContains('open_new_window.html', $this->driver->getCurrentURL());
+ $this->assertStringContainsString('open_new_window.html', $this->driver->getCurrentURL());
$this->assertSame($originalWindowHandle, $this->driver->getWindowHandle());
/**
@@ -37,14 +38,19 @@ public function testShouldSwitchToWindow()
$this->driver->switchTo()->window($newWindowHandle);
+ $this->driver->wait()->until(function () {
+ // The window contents is sometimes not yet loaded and needs a while to actually show the index.html page
+ return mb_strpos($this->driver->getCurrentURL(), 'index.html') !== false;
+ });
+
// After switchTo() is called, the active window should be changed
- $this->assertContains('index.html', $this->driver->getCurrentURL());
+ $this->assertStringContainsString('index.html', $this->driver->getCurrentURL());
$this->assertNotSame($originalWindowHandle, $this->driver->getWindowHandle());
}
- public function testActiveElement()
+ public function testActiveElement(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$activeElement = $this->driver->switchTo()->activeElement();
$this->assertInstanceOf(RemoteWebElement::class, $activeElement);
@@ -56,65 +62,104 @@ public function testActiveElement()
$this->assertSame('test_name', $activeElement->getAttribute('name'));
}
- public function testShouldSwitchToFrameByItsId()
+ public function testShouldSwitchToFrameByItsId(): void
{
$parentPage = 'This is the host page which contains an iFrame';
$firstChildFrame = 'This is the content of the iFrame';
$secondChildFrame = 'open new window';
- $this->driver->get($this->getTestPageUrl('page_with_frame.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::PAGE_WITH_FRAME));
- $this->assertContains($parentPage, $this->driver->getPageSource());
+ $this->assertStringContainsString($parentPage, $this->driver->getPageSource());
$this->driver->switchTo()->frame(0);
- $this->assertContains($firstChildFrame, $this->driver->getPageSource());
+ $this->assertStringContainsString($firstChildFrame, $this->driver->getPageSource());
$this->driver->switchTo()->frame(null);
- $this->assertContains($parentPage, $this->driver->getPageSource());
+ $this->assertStringContainsString($parentPage, $this->driver->getPageSource());
$this->driver->switchTo()->frame(1);
- $this->assertContains($secondChildFrame, $this->driver->getPageSource());
+ $this->assertStringContainsString($secondChildFrame, $this->driver->getPageSource());
$this->driver->switchTo()->frame(null);
- $this->assertContains($parentPage, $this->driver->getPageSource());
+ $this->assertStringContainsString($parentPage, $this->driver->getPageSource());
+
+ $this->driver->switchTo()->frame(0);
+ $this->assertStringContainsString($firstChildFrame, $this->driver->getPageSource());
+
+ $this->driver->switchTo()->defaultContent();
+ $this->assertStringContainsString($parentPage, $this->driver->getPageSource());
}
- public function testShouldSwitchToParentFrame()
+ public function testShouldSwitchToParentFrame(): void
{
$parentPage = 'This is the host page which contains an iFrame';
$firstChildFrame = 'This is the content of the iFrame';
- $this->driver->get($this->getTestPageUrl('page_with_frame.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::PAGE_WITH_FRAME));
- $this->assertContains($parentPage, $this->driver->getPageSource());
+ $this->assertStringContainsString($parentPage, $this->driver->getPageSource());
$this->driver->switchTo()->frame(0);
- $this->assertContains($firstChildFrame, $this->driver->getPageSource());
+ $this->assertStringContainsString($firstChildFrame, $this->driver->getPageSource());
$this->driver->switchTo()->parent();
- $this->assertContains($parentPage, $this->driver->getPageSource());
+ $this->assertStringContainsString($parentPage, $this->driver->getPageSource());
}
- public function testShouldSwitchToFrameByElement()
+ public function testShouldSwitchToFrameByElement(): void
{
- $this->driver->get($this->getTestPageUrl('page_with_frame.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::PAGE_WITH_FRAME));
$element = $this->driver->findElement(WebDriverBy::id('iframe_content'));
$this->driver->switchTo()->frame($element);
- $this->assertContains('This is the content of the iFrame', $this->driver->getPageSource());
+ $this->assertStringContainsString('This is the content of the iFrame', $this->driver->getPageSource());
+ }
+
+ /**
+ * @group exclude-saucelabs
+ */
+ public function testShouldCreateNewWindow(): void
+ {
+ self::skipForJsonWireProtocol('Create new window is not supported in JsonWire protocol');
+
+ // Ensure that the initial context matches.
+ $initialHandle = $this->driver->getWindowHandle();
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
+ $this->assertEquals($this->getTestPageUrl(TestPage::INDEX), $this->driver->getCurrentUrl());
+ $source = $this->driver->getPageSource();
+ $this->assertStringContainsString('', $source);
+ $this->assertStringContainsString('Welcome to the php-webdriver testing page.', $source);
+ $windowHandles = $this->driver->getWindowHandles();
+ $this->assertCount(1, $windowHandles);
+
+ // Create a new window
+ $this->driver->switchTo()->newWindow();
+
+ $windowHandles = $this->driver->getWindowHandles();
+ $this->assertCount(2, $windowHandles);
+
+ $newWindowHandle = $this->driver->getWindowHandle();
+ $this->driver->get($this->getTestPageUrl(TestPage::UPLOAD));
+ $this->assertEquals($this->getTestPageUrl(TestPage::UPLOAD), $this->driver->getCurrentUrl());
+ $this->assertNotEquals($initialHandle, $newWindowHandle);
+
+ // Switch back to original context.
+ $this->driver->switchTo()->window($initialHandle);
+ $this->assertEquals($this->getTestPageUrl(TestPage::INDEX), $this->driver->getCurrentUrl());
}
/**
* @group exclude-saucelabs
*/
- public function testShouldNotAcceptStringAsFrameIdInW3cMode()
+ public function testShouldNotAcceptStringAsFrameIdInW3cMode(): void
{
self::skipForJsonWireProtocol();
- $this->driver->get($this->getTestPageUrl('page_with_frame.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::PAGE_WITH_FRAME));
- $this->expectException(\InvalidArgumentException::class);
+ $this->expectException(LogicException::class);
$this->expectExceptionMessage(
'In W3C compliance mode frame must be either instance of WebDriverElement, integer or null'
);
@@ -125,14 +170,14 @@ public function testShouldNotAcceptStringAsFrameIdInW3cMode()
/**
* @group exclude-saucelabs
*/
- public function testShouldAcceptStringAsFrameIdInJsonWireMode()
+ public function testShouldAcceptStringAsFrameIdInJsonWireMode(): void
{
self::skipForW3cProtocol();
- $this->driver->get($this->getTestPageUrl('page_with_frame.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::PAGE_WITH_FRAME));
$this->driver->switchTo()->frame('iframe_content');
- $this->assertContains('This is the content of the iFrame', $this->driver->getPageSource());
+ $this->assertStringContainsString('This is the content of the iFrame', $this->driver->getPageSource());
}
}
diff --git a/tests/functional/RemoteWebDriverCreateTest.php b/tests/functional/RemoteWebDriverCreateTest.php
index bf2d13620..a39a91070 100644
--- a/tests/functional/RemoteWebDriverCreateTest.php
+++ b/tests/functional/RemoteWebDriverCreateTest.php
@@ -1,20 +1,21 @@
-driver = RemoteWebDriver::create(
$this->serverUrl,
@@ -31,18 +32,26 @@ public function testShouldStartBrowserAndCreateInstanceOfRemoteWebDriver()
$this->assertInstanceOf(HttpCommandExecutor::class, $this->driver->getCommandExecutor());
$this->assertNotEmpty($this->driver->getCommandExecutor()->getAddressOfRemoteServer());
- $this->assertInternalType('string', $this->driver->getSessionID());
+ $this->assertIsString($this->driver->getSessionID());
$this->assertNotEmpty($this->driver->getSessionID());
$returnedCapabilities = $this->driver->getCapabilities();
$this->assertInstanceOf(WebDriverCapabilities::class, $returnedCapabilities);
- $this->assertSame($this->desiredCapabilities->getBrowserName(), $returnedCapabilities->getBrowserName());
+
+ // MicrosoftEdge on Sauce Labs started to identify itself back as "msedge"
+ if ($this->desiredCapabilities->getBrowserName() !== WebDriverBrowserType::MICROSOFT_EDGE) {
+ $this->assertEqualsIgnoringCase(
+ $this->desiredCapabilities->getBrowserName(),
+ $returnedCapabilities->getBrowserName()
+ );
+ }
+
$this->assertNotEmpty($returnedCapabilities->getPlatform());
$this->assertNotEmpty($returnedCapabilities);
$this->assertNotEmpty($returnedCapabilities->getVersion());
}
- public function testShouldAcceptCapabilitiesAsAnArray()
+ public function testShouldAcceptCapabilitiesAsAnArray(): void
{
// Method has a side-effect of converting whole content of desiredCapabilities to an array
$this->desiredCapabilities->toArray();
@@ -53,9 +62,11 @@ public function testShouldAcceptCapabilitiesAsAnArray()
$this->connectionTimeout,
$this->requestTimeout
);
+
+ $this->assertNotNull($this->driver->getCapabilities());
}
- public function testShouldCreateWebDriverWithRequiredCapabilities()
+ public function testShouldCreateWebDriverWithRequiredCapabilities(): void
{
$requiredCapabilities = new DesiredCapabilities();
@@ -72,7 +83,25 @@ public function testShouldCreateWebDriverWithRequiredCapabilities()
$this->assertInstanceOf(RemoteWebDriver::class, $this->driver);
}
- public function testShouldCreateInstanceFromExistingSessionId()
+ /**
+ * Capabilities (browser name) must be defined when executing via Selenium proxy (standalone server,
+ * Saucelabs etc.). But when running directly via browser driver, they could be empty.
+ * However the the browser driver must be able to create non-headless instance (eg. inside xvfb).
+ * @group exclude-saucelabs
+ */
+ public function testShouldCreateWebDriverWithoutCapabilities(): void
+ {
+ if (getenv('GECKODRIVER') !== '1' && empty(getenv('CHROMEDRIVER_PATH'))) {
+ $this->markTestSkipped('This test makes sense only when run directly via specific browser driver');
+ }
+
+ $this->driver = RemoteWebDriver::create($this->serverUrl);
+
+ $this->assertInstanceOf(RemoteWebDriver::class, $this->driver);
+ $this->assertNotEmpty($this->driver->getSessionID());
+ }
+
+ public function testShouldCreateInstanceFromExistingSessionId(): void
{
// Create driver instance and load page "index.html"
$originalDriver = RemoteWebDriver::create(
@@ -81,16 +110,66 @@ public function testShouldCreateInstanceFromExistingSessionId()
$this->connectionTimeout,
$this->requestTimeout
);
- $originalDriver->get($this->getTestPageUrl('index.html'));
- $this->assertContains('/index.html', $originalDriver->getCurrentURL());
+ $originalDriver->get($this->getTestPageUrl(TestPage::INDEX));
+ $this->assertStringContainsString('/index.html', $originalDriver->getCurrentURL());
- // Store session ID
+ // Store session attributes
$sessionId = $originalDriver->getSessionID();
+ $isW3cCompliant = $originalDriver->isW3cCompliant();
+ $originalCapabilities = $originalDriver->getCapabilities();
+
+ $capabilitiesForSessionReuse = $originalCapabilities;
+ if ($this->isSeleniumServerUsed()) {
+ // do not provide capabilities when selenium server is used, to test they are read from selenium server
+ $capabilitiesForSessionReuse = null;
+ }
// Create new RemoteWebDriver instance based on the session ID
- $this->driver = RemoteWebDriver::createBySessionID($sessionId, $this->serverUrl);
+ $this->driver = RemoteWebDriver::createBySessionID(
+ $sessionId,
+ $this->serverUrl,
+ null,
+ null,
+ $isW3cCompliant,
+ $capabilitiesForSessionReuse
+ );
+
+ // Capabilities should be retrieved and be set to the driver instance
+ $returnedCapabilities = $this->driver->getCapabilities();
+ $this->assertInstanceOf(WebDriverCapabilities::class, $returnedCapabilities);
+
+ $expectedBrowserName = $this->desiredCapabilities->getBrowserName();
+
+ $this->assertEqualsIgnoringCase(
+ $expectedBrowserName,
+ $returnedCapabilities->getBrowserName()
+ );
+ $this->assertEqualsCanonicalizing($originalCapabilities, $this->driver->getCapabilities());
// Check we reused the previous instance (window) and it has the same URL
- $this->assertContains('/index.html', $this->driver->getCurrentURL());
+ $this->assertStringContainsString('/index.html', $this->driver->getCurrentURL());
+
+ // Do some interaction with the new driver
+ $this->assertNotEmpty($this->driver->findElement(WebDriverBy::id('id_test'))->getText());
+ }
+
+ public function testShouldRequireCapabilitiesToBeSetToReuseExistingSession(): void
+ {
+ $this->expectException(UnexpectedResponseException::class);
+ $this->expectExceptionMessage(
+ 'Existing Capabilities were not provided, and they also cannot be read from Selenium Grid'
+ );
+
+ // Do not provide capabilities, they also cannot be retrieved from the Selenium Grid
+ RemoteWebDriver::createBySessionID(
+ 'sessionId',
+ 'http://localhost:332/', // nothing should be running there
+ null,
+ null
+ );
+ }
+
+ protected function createWebDriver(): void
+ {
}
}
diff --git a/tests/functional/RemoteWebDriverFindElementTest.php b/tests/functional/RemoteWebDriverFindElementTest.php
index 4ac265381..f638aa527 100644
--- a/tests/functional/RemoteWebDriverFindElementTest.php
+++ b/tests/functional/RemoteWebDriverFindElementTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$this->expectException(NoSuchElementException::class);
$this->driver->findElement(WebDriverBy::id('not_existing'));
}
- public function testShouldFindElementIfExistsOnAPage()
+ public function testShouldFindElementIfExistsOnAPage(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::id('id_test'));
$this->assertInstanceOf(RemoteWebElement::class, $element);
}
- public function testShouldReturnEmptyArrayIfElementsCannotBeFound()
+ public function testShouldReturnEmptyArrayIfElementsCannotBeFound(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$elements = $this->driver->findElements(WebDriverBy::cssSelector('not_existing'));
- $this->assertInternalType('array', $elements);
+ $this->assertIsArray($elements);
$this->assertCount(0, $elements);
}
- public function testShouldFindMultipleElements()
+ public function testShouldFindMultipleElements(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$elements = $this->driver->findElements(WebDriverBy::cssSelector('ul > li'));
- $this->assertInternalType('array', $elements);
+ $this->assertIsArray($elements);
$this->assertCount(5, $elements);
$this->assertContainsOnlyInstancesOf(RemoteWebElement::class, $elements);
}
@@ -52,13 +52,13 @@ public function testShouldFindMultipleElements()
/**
* @group exclude-saucelabs
*/
- public function testEscapeCssSelector()
+ public function testEscapeCssSelector(): void
{
self::skipForJsonWireProtocol(
'CSS selectors containing special characters are not supported by the legacy protocol'
);
- $this->driver->get($this->getTestPageUrl('escape_css.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::ESCAPE_CSS));
$element = $this->driver->findElement(WebDriverBy::id('.fo\'oo'));
$this->assertSame('Foo', $element->getText());
diff --git a/tests/functional/RemoteWebDriverTest.php b/tests/functional/RemoteWebDriverTest.php
index 28b4e52ab..95a01b9b7 100644
--- a/tests/functional/RemoteWebDriverTest.php
+++ b/tests/functional/RemoteWebDriverTest.php
@@ -1,10 +1,9 @@
-driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$this->assertEquals(
'php-webdriver test page',
@@ -28,9 +27,9 @@ public function testShouldGetPageTitle()
* @covers ::get
* @covers ::getCurrentURL
*/
- public function testShouldGetCurrentUrl()
+ public function testShouldGetCurrentUrl(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$this->assertStringEndsWith('/index.html', $this->driver->getCurrentURL());
}
@@ -38,20 +37,20 @@ public function testShouldGetCurrentUrl()
/**
* @covers ::getPageSource
*/
- public function testShouldGetPageSource()
+ public function testShouldGetPageSource(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$source = $this->driver->getPageSource();
- $this->assertContains('', $source);
- $this->assertContains('Welcome to the php-webdriver testing page.', $source);
+ $this->assertStringContainsString('', $source);
+ $this->assertStringContainsString('Welcome to the php-webdriver testing page.', $source);
}
/**
* @covers ::getSessionID
* @covers ::isW3cCompliant
*/
- public function testShouldGetSessionId()
+ public function testShouldGetSessionId(): void
{
// This tests is intentionally included in another test, to not slow down build.
// @TODO Remove following in 2.0
@@ -63,7 +62,7 @@ public function testShouldGetSessionId()
$sessionId = $this->driver->getSessionID();
- $this->assertInternalType('string', $sessionId);
+ $this->assertIsString($sessionId);
$this->assertNotEmpty($sessionId);
}
@@ -71,15 +70,13 @@ public function testShouldGetSessionId()
* @group exclude-saucelabs
* @covers ::getAllSessions
*/
- public function testShouldGetAllSessions()
+ public function testShouldGetAllSessions(): void
{
- if (getenv('GECKODRIVER') === '1') {
- $this->markTestSkipped('"getAllSessions" is not supported by the W3C specification');
- }
+ self::skipForW3cProtocol();
$sessions = RemoteWebDriver::getAllSessions($this->serverUrl, 30000);
- $this->assertInternalType('array', $sessions);
+ $this->assertIsArray($sessions);
$this->assertCount(1, $sessions);
$this->assertArrayHasKey('capabilities', $sessions[0]);
@@ -92,23 +89,25 @@ public function testShouldGetAllSessions()
* @covers ::getCommandExecutor
* @covers ::quit
*/
- public function testShouldQuitAndUnsetExecutor()
+ public function testShouldQuitAndUnsetExecutor(): void
{
- if (getenv('GECKODRIVER') === '1') {
- $this->markTestSkipped('"getAllSessions" is not supported by the W3C specification');
- }
+ self::skipForW3cProtocol();
$this->assertCount(
1,
- RemoteWebDriver::getAllSessions($this->serverUrl, 30000)
+ RemoteWebDriver::getAllSessions($this->serverUrl)
);
$this->assertInstanceOf(HttpCommandExecutor::class, $this->driver->getCommandExecutor());
$this->driver->quit();
+ // Wait a while until chromedriver finishes deleting the session.
+ // https://bugs.chromium.org/p/chromedriver/issues/detail?id=3736
+ usleep(250000); // 250 ms
+
$this->assertCount(
0,
- RemoteWebDriver::getAllSessions($this->serverUrl, 30000)
+ RemoteWebDriver::getAllSessions($this->serverUrl)
);
$this->assertNull($this->driver->getCommandExecutor());
}
@@ -117,14 +116,14 @@ public function testShouldQuitAndUnsetExecutor()
* @covers ::getWindowHandle
* @covers ::getWindowHandles
*/
- public function testShouldGetWindowHandles()
+ public function testShouldGetWindowHandles(): void
{
- $this->driver->get($this->getTestPageUrl('open_new_window.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::OPEN_NEW_WINDOW));
$windowHandle = $this->driver->getWindowHandle();
$windowHandles = $this->driver->getWindowHandles();
- $this->assertInternalType('string', $windowHandle);
+ $this->assertIsString($windowHandle);
$this->assertNotEmpty($windowHandle);
$this->assertSame([$windowHandle], $windowHandles);
@@ -141,9 +140,9 @@ public function testShouldGetWindowHandles()
/**
* @covers ::close
*/
- public function testShouldCloseWindow()
+ public function testShouldCloseWindow(): void
{
- $this->driver->get($this->getTestPageUrl('open_new_window.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::OPEN_NEW_WINDOW));
$this->driver->findElement(WebDriverBy::cssSelector('a'))->click();
$this->driver->wait()->until(WebDriverExpectedCondition::numberOfWindowsToBe(2));
@@ -159,9 +158,9 @@ public function testShouldCloseWindow()
* @covers ::executeScript
* @group exclude-saucelabs
*/
- public function testShouldExecuteScriptAndDoNotBlockExecution()
+ public function testShouldExecuteScriptAndDoNotBlockExecution(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::id('id_test'));
$this->assertSame('Test by ID', $element->getText());
@@ -189,11 +188,11 @@ function(){document.getElementById("id_test").innerHTML = "Text changed by scrip
* @covers ::executeAsyncScript
* @covers \Facebook\WebDriver\WebDriverTimeouts::setScriptTimeout
*/
- public function testShouldExecuteAsyncScriptAndWaitUntilItIsFinished()
+ public function testShouldExecuteAsyncScriptAndWaitUntilItIsFinished(): void
{
$this->driver->manage()->timeouts()->setScriptTimeout(1);
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::id('id_test'));
$this->assertSame('Test by ID', $element->getText());
@@ -229,11 +228,11 @@ function(){
* @covers ::prepareScriptArguments
* @group exclude-saucelabs
*/
- public function testShouldExecuteScriptWithParamsAndReturnValue()
+ public function testShouldExecuteScriptWithParamsAndReturnValue(): void
{
$this->driver->manage()->timeouts()->setScriptTimeout(1);
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element1 = $this->driver->findElement(WebDriverBy::id('id_test'));
$element2 = $this->driver->findElement(WebDriverBy::className('test_class'));
@@ -251,22 +250,19 @@ public function testShouldExecuteScriptWithParamsAndReturnValue()
/**
* @covers ::takeScreenshot
+ * @covers \Facebook\WebDriver\Support\ScreenshotHelper
*/
- public function testShouldTakeScreenshot()
+ public function testShouldTakeScreenshot(): void
{
if (!extension_loaded('gd')) {
$this->markTestSkipped('GD extension must be enabled');
}
- if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) {
- $this->markTestSkipped('Screenshots are not supported by HtmlUnit browser');
- }
-
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$outputPng = $this->driver->takeScreenshot();
$image = imagecreatefromstring($outputPng);
- $this->assertInternalType('resource', $image);
+ $this->assertNotFalse($image);
$this->assertGreaterThan(0, imagesx($image));
$this->assertGreaterThan(0, imagesy($image));
@@ -274,25 +270,24 @@ public function testShouldTakeScreenshot()
/**
* @covers ::takeScreenshot
+ * @covers \Facebook\WebDriver\Support\ScreenshotHelper
+ * @group exclude-safari
+ * Safari is returning different color profile and it does not have way to configure "force-color-profile"
*/
- public function testShouldSaveScreenshotToFile()
+ public function testShouldSaveScreenshotToFile(): void
{
if (!extension_loaded('gd')) {
$this->markTestSkipped('GD extension must be enabled');
}
- if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) {
- $this->markTestSkipped('Screenshots are not supported by HtmlUnit browser');
- }
- // Intentionally save screenshot to subdirectory to tests it is being created
$screenshotPath = sys_get_temp_dir() . '/' . uniqid('php-webdriver-') . '/selenium-screenshot.png';
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$this->driver->takeScreenshot($screenshotPath);
$image = imagecreatefrompng($screenshotPath);
- $this->assertInternalType('resource', $image);
+ $this->assertNotFalse($image);
$this->assertGreaterThan(0, imagesx($image));
$this->assertGreaterThan(0, imagesy($image));
@@ -319,13 +314,15 @@ public function testShouldSaveScreenshotToFile()
* @group exclude-saucelabs
* Status endpoint is not supported on Sauce Labs
*/
- public function testShouldGetRemoteEndStatus()
+ public function testShouldGetRemoteEndStatus(): void
{
$status = $this->driver->getStatus();
- $this->assertInternalType('boolean', $status->isReady());
- $this->assertNotEmpty($status->getMessage());
+ $this->assertIsBool($status->isReady());
+ $this->assertIsArray($status->getMeta());
- $this->assertInternalType('array', $status->getMeta());
+ if (getenv('BROWSER_NAME') !== 'safari') {
+ $this->assertNotEmpty($status->getMessage());
+ }
}
}
diff --git a/tests/functional/RemoteWebElementTest.php b/tests/functional/RemoteWebElementTest.php
index 46e3509bb..6a21cb36c 100644
--- a/tests/functional/RemoteWebElementTest.php
+++ b/tests/functional/RemoteWebElementTest.php
@@ -1,10 +1,11 @@
-driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$elementWithSimpleText = $this->driver->findElement(WebDriverBy::id('text-simple'));
$elementWithTextWithSpaces = $this->driver->findElement(WebDriverBy::id('text-with-spaces'));
$this->assertEquals('Foo bar text', $elementWithSimpleText->getText());
+
$this->assertEquals('Multiple spaces are stripped', $elementWithTextWithSpaces->getText());
}
/**
* @covers ::getAttribute
*/
- public function testShouldGetAttributeValue()
+ public function testShouldGetAttributeValue(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::id('text-simple'));
$this->assertSame('note', $element->getAttribute('role'));
$this->assertSame('height: 5em; border: 1px solid black;', $element->getAttribute('style'));
$this->assertSame('text-simple', $element->getAttribute('id'));
+ $this->assertNull($element->getAttribute('notExisting'));
+ }
+
+ /**
+ * @covers ::getDomProperty
+ */
+ public function testShouldGetDomPropertyValue(): void
+ {
+ self::skipForJsonWireProtocol();
+
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
+
+ $element = $this->driver->findElement(WebDriverBy::id('div-with-html'));
+
+ $this->assertStringContainsString(
+ ' This div has some more html inside.
',
+ $element->getDomProperty('innerHTML')
+ );
+ $this->assertSame('foo bar', $element->getDomProperty('className')); // IDL property
+ $this->assertSame('foo bar', $element->getAttribute('class')); // HTML attribute should be the same
+ $this->assertSame('DIV', $element->getDomProperty('tagName'));
+ $this->assertSame(2, $element->getDomProperty('childElementCount'));
+ $this->assertNull($element->getDomProperty('notExistingProperty'));
}
/**
* @covers ::getLocation
*/
- public function testShouldGetLocation()
+ public function testShouldGetLocation(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::id('element-with-location'));
$elementLocation = $element->getLocation();
$this->assertInstanceOf(WebDriverPoint::class, $elementLocation);
$this->assertSame(33, $elementLocation->getX());
- $this->assertSame(500, $elementLocation->getY());
+ $this->assertSame(550, $elementLocation->getY());
+ }
+
+ /**
+ * @covers ::getLocationOnScreenOnceScrolledIntoView
+ */
+ public function testShouldGetLocationOnScreenOnceScrolledIntoView(): void
+ {
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
+
+ $element = $this->driver->findElement(WebDriverBy::id('element-out-of-viewport'));
+
+ // Location before scrolling into view is out of viewport
+ $elementLocation = $element->getLocation();
+ $this->assertInstanceOf(WebDriverPoint::class, $elementLocation);
+ $this->assertSame(33, $elementLocation->getX());
+ $this->assertSame(5000, $elementLocation->getY());
+
+ // Location once scrolled into view
+ $elementLocationOnceScrolledIntoView = $element->getLocationOnScreenOnceScrolledIntoView();
+ $this->assertInstanceOf(WebDriverPoint::class, $elementLocationOnceScrolledIntoView);
+ $this->assertSame(33, $elementLocationOnceScrolledIntoView->getX());
+ $this->assertLessThan(
+ 1000, // screen size is ~768, so this should be less
+ $elementLocationOnceScrolledIntoView->getY()
+ );
}
/**
* @covers ::getSize
*/
- public function testShouldGetSize()
+ public function testShouldGetSize(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::id('element-with-location'));
@@ -73,9 +125,9 @@ public function testShouldGetSize()
/**
* @covers ::getCSSValue
*/
- public function testShouldGetCssValue()
+ public function testShouldGetCssValue(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$elementWithBorder = $this->driver->findElement(WebDriverBy::id('text-simple'));
$elementWithoutBorder = $this->driver->findElement(WebDriverBy::id('text-with-spaces'));
@@ -84,7 +136,7 @@ public function testShouldGetCssValue()
$this->assertSame('none', $elementWithoutBorder->getCSSValue('border-left-style'));
// Browser could report color in either rgb (like MS Edge) or rgba (like everyone else)
- $this->assertRegExp(
+ $this->assertMatchesRegularExpression(
'/rgba?\(0, 0, 0(, 1)?\)/',
$elementWithBorder->getCSSValue('border-left-color')
);
@@ -93,9 +145,9 @@ public function testShouldGetCssValue()
/**
* @covers ::getTagName
*/
- public function testShouldGetTagName()
+ public function testShouldGetTagName(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$paragraphElement = $this->driver->findElement(WebDriverBy::id('id_test'));
@@ -105,24 +157,89 @@ public function testShouldGetTagName()
/**
* @covers ::click
*/
- public function testShouldClick()
+ public function testShouldClick(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$linkElement = $this->driver->findElement(WebDriverBy::id('a-form'));
$linkElement->click();
$this->driver->wait()->until(
- WebDriverExpectedCondition::urlContains('form.html')
+ WebDriverExpectedCondition::urlContains(TestPage::FORM)
);
+
+ $this->assertTrue(true); // To generate coverage, see https://github.com/sebastianbergmann/phpunit/issues/3016
+ }
+
+ /**
+ * This test checks that the workarounds in place for https://github.com/mozilla/geckodriver/issues/653 work as
+ * expected where child links can be clicked.
+ *
+ * @covers ::click
+ * @covers ::clickChildElement
+ * @group exclude-chrome
+ * @group exclude-edge
+ */
+ public function testGeckoDriverShouldClickOnBlockLevelElement(): void
+ {
+ self::skipForUnmatchedBrowsers(['firefox']);
+
+ $links = [
+ 'a-index-plain',
+ 'a-index-block-child',
+ 'a-index-block-child-hidden',
+ 'a-index-second-child-hidden',
+ ];
+
+ foreach ($links as $linkid) {
+ $this->driver->get($this->getTestPageUrl(TestPage::GECKO_653));
+ $linkElement = $this->driver->findElement(WebDriverBy::id($linkid));
+
+ $linkElement->click();
+ $this->assertStringContainsString('index.html', $this->driver->getCurrentUrl());
+ }
+ }
+
+ /**
+ * This test checks that the workarounds in place for https://github.com/mozilla/geckodriver/issues/653 work as
+ * expected where child links cannot be clicked, and that appropriate exceptions are thrown.
+ *
+ * @covers ::click
+ * @covers ::clickChildElement
+ * @group exclude-chrome
+ * @group exclude-edge
+ */
+ public function testGeckoDriverShouldClickNotInteractable(): void
+ {
+ self::skipForUnmatchedBrowsers(['firefox']);
+
+ $this->driver->get($this->getTestPageUrl(TestPage::GECKO_653));
+
+ $linkElement = $this->driver->findElement(WebDriverBy::id('a-index-plain-hidden'));
+
+ try {
+ $linkElement->click();
+ $this->fail('No exception was thrown when clicking an inaccessible link');
+ } catch (ElementNotInteractableException $e) {
+ $this->assertInstanceOf(ElementNotInteractableException::class, $e);
+ }
+
+ $linkElement = $this->driver->findElement(WebDriverBy::id('a-index-hidden-block-child'));
+
+ try {
+ $linkElement->click();
+ $this->fail('No exception was thrown when clicking an inaccessible link');
+ } catch (ElementNotInteractableException $e) {
+ $this->assertInstanceOf(ElementNotInteractableException::class, $e);
+ }
}
/**
* @covers ::clear
*/
- public function testShouldClearFormElementText()
+ public function testShouldClearFormElementText(): void
{
- $this->driver->get($this->getTestPageUrl('form.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::FORM));
$input = $this->driver->findElement(WebDriverBy::id('input-text'));
$textarea = $this->driver->findElement(WebDriverBy::id('textarea'));
@@ -139,9 +256,9 @@ public function testShouldClearFormElementText()
/**
* @covers ::sendKeys
*/
- public function testShouldSendKeysToFormElement()
+ public function testShouldSendKeysToFormElement(): void
{
- $this->driver->get($this->getTestPageUrl('form.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::FORM));
$input = $this->driver->findElement(WebDriverBy::id('input-text'));
$textarea = $this->driver->findElement(WebDriverBy::id('textarea'));
@@ -152,19 +269,50 @@ public function testShouldSendKeysToFormElement()
$input->sendKeys(' baz');
$this->assertSame('foo bar baz', $input->getAttribute('value'));
+ $input->clear();
+ $input->sendKeys([WebDriverKeys::SHIFT, 'H', WebDriverKeys::NULL, 'ello']);
+ $this->assertSame('Hello', $input->getAttribute('value'));
+
$textarea->clear();
$textarea->sendKeys('foo bar');
$this->assertSame('foo bar', $textarea->getAttribute('value'));
$textarea->sendKeys(' baz');
$this->assertSame('foo bar baz', $textarea->getAttribute('value'));
+
+ $textarea->clear();
+ $textarea->sendKeys([WebDriverKeys::SHIFT, 'H', WebDriverKeys::NULL, 'ello']);
+ $this->assertSame('Hello', $textarea->getAttribute('value'));
+
+ // Send keys as array
+ $textarea->clear();
+ $textarea->sendKeys(['bat', 1, '3', ' ', 3, '7']);
+ $this->assertSame('bat13 37', $textarea->getAttribute('value'));
+ }
+
+ /**
+ * @covers ::isDisplayed
+ * @covers \Facebook\WebDriver\Remote\RemoteWebDriver::execute
+ * @covers \Facebook\WebDriver\Support\IsElementDisplayedAtom
+ */
+ public function testShouldDetectElementDisplayedness(): void
+ {
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
+
+ $visibleElement = $this->driver->findElement(WebDriverBy::cssSelector('.test_class'));
+ $elementOutOfViewport = $this->driver->findElement(WebDriverBy::id('element-out-of-viewport'));
+ $hiddenElement = $this->driver->findElement(WebDriverBy::id('hidden-element'));
+
+ $this->assertTrue($visibleElement->isDisplayed());
+ $this->assertTrue($elementOutOfViewport->isDisplayed());
+ $this->assertFalse($hiddenElement->isDisplayed());
}
/**
* @covers ::isEnabled
*/
- public function testShouldDetectEnabledInputs()
+ public function testShouldDetectEnabledInputs(): void
{
- $this->driver->get($this->getTestPageUrl('form.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::FORM));
$inputEnabled = $this->driver->findElement(WebDriverBy::id('input-text'));
$inputDisabled = $this->driver->findElement(WebDriverBy::id('input-text-disabled'));
@@ -176,9 +324,9 @@ public function testShouldDetectEnabledInputs()
/**
* @covers ::isSelected
*/
- public function testShouldSelectedInputsOrOptions()
+ public function testShouldSelectedInputsOrOptions(): void
{
- $this->driver->get($this->getTestPageUrl('form.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::FORM));
$checkboxSelected = $this->driver->findElement(
WebDriverBy::cssSelector('input[name=checkbox][value=second]')
@@ -204,9 +352,9 @@ public function testShouldSelectedInputsOrOptions()
* @covers ::submit
* @group exclude-edge
*/
- public function testShouldSubmitFormBySubmitEventOnForm()
+ public function testShouldSubmitFormBySubmitEventOnForm(): void
{
- $this->driver->get($this->getTestPageUrl('form.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::FORM));
$formElement = $this->driver->findElement(WebDriverBy::cssSelector('form'));
@@ -222,9 +370,9 @@ public function testShouldSubmitFormBySubmitEventOnForm()
/**
* @covers ::submit
*/
- public function testShouldSubmitFormBySubmitEventOnFormInputElement()
+ public function testShouldSubmitFormBySubmitEventOnFormInputElement(): void
{
- $this->driver->get($this->getTestPageUrl('form.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::FORM));
$inputTextElement = $this->driver->findElement(WebDriverBy::id('input-text'));
@@ -240,9 +388,9 @@ public function testShouldSubmitFormBySubmitEventOnFormInputElement()
/**
* @covers ::click
*/
- public function testShouldSubmitFormByClickOnSubmitInput()
+ public function testShouldSubmitFormByClickOnSubmitInput(): void
{
- $this->driver->get($this->getTestPageUrl('form.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::FORM));
$submitElement = $this->driver->findElement(WebDriverBy::id('submit'));
@@ -257,13 +405,10 @@ public function testShouldSubmitFormByClickOnSubmitInput()
/**
* @covers ::equals
- * @group exclude-saucelabs
*/
- public function testShouldCompareEqualsElement()
+ public function testShouldCompareEqualsElement(): void
{
- self::skipForW3cProtocol('"equals" is not supported by the W3C specification');
-
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$firstElement = $this->driver->findElement(WebDriverBy::cssSelector('ul.list'));
$differentElement = $this->driver->findElement(WebDriverBy::cssSelector('#text-simple'));
@@ -280,18 +425,18 @@ public function testShouldCompareEqualsElement()
/**
* @covers ::findElement
*/
- public function testShouldThrowExceptionIfChildElementCannotBeFound()
+ public function testShouldThrowExceptionIfChildElementCannotBeFound(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::cssSelector('ul.list'));
$this->expectException(NoSuchElementException::class);
$element->findElement(WebDriverBy::id('not_existing'));
}
- public function testShouldFindChildElementIfExistsOnAPage()
+ public function testShouldFindChildElementIfExistsOnAPage(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::cssSelector('ul.list'));
$childElement = $element->findElement(WebDriverBy::cssSelector('li'));
@@ -301,26 +446,26 @@ public function testShouldFindChildElementIfExistsOnAPage()
$this->assertSame('First', $childElement->getText());
}
- public function testShouldReturnEmptyArrayIfChildElementsCannotBeFound()
+ public function testShouldReturnEmptyArrayIfChildElementsCannotBeFound(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::cssSelector('ul.list'));
$childElements = $element->findElements(WebDriverBy::cssSelector('not_existing'));
- $this->assertInternalType('array', $childElements);
+ $this->assertIsArray($childElements);
$this->assertCount(0, $childElements);
}
- public function testShouldFindMultipleChildElements()
+ public function testShouldFindMultipleChildElements(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::cssSelector('ul.list'));
$allElements = $this->driver->findElements(WebDriverBy::cssSelector('li'));
$childElements = $element->findElements(WebDriverBy::cssSelector('li'));
- $this->assertInternalType('array', $childElements);
+ $this->assertIsArray($childElements);
$this->assertCount(5, $allElements); // there should be 5 elements on page
$this->assertCount(3, $childElements); // but we should find only subelements of one
+Mouse events:
+Keyboard events:
+
+
+
+
+
+Sortable using jQuery
+
+
+
+
+
+ 1-1
+ 1-2
+ 1-3
+ 1-4
+ 1-5
+
+
+
+
+ 2-1
+ 2-2
+ 2-3
+ 2-4
+ 2-5
+
+
+
+
+
+