diff --git a/.coveralls.yml b/.coveralls.yml index 8115fc995..5ae55a93b 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1,2 +1,2 @@ -coverage_clover: ./logs/coverage-clover.xml +coverage_clover: ./logs/clover.xml json_path: ./logs/coveralls-upload.json diff --git a/.gitattributes b/.gitattributes index c3b6383af..2fdc4ff88 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,15 @@ * text=auto +/scripts export-ignore /tests export-ignore +/tools export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/.travis.yml export-ignore /example.php export-ignore /phpunit.xml.dist export-ignore +/.github export-ignore +/.php-cs-fixer.dist.php export-ignore +/phpstan.neon export-ignore +/.coveralls.yml export-ignore +/logs export-ignore +/mlc_config.json export-ignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..a79b02b1f --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,94 @@ +# Contributing to php-webdriver + +We love to have your help to make php-webdriver better! + +Feel free to open an [issue](https://github.com/php-webdriver/php-webdriver/issues) if you run into any problem, or +send a pull request (see bellow) with your contribution. + +## Before you contribute + +Do not hesitate to ask for a guidance before you implement notable change, or a new feature - use the associated [issue](https://github.com/php-webdriver/php-webdriver/issues) or use [Discussions](https://github.com/php-webdriver/php-webdriver/discussions). +Because any new code means increased effort in library maintenance (which is being done by volunteers in their free time), +please understand not every pull request is automatically accepted. This is why we recommend using the mentioned channels to discuss bigger changes in the source code first. + +When you are going to contribute, also please keep in mind that this webdriver client aims to be similar to clients in languages Java/Ruby/Python/C#. +Here is the [official documentation](https://www.selenium.dev/documentation/en/) and overview of [the official Java API](http://seleniumhq.github.io/selenium/docs/api/java/) + +## Workflow when contributing a patch + +1. Fork the project on GitHub +2. Implement your code changes into separate branch +3. Make sure all PHPUnit tests passes and code-style matches PSR-2 (see below). We also have CI builds which will automatically run tests on your pull request. Make sure to fix any reported issues reported by these automated tests. +4. When implementing a notable change, fix or a new feature, add record to the Unreleased section of [CHANGELOG.md](../CHANGELOG.md) +5. Submit your [pull request](https://github.com/php-webdriver/php-webdriver/pulls) against `main` branch + +### Run automated code checks + +To make sure your code comply with [PSR-2](http://www.php-fig.org/psr/psr-2/) coding style, tests passes and to execute other automated checks, run locally: + +```sh +composer all +``` + +To run functional tests locally there is some additional setup needed - see below. Without this setup, functional tests will be skipped. + + +For easier development there are also few other prepared commands: +- `composer fix` - to auto-fix the codestyle and composer.json +- `composer analyze` - to run only code analysis (without tests) +- `composer test` - to run all tests + +### Unit tests + +There are two test-suites: one with **unit tests** only (`unit`), and second with **functional tests** (`functional`), +which requires running Selenium server and local PHP server. + +To execute **all tests** in both suites run: + +```sh +composer test +``` + +If you want to execute **just the unit tests**, run: + +```sh +composer test -- --testsuite unit +``` + +**Functional tests** are run against a real browser. It means they take a bit longer and also require an additional setup: +you must first [download](https://www.selenium.dev/downloads/) and start the Selenium standalone server, +then start the local PHP server which will serve the test pages and then run the `functional` test suite: + +```sh +export BROWSER_NAME="htmlunit" # see below for other browsers +java -jar selenium-server-X.XX.0.jar standalone --log selenium.log & +php -S localhost:8000 -t tests/functional/web/ & +# Use following to run both unit and functional tests +composer all +# Or this to run only functional tests: +composer test -- --testsuite functional +``` + +If you want to run tests in different browser then "htmlunit" (Chrome or Firefox), you need to set up the browser driver (Chromedriver/Geckodriver), as it is [explained in wiki](https://github.com/php-webdriver/php-webdriver/wiki/Chrome) +and then the `BROWSER_NAME` environment variable: + +```sh +... +export BROWSER_NAME="chrome" +composer all +``` + +To test with Firefox/Geckodriver, you must also set `GECKODRIVER` environment variable: + +```sh +export GECKODRIVER=1 +export BROWSER_NAME="firefox" +composer all +``` + +To see the tests as they are happening (in the browser window), you can disable headless mode. This is useful eg. when debugging the tests or writing a new one: + +```sh +export DISABLE_HEADLESS="1" +composer all +``` diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..bac315d1d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,124 @@ +name: 🐛 Bug report +description: Create a bug report to help us improve php-webdriver +labels: [ "bug" ] +body: + - type: markdown + attributes: + value: | + If you have a question, [ask in Discussions](https://github.com/php-webdriver/php-webdriver/discussions) instead of filling a bug. + + If you are reporting a bug, please **fill as much as possible information**, otherwise the community and maintainers cannot provide a prompt feedback and help solving the issue. + - type: textarea + id: bug-description + attributes: + label: Bug description + description: | + A clear description of what the bug is. + validations: + required: true + + - type: textarea + id: steps-to-reproduce + attributes: + label: How could the issue be reproduced + description: | + Provide steps to reproduce the behavior. Please include everything relevant - the PHP code you use to initialize driver instance, the PHP code causing the error, HTML snippet or URL of the page where you encounter the issue etc. + This will be automatically formatted into code, so no need for backticks ```. + placeholder: | + // For example you can provide how you create WebDriver instance: + $capabilities = DesiredCapabilities::chrome(); + $driver = RemoteWebDriver::create('http://localhost:4444/', $capabilities); + // And the code you use to execute the php-webdriver commands, for example: + $driver->get('http://site.localhost/foo.html'); + $button = $driver->findElement(WebDriverBy::cssSelector('#foo')); + $button->click(); + +
+ +
+ + render: shell + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: | + A clear and concise description of what you expected to happen. + validations: + required: false + + - type: input + id: php-webdriver-version + attributes: + label: Php-webdriver version + description: You can run `composer show php-webdriver/webdriver` to find the version number + placeholder: | + For example: 1.13.0 + validations: + required: true + + - type: input + id: php-version + attributes: + label: PHP version + description: You can run `php -v` to find the version + placeholder: | + For example: 8.1.11 + validations: + required: true + + - type: input + id: how-start + attributes: + label: How do you start the browser driver or Selenium server + description: | + For example: Selenium server jar, Selenium in Docker, chromedriver command, Laravel Dusk, SauceLabs etc. + If relevant, provide the complete command you use to start the browser driver or Selenium server + validations: + required: true + + - type: input + id: selenium-version + attributes: + label: Selenium server / Selenium Docker image version + description: Relevant only if you use Selenium server / Selenium in Docker + validations: + required: false + + - type: input + id: browser-driver + attributes: + label: Browser driver (chromedriver/geckodriver...) version + description: You can run `chromedriver --version` or `geckodriver --version` to find the version + placeholder: | + For example: geckodriver 0.31.0 + validations: + required: false + + - type: input + id: browser + attributes: + label: Browser name and version + placeholder: | + For example: Firefox 105.0.2 + validations: + required: false + + - type: input + id: operating-system + attributes: + label: Operating system + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: | + Add any other context or you notes about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..e9a259980 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: ❓ Questions and Help + url: https://github.com/php-webdriver/php-webdriver/discussions + about: Please ask and answer questions here + - name: 💡 Ideas and feature requests + url: https://github.com/php-webdriver/php-webdriver/discussions + about: Suggest an idea for php-webdriver diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 000000000..50112e87a --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + time: "04:00" diff --git a/.github/workflows/coveralls-workaround.yaml b/.github/workflows/coveralls-workaround.yaml new file mode 100644 index 000000000..d6044861d --- /dev/null +++ b/.github/workflows/coveralls-workaround.yaml @@ -0,0 +1,48 @@ +name: Coveralls coverage +# Must be run in separate workflow to have access to repository secrets even for PR from forks. +# See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + +permissions: + contents: read + +on: + workflow_run: + workflows: [ "Tests" ] + types: + - completed + +jobs: + coveralls: + name: Coveralls coverage workaround + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + steps: + # see https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + - name: 'Download artifact' + uses: actions/github-script@v8 + with: + script: | + var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "data" + })[0]; + var download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/data.zip', Buffer.from(download.data)); + - run: unzip data.zip + - name: Coveralls coverage workaround + # see https://github.com/lemurheavy/coveralls-public/issues/1653#issuecomment-1251587119 + run: | + BUILD_NUM=$(cat run_id) + curl --location --request GET "https://coveralls.io/rerun_build?repo_token=${{%20secrets.COVERALLS_REPO_TOKEN%20}}&build_num=$BUILD_NUM" diff --git a/.github/workflows/docs-lint.yml b/.github/workflows/docs-lint.yml new file mode 100644 index 000000000..42baa0000 --- /dev/null +++ b/.github/workflows/docs-lint.yml @@ -0,0 +1,26 @@ +name: Lint PHP documentation + +permissions: + contents: read + +on: + push: + pull_request: + branches: + - 'main' + +jobs: + lint-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Lint PHP documentation + uses: sudo-bot/action-doctum@v5 + with: + config-file: scripts/doctum.php + method: 'parse' + cli-args: '--output-format=github --no-ansi --no-progress -v --ignore-parse-errors' diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml new file mode 100644 index 000000000..d0da12f8b --- /dev/null +++ b/.github/workflows/docs-publish.yml @@ -0,0 +1,38 @@ +name: Publish API documentation + +permissions: + contents: read + +on: + repository_dispatch: + types: [ run-build-api-docs ] + workflow_dispatch: + schedule: + - cron: "00 12 * * *" + +jobs: + publish-pages: + environment: + name: API documentation + url: https://php-webdriver.github.io/php-webdriver/ + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ssh-key: ${{ secrets.SSH_KEY_DEPLOY }} + + - name: Build PHP documentation + uses: sudo-bot/action-doctum@v5 + with: + config-file: scripts/doctum.php + method: 'update' + cli-args: '--output-format=github --no-ansi --no-progress -v --ignore-parse-errors' + + - name: Set commit author + run: | + git config user.name "Automated" + git config user.email "actions@users.noreply.github.com" + - name: Push the changes + run: ./scripts/update-built-docs.sh diff --git a/.github/workflows/no-response.yaml b/.github/workflows/no-response.yaml new file mode 100644 index 000000000..7147e3792 --- /dev/null +++ b/.github/workflows/no-response.yaml @@ -0,0 +1,29 @@ +name: No Response + +permissions: + issues: write + +# Both `issue_comment` and `scheduled` event types are required for this Action to work properly. +on: + issue_comment: + types: [created] + schedule: + - cron: '* */8 * * *' # every hour at :33 + +jobs: + noResponse: + runs-on: ubuntu-latest + steps: + - uses: lee-dohm/no-response@v0.5.0 + with: + token: ${{ github.token }} + daysUntilClose: 14 + responseRequiredLabel: 'waiting for reaction' + closeComment: > + This issue has been automatically closed because there has been no response + to our request for more information from the original author. With only the + information that is currently in the issue, we don't have enough information + to take action. + + If the original issue author adds comment with more information, + this issue will be automatically reopened and we can investigate further. diff --git a/.github/workflows/sauce-labs.yaml b/.github/workflows/sauce-labs.yaml new file mode 100644 index 000000000..812fbfe25 --- /dev/null +++ b/.github/workflows/sauce-labs.yaml @@ -0,0 +1,74 @@ +name: Sauce Labs + +permissions: + contents: read + +on: + push: + schedule: + - cron: '0 3 * * *' + +jobs: + tests: + runs-on: ubuntu-latest + # Source: https://github.community/t/do-not-run-cron-workflows-in-forks/17636/2 + if: (github.event_name == 'schedule' && github.repository == 'php-webdriver/php-webdriver') || (github.event_name != 'schedule') + env: + SAUCELABS: 1 + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + strategy: + fail-fast: false + matrix: + include: + # Chrome 74 is the last version which doesn't use W3C WebDriver by default and rather use OSS protocol + - { name: "Chrome 74, OSS protocol", BROWSER_NAME: "chrome", VERSION: "74.0", PLATFORM: "Windows 11", w3c: false, tunnel-name: "gh-1-chrome-oss-legacy" } + - { name: "Chrome latest, W3C protocol", BROWSER_NAME: "chrome", VERSION: "latest", PLATFORM: "Windows 11", w3c: true, tunnel-name: "gh-2-chrome-w3c" } + - { name: "Edge latest, W3C protocol", BROWSER_NAME: "MicrosoftEdge", VERSION: "latest", PLATFORM: "Windows 11", w3c: true, tunnel-name: "gh-3-MicrosoftEdge" } + + name: ${{ matrix.name }} (${{ matrix.tunnel-name }}) + steps: + - uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: mbstring, intl, zip + coverage: none + + - name: Install PHP dependencies + run: composer update --no-interaction + + - name: Start local PHP server + run: | + php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & + + - name: Start Sauce Connect + uses: saucelabs/sauce-connect-action@v3 + with: + username: ${{ secrets.SAUCE_USERNAME }} + accessKey: ${{ secrets.SAUCE_ACCESS_KEY }} + tunnelName: ${{ matrix.tunnel-name }} + proxyLocalhost: allow + region: 'us-west-1' + + - name: Run tests + env: + BROWSER_NAME: ${{ matrix.BROWSER_NAME }} + VERSION: ${{ matrix.VERSION }} + PLATFORM: ${{ matrix.PLATFORM }} + DISABLE_W3C_PROTOCOL: "${{ matrix.w3c && '0' || '1' }}" + SAUCE_TUNNEL_NAME: ${{ matrix.tunnel-name }} + run: | + if [ -n "$SAUCELABS" ]; then EXCLUDE_GROUP+="exclude-saucelabs,"; fi + if [ "$BROWSER_NAME" = "MicrosoftEdge" ]; then EXCLUDE_GROUP+="exclude-edge,"; fi + if [ "$BROWSER_NAME" = "firefox" ]; then EXCLUDE_GROUP+="exclude-firefox,"; fi + if [ "$BROWSER_NAME" = "chrome" ]; then EXCLUDE_GROUP+="exclude-chrome,"; fi + if [ -n "$EXCLUDE_GROUP" ]; then EXTRA_PARAMS+=" --exclude-group $EXCLUDE_GROUP"; fi + ./vendor/bin/phpunit --testsuite functional $EXTRA_PARAMS + + - name: Print logs + if: ${{ always() }} + run: | + if [ -f ./logs/php-server.log ]; then cat ./logs/php-server.log; fi diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 000000000..d215e92ce --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,214 @@ +name: Tests + +permissions: + contents: read + +on: + push: + pull_request: + schedule: + - cron: '0 3 * * *' + +jobs: + analyze: + name: "Code style and static analysis" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: mbstring, intl, zip + + - name: Install PHP dependencies + run: composer update --no-interaction + + - name: Lint + run: composer lint + + - name: Run analysis + run: composer analyze + + markdown-link-check: + name: "Markdown link check" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: tcort/github-action-markdown-link-check@v1 + with: + use-verbose-mode: 'yes' + + unit-tests: + name: "Unit tests" + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + dependencies: [''] + include: + - { php-version: '7.3', dependencies: '--prefer-lowest' } + + steps: + - uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, intl, zip + coverage: xdebug + ini-values: ${{ matrix.xdebug-ini-values }} + + - name: Install PHP dependencies + run: composer update --no-interaction ${{ matrix.dependencies }} + + - name: Run tests + run: vendor/bin/phpunit --testsuite unit --colors=always --coverage-clover ./logs/clover.xml + + - name: Submit coverage to Coveralls + # We use php-coveralls library for this, as the official Coveralls GitHub Action lacks support for clover reports: + # https://github.com/coverallsapp/github-action/issues/15 + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_PARALLEL: true + COVERALLS_FLAG_NAME: ${{ github.job }}-PHP-${{ matrix.php-version }} ${{ matrix.dependencies }} + run: | + composer global require php-coveralls/php-coveralls + ~/.composer/vendor/bin/php-coveralls -v + + functional-tests: + runs-on: ${{ matrix.os }} + env: + SELENIUM_SERVER_DOWNLOAD_URL: https://github.com/SeleniumHQ/selenium/releases/download/selenium-4.38.0/selenium-server-4.38.0.jar + + strategy: + fail-fast: false + matrix: + os: ['ubuntu-latest'] + browser: ['chrome', 'firefox'] + selenium-server: [true, false] # Whether to run via Selenium server or directly via browser driver + w3c: [true] # Although all builds negotiate protocol by default, it implies W3C protocol for both Chromedriver and Geckodriver + include: + - { browser: 'safari', os: 'macos-latest', selenium-server: false, w3c: true } + # Force OSS (JsonWire) protocol on ChromeDriver - to make sure we keep compatibility: + - { browser: 'chrome', os: 'ubuntu-latest', selenium-server: false, w3c: false } + + name: "Functional tests (${{ matrix.browser }}, Selenium server: ${{ matrix.selenium-server }}, W3C: ${{ matrix.w3c }})" + + steps: + - uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: mbstring, intl, zip + coverage: xdebug + + - name: Install PHP dependencies + run: composer update --no-interaction + + - name: Start Selenium standalone server + # If you want to run your Selenium WebDriver tests on GitHub actions, we recommend using service containers + # with eg. selenium/standalone-chrome image. See https://docs.github.com/en/actions/guides/about-service-containers + # But for the purpose of testing this library itself, we need more control, so we set everything up manually. + if: ${{ matrix.selenium-server }} + run: | + mkdir -p build logs + wget -q -t 3 -O build/selenium-server.jar $SELENIUM_SERVER_DOWNLOAD_URL + java -jar build/selenium-server.jar standalone --version + xvfb-run --server-args="-screen 0, 1280x720x24" --auto-servernum java -jar build/selenium-server.jar standalone --log logs/selenium-server.log & + + - name: Start ChromeDriver + if: ${{ !matrix.selenium-server && matrix.browser == 'chrome' }} + run: | + google-chrome --version + xvfb-run --server-args="-screen 0, 1280x720x24" --auto-servernum \ + chromedriver --port=4444 &> ./logs/chromedriver.log & + + - name: Start GeckoDriver + if: ${{ !matrix.selenium-server && matrix.browser == 'firefox' }} + run: | + firefox --version + geckodriver --version + xvfb-run --server-args="-screen 0, 1280x720x24" --auto-servernum \ + geckodriver &> ./logs/geckodriver.log & + + - name: Start SafariDriver + if: ${{ !matrix.selenium-server && matrix.browser == 'safari' }} + run: | + defaults read /Applications/Safari.app/Contents/Info CFBundleShortVersionString + /usr/bin/safaridriver -p 4444 --diagnose & + + - name: Start local PHP server + run: | + php -S 127.0.0.1:8000 -t tests/functional/web/ &> ./logs/php-server.log & + + - name: Wait for browser & PHP to start + timeout-minutes: 1 + run: | + while ! nc -z localhost 4444 ./data/run_id + - uses: actions/upload-artifact@v6 + with: + name: data + path: data/ diff --git a/.gitignore b/.gitignore index deb25f3ae..a9384110d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ composer.phar composer.lock vendor +tools/php-cs-fixer/vendor .php_cs.cache +.php-cs-fixer.cache +.phpunit.result.cache phpunit.xml logs/ +build/ # generic files to ignore *.lock diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 000000000..f754fb8b7 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,139 @@ +notPath('Firefox/FirefoxProfile.php') // need to use str_* instead of mb_str_* methods + ->in([__DIR__ . '/lib', __DIR__ . '/tests']); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => ['statements' => ['return', 'try']], + 'braces' => ['allow_single_line_anonymous_class_with_empty_body' => true, 'allow_single_line_closure' => true], + 'cast_spaces' => true, + 'class_attributes_separation' => ['elements' => ['method' => 'one', 'trait_import' => 'none']], + 'clean_namespace' => true, + 'combine_nested_dirname' => true, + 'compact_nullable_typehint' => true, + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => true, + //'declare_strict_types' => true, // TODO: used only in tests, use in lib in next major version + 'fopen_flag_order' => true, + 'fopen_flags' => true, + 'full_opening_tag' => true, + 'function_typehint_space' => true, + 'heredoc_indentation' => ['indentation' => 'same_as_start'], + 'implode_call' => true, + 'is_null' => true, + 'lambda_not_used_import' => true, + 'list_syntax' => true, + 'lowercase_cast' => true, + 'lowercase_static_reference' => true, + 'magic_constant_casing' => true, + 'magic_method_casing' => true, + 'mb_str_functions' => true, + 'method_argument_space' => ['after_heredoc' => true], + 'native_function_casing' => true, + 'native_function_type_declaration_casing' => true, + 'new_with_braces' => true, + 'no_alias_functions' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_comment' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'normalize_index_brace' => true, + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'break', + 'case', + 'continue', + 'curly_brace_block', + 'default', + 'extra', + 'parenthesis_brace_block', + 'return', + 'square_brace_block', + 'switch', + 'throw', + 'use', + ], + ], + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_superfluous_phpdoc_tags' => [ + 'allow_mixed' => true, + 'remove_inheritdoc' => true, + 'allow_unused_params' => true, // Used in RemoteWebDriver::createBySessionID to maintain BC + ], + 'no_trailing_comma_in_singleline' => true, + 'no_unreachable_default_argument_value' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'no_useless_sprintf' => true, + 'no_whitespace_before_comma_in_array' => ['after_heredoc' => true], + 'no_whitespace_in_blank_line' => true, + 'non_printable_character' => true, + 'nullable_type_declaration' => [ + 'syntax' => 'question_mark', + ], + 'nullable_type_declaration_for_default_null_value' => true, + 'object_operator_without_whitespace' => true, + 'ordered_class_elements' => true, + 'ordered_imports' => true, + 'php_unit_construct' => true, + 'php_unit_dedicate_assert' => true, + 'php_unit_dedicate_assert_internal_type' => true, + 'php_unit_expectation' => ['target' => '8.4'], + 'php_unit_method_casing' => ['case' => 'camel_case'], + 'php_unit_mock_short_will_return' => true, + 'php_unit_mock' => true, + 'php_unit_namespaced' => ['target' => '6.0'], + 'php_unit_no_expectation_annotation' => true, + 'php_unit_set_up_tear_down_visibility' => true, + 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'], + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_indent' => true, + 'phpdoc_no_access' => true, + // 'phpdoc_no_empty_return' => true, // disabled to allow forward compatibility with PHP 8.1 + 'phpdoc_no_package' => true, + 'phpdoc_order_by_value' => ['annotations' => ['covers', 'group', 'throws']], + 'phpdoc_order' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_trim' => true, + //'phpdoc_to_param_type' => true, // TODO: used only in tests, use in lib in next major version + //'phpdoc_to_return_type' => true, // TODO: used only in tests, use in lib in next major version + 'phpdoc_types' => true, + 'phpdoc_var_annotation_correct_order' => true, + 'pow_to_exponentiation' => true, + 'psr_autoloading' => true, + 'random_api_migration' => true, + 'self_accessor' => true, + 'set_type_to_cast' => true, + 'short_scalar_cast' => true, + 'single_blank_line_before_namespace' => true, + 'single_quote' => true, + 'single_space_after_construct' => true, + 'single_trait_insert_per_statement' => true, + 'space_after_semicolon' => true, + 'standardize_not_equals' => true, + 'strict_param' => true, + 'switch_continue_to_break' => true, + 'ternary_operator_spaces' => true, + 'ternary_to_elvis_operator' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arrays'], 'after_heredoc' => true], + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'visibility_required' => ['elements' => ['method', 'property', 'const']], + //'void_return' => true, // TODO: used only in tests, use in lib in next major version + 'whitespace_after_comma_in_array' => true, + 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], + ]) + ->setRiskyAllowed(true) + ->setFinder($finder); diff --git a/.php_cs.dist b/.php_cs.dist deleted file mode 100644 index 48aa29de7..000000000 --- a/.php_cs.dist +++ /dev/null @@ -1,83 +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'], - 'linebreak_after_opening_tag' => true, - 'lowercase_cast' => true, - 'mb_str_functions' => true, - 'method_separation' => true, - 'native_function_casing' => true, - 'new_with_braces' => true, - 'no_alias_functions' => true, - 'no_blank_lines_after_class_opening' => true, - 'no_blank_lines_after_phpdoc' => true, - 'no_empty_comment' => true, - 'no_empty_phpdoc' => true, - 'no_empty_statement' => true, - 'no_extra_consecutive_blank_lines' => [ - 'use', - 'break', - 'continue', - 'extra', - 'return', - 'throw', - 'useTrait', - 'curly_brace_block', - 'parenthesis_brace_block', - 'square_brace_block', - ], - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_trailing_comma_in_singleline_array' => true, - 'no_unreachable_default_argument_value' => true, - 'no_unused_imports' => true, - 'no_useless_else' => true, - 'no_useless_return' => true, - 'no_whitespace_in_blank_line' => true, - 'object_operator_without_whitespace' => true, - 'ordered_class_elements' => true, - 'ordered_imports' => true, - 'php_unit_construct' => true, - 'php_unit_dedicate_assert' => true, - 'php_unit_expectation' => true, - 'php_unit_mock' => true, - 'php_unit_no_expectation_annotation' => true, - 'phpdoc_add_missing_param_annotation' => true, - 'phpdoc_indent' => true, - 'phpdoc_no_access' => true, - 'phpdoc_no_empty_return' => true, - 'phpdoc_no_package' => true, - 'phpdoc_order' => true, - 'phpdoc_scalar' => true, - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_trim' => true, - 'phpdoc_types' => true, - 'psr4' => true, - 'self_accessor' => true, - 'short_scalar_cast' => true, - 'single_blank_line_before_namespace' => true, - 'single_quote' => true, - 'space_after_semicolon' => true, - 'standardize_not_equals' => true, - 'ternary_operator_spaces' => true, - 'trailing_comma_in_multiline_array' => true, - 'trim_array_spaces' => true, - 'unary_operator_spaces' => true, - 'visibility_required' => true, - 'whitespace_after_comma_in_array' => true, - 'yoda_style' => false, - ]) - ->setRiskyAllowed(true) - ->setFinder($finder); diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b6850baf1..000000000 --- a/.travis.yml +++ /dev/null @@ -1,125 +0,0 @@ -language: php -sudo: false -dist: trusty - -php: - - '5.6' - - '7.0' - - '7.1' - - '7.2' - - '7.3' - -env: - global: - - DISPLAY=:99.0 - - BROWSER_NAME="htmlunit" - -matrix: - include: - # Codestyle check build - - php: '7.3' - env: CHECK_CODESTYLE=1 - before_install: - - phpenv config-rm xdebug.ini - before_script: ~ - script: - - composer require phpstan/phpstan-shim # Not part of require-dev, because it won't install on PHP 5.6 - - composer analyze - - composer codestyle:check - after_script: ~ - after_success: ~ - - # Build with lowest possible dependencies on lowest possible PHP - - php: '5.6' - env: DEPENDENCIES="--prefer-lowest" - - # Firefox inside Travis environment - - php: '7.3' - env: BROWSER_NAME="firefox" - addons: - firefox: "45.8.0esr" - - # Stable Chrome + Chromedriver 74 inside Travis environment - - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="74.0.3729.6" - addons: - chrome: stable - - # Stable Chrome + Chromedriver 75+ inside Travis environment - - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="75.0.3770.8" - addons: - chrome: stable - - # Saucelabs builds - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="firefox" VERSION="47.0" PLATFORM="Windows 10" - before_script: - - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" - addons: - sauce_connect: true - jwt: - secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="74.0" PLATFORM="Windows 10" # 74 is the last version which don't use W3C WebDriver by default - before_script: - - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" - addons: - sauce_connect: true - jwt: - secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="75.0" PLATFORM="Windows 10" - before_script: - - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" - addons: - sauce_connect: true - jwt: - secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="MicrosoftEdge" VERSION="16.16299" PLATFORM="Windows 10" - before_script: - - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" - addons: - sauce_connect: true - jwt: - secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - -cache: - directories: - - $HOME/.composer/cache - - jar - -install: - - travis_retry composer self-update - - travis_retry composer update --no-interaction $DEPENDENCIES - -before_script: - - if [ "$BROWSER_NAME" = "chrome" ]; then mkdir chromedriver; wget -q -t 3 https://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip; unzip chromedriver_linux64 -d chromedriver; fi - - if [ "$BROWSER_NAME" = "chrome" ]; then export CHROMEDRIVER_PATH=$PWD/chromedriver/chromedriver; fi - - sh -e /etc/init.d/xvfb start - - if [ ! -f jar/selenium-server-standalone-3.8.1.jar ]; then wget -q -t 3 -P jar https://selenium-release.storage.googleapis.com/3.8/selenium-server-standalone-3.8.1.jar; fi - - java -Dwebdriver.firefox.marionette=false -Dwebdriver.chrome.driver="$CHROMEDRIVER_PATH" -jar jar/selenium-server-standalone-3.8.1.jar -enablePassThrough false -log ./logs/selenium.log & - - until $(echo | nc localhost 4444); do sleep 1; echo Waiting for Selenium server on port 4444...; done; echo "Selenium server started" - - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" - -script: - - if [ -n "$SAUCELABS" ]; then EXCLUDE_GROUP+="exclude-saucelabs,"; fi - - if [ "$BROWSER_NAME" = "MicrosoftEdge" ]; then EXCLUDE_GROUP+="exclude-edge,"; fi - - if [ -n "$EXCLUDE_GROUP" ]; then EXTRA_PARAMS+=" --exclude-group $EXCLUDE_GROUP"; fi - - ./vendor/bin/phpunit --coverage-clover ./logs/coverage-clover.xml $EXTRA_PARAMS - -after_script: - - if [ -f ./logs/selenium.log ]; then cat ./logs/selenium.log; fi - - if [ -f ./logs/php-server.log ]; then cat ./logs/php-server.log; fi - -after_success: - - travis_retry php vendor/bin/php-coveralls -v diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e4ca748..a468c26f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,169 @@ 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 ` 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 7023c466e..b4c1e307d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,10 +1,29 @@ parameters: - ignoreErrors: - - '#Class Symfony\\Component\\Process\\ProcessBuilder not found.#' - - '#Instantiated class Symfony\\Component\\Process\\ProcessBuilder not found.#' - - '#Call to method setPrefix\(\) on an unknown class Symfony\\Component\\Process\\ProcessBuilder#' - # To be fixed: - - '#Call to an undefined method RecursiveIteratorIterator::getSubPathName\(\)#' - - '#Call to an undefined method Facebook\\WebDriver\\WebDriver::getTouch\(\)#' - - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::getCoordinates\(\)#' - - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::equals\(\)#' + level: 2 + paths: + - lib/ + - tests/ + + ignoreErrors: + # To be fixed in next major version: + - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriver::getTouch\(\)#' + path: 'lib/Interactions/WebDriverTouchActions.php' + - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriver::getTouch\(\)#' + path: 'lib/Support/Events/EventFiringWebDriver.php' + - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::getCoordinates\(\)#' + path: 'lib/Support/Events/EventFiringWebElement.php' + - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::equals\(\)#' + path: 'lib/Support/Events/EventFiringWebElement.php' + - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::takeElementScreenshot\(\)#' + path: 'lib/Support/Events/EventFiringWebElement.php' + - message: '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::getShadowRoot\(\)#' + path: 'lib/Support/Events/EventFiringWebElement.php' + - '#Unsafe usage of new static\(\)#' + + # Parameter is intentionally not part of signature to not break BC + - message: '#PHPDoc tag \@param references unknown parameter: \$isW3cCompliant#' + path: 'lib/Remote/RemoteWebDriver.php' + - message: '#PHPDoc tag \@param references unknown parameter: \$existingCapabilities#' + path: 'lib/Remote/RemoteWebDriver.php' + + inferPrivatePropertyTypeFromConstructor: true diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 430a8fb5c..c58c72eaa 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,9 @@ - - - - + tests/unit @@ -24,14 +13,13 @@ - - + + ./lib - - + + - diff --git a/scripts/docs-template.html b/scripts/docs-template.html new file mode 100644 index 000000000..4814f5ec5 --- /dev/null +++ b/scripts/docs-template.html @@ -0,0 +1,7 @@ + + + + + Taking you to the latest documentation. + + \ No newline at end of file diff --git a/scripts/doctum.php b/scripts/doctum.php new file mode 100644 index 000000000..08e722917 --- /dev/null +++ b/scripts/doctum.php @@ -0,0 +1,30 @@ +files() + ->name('*.php') + ->in($srcRoot . 'lib'); + +$versions = GitVersionCollection::create($srcRoot) + ->addFromTags('1.*') // only latest minor version + ->addFromTags('0.6.0') + ->add('main', 'main branch') +; + +return new Doctum($iterator, [ + 'title' => 'php-webdriver API', + 'theme' => 'default', + 'build_dir' => $root . '/build/dist/%version%/', + 'cache_dir' => $root . '/build/cache/%version%/', + 'include_parent_data' => true, + 'remote_repository' => new GitHubRemoteRepository('php-webdriver/php-webdriver', $srcRoot), + 'versions' => $versions, + 'base_url' => 'https://php-webdriver.github.io/php-webdriver/%version%/' +]); diff --git a/scripts/update-built-docs.sh b/scripts/update-built-docs.sh new file mode 100755 index 000000000..76646bed5 --- /dev/null +++ b/scripts/update-built-docs.sh @@ -0,0 +1,50 @@ +#!/bin/sh +set -e + +cleanup() { + git ls-files ./ | xargs -r -n 1 rm + rm -rfd ./* +} + +copyToTemp() { + TEMP_DIR="$(mktemp -d --suffix=_doctum-build-php-webdriver)" + cp -rp build/dist/* "${TEMP_DIR}" + cp ./scripts/docs-template.html "${TEMP_DIR}/index.html" +} + +emptyAndRemoveTemp() { + mv "${TEMP_DIR}"/* ./ + # Create symlink for main to latest + ln -s -r ./main ./latest + # Create symlink for main to master + ln -s -r ./main ./master + # Create symlink for main to community + ln -s -r ./main ./community + rm -rf "${TEMP_DIR}" +} + +commitAndPushChanges() { + # Push the changes, only if there is changes + git add -A + git diff-index --quiet HEAD || git commit -m "Api documentations update ($(date --rfc-3339=seconds --utc))" -m "#apidocs" && if [ -z "${SKIP_PUSH}" ]; then git push; fi +} + +if [ ! -d ./build/dist ]; then + echo 'Missing built docs' + exit 1 +fi + +# Remove cache dir, do not upload it +rm -rf ./build/cache + +copyToTemp +# Remove build dir, do not upload it +rm -rf ./build + +git checkout gh-pages + +cleanup +emptyAndRemoveTemp +commitAndPushChanges + +git checkout - > /dev/null diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 801396034..d9cb0c2c7 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,16 +1,3 @@ -desiredCapabilities->getBrowserName() !== WebDriverBrowserType::CHROME) { + $this->markTestSkipped('ChromeDevTools are available only in Chrome'); + } + } + + public function testShouldExecuteDevToolsCommandWithoutParameters(): void + { + $devTools = new ChromeDevToolsDriver($this->driver); + + $result = $devTools->execute('Performance.enable'); + + $this->assertSame([], $result); + } + + public function testShouldExecuteDevToolsCommandWithParameters(): void + { + $devTools = new ChromeDevToolsDriver($this->driver); + + $result = $devTools->execute('Runtime.evaluate', [ + 'returnByValue' => true, + 'expression' => '42 + 1', + ]); + + $this->assertSame('number', $result['result']['type']); + $this->assertSame(43, $result['result']['value']); + } +} diff --git a/tests/functional/Chrome/ChromeDriverServiceTest.php b/tests/functional/Chrome/ChromeDriverServiceTest.php index 781a68bae..9b6c46336 100644 --- a/tests/functional/Chrome/ChromeDriverServiceTest.php +++ b/tests/functional/Chrome/ChromeDriverServiceTest.php @@ -1,20 +1,8 @@ -markTestSkipped('The test is run only when running against local chrome'); + } + } + + protected function tearDown(): void { - if (!getenv('BROWSER_NAME') === 'chrome' || getenv('SAUCELABS') || !getenv('CHROMEDRIVER_PATH')) { - $this->markTestSkipped('ChromeDriverServiceTest is run only when running against local chrome'); + if ($this->driverService !== null && $this->driverService->isRunning()) { + $this->driverService->stop(); } } - public function testShouldStartAndStopServiceCreatedUsingShortcutConstructor() + public function testShouldStartAndStopServiceCreatedUsingShortcutConstructor(): void { // The createDefaultService() method expect path to the executable to be present in the environment variable - putenv(ChromeDriverService::CHROME_DRIVER_EXE_PROPERTY . '=' . getenv('CHROMEDRIVER_PATH')); + putenv(ChromeDriverService::CHROME_DRIVER_EXECUTABLE . '=' . getenv('CHROMEDRIVER_PATH')); - $driverService = ChromeDriverService::createDefaultService(); + $this->driverService = ChromeDriverService::createDefaultService(); - $this->assertSame('http://localhost:9515/', $driverService->getURL()); + $this->assertSame('http://localhost:9515/', $this->driverService->getURL()); - $this->assertInstanceOf(ChromeDriverService::class, $driverService->start()); - $this->assertTrue($driverService->isRunning()); + $this->assertInstanceOf(ChromeDriverService::class, $this->driverService->start()); + $this->assertTrue($this->driverService->isRunning()); - $this->assertInstanceOf(ChromeDriverService::class, $driverService->start()); + $this->assertInstanceOf(ChromeDriverService::class, $this->driverService->start()); - $this->assertInstanceOf(ChromeDriverService::class, $driverService->stop()); - $this->assertFalse($driverService->isRunning()); + $this->assertInstanceOf(ChromeDriverService::class, $this->driverService->stop()); + $this->assertFalse($this->driverService->isRunning()); - $this->assertInstanceOf(ChromeDriverService::class, $driverService->stop()); + $this->assertInstanceOf(ChromeDriverService::class, $this->driverService->stop()); } - public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor() + public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor(): void { - $driverService = new ChromeDriverService(getenv('CHROMEDRIVER_PATH'), 9515, ['--port=9515']); + $this->driverService = new ChromeDriverService(getenv('CHROMEDRIVER_PATH'), 9515, ['--port=9515']); - $this->assertSame('http://localhost:9515/', $driverService->getURL()); + $this->assertSame('http://localhost:9515/', $this->driverService->getURL()); - $driverService->start(); - $this->assertTrue($driverService->isRunning()); + $this->driverService->start(); + $this->assertTrue($this->driverService->isRunning()); - $driverService->stop(); - $this->assertFalse($driverService->isRunning()); + $this->driverService->stop(); + $this->assertFalse($this->driverService->isRunning()); } - public function testShouldThrowExceptionIfExecutableCannotBeFound() + public function testShouldThrowExceptionIfExecutableIsNotExecutable(): void { - putenv(ChromeDriverService::CHROME_DRIVER_EXE_PROPERTY . '=/not/existing'); + putenv(ChromeDriverService::CHROME_DRIVER_EXECUTABLE . '=' . __FILE__); $this->expectException(\Exception::class); - $this->expectExceptionMessage('\'/not/existing\' is not a file.'); + $this->expectExceptionMessage('is not executable'); ChromeDriverService::createDefaultService(); } - public function testShouldThrowExceptionIfExecutableIsNotExecutable() + public function testShouldUseDefaultExecutableIfNoneProvided(): void { - putenv(ChromeDriverService::CHROME_DRIVER_EXE_PROPERTY . '=' . __FILE__); + // Put path where ChromeDriver binary is actually located to system PATH, to make sure we can locate it + putenv('PATH=' . getenv('PATH') . ':' . dirname(getenv('CHROMEDRIVER_PATH'))); - $this->expectException(\Exception::class); - $this->expectExceptionMessage('is not executable'); - ChromeDriverService::createDefaultService(); + // Unset CHROME_DRIVER_EXECUTABLE so that ChromeDriverService will attempt to run the binary from system PATH + putenv(ChromeDriverService::CHROME_DRIVER_EXECUTABLE . '='); + + $this->driverService = ChromeDriverService::createDefaultService(); + + $this->assertSame('http://localhost:9515/', $this->driverService->getURL()); + + $this->assertInstanceOf(ChromeDriverService::class, $this->driverService->start()); + $this->assertTrue($this->driverService->isRunning()); } } diff --git a/tests/functional/Chrome/ChromeDriverTest.php b/tests/functional/Chrome/ChromeDriverTest.php index 89eb766cd..407b65bd8 100644 --- a/tests/functional/Chrome/ChromeDriverTest.php +++ b/tests/functional/Chrome/ChromeDriverTest.php @@ -1,68 +1,97 @@ -markTestSkipped('ChromeDriverServiceTest is run only when running against local chrome'); + if (getenv('BROWSER_NAME') !== 'chrome' || empty(getenv('CHROMEDRIVER_PATH')) + || WebDriverTestCase::isSauceLabsBuild()) { + $this->markTestSkipped('The test is run only when running against local chrome'); } } - protected function tearDown() + protected function tearDown(): void { if ($this->driver instanceof RemoteWebDriver && $this->driver->getCommandExecutor() !== null) { $this->driver->quit(); } } - public function testShouldStartChromeDriver() + /** + * @dataProvider provideDialect + */ + public function testShouldStartChromeDriver(bool $isW3cDialect): void + { + $this->startChromeDriver($isW3cDialect); + $this->assertInstanceOf(ChromeDriver::class, $this->driver); + $this->assertInstanceOf(DriverCommandExecutor::class, $this->driver->getCommandExecutor()); + + // Make sure actual browser capabilities were set + $this->assertNotEmpty($this->driver->getCapabilities()->getVersion()); + $this->assertNotEmpty($this->driver->getCapabilities()->getCapability('goog:chromeOptions')); + + $this->driver->get('http://localhost:8000/'); + + $this->assertSame('http://localhost:8000/', $this->driver->getCurrentURL()); + } + + /** + * @return array[] + */ + public function provideDialect(): array + { + return [ + 'w3c' => [true], + 'oss' => [false], + ]; + } + + public function testShouldInstantiateDevTools(): void + { + $this->startChromeDriver(); + + $devTools = $this->driver->getDevTools(); + + $this->assertInstanceOf(ChromeDevToolsDriver::class, $devTools); + + $this->driver->get('http://localhost:8000/'); + + $cdpResult = $devTools->execute( + 'Runtime.evaluate', + ['expression' => 'window.location.toString()'] + ); + + $this->assertSame(['result' => ['type' => 'string', 'value' => 'http://localhost:8000/']], $cdpResult); + } + + private function startChromeDriver($w3cDialect = true): void { // The createDefaultService() method expect path to the executable to be present in the environment variable - putenv(ChromeDriverService::CHROME_DRIVER_EXE_PROPERTY . '=' . getenv('CHROMEDRIVER_PATH')); + 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']); + $chromeOptions->addArguments(['--no-sandbox', '--headless', '--disable-search-engine-choice-screen']); + $chromeOptions->setExperimentalOption('w3c', $w3cDialect); $desiredCapabilities = DesiredCapabilities::chrome(); $desiredCapabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); $this->driver = ChromeDriver::start($desiredCapabilities); - - $this->assertInstanceOf(ChromeDriver::class, $this->driver); - $this->assertInstanceOf(DriverCommandExecutor::class, $this->driver->getCommandExecutor()); - - $this->driver->get('http://localhost:8000/'); - - $this->assertSame('http://localhost:8000/', $this->driver->getCurrentURL()); - - $this->driver->quit(); } } diff --git a/tests/functional/FileUploadTest.php b/tests/functional/FileUploadTest.php index 5b2f6d2d3..e812353ea 100644 --- a/tests/functional/FileUploadTest.php +++ b/tests/functional/FileUploadTest.php @@ -1,17 +1,4 @@ -driver->get($this->getTestPageUrl('upload.html')); + $this->driver->get($this->getTestPageUrl(TestPage::UPLOAD)); $fileElement = $this->driver->findElement(WebDriverBy::name('upload')); @@ -54,7 +45,7 @@ public function testShouldUploadAFile() $this->assertSame('10', $uploadedFileSize); } - private function getTestFilePath() + private function getTestFilePath(): string { return __DIR__ . '/Fixtures/FileUploadTestFile.txt'; } diff --git a/tests/functional/Firefox/FirefoxDriverServiceTest.php b/tests/functional/Firefox/FirefoxDriverServiceTest.php new file mode 100644 index 000000000..feabe0dbf --- /dev/null +++ b/tests/functional/Firefox/FirefoxDriverServiceTest.php @@ -0,0 +1,83 @@ +markTestSkipped('The test is run only when running against local firefox'); + } + } + + protected function tearDown(): void + { + if ($this->driverService !== null && $this->driverService->isRunning()) { + $this->driverService->stop(); + } + } + + public function testShouldStartAndStopServiceCreatedUsingShortcutConstructor(): void + { + // The createDefaultService() method expect path to the executable to be present in the environment variable + putenv(FirefoxDriverService::WEBDRIVER_FIREFOX_DRIVER . '=' . getenv('GECKODRIVER_PATH')); + + $this->driverService = FirefoxDriverService::createDefaultService(); + + $this->assertSame('http://localhost:9515/', $this->driverService->getURL()); + + $this->assertInstanceOf(FirefoxDriverService::class, $this->driverService->start()); + $this->assertTrue($this->driverService->isRunning()); + + $this->assertInstanceOf(FirefoxDriverService::class, $this->driverService->start()); + + $this->assertInstanceOf(FirefoxDriverService::class, $this->driverService->stop()); + $this->assertFalse($this->driverService->isRunning()); + + $this->assertInstanceOf(FirefoxDriverService::class, $this->driverService->stop()); + } + + public function testShouldStartAndStopServiceCreatedUsingDefaultConstructor(): void + { + $this->driverService = new FirefoxDriverService(getenv('GECKODRIVER_PATH'), 9515, ['-p=9515']); + + $this->assertSame('http://localhost:9515/', $this->driverService->getURL()); + + $this->driverService->start(); + $this->assertTrue($this->driverService->isRunning()); + + $this->driverService->stop(); + $this->assertFalse($this->driverService->isRunning()); + } + + public function testShouldUseDefaultExecutableIfNoneProvided(): void + { + // Put path where geckodriver binary is actually located to system PATH, to make sure we can locate it + putenv('PATH=' . getenv('PATH') . ':' . dirname(getenv('GECKODRIVER_PATH'))); + + // Unset WEBDRIVER_FIREFOX_BINARY so that FirefoxDriverService will attempt to run the binary from system PATH + putenv(FirefoxDriverService::WEBDRIVER_FIREFOX_DRIVER . '='); + + $this->driverService = FirefoxDriverService::createDefaultService(); + + $this->assertSame('http://localhost:9515/', $this->driverService->getURL()); + + $this->assertInstanceOf(FirefoxDriverService::class, $this->driverService->start()); + $this->assertTrue($this->driverService->isRunning()); + } +} diff --git a/tests/functional/Firefox/FirefoxDriverTest.php b/tests/functional/Firefox/FirefoxDriverTest.php new file mode 100644 index 000000000..900746d2f --- /dev/null +++ b/tests/functional/Firefox/FirefoxDriverTest.php @@ -0,0 +1,86 @@ +markTestSkipped('The test is run only when running against local firefox'); + } + } + + protected function tearDown(): void + { + if ($this->driver instanceof RemoteWebDriver && $this->driver->getCommandExecutor() !== null) { + $this->driver->quit(); + } + } + + public function testShouldStartFirefoxDriver(): void + { + $this->startFirefoxDriver(); + $this->assertInstanceOf(FirefoxDriver::class, $this->driver); + $this->assertInstanceOf(DriverCommandExecutor::class, $this->driver->getCommandExecutor()); + + // Make sure actual browser capabilities were set + $this->assertNotEmpty($this->driver->getCapabilities()->getVersion()); + $this->assertNotEmpty($this->driver->getCapabilities()->getCapability('moz:profile')); + $this->assertTrue($this->driver->getCapabilities()->getCapability('moz:headless')); + + // Ensure browser is responding to basic command + $this->driver->get('http://localhost:8000/'); + $this->assertSame('http://localhost:8000/', $this->driver->getCurrentURL()); + } + + public function testShouldSetPreferenceWithFirefoxOptions(): void + { + $firefoxOptions = new FirefoxOptions(); + $firefoxOptions->setPreference('javascript.enabled', false); + + $this->startFirefoxDriver($firefoxOptions); + + $this->driver->get('http://localhost:8000/'); + + $noScriptElement = $this->driver->findElement(WebDriverBy::id('noscript')); + $this->assertEquals( + 'This element is only shown with JavaScript disabled.', + $noScriptElement->getText() + ); + } + + private function startFirefoxDriver(?FirefoxOptions $firefoxOptions = null): void + { + // The createDefaultService() method expect path to the executable to be present in the environment variable + putenv(FirefoxDriverService::WEBDRIVER_FIREFOX_DRIVER . '=' . getenv('GECKODRIVER_PATH')); + + if ($firefoxOptions === null) { + $firefoxOptions = new FirefoxOptions(); + } + $firefoxOptions->addArguments(['-headless']); + $desiredCapabilities = DesiredCapabilities::firefox(); + $desiredCapabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions); + + $this->driver = FirefoxDriver::start($desiredCapabilities); + } +} diff --git a/tests/functional/Firefox/FirefoxProfileTest.php b/tests/functional/Firefox/FirefoxProfileTest.php new file mode 100644 index 000000000..a8ff2c62d --- /dev/null +++ b/tests/functional/Firefox/FirefoxProfileTest.php @@ -0,0 +1,128 @@ +` element + * with some text at the end of each page Firefox renders. + * + * In case the extension will need to be modified, steps below must be followed, + * otherwise firefox won't load the modified extension: + * + * - Extract the xpi file (it is a zip archive) to some temporary directory + * - Make needed changes in the files + * - Install web-ext tool from Mozilla (@see https://github.com/mozilla/web-ext) + * - Sign in to https://addons.mozilla.org/cs/developers/addon/api/key/ to get your JWT API key and JWT secret + * - Run `web-ext sign --channel=unlisted --api-key=[you-api-key] --api-secret=[your-api-secret]` in the extension dir + * - Store the output file (`web-ext-artifacts/[...].xpi`) to the Fixtures/ directory + * + * @group exclude-saucelabs + * @covers \Facebook\WebDriver\Firefox\FirefoxProfile + */ +class FirefoxProfileTest extends TestCase +{ + /** @var FirefoxDriver */ + protected $driver; + + protected $firefoxTestExtensionFilename = __DIR__ . '/Fixtures/FirefoxExtension.xpi'; + + protected function setUp(): void + { + if (getenv('BROWSER_NAME') !== 'firefox' || empty(getenv('GECKODRIVER_PATH')) + || WebDriverTestCase::isSauceLabsBuild()) { + $this->markTestSkipped('The test is run only when running against local firefox'); + } + } + + protected function tearDown(): void + { + if ($this->driver instanceof RemoteWebDriver && $this->driver->getCommandExecutor() !== null) { + $this->driver->quit(); + } + } + + public function testShouldStartDriverWithEmptyProfile(): void + { + $firefoxProfile = new FirefoxProfile(); + $this->startFirefoxDriverWithProfile($firefoxProfile); + + $this->driver->get('http://localhost:8000/'); + $element = $this->driver->findElement(WebDriverBy::id('welcome')); + $this->assertSame( + 'Welcome to the php-webdriver testing page.', + $element->getText() + ); + } + + public function testShouldInstallExtension(): void + { + $firefoxProfile = new FirefoxProfile(); + $firefoxProfile->addExtension($this->firefoxTestExtensionFilename); + $this->startFirefoxDriverWithProfile($firefoxProfile); + + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); + + $this->assertInstanceOf(RemoteWebDriver::class, $this->driver); + + // it sometimes takes split of a second for the extension to render the element, so we must use wait + $element = $this->driver->wait(5, 1)->until( + WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::id('webDriverExtensionTest')) + ); + + $this->assertEquals('This element was added by browser extension', $element->getText()); + } + + public function testShouldUseProfilePreferences(): void + { + $firefoxProfile = new FirefoxProfile(); + + // Please note, although it is possible to set preferences right into the profile (what this test does), + // we recommend using the setPreference() method on FirefoxOptions instead, so that you don't need to + // create FirefoxProfile. + $firefoxProfile->setPreference('javascript.enabled', false); + $this->assertSame('false', $firefoxProfile->getPreference('javascript.enabled')); + + $this->startFirefoxDriverWithProfile($firefoxProfile); + $this->driver->get('http://localhost:8000/'); + + $noScriptElement = $this->driver->findElement(WebDriverBy::id('noscript')); + $this->assertEquals( + 'This element is only shown with JavaScript disabled.', + $noScriptElement->getText() + ); + } + + protected function getTestPageUrl($path): string + { + $host = 'http://localhost:8000/'; + if ($alternateHost = getenv('FIXTURES_HOST')) { + $host = $alternateHost; + } + + return $host . '/' . $path; + } + + private function startFirefoxDriverWithProfile(FirefoxProfile $firefoxProfile): void + { + // The createDefaultService() method expect path to the executable to be present in the environment variable + putenv(FirefoxDriverService::WEBDRIVER_FIREFOX_DRIVER . '=' . getenv('GECKODRIVER_PATH')); + + $firefoxOptions = new FirefoxOptions(); + $firefoxOptions->addArguments(['-headless']); + $firefoxOptions->setProfile($firefoxProfile); + $desiredCapabilities = DesiredCapabilities::firefox(); + $desiredCapabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions); + + $this->driver = FirefoxDriver::start($desiredCapabilities); + } +} diff --git a/tests/functional/Firefox/Fixtures/FirefoxExtension.xpi b/tests/functional/Firefox/Fixtures/FirefoxExtension.xpi new file mode 100644 index 000000000..c5805fbeb Binary files /dev/null and b/tests/functional/Firefox/Fixtures/FirefoxExtension.xpi differ diff --git a/tests/functional/Remote/JsonWireCompatTest.php b/tests/functional/Remote/JsonWireCompatTest.php new file mode 100644 index 000000000..8dfb5e29a --- /dev/null +++ b/tests/functional/Remote/JsonWireCompatTest.php @@ -0,0 +1,17 @@ +expectException(UnexpectedResponseException::class); + $this->expectExceptionMessage('Unexpected server response for getting an element. Expected array'); + + JsonWireCompat::getElement(null); + } +} diff --git a/tests/functional/RemoteKeyboardTest.php b/tests/functional/RemoteKeyboardTest.php new file mode 100644 index 000000000..897d33cf7 --- /dev/null +++ b/tests/functional/RemoteKeyboardTest.php @@ -0,0 +1,84 @@ +driver->get($this->getTestPageUrl(TestPage::EVENTS)); + + $this->driver->getKeyboard()->sendKeys('ab'); + $this->driver->getKeyboard()->pressKey(WebDriverKeys::SHIFT); + + $this->driver->getKeyboard()->sendKeys('cd' . WebDriverKeys::NULL . 'e'); + + $this->driver->getKeyboard()->pressKey(WebDriverKeys::SHIFT); + $this->driver->getKeyboard()->pressKey('f'); + $this->driver->getKeyboard()->releaseKey(WebDriverKeys::SHIFT); + $this->driver->getKeyboard()->releaseKey('f'); + + if (self::isW3cProtocolBuild()) { + $this->assertEquals( + [ + 'keydown "a"', + 'keyup "a"', + 'keydown "b"', + 'keyup "b"', + 'keydown "Shift"', + 'keydown "C"', + 'keyup "C"', + 'keydown "D"', + 'keyup "D"', + 'keyup "Shift"', + 'keydown "e"', + 'keyup "e"', + 'keydown "Shift"', + 'keydown "F"', + 'keyup "Shift"', + 'keyup "f"', + ], + $this->retrieveLoggedKeyboardEvents() + ); + } else { + $this->assertEquals( + [ + 'keydown "a"', + 'keyup "a"', + 'keydown "b"', + 'keyup "b"', + 'keydown "Shift"', + 'keydown "C"', + 'keyup "C"', + 'keydown "D"', + 'keyup "D"', + 'keyup "Shift"', + 'keydown "e"', + 'keyup "e"', + 'keydown "Shift"', + 'keydown "F"', // pressKey behaves differently on old protocol + 'keyup "F"', + 'keyup "Shift"', + 'keydown "f"', + 'keyup "f"', + ], + $this->retrieveLoggedKeyboardEvents() + ); + } + } +} diff --git a/tests/functional/RemoteTargetLocatorTest.php b/tests/functional/RemoteTargetLocatorTest.php new file mode 100644 index 000000000..a3080e67a --- /dev/null +++ b/tests/functional/RemoteTargetLocatorTest.php @@ -0,0 +1,183 @@ +driver->get($this->getTestPageUrl(TestPage::OPEN_NEW_WINDOW)); + $originalWindowHandle = $this->driver->getWindowHandle(); + $windowHandlesBefore = $this->driver->getWindowHandles(); + + $this->driver->findElement(WebDriverBy::cssSelector('a#open-new-window')) + ->click(); + + $this->driver->wait()->until( + WebDriverExpectedCondition::numberOfWindowsToBe(2) + ); + + // At first the window should not be switched + $this->assertStringContainsString('open_new_window.html', $this->driver->getCurrentURL()); + $this->assertSame($originalWindowHandle, $this->driver->getWindowHandle()); + + /** + * @see https://w3c.github.io/webdriver/#get-window-handles + * > "The order in which the window handles are returned is arbitrary." + * Thus we must first find out which window handle is the new one + */ + $windowHandlesAfter = $this->driver->getWindowHandles(); + $newWindowHandle = array_diff($windowHandlesAfter, $windowHandlesBefore); + $newWindowHandle = reset($newWindowHandle); + + $this->driver->switchTo()->window($newWindowHandle); + + $this->driver->wait()->until(function () { + // The window contents is sometimes not yet loaded and needs a while to actually show the index.html page + return mb_strpos($this->driver->getCurrentURL(), 'index.html') !== false; + }); + + // After switchTo() is called, the active window should be changed + $this->assertStringContainsString('index.html', $this->driver->getCurrentURL()); + $this->assertNotSame($originalWindowHandle, $this->driver->getWindowHandle()); + } + + public function testActiveElement(): void + { + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); + + $activeElement = $this->driver->switchTo()->activeElement(); + $this->assertInstanceOf(RemoteWebElement::class, $activeElement); + $this->assertSame('body', $activeElement->getTagName()); + + $this->driver->findElement(WebDriverBy::name('test_name'))->click(); + $activeElement = $this->driver->switchTo()->activeElement(); + $this->assertSame('input', $activeElement->getTagName()); + $this->assertSame('test_name', $activeElement->getAttribute('name')); + } + + 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(TestPage::PAGE_WITH_FRAME)); + + $this->assertStringContainsString($parentPage, $this->driver->getPageSource()); + + $this->driver->switchTo()->frame(0); + $this->assertStringContainsString($firstChildFrame, $this->driver->getPageSource()); + + $this->driver->switchTo()->frame(null); + $this->assertStringContainsString($parentPage, $this->driver->getPageSource()); + + $this->driver->switchTo()->frame(1); + $this->assertStringContainsString($secondChildFrame, $this->driver->getPageSource()); + + $this->driver->switchTo()->frame(null); + $this->assertStringContainsString($parentPage, $this->driver->getPageSource()); + + $this->driver->switchTo()->frame(0); + $this->assertStringContainsString($firstChildFrame, $this->driver->getPageSource()); + + $this->driver->switchTo()->defaultContent(); + $this->assertStringContainsString($parentPage, $this->driver->getPageSource()); + } + + public function testShouldSwitchToParentFrame(): void + { + $parentPage = 'This is the host page which contains an iFrame'; + $firstChildFrame = 'This is the content of the iFrame'; + + $this->driver->get($this->getTestPageUrl(TestPage::PAGE_WITH_FRAME)); + + $this->assertStringContainsString($parentPage, $this->driver->getPageSource()); + + $this->driver->switchTo()->frame(0); + $this->assertStringContainsString($firstChildFrame, $this->driver->getPageSource()); + + $this->driver->switchTo()->parent(); + $this->assertStringContainsString($parentPage, $this->driver->getPageSource()); + } + + public function testShouldSwitchToFrameByElement(): void + { + $this->driver->get($this->getTestPageUrl(TestPage::PAGE_WITH_FRAME)); + + $element = $this->driver->findElement(WebDriverBy::id('iframe_content')); + $this->driver->switchTo()->frame($element); + + $this->assertStringContainsString('This is the content of the iFrame', $this->driver->getPageSource()); + } + + /** + * @group exclude-saucelabs + */ + public function testShouldCreateNewWindow(): void + { + self::skipForJsonWireProtocol('Create new window is not supported in JsonWire protocol'); + + // Ensure that the initial context matches. + $initialHandle = $this->driver->getWindowHandle(); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); + $this->assertEquals($this->getTestPageUrl(TestPage::INDEX), $this->driver->getCurrentUrl()); + $source = $this->driver->getPageSource(); + $this->assertStringContainsString('

