Skip to content

Commit 6dd8b58

Browse files
authored
Merge pull request #560 from dunglas/geckodriver
W3C-compliant implementation, native geckodriver support
2 parents e927791 + 7119d7f commit 6dd8b58

25 files changed

+717
-84
lines changed

.travis.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,21 @@ matrix:
3434
env: DEPENDENCIES="--prefer-lowest"
3535

3636
# Firefox inside Travis environment
37-
- php: '7.3'
37+
- name: 'Firefox 45 on Travis (OSS protocol)'
38+
php: '7.3'
3839
env: BROWSER_NAME="firefox"
3940
addons:
4041
firefox: "45.8.0esr"
4142

43+
# Firefox with Geckodriver (W3C mode) inside Travis environment
44+
- name: 'Firefox latest on Travis (W3C protocol)'
45+
php: 7.3
46+
env:
47+
- BROWSER_NAME="firefox"
48+
- GECKODRIVER="1"
49+
addons:
50+
firefox: latest
51+
4252
# Stable Chrome + Chromedriver inside Travis environment
4353
- php: '7.3'
4454
env: BROWSER_NAME="chrome" CHROME_HEADLESS="1"
@@ -98,9 +108,14 @@ install:
98108
before_script:
99109
- 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
100110
- if [ "$BROWSER_NAME" = "chrome" ]; then export CHROMEDRIVER_PATH=$PWD/chromedriver/chromedriver; fi
111+
- 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
101112
- sh -e /etc/init.d/xvfb start
102113
- 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
103-
- 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 &
114+
- if [ "$GECKODRIVER" = "1" ]; then
115+
geckodriver/geckodriver &> ./logs/geckodriver.log &
116+
else
117+
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 &
118+
fi
104119
- until $(echo | nc localhost 4444); do sleep 1; echo Waiting for Selenium server on port 4444...; done; echo "Selenium server started"
105120
- php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log &
106121
- until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started"
@@ -114,6 +129,7 @@ script:
114129
after_script:
115130
- if [ -f ./logs/selenium.log ]; then cat ./logs/selenium.log; fi
116131
- if [ -f ./logs/php-server.log ]; then cat ./logs/php-server.log; fi
132+
- if [ -f ./logs/geckodriver.log ]; then cat ./logs/geckodriver.log; fi
117133

118134
after_success:
119135
- travis_retry php vendor/bin/php-coveralls -v

