Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,21 @@ matrix:
env: DEPENDENCIES="--prefer-lowest"

# Firefox inside Travis environment
- php: '7.3'
- name: 'Firefox 45 on Travis (OSS protocol)'
php: '7.3'
env: BROWSER_NAME="firefox"
addons:
firefox: "45.8.0esr"

# Firefox with Geckodriver (W3C mode) inside Travis environment
- name: 'Firefox latest on Travis (W3C protocol)'
php: 7.3
env:
- BROWSER_NAME="firefox"
- GECKODRIVER="1"
addons:
firefox: latest

# Stable Chrome + Chromedriver inside Travis environment
- php: '7.3'
env: BROWSER_NAME="chrome" CHROME_HEADLESS="1"
Expand Down Expand Up @@ -98,9 +108,14 @@ install:
before_script:
- if [ "$BROWSER_NAME" = "chrome" ]; then mkdir chromedriver; CHROMEDRIVER_VERSION=$(wget -qO- "https://chromedriver.storage.googleapis.com/LATEST_RELEASE"); 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
- if [ "$GECKODRIVER" = "1" ]; then mkdir geckodriver; wget -q -t 3 https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz; tar xzf geckodriver-v0.26.0-linux64.tar.gz -C geckodriver; fi
- 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 &
- if [ "$GECKODRIVER" = "1" ]; then
geckodriver/geckodriver &> ./logs/geckodriver.log &
else
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 &
fi
- until $(echo | nc localhost 4444); do sleep 1; echo Waiting for Selenium server on port 4444...; done; echo "Selenium server started"
- php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log &
- until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started"
Expand All @@ -114,6 +129,7 @@ script:
after_script:
- if [ -f ./logs/selenium.log ]; then cat ./logs/selenium.log; fi
- if [ -f ./logs/php-server.log ]; then cat ./logs/php-server.log; fi
- if [ -f ./logs/geckodriver.log ]; then cat ./logs/geckodriver.log; fi