', $source); + $this->assertStringContainsString('Welcome to the php-webdriver testing page.', $source); + $windowHandles = $this->driver->getWindowHandles(); + $this->assertCount(1, $windowHandles); + + // Create a new window + $this->driver->switchTo()->newWindow(); + + $windowHandles = $this->driver->getWindowHandles(); + $this->assertCount(2, $windowHandles); + + $newWindowHandle = $this->driver->getWindowHandle(); + $this->driver->get($this->getTestPageUrl(TestPage::UPLOAD)); + $this->assertEquals($this->getTestPageUrl(TestPage::UPLOAD), $this->driver->getCurrentUrl()); + $this->assertNotEquals($initialHandle, $newWindowHandle); + + // Switch back to original context. + $this->driver->switchTo()->window($initialHandle); + $this->assertEquals($this->getTestPageUrl(TestPage::INDEX), $this->driver->getCurrentUrl()); + } + + /** + * @group exclude-saucelabs + */ + public function testShouldNotAcceptStringAsFrameIdInW3cMode(): void + { + self::skipForJsonWireProtocol(); + + $this->driver->get($this->getTestPageUrl(TestPage::PAGE_WITH_FRAME)); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'In W3C compliance mode frame must be either instance of WebDriverElement, integer or null' + ); + + $this->driver->switchTo()->frame('iframe_content'); + } + + /** + * @group exclude-saucelabs + */ + public function testShouldAcceptStringAsFrameIdInJsonWireMode(): void + { + self::skipForW3cProtocol(); + + $this->driver->get($this->getTestPageUrl(TestPage::PAGE_WITH_FRAME)); + + $this->driver->switchTo()->frame('iframe_content'); + + $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 90c072922..a39a91070 100644 --- a/tests/functional/RemoteWebDriverCreateTest.php +++ b/tests/functional/RemoteWebDriverCreateTest.php @@ -1,39 +1,30 @@ -driver = RemoteWebDriver::create( $this->serverUrl, $this->desiredCapabilities, $this->connectionTimeout, - $this->requestTimeout + $this->requestTimeout, + null, + null, + null ); $this->assertInstanceOf(RemoteWebDriver::class, $this->driver); @@ -41,15 +32,26 @@ public function testShouldStartBrowserAndCreateInstanceOfRemoteWebDriver() $this->assertInstanceOf(HttpCommandExecutor::class, $this->driver->getCommandExecutor()); $this->assertNotEmpty($this->driver->getCommandExecutor()->getAddressOfRemoteServer()); - $this->assertInternalType('string', $this->driver->getSessionID()); + $this->assertIsString($this->driver->getSessionID()); $this->assertNotEmpty($this->driver->getSessionID()); $returnedCapabilities = $this->driver->getCapabilities(); $this->assertInstanceOf(WebDriverCapabilities::class, $returnedCapabilities); - $this->assertSame($this->desiredCapabilities->getBrowserName(), $returnedCapabilities->getBrowserName()); + + // MicrosoftEdge on Sauce Labs started to identify itself back as "msedge" + if ($this->desiredCapabilities->getBrowserName() !== WebDriverBrowserType::MICROSOFT_EDGE) { + $this->assertEqualsIgnoringCase( + $this->desiredCapabilities->getBrowserName(), + $returnedCapabilities->getBrowserName() + ); + } + + $this->assertNotEmpty($returnedCapabilities->getPlatform()); + $this->assertNotEmpty($returnedCapabilities); + $this->assertNotEmpty($returnedCapabilities->getVersion()); } - public function testShouldAcceprCapabilitiesAsAnArray() + public function testShouldAcceptCapabilitiesAsAnArray(): void { // Method has a side-effect of converting whole content of desiredCapabilities to an array $this->desiredCapabilities->toArray(); @@ -60,9 +62,11 @@ public function testShouldAcceprCapabilitiesAsAnArray() $this->connectionTimeout, $this->requestTimeout ); + + $this->assertNotNull($this->driver->getCapabilities()); } - public function testShouldCreateWebDriverWithRequiredCapabilities() + public function testShouldCreateWebDriverWithRequiredCapabilities(): void { $requiredCapabilities = new DesiredCapabilities(); @@ -79,7 +83,25 @@ public function testShouldCreateWebDriverWithRequiredCapabilities() $this->assertInstanceOf(RemoteWebDriver::class, $this->driver); } - public function testShouldCreateInstanceFromExistingSessionId() + /** + * Capabilities (browser name) must be defined when executing via Selenium proxy (standalone server, + * Saucelabs etc.). But when running directly via browser driver, they could be empty. + * However the the browser driver must be able to create non-headless instance (eg. inside xvfb). + * @group exclude-saucelabs + */ + public function testShouldCreateWebDriverWithoutCapabilities(): void + { + if (getenv('GECKODRIVER') !== '1' && empty(getenv('CHROMEDRIVER_PATH'))) { + $this->markTestSkipped('This test makes sense only when run directly via specific browser driver'); + } + + $this->driver = RemoteWebDriver::create($this->serverUrl); + + $this->assertInstanceOf(RemoteWebDriver::class, $this->driver); + $this->assertNotEmpty($this->driver->getSessionID()); + } + + public function testShouldCreateInstanceFromExistingSessionId(): void { // Create driver instance and load page "index.html" $originalDriver = RemoteWebDriver::create( @@ -88,16 +110,66 @@ public function testShouldCreateInstanceFromExistingSessionId() $this->connectionTimeout, $this->requestTimeout ); - $originalDriver->get($this->getTestPageUrl('index.html')); - $this->assertContains('/index.html', $originalDriver->getCurrentURL()); + $originalDriver->get($this->getTestPageUrl(TestPage::INDEX)); + $this->assertStringContainsString('/index.html', $originalDriver->getCurrentURL()); - // Store session ID + // Store session attributes $sessionId = $originalDriver->getSessionID(); + $isW3cCompliant = $originalDriver->isW3cCompliant(); + $originalCapabilities = $originalDriver->getCapabilities(); + + $capabilitiesForSessionReuse = $originalCapabilities; + if ($this->isSeleniumServerUsed()) { + // do not provide capabilities when selenium server is used, to test they are read from selenium server + $capabilitiesForSessionReuse = null; + } // Create new RemoteWebDriver instance based on the session ID - $this->driver = RemoteWebDriver::createBySessionID($sessionId, $this->serverUrl); + $this->driver = RemoteWebDriver::createBySessionID( + $sessionId, + $this->serverUrl, + null, + null, + $isW3cCompliant, + $capabilitiesForSessionReuse + ); + + // Capabilities should be retrieved and be set to the driver instance + $returnedCapabilities = $this->driver->getCapabilities(); + $this->assertInstanceOf(WebDriverCapabilities::class, $returnedCapabilities); + + $expectedBrowserName = $this->desiredCapabilities->getBrowserName(); + + $this->assertEqualsIgnoringCase( + $expectedBrowserName, + $returnedCapabilities->getBrowserName() + ); + $this->assertEqualsCanonicalizing($originalCapabilities, $this->driver->getCapabilities()); // Check we reused the previous instance (window) and it has the same URL - $this->assertContains('/index.html', $this->driver->getCurrentURL()); + $this->assertStringContainsString('/index.html', $this->driver->getCurrentURL()); + + // Do some interaction with the new driver + $this->assertNotEmpty($this->driver->findElement(WebDriverBy::id('id_test'))->getText()); + } + + public function testShouldRequireCapabilitiesToBeSetToReuseExistingSession(): void + { + $this->expectException(UnexpectedResponseException::class); + $this->expectExceptionMessage( + 'Existing Capabilities were not provided, and they also cannot be read from Selenium Grid' + ); + + // Do not provide capabilities, they also cannot be retrieved from the Selenium Grid + RemoteWebDriver::createBySessionID( + 'sessionId', + 'http://localhost:332/', // nothing should be running there + null, + null + ); + } + + protected function createWebDriver(): void + { } } diff --git a/tests/functional/RemoteWebDriverFindElementTest.php b/tests/functional/RemoteWebDriverFindElementTest.php index eea14b6e5..f638aa527 100644 --- a/tests/functional/RemoteWebDriverFindElementTest.php +++ b/tests/functional/RemoteWebDriverFindElementTest.php @@ -1,17 +1,4 @@ -driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $this->expectException(NoSuchElementException::class); $this->driver->findElement(WebDriverBy::id('not_existing')); } - public function testShouldFindElementIfExistsOnAPage() + public function testShouldFindElementIfExistsOnAPage(): void { - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $element = $this->driver->findElement(WebDriverBy::id('id_test')); $this->assertInstanceOf(RemoteWebElement::class, $element); } - public function testShouldReturnEmptyArrayIfElementsCannotBeFound() + public function testShouldReturnEmptyArrayIfElementsCannotBeFound(): void { - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $elements = $this->driver->findElements(WebDriverBy::cssSelector('not_existing')); - $this->assertInternalType('array', $elements); + $this->assertIsArray($elements); $this->assertCount(0, $elements); } - public function testShouldFindMultipleElements() + public function testShouldFindMultipleElements(): void { - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $elements = $this->driver->findElements(WebDriverBy::cssSelector('ul > li')); - $this->assertInternalType('array', $elements); + $this->assertIsArray($elements); $this->assertCount(5, $elements); $this->assertContainsOnlyInstancesOf(RemoteWebElement::class, $elements); } + + /** + * @group exclude-saucelabs + */ + public function testEscapeCssSelector(): void + { + self::skipForJsonWireProtocol( + 'CSS selectors containing special characters are not supported by the legacy protocol' + ); + + $this->driver->get($this->getTestPageUrl(TestPage::ESCAPE_CSS)); + + $element = $this->driver->findElement(WebDriverBy::id('.fo\'oo')); + $this->assertSame('Foo', $element->getText()); + + $element = $this->driver->findElement(WebDriverBy::className('#ba\'r')); + $this->assertSame('Bar', $element->getText()); + + $element = $this->driver->findElement(WebDriverBy::name('.#ba\'z')); + $this->assertSame('Baz', $element->getText()); + } } diff --git a/tests/functional/RemoteWebDriverTest.php b/tests/functional/RemoteWebDriverTest.php index d996d304a..95a01b9b7 100644 --- a/tests/functional/RemoteWebDriverTest.php +++ b/tests/functional/RemoteWebDriverTest.php @@ -1,23 +1,9 @@ -driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $this->assertEquals( 'php-webdriver test page', @@ -38,12 +24,12 @@ public function testShouldGetPageTitle() } /** - * @covers ::getCurrentURL * @covers ::get + * @covers ::getCurrentURL */ - public function testShouldGetCurrentUrl() + public function testShouldGetCurrentUrl(): void { - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $this->assertStringEndsWith('/index.html', $this->driver->getCurrentURL()); } @@ -51,23 +37,32 @@ public function testShouldGetCurrentUrl() /** * @covers ::getPageSource */ - public function testShouldGetPageSource() + public function testShouldGetPageSource(): void { - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $source = $this->driver->getPageSource(); - $this->assertContains('