CONTRIBUTING.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,23 @@ For the functional tests you must first [download](http://selenium-release.stora
3737
the selenium standalone server, start the local PHP server which will serve the test pages and then run the `functional`
3838
test suite:
3939

40-
java -jar selenium-server-standalone-2.53.1.jar -log selenium.log &
40+
java -jar selenium-server-standalone-3.9.1.jar -log selenium.log &
4141
php -S localhost:8000 -t tests/functional/web/ &
4242
./vendor/bin/phpunit --testsuite functional
43-
43+
4444
The functional tests will be started in HtmlUnit headless browser by default. If you want to run them in eg. Firefox,
4545
simply set the `BROWSER_NAME` environment variable:
4646

4747
...
4848
export BROWSER_NAME="firefox"
4949
./vendor/bin/phpunit --testsuite functional
5050

51+
To test with Geckodriver, [download](https://github.com/mozilla/geckodriver/releases) and start the server, then run:
52+
53+
export GECKODRIVER=1
54+
export BROWSER_NAME=firefox
55+
./vendor/bin/phpunit --testsuite functional
56+
5157
### Check coding style
5258

5359
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:

lib/AbstractWebDriverCheckboxOrRadio.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ protected function getRelatedElements($value = null)
211211
$form = $this->element->findElement(WebDriverBy::xpath('ancestor::form'));
212212

213213
$formId = $form->getAttribute('id');
214-
if ($formId === '') {
214+
if (!$formId) {
215215
return $form->findElements(WebDriverBy::xpath(
216216
sprintf('.//input[@name = %s%s]', XPathEscaper::escapeQuotes($this->name), $valueSelector)
217217
));

lib/Cookie.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,13 @@ public function isHttpOnly()
190190
*/
191191
public function toArray()
192192
{
193-
return $this->cookie;
193+
$cookie = $this->cookie;
194+
if (!isset($cookie['secure'])) {
195+
// Passing a boolean value for the "secure" flag is mandatory when using geckodriver
196+
$cookie['secure'] = false;
197+
}
198+
199+
return $cookie;
194200
}
195201

196202
public function offsetExists($offset)

lib/Exception/WebDriverException.php

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function getResults()
4242
/**
4343
* Throw WebDriverExceptions based on WebDriver status code.
4444
*
45-
* @param int $status_code
45+
* @param int|string $status_code
4646
* @param string $message
4747
* @param mixed $results
4848
*
@@ -85,6 +85,54 @@ public function getResults()
8585
*/
8686
public static function throwException($status_code, $message, $results)
8787
{
88+
if (is_string($status_code)) {
89+
// see https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors
90+
switch ($status_code) {
91+
case 'no such element':
92+
throw new NoSuchElementException($message, $results);
93+
case 'no such frame':
94+
throw new NoSuchFrameException($message, $results);
95+
case 'unknown command':
96+
throw new UnknownCommandException($message, $results);
97+
case 'stale element reference':
98+
throw new StaleElementReferenceException($message, $results);
99+
case 'invalid element state':
100+
throw new InvalidElementStateException($message, $results);
101+
case 'unknown error':
102+
throw new UnknownServerException($message, $results);
103+
case 'unsupported operation':
104+
throw new ExpectedException($message, $results);
105+
case 'element not interactable':
106+
throw new ElementNotSelectableException($message, $results);
107+
case 'no such window':
108+
throw new NoSuchDocumentException($message, $results);
109+
case 'javascript error':
110+
throw new UnexpectedJavascriptException($message, $results);
111+
case 'timeout':
112+
throw new TimeOutException($message, $results);
113+
case 'no such window':
114+
throw new NoSuchWindowException($message, $results);
115+
case 'invalid cookie domain':
116+
throw new InvalidCookieDomainException($message, $results);
117+
case 'unable to set cookie':
118+
throw new UnableToSetCookieException($message, $results);
119+
case 'unexpected alert open':
120+
throw new UnexpectedAlertOpenException($message, $results);
121+
case 'no such alert':
122+
throw new NoAlertOpenException($message, $results);
123+
case 'script timeout':
124+
throw new ScriptTimeoutException($message, $results);
125+
case 'invalid selector':
126+
throw new InvalidSelectorException($message, $results);
127+
case 'session not created':
128+
throw new SessionNotCreatedException($message, $results);
129+
case 'move target out of bounds':
130+
throw new MoveTargetOutOfBoundsException($message, $results);
131+
default:
132+
throw new UnrecognizedExceptionException($message, $results);
133+
}
134+
}
135+
88136
switch ($status_code) {
89137
case 1:
90138
throw new IndexOutOfBoundsException($message, $results);

lib/Interactions/WebDriverActions.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
use Facebook\WebDriver\Interactions\Internal\WebDriverMouseMoveAction;
2626
use Facebook\WebDriver\Interactions\Internal\WebDriverMoveToOffsetAction;
2727
use Facebook\WebDriver\Interactions\Internal\WebDriverSendKeysAction;
28-
use Facebook\WebDriver\WebDriver;
2928
use Facebook\WebDriver\WebDriverElement;
3029
use Facebook\WebDriver\WebDriverHasInputDevices;
3130

lib/Remote/DriverCommand.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ class DriverCommand
146146
const GET_NETWORK_CONNECTION = 'getNetworkConnection';
147147
const SET_NETWORK_CONNECTION = 'setNetworkConnection';
148148

149+
// W3C specific
150+
const ACTIONS = 'actions';
151+
const GET_ELEMENT_PROPERTY = 'getElementProperty';
152+
149153
private function __construct()
150154
{
151155
}

lib/Remote/HttpCommandExecutor.php

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,29 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
137137
DriverCommand::TOUCH_SCROLL => ['method' => 'POST', 'url' => '/session/:sessionId/touch/scroll'],
138138
DriverCommand::TOUCH_UP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/up'],
139139
];
140+
/**
141+
* @var array Will be merged with $commands
142+
*/
143+
protected static $w3cCompliantCommands = [
144+
DriverCommand::ACCEPT_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/accept'],
145+
DriverCommand::ACTIONS => ['method' => 'POST', 'url' => '/session/:sessionId/actions'],
146+
DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/dismiss'],
147+
DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'],
148+
DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/async'],
149+
DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window'],
150+
DriverCommand::GET_ELEMENT_LOCATION => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'],
151+
DriverCommand::GET_ELEMENT_PROPERTY => [
152+
'method' => 'GET',
153+
'url' => '/session/:sessionId/element/:id/property/:name',
154+
],
155+
DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'],
156+
DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window/handles'],
157+
DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'],
158+
DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
159+
DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert/text'],
160+
DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
161+
DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
162+
];
140163
/**
141164
* @var string
142165
*/
@@ -145,6 +168,10 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
145168
* @var resource
146169
*/
147170
protected $curl;
171+
/**
172+
* @var bool
173+
*/
174+
protected $isW3cCompliant = true;
148175

149176
/**
150177
* @param string $url
@@ -153,6 +180,8 @@ class HttpCommandExecutor implements WebDriverCommandExecutor
153180
*/
154181
public function __construct($url, $http_proxy = null, $http_proxy_port = null)
155182
{
183+
self::$w3cCompliantCommands = array_merge(self::$commands, self::$w3cCompliantCommands);
184+
156185
$this->url = $url;
157186
$this->curl = curl_init();
158187

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

211+
public function disableW3cCompliance()
212+
{
213+
$this->isW3cCompliant = false;
214+
}
215+
182216
/**
183217
* Set timeout for the connect phase
184218
*
@@ -226,11 +260,19 @@ public function setRequestTimeout($timeout_in_ms)
226260
*/
227261
public function execute(WebDriverCommand $command)
228262
{
229-
if (!isset(self::$commands[$command->getName()])) {
230-
throw new InvalidArgumentException($command->getName() . ' is not a valid command.');
263+
$commandName = $command->getName();
264+
if (!isset(self::$commands[$commandName])) {
265+
if ($this->isW3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) {
266+
throw new InvalidArgumentException($command->getName() . ' is not a valid command.');
267+
}
268+
}
269+
270+
if ($this->isW3cCompliant) {
271+
$raw = self::$w3cCompliantCommands[$command->getName()];
272+
} else {
273+
$raw = self::$commands[$command->getName()];
231274
}
232275

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

272314
$encoded_params = null;
273315

274-
if ($http_method === 'POST' && $params && is_array($params)) {
275-
$encoded_params = json_encode($params);
316+
if ($http_method === 'POST') {
317+
if ($params && is_array($params)) {
318+
$encoded_params = json_encode($params);
319+
} elseif ($this->isW3cCompliant) {
320+
// POST body must be valid JSON in W3C, even if empty: https://www.w3.org/TR/webdriver/#processing-model
321+
$encoded_params = '{}';
322+
}
276323
}
277324

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

319366
$sessionId = null;
320-
if (is_array($results) && array_key_exists('sessionId', $results)) {
367+
if (is_array($value) && array_key_exists('sessionId', $value)) {
368+
// W3C's WebDriver
369+
$sessionId = $value['sessionId'];
370+
} elseif (is_array($results) && array_key_exists('sessionId', $results)) {
371+
// Legacy JsonWire
321372
$sessionId = $results['sessionId'];
322373
}
323374

375+
// @see https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors
376+
if (isset($value['error'])) {
377+
// W3C's WebDriver
378+
WebDriverException::throwException($value['error'], $message, $results);
379+
}
380+
324381
$status = isset($results['status']) ? $results['status'] : 0;
325-
if ($status != 0) {
382+
if ($status !== 0) {
383+
// Legacy JsonWire
326384
WebDriverException::throwException($status, $message, $results);
327385
}
328386

0 commit comments

Comments
 (0)