diff --git a/.gitattributes b/.gitattributes
index 47a5c0fdf..2fdc4ff88 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,11 +2,14 @@
/scripts export-ignore
/tests export-ignore
+/tools export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/example.php export-ignore
/phpunit.xml.dist export-ignore
/.github export-ignore
-/.php_cs.dist export-ignore
+/.php-cs-fixer.dist.php export-ignore
/phpstan.neon export-ignore
/.coveralls.yml export-ignore
+/logs export-ignore
+/mlc_config.json export-ignore
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index c10d85094..a79b02b1f 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -27,8 +27,6 @@ Here is the [official documentation](https://www.selenium.dev/documentation/en/)
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 preinstall # Run this only on the first run - this will install some additional dependencies
-
composer all
```
@@ -63,7 +61,7 @@ then start the local PHP server which will serve the test pages and then run the
```sh
export BROWSER_NAME="htmlunit" # see below for other browsers
-java -jar selenium-server-standalone-X.X.X.jar -log selenium.log &
+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
@@ -71,7 +69,7 @@ composer all
composer test -- --testsuite functional
```
-If you want to run tests in different browser then "htmlunit" (Chrome or Firefox), you need to setup the browser driver (Chromedriver/Geckodriver), as it is [explained in wiki](https://github.com/php-webdriver/php-webdriver/wiki/Chrome)
+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
@@ -87,3 +85,10 @@ 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.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index dcfc43785..000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,65 +0,0 @@
----
-name: 🐛 Bug report
-about: Create a bug report to help us improve php-webdriver
-title: ''
-labels: bug
-assignees: ''
----
-
-
-
-### Bug description
-
-
-### How could the issue be reproduced
-
-Steps to reproduce the behavior:
-1.
-2.
-3. ...
-
-
-
-```php
-// You can insert your PHP code here (or remove this block if it is not relevant for the issue).
-
-// For example you can provide how you create WebDrivere 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();
-```
-
-```html
-
-
- Foo
-
-```
-
-### Expected behavior
-
-
-### Details
-
-
-* Php-webdriver version:
-* PHP version:
-* How do you start the browser driver or Selenium server:
-
-
-* Selenium server version:
-* Browser driver (chromedriver/geckodriver...) version:
-* Browser used + version:
-* Operating system:
-
-### Additional context
-
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/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/no-response.yml b/.github/no-response.yml
deleted file mode 100644
index 320baf22f..000000000
--- a/.github/no-response.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# Configuration for probot-no-response - https://github.com/probot/no-response
-
-# Number of days of inactivity before an Issue is closed for lack of response
-daysUntilClose: 14
-
-# Label requiring a response
-responseRequiredLabel: "waiting for reaction"
-
-# Comment to post when closing an Issue for lack of response. Set to `false` to disable
-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/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
index e183bdfac..812fbfe25 100644
--- a/.github/workflows/sauce-labs.yaml
+++ b/.github/workflows/sauce-labs.yaml
@@ -1,5 +1,8 @@
name: Sauce Labs
+permissions:
+ contents: read
+
on:
push:
schedule:
@@ -8,6 +11,8 @@ on:
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 }}
@@ -15,21 +20,20 @@ jobs:
strategy:
fail-fast: false
matrix:
- w3c: [true]
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 10", w3c: false, tunnel-id: "gh-1" }
- - { name: "Chrome latest, W3C protocol", BROWSER_NAME: "chrome", VERSION: "latest", PLATFORM: "Windows 10", tunnel-id: "gh-2" }
- - { name: "Edge latest, W3C protocol", BROWSER_NAME: "MicrosoftEdge", VERSION: "latest", PLATFORM: "Windows 10", tunnel-id: "gh-3" }
+ - { 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-id }})
+ name: ${{ matrix.name }} (${{ matrix.tunnel-name }})
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- php-version: '7.4'
+ php-version: '8.3'
extensions: mbstring, intl, zip
coverage: none
@@ -41,11 +45,13 @@ jobs:
php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log &
- name: Start Sauce Connect
- uses: saucelabs/sauce-connect-action@v1
+ uses: saucelabs/sauce-connect-action@v3
with:
username: ${{ secrets.SAUCE_USERNAME }}
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
- tunnelIdentifier: ${{ matrix.tunnel-id }}
+ tunnelName: ${{ matrix.tunnel-name }}
+ proxyLocalhost: allow
+ region: 'us-west-1'
- name: Run tests
env:
@@ -53,7 +59,7 @@ jobs:
VERSION: ${{ matrix.VERSION }}
PLATFORM: ${{ matrix.PLATFORM }}
DISABLE_W3C_PROTOCOL: "${{ matrix.w3c && '0' || '1' }}"
- SAUCE_TUNNEL_IDENTIFIER: ${{ matrix.tunnel-id }}
+ 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
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 45e5761e9..d215e92ce 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -1,5 +1,8 @@
name: Tests
+permissions:
+ contents: read
+
on:
push:
pull_request:
@@ -11,16 +14,16 @@ jobs:
name: "Code style and static analysis"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- php-version: '7.4'
+ php-version: '8.3'
extensions: mbstring, intl, zip
- name: Install PHP dependencies
- run: composer preinstall
+ run: composer update --no-interaction
- name: Lint
run: composer lint
@@ -32,8 +35,8 @@ jobs:
name: "Markdown link check"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: gaurav-nelson/github-action-markdown-link-check@v1
+ - uses: actions/checkout@v6
+ - uses: tcort/github-action-markdown-link-check@v1
with:
use-verbose-mode: 'yes'
@@ -43,14 +46,13 @@ jobs:
strategy:
matrix:
- php-version: ['5.6', '7.1', '7.2', '7.3', '7.4', '8.0']
+ php-version: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
dependencies: ['']
include:
- - { php-version: '5.6', dependencies: '--prefer-lowest' }
- - { php-version: '8.1', dependencies: '--ignore-platform-req=php' }
+ - { php-version: '7.3', dependencies: '--prefer-lowest' }
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -63,10 +65,6 @@ jobs:
- name: Install PHP dependencies
run: composer update --no-interaction ${{ matrix.dependencies }}
- - name: Apply PHPUnit patches
- if: ${{ matrix.php-version < 7.2 }}
- run: scripts/apply-phpunit-patches.sh
-
- name: Run tests
run: vendor/bin/phpunit --testsuite unit --colors=always --coverage-clover ./logs/clover.xml
@@ -84,7 +82,7 @@ jobs:
functional-tests:
runs-on: ${{ matrix.os }}
env:
- SELENIUM_SERVER_DOWNLOAD_URL: http://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar
+ 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
@@ -101,12 +99,12 @@ jobs:
name: "Functional tests (${{ matrix.browser }}, Selenium server: ${{ matrix.selenium-server }}, W3C: ${{ matrix.w3c }})"
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- php-version: 7.4
+ php-version: '8.3'
extensions: mbstring, intl, zip
coverage: xdebug
@@ -116,20 +114,20 @@ jobs:
- 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.
+ # 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
- wget -q -t 3 -O build/selenium-server-standalone.jar $SELENIUM_SERVER_DOWNLOAD_URL
- java -jar build/selenium-server-standalone.jar -version
- xvfb-run --server-args="-screen 0, 1280x720x24" --auto-servernum java -jar build/selenium-server-standalone.jar -log logs/selenium-server.log &
+ 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 --url-base=/wd/hub &> ./logs/chromedriver.log &
+ chromedriver --port=4444 &> ./logs/chromedriver.log &
- name: Start GeckoDriver
if: ${{ !matrix.selenium-server && matrix.browser == 'firefox' }}
@@ -150,6 +148,7 @@ jobs:
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 d5605367c..a9384110d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +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 a438f2095..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' => false,
- '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/CHANGELOG.md b/CHANGELOG.md
index 085f24450..a468c26f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,84 @@ 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.
diff --git a/README.md b/README.md
index 689a497ab..f3e8d3801 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# php-webdriver – Selenium WebDriver bindings for PHP
[](https://packagist.org/packages/php-webdriver/webdriver)
-[](https://github.com/php-webdriver/php-webdriver/actions)
-[](https://saucelabs.com/u/php-webdriver)
+[](https://github.com/php-webdriver/php-webdriver/actions)
+[](https://saucelabs.com/u/php-webdriver)
[](https://packagist.org/packages/php-webdriver/webdriver)
## Description
@@ -10,13 +10,11 @@ Php-webdriver library is PHP language binding for Selenium WebDriver, which allo
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/).
+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
@@ -58,7 +56,7 @@ This could be Selenium standalone server, but for local development, you can sen
📙 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).
-Install the latest Chrome and [Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads).
+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!
Run `chromedriver` binary, you can pass `port` argument, so that it listens on port 4444:
@@ -208,8 +206,8 @@ 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
diff --git a/composer.json b/composer.json
index d5d3f22f5..f44663100 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,8 @@
{
"name": "php-webdriver/webdriver",
- "type": "library",
"description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.",
+ "license": "MIT",
+ "type": "library",
"keywords": [
"webdriver",
"selenium",
@@ -10,34 +11,32 @@
"chromedriver"
],
"homepage": "https://github.com/php-webdriver/php-webdriver",
- "license": "MIT",
"require": {
- "php": "^5.6 || ~7.0 || ^8.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"
- },
- "replace": {
- "facebook/webdriver": "*"
+ "symfony/process": "^5.0 || ^6.0 || ^7.0 || ^8.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^2.0",
- "ondram/ci-detector": "^2.1 || ^3.5 || ^4.0",
+ "ergebnis/composer-normalize": "^2.20.0",
+ "ondram/ci-detector": "^4.0",
"php-coveralls/php-coveralls": "^2.4",
- "php-mock/php-mock-phpunit": "^1.1 || ^2.0",
+ "php-mock/php-mock-phpunit": "^2.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
- "phpunit/phpunit": "^5.7 || ^7 || ^8 || ^9",
+ "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"
},
- "suggest": {
- "ext-SimpleXML": "For Firefox profile creation"
+ "replace": {
+ "facebook/webdriver": "*"
},
- "config": {
- "sort-packages": true
+ "suggest": {
+ "ext-simplexml": "For Firefox profile creation"
},
+ "minimum-stability": "dev",
+ "prefer-stable": true,
"autoload": {
"psr-4": {
"Facebook\\WebDriver\\": "lib/"
@@ -57,35 +56,43 @@
"tests/functional/"
]
},
- "minimum-stability": "beta",
+ "config": {
+ "allow-plugins": {
+ "ergebnis/composer-normalize": true
+ },
+ "sort-packages": true
+ },
"scripts": {
+ "post-install-cmd": [
+ "@composer install --working-dir=tools/php-cs-fixer --no-progress --no-interaction",
+ "@composer install --working-dir=tools/phpstan --no-progress --no-interaction"
+ ],
+ "post-update-cmd": [
+ "@composer update --working-dir=tools/php-cs-fixer --no-progress --no-interaction",
+ "@composer update --working-dir=tools/phpstan --no-progress --no-interaction"
+ ],
"all": [
"@lint",
"@analyze",
"@test"
],
"analyze": [
- "vendor/bin/phpstan analyze -c phpstan.neon --ansi",
- "vendor/bin/php-cs-fixer fix --diff --diff-format=udiff --dry-run -vvv --ansi",
- "vendor/bin/phpcs --standard=PSR2 ./lib/ ./tests/"
+ "@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",
- "vendor/bin/php-cs-fixer fix --diff --diff-format=udiff -vvv || exit 0",
- "vendor/bin/phpcbf --standard=PSR2 ./lib/ ./tests/"
+ "@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": [
- "vendor/bin/parallel-lint -j 10 ./lib ./tests example.php",
+ "@php vendor/bin/parallel-lint -j 10 ./lib ./tests example.php",
"@composer validate",
"@composer normalize --dry-run"
],
- "preinstall": [
- "@composer update --no-progress --no-interaction",
- "@composer require --dev phpstan/phpstan",
- "@composer require --dev ergebnis/composer-normalize"
- ],
"test": [
- "vendor/bin/phpunit --colors=always"
+ "@php vendor/bin/phpunit --colors=always"
]
}
}
diff --git a/example.php b/example.php
index 4491df386..92cd431d6 100644
--- a/example.php
+++ b/example.php
@@ -10,8 +10,8 @@
require_once('vendor/autoload.php');
-// This is where Selenium server 2/3 listens by default. For Selenium 4, Chromedriver or Geckodriver, use http://localhost:4444/
-$host = 'http://localhost:4444/wd/hub';
+// This is where Selenium, Chromedriver and Geckodriver 4 listens by default. For Selenium 2/3, use http://localhost:4444/wd/hub
+$host = 'http://localhost:4444/';
$capabilities = DesiredCapabilities::chrome();
diff --git a/lib/AbstractWebDriverCheckboxOrRadio.php b/lib/AbstractWebDriverCheckboxOrRadio.php
index 9cd04c967..00aee242a 100644
--- a/lib/AbstractWebDriverCheckboxOrRadio.php
+++ b/lib/AbstractWebDriverCheckboxOrRadio.php
@@ -2,9 +2,9 @@
namespace Facebook\WebDriver;
+use Facebook\WebDriver\Exception\InvalidElementStateException;
use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Exception\UnexpectedTagNameException;
-use Facebook\WebDriver\Exception\WebDriverException;
use Facebook\WebDriver\Support\XPathEscaper;
/**
@@ -30,7 +30,7 @@ public function __construct(WebDriverElement $element)
$this->name = $element->getAttribute('name');
if ($this->name === null) {
- throw new WebDriverException('The input does not have a "name" attribute.');
+ throw new InvalidElementStateException('The input does not have a "name" attribute.');
}
$this->element = $element;
@@ -66,7 +66,7 @@ public function getFirstSelectedOption()
}
throw new NoSuchElementException(
- sprintf('No %s are selected', 'radio' === $this->type ? 'radio buttons' : 'checkboxes')
+ sprintf('No %s are selected', $this->type === 'radio' ? 'radio buttons' : 'checkboxes')
);
}
diff --git a/lib/Chrome/ChromeDevToolsDriver.php b/lib/Chrome/ChromeDevToolsDriver.php
index ffbb91a18..2d95d274b 100644
--- a/lib/Chrome/ChromeDevToolsDriver.php
+++ b/lib/Chrome/ChromeDevToolsDriver.php
@@ -11,7 +11,7 @@
*/
class ChromeDevToolsDriver
{
- const SEND_COMMAND = [
+ public const SEND_COMMAND = [
'method' => 'POST',
'url' => '/session/:sessionId/goog/cdp/execute',
];
diff --git a/lib/Chrome/ChromeDriver.php b/lib/Chrome/ChromeDriver.php
index a8feac7b8..e947a498e 100644
--- a/lib/Chrome/ChromeDriver.php
+++ b/lib/Chrome/ChromeDriver.php
@@ -4,7 +4,6 @@
use Facebook\WebDriver\Local\LocalWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
-use Facebook\WebDriver\Remote\DriverCommand;
use Facebook\WebDriver\Remote\Service\DriverCommandExecutor;
use Facebook\WebDriver\Remote\WebDriverCommand;
@@ -22,8 +21,10 @@ class ChromeDriver extends LocalWebDriver
* @todo Remove $service parameter. Use `ChromeDriver::startUsingDriverService` to pass custom $service instance.
* @return static
*/
- public static function start(DesiredCapabilities $desired_capabilities = null, ChromeDriverService $service = null)
- {
+ public static function start(
+ ?DesiredCapabilities $desired_capabilities = null,
+ ?ChromeDriverService $service = null
+ ) {
if ($service === null) { // TODO: Remove the condition (always create default service)
$service = ChromeDriverService::createDefaultService();
}
@@ -41,16 +42,14 @@ public static function start(DesiredCapabilities $desired_capabilities = null, C
*/
public static function startUsingDriverService(
ChromeDriverService $service,
- DesiredCapabilities $capabilities = null
+ ?DesiredCapabilities $capabilities = null
) {
if ($capabilities === null) {
$capabilities = DesiredCapabilities::chrome();
}
$executor = new DriverCommandExecutor($service);
- $newSessionCommand = new WebDriverCommand(
- null,
- DriverCommand::NEW_SESSION,
+ $newSessionCommand = WebDriverCommand::newSession(
[
'capabilities' => [
'firstMatch' => [(object) $capabilities->toW3cCompatibleArray()],
@@ -76,9 +75,7 @@ public static function startUsingDriverService(
*/
public function startSession(DesiredCapabilities $desired_capabilities)
{
- $command = new WebDriverCommand(
- null,
- DriverCommand::NEW_SESSION,
+ $command = WebDriverCommand::newSession(
[
'capabilities' => [
'firstMatch' => [(object) $desired_capabilities->toW3cCompatibleArray()],
diff --git a/lib/Chrome/ChromeDriverService.php b/lib/Chrome/ChromeDriverService.php
index a6d3eb834..902a48ded 100644
--- a/lib/Chrome/ChromeDriverService.php
+++ b/lib/Chrome/ChromeDriverService.php
@@ -10,14 +10,14 @@ class ChromeDriverService extends DriverService
* The environment variable storing the path to the chrome driver executable.
* @deprecated Use ChromeDriverService::CHROME_DRIVER_EXECUTABLE
*/
- const CHROME_DRIVER_EXE_PROPERTY = 'webdriver.chrome.driver';
+ public const CHROME_DRIVER_EXE_PROPERTY = 'webdriver.chrome.driver';
/** @var string The environment variable storing the path to the chrome driver executable */
- const CHROME_DRIVER_EXECUTABLE = 'WEBDRIVER_CHROME_DRIVER';
+ public const CHROME_DRIVER_EXECUTABLE = 'WEBDRIVER_CHROME_DRIVER';
/**
* @var string Default executable used when no other is provided
* @internal
*/
- const DEFAULT_EXECUTABLE = 'chromedriver';
+ public const DEFAULT_EXECUTABLE = 'chromedriver';
/**
* @return static
diff --git a/lib/Chrome/ChromeOptions.php b/lib/Chrome/ChromeOptions.php
index 6599197b6..daea44c67 100644
--- a/lib/Chrome/ChromeOptions.php
+++ b/lib/Chrome/ChromeOptions.php
@@ -4,23 +4,23 @@
use Facebook\WebDriver\Remote\DesiredCapabilities;
use JsonSerializable;
+use ReturnTypeWillChange;
/**
* The class manages the capabilities in ChromeDriver.
*
- * @see https://sites.google.com/a/chromium.org/chromedriver/capabilities
+ * @see https://sites.google.com/chromium.org/driver/capabilities
*/
class ChromeOptions implements JsonSerializable
{
/**
- * The key of chromeOptions in desired capabilities (in legacy OSS JsonWire protocol)
- * @todo Replace value with 'goog:chromeOptions' after JsonWire protocol support is removed
+ * The key of chromeOptions in desired capabilities
*/
- const CAPABILITY = 'chromeOptions';
+ public const CAPABILITY = 'goog:chromeOptions';
/**
- * The key of chromeOptions in desired capabilities (in W3C compatible protocol)
+ * @deprecated Use CAPABILITY instead
*/
- const CAPABILITY_W3C = 'goog:chromeOptions';
+ public const CAPABILITY_W3C = self::CAPABILITY;
/**
* @var array
*/
@@ -43,6 +43,7 @@ class ChromeOptions implements JsonSerializable
*
* @return array
*/
+ #[ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
@@ -63,7 +64,6 @@ public function setBinary($path)
}
/**
- * @param array $arguments
* @return ChromeOptions
*/
public function addArguments(array $arguments)
@@ -77,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)
@@ -105,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
diff --git a/lib/Cookie.php b/lib/Cookie.php
index 1005bd0dc..2ae257bd1 100644
--- a/lib/Cookie.php
+++ b/lib/Cookie.php
@@ -2,14 +2,14 @@
namespace Facebook\WebDriver;
-use InvalidArgumentException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
/**
* Set values of an cookie.
*
* Implements ArrayAccess for backwards compatibility.
*
- * @see https://w3c.github.io/webdriver/webdriver-spec.html#cookies
+ * @see https://w3c.github.io/webdriver/#cookies
*/
class Cookie implements \ArrayAccess
{
@@ -36,10 +36,10 @@ public function __construct($name, $value)
public static function createFromArray(array $cookieArray)
{
if (!isset($cookieArray['name'])) {
- throw new InvalidArgumentException('Cookie name should be set');
+ throw LogicException::forError('Cookie name should be set');
}
if (!isset($cookieArray['value'])) {
- throw new InvalidArgumentException('Cookie value should be set');
+ throw LogicException::forError('Cookie value should be set');
}
$cookie = new self($cookieArray['name'], $cookieArray['value']);
@@ -107,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);
@@ -207,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) {
@@ -226,6 +242,11 @@ public function offsetSet($offset, $value)
}
}
+ /**
+ * @param mixed $offset
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
unset($this->cookie[$offset]);
@@ -237,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 ";"');
}
}
@@ -251,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 @@
+getMessage(), $this->getCode(), $previous);
- }
-}
diff --git a/lib/Exception/Internal/DriverServerDiedException.php b/lib/Exception/Internal/DriverServerDiedException.php
new file mode 100644
index 000000000..7c23df5ed
--- /dev/null
+++ b/lib/Exception/Internal/DriverServerDiedException.php
@@ -0,0 +1,16 @@
+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';
/**
* Creates a new FirefoxDriver using default configuration.
@@ -20,7 +26,7 @@ class FirefoxDriver extends LocalWebDriver
*
* @return static
*/
- public static function start(DesiredCapabilities $capabilities = null)
+ public static function start(?DesiredCapabilities $capabilities = null)
{
$service = FirefoxDriverService::createDefaultService();
@@ -37,16 +43,14 @@ public static function start(DesiredCapabilities $capabilities = null)
*/
public static function startUsingDriverService(
FirefoxDriverService $service,
- DesiredCapabilities $capabilities = null
+ ?DesiredCapabilities $capabilities = null
) {
if ($capabilities === null) {
$capabilities = DesiredCapabilities::firefox();
}
$executor = new DriverCommandExecutor($service);
- $newSessionCommand = new WebDriverCommand(
- null,
- DriverCommand::NEW_SESSION,
+ $newSessionCommand = WebDriverCommand::newSession(
[
'capabilities' => [
'firstMatch' => [(object) $capabilities->toW3cCompatibleArray()],
diff --git a/lib/Firefox/FirefoxDriverService.php b/lib/Firefox/FirefoxDriverService.php
index 525c5b5bc..83c6a28fb 100644
--- a/lib/Firefox/FirefoxDriverService.php
+++ b/lib/Firefox/FirefoxDriverService.php
@@ -9,12 +9,12 @@ class FirefoxDriverService extends DriverService
/**
* @var string Name of the environment variable storing the path to the driver binary
*/
- const WEBDRIVER_FIREFOX_DRIVER = 'WEBDRIVER_FIREFOX_DRIVER';
+ public const WEBDRIVER_FIREFOX_DRIVER = 'WEBDRIVER_FIREFOX_DRIVER';
/**
* @var string Default executable used when no other is provided
* @internal
*/
- const DEFAULT_EXECUTABLE = 'geckodriver';
+ public const DEFAULT_EXECUTABLE = 'geckodriver';
/**
* @return static
diff --git a/lib/Firefox/FirefoxOptions.php b/lib/Firefox/FirefoxOptions.php
index fc16fdb75..84cf94cef 100644
--- a/lib/Firefox/FirefoxOptions.php
+++ b/lib/Firefox/FirefoxOptions.php
@@ -2,6 +2,9 @@
namespace Facebook\WebDriver\Firefox;
+use Facebook\WebDriver\Exception\Internal\LogicException;
+use ReturnTypeWillChange;
+
/**
* Class to manage Firefox-specific capabilities
*
@@ -10,11 +13,13 @@
class FirefoxOptions implements \JsonSerializable
{
/** @var string The key of FirefoxOptions in desired capabilities */
- const CAPABILITY = 'moz:firefoxOptions';
+ public const CAPABILITY = 'moz:firefoxOptions';
+ /** @var string */
+ public const OPTION_ARGS = 'args';
/** @var string */
- const OPTION_ARGS = 'args';
+ public const OPTION_PREFS = 'prefs';
/** @var string */
- const OPTION_PREFS = 'prefs';
+ public const OPTION_PROFILE = 'profile';
/** @var array */
private $options = [];
@@ -22,6 +27,8 @@ class FirefoxOptions implements \JsonSerializable
private $arguments = [];
/** @var array */
private $preferences = [];
+ /** @var FirefoxProfile */
+ private $profile;
public function __construct()
{
@@ -43,10 +50,13 @@ public function __construct()
public function setOption($name, $value)
{
if ($name === self::OPTION_PREFS) {
- throw new \InvalidArgumentException('Use setPreference() method to set Firefox preferences');
+ throw LogicException::forError('Use setPreference() method to set Firefox preferences');
}
if ($name === self::OPTION_ARGS) {
- throw new \InvalidArgumentException('Use addArguments() method to add Firefox arguments');
+ 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;
@@ -85,6 +95,17 @@ public function setPreference($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
*/
@@ -97,10 +118,14 @@ public function toArray()
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 159a9c806..2a33fb003 100644
--- a/lib/Firefox/FirefoxPreferences.php
+++ b/lib/Firefox/FirefoxPreferences.php
@@ -11,13 +11,13 @@
class FirefoxPreferences
{
/** @var string Port WebDriver uses to communicate with Firefox instance */
- const WEBDRIVER_FIREFOX_PORT = 'webdriver_firefox_port';
+ public const WEBDRIVER_FIREFOX_PORT = 'webdriver_firefox_port';
/** @var string Should the reader view (FF 38+) be enabled? */
- const READER_PARSE_ON_LOAD_ENABLED = 'reader.parse-on-load.enabled';
+ public const READER_PARSE_ON_LOAD_ENABLED = 'reader.parse-on-load.enabled';
/** @var string Browser homepage */
- const BROWSER_STARTUP_HOMEPAGE = 'browser.startup.homepage';
+ public const BROWSER_STARTUP_HOMEPAGE = 'browser.startup.homepage';
/** @var string Should the Devtools JSON view be enabled? */
- const DEVTOOLS_JSONVIEW = 'devtools.jsonview.enabled';
+ public const DEVTOOLS_JSONVIEW = 'devtools.jsonview.enabled';
private function __construct()
{
diff --git a/lib/Firefox/FirefoxProfile.php b/lib/Firefox/FirefoxProfile.php
index cce9baaf4..95e8004a8 100644
--- a/lib/Firefox/FirefoxProfile.php
+++ b/lib/Firefox/FirefoxProfile.php
@@ -2,7 +2,9 @@
namespace Facebook\WebDriver\Firefox;
-use Facebook\WebDriver\Exception\WebDriverException;
+use Facebook\WebDriver\Exception\Internal\IOException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
+use Facebook\WebDriver\Exception\Internal\RuntimeException;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
@@ -71,7 +73,7 @@ public function setRdfFile($rdf_file)
/**
* @param string $key
* @param string|bool|int $value
- * @throws WebDriverException
+ * @throws LogicException
* @return FirefoxProfile
*/
public function setPreference($key, $value)
@@ -85,7 +87,7 @@ public function setPreference($key, $value)
if (is_bool($value)) {
$value = $value ? 'true' : 'false';
} else {
- throw new WebDriverException(
+ throw LogicException::forError(
'The value of the preference should be either a string, int or bool.'
);
}
@@ -184,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 = '')
@@ -239,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
+ );
}
}
@@ -269,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)
@@ -280,12 +267,52 @@ private function extractTo($xpi, $target_dir)
$zip->extractTo($target_dir);
$zip->close();
} else {
- throw new \Exception("Failed to open the firefox extension. '$xpi'");
+ throw IOException::forFileError('Failed to open the firefox extension.', $xpi);
}
} else {
- throw new \Exception("Firefox extension doesn't exist. '$xpi'");
+ throw IOException::forFileError('Firefox extension doesn\'t exist.', $xpi);
}
return $this;
}
+
+ private function parseExtensionName($extensionPath)
+ {
+ $temp_dir = $this->createTempDirectory();
+
+ $this->extractTo($extensionPath, $temp_dir);
+
+ $mozillaRsaPath = $temp_dir . '/META-INF/mozilla.rsa';
+ $mozillaRsaBinaryData = file_get_contents($mozillaRsaPath);
+ $mozillaRsaHex = bin2hex($mozillaRsaBinaryData);
+
+ //We need to find the plugin id. This is the second occurrence of object identifier "2.5.4.3 commonName".
+
+ //That is marker "2.5.4.3 commonName" in hex:
+ $objectIdentifierHexMarker = '0603550403';
+
+ $firstMarkerPosInHex = strpos($mozillaRsaHex, $objectIdentifierHexMarker); // phpcs:ignore
+
+ $secondMarkerPosInHexString =
+ strpos($mozillaRsaHex, $objectIdentifierHexMarker, $firstMarkerPosInHex + 2); // phpcs:ignore
+
+ if ($secondMarkerPosInHexString === false) {
+ throw RuntimeException::forError('Cannot install extension. Cannot fetch extension commonName');
+ }
+
+ // phpcs:ignore
+ $commonNameStringPositionInBinary = ($secondMarkerPosInHexString + strlen($objectIdentifierHexMarker)) / 2;
+
+ $commonNameStringLength = ord($mozillaRsaBinaryData[$commonNameStringPositionInBinary + 1]);
+ // phpcs:ignore
+ $extensionCommonName = substr(
+ $mozillaRsaBinaryData,
+ $commonNameStringPositionInBinary + 2,
+ $commonNameStringLength
+ );
+
+ $this->deleteDirectory($temp_dir);
+
+ return $extensionCommonName;
+ }
}
diff --git a/lib/Interactions/Internal/WebDriverCoordinates.php b/lib/Interactions/Internal/WebDriverCoordinates.php
index 387bdbea6..3a92f0a64 100644
--- a/lib/Interactions/Internal/WebDriverCoordinates.php
+++ b/lib/Interactions/Internal/WebDriverCoordinates.php
@@ -11,7 +11,8 @@
class WebDriverCoordinates
{
/**
- * @var null
+ * @var mixed
+ * @todo remove in next major version (if it is unused)
*/
private $onScreen;
/**
@@ -28,9 +29,7 @@ class WebDriverCoordinates
private $auxiliary;
/**
- * @param null $on_screen
- * @param callable $in_view_port
- * @param callable $on_page
+ * @param mixed $on_screen
* @param string $auxiliary
*/
public function __construct($on_screen, callable $in_view_port, callable $on_page, $auxiliary)
diff --git a/lib/Interactions/Internal/WebDriverKeysRelatedAction.php b/lib/Interactions/Internal/WebDriverKeysRelatedAction.php
index 69f4aa179..a5ba0875f 100644
--- a/lib/Interactions/Internal/WebDriverKeysRelatedAction.php
+++ b/lib/Interactions/Internal/WebDriverKeysRelatedAction.php
@@ -24,15 +24,10 @@ abstract class WebDriverKeysRelatedAction
*/
protected $locationProvider;
- /**
- * @param WebDriverKeyboard $keyboard
- * @param WebDriverMouse $mouse
- * @param WebDriverLocatable $location_provider
- */
public function __construct(
WebDriverKeyboard $keyboard,
WebDriverMouse $mouse,
- WebDriverLocatable $location_provider = null
+ ?WebDriverLocatable $location_provider = null
) {
$this->keyboard = $keyboard;
$this->mouse = $mouse;
diff --git a/lib/Interactions/Internal/WebDriverMouseAction.php b/lib/Interactions/Internal/WebDriverMouseAction.php
index 5cb0cfd10..ecb1127ee 100644
--- a/lib/Interactions/Internal/WebDriverMouseAction.php
+++ b/lib/Interactions/Internal/WebDriverMouseAction.php
@@ -19,11 +19,7 @@ class WebDriverMouseAction
*/
protected $locationProvider;
- /**
- * @param WebDriverMouse $mouse
- * @param WebDriverLocatable|null $location_provider
- */
- public function __construct(WebDriverMouse $mouse, WebDriverLocatable $location_provider = null)
+ public function __construct(WebDriverMouse $mouse, ?WebDriverLocatable $location_provider = null)
{
$this->mouse = $mouse;
$this->locationProvider = $location_provider;
diff --git a/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php b/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php
index 98fd824d7..c865da46a 100644
--- a/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php
+++ b/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php
@@ -18,14 +18,12 @@ class WebDriverMoveToOffsetAction extends WebDriverMouseAction implements WebDri
private $yOffset;
/**
- * @param WebDriverMouse $mouse
- * @param WebDriverLocatable|null $location_provider
* @param int|null $x_offset
* @param int|null $y_offset
*/
public function __construct(
WebDriverMouse $mouse,
- WebDriverLocatable $location_provider = null,
+ ?WebDriverLocatable $location_provider = null,
$x_offset = null,
$y_offset = null
) {
diff --git a/lib/Interactions/Internal/WebDriverSendKeysAction.php b/lib/Interactions/Internal/WebDriverSendKeysAction.php
index 4e65cc27c..2ed3cfd06 100644
--- a/lib/Interactions/Internal/WebDriverSendKeysAction.php
+++ b/lib/Interactions/Internal/WebDriverSendKeysAction.php
@@ -15,15 +15,12 @@ class WebDriverSendKeysAction extends WebDriverKeysRelatedAction implements WebD
private $keys = '';
/**
- * @param WebDriverKeyboard $keyboard
- * @param WebDriverMouse $mouse
- * @param WebDriverLocatable $location_provider
* @param string $keys
*/
public function __construct(
WebDriverKeyboard $keyboard,
WebDriverMouse $mouse,
- WebDriverLocatable $location_provider = null,
+ ?WebDriverLocatable $location_provider = null,
$keys = ''
) {
parent::__construct($keyboard, $mouse, $location_provider);
diff --git a/lib/Interactions/Internal/WebDriverSingleKeyAction.php b/lib/Interactions/Internal/WebDriverSingleKeyAction.php
index 9b1a014d5..6efe9384a 100644
--- a/lib/Interactions/Internal/WebDriverSingleKeyAction.php
+++ b/lib/Interactions/Internal/WebDriverSingleKeyAction.php
@@ -2,6 +2,7 @@
namespace Facebook\WebDriver\Interactions\Internal;
+use Facebook\WebDriver\Exception\Internal\LogicException;
use Facebook\WebDriver\Internal\WebDriverLocatable;
use Facebook\WebDriver\WebDriverAction;
use Facebook\WebDriver\WebDriverKeyboard;
@@ -10,7 +11,7 @@
abstract class WebDriverSingleKeyAction extends WebDriverKeysRelatedAction implements WebDriverAction
{
- const MODIFIER_KEYS = [
+ public const MODIFIER_KEYS = [
WebDriverKeys::SHIFT,
WebDriverKeys::LEFT_SHIFT,
WebDriverKeys::RIGHT_SHIFT,
@@ -35,13 +36,13 @@ abstract class WebDriverSingleKeyAction extends WebDriverKeysRelatedAction imple
public function __construct(
WebDriverKeyboard $keyboard,
WebDriverMouse $mouse,
- WebDriverLocatable $location_provider = null,
+ ?WebDriverLocatable $location_provider = null,
$key = ''
) {
parent::__construct($keyboard, $mouse, $location_provider);
if (!in_array($key, self::MODIFIER_KEYS, true)) {
- throw new \InvalidArgumentException(
+ throw LogicException::forError(
sprintf(
'keyDown / keyUp actions can only be used for modifier keys, but "%s" was given',
$key
diff --git a/lib/Interactions/Touch/WebDriverDownAction.php b/lib/Interactions/Touch/WebDriverDownAction.php
index 2e0f1e5d1..225726fcf 100644
--- a/lib/Interactions/Touch/WebDriverDownAction.php
+++ b/lib/Interactions/Touch/WebDriverDownAction.php
@@ -16,7 +16,6 @@ class WebDriverDownAction extends WebDriverTouchAction implements WebDriverActio
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
* @param int $x
* @param int $y
*/
diff --git a/lib/Interactions/Touch/WebDriverFlickAction.php b/lib/Interactions/Touch/WebDriverFlickAction.php
index 5430852ac..acdb7fc3e 100644
--- a/lib/Interactions/Touch/WebDriverFlickAction.php
+++ b/lib/Interactions/Touch/WebDriverFlickAction.php
@@ -16,7 +16,6 @@ class WebDriverFlickAction extends WebDriverTouchAction implements WebDriverActi
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
* @param int $x
* @param int $y
*/
diff --git a/lib/Interactions/Touch/WebDriverFlickFromElementAction.php b/lib/Interactions/Touch/WebDriverFlickFromElementAction.php
index 799febe10..28d359718 100644
--- a/lib/Interactions/Touch/WebDriverFlickFromElementAction.php
+++ b/lib/Interactions/Touch/WebDriverFlickFromElementAction.php
@@ -21,8 +21,6 @@ class WebDriverFlickFromElementAction extends WebDriverTouchAction implements We
private $speed;
/**
- * @param WebDriverTouchScreen $touch_screen
- * @param WebDriverElement $element
* @param int $x
* @param int $y
* @param int $speed
diff --git a/lib/Interactions/Touch/WebDriverMoveAction.php b/lib/Interactions/Touch/WebDriverMoveAction.php
index 8cdf5eb99..d0a5f85f9 100644
--- a/lib/Interactions/Touch/WebDriverMoveAction.php
+++ b/lib/Interactions/Touch/WebDriverMoveAction.php
@@ -10,7 +10,6 @@ class WebDriverMoveAction extends WebDriverTouchAction implements WebDriverActio
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
* @param int $x
* @param int $y
*/
diff --git a/lib/Interactions/Touch/WebDriverScrollAction.php b/lib/Interactions/Touch/WebDriverScrollAction.php
index 0fd40c5b4..952d57e34 100644
--- a/lib/Interactions/Touch/WebDriverScrollAction.php
+++ b/lib/Interactions/Touch/WebDriverScrollAction.php
@@ -10,7 +10,6 @@ class WebDriverScrollAction extends WebDriverTouchAction implements WebDriverAct
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
* @param int $x
* @param int $y
*/
diff --git a/lib/Interactions/Touch/WebDriverScrollFromElementAction.php b/lib/Interactions/Touch/WebDriverScrollFromElementAction.php
index ba68bc62c..217564dc7 100644
--- a/lib/Interactions/Touch/WebDriverScrollFromElementAction.php
+++ b/lib/Interactions/Touch/WebDriverScrollFromElementAction.php
@@ -11,8 +11,6 @@ class WebDriverScrollFromElementAction extends WebDriverTouchAction implements W
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
- * @param WebDriverElement $element
* @param int $x
* @param int $y
*/
diff --git a/lib/Interactions/Touch/WebDriverTouchAction.php b/lib/Interactions/Touch/WebDriverTouchAction.php
index 3919170a9..10100ea21 100644
--- a/lib/Interactions/Touch/WebDriverTouchAction.php
+++ b/lib/Interactions/Touch/WebDriverTouchAction.php
@@ -19,13 +19,9 @@ abstract class WebDriverTouchAction
*/
protected $locationProvider;
- /**
- * @param WebDriverTouchScreen $touch_screen
- * @param WebDriverLocatable $location_provider
- */
public function __construct(
WebDriverTouchScreen $touch_screen,
- WebDriverLocatable $location_provider = null
+ ?WebDriverLocatable $location_provider = null
) {
$this->touchScreen = $touch_screen;
$this->locationProvider = $location_provider;
diff --git a/lib/Interactions/Touch/WebDriverTouchScreen.php b/lib/Interactions/Touch/WebDriverTouchScreen.php
index 21696fc90..ff9f9c4d1 100644
--- a/lib/Interactions/Touch/WebDriverTouchScreen.php
+++ b/lib/Interactions/Touch/WebDriverTouchScreen.php
@@ -12,7 +12,6 @@ interface WebDriverTouchScreen
/**
* Single tap on the touch enabled device.
*
- * @param WebDriverElement $element
* @return $this
*/
public function tap(WebDriverElement $element);
@@ -20,7 +19,6 @@ public function tap(WebDriverElement $element);
/**
* Double tap on the touch screen using finger motion events.
*
- * @param WebDriverElement $element
* @return $this
*/
public function doubleTap(WebDriverElement $element);
@@ -48,7 +46,6 @@ public function flick($xspeed, $yspeed);
* Flick on the touch screen using finger motion events.
* This flickcommand starts at a particular screen location.
*
- * @param WebDriverElement $element
* @param int $xoffset
* @param int $yoffset
* @param int $speed
@@ -64,7 +61,6 @@ public function flickFromElement(
/**
* Long press on the touch screen using finger motion events.
*
- * @param WebDriverElement $element
* @return $this
*/
public function longPress(WebDriverElement $element);
@@ -92,7 +88,6 @@ public function scroll($xoffset, $yoffset);
* Scroll on the touch screen using finger based motion events. Use this
* command to start scrolling at a particular screen location.
*
- * @param WebDriverElement $element
* @param int $xoffset
* @param int $yoffset
* @return $this
diff --git a/lib/Interactions/WebDriverActions.php b/lib/Interactions/WebDriverActions.php
index b5172e2e7..031d91ac6 100644
--- a/lib/Interactions/WebDriverActions.php
+++ b/lib/Interactions/WebDriverActions.php
@@ -25,9 +25,6 @@ class WebDriverActions
protected $mouse;
protected $action;
- /**
- * @param WebDriverHasInputDevices $driver
- */
public function __construct(WebDriverHasInputDevices $driver)
{
$this->driver = $driver;
@@ -48,10 +45,9 @@ public function perform()
* Mouse click.
* If $element is provided, move to the middle of the element first.
*
- * @param WebDriverElement $element
* @return WebDriverActions
*/
- public function click(WebDriverElement $element = null)
+ public function click(?WebDriverElement $element = null)
{
$this->action->addAction(
new WebDriverClickAction($this->mouse, $element)
@@ -64,10 +60,9 @@ public function click(WebDriverElement $element = null)
* Mouse click and hold.
* If $element is provided, move to the middle of the element first.
*
- * @param WebDriverElement $element
* @return WebDriverActions
*/
- public function clickAndHold(WebDriverElement $element = null)
+ public function clickAndHold(?WebDriverElement $element = null)
{
$this->action->addAction(
new WebDriverClickAndHoldAction($this->mouse, $element)
@@ -80,10 +75,9 @@ public function clickAndHold(WebDriverElement $element = null)
* Context-click (right click).
* If $element is provided, move to the middle of the element first.
*
- * @param WebDriverElement $element
* @return WebDriverActions
*/
- public function contextClick(WebDriverElement $element = null)
+ public function contextClick(?WebDriverElement $element = null)
{
$this->action->addAction(
new WebDriverContextClickAction($this->mouse, $element)
@@ -96,10 +90,9 @@ public function contextClick(WebDriverElement $element = null)
* Double click.
* If $element is provided, move to the middle of the element first.
*
- * @param WebDriverElement $element
* @return WebDriverActions
*/
- public function doubleClick(WebDriverElement $element = null)
+ public function doubleClick(?WebDriverElement $element = null)
{
$this->action->addAction(
new WebDriverDoubleClickAction($this->mouse, $element)
@@ -111,8 +104,6 @@ public function doubleClick(WebDriverElement $element = null)
/**
* Drag and drop from $source to $target.
*
- * @param WebDriverElement $source
- * @param WebDriverElement $target
* @return WebDriverActions
*/
public function dragAndDrop(WebDriverElement $source, WebDriverElement $target)
@@ -133,7 +124,6 @@ public function dragAndDrop(WebDriverElement $source, WebDriverElement $target)
/**
* Drag $source and drop by offset ($x_offset, $y_offset).
*
- * @param WebDriverElement $source
* @param int $x_offset
* @param int $y_offset
* @return WebDriverActions
@@ -174,7 +164,6 @@ public function moveByOffset($x_offset, $y_offset)
* Extra shift, calculated from the top-left corner of the element, can be set by passing $x_offset and $y_offset
* parameters.
*
- * @param WebDriverElement $element
* @param int $x_offset
* @param int $y_offset
* @return WebDriverActions
@@ -195,10 +184,9 @@ public function moveToElement(WebDriverElement $element, $x_offset = null, $y_of
* Release the mouse button.
* If $element is provided, move to the middle of the element first.
*
- * @param WebDriverElement $element
* @return WebDriverActions
*/
- public function release(WebDriverElement $element = null)
+ public function release(?WebDriverElement $element = null)
{
$this->action->addAction(
new WebDriverButtonReleaseAction($this->mouse, $element)
@@ -212,11 +200,10 @@ public function release(WebDriverElement $element = null)
* If $element is provided, focus on that element first.
*
* @see WebDriverKeys for special keys like CONTROL, ALT, etc.
- * @param WebDriverElement $element
* @param string $key
* @return WebDriverActions
*/
- public function keyDown(WebDriverElement $element = null, $key = null)
+ public function keyDown(?WebDriverElement $element = null, $key = null)
{
$this->action->addAction(
new WebDriverKeyDownAction($this->keyboard, $this->mouse, $element, $key)
@@ -230,11 +217,10 @@ public function keyDown(WebDriverElement $element = null, $key = null)
* If $element is provided, focus on that element first.
*
* @see WebDriverKeys for special keys like CONTROL, ALT, etc.
- * @param WebDriverElement $element
* @param string $key
* @return WebDriverActions
*/
- public function keyUp(WebDriverElement $element = null, $key = null)
+ public function keyUp(?WebDriverElement $element = null, $key = null)
{
$this->action->addAction(
new WebDriverKeyUpAction($this->keyboard, $this->mouse, $element, $key)
@@ -248,11 +234,10 @@ public function keyUp(WebDriverElement $element = null, $key = null)
* If $element is provided, focus on that element first (using single mouse click).
*
* @see WebDriverKeys for special keys like CONTROL, ALT, etc.
- * @param WebDriverElement $element
* @param string $keys
* @return WebDriverActions
*/
- public function sendKeys(WebDriverElement $element = null, $keys = null)
+ public function sendKeys(?WebDriverElement $element = null, $keys = null)
{
$this->action->addAction(
new WebDriverSendKeysAction(
diff --git a/lib/Interactions/WebDriverCompositeAction.php b/lib/Interactions/WebDriverCompositeAction.php
index 168bf5ff5..955d61986 100644
--- a/lib/Interactions/WebDriverCompositeAction.php
+++ b/lib/Interactions/WebDriverCompositeAction.php
@@ -17,7 +17,6 @@ class WebDriverCompositeAction implements WebDriverAction
/**
* Add an WebDriverAction to the sequence.
*
- * @param WebDriverAction $action
* @return WebDriverCompositeAction The current instance.
*/
public function addAction(WebDriverAction $action)
diff --git a/lib/Interactions/WebDriverTouchActions.php b/lib/Interactions/WebDriverTouchActions.php
index da0e47815..fd3298410 100644
--- a/lib/Interactions/WebDriverTouchActions.php
+++ b/lib/Interactions/WebDriverTouchActions.php
@@ -33,7 +33,6 @@ public function __construct(WebDriver $driver)
}
/**
- * @param WebDriverElement $element
* @return WebDriverTouchActions
*/
public function tap(WebDriverElement $element)
@@ -102,7 +101,6 @@ public function scroll($x, $y)
}
/**
- * @param WebDriverElement $element
* @param int $x
* @param int $y
* @return WebDriverTouchActions
@@ -117,7 +115,6 @@ public function scrollFromElement(WebDriverElement $element, $x, $y)
}
/**
- * @param WebDriverElement $element
* @return WebDriverTouchActions
*/
public function doubleTap(WebDriverElement $element)
@@ -130,7 +127,6 @@ public function doubleTap(WebDriverElement $element)
}
/**
- * @param WebDriverElement $element
* @return WebDriverTouchActions
*/
public function longPress(WebDriverElement $element)
@@ -157,7 +153,6 @@ public function flick($x, $y)
}
/**
- * @param WebDriverElement $element
* @param int $x
* @param int $y
* @param int $speed
diff --git a/lib/Local/LocalWebDriver.php b/lib/Local/LocalWebDriver.php
index d695ff016..a23aefe1a 100644
--- a/lib/Local/LocalWebDriver.php
+++ b/lib/Local/LocalWebDriver.php
@@ -2,27 +2,17 @@
namespace Facebook\WebDriver\Local;
-use Facebook\WebDriver\Exception\WebDriverException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
/**
+ * @codeCoverageIgnore
* @todo Break inheritance from RemoteWebDriver in next major version. (Composition over inheritance!)
*/
abstract class LocalWebDriver extends RemoteWebDriver
{
- /**
- * @param string $selenium_server_url
- * @param null $desired_capabilities
- * @param null $connection_timeout_in_ms
- * @param null $request_timeout_in_ms
- * @param null $http_proxy
- * @param null $http_proxy_port
- * @param DesiredCapabilities|null $required_capabilities
- * @throws WebDriverException
- * @return RemoteWebDriver
- * @todo Remove in next major version (should not be inherited)
- */
+ // @todo Remove in next major version (should not be inherited)
public static function create(
$selenium_server_url = 'http://localhost:4444/wd/hub',
$desired_capabilities = null,
@@ -30,26 +20,18 @@ public static function create(
$request_timeout_in_ms = null,
$http_proxy = null,
$http_proxy_port = null,
- DesiredCapabilities $required_capabilities = null
+ ?DesiredCapabilities $required_capabilities = null
) {
- throw new WebDriverException('Use start() method to start local WebDriver.');
+ throw LogicException::forError('Use start() method to start local WebDriver.');
}
- /**
- * @param string $session_id
- * @param string $selenium_server_url
- * @param null $connection_timeout_in_ms
- * @param null $request_timeout_in_ms
- * @throws WebDriverException
- * @return RemoteWebDriver
- * @todo Remove in next major version (should not be inherited)
- */
+ // @todo Remove in next major version (should not be inherited)
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('Use start() method to start local WebDriver.');
+ throw LogicException::forError('Use start() method to start local WebDriver.');
}
}
diff --git a/lib/Net/URLChecker.php b/lib/Net/URLChecker.php
index a4ff2df00..5fe5d4abe 100644
--- a/lib/Net/URLChecker.php
+++ b/lib/Net/URLChecker.php
@@ -7,8 +7,8 @@
class URLChecker
{
- const POLL_INTERVAL_MS = 500;
- const CONNECT_TIMEOUT_MS = 500;
+ public const POLL_INTERVAL_MS = 500;
+ public const CONNECT_TIMEOUT_MS = 500;
public function waitUntilAvailable($timeout_in_ms, $url)
{
@@ -59,6 +59,7 @@ private function getHTTPResponseCode($url)
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, self::CONNECT_TIMEOUT_MS);
$code = null;
+
try {
curl_exec($ch);
$info = curl_getinfo($ch);
diff --git a/lib/Remote/CustomWebDriverCommand.php b/lib/Remote/CustomWebDriverCommand.php
index 157902199..cef2c95b2 100644
--- a/lib/Remote/CustomWebDriverCommand.php
+++ b/lib/Remote/CustomWebDriverCommand.php
@@ -2,12 +2,13 @@
namespace Facebook\WebDriver\Remote;
+use Facebook\WebDriver\Exception\Internal\LogicException;
use Facebook\WebDriver\Exception\WebDriverException;
class CustomWebDriverCommand extends WebDriverCommand
{
- const METHOD_GET = 'GET';
- const METHOD_POST = 'POST';
+ public const METHOD_GET = 'GET';
+ public const METHOD_POST = 'POST';
/** @var string */
private $customUrl;
@@ -18,7 +19,6 @@ class CustomWebDriverCommand extends WebDriverCommand
* @param string $session_id
* @param string $url
* @param string $method
- * @param array $parameters
*/
public function __construct($session_id, $url, $method, array $parameters)
{
@@ -34,7 +34,7 @@ public function __construct($session_id, $url, $method, array $parameters)
public function getCustomUrl()
{
if ($this->customUrl === null) {
- throw new WebDriverException('URL of custom command is not set');
+ throw LogicException::forError('URL of custom command is not set');
}
return $this->customUrl;
@@ -47,7 +47,7 @@ public function getCustomUrl()
public function getCustomMethod()
{
if ($this->customMethod === null) {
- throw new WebDriverException('Method of custom command is not set');
+ throw LogicException::forError('Method of custom command is not set');
}
return $this->customMethod;
@@ -62,7 +62,7 @@ protected function setCustomRequestParameters($custom_url, $custom_method)
{
$allowedMethods = [static::METHOD_GET, static::METHOD_POST];
if (!in_array($custom_method, $allowedMethods, true)) {
- throw new WebDriverException(
+ throw LogicException::forError(
sprintf(
'Invalid custom method "%s", must be one of [%s]',
$custom_method,
@@ -73,7 +73,7 @@ protected function setCustomRequestParameters($custom_url, $custom_method)
$this->customMethod = $custom_method;
if (mb_strpos($custom_url, '/') !== 0) {
- throw new WebDriverException(
+ throw LogicException::forError(
sprintf('URL of custom command has to start with / but is "%s"', $custom_url)
);
}
diff --git a/lib/Remote/DesiredCapabilities.php b/lib/Remote/DesiredCapabilities.php
index 5b726406d..88aa6b141 100644
--- a/lib/Remote/DesiredCapabilities.php
+++ b/lib/Remote/DesiredCapabilities.php
@@ -2,8 +2,8 @@
namespace Facebook\WebDriver\Remote;
-use Exception;
use Facebook\WebDriver\Chrome\ChromeOptions;
+use Facebook\WebDriver\Exception\UnsupportedOperationException;
use Facebook\WebDriver\Firefox\FirefoxDriver;
use Facebook\WebDriver\Firefox\FirefoxOptions;
use Facebook\WebDriver\Firefox\FirefoxProfile;
@@ -20,7 +20,6 @@ class DesiredCapabilities implements WebDriverCapabilities
WebDriverCapabilityType::PLATFORM => 'platformName',
WebDriverCapabilityType::VERSION => 'browserVersion',
WebDriverCapabilityType::ACCEPT_SSL_CERTS => 'acceptInsecureCerts',
- ChromeOptions::CAPABILITY => ChromeOptions::CAPABILITY_W3C,
];
public function __construct(array $capabilities = [])
@@ -30,7 +29,7 @@ public function __construct(array $capabilities = [])
public static function createFromW3cCapabilities(array $capabilities = [])
{
- $w3cToOss = array_flip(static::$ossToW3c);
+ $w3cToOss = array_flip(self::$ossToW3c);
foreach ($w3cToOss as $w3cCapability => $ossCapability) {
// Copy W3C capabilities to OSS ones
@@ -152,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
*/
@@ -160,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.'
);
@@ -229,16 +228,16 @@ public function toW3cCompatibleArray()
}
// Convert capabilities with changed name
- if (array_key_exists($capabilityKey, static::$ossToW3c)) {
+ if (array_key_exists($capabilityKey, self::$ossToW3c)) {
if ($capabilityKey === WebDriverCapabilityType::PLATFORM) {
- $w3cCapabilities[static::$ossToW3c[$capabilityKey]] = mb_strtolower($capabilityValue);
+ $w3cCapabilities[self::$ossToW3c[$capabilityKey]] = mb_strtolower($capabilityValue);
// Remove platformName if it is set to "any"
- if ($w3cCapabilities[static::$ossToW3c[$capabilityKey]] === 'any') {
- unset($w3cCapabilities[static::$ossToW3c[$capabilityKey]]);
+ if ($w3cCapabilities[self::$ossToW3c[$capabilityKey]] === 'any') {
+ unset($w3cCapabilities[self::$ossToW3c[$capabilityKey]]);
}
} else {
- $w3cCapabilities[static::$ossToW3c[$capabilityKey]] = $capabilityValue;
+ $w3cCapabilities[self::$ossToW3c[$capabilityKey]] = $capabilityValue;
}
}
@@ -250,16 +249,7 @@ public function toW3cCompatibleArray()
// Convert ChromeOptions
if (array_key_exists(ChromeOptions::CAPABILITY, $ossCapabilities)) {
- if (array_key_exists(ChromeOptions::CAPABILITY_W3C, $ossCapabilities)) {
- $w3cCapabilities[ChromeOptions::CAPABILITY_W3C] = new \ArrayObject(
- array_merge_recursive(
- (array) $ossCapabilities[ChromeOptions::CAPABILITY],
- (array) $ossCapabilities[ChromeOptions::CAPABILITY_W3C]
- )
- );
- } else {
- $w3cCapabilities[ChromeOptions::CAPABILITY_W3C] = $ossCapabilities[ChromeOptions::CAPABILITY];
- }
+ $w3cCapabilities[ChromeOptions::CAPABILITY] = $ossCapabilities[ChromeOptions::CAPABILITY];
}
// Convert Firefox profile
@@ -433,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 97b9090ef..a3a230b7c 100644
--- a/lib/Remote/DriverCommand.php
+++ b/lib/Remote/DriverCommand.php
@@ -9,140 +9,143 @@
*/
class DriverCommand
{
- const GET_ALL_SESSIONS = 'getAllSessions';
- const GET_CAPABILITIES = 'getCapabilities';
- const NEW_SESSION = 'newSession';
- const STATUS = 'status';
- const CLOSE = 'close';
- const QUIT = 'quit';
- const GET = 'get';
- const GO_BACK = 'goBack';
- const GO_FORWARD = 'goForward';
- const REFRESH = 'refresh';
- const ADD_COOKIE = 'addCookie';
- const GET_ALL_COOKIES = 'getCookies';
- const DELETE_COOKIE = 'deleteCookie';
- const DELETE_ALL_COOKIES = 'deleteAllCookies';
- const FIND_ELEMENT = 'findElement';
- const FIND_ELEMENTS = 'findElements';
- const FIND_CHILD_ELEMENT = 'findChildElement';
- const FIND_CHILD_ELEMENTS = 'findChildElements';
- const CLEAR_ELEMENT = 'clearElement';
- const CLICK_ELEMENT = 'clickElement';
- const SEND_KEYS_TO_ELEMENT = 'sendKeysToElement';
- const SEND_KEYS_TO_ACTIVE_ELEMENT = 'sendKeysToActiveElement';
- const SUBMIT_ELEMENT = 'submitElement';
- const UPLOAD_FILE = 'uploadFile';
- const GET_CURRENT_WINDOW_HANDLE = 'getCurrentWindowHandle';
- const GET_WINDOW_HANDLES = 'getWindowHandles';
- const GET_CURRENT_CONTEXT_HANDLE = 'getCurrentContextHandle';
- const GET_CONTEXT_HANDLES = 'getContextHandles';
+ public const GET_ALL_SESSIONS = 'getAllSessions';
+ public const GET_CAPABILITIES = 'getCapabilities';
+ public const NEW_SESSION = 'newSession';
+ public const STATUS = 'status';
+ public const CLOSE = 'close';
+ public const QUIT = 'quit';
+ public const GET = 'get';
+ public const GO_BACK = 'goBack';
+ public const GO_FORWARD = 'goForward';
+ public const REFRESH = 'refresh';
+ public const ADD_COOKIE = 'addCookie';
+ public const GET_ALL_COOKIES = 'getCookies';
+ public const DELETE_COOKIE = 'deleteCookie';
+ public const DELETE_ALL_COOKIES = 'deleteAllCookies';
+ public const FIND_ELEMENT = 'findElement';
+ public const FIND_ELEMENTS = 'findElements';
+ public const FIND_CHILD_ELEMENT = 'findChildElement';
+ public const FIND_CHILD_ELEMENTS = 'findChildElements';
+ public const CLEAR_ELEMENT = 'clearElement';
+ public const CLICK_ELEMENT = 'clickElement';
+ public const SEND_KEYS_TO_ELEMENT = 'sendKeysToElement';
+ public const SEND_KEYS_TO_ACTIVE_ELEMENT = 'sendKeysToActiveElement';
+ public const SUBMIT_ELEMENT = 'submitElement';
+ public const UPLOAD_FILE = 'uploadFile';
+ public const GET_CURRENT_WINDOW_HANDLE = 'getCurrentWindowHandle';
+ public const GET_WINDOW_HANDLES = 'getWindowHandles';
+ public const GET_CURRENT_CONTEXT_HANDLE = 'getCurrentContextHandle';
+ public const GET_CONTEXT_HANDLES = 'getContextHandles';
// Switching between to window/frame/iframe
- const SWITCH_TO_WINDOW = 'switchToWindow';
- const SWITCH_TO_CONTEXT = 'switchToContext';
- const SWITCH_TO_FRAME = 'switchToFrame';
- const SWITCH_TO_PARENT_FRAME = 'switchToParentFrame';
- const GET_ACTIVE_ELEMENT = 'getActiveElement';
+ public const SWITCH_TO_WINDOW = 'switchToWindow';
+ public const SWITCH_TO_CONTEXT = 'switchToContext';
+ public const SWITCH_TO_FRAME = 'switchToFrame';
+ public const SWITCH_TO_PARENT_FRAME = 'switchToParentFrame';
+ public const GET_ACTIVE_ELEMENT = 'getActiveElement';
// Information of the page
- const GET_CURRENT_URL = 'getCurrentUrl';
- const GET_PAGE_SOURCE = 'getPageSource';
- const GET_TITLE = 'getTitle';
+ public const GET_CURRENT_URL = 'getCurrentUrl';
+ public const GET_PAGE_SOURCE = 'getPageSource';
+ public const GET_TITLE = 'getTitle';
// Javascript API
- const EXECUTE_SCRIPT = 'executeScript';
- const EXECUTE_ASYNC_SCRIPT = 'executeAsyncScript';
+ public const EXECUTE_SCRIPT = 'executeScript';
+ public const EXECUTE_ASYNC_SCRIPT = 'executeAsyncScript';
// API getting information from an element.
- const GET_ELEMENT_TEXT = 'getElementText';
- const GET_ELEMENT_TAG_NAME = 'getElementTagName';
- const IS_ELEMENT_SELECTED = 'isElementSelected';
- const IS_ELEMENT_ENABLED = 'isElementEnabled';
- const IS_ELEMENT_DISPLAYED = 'isElementDisplayed';
- const GET_ELEMENT_LOCATION = 'getElementLocation';
- const GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW = 'getElementLocationOnceScrolledIntoView';
- const GET_ELEMENT_SIZE = 'getElementSize';
- const GET_ELEMENT_ATTRIBUTE = 'getElementAttribute';
- const GET_ELEMENT_VALUE_OF_CSS_PROPERTY = 'getElementValueOfCssProperty';
- const ELEMENT_EQUALS = 'elementEquals';
- const SCREENSHOT = 'screenshot';
+ public const GET_ELEMENT_TEXT = 'getElementText';
+ public const GET_ELEMENT_TAG_NAME = 'getElementTagName';
+ public const IS_ELEMENT_SELECTED = 'isElementSelected';
+ public const IS_ELEMENT_ENABLED = 'isElementEnabled';
+ public const IS_ELEMENT_DISPLAYED = 'isElementDisplayed';
+ public const GET_ELEMENT_LOCATION = 'getElementLocation';
+ public const GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW = 'getElementLocationOnceScrolledIntoView';
+ public const GET_ELEMENT_SIZE = 'getElementSize';
+ public const GET_ELEMENT_ATTRIBUTE = 'getElementAttribute';
+ public const GET_ELEMENT_VALUE_OF_CSS_PROPERTY = 'getElementValueOfCssProperty';
+ public const ELEMENT_EQUALS = 'elementEquals';
+ public const SCREENSHOT = 'screenshot';
// Alert API
- const ACCEPT_ALERT = 'acceptAlert';
- const DISMISS_ALERT = 'dismissAlert';
- const GET_ALERT_TEXT = 'getAlertText';
- const SET_ALERT_VALUE = 'setAlertValue';
+ public const ACCEPT_ALERT = 'acceptAlert';
+ public const DISMISS_ALERT = 'dismissAlert';
+ public const GET_ALERT_TEXT = 'getAlertText';
+ public const SET_ALERT_VALUE = 'setAlertValue';
// Timeout API
- const SET_TIMEOUT = 'setTimeout';
- const IMPLICITLY_WAIT = 'implicitlyWait';
- const SET_SCRIPT_TIMEOUT = 'setScriptTimeout';
+ public const SET_TIMEOUT = 'setTimeout';
+ public const IMPLICITLY_WAIT = 'implicitlyWait';
+ public const SET_SCRIPT_TIMEOUT = 'setScriptTimeout';
/** @deprecated */
- const EXECUTE_SQL = 'executeSQL';
- const GET_LOCATION = 'getLocation';
- const SET_LOCATION = 'setLocation';
- const GET_APP_CACHE = 'getAppCache';
- const GET_APP_CACHE_STATUS = 'getStatus';
- const CLEAR_APP_CACHE = 'clearAppCache';
- const IS_BROWSER_ONLINE = 'isBrowserOnline';
- const SET_BROWSER_ONLINE = 'setBrowserOnline';
+ public const EXECUTE_SQL = 'executeSQL';
+ public const GET_LOCATION = 'getLocation';
+ public const SET_LOCATION = 'setLocation';
+ public const GET_APP_CACHE = 'getAppCache';
+ public const GET_APP_CACHE_STATUS = 'getStatus';
+ public const CLEAR_APP_CACHE = 'clearAppCache';
+ public const IS_BROWSER_ONLINE = 'isBrowserOnline';
+ public const SET_BROWSER_ONLINE = 'setBrowserOnline';
// Local storage
- const GET_LOCAL_STORAGE_ITEM = 'getLocalStorageItem';
- const GET_LOCAL_STORAGE_KEYS = 'getLocalStorageKeys';
- const SET_LOCAL_STORAGE_ITEM = 'setLocalStorageItem';
- const REMOVE_LOCAL_STORAGE_ITEM = 'removeLocalStorageItem';
- const CLEAR_LOCAL_STORAGE = 'clearLocalStorage';
- const GET_LOCAL_STORAGE_SIZE = 'getLocalStorageSize';
+ public const GET_LOCAL_STORAGE_ITEM = 'getLocalStorageItem';
+ public const GET_LOCAL_STORAGE_KEYS = 'getLocalStorageKeys';
+ public const SET_LOCAL_STORAGE_ITEM = 'setLocalStorageItem';
+ public const REMOVE_LOCAL_STORAGE_ITEM = 'removeLocalStorageItem';
+ public const CLEAR_LOCAL_STORAGE = 'clearLocalStorage';
+ public const GET_LOCAL_STORAGE_SIZE = 'getLocalStorageSize';
// Session storage
- const GET_SESSION_STORAGE_ITEM = 'getSessionStorageItem';
- const GET_SESSION_STORAGE_KEYS = 'getSessionStorageKey';
- const SET_SESSION_STORAGE_ITEM = 'setSessionStorageItem';
- const REMOVE_SESSION_STORAGE_ITEM = 'removeSessionStorageItem';
- const CLEAR_SESSION_STORAGE = 'clearSessionStorage';
- const GET_SESSION_STORAGE_SIZE = 'getSessionStorageSize';
+ public const GET_SESSION_STORAGE_ITEM = 'getSessionStorageItem';
+ public const GET_SESSION_STORAGE_KEYS = 'getSessionStorageKey';
+ public const SET_SESSION_STORAGE_ITEM = 'setSessionStorageItem';
+ public const REMOVE_SESSION_STORAGE_ITEM = 'removeSessionStorageItem';
+ public const CLEAR_SESSION_STORAGE = 'clearSessionStorage';
+ public const GET_SESSION_STORAGE_SIZE = 'getSessionStorageSize';
// Screen orientation
- const SET_SCREEN_ORIENTATION = 'setScreenOrientation';
- const GET_SCREEN_ORIENTATION = 'getScreenOrientation';
+ public const SET_SCREEN_ORIENTATION = 'setScreenOrientation';
+ public const GET_SCREEN_ORIENTATION = 'getScreenOrientation';
// These belong to the Advanced user interactions - an element is optional for these commands.
- const CLICK = 'mouseClick';
- const DOUBLE_CLICK = 'mouseDoubleClick';
- const MOUSE_DOWN = 'mouseButtonDown';
- const MOUSE_UP = 'mouseButtonUp';
- const MOVE_TO = 'mouseMoveTo';
+ public const CLICK = 'mouseClick';
+ public const DOUBLE_CLICK = 'mouseDoubleClick';
+ public const MOUSE_DOWN = 'mouseButtonDown';
+ public const MOUSE_UP = 'mouseButtonUp';
+ public const MOVE_TO = 'mouseMoveTo';
// Those allow interactions with the Input Methods installed on the system.
- const IME_GET_AVAILABLE_ENGINES = 'imeGetAvailableEngines';
- const IME_GET_ACTIVE_ENGINE = 'imeGetActiveEngine';
- const IME_IS_ACTIVATED = 'imeIsActivated';
- const IME_DEACTIVATE = 'imeDeactivate';
- const IME_ACTIVATE_ENGINE = 'imeActivateEngine';
+ public const IME_GET_AVAILABLE_ENGINES = 'imeGetAvailableEngines';
+ public const IME_GET_ACTIVE_ENGINE = 'imeGetActiveEngine';
+ public const IME_IS_ACTIVATED = 'imeIsActivated';
+ public const IME_DEACTIVATE = 'imeDeactivate';
+ public const IME_ACTIVATE_ENGINE = 'imeActivateEngine';
// These belong to the Advanced Touch API
- const TOUCH_SINGLE_TAP = 'touchSingleTap';
- const TOUCH_DOWN = 'touchDown';
- const TOUCH_UP = 'touchUp';
- const TOUCH_MOVE = 'touchMove';
- const TOUCH_SCROLL = 'touchScroll';
- const TOUCH_DOUBLE_TAP = 'touchDoubleTap';
- const TOUCH_LONG_PRESS = 'touchLongPress';
- const TOUCH_FLICK = 'touchFlick';
+ public const TOUCH_SINGLE_TAP = 'touchSingleTap';
+ public const TOUCH_DOWN = 'touchDown';
+ public const TOUCH_UP = 'touchUp';
+ public const TOUCH_MOVE = 'touchMove';
+ public const TOUCH_SCROLL = 'touchScroll';
+ public const TOUCH_DOUBLE_TAP = 'touchDoubleTap';
+ public const TOUCH_LONG_PRESS = 'touchLongPress';
+ public const TOUCH_FLICK = 'touchFlick';
// Window API (beta)
- const SET_WINDOW_SIZE = 'setWindowSize';
- const SET_WINDOW_POSITION = 'setWindowPosition';
- const GET_WINDOW_SIZE = 'getWindowSize';
- const GET_WINDOW_POSITION = 'getWindowPosition';
- const MAXIMIZE_WINDOW = 'maximizeWindow';
- const FULLSCREEN_WINDOW = 'fullscreenWindow';
+ public const SET_WINDOW_SIZE = 'setWindowSize';
+ public const SET_WINDOW_POSITION = 'setWindowPosition';
+ public const GET_WINDOW_SIZE = 'getWindowSize';
+ public const GET_WINDOW_POSITION = 'getWindowPosition';
+ public const MAXIMIZE_WINDOW = 'maximizeWindow';
+ public const FULLSCREEN_WINDOW = 'fullscreenWindow';
// Logging API
- const GET_AVAILABLE_LOG_TYPES = 'getAvailableLogTypes';
- const GET_LOG = 'getLog';
- const GET_SESSION_LOGS = 'getSessionLogs';
+ public const GET_AVAILABLE_LOG_TYPES = 'getAvailableLogTypes';
+ public const GET_LOG = 'getLog';
+ public const GET_SESSION_LOGS = 'getSessionLogs';
// Mobile API
- const GET_NETWORK_CONNECTION = 'getNetworkConnection';
- const SET_NETWORK_CONNECTION = 'setNetworkConnection';
+ public const GET_NETWORK_CONNECTION = 'getNetworkConnection';
+ public const SET_NETWORK_CONNECTION = 'setNetworkConnection';
// Custom command
- const CUSTOM_COMMAND = 'customCommand';
+ public const CUSTOM_COMMAND = 'customCommand';
// W3C specific
- const ACTIONS = 'actions';
- const GET_ELEMENT_PROPERTY = 'getElementProperty';
- const GET_NAMED_COOKIE = 'getNamedCookie';
- const NEW_WINDOW = 'newWindow';
- const TAKE_ELEMENT_SCREENSHOT = 'takeElementScreenshot';
- const MINIMIZE_WINDOW = 'minimizeWindow';
+ public const ACTIONS = 'actions';
+ public const GET_ELEMENT_PROPERTY = 'getElementProperty';
+ public const GET_NAMED_COOKIE = 'getNamedCookie';
+ public const NEW_WINDOW = 'newWindow';
+ public const TAKE_ELEMENT_SCREENSHOT = 'takeElementScreenshot';
+ public const MINIMIZE_WINDOW = 'minimizeWindow';
+ public const GET_ELEMENT_SHADOW_ROOT = 'getElementShadowRoot';
+ public const FIND_ELEMENT_FROM_SHADOW_ROOT = 'findElementFromShadowRoot';
+ public const FIND_ELEMENTS_FROM_SHADOW_ROOT = 'findElementsFromShadowRoot';
private function __construct()
{
diff --git a/lib/Remote/ExecuteMethod.php b/lib/Remote/ExecuteMethod.php
index 3f1636e5b..ba659e6d2 100644
--- a/lib/Remote/ExecuteMethod.php
+++ b/lib/Remote/ExecuteMethod.php
@@ -6,7 +6,6 @@ interface ExecuteMethod
{
/**
* @param string $command_name
- * @param array $parameters
* @return WebDriverResponse
*/
public function execute($command_name, array $parameters = []);
diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php
index 69790e0e1..3e3ef1719 100644
--- a/lib/Remote/HttpCommandExecutor.php
+++ b/lib/Remote/HttpCommandExecutor.php
@@ -2,18 +2,18 @@
namespace Facebook\WebDriver\Remote;
-use BadMethodCallException;
-use Facebook\WebDriver\Exception\WebDriverCurlException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
+use Facebook\WebDriver\Exception\Internal\UnexpectedResponseException;
+use Facebook\WebDriver\Exception\Internal\WebDriverCurlException;
use Facebook\WebDriver\Exception\WebDriverException;
use Facebook\WebDriver\WebDriverCommandExecutor;
-use InvalidArgumentException;
/**
* Command executor talking to the standalone server via HTTP.
*/
class HttpCommandExecutor implements WebDriverCommandExecutor
{
- const DEFAULT_HTTP_HEADERS = [
+ public const DEFAULT_HTTP_HEADERS = [
'Content-Type: application/json;charset=UTF-8',
'Accept: application/json',
];
@@ -141,6 +141,14 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/dismiss'],
DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/async'],
DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'],
+ DriverCommand::FIND_ELEMENT_FROM_SHADOW_ROOT => [
+ 'method' => 'POST',
+ 'url' => '/session/:sessionId/shadow/:id/element',
+ ],
+ DriverCommand::FIND_ELEMENTS_FROM_SHADOW_ROOT => [
+ 'method' => 'POST',
+ 'url' => '/session/:sessionId/shadow/:id/elements',
+ ],
DriverCommand::FULLSCREEN_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/fullscreen'],
DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'GET', 'url' => '/session/:sessionId/element/active'],
DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'],
@@ -150,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'],
@@ -163,6 +175,8 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
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
@@ -208,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()
@@ -257,9 +272,6 @@ public function setRequestTimeout($timeout_in_ms)
}
/**
- * @param WebDriverCommand $command
- *
- * @throws WebDriverException
* @return WebDriverResponse
*/
public function execute(WebDriverCommand $command)
@@ -268,7 +280,8 @@ public function execute(WebDriverCommand $command)
$http_method = $http_options['method'];
$url = $http_options['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] === ':') {
@@ -278,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);
@@ -296,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:']));
@@ -320,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, JSON_UNESCAPED_SLASHES));
- }
-
- 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;
@@ -365,13 +355,13 @@ public function execute(WebDriverCommand $command)
$sessionId = $results['sessionId'];
}
- // @see https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors
+ // @see https://w3c.github.io/webdriver/#errors
if (isset($value['error'])) {
// W3C's WebDriver
WebDriverException::throwException($value['error'], $message, $results);
}
- $status = isset($results['status']) ? $results['status'] : 0;
+ $status = $results['status'] ?? 0;
if ($status !== 0) {
// Legacy JsonWire
WebDriverException::throwException($status, $message, $results);
@@ -400,7 +390,7 @@ protected function getCommandHttpOptions(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.');
+ throw LogicException::forError($command->getName() . ' is not a valid command.');
}
}
diff --git a/lib/Remote/JsonWireCompat.php b/lib/Remote/JsonWireCompat.php
index 65a6956ba..b9e1b5ee4 100644
--- a/lib/Remote/JsonWireCompat.php
+++ b/lib/Remote/JsonWireCompat.php
@@ -2,6 +2,7 @@
namespace Facebook\WebDriver\Remote;
+use Facebook\WebDriver\Exception\Internal\UnexpectedResponseException;
use Facebook\WebDriver\WebDriverBy;
/**
@@ -14,12 +15,21 @@ abstract class JsonWireCompat
/**
* Element identifier defined in the W3C's WebDriver protocol.
*
- * @see https://w3c.github.io/webdriver/webdriver-spec.html#elements
+ * @see https://w3c.github.io/webdriver/#elements
*/
- const WEB_DRIVER_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf';
+ public const WEB_DRIVER_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf';
- public static function getElement(array $rawElement)
+ /**
+ * @param mixed $rawElement Value is validated to by an array, exception is thrown otherwise
+ * @throws UnexpectedResponseException When value of other type than array is given
+ */
+ public static function getElement($rawElement)
{
+ // The method intentionally accept mixed, so that assertion of the rawElement format could be done on one place
+ if (!is_array($rawElement)) {
+ throw UnexpectedResponseException::forElementNotArray($rawElement);
+ }
+
if (array_key_exists(self::WEB_DRIVER_ELEMENT_IDENTIFIER, $rawElement)) {
// W3C's WebDriver
return $rawElement[self::WEB_DRIVER_ELEMENT_IDENTIFIER];
@@ -30,7 +40,6 @@ public static function getElement(array $rawElement)
}
/**
- * @param WebDriverBy $by
* @param bool $isW3cCompliant
*
* @return array
diff --git a/lib/Remote/RemoteExecuteMethod.php b/lib/Remote/RemoteExecuteMethod.php
index 6cc0c26a7..265abcc1e 100644
--- a/lib/Remote/RemoteExecuteMethod.php
+++ b/lib/Remote/RemoteExecuteMethod.php
@@ -9,9 +9,6 @@ class RemoteExecuteMethod implements ExecuteMethod
*/
private $driver;
- /**
- * @param RemoteWebDriver $driver
- */
public function __construct(RemoteWebDriver $driver)
{
$this->driver = $driver;
@@ -19,7 +16,6 @@ public function __construct(RemoteWebDriver $driver)
/**
* @param string $command_name
- * @param array $parameters
* @return mixed
*/
public function execute($command_name, array $parameters = [])
diff --git a/lib/Remote/RemoteMouse.php b/lib/Remote/RemoteMouse.php
index 3495ea745..fee209f94 100644
--- a/lib/Remote/RemoteMouse.php
+++ b/lib/Remote/RemoteMouse.php
@@ -11,11 +11,11 @@
class RemoteMouse implements WebDriverMouse
{
/** @internal */
- const BUTTON_LEFT = 0;
+ public const BUTTON_LEFT = 0;
/** @internal */
- const BUTTON_MIDDLE = 1;
+ public const BUTTON_MIDDLE = 1;
/** @internal */
- const BUTTON_RIGHT = 2;
+ public const BUTTON_RIGHT = 2;
/**
* @var RemoteExecuteMethod
@@ -27,7 +27,6 @@ class RemoteMouse implements WebDriverMouse
private $isW3cCompliant;
/**
- * @param RemoteExecuteMethod $executor
* @param bool $isW3cCompliant
*/
public function __construct(RemoteExecuteMethod $executor, $isW3cCompliant = false)
@@ -37,11 +36,9 @@ public function __construct(RemoteExecuteMethod $executor, $isW3cCompliant = fal
}
/**
- * @param null|WebDriverCoordinates $where
- *
* @return RemoteMouse
*/
- public function click(WebDriverCoordinates $where = null)
+ public function click(?WebDriverCoordinates $where = null)
{
if ($this->isW3cCompliant) {
$moveAction = $where ? [$this->createMoveAction($where)] : [];
@@ -68,11 +65,9 @@ public function click(WebDriverCoordinates $where = null)
}
/**
- * @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)] : [];
@@ -108,15 +103,13 @@ public function contextClick(WebDriverCoordinates $where = null)
}
/**
- * @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' => [
[
@@ -138,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, [
@@ -172,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
) {
@@ -215,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)] : [];
@@ -249,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);
@@ -260,22 +245,21 @@ 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' => 100, // to simulate human delay
- 'x' => $x_offset === null ? 0 : $x_offset,
- 'y' => $y_offset === null ? 0 : $y_offset,
+ 'x' => $x_offset ?? 0,
+ 'y' => $y_offset ?? 0,
];
if ($where !== null) {
diff --git a/lib/Remote/RemoteStatus.php b/lib/Remote/RemoteStatus.php
index 3d123bd45..73ebeba21 100644
--- a/lib/Remote/RemoteStatus.php
+++ b/lib/Remote/RemoteStatus.php
@@ -29,7 +29,6 @@ protected function __construct($isReady, $message, array $meta = [])
}
/**
- * @param array $responseBody
* @return RemoteStatus
*/
public static function createFromResponse(array $responseBody)
diff --git a/lib/Remote/RemoteTargetLocator.php b/lib/Remote/RemoteTargetLocator.php
index 5b9881371..979b9c4f6 100644
--- a/lib/Remote/RemoteTargetLocator.php
+++ b/lib/Remote/RemoteTargetLocator.php
@@ -2,7 +2,7 @@
namespace Facebook\WebDriver\Remote;
-use Facebook\WebDriver\Exception\UnsupportedOperationException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
use Facebook\WebDriver\WebDriverAlert;
use Facebook\WebDriver\WebDriverElement;
use Facebook\WebDriver\WebDriverTargetLocator;
@@ -53,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'
);
}
@@ -111,17 +111,17 @@ public function window($handle)
* @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 UnsupportedOperationException
+ * @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 new \InvalidArgumentException('Window type must by either "tab" or "window"');
+ throw LogicException::forError('Window type must by either "tab" or "window"');
}
if (!$this->isW3cCompliant) {
- throw new UnsupportedOperationException('New window is only supported in W3C mode');
+ throw LogicException::forError('New window is only supported in W3C mode');
}
$response = $this->executor->execute(DriverCommand::NEW_WINDOW, ['type' => $windowType]);
diff --git a/lib/Remote/RemoteTouchScreen.php b/lib/Remote/RemoteTouchScreen.php
index 889c12e6c..951c8619a 100644
--- a/lib/Remote/RemoteTouchScreen.php
+++ b/lib/Remote/RemoteTouchScreen.php
@@ -15,17 +15,12 @@ class RemoteTouchScreen implements WebDriverTouchScreen
*/
private $executor;
- /**
- * @param RemoteExecuteMethod $executor
- */
public function __construct(RemoteExecuteMethod $executor)
{
$this->executor = $executor;
}
/**
- * @param WebDriverElement $element
- *
* @return RemoteTouchScreen The instance.
*/
public function tap(WebDriverElement $element)
@@ -39,8 +34,6 @@ public function tap(WebDriverElement $element)
}
/**
- * @param WebDriverElement $element
- *
* @return RemoteTouchScreen The instance.
*/
public function doubleTap(WebDriverElement $element)
@@ -86,7 +79,6 @@ public function flick($xspeed, $yspeed)
}
/**
- * @param WebDriverElement $element
* @param int $xoffset
* @param int $yoffset
* @param int $speed
@@ -106,8 +98,6 @@ public function flickFromElement(WebDriverElement $element, $xoffset, $yoffset,
}
/**
- * @param WebDriverElement $element
- *
* @return RemoteTouchScreen The instance.
*/
public function longPress(WebDriverElement $element)
@@ -153,7 +143,6 @@ public function scroll($xoffset, $yoffset)
}
/**
- * @param WebDriverElement $element
* @param int $xoffset
* @param int $yoffset
*
diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php
index 215b7fd21..3d65aaf0b 100644
--- a/lib/Remote/RemoteWebDriver.php
+++ b/lib/Remote/RemoteWebDriver.php
@@ -2,8 +2,11 @@
namespace Facebook\WebDriver\Remote;
+use Facebook\WebDriver\Exception\Internal\UnexpectedResponseException;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Facebook\WebDriver\JavaScriptExecutor;
+use Facebook\WebDriver\Support\IsElementDisplayedAtom;
+use Facebook\WebDriver\Support\ScreenshotHelper;
use Facebook\WebDriver\WebDriver;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverCapabilities;
@@ -21,7 +24,7 @@ class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInpu
*/
protected $executor;
/**
- * @var WebDriverCapabilities
+ * @var WebDriverCapabilities|null
*/
protected $capabilities;
@@ -51,24 +54,19 @@ class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInpu
protected $isW3cCompliant;
/**
- * @param HttpCommandExecutor $commandExecutor
* @param string $sessionId
- * @param WebDriverCapabilities|null $capabilities
* @param bool $isW3cCompliant false to use the legacy JsonWire protocol, true for the W3C WebDriver spec
*/
protected function __construct(
HttpCommandExecutor $commandExecutor,
$sessionId,
- WebDriverCapabilities $capabilities = null,
+ WebDriverCapabilities $capabilities,
$isW3cCompliant = false
) {
$this->executor = $commandExecutor;
$this->sessionID = $sessionId;
$this->isW3cCompliant = $isW3cCompliant;
-
- if ($capabilities !== null) {
- $this->capabilities = $capabilities;
- }
+ $this->capabilities = $capabilities;
}
/**
@@ -91,7 +89,7 @@ public static function create(
$request_timeout_in_ms = null,
$http_proxy = null,
$http_proxy_port = null,
- DesiredCapabilities $required_capabilities = null
+ ?DesiredCapabilities $required_capabilities = null
) {
$selenium_server_url = preg_replace('#/+$#', '', $selenium_server_url);
@@ -126,11 +124,7 @@ public static function create(
$parameters['desiredCapabilities'] = (object) $desired_capabilities->toArray();
- $command = new WebDriverCommand(
- null,
- DriverCommand::NEW_SESSION,
- $parameters
- );
+ $command = WebDriverCommand::newSession($parameters);
$response = $executor->execute($command);
@@ -140,14 +134,22 @@ public static function create(
/**
* [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(
@@ -158,6 +160,7 @@ public static function createBySessionID(
) {
// BC layer to not break the method signature
$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) {
@@ -171,7 +174,12 @@ public static function createBySessionID(
$executor->disableW3cCompliance();
}
- return new static($executor, $session_id, null, $isW3cCompliant);
+ // 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);
}
/**
@@ -201,7 +209,6 @@ public function 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
*/
@@ -218,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
*/
@@ -229,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));
@@ -368,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);
}
/**
@@ -551,7 +547,7 @@ public function getSessionID()
/**
* Get capabilities of the RemoteWebDriver.
*
- * @return WebDriverCapabilities
+ * @return WebDriverCapabilities|null
*/
public function getCapabilities()
{
@@ -561,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
@@ -581,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,
@@ -660,7 +670,6 @@ protected static function createFromResponse(WebDriverResponse $response, HttpCo
/**
* Prepare arguments for JavaScript injection
*
- * @param array $arguments
* @return array
*/
protected function prepareScriptArguments(array $arguments)
@@ -726,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 6dc379378..e0ce43b55 100644
--- a/lib/Remote/RemoteWebElement.php
+++ b/lib/Remote/RemoteWebElement.php
@@ -3,9 +3,14 @@
namespace Facebook\WebDriver\Remote;
use Facebook\WebDriver\Exception\ElementNotInteractableException;
-use Facebook\WebDriver\Exception\WebDriverException;
+use Facebook\WebDriver\Exception\Internal\IOException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
+use Facebook\WebDriver\Exception\Internal\UnexpectedResponseException;
+use Facebook\WebDriver\Exception\PhpWebDriverExceptionInterface;
+use Facebook\WebDriver\Exception\UnsupportedOperationException;
use Facebook\WebDriver\Interactions\Internal\WebDriverCoordinates;
use Facebook\WebDriver\Internal\WebDriverLocatable;
+use Facebook\WebDriver\Support\ScreenshotHelper;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverDimension;
use Facebook\WebDriver\WebDriverElement;
@@ -36,7 +41,6 @@ class RemoteWebElement implements WebDriverElement, WebDriverLocatable
protected $isW3cCompliant;
/**
- * @param RemoteExecuteMethod $executor
* @param string $id
* @param bool $isW3cCompliant
*/
@@ -51,7 +55,7 @@ public function __construct(RemoteExecuteMethod $executor, $id, $isW3cCompliant
/**
* Clear content editable or resettable element
*
- * @return RemoteWebElement The current instance.
+ * @return $this The current instance.
*/
public function clear()
{
@@ -66,7 +70,7 @@ public function clear()
/**
* Click this element.
*
- * @return RemoteWebElement The current instance.
+ * @return $this The current instance.
*/
public function click()
{
@@ -88,8 +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)
@@ -108,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
*/
@@ -122,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));
@@ -132,9 +146,12 @@ public function findElements(WebDriverBy $by)
/**
* 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)
{
@@ -162,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.
*
@@ -206,11 +245,11 @@ public function getLocationOnScreenOnceScrolledIntoView()
{
if ($this->isW3cCompliant) {
$script = <<executor->execute(DriverCommand::EXECUTE_SCRIPT, [
'script' => $script,
@@ -342,7 +381,7 @@ 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)
{
@@ -373,7 +412,7 @@ public function sendKeys($value)
// 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 (WebDriverException $e) {
+ } catch (PhpWebDriverExceptionInterface $e) {
$fileName = $local_file;
}
@@ -404,8 +443,7 @@ public function sendKeys($value)
*
* eg. `$element->setFileDetector(new LocalFileDetector);`
*
- * @param FileDetector $detector
- * @return RemoteWebElement
+ * @return $this
* @see FileDetector
* @see LocalFileDetector
* @see UselessFileDetector
@@ -420,14 +458,14 @@ 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 = $script = <<executor->execute(DriverCommand::EXECUTE_SCRIPT, [
'script' => $submitPolyfill,
'args' => [[JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $this->id]],
@@ -474,29 +512,12 @@ public function getID()
*/
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 elements IDs refer to the same DOM element.
*
- * @param WebDriverElement $other
* @return bool
*/
public function equals(WebDriverElement $other)
@@ -511,6 +532,27 @@ 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.
*
@@ -562,13 +604,13 @@ 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);
@@ -595,7 +637,7 @@ protected function createTemporaryZipArchive($fileToZip)
$zip = new ZipArchive();
if (($errorCode = $zip->open($tempZipPath, ZipArchive::CREATE)) !== true) {
- throw new WebDriverException(sprintf('Error creating zip archive: %s', $errorCode));
+ throw IOException::forFileError(sprintf('Error creating zip archive: %s', $errorCode), $tempZipPath);
}
$info = pathinfo($fileToZip);
diff --git a/lib/Remote/Service/DriverCommandExecutor.php b/lib/Remote/Service/DriverCommandExecutor.php
index 36dc2f751..5e3ef8399 100644
--- a/lib/Remote/Service/DriverCommandExecutor.php
+++ b/lib/Remote/Service/DriverCommandExecutor.php
@@ -2,7 +2,7 @@
namespace Facebook\WebDriver\Remote\Service;
-use Facebook\WebDriver\Exception\DriverServerDiedException;
+use Facebook\WebDriver\Exception\Internal\DriverServerDiedException;
use Facebook\WebDriver\Exception\WebDriverException;
use Facebook\WebDriver\Remote\DriverCommand;
use Facebook\WebDriver\Remote\HttpCommandExecutor;
@@ -26,10 +26,8 @@ public function __construct(DriverService $service)
}
/**
- * @param WebDriverCommand $command
- *
- * @throws WebDriverException
* @throws \Exception
+ * @throws WebDriverException
* @return WebDriverResponse
*/
public function execute(WebDriverCommand $command)
diff --git a/lib/Remote/Service/DriverService.php b/lib/Remote/Service/DriverService.php
index 8b8914815..028b8cd83 100644
--- a/lib/Remote/Service/DriverService.php
+++ b/lib/Remote/Service/DriverService.php
@@ -2,10 +2,10 @@
namespace Facebook\WebDriver\Remote\Service;
-use Exception;
+use Facebook\WebDriver\Exception\Internal\IOException;
+use Facebook\WebDriver\Exception\Internal\RuntimeException;
use Facebook\WebDriver\Net\URLChecker;
use Symfony\Component\Process\Process;
-use Symfony\Component\Process\ProcessBuilder;
/**
* Start local WebDriver service (when remote WebDriver server is not used).
@@ -123,7 +123,7 @@ protected static function checkExecutable($executable)
/**
* @param string $executable
- * @throws Exception
+ * @throws IOException
*/
protected function setExecutable($executable)
{
@@ -133,12 +133,10 @@ protected function setExecutable($executable)
return;
}
- throw new Exception(
- sprintf(
- '"%s" is not executable. Make sure the path is correct or use environment variable to specify'
- . ' location of the executable.',
- $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
);
}
@@ -150,33 +148,12 @@ protected function checkWasStarted($process)
usleep(10000); // wait 10ms, otherwise the asynchronous process failure may not yet be propagated
if (!$process->isRunning()) {
- throw new Exception(
- sprintf(
- 'Error starting driver executable "%s": %s',
- $process->getCommandLine(),
- $process->getErrorOutput()
- )
- );
+ throw RuntimeException::forDriverError($process);
}
}
- /**
- * @return Process
- */
- private function createProcess()
+ private function createProcess(): 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();
- }
- // Safe to use since Symfony 3.3
$commandLine = array_merge([$this->executable], $this->args);
return new Process($commandLine, null, $this->environment);
@@ -184,11 +161,8 @@ private function createProcess()
/**
* Check whether given file is executable directly or using system PATH
- *
- * @param string $filename
- * @return bool
*/
- private function isExecutable($filename)
+ private function isExecutable(string $filename): bool
{
if (is_executable($filename)) {
return true;
diff --git a/lib/Remote/ShadowRoot.php b/lib/Remote/ShadowRoot.php
new file mode 100644
index 000000000..5d419b19a
--- /dev/null
+++ b/lib/Remote/ShadowRoot.php
@@ -0,0 +1,98 @@
+executor = $executor;
+ $this->id = $id;
+ }
+
+ /**
+ * @return self
+ */
+ public static function createFromResponse(RemoteExecuteMethod $executor, array $response)
+ {
+ if (empty($response[self::SHADOW_ROOT_IDENTIFIER])) {
+ throw new UnknownErrorException('Shadow root is missing in server response');
+ }
+
+ return new self($executor, $response[self::SHADOW_ROOT_IDENTIFIER]);
+ }
+
+ /**
+ * @return RemoteWebElement
+ */
+ public function findElement(WebDriverBy $locator)
+ {
+ $params = JsonWireCompat::getUsing($locator, true);
+ $params[':id'] = $this->id;
+
+ $rawElement = $this->executor->execute(
+ DriverCommand::FIND_ELEMENT_FROM_SHADOW_ROOT,
+ $params
+ );
+
+ return new RemoteWebElement($this->executor, JsonWireCompat::getElement($rawElement), true);
+ }
+
+ /**
+ * @return WebDriverElement[]
+ */
+ public function findElements(WebDriverBy $locator)
+ {
+ $params = JsonWireCompat::getUsing($locator, true);
+ $params[':id'] = $this->id;
+
+ $rawElements = $this->executor->execute(
+ DriverCommand::FIND_ELEMENTS_FROM_SHADOW_ROOT,
+ $params
+ );
+
+ if (!is_array($rawElements)) {
+ throw UnexpectedResponseException::forError(
+ 'Server response to findElementsFromShadowRoot command is not an array'
+ );
+ }
+
+ $elements = [];
+ foreach ($rawElements as $rawElement) {
+ $elements[] = new RemoteWebElement($this->executor, JsonWireCompat::getElement($rawElement), true);
+ }
+
+ return $elements;
+ }
+
+ /**
+ * @return string
+ */
+ public function getID()
+ {
+ return $this->id;
+ }
+}
diff --git a/lib/Remote/WebDriverBrowserType.php b/lib/Remote/WebDriverBrowserType.php
index 77badd6cc..9e3f4e0ff 100644
--- a/lib/Remote/WebDriverBrowserType.php
+++ b/lib/Remote/WebDriverBrowserType.php
@@ -9,30 +9,30 @@
*/
class WebDriverBrowserType
{
- const FIREFOX = 'firefox';
- const FIREFOX_PROXY = 'firefoxproxy';
- const FIREFOX_CHROME = 'firefoxchrome';
- const GOOGLECHROME = 'googlechrome';
- const SAFARI = 'safari';
- const SAFARI_PROXY = 'safariproxy';
- const OPERA = 'opera';
- const MICROSOFT_EDGE = 'MicrosoftEdge';
- const IEXPLORE = 'iexplore';
- const IEXPLORE_PROXY = 'iexploreproxy';
- const CHROME = 'chrome';
- const KONQUEROR = 'konqueror';
- const MOCK = 'mock';
- const IE_HTA = 'iehta';
- const ANDROID = 'android';
- const HTMLUNIT = 'htmlunit';
- const IE = 'internet explorer';
- const IPHONE = 'iphone';
- const IPAD = 'iPad';
+ public const FIREFOX = 'firefox';
+ public const FIREFOX_PROXY = 'firefoxproxy';
+ public const FIREFOX_CHROME = 'firefoxchrome';
+ public const GOOGLECHROME = 'googlechrome';
+ public const SAFARI = 'safari';
+ public const SAFARI_PROXY = 'safariproxy';
+ public const OPERA = 'opera';
+ public const MICROSOFT_EDGE = 'MicrosoftEdge';
+ public const IEXPLORE = 'iexplore';
+ public const IEXPLORE_PROXY = 'iexploreproxy';
+ public const CHROME = 'chrome';
+ public const KONQUEROR = 'konqueror';
+ public const MOCK = 'mock';
+ public const IE_HTA = 'iehta';
+ public const ANDROID = 'android';
+ public const HTMLUNIT = 'htmlunit';
+ public const IE = 'internet explorer';
+ public const IPHONE = 'iphone';
+ public const IPAD = 'iPad';
/**
* @deprecated PhantomJS is no longer developed and its support will be removed in next major version.
* Use headless Chrome or Firefox instead.
*/
- const PHANTOMJS = 'phantomjs';
+ public const PHANTOMJS = 'phantomjs';
private function __construct()
{
diff --git a/lib/Remote/WebDriverCapabilityType.php b/lib/Remote/WebDriverCapabilityType.php
index 45833339a..ee2538061 100644
--- a/lib/Remote/WebDriverCapabilityType.php
+++ b/lib/Remote/WebDriverCapabilityType.php
@@ -9,22 +9,22 @@
*/
class WebDriverCapabilityType
{
- const BROWSER_NAME = 'browserName';
- const VERSION = 'version';
- const PLATFORM = 'platform';
- const JAVASCRIPT_ENABLED = 'javascriptEnabled';
- const TAKES_SCREENSHOT = 'takesScreenshot';
- const HANDLES_ALERTS = 'handlesAlerts';
- const DATABASE_ENABLED = 'databaseEnabled';
- const LOCATION_CONTEXT_ENABLED = 'locationContextEnabled';
- const APPLICATION_CACHE_ENABLED = 'applicationCacheEnabled';
- const BROWSER_CONNECTION_ENABLED = 'browserConnectionEnabled';
- const CSS_SELECTORS_ENABLED = 'cssSelectorsEnabled';
- const WEB_STORAGE_ENABLED = 'webStorageEnabled';
- const ROTATABLE = 'rotatable';
- const ACCEPT_SSL_CERTS = 'acceptSslCerts';
- const NATIVE_EVENTS = 'nativeEvents';
- const PROXY = 'proxy';
+ public const BROWSER_NAME = 'browserName';
+ public const VERSION = 'version';
+ public const PLATFORM = 'platform';
+ public const JAVASCRIPT_ENABLED = 'javascriptEnabled';
+ public const TAKES_SCREENSHOT = 'takesScreenshot';
+ public const HANDLES_ALERTS = 'handlesAlerts';
+ public const DATABASE_ENABLED = 'databaseEnabled';
+ public const LOCATION_CONTEXT_ENABLED = 'locationContextEnabled';
+ public const APPLICATION_CACHE_ENABLED = 'applicationCacheEnabled';
+ public const BROWSER_CONNECTION_ENABLED = 'browserConnectionEnabled';
+ public const CSS_SELECTORS_ENABLED = 'cssSelectorsEnabled';
+ public const WEB_STORAGE_ENABLED = 'webStorageEnabled';
+ public const ROTATABLE = 'rotatable';
+ public const ACCEPT_SSL_CERTS = 'acceptSslCerts';
+ public const NATIVE_EVENTS = 'nativeEvents';
+ public const PROXY = 'proxy';
private function __construct()
{
diff --git a/lib/Remote/WebDriverCommand.php b/lib/Remote/WebDriverCommand.php
index e21fbdaae..27f21b605 100644
--- a/lib/Remote/WebDriverCommand.php
+++ b/lib/Remote/WebDriverCommand.php
@@ -4,7 +4,7 @@
class WebDriverCommand
{
- /** @var string */
+ /** @var string|null */
protected $sessionID;
/** @var string */
protected $name;
@@ -16,6 +16,7 @@ class WebDriverCommand
* @param string $name Constant from DriverCommand
* @param array $parameters
* @todo In 2.0 force parameters to be an array, then remove is_array() checks in HttpCommandExecutor
+ * @todo In 2.0 make constructor private. Use by default static `::create()` with sessionID type string.
*/
public function __construct($session_id, $name, $parameters)
{
@@ -24,6 +25,15 @@ public function __construct($session_id, $name, $parameters)
$this->parameters = $parameters;
}
+ /**
+ * @return self
+ */
+ public static function newSession(array $parameters)
+ {
+ // TODO: In 2.0 call empty constructor and assign properties directly.
+ return new self(null, DriverCommand::NEW_SESSION, $parameters);
+ }
+
/**
* @return string
*/
@@ -33,7 +43,7 @@ public function getName()
}
/**
- * @return string
+ * @return string|null Could be null for newSession command
*/
public function getSessionID()
{
diff --git a/lib/Support/Events/EventFiringWebDriver.php b/lib/Support/Events/EventFiringWebDriver.php
index 81a27816b..21e5a6887 100644
--- a/lib/Support/Events/EventFiringWebDriver.php
+++ b/lib/Support/Events/EventFiringWebDriver.php
@@ -26,11 +26,7 @@ class EventFiringWebDriver implements WebDriver, JavaScriptExecutor
*/
protected $dispatcher;
- /**
- * @param WebDriver $driver
- * @param WebDriverDispatcher $dispatcher
- */
- public function __construct(WebDriver $driver, WebDriverDispatcher $dispatcher = null)
+ public function __construct(WebDriver $driver, ?WebDriverDispatcher $dispatcher = null)
{
$this->dispatcher = $dispatcher ?: new WebDriverDispatcher();
if (!$this->dispatcher->getDefaultDriver()) {
@@ -63,6 +59,7 @@ public function getWebDriver()
public function get($url)
{
$this->dispatch('beforeNavigateTo', $url, $this);
+
try {
$this->driver->get($url);
} catch (WebDriverException $exception) {
@@ -75,7 +72,6 @@ public function get($url)
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return array
*/
@@ -99,7 +95,6 @@ public function findElements(WebDriverBy $by)
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return EventFiringWebElement
*/
@@ -121,7 +116,6 @@ public function findElement(WebDriverBy $by)
/**
* @param string $script
- * @param array $arguments
* @throws WebDriverException
* @return mixed
*/
@@ -149,7 +143,6 @@ public function executeScript($script, array $arguments = [])
/**
* @param string $script
- * @param array $arguments
* @throws WebDriverException
* @return mixed
*/
@@ -162,6 +155,7 @@ public function executeAsyncScript($script, array $arguments = [])
}
$this->dispatch('beforeScript', $script, $this);
+
try {
$result = $this->driver->executeAsyncScript($script, $arguments);
} catch (WebDriverException $exception) {
@@ -373,7 +367,6 @@ public function execute($name, $params)
}
/**
- * @param WebDriverElement $element
* @return EventFiringWebElement
*/
protected function newElement(WebDriverElement $element)
@@ -394,9 +387,6 @@ protected function dispatch($method, ...$arguments)
$this->dispatcher->dispatch($method, $arguments);
}
- /**
- * @param WebDriverException $exception
- */
protected function dispatchOnException(WebDriverException $exception)
{
$this->dispatch('onException', $exception, $this);
diff --git a/lib/Support/Events/EventFiringWebDriverNavigation.php b/lib/Support/Events/EventFiringWebDriverNavigation.php
index 59eab3fd7..27ea34205 100644
--- a/lib/Support/Events/EventFiringWebDriverNavigation.php
+++ b/lib/Support/Events/EventFiringWebDriverNavigation.php
@@ -17,10 +17,6 @@ class EventFiringWebDriverNavigation implements WebDriverNavigationInterface
*/
protected $dispatcher;
- /**
- * @param WebDriverNavigationInterface $navigator
- * @param WebDriverDispatcher $dispatcher
- */
public function __construct(WebDriverNavigationInterface $navigator, WebDriverDispatcher $dispatcher)
{
$this->navigator = $navigator;
@@ -49,6 +45,7 @@ public function back()
'beforeNavigateBack',
$this->getDispatcher()->getDefaultDriver()
);
+
try {
$this->navigator->back();
} catch (WebDriverException $exception) {
@@ -68,6 +65,7 @@ public function forward()
'beforeNavigateForward',
$this->getDispatcher()->getDefaultDriver()
);
+
try {
$this->navigator->forward();
} catch (WebDriverException $exception) {
@@ -130,9 +128,6 @@ protected function dispatch($method, ...$arguments)
$this->dispatcher->dispatch($method, $arguments);
}
- /**
- * @param WebDriverException $exception
- */
protected function dispatchOnException(WebDriverException $exception)
{
$this->dispatch('onException', $exception);
diff --git a/lib/Support/Events/EventFiringWebElement.php b/lib/Support/Events/EventFiringWebElement.php
index 0e9271665..6caa08684 100644
--- a/lib/Support/Events/EventFiringWebElement.php
+++ b/lib/Support/Events/EventFiringWebElement.php
@@ -22,10 +22,6 @@ class EventFiringWebElement implements WebDriverElement, WebDriverLocatable
*/
protected $dispatcher;
- /**
- * @param WebDriverElement $element
- * @param WebDriverDispatcher $dispatcher
- */
public function __construct(WebDriverElement $element, WebDriverDispatcher $dispatcher)
{
$this->element = $element;
@@ -56,6 +52,7 @@ public function getElement()
public function sendKeys($value)
{
$this->dispatch('beforeChangeValueOf', $this);
+
try {
$this->element->sendKeys($value);
} catch (WebDriverException $exception) {
@@ -74,6 +71,7 @@ public function sendKeys($value)
public function click()
{
$this->dispatch('beforeClickOn', $this);
+
try {
$this->element->click();
} catch (WebDriverException $exception) {
@@ -86,7 +84,6 @@ public function click()
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return EventFiringWebElement
*/
@@ -117,7 +114,6 @@ public function findElement(WebDriverBy $by)
}
/**
- * @param WebDriverBy $by
* @throws WebDriverException
* @return array
*/
@@ -129,6 +125,7 @@ public function findElements(WebDriverBy $by)
$this,
$this->dispatcher->getDefaultDriver()
);
+
try {
$elements = [];
foreach ($this->element->findElements($by) as $element) {
@@ -352,7 +349,6 @@ public function getID()
/**
* Test if two element IDs refer to the same DOM element.
*
- * @param WebDriverElement $other
* @return bool
*/
public function equals(WebDriverElement $other)
@@ -365,9 +361,26 @@ public function equals(WebDriverElement $other)
}
}
- /**
- * @param WebDriverException $exception
- */
+ public function takeElementScreenshot($save_as = null)
+ {
+ try {
+ return $this->element->takeElementScreenshot($save_as);
+ } catch (WebDriverException $exception) {
+ $this->dispatchOnException($exception);
+ throw $exception;
+ }
+ }
+
+ public function getShadowRoot()
+ {
+ try {
+ return $this->element->getShadowRoot();
+ } catch (WebDriverException $exception) {
+ $this->dispatchOnException($exception);
+ throw $exception;
+ }
+ }
+
protected function dispatchOnException(WebDriverException $exception)
{
$this->dispatch(
@@ -391,7 +404,6 @@ protected function dispatch($method, ...$arguments)
}
/**
- * @param WebDriverElement $element
* @return static
*/
protected function newElement(WebDriverElement $element)
diff --git a/lib/Support/IsElementDisplayedAtom.php b/lib/Support/IsElementDisplayedAtom.php
new file mode 100644
index 000000000..d95e4f01f
--- /dev/null
+++ b/lib/Support/IsElementDisplayedAtom.php
@@ -0,0 +1,71 @@
+driver = $driver;
+ }
+
+ public static function match($browserName)
+ {
+ return !in_array($browserName, self::BROWSERS_WITH_ENDPOINT_SUPPORT, true);
+ }
+
+ public function execute($params)
+ {
+ $element = new RemoteWebElement(
+ new RemoteExecuteMethod($this->driver),
+ $params[':id'],
+ $this->driver->isW3cCompliant()
+ );
+
+ return $this->executeAtom('isElementDisplayed', $element);
+ }
+
+ protected function executeAtom($atomName, ...$params)
+ {
+ return $this->driver->executeScript(
+ sprintf('%s; return (%s).apply(null, arguments);', $this->loadAtomScript($atomName), $atomName),
+ $params
+ );
+ }
+
+ private function loadAtomScript($atomName)
+ {
+ return file_get_contents(__DIR__ . '/../scripts/' . $atomName . '.js');
+ }
+}
diff --git a/lib/Support/ScreenshotHelper.php b/lib/Support/ScreenshotHelper.php
new file mode 100644
index 000000000..956f56147
--- /dev/null
+++ b/lib/Support/ScreenshotHelper.php
@@ -0,0 +1,81 @@
+executor = $executor;
+ }
+
+ /**
+ * @param string|null $saveAs
+ * @throws WebDriverException
+ * @return string
+ */
+ public function takePageScreenshot($saveAs = null)
+ {
+ $commandToExecute = [DriverCommand::SCREENSHOT];
+
+ return $this->takeScreenshot($commandToExecute, $saveAs);
+ }
+
+ public function takeElementScreenshot($elementId, $saveAs = null)
+ {
+ $commandToExecute = [DriverCommand::TAKE_ELEMENT_SCREENSHOT, [':id' => $elementId]];
+
+ return $this->takeScreenshot($commandToExecute, $saveAs);
+ }
+
+ private function takeScreenshot(array $commandToExecute, $saveAs = null)
+ {
+ $response = $this->executor->execute(...$commandToExecute);
+
+ if (!is_string($response)) {
+ throw UnexpectedResponseException::forError(
+ 'Error taking screenshot, no data received from the remote end'
+ );
+ }
+
+ $screenshot = base64_decode($response, true);
+
+ if ($screenshot === false) {
+ throw UnexpectedResponseException::forError('Error decoding screenshot data');
+ }
+
+ if ($saveAs !== null) {
+ $this->saveScreenshotToPath($screenshot, $saveAs);
+ }
+
+ return $screenshot;
+ }
+
+ private function saveScreenshotToPath($screenshot, $path)
+ {
+ $this->createDirectoryIfNotExists(dirname($path));
+
+ file_put_contents($path, $screenshot);
+ }
+
+ private function createDirectoryIfNotExists($directoryPath)
+ {
+ if (!file_exists($directoryPath)) {
+ if (!mkdir($directoryPath, 0777, true) && !is_dir($directoryPath)) {
+ throw IOException::forFileError('Directory cannot be not created', $directoryPath);
+ }
+ }
+ }
+}
diff --git a/lib/WebDriverCheckboxes.php b/lib/WebDriverCheckboxes.php
index 5467589c8..abc8cc46c 100644
--- a/lib/WebDriverCheckboxes.php
+++ b/lib/WebDriverCheckboxes.php
@@ -2,7 +2,7 @@
namespace Facebook\WebDriver;
-use Facebook\WebDriver\Exception\WebDriverException;
+use Facebook\WebDriver\Exception\InvalidElementStateException;
/**
* Provides helper methods for checkboxes.
@@ -15,7 +15,7 @@ public function __construct(WebDriverElement $element)
$this->type = $element->getAttribute('type');
if ($this->type !== 'checkbox') {
- throw new WebDriverException('The input must be of type "checkbox".');
+ throw new InvalidElementStateException('The input must be of type "checkbox".');
}
}
diff --git a/lib/WebDriverCommandExecutor.php b/lib/WebDriverCommandExecutor.php
index 4031c235d..7f6bb3ece 100644
--- a/lib/WebDriverCommandExecutor.php
+++ b/lib/WebDriverCommandExecutor.php
@@ -11,8 +11,6 @@
interface WebDriverCommandExecutor
{
/**
- * @param WebDriverCommand $command
- *
* @return WebDriverResponse
*/
public function execute(WebDriverCommand $command);
diff --git a/lib/WebDriverDispatcher.php b/lib/WebDriverDispatcher.php
index 851214719..fe1ecb0f4 100644
--- a/lib/WebDriverDispatcher.php
+++ b/lib/WebDriverDispatcher.php
@@ -19,7 +19,6 @@ class WebDriverDispatcher
* this is needed so that EventFiringWebElement can pass the driver to the
* exception handling
*
- * @param EventFiringWebDriver $driver
* @return $this
*/
public function setDefaultDriver(EventFiringWebDriver $driver)
@@ -38,7 +37,6 @@ public function getDefaultDriver()
}
/**
- * @param WebDriverEventListener $listener
* @return $this
*/
public function register(WebDriverEventListener $listener)
@@ -49,7 +47,6 @@ public function register(WebDriverEventListener $listener)
}
/**
- * @param WebDriverEventListener $listener
* @return $this
*/
public function unregister(WebDriverEventListener $listener)
diff --git a/lib/WebDriverElement.php b/lib/WebDriverElement.php
index 538e0a8e5..8ffa18383 100644
--- a/lib/WebDriverElement.php
+++ b/lib/WebDriverElement.php
@@ -2,6 +2,8 @@
namespace Facebook\WebDriver;
+use Facebook\WebDriver\Remote\ShadowRoot;
+
/**
* Interface for an HTML element in the WebDriver framework.
*/
@@ -22,13 +24,26 @@ public function clear();
public function click();
/**
- * Get the value of a the given attribute of the element.
+ * Get the value of the given attribute of the element.
+ * Attribute is meant what is declared in the HTML markup of the element.
+ * To read a value of a IDL "JavaScript" property (like `innerHTML`), use `getDomProperty()` method.
*
* @param string $attribute_name The name of the attribute.
* @return string|null The value of the attribute.
*/
public function getAttribute($attribute_name);
+ /*
+ * Gets the value of a IDL JavaScript property of this element (for example `innerHTML`, `tagName` etc.).
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Glossary/IDL
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Element#properties
+ * @param string $propertyName
+ * @return string|null The property's current value or null if the value is not set or the property does not exist.
+ * @todo Add in next major release (BC)
+ */
+ // public function getDomProperty($propertyName);
+
/**
* Get the value of a given CSS property.
*
@@ -128,4 +143,12 @@ public function getID();
* @todo Add in next major release (BC)
*/
//public function takeElementScreenshot($save_as = null);
+
+ /**
+ * Get representation of an element's shadow root for accessing the shadow DOM of a web component.
+ *
+ * @return ShadowRoot
+ * @todo Add in next major release (BC)
+ */
+ //public function getShadowRoot();
}
diff --git a/lib/WebDriverEventListener.php b/lib/WebDriverEventListener.php
index 673d85bee..9ef98d8f3 100644
--- a/lib/WebDriverEventListener.php
+++ b/lib/WebDriverEventListener.php
@@ -10,85 +10,43 @@ interface WebDriverEventListener
{
/**
* @param string $url
- * @param EventFiringWebDriver $driver
*/
public function beforeNavigateTo($url, EventFiringWebDriver $driver);
/**
* @param string $url
- * @param EventFiringWebDriver $driver
*/
public function afterNavigateTo($url, EventFiringWebDriver $driver);
- /**
- * @param EventFiringWebDriver $driver
- */
public function beforeNavigateBack(EventFiringWebDriver $driver);
- /**
- * @param EventFiringWebDriver $driver
- */
public function afterNavigateBack(EventFiringWebDriver $driver);
- /**
- * @param EventFiringWebDriver $driver
- */
public function beforeNavigateForward(EventFiringWebDriver $driver);
- /**
- * @param EventFiringWebDriver $driver
- */
public function afterNavigateForward(EventFiringWebDriver $driver);
- /**
- * @param WebDriverBy $by
- * @param EventFiringWebElement|null $element
- * @param EventFiringWebDriver $driver
- */
public function beforeFindBy(WebDriverBy $by, $element, EventFiringWebDriver $driver);
- /**
- * @param WebDriverBy $by
- * @param EventFiringWebElement|null $element
- * @param EventFiringWebDriver $driver
- */
public function afterFindBy(WebDriverBy $by, $element, EventFiringWebDriver $driver);
/**
* @param string $script
- * @param EventFiringWebDriver $driver
*/
public function beforeScript($script, EventFiringWebDriver $driver);
/**
* @param string $script
- * @param EventFiringWebDriver $driver
*/
public function afterScript($script, EventFiringWebDriver $driver);
- /**
- * @param EventFiringWebElement $element
- */
public function beforeClickOn(EventFiringWebElement $element);
- /**
- * @param EventFiringWebElement $element
- */
public function afterClickOn(EventFiringWebElement $element);
- /**
- * @param EventFiringWebElement $element
- */
public function beforeChangeValueOf(EventFiringWebElement $element);
- /**
- * @param EventFiringWebElement $element
- */
public function afterChangeValueOf(EventFiringWebElement $element);
- /**
- * @param WebDriverException $exception
- * @param EventFiringWebDriver $driver
- */
- public function onException(WebDriverException $exception, EventFiringWebDriver $driver = null);
+ public function onException(WebDriverException $exception, ?EventFiringWebDriver $driver = null);
}
diff --git a/lib/WebDriverExpectedCondition.php b/lib/WebDriverExpectedCondition.php
index 02d2e71e0..188f3c4d8 100644
--- a/lib/WebDriverExpectedCondition.php
+++ b/lib/WebDriverExpectedCondition.php
@@ -2,6 +2,7 @@
namespace Facebook\WebDriver;
+use Facebook\WebDriver\Exception\Internal\LogicException;
use Facebook\WebDriver\Exception\NoSuchAlertException;
use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Exception\NoSuchFrameException;
@@ -375,9 +376,7 @@ public static function invisibilityOfElementLocated(WebDriverBy $by)
function (WebDriver $driver) use ($by) {
try {
return !$driver->findElement($by)->isDisplayed();
- } catch (NoSuchElementException $e) {
- return true;
- } catch (StaleElementReferenceException $e) {
+ } catch (NoSuchElementException|StaleElementReferenceException $e) {
return true;
}
}
@@ -397,9 +396,7 @@ public static function invisibilityOfElementWithText(WebDriverBy $by, $text)
function (WebDriver $driver) use ($by, $text) {
try {
return !($driver->findElement($by)->getText() === $text);
- } catch (NoSuchElementException $e) {
- return true;
- } catch (StaleElementReferenceException $e) {
+ } catch (NoSuchElementException|StaleElementReferenceException $e) {
return true;
}
}
@@ -422,6 +419,7 @@ function (WebDriver $driver) use ($visibility_of_element_located) {
$visibility_of_element_located->getApply(),
$driver
);
+
try {
if ($element !== null && $element->isEnabled()) {
return $element;
@@ -525,7 +523,7 @@ function (WebDriver $driver) use ($element_or_by, $selected) {
);
}
- throw new \InvalidArgumentException('Instance of either WebDriverElement or WebDriverBy must be given');
+ throw LogicException::forError('Instance of either WebDriverElement or WebDriverBy must be given');
}
/**
diff --git a/lib/WebDriverKeys.php b/lib/WebDriverKeys.php
index 5bba2eff0..40f3ffbf5 100644
--- a/lib/WebDriverKeys.php
+++ b/lib/WebDriverKeys.php
@@ -9,85 +9,85 @@
*/
class WebDriverKeys
{
- const NULL = "\xEE\x80\x80";
- const CANCEL = "\xEE\x80\x81";
- const HELP = "\xEE\x80\x82";
- const BACKSPACE = "\xEE\x80\x83";
- const TAB = "\xEE\x80\x84";
- const CLEAR = "\xEE\x80\x85";
- const RETURN_KEY = "\xEE\x80\x86";
- const ENTER = "\xEE\x80\x87";
- const SHIFT = "\xEE\x80\x88";
- const CONTROL = "\xEE\x80\x89";
- const ALT = "\xEE\x80\x8A";
- const PAUSE = "\xEE\x80\x8B";
- const ESCAPE = "\xEE\x80\x8C";
- const SPACE = "\xEE\x80\x8D";
- const PAGE_UP = "\xEE\x80\x8E";
- const PAGE_DOWN = "\xEE\x80\x8F";
- const END = "\xEE\x80\x90";
- const HOME = "\xEE\x80\x91";
- const ARROW_LEFT = "\xEE\x80\x92";
- const ARROW_UP = "\xEE\x80\x93";
- const ARROW_RIGHT = "\xEE\x80\x94";
- const ARROW_DOWN = "\xEE\x80\x95";
- const INSERT = "\xEE\x80\x96";
- const DELETE = "\xEE\x80\x97";
- const SEMICOLON = "\xEE\x80\x98";
- const EQUALS = "\xEE\x80\x99";
- const NUMPAD0 = "\xEE\x80\x9A";
- const NUMPAD1 = "\xEE\x80\x9B";
- const NUMPAD2 = "\xEE\x80\x9C";
- const NUMPAD3 = "\xEE\x80\x9D";
- const NUMPAD4 = "\xEE\x80\x9E";
- const NUMPAD5 = "\xEE\x80\x9F";
- const NUMPAD6 = "\xEE\x80\xA0";
- const NUMPAD7 = "\xEE\x80\xA1";
- const NUMPAD8 = "\xEE\x80\xA2";
- const NUMPAD9 = "\xEE\x80\xA3";
- const MULTIPLY = "\xEE\x80\xA4";
- const ADD = "\xEE\x80\xA5";
- const SEPARATOR = "\xEE\x80\xA6";
- const SUBTRACT = "\xEE\x80\xA7";
- const DECIMAL = "\xEE\x80\xA8";
- const DIVIDE = "\xEE\x80\xA9";
- const F1 = "\xEE\x80\xB1";
- const F2 = "\xEE\x80\xB2";
- const F3 = "\xEE\x80\xB3";
- const F4 = "\xEE\x80\xB4";
- const F5 = "\xEE\x80\xB5";
- const F6 = "\xEE\x80\xB6";
- const F7 = "\xEE\x80\xB7";
- const F8 = "\xEE\x80\xB8";
- const F9 = "\xEE\x80\xB9";
- const F10 = "\xEE\x80\xBA";
- const F11 = "\xEE\x80\xBB";
- const F12 = "\xEE\x80\xBC";
- const META = "\xEE\x80\xBD";
- const ZENKAKU_HANKAKU = "\xEE\x80\xC0";
- const RIGHT_SHIFT = "\xEE\x81\x90";
- const RIGHT_CONTROL = "\xEE\x81\x91";
- const RIGHT_ALT = "\xEE\x81\x92";
- const RIGHT_META = "\xEE\x81\x93";
- const NUMPAD_PAGE_UP = "\xEE\x81\x94";
- const NUMPAD_PAGE_DOWN = "\xEE\x81\x95";
- const NUMPAD_END = "\xEE\x81\x96";
- const NUMPAD_HOME = "\xEE\x81\x97";
- const NUMPAD_ARROW_LEFT = "\xEE\x81\x98";
- const NUMPAD_ARROW_UP = "\xEE\x81\x99";
- const NUMPAD_ARROW_RIGHT = "\xEE\x81\x9A";
- const NUMPAD_ARROW_DOWN = "\xEE\x81\x9B";
- const NUMPAD_ARROW_INSERT = "\xEE\x81\x9C";
- const NUMPAD_ARROW_DELETE = "\xEE\x81\x9D";
+ public const NULL = "\xEE\x80\x80";
+ public const CANCEL = "\xEE\x80\x81";
+ public const HELP = "\xEE\x80\x82";
+ public const BACKSPACE = "\xEE\x80\x83";
+ public const TAB = "\xEE\x80\x84";
+ public const CLEAR = "\xEE\x80\x85";
+ public const RETURN_KEY = "\xEE\x80\x86";
+ public const ENTER = "\xEE\x80\x87";
+ public const SHIFT = "\xEE\x80\x88";
+ public const CONTROL = "\xEE\x80\x89";
+ public const ALT = "\xEE\x80\x8A";
+ public const PAUSE = "\xEE\x80\x8B";
+ public const ESCAPE = "\xEE\x80\x8C";
+ public const SPACE = "\xEE\x80\x8D";
+ public const PAGE_UP = "\xEE\x80\x8E";
+ public const PAGE_DOWN = "\xEE\x80\x8F";
+ public const END = "\xEE\x80\x90";
+ public const HOME = "\xEE\x80\x91";
+ public const ARROW_LEFT = "\xEE\x80\x92";
+ public const ARROW_UP = "\xEE\x80\x93";
+ public const ARROW_RIGHT = "\xEE\x80\x94";
+ public const ARROW_DOWN = "\xEE\x80\x95";
+ public const INSERT = "\xEE\x80\x96";
+ public const DELETE = "\xEE\x80\x97";
+ public const SEMICOLON = "\xEE\x80\x98";
+ public const EQUALS = "\xEE\x80\x99";
+ public const NUMPAD0 = "\xEE\x80\x9A";
+ public const NUMPAD1 = "\xEE\x80\x9B";
+ public const NUMPAD2 = "\xEE\x80\x9C";
+ public const NUMPAD3 = "\xEE\x80\x9D";
+ public const NUMPAD4 = "\xEE\x80\x9E";
+ public const NUMPAD5 = "\xEE\x80\x9F";
+ public const NUMPAD6 = "\xEE\x80\xA0";
+ public const NUMPAD7 = "\xEE\x80\xA1";
+ public const NUMPAD8 = "\xEE\x80\xA2";
+ public const NUMPAD9 = "\xEE\x80\xA3";
+ public const MULTIPLY = "\xEE\x80\xA4";
+ public const ADD = "\xEE\x80\xA5";
+ public const SEPARATOR = "\xEE\x80\xA6";
+ public const SUBTRACT = "\xEE\x80\xA7";
+ public const DECIMAL = "\xEE\x80\xA8";
+ public const DIVIDE = "\xEE\x80\xA9";
+ public const F1 = "\xEE\x80\xB1";
+ public const F2 = "\xEE\x80\xB2";
+ public const F3 = "\xEE\x80\xB3";
+ public const F4 = "\xEE\x80\xB4";
+ public const F5 = "\xEE\x80\xB5";
+ public const F6 = "\xEE\x80\xB6";
+ public const F7 = "\xEE\x80\xB7";
+ public const F8 = "\xEE\x80\xB8";
+ public const F9 = "\xEE\x80\xB9";
+ public const F10 = "\xEE\x80\xBA";
+ public const F11 = "\xEE\x80\xBB";
+ public const F12 = "\xEE\x80\xBC";
+ public const META = "\xEE\x80\xBD";
+ public const ZENKAKU_HANKAKU = "\xEE\x80\xC0";
+ public const RIGHT_SHIFT = "\xEE\x81\x90";
+ public const RIGHT_CONTROL = "\xEE\x81\x91";
+ public const RIGHT_ALT = "\xEE\x81\x92";
+ public const RIGHT_META = "\xEE\x81\x93";
+ public const NUMPAD_PAGE_UP = "\xEE\x81\x94";
+ public const NUMPAD_PAGE_DOWN = "\xEE\x81\x95";
+ public const NUMPAD_END = "\xEE\x81\x96";
+ public const NUMPAD_HOME = "\xEE\x81\x97";
+ public const NUMPAD_ARROW_LEFT = "\xEE\x81\x98";
+ public const NUMPAD_ARROW_UP = "\xEE\x81\x99";
+ public const NUMPAD_ARROW_RIGHT = "\xEE\x81\x9A";
+ public const NUMPAD_ARROW_DOWN = "\xEE\x81\x9B";
+ public const NUMPAD_ARROW_INSERT = "\xEE\x81\x9C";
+ public const NUMPAD_ARROW_DELETE = "\xEE\x81\x9D";
// Aliases
- const LEFT_SHIFT = self::SHIFT;
- const LEFT_CONTROL = self::CONTROL;
- const LEFT_ALT = self::ALT;
- const LEFT = self::ARROW_LEFT;
- const UP = self::ARROW_UP;
- const RIGHT = self::ARROW_RIGHT;
- const DOWN = self::ARROW_DOWN;
- const COMMAND = self::META;
+ public const LEFT_SHIFT = self::SHIFT;
+ public const LEFT_CONTROL = self::CONTROL;
+ public const LEFT_ALT = self::ALT;
+ public const LEFT = self::ARROW_LEFT;
+ public const UP = self::ARROW_UP;
+ public const RIGHT = self::ARROW_RIGHT;
+ public const DOWN = self::ARROW_DOWN;
+ public const COMMAND = self::META;
/**
* Encode input of `sendKeys()` to appropriate format according to protocol.
diff --git a/lib/WebDriverMouse.php b/lib/WebDriverMouse.php
index 5178a314b..7c9362329 100644
--- a/lib/WebDriverMouse.php
+++ b/lib/WebDriverMouse.php
@@ -10,31 +10,26 @@
interface WebDriverMouse
{
/**
- * @param WebDriverCoordinates $where
* @return WebDriverMouse
*/
public function click(WebDriverCoordinates $where);
/**
- * @param WebDriverCoordinates $where
* @return WebDriverMouse
*/
public function contextClick(WebDriverCoordinates $where);
/**
- * @param WebDriverCoordinates $where
* @return WebDriverMouse
*/
public function doubleClick(WebDriverCoordinates $where);
/**
- * @param WebDriverCoordinates $where
* @return WebDriverMouse
*/
public function mouseDown(WebDriverCoordinates $where);
/**
- * @param WebDriverCoordinates $where
* @param int $x_offset
* @param int $y_offset
* @return WebDriverMouse
@@ -46,7 +41,6 @@ public function mouseMove(
);
/**
- * @param WebDriverCoordinates $where
* @return WebDriverMouse
*/
public function mouseUp(WebDriverCoordinates $where);
diff --git a/lib/WebDriverOptions.php b/lib/WebDriverOptions.php
index 84f2270eb..c84757fe8 100644
--- a/lib/WebDriverOptions.php
+++ b/lib/WebDriverOptions.php
@@ -2,10 +2,10 @@
namespace Facebook\WebDriver;
+use Facebook\WebDriver\Exception\Internal\LogicException;
use Facebook\WebDriver\Exception\NoSuchCookieException;
use Facebook\WebDriver\Remote\DriverCommand;
use Facebook\WebDriver\Remote\ExecuteMethod;
-use InvalidArgumentException;
/**
* Managing stuff you would do in a browser.
@@ -40,7 +40,7 @@ public function addCookie($cookie)
$cookie = Cookie::createFromArray($cookie);
}
if (!$cookie instanceof Cookie) {
- throw new InvalidArgumentException('Cookie must be set from instance of Cookie class or from array.');
+ throw LogicException::forError('Cookie must be set from instance of Cookie class or from array.');
}
$this->executor->execute(
diff --git a/lib/WebDriverPlatform.php b/lib/WebDriverPlatform.php
index d7194e05e..a589f3013 100644
--- a/lib/WebDriverPlatform.php
+++ b/lib/WebDriverPlatform.php
@@ -9,15 +9,15 @@
*/
class WebDriverPlatform
{
- const ANDROID = 'ANDROID';
+ public const ANDROID = 'ANDROID';
/** @deprecated ANY has no meaning in W3C WebDriver, see https://github.com/php-webdriver/php-webdriver/pull/731 */
- const ANY = 'ANY';
- const LINUX = 'LINUX';
- const MAC = 'MAC';
- const UNIX = 'UNIX';
- const VISTA = 'VISTA';
- const WINDOWS = 'WINDOWS';
- const XP = 'XP';
+ public const ANY = 'ANY';
+ public const LINUX = 'LINUX';
+ public const MAC = 'MAC';
+ public const UNIX = 'UNIX';
+ public const VISTA = 'VISTA';
+ public const WINDOWS = 'WINDOWS';
+ public const XP = 'XP';
private function __construct()
{
diff --git a/lib/WebDriverRadios.php b/lib/WebDriverRadios.php
index e1687983e..aeaaaecac 100644
--- a/lib/WebDriverRadios.php
+++ b/lib/WebDriverRadios.php
@@ -2,8 +2,8 @@
namespace Facebook\WebDriver;
+use Facebook\WebDriver\Exception\InvalidElementStateException;
use Facebook\WebDriver\Exception\UnsupportedOperationException;
-use Facebook\WebDriver\Exception\WebDriverException;
/**
* Provides helper methods for radio buttons.
@@ -16,7 +16,7 @@ public function __construct(WebDriverElement $element)
$this->type = $element->getAttribute('type');
if ($this->type !== 'radio') {
- throw new WebDriverException('The input must be of type "radio".');
+ throw new InvalidElementStateException('The input must be of type "radio".');
}
}
diff --git a/lib/WebDriverSearchContext.php b/lib/WebDriverSearchContext.php
index 8a723d8e7..5fb1daaf9 100644
--- a/lib/WebDriverSearchContext.php
+++ b/lib/WebDriverSearchContext.php
@@ -2,19 +2,18 @@
namespace Facebook\WebDriver;
+use Facebook\WebDriver\Exception\NoSuchElementException;
+
/**
- * The interface for WebDriver and WebDriverElement which is able to search for
- * WebDriverElement inside.
+ * The interface for WebDriver and WebDriverElement which is able to search for WebDriverElement inside.
*/
interface WebDriverSearchContext
{
/**
- * Find the first WebDriverElement within this element using the given
- * mechanism.
+ * Find the first WebDriverElement within this element using the given mechanism.
*
- * @param WebDriverBy $locator
- * @return WebDriverElement NoSuchElementException is thrown in
- * HttpCommandExecutor if no element is found.
+ * @throws NoSuchElementException If no element is found
+ * @return WebDriverElement
* @see WebDriverBy
*/
public function findElement(WebDriverBy $locator);
@@ -22,9 +21,7 @@ public function findElement(WebDriverBy $locator);
/**
* Find all WebDriverElements within this element using the given mechanism.
*
- * @param WebDriverBy $locator
- * @return WebDriverElement[] A list of all WebDriverElements, or an empty array if
- * nothing matches
+ * @return WebDriverElement[] A list of all WebDriverElements, or an empty array if nothing matches
* @see WebDriverBy
*/
public function findElements(WebDriverBy $locator);
diff --git a/lib/WebDriverSelect.php b/lib/WebDriverSelect.php
index 53d894b96..85e1d26e4 100644
--- a/lib/WebDriverSelect.php
+++ b/lib/WebDriverSelect.php
@@ -26,7 +26,14 @@ public function __construct(WebDriverElement $element)
}
$this->element = $element;
$value = $element->getAttribute('multiple');
- $this->isMulti = $value === 'true';
+
+ /**
+ * There is a bug in safari webdriver that returns 'multiple' instead of 'true' which does not match the spec.
+ * Apple Feedback #FB12760673
+ *
+ * @see https://www.w3.org/TR/webdriver2/#get-element-attribute
+ */
+ $this->isMulti = $value === 'true' || $value === 'multiple';
}
public function isMultiple()
@@ -223,7 +230,6 @@ public function deselectByVisiblePartialText($text)
/**
* Mark option selected
- * @param WebDriverElement $option
*/
protected function selectOption(WebDriverElement $option)
{
@@ -234,7 +240,6 @@ protected function selectOption(WebDriverElement $option)
/**
* Mark option not selected
- * @param WebDriverElement $option
*/
protected function deselectOption(WebDriverElement $option)
{
diff --git a/lib/WebDriverTargetLocator.php b/lib/WebDriverTargetLocator.php
index 08096bcab..8787f66c5 100644
--- a/lib/WebDriverTargetLocator.php
+++ b/lib/WebDriverTargetLocator.php
@@ -8,9 +8,9 @@
interface WebDriverTargetLocator
{
/** @var string */
- const WINDOW_TYPE_WINDOW = 'window';
+ public const WINDOW_TYPE_WINDOW = 'window';
/** @var string */
- const WINDOW_TYPE_TAB = 'tab';
+ public const WINDOW_TYPE_TAB = 'tab';
/**
* Set the current browsing context to the current top-level browsing context.
diff --git a/lib/WebDriverTimeouts.php b/lib/WebDriverTimeouts.php
index add6a0900..73a5cddf8 100644
--- a/lib/WebDriverTimeouts.php
+++ b/lib/WebDriverTimeouts.php
@@ -28,7 +28,7 @@ public function __construct(ExecuteMethod $executor, $isW3cCompliant = false)
/**
* Specify the amount of time the driver should wait when searching for an element if it is not immediately present.
*
- * @param int $seconds Wait time in second.
+ * @param null|int|float $seconds Wait time in seconds.
* @return WebDriverTimeouts The current instance.
*/
public function implicitlyWait($seconds)
@@ -36,15 +36,18 @@ public function implicitlyWait($seconds)
if ($this->isW3cCompliant) {
$this->executor->execute(
DriverCommand::IMPLICITLY_WAIT,
- ['implicit' => $seconds * 1000]
+ ['implicit' => $seconds === null ? null : floor($seconds * 1000)]
);
return $this;
}
+ if ($seconds === null) {
+ throw new \InvalidArgumentException('JsonWire Protocol implicit-wait-timeout value cannot be null');
+ }
$this->executor->execute(
DriverCommand::IMPLICITLY_WAIT,
- ['ms' => $seconds * 1000]
+ ['ms' => floor($seconds * 1000)]
);
return $this;
@@ -53,7 +56,7 @@ public function implicitlyWait($seconds)
/**
* Set the amount of time to wait for an asynchronous script to finish execution before throwing an error.
*
- * @param int $seconds Wait time in second.
+ * @param null|int|float $seconds Wait time in seconds.
* @return WebDriverTimeouts The current instance.
*/
public function setScriptTimeout($seconds)
@@ -61,15 +64,18 @@ public function setScriptTimeout($seconds)
if ($this->isW3cCompliant) {
$this->executor->execute(
DriverCommand::SET_SCRIPT_TIMEOUT,
- ['script' => $seconds * 1000]
+ ['script' => $seconds === null ? null : floor($seconds * 1000)]
);
return $this;
}
+ if ($seconds === null) {
+ throw new \InvalidArgumentException('JsonWire Protocol script-timeout value cannot be null');
+ }
$this->executor->execute(
DriverCommand::SET_SCRIPT_TIMEOUT,
- ['ms' => $seconds * 1000]
+ ['ms' => floor($seconds * 1000)]
);
return $this;
@@ -78,7 +84,7 @@ public function setScriptTimeout($seconds)
/**
* Set the amount of time to wait for a page load to complete before throwing an error.
*
- * @param int $seconds Wait time in second.
+ * @param null|int|float $seconds Wait time in seconds.
* @return WebDriverTimeouts The current instance.
*/
public function pageLoadTimeout($seconds)
@@ -86,15 +92,18 @@ public function pageLoadTimeout($seconds)
if ($this->isW3cCompliant) {
$this->executor->execute(
DriverCommand::SET_SCRIPT_TIMEOUT,
- ['pageLoad' => $seconds * 1000]
+ ['pageLoad' => $seconds === null ? null : floor($seconds * 1000)]
);
return $this;
}
+ if ($seconds === null) {
+ throw new \InvalidArgumentException('JsonWire Protocol page-load-timeout value cannot be null');
+ }
$this->executor->execute(DriverCommand::SET_TIMEOUT, [
'type' => 'page load',
- 'ms' => $seconds * 1000,
+ 'ms' => floor($seconds * 1000),
]);
return $this;
diff --git a/lib/WebDriverUpAction.php b/lib/WebDriverUpAction.php
index 62d275dea..3b045df4b 100644
--- a/lib/WebDriverUpAction.php
+++ b/lib/WebDriverUpAction.php
@@ -11,7 +11,6 @@ class WebDriverUpAction extends WebDriverTouchAction implements WebDriverAction
private $y;
/**
- * @param WebDriverTouchScreen $touch_screen
* @param int $x
* @param int $y
*/
diff --git a/lib/WebDriverWait.php b/lib/WebDriverWait.php
index 34dd057ae..d2176b92a 100644
--- a/lib/WebDriverWait.php
+++ b/lib/WebDriverWait.php
@@ -28,7 +28,7 @@ class WebDriverWait
public function __construct(WebDriver $driver, $timeout_in_second = null, $interval_in_millisecond = null)
{
$this->driver = $driver;
- $this->timeout = isset($timeout_in_second) ? $timeout_in_second : 30;
+ $this->timeout = $timeout_in_second ?? 30;
$this->interval = $interval_in_millisecond ?: 250;
}
@@ -38,9 +38,9 @@ public function __construct(WebDriver $driver, $timeout_in_second = null, $inter
* @param callable|WebDriverExpectedCondition $func_or_ec
* @param string $message
*
+ * @throws \Exception
* @throws NoSuchElementException
* @throws TimeoutException
- * @throws \Exception
* @return mixed The return value of $func_or_ec
*/
public function until($func_or_ec, $message = '')
diff --git a/lib/WebDriverWindow.php b/lib/WebDriverWindow.php
index e212f92ed..2a69fe240 100644
--- a/lib/WebDriverWindow.php
+++ b/lib/WebDriverWindow.php
@@ -3,6 +3,7 @@
namespace Facebook\WebDriver;
use Facebook\WebDriver\Exception\IndexOutOfBoundsException;
+use Facebook\WebDriver\Exception\Internal\LogicException;
use Facebook\WebDriver\Exception\UnsupportedOperationException;
use Facebook\WebDriver\Remote\DriverCommand;
use Facebook\WebDriver\Remote\ExecuteMethod;
@@ -120,7 +121,6 @@ public function fullscreen()
* Set the size of the current window. This will change the outer window
* dimension, not just the view port.
*
- * @param WebDriverDimension $size
* @return WebDriverWindow The instance.
*/
public function setSize(WebDriverDimension $size)
@@ -139,7 +139,6 @@ public function setSize(WebDriverDimension $size)
* Set the position of the current window. This is relative to the upper left
* corner of the screen.
*
- * @param WebDriverPoint $position
* @return WebDriverWindow The instance.
*/
public function setPosition(WebDriverPoint $position)
@@ -175,10 +174,8 @@ public function getScreenOrientation()
public function setScreenOrientation($orientation)
{
$orientation = mb_strtoupper($orientation);
- if (!in_array($orientation, ['PORTRAIT', 'LANDSCAPE'])) {
- throw new IndexOutOfBoundsException(
- 'Orientation must be either PORTRAIT, or LANDSCAPE'
- );
+ if (!in_array($orientation, ['PORTRAIT', 'LANDSCAPE'], true)) {
+ throw LogicException::forError('Orientation must be either PORTRAIT, or LANDSCAPE');
}
$this->executor->execute(
diff --git a/lib/scripts/isElementDisplayed.js b/lib/scripts/isElementDisplayed.js
new file mode 100644
index 000000000..f24bfa55e
--- /dev/null
+++ b/lib/scripts/isElementDisplayed.js
@@ -0,0 +1,219 @@
+/*
+ * Imported from WebdriverIO project.
+ * https://github.com/webdriverio/webdriverio/blob/main/packages/webdriverio/src/scripts/isElementDisplayed.ts
+ *
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * check if element is visible
+ * @param {HTMLElement} elem element to check
+ * @return {Boolean} true if element is within viewport
+ */
+function isElementDisplayed(element) {
+ function nodeIsElement(node) {
+ if (!node) {
+ return false;
+ }
+
+ switch (node.nodeType) {
+ case Node.ELEMENT_NODE:
+ case Node.DOCUMENT_NODE:
+ case Node.DOCUMENT_FRAGMENT_NODE:
+ return true;
+ default:
+ return false;
+ }
+ }
+ function parentElementForElement(element) {
+ if (!element) {
+ return null;
+ }
+ return enclosingNodeOrSelfMatchingPredicate(element.parentNode, nodeIsElement);
+ }
+ function enclosingNodeOrSelfMatchingPredicate(targetNode, predicate) {
+ for (let node = targetNode; node && node !== targetNode.ownerDocument; node = node.parentNode)
+ if (predicate(node)) {
+ return node;
+ }
+ return null;
+ }
+ function enclosingElementOrSelfMatchingPredicate(targetElement, predicate) {
+ for (let element = targetElement; element && element !== targetElement.ownerDocument; element = parentElementForElement(element))
+ if (predicate(element)) {
+ return element;
+ }
+ return null;
+ }
+ function cascadedStylePropertyForElement(element, property) {
+ if (!element || !property) {
+ return null;
+ }
+ // if document-fragment, skip it and use element.host instead. This happens
+ // when the element is inside a shadow root.
+ // window.getComputedStyle errors on document-fragment.
+ if (element instanceof ShadowRoot) {
+ element = element.host;
+ }
+ let computedStyle = window.getComputedStyle(element);
+ let computedStyleProperty = computedStyle.getPropertyValue(property);
+ if (computedStyleProperty && computedStyleProperty !== 'inherit') {
+ return computedStyleProperty;
+ }
+ // Ideally getPropertyValue would return the 'used' or 'actual' value, but
+ // it doesn't for legacy reasons. So we need to do our own poor man's cascade.
+ // Fall back to the first non-'inherit' value found in an ancestor.
+ // In any case, getPropertyValue will not return 'initial'.
+ // FIXME: will this incorrectly inherit non-inheritable CSS properties?
+ // I think all important non-inheritable properties (width, height, etc.)
+ // for our purposes here are specially resolved, so this may not be an issue.
+ // Specification is here: https://drafts.csswg.org/cssom/#resolved-values
+ let parentElement = parentElementForElement(element);
+ return cascadedStylePropertyForElement(parentElement, property);
+ }
+ function elementSubtreeHasNonZeroDimensions(element) {
+ let boundingBox = element.getBoundingClientRect();
+ if (boundingBox.width > 0 && boundingBox.height > 0) {
+ return true;
+ }
+ // Paths can have a zero width or height. Treat them as shown if the stroke width is positive.
+ if (element.tagName.toUpperCase() === 'PATH' && boundingBox.width + boundingBox.height > 0) {
+ let strokeWidth = cascadedStylePropertyForElement(element, 'stroke-width');
+ return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);
+ }
+ let cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow');
+ if (cascadedOverflow === 'hidden') {
+ return false;
+ }
+ // If the container's overflow is not hidden and it has zero size, consider the
+ // container to have non-zero dimensions if a child node has non-zero dimensions.
+ return Array.from(element.childNodes).some((childNode) => {
+ if (childNode.nodeType === Node.TEXT_NODE) {
+ return true;
+ }
+ if (nodeIsElement(childNode)) {
+ return elementSubtreeHasNonZeroDimensions(childNode);
+ }
+ return false;
+ });
+ }
+ function elementOverflowsContainer(element) {
+ let cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow');
+ if (cascadedOverflow !== 'hidden') {
+ return false;
+ }
+ // FIXME: this needs to take into account the scroll position of the element,
+ // the display modes of it and its ancestors, and the container it overflows.
+ // See Selenium's bot.dom.getOverflowState atom for an exhaustive list of edge cases.
+ return true;
+ }
+ function isElementSubtreeHiddenByOverflow(element) {
+ if (!element) {
+ return false;
+ }
+ if (!elementOverflowsContainer(element)) {
+ return false;
+ }
+ if (!element.childNodes.length) {
+ return false;
+ }
+ // This element's subtree is hidden by overflow if all child subtrees are as well.
+ return Array.from(element.childNodes).every((childNode) => {
+ // Returns true if the child node is overflowed or otherwise hidden.
+ // Base case: not an element, has zero size, scrolled out, or doesn't overflow container.
+ // Visibility of text nodes is controlled by parent
+ if (childNode.nodeType === Node.TEXT_NODE) {
+ return false;
+ }
+ if (!nodeIsElement(childNode)) {
+ return true;
+ }
+ if (!elementSubtreeHasNonZeroDimensions(childNode)) {
+ return true;
+ }
+ // Recurse.
+ return isElementSubtreeHiddenByOverflow(childNode);
+ });
+ }
+ // walk up the tree testing for a shadow root
+ function isElementInsideShadowRoot(element) {
+ if (!element) {
+ return false;
+ }
+ if (element.parentNode && element.parentNode.host) {
+ return true;
+ }
+ return isElementInsideShadowRoot(element.parentNode);
+ }
+ // This is a partial reimplementation of Selenium's "element is displayed" algorithm.
+ // When the W3C specification's algorithm stabilizes, we should implement that.
+ // If this command is misdirected to the wrong document (and is NOT inside a shadow root), treat it as not shown.
+ if (!isElementInsideShadowRoot(element) && !document.contains(element)) {
+ return false;
+ }
+ // Special cases for specific tag names.
+ switch (element.tagName.toUpperCase()) {
+ case 'BODY':
+ return true;
+ case 'SCRIPT':
+ case 'NOSCRIPT':
+ return false;
+ case 'OPTGROUP':
+ case 'OPTION': {
+ // Option/optgroup are considered shown if the containing is shown.
+ let enclosingSelectElement = enclosingNodeOrSelfMatchingPredicate(element, (e) => e.tagName.toUpperCase() === 'SELECT');
+ return isElementDisplayed(enclosingSelectElement);
+ }
+ case 'INPUT':
+ // is considered not shown.
+ if (element.type === 'hidden') {
+ return false;
+ }
+ break;
+ // case 'MAP':
+ // FIXME: Selenium has special handling for elements. We don't do anything now.
+ default:
+ break;
+ }
+ if (cascadedStylePropertyForElement(element, 'visibility') !== 'visible') {
+ return false;
+ }
+ let hasAncestorWithZeroOpacity = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {
+ return Number(cascadedStylePropertyForElement(e, 'opacity')) === 0;
+ });
+ let hasAncestorWithDisplayNone = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {
+ return cascadedStylePropertyForElement(e, 'display') === 'none';
+ });
+ if (hasAncestorWithZeroOpacity || hasAncestorWithDisplayNone) {
+ return false;
+ }
+ if (!elementSubtreeHasNonZeroDimensions(element)) {
+ return false;
+ }
+ if (isElementSubtreeHiddenByOverflow(element)) {
+ return false;
+ }
+ return true;
+}
+
diff --git a/mlc_config.json b/mlc_config.json
new file mode 100644
index 000000000..5d70a03e0
--- /dev/null
+++ b/mlc_config.json
@@ -0,0 +1,7 @@
+{
+ "ignorePatterns": [
+ {
+ "pattern": "^https://stackoverflow\\.com/questions/tagged/php\\+selenium-webdriver"
+ }
+ ]
+}
diff --git a/phpstan.neon b/phpstan.neon
index 846484976..b4c1e307d 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -5,14 +5,25 @@ parameters:
- tests/
ignoreErrors:
- # To be fixed:
- - '#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 2e81aef6d..c58c72eaa 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,14 +1,9 @@
-
-
-
-
+
tests/unit
@@ -18,14 +13,13 @@
-
-
+
+
./lib
-
-
+
+
-
diff --git a/scripts/apply-phpunit-patches.sh b/scripts/apply-phpunit-patches.sh
deleted file mode 100755
index 478e01c74..000000000
--- a/scripts/apply-phpunit-patches.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/sh
-
-# All commands below must no fail
-set -e
-
-# Be in the root dir
-cd "$(dirname $0)/../"
-
-find tests/ -type f -print0 | xargs -0 sed -i 's/function setUpBeforeClass(): void/function setUpBeforeClass()/g';
-find tests/ -type f -print0 | xargs -0 sed -i 's/function setUp(): void/function setUp()/g';
-find tests/ -type f -print0 | xargs -0 sed -i 's/function tearDown(): void/function tearDown()/g';
-
-sed -i 's/endTest(\\PHPUnit\\Framework\\Test \$test, float \$time): void/endTest(\\PHPUnit_Framework_Test \$test, \$time)/g' tests/functional/ReportSauceLabsStatusListener.php;
-sed -i 's/: void/ /g' tests/functional/ReportSauceLabsStatusListener.php;
-# Drop the listener from the config file
-sed -i '//,+2d' phpunit.xml.dist;
-sed -i 's/function runBare(): void/function runBare()/g' tests/functional/WebDriverTestCase.php;
-
-# Return back to original dir
-cd - > /dev/null
diff --git a/scripts/docs-template.html b/scripts/docs-template.html
new file mode 100644
index 000000000..4814f5ec5
--- /dev/null
+++ b/scripts/docs-template.html
@@ -0,0 +1,7 @@
+
+
+
+
+ Taking you to the latest documentation.
+
+
\ No newline at end of file
diff --git a/scripts/doctum.php b/scripts/doctum.php
new file mode 100644
index 000000000..08e722917
--- /dev/null
+++ b/scripts/doctum.php
@@ -0,0 +1,30 @@
+files()
+ ->name('*.php')
+ ->in($srcRoot . 'lib');
+
+$versions = GitVersionCollection::create($srcRoot)
+ ->addFromTags('1.*') // only latest minor version
+ ->addFromTags('0.6.0')
+ ->add('main', 'main branch')
+;
+
+return new Doctum($iterator, [
+ 'title' => 'php-webdriver API',
+ 'theme' => 'default',
+ 'build_dir' => $root . '/build/dist/%version%/',
+ 'cache_dir' => $root . '/build/cache/%version%/',
+ 'include_parent_data' => true,
+ 'remote_repository' => new GitHubRemoteRepository('php-webdriver/php-webdriver', $srcRoot),
+ 'versions' => $versions,
+ 'base_url' => 'https://php-webdriver.github.io/php-webdriver/%version%/'
+]);
diff --git a/scripts/update-built-docs.sh b/scripts/update-built-docs.sh
new file mode 100755
index 000000000..76646bed5
--- /dev/null
+++ b/scripts/update-built-docs.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+set -e
+
+cleanup() {
+ git ls-files ./ | xargs -r -n 1 rm
+ rm -rfd ./*
+}
+
+copyToTemp() {
+ TEMP_DIR="$(mktemp -d --suffix=_doctum-build-php-webdriver)"
+ cp -rp build/dist/* "${TEMP_DIR}"
+ cp ./scripts/docs-template.html "${TEMP_DIR}/index.html"
+}
+
+emptyAndRemoveTemp() {
+ mv "${TEMP_DIR}"/* ./
+ # Create symlink for main to latest
+ ln -s -r ./main ./latest
+ # Create symlink for main to master
+ ln -s -r ./main ./master
+ # Create symlink for main to community
+ ln -s -r ./main ./community
+ rm -rf "${TEMP_DIR}"
+}
+
+commitAndPushChanges() {
+ # Push the changes, only if there is changes
+ git add -A
+ git diff-index --quiet HEAD || git commit -m "Api documentations update ($(date --rfc-3339=seconds --utc))" -m "#apidocs" && if [ -z "${SKIP_PUSH}" ]; then git push; fi
+}
+
+if [ ! -d ./build/dist ]; then
+ echo 'Missing built docs'
+ exit 1
+fi
+
+# Remove cache dir, do not upload it
+rm -rf ./build/cache
+
+copyToTemp
+# Remove build dir, do not upload it
+rm -rf ./build
+
+git checkout gh-pages
+
+cleanup
+emptyAndRemoveTemp
+commitAndPushChanges
+
+git checkout - > /dev/null
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 6c8c4f51b..d9cb0c2c7 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,3 +1,3 @@
-driver);
@@ -31,7 +31,7 @@ public function testShouldExecuteDevToolsCommandWithoutParameters()
$this->assertSame([], $result);
}
- public function testShouldExecuteDevToolsCommandWithParameters()
+ public function testShouldExecuteDevToolsCommandWithParameters(): void
{
$devTools = new ChromeDevToolsDriver($this->driver);
diff --git a/tests/functional/Chrome/ChromeDriverServiceTest.php b/tests/functional/Chrome/ChromeDriverServiceTest.php
index d83ca6af4..9b6c46336 100644
--- a/tests/functional/Chrome/ChromeDriverServiceTest.php
+++ b/tests/functional/Chrome/ChromeDriverServiceTest.php
@@ -1,4 +1,4 @@
-assertInstanceOf(ChromeDriverService::class, $this->driverService->stop());
}
- public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor()
+ public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor(): void
{
$this->driverService = new ChromeDriverService(getenv('CHROMEDRIVER_PATH'), 9515, ['--port=9515']);
@@ -63,7 +63,7 @@ public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor()
$this->assertFalse($this->driverService->isRunning());
}
- public function testShouldThrowExceptionIfExecutableIsNotExecutable()
+ public function testShouldThrowExceptionIfExecutableIsNotExecutable(): void
{
putenv(ChromeDriverService::CHROME_DRIVER_EXECUTABLE . '=' . __FILE__);
@@ -72,7 +72,7 @@ public function testShouldThrowExceptionIfExecutableIsNotExecutable()
ChromeDriverService::createDefaultService();
}
- public function testShouldUseDefaultExecutableIfNoneProvided()
+ public function testShouldUseDefaultExecutableIfNoneProvided(): void
{
// 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')));
diff --git a/tests/functional/Chrome/ChromeDriverTest.php b/tests/functional/Chrome/ChromeDriverTest.php
index 695fa57e3..407b65bd8 100644
--- a/tests/functional/Chrome/ChromeDriverTest.php
+++ b/tests/functional/Chrome/ChromeDriverTest.php
@@ -1,4 +1,4 @@
-startChromeDriver($isW3cDialect);
$this->assertInstanceOf(ChromeDriver::class, $this->driver);
@@ -55,7 +54,7 @@ public function testShouldStartChromeDriver($isW3cDialect)
/**
* @return array[]
*/
- public function provideDialect()
+ public function provideDialect(): array
{
return [
'w3c' => [true],
@@ -63,7 +62,7 @@ public function provideDialect()
];
}
- public function testShouldInstantiateDevTools()
+ public function testShouldInstantiateDevTools(): void
{
$this->startChromeDriver();
@@ -81,14 +80,14 @@ public function testShouldInstantiateDevTools()
$this->assertSame(['result' => ['type' => 'string', 'value' => 'http://localhost:8000/']], $cdpResult);
}
- private function startChromeDriver($w3cDialect = true)
+ 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);
diff --git a/tests/functional/FileUploadTest.php b/tests/functional/FileUploadTest.php
index f4728be41..e812353ea 100644
--- a/tests/functional/FileUploadTest.php
+++ b/tests/functional/FileUploadTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('upload.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::UPLOAD));
$fileElement = $this->driver->findElement(WebDriverBy::name('upload'));
@@ -43,7 +45,7 @@ public function testShouldUploadAFile()
$this->assertSame('10', $uploadedFileSize);
}
- private function getTestFilePath()
+ private function getTestFilePath(): string
{
return __DIR__ . '/Fixtures/FileUploadTestFile.txt';
}
diff --git a/tests/functional/Firefox/FirefoxDriverServiceTest.php b/tests/functional/Firefox/FirefoxDriverServiceTest.php
index e39259e2b..feabe0dbf 100644
--- a/tests/functional/Firefox/FirefoxDriverServiceTest.php
+++ b/tests/functional/Firefox/FirefoxDriverServiceTest.php
@@ -1,4 +1,4 @@
-markTestSkipped('The test is run only when running against local chrome');
+ $this->markTestSkipped('The test is run only when running against local firefox');
}
}
@@ -32,7 +32,7 @@ protected function tearDown(): void
}
}
- public function testShouldStartAndStopServiceCreatedUsingShortcutConstructor()
+ 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'));
@@ -52,7 +52,7 @@ public function testShouldStartAndStopServiceCreatedUsingShortcutConstructor()
$this->assertInstanceOf(FirefoxDriverService::class, $this->driverService->stop());
}
- public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor()
+ public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor(): void
{
$this->driverService = new FirefoxDriverService(getenv('GECKODRIVER_PATH'), 9515, ['-p=9515']);
@@ -65,7 +65,7 @@ public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor()
$this->assertFalse($this->driverService->isRunning());
}
- public function testShouldUseDefaultExecutableIfNoneProvided()
+ 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')));
diff --git a/tests/functional/Firefox/FirefoxDriverTest.php b/tests/functional/Firefox/FirefoxDriverTest.php
index ab6a03846..900746d2f 100644
--- a/tests/functional/Firefox/FirefoxDriverTest.php
+++ b/tests/functional/Firefox/FirefoxDriverTest.php
@@ -1,14 +1,18 @@
-markTestSkipped('The test is run only when running against local chrome');
+ $this->markTestSkipped('The test is run only when running against local firefox');
}
}
@@ -33,7 +37,7 @@ protected function tearDown(): void
}
}
- public function testShouldStartFirefoxDriver()
+ public function testShouldStartFirefoxDriver(): void
{
$this->startFirefoxDriver();
$this->assertInstanceOf(FirefoxDriver::class, $this->driver);
@@ -49,12 +53,30 @@ public function testShouldStartFirefoxDriver()
$this->assertSame('http://localhost:8000/', $this->driver->getCurrentURL());
}
- private function startFirefoxDriver()
+ 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'));
- $firefoxOptions = new FirefoxOptions();
+ if ($firefoxOptions === null) {
+ $firefoxOptions = new FirefoxOptions();
+ }
$firefoxOptions->addArguments(['-headless']);
$desiredCapabilities = DesiredCapabilities::firefox();
$desiredCapabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions);
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 947c8b6f9..897d33cf7 100644
--- a/tests/functional/RemoteKeyboardTest.php
+++ b/tests/functional/RemoteKeyboardTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('events.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::EVENTS));
$this->driver->getKeyboard()->sendKeys('ab');
$this->driver->getKeyboard()->pressKey(WebDriverKeys::SHIFT);
diff --git a/tests/functional/RemoteTargetLocatorTest.php b/tests/functional/RemoteTargetLocatorTest.php
index a9e03e514..a3080e67a 100644
--- a/tests/functional/RemoteTargetLocatorTest.php
+++ b/tests/functional/RemoteTargetLocatorTest.php
@@ -1,7 +1,8 @@
-driver->get($this->getTestPageUrl('open_new_window.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::OPEN_NEW_WINDOW));
$originalWindowHandle = $this->driver->getWindowHandle();
$windowHandlesBefore = $this->driver->getWindowHandles();
@@ -23,7 +24,7 @@ public function testShouldSwitchToWindow()
);
// At first the window should not be switched
- $this->compatAssertStringContainsString('open_new_window.html', $this->driver->getCurrentURL());
+ $this->assertStringContainsString('open_new_window.html', $this->driver->getCurrentURL());
$this->assertSame($originalWindowHandle, $this->driver->getWindowHandle());
/**
@@ -43,13 +44,13 @@ public function testShouldSwitchToWindow()
});
// After switchTo() is called, the active window should be changed
- $this->compatAssertStringContainsString('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);
@@ -61,75 +62,75 @@ 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->compatAssertStringContainsString($parentPage, $this->driver->getPageSource());
+ $this->assertStringContainsString($parentPage, $this->driver->getPageSource());
$this->driver->switchTo()->frame(0);
- $this->compatAssertStringContainsString($firstChildFrame, $this->driver->getPageSource());
+ $this->assertStringContainsString($firstChildFrame, $this->driver->getPageSource());
$this->driver->switchTo()->frame(null);
- $this->compatAssertStringContainsString($parentPage, $this->driver->getPageSource());
+ $this->assertStringContainsString($parentPage, $this->driver->getPageSource());
$this->driver->switchTo()->frame(1);
- $this->compatAssertStringContainsString($secondChildFrame, $this->driver->getPageSource());
+ $this->assertStringContainsString($secondChildFrame, $this->driver->getPageSource());
$this->driver->switchTo()->frame(null);
- $this->compatAssertStringContainsString($parentPage, $this->driver->getPageSource());
+ $this->assertStringContainsString($parentPage, $this->driver->getPageSource());
$this->driver->switchTo()->frame(0);
- $this->compatAssertStringContainsString($firstChildFrame, $this->driver->getPageSource());
+ $this->assertStringContainsString($firstChildFrame, $this->driver->getPageSource());
$this->driver->switchTo()->defaultContent();
- $this->compatAssertStringContainsString($parentPage, $this->driver->getPageSource());
+ $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->compatAssertStringContainsString($parentPage, $this->driver->getPageSource());
+ $this->assertStringContainsString($parentPage, $this->driver->getPageSource());
$this->driver->switchTo()->frame(0);
- $this->compatAssertStringContainsString($firstChildFrame, $this->driver->getPageSource());
+ $this->assertStringContainsString($firstChildFrame, $this->driver->getPageSource());
$this->driver->switchTo()->parent();
- $this->compatAssertStringContainsString($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->compatAssertStringContainsString('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()
+ 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('index.html'));
- $this->assertEquals($this->getTestPageUrl('index.html'), $this->driver->getCurrentUrl());
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
+ $this->assertEquals($this->getTestPageUrl(TestPage::INDEX), $this->driver->getCurrentUrl());
$source = $this->driver->getPageSource();
- $this->compatAssertStringContainsString('', $source);
- $this->compatAssertStringContainsString('Welcome to the php-webdriver testing page.', $source);
+ $this->assertStringContainsString('', $source);
+ $this->assertStringContainsString('Welcome to the php-webdriver testing page.', $source);
$windowHandles = $this->driver->getWindowHandles();
$this->assertCount(1, $windowHandles);
@@ -140,25 +141,25 @@ public function testShouldCreateNewWindow()
$this->assertCount(2, $windowHandles);
$newWindowHandle = $this->driver->getWindowHandle();
- $this->driver->get($this->getTestPageUrl('upload.html'));
- $this->assertEquals($this->getTestPageUrl('upload.html'), $this->driver->getCurrentUrl());
+ $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('index.html'), $this->driver->getCurrentUrl());
+ $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'
);
@@ -169,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->compatAssertStringContainsString('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 6473e9b4a..a39a91070 100644
--- a/tests/functional/RemoteWebDriverCreateTest.php
+++ b/tests/functional/RemoteWebDriverCreateTest.php
@@ -1,19 +1,21 @@
-driver = RemoteWebDriver::create(
$this->serverUrl,
@@ -30,7 +32,7 @@ public function testShouldStartBrowserAndCreateInstanceOfRemoteWebDriver()
$this->assertInstanceOf(HttpCommandExecutor::class, $this->driver->getCommandExecutor());
$this->assertNotEmpty($this->driver->getCommandExecutor()->getAddressOfRemoteServer());
- $this->assertTrue(is_string($this->driver->getSessionID()));
+ $this->assertIsString($this->driver->getSessionID());
$this->assertNotEmpty($this->driver->getSessionID());
$returnedCapabilities = $this->driver->getCapabilities();
@@ -49,7 +51,7 @@ public function testShouldStartBrowserAndCreateInstanceOfRemoteWebDriver()
$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();
@@ -64,7 +66,7 @@ public function testShouldAcceptCapabilitiesAsAnArray()
$this->assertNotNull($this->driver->getCapabilities());
}
- public function testShouldCreateWebDriverWithRequiredCapabilities()
+ public function testShouldCreateWebDriverWithRequiredCapabilities(): void
{
$requiredCapabilities = new DesiredCapabilities();
@@ -87,7 +89,7 @@ public function testShouldCreateWebDriverWithRequiredCapabilities()
* However the the browser driver must be able to create non-headless instance (eg. inside xvfb).
* @group exclude-saucelabs
*/
- public function testShouldCreateWebDriverWithoutCapabilities()
+ 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');
@@ -99,7 +101,7 @@ public function testShouldCreateWebDriverWithoutCapabilities()
$this->assertNotEmpty($this->driver->getSessionID());
}
- public function testShouldCreateInstanceFromExistingSessionId()
+ public function testShouldCreateInstanceFromExistingSessionId(): void
{
// Create driver instance and load page "index.html"
$originalDriver = RemoteWebDriver::create(
@@ -108,24 +110,66 @@ public function testShouldCreateInstanceFromExistingSessionId()
$this->connectionTimeout,
$this->requestTimeout
);
- $originalDriver->get($this->getTestPageUrl('index.html'));
- $this->compatAssertStringContainsString('/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, null, null, $isW3cCompliant);
+ $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->compatAssertStringContainsString('/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());
}
- protected function createWebDriver()
+ 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 5a7d18e70..f638aa527 100644
--- a/tests/functional/RemoteWebDriverFindElementTest.php
+++ b/tests/functional/RemoteWebDriverFindElementTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$this->expectException(NoSuchElementException::class);
$this->driver->findElement(WebDriverBy::id('not_existing'));
}
- public function testShouldFindElementIfExistsOnAPage()
+ public function testShouldFindElementIfExistsOnAPage(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::id('id_test'));
$this->assertInstanceOf(RemoteWebElement::class, $element);
}
- public function testShouldReturnEmptyArrayIfElementsCannotBeFound()
+ public function testShouldReturnEmptyArrayIfElementsCannotBeFound(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$elements = $this->driver->findElements(WebDriverBy::cssSelector('not_existing'));
- $this->assertTrue(is_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->assertTrue(is_array($elements));
+ $this->assertIsArray($elements);
$this->assertCount(5, $elements);
$this->assertContainsOnlyInstancesOf(RemoteWebElement::class, $elements);
}
@@ -52,13 +52,13 @@ public function testShouldFindMultipleElements()
/**
* @group exclude-saucelabs
*/
- public function testEscapeCssSelector()
+ public function testEscapeCssSelector(): void
{
self::skipForJsonWireProtocol(
'CSS selectors containing special characters are not supported by the legacy protocol'
);
- $this->driver->get($this->getTestPageUrl('escape_css.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::ESCAPE_CSS));
$element = $this->driver->findElement(WebDriverBy::id('.fo\'oo'));
$this->assertSame('Foo', $element->getText());
diff --git a/tests/functional/RemoteWebDriverTest.php b/tests/functional/RemoteWebDriverTest.php
index 25b01c40f..95a01b9b7 100644
--- a/tests/functional/RemoteWebDriverTest.php
+++ b/tests/functional/RemoteWebDriverTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$this->assertEquals(
'php-webdriver test page',
@@ -27,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());
}
@@ -37,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->compatAssertStringContainsString('', $source);
- $this->compatAssertStringContainsString('Welcome to the php-webdriver testing page.', $source);
+ $this->assertStringContainsString('', $source);
+ $this->assertStringContainsString('Welcome to the php-webdriver testing page.', $source);
}
/**
* @covers ::getSessionID
* @covers ::isW3cCompliant
*/
- public function testShouldGetSessionId()
+ public function testShouldGetSessionId(): void
{
// This tests is intentionally included in another test, to not slow down build.
// @TODO Remove following in 2.0
@@ -62,7 +62,7 @@ public function testShouldGetSessionId()
$sessionId = $this->driver->getSessionID();
- $this->assertTrue(is_string($sessionId));
+ $this->assertIsString($sessionId);
$this->assertNotEmpty($sessionId);
}
@@ -70,13 +70,13 @@ public function testShouldGetSessionId()
* @group exclude-saucelabs
* @covers ::getAllSessions
*/
- public function testShouldGetAllSessions()
+ public function testShouldGetAllSessions(): void
{
self::skipForW3cProtocol();
$sessions = RemoteWebDriver::getAllSessions($this->serverUrl, 30000);
- $this->assertTrue(is_array($sessions));
+ $this->assertIsArray($sessions);
$this->assertCount(1, $sessions);
$this->assertArrayHasKey('capabilities', $sessions[0]);
@@ -89,7 +89,7 @@ public function testShouldGetAllSessions()
* @covers ::getCommandExecutor
* @covers ::quit
*/
- public function testShouldQuitAndUnsetExecutor()
+ public function testShouldQuitAndUnsetExecutor(): void
{
self::skipForW3cProtocol();
@@ -116,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->assertTrue(is_string($windowHandle));
+ $this->assertIsString($windowHandle);
$this->assertNotEmpty($windowHandle);
$this->assertSame([$windowHandle], $windowHandles);
@@ -140,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));
@@ -158,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());
@@ -188,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());
@@ -228,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'));
@@ -250,13 +250,14 @@ 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');
}
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$outputPng = $this->driver->takeScreenshot();
@@ -269,18 +270,19 @@ 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');
}
- // 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);
@@ -312,12 +314,12 @@ 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->assertTrue(is_bool($status->isReady()));
- $this->assertTrue(is_array($status->getMeta()));
+ $this->assertIsBool($status->isReady());
+ $this->assertIsArray($status->getMeta());
if (getenv('BROWSER_NAME') !== 'safari') {
$this->assertNotEmpty($status->getMessage());
diff --git a/tests/functional/RemoteWebElementTest.php b/tests/functional/RemoteWebElementTest.php
index f10c7487b..6a21cb36c 100644
--- a/tests/functional/RemoteWebElementTest.php
+++ b/tests/functional/RemoteWebElementTest.php
@@ -1,4 +1,4 @@
-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'));
@@ -33,38 +33,61 @@ public function testShouldGetText()
/**
* @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()
+ public function testShouldGetLocationOnScreenOnceScrolledIntoView(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::id('element-out-of-viewport'));
@@ -87,9 +110,9 @@ public function testShouldGetLocationOnScreenOnceScrolledIntoView()
/**
* @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'));
@@ -102,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'));
@@ -122,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'));
@@ -134,15 +157,15 @@ 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
@@ -157,7 +180,7 @@ public function testShouldClick()
* @group exclude-chrome
* @group exclude-edge
*/
- public function testGeckoDriverShouldClickOnBlockLevelElement()
+ public function testGeckoDriverShouldClickOnBlockLevelElement(): void
{
self::skipForUnmatchedBrowsers(['firefox']);
@@ -169,7 +192,7 @@ public function testGeckoDriverShouldClickOnBlockLevelElement()
];
foreach ($links as $linkid) {
- $this->driver->get($this->getTestPageUrl('gecko653.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::GECKO_653));
$linkElement = $this->driver->findElement(WebDriverBy::id($linkid));
$linkElement->click();
@@ -186,13 +209,14 @@ public function testGeckoDriverShouldClickOnBlockLevelElement()
* @group exclude-chrome
* @group exclude-edge
*/
- public function testGeckoDriverShouldClickNotInteractable()
+ public function testGeckoDriverShouldClickNotInteractable(): void
{
self::skipForUnmatchedBrowsers(['firefox']);
- $this->driver->get($this->getTestPageUrl('gecko653.html'));
+ $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');
@@ -201,6 +225,7 @@ public function testGeckoDriverShouldClickNotInteractable()
}
$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');
@@ -212,9 +237,9 @@ public function testGeckoDriverShouldClickNotInteractable()
/**
* @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'));
@@ -231,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'));
@@ -264,12 +289,30 @@ public function testShouldSendKeysToFormElement()
$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'));
@@ -281,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]')
@@ -309,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'));
@@ -327,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'));
@@ -345,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'));
@@ -363,9 +406,9 @@ public function testShouldSubmitFormByClickOnSubmitInput()
/**
* @covers ::equals
*/
- public function testShouldCompareEqualsElement()
+ public function testShouldCompareEqualsElement(): void
{
- $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'));
@@ -382,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'));
@@ -403,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->assertTrue(is_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->assertTrue(is_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
$this->assertContainsOnlyInstancesOf(RemoteWebElement::class, $childElements);
@@ -430,9 +473,10 @@ public function testShouldFindMultipleChildElements()
/**
* @covers ::takeElementScreenshot
+ * @covers \Facebook\WebDriver\Support\ScreenshotHelper
* @group exclude-saucelabs
*/
- public function testShouldTakeAndSaveElementScreenshot()
+ public function testShouldTakeAndSaveElementScreenshot(): void
{
self::skipForJsonWireProtocol('Take element screenshot is only part of W3C protocol');
@@ -444,10 +488,9 @@ public function testShouldTakeAndSaveElementScreenshot()
$isCi = (new CiDetector())->isCiDetected();
$isSafari = getenv('BROWSER_NAME') === 'safari';
- // Intentionally save screenshot to subdirectory to tests it is being created
$screenshotPath = sys_get_temp_dir() . '/' . uniqid('php-webdriver-') . '/element-screenshot.png';
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$element = $this->driver->findElement(WebDriverBy::id('red-box'));
@@ -472,8 +515,11 @@ public function testShouldTakeAndSaveElementScreenshot()
// Assert string output
$imageFromString = imagecreatefromstring($outputPngString);
- $this->assertNotFalse($imageFromString);
- $this->assertTrue(is_resource($imageFromString));
+ if (version_compare(phpversion(), '8.0.0', '>=')) {
+ $this->assertInstanceOf(\GdImage::class, $imageFromString);
+ } else {
+ $this->assertTrue(is_resource($imageFromString));
+ }
if ($isSafari && !$isCi) {
$this->assertEquals(10, imagesx($imageFromString));
diff --git a/tests/functional/ReportSauceLabsStatusListener.php b/tests/functional/ReportSauceLabsStatusListener.php
index 7c3bed873..1a9077807 100644
--- a/tests/functional/ReportSauceLabsStatusListener.php
+++ b/tests/functional/ReportSauceLabsStatusListener.php
@@ -1,4 +1,4 @@
-retrieveLoggerEvents(WebDriverBy::id('keyboardEventsLog'));
}
- /**
- * @return array
- */
- private function retrieveLoggedMouseEvents()
+ private function retrieveLoggedMouseEvents(): array
{
return $this->retrieveLoggerEvents(WebDriverBy::id('mouseEventsLog'));
}
diff --git a/tests/functional/ShadowDomTest.php b/tests/functional/ShadowDomTest.php
new file mode 100644
index 000000000..dc95eaa84
--- /dev/null
+++ b/tests/functional/ShadowDomTest.php
@@ -0,0 +1,75 @@
+driver->get($this->getTestPageUrl(TestPage::WEB_COMPONENTS));
+
+ $element = $this->driver->findElement(WebDriverBy::cssSelector('custom-checkbox-element'));
+
+ $shadowRoot = $element->getShadowRoot();
+
+ $this->assertInstanceOf(ShadowRoot::class, $shadowRoot);
+ }
+
+ public function testShouldThrowExceptionWhenGettingShadowRootWithElementNotHavingShadowRoot(): void
+ {
+ $this->driver->get($this->getTestPageUrl(TestPage::WEB_COMPONENTS));
+
+ $element = $this->driver->findElement(WebDriverBy::cssSelector('#no-shadow-root'));
+
+ $this->expectException(NoSuchShadowRootException::class);
+ $element->getShadowRoot();
+ }
+
+ public function testShouldFindElementUnderShadowRoot(): void
+ {
+ $this->driver->get($this->getTestPageUrl(TestPage::WEB_COMPONENTS));
+
+ $element = $this->driver->findElement(WebDriverBy::cssSelector('custom-checkbox-element'));
+
+ $shadowRoot = $element->getShadowRoot();
+
+ $elementInShadow = $shadowRoot->findElement(WebDriverBy::cssSelector('input'));
+ $this->assertSame('checkbox', $elementInShadow->getAttribute('type'));
+
+ $elementsInShadow = $shadowRoot->findElements(WebDriverBy::cssSelector('div'));
+ $this->assertCount(2, $elementsInShadow);
+ }
+
+ public function testShouldReferenceTheSameShadowRootAsFromExecuteScript(): void
+ {
+ $this->driver->get($this->getTestPageUrl(TestPage::WEB_COMPONENTS));
+
+ $element = $this->driver->findElement(WebDriverBy::cssSelector('custom-checkbox-element'));
+
+ /** @var WebDriverElement $elementFromScript */
+ $elementFromScript = $this->driver->executeScript(
+ 'return arguments[0].shadowRoot;',
+ [$element]
+ );
+
+ $shadowRoot = $element->getShadowRoot();
+
+ $this->assertSame($shadowRoot->getId(), reset($elementFromScript));
+ }
+}
diff --git a/tests/functional/TestPage.php b/tests/functional/TestPage.php
new file mode 100644
index 000000000..92629ed54
--- /dev/null
+++ b/tests/functional/TestPage.php
@@ -0,0 +1,25 @@
+driver->get($this->getTestPageUrl('events.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::EVENTS));
$element = $this->driver->findElement(WebDriverBy::id('item-1'));
@@ -47,9 +47,9 @@ public function testShouldClickOnElement()
$this->assertSame($logs, $loggedEvents);
}
- public function testShouldClickAndHoldOnElementAndRelease()
+ public function testShouldClickAndHoldOnElementAndRelease(): void
{
- $this->driver->get($this->getTestPageUrl('events.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::EVENTS));
$element = $this->driver->findElement(WebDriverBy::id('item-1'));
@@ -72,13 +72,13 @@ public function testShouldClickAndHoldOnElementAndRelease()
/**
* @group exclude-saucelabs
*/
- public function testShouldContextClickOnElement()
+ public function testShouldContextClickOnElement(): void
{
if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::MICROSOFT_EDGE) {
$this->markTestSkipped('Getting stuck in EdgeDriver');
}
- $this->driver->get($this->getTestPageUrl('events.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::EVENTS));
$element = $this->driver->findElement(WebDriverBy::id('item-2'));
@@ -97,9 +97,9 @@ public function testShouldContextClickOnElement()
* @group exclude-safari
* https://github.com/webdriverio/webdriverio/issues/231
*/
- public function testShouldDoubleClickOnElement()
+ public function testShouldDoubleClickOnElement(): void
{
- $this->driver->get($this->getTestPageUrl('events.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::EVENTS));
$element = $this->driver->findElement(WebDriverBy::id('item-3'));
@@ -113,9 +113,9 @@ public function testShouldDoubleClickOnElement()
/**
* @group exclude-saucelabs
*/
- public function testShouldSendKeysUpAndDown()
+ public function testShouldSendKeysUpAndDown(): void
{
- $this->driver->get($this->getTestPageUrl('events.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::EVENTS));
$this->driver->action()
->keyDown(null, WebDriverKeys::CONTROL)
@@ -142,9 +142,9 @@ public function testShouldSendKeysUpAndDown()
* @group exclude-safari
* https://developer.apple.com/forums/thread/662677
*/
- public function testShouldMoveToElement()
+ public function testShouldMoveToElement(): void
{
- $this->driver->get($this->getTestPageUrl('sortable.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::SORTABLE));
$item13 = $this->driver->findElement(WebDriverBy::id('item-1-3'));
$item24 = $this->driver->findElement(WebDriverBy::id('item-2-4'));
@@ -165,9 +165,9 @@ public function testShouldMoveToElement()
* @group exclude-safari
* https://developer.apple.com/forums/thread/662677
*/
- public function testShouldMoveByOffset()
+ public function testShouldMoveByOffset(): void
{
- $this->driver->get($this->getTestPageUrl('sortable.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::SORTABLE));
$item13 = $this->driver->findElement(WebDriverBy::id('item-1-3'));
@@ -184,13 +184,13 @@ public function testShouldMoveByOffset()
}
/**
- * @group exclude-saucelabs
* @group exclude-safari
* https://developer.apple.com/forums/thread/662677
+ * @group exclude-saucelabs
*/
- public function testShouldDragAndDrop()
+ public function testShouldDragAndDrop(): void
{
- $this->driver->get($this->getTestPageUrl('sortable.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::SORTABLE));
$item13 = $this->driver->findElement(WebDriverBy::id('item-1-3'));
$item24 = $this->driver->findElement(WebDriverBy::id('item-2-4'));
@@ -221,9 +221,9 @@ public function testShouldDragAndDrop()
* https://developer.apple.com/forums/thread/662677
* it does not work even with Python Selenium, looks like Safaridriver does not implements Interaction API
*/
- public function testShouldDragAndDropBy()
+ public function testShouldDragAndDropBy(): void
{
- $this->driver->get($this->getTestPageUrl('sortable.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::SORTABLE));
$item13 = $this->driver->findElement(WebDriverBy::id('item-1-3'));
@@ -250,10 +250,7 @@ public function testShouldDragAndDropBy()
);
}
- /**
- * @return array
- */
- private function retrieveListContent()
+ private function retrieveListContent(): array
{
return [
$this->retrieveLoggerEvents(WebDriverBy::cssSelector('#sortable1')),
diff --git a/tests/functional/WebDriverAlertTest.php b/tests/functional/WebDriverAlertTest.php
index e75533b8d..0d2e92a44 100644
--- a/tests/functional/WebDriverAlertTest.php
+++ b/tests/functional/WebDriverAlertTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('alert.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::ALERT));
}
- public function testShouldAcceptAlert()
+ public function testShouldAcceptAlert(): void
{
// Open alert (it is delayed for 1 second, to make sure following wait for alertIsPresent works properly)
$this->driver->findElement(WebDriverBy::id('open-alert-delayed'))->click();
@@ -39,7 +39,7 @@ public function testShouldAcceptAlert()
$this->driver->switchTo()->alert()->accept();
}
- public function testShouldAcceptAndDismissConfirmation()
+ public function testShouldAcceptAndDismissConfirmation(): void
{
// Open confirmation
$this->driver->findElement(WebDriverBy::id('open-confirm'))->click();
@@ -61,7 +61,7 @@ public function testShouldAcceptAndDismissConfirmation()
$this->assertSame('dismissed', $this->getResultText());
}
- public function testShouldSubmitPromptText()
+ public function testShouldSubmitPromptText(): void
{
// Open confirmation
$this->driver->findElement(WebDriverBy::id('open-prompt'))->click();
@@ -77,7 +77,7 @@ public function testShouldSubmitPromptText()
$this->assertSame('Text entered to prompt', $this->getResultText());
}
- private function getResultText()
+ private function getResultText(): string
{
return $this->driver
->findElement(WebDriverBy::id('result'))
diff --git a/tests/functional/WebDriverByTest.php b/tests/functional/WebDriverByTest.php
index 6ff0c8a55..2aadf8de9 100644
--- a/tests/functional/WebDriverByTest.php
+++ b/tests/functional/WebDriverByTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('index.html'));
+ string $webDriverByLocatorMethod,
+ string $webDriverByLocatorValue,
+ ?string $expectedText = null,
+ ?string $expectedAttributeValue = null
+ ): void {
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
$by = call_user_func([WebDriverBy::class, $webDriverByLocatorMethod], $webDriverByLocatorValue);
$element = $this->driver->findElement($by);
@@ -42,7 +38,7 @@ public function testShouldFindTextElementByLocator(
/**
* @return array[]
*/
- public function provideTextElements()
+ public function provideTextElements(): array
{
return [
'id' => ['id', 'id_test', 'Test by ID'],
diff --git a/tests/functional/WebDriverCheckboxesTest.php b/tests/functional/WebDriverCheckboxesTest.php
index e42ab144b..64372a4c8 100644
--- a/tests/functional/WebDriverCheckboxesTest.php
+++ b/tests/functional/WebDriverCheckboxesTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('form_checkbox_radio.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::FORM_CHECKBOX_RADIO));
}
- public function testIsMultiple()
+ public function testIsMultiple(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))
@@ -27,7 +27,7 @@ public function testIsMultiple()
$this->assertTrue($checkboxes->isMultiple());
}
- public function testGetOptions()
+ public function testGetOptions(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//form[2]//input[@type="checkbox"]'))
@@ -36,7 +36,7 @@ public function testGetOptions()
$this->assertNotEmpty($checkboxes->getOptions());
}
- public function testGetFirstSelectedOption()
+ public function testGetFirstSelectedOption(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))
@@ -47,7 +47,7 @@ public function testGetFirstSelectedOption()
$this->assertSame('j2a', $checkboxes->getFirstSelectedOption()->getAttribute('value'));
}
- public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentForm()
+ public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentForm(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@id="j5b"]'))
@@ -56,7 +56,7 @@ public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociate
$this->assertEquals('j5b', $checkboxes->getFirstSelectedOption()->getAttribute('value'));
}
- public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentFormWithoutId()
+ public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentFormWithoutId(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@id="j5d"]'))
@@ -65,7 +65,7 @@ public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociate
$this->assertEquals('j5c', $checkboxes->getFirstSelectedOption()->getAttribute('value'));
}
- public function testSelectByValue()
+ public function testSelectByValue(): void
{
$selectedOptions = ['j2b', 'j2c'];
@@ -83,7 +83,7 @@ public function testSelectByValue()
$this->assertSame($selectedOptions, $selectedValues);
}
- public function testSelectByValueInvalid()
+ public function testSelectByValueInvalid(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))
@@ -94,7 +94,7 @@ public function testSelectByValueInvalid()
$checkboxes->selectByValue('notexist');
}
- public function testSelectByIndex()
+ public function testSelectByIndex(): void
{
$selectedOptions = [1 => 'j2b', 2 => 'j2c'];
@@ -112,7 +112,7 @@ public function testSelectByIndex()
$this->assertSame(array_values($selectedOptions), $selectedValues);
}
- public function testSelectByIndexInvalid()
+ public function testSelectByIndexInvalid(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))
@@ -125,11 +125,8 @@ public function testSelectByIndexInvalid()
/**
* @dataProvider provideSelectByVisibleTextData
- *
- * @param string $text
- * @param string $value
*/
- public function testSelectByVisibleText($text, $value)
+ public function testSelectByVisibleText(string $text, string $value): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))
@@ -143,7 +140,7 @@ public function testSelectByVisibleText($text, $value)
/**
* @return array[]
*/
- public function provideSelectByVisibleTextData()
+ public function provideSelectByVisibleTextData(): array
{
return [
['J 2 B', 'j2b'],
@@ -153,11 +150,8 @@ public function provideSelectByVisibleTextData()
/**
* @dataProvider provideSelectByVisiblePartialTextData
- *
- * @param string $text
- * @param string $value
*/
- public function testSelectByVisiblePartialText($text, $value)
+ public function testSelectByVisiblePartialText(string $text, string $value): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))
@@ -171,7 +165,7 @@ public function testSelectByVisiblePartialText($text, $value)
/**
* @return array[]
*/
- public function provideSelectByVisiblePartialTextData()
+ public function provideSelectByVisiblePartialTextData(): array
{
return [
['2 B', 'j2b'],
@@ -179,7 +173,7 @@ public function provideSelectByVisiblePartialTextData()
];
}
- public function testDeselectAll()
+ public function testDeselectAll(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))
@@ -191,7 +185,7 @@ public function testDeselectAll()
$this->assertEmpty($checkboxes->getAllSelectedOptions());
}
- public function testDeselectByIndex()
+ public function testDeselectByIndex(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))
@@ -203,7 +197,7 @@ public function testDeselectByIndex()
$this->assertEmpty($checkboxes->getAllSelectedOptions());
}
- public function testDeselectByValue()
+ public function testDeselectByValue(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))
@@ -215,7 +209,7 @@ public function testDeselectByValue()
$this->assertEmpty($checkboxes->getAllSelectedOptions());
}
- public function testDeselectByVisibleText()
+ public function testDeselectByVisibleText(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))
@@ -227,7 +221,7 @@ public function testDeselectByVisibleText()
$this->assertEmpty($checkboxes->getAllSelectedOptions());
}
- public function testDeselectByVisiblePartialText()
+ public function testDeselectByVisiblePartialText(): void
{
$checkboxes = new WebDriverCheckboxes(
$this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))
diff --git a/tests/functional/WebDriverNavigationTest.php b/tests/functional/WebDriverNavigationTest.php
index 630656ea4..400ef86f0 100644
--- a/tests/functional/WebDriverNavigationTest.php
+++ b/tests/functional/WebDriverNavigationTest.php
@@ -1,4 +1,4 @@
-driver->navigate()->to($this->getTestPageUrl('index.html'));
+ $this->driver->navigate()->to($this->getTestPageUrl(TestPage::INDEX));
$this->assertStringEndsWith('/index.html', $this->driver->getCurrentURL());
}
@@ -22,27 +22,27 @@ public function testShouldNavigateToUrl()
* @covers ::back
* @covers ::forward
*/
- public function testShouldNavigateBackAndForward()
+ public function testShouldNavigateBackAndForward(): 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->driver->navigate()->back();
$this->driver->wait()->until(
- WebDriverExpectedCondition::urlContains('index.html')
+ WebDriverExpectedCondition::urlContains(TestPage::INDEX)
);
$this->driver->navigate()->forward();
$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
@@ -51,9 +51,9 @@ public function testShouldNavigateBackAndForward()
/**
* @covers ::refresh
*/
- public function testShouldRefreshPage()
+ public function testShouldRefreshPage(): void
{
- $this->driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
// Change input element content, to make sure it was refreshed (=> cleared to original value)
$inputElement = $this->driver->findElement(WebDriverBy::name('test_name'));
diff --git a/tests/functional/WebDriverOptionsCookiesTest.php b/tests/functional/WebDriverOptionsCookiesTest.php
index 2b0301256..a71ebafc0 100644
--- a/tests/functional/WebDriverOptionsCookiesTest.php
+++ b/tests/functional/WebDriverOptionsCookiesTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('index.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::INDEX));
}
- public function testShouldSetGetAndDeleteCookies()
+ public function testShouldSetGetAndDeleteCookies(): void
{
$cookie1 = new Cookie('cookie1', 'cookie1Value');
$cookie2 = new Cookie('cookie2', 'cookie2Value');
diff --git a/tests/functional/WebDriverRadiosTest.php b/tests/functional/WebDriverRadiosTest.php
index b6f900d8a..f35d37a7c 100644
--- a/tests/functional/WebDriverRadiosTest.php
+++ b/tests/functional/WebDriverRadiosTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('form_checkbox_radio.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::FORM_CHECKBOX_RADIO));
}
- public function testIsMultiple()
+ public function testIsMultiple(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
$this->assertFalse($radios->isMultiple());
}
- public function testGetOptions()
+ public function testGetOptions(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
$values = [];
@@ -37,7 +37,7 @@ public function testGetOptions()
$this->assertSame(['j3a', 'j3b', 'j3c'], $values);
}
- public function testGetFirstSelectedOption()
+ public function testGetFirstSelectedOption(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
@@ -46,14 +46,14 @@ public function testGetFirstSelectedOption()
$this->assertSame('j3a', $radios->getFirstSelectedOption()->getAttribute('value'));
}
- public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentForm()
+ public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentForm(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@id="j4b"]')));
$this->assertEquals('j4b', $radios->getFirstSelectedOption()->getAttribute('value'));
}
- public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentFormWithoutId()
+ public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentFormWithoutId(): void
{
$radios = new WebDriverRadios(
$this->driver->findElement(WebDriverBy::xpath('//input[@id="j4c"]'))
@@ -62,7 +62,7 @@ public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociate
$this->assertEquals('j4c', $radios->getFirstSelectedOption()->getAttribute('value'));
}
- public function testSelectByValue()
+ public function testSelectByValue(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
$radios->selectByValue('j3b');
@@ -73,7 +73,7 @@ public function testSelectByValue()
$this->assertSame('j3b', $selectedOptions[0]->getAttribute('value'));
}
- public function testSelectByValueInvalid()
+ public function testSelectByValueInvalid(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
@@ -82,7 +82,7 @@ public function testSelectByValueInvalid()
$radios->selectByValue('notexist');
}
- public function testSelectByIndex()
+ public function testSelectByIndex(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
$radios->selectByIndex(1);
@@ -92,7 +92,7 @@ public function testSelectByIndex()
$this->assertSame('j3b', $allSelectedOptions[0]->getAttribute('value'));
}
- public function testSelectByIndexInvalid()
+ public function testSelectByIndexInvalid(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
@@ -103,11 +103,8 @@ public function testSelectByIndexInvalid()
/**
* @dataProvider provideSelectByVisibleTextData
- *
- * @param string $text
- * @param string $value
*/
- public function testSelectByVisibleText($text, $value)
+ public function testSelectByVisibleText(string $text, string $value): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
$radios->selectByVisibleText($text);
@@ -117,7 +114,7 @@ public function testSelectByVisibleText($text, $value)
/**
* @return array[]
*/
- public function provideSelectByVisibleTextData()
+ public function provideSelectByVisibleTextData(): array
{
return [
['J 3 B', 'j3b'],
@@ -127,11 +124,8 @@ public function provideSelectByVisibleTextData()
/**
* @dataProvider provideSelectByVisiblePartialTextData
- *
- * @param string $text
- * @param string $value
*/
- public function testSelectByVisiblePartialText($text, $value)
+ public function testSelectByVisiblePartialText(string $text, string $value): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
$radios->selectByVisiblePartialText($text);
@@ -141,7 +135,7 @@ public function testSelectByVisiblePartialText($text, $value)
/**
* @return array[]
*/
- public function provideSelectByVisiblePartialTextData()
+ public function provideSelectByVisiblePartialTextData(): array
{
return [
['3 B', 'j3b'],
@@ -149,7 +143,7 @@ public function provideSelectByVisiblePartialTextData()
];
}
- public function testDeselectAllRadio()
+ public function testDeselectAllRadio(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
@@ -158,7 +152,7 @@ public function testDeselectAllRadio()
$radios->deselectAll();
}
- public function testDeselectByIndexRadio()
+ public function testDeselectByIndexRadio(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
@@ -167,7 +161,7 @@ public function testDeselectByIndexRadio()
$radios->deselectByIndex(0);
}
- public function testDeselectByValueRadio()
+ public function testDeselectByValueRadio(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
@@ -176,7 +170,7 @@ public function testDeselectByValueRadio()
$radios->deselectByValue('val');
}
- public function testDeselectByVisibleTextRadio()
+ public function testDeselectByVisibleTextRadio(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
@@ -185,7 +179,7 @@ public function testDeselectByVisibleTextRadio()
$radios->deselectByVisibleText('AB');
}
- public function testDeselectByVisiblePartialTextRadio()
+ public function testDeselectByVisiblePartialTextRadio(): void
{
$radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]')));
diff --git a/tests/functional/WebDriverSelectTest.php b/tests/functional/WebDriverSelectTest.php
index aa3cce1f2..a5640b1be 100644
--- a/tests/functional/WebDriverSelectTest.php
+++ b/tests/functional/WebDriverSelectTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('form.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::FORM));
}
- public function testShouldCreateNewInstanceForSelectElementAndDetectIfItIsMultiple()
+ /**
+ * @dataProvider multipleSelectDataProvider
+ */
+ public function testShouldCreateNewInstanceForSelectElementAndDetectIfItIsMultiple(string $selector): void
{
$originalElement = $this->driver->findElement(WebDriverBy::cssSelector('#select'));
- $originalMultipleElement = $this->driver->findElement(WebDriverBy::cssSelector('#select-multiple'));
+ $originalMultipleElement = $this->driver->findElement(WebDriverBy::cssSelector($selector));
$select = new WebDriverSelect($originalElement);
$selectMultiple = new WebDriverSelect($originalMultipleElement);
@@ -35,7 +38,16 @@ public function testShouldCreateNewInstanceForSelectElementAndDetectIfItIsMultip
$this->assertTrue($selectMultiple->isMultiple());
}
- public function testShouldThrowExceptionWhenNotInstantiatedOnSelectElement()
+ public static function multipleSelectDataProvider(): array
+ {
+ return [
+ ['#select-multiple'],
+ ['#select-multiple-2'],
+ ['#select-multiple-3'],
+ ];
+ }
+
+ public function testShouldThrowExceptionWhenNotInstantiatedOnSelectElement(): void
{
$notSelectElement = $this->driver->findElement(WebDriverBy::cssSelector('textarea'));
@@ -46,9 +58,8 @@ public function testShouldThrowExceptionWhenNotInstantiatedOnSelectElement()
/**
* @dataProvider provideSelectSelector
- * @param string $selector
*/
- public function testShouldGetOptionsOfSelect($selector)
+ public function testShouldGetOptionsOfSelect(string $selector): void
{
$originalElement = $this->driver->findElement(WebDriverBy::cssSelector($selector));
$select = new WebDriverSelect($originalElement);
@@ -62,7 +73,7 @@ public function testShouldGetOptionsOfSelect($selector)
/**
* @return array[]
*/
- public function provideSelectSelector()
+ public function provideSelectSelector(): array
{
return [
'simple ' => ['#select'],
@@ -70,7 +81,7 @@ public function provideSelectSelector()
];
}
- public function testShouldDefaultSelectedOptionOfSimpleSelect()
+ public function testShouldDefaultSelectedOptionOfSimpleSelect(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
@@ -85,7 +96,7 @@ public function testShouldDefaultSelectedOptionOfSimpleSelect()
$this->assertSame('First', $firstSelectedOption->getText());
}
- public function testShouldReturnEmptyArrayIfNoOptionsOfMultipleSelectSelected()
+ public function testShouldReturnEmptyArrayIfNoOptionsOfMultipleSelectSelected(): void
{
$select = $this->getWebDriverSelectForMultipleSelect();
@@ -94,7 +105,7 @@ public function testShouldReturnEmptyArrayIfNoOptionsOfMultipleSelectSelected()
$this->assertSame([], $selectedOptions);
}
- public function testShouldThrowExceptionIfThereIsNoFirstSelectedOptionOfMultipleSelect()
+ public function testShouldThrowExceptionIfThereIsNoFirstSelectedOptionOfMultipleSelect(): void
{
$select = $this->getWebDriverSelectForMultipleSelect();
@@ -103,7 +114,7 @@ public function testShouldThrowExceptionIfThereIsNoFirstSelectedOptionOfMultiple
$select->getFirstSelectedOption();
}
- public function testShouldSelectOptionOfSimpleSelectByIndex()
+ public function testShouldSelectOptionOfSimpleSelectByIndex(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
$this->assertSame('first', $select->getFirstSelectedOption()->getAttribute('value'));
@@ -120,7 +131,7 @@ public function testShouldSelectOptionOfSimpleSelectByIndex()
* @group exclude-edge
* https://connect.microsoft.com/IE/feedback/details/2020772/-microsoft-edge-webdriver-cannot-select-multiple-on-select-html-tag
*/
- public function testShouldSelectOptionOfMultipleSelectByIndex()
+ public function testShouldSelectOptionOfMultipleSelectByIndex(): void
{
$select = $this->getWebDriverSelectForMultipleSelect();
$this->assertSame([], $select->getAllSelectedOptions());
@@ -138,7 +149,7 @@ public function testShouldSelectOptionOfMultipleSelectByIndex()
$this->assertContainsOptionsWithValues(['second', 'fourth', 'fifth'], $select->getAllSelectedOptions());
}
- public function testShouldThrowExceptionIfThereIsNoOptionIndexToSelect()
+ public function testShouldThrowExceptionIfThereIsNoOptionIndexToSelect(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
@@ -147,7 +158,7 @@ public function testShouldThrowExceptionIfThereIsNoOptionIndexToSelect()
$select->selectByIndex(1337);
}
- public function testShouldSelectOptionOfSimpleSelectByValue()
+ public function testShouldSelectOptionOfSimpleSelectByValue(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
$this->assertSame('first', $select->getFirstSelectedOption()->getAttribute('value'));
@@ -164,7 +175,7 @@ public function testShouldSelectOptionOfSimpleSelectByValue()
* @group exclude-edge
* https://connect.microsoft.com/IE/feedback/details/2020772/-microsoft-edge-webdriver-cannot-select-multiple-on-select-html-tag
*/
- public function testShouldSelectOptionOfMultipleSelectByValue()
+ public function testShouldSelectOptionOfMultipleSelectByValue(): void
{
$select = $this->getWebDriverSelectForMultipleSelect();
$this->assertSame([], $select->getAllSelectedOptions());
@@ -182,7 +193,7 @@ public function testShouldSelectOptionOfMultipleSelectByValue()
$this->assertContainsOptionsWithValues(['second', 'fourth', 'fifth'], $select->getAllSelectedOptions());
}
- public function testShouldThrowExceptionIfThereIsNoOptionValueToSelect()
+ public function testShouldThrowExceptionIfThereIsNoOptionValueToSelect(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
@@ -191,7 +202,7 @@ public function testShouldThrowExceptionIfThereIsNoOptionValueToSelect()
$select->selectByValue(1337);
}
- public function testShouldSelectOptionOfSimpleSelectByVisibleText()
+ public function testShouldSelectOptionOfSimpleSelectByVisibleText(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
$this->assertSame('first', $select->getFirstSelectedOption()->getAttribute('value'));
@@ -208,7 +219,7 @@ public function testShouldSelectOptionOfSimpleSelectByVisibleText()
* @group exclude-edge
* https://connect.microsoft.com/IE/feedback/details/2020772/-microsoft-edge-webdriver-cannot-select-multiple-on-select-html-tag
*/
- public function testShouldSelectOptionOfMultipleSelectByVisibleText()
+ public function testShouldSelectOptionOfMultipleSelectByVisibleText(): void
{
$select = $this->getWebDriverSelectForMultipleSelect();
$this->assertSame([], $select->getAllSelectedOptions());
@@ -226,7 +237,7 @@ public function testShouldSelectOptionOfMultipleSelectByVisibleText()
$this->assertContainsOptionsWithValues(['second', 'fourth', 'fifth'], $select->getAllSelectedOptions());
}
- public function testShouldThrowExceptionIfThereIsNoOptionVisibleTextToSelect()
+ public function testShouldThrowExceptionIfThereIsNoOptionVisibleTextToSelect(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
@@ -235,7 +246,7 @@ public function testShouldThrowExceptionIfThereIsNoOptionVisibleTextToSelect()
$select->selectByVisibleText('second'); // the option is "This is second option"
}
- public function testShouldSelectOptionOfSimpleSelectByVisiblePartialText()
+ public function testShouldSelectOptionOfSimpleSelectByVisiblePartialText(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
$this->assertSame('first', $select->getFirstSelectedOption()->getAttribute('value'));
@@ -252,7 +263,7 @@ public function testShouldSelectOptionOfSimpleSelectByVisiblePartialText()
* @group exclude-edge
* https://connect.microsoft.com/IE/feedback/details/2020772/-microsoft-edge-webdriver-cannot-select-multiple-on-select-html-tag
*/
- public function testShouldSelectOptionOfMultipleSelectByVisiblePartialText()
+ public function testShouldSelectOptionOfMultipleSelectByVisiblePartialText(): void
{
$select = $this->getWebDriverSelectForMultipleSelect();
$this->assertSame([], $select->getAllSelectedOptions());
@@ -273,7 +284,7 @@ public function testShouldSelectOptionOfMultipleSelectByVisiblePartialText()
);
}
- public function testShouldThrowExceptionIfThereIsNoOptionVisiblePartialTextToSelect()
+ public function testShouldThrowExceptionIfThereIsNoOptionVisiblePartialTextToSelect(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
@@ -282,7 +293,7 @@ public function testShouldThrowExceptionIfThereIsNoOptionVisiblePartialTextToSel
$select->selectByVisiblePartialText('Not existing option');
}
- public function testShouldThrowExceptionWhenDeselectingOnSimpleSelect()
+ public function testShouldThrowExceptionWhenDeselectingOnSimpleSelect(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
@@ -295,7 +306,7 @@ public function testShouldThrowExceptionWhenDeselectingOnSimpleSelect()
* @group exclude-edge
* https://connect.microsoft.com/IE/feedback/details/2020772/-microsoft-edge-webdriver-cannot-select-multiple-on-select-html-tag
*/
- public function testShouldDeselectAllOptionsOnMultipleSelect()
+ public function testShouldDeselectAllOptionsOnMultipleSelect(): void
{
$select = $this->getWebDriverSelectForMultipleSelect();
@@ -313,7 +324,7 @@ public function testShouldDeselectAllOptionsOnMultipleSelect()
* @group exclude-edge
* https://connect.microsoft.com/IE/feedback/details/2020772/-microsoft-edge-webdriver-cannot-select-multiple-on-select-html-tag
*/
- public function testShouldDeselectOptionOnMultipleSelectByIndex()
+ public function testShouldDeselectOptionOnMultipleSelectByIndex(): void
{
$select = $this->getWebDriverSelectForMultipleSelect();
$select->selectByValue('fourth'); // index 3
@@ -326,7 +337,7 @@ public function testShouldDeselectOptionOnMultipleSelectByIndex()
$this->assertContainsOptionsWithValues(['second'], $select->getAllSelectedOptions());
}
- public function testShouldThrowExceptionIfDeselectingSimpleSelectByIndex()
+ public function testShouldThrowExceptionIfDeselectingSimpleSelectByIndex(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
@@ -339,7 +350,7 @@ public function testShouldThrowExceptionIfDeselectingSimpleSelectByIndex()
* @group exclude-edge
* https://connect.microsoft.com/IE/feedback/details/2020772/-microsoft-edge-webdriver-cannot-select-multiple-on-select-html-tag
*/
- public function testShouldDeselectOptionOnMultipleSelectByValue()
+ public function testShouldDeselectOptionOnMultipleSelectByValue(): void
{
$select = $this->getWebDriverSelectForMultipleSelect();
$select->selectByValue('third');
@@ -352,7 +363,7 @@ public function testShouldDeselectOptionOnMultipleSelectByValue()
$this->assertContainsOptionsWithValues(['first'], $select->getAllSelectedOptions());
}
- public function testShouldThrowExceptionIfDeselectingSimpleSelectByValue()
+ public function testShouldThrowExceptionIfDeselectingSimpleSelectByValue(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
@@ -365,7 +376,7 @@ public function testShouldThrowExceptionIfDeselectingSimpleSelectByValue()
* @group exclude-edge
* https://connect.microsoft.com/IE/feedback/details/2020772/-microsoft-edge-webdriver-cannot-select-multiple-on-select-html-tag
*/
- public function testShouldDeselectOptionOnMultipleSelectByVisibleText()
+ public function testShouldDeselectOptionOnMultipleSelectByVisibleText(): void
{
$select = $this->getWebDriverSelectForMultipleSelect();
$select->selectByValue('fourth'); // text 'Fourth with spaces inside'
@@ -380,7 +391,7 @@ public function testShouldDeselectOptionOnMultipleSelectByVisibleText()
$this->assertContainsOptionsWithValues(['second'], $select->getAllSelectedOptions());
}
- public function testShouldThrowExceptionIfDeselectingSimpleSelectByVisibleText()
+ public function testShouldThrowExceptionIfDeselectingSimpleSelectByVisibleText(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
@@ -393,7 +404,7 @@ public function testShouldThrowExceptionIfDeselectingSimpleSelectByVisibleText()
* @group exclude-edge
* https://connect.microsoft.com/IE/feedback/details/2020772/-microsoft-edge-webdriver-cannot-select-multiple-on-select-html-tag
*/
- public function testShouldDeselectOptionOnMultipleSelectByVisiblePartialText()
+ public function testShouldDeselectOptionOnMultipleSelectByVisiblePartialText(): void
{
$select = $this->getWebDriverSelectForMultipleSelect();
$select->selectByValue('fourth'); // text 'Fourth with spaces inside'
@@ -414,7 +425,7 @@ public function testShouldDeselectOptionOnMultipleSelectByVisiblePartialText()
$this->assertContainsOptionsWithValues(['first'], $select->getAllSelectedOptions());
}
- public function testShouldThrowExceptionIfDeselectingSimpleSelectByVisiblePartialText()
+ public function testShouldThrowExceptionIfDeselectingSimpleSelectByVisiblePartialText(): void
{
$select = $this->getWebDriverSelectForSimpleSelect();
@@ -423,20 +434,14 @@ public function testShouldThrowExceptionIfDeselectingSimpleSelectByVisiblePartia
$select->deselectByVisiblePartialText('First');
}
- /**
- * @return WebDriverSelect
- */
- protected function getWebDriverSelectForSimpleSelect()
+ protected function getWebDriverSelectForSimpleSelect(): WebDriverSelect
{
$originalElement = $this->driver->findElement(WebDriverBy::cssSelector('#select'));
return new WebDriverSelect($originalElement);
}
- /**
- * @return WebDriverSelect
- */
- protected function getWebDriverSelectForMultipleSelect()
+ protected function getWebDriverSelectForMultipleSelect(): WebDriverSelect
{
$originalElement = $this->driver->findElement(WebDriverBy::cssSelector('#select-multiple'));
@@ -445,9 +450,8 @@ protected function getWebDriverSelectForMultipleSelect()
/**
* @param string[] $expectedValues
- * @param array $options
*/
- private function assertContainsOptionsWithValues(array $expectedValues, array $options)
+ private function assertContainsOptionsWithValues(array $expectedValues, array $options): void
{
$expectedCount = count($expectedValues);
$this->assertContainsOnlyInstancesOf(WebDriverElement::class, $options);
diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php
index 74c4d41e9..db0fd8b0f 100644
--- a/tests/functional/WebDriverTestCase.php
+++ b/tests/functional/WebDriverTestCase.php
@@ -1,4 +1,4 @@
-setUpSauceLabs();
} else {
$browserName = getenv('BROWSER_NAME');
+ $disableHeadless = filter_var(getenv('DISABLE_HEADLESS') ?: '', FILTER_VALIDATE_BOOLEAN);
if ($browserName === '' || $browserName === false) {
$this->markTestSkipped(
'To execute functional tests browser name must be provided in BROWSER_NAME environment variable'
@@ -46,29 +47,31 @@ protected function setUp(): void
if ($browserName === WebDriverBrowserType::CHROME) {
$chromeOptions = new ChromeOptions();
- // --no-sandbox is a workaround for Chrome crashing: https://github.com/SeleniumHQ/selenium/issues/4961
+
$chromeOptions->addArguments([
- '--headless',
- '--window-size=1024,768',
- '--no-sandbox',
+ '--screen-info={1280x720}',
+ '--no-sandbox', // workaround for https://github.com/SeleniumHQ/selenium/issues/4961
'--force-color-profile=srgb',
+ '--disable-search-engine-choice-screen',
]);
+ if (!$disableHeadless) {
+ $chromeOptions->addArguments(['--headless']);
+ }
+
if (!static::isW3cProtocolBuild()) {
$chromeOptions->setExperimentalOption('w3c', false);
}
$this->desiredCapabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions);
} elseif ($browserName === WebDriverBrowserType::FIREFOX) {
- if (getenv('GECKODRIVER') === '1') {
- $this->serverUrl = 'http://localhost:4444/';
+ $firefoxOptions = new FirefoxOptions();
+
+ if (!$disableHeadless) {
+ $firefoxOptions->addArguments(['-headless']);
}
- $firefoxOptions = new FirefoxOptions();
- $firefoxOptions->addArguments(['-headless']);
$this->desiredCapabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions);
- } elseif ($browserName === WebDriverBrowserType::SAFARI) {
- $this->serverUrl = 'http://localhost:4444/';
}
$this->desiredCapabilities->setBrowserName($browserName);
@@ -89,35 +92,34 @@ protected function tearDown(): void
if (getenv('BROWSER_NAME') === 'safari') {
// The Safari instance is already paired with another WebDriver session
- usleep(2e5); // 200ms
+ usleep(200000); // 200ms
}
}
}
- /**
- * @return bool
- */
- public static function isSauceLabsBuild()
+ public static function isSauceLabsBuild(): bool
{
return getenv('SAUCELABS') ? true : false;
}
- /**
- * @return bool
- */
- public static function isW3cProtocolBuild()
+ public static function isW3cProtocolBuild(): bool
{
return getenv('DISABLE_W3C_PROTOCOL') !== '1';
}
- public static function skipForW3cProtocol($message = 'Not supported by W3C specification')
+ public static function isSeleniumServerUsed(): bool
+ {
+ return getenv('SELENIUM_SERVER') === '1';
+ }
+
+ public static function skipForW3cProtocol($message = 'Not supported by W3C specification'): void
{
if (static::isW3cProtocolBuild()) {
static::markTestSkipped($message);
}
}
- public static function skipForJsonWireProtocol($message = 'Not supported by JsonWire protocol')
+ public static function skipForJsonWireProtocol($message = 'Not supported by JsonWire protocol'): void
{
if (!static::isW3cProtocolBuild()) {
static::markTestSkipped($message);
@@ -128,15 +130,14 @@ public static function skipForJsonWireProtocol($message = 'Not supported by Json
* Mark a test as skipped if the current browser is not in the list of browsers.
*
* @param string[] $browsers List of browsers for this test
- * @param string|null $message
*/
- public static function skipForUnmatchedBrowsers($browsers = [], $message = null)
+ public static function skipForUnmatchedBrowsers(array $browsers = [], ?string $message = null): void
{
$browserName = (string) getenv('BROWSER_NAME');
- if (array_search($browserName, $browsers) === false) {
+ if (!in_array($browserName, $browsers, true)) {
if (!$message) {
- $browserlist = implode(', ', $browsers);
- $message = 'Browser ' . $browserName . ' not supported for this test (' . $browserlist . ')';
+ $browserList = implode(', ', $browsers);
+ $message = 'Browser ' . $browserName . ' not supported for this test (' . $browserList . ')';
}
static::markTestSkipped($message);
@@ -169,11 +170,8 @@ public function runBare(): void
/**
* Get the URL of given test HTML on running webserver.
- *
- * @param string $path
- * @return string
*/
- protected function getTestPageUrl($path)
+ protected function getTestPageUrl(string $path): string
{
$host = 'http://localhost:8000/';
if ($alternateHost = getenv('FIXTURES_HOST')) {
@@ -183,7 +181,7 @@ protected function getTestPageUrl($path)
return $host . '/' . $path;
}
- protected function setUpSauceLabs()
+ protected function setUpSauceLabs(): void
{
$this->serverUrl = sprintf(
'http://%s:%s@ondemand.saucelabs.com/wd/hub',
@@ -200,14 +198,14 @@ protected function setUpSauceLabs()
if ($ciDetector->isCiDetected()) {
$ci = $ciDetector->detect();
if (!empty($ci->getBuildNumber())) {
- // SAUCE_TUNNEL_IDENTIFIER appended as a workaround for GH actions not having environment value
+ // SAUCE_TUNNEL_NAME appended as a workaround for GH actions not having environment value
// to distinguish runs of the matrix
- $build = $ci->getBuildNumber() . '.' . getenv('SAUCE_TUNNEL_IDENTIFIER');
+ $build = $ci->getBuildNumber() . '.' . getenv('SAUCE_TUNNEL_NAME');
}
}
- if (getenv('SAUCE_TUNNEL_IDENTIFIER')) {
- $tunnelIdentifier = getenv('SAUCE_TUNNEL_IDENTIFIER');
+ if (getenv('SAUCE_TUNNEL_NAME')) {
+ $tunnelName = getenv('SAUCE_TUNNEL_NAME');
}
if (static::isW3cProtocolBuild()) {
@@ -218,16 +216,16 @@ protected function setUpSauceLabs()
if (isset($build)) {
$sauceOptions['build'] = $build;
}
- if (isset($tunnelIdentifier)) {
- $sauceOptions['tunnelIdentifier'] = $tunnelIdentifier;
+ if (isset($tunnelName)) {
+ $sauceOptions['tunnelName'] = $tunnelName;
}
$this->desiredCapabilities->setCapability('sauce:options', (object) $sauceOptions);
} else {
$this->desiredCapabilities->setCapability('name', $name);
$this->desiredCapabilities->setCapability('tags', $tags);
- if (isset($tunnelIdentifier)) {
- $this->desiredCapabilities->setCapability('tunnel-identifier', $tunnelIdentifier);
+ if (isset($tunnelName)) {
+ $this->desiredCapabilities->setCapability('tunnel-identifier', $tunnelName);
}
if (isset($build)) {
$this->desiredCapabilities->setCapability('build', $build);
@@ -235,23 +233,7 @@ protected function setUpSauceLabs()
}
}
- /**
- * Uses assertStringContainsString when it is available or uses assertContains for old phpunit versions
- * @param string $needle
- * @param string $haystack
- * @param string $message
- */
- protected function compatAssertStringContainsString($needle, $haystack, $message = '')
- {
- if (method_exists($this, 'assertStringContainsString')) {
- parent::assertStringContainsString($needle, $haystack, $message);
-
- return;
- }
- parent::assertContains($needle, $haystack, $message);
- }
-
- protected function createWebDriver()
+ protected function createWebDriver(): void
{
$this->driver = RemoteWebDriver::create(
$this->serverUrl,
diff --git a/tests/functional/WebDriverTimeoutsTest.php b/tests/functional/WebDriverTimeoutsTest.php
index 72e5fd567..52534ae37 100644
--- a/tests/functional/WebDriverTimeoutsTest.php
+++ b/tests/functional/WebDriverTimeoutsTest.php
@@ -1,4 +1,4 @@
-driver->get($this->getTestPageUrl('delayed_element.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::DELAYED_ELEMENT));
$this->expectException(NoSuchElementException::class);
$this->driver->findElement(WebDriverBy::id('delayed'));
@@ -27,9 +27,9 @@ public function testShouldFailGettingDelayedElementWithoutWait()
* @covers ::__construct
* @covers ::implicitlyWait
*/
- public function testShouldGetDelayedElementWithImplicitWait()
+ public function testShouldGetDelayedElementWithImplicitWait(): void
{
- $this->driver->get($this->getTestPageUrl('delayed_element.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::DELAYED_ELEMENT));
$this->driver->manage()->timeouts()->implicitlyWait(2);
$element = $this->driver->findElement(WebDriverBy::id('delayed'));
@@ -42,12 +42,12 @@ public function testShouldGetDelayedElementWithImplicitWait()
* @covers ::__construct
* @covers ::pageLoadTimeout
*/
- public function testShouldFailIfPageIsLoadingLongerThanPageLoadTimeout()
+ public function testShouldFailIfPageIsLoadingLongerThanPageLoadTimeout(): void
{
$this->driver->manage()->timeouts()->pageLoadTimeout(1);
try {
- $this->driver->get($this->getTestPageUrl('slow_loading.html'));
+ $this->driver->get($this->getTestPageUrl(TestPage::SLOW_LOADING));
$this->fail('ScriptTimeoutException or TimeoutException exception should be thrown');
} catch (TimeoutException $e) { // thrown by Selenium 3.0.0+
} catch (ScriptTimeoutException $e) { // thrown by Selenium 2
diff --git a/tests/functional/WebDriverWindowTest.php b/tests/functional/WebDriverWindowTest.php
index 3b100d8f7..219126a7e 100644
--- a/tests/functional/WebDriverWindowTest.php
+++ b/tests/functional/WebDriverWindowTest.php
@@ -1,4 +1,4 @@
-driver->manage()
->window()
@@ -20,7 +20,7 @@ public function testShouldGetPosition()
$this->assertGreaterThanOrEqual(0, $position->getY());
}
- public function testShouldGetSize()
+ public function testShouldGetSize(): void
{
$size = $this->driver->manage()
->window()
@@ -30,7 +30,7 @@ public function testShouldGetSize()
$this->assertGreaterThan(0, $size->getHeight());
}
- public function testShouldMaximizeWindow()
+ public function testShouldMaximizeWindow(): void
{
$sizeBefore = $this->driver->manage()
->window()
@@ -49,10 +49,11 @@ public function testShouldMaximizeWindow()
}
/**
- * @group exclude-saucelabs
* @group exclude-edge
+ * @group exclude-safari
+ * @group exclude-saucelabs
*/
- public function testShouldFullscreenWindow()
+ public function testShouldFullscreenWindow(): void
{
self::skipForJsonWireProtocol('"fullscreen" window is not supported in JsonWire protocol');
@@ -74,12 +75,12 @@ public function testShouldFullscreenWindow()
}
/**
- * @group exclude-saucelabs
+ * @see https://bugs.chromium.org/p/chromium/issues/detail?id=1038050
* @group exclude-chrome
* @group exclude-safari
- * @see https://bugs.chromium.org/p/chromium/issues/detail?id=1038050
+ * @group exclude-saucelabs
*/
- public function testShouldMinimizeWindow()
+ public function testShouldMinimizeWindow(): void
{
self::skipForJsonWireProtocol('"minimize" window is not supported in JsonWire protocol');
@@ -95,7 +96,7 @@ public function testShouldMinimizeWindow()
/**
* @group exclude-saucelabs
*/
- public function testShouldSetSize()
+ public function testShouldSetSize(): void
{
$sizeBefore = $this->driver->manage()
->window()
@@ -118,7 +119,7 @@ public function testShouldSetSize()
/**
* @todo Skip when running headless mode
*/
- public function testShouldSetWindowPosition()
+ public function testShouldSetWindowPosition(): void
{
$this->driver->manage()
->window()
diff --git a/tests/functional/web/form.html b/tests/functional/web/form.html
index 7463750a6..597baca2e 100644
--- a/tests/functional/web/form.html
+++ b/tests/functional/web/form.html
@@ -48,6 +48,22 @@
Fourth with spaces inside
Fifth surrounded by spaces
+
+
+ First
+ This is second option
+ This is not second option
+ Fourth with spaces inside
+ Fifth surrounded by spaces
+
+
+
+ First
+ This is second option
+ This is not second option
+ Fourth with spaces inside
+ Fifth surrounded by spaces
+
diff --git a/tests/functional/web/index.html b/tests/functional/web/index.html
index 6107856ed..6202e3e9b 100644
--- a/tests/functional/web/index.html
+++ b/tests/functional/web/index.html
@@ -15,7 +15,6 @@
-
Welcome to the php-webdriver testing page.
@@ -37,6 +36,8 @@ Welcome to the php-webdriver testing page.
Events
|
Sortable
+ |
+ WebComponents
Test by ID
Test by Class
@@ -60,7 +61,16 @@ Welcome to the php-webdriver testing page.
stripped
-
+
+
This div has some more html inside.
+
+
+
+
+ This element is only shown with JavaScript disabled.
+
+
+
Foo
@@ -68,5 +78,9 @@
Welcome to the php-webdriver testing page.
Bar
+
+ This is not visible!
+
+