', $source); - $this->assertContains('Welcome to the facebook/php-webdriver testing page.', $source); + $this->assertStringContainsString('

', $source); + $this->assertStringContainsString('Welcome to the php-webdriver testing page.', $source); } /** * @covers ::getSessionID + * @covers ::isW3cCompliant */ - public function testShouldGetSessionId() + public function testShouldGetSessionId(): void { + // This tests is intentionally included in another test, to not slow down build. + // @TODO Remove following in 2.0 + if (self::isW3cProtocolBuild()) { + $this->assertTrue($this->driver->isW3cCompliant()); + } else { + $this->assertFalse($this->driver->isW3cCompliant()); + } + $sessionId = $this->driver->getSessionID(); - $this->assertInternalType('string', $sessionId); + $this->assertIsString($sessionId); $this->assertNotEmpty($sessionId); } @@ -75,11 +70,13 @@ public function testShouldGetSessionId() * @group exclude-saucelabs * @covers ::getAllSessions */ - public function testShouldGetAllSessions() + public function testShouldGetAllSessions(): void { - $sessions = RemoteWebDriver::getAllSessions($this->serverUrl); + self::skipForW3cProtocol(); + + $sessions = RemoteWebDriver::getAllSessions($this->serverUrl, 30000); - $this->assertInternalType('array', $sessions); + $this->assertIsArray($sessions); $this->assertCount(1, $sessions); $this->assertArrayHasKey('capabilities', $sessions[0]); @@ -92,14 +89,26 @@ public function testShouldGetAllSessions() * @covers ::getCommandExecutor * @covers ::quit */ - public function testShouldQuitAndUnsetExecutor() + public function testShouldQuitAndUnsetExecutor(): void { - $this->assertCount(1, RemoteWebDriver::getAllSessions($this->serverUrl)); + self::skipForW3cProtocol(); + + $this->assertCount( + 1, + RemoteWebDriver::getAllSessions($this->serverUrl) + ); $this->assertInstanceOf(HttpCommandExecutor::class, $this->driver->getCommandExecutor()); $this->driver->quit(); - $this->assertCount(0, RemoteWebDriver::getAllSessions($this->serverUrl)); + // Wait a while until chromedriver finishes deleting the session. + // https://bugs.chromium.org/p/chromedriver/issues/detail?id=3736 + usleep(250000); // 250 ms + + $this->assertCount( + 0, + RemoteWebDriver::getAllSessions($this->serverUrl) + ); $this->assertNull($this->driver->getCommandExecutor()); } @@ -107,14 +116,14 @@ public function testShouldQuitAndUnsetExecutor() * @covers ::getWindowHandle * @covers ::getWindowHandles */ - public function testShouldGetWindowHandles() + public function testShouldGetWindowHandles(): void { - $this->driver->get($this->getTestPageUrl('open_new_window.html')); + $this->driver->get($this->getTestPageUrl(TestPage::OPEN_NEW_WINDOW)); $windowHandle = $this->driver->getWindowHandle(); $windowHandles = $this->driver->getWindowHandles(); - $this->assertInternalType('string', $windowHandle); + $this->assertIsString($windowHandle); $this->assertNotEmpty($windowHandle); $this->assertSame([$windowHandle], $windowHandles); @@ -131,11 +140,13 @@ 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)); + $this->assertCount(2, $this->driver->getWindowHandles()); $this->driver->close(); @@ -147,21 +158,25 @@ 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()); $start = microtime(true); - $this->driver->executeScript(' + $scriptResult = $this->driver->executeScript(' setTimeout( function(){document.getElementById("id_test").innerHTML = "Text changed by script";}, 250 - )'); + ); + return "returned value"; + '); $end = microtime(true); + $this->assertSame('returned value', $scriptResult); + $this->assertLessThan(250, $end - $start, 'executeScript() should not block execution'); // If we wait, the script should be executed and its value changed @@ -173,28 +188,30 @@ 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()); $start = microtime(true); - $this->driver->executeAsyncScript( + $scriptResult = $this->driver->executeAsyncScript( 'var callback = arguments[arguments.length - 1]; setTimeout( function(){ document.getElementById("id_test").innerHTML = "Text changed by script"; - callback(); + callback("returned value"); }, 250 );' ); $end = microtime(true); + $this->assertSame('returned value', $scriptResult); + $this->assertGreaterThan( 0.250, $end - $start, @@ -206,24 +223,46 @@ function(){ $this->assertSame('Text changed by script', $element->getText()); } + /** + * @covers ::executeScript + * @covers ::prepareScriptArguments + * @group exclude-saucelabs + */ + public function testShouldExecuteScriptWithParamsAndReturnValue(): void + { + $this->driver->manage()->timeouts()->setScriptTimeout(1); + + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); + + $element1 = $this->driver->findElement(WebDriverBy::id('id_test')); + $element2 = $this->driver->findElement(WebDriverBy::className('test_class')); + + $scriptResult = $this->driver->executeScript( + 'var element1 = arguments[0]; + var element2 = arguments[1]; + return "1: " + element1.innerText + ", 2: " + element2.innerText; + ', + [$element1, $element2] + ); + + $this->assertSame('1: Test by ID, 2: Test by Class', $scriptResult); + } + /** * @covers ::takeScreenshot + * @covers \Facebook\WebDriver\Support\ScreenshotHelper */ - public function testShouldTakeScreenshot() + public function testShouldTakeScreenshot(): void { if (!extension_loaded('gd')) { $this->markTestSkipped('GD extension must be enabled'); } - if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) { - $this->markTestSkipped('Screenshots are not supported by HtmlUnit browser'); - } - - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $outputPng = $this->driver->takeScreenshot(); $image = imagecreatefromstring($outputPng); - $this->assertInternalType('resource', $image); + $this->assertNotFalse($image); $this->assertGreaterThan(0, imagesx($image)); $this->assertGreaterThan(0, imagesy($image)); @@ -231,28 +270,59 @@ public function testShouldTakeScreenshot() /** * @covers ::takeScreenshot + * @covers \Facebook\WebDriver\Support\ScreenshotHelper + * @group exclude-safari + * Safari is returning different color profile and it does not have way to configure "force-color-profile" */ - public function testShouldSaveScreenshotToFile() + public function testShouldSaveScreenshotToFile(): void { if (!extension_loaded('gd')) { $this->markTestSkipped('GD extension must be enabled'); } - if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) { - $this->markTestSkipped('Screenshots are not supported by HtmlUnit browser'); - } - $screenshotPath = sys_get_temp_dir() . '/selenium-screenshot.png'; + $screenshotPath = sys_get_temp_dir() . '/' . uniqid('php-webdriver-') . '/selenium-screenshot.png'; - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $this->driver->takeScreenshot($screenshotPath); $image = imagecreatefrompng($screenshotPath); - $this->assertInternalType('resource', $image); + $this->assertNotFalse($image); $this->assertGreaterThan(0, imagesx($image)); $this->assertGreaterThan(0, imagesy($image)); + // Validate expected red box is present on the screenshot + $this->assertSame( + ['red' => 255, 'green' => 0, 'blue' => 0, 'alpha' => 0], + imagecolorsforindex($image, imagecolorat($image, 5, 5)) + ); + + // And whitespace has expected background color + $this->assertSame( + ['red' => 250, 'green' => 250, 'blue' => 255, 'alpha' => 0], + imagecolorsforindex($image, imagecolorat($image, 15, 5)) + ); + unlink($screenshotPath); + rmdir(dirname($screenshotPath)); + } + + /** + * @covers ::getStatus + * @covers \Facebook\WebDriver\Remote\RemoteStatus + * @group exclude-saucelabs + * Status endpoint is not supported on Sauce Labs + */ + public function testShouldGetRemoteEndStatus(): void + { + $status = $this->driver->getStatus(); + + $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 d4614a431..6a21cb36c 100644 --- a/tests/functional/RemoteWebElementTest.php +++ b/tests/functional/RemoteWebElementTest.php @@ -1,22 +1,11 @@ -driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $elementWithSimpleText = $this->driver->findElement(WebDriverBy::id('text-simple')); $elementWithTextWithSpaces = $this->driver->findElement(WebDriverBy::id('text-with-spaces')); $this->assertEquals('Foo bar text', $elementWithSimpleText->getText()); + $this->assertEquals('Multiple spaces are stripped', $elementWithTextWithSpaces->getText()); } /** * @covers ::getAttribute */ - public function testShouldGetAttributeValue() + public function testShouldGetAttributeValue(): void { - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $element = $this->driver->findElement(WebDriverBy::id('text-simple')); $this->assertSame('note', $element->getAttribute('role')); $this->assertSame('height: 5em; border: 1px solid black;', $element->getAttribute('style')); $this->assertSame('text-simple', $element->getAttribute('id')); + $this->assertNull($element->getAttribute('notExisting')); + } + + /** + * @covers ::getDomProperty + */ + public function testShouldGetDomPropertyValue(): void + { + self::skipForJsonWireProtocol(); + + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); + + $element = $this->driver->findElement(WebDriverBy::id('div-with-html')); + + $this->assertStringContainsString( + '

