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
new file mode 100644
index 000000000..a79b02b1f
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,94 @@
+# Contributing to php-webdriver
+
+We love to have your help to make php-webdriver better!
+
+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.
+
+## 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 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
+```
+
+To run functional tests locally there is some additional setup needed - see below. Without this setup, functional tests will be skipped.
+
+
+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
+
+### Unit tests
+
+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.
+
+To execute **all tests** in both suites run:
+
+```sh
+composer test
+```
+
+If you want to execute **just the unit tests**, run:
+
+```sh
+composer test -- --testsuite unit
+```
+
+**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:
+
+```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
+```
+
+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:
+
+```sh
+...
+export BROWSER_NAME="chrome"
+composer all
+```
+
+To test with Firefox/Geckodriver, you must also set `GECKODRIVER` environment variable:
+
+```sh
+export GECKODRIVER=1
+export BROWSER_NAME="firefox"
+composer all
+```
+
+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:
+
+```sh
+export DISABLE_HEADLESS="1"
+composer all
+```
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/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 aa9bce590..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,186 +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:
- # Codestyle check build
- - name: 'Code style and static analysis'
- php: '7.4'
- env: CHECK_CODESTYLE=1
- before_install:
- - phpenv config-rm xdebug.ini
- before_script: ~
- script:
- - composer require phpstan/phpstan # Not part of require-dev, because it won't install on PHP 5.6
- - composer analyze
- - composer codestyle:check
- after_script: ~
- after_success: ~
-
- # 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: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE=
-
- - 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: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE=
-
- - 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: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE=
-
- - 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: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE=
-
-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 15d1d2524..a468c26f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,12 +3,157 @@ 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/).
+- The Packagist package was renamed to [`php-webdriver/webdriver`](https://packagist.org/packages/php-webdriver/webdriver) and the original [`facebook/webdriver`](https://packagist.org/packages/facebook/webdriver) was marked as abandoned.
- Revert no longer needed workaround for Chromedriver bug [2943](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943).
- Allow installation of Symfony 5 components.
- Rename environment variable used to pass path to ChromeDriver executable from `webdriver.chrome.driver` to `WEBDRIVER_CHROME_DRIVER`. However the old one also still works to keep backward compatibility
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/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 741a140a9..000000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# Contributing to php-webdriver
-
-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
-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)
-
-## 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
-
-Note before any pull request can be accepted, a [Contributors Licensing Agreement](https://developers.facebook.com/opensource/cla) must be signed.
-
-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/)
-
-### Run unit tests
-
-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:
-
- ./vendor/bin/phpunit
-
-If you want to execute just the unit tests, run:
-
- ./vendor/bin/phpunit --testsuite unit
-
-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:
-
- 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
-
-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:
-
- ...
- export BROWSER_NAME="firefox"
- ./vendor/bin/phpunit --testsuite functional
-
-To test with Geckodriver, [download](https://github.com/mozilla/geckodriver/releases) and start the server, then run:
-
- export GECKODRIVER=1
- export BROWSER_NAME=firefox
- ./vendor/bin/phpunit --testsuite functional
-
-### Check coding style
-
-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:
-
- composer codestyle:check
-
-To auto-fix the codestyle simply run:
-
- composer codestyle:fix
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
deleted file mode 100644
index 3eef8cde7..000000000
--- a/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/LICENSE.md b/LICENSE.md
index 0b3f67bd7..611fa7390 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,201 +1,22 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "{}"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright 2004-present Facebook
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+MIT License
+
+Copyright (c) 2004-2020 Facebook
+Copyright (c) 2020-present [open-source contributors](https://github.com/php-webdriver/php-webdriver/graphs/contributors)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 71742583b..f3e8d3801 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,20 @@
# php-webdriver – Selenium WebDriver bindings for PHP
-[](https://packagist.org/packages/facebook/webdriver)
-[](https://travis-ci.org/facebook/php-webdriver)
-[](https://saucelabs.com/u/php-webdriver)
-[](https://packagist.org/packages/facebook/webdriver)
-[](https://packagist.org/packages/facebook/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://facebook.github.io/php-webdriver/](https://facebook.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
@@ -33,88 +26,203 @@ If you don't already use Composer, you can download the `composer.phar` binary:
Then install the library:
- php composer.phar require facebook/webdriver
+ 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/facebook/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/facebook/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 d422149ee..f44663100 100644
--- a/composer.json
+++ b/composer.json
@@ -1,72 +1,98 @@
{
- "name": "facebook/webdriver",
- "description": "A PHP client for Selenium WebDriver",
- "keywords": ["webdriver", "selenium", "php", "facebook"],
- "homepage": "https://github.com/facebook/php-webdriver",
+ "name": "php-webdriver/webdriver",
+ "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.",
+ "license": "MIT",
"type": "library",
- "license": "Apache-2.0",
- "support": {
- "issues": "https://github.com/facebook/php-webdriver/issues",
- "forum": "https://www.facebook.com/groups/phpwebdriver/",
- "source": "https://github.com/facebook/php-webdriver"
- },
- "minimum-stability": "beta",
+ "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/"
+ ]
+ },
+ "config": {
+ "allow-plugins": {
+ "ergebnis/composer-normalize": true
},
- "classmap": ["tests/functional/"]
+ "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"
],
- "codestyle:fix": [
- "vendor/bin/php-cs-fixer fix --diff --diff-format=udiff -vvv || exit 0",
- "vendor/bin/phpcbf --standard=PSR2 ./lib/ ./tests/"
+ "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"
+ ],
+ "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 4dbb93900..92cd431d6 100644
--- a/example.php
+++ b/example.php
@@ -1,20 +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 9c5c61cc2..00aee242a 100644
--- a/lib/AbstractWebDriverCheckboxOrRadio.php
+++ b/lib/AbstractWebDriverCheckboxOrRadio.php
@@ -1,23 +1,10 @@
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;
@@ -79,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 107854b4a..e947a498e 100644
--- a/lib/Chrome/ChromeDriver.php
+++ b/lib/Chrome/ChromeDriver.php
@@ -1,101 +1,107 @@
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 bade9e8b0..902a48ded 100644
--- a/lib/Chrome/ChromeDriverService.php
+++ b/lib/Chrome/ChromeDriverService.php
@@ -1,17 +1,4 @@
toArray();
+ }
+
/**
* Sets the path of the Chrome executable. The path should be either absolute
* or relative to the location running ChromeDriver server.
@@ -65,7 +64,6 @@ public function setBinary($path)
}
/**
- * @param array $arguments
* @return ChromeOptions
*/
public function addArguments(array $arguments)
@@ -79,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)
@@ -107,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
@@ -130,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 170b46486..2ae257bd1 100644
--- a/lib/Cookie.php
+++ b/lib/Cookie.php
@@ -1,28 +1,15 @@
setHttpOnly($cookieArray['httpOnly']);
}
+ if (isset($cookieArray['sameSite'])) {
+ $cookie->setSameSite($cookieArray['sameSite']);
+ }
return $cookie;
}
@@ -117,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);
@@ -185,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
*/
@@ -199,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) {
@@ -218,6 +242,11 @@ public function offsetSet($offset, $value)
}
}
+ /**
+ * @param mixed $offset
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
unset($this->cookie[$offset]);
@@ -229,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 ";"');
}
}
@@ -243,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 be1199c24..2a33fb003 100644
--- a/lib/Firefox/FirefoxPreferences.php
+++ b/lib/Firefox/FirefoxPreferences.php
@@ -1,17 +1,4 @@
open($temp_zip, ZipArchive::CREATE);
$dir = new RecursiveDirectoryIterator($temp_dir);
@@ -195,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 = '')
@@ -250,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
+ );
}
}
@@ -280,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)
@@ -291,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/WebDriverButtonReleaseAction.php b/lib/Interactions/Internal/WebDriverButtonReleaseAction.php
index 5df3cb6db..91cad24af 100644
--- a/lib/Interactions/Internal/WebDriverButtonReleaseAction.php
+++ b/lib/Interactions/Internal/WebDriverButtonReleaseAction.php
@@ -1,17 +1,4 @@
keyboard = $keyboard;
$this->mouse = $mouse;
diff --git a/lib/Interactions/Internal/WebDriverMouseAction.php b/lib/Interactions/Internal/WebDriverMouseAction.php
index 2bec07386..ecb1127ee 100644
--- a/lib/Interactions/Internal/WebDriverMouseAction.php
+++ b/lib/Interactions/Internal/WebDriverMouseAction.php
@@ -1,17 +1,4 @@
mouse = $mouse;
$this->locationProvider = $location_provider;
diff --git a/lib/Interactions/Internal/WebDriverMouseMoveAction.php b/lib/Interactions/Internal/WebDriverMouseMoveAction.php
index 87c6d7cbb..1969f01ba 100644
--- a/lib/Interactions/Internal/WebDriverMouseMoveAction.php
+++ b/lib/Interactions/Internal/WebDriverMouseMoveAction.php
@@ -1,17 +1,4 @@
key = $key;
}
}
diff --git a/lib/Interactions/Touch/WebDriverDoubleTapAction.php b/lib/Interactions/Touch/WebDriverDoubleTapAction.php
index 47664fe23..25a1761b3 100644
--- a/lib/Interactions/Touch/WebDriverDoubleTapAction.php
+++ b/lib/Interactions/Touch/WebDriverDoubleTapAction.php
@@ -1,17 +1,4 @@
touchScreen = $touch_screen;
$this->locationProvider = $location_provider;
diff --git a/lib/Interactions/Touch/WebDriverTouchScreen.php b/lib/Interactions/Touch/WebDriverTouchScreen.php
index 4bb6f1100..ff9f9c4d1 100644
--- a/lib/Interactions/Touch/WebDriverTouchScreen.php
+++ b/lib/Interactions/Touch/WebDriverTouchScreen.php
@@ -1,17 +1,4 @@
driver = $driver;
@@ -61,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)
@@ -77,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)
@@ -93,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)
@@ -109,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)
@@ -124,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)
@@ -146,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
@@ -187,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
@@ -208,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)
@@ -225,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)
@@ -243,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)
@@ -258,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 d4ca94c5d..955d61986 100644
--- a/lib/Interactions/WebDriverCompositeAction.php
+++ b/lib/Interactions/WebDriverCompositeAction.php
@@ -1,17 +1,4 @@
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 1e49eb9f5..88aa6b141 100644
--- a/lib/Remote/DesiredCapabilities.php
+++ b/lib/Remote/DesiredCapabilities.php
@@ -1,24 +1,11 @@
'platformName',
WebDriverCapabilityType::VERSION => 'browserVersion',
WebDriverCapabilityType::ACCEPT_SSL_CERTS => 'acceptInsecureCerts',
- ChromeOptions::CAPABILITY => ChromeOptions::CAPABILITY_W3C,
];
public function __construct(array $capabilities = [])
@@ -43,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
@@ -109,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;
@@ -156,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
*/
@@ -164,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.'
);
@@ -176,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()
@@ -188,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
) {
@@ -226,11 +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[self::$ossToW3c[$capabilityKey]] === 'any') {
+ unset($w3cCapabilities[self::$ossToW3c[$capabilityKey]]);
+ }
} else {
- $w3cCapabilities[static::$ossToW3c[$capabilityKey]] = $capabilityValue;
+ $w3cCapabilities[self::$ossToW3c[$capabilityKey]] = $capabilityValue;
}
}
@@ -242,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];
}
}
@@ -296,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;
}
@@ -395,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()
@@ -424,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 88091023e..a3a230b7c 100644
--- a/lib/Remote/DriverCommand.php
+++ b/lib/Remote/DriverCommand.php
@@ -1,17 +1,4 @@
['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
@@ -153,6 +141,15 @@ 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'],
DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window'],
@@ -161,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'],
@@ -168,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
@@ -218,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()
@@ -267,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] === ':') {
@@ -299,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);
@@ -317,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:']));
@@ -341,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;
@@ -386,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);
@@ -412,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 3488b8604..b9e1b5ee4 100644
--- a/lib/Remote/JsonWireCompat.php
+++ b/lib/Remote/JsonWireCompat.php
@@ -1,20 +1,8 @@
driver = $driver;
@@ -32,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/RemoteKeyboard.php b/lib/Remote/RemoteKeyboard.php
index 40528964f..095b0c573 100644
--- a/lib/Remote/RemoteKeyboard.php
+++ b/lib/Remote/RemoteKeyboard.php
@@ -1,17 +1,4 @@
isW3cCompliant) {
$moveAction = $where ? [$this->createMoveAction($where)] : [];
@@ -67,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)] : [];
@@ -91,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,
],
]),
],
@@ -109,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' => [
[
@@ -146,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, [
@@ -163,8 +146,7 @@ public function mouseDown(WebDriverCoordinates $where = null)
$this->createMoveAction($where),
[
'type' => 'pointerDown',
- 'duration' => 0,
- 'button' => 0,
+ 'button' => self::BUTTON_LEFT,
],
],
],
@@ -181,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
) {
@@ -224,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)] : [];
@@ -242,8 +221,7 @@ public function mouseUp(WebDriverCoordinates $where = null)
'actions' => array_merge($moveAction, [
[
'type' => 'pointerUp',
- 'duration' => 0,
- 'button' => 0,
+ 'button' => self::BUTTON_LEFT,
],
]),
],
@@ -259,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);
@@ -270,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;
@@ -303,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 031d3ed76..73ebeba21 100644
--- a/lib/Remote/RemoteStatus.php
+++ b/lib/Remote/RemoteStatus.php
@@ -1,17 +1,4 @@
executor = $executor;
$this->driver = $driver;
@@ -40,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()
{
@@ -54,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)
{
@@ -75,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'
);
}
@@ -100,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()
{
@@ -110,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)
{
@@ -130,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 8c3981474..951c8619a 100644
--- a/lib/Remote/RemoteTouchScreen.php
+++ b/lib/Remote/RemoteTouchScreen.php
@@ -1,17 +1,4 @@
executor = $executor;
}
/**
- * @param WebDriverElement $element
- *
* @return RemoteTouchScreen The instance.
*/
public function tap(WebDriverElement $element)
@@ -52,8 +34,6 @@ public function tap(WebDriverElement $element)
}
/**
- * @param WebDriverElement $element
- *
* @return RemoteTouchScreen The instance.
*/
public function doubleTap(WebDriverElement $element)
@@ -99,7 +79,6 @@ public function flick($xspeed, $yspeed)
}
/**
- * @param WebDriverElement $element
* @param int $xoffset
* @param int $yoffset
* @param int $speed
@@ -119,8 +98,6 @@ public function flickFromElement(WebDriverElement $element, $xoffset, $yoffset,
}
/**
- * @param WebDriverElement $element
- *
* @return RemoteTouchScreen The instance.
*/
public function longPress(WebDriverElement $element)
@@ -166,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 bac55c9b1..3d65aaf0b 100644
--- a/lib/Remote/RemoteWebDriver.php
+++ b/lib/Remote/RemoteWebDriver.php
@@ -1,22 +1,12 @@
executor = $commandExecutor;
$this->sessionID = $sessionId;
$this->isW3cCompliant = $isW3cCompliant;
-
- if ($capabilities !== null) {
- $this->capabilities = $capabilities;
- }
+ $this->capabilities = $capabilities;
}
/**
@@ -104,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);
@@ -122,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
@@ -135,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(
@@ -183,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);
@@ -192,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);
}
/**
@@ -207,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
*/
@@ -227,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
*/
@@ -238,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));
@@ -307,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()
@@ -374,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);
}
/**
@@ -557,7 +547,7 @@ public function getSessionID()
/**
* Get capabilities of the RemoteWebDriver.
*
- * @return WebDriverCapabilities
+ * @return WebDriverCapabilities|null
*/
public function getCapabilities()
{
@@ -567,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
@@ -587,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,
@@ -602,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
@@ -611,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)
@@ -680,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 6b2befb2a..e0ce43b55 100644
--- a/lib/Remote/RemoteWebElement.php
+++ b/lib/Remote/RemoteWebElement.php
@@ -1,24 +1,16 @@
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;
}
@@ -94,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)
@@ -115,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
*/
@@ -129,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));
@@ -138,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)
{
@@ -169,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.
*
@@ -211,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']);
}
@@ -227,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();
@@ -318,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
*/
@@ -334,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
@@ -396,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]],
]);
@@ -432,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, [
@@ -476,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
*
@@ -493,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 28cc5e438..5e3ef8399 100644
--- a/lib/Remote/Service/DriverCommandExecutor.php
+++ b/lib/Remote/Service/DriverCommandExecutor.php
@@ -1,20 +1,8 @@
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 eabf24e7a..028b8cd83 100644
--- a/lib/Remote/Service/DriverService.php
+++ b/lib/Remote/Service/DriverService.php
@@ -1,27 +1,15 @@
executable = self::checkExecutable($executable);
+ $this->setExecutable($executable);
$this->url = sprintf('http://localhost:%d', $port);
$this->args = $args;
$this->environment = $environment ?: $_ENV;
@@ -84,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');
@@ -121,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/UselessFileDetector.php b/lib/Remote/UselessFileDetector.php
index 5ddd78e7d..6bce0e004 100644
--- a/lib/Remote/UselessFileDetector.php
+++ b/lib/Remote/UselessFileDetector.php
@@ -1,17 +1,4 @@
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
*/
@@ -46,7 +43,7 @@ public function getName()
}
/**
- * @return string
+ * @return string|null Could be null for newSession command
*/
public function getSessionID()
{
diff --git a/lib/Remote/WebDriverResponse.php b/lib/Remote/WebDriverResponse.php
index 92ac6f66d..39b19bf09 100644
--- a/lib/Remote/WebDriverResponse.php
+++ b/lib/Remote/WebDriverResponse.php
@@ -1,17 +1,4 @@
dispatcher = $dispatcher ?: new WebDriverDispatcher();
if (!$this->dispatcher->getDefaultDriver()) {
@@ -76,6 +59,7 @@ public function getWebDriver()
public function get($url)
{
$this->dispatch('beforeNavigateTo', $url, $this);
+
try {
$this->driver->get($url);
} catch (WebDriverException $exception) {
@@ -88,7 +72,6 @@ public function get($url)
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return array
*/
@@ -112,7 +95,6 @@ public function findElements(WebDriverBy $by)
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return EventFiringWebElement
*/
@@ -134,7 +116,6 @@ public function findElement(WebDriverBy $by)
/**
* @param string $script
- * @param array $arguments
* @throws WebDriverException
* @return mixed
*/
@@ -162,7 +143,6 @@ public function executeScript($script, array $arguments = [])
/**
* @param string $script
- * @param array $arguments
* @throws WebDriverException
* @return mixed
*/
@@ -175,6 +155,7 @@ public function executeAsyncScript($script, array $arguments = [])
}
$this->dispatch('beforeScript', $script, $this);
+
try {
$result = $this->driver->executeAsyncScript($script, $arguments);
} catch (WebDriverException $exception) {
@@ -386,7 +367,6 @@ public function execute($name, $params)
}
/**
- * @param WebDriverElement $element
* @return EventFiringWebElement
*/
protected function newElement(WebDriverElement $element)
@@ -407,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 0d26a39f1..27ea34205 100644
--- a/lib/Support/Events/EventFiringWebDriverNavigation.php
+++ b/lib/Support/Events/EventFiringWebDriverNavigation.php
@@ -1,17 +1,4 @@
navigator = $navigator;
@@ -62,6 +45,7 @@ public function back()
'beforeNavigateBack',
$this->getDispatcher()->getDefaultDriver()
);
+
try {
$this->navigator->back();
} catch (WebDriverException $exception) {
@@ -81,6 +65,7 @@ public function forward()
'beforeNavigateForward',
$this->getDispatcher()->getDefaultDriver()
);
+
try {
$this->navigator->forward();
} catch (WebDriverException $exception) {
@@ -143,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 8df5cbb78..6caa08684 100644
--- a/lib/Support/Events/EventFiringWebElement.php
+++ b/lib/Support/Events/EventFiringWebElement.php
@@ -1,17 +1,4 @@
element = $element;
@@ -69,6 +52,7 @@ public function getElement()
public function sendKeys($value)
{
$this->dispatch('beforeChangeValueOf', $this);
+
try {
$this->element->sendKeys($value);
} catch (WebDriverException $exception) {
@@ -87,6 +71,7 @@ public function sendKeys($value)
public function click()
{
$this->dispatch('beforeClickOn', $this);
+
try {
$this->element->click();
} catch (WebDriverException $exception) {
@@ -99,7 +84,6 @@ public function click()
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return EventFiringWebElement
*/
@@ -130,7 +114,6 @@ public function findElement(WebDriverBy $by)
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return array
*/
@@ -142,6 +125,7 @@ public function findElements(WebDriverBy $by)
$this,
$this->dispatcher->getDefaultDriver()
);
+
try {
$elements = [];
foreach ($this->element->findElements($by) as $element) {
@@ -365,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)
@@ -378,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(
@@ -404,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/Support/XPathEscaper.php b/lib/Support/XPathEscaper.php
index c90cefa87..eb85081ca 100644
--- a/lib/Support/XPathEscaper.php
+++ b/lib/Support/XPathEscaper.php
@@ -1,17 +1,4 @@
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 0a50ccdde..7f6bb3ece 100644
--- a/lib/WebDriverCommandExecutor.php
+++ b/lib/WebDriverCommandExecutor.php
@@ -1,17 +1,4 @@
findElement($by)->isDisplayed();
- } catch (NoSuchElementException $e) {
- return true;
- } catch (StaleElementReferenceException $e) {
+ } catch (NoSuchElementException|StaleElementReferenceException $e) {
return true;
}
}
@@ -410,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;
}
}
@@ -435,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;
@@ -538,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');
}
/**
@@ -558,7 +543,7 @@ function (WebDriver $driver) {
$alert->getText();
return $alert;
- } catch (NoAlertOpenException $e) {
+ } catch (NoSuchAlertException $e) {
return null;
}
}
diff --git a/lib/WebDriverHasInputDevices.php b/lib/WebDriverHasInputDevices.php
index 7cb1992eb..efe41ae42 100644
--- a/lib/WebDriverHasInputDevices.php
+++ b/lib/WebDriverHasInputDevices.php
@@ -1,17 +1,4 @@
executor->execute(
diff --git a/lib/WebDriverPlatform.php b/lib/WebDriverPlatform.php
index 5609eb0b8..a589f3013 100644
--- a/lib/WebDriverPlatform.php
+++ b/lib/WebDriverPlatform.php
@@ -1,17 +1,4 @@
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 4d784c682..5fb1daaf9 100644
--- a/lib/WebDriverSearchContext.php
+++ b/lib/WebDriverSearchContext.php
@@ -1,33 +1,19 @@
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()
@@ -236,7 +230,6 @@ public function deselectByVisiblePartialText($text)
/**
* Mark option selected
- * @param WebDriverElement $option
*/
protected function selectOption(WebDriverElement $option)
{
@@ -247,7 +240,6 @@ protected function selectOption(WebDriverElement $option)
/**
* Mark option not selected
- * @param WebDriverElement $option
*/
protected function deselectOption(WebDriverElement $option)
{
diff --git a/lib/WebDriverSelectInterface.php b/lib/WebDriverSelectInterface.php
index 830416a51..030a783e9 100644
--- a/lib/WebDriverSelectInterface.php
+++ b/lib/WebDriverSelectInterface.php
@@ -1,17 +1,4 @@
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;
@@ -66,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)
@@ -74,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;
@@ -91,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)
@@ -99,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 e8a480c9e..3b045df4b 100644
--- a/lib/WebDriverUpAction.php
+++ b/lib/WebDriverUpAction.php
@@ -1,17 +1,4 @@
driver = $driver;
- $this->timeout = isset($timeout_in_second) ? $timeout_in_second : 30;
+ $this->timeout = $timeout_in_second ?? 30;
$this->interval = $interval_in_millisecond ?: 250;
}
@@ -51,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 c613732fa..2a69fe240 100644
--- a/lib/WebDriverWindow.php
+++ b/lib/WebDriverWindow.php
@@ -1,21 +1,9 @@
executor->execute(
- DriverCommand::MAXIMIZE_WINDOW,
- [':windowHandle' => 'current']
- );
+ if ($this->isW3cCompliant) {
+ $this->executor->execute(DriverCommand::MAXIMIZE_WINDOW, []);
+ } else {
+ $this->executor->execute(
+ DriverCommand::MAXIMIZE_WINDOW,
+ [':windowHandle' => 'current']
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Makes the current window full screen.
+ *
+ * @return WebDriverWindow The instance.
+ */
+ public function fullscreen()
+ {
+ if (!$this->isW3cCompliant) {
+ throw new UnsupportedOperationException('The Fullscreen window command is only supported in W3C mode');
+ }
+
+ $this->executor->execute(DriverCommand::FULLSCREEN_WINDOW, []);
return $this;
}
@@ -113,7 +121,6 @@ public function maximize()
* 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)
@@ -132,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)
@@ -168,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 801396034..d9cb0c2c7 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,16 +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 41069ecef..9b6c46336 100644
--- a/tests/functional/Chrome/ChromeDriverServiceTest.php
+++ b/tests/functional/Chrome/ChromeDriverServiceTest.php
@@ -1,20 +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 2c10194de..407b65bd8 100644
--- a/tests/functional/Chrome/ChromeDriverTest.php
+++ b/tests/functional/Chrome/ChromeDriverTest.php
@@ -1,68 +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 1029a2363..e812353ea 100644
--- a/tests/functional/FileUploadTest.php
+++ b/tests/functional/FileUploadTest.php
@@ -1,17 +1,4 @@
-driver->get($this->getTestPageUrl('upload.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::UPLOAD));
$fileElement = $this->driver->findElement(WebDriverBy::name('upload'));
@@ -56,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 ce0064de8..897d33cf7 100644
--- a/tests/functional/RemoteKeyboardTest.php
+++ b/tests/functional/RemoteKeyboardTest.php
@@ -1,42 +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);
@@ -68,7 +53,7 @@ public function testShouldPressSendAndReleaseKeys()
'keyup "Shift"',
'keyup "f"',
],
- $this->retrieveLoggedEvents()
+ $this->retrieveLoggedKeyboardEvents()
);
} else {
$this->assertEquals(
@@ -92,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 4e6fb5803..a3080e67a 100644
--- a/tests/functional/RemoteTargetLocatorTest.php
+++ b/tests/functional/RemoteTargetLocatorTest.php
@@ -1,20 +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();
@@ -36,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());
/**
@@ -50,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);
@@ -69,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'
);
@@ -138,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 455313354..a39a91070 100644
--- a/tests/functional/RemoteWebDriverCreateTest.php
+++ b/tests/functional/RemoteWebDriverCreateTest.php
@@ -1,33 +1,21 @@
-driver = RemoteWebDriver::create(
$this->serverUrl,
@@ -44,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();
@@ -66,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();
@@ -85,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(
@@ -94,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 c5671b497..f638aa527 100644
--- a/tests/functional/RemoteWebDriverFindElementTest.php
+++ b/tests/functional/RemoteWebDriverFindElementTest.php
@@ -1,17 +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);
}
@@ -65,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 59d9d108d..95a01b9b7 100644
--- a/tests/functional/RemoteWebDriverTest.php
+++ b/tests/functional/RemoteWebDriverTest.php
@@ -1,23 +1,9 @@
-driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$this->assertEquals(
'php-webdriver test page',
@@ -41,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());
}
@@ -51,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 facebook/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
@@ -76,7 +62,7 @@ public function testShouldGetSessionId()
$sessionId = $this->driver->getSessionID();
- $this->assertInternalType('string', $sessionId);
+ $this->assertIsString($sessionId);
$this->assertNotEmpty($sessionId);
}
@@ -84,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]);
@@ -105,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());
}
@@ -130,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);
@@ -154,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));
@@ -172,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());
@@ -202,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());
@@ -242,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'));
@@ -264,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));
@@ -287,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));
@@ -332,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 bd6e4f6f0..6a21cb36c 100644
--- a/tests/functional/RemoteWebElementTest.php
+++ b/tests/functional/RemoteWebElementTest.php
@@ -1,23 +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'));
@@ -86,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'));
@@ -97,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')
);
@@ -106,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'));
@@ -118,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'));
@@ -152,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'));
@@ -165,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'));
@@ -189,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]')
@@ -217,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'));
@@ -235,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'));
@@ -253,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'));
@@ -270,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'));
@@ -293,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'));
@@ -314,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
+
+
+
+
+
+