after_success:
- travis_retry php vendor/bin/php-coveralls -v
10 changes: 8 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,23 @@ For the functional tests you must first [download](http://selenium-release.stora
the selenium standalone server, start the local PHP server which will serve the test pages and then run the `functional`
test suite:

java -jar selenium-server-standalone-2.53.1.jar -log selenium.log &
java -jar selenium-server-standalone-3.9.1.jar -log selenium.log &
php -S localhost:8000 -t tests/functional/web/ &
./vendor/bin/phpunit --testsuite functional

The functional tests will be started in HtmlUnit headless browser by default. If you want to run them in eg. Firefox,
simply set the `BROWSER_NAME` environment variable:

...
export BROWSER_NAME="firefox"
./vendor/bin/phpunit --testsuite functional

To test with Geckodriver, [download](https://github.com/mozilla/geckodriver/releases) and start the server, then run:

export GECKODRIVER=1
export BROWSER_NAME=firefox
./vendor/bin/phpunit --testsuite functional

### Check coding style

Your code-style should comply with [PSR-2](http://www.php-fig.org/psr/psr-2/). To make sure your code matches this requirement run:
Expand Down
2 changes: 1 addition & 1 deletion lib/AbstractWebDriverCheckboxOrRadio.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ protected function getRelatedElements($value = null)
$form = $this->element->findElement(WebDriverBy::xpath('ancestor::form'));

$formId = $form->getAttribute('id');
if ($formId === '') {
if (!$formId) {
return $form->findElements(WebDriverBy::xpath(
sprintf('.//input[@name = %s%s]', XPathEscaper::escapeQuotes($this->name), $valueSelector)
));
Expand Down
8 changes: 7 additions & 1 deletion lib/Cookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,13 @@ public function isHttpOnly()
*/
public function toArray()
{
return $this->cookie;
$cookie = $this->cookie;
if (!isset($cookie['secure'])) {
// Passing a boolean value for the "secure" flag is mandatory when using geckodriver
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this? Is it bug or feature? Is there an issue in geckodriver for this?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's indeed a bug. The W3C spec marks it as optional: https://www.w3.org/TR/webdriver1/#dfn-table-for-cookie-conversion

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's a bug... But we have to workaround it for Geckodriver support.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you reported it to the geckodriver team ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not yet

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chrome now has the same behavior: Codeception/Codeception@ec2fc73#diff-b69c529efcc6368b5c564f8b36665a22R827

We should leave this as is.

$cookie['secure'] = false;
}

return $cookie;
}

public function offsetExists($offset)
Expand Down
50 changes: 49 additions & 1 deletion lib/Exception/WebDriverException.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function getResults()
/**
* Throw WebDriverExceptions based on WebDriver status code.
*
* @param int $status_code
* @param int|string $status_code
* @param string $message
* @param mixed $results
*
Expand Down Expand Up @@ -85,6 +85,54 @@ public function getResults()
*/
public static function throwException($status_code, $message, $results)
{
if (is_string($status_code)) {
// see https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors
switch ($status_code) {
case 'no such element':
throw new NoSuchElementException($message, $results);
case 'no such frame':
throw new NoSuchFrameException($message, $results);
case 'unknown command':
throw new UnknownCommandException($message, $results);
case 'stale element reference':
throw new StaleElementReferenceException($message, $results);
case 'invalid element state':
throw new InvalidElementStateException($message, $results);
case 'unknown error':
throw new UnknownServerException($message, $results);
case 'unsupported operation':
throw new ExpectedException($message, $results);
case 'element not interactable':
throw new ElementNotSelectableException($message, $results);
case 'no such window':
throw new NoSuchDocumentException($message, $results);
case 'javascript error':
throw new UnexpectedJavascriptException($message, $results);
case 'timeout':
throw new TimeOutException($message, $results);
case 'no such window':
throw new NoSuchWindowException($message, $results);
case 'invalid cookie domain':
throw new InvalidCookieDomainException($message, $results);
case 'unable to set cookie':
throw new UnableToSetCookieException($message, $results);
case 'unexpected alert open':
throw new UnexpectedAlertOpenException($message, $results);
case 'no such alert':
throw new NoAlertOpenException($message, $results);
case 'script timeout':
throw new ScriptTimeoutException($message, $results);
case 'invalid selector':
throw new InvalidSelectorException($message, $results);
case 'session not created':
throw new SessionNotCreatedException($message, $results);
case 'move target out of bounds':
throw new MoveTargetOutOfBoundsException($message, $results);
default:
throw new UnrecognizedExceptionException($message, $results);
}
}

switch ($status_code) {
case 1:
throw new IndexOutOfBoundsException($message, $results);
Expand Down
1 change: 0 additions & 1 deletion lib/Interactions/WebDriverActions.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
use Facebook\WebDriver\Interactions\Internal\WebDriverMouseMoveAction;
use Facebook\WebDriver\Interactions\Internal\WebDriverMoveToOffsetAction;
use Facebook\WebDriver\Interactions\Internal\WebDriverSendKeysAction;
use Facebook\WebDriver\WebDriver;
use Facebook\WebDriver\WebDriverElement;
use Facebook\WebDriver\WebDriverHasInputDevices;

Expand Down
4 changes: 4 additions & 0 deletions lib/Remote/DriverCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ class DriverCommand
const GET_NETWORK_CONNECTION = 'getNetworkConnection';
const SET_NETWORK_CONNECTION = 'setNetworkConnection';

// W3C specific
const ACTIONS = 'actions';
const GET_ELEMENT_PROPERTY = 'getElementProperty';

private function __construct()
{
}
Expand Down
72 changes: 65 additions & 7 deletions lib/Remote/HttpCommandExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,29 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
DriverCommand::TOUCH_SCROLL => ['method' => 'POST', 'url' => '/session/:sessionId/touch/scroll'],
DriverCommand::TOUCH_UP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/up'],
];
/**
* @var array Will be merged with $commands
*/
protected static $w3cCompliantCommands = [
DriverCommand::ACCEPT_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/accept'],
DriverCommand::ACTIONS => ['method' => 'POST', 'url' => '/session/:sessionId/actions'],
DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/dismiss'],
DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'],
DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/async'],
DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window'],
DriverCommand::GET_ELEMENT_LOCATION => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'],
DriverCommand::GET_ELEMENT_PROPERTY => [
'method' => 'GET',
'url' => '/session/:sessionId/element/:id/property/:name',
],
DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'],
DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window/handles'],
DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'],
DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert/text'],
DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
];
/**
* @var string
*/
Expand All @@ -145,6 +168,10 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
* @var resource
*/
protected $curl;
/**
* @var bool
*/
protected $isW3cCompliant = true;

/**
* @param string $url
Expand All @@ -153,6 +180,8 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
*/
public function __construct($url, $http_proxy = null, $http_proxy_port = null)
{
self::$w3cCompliantCommands = array_merge(self::$commands, self::$w3cCompliantCommands);

$this->url = $url;
$this->curl = curl_init();

Expand All @@ -179,6 +208,11 @@ public function __construct($url, $http_proxy = null, $http_proxy_port = null)
$this->setConnectionTimeout(30000);
}

public function disableW3cCompliance()
{
$this->isW3cCompliant = false;
}

/**
* Set timeout for the connect phase
*
Expand Down Expand Up @@ -226,11 +260,19 @@ public function setRequestTimeout($timeout_in_ms)
*/
public function execute(WebDriverCommand $command)
{
if (!isset(self::$commands[$command->getName()])) {
throw new InvalidArgumentException($command->getName() . ' is not a valid command.');
$commandName = $command->getName();
if (!isset(self::$commands[$commandName])) {
if ($this->isW3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) {
throw new InvalidArgumentException($command->getName() . ' is not a valid command.');
}
}

if ($this->isW3cCompliant) {
$raw = self::$w3cCompliantCommands[$command->getName()];
} else {
$raw = self::$commands[$command->getName()];
}

$raw = self::$commands[$command->getName()];
$http_method = $raw['method'];
$url = $raw['url'];
$url = str_replace(':sessionId', $command->getSessionID(), $url);
Expand Down Expand Up @@ -271,8 +313,13 @@ public function execute(WebDriverCommand $command)

$encoded_params = null;

if ($http_method === 'POST' && $params && is_array($params)) {
$encoded_params = json_encode($params);
if ($http_method === 'POST') {
if ($params && is_array($params)) {
$encoded_params = json_encode($params);
} elseif ($this->isW3cCompliant) {
// POST body must be valid JSON in W3C, even if empty: https://www.w3.org/TR/webdriver/#processing-model
$encoded_params = '{}';
}
}

curl_setopt($this->curl, CURLOPT_POSTFIELDS, $encoded_params);
Expand Down Expand Up @@ -317,12 +364,23 @@ public function execute(WebDriverCommand $command)
}

$sessionId = null;
if (is_array($results) && array_key_exists('sessionId', $results)) {
if (is_array($value) && array_key_exists('sessionId', $value)) {
// W3C's WebDriver
$sessionId = $value['sessionId'];
} elseif (is_array($results) && array_key_exists('sessionId', $results)) {
// Legacy JsonWire
$sessionId = $results['sessionId'];
}

// @see https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors
if (isset($value['error'])) {
// W3C's WebDriver
WebDriverException::throwException($value['error'], $message, $results);
}

$status = isset($results['status']) ? $results['status'] : 0;
if ($status != 0) {
if ($status !== 0) {
// Legacy JsonWire
WebDriverException::throwException($status, $message, $results);
}

Expand Down
Loading