├── .editorconfig ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── bin ├── start_webserver.sh ├── browser │ ├── chrome.sh │ ├── msedge.sh │ └── firefox.sh └── start_driver.sh ├── tests ├── Custom │ ├── WebDriverTest.php │ ├── PromptTest.php │ ├── DesiredCapabilitiesTest.php │ ├── WindowNameTest.php │ ├── TimeoutTest.php │ └── KeyboardWriteTest.php ├── ScreenshotListener.php └── WebDriverConfig.php ├── LICENSE ├── CHANGELOG.md ├── README.md ├── composer.json ├── src ├── WebDriverFactory.php └── WebDriver.php └── phpunit.xml.dist /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 4 4 | 5 | [{*.yml, *.yaml}] 6 | indent_size = 2 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.phar 3 | composer.lock 4 | phpunit.xml 5 | chromedriver/ 6 | geckodriver/ 7 | msedgedriver/ 8 | logs/ 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | version: 2 3 | updates: 4 | - package-ecosystem: "composer" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday 8 | interval: "weekly" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | # Check for updates to GitHub Actions every weekday 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /bin/start_webserver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | # see https://github.com/minkphp/driver-testsuite/pull/28 6 | export USE_ZEND_ALLOC=0 7 | php -S localhost:8002 -t ./vendor/mink/driver-testsuite/web-fixtures 8 | 9 | WEBSERVER_PID=$! 10 | 11 | ATTEMPT=0 12 | until $(echo | nc localhost 8002); do 13 | if [ $ATTEMPT -gt 5 ]; then 14 | echo "Failed to php server driver" 15 | cat ./logs/mink-test-server.log 16 | exit 1; 17 | fi; 18 | sleep 1; 19 | echo waiting for PHP server on port 8002...; 20 | ATTEMPT=$((ATTEMPT + 1)) 21 | done; 22 | echo "PHP server started" 23 | -------------------------------------------------------------------------------- /tests/Custom/WebDriverTest.php: -------------------------------------------------------------------------------- 1 | getSession(); 13 | $session->start(); 14 | /** @var WebDriver $driver */ 15 | $driver = $session->getDriver(); 16 | $this->assertNotEmpty($driver->getWebDriverSessionId(), 'Started session has an ID'); 17 | 18 | $driver = new WebDriver(); 19 | $this->assertNull($driver->getWebDriverSessionId(), 'Not started session don\'t have an ID'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bin/browser/chrome.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | MACHINE_FAMILY=$1 6 | DRIVER_VERSION=$2 7 | 8 | if [[ "$DRIVER_VERSION" == "latest" ]]; then 9 | DRIVER_VERSION=$(curl -sS https://chromedriver.storage.googleapis.com/LATEST_RELEASE) 10 | fi 11 | 12 | mkdir -p chromedriver 13 | 14 | if [[ $MACHINE_FAMILY == "windows" ]]; then 15 | PLATFORM="win32" 16 | fi 17 | 18 | if [[ $MACHINE_FAMILY == "linux" ]]; then 19 | PLATFORM="linux64" 20 | fi 21 | 22 | if [[ $MACHINE_FAMILY == "mac" ]]; then 23 | PLATFORM="mac64" 24 | fi 25 | 26 | wget -q -t 3 "https://chromedriver.storage.googleapis.com/${DRIVER_VERSION}/chromedriver_${PLATFORM}.zip" -O driver.zip 27 | unzip -qo driver.zip -d chromedriver/ 28 | 29 | ./chromedriver/chromedriver --port=4444 --verbose --enable-chrome-logs --whitelisted-ips= 30 | -------------------------------------------------------------------------------- /bin/browser/msedge.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | MACHINE_FAMILY=$1 6 | DRIVER_VERSION=$2 7 | 8 | if [[ "$DRIVER_VERSION" == "latest" ]]; then 9 | DRIVER_VERSION=$(curl -sS https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver/LATEST_STABLE -o - | cut -b3-23) 10 | fi 11 | 12 | mkdir -p msedgedriver 13 | 14 | if [[ $MACHINE_FAMILY == "windows" ]]; then 15 | PLATFORM="win64" 16 | fi 17 | 18 | if [[ $MACHINE_FAMILY == "linux" ]]; then 19 | PLATFORM="linux64" 20 | fi 21 | 22 | if [[ $MACHINE_FAMILY == "mac" ]]; then 23 | PLATFORM="mac64" 24 | fi 25 | 26 | wget -q -t 3 "https://msedgedriver.azureedge.net/${DRIVER_VERSION}/edgedriver_${PLATFORM}.zip" -O driver.zip 27 | unzip -qo driver.zip -d msedgedriver/ 28 | 29 | ./msedgedriver/msedgedriver --port=4444 --verbose --enable-chrome-logs --whitelisted-ips= 30 | -------------------------------------------------------------------------------- /bin/start_driver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | usage() { 6 | echo "Usage: ./bin/start_driver.sh [version]" 7 | } 8 | 9 | CURRENT_DIR=$(dirname $0) 10 | 11 | if [[ -z "$1" ]]; then 12 | usage; 13 | exit 1; 14 | else 15 | BROWSER_NAME="$1" 16 | fi 17 | 18 | if [[ -n "$2" ]]; then 19 | DRIVER_VERSION="$2" 20 | else 21 | DRIVER_VERSION="latest" 22 | fi 23 | 24 | mkdir -p ./logs 25 | rm -rf ./logs/* 26 | 27 | UNAME=$(uname -s) 28 | 29 | case "$UNAME" in 30 | *NT*) MACHINE_FAMILY=windows ;; 31 | Linux*) MACHINE_FAMILY=linux ;; 32 | Darwin*) MACHINE_FAMILY=mac ;; 33 | esac 34 | 35 | if [ -z "$BROWSER_NAME" ]; then 36 | echo "Environment variable BROWSER_NAME must be defined" 37 | exit 1 38 | fi 39 | 40 | if [[ ! -f "$CURRENT_DIR/browser/$BROWSER_NAME.sh" || ! -x "$CURRENT_DIR/browser/$BROWSER_NAME.sh" ]]; then 41 | echo "File '$CURRENT_DIR/browser/$BROWSER_NAME.sh' does not exists or is not executable" 42 | exit 1 43 | fi; 44 | 45 | exec "$CURRENT_DIR/browser/$BROWSER_NAME.sh" "$MACHINE_FAMILY" "$DRIVER_VERSION" 46 | -------------------------------------------------------------------------------- /bin/browser/firefox.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | MACHINE_FAMILY=$1 6 | DRIVER_VERSION=$2 7 | 8 | if [[ "$DRIVER_VERSION" == "latest" ]]; then 9 | DRIVER_VERSION=$(curl -sS https://api.github.com/repos/mozilla/geckodriver/releases/latest | grep -E -o 'tag_name([^,]+)' | tr -d \" | tr -d " " | cut -d':' -f2) 10 | fi 11 | 12 | mkdir -p geckodriver 13 | 14 | EXTENSION="tar.gz" 15 | 16 | if [[ $MACHINE_FAMILY == "windows" ]]; then 17 | PLATFORM="win64" 18 | EXTENSION="zip" 19 | fi 20 | 21 | if [[ $MACHINE_FAMILY == "linux" ]]; then 22 | PLATFORM="linux64" 23 | fi 24 | 25 | if [[ $MACHINE_FAMILY == "mac" ]]; then 26 | PLATFORM="macos" 27 | fi 28 | 29 | wget -q -t 3 "https://github.com/mozilla/geckodriver/releases/download/${DRIVER_VERSION}/geckodriver-$DRIVER_VERSION-${PLATFORM}.${EXTENSION}" -O "driver.${EXTENSION}" 30 | 31 | if [[ "$EXTENSION" == "tar.gz" ]]; then 32 | tar -xf driver.tar.gz -C ./geckodriver/; 33 | else 34 | unzip -qo driver.zip -d geckodriver/ 35 | fi; 36 | 37 | ./geckodriver/geckodriver --host 127.0.0.1 -vv --port 4444 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Oleg Andreyev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.0.1] - 2021-01-31 8 | ### Fixed 9 | - Clear the Firefox window handle cache when starting a session 10 | 11 | ## [1.0.0] - 2021-01-16 12 | ### Added 13 | - Initial implementation as a hard fork from https://github.com/minkphp/MinkSelenium2Driver 14 | - Using .editorconfig 15 | - Using GitHub Action instead of Travis CI 16 | - Using Dependabot 17 | - Using own fork of driver-testsuite 18 | - Added Screenshot listener (for unit tests) 19 | - Added LICENCE file 20 | - Prompt support 21 | - Workaround for https://github.com/mozilla/geckodriver/issues/149 22 | - Workaround for https://github.com/mozilla/geckodriver/issues/653 23 | - Workaround for https://github.com/mozilla/geckodriver/issues/1816 24 | - Handling `input[type=time]`, `input[type=date]` and `input[type=color]` 25 | 26 | [Unreleased]: https://github.com/oleg-andreyev/MinkPhpWebDriver/compare/v1.0.1...HEAD 27 | [1.0.1]: https://github.com/oleg-andreyev/MinkPhpWebDriver/compare/v1.0.0...v1.0.1 28 | [1.0.0]: https://github.com/oleg-andreyev/MinkPhpWebDriver/compare/07b0f6be5c4ec82b041b62b99bd48786a4373ad0...v1.0.0 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MinkPhpWebDriver 2 | ================================= 3 | 4 | Initially it's started as [PR](https://github.com/minkphp/MinkSelenium2Driver/pull/304) to MinkSelenium2Driver 5 | 6 | Major updates include: 7 | - Switch to using `facebook/webdriver` 8 | - Update minimum php version to 7.2 9 | - Tested against the latest Google Chrome and Mozilla Firefox both in GUI and Headless modes 10 | 11 | ## Setup 12 | 13 | Install via `oleg-andreyev/mink-phpwebdriver-extension` 14 | ```bash 15 | $ composer require --dev oleg-andreyev/mink-phpwebdriver-extension 16 | ``` 17 | 18 | Add this extension to your `behat.yml` (see below) 19 | 20 | - Set the wd_host to this server instead 21 | ```yaml 22 | default: 23 | extensions: 24 | OAndreyev\MinkPhpWebdriverExtension: ~ 25 | Behat\MinkExtension: 26 | default_session: webdriver 27 | webdriver: 28 | wd_host: "http://0.0.0.0:4444/wd/hub" 29 | browser: 'chrome' 30 | ``` 31 | ## Testing 32 | 33 | ```bash 34 | $ ./bin/start_webdriver.sh & 35 | #./bin/start_driver.sh 36 | $ ./bin/start_driver.sh chrome latest & 37 | $ BROWSER_NAME=chrome ./vendor/bin/simple-phpunit 38 | ``` 39 | 40 | This will download the latest driver for specified browser and will execute phpunit 41 | 42 | ## Copyright 43 | 44 | Copyright (c) 2019 Oleg Andreyev 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oleg-andreyev/mink-phpwebdriver", 3 | "description": "MinkPhpWebDriver driver for Mink framework", 4 | "keywords": [ 5 | "webdriver", 6 | "w3c", 7 | "jsonwire", 8 | "javascript", 9 | "ajax", 10 | "testing", 11 | "browser" 12 | ], 13 | "type": "mink-driver", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Oleg Andreyev", 18 | "email": "oleg@andreyev.lv", 19 | "role": "Main Developer" 20 | } 21 | ], 22 | "repositories": [ 23 | { 24 | "type": "vcs", 25 | "url": "https://github.com/oleg-andreyev/driver-testsuite.git", 26 | "no-api": true 27 | } 28 | ], 29 | "require": { 30 | "php": ">=7.2", 31 | "behat/mink": "^1.8", 32 | "php-webdriver/webdriver": "^1.8" 33 | }, 34 | "require-dev": { 35 | "ext-json": "*", 36 | "roave/security-advisories": "dev-master", 37 | "mink/driver-testsuite": "dev-integration-branch", 38 | "behat/mink-extension": "^2.3", 39 | "symfony/phpunit-bridge": "^5.2" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "OAndreyev\\Mink\\Driver\\": "src/" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "OAndreyev\\Mink\\Tests\\Driver\\": "tests" 49 | } 50 | }, 51 | "extra": { 52 | "branch-alias": { 53 | "dev-master": "1.0.x-dev" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/WebDriverFactory.php: -------------------------------------------------------------------------------- 1 | guessCapabilities(), $extraCapabilities, $config['capabilities']); 27 | 28 | // Build driver definition 29 | return new Definition(WebDriver::class, [ 30 | $config['browser'], 31 | $capabilities, 32 | $config['wd_host'], 33 | ]); 34 | } 35 | 36 | /** 37 | * Guess capabilities from environment 38 | * 39 | * @return array 40 | */ 41 | protected function guessCapabilities() 42 | { 43 | if (getenv('CI')) { 44 | return [ 45 | 'tunnel-identifier' => getenv('GITHUB_RUN_ID'), 46 | 'build' => getenv('GITHUB_RUN_NUMBER'), 47 | 'tags' => ['GitHub Actions', 'PHP ' . PHP_VERSION], 48 | ]; 49 | } 50 | 51 | return [ 52 | 'tags' => [php_uname('n'), 'PHP ' . PHP_VERSION], 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Custom/PromptTest.php: -------------------------------------------------------------------------------- 1 | getSession(); 13 | $session->visit($this->pathTo('/prompt.html')); 14 | /** @var WebDriver $driver */ 15 | $driver = $session->getDriver(); 16 | 17 | $alert = $driver->getCurrentPromptOrAlert(); 18 | self::assertEquals('Can you handle this?', $alert->getText()); 19 | 20 | // @see https://bugs.chromium.org/p/chromedriver/issues/detail?id=1120#c11 21 | $alert->sendKeys('yes'); 22 | $alert->accept(); 23 | 24 | $element = $session->getPage()->find('css', '#prompt_result'); 25 | self::assertEquals('Prompt Result: yes', $element->getText()); 26 | } 27 | 28 | public function testPromptDismiss() 29 | { 30 | $session = $this->getSession(); 31 | $session->visit($this->pathTo('/prompt.html')); 32 | /** @var WebDriver $driver */ 33 | $driver = $session->getDriver(); 34 | 35 | $alert = $driver->getCurrentPromptOrAlert(); 36 | self::assertEquals('Can you handle this?', $alert->getText()); 37 | 38 | // @see https://bugs.chromium.org/p/chromedriver/issues/detail?id=1120#c11 39 | $alert->sendKeys('yes'); 40 | $alert->dismiss(); 41 | 42 | $element = $session->getPage()->find('css', '#prompt_result'); 43 | self::assertNull($element); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/ScreenshotListener.php: -------------------------------------------------------------------------------- 1 | makeScreenshot($test); 19 | } 20 | 21 | public function addWarning(Test $test, Warning $e, float $time): void 22 | { 23 | } 24 | 25 | public function addFailure(Test $test, AssertionFailedError $e, float $time): void 26 | { 27 | $this->makeScreenshot($test); 28 | } 29 | 30 | public function addIncompleteTest(Test $test, \Throwable $t, float $time): void 31 | { 32 | } 33 | 34 | public function addRiskyTest(Test $test, \Throwable $t, float $time): void 35 | { 36 | } 37 | 38 | public function addSkippedTest(Test $test, \Throwable $t, float $time): void 39 | { 40 | } 41 | 42 | public function startTestSuite(TestSuite $suite): void 43 | { 44 | } 45 | 46 | public function endTestSuite(TestSuite $suite): void 47 | { 48 | } 49 | 50 | public function startTest(Test $test): void 51 | { 52 | } 53 | 54 | public function endTest(Test $test, float $time): void 55 | { 56 | } 57 | 58 | private function makeScreenshot(Test $test): void 59 | { 60 | /** @var Session $session */ 61 | $session = \Closure::bind(function () { 62 | /** @var TestCase $this */ 63 | return $this->getSession(); 64 | }, $test, $test)(); 65 | 66 | if (!$session->isStarted()) { 67 | return; 68 | } 69 | 70 | $filename = str_replace(['#', ' ', '.', ',', '"', '\''], '_', $test->getName()); 71 | $session->getDriver()->getScreenshot(getcwd() . '/logs/' . $filename . '.png'); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Custom/DesiredCapabilitiesTest.php: -------------------------------------------------------------------------------- 1 | 'firefox', 16 | 'version' => '30', 17 | 'platform' => 'ANY', 18 | 'browserVersion' => '30', 19 | 'browser' => 'firefox', 20 | 'name' => 'Selenium2 Mink Driver Test', 21 | 'deviceOrientation' => 'portrait', 22 | 'deviceType' => 'tablet', 23 | 'selenium-version' => '2.45.0' 24 | ); 25 | 26 | $driver = new WebDriver('firefox', $expectedCaps); 27 | $this->assertNotEmpty($driver->getDesiredCapabilities(), 'desiredCapabilities empty'); 28 | $this->assertInstanceOf(DesiredCapabilities::class, $driver->getDesiredCapabilities()); 29 | $toArray = $driver->getDesiredCapabilities()->toArray(); 30 | foreach ($expectedCaps as $key => $v) { 31 | $this->assertEquals($expectedCaps[$key], $toArray[$key]); 32 | } 33 | } 34 | 35 | public function testSetDesiredCapabilities() 36 | { 37 | $this->expectException(DriverException::class); 38 | $this->expectExceptionMessage('Unable to set desiredCapabilities, the session has already started'); 39 | 40 | $caps = array( 41 | 'browserName' => 'firefox', 42 | 'version' => '30', 43 | 'platform' => 'ANY', 44 | 'browserVersion' => '30', 45 | 'browser' => 'firefox', 46 | 'name' => 'Selenium2 Mink Driver Test', 47 | 'deviceOrientation' => 'portrait', 48 | 'deviceType' => 'tablet', 49 | 'selenium-version' => '2.45.0' 50 | ); 51 | $session = $this->getSession(); 52 | $session->start(); 53 | 54 | /** @var WebDriver $driver */ 55 | $driver = $session->getDriver(); 56 | $driver->setDesiredCapabilities($caps); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Custom/WindowNameTest.php: -------------------------------------------------------------------------------- 1 | getSession(); 12 | $session->start(); 13 | 14 | $windowNames = $session->getWindowNames(); 15 | $this->assertArrayHasKey(0, $windowNames); 16 | 17 | $windowName = $session->getWindowName(); 18 | 19 | $this->assertIsString($windowName); 20 | $this->assertContains($windowName, $windowNames, 'The current window name is one of the available window names.'); 21 | } 22 | 23 | public function testReopenWindow() 24 | { 25 | $this->getSession()->visit($this->pathTo('/window.html')); 26 | $session = $this->getSession(); 27 | $page = $session->getPage(); 28 | $webAssert = $this->getAssertSession(); 29 | 30 | $page->clickLink('Popup #1'); 31 | $session->switchToWindow('popup_1'); 32 | $el = $webAssert->elementExists('css', '#text'); 33 | $this->assertSame('Popup#1 div text', $el->getText()); 34 | 35 | $session->executeScript('window.close();'); 36 | 37 | $session->switchToWindow(null); 38 | 39 | $page->clickLink('Popup #1'); 40 | $session->switchToWindow('popup_1'); 41 | $el = $webAssert->elementExists('css', '#text'); 42 | $this->assertSame('Popup#1 div text', $el->getText()); 43 | } 44 | 45 | public function testSwitchWindowAfterReset() 46 | { 47 | $session = $this->getSession(); 48 | $page = $session->getPage(); 49 | $webAssert = $this->getAssertSession(); 50 | 51 | $session->restart(); 52 | $session->visit($this->pathTo('/window.html')); 53 | $page->clickLink('Popup #1'); 54 | $session->switchToWindow('popup_1'); 55 | $el = $webAssert->elementExists('css', '#text'); 56 | $this->assertSame('Popup#1 div text', $el->getText()); 57 | 58 | $session->restart(); 59 | $session->visit($this->pathTo('/window.html')); 60 | $page->clickLink('Popup #2'); 61 | $session->switchToWindow('popup_2'); 62 | $el = $webAssert->elementExists('css', '#text'); 63 | $this->assertSame('Popup#2 div text', $el->getText()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | tests 19 | vendor/mink/driver-testsuite/tests 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ./src 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | env: 10 | DRIVER_URL: "http://localhost:4444" 11 | WEB_FIXTURES_HOST: "http://localhost:8002" 12 | 13 | defaults: 14 | run: 15 | shell: bash 16 | 17 | jobs: 18 | tests: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | php: [ 7.2, 7.3, 7.4, 8.0 ] 24 | browser: [ chrome, firefox ] 25 | experimental: [false] 26 | include: 27 | - php: 8.1 28 | experimental: true 29 | browser: chrome 30 | continue-on-error: ${{ matrix.experimental }} 31 | timeout-minutes: 10 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v2 35 | 36 | - name: Setup PHP 37 | uses: shivammathur/setup-php@2.12.0 38 | with: 39 | php-version: ${{ matrix.php }} 40 | coverage: none 41 | extensions: zip, :xdebug 42 | tools: composer 43 | 44 | - name: Determine composer cache directory 45 | id: composer-cache 46 | run: echo "::set-output name=directory::$(composer config cache-dir)" 47 | 48 | - name: Cache composer dependencies 49 | uses: actions/cache@v2.1.6 50 | with: 51 | path: ${{ steps.composer-cache.outputs.directory }} 52 | key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} 53 | restore-keys: ${{ matrix.php }}-composer- 54 | 55 | - name: Install dependencies 56 | run: | 57 | echo "::group::apt-get install" 58 | sudo apt-get install xvfb 59 | echo "::endgroup::" 60 | 61 | echo "::group::composer update" 62 | composer update --no-interaction --prefer-dist 63 | echo "::endgroup::" 64 | 65 | echo "::group::composer require symfony/phpunit-bridge" 66 | composer require symfony/phpunit-bridge 67 | echo "::endgroup::" 68 | 69 | - name: Start webdriver 70 | shell: bash 71 | run: | 72 | mkdir -p ./logs 73 | if [[ "${{ matrix.browser }}" == "chrome" ]]; then 74 | xvfb-run --auto-servernum $CHROMEWEBDRIVER/chromedriver --port=4444 --verbose --whitelisted-ips= &> ./logs/webdriver.log & 75 | else 76 | xvfb-run --auto-servernum $GECKOWEBDRIVER/geckodriver --host 127.0.0.1 -vv --port 4444 &>./logs/webdriver.log & 77 | fi; 78 | 79 | - name: Start webserver 80 | shell: bash 81 | run: | 82 | export USE_ZEND_ALLOC=0 83 | mkdir -p ./logs 84 | php -S localhost:8002 -t ./vendor/mink/driver-testsuite/web-fixtures &> ./logs/mink-test-server.log & 85 | 86 | - name: Run tests 87 | run: | 88 | export BROWSER_NAME="${{ matrix.browser }}" 89 | ./vendor/bin/simple-phpunit -v 90 | 91 | - name: Archive logs artifacts 92 | if: ${{ failure() }} 93 | uses: actions/upload-artifact@v2 94 | with: 95 | name: logs_browser-${{ matrix.browser }}_php-${{ matrix.php }} 96 | path: | 97 | logs 98 | -------------------------------------------------------------------------------- /tests/Custom/TimeoutTest.php: -------------------------------------------------------------------------------- 1 | session = $this->getSession(); 26 | $this->driver = $this->session->getDriver(); 27 | } 28 | 29 | protected function tearDown(): void 30 | { 31 | // https://developer.mozilla.org/en-US/docs/Web/WebDriver/Commands/SetTimeouts 32 | $this->driver->setTimeouts(array('implicit' => 0, 'pageLoad' => 300000, 'script' => 30000)); 33 | } 34 | 35 | 36 | public function testInvalidTimeoutSettingThrowsException() 37 | { 38 | $this->expectException(DriverException::class); 39 | $this->session->start(); 40 | $this->driver->setTimeouts(array('invalid' => 0)); 41 | } 42 | 43 | public function testShortTimeoutDoesNotWaitForElementToAppear() 44 | { 45 | $this->driver->setTimeouts(array('implicit' => 0)); 46 | 47 | $this->session->visit($this->pathTo('/js_test.html')); 48 | $this->findById('waitable')->click(); 49 | 50 | $element = $this->session->getPage()->find('css', '#waitable > div'); 51 | 52 | $this->assertNull($element); 53 | } 54 | 55 | public function testLongTimeoutWaitsForElementToAppear() 56 | { 57 | $this->driver->setTimeouts(array('implicit' => 5000)); 58 | 59 | $this->session->visit($this->pathTo('/js_test.html')); 60 | $this->findById('waitable')->click(); 61 | $element = $this->session->getPage()->find('css', '#waitable > div'); 62 | 63 | $this->assertNotNull($element); 64 | } 65 | 66 | public function testPageLoadTimeout() 67 | { 68 | $this->expectException(DriverException::class); 69 | $this->driver->setTimeouts(array('pageLoad' => 1)); 70 | $this->session->visit($this->pathTo('/page_load.php?sleep=2')); 71 | } 72 | 73 | public function testPageReloadTimeout() 74 | { 75 | $this->expectException(DriverException::class); 76 | $this->session->visit($this->pathTo('/page_load.php?sleep=2')); 77 | $this->driver->setTimeouts(array('pageLoad' => 1)); 78 | $this->session->reload(); 79 | } 80 | 81 | public function testScriptTimeout() 82 | { 83 | $this->expectException(DriverException::class); 84 | $this->driver->setTimeouts(array('script' => 1)); 85 | $this->session->visit($this->pathTo('/js_test.html')); 86 | 87 | // @see https://w3c.github.io/webdriver/#execute-async-script 88 | $this->driver->executeAsyncScript( 89 | 'var callback = arguments[arguments.length - 1]; 90 | setTimeout( 91 | function(){ 92 | callback(); 93 | }, 94 | 2000 95 | );' 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Custom/KeyboardWriteTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('\OAndreyev\Mink\Tests\Driver\Custom\KeyboardWriteTest::testKeyboardEvents is skipped due to weird output from Firefox'); 17 | } 18 | 19 | $this->getSession()->visit($this->pathTo('/keyboard_test.html')); 20 | $webAssert = $this->getAssertSession(); 21 | 22 | $input = $webAssert->elementExists('css', '#test-target'); 23 | $event = $webAssert->elementExists('css', '#console-log'); 24 | 25 | $input->setValue($inputValue); 26 | $result = $input->getValue(); 27 | $consoleLog = $event->getHtml(); 28 | 29 | $this->assertEquals($inputExpected, $result); 30 | $this->assertEquals($consoleExpected, $consoleLog); 31 | } 32 | 33 | public function keyboardEventsDataProvider() 34 | { 35 | yield [ 36 | "Test", 37 | "Test", 38 | "Key \"Shift\" pressed [event: keydown] 39 | Key \"T\" pressed [event: keydown] 40 | Key \"T\" pressed and released [event: keypress] 41 | Key \"T\" input [event: input] 42 | Key \"T\" released [event: keyup] 43 | Key \"Shift\" released [event: keyup] 44 | Key \"e\" pressed [event: keydown] 45 | Key \"e\" pressed and released [event: keypress] 46 | Key \"e\" input [event: input] 47 | Key \"e\" released [event: keyup] 48 | Key \"s\" pressed [event: keydown] 49 | Key \"s\" pressed and released [event: keypress] 50 | Key \"s\" input [event: input] 51 | Key \"s\" released [event: keyup] 52 | Key \"t\" pressed [event: keydown] 53 | Key \"t\" pressed and released [event: keypress] 54 | Key \"t\" input [event: input] 55 | Key \"t\" released [event: keyup] 56 | " 57 | ]; 58 | 59 | yield [ 60 | WebDriverKeys::SHIFT . 't' . WebDriverKeys::SHIFT . 'est', 61 | "Test", 62 | "Key \"Shift\" pressed [event: keydown] 63 | Key \"T\" pressed [event: keydown] 64 | Key \"T\" pressed and released [event: keypress] 65 | Key \"T\" input [event: input] 66 | Key \"T\" released [event: keyup] 67 | Key \"Shift\" released [event: keyup] 68 | Key \"e\" pressed [event: keydown] 69 | Key \"e\" pressed and released [event: keypress] 70 | Key \"e\" input [event: input] 71 | Key \"e\" released [event: keyup] 72 | Key \"s\" pressed [event: keydown] 73 | Key \"s\" pressed and released [event: keypress] 74 | Key \"s\" input [event: input] 75 | Key \"s\" released [event: keyup] 76 | Key \"t\" pressed [event: keydown] 77 | Key \"t\" pressed and released [event: keypress] 78 | Key \"t\" input [event: input] 79 | Key \"t\" released [event: keyup] 80 | " 81 | ]; 82 | 83 | yield [ 84 | WebDriverKeys::SHIFT . '5', 85 | '%', 86 | "Key \"Shift\" pressed [event: keydown] 87 | Key \"%\" pressed [event: keydown] 88 | Key \"%\" pressed and released [event: keypress] 89 | Key \"%\" input [event: input] 90 | Key \"%\" released [event: keyup] 91 | Key \"Shift\" released [event: keyup] 92 | " 93 | ]; 94 | 95 | yield [ 96 | WebDriverKeys::SHIFT . 'test', 97 | 'TEST', 98 | "Key \"Shift\" pressed [event: keydown] 99 | Key \"T\" pressed [event: keydown] 100 | Key \"T\" pressed and released [event: keypress] 101 | Key \"T\" input [event: input] 102 | Key \"T\" released [event: keyup] 103 | Key \"E\" pressed [event: keydown] 104 | Key \"E\" pressed and released [event: keypress] 105 | Key \"E\" input [event: input] 106 | Key \"E\" released [event: keyup] 107 | Key \"S\" pressed [event: keydown] 108 | Key \"S\" pressed and released [event: keypress] 109 | Key \"S\" input [event: input] 110 | Key \"S\" released [event: keyup] 111 | Key \"T\" pressed [event: keydown] 112 | Key \"T\" pressed and released [event: keypress] 113 | Key \"T\" input [event: input] 114 | Key \"T\" released [event: keyup] 115 | Key \"Shift\" released [event: keyup] 116 | " 117 | ]; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/WebDriverConfig.php: -------------------------------------------------------------------------------- 1 | setBrowserName('msedge'); 45 | } 46 | } else { 47 | $desiredCapabilities = new DesiredCapabilities(); 48 | } 49 | 50 | $capabilityMap = [ 51 | 'firefox' => FirefoxDriver::PROFILE, 52 | 'chrome' => ChromeOptions::CAPABILITY_W3C, 53 | 'msedge' => ChromeOptions::CAPABILITY_W3C, 54 | ]; 55 | 56 | if (isset($capabilityMap[$browser])) { 57 | $optionsOrProfile = $desiredCapabilities->getCapability($capabilityMap[$browser]); 58 | if ($browser === 'chrome' || $browser === 'msedge') { 59 | if (!$optionsOrProfile) { 60 | $optionsOrProfile = new ChromeOptions(); 61 | } 62 | $optionsOrProfile = $this->buildChromeOptions($desiredCapabilities, $optionsOrProfile, $driverOptions); 63 | } else if ($browser === 'firefox') { 64 | $optionsOrProfile = $this->buildFirefoxProfile($desiredCapabilities, $optionsOrProfile, $driverOptions); 65 | } 66 | 67 | $desiredCapabilities->setCapability($capabilityMap[$browser], $optionsOrProfile); 68 | } 69 | 70 | $driver = new WebDriver($browser, [], $seleniumHost); 71 | $driver->setDesiredCapabilities($desiredCapabilities); 72 | 73 | // https://developer.mozilla.org/en-US/docs/Web/WebDriver/Commands/SetTimeouts 74 | $driver->setTimeouts(array('implicit' => 0, 'pageLoad' => 300000, 'script' => 30000)); 75 | 76 | return $this->driver = $driver; 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function skipMessage($testCase, $test) 83 | { 84 | $desiredCapabilities = $this->driver->getDesiredCapabilities(); 85 | $chromeOptions = $desiredCapabilities->getCapability(ChromeOptions::CAPABILITY_W3C); 86 | 87 | $headless = $desiredCapabilities->getBrowserName() === 'chrome' 88 | && $chromeOptions instanceof ChromeOptions 89 | && in_array('headless', $chromeOptions->toArray()['args'] ?? [], true); 90 | 91 | if ( 92 | 'Behat\Mink\Tests\Driver\Js\WindowTest' === $testCase 93 | && (0 === strpos($test, 'testWindowMaximize')) 94 | && ('true' === getenv('CI') || $headless) 95 | ) { 96 | return 'Maximizing the window does not work when running the browser in Xvfb/Headless.'; 97 | } 98 | 99 | return parent::skipMessage($testCase, $test); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | protected function supportsCss() 106 | { 107 | return true; 108 | } 109 | 110 | /** 111 | * @param ChromeOptions $optionsOrProfile 112 | * @param array $driverOptions 113 | * 114 | * @return ChromeOptions 115 | */ 116 | private function buildChromeOptions(DesiredCapabilities $desiredCapabilities, ChromeOptions $optionsOrProfile, array $driverOptions = []) 117 | { 118 | $binary = $driverOptions['binary'] ?? null; 119 | $optionsOrProfile->setBinary($binary); 120 | 121 | $args = $driverOptions['args'] ?? []; 122 | $optionsOrProfile->addArguments($args); 123 | 124 | return $optionsOrProfile; 125 | 126 | // TODO 127 | //$capability->addEncodedExtension(); 128 | //$capability->addExtension(); 129 | //$capability->addEncodedExtensions(); 130 | //$capability->addExtensions(); 131 | } 132 | 133 | /** 134 | * @param FirefoxProfile $optionsOrProfile 135 | * @param array $driverOptions 136 | * 137 | * @return FirefoxProfile 138 | * @throws WebDriverException 139 | */ 140 | private function buildFirefoxProfile(DesiredCapabilities $desiredCapabilities, FirefoxProfile $optionsOrProfile, array $driverOptions) 141 | { 142 | if (isset($driverOptions['binary'])) { 143 | $firefoxOptions = $desiredCapabilities->getCapability('moz:firefoxOptions'); 144 | if (empty($firefoxOptions)) { 145 | $firefoxOptions = []; 146 | } 147 | $firefoxOptions = array_merge($firefoxOptions, ['binary' => $driverOptions['binary']]); 148 | $desiredCapabilities->setCapability('moz:firefoxOptions', $firefoxOptions); 149 | } 150 | if (isset($driverOptions['log'])) { 151 | $firefoxOptions = $desiredCapabilities->getCapability('moz:firefoxOptions'); 152 | if (empty($firefoxOptions)) { 153 | $firefoxOptions = []; 154 | } 155 | $firefoxOptions = array_merge($firefoxOptions, ['log' => $driverOptions['log']]); 156 | $desiredCapabilities->setCapability('moz:firefoxOptions', $firefoxOptions); 157 | } 158 | if (isset($driverOptions['args'])) { 159 | $firefoxOptions = $desiredCapabilities->getCapability('moz:firefoxOptions'); 160 | if (empty($firefoxOptions)) { 161 | $firefoxOptions = []; 162 | } 163 | $firefoxOptions = array_merge($firefoxOptions, ['args' => $driverOptions['args']]); 164 | $desiredCapabilities->setCapability('moz:firefoxOptions', $firefoxOptions); 165 | } 166 | $preferences = isset($driverOptions['preference']) ? $driverOptions['preference'] : []; 167 | foreach ($preferences as $key => $preference) { 168 | $optionsOrProfile->setPreference($key, $preference); 169 | // TODO 170 | // $capability->setRdfFile($key, $preference); 171 | // $capability->addExtensionDatas($key, $preference); 172 | // $capability->addExtension($key, $preference); 173 | } 174 | return $optionsOrProfile; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/WebDriver.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace OAndreyev\Mink\Driver; 12 | 13 | use Behat\Mink\Driver\CoreDriver; 14 | use Behat\Mink\Exception\DriverException; 15 | use Behat\Mink\Exception\UnsupportedDriverActionException; 16 | use Facebook\WebDriver\Cookie; 17 | use Facebook\WebDriver\Exception\NoSuchCookieException; 18 | use Facebook\WebDriver\Exception\NoSuchElementException; 19 | use Facebook\WebDriver\Exception\ScriptTimeoutException; 20 | use Facebook\WebDriver\Exception\TimeOutException; 21 | use Facebook\WebDriver\Remote\DesiredCapabilities; 22 | use Facebook\WebDriver\Remote\LocalFileDetector; 23 | use Facebook\WebDriver\Remote\RemoteWebDriver; 24 | use Facebook\WebDriver\Remote\RemoteWebElement; 25 | use Facebook\WebDriver\WebDriverAlert; 26 | use Facebook\WebDriver\WebDriverBy; 27 | use Facebook\WebDriver\WebDriverDimension; 28 | use Facebook\WebDriver\WebDriverElement; 29 | use Facebook\WebDriver\WebDriverKeys as Keys; 30 | use Facebook\WebDriver\WebDriverRadios; 31 | use Facebook\WebDriver\WebDriverSelect; 32 | use Facebook\WebDriver\Exception\ElementNotInteractableException; 33 | 34 | /** 35 | * WebDriver driver. 36 | * 37 | * @author Oleg Andreyev 38 | */ 39 | class WebDriver extends CoreDriver 40 | { 41 | const MODIFIER_KEYS = [ 42 | Keys::SHIFT, Keys::CONTROL, Keys::ALT, Keys::META, Keys::COMMAND, 43 | Keys::LEFT_ALT, Keys::LEFT_CONTROL, Keys::LEFT_SHIFT 44 | ]; 45 | 46 | /** 47 | * The WebDriver instance 48 | * 49 | * @var RemoteWebDriver 50 | */ 51 | private $webDriver; 52 | 53 | /** 54 | * @var string 55 | */ 56 | private $browserName; 57 | 58 | /** 59 | * @var DesiredCapabilities|null 60 | */ 61 | private $desiredCapabilities; 62 | 63 | /** 64 | * The timeout configuration 65 | * 66 | * @var array 67 | */ 68 | private $timeouts = array(); 69 | 70 | /** 71 | * Wd host 72 | * 73 | * @var string 74 | */ 75 | private $wdHost; 76 | 77 | /** 78 | * @var string 79 | */ 80 | private $rootWindow; 81 | 82 | /** 83 | * @var array 84 | */ 85 | private $windows = []; 86 | 87 | /** 88 | * Instantiates the driver. 89 | * 90 | * @param string $browserName Browser name 91 | * @param array $desiredCapabilities The desired capabilities 92 | * @param string $wdHost The WebDriver host 93 | */ 94 | public function __construct($browserName = 'firefox', $desiredCapabilities = null, $wdHost = 'http://localhost:4444/wd/hub') 95 | { 96 | $this->wdHost = $wdHost; 97 | $this->browserName = $browserName; 98 | 99 | if ($browserName === 'firefox') { 100 | $this->desiredCapabilities = DesiredCapabilities::firefox(); 101 | } else if ($browserName === 'chrome') { 102 | $this->desiredCapabilities = DesiredCapabilities::chrome(); 103 | } else { 104 | $this->desiredCapabilities = new DesiredCapabilities(); 105 | } 106 | 107 | if ($desiredCapabilities) { 108 | foreach ($desiredCapabilities as $key => $val) { 109 | $this->desiredCapabilities->setCapability($key, $val); 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * Sets the timeouts to apply to the webdriver session 116 | * 117 | * @param array $timeouts The session timeout settings: Array of {script, implicit, page} => time in milliseconds 118 | */ 119 | public function setTimeouts(array $timeouts) 120 | { 121 | // TODO: driver does not have getTimeouts 122 | $this->timeouts = $timeouts; 123 | 124 | if ($this->isStarted()) { 125 | $this->applyTimeouts(); 126 | } 127 | } 128 | 129 | /** 130 | * Applies timeouts to the current session 131 | */ 132 | private function applyTimeouts() 133 | { 134 | // @see https://w3c.github.io/webdriver/#set-timeouts 135 | $timeouts = $this->webDriver->manage()->timeouts(); 136 | if (isset($this->timeouts['implicit'])) { 137 | $timeouts->implicitlyWait($this->timeouts['implicit']); 138 | } else if (isset($this->timeouts['pageLoad'])) { 139 | $timeouts->pageLoadTimeout($this->timeouts['pageLoad']); 140 | } else if (isset($this->timeouts['script'])) { 141 | $timeouts->setScriptTimeout($this->timeouts['script']); 142 | } else { 143 | throw new DriverException('Invalid timeout option'); 144 | } 145 | } 146 | 147 | /** 148 | * Sets the browser name 149 | * 150 | * @param string $browserName the name of the browser to start, default is 'firefox' 151 | */ 152 | protected function setBrowserName($browserName = 'firefox') 153 | { 154 | $this->browserName = $browserName; 155 | } 156 | 157 | /** 158 | * Sets the desired capabilities - called on construction. If null is provided, will set the 159 | * defaults as desired. 160 | * 161 | * See http://code.google.com/p/selenium/wiki/DesiredCapabilities 162 | * 163 | * @param DesiredCapabilities|array|null $desiredCapabilities 164 | * 165 | * @throws DriverException 166 | */ 167 | public function setDesiredCapabilities($desiredCapabilities = null) 168 | { 169 | if ($this->isStarted()) { 170 | throw new DriverException('Unable to set desiredCapabilities, the session has already started'); 171 | } 172 | 173 | if (is_array($desiredCapabilities)) { 174 | $desiredCapabilities = new DesiredCapabilities($desiredCapabilities); 175 | } else if ($desiredCapabilities === null) { 176 | $desiredCapabilities = new DesiredCapabilities(); 177 | } 178 | 179 | $this->desiredCapabilities = $desiredCapabilities; 180 | } 181 | 182 | /** 183 | * Gets the desiredCapabilities 184 | * 185 | * @return DesiredCapabilities 186 | */ 187 | public function getDesiredCapabilities() 188 | { 189 | return $this->desiredCapabilities; 190 | } 191 | 192 | /** 193 | * @return WebDriver 194 | */ 195 | public function getWebDriver() 196 | { 197 | return $this->webDriver; 198 | } 199 | 200 | /** 201 | * Returns the default capabilities 202 | * 203 | * @return array 204 | */ 205 | public static function getDefaultCapabilities() 206 | { 207 | return array( 208 | 'browserName' => 'firefox', 209 | 'name' => 'Behat Test', 210 | ); 211 | } 212 | 213 | /** 214 | * Executes JS on a given element - pass in a js script string and {{ELEMENT}} will 215 | * be replaced with a reference to the result of the $xpath query 216 | * 217 | * @example $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.childNodes.length'); 218 | * 219 | * @param string $xpath the xpath to search with 220 | * @param string $script the script to execute 221 | * @param Boolean $sync whether to run the script synchronously (default is TRUE) 222 | * 223 | * @return mixed 224 | */ 225 | private function executeJsOnXpath($xpath, $script, $sync = true) 226 | { 227 | $element = $this->findElement($xpath); 228 | return $this->executeJsOnElement($element, $script, $sync); 229 | } 230 | 231 | /** 232 | * Executes JS on a given element - pass in a js script string and {{ELEMENT}} will 233 | * be replaced with a reference to the element 234 | * 235 | * @example $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.childNodes.length'); 236 | * 237 | * @param WebDriverElement $element the webdriver element 238 | * @param string $script the script to execute 239 | * @param Boolean $sync whether to run the script synchronously (default is TRUE) 240 | * 241 | * @return mixed 242 | */ 243 | private function executeJsOnElement(WebDriverElement $element, $script, $sync = true) 244 | { 245 | $script = str_replace('{{ELEMENT}}', 'arguments[0]', $script); 246 | 247 | if ($sync) { 248 | return $this->webDriver->executeScript($script, [$element]); 249 | } 250 | 251 | return $this->webDriver->executeAsyncScript($script, [$element]); 252 | } 253 | 254 | /** 255 | * {@inheritdoc} 256 | */ 257 | public function start() 258 | { 259 | if ($this->webDriver) { 260 | return $this->webDriver; 261 | } 262 | 263 | try { 264 | $this->webDriver = RemoteWebDriver::create($this->wdHost, $this->desiredCapabilities); 265 | if (\count($this->timeouts)) { 266 | $this->applyTimeouts(); 267 | } 268 | $this->rootWindow = $this->webDriver->getWindowHandle(); 269 | $this->windows = []; 270 | } catch (\Exception $e) { 271 | throw new DriverException('Could not open connection: ' . $e->getMessage(), 0, $e); 272 | } 273 | 274 | if (!$this->webDriver) { 275 | throw new DriverException('Could not connect to a WebDriver server'); 276 | } 277 | } 278 | 279 | /** 280 | * {@inheritdoc} 281 | */ 282 | public function isStarted() 283 | { 284 | return $this->webDriver !== null; 285 | } 286 | 287 | /** 288 | * {@inheritdoc} 289 | */ 290 | public function stop() 291 | { 292 | if (!$this->webDriver) { 293 | throw new DriverException('Could not connect to a Selenium 2 / WebDriver server'); 294 | } 295 | 296 | try { 297 | $this->webDriver->quit(); 298 | $this->webDriver = null; 299 | } catch (\Exception $e) { 300 | throw new DriverException('Could not close connection', 0, $e); 301 | } 302 | } 303 | 304 | /** 305 | * {@inheritdoc} 306 | */ 307 | public function reset() 308 | { 309 | $this->webDriver->manage()->deleteAllCookies(); 310 | // TODO: resizeWindow does not accept NULL 311 | $this->maximizeWindow(); 312 | // reset timeout 313 | $this->timeouts = []; 314 | } 315 | 316 | /** 317 | * {@inheritdoc} 318 | */ 319 | public function visit($url) 320 | { 321 | try { 322 | $this->webDriver->navigate()->to($url); 323 | } catch (TimeOutException $e) { 324 | throw new DriverException($e->getMessage(), $e->getCode(), $e); 325 | } 326 | } 327 | 328 | /** 329 | * {@inheritdoc} 330 | */ 331 | public function getCurrentUrl() 332 | { 333 | return $this->webDriver->getCurrentURL(); 334 | } 335 | 336 | /** 337 | * {@inheritdoc} 338 | */ 339 | public function reload() 340 | { 341 | try { 342 | $this->webDriver->navigate()->refresh(); 343 | } catch (TimeOutException $e) { 344 | throw new DriverException($e->getMessage(), $e->getCode(), $e); 345 | } 346 | } 347 | 348 | /** 349 | * {@inheritdoc} 350 | */ 351 | public function forward() 352 | { 353 | $this->webDriver->navigate()->forward(); 354 | } 355 | 356 | /** 357 | * {@inheritdoc} 358 | */ 359 | public function back() 360 | { 361 | $this->webDriver->navigate()->back(); 362 | } 363 | 364 | /** 365 | * {@inheritdoc} 366 | */ 367 | public function switchToWindow($name = null) 368 | { 369 | if ($this->browserName === 'firefox') { 370 | // Firefox stores window IDs rather than window names and does not provide a working way to map the ids to 371 | // names. 372 | // Each time we switch to a window, we fetch the list of window IDs, and attempt to map them. 373 | // This involves switching to that window and fetching the window.name. 374 | // @see https://github.com/mozilla/geckodriver/issues/149 375 | $handles = []; 376 | foreach ($this->getWindowNames() as $id) { 377 | if ($id === $this->rootWindow) { 378 | // Do not put the root window into the list of handles. 379 | continue; 380 | } 381 | 382 | $title = array_search($id, $this->windows, true); 383 | if ($title !== false) { 384 | // This window is current and the name already stored. 385 | // Use the currently stored id from $this->windows to avoid switching window unnecessarily. 386 | $handles[$title] = $id; 387 | } else { 388 | // This window title is unknown. Switch to the window by ID and find the name. 389 | $this->webDriver->switchTo()->window($id); 390 | $title = $this->evaluateScript('window.name'); 391 | 392 | $handles[$title] = $id; 393 | } 394 | } 395 | 396 | // Store the window name => id mappings. 397 | $this->windows = $handles; 398 | 399 | if (null === $name) { 400 | $name = $this->rootWindow; 401 | } else if (array_key_exists($name, $this->windows)) { 402 | $name = $this->windows[$name]; 403 | } 404 | } 405 | 406 | $this->webDriver->switchTo()->window($name); 407 | } 408 | 409 | /** 410 | * {@inheritdoc} 411 | */ 412 | public function switchToIFrame($name = null) 413 | { 414 | if ($name) { 415 | $element = $this->webDriver->findElement(WebDriverBy::name($name)); 416 | $this->webDriver->switchTo()->frame($element); 417 | } else { 418 | $this->webDriver->switchTo()->defaultContent(); 419 | } 420 | } 421 | 422 | /** 423 | * {@inheritdoc} 424 | */ 425 | public function setCookie($name, $value = null) 426 | { 427 | if (null === $value) { 428 | $this->webDriver->manage()->deleteCookieNamed($name); 429 | 430 | return; 431 | } 432 | 433 | $cookie = new Cookie($name, \rawurlencode($value)); 434 | $this->webDriver->manage()->addCookie($cookie); 435 | } 436 | 437 | /** 438 | * {@inheritdoc} 439 | */ 440 | public function getCookie($name) 441 | { 442 | try { 443 | $cookie = $this->webDriver->manage()->getCookieNamed($name); 444 | } catch (NoSuchCookieException $e) { 445 | return null; 446 | } 447 | 448 | return \rawurldecode($cookie->getValue()); 449 | } 450 | 451 | /** 452 | * {@inheritdoc} 453 | */ 454 | public function getContent() 455 | { 456 | $source = $this->webDriver->getPageSource(); 457 | return str_replace(array("\r", "\r\n", "\n"), \PHP_EOL, $source); 458 | } 459 | 460 | /** 461 | * {@inheritdoc} 462 | */ 463 | public function getScreenshot($save_as = null) 464 | { 465 | return $this->webDriver->takeScreenshot($save_as); 466 | } 467 | 468 | /** 469 | * {@inheritdoc} 470 | */ 471 | public function getWindowNames() 472 | { 473 | return $this->webDriver->getWindowHandles(); 474 | } 475 | 476 | /** 477 | * {@inheritdoc} 478 | */ 479 | public function getWindowName() 480 | { 481 | return $this->webDriver->getWindowHandle(); 482 | } 483 | 484 | /** 485 | * {@inheritdoc} 486 | */ 487 | public function findElementXpaths($xpath) 488 | { 489 | $nodes = $this->webDriver->findElements(WebDriverBy::xpath($xpath)); 490 | 491 | $elements = array(); 492 | foreach ($nodes as $i => $node) { 493 | $elements[] = sprintf('(%s)[%d]', $xpath, $i + 1); 494 | } 495 | 496 | return $elements; 497 | } 498 | 499 | /** 500 | * {@inheritdoc} 501 | */ 502 | public function getTagName($xpath) 503 | { 504 | $element = $this->findElement($xpath); 505 | return $element->getTagName(); 506 | } 507 | 508 | /** 509 | * {@inheritdoc} 510 | */ 511 | public function getText($xpath) 512 | { 513 | $element = $this->findElement($xpath); 514 | $text = $element->getText(); 515 | 516 | $text = (string) str_replace(array("\r", "\r\n", "\n"), ' ', $text); 517 | 518 | return $text; 519 | } 520 | 521 | /** 522 | * {@inheritdoc} 523 | */ 524 | public function getHtml($xpath) 525 | { 526 | return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.innerHTML;'); 527 | } 528 | 529 | /** 530 | * {@inheritdoc} 531 | */ 532 | public function getOuterHtml($xpath) 533 | { 534 | return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.outerHTML;'); 535 | } 536 | 537 | /** 538 | * {@inheritdoc} 539 | */ 540 | public function getAttribute($xpath, $name) 541 | { 542 | $element = $this->findElement($xpath); 543 | 544 | /** 545 | * If attribute is present but does not have value, it's considered as Boolean Attributes https://html.spec.whatwg.org/#boolean-attributes 546 | * but here result may be unexpected in case of , my-attr should return TRUE, but it will return "empty string" 547 | * 548 | * @see https://w3c.github.io/webdriver/#get-element-attribute 549 | */ 550 | $hasAttribute = $this->hasAttribute($element, $name); 551 | if ($hasAttribute) { 552 | $value = $element->getAttribute($name); 553 | } else { 554 | $value = null; 555 | } 556 | 557 | return $value; 558 | } 559 | 560 | /** 561 | * @param WebDriverElement $element 562 | * @param string $name 563 | * 564 | * @return bool 565 | */ 566 | private function hasAttribute(WebDriverElement $element, $name) 567 | { 568 | return $this->executeJsOnElement($element, "return {{ELEMENT}}.hasAttribute('$name')"); 569 | } 570 | 571 | /** 572 | * {@inheritdoc} 573 | */ 574 | public function getValue($xpath) 575 | { 576 | $element = $this->findElement($xpath); 577 | $elementName = strtolower($element->getTagName()); 578 | $elementType = strtolower($element->getAttribute('type')); 579 | 580 | // Getting the value of a checkbox returns its value if selected. 581 | if ('input' === $elementName && 'checkbox' === $elementType) { 582 | return $element->isSelected() ? $element->getAttribute('value') : null; 583 | } 584 | 585 | if ('input' === $elementName && 'radio' === $elementType) { 586 | $radios = new WebDriverRadios($element); 587 | try { 588 | return $radios->getFirstSelectedOption()->getAttribute('value'); 589 | } catch (NoSuchElementException $e) { 590 | // TODO: Need to distinguish missing element and no radio selected 591 | if ($e->getMessage() === 'No radio buttons are selected') { 592 | return null; 593 | } 594 | 595 | throw $e; 596 | } 597 | } 598 | 599 | // Using $element->attribute('value') on a select only returns the first selected option 600 | // even when it is a multiple select, so a custom retrieval is needed. 601 | if ('select' === $elementName) { 602 | $select = new WebDriverSelect($element); 603 | if ($select->isMultiple()) { 604 | return \array_map(function (WebDriverElement $element) { 605 | return $element->getAttribute('value'); 606 | }, $select->getAllSelectedOptions()); 607 | } 608 | 609 | try { 610 | return $select->getFirstSelectedOption()->getAttribute('value'); 611 | } catch (NoSuchElementException $e) { 612 | // TODO: Need to distinguish missing element and no option selected 613 | if ($e->getMessage() === 'No options are selected') { 614 | return ''; 615 | } 616 | 617 | throw $e; 618 | } 619 | } 620 | 621 | return $element->getAttribute('value'); 622 | } 623 | 624 | /** 625 | * {@inheritdoc} 626 | */ 627 | public function setValue($xpath, $value) 628 | { 629 | $element = $this->findElement($xpath); 630 | $elementName = strtolower($element->getTagName()); 631 | 632 | if ('select' === $elementName) { 633 | $select = new WebDriverSelect($element); 634 | 635 | if (is_array($value)) { 636 | $select->deselectAll(); 637 | foreach ($value as $option) { 638 | $select->selectByValue($option); 639 | } 640 | 641 | return; 642 | } 643 | 644 | $select->selectByValue($value); 645 | 646 | return; 647 | } 648 | 649 | if ('input' === $elementName) { 650 | $elementType = strtolower($element->getAttribute('type')); 651 | 652 | if (in_array($elementType, array('submit', 'image', 'button', 'reset'))) { 653 | throw new DriverException(sprintf('Impossible to set value an element with XPath "%s" as it is not a select, textarea or textbox', $xpath)); 654 | } 655 | 656 | if ('checkbox' === $elementType) { 657 | if ($element->isSelected() xor (bool) $value) { 658 | $this->clickOnElement($element); 659 | } 660 | 661 | return; 662 | } 663 | 664 | if ('radio' === $elementType) { 665 | $radios = new WebDriverRadios($element); 666 | $radios->selectByValue($value); 667 | return; 668 | } 669 | 670 | if ('file' === $elementType) { 671 | $this->attachFile($xpath, $value); 672 | return; 673 | } 674 | 675 | // WebDriver does not support setting value in color inputs. 676 | // Each OS will show native color picker 677 | // See https://code.google.com/p/selenium/issues/detail?id=7650 678 | if ('color' === $elementType) { 679 | $this->executeJsOnElement($element, sprintf('return {{ELEMENT}}.value = "%s"', $value)); 680 | return; 681 | } 682 | 683 | // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement 684 | if ('date' === $elementType || 'time' === $elementType) { 685 | $date = date(DATE_ATOM, strtotime($value)); 686 | $this->executeJsOnElement($element, sprintf('return {{ELEMENT}}.valueAsDate = new Date("%s")', $date)); 687 | return; 688 | } 689 | } 690 | 691 | $value = (string) $value; 692 | 693 | if (in_array($elementName, array('input', 'textarea'))) { 694 | $existingValueLength = strlen($element->getAttribute('value')); 695 | // Add the TAB key to ensure we unfocus the field as browsers are triggering the change event only 696 | // after leaving the field. 697 | $value = str_repeat(Keys::BACKSPACE . Keys::DELETE, $existingValueLength) . $value; 698 | } 699 | 700 | $element->sendKeys($value); 701 | 702 | // Trigger a change event. 703 | $script = <<executeJsOnXpath($xpath, $script); 711 | } 712 | 713 | /** 714 | * {@inheritdoc} 715 | */ 716 | public function check($xpath) 717 | { 718 | $element = $this->findElement($xpath); 719 | $this->ensureInputType($element, $xpath, 'checkbox', 'check'); 720 | 721 | if ($element->isSelected()) { 722 | return; 723 | } 724 | 725 | $this->clickOnElement($element); 726 | } 727 | 728 | /** 729 | * {@inheritdoc} 730 | */ 731 | public function uncheck($xpath) 732 | { 733 | $element = $this->findElement($xpath); 734 | $this->ensureInputType($element, $xpath, 'checkbox', 'uncheck'); 735 | 736 | if (!$element->isSelected()) { 737 | return; 738 | } 739 | 740 | $this->clickOnElement($element); 741 | } 742 | 743 | /** 744 | * {@inheritdoc} 745 | */ 746 | public function isChecked($xpath) 747 | { 748 | return $this->isSelected($xpath); 749 | } 750 | 751 | /** 752 | * {@inheritdoc} 753 | */ 754 | public function selectOption($xpath, $value, $multiple = false) 755 | { 756 | $element = $this->findElement($xpath); 757 | $tagName = strtolower($element->getTagName()); 758 | 759 | if ('input' === $tagName && 'radio' === strtolower($element->getAttribute('type'))) { 760 | $element = new WebDriverRadios($element); 761 | $element->selectByValue($value); 762 | return; 763 | } 764 | 765 | if ('select' === $tagName) { 766 | $element = new WebDriverSelect($element); 767 | if (!$multiple && $element->isMultiple()) { 768 | $element->deselectAll(); 769 | } 770 | 771 | try { 772 | $element->selectByValue($value); 773 | } catch (NoSuchElementException $e) { 774 | // option may not have value attribute, so try to select by visible text 775 | $element->selectByVisibleText($value); 776 | } 777 | 778 | return; 779 | } 780 | 781 | throw new DriverException(sprintf('Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', $xpath)); 782 | } 783 | 784 | /** 785 | * {@inheritdoc} 786 | */ 787 | public function isSelected($xpath) 788 | { 789 | $element = $this->findElement($xpath); 790 | return $element->isSelected(); 791 | } 792 | 793 | /** 794 | * {@inheritdoc} 795 | */ 796 | public function click($xpath) 797 | { 798 | $element = $this->findElement($xpath); 799 | $this->clickOnElement($element); 800 | } 801 | 802 | /** 803 | * Attempt to ensure that the node is in the viewport. 804 | * 805 | * @param WebDriverElement $element 806 | */ 807 | private function scrollElementIntoViewIfRequired(WebDriverElement $element) 808 | { 809 | $js = <<executeJsOnElement($element, $js); 820 | } 821 | 822 | private function clickOnElement(WebDriverElement $element) 823 | { 824 | if ($this->browserName === 'firefox') { 825 | // TODO: Raise a bug against geckodrvier. 826 | // Firefox does not move cursor over an element in breach of https://w3c.github.io/webdriver/#element-click 827 | // section 8.Otherwise. 828 | $this->scrollElementIntoViewIfRequired($element); 829 | $this->mouseOverElement($element); 830 | } 831 | 832 | if ($this->browserName === 'firefox') { 833 | try { 834 | $element->click(); 835 | } catch (ElementNotInteractableException $e) { 836 | // There is a bug in Geckodriver which means that it is unable to click any link which contains a block 837 | // level node. See https://github.com/mozilla/geckodriver/issues/653. 838 | // The workaround is to click on a descendant node instead. 839 | $children = $element->findElements(WebDriverBy::xpath('./*')); 840 | foreach ($children as $child) { 841 | // Call ourselves recursively surpressing the same ElementNotInteractableException exception until 842 | // we run out of potential children to click. 843 | try { 844 | $this->clickOnElement($child); 845 | return; 846 | } catch (ElementNotInteractableException $e) { 847 | } 848 | } 849 | 850 | throw $e; 851 | } 852 | } else { 853 | $element->click(); 854 | } 855 | } 856 | 857 | /** 858 | * {@inheritdoc} 859 | */ 860 | public function doubleClick($xpath) 861 | { 862 | $element = $this->findElement($xpath); 863 | $this->webDriver->action()->doubleClick($element)->perform(); 864 | } 865 | 866 | /** 867 | * {@inheritdoc} 868 | */ 869 | public function rightClick($xpath) 870 | { 871 | $element = $this->findElement($xpath); 872 | $this->webDriver->action()->contextClick($element)->perform(); 873 | } 874 | 875 | /** 876 | * {@inheritdoc} 877 | */ 878 | public function attachFile($xpath, $path) 879 | { 880 | $element = $this->findElement($xpath); 881 | $this->ensureInputType($element, $xpath, 'file', 'attach a file on'); 882 | 883 | $element->setFileDetector(new LocalFileDetector()); 884 | return $element->sendKeys($path); 885 | } 886 | 887 | /** 888 | * {@inheritdoc} 889 | */ 890 | public function isVisible($xpath) 891 | { 892 | $element = $this->findElement($xpath); 893 | return $element->isDisplayed(); 894 | } 895 | 896 | /** 897 | * {@inheritdoc} 898 | */ 899 | public function mouseOver($xpath) 900 | { 901 | $element = $this->findElement($xpath); 902 | $this->webDriver->action()->moveToElement($element)->perform(); 903 | } 904 | 905 | private function mouseOverElement(WebDriverElement $element) 906 | { 907 | $this->webDriver->action()->moveToElement($element)->perform(); 908 | } 909 | 910 | /** 911 | * {@inheritdoc} 912 | */ 913 | public function focus($xpath) 914 | { 915 | $element = $this->findElement($xpath); 916 | $action = $this->webDriver->action(); 917 | 918 | $action->moveToElement($element)->perform(); 919 | 920 | // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus 921 | $this->executeJsOnElement($element, 'return {{ELEMENT}}.focus()'); 922 | } 923 | 924 | /** 925 | * {@inheritdoc} 926 | */ 927 | public function blur($xpath) 928 | { 929 | $element = $this->findElement($xpath); 930 | 931 | // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur 932 | $this->executeJsOnElement($element, 'return {{ELEMENT}}.blur()'); 933 | } 934 | 935 | /** 936 | * {@inheritdoc} 937 | */ 938 | public function keyPress($xpath, $char, $modifier = null) 939 | { 940 | $this->sendKey($xpath, $char, $modifier); 941 | } 942 | 943 | /** 944 | * Performs a modifier key press. Does not release the modifier key - subsequent interactions 945 | * may assume it's kept pressed. 946 | * Note that the modifier key is never released implicitly - either 947 | * keyUp(theKey) or sendKeys(Keys.NULL) 948 | * must be called to release the modifier. 949 | * 950 | * @param string $xpath 951 | * @param string $key Either {@link Keys::SHIFT}, {@link Keys::ALT} or {@link Keys::CONTROL}. 952 | * If the provided key is none of those, {@link InvalidArgumentException} is thrown. 953 | * @param null $modifier @deprecated 954 | * 955 | * @throws \InvalidArgumentException 956 | * 957 | * @return void 958 | */ 959 | public function keyDown($xpath, $key, $modifier = null) 960 | { 961 | // Own implementation of https://github.com/php-webdriver/php-webdriver/pull/803 962 | $element = $this->findElement($xpath); 963 | 964 | $action = $this->webDriver->action(); 965 | $keyModifier = $this->keyModifier($key); 966 | 967 | if (!in_array($keyModifier, self::MODIFIER_KEYS, true)) { 968 | throw new \InvalidArgumentException('Key Down / Up events only make sense for modifier keys.'); 969 | } 970 | 971 | $action->keyDown($element, $keyModifier); 972 | $action->perform(); 973 | } 974 | 975 | /** 976 | * Performs a modifier key release. Releasing a non-depressed modifier key will yield undefined 977 | * behaviour. 978 | * 979 | * @param string $xpath 980 | * @param string $key Either {@link Keys::SHIFT}, {@link Keys::ALT} or {@link Keys::CONTROL}. 981 | * If the provided key is none of those, {@link InvalidArgumentException} is thrown. 982 | * 983 | * @param null $modifier @deprecated 984 | * 985 | * @throws \InvalidArgumentException 986 | * 987 | * @return void 988 | */ 989 | public function keyUp($xpath, $key, $modifier = null) 990 | { 991 | // Own implementation of https://github.com/php-webdriver/php-webdriver/pull/803 992 | $element = $this->findElement($xpath); 993 | 994 | $action = $this->webDriver->action(); 995 | $keyModifier = $this->keyModifier($key); 996 | 997 | if (!in_array($keyModifier, self::MODIFIER_KEYS, true)) { 998 | throw new \InvalidArgumentException('Key Down / Up events only make sense for modifier keys.'); 999 | } 1000 | 1001 | $action->keyUp($element, $keyModifier); 1002 | $action->perform(); 1003 | } 1004 | 1005 | /** 1006 | * {@inheritdoc} 1007 | */ 1008 | public function dragTo($sourceXpath, $destinationXpath) 1009 | { 1010 | $source = $this->findElement($sourceXpath); 1011 | $destination = $this->findElement($destinationXpath); 1012 | $action = $this->webDriver->action(); 1013 | 1014 | $action->dragAndDrop($source, $destination); 1015 | $action->perform(); 1016 | } 1017 | 1018 | /** 1019 | * {@inheritdoc} 1020 | */ 1021 | public function executeScript($script) 1022 | { 1023 | if (preg_match('/^function[\s\(]/', $script)) { 1024 | $script = preg_replace('/;$/', '', $script); 1025 | $script = '(' . $script . ')'; 1026 | } 1027 | 1028 | $this->webDriver->executeScript($script); 1029 | } 1030 | 1031 | public function executeAsyncScript($script) 1032 | { 1033 | if (preg_match('/^function[\s\(]/', $script)) { 1034 | $script = preg_replace('/;$/', '', $script); 1035 | $script = '(' . $script . ')'; 1036 | } 1037 | 1038 | try { 1039 | $this->webDriver->executeAsyncScript($script); 1040 | } catch (ScriptTimeoutException $e) { 1041 | throw new DriverException($e->getMessage(), $e->getCode(), $e); 1042 | } 1043 | } 1044 | 1045 | /** 1046 | * {@inheritdoc} 1047 | */ 1048 | public function evaluateScript($script) 1049 | { 1050 | if (0 !== strpos(trim($script), 'return ')) { 1051 | $script = 'return ' . $script; 1052 | } 1053 | 1054 | return $this->webDriver->executeScript($script); 1055 | } 1056 | 1057 | /** 1058 | * {@inheritdoc} 1059 | */ 1060 | public function wait($timeout, $condition) 1061 | { 1062 | $seconds = $timeout / 1000.0; 1063 | $wait = $this->webDriver->wait($seconds); 1064 | 1065 | if (is_string($condition)) { 1066 | $script = "return $condition;"; 1067 | $condition = static function (RemoteWebDriver $driver) use ($script) { 1068 | return $driver->executeScript($script); 1069 | }; 1070 | } 1071 | 1072 | try { 1073 | return (bool) $wait->until($condition); 1074 | } catch (TimeOutException $e) { 1075 | return false; 1076 | } 1077 | } 1078 | 1079 | /** 1080 | * {@inheritdoc} 1081 | */ 1082 | public function resizeWindow($width, $height, $name = null) 1083 | { 1084 | $dimension = new WebDriverDimension($width, $height); 1085 | if ($name) { 1086 | throw new UnsupportedDriverActionException('Named windows are not supported yet'); 1087 | } 1088 | 1089 | $this->webDriver->manage()->window()->setSize($dimension); 1090 | } 1091 | 1092 | /** 1093 | * {@inheritdoc} 1094 | */ 1095 | public function submitForm($xpath) 1096 | { 1097 | $element = $this->findElement($xpath); 1098 | $element->submit(); 1099 | } 1100 | 1101 | /** 1102 | * {@inheritdoc} 1103 | */ 1104 | public function maximizeWindow($name = null) 1105 | { 1106 | if ($name) { 1107 | throw new UnsupportedDriverActionException('Named window is not supported'); 1108 | } 1109 | 1110 | $this->webDriver->manage()->window()->maximize(); 1111 | } 1112 | 1113 | /** 1114 | * Returns Session ID of WebDriver or `null`, when session not started yet. 1115 | * 1116 | * @return string|null 1117 | */ 1118 | public function getWebDriverSessionId() 1119 | { 1120 | if (!$this->isStarted()) { 1121 | return null; 1122 | } 1123 | 1124 | return $this->webDriver->getSessionID(); 1125 | } 1126 | 1127 | /** 1128 | * @param string $xpath 1129 | * 1130 | * @return RemoteWebElement 1131 | */ 1132 | private function findElement($xpath) 1133 | { 1134 | return $this->webDriver->findElement(WebDriverBy::xpath($xpath)); 1135 | } 1136 | 1137 | /** 1138 | * Ensures the element is a checkbox 1139 | * 1140 | * @param WebDriverElement $element 1141 | * @param string $xpath 1142 | * @param string $type 1143 | * @param string $action 1144 | * 1145 | * @throws DriverException 1146 | */ 1147 | private function ensureInputType(WebDriverElement $element, $xpath, $type, $action) 1148 | { 1149 | if ('input' !== strtolower($element->getTagName()) || $type !== strtolower($element->getAttribute('type'))) { 1150 | $message = 'Impossible to %s the element with XPath "%s" as it is not a %s input'; 1151 | 1152 | throw new DriverException(sprintf($message, $action, $xpath, $type)); 1153 | } 1154 | } 1155 | 1156 | /** 1157 | * Converts alt/ctrl/shift/meta to corresponded Keys::* constant 1158 | * 1159 | * @param string $modifier 1160 | * 1161 | * @return string 1162 | */ 1163 | private function keyModifier($modifier) 1164 | { 1165 | if ($modifier === 'alt') { 1166 | $modifier = Keys::ALT; 1167 | } else if ($modifier === 'left alt') { 1168 | $modifier = Keys::LEFT_ALT; 1169 | } else if ($modifier === 'ctrl') { 1170 | $modifier = Keys::CONTROL; 1171 | } else if ($modifier === 'left ctrl') { 1172 | $modifier = Keys::LEFT_CONTROL; 1173 | } else if ($modifier === 'shift') { 1174 | $modifier = Keys::SHIFT; 1175 | } else if ($modifier === 'left shift') { 1176 | $modifier = Keys::LEFT_SHIFT; 1177 | } else if ($modifier === 'meta') { 1178 | $modifier = Keys::META; 1179 | } else if ($modifier === 'command') { 1180 | $modifier = Keys::COMMAND; 1181 | } 1182 | 1183 | return $modifier; 1184 | } 1185 | 1186 | /** 1187 | * Decodes char 1188 | * 1189 | * @param int|string $char if int is passed it will be converted to char using `chr` function 1190 | * 1191 | * @return string 1192 | */ 1193 | private function decodeChar($char) 1194 | { 1195 | if (\is_numeric($char)) { 1196 | return \chr($char); 1197 | } 1198 | 1199 | return $char; 1200 | } 1201 | 1202 | /** 1203 | * @param $xpath 1204 | * @param $char 1205 | * @param $modifier 1206 | */ 1207 | private function sendKey($xpath, $char, $modifier) 1208 | { 1209 | $element = $this->findElement($xpath); 1210 | $char = $this->decodeChar($char); 1211 | $element->sendKeys(($modifier ? $this->keyModifier($modifier) : '') . $char); 1212 | } 1213 | 1214 | public function getCurrentPromptOrAlert(): ?WebDriverAlert 1215 | { 1216 | if (!$this->isStarted()) { 1217 | return null; 1218 | } 1219 | 1220 | return $this->webDriver->switchTo()->alert(); 1221 | } 1222 | } 1223 | --------------------------------------------------------------------------------