This div has some more html inside.

', + $element->getDomProperty('innerHTML') + ); + $this->assertSame('foo bar', $element->getDomProperty('className')); // IDL property + $this->assertSame('foo bar', $element->getAttribute('class')); // HTML attribute should be the same + $this->assertSame('DIV', $element->getDomProperty('tagName')); + $this->assertSame(2, $element->getDomProperty('childElementCount')); + $this->assertNull($element->getDomProperty('notExistingProperty')); } /** * @covers ::getLocation */ - public function testShouldGetLocation() + public function testShouldGetLocation(): void { - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $element = $this->driver->findElement(WebDriverBy::id('element-with-location')); $elementLocation = $element->getLocation(); $this->assertInstanceOf(WebDriverPoint::class, $elementLocation); $this->assertSame(33, $elementLocation->getX()); - $this->assertSame(500, $elementLocation->getY()); + $this->assertSame(550, $elementLocation->getY()); + } + + /** + * @covers ::getLocationOnScreenOnceScrolledIntoView + */ + public function testShouldGetLocationOnScreenOnceScrolledIntoView(): void + { + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); + + $element = $this->driver->findElement(WebDriverBy::id('element-out-of-viewport')); + + // Location before scrolling into view is out of viewport + $elementLocation = $element->getLocation(); + $this->assertInstanceOf(WebDriverPoint::class, $elementLocation); + $this->assertSame(33, $elementLocation->getX()); + $this->assertSame(5000, $elementLocation->getY()); + + // Location once scrolled into view + $elementLocationOnceScrolledIntoView = $element->getLocationOnScreenOnceScrolledIntoView(); + $this->assertInstanceOf(WebDriverPoint::class, $elementLocationOnceScrolledIntoView); + $this->assertSame(33, $elementLocationOnceScrolledIntoView->getX()); + $this->assertLessThan( + 1000, // screen size is ~768, so this should be less + $elementLocationOnceScrolledIntoView->getY() + ); } /** * @covers ::getSize */ - public function testShouldGetSize() + public function testShouldGetSize(): void { - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $element = $this->driver->findElement(WebDriverBy::id('element-with-location')); @@ -85,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')); @@ -96,7 +136,7 @@ public function testShouldGetCssValue() $this->assertSame('none', $elementWithoutBorder->getCSSValue('border-left-style')); // Browser could report color in either rgb (like MS Edge) or rgba (like everyone else) - $this->assertRegExp( + $this->assertMatchesRegularExpression( '/rgba?\(0, 0, 0(, 1)?\)/', $elementWithBorder->getCSSValue('border-left-color') ); @@ -105,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')); @@ -117,24 +157,89 @@ public function testShouldGetTagName() /** * @covers ::click */ - public function testShouldClick() + public function testShouldClick(): void { - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $linkElement = $this->driver->findElement(WebDriverBy::id('a-form')); $linkElement->click(); $this->driver->wait()->until( - WebDriverExpectedCondition::urlContains('form.html') + WebDriverExpectedCondition::urlContains(TestPage::FORM) ); + + $this->assertTrue(true); // To generate coverage, see https://github.com/sebastianbergmann/phpunit/issues/3016 + } + + /** + * This test checks that the workarounds in place for https://github.com/mozilla/geckodriver/issues/653 work as + * expected where child links can be clicked. + * + * @covers ::click + * @covers ::clickChildElement + * @group exclude-chrome + * @group exclude-edge + */ + public function testGeckoDriverShouldClickOnBlockLevelElement(): void + { + self::skipForUnmatchedBrowsers(['firefox']); + + $links = [ + 'a-index-plain', + 'a-index-block-child', + 'a-index-block-child-hidden', + 'a-index-second-child-hidden', + ]; + + foreach ($links as $linkid) { + $this->driver->get($this->getTestPageUrl(TestPage::GECKO_653)); + $linkElement = $this->driver->findElement(WebDriverBy::id($linkid)); + + $linkElement->click(); + $this->assertStringContainsString('index.html', $this->driver->getCurrentUrl()); + } + } + + /** + * This test checks that the workarounds in place for https://github.com/mozilla/geckodriver/issues/653 work as + * expected where child links cannot be clicked, and that appropriate exceptions are thrown. + * + * @covers ::click + * @covers ::clickChildElement + * @group exclude-chrome + * @group exclude-edge + */ + public function testGeckoDriverShouldClickNotInteractable(): void + { + self::skipForUnmatchedBrowsers(['firefox']); + + $this->driver->get($this->getTestPageUrl(TestPage::GECKO_653)); + + $linkElement = $this->driver->findElement(WebDriverBy::id('a-index-plain-hidden')); + + try { + $linkElement->click(); + $this->fail('No exception was thrown when clicking an inaccessible link'); + } catch (ElementNotInteractableException $e) { + $this->assertInstanceOf(ElementNotInteractableException::class, $e); + } + + $linkElement = $this->driver->findElement(WebDriverBy::id('a-index-hidden-block-child')); + + try { + $linkElement->click(); + $this->fail('No exception was thrown when clicking an inaccessible link'); + } catch (ElementNotInteractableException $e) { + $this->assertInstanceOf(ElementNotInteractableException::class, $e); + } } /** * @covers ::clear */ - public function testShouldClearFormElementText() + public function testShouldClearFormElementText(): void { - $this->driver->get($this->getTestPageUrl('form.html')); + $this->driver->get($this->getTestPageUrl(TestPage::FORM)); $input = $this->driver->findElement(WebDriverBy::id('input-text')); $textarea = $this->driver->findElement(WebDriverBy::id('textarea')); @@ -151,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')); @@ -164,19 +269,50 @@ public function testShouldSendKeysToFormElement() $input->sendKeys(' baz'); $this->assertSame('foo bar baz', $input->getAttribute('value')); + $input->clear(); + $input->sendKeys([WebDriverKeys::SHIFT, 'H', WebDriverKeys::NULL, 'ello']); + $this->assertSame('Hello', $input->getAttribute('value')); + $textarea->clear(); $textarea->sendKeys('foo bar'); $this->assertSame('foo bar', $textarea->getAttribute('value')); $textarea->sendKeys(' baz'); $this->assertSame('foo bar baz', $textarea->getAttribute('value')); + + $textarea->clear(); + $textarea->sendKeys([WebDriverKeys::SHIFT, 'H', WebDriverKeys::NULL, 'ello']); + $this->assertSame('Hello', $textarea->getAttribute('value')); + + // Send keys as array + $textarea->clear(); + $textarea->sendKeys(['bat', 1, '3', ' ', 3, '7']); + $this->assertSame('bat13 37', $textarea->getAttribute('value')); + } + + /** + * @covers ::isDisplayed + * @covers \Facebook\WebDriver\Remote\RemoteWebDriver::execute + * @covers \Facebook\WebDriver\Support\IsElementDisplayedAtom + */ + public function testShouldDetectElementDisplayedness(): void + { + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); + + $visibleElement = $this->driver->findElement(WebDriverBy::cssSelector('.test_class')); + $elementOutOfViewport = $this->driver->findElement(WebDriverBy::id('element-out-of-viewport')); + $hiddenElement = $this->driver->findElement(WebDriverBy::id('hidden-element')); + + $this->assertTrue($visibleElement->isDisplayed()); + $this->assertTrue($elementOutOfViewport->isDisplayed()); + $this->assertFalse($hiddenElement->isDisplayed()); } /** * @covers ::isEnabled */ - public function testShouldDetectEnabledInputs() + public function testShouldDetectEnabledInputs(): void { - $this->driver->get($this->getTestPageUrl('form.html')); + $this->driver->get($this->getTestPageUrl(TestPage::FORM)); $inputEnabled = $this->driver->findElement(WebDriverBy::id('input-text')); $inputDisabled = $this->driver->findElement(WebDriverBy::id('input-text-disabled')); @@ -188,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]') @@ -216,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')); @@ -234,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')); @@ -252,9 +388,9 @@ public function testShouldSubmitFormBySubmitEventOnFormInputElement() /** * @covers ::click */ - public function testShouldSubmitFormByClickOnSubmitInput() + public function testShouldSubmitFormByClickOnSubmitInput(): void { - $this->driver->get($this->getTestPageUrl('form.html')); + $this->driver->get($this->getTestPageUrl(TestPage::FORM)); $submitElement = $this->driver->findElement(WebDriverBy::id('submit')); @@ -270,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')); @@ -289,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')); @@ -310,28 +446,96 @@ public function testShouldFindChildElementIfExistsOnAPage() $this->assertSame('First', $childElement->getText()); } - public function testShouldReturnEmptyArrayIfChildElementsCannotBeFound() + public function testShouldReturnEmptyArrayIfChildElementsCannotBeFound(): void { - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $element = $this->driver->findElement(WebDriverBy::cssSelector('ul.list')); $childElements = $element->findElements(WebDriverBy::cssSelector('not_existing')); - $this->assertInternalType('array', $childElements); + $this->assertIsArray($childElements); $this->assertCount(0, $childElements); } - public function testShouldFindMultipleChildElements() + public function testShouldFindMultipleChildElements(): void { - $this->driver->get($this->getTestPageUrl('index.html')); + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); $element = $this->driver->findElement(WebDriverBy::cssSelector('ul.list')); $allElements = $this->driver->findElements(WebDriverBy::cssSelector('li')); $childElements = $element->findElements(WebDriverBy::cssSelector('li')); - $this->assertInternalType('array', $childElements); + $this->assertIsArray($childElements); $this->assertCount(5, $allElements); // there should be 5
  • elements on page $this->assertCount(3, $childElements); // but we should find only subelements of one
      $this->assertContainsOnlyInstancesOf(RemoteWebElement::class, $childElements); } + + /** + * @covers ::takeElementScreenshot + * @covers \Facebook\WebDriver\Support\ScreenshotHelper + * @group exclude-saucelabs + */ + public function testShouldTakeAndSaveElementScreenshot(): void + { + self::skipForJsonWireProtocol('Take element screenshot is only part of W3C protocol'); + + if (!extension_loaded('gd')) { + $this->markTestSkipped('GD extension must be enabled'); + } + + // When running this test on real devices, it has a retina display so 5px will be converted into 10px + $isCi = (new CiDetector())->isCiDetected(); + $isSafari = getenv('BROWSER_NAME') === 'safari'; + + $screenshotPath = sys_get_temp_dir() . '/' . uniqid('php-webdriver-') . '/element-screenshot.png'; + + $this->driver->get($this->getTestPageUrl(TestPage::INDEX)); + + $element = $this->driver->findElement(WebDriverBy::id('red-box')); + + $outputPngString = $element->takeElementScreenshot($screenshotPath); + + // Assert file output + $imageFromFile = imagecreatefrompng($screenshotPath); + + if ($isSafari && !$isCi) { + $this->assertEquals(10, imagesx($imageFromFile)); + $this->assertEquals(10, imagesy($imageFromFile)); + } else { + $this->assertEquals(5, imagesx($imageFromFile)); + $this->assertEquals(5, imagesy($imageFromFile)); + } + + // Validate element is actually red + $this->assertSame( + ['red' => 255, 'green' => 0, 'blue' => 0, 'alpha' => 0], + imagecolorsforindex($imageFromFile, imagecolorat($imageFromFile, 0, 0)) + ); + + // Assert string output + $imageFromString = imagecreatefromstring($outputPngString); + 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)); + $this->assertEquals(10, imagesy($imageFromString)); + } else { + $this->assertEquals(5, imagesx($imageFromString)); + $this->assertEquals(5, imagesy($imageFromString)); + } + + // Validate element is actually red + $this->assertSame( + ['red' => 255, 'green' => 0, 'blue' => 0, 'alpha' => 0], + imagecolorsforindex($imageFromString, imagecolorat($imageFromString, 0, 0)) + ); + + unlink($screenshotPath); + rmdir(dirname($screenshotPath)); + } } diff --git a/tests/functional/ReportSauceLabsStatusListener.php b/tests/functional/ReportSauceLabsStatusListener.php index 8d25b76ff..1a9077807 100644 --- a/tests/functional/ReportSauceLabsStatusListener.php +++ b/tests/functional/ReportSauceLabsStatusListener.php @@ -1,26 +1,14 @@ -driver instanceof RemoteWebDriver) { return; @@ -44,39 +32,70 @@ public function endTest(\PHPUnit_Framework_Test $test, $time) ); $data = [ - 'passed' => ($testStatus === \PHPUnit_Runner_BaseTestRunner::STATUS_PASSED), + 'passed' => ($testStatus === \PHPUnit\Runner\BaseTestRunner::STATUS_PASSED), 'custom-data' => ['message' => $test->getStatusMessage()], ]; $this->submitToSauceLabs($endpointUrl, $data); } - /** - * @param int $testStatus - * @return bool - */ - private function testWasSkippedOrIncomplete($testStatus) + public function addError(\PHPUnit\Framework\Test $test, Throwable $t, float $time): void { - if ($testStatus === \PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED - || $testStatus === \PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE) { + } + + public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $w, float $time): void + { + } + + public function addFailure( + \PHPUnit\Framework\Test $test, + \PHPUnit\Framework\AssertionFailedError $e, + float $time + ): void { + } + + public function addIncompleteTest(\PHPUnit\Framework\Test $test, Throwable $t, float $time): void + { + } + + public function addRiskyTest(\PHPUnit\Framework\Test $test, Throwable $t, float $time): void + { + } + + public function addSkippedTest(\PHPUnit\Framework\Test $test, Throwable $t, float $time): void + { + } + + public function startTestSuite(\PHPUnit\Framework\TestSuite $suite): void + { + } + + public function endTestSuite(\PHPUnit\Framework\TestSuite $suite): void + { + } + + public function startTest(\PHPUnit\Framework\Test $test): void + { + } + + private function testWasSkippedOrIncomplete(int $testStatus): bool + { + if ($testStatus === \PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED + || $testStatus === \PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE) { return true; } return false; } - /** - * @param string $url - * @param array $data - */ - private function submitToSauceLabs($url, array $data) + private function submitToSauceLabs(string $url, array $data): void { $curl = curl_init($url); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT'); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($curl, CURLOPT_USERPWD, getenv('SAUCE_USERNAME') . ':' . getenv('SAUCE_ACCESS_KEY')); - curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data, JSON_THROW_ON_ERROR)); // Disable sending 'Expect: 100-Continue' header, as it is causing issues with eg. squid proxy curl_setopt($curl, CURLOPT_HTTPHEADER, ['Expect:']); diff --git a/tests/functional/RetrieveEventsTrait.php b/tests/functional/RetrieveEventsTrait.php new file mode 100644 index 000000000..addaf1c47 --- /dev/null +++ b/tests/functional/RetrieveEventsTrait.php @@ -0,0 +1,33 @@ +retrieveLoggerEvents(WebDriverBy::id('keyboardEventsLog')); + } + + private function retrieveLoggedMouseEvents(): array + { + return $this->retrieveLoggerEvents(WebDriverBy::id('mouseEventsLog')); + } + + /** + * @return false|string[] + */ + private function retrieveLoggerEvents(WebDriverBy $by) + { + $logElement = $this->driver->findElement($by); + + $text = trim($logElement->getText()); + + return array_map('trim', explode("\n", $text)); + } +} 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')); - } + use RetrieveEventsTrait; - /** - * @covers ::__construct - * @covers ::click - * @covers ::perform - */ - public function testShouldClickOnElement() + public function testShouldClickOnElement(): void { - if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) { - $this->markTestSkipped('Not supported by HtmlUnit browser'); - } + $this->driver->get($this->getTestPageUrl(TestPage::EVENTS)); $element = $this->driver->findElement(WebDriverBy::id('item-1')); @@ -46,23 +35,21 @@ public function testShouldClickOnElement() ->click($element) ->perform(); - $this->assertSame( - ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1'], - $this->retrieveLoggedEvents() - ); + $logs = ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1']; + $loggedEvents = $this->retrieveLoggedMouseEvents(); + + if (getenv('GECKODRIVER') === '1') { + $loggedEvents = array_slice($loggedEvents, 0, count($logs)); + // Firefox sometimes triggers some extra events + // it's not related to Geckodriver, it's Firefox's own behavior + } + + $this->assertSame($logs, $loggedEvents); } - /** - * @covers ::__construct - * @covers ::clickAndHold - * @covers ::release - * @covers ::perform - */ - public function testShouldClickAndHoldOnElementAndRelease() + public function testShouldClickAndHoldOnElementAndRelease(): void { - if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) { - $this->markTestSkipped('Not supported by HtmlUnit browser'); - } + $this->driver->get($this->getTestPageUrl(TestPage::EVENTS)); $element = $this->driver->findElement(WebDriverBy::id('item-1')); @@ -71,34 +58,35 @@ public function testShouldClickAndHoldOnElementAndRelease() ->release() ->perform(); - $this->assertSame( - ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1'], - $this->retrieveLoggedEvents() - ); + if (self::isW3cProtocolBuild()) { + $this->assertContains('mouseover item-1', $this->retrieveLoggedMouseEvents()); + $this->assertContains('mousedown item-1', $this->retrieveLoggedMouseEvents()); + } else { + $this->assertSame( + ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1'], + $this->retrieveLoggedMouseEvents() + ); + } } /** - * @covers ::__construct - * @covers ::contextClick - * @covers ::perform + * @group exclude-saucelabs */ - public function testShouldContextClickOnElement() + public function testShouldContextClickOnElement(): void { - if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) { - $this->markTestSkipped('Not supported by HtmlUnit browser'); - } - if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::MICROSOFT_EDGE) { $this->markTestSkipped('Getting stuck in EdgeDriver'); } + $this->driver->get($this->getTestPageUrl(TestPage::EVENTS)); + $element = $this->driver->findElement(WebDriverBy::id('item-2')); $this->driver->action() ->contextClick($element) ->perform(); - $loggedEvents = $this->retrieveLoggedEvents(); + $loggedEvents = $this->retrieveLoggedMouseEvents(); $this->assertContains('mousedown item-2', $loggedEvents); $this->assertContains('mouseup item-2', $loggedEvents); @@ -106,15 +94,12 @@ public function testShouldContextClickOnElement() } /** - * @covers ::__construct - * @covers ::doubleClick - * @covers ::perform + * @group exclude-safari + * https://github.com/webdriverio/webdriverio/issues/231 */ - public function testShouldDoubleClickOnElement() + public function testShouldDoubleClickOnElement(): void { - if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) { - $this->markTestSkipped('Not supported by HtmlUnit browser'); - } + $this->driver->get($this->getTestPageUrl(TestPage::EVENTS)); $element = $this->driver->findElement(WebDriverBy::id('item-3')); @@ -122,16 +107,154 @@ public function testShouldDoubleClickOnElement() ->doubleClick($element) ->perform(); - $this->assertContains('dblclick item-3', $this->retrieveLoggedEvents()); + $this->assertContains('dblclick item-3', $this->retrieveLoggedMouseEvents()); } /** - * @return array + * @group exclude-saucelabs */ - private function retrieveLoggedEvents() + public function testShouldSendKeysUpAndDown(): void { - $logElement = $this->driver->findElement(WebDriverBy::id('log')); + $this->driver->get($this->getTestPageUrl(TestPage::EVENTS)); + + $this->driver->action() + ->keyDown(null, WebDriverKeys::CONTROL) + ->keyUp(null, WebDriverKeys::CONTROL) + ->sendKeys(null, 'ab') + ->perform(); - return explode("\n", $logElement->getText()); + $events = $this->retrieveLoggedKeyboardEvents(); + + $this->assertEquals( + [ + 'keydown "Control"', + 'keyup "Control"', + 'keydown "a"', + 'keyup "a"', + 'keydown "b"', + 'keyup "b"', + ], + $events + ); + } + + /** + * @group exclude-safari + * https://developer.apple.com/forums/thread/662677 + */ + public function testShouldMoveToElement(): void + { + $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')); + + $this->driver->action() + ->clickAndHold($item13) + ->moveToElement($item24) + ->release() + ->perform(); + + $this->assertSame( + [['1-1', '1-2', '1-4', '1-5'], ['2-1', '2-2', '2-3', '2-4', '1-3', '2-5']], + $this->retrieveListContent() + ); + } + + /** + * @group exclude-safari + * https://developer.apple.com/forums/thread/662677 + */ + public function testShouldMoveByOffset(): void + { + $this->driver->get($this->getTestPageUrl(TestPage::SORTABLE)); + + $item13 = $this->driver->findElement(WebDriverBy::id('item-1-3')); + + $this->driver->action() + ->clickAndHold($item13) + ->moveByOffset(100, 55) + ->release() + ->perform(); + + $this->assertSame( + [['1-1', '1-2', '1-4', '1-5'], ['2-1', '2-2', '2-3', '2-4', '1-3', '2-5']], + $this->retrieveListContent() + ); + } + + /** + * @group exclude-safari + * https://developer.apple.com/forums/thread/662677 + * @group exclude-saucelabs + */ + public function testShouldDragAndDrop(): void + { + $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')); + + $this->driver->action() + ->dragAndDrop($item13, $item24) + ->perform(); + + $this->assertSame( + [['1-1', '1-2', '1-4', '1-5'], ['2-1', '2-2', '2-3', '2-4', '1-3', '2-5']], + $this->retrieveListContent() + ); + + $item21 = $this->driver->findElement(WebDriverBy::id('item-2-1')); + + $this->driver->action() + ->dragAndDrop($item24, $item21) + ->perform(); + + $this->assertSame( + [['1-1', '1-2', '1-4', '1-5'], ['2-4', '2-1', '2-2', '2-3', '1-3', '2-5']], + $this->retrieveListContent() + ); + } + + /** + * @group exclude-safari + * 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(): void + { + $this->driver->get($this->getTestPageUrl(TestPage::SORTABLE)); + + $item13 = $this->driver->findElement(WebDriverBy::id('item-1-3')); + + $this->driver->action() + ->dragAndDropBy($item13, 100, 55) + ->perform(); + + $this->assertSame( + [['1-1', '1-2', '1-4', '1-5'], ['2-1', '2-2', '2-3', '2-4', '1-3', '2-5']], + $this->retrieveListContent() + ); + + $item25 = $this->driver->findElement(WebDriverBy::id('item-2-5')); + $item22 = $this->driver->findElement(WebDriverBy::id('item-2-2')); + + $this->driver->action() + ->dragAndDropBy($item25, 0, -130) + ->dragAndDropBy($item22, -100, -35) + ->perform(); + + $this->assertSame( + [['1-1', '2-2', '1-2', '1-4', '1-5'], ['2-1', '2-5', '2-3', '2-4', '1-3']], + $this->retrieveListContent() + ); + } + + private function retrieveListContent(): array + { + return [ + $this->retrieveLoggerEvents(WebDriverBy::cssSelector('#sortable1')), + $this->retrieveLoggerEvents(WebDriverBy::cssSelector('#sortable2')), + ]; } } diff --git a/tests/functional/WebDriverAlertTest.php b/tests/functional/WebDriverAlertTest.php index 163bee157..0d2e92a44 100644 --- a/tests/functional/WebDriverAlertTest.php +++ b/tests/functional/WebDriverAlertTest.php @@ -1,45 +1,27 @@ -markTestSkipped('Alerts not yet supported by headless Chrome'); - } - parent::setUp(); - $this->driver->get($this->getTestPageUrl('alert.html')); + $this->driver->get($this->getTestPageUrl(TestPage::ALERT)); } - public function testShouldAcceptAlert() + public function testShouldAcceptAlert(): void { - // Open alert - $this->driver->findElement(WebDriverBy::id('open-alert'))->click(); + // 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(); // Wait until present $this->driver->wait()->until(WebDriverExpectedCondition::alertIsPresent()); @@ -48,17 +30,17 @@ public function testShouldAcceptAlert() $this->driver->switchTo()->alert()->accept(); - $this->expectException(NoAlertOpenException::class); + if (self::isW3cProtocolBuild()) { + $this->expectException(NoSuchAlertException::class); + } else { + $this->expectException(NoAlertOpenException::class); + } + $this->driver->switchTo()->alert()->accept(); } - public function testShouldAcceptAndDismissConfirmation() + public function testShouldAcceptAndDismissConfirmation(): void { - if ($this->desiredCapabilities->getBrowserName() == WebDriverBrowserType::HTMLUNIT) { - /** @see https://github.com/SeleniumHQ/htmlunit-driver/issues/14 */ - $this->markTestSkipped('Not supported by HtmlUnit browser'); - } - // Open confirmation $this->driver->findElement(WebDriverBy::id('open-confirm'))->click(); @@ -79,13 +61,8 @@ public function testShouldAcceptAndDismissConfirmation() $this->assertSame('dismissed', $this->getResultText()); } - public function testShouldSubmitPromptText() + public function testShouldSubmitPromptText(): void { - if ($this->desiredCapabilities->getBrowserName() == WebDriverBrowserType::HTMLUNIT) { - /** @see https://github.com/SeleniumHQ/htmlunit-driver/issues/14 */ - $this->markTestSkipped('Not supported by HtmlUnit browser'); - } - // Open confirmation $this->driver->findElement(WebDriverBy::id('open-prompt'))->click(); @@ -100,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 028db7920..2aadf8de9 100644 --- a/tests/functional/WebDriverByTest.php +++ b/tests/functional/WebDriverByTest.php @@ -1,17 +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); @@ -52,7 +35,10 @@ public function testShouldFindTextElementByLocator( } } - public function textElementsProvider() + /** + * @return array[] + */ + 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 48b72a53c..64372a4c8 100644 --- a/tests/functional/WebDriverCheckboxesTest.php +++ b/tests/functional/WebDriverCheckboxesTest.php @@ -1,36 +1,24 @@ -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"]')) @@ -39,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"]')) @@ -48,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"]')) @@ -59,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"]')) @@ -68,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"]')) @@ -77,7 +65,7 @@ public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociate $this->assertEquals('j5c', $checkboxes->getFirstSelectedOption()->getAttribute('value')); } - public function testSelectByValue() + public function testSelectByValue(): void { $selectedOptions = ['j2b', 'j2c']; @@ -95,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"]')) @@ -106,7 +94,7 @@ public function testSelectByValueInvalid() $checkboxes->selectByValue('notexist'); } - public function testSelectByIndex() + public function testSelectByIndex(): void { $selectedOptions = [1 => 'j2b', 2 => 'j2c']; @@ -124,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"]')) @@ -136,12 +124,9 @@ public function testSelectByIndexInvalid() } /** - * @dataProvider selectByVisibleTextDataProvider - * - * @param string $text - * @param string $value + * @dataProvider provideSelectByVisibleTextData */ - public function testSelectByVisibleText($text, $value) + public function testSelectByVisibleText(string $text, string $value): void { $checkboxes = new WebDriverCheckboxes( $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) @@ -153,9 +138,9 @@ public function testSelectByVisibleText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisibleTextDataProvider() + public function provideSelectByVisibleTextData(): array { return [ ['J 2 B', 'j2b'], @@ -164,12 +149,9 @@ public function selectByVisibleTextDataProvider() } /** - * @dataProvider selectByVisiblePartialTextDataProvider - * - * @param string $text - * @param string $value + * @dataProvider provideSelectByVisiblePartialTextData */ - public function testSelectByVisiblePartialText($text, $value) + public function testSelectByVisiblePartialText(string $text, string $value): void { $checkboxes = new WebDriverCheckboxes( $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) @@ -181,9 +163,9 @@ public function testSelectByVisiblePartialText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisiblePartialTextDataProvider() + public function provideSelectByVisiblePartialTextData(): array { return [ ['2 B', 'j2b'], @@ -191,7 +173,7 @@ public function selectByVisiblePartialTextDataProvider() ]; } - public function testDeselectAll() + public function testDeselectAll(): void { $checkboxes = new WebDriverCheckboxes( $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) @@ -203,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"]')) @@ -215,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"]')) @@ -227,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"]')) @@ -239,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 d3aaebcb6..400ef86f0 100644 --- a/tests/functional/WebDriverNavigationTest.php +++ b/tests/functional/WebDriverNavigationTest.php @@ -1,17 +1,4 @@ -driver->navigate()->to($this->getTestPageUrl('index.html')); + $this->driver->navigate()->to($this->getTestPageUrl(TestPage::INDEX)); $this->assertStringEndsWith('/index.html', $this->driver->getCurrentURL()); } @@ -35,36 +22,38 @@ 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 } /** * @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 new file mode 100644 index 000000000..a71ebafc0 --- /dev/null +++ b/tests/functional/WebDriverOptionsCookiesTest.php @@ -0,0 +1,94 @@ +driver->get($this->getTestPageUrl(TestPage::INDEX)); + } + + public function testShouldSetGetAndDeleteCookies(): void + { + $cookie1 = new Cookie('cookie1', 'cookie1Value'); + $cookie2 = new Cookie('cookie2', 'cookie2Value'); + + // Verify initial state - no cookies are present + $this->assertSame([], $this->driver->manage()->getCookies()); + + // Add cookie1 + $this->driver->manage()->addCookie($cookie1); + + // get all cookies + $cookiesWithOneCookie = $this->driver->manage()->getCookies(); + $this->assertCount(1, $cookiesWithOneCookie); + $this->assertContainsOnlyInstancesOf(Cookie::class, $cookiesWithOneCookie); + $this->assertSame('cookie1', $cookiesWithOneCookie[0]->getName()); + $this->assertSame('cookie1Value', $cookiesWithOneCookie[0]->getValue()); + $this->assertSame('/', $cookiesWithOneCookie[0]->getPath()); + $this->assertSame('localhost', $cookiesWithOneCookie[0]->getDomain()); + + // Add cookie2 + $this->driver->manage()->addCookie($cookie2); + + // get all cookies + $cookiesWithTwoCookies = $this->driver->manage()->getCookies(); + + $this->assertCount(2, $cookiesWithTwoCookies); + $this->assertContainsOnlyInstancesOf(Cookie::class, $cookiesWithTwoCookies); + + // normalize received cookies (their order is arbitrary) + $normalizedCookies = [ + $cookiesWithTwoCookies[0]->getName() => $cookiesWithTwoCookies[0]->getValue(), + $cookiesWithTwoCookies[1]->getName() => $cookiesWithTwoCookies[1]->getValue(), + ]; + ksort($normalizedCookies); + $this->assertSame(['cookie1' => 'cookie1Value', 'cookie2' => 'cookie2Value'], $normalizedCookies); + + // getCookieNamed() + $onlyCookieOne = $this->driver->manage()->getCookieNamed('cookie1'); + $this->assertInstanceOf(Cookie::class, $onlyCookieOne); + $this->assertSame('cookie1', $onlyCookieOne->getName()); + $this->assertSame('cookie1Value', $onlyCookieOne->getValue()); + + // deleteCookieNamed() + $this->driver->manage()->deleteCookieNamed('cookie1'); + $cookiesWithOnlySecondCookie = $this->driver->manage()->getCookies(); + $this->assertCount(1, $cookiesWithOnlySecondCookie); + $this->assertSame('cookie2', $cookiesWithOnlySecondCookie[0]->getName()); + + // getting non-existent cookie should throw an exception in W3C mode but return null in JsonWire mode + if (self::isW3cProtocolBuild()) { + try { + $noSuchCookieExceptionThrown = false; + $this->driver->manage()->getCookieNamed('cookie1'); + } catch (NoSuchCookieException $e) { + $noSuchCookieExceptionThrown = true; + } finally { + $this->assertTrue($noSuchCookieExceptionThrown, 'NoSuchCookieException was not thrown'); + } + } else { + $this->assertNull($this->driver->manage()->getCookieNamed('cookie1')); + } + + // deleting non-existent cookie shod not throw an error + $this->driver->manage()->deleteCookieNamed('cookie1'); + + // Add cookie3 + $this->driver->manage()->addCookie($cookie1); + $this->assertCount(2, $this->driver->manage()->getCookies()); + + // Delete all cookies + $this->driver->manage()->deleteAllCookies(); + + $this->assertSame([], $this->driver->manage()->getCookies()); + } +} diff --git a/tests/functional/WebDriverRadiosTest.php b/tests/functional/WebDriverRadiosTest.php index 4d659772b..f35d37a7c 100644 --- a/tests/functional/WebDriverRadiosTest.php +++ b/tests/functional/WebDriverRadiosTest.php @@ -1,17 +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 = []; @@ -49,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"]'))); @@ -58,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"]')) @@ -74,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'); @@ -85,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"]'))); @@ -94,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); @@ -104,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"]'))); @@ -114,12 +102,9 @@ public function testSelectByIndexInvalid() } /** - * @dataProvider selectByVisibleTextDataProvider - * - * @param string $text - * @param string $value + * @dataProvider provideSelectByVisibleTextData */ - 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); @@ -127,9 +112,9 @@ public function testSelectByVisibleText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisibleTextDataProvider() + public function provideSelectByVisibleTextData(): array { return [ ['J 3 B', 'j3b'], @@ -138,12 +123,9 @@ public function selectByVisibleTextDataProvider() } /** - * @dataProvider selectByVisiblePartialTextDataProvider - * - * @param string $text - * @param string $value + * @dataProvider provideSelectByVisiblePartialTextData */ - 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); @@ -151,9 +133,9 @@ public function testSelectByVisiblePartialText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisiblePartialTextDataProvider() + public function provideSelectByVisiblePartialTextData(): array { return [ ['3 B', 'j3b'], @@ -161,7 +143,7 @@ public function selectByVisiblePartialTextDataProvider() ]; } - public function testDeselectAllRadio() + public function testDeselectAllRadio(): void { $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); @@ -170,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"]'))); @@ -179,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"]'))); @@ -188,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"]'))); @@ -197,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 b25dd03af..a5640b1be 100644 --- a/tests/functional/WebDriverSelectTest.php +++ b/tests/functional/WebDriverSelectTest.php @@ -1,17 +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); @@ -48,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')); @@ -58,10 +57,9 @@ public function testShouldThrowExceptionWhenNotInstantiatedOnSelectElement() } /** - * @dataProvider selectSelectorProvider - * @param string $selector + * @dataProvider provideSelectSelector */ - public function testShouldGetOptionsOfSelect($selector) + public function testShouldGetOptionsOfSelect(string $selector): void { $originalElement = $this->driver->findElement(WebDriverBy::cssSelector($selector)); $select = new WebDriverSelect($originalElement); @@ -72,7 +70,10 @@ public function testShouldGetOptionsOfSelect($selector) $this->assertCount(5, $options); } - public function selectSelectorProvider() + /** + * @return array[] + */ + public function provideSelectSelector(): array { return [ 'simple + +